Compare commits

...

30 Commits

Author SHA1 Message Date
2947e2c539 Merge pull request 'SASL2 and friends' (#34) from feat/sasl2 into master
Reviewed-on: https://codeberg.org/moxxy/moxxmpp/pulls/34
2023-04-02 21:36:02 +00:00
ac8433f51f chore(core): Refactor RFC6120 implementations 2023-04-02 23:33:53 +02:00
808371b271 chore(tests): Fix tests 2023-04-02 23:23:50 +02:00
7fdd83ea69 chore(xep): Clean the SASL2 implementation 2023-04-02 23:06:02 +02:00
68e2a65dcf docs(tests): Mention that we need prosody-trunk 2023-04-02 19:49:00 +02:00
d977a74446 feat(tests): Add an integration test for SASL2 2023-04-02 17:20:14 +02:00
29f0419154 feat(meta): Remove log redaction 2023-04-02 14:38:32 +02:00
b354ca8d0a feat(xep): Improve the ergonomics of Bind2 negotiators 2023-04-02 13:39:43 +02:00
ec6b5ab753 feat(xep): Allow inline enablement of carbons 2023-04-02 12:44:09 +02:00
ce1815d1f3 fix(tests): Fix namespace of <bound /> 2023-04-02 12:43:28 +02:00
fbb495dc2f feat(xep): Allow inlining CSI 2023-04-01 23:16:37 +02:00
4a6aa79e56 fix(xep): When using FAST, fallback to other SASL mechanisms on failure 2023-04-01 21:42:52 +02:00
0033d0eb6e feat(xep): Implement FAST 2023-04-01 21:10:46 +02:00
24cb05f91b feat(xep): Handle inline stream enablement with Bind2 2023-04-01 17:38:40 +02:00
91f763ac26 feat(xep): Allow negotiating SM enabling inline with Bind2 2023-04-01 17:16:29 +02:00
51edb61443 feat(xep): Implement SASL2 inline stream resumption 2023-04-01 15:50:13 +02:00
4e01d32e90 feat(xep): Allow setting a tag when using Bind2 2023-04-01 13:15:46 +02:00
f2fe06104c fix(core): Fix formatting 2023-04-01 13:13:19 +02:00
89fe8f0a9c feat(core): Make the PresenceManager optional 2023-04-01 13:09:43 +02:00
9358175925 feat(xep): Inline resource binding with Bind2 2023-04-01 13:00:35 +02:00
564a237986 feat(xep): Set the resource if SASL2 resulted in a resource 2023-04-01 12:51:26 +02:00
cf425917cf feat(core): Reset the resource if lastResource is null 2023-04-01 12:39:15 +02:00
63b7abd6f9 fix(core): Prevent resource binding if we already have a resource 2023-04-01 12:38:18 +02:00
f460e5ebe9 feat(core): Handle less resource binding in the core connection class 2023-04-01 12:28:11 +02:00
af8bc606d6 feat(xep): Guard against random data in the SASL2 result 2023-04-01 00:51:51 +02:00
30482c86f0 feat(xep): Implement inline negotiation 2023-04-01 00:47:45 +02:00
f86dbe6af8 feat(core): Verify the server signature with SASL2 2023-03-31 23:52:48 +02:00
478b5b8770 feat(core): Make SCRAM SASL2 aware 2023-03-31 21:09:16 +02:00
7ab3f4f0d9 feat(xep): Implement negotiating PLAIN via SASL2 2023-03-31 20:53:06 +02:00
2e60e9841e feat(xep): Begin work on SASL2 2023-03-31 19:02:57 +02:00
58 changed files with 3023 additions and 262 deletions

View File

@ -2,11 +2,11 @@
"nodes": {
"flake-utils": {
"locked": {
"lastModified": 1667395993,
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
"lastModified": 1678901627,
"narHash": "sha256-U02riOqrKKzwjsxc/400XnElV+UtPUQWpANPlyazjH0=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
"rev": "93a2b84fc4b70d9e089d029deacc3583435c2ed6",
"type": "github"
},
"original": {
@ -31,10 +31,27 @@
"type": "github"
}
},
"nixpkgs-unstable": {
"locked": {
"lastModified": 1680273054,
"narHash": "sha256-Bs6/5LpvYp379qVqGt9mXxxx9GSE789k3oFc+OAL07M=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "3364b5b117f65fe1ce65a3cdd5612a078a3b31e3",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
"nixpkgs": "nixpkgs",
"nixpkgs-unstable": "nixpkgs-unstable"
}
}
},

View File

@ -2,10 +2,11 @@
description = "moxxmpp";
inputs = {
nixpkgs.url = "github:AtaraxiaSjel/nixpkgs/update/flutter";
nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }: flake-utils.lib.eachDefaultSystem (system: let
outputs = { self, nixpkgs, nixpkgs-unstable, flake-utils }: flake-utils.lib.eachDefaultSystem (system: let
pkgs = import nixpkgs {
inherit system;
config = {
@ -13,6 +14,9 @@
allowUnfree = true;
};
};
unstable = import nixpkgs-unstable {
inherit system;
};
android = pkgs.androidenv.composeAndroidPackages {
# TODO: Find a way to pin these
#toolsVersion = "26.1.1";
@ -46,7 +50,26 @@
};
};
devShell = pkgs.mkShell {
devShell = let
prosody-newer-community-modules = unstable.prosody.overrideAttrs (old: {
communityModules = pkgs.fetchhg {
url = "https://hg.prosody.im/prosody-modules";
rev = "e3a3a6c86a9f";
sha256 = "sha256-C2x6PCv0sYuj4/SroDOJLsNPzfeNCodYKbMqmNodFrk=";
};
src = pkgs.fetchhg {
url = "https://hg.prosody.im/trunk";
rev = "8a2f75e38eb2";
sha256 = "sha256-zMNp9+wQ/hvUVyxFl76DqCVzQUPP8GkNdstiTDkG8Hw=";
};
});
prosody-sasl2 = prosody-newer-community-modules.override {
withCommunityModules = [
"sasl2" "sasl2_fast" "sasl2_sm" "sasl2_bind2"
];
};
in pkgs.mkShell {
buildInputs = with pkgs; [
flutter pinnedJDK android.platform-tools dart # Dart
gitlint # Code hygiene
@ -71,6 +94,10 @@
# For the scripts in ./scripts/
pythonEnv
# For integration testing against a local prosody server
prosody-sasl2
mkcert
];
CPATH = "${pkgs.xorg.libX11.dev}/include:${pkgs.xorg.xorgproto}/include";

6
integration_tests/.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
# Files and directories created by pub.
.dart_tool/
.packages
# Conventional directory for build output.
build/

View File

@ -0,0 +1,5 @@
# Integration Tests
The included `./prosody.cfg.lua` config file must be used for integration testing.
Additionally, ensure that a user `testuser@localhost` with the password `abc123`
exists. Note that this currently requires prosody-trunk.

View File

@ -0,0 +1 @@
include: ../analysis_options.yaml

View File

@ -0,0 +1,24 @@
-----BEGIN CERTIFICATE-----
MIIEAzCCAmugAwIBAgIQd61NPnP8++X7h8a+85C6DjANBgkqhkiG9w0BAQsFADBZ
MR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExFzAVBgNVBAsMDmFsZXhh
bmRlckBtaWt1MR4wHAYDVQQDDBVta2NlcnQgYWxleGFuZGVyQG1pa3UwHhcNMjMw
NDAyMTM1ODIxWhcNMjUwNzAyMTM1ODIxWjBCMScwJQYDVQQKEx5ta2NlcnQgZGV2
ZWxvcG1lbnQgY2VydGlmaWNhdGUxFzAVBgNVBAsMDmFsZXhhbmRlckBtaWt1MIIB
IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1DElEXPY+VDQP7cSikK0ne0K
gDgorGYPG9R7lOeuPLHyFYYry78+hB037OT0BOyA2uTu1yrog0dI/4YGicPDIqXh
IgHfjV+4kMi5SgO7ECWOBmZFqTC3bBwvbNtoW40aFjYSFaOkm/nnfp+nalEJJZ/N
kSkD4gdT3pH1ClsovlI4BlsxeIoJtyGzxMidJVXDAqMNraLatzJBwnT3OEs93xTf
7Kd1KUpQp9OZFrGi15zv/n6tCmrcC3xMOVHuYkhW0UCTFmev7ZqbghQsQ9N9s0E6
kk9rUf9xtMNH4Af6+2YRkT1DAGQ6FkXl1nQdB5H5XRgOBl+3k9s8wUrxQvQddQID
AQABo14wXDAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwHwYD
VR0jBBgwFoAU54aUZ+dytAOBTsYIdGtSnjiig/gwFAYDVR0RBA0wC4IJbG9jYWxo
b3N0MA0GCSqGSIb3DQEBCwUAA4IBgQBU8p7Ua0Cs+lXlWmtCh2j+YF9R+dvc+3Iw
dYEzCmYd375uxPctyHXW0yYjyuH9WuYn0F7OicEFEeC2+exHND+/z0J2Zv5yu34r
SfgHVfvE/Vxisn9InYrUCVtfRwLDF3HgLyIlm8FVzIyiIANhpe6vJdqjEWTsiL2X
I6hoDf1xlRgEqUx+Wxl2IFWrg+1SPPGTQzDPImiRlz8d+9ZJ9v48vaV5+aITMvDP
Gfm/bnNXXd5Gf7nGwL8zFHiwLoYQ5AUYl0IfXYwFAXJ72+LjiRT33IOidVJF0gsQ
6k9cTsc4lIrt4FOzdchalbF1Eu2prieWoZxz0apG8OuUeAhaB+t8kT6swAkwvkLW
OnlSATm9Cls9Pc4XDHTbZlbMmwF2Jmukgz/l1vlTutt4ZgZwQkSEa9Qfoi9Zym0R
iKls1CgD49zguR/cFDKK3agvfv6Afw6HdgaS/WqcI/Ros7b+RCkbAlAG5gqr6BLQ
8RGyVjZSC4Mz/ddcnMEpRAnjuFJjhGA=
-----END CERTIFICATE-----

View File

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDUMSURc9j5UNA/
txKKQrSd7QqAOCisZg8b1HuU5648sfIVhivLvz6EHTfs5PQE7IDa5O7XKuiDR0j/
hgaJw8MipeEiAd+NX7iQyLlKA7sQJY4GZkWpMLdsHC9s22hbjRoWNhIVo6Sb+ed+
n6dqUQkln82RKQPiB1PekfUKWyi+UjgGWzF4igm3IbPEyJ0lVcMCow2totq3MkHC
dPc4Sz3fFN/sp3UpSlCn05kWsaLXnO/+fq0KatwLfEw5Ue5iSFbRQJMWZ6/tmpuC
FCxD032zQTqST2tR/3G0w0fgB/r7ZhGRPUMAZDoWReXWdB0HkfldGA4GX7eT2zzB
SvFC9B11AgMBAAECggEAYaj4yY6LFzxVjG2i79WBsYnOonK2bZpPa9ygwEjdTXwM
0lE9SPoNONsFyVca5EVBjP1+27MY7orZkxlJWxCpeAHmmzNHg5bBqIlpliIfb3AJ
bPKXLyaH1Q8n2K8m2bQYhI6ARktZ0Jv1KrcqY2lGj3V8NEovSlFbDX4ZzJlmKCly
d4Ia6eQ7f9AjgsOwpQGeCTF7WLaVDnch6D4JfCGrW08lFeaqogiBQczsOE3hcNSd
tEul21Z0CkC7Iiw28KdkApPINquo1VYdAcOvUCOXkwJfPC1gsJwK4O2jxfi9v5NF
uU1niK0/00b396pQKvXpkfViynexwzK0MZCoo3zuQQKBgQDzaZexcniQNDyWqN3C
oMe4V3rnxs+aO/lu8Ed3mng+Jf4vuarZlxNot7WRBMGT/T+b7/UIrqRJy50CYAPY
3RRR84tLg3UMwUWhDYsPucNc2icODBG4c+QWJ300W19r+J+iT8PwS9AbH2n094Rn
LCRYFrX5aMsgIH5uwuncKzweMQKBgQDfKj2i1ptC53aOcr1tMCFYcnMGtaAZ8u6+
cKSgnzKlTw/g0EYlGcETUnCyZe0oVYWp3y859FBXU0JMDmxu84aYEZNF6BwRVlpF
feQgtUFZHyf9MepQGhjIJ5El8n7jhh1bsBY18QbDFe6/GtqPx/mQEF7vE+wPFl9h
putwdv3OhQKBgGKPyi2/BVSW4kW7IPiTM+vP+GNrnFp+mHS0dKvYb4HyzmcyzhyH
UQOhB7Mt8thivmP9GQIn/TwoZ24zxLsGYhkA/dFY7Id6pyAcpMd8V7/8Ub4dYvuG
acASw1709MF6jeEiXVuqxxyEbtoTc5h3Rkwo/gx8w2tB3RAqepl9JD2xAoGAfVL3
ci8a2iOqTKza/Cp/T3BWcHonAuuOb5xKl3lPs84GmLXd7o/cAcHWUBk1aeU9Pvx7
RQyS4bd8D8I52sUf3N5h2mxS9tmLsGLWbhfcLvR0PJh/gaRmLmEp/imEYLm8WvU0
Q+6rYXs7rE6kVwJygBjxd0m003Q49FoM9gec2RECgYEA5SLAe2UmJSLIb0DKk27o
nSfARDSdi9N40vIjDFHmDRdKTOYicED/f7KqXnxVpvFxDdCvJ7xeC4V7vkaqiiwd
/oMLQq0GjmBxG/PNd1AFIWDydyH+JcY6U4XWIzIw92OKVYC/KMvd2f9orTfmDyAU
RsGMfgV90kCzouAZKy3yPmo=
-----END PRIVATE KEY-----

View File

@ -0,0 +1,56 @@
admins = { }
plugin_paths = {}
modules_enabled = {
-- Generally required
"disco"; -- Service discovery
"roster"; -- Allow users to have a roster. Recommended ;)
"saslauth"; -- Authentication for clients and servers. Recommended if you want to log in.
"tls"; -- Add support for secure TLS on c2s/s2s connections
-- Not essential, but recommended
"blocklist"; -- Allow users to block communications with other users
"bookmarks"; -- Synchronise the list of open rooms between clients
"carbons"; -- Keep multiple online clients in sync
"dialback"; -- Support for verifying remote servers using DNS
"limits"; -- Enable bandwidth limiting for XMPP connections
"pep"; -- Allow users to store public and private data in their account
"private"; -- Legacy account storage mechanism (XEP-0049)
"smacks"; -- Stream management and resumption (XEP-0198)
"vcard4"; -- User profiles (stored in PEP)
"vcard_legacy"; -- Conversion between legacy vCard and PEP Avatar, vcard
-- Nice to have
"csi_simple"; -- Simple but effective traffic optimizations for mobile devices
"invites"; -- Create and manage invites
"invites_adhoc"; -- Allow admins/users to create invitations via their client
"invites_register"; -- Allows invited users to create accounts
"ping"; -- Replies to XMPP pings with pongs
"register"; -- Allow users to register on this server using a client and change passwords
"time"; -- Let others know the time here on this server
"uptime"; -- Report how long server has been running
"version"; -- Replies to server version requests
-- SASL2
"sasl2";
"sasl2_sm";
"sasl2_fast";
"sasl2_bind2";
}
s2s_secure_auth = false
-- Authentication
authentication = "internal_plain"
-- Storage
storage = "internal"
data_path = "/tmp/prosody-data/"
log = {
debug = "*console";
}
pidfile = "/tmp/prosody.pid"
VirtualHost "localhost"

View File

@ -0,0 +1,16 @@
name: integration_tests
description: A sample command-line application.
version: 1.0.0
environment:
sdk: '>=2.18.0 <3.0.0'
dependencies:
logging: ^1.0.2
moxxmpp: 0.2.0
moxxmpp_socket_tcp: 0.2.1
dev_dependencies:
lints: ^2.0.0
test: ^1.16.0
very_good_analysis: ^3.0.1

View File

@ -0,0 +1,67 @@
import 'package:logging/logging.dart';
import 'package:moxxmpp/moxxmpp.dart';
import 'package:moxxmpp_socket_tcp/moxxmpp_socket_tcp.dart';
import 'package:test/test.dart';
class TestingTCPSocketWrapper extends TCPSocketWrapper {
@override
bool onBadCertificate(dynamic certificate, String domain) {
return true;
}
}
void main() {
Logger.root.level = Level.ALL;
Logger.root.onRecord.listen((record) {
// ignore: avoid_print
print(
'[${record.level.name}] (${record.loggerName}) ${record.time}: ${record.message}',
);
});
test('Test authenticating against Prosody with SASL2, Bind2, and FAST', () async {
final conn = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
TestingTCPSocketWrapper(),
)..setConnectionSettings(
ConnectionSettings(
jid: JID.fromString('testuser@localhost'),
password: 'abc123',
useDirectTLS: false,
host: '127.0.0.1',
port: 5222,
),
);
final csi = CSIManager();
await csi.setInactive(sendNonza: false);
await conn.registerManagers([
RosterManager(TestingRosterStateManager('', [])),
DiscoManager([]),
]);
await conn.registerFeatureNegotiators([
SaslPlainNegotiator(),
ResourceBindingNegotiator(),
FASTSaslNegotiator(),
Bind2Negotiator(),
StartTlsNegotiator(),
Sasl2Negotiator(
userAgent: const UserAgent(
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
software: 'moxxmpp',
device: "PapaTutuWawa's awesome device",
),
),
]);
final result = await conn.connect(
waitUntilLogin: true,
shouldReconnect: false,
enableReconnectOnSuccess: false,
);
expect(result.isType<bool>(), true);
expect(conn.getNegotiatorById<Sasl2Negotiator>(sasl2Negotiator)!.state, NegotiatorState.done);
expect(conn.getNegotiatorById<FASTSaslNegotiator>(saslFASTNegotiator)!.fastToken != null, true,);
});
}

View File

@ -3,6 +3,9 @@
- **BREAKING**: Removed `connectAwaitable` and merged it with `connect`.
- **BREAKING**: Removed `allowPlainAuth` from `ConnectionSettings`. If you don't want to use SASL PLAIN, don't register the negotiator. If you want to only conditionally use SASL PLAIN, extend the `SaslPlainNegotiator` and override its `matchesFeature` method to only call the super method when SASL PLAIN should be used.
- **BREAKING**: The user avatar's `subscribe` and `unsubscribe` no longer subscribe to the `:data` PubSub nodes
- Renamed `ResourceBindingSuccessEvent` to `ResourceBoundEvent`
- **BREAKING**: Removed `isFeatureSupported` from the manager attributes. The managers now all have a method `isFeatureSupported` that works the same
- The `PresenceManager` is now optional
## 0.1.6+1

View File

@ -18,17 +18,17 @@ export 'package:moxxmpp/src/namespaces.dart';
export 'package:moxxmpp/src/negotiators/manager.dart';
export 'package:moxxmpp/src/negotiators/namespaces.dart';
export 'package:moxxmpp/src/negotiators/negotiator.dart';
export 'package:moxxmpp/src/negotiators/resource_binding.dart';
export 'package:moxxmpp/src/negotiators/sasl/errors.dart';
export 'package:moxxmpp/src/negotiators/sasl/negotiator.dart';
export 'package:moxxmpp/src/negotiators/sasl/plain.dart';
export 'package:moxxmpp/src/negotiators/sasl/scram.dart';
export 'package:moxxmpp/src/negotiators/starttls.dart';
export 'package:moxxmpp/src/ping.dart';
export 'package:moxxmpp/src/presence.dart';
export 'package:moxxmpp/src/reconnect.dart';
export 'package:moxxmpp/src/rfcs/rfc_2782.dart';
export 'package:moxxmpp/src/rfcs/rfc_4790.dart';
export 'package:moxxmpp/src/rfcs/rfc_6120/resource_binding.dart';
export 'package:moxxmpp/src/rfcs/rfc_6120/sasl/errors.dart';
export 'package:moxxmpp/src/rfcs/rfc_6120/sasl/negotiator.dart';
export 'package:moxxmpp/src/rfcs/rfc_6120/sasl/plain.dart';
export 'package:moxxmpp/src/rfcs/rfc_6120/sasl/scram.dart';
export 'package:moxxmpp/src/rfcs/rfc_6120/starttls.dart';
export 'package:moxxmpp/src/roster/errors.dart';
export 'package:moxxmpp/src/roster/roster.dart';
export 'package:moxxmpp/src/roster/state.dart';
@ -38,6 +38,7 @@ export 'package:moxxmpp/src/stanza.dart';
export 'package:moxxmpp/src/stringxml.dart';
export 'package:moxxmpp/src/types/result.dart';
export 'package:moxxmpp/src/xeps/staging/extensible_file_thumbnails.dart';
export 'package:moxxmpp/src/xeps/staging/fast.dart';
export 'package:moxxmpp/src/xeps/staging/file_upload_notification.dart';
export 'package:moxxmpp/src/xeps/xep_0004.dart';
export 'package:moxxmpp/src/xeps/xep_0030/errors.dart';
@ -76,6 +77,11 @@ export 'package:moxxmpp/src/xeps/xep_0384/helpers.dart';
export 'package:moxxmpp/src/xeps/xep_0384/types.dart';
export 'package:moxxmpp/src/xeps/xep_0384/xep_0384.dart';
export 'package:moxxmpp/src/xeps/xep_0385.dart';
export 'package:moxxmpp/src/xeps/xep_0386.dart';
export 'package:moxxmpp/src/xeps/xep_0388/errors.dart';
export 'package:moxxmpp/src/xeps/xep_0388/negotiators.dart';
export 'package:moxxmpp/src/xeps/xep_0388/user_agent.dart';
export 'package:moxxmpp/src/xeps/xep_0388/xep_0388.dart';
export 'package:moxxmpp/src/xeps/xep_0414.dart';
export 'package:moxxmpp/src/xeps/xep_0424.dart';
export 'package:moxxmpp/src/xeps/xep_0444.dart';

View File

@ -9,6 +9,7 @@ import 'package:moxxmpp/src/connectivity.dart';
import 'package:moxxmpp/src/errors.dart';
import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/iq.dart';
import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/attributes.dart';
import 'package:moxxmpp/src/managers/base.dart';
import 'package:moxxmpp/src/managers/data.dart';
@ -63,14 +64,15 @@ enum StanzaFromType {
/// Nonza describing the XMPP stream header.
class StreamHeaderNonza extends XMLNode {
StreamHeaderNonza(String serverDomain)
StreamHeaderNonza(JID jid)
: super(
tag: 'stream:stream',
attributes: <String, String>{
'xmlns': stanzaXmlns,
'version': '1.0',
'xmlns:stream': streamXmlns,
'to': serverDomain,
'to': jid.domain,
'from': jid.toBare().toString(),
'xml:lang': 'en',
},
closeTag: false,
@ -133,9 +135,6 @@ class XmppConnection {
StreamController.broadcast();
final Map<String, XmppManagerBase> _xmppManagers = {};
/// Disco info we got after binding a resource (xmlns)
final List<String> _serverFeatures = List.empty(growable: true);
/// The buffer object to keep split up stanzas together
final XmlStreamBuffer _streamBuffer = XmlStreamBuffer();
@ -150,11 +149,12 @@ class XmppConnection {
/// The currently bound resource or '' if none has been bound yet.
String _resource = '';
String get resource => _resource;
/// True if we are authenticated. False if not.
bool _isAuthenticated = false;
/// Timer for the connecting timeout
/// Timer for the connecting timeout.
Timer? _connectingTimeoutTimer;
/// Completers for certain actions
@ -201,8 +201,6 @@ class XmppConnection {
ReconnectionPolicy get reconnectionPolicy => _reconnectionPolicy;
List<String> get serverFeatures => _serverFeatures;
bool get isAuthenticated => _isAuthenticated;
/// Return the registered feature negotiator that has id [id]. Returns null if
@ -221,7 +219,6 @@ class XmppConnection {
sendEvent: _sendEvent,
getConnectionSettings: () => _connectionSettings,
getManagerById: getManagerById,
isFeatureSupported: _serverFeatures.contains,
getFullJID: () => _connectionSettings.jid.withResource(_resource),
getSocket: () => _socket,
getConnection: () => this,
@ -253,13 +250,29 @@ class XmppConnection {
}
}
// Mark the current connection as authenticated.
void _setAuthenticated() {
_sendEvent(AuthenticationSuccessEvent());
_isAuthenticated = true;
}
/// Remove [feature] from the stream features we are currently negotiating.
void _removeNegotiatingFeature(String feature) {
_streamFeatures.removeWhere((node) {
return node.attributes['xmlns'] == feature;
});
}
/// Register a list of negotiator with the connection.
void registerFeatureNegotiators(List<XmppFeatureNegotiatorBase> negotiators) {
Future<void> registerFeatureNegotiators(
List<XmppFeatureNegotiatorBase> negotiators,
) async {
for (final negotiator in negotiators) {
_log.finest('Registering ${negotiator.id}');
negotiator.register(
NegotiatorAttributes(
sendRawXML,
() => this,
() => _connectionSettings,
_sendEvent,
getNegotiatorById,
@ -267,12 +280,19 @@ class XmppConnection {
() => _connectionSettings.jid.withResource(_resource),
() => _socket,
() => _isAuthenticated,
_setAuthenticated,
setResource,
_removeNegotiatingFeature,
),
);
_featureNegotiators[negotiator.id] = negotiator;
}
_log.finest('Negotiators registered');
for (final negotiator in _featureNegotiators.values) {
await negotiator.postRegisterCallback();
}
}
/// Reset all registered negotiators.
@ -296,13 +316,8 @@ class XmppConnection {
/// A [PresenceManager] is required, so have a wrapper for getting it.
/// Returns the registered [PresenceManager].
PresenceManager getPresenceManager() {
assert(
_xmppManagers.containsKey(presenceManager),
'A PresenceManager is mandatory',
);
return getManagerById(presenceManager)!;
PresenceManager? getPresenceManager() {
return getManagerById(presenceManager);
}
/// A [DiscoManager] is required so, have a wrapper for getting it.
@ -399,7 +414,7 @@ class XmppConnection {
// Close the socket
_socket.close();
if (!error.isRecoverable()) {
// We cannot recover this error
_log.severe(
@ -454,10 +469,10 @@ class XmppConnection {
}
/// Sends an [XMLNode] without any further processing to the server.
void sendRawXML(XMLNode node, {String? redact}) {
void sendRawXML(XMLNode node) {
final string = node.toXml();
_log.finest('==> $string');
_socket.write(string, redact: redact);
_socket.write(string);
}
/// Sends [raw] to the server.
@ -657,9 +672,14 @@ class XmppConnection {
}
/// Sets the resource of the connection
void setResource(String resource) {
@visibleForTesting
void setResource(String resource, {bool triggerEvent = true}) {
_log.finest('Updating _resource to $resource');
_resource = resource;
if (triggerEvent) {
_sendEvent(ResourceBoundEvent(resource));
}
}
/// Returns the connection's events as a stream.
@ -839,9 +859,6 @@ class XmppConnection {
// Tell consumers of the event stream that we're done with stream feature
// negotiations
await _sendEvent(StreamNegotiationsDoneEvent());
// Send out initial presence
await getPresenceManager().sendInitialPresence();
}
Future<void> _executeCurrentNegotiator(XMLNode nonza) async {
@ -895,10 +912,7 @@ class XmppConnection {
_streamFeatures.clear();
_sendStreamHeader();
} else {
_streamFeatures.removeWhere((node) {
return node.attributes['xmlns'] ==
_currentNegotiator!.negotiatingXmlns;
});
_removeNegotiatingFeature(_currentNegotiator!.negotiatingXmlns);
_currentNegotiator = null;
if (_isMandatoryNegotiationDone(_streamFeatures) &&
@ -1010,25 +1024,12 @@ class XmppConnection {
Future<void> _sendEvent(XmppEvent event) async {
_log.finest('Event: ${event.toString()}');
// Specific event handling
if (event is ResourceBindingSuccessEvent) {
_log.finest(
'Received ResourceBindingSuccessEvent. Setting _resource to ${event.resource}',
);
setResource(event.resource);
_log.finest('Resetting _serverFeatures');
_serverFeatures.clear();
} else if (event is AuthenticationSuccessEvent) {
_log.finest(
'Received AuthenticationSuccessEvent. Setting _isAuthenticated to true',
);
_isAuthenticated = true;
}
for (final manager in _xmppManagers.values) {
await manager.onXmppEvent(event);
}
for (final negotiator in _featureNegotiators.values) {
await negotiator.onXmppEvent(event);
}
_eventStreamController.add(event);
}
@ -1038,11 +1039,11 @@ class XmppConnection {
_socket.write(
XMLNode(
tag: 'xml',
attributes: <String, String>{'version': '1.0'},
attributes: {'version': '1.0'},
closeTag: false,
isDeclaration: true,
children: [
StreamHeaderNonza(_connectionSettings.jid.domain),
StreamHeaderNonza(_connectionSettings.jid),
],
).toXml(),
);
@ -1067,7 +1068,7 @@ class XmppConnection {
await _reconnectionPolicy.setShouldReconnect(false);
if (triggeredByUser) {
getPresenceManager().sendUnavailablePresence();
getPresenceManager()?.sendUnavailablePresence();
}
_socket.prepareDisconnect();
@ -1087,10 +1088,6 @@ class XmppConnection {
/// Make sure that all required managers are registered
void _runPreConnectionAssertions() {
assert(
_xmppManagers.containsKey(presenceManager),
'A PresenceManager is mandatory',
);
assert(
_xmppManagers.containsKey(rosterManager),
'A RosterManager is mandatory',
@ -1138,7 +1135,9 @@ class XmppConnection {
}
if (lastResource != null) {
setResource(lastResource);
setResource(lastResource, triggerEvent: false);
} else {
setResource('', triggerEvent: false);
}
_enableReconnectOnSuccess = enableReconnectOnSuccess;
@ -1159,13 +1158,16 @@ class XmppConnection {
}
final smManager = getStreamManagementManager();
String? host;
int? port;
var host = _connectionSettings.host;
var port = _connectionSettings.port;
if (smManager?.state.streamResumptionLocation != null) {
// TODO(Unknown): Maybe wrap this in a try catch?
final parsed = Uri.parse(smManager!.state.streamResumptionLocation!);
host = parsed.host;
port = parsed.port;
} else {
host = _connectionSettings.host;
port = _connectionSettings.port;
}
final result = await _socket.connect(

View File

@ -160,8 +160,10 @@ class StreamManagementEnabledEvent extends XmppEvent {
}
/// Triggered when we bound a resource
class ResourceBindingSuccessEvent extends XmppEvent {
ResourceBindingSuccessEvent({required this.resource});
class ResourceBoundEvent extends XmppEvent {
ResourceBoundEvent(this.resource);
/// The resource that was just bound.
final String resource;
}

View File

@ -16,7 +16,6 @@ class XmppManagerAttributes {
required this.getManagerById,
required this.sendEvent,
required this.getConnectionSettings,
required this.isFeatureSupported,
required this.getFullJID,
required this.getSocket,
required this.getConnection,
@ -45,9 +44,6 @@ class XmppManagerAttributes {
/// (Maybe) Get a Manager attached to the connection by its Id.
final T? Function<T extends XmppManagerBase>(String) getManagerById;
/// Returns true if a server feature is supported
final bool Function(String) isFeatureSupported;
/// Returns the full JID of the current account
final JID Function() getFullJID;

View File

@ -6,6 +6,7 @@ import 'package:moxxmpp/src/managers/data.dart';
import 'package:moxxmpp/src/managers/handlers.dart';
import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/xeps/xep_0030/errors.dart';
import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
import 'package:moxxmpp/src/xeps/xep_0198/xep_0198.dart';
@ -31,6 +32,29 @@ abstract class XmppManagerBase {
return _managerAttributes;
}
/// Resolves to true when the server supports the disco feature [xmlns]. Resolves
/// to false when either the disco request fails or the server does not
/// support [xmlns].
/// Note that this function requires a registered DiscoManager.
@protected
Future<bool> isFeatureSupported(String xmlns) async {
final dm = _managerAttributes.getManagerById<DiscoManager>(discoManager);
assert(
dm != null,
'The DiscoManager must be registered for isFeatureSupported to work',
);
final result = await dm!.discoInfoQuery(
_managerAttributes.getConnectionSettings().jid.domain,
shouldEncrypt: false,
);
if (result.isType<DiscoError>()) {
return false;
}
return result.get<DiscoInfo>().features.contains(xmlns);
}
/// Return the StanzaHandlers associated with this manager that deal with stanzas we
/// send. These are run before the stanza is sent. The higher the value of the
/// handler's priority, the earlier it is run.

View File

@ -116,6 +116,12 @@ const omemoBundlesXmlns = 'urn:xmpp:omemo:2:bundles';
// XEP-0385
const simsXmlns = 'urn:xmpp:sims:1';
// XEP-0386
const bind2Xmlns = 'urn:xmpp:bind:0';
// XEP-0388
const sasl2Xmlns = 'urn:xmpp:sasl:2';
// XEP-0420
const sceXmlns = 'urn:xmpp:sce:1';
@ -154,3 +160,6 @@ const fallbackXmlns = 'urn:xmpp:feature-fallback:0';
// ???
const urlDataXmlns = 'http://jabber.org/protocol/url-data';
// XEP-XXXX
const fastXmlns = 'urn:xmpp:fast:0';

View File

@ -7,3 +7,7 @@ const rosterNegotiator = 'im.moxxmpp.core.roster';
const resourceBindingNegotiator = 'im.moxxmpp.core.resource';
const streamManagementNegotiator = 'im.moxxmpp.xeps.sm';
const startTlsNegotiator = 'im.moxxmpp.core.starttls';
const sasl2Negotiator = 'org.moxxmpp.sasl.sasl2';
const bind2Negotiator = 'org.moxxmpp.bind2';
const saslFASTNegotiator = 'org.moxxmpp.sasl.fast';
const carbonsNegotiator = 'org.moxxmpp.bind2.carbons';

View File

@ -1,4 +1,6 @@
import 'package:meta/meta.dart';
import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/connection.dart';
import 'package:moxxmpp/src/errors.dart';
import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/jid.dart';
@ -27,6 +29,7 @@ abstract class NegotiatorError extends XmppError {}
class NegotiatorAttributes {
const NegotiatorAttributes(
this.sendNonza,
this.getConnection,
this.getConnectionSettings,
this.sendEvent,
this.getNegotiatorById,
@ -34,15 +37,21 @@ class NegotiatorAttributes {
this.getFullJID,
this.getSocket,
this.isAuthenticated,
this.setAuthenticated,
this.setResource,
this.removeNegotiatingFeature,
);
/// Sends the nonza nonza and optionally redacts it in logs if redact is not null.
final void Function(XMLNode nonza, {String? redact}) sendNonza;
final void Function(XMLNode nonza) sendNonza;
/// Returns the connection settings.
final ConnectionSettings Function() getConnectionSettings;
/// Send an event event to the connection's event bus
/// Returns the connection object.
final XmppConnection Function() getConnection;
/// Send an event event to the connection's event bus.
final Future<void> Function(XmppEvent event) sendEvent;
/// Returns the negotiator with id id of the connection or null.
@ -60,6 +69,17 @@ class NegotiatorAttributes {
/// Returns true if the stream is authenticated. Returns false if not.
final bool Function() isAuthenticated;
/// Sets the resource of the connection. If triggerEvent is true, then a
/// [ResourceBoundEvent] is triggered.
final void Function(String, {bool triggerEvent}) setResource;
/// Sets the authentication state of the connection to true.
final void Function() setAuthenticated;
/// Remove a stream feature from our internal cache. This is useful for when you
/// negotiated a feature for another negotiator, like SASL2.
final void Function(String) removeNegotiatingFeature;
}
abstract class XmppFeatureNegotiatorBase {
@ -104,6 +124,9 @@ abstract class XmppFeatureNegotiatorBase {
null;
}
/// Called when an event is triggered in the [XmppConnection].
Future<void> onXmppEvent(XmppEvent event) async {}
/// Called with the currently received nonza [nonza] when the negotiator is active.
/// If the negotiator is just elected to be the next one, then [nonza] is equal to
/// the <stream:features /> nonza.
@ -120,5 +143,10 @@ abstract class XmppFeatureNegotiatorBase {
state = NegotiatorState.ready;
}
@protected
NegotiatorAttributes get attributes => _attributes;
/// Run after all negotiators are registered. Useful for registering callbacks against
/// other negotiators. By default this function does nothing.
Future<void> postRegisterCallback() async {}
}

View File

@ -1,3 +1,4 @@
import 'dart:async';
import 'package:moxxmpp/src/connection.dart';
import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/jid.dart';
@ -6,8 +7,10 @@ import 'package:moxxmpp/src/managers/data.dart';
import 'package:moxxmpp/src/managers/handlers.dart';
import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/negotiators/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/xeps/xep_0198/negotiator.dart';
/// A function that will be called when presence, outside of subscription request
/// management, will be sent. Useful for managers that want to add [XMLNode]s to said
@ -42,6 +45,20 @@ class PresenceManager extends XmppManagerBase {
_presenceCallbacks.add(callback);
}
@override
Future<void> onXmppEvent(XmppEvent event) async {
if (event is StreamNegotiationsDoneEvent) {
// Send initial presence only when we have not resumed the stream
final sm = getAttributes().getNegotiatorById<StreamManagementNegotiator>(
streamManagementNegotiator,
);
final isResumed = sm?.isResumed ?? false;
if (!isResumed) {
unawaited(sendInitialPresence());
}
}
}
Future<StanzaHandlerData> _onPresence(
Stanza presence,
StanzaHandlerData state,

View File

@ -1,4 +1,4 @@
import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/negotiators/namespaces.dart';
@ -30,10 +30,13 @@ class ResourceBindingNegotiator extends XmppFeatureNegotiatorBase {
if (sm != null) {
return super.matchesFeature(features) &&
!sm.streamResumed &&
attributes.isAuthenticated();
attributes.isAuthenticated() &&
attributes.getConnection().resource.isEmpty;
}
return super.matchesFeature(features) && attributes.isAuthenticated();
return super.matchesFeature(features) &&
attributes.isAuthenticated() &&
attributes.getConnection().resource.isEmpty;
}
@override
@ -65,11 +68,9 @@ class ResourceBindingNegotiator extends XmppFeatureNegotiatorBase {
}
final bind = nonza.firstTag('bind')!;
final jid = bind.firstTag('jid')!;
final resource = jid.innerText().split('/')[1];
await attributes
.sendEvent(ResourceBindingSuccessEvent(resource: resource));
final rawJid = bind.firstTag('jid')!.innerText();
final resource = JID.fromString(rawJid).resource;
attributes.setResource(resource);
return const Result(NegotiatorState.done);
}
}

View File

@ -3,21 +3,23 @@ import 'package:logging/logging.dart';
import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/negotiators/namespaces.dart';
import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/negotiators/sasl/errors.dart';
import 'package:moxxmpp/src/negotiators/sasl/negotiator.dart';
import 'package:moxxmpp/src/negotiators/sasl/nonza.dart';
import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/errors.dart';
import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/nonza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
import 'package:moxxmpp/src/xeps/xep_0388/negotiators.dart';
import 'package:moxxmpp/src/xeps/xep_0388/xep_0388.dart';
import 'package:saslprep/saslprep.dart';
class SaslPlainAuthNonza extends SaslAuthNonza {
SaslPlainAuthNonza(String username, String password)
SaslPlainAuthNonza(String data)
: super(
'PLAIN',
base64.encode(utf8.encode('\u0000$username\u0000$password')),
data,
);
}
class SaslPlainNegotiator extends SaslNegotiator {
class SaslPlainNegotiator extends Sasl2AuthenticationNegotiator {
SaslPlainNegotiator()
: _authSent = false,
_log = Logger('SaslPlainNegotiator'),
@ -47,17 +49,16 @@ class SaslPlainNegotiator extends SaslNegotiator {
XMLNode nonza,
) async {
if (!_authSent) {
final settings = attributes.getConnectionSettings();
final data = await getRawStep('');
attributes.sendNonza(
SaslPlainAuthNonza(settings.jid.local, settings.password),
redact: SaslPlainAuthNonza('******', '******').toXml(),
SaslPlainAuthNonza(data),
);
_authSent = true;
return const Result(NegotiatorState.ready);
} else {
final tag = nonza.tag;
if (tag == 'success') {
await attributes.sendEvent(AuthenticationSuccessEvent());
attributes.setAuthenticated();
return const Result(NegotiatorState.done);
} else {
// We assume it's a <failure/>
@ -76,4 +77,34 @@ class SaslPlainNegotiator extends SaslNegotiator {
super.reset();
}
@override
Future<void> postRegisterCallback() async {
attributes
.getNegotiatorById<Sasl2Negotiator>(sasl2Negotiator)
?.registerSaslNegotiator(this);
}
@override
Future<String> getRawStep(String input) async {
final settings = attributes.getConnectionSettings();
final prep = Saslprep.saslprep(settings.password);
return base64.encode(
utf8.encode('\u0000${settings.jid.local}\u0000$prep'),
);
}
@override
Future<Result<bool, NegotiatorError>> onSasl2Success(XMLNode response) async {
state = NegotiatorState.done;
return const Result(true);
}
@override
Future<void> onSasl2Failure(XMLNode response) async {}
@override
Future<List<XMLNode>> onSasl2FeaturesReceived(XMLNode sasl2Features) async {
return [];
}
}

View File

@ -6,15 +6,28 @@ import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/negotiators/namespaces.dart';
import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/negotiators/sasl/errors.dart';
import 'package:moxxmpp/src/negotiators/sasl/kv.dart';
import 'package:moxxmpp/src/negotiators/sasl/negotiator.dart';
import 'package:moxxmpp/src/negotiators/sasl/nonza.dart';
import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/errors.dart';
import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/kv.dart';
import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/nonza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
import 'package:moxxmpp/src/xeps/xep_0388/negotiators.dart';
import 'package:moxxmpp/src/xeps/xep_0388/xep_0388.dart';
import 'package:random_string/random_string.dart';
import 'package:saslprep/saslprep.dart';
abstract class SaslScramError extends NegotiatorError {}
class NoAdditionalDataError extends SaslScramError {
@override
bool isRecoverable() => false;
}
class InvalidServerSignatureError extends SaslScramError {
@override
bool isRecoverable() => false;
}
// NOTE: Inspired by https://github.com/vukoye/xmpp_dart/blob/3b1a0588562b9e591488c99d834088391840911d/lib/src/features/sasl/ScramSaslHandler.dart
enum ScramHashType { sha1, sha256, sha512 }
@ -95,7 +108,7 @@ enum ScramState { preSent, initialMessageSent, challengeResponseSent, error }
const gs2Header = 'n,,';
class SaslScramNegotiator extends SaslNegotiator {
class SaslScramNegotiator extends Sasl2AuthenticationNegotiator {
// NOTE: NEVER, and I mean, NEVER set clientNonce or initalMessageNoGS2. They are just there for testing
SaslScramNegotiator(
int priority,
@ -230,29 +243,23 @@ class SaslScramNegotiator extends SaslNegotiator {
return false;
}
bool _checkSignature(String base64Signature) {
final signature =
parseKeyValue(utf8.decode(base64.decode(base64Signature)));
return signature['v']! == _serverSignature;
}
@override
Future<Result<NegotiatorState, NegotiatorError>> negotiate(
XMLNode nonza,
) async {
switch (_scramState) {
case ScramState.preSent:
if (clientNonce == null || clientNonce == '') {
clientNonce = randomAlphaNumeric(
40,
provider: CoreRandomProvider.from(Random.secure()),
);
}
initialMessageNoGS2 =
'n=${attributes.getConnectionSettings().jid.local},r=$clientNonce';
_scramState = ScramState.initialMessageSent;
attributes.sendNonza(
SaslScramAuthNonza(
body: base64.encode(utf8.encode(gs2Header + initialMessageNoGS2)),
body: await getRawStep(''),
type: hashType,
),
redact: SaslScramAuthNonza(body: '******', type: hashType).toXml(),
);
return const Result(NegotiatorState.ready);
case ScramState.initialMessageSent:
@ -266,13 +273,8 @@ class SaslScramNegotiator extends SaslNegotiator {
);
}
final challengeBase64 = nonza.innerText();
final response = await calculateChallengeResponse(challengeBase64);
final responseBase64 = base64.encode(utf8.encode(response));
_scramState = ScramState.challengeResponseSent;
attributes.sendNonza(
SaslScramResponseNonza(body: responseBase64),
redact: SaslScramResponseNonza(body: '******').toXml(),
SaslScramResponseNonza(body: await getRawStep(nonza.innerText())),
);
return const Result(NegotiatorState.ready);
case ScramState.challengeResponseSent:
@ -286,10 +288,7 @@ class SaslScramNegotiator extends SaslNegotiator {
);
}
// NOTE: This assumes that the string is always "v=..." and contains no other parameters
final signature =
parseKeyValue(utf8.decode(base64.decode(nonza.innerText())));
if (signature['v']! != _serverSignature) {
if (!_checkSignature(nonza.innerText())) {
// TODO(Unknown): Notify of a signature mismatch
//final error = nonza.children.first.tag;
//attributes.sendEvent(AuthenticationFailedEvent(error));
@ -299,7 +298,7 @@ class SaslScramNegotiator extends SaslNegotiator {
);
}
await attributes.sendEvent(AuthenticationSuccessEvent());
attributes.setAuthenticated();
return const Result(NegotiatorState.done);
case ScramState.error:
return Result(
@ -314,4 +313,65 @@ class SaslScramNegotiator extends SaslNegotiator {
super.reset();
}
@override
Future<String> getRawStep(String input) async {
switch (_scramState) {
case ScramState.preSent:
if (clientNonce == null || clientNonce == '') {
clientNonce = randomAlphaNumeric(
40,
provider: CoreRandomProvider.from(Random.secure()),
);
}
initialMessageNoGS2 =
'n=${attributes.getConnectionSettings().jid.local},r=$clientNonce';
_scramState = ScramState.initialMessageSent;
return base64.encode(utf8.encode(gs2Header + initialMessageNoGS2));
case ScramState.initialMessageSent:
final challengeBase64 = input;
final response = await calculateChallengeResponse(challengeBase64);
final responseBase64 = base64.encode(utf8.encode(response));
_scramState = ScramState.challengeResponseSent;
return responseBase64;
case ScramState.challengeResponseSent:
case ScramState.error:
return '';
}
}
@override
Future<void> postRegisterCallback() async {
attributes
.getNegotiatorById<Sasl2Negotiator>(sasl2Negotiator)
?.registerSaslNegotiator(this);
}
@override
Future<List<XMLNode>> onSasl2FeaturesReceived(XMLNode sasl2Features) async {
return [];
}
@override
Future<void> onSasl2Failure(XMLNode response) async {}
@override
Future<Result<bool, NegotiatorError>> onSasl2Success(XMLNode response) async {
// When we're done with SASL2, check the additional data to verify the server
// signature.
state = NegotiatorState.done;
final additionalData = response.firstTag('additional-data');
if (additionalData == null) {
return Result(NoAdditionalDataError());
}
if (!_checkSignature(additionalData.innerText())) {
return Result(InvalidServerSignatureError());
}
return const Result(true);
}
}

View File

@ -5,8 +5,13 @@ class ConnectionSettings {
required this.jid,
required this.password,
required this.useDirectTLS,
this.host,
this.port,
});
final JID jid;
final String password;
final bool useDirectTLS;
final String? host;
final int? port;
}

View File

@ -30,9 +30,8 @@ abstract class BaseSocketWrapper {
/// reused by calling [this.connect] again.
void close();
/// Write [data] into the socket. If [redact] is not null, then [redact] will be
/// logged instead of [data].
void write(String data, {String? redact});
/// Write [data] into the socket.
void write(String data);
/// This must connect to [host]:[port] and initialize the streams accordingly.
/// [domain] is the domain that TLS should be validated against, in case the Socket

View File

@ -146,4 +146,6 @@ class XMLNode {
String innerText() {
return text ?? '';
}
String? get xmlns => attributes['xmlns'] as String?;
}

View File

@ -0,0 +1,167 @@
import 'package:collection/collection.dart';
import 'package:logging/logging.dart';
import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/negotiators/namespaces.dart';
import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
import 'package:moxxmpp/src/xeps/xep_0388/negotiators.dart';
import 'package:moxxmpp/src/xeps/xep_0388/xep_0388.dart';
/// This event is triggered whenever a new FAST token is received.
class NewFASTTokenReceivedEvent extends XmppEvent {
NewFASTTokenReceivedEvent(this.token);
/// The token.
final FASTToken token;
}
/// This event is triggered whenever a new FAST token is invalidated because it's
/// invalid.
class InvalidateFASTTokenEvent extends XmppEvent {
InvalidateFASTTokenEvent();
}
/// The description of a token for FAST authentication.
class FASTToken {
const FASTToken(
this.token,
this.expiry,
);
factory FASTToken.fromXml(XMLNode token) {
assert(
token.tag == 'token',
'Token can only be deserialised from a <token /> element',
);
assert(
token.xmlns == fastXmlns,
'Token can only be deserialised from a <token /> element',
);
return FASTToken(
token.attributes['token']! as String,
token.attributes['expiry']! as String,
);
}
/// The actual token.
final String token;
/// The token's expiry.
final String expiry;
}
// TODO(Unknown): Implement multiple hash functions, similar to how we do SCRAM
class FASTSaslNegotiator extends Sasl2AuthenticationNegotiator {
FASTSaslNegotiator() : super(20, saslFASTNegotiator, 'HT-SHA-256-NONE');
final Logger _log = Logger('FASTSaslNegotiator');
/// The token, if non-null, to use for authentication.
FASTToken? fastToken;
@override
bool matchesFeature(List<XMLNode> features) {
if (fastToken == null) {
return false;
}
if (super.matchesFeature(features)) {
if (!attributes.getSocket().isSecure()) {
_log.warning(
'Refusing to match SASL feature due to unsecured connection',
);
return false;
}
return true;
}
return false;
}
@override
bool canInlineFeature(List<XMLNode> features) {
return features.firstWhereOrNull(
(child) => child.tag == 'fast' && child.xmlns == fastXmlns,
) !=
null;
}
@override
Future<Result<NegotiatorState, NegotiatorError>> negotiate(
XMLNode nonza,
) async {
// TODO(Unknown): Is FAST supposed to work without SASL2?
return const Result(NegotiatorState.done);
}
@override
Future<Result<bool, NegotiatorError>> onSasl2Success(XMLNode response) async {
final token = response.firstTag('token', xmlns: fastXmlns);
if (token != null) {
fastToken = FASTToken.fromXml(token);
await attributes.sendEvent(
NewFASTTokenReceivedEvent(fastToken!),
);
}
state = NegotiatorState.done;
return const Result(true);
}
@override
Future<void> onSasl2Failure(XMLNode response) async {
fastToken = null;
await attributes.sendEvent(
InvalidateFASTTokenEvent(),
);
}
@override
bool shouldRetrySasl() => true;
@override
Future<List<XMLNode>> onSasl2FeaturesReceived(XMLNode sasl2Features) async {
if (fastToken != null && pickedForSasl2) {
// Specify that we are using a token
return [
// As we don't do TLS 0-RTT, we don't have to specify `count`.
XMLNode.xmlns(
tag: 'fast',
xmlns: fastXmlns,
),
];
}
// Only request a new token when we don't already have one and we are not picked
// for SASL
if (!pickedForSasl2) {
return [
XMLNode.xmlns(
tag: 'request-token',
xmlns: fastXmlns,
attributes: {
'mechanism': 'HT-SHA-256-NONE',
},
),
];
} else {
return [];
}
}
@override
Future<String> getRawStep(String input) async {
return fastToken!.token;
}
@override
Future<void> postRegisterCallback() async {
attributes
.getNegotiatorById<Sasl2Negotiator>(sasl2Negotiator)
?.registerSaslNegotiator(this);
}
}

View File

@ -1,4 +1,6 @@
import 'package:collection/collection.dart';
import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart';
@ -10,6 +12,9 @@ import 'package:moxxmpp/src/xeps/xep_0198/nonzas.dart';
import 'package:moxxmpp/src/xeps/xep_0198/state.dart';
import 'package:moxxmpp/src/xeps/xep_0198/xep_0198.dart';
import 'package:moxxmpp/src/xeps/xep_0352.dart';
import 'package:moxxmpp/src/xeps/xep_0386.dart';
import 'package:moxxmpp/src/xeps/xep_0388/negotiators.dart';
import 'package:moxxmpp/src/xeps/xep_0388/xep_0388.dart';
enum _StreamManagementNegotiatorState {
// We have not done anything yet
@ -23,26 +28,71 @@ enum _StreamManagementNegotiatorState {
/// NOTE: The stream management negotiator requires that loadState has been called on the
/// StreamManagementManager at least once before connecting, if stream resumption
/// is wanted.
class StreamManagementNegotiator extends XmppFeatureNegotiatorBase {
class StreamManagementNegotiator extends Sasl2FeatureNegotiator
implements Bind2FeatureNegotiatorInterface {
StreamManagementNegotiator()
: _state = _StreamManagementNegotiatorState.ready,
_supported = false,
_resumeFailed = false,
_isResumed = false,
_log = Logger('StreamManagementNegotiator'),
super(10, false, smXmlns, streamManagementNegotiator);
_StreamManagementNegotiatorState _state;
bool _resumeFailed;
bool _isResumed;
: super(10, false, smXmlns, streamManagementNegotiator);
final Logger _log;
/// Stream Management negotiation state.
_StreamManagementNegotiatorState _state =
_StreamManagementNegotiatorState.ready;
/// Flag indicating whether the resume failed (true) or succeeded (false).
bool _resumeFailed = false;
bool get resumeFailed => _resumeFailed;
/// Flag indicating whether the current stream is resumed (true) or not (false).
bool _isResumed = false;
bool get isResumed => _isResumed;
/// Flag indicating that stream enablement failed
bool _streamEnablementFailed = false;
bool get streamEnablementFailed => _streamEnablementFailed;
/// Logger
final Logger _log = Logger('StreamManagementNegotiator');
/// True if Stream Management is supported on this stream.
bool _supported;
bool _supported = false;
bool get isSupported => _supported;
/// True if the current stream is resumed. False if not.
bool get isResumed => _isResumed;
/// True if we requested stream enablement inline
bool _inlineStreamEnablementRequested = false;
/// Cached resource for stream resumption
String _resource = '';
@visibleForTesting
void setResource(String resource) {
_resource = resource;
}
@override
bool canInlineFeature(List<XMLNode> features) {
final sm = attributes.getManagerById<StreamManagementManager>(smManager)!;
// We do not check here for authentication as enabling/resuming happens inline
// with the authentication.
if (sm.state.streamResumptionId != null && !_resumeFailed) {
// We can try to resume the stream or enable the stream
return features.firstWhereOrNull(
(child) => child.xmlns == smXmlns,
) !=
null;
} else {
// We can try to enable SM
return features.firstWhereOrNull(
(child) => child.tag == 'enable' && child.xmlns == smXmlns,
) !=
null;
}
}
@override
Future<void> onXmppEvent(XmppEvent event) async {
if (event is ResourceBoundEvent) {
_resource = event.resource;
}
}
@override
bool matchesFeature(List<XMLNode> features) {
@ -53,13 +103,66 @@ class StreamManagementNegotiator extends XmppFeatureNegotiatorBase {
return super.matchesFeature(features) && attributes.isAuthenticated();
} else {
// We cannot do a stream resumption
final br = attributes.getNegotiatorById(resourceBindingNegotiator);
return super.matchesFeature(features) &&
br?.state == NegotiatorState.done &&
attributes.getConnection().resource.isNotEmpty &&
attributes.isAuthenticated();
}
}
Future<void> _onStreamResumptionFailed() async {
await attributes.sendEvent(StreamResumeFailedEvent());
final sm = attributes.getManagerById<StreamManagementManager>(smManager)!;
// We have to do this because we otherwise get a stanza stuck in the queue,
// thus spamming the server on every <a /> nonza we receive.
// ignore: cascade_invocations
await sm.setState(StreamManagementState(0, 0));
await sm.commitState();
_resumeFailed = true;
_isResumed = false;
_state = _StreamManagementNegotiatorState.ready;
}
Future<void> _onStreamResumptionSuccessful(XMLNode resumed) async {
assert(resumed.tag == 'resumed', 'The correct element must be passed');
final h = int.parse(resumed.attributes['h']! as String);
await attributes.sendEvent(StreamResumedEvent(h: h));
_resumeFailed = false;
_isResumed = true;
if (attributes.getConnection().resource.isEmpty && _resource.isNotEmpty) {
attributes.setResource(_resource);
} else if (attributes.getConnection().resource.isNotEmpty &&
_resource.isEmpty) {
_resource = attributes.getConnection().resource;
}
}
Future<void> _onStreamEnablementSuccessful(XMLNode enabled) async {
assert(enabled.tag == 'enabled', 'The correct element must be used');
assert(enabled.xmlns == smXmlns, 'The correct element must be used');
final id = enabled.attributes['id'] as String?;
if (id != null && ['true', '1'].contains(enabled.attributes['resume'])) {
_log.info('Stream Resumption available');
}
await attributes.sendEvent(
StreamManagementEnabledEvent(
resource: attributes.getFullJID().resource,
id: id,
location: enabled.attributes['location'] as String?,
),
);
}
void _onStreamEnablementFailed() {
_streamEnablementFailed = true;
}
@override
Future<Result<NegotiatorState, NegotiatorError>> negotiate(
XMLNode nonza,
@ -103,54 +206,26 @@ class StreamManagementNegotiator extends XmppFeatureNegotiatorBase {
csi.restoreCSIState();
}
final h = int.parse(nonza.attributes['h']! as String);
await attributes.sendEvent(StreamResumedEvent(h: h));
_resumeFailed = false;
_isResumed = true;
await _onStreamResumptionSuccessful(nonza);
return const Result(NegotiatorState.skipRest);
} else {
// We assume it is <failed />
_log.info(
'Stream resumption failed. Expected <resumed />, got ${nonza.tag}, Proceeding with new stream...',
);
await attributes.sendEvent(StreamResumeFailedEvent());
final sm =
attributes.getManagerById<StreamManagementManager>(smManager)!;
// We have to do this because we otherwise get a stanza stuck in the queue,
// thus spamming the server on every <a /> nonza we receive.
// ignore: cascade_invocations
await sm.setState(StreamManagementState(0, 0));
await sm.commitState();
_resumeFailed = true;
_isResumed = false;
_state = _StreamManagementNegotiatorState.ready;
await _onStreamResumptionFailed();
return const Result(NegotiatorState.retryLater);
}
case _StreamManagementNegotiatorState.enableRequested:
if (nonza.tag == 'enabled') {
_log.finest('Stream Management enabled');
final id = nonza.attributes['id'] as String?;
if (id != null &&
['true', '1'].contains(nonza.attributes['resume'])) {
_log.info('Stream Resumption available');
}
await attributes.sendEvent(
StreamManagementEnabledEvent(
resource: attributes.getFullJID().resource,
id: id,
location: nonza.attributes['location'] as String?,
),
);
await _onStreamEnablementSuccessful(nonza);
return const Result(NegotiatorState.done);
} else {
// We assume a <failed />
_log.warning('Stream Management enablement failed');
_onStreamEnablementFailed();
return const Result(NegotiatorState.done);
}
}
@ -162,7 +237,97 @@ class StreamManagementNegotiator extends XmppFeatureNegotiatorBase {
_supported = false;
_resumeFailed = false;
_isResumed = false;
_inlineStreamEnablementRequested = false;
_streamEnablementFailed = false;
super.reset();
}
@override
Future<List<XMLNode>> onBind2FeaturesReceived(
List<String> bind2Features,
) async {
if (!bind2Features.contains(smXmlns)) {
return [];
}
_inlineStreamEnablementRequested = true;
return [
StreamManagementEnableNonza(),
];
}
@override
Future<void> onBind2Success(XMLNode response) async {}
@override
Future<List<XMLNode>> onSasl2FeaturesReceived(XMLNode sasl2Features) async {
final inline = sasl2Features.firstTag('inline')!;
final resume = inline.firstTag('resume', xmlns: smXmlns);
if (resume == null) {
return [];
}
final sm = attributes.getManagerById<StreamManagementManager>(smManager)!;
final srid = sm.state.streamResumptionId;
final h = sm.state.s2c;
if (srid == null) {
_log.finest('No srid');
return [];
}
return [
StreamManagementResumeNonza(
srid,
h,
),
];
}
@override
Future<Result<bool, NegotiatorError>> onSasl2Success(XMLNode response) async {
final enabled = response
.firstTag('bound', xmlns: bind2Xmlns)
?.firstTag('enabled', xmlns: smXmlns);
final resumed = response.firstTag('resumed', xmlns: smXmlns);
// We can only enable or resume->fail->enable. Thus, we check for enablement first
// and then exit.
if (_inlineStreamEnablementRequested) {
if (enabled != null) {
_log.finest('Inline stream enablement successful');
await _onStreamEnablementSuccessful(enabled);
return const Result(true);
} else {
_log.warning('Inline stream enablement failed');
_onStreamEnablementFailed();
}
}
if (resumed == null) {
_log.warning('Inline stream resumption failed');
await _onStreamResumptionFailed();
state = NegotiatorState.done;
return const Result(true);
}
_log.finest('Inline stream resumption successful');
await _onStreamResumptionSuccessful(resumed);
state = NegotiatorState.skipRest;
attributes.removeNegotiatingFeature(smXmlns);
attributes.removeNegotiatingFeature(bindXmlns);
return const Result(true);
}
@override
Future<void> postRegisterCallback() async {
attributes
.getNegotiatorById<Sasl2Negotiator>(sasl2Negotiator)
?.registerNegotiator(this);
attributes
.getNegotiatorById<Bind2Negotiator>(bind2Negotiator)
?.registerNegotiator(this);
}
}

View File

@ -1,3 +1,4 @@
import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
import 'package:moxxmpp/src/connection.dart';
import 'package:moxxmpp/src/events.dart';
@ -7,10 +8,12 @@ import 'package:moxxmpp/src/managers/data.dart';
import 'package:moxxmpp/src/managers/handlers.dart';
import 'package:moxxmpp/src/managers/namespaces.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/negotiators/namespaces.dart';
import 'package:moxxmpp/src/stanza.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/xeps/xep_0030/xep_0030.dart';
import 'package:moxxmpp/src/xeps/xep_0297.dart';
import 'package:moxxmpp/src/xeps/xep_0386.dart';
/// This manager class implements support for XEP-0280.
class CarbonsManager extends XmppManagerBase {
@ -173,6 +176,16 @@ class CarbonsManager extends XmppManagerBase {
_isEnabled = true;
}
@internal
void setEnabled() {
_isEnabled = true;
}
@internal
void setDisabled() {
_isEnabled = false;
}
/// Checks if a carbon sent by [senderJid] is valid to prevent vulnerabilities like
/// the ones listed at https://xmpp.org/extensions/xep-0280.html#security.
///
@ -185,3 +198,55 @@ class CarbonsManager extends XmppManagerBase {
);
}
}
class CarbonsNegotiator extends Bind2FeatureNegotiator {
CarbonsNegotiator() : super(0, carbonsXmlns, carbonsNegotiator);
/// Flag indicating whether we requested to enable carbons inline (true) or not
/// (false).
bool _requestedEnablement = false;
/// Logger
final Logger _log = Logger('CarbonsNegotiator');
@override
Future<void> onBind2Success(XMLNode response) async {
if (!_requestedEnablement) {
return;
}
final enabled = response.firstTag('enabled', xmlns: carbonsXmlns);
final cm = attributes.getManagerById<CarbonsManager>(carbonsManager)!;
if (enabled != null) {
_log.finest('Successfully enabled Message Carbons inline');
cm.setEnabled();
} else {
_log.warning('Failed to enable Message Carbons inline');
cm.setDisabled();
}
}
@override
Future<List<XMLNode>> onBind2FeaturesReceived(
List<String> bind2Features,
) async {
if (!bind2Features.contains(carbonsXmlns)) {
return [];
}
_requestedEnablement = true;
return [
XMLNode.xmlns(
tag: 'enable',
xmlns: carbonsXmlns,
),
];
}
@override
void reset() {
_requestedEnablement = false;
super.reset();
}
}

View File

@ -5,6 +5,7 @@ import 'package:moxxmpp/src/negotiators/namespaces.dart';
import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
import 'package:moxxmpp/src/xeps/xep_0386.dart';
class CSIActiveNonza extends XMLNode {
CSIActiveNonza()
@ -23,7 +24,8 @@ class CSIInactiveNonza extends XMLNode {
}
/// A Stub negotiator that is just for "intercepting" the stream feature.
class CSINegotiator extends XmppFeatureNegotiatorBase {
class CSINegotiator extends XmppFeatureNegotiatorBase
implements Bind2FeatureNegotiatorInterface {
CSINegotiator() : super(11, false, csiXmlns, csiNegotiator);
/// True if CSI is supported. False otherwise.
@ -40,19 +42,47 @@ class CSINegotiator extends XmppFeatureNegotiatorBase {
return const Result(NegotiatorState.done);
}
@override
Future<List<XMLNode>> onBind2FeaturesReceived(
List<String> bind2Features,
) async {
if (!bind2Features.contains(csiXmlns)) {
return [];
}
_supported = true;
final active = attributes.getManagerById<CSIManager>(csiManager)!.isActive;
return [
if (active) CSIActiveNonza() else CSIInactiveNonza(),
];
}
@override
Future<void> onBind2Success(XMLNode response) async {}
@override
void reset() {
_supported = false;
super.reset();
}
@override
Future<void> postRegisterCallback() async {
attributes
.getNegotiatorById<Bind2Negotiator>(bind2Negotiator)
?.registerNegotiator(this);
}
}
/// The manager requires a CSINegotiator to be registered as a feature negotiator.
class CSIManager extends XmppManagerBase {
CSIManager() : super(csiManager);
/// Flag indicating whether the application is currently active and the CSI
/// traffic optimisation should be disabled (true).
bool _isActive = true;
bool get isActive => _isActive;
@override
Future<bool> isSupported() async {
@ -71,23 +101,31 @@ class CSIManager extends XmppManagerBase {
}
}
/// Tells the server to top optimizing traffic
Future<void> setActive() async {
/// Tells the server to stop optimizing traffic.
/// If [sendNonza] is false, then no nonza is sent. This is useful
/// for setting up the CSI manager for Bind2.
Future<void> setActive({bool sendNonza = true}) async {
_isActive = true;
final attrs = getAttributes();
if (await isSupported()) {
attrs.sendNonza(CSIActiveNonza());
if (sendNonza) {
final attrs = getAttributes();
if (await isSupported()) {
attrs.sendNonza(CSIActiveNonza());
}
}
}
/// Tells the server to optimize traffic following XEP-0352
Future<void> setInactive() async {
/// If [sendNonza] is false, then no nonza is sent. This is useful
/// for setting up the CSI manager for Bind2.
Future<void> setInactive({bool sendNonza = true}) async {
_isActive = false;
final attrs = getAttributes();
if (await isSupported()) {
attrs.sendNonza(CSIInactiveNonza());
if (sendNonza) {
final attrs = getAttributes();
if (await isSupported()) {
attrs.sendNonza(CSIInactiveNonza());
}
}
}
}

View File

@ -0,0 +1,142 @@
import 'package:collection/collection.dart';
import 'package:meta/meta.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/negotiators/namespaces.dart';
import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
import 'package:moxxmpp/src/xeps/xep_0388/negotiators.dart';
import 'package:moxxmpp/src/xeps/xep_0388/xep_0388.dart';
/// An interface that allows registering against Bind2's feature list in order to
/// negotiate features inline with Bind2.
// ignore: one_member_abstracts
abstract class Bind2FeatureNegotiatorInterface {
/// Called by the Bind2 negotiator when Bind2 features are received. The returned
/// [XMLNode]s are added to Bind2's bind request.
Future<List<XMLNode>> onBind2FeaturesReceived(List<String> bind2Features);
/// Called by the Bind2 negotiator when Bind2 results are received.
Future<void> onBind2Success(XMLNode result);
}
/// A class that allows for simple negotiators that only registers itself against
/// the Bind2 negotiator. You only have to implement the functions required by
/// [Bind2FeatureNegotiatorInterface].
abstract class Bind2FeatureNegotiator extends XmppFeatureNegotiatorBase
implements Bind2FeatureNegotiatorInterface {
Bind2FeatureNegotiator(
int priority,
String negotiatingXmlns,
String id,
) : super(priority, false, negotiatingXmlns, id);
@override
bool matchesFeature(List<XMLNode> features) => false;
@override
Future<Result<NegotiatorState, NegotiatorError>> negotiate(
XMLNode nonza,
) async {
return const Result(NegotiatorState.done);
}
@mustCallSuper
@override
Future<void> postRegisterCallback() async {
attributes
.getNegotiatorById<Bind2Negotiator>(bind2Negotiator)!
.registerNegotiator(this);
}
}
/// A negotiator implementing XEP-0386. This negotiator is useless on its own
/// and requires a [Sasl2Negotiator] to be registered.
class Bind2Negotiator extends Sasl2FeatureNegotiator {
Bind2Negotiator() : super(0, false, bind2Xmlns, bind2Negotiator);
/// A list of negotiators that can work with Bind2.
final List<Bind2FeatureNegotiatorInterface> _negotiators =
List<Bind2FeatureNegotiatorInterface>.empty(growable: true);
/// A tag to sent to the server when requesting Bind2.
String? tag;
/// Register [negotiator] against the Bind2 negotiator to append data to the Bind2
/// negotiation.
void registerNegotiator(Bind2FeatureNegotiatorInterface negotiator) {
_negotiators.add(negotiator);
}
@override
Future<Result<NegotiatorState, NegotiatorError>> negotiate(
XMLNode nonza,
) async {
return const Result(NegotiatorState.done);
}
@override
Future<List<XMLNode>> onSasl2FeaturesReceived(XMLNode sasl2Features) async {
final children = List<XMLNode>.empty(growable: true);
if (_negotiators.isNotEmpty) {
final inline = sasl2Features
.firstTag('inline')!
.firstTag('bind', xmlns: bind2Xmlns)!
.firstTag('inline');
if (inline != null) {
final features = inline.children
.where((child) => child.tag == 'feature')
.map((child) => child.attributes['var']! as String)
.toList();
// Only call the negotiators if Bind2 allows doing stuff inline
for (final negotiator in _negotiators) {
children.addAll(await negotiator.onBind2FeaturesReceived(features));
}
}
}
return [
XMLNode.xmlns(
tag: 'bind',
xmlns: bind2Xmlns,
children: [
if (tag != null)
XMLNode(
tag: 'tag',
text: tag,
),
...children,
],
),
];
}
@override
bool canInlineFeature(List<XMLNode> features) {
return features.firstWhereOrNull(
(child) => child.tag == 'bind' && child.xmlns == bind2Xmlns,
) !=
null;
}
@override
Future<Result<bool, NegotiatorError>> onSasl2Success(XMLNode response) async {
final bound = response.firstTag('bound', xmlns: bind2Xmlns);
if (bound != null) {
for (final negotiator in _negotiators) {
await negotiator.onBind2Success(bound);
}
}
attributes.removeNegotiatingFeature(bindXmlns);
return const Result(true);
}
@override
Future<void> postRegisterCallback() async {
attributes
.getNegotiatorById<Sasl2Negotiator>(sasl2Negotiator)!
.registerNegotiator(this);
}
}

View File

@ -0,0 +1,8 @@
import 'package:moxxmpp/src/negotiators/negotiator.dart';
/// Triggered by the SASL2 negotiator when no SASL mechanism was chosen during
/// negotiation.
class NoSASLMechanismSelectedError extends NegotiatorError {
@override
bool isRecoverable() => false;
}

View File

@ -0,0 +1,72 @@
import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/negotiator.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
/// A special type of [XmppFeatureNegotiatorBase] that is aware of SASL2.
abstract class Sasl2FeatureNegotiator extends XmppFeatureNegotiatorBase {
Sasl2FeatureNegotiator(
super.priority,
super.sendStreamHeaderWhenDone,
super.negotiatingXmlns,
super.id,
);
/// Called by the SASL2 negotiator when we received the SASL2 stream features
/// [sasl2Features]. The return value is a list of XML elements that should be
/// added to the SASL2 <authenticate /> nonza.
/// This method is only called when the <inline /> element contains an item with
/// xmlns equal to [negotiatingXmlns].
Future<List<XMLNode>> onSasl2FeaturesReceived(XMLNode sasl2Features);
/// Called by the SASL2 negotiator when the SASL2 negotiations are done. [response]
/// is the entire response nonza.
/// This method is only called when the previous <inline /> element contains an
/// item with xmlns equal to [negotiatingXmlns].
Future<Result<bool, NegotiatorError>> onSasl2Success(XMLNode response);
/// Called by the SASL2 negotiator when the SASL2 negotiations have failed. [response]
/// is the entire response nonza.
Future<void> onSasl2Failure(XMLNode response) async {}
/// Called by the SASL2 negotiator to find out whether the negotiator is willing
/// to inline a feature. [features] is the list of elements inside the <inline />
/// element.
bool canInlineFeature(List<XMLNode> features);
}
/// A special type of [SaslNegotiator] that is aware of SASL2.
abstract class Sasl2AuthenticationNegotiator extends SaslNegotiator
implements Sasl2FeatureNegotiator {
Sasl2AuthenticationNegotiator(super.priority, super.id, super.mechanismName);
/// Flag indicating whether this negotiator was chosen during SASL2 as the SASL
/// negotiator to use.
bool _pickedForSasl2 = false;
bool get pickedForSasl2 => _pickedForSasl2;
/// Perform a SASL step with [input] as the already parsed input data. Returns
/// the base64-encoded response data.
Future<String> getRawStep(String input);
/// Tells the negotiator that it has been selected as the SASL negotiator for SASL2.
void pickForSasl2() {
_pickedForSasl2 = true;
}
/// When SASL2 fails, should we retry (true) or just fail (false).
/// Defaults to just returning false.
bool shouldRetrySasl() => false;
@override
void reset() {
_pickedForSasl2 = false;
super.reset();
}
@override
bool canInlineFeature(List<XMLNode> features) {
return true;
}
}

View File

@ -0,0 +1,46 @@
import 'package:moxxmpp/src/stringxml.dart';
/// A data class describing the user agent. See https://dyn.eightysoft.de/final/xep-0388.html#initiation
class UserAgent {
const UserAgent({
this.id,
this.software,
this.device,
});
/// The identifier of the software/device combo connecting. SHOULD be a UUIDv4.
final String? id;
/// The software's name that's connecting at the moment.
final String? software;
/// The name of the device.
final String? device;
XMLNode toXml() {
assert(
id != null || software != null || device != null,
'A completely empty user agent makes no sense',
);
return XMLNode(
tag: 'user-agent',
attributes: id != null
? {
'id': id,
}
: {},
children: [
if (software != null)
XMLNode(
tag: 'software',
text: software,
),
if (device != null)
XMLNode(
tag: 'device',
text: device,
),
],
);
}
}

View File

@ -0,0 +1,209 @@
import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/negotiators/namespaces.dart';
import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/errors.dart';
import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
import 'package:moxxmpp/src/xeps/xep_0388/errors.dart';
import 'package:moxxmpp/src/xeps/xep_0388/negotiators.dart';
import 'package:moxxmpp/src/xeps/xep_0388/user_agent.dart';
/// The state of the SASL2 negotiation
enum Sasl2State {
// No request has been sent yet.
idle,
// We have sent the <authenticate /> nonza.
authenticateSent,
}
/// A negotiator that implements XEP-0388 SASL2. Alone, it does nothing. Has to be
/// registered with other negotiators that register themselves against this one.
class Sasl2Negotiator extends XmppFeatureNegotiatorBase {
Sasl2Negotiator({
this.userAgent,
}) : super(100, false, sasl2Xmlns, sasl2Negotiator);
/// The user agent data that will be sent to the server when authenticating.
final UserAgent? userAgent;
/// List of callbacks that are registered against us. Will be called once we get
/// SASL2 features.
final List<Sasl2FeatureNegotiator> _featureNegotiators =
List<Sasl2FeatureNegotiator>.empty(growable: true);
/// List of SASL negotiators, sorted by their priority. The higher the priority, the
/// lower its index.
final List<Sasl2AuthenticationNegotiator> _saslNegotiators =
List<Sasl2AuthenticationNegotiator>.empty(growable: true);
/// The state the SASL2 negotiator is currently in.
Sasl2State _sasl2State = Sasl2State.idle;
/// The SASL negotiator that will negotiate authentication.
Sasl2AuthenticationNegotiator? _currentSaslNegotiator;
/// The SASL2 <authentication /> element we received with the stream features.
XMLNode? _sasl2Data;
final List<String> _activeSasl2Negotiators =
List<String>.empty(growable: true);
/// Register a SASL negotiator so that we can use that SASL implementation during
/// SASL2.
void registerSaslNegotiator(Sasl2AuthenticationNegotiator negotiator) {
_featureNegotiators.add(negotiator);
_saslNegotiators
..add(negotiator)
..sort((a, b) => b.priority.compareTo(a.priority));
}
/// Register a feature negotiator so that we can negotitate that feature inline with
/// the SASL authentication.
void registerNegotiator(Sasl2FeatureNegotiator negotiator) {
_featureNegotiators.add(negotiator);
}
@override
bool matchesFeature(List<XMLNode> features) {
// Only do SASL2 when the socket is secure
return attributes.getSocket().isSecure() && super.matchesFeature(features);
}
@override
Future<Result<NegotiatorState, NegotiatorError>> negotiate(
XMLNode nonza,
) async {
switch (_sasl2State) {
case Sasl2State.idle:
_sasl2Data = nonza.firstTag('authentication', xmlns: sasl2Xmlns);
final mechanisms = XMLNode.xmlns(
tag: 'mechanisms',
xmlns: saslXmlns,
children:
_sasl2Data!.children.where((c) => c.tag == 'mechanism').toList(),
);
for (final negotiator in _saslNegotiators) {
if (negotiator.matchesFeature([mechanisms])) {
_currentSaslNegotiator = negotiator;
_currentSaslNegotiator!.pickForSasl2();
break;
}
}
// We must have a SASL negotiator by now
if (_currentSaslNegotiator == null) {
return Result(NoSASLMechanismSelectedError());
}
// Collect additional data by interested negotiators
final inline = _sasl2Data!.firstTag('inline');
final children = List<XMLNode>.empty(growable: true);
if (inline != null && inline.children.isNotEmpty) {
for (final negotiator in _featureNegotiators) {
if (negotiator.canInlineFeature(inline.children)) {
_activeSasl2Negotiators.add(negotiator.id);
children.addAll(
await negotiator.onSasl2FeaturesReceived(_sasl2Data!),
);
}
}
}
// Build the authenticate nonza
final authenticate = XMLNode.xmlns(
tag: 'authenticate',
xmlns: sasl2Xmlns,
attributes: {
'mechanism': _currentSaslNegotiator!.mechanismName,
},
children: [
XMLNode(
tag: 'initial-response',
text: await _currentSaslNegotiator!.getRawStep(''),
),
if (userAgent != null) userAgent!.toXml(),
...children,
],
);
_sasl2State = Sasl2State.authenticateSent;
attributes.sendNonza(authenticate);
return const Result(NegotiatorState.ready);
case Sasl2State.authenticateSent:
if (nonza.tag == 'success') {
// Tell the dependent negotiators about the result
final negotiators = _featureNegotiators
.where(
(negotiator) => _activeSasl2Negotiators.contains(negotiator.id),
)
.toList()
..add(_currentSaslNegotiator!);
for (final negotiator in negotiators) {
final result = await negotiator.onSasl2Success(nonza);
if (!result.isType<bool>()) {
return Result(result.get<NegotiatorError>());
}
}
// We're done
attributes.setAuthenticated();
attributes.removeNegotiatingFeature(saslXmlns);
// Check if we also received a resource with the SASL2 success
final jid = JID.fromString(
nonza.firstTag('authorization-identifier')!.innerText(),
);
if (!jid.isBare()) {
attributes.setResource(jid.resource);
}
return const Result(NegotiatorState.done);
} else if (nonza.tag == 'challenge') {
// We still have to negotiate
final challenge = nonza.innerText();
final response = XMLNode.xmlns(
tag: 'response',
xmlns: sasl2Xmlns,
text: await _currentSaslNegotiator!.getRawStep(challenge),
);
attributes.sendNonza(response);
} else if (nonza.tag == 'failure') {
final negotiators = _featureNegotiators
.where(
(negotiator) => _activeSasl2Negotiators.contains(negotiator.id),
)
.toList()
..add(_currentSaslNegotiator!);
for (final negotiator in negotiators) {
await negotiator.onSasl2Failure(nonza);
}
// Check if we should retry and, if we should, reset the current
// negotiator, this negotiator, and retry.
if (_currentSaslNegotiator!.shouldRetrySasl()) {
_currentSaslNegotiator!.reset();
reset();
return const Result(
NegotiatorState.retryLater,
);
}
return Result(
SaslError.fromFailure(nonza),
);
}
}
return const Result(NegotiatorState.ready);
}
@override
void reset() {
_currentSaslNegotiator = null;
_sasl2State = Sasl2State.idle;
_sasl2Data = null;
_activeSasl2Negotiators.clear();
super.reset();
}
}

View File

@ -56,7 +56,6 @@ class TestingManagerHolder {
sendNonza: (_) {},
sendEvent: (_) {},
getSocket: () => _socket,
isFeatureSupported: (_) => false,
getNegotiatorById: getNegotiatorNullStub,
getFullJID: () => jid,
getManagerById: _getManagerById,

View File

@ -58,7 +58,7 @@ List<ExpectationBase> buildAuthenticatedPlay(ConnectionSettings settings) {
);
return [
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='${settings.jid.domain}' xml:lang='en'>",
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='${settings.jid.domain}' from='${settings.jid.toBare().toString()}' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
@ -77,7 +77,7 @@ List<ExpectationBase> buildAuthenticatedPlay(ConnectionSettings settings) {
'<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl" />',
),
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='${settings.jid.domain}' xml:lang='en'>",
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='${settings.jid.domain}' from='${settings.jid.toBare().toString()}' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"

View File

@ -46,7 +46,7 @@ void main() {
final stubSocket = StubTCPSocket(
[
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' xml:lang='en'>",
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='user@test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"

View File

@ -1,4 +1,4 @@
import 'package:moxxmpp/src/negotiators/sasl/kv.dart';
import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/kv.dart';
import 'package:test/test.dart';
void main() {

View File

@ -46,6 +46,11 @@ void main() {
)..register(
NegotiatorAttributes(
(XMLNode _, {String? redact}) {},
() => XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
fakeSocket,
),
() => ConnectionSettings(
jid: JID.fromString('user@server'),
password: 'pencil',
@ -57,6 +62,9 @@ void main() {
() => JID.fromString('user@server'),
() => fakeSocket,
() => false,
() {},
(_, {bool triggerEvent = true}) {},
(_) {},
),
);
@ -139,6 +147,11 @@ void main() {
)..register(
NegotiatorAttributes(
(XMLNode n, {String? redact}) => lastMessage = n.innerText(),
() => XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
StubTCPSocket([]),
),
() => ConnectionSettings(
jid: JID.fromString('user@server'),
password: 'pencil',
@ -150,6 +163,9 @@ void main() {
() => JID.fromString('user@server'),
() => fakeSocket,
() => false,
() {},
(_, {bool triggerEvent = true}) {},
(_) {},
),
);
@ -187,6 +203,11 @@ void main() {
)..register(
NegotiatorAttributes(
(XMLNode _, {String? redact}) {},
() => XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
StubTCPSocket([]),
),
() => ConnectionSettings(
jid: JID.fromString('user@server'),
password: 'pencil',
@ -198,6 +219,9 @@ void main() {
() => JID.fromString('user@server'),
() => fakeSocket,
() => false,
() {},
(_, {bool triggerEvent = true}) {},
(_) {},
),
);
@ -225,6 +249,11 @@ void main() {
)..register(
NegotiatorAttributes(
(XMLNode _, {String? redact}) {},
() => XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
StubTCPSocket([]),
),
() => ConnectionSettings(
jid: JID.fromString('user@server'),
password: 'pencil',
@ -236,6 +265,9 @@ void main() {
() => JID.fromString('user@server'),
() => fakeSocket,
() => false,
() {},
(_, {bool triggerEvent = true}) {},
(_) {},
),
);
@ -266,6 +298,11 @@ void main() {
)..register(
NegotiatorAttributes(
(XMLNode _, {String? redact}) {},
() => XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
StubTCPSocket([]),
),
() => ConnectionSettings(
jid: JID.fromString('user@server'),
password: 'pencil',
@ -277,6 +314,9 @@ void main() {
() => JID.fromString('user@server'),
() => fakeSocket,
() => false,
() {},
(_, {bool triggerEvent = true}) {},
(_) {},
),
);

View File

@ -23,8 +23,8 @@ void main() {
);
expect(
StreamHeaderNonza('uwu.server').toXml(),
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='uwu.server' xml:lang='en'>",
StreamHeaderNonza(JID.fromString('user@uwu.server')).toXml(),
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='uwu.server' from='user@uwu.server' xml:lang='en'>",
);
expect(

View File

@ -12,7 +12,7 @@ void main() {
final fakeSocket = StubTCPSocket(
[
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' xml:lang='en'>",
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
@ -31,7 +31,7 @@ void main() {
'<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl" />',
),
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' xml:lang='en'>",
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
@ -84,7 +84,7 @@ void main() {
DiscoManager([]),
EntityCapabilitiesManager('http://moxxmpp.example'),
]);
conn.registerFeatureNegotiators([
await conn.registerFeatureNegotiators([
SaslPlainNegotiator(),
SaslScramNegotiator(10, '', '', ScramHashType.sha512),
ResourceBindingNegotiator(),

View File

@ -169,12 +169,11 @@ void main() {
MessageManager(),
RosterManager(TestingRosterStateManager(null, [])),
]);
connection
..registerFeatureNegotiators([
SaslPlainNegotiator(),
ResourceBindingNegotiator(),
])
..setConnectionSettings(TestingManagerHolder.settings);
await connection.registerFeatureNegotiators([
SaslPlainNegotiator(),
ResourceBindingNegotiator(),
]);
connection.setConnectionSettings(TestingManagerHolder.settings);
await connection.connect(
waitUntilLogin: true,
);

View File

@ -63,7 +63,6 @@ XmppManagerAttributes mkAttributes(void Function(Stanza) callback) {
password: 'password',
useDirectTLS: true,
),
isFeatureSupported: (_) => false,
getFullJID: () => JID.fromString('hallo@example.server/uwu'),
getSocket: () => StubTCPSocket([]),
getConnection: () => XmppConnection(
@ -228,7 +227,7 @@ void main() {
() async {
final fakeSocket = StubTCPSocket([
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' xml:lang='en'>",
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
@ -247,7 +246,7 @@ void main() {
'<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl" />',
),
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' xml:lang='en'>",
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
@ -298,7 +297,7 @@ void main() {
CarbonsManager()..forceEnable(),
EntityCapabilitiesManager('http://moxxmpp.example'),
]);
conn.registerFeatureNegotiators([
await conn.registerFeatureNegotiators([
SaslPlainNegotiator(),
ResourceBindingNegotiator(),
StreamManagementNegotiator(),
@ -343,7 +342,7 @@ void main() {
test('Test counting incoming stanzas that are awaited', () async {
final fakeSocket = StubTCPSocket([
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' xml:lang='en'>",
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
@ -362,7 +361,7 @@ void main() {
'<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl" />',
),
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' xml:lang='en'>",
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
@ -391,10 +390,10 @@ void main() {
"<enable xmlns='urn:xmpp:sm:3' resume='true' />",
'<enabled xmlns="urn:xmpp:sm:3" id="some-long-sm-id" resume="true" />',
),
// StringExpectation(
// "<presence xmlns='jabber:client' from='polynomdivision@test.server/MU29eEZn'><show>chat</show></presence>",
// '<iq type="result" />',
// ),
StringExpectation(
"<presence xmlns='jabber:client' from='polynomdivision@test.server/MU29eEZn'><show>chat</show></presence>",
'<iq type="result" />',
),
StanzaExpectation(
"<iq to='user@example.com' type='get' id='a' xmlns='jabber:client' />",
"<iq from='user@example.com' type='result' id='a' />",
@ -423,7 +422,7 @@ void main() {
CarbonsManager()..forceEnable(),
//EntityCapabilitiesManager('http://moxxmpp.example'),
]);
conn.registerFeatureNegotiators([
await conn.registerFeatureNegotiators([
SaslPlainNegotiator(),
ResourceBindingNegotiator(),
StreamManagementNegotiator(),
@ -432,7 +431,7 @@ void main() {
await conn.connect(
waitUntilLogin: true,
);
expect(fakeSocket.getState(), 5);
expect(fakeSocket.getState(), 6);
expect(await conn.getConnectionState(), XmppConnectionState.connected);
expect(
conn
@ -450,7 +449,7 @@ void main() {
addFrom: StanzaFromType.none,
);
expect(sm.state.s2c, /*2*/ 1);
expect(sm.state.s2c, 2);
});
});
@ -513,7 +512,7 @@ void main() {
test('Test successful stream enablement', () async {
final fakeSocket = StubTCPSocket([
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' xml:lang='en'>",
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
@ -532,7 +531,7 @@ void main() {
'<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl" />',
),
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' xml:lang='en'>",
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
@ -580,7 +579,7 @@ void main() {
DiscoManager([]),
StreamManagementManager(),
]);
conn.registerFeatureNegotiators([
await conn.registerFeatureNegotiators([
SaslPlainNegotiator(),
ResourceBindingNegotiator(),
StreamManagementNegotiator(),
@ -590,7 +589,7 @@ void main() {
waitUntilLogin: true,
);
expect(fakeSocket.getState(), 5);
expect(fakeSocket.getState(), 6);
expect(await conn.getConnectionState(), XmppConnectionState.connected);
expect(
conn
@ -603,7 +602,7 @@ void main() {
test('Test a failed stream resumption', () async {
final fakeSocket = StubTCPSocket([
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' xml:lang='en'>",
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
@ -622,7 +621,7 @@ void main() {
'<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl" />',
),
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' xml:lang='en'>",
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
@ -674,7 +673,7 @@ void main() {
DiscoManager([]),
StreamManagementManager(),
]);
conn.registerFeatureNegotiators([
await conn.registerFeatureNegotiators([
SaslPlainNegotiator(),
ResourceBindingNegotiator(),
StreamManagementNegotiator(),
@ -690,7 +689,7 @@ void main() {
await conn.connect(
waitUntilLogin: true,
);
expect(fakeSocket.getState(), 6);
expect(fakeSocket.getState(), 7);
expect(await conn.getConnectionState(), XmppConnectionState.connected);
expect(
conn
@ -703,7 +702,7 @@ void main() {
test('Test a successful stream resumption', () async {
final fakeSocket = StubTCPSocket([
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' xml:lang='en'>",
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
@ -722,7 +721,7 @@ void main() {
'<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl" />',
),
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' xml:lang='en'>",
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
@ -765,7 +764,7 @@ void main() {
DiscoManager([]),
StreamManagementManager(),
]);
conn.registerFeatureNegotiators([
await conn.registerFeatureNegotiators([
SaslPlainNegotiator(),
ResourceBindingNegotiator(),
StreamManagementNegotiator(),
@ -789,4 +788,298 @@ void main() {
expect(sm.streamResumed, true);
});
});
test('Test SASL2 inline stream resumption', () async {
final fakeSocket = StubTCPSocket([
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
<mechanism>PLAIN</mechanism>
</mechanisms>
<authentication xmlns='urn:xmpp:sasl:2'>
<mechanism>PLAIN</mechanism>
<inline>
<resume xmlns="urn:xmpp:sm:3" />
</inline>
</authentication>
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
<required/>
</bind>
</stream:features>''',
),
StanzaExpectation(
"<authenticate xmlns='urn:xmpp:sasl:2' mechanism='PLAIN'><user-agent id='d4565fa7-4d72-4749-b3d3-740edbf87770'><software>moxxmpp</software><device>PapaTutuWawa's awesome device</device></user-agent><initial-response>AHBvbHlub21kaXZpc2lvbgBhYWFh</initial-response><resume xmlns='urn:xmpp:sm:3' previd='test-prev-id' h='2' /></authenticate>",
'''
<success xmlns='urn:xmpp:sasl:2'>
<authorization-identifier>polynomdivision@test.server</authorization-identifier>
<resumed xmlns='urn:xmpp:sm:3' h='25' previd='test-prev-id' />
</success>
''',
),
]);
final sm = StreamManagementManager();
await sm.setState(
sm.state.copyWith(
c2s: 25,
s2c: 2,
streamResumptionId: 'test-prev-id',
),
);
final conn = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
fakeSocket,
)
..setConnectionSettings(
ConnectionSettings(
jid: JID.fromString('polynomdivision@test.server'),
password: 'aaaa',
useDirectTLS: true,
),
)
..setResource('test-resource', triggerEvent: false);
await conn.registerManagers([
RosterManager(TestingRosterStateManager('', [])),
DiscoManager([]),
sm,
]);
await conn.registerFeatureNegotiators([
SaslPlainNegotiator(),
ResourceBindingNegotiator(),
StreamManagementNegotiator()..setResource('test-resource'),
Sasl2Negotiator(
userAgent: const UserAgent(
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
software: 'moxxmpp',
device: "PapaTutuWawa's awesome device",
),
),
]);
final result = await conn.connect(
waitUntilLogin: true,
shouldReconnect: false,
enableReconnectOnSuccess: false,
);
expect(result.isType<NegotiatorError>(), false);
expect(
sm.state.c2s,
25,
);
expect(
sm.state.s2c,
2,
);
expect(conn.resource, 'test-resource');
});
test('Test SASL2 inline stream resumption with Bind2', () async {
final fakeSocket = StubTCPSocket([
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
<mechanism>PLAIN</mechanism>
</mechanisms>
<authentication xmlns='urn:xmpp:sasl:2'>
<mechanism>PLAIN</mechanism>
<inline>
<resume xmlns="urn:xmpp:sm:3" />
<bind xmlns="urn:xmpp:bind:0">
<inline>
<feature var="urn:xmpp:sm:3" />
</inline>
</bind>
</inline>
</authentication>
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
<required/>
</bind>
</stream:features>''',
),
StanzaExpectation(
"<authenticate xmlns='urn:xmpp:sasl:2' mechanism='PLAIN'><user-agent id='d4565fa7-4d72-4749-b3d3-740edbf87770'><software>moxxmpp</software><device>PapaTutuWawa's awesome device</device></user-agent><initial-response>AHBvbHlub21kaXZpc2lvbgBhYWFh</initial-response><bind xmlns='urn:xmpp:bind:0'><enable xmlns='urn:xmpp:sm:3' resume='true' /></bind><resume xmlns='urn:xmpp:sm:3' previd='test-prev-id' h='2' /></authenticate>",
'''
<success xmlns='urn:xmpp:sasl:2'>
<authorization-identifier>polynomdivision@test.server</authorization-identifier>
<resumed xmlns='urn:xmpp:sm:3' h='25' previd='test-prev-id' />
</success>
''',
),
]);
final sm = StreamManagementManager();
await sm.setState(
sm.state.copyWith(
c2s: 25,
s2c: 2,
streamResumptionId: 'test-prev-id',
),
);
final conn = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
fakeSocket,
)
..setConnectionSettings(
ConnectionSettings(
jid: JID.fromString('polynomdivision@test.server'),
password: 'aaaa',
useDirectTLS: true,
),
)
..setResource('test-resource', triggerEvent: false);
await conn.registerManagers([
RosterManager(TestingRosterStateManager('', [])),
DiscoManager([]),
sm,
]);
await conn.registerFeatureNegotiators([
SaslPlainNegotiator(),
ResourceBindingNegotiator(),
StreamManagementNegotiator()..setResource('test-resource'),
Bind2Negotiator(),
Sasl2Negotiator(
userAgent: const UserAgent(
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
software: 'moxxmpp',
device: "PapaTutuWawa's awesome device",
),
),
]);
final result = await conn.connect(
waitUntilLogin: true,
shouldReconnect: false,
enableReconnectOnSuccess: false,
);
expect(result.isType<NegotiatorError>(), false);
expect(
sm.state.c2s,
25,
);
expect(
sm.state.s2c,
2,
);
expect(conn.resource, 'test-resource');
});
test('Test failed SASL2 inline stream resumption with Bind2', () async {
final fakeSocket = StubTCPSocket([
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
<mechanism>PLAIN</mechanism>
</mechanisms>
<authentication xmlns='urn:xmpp:sasl:2'>
<mechanism>PLAIN</mechanism>
<inline>
<resume xmlns="urn:xmpp:sm:3" />
<bind xmlns="urn:xmpp:bind:0">
<inline>
<feature var="urn:xmpp:sm:3" />
</inline>
</bind>
</inline>
</authentication>
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
<required/>
</bind>
</stream:features>''',
),
StanzaExpectation(
"<authenticate xmlns='urn:xmpp:sasl:2' mechanism='PLAIN'><user-agent id='d4565fa7-4d72-4749-b3d3-740edbf87770'><software>moxxmpp</software><device>PapaTutuWawa's awesome device</device></user-agent><initial-response>AHBvbHlub21kaXZpc2lvbgBhYWFh</initial-response><bind xmlns='urn:xmpp:bind:0'><enable xmlns='urn:xmpp:sm:3' resume='true' /></bind><resume xmlns='urn:xmpp:sm:3' previd='test-prev-id' h='2' /></authenticate>",
'''
<success xmlns='urn:xmpp:sasl:2'>
<authorization-identifier>polynomdivision@test.server/test-resource</authorization-identifier>
<failed xmlns='urn:xmpp:sm:3' />
<bound xmlns='urn:xmpp:bind:0'>
<failed xmlns='urn:xmpp:sm:3' />
</bound>
</success>
''',
),
]);
final sm = StreamManagementManager();
await sm.setState(
sm.state.copyWith(
c2s: 25,
s2c: 2,
streamResumptionId: 'test-prev-id',
),
);
final conn = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
fakeSocket,
)
..setConnectionSettings(
ConnectionSettings(
jid: JID.fromString('polynomdivision@test.server'),
password: 'aaaa',
useDirectTLS: true,
),
)
..setResource('test-resource', triggerEvent: false);
await conn.registerManagers([
RosterManager(TestingRosterStateManager('', [])),
DiscoManager([]),
sm,
]);
final smn = StreamManagementNegotiator();
await conn.registerFeatureNegotiators([
SaslPlainNegotiator(),
ResourceBindingNegotiator(),
smn,
Bind2Negotiator(),
Sasl2Negotiator(
userAgent: const UserAgent(
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
software: 'moxxmpp',
device: "PapaTutuWawa's awesome device",
),
),
]);
final result = await conn.connect(
waitUntilLogin: true,
shouldReconnect: false,
enableReconnectOnSuccess: false,
);
expect(result.isType<NegotiatorError>(), false);
expect(smn.isResumed, false);
expect(smn.resumeFailed, true);
expect(smn.streamEnablementFailed, true);
expect(conn.resource, 'test-resource');
});
}

View File

@ -1,8 +1,11 @@
import 'package:moxxmpp/moxxmpp.dart';
import 'package:test/test.dart';
import '../helpers/logging.dart';
import '../helpers/xmpp.dart';
void main() {
initLogger();
test("Test if we're vulnerable against CVE-2020-26547 style vulnerabilities",
() async {
final attributes = XmppManagerAttributes(
@ -27,7 +30,6 @@ void main() {
password: 'password',
useDirectTLS: true,
),
isFeatureSupported: (_) => false,
getFullJID: () => JID.fromString('bob@xmpp.example/uwu'),
getSocket: () => StubTCPSocket([]),
getConnection: () => XmppConnection(
@ -53,4 +55,94 @@ void main() {
false,
);
});
test('Test enabling message carbons inline with Bind2', () async {
final fakeSocket = StubTCPSocket([
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
<mechanism>PLAIN</mechanism>
</mechanisms>
<authentication xmlns='urn:xmpp:sasl:2'>
<mechanism>PLAIN</mechanism>
<inline>
<resume xmlns="urn:xmpp:sm:3" />
<bind xmlns="urn:xmpp:bind:0">
<inline>
<feature var="urn:xmpp:sm:3" />
<feature var="urn:xmpp:carbons:2" />
</inline>
</bind>
</inline>
</authentication>
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
<required/>
</bind>
</stream:features>''',
),
StanzaExpectation(
"<authenticate xmlns='urn:xmpp:sasl:2' mechanism='PLAIN'><user-agent id='d4565fa7-4d72-4749-b3d3-740edbf87770'><software>moxxmpp</software><device>PapaTutuWawa's awesome device</device></user-agent><initial-response>AHBvbHlub21kaXZpc2lvbgBhYWFh</initial-response><bind xmlns='urn:xmpp:bind:0'><enable xmlns='urn:xmpp:carbons:2' /></bind></authenticate>",
'''
<success xmlns='urn:xmpp:sasl:2'>
<authorization-identifier>polynomdivision@test.server/test-resource</authorization-identifier>
<bound xmlns='urn:xmpp:bind:0'>
<enabled xmlns='urn:xmpp:carbons:2' />
</bound>
</success>
''',
),
]);
final conn = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
fakeSocket,
)
..setConnectionSettings(
ConnectionSettings(
jid: JID.fromString('polynomdivision@test.server'),
password: 'aaaa',
useDirectTLS: true,
),
)
..setResource('test-resource', triggerEvent: false);
await conn.registerManagers([
RosterManager(TestingRosterStateManager('', [])),
DiscoManager([]),
CarbonsManager(),
]);
await conn.registerFeatureNegotiators([
SaslPlainNegotiator(),
ResourceBindingNegotiator(),
CarbonsNegotiator(),
Bind2Negotiator(),
Sasl2Negotiator(
userAgent: const UserAgent(
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
software: 'moxxmpp',
device: "PapaTutuWawa's awesome device",
),
),
]);
final result = await conn.connect(
waitUntilLogin: true,
shouldReconnect: false,
enableReconnectOnSuccess: false,
);
expect(result.isType<NegotiatorError>(), false);
expect(conn.resource, 'test-resource');
expect(
conn.getManagerById<CarbonsManager>(carbonsManager)!.isEnabled,
true,
);
});
}

View File

@ -1,5 +1,6 @@
import 'package:moxxmpp/moxxmpp.dart';
import 'package:test/test.dart';
import '../helpers/logging.dart';
import '../helpers/xmpp.dart';
class MockedCSINegotiator extends CSINegotiator {
@ -28,6 +29,8 @@ T? getUnsupportedCSINegotiator<T extends XmppFeatureNegotiatorBase>(String id) {
}
void main() {
initLogger();
group('Test the XEP-0352 implementation', () {
test('Test setting the CSI state when CSI is unsupported', () {
var nonzaSent = false;
@ -55,7 +58,6 @@ void main() {
),
getManagerById: getManagerNullStub,
getNegotiatorById: getUnsupportedCSINegotiator,
isFeatureSupported: (_) => false,
getFullJID: () => JID.fromString('some.user@example.server/aaaaa'),
getSocket: () => StubTCPSocket([]),
getConnection: () => XmppConnection(
@ -99,7 +101,6 @@ void main() {
),
getManagerById: getManagerNullStub,
getNegotiatorById: getSupportedCSINegotiator,
isFeatureSupported: (_) => false,
getFullJID: () => JID.fromString('some.user@example.server/aaaaa'),
getSocket: () => StubTCPSocket([]),
getConnection: () => XmppConnection(
@ -113,4 +114,99 @@ void main() {
..setInactive();
});
});
test('Test CSI with Bind2', () async {
final fakeSocket = StubTCPSocket([
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
<mechanism>PLAIN</mechanism>
</mechanisms>
<authentication xmlns='urn:xmpp:sasl:2'>
<mechanism>PLAIN</mechanism>
<inline>
<bind xmlns="urn:xmpp:bind:0">
<inline>
<feature var="urn:xmpp:csi:0" />
</inline>
</bind>
</inline>
</authentication>
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
<required/>
</bind>
</stream:features>''',
),
StanzaExpectation(
'''
<authenticate xmlns='urn:xmpp:sasl:2' mechanism='PLAIN'>
<user-agent id='d4565fa7-4d72-4749-b3d3-740edbf87770'>
<software>moxxmpp</software>
<device>PapaTutuWawa's awesome device</device>
</user-agent>
<initial-response>AHBvbHlub21kaXZpc2lvbgBhYWFh</initial-response>
<bind xmlns='urn:xmpp:bind:0'>
<inactive xmlns='urn:xmpp:csi:0' />
</bind>
</authenticate>''',
'''
<success xmlns='urn:xmpp:sasl:2'>
<authorization-identifier>polynomdivision@test.server/test-resource</authorization-identifier>
</success>
''',
),
]);
final conn = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
fakeSocket,
)..setConnectionSettings(
ConnectionSettings(
jid: JID.fromString('polynomdivision@test.server'),
password: 'aaaa',
useDirectTLS: true,
),
);
final csi = CSIManager();
await csi.setInactive(sendNonza: false);
await conn.registerManagers([
RosterManager(TestingRosterStateManager('', [])),
DiscoManager([]),
csi,
]);
await conn.registerFeatureNegotiators([
SaslPlainNegotiator(),
ResourceBindingNegotiator(),
FASTSaslNegotiator(),
Bind2Negotiator(),
CSINegotiator(),
Sasl2Negotiator(
userAgent: const UserAgent(
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
software: 'moxxmpp',
device: "PapaTutuWawa's awesome device",
),
),
]);
final result = await conn.connect(
waitUntilLogin: true,
shouldReconnect: false,
enableReconnectOnSuccess: false,
);
expect(result.isType<NegotiatorError>(), false);
expect(fakeSocket.getState(), 2);
expect(
conn.getNegotiatorById<CSINegotiator>(csiNegotiator)!.isSupported,
true,
);
});
}

View File

@ -0,0 +1,153 @@
import 'package:moxxmpp/moxxmpp.dart';
import 'package:test/test.dart';
import '../helpers/logging.dart';
import '../helpers/xmpp.dart';
void main() {
initLogger();
test('Test simple Bind2 negotiation', () async {
final fakeSocket = StubTCPSocket([
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
<mechanism>PLAIN</mechanism>
</mechanisms>
<authentication xmlns='urn:xmpp:sasl:2'>
<mechanism>PLAIN</mechanism>
<inline>
<bind xmlns="urn:xmpp:bind:0" />
</inline>
</authentication>
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
<required/>
</bind>
</stream:features>''',
),
StanzaExpectation(
"<authenticate xmlns='urn:xmpp:sasl:2' mechanism='PLAIN'><user-agent id='d4565fa7-4d72-4749-b3d3-740edbf87770'><software>moxxmpp</software><device>PapaTutuWawa's awesome device</device></user-agent><initial-response>AHBvbHlub21kaXZpc2lvbgBhYWFh</initial-response><bind xmlns='urn:xmpp:bind:0' /></authenticate>",
'''
<success xmlns='urn:xmpp:sasl:2'>
<authorization-identifier>polynomdivision@test.server/random.resource</authorization-identifier>
</success>
''',
),
]);
final conn = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
fakeSocket,
)..setConnectionSettings(
ConnectionSettings(
jid: JID.fromString('polynomdivision@test.server'),
password: 'aaaa',
useDirectTLS: true,
),
);
await conn.registerManagers([
PresenceManager(),
RosterManager(TestingRosterStateManager('', [])),
DiscoManager([]),
]);
await conn.registerFeatureNegotiators([
SaslPlainNegotiator(),
ResourceBindingNegotiator(),
Bind2Negotiator(),
Sasl2Negotiator(
userAgent: const UserAgent(
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
software: 'moxxmpp',
device: "PapaTutuWawa's awesome device",
),
),
]);
final result = await conn.connect(
waitUntilLogin: true,
shouldReconnect: false,
enableReconnectOnSuccess: false,
);
expect(result.isType<NegotiatorError>(), false);
expect(conn.resource, 'random.resource');
});
test('Test simple Bind2 negotiation with a provided tag', () async {
final fakeSocket = StubTCPSocket([
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
<mechanism>PLAIN</mechanism>
</mechanisms>
<authentication xmlns='urn:xmpp:sasl:2'>
<mechanism>PLAIN</mechanism>
<inline>
<bind xmlns="urn:xmpp:bind:0" />
</inline>
</authentication>
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
<required/>
</bind>
</stream:features>''',
),
StanzaExpectation(
"<authenticate xmlns='urn:xmpp:sasl:2' mechanism='PLAIN'><user-agent id='d4565fa7-4d72-4749-b3d3-740edbf87770'><software>moxxmpp</software><device>PapaTutuWawa's awesome device</device></user-agent><initial-response>AHBvbHlub21kaXZpc2lvbgBhYWFh</initial-response><bind xmlns='urn:xmpp:bind:0'><tag>moxxmpp</tag></bind></authenticate>",
'''
<success xmlns='urn:xmpp:sasl:2'>
<authorization-identifier>polynomdivision@test.server/moxxmpp.random.resource</authorization-identifier>
</success>
''',
),
]);
final conn = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
fakeSocket,
)..setConnectionSettings(
ConnectionSettings(
jid: JID.fromString('polynomdivision@test.server'),
password: 'aaaa',
useDirectTLS: true,
),
);
await conn.registerManagers([
RosterManager(TestingRosterStateManager('', [])),
DiscoManager([]),
]);
await conn.registerFeatureNegotiators([
SaslPlainNegotiator(),
ResourceBindingNegotiator(),
Bind2Negotiator()..tag = 'moxxmpp',
Sasl2Negotiator(
userAgent: const UserAgent(
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
software: 'moxxmpp',
device: "PapaTutuWawa's awesome device",
),
),
]);
final result = await conn.connect(
waitUntilLogin: true,
shouldReconnect: false,
enableReconnectOnSuccess: false,
);
expect(result.isType<NegotiatorError>(), false);
expect(conn.resource, 'moxxmpp.random.resource');
});
}

View File

@ -0,0 +1,473 @@
import 'package:collection/collection.dart';
import 'package:moxxmpp/moxxmpp.dart';
import 'package:test/test.dart';
import '../helpers/logging.dart';
import '../helpers/xmpp.dart';
class ExampleNegotiator extends Sasl2FeatureNegotiator {
ExampleNegotiator()
: super(0, false, 'invalid:example:dont:use', 'testNegotiator');
String? value;
@override
Future<Result<NegotiatorState, NegotiatorError>> negotiate(
XMLNode nonza,
) async {
return const Result(NegotiatorState.done);
}
@override
bool canInlineFeature(List<XMLNode> features) {
return features.firstWhereOrNull(
(child) => child.xmlns == 'invalid:example:dont:use',
) !=
null;
}
@override
Future<void> postRegisterCallback() async {
attributes
.getNegotiatorById<Sasl2Negotiator>(sasl2Negotiator)!
.registerNegotiator(this);
}
@override
Future<List<XMLNode>> onSasl2FeaturesReceived(XMLNode nonza) async {
return [
XMLNode.xmlns(
tag: 'test-data-request',
xmlns: 'invalid:example:dont:use',
),
];
}
@override
Future<Result<bool, NegotiatorError>> onSasl2Success(XMLNode nonza) async {
final child =
nonza.firstTag('test-data', xmlns: 'invalid:example:dont:use');
if (child == null) {
return const Result(true);
}
value = child.innerText();
return const Result(true);
}
}
void main() {
initLogger();
test('Test simple SASL2 negotiation', () async {
final fakeSocket = StubTCPSocket([
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
<mechanism>PLAIN</mechanism>
</mechanisms>
<authentication xmlns='urn:xmpp:sasl:2'>
<mechanism>PLAIN</mechanism>
</authentication>
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
<required/>
</bind>
</stream:features>''',
),
StanzaExpectation(
"<authenticate xmlns='urn:xmpp:sasl:2' mechanism='PLAIN'><user-agent id='d4565fa7-4d72-4749-b3d3-740edbf87770'><software>moxxmpp</software><device>PapaTutuWawa's awesome device</device></user-agent><initial-response>AHBvbHlub21kaXZpc2lvbgBhYWFh</initial-response></authenticate>",
'''
<success xmlns='urn:xmpp:sasl:2'>
<authorization-identifier>polynomdivision@test.server</authorization-identifier>
</success>
''',
),
StanzaExpectation(
"<iq xmlns='jabber:client' type='set' id='aaaa'><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind' /></iq>",
'''
'<iq xmlns="jabber:client" type="result" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><jid>polynomdivision@test.server/MU29eEZn</jid></bind></iq>',
''',
adjustId: true,
ignoreId: true,
),
]);
final conn = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
fakeSocket,
)..setConnectionSettings(
ConnectionSettings(
jid: JID.fromString('polynomdivision@test.server'),
password: 'aaaa',
useDirectTLS: true,
),
);
await conn.registerManagers([
PresenceManager(),
RosterManager(TestingRosterStateManager('', [])),
DiscoManager([]),
]);
await conn.registerFeatureNegotiators([
SaslPlainNegotiator(),
ResourceBindingNegotiator(),
Sasl2Negotiator(
userAgent: const UserAgent(
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
software: 'moxxmpp',
device: "PapaTutuWawa's awesome device",
),
),
]);
final result = await conn.connect(
waitUntilLogin: true,
shouldReconnect: false,
enableReconnectOnSuccess: false,
);
expect(result.isType<NegotiatorError>(), false);
});
test('Test SCRAM-SHA-1 SASL2 negotiation with a valid signature', () async {
final fakeSocket = StubTCPSocket([
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='server' from='user@server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
<mechanism>PLAIN</mechanism>
<mechanism>SCRAM-SHA-256</mechanism>
</mechanisms>
<authentication xmlns='urn:xmpp:sasl:2'>
<mechanism>PLAIN</mechanism>
<mechanism>SCRAM-SHA-256</mechanism>
</authentication>
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
<required/>
</bind>
</stream:features>''',
),
StanzaExpectation(
"<authenticate xmlns='urn:xmpp:sasl:2' mechanism='SCRAM-SHA-256'><user-agent id='d4565fa7-4d72-4749-b3d3-740edbf87770'><software>moxxmpp</software><device>PapaTutuWawa's awesome device</device></user-agent><initial-response>biwsbj11c2VyLHI9ck9wck5HZndFYmVSV2diTkVrcU8=</initial-response></authenticate>",
'''
<challenge xmlns='urn:xmpp:sasl:2'>cj1yT3ByTkdmd0ViZVJXZ2JORWtxTyVodllEcFdVYTJSYVRDQWZ1eEZJbGopaE5sRiRrMCxzPVcyMlphSjBTTlk3c29Fc1VFamI2Z1E9PSxpPTQwOTY=</challenge>
''',
),
StanzaExpectation(
'<response xmlns="urn:xmpp:sasl:2">Yz1iaXdzLHI9ck9wck5HZndFYmVSV2diTkVrcU8laHZZRHBXVWEyUmFUQ0FmdXhGSWxqKWhObEYkazAscD1kSHpiWmFwV0lrNGpVaE4rVXRlOXl0YWc5empmTUhnc3FtbWl6N0FuZFZRPQ==</response>',
'<success xmlns="urn:xmpp:sasl:2"><additional-data>dj02cnJpVFJCaTIzV3BSUi93dHVwK21NaFVaVW4vZEI1bkxUSlJzamw5NUc0PQ==</additional-data><authorization-identifier>user@server</authorization-identifier></success>',
),
StanzaExpectation(
"<iq xmlns='jabber:client' type='set' id='aaaa'><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind' /></iq>",
'''
'<iq xmlns="jabber:client" type="result" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><jid>polynomdivision@test.server/MU29eEZn</jid></bind></iq>',
''',
adjustId: true,
ignoreId: true,
),
]);
final conn = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
fakeSocket,
)..setConnectionSettings(
ConnectionSettings(
jid: JID.fromString('user@server'),
password: 'pencil',
useDirectTLS: true,
),
);
await conn.registerManagers([
PresenceManager(),
RosterManager(TestingRosterStateManager('', [])),
DiscoManager([]),
]);
await conn.registerFeatureNegotiators([
SaslPlainNegotiator(),
SaslScramNegotiator(
10,
'n=user,r=rOprNGfwEbeRWgbNEkqO',
'rOprNGfwEbeRWgbNEkqO',
ScramHashType.sha256,
),
ResourceBindingNegotiator(),
Sasl2Negotiator(
userAgent: const UserAgent(
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
software: 'moxxmpp',
device: "PapaTutuWawa's awesome device",
),
),
]);
final result = await conn.connect(
waitUntilLogin: true,
shouldReconnect: false,
enableReconnectOnSuccess: false,
);
expect(result.isType<XmppError>(), false);
});
test('Test SCRAM-SHA-1 SASL2 negotiation with an invalid signature',
() async {
final fakeSocket = StubTCPSocket([
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='server' from='user@server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
<mechanism>PLAIN</mechanism>
<mechanism>SCRAM-SHA-256</mechanism>
</mechanisms>
<authentication xmlns='urn:xmpp:sasl:2'>
<mechanism>PLAIN</mechanism>
<mechanism>SCRAM-SHA-256</mechanism>
</authentication>
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
<required/>
</bind>
</stream:features>''',
),
StanzaExpectation(
"<authenticate xmlns='urn:xmpp:sasl:2' mechanism='SCRAM-SHA-256'><user-agent id='d4565fa7-4d72-4749-b3d3-740edbf87770'><software>moxxmpp</software><device>PapaTutuWawa's awesome device</device></user-agent><initial-response>biwsbj11c2VyLHI9ck9wck5HZndFYmVSV2diTkVrcU8=</initial-response></authenticate>",
'''
<challenge xmlns='urn:xmpp:sasl:2'>cj1yT3ByTkdmd0ViZVJXZ2JORWtxTyVodllEcFdVYTJSYVRDQWZ1eEZJbGopaE5sRiRrMCxzPVcyMlphSjBTTlk3c29Fc1VFamI2Z1E9PSxpPTQwOTY=</challenge>
''',
),
StanzaExpectation(
'<response xmlns="urn:xmpp:sasl:2">Yz1iaXdzLHI9ck9wck5HZndFYmVSV2diTkVrcU8laHZZRHBXVWEyUmFUQ0FmdXhGSWxqKWhObEYkazAscD1kSHpiWmFwV0lrNGpVaE4rVXRlOXl0YWc5empmTUhnc3FtbWl6N0FuZFZRPQ==</response>',
'<success xmlns="urn:xmpp:sasl:2"><additional-data>dj1zbUY5cHFWOFM3c3VBb1pXamE0ZEpSa0ZzS1E9</additional-data><authorization-identifier>user@server</authorization-identifier></success>',
),
]);
final conn = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
fakeSocket,
)..setConnectionSettings(
ConnectionSettings(
jid: JID.fromString('user@server'),
password: 'pencil',
useDirectTLS: true,
),
);
await conn.registerManagers([
PresenceManager(),
RosterManager(TestingRosterStateManager('', [])),
DiscoManager([]),
]);
await conn.registerFeatureNegotiators([
SaslPlainNegotiator(),
SaslScramNegotiator(
10,
'n=user,r=rOprNGfwEbeRWgbNEkqO',
'rOprNGfwEbeRWgbNEkqO',
ScramHashType.sha256,
),
ResourceBindingNegotiator(),
Sasl2Negotiator(
userAgent: const UserAgent(
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
software: 'moxxmpp',
device: "PapaTutuWawa's awesome device",
),
),
]);
final result = await conn.connect(
waitUntilLogin: true,
shouldReconnect: false,
enableReconnectOnSuccess: false,
);
expect(result.isType<NegotiatorError>(), true);
expect(result.get<NegotiatorError>() is InvalidServerSignatureError, true);
});
test('Test simple SASL2 inlining', () async {
final fakeSocket = StubTCPSocket([
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
<mechanism>PLAIN</mechanism>
</mechanisms>
<authentication xmlns='urn:xmpp:sasl:2'>
<mechanism>PLAIN</mechanism>
<inline>
<test-feature xmlns="invalid:example:dont:use" />
</inline>
</authentication>
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
<required/>
</bind>
</stream:features>''',
),
StanzaExpectation(
"<authenticate xmlns='urn:xmpp:sasl:2' mechanism='PLAIN'><user-agent id='d4565fa7-4d72-4749-b3d3-740edbf87770'><software>moxxmpp</software><device>PapaTutuWawa's awesome device</device></user-agent><initial-response>AHBvbHlub21kaXZpc2lvbgBhYWFh</initial-response><test-data-request xmlns='invalid:example:dont:use' /></authenticate>",
'''
<success xmlns='urn:xmpp:sasl:2'>
<authorization-identifier>polynomdivision@test.server</authorization-identifier>
<test-data xmlns='invalid:example:dont:use'>Hello World</test-data>
</success>
''',
),
StanzaExpectation(
"<iq xmlns='jabber:client' type='set' id='aaaa'><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind' /></iq>",
'''
'<iq xmlns="jabber:client" type="result" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><jid>polynomdivision@test.server/MU29eEZn</jid></bind></iq>',
''',
adjustId: true,
ignoreId: true,
),
]);
final conn = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
fakeSocket,
)..setConnectionSettings(
ConnectionSettings(
jid: JID.fromString('polynomdivision@test.server'),
password: 'aaaa',
useDirectTLS: true,
),
);
await conn.registerManagers([
PresenceManager(),
RosterManager(TestingRosterStateManager('', [])),
DiscoManager([]),
]);
await conn.registerFeatureNegotiators([
SaslPlainNegotiator(),
ResourceBindingNegotiator(),
ExampleNegotiator(),
Sasl2Negotiator(
userAgent: const UserAgent(
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
software: 'moxxmpp',
device: "PapaTutuWawa's awesome device",
),
),
]);
final result = await conn.connect(
waitUntilLogin: true,
shouldReconnect: false,
enableReconnectOnSuccess: false,
);
expect(result.isType<NegotiatorError>(), false);
expect(
conn.getNegotiatorById<ExampleNegotiator>('testNegotiator')!.value,
'Hello World',
);
});
test('Test simple SASL2 inlining 2', () async {
final fakeSocket = StubTCPSocket([
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
<mechanism>PLAIN</mechanism>
</mechanisms>
<authentication xmlns='urn:xmpp:sasl:2'>
<mechanism>PLAIN</mechanism>
<inline>
<resume xmlns="urn:xmpp:sm:3" />
</inline>
</authentication>
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
<required/>
</bind>
</stream:features>''',
),
StanzaExpectation(
"<authenticate xmlns='urn:xmpp:sasl:2' mechanism='PLAIN'><user-agent id='d4565fa7-4d72-4749-b3d3-740edbf87770'><software>moxxmpp</software><device>PapaTutuWawa's awesome device</device></user-agent><initial-response>AHBvbHlub21kaXZpc2lvbgBhYWFh</initial-response></authenticate>",
'''
<success xmlns='urn:xmpp:sasl:2'>
<authorization-identifier>polynomdivision@test.server</authorization-identifier>
</success>
''',
),
StanzaExpectation(
"<iq xmlns='jabber:client' type='set' id='aaaa'><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind' /></iq>",
'''
'<iq xmlns="jabber:client" type="result" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><jid>polynomdivision@test.server/MU29eEZn</jid></bind></iq>',
''',
adjustId: true,
ignoreId: true,
),
]);
final conn = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
fakeSocket,
)..setConnectionSettings(
ConnectionSettings(
jid: JID.fromString('polynomdivision@test.server'),
password: 'aaaa',
useDirectTLS: true,
),
);
await conn.registerManagers([
PresenceManager(),
RosterManager(TestingRosterStateManager('', [])),
DiscoManager([]),
]);
await conn.registerFeatureNegotiators([
SaslPlainNegotiator(),
ResourceBindingNegotiator(),
ExampleNegotiator(),
Sasl2Negotiator(
userAgent: const UserAgent(
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
software: 'moxxmpp',
device: "PapaTutuWawa's awesome device",
),
),
]);
final result = await conn.connect(
waitUntilLogin: true,
shouldReconnect: false,
enableReconnectOnSuccess: false,
);
expect(result.isType<NegotiatorError>(), false);
expect(
conn.getNegotiatorById<ExampleNegotiator>('testNegotiator')!.value,
null,
);
});
}

View File

@ -0,0 +1,269 @@
import 'package:moxxmpp/moxxmpp.dart';
import 'package:test/test.dart';
import '../helpers/logging.dart';
import '../helpers/xmpp.dart';
void main() {
initLogger();
test('Test FAST authentication without a token', () async {
final fakeSocket = StubTCPSocket([
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
<mechanism>PLAIN</mechanism>
<mechanism>HT-SHA-256-NONE</mechanism>
</mechanisms>
<authentication xmlns='urn:xmpp:sasl:2'>
<mechanism>PLAIN</mechanism>
<mechanism>HT-SHA-256-NONE</mechanism>
<mechanism>HT-SHA-256-ENDP</mechanism>
<inline>
<bind xmlns="urn:xmpp:bind:0" />
<fast xmlns="urn:xmpp:fast:0">
<mechanism>HT-SHA-256-NONE</mechanism>
<mechanism>HT-SHA-256-ENDP</mechanism>
</fast>
</inline>
</authentication>
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
<required/>
</bind>
</stream:features>''',
),
StanzaExpectation(
"<authenticate xmlns='urn:xmpp:sasl:2' mechanism='PLAIN'><user-agent id='d4565fa7-4d72-4749-b3d3-740edbf87770'><software>moxxmpp</software><device>PapaTutuWawa's awesome device</device></user-agent><initial-response>AHBvbHlub21kaXZpc2lvbgBhYWFh</initial-response><request-token xmlns='urn:xmpp:fast:0' mechanism='HT-SHA-256-NONE' /></authenticate>",
'''
<success xmlns='urn:xmpp:sasl:2'>
<authorization-identifier>polynomdivision@test.server</authorization-identifier>
<token xmlns='urn:xmpp:fast:0'
expiry='2020-03-12T14:36:15Z'
token='WXZzciBwYmFmdmZnZiBqdmd1IGp2eXFhcmZm' />
</success>
''',
),
StanzaExpectation(
'<iq xmlns="jabber:client" type="set" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/></iq>',
'<iq xmlns="jabber:client" type="result" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><jid>polynomdivision@test.server/MU29eEZn</jid></bind></iq>',
ignoreId: true,
),
StringExpectation(
'',
'',
),
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
<mechanism>PLAIN</mechanism>
<mechanism>HT-SHA-256-NONE</mechanism>
</mechanisms>
<authentication xmlns='urn:xmpp:sasl:2'>
<mechanism>PLAIN</mechanism>
<mechanism>HT-SHA-256-NONE</mechanism>
<mechanism>HT-SHA-256-ENDP</mechanism>
<inline>
<bind xmlns="urn:xmpp:bind:0" />
<fast xmlns="urn:xmpp:fast:0">
<mechanism>HT-SHA-256-NONE</mechanism>
<mechanism>HT-SHA-256-ENDP</mechanism>
</fast>
</inline>
</authentication>
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
<required/>
</bind>
</stream:features>''',
),
StanzaExpectation(
"<authenticate xmlns='urn:xmpp:sasl:2' mechanism='HT-SHA-256-NONE'><user-agent id='d4565fa7-4d72-4749-b3d3-740edbf87770'><software>moxxmpp</software><device>PapaTutuWawa's awesome device</device></user-agent><initial-response>WXZzciBwYmFmdmZnZiBqdmd1IGp2eXFhcmZm</initial-response><fast xmlns='urn:xmpp:fast:0' /></authenticate>",
'''
<success xmlns='urn:xmpp:sasl:2'>
<authorization-identifier>polynomdivision@test.server</authorization-identifier>
</success>
''',
),
StanzaExpectation(
'<iq xmlns="jabber:client" type="set" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/></iq>',
'<iq xmlns="jabber:client" type="result" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><jid>polynomdivision@test.server/MU29eEZn</jid></bind></iq>',
ignoreId: true,
),
]);
final conn = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
fakeSocket,
)..setConnectionSettings(
ConnectionSettings(
jid: JID.fromString('polynomdivision@test.server'),
password: 'aaaa',
useDirectTLS: true,
),
);
await conn.registerManagers([
RosterManager(TestingRosterStateManager('', [])),
DiscoManager([]),
]);
await conn.registerFeatureNegotiators([
SaslPlainNegotiator(),
ResourceBindingNegotiator(),
FASTSaslNegotiator(),
Sasl2Negotiator(
userAgent: const UserAgent(
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
software: 'moxxmpp',
device: "PapaTutuWawa's awesome device",
),
),
]);
final result1 = await conn.connect(
waitUntilLogin: true,
shouldReconnect: false,
enableReconnectOnSuccess: false,
);
expect(result1.isType<NegotiatorError>(), false);
expect(conn.resource, 'MU29eEZn');
expect(fakeSocket.getState(), 3);
final token = conn
.getNegotiatorById<FASTSaslNegotiator>(saslFASTNegotiator)!
.fastToken;
expect(token != null, true);
expect(token!.token, 'WXZzciBwYmFmdmZnZiBqdmd1IGp2eXFhcmZm');
// Disconnect
await conn.disconnect();
// Connect again, but use FAST this time
final result2 = await conn.connect(
waitUntilLogin: true,
shouldReconnect: false,
enableReconnectOnSuccess: false,
);
expect(result2.isType<NegotiatorError>(), false);
expect(conn.resource, 'MU29eEZn');
expect(fakeSocket.getState(), 7);
});
test('Test failed FAST authentication with a token', () async {
final fakeSocket = StubTCPSocket([
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
<mechanism>PLAIN</mechanism>
<mechanism>HT-SHA-256-NONE</mechanism>
</mechanisms>
<authentication xmlns='urn:xmpp:sasl:2'>
<mechanism>PLAIN</mechanism>
<mechanism>HT-SHA-256-NONE</mechanism>
<mechanism>HT-SHA-256-ENDP</mechanism>
<inline>
<bind xmlns="urn:xmpp:bind:0" />
<fast xmlns="urn:xmpp:fast:0">
<mechanism>HT-SHA-256-NONE</mechanism>
<mechanism>HT-SHA-256-ENDP</mechanism>
</fast>
</inline>
</authentication>
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
<required/>
</bind>
</stream:features>''',
),
StanzaExpectation(
"<authenticate xmlns='urn:xmpp:sasl:2' mechanism='HT-SHA-256-NONE'><user-agent id='d4565fa7-4d72-4749-b3d3-740edbf87770'><software>moxxmpp</software><device>PapaTutuWawa's awesome device</device></user-agent><initial-response>WXZzciBwYmFmdmZnZiBqdmd1IGp2eXFhcmZm</initial-response><fast xmlns='urn:xmpp:fast:0' /></authenticate>",
'''
<failure xmlns='urn:xmpp:sasl:2'>
<not-authorized xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>
</failure>
''',
),
StanzaExpectation(
"<authenticate xmlns='urn:xmpp:sasl:2' mechanism='PLAIN'><user-agent id='d4565fa7-4d72-4749-b3d3-740edbf87770'><software>moxxmpp</software><device>PapaTutuWawa's awesome device</device></user-agent><initial-response>AHBvbHlub21kaXZpc2lvbgBhYWFh</initial-response><request-token xmlns='urn:xmpp:fast:0' mechanism='HT-SHA-256-NONE' /></authenticate>",
'''
<success xmlns='urn:xmpp:sasl:2'>
<authorization-identifier>polynomdivision@test.server</authorization-identifier>
<token xmlns='urn:xmpp:fast:0'
expiry='2020-03-12T14:36:15Z'
token='ed00e36cb42449a365a306a413f51ffd5ea8' />
</success>
''',
),
StanzaExpectation(
'<iq xmlns="jabber:client" type="set" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/></iq>',
'<iq xmlns="jabber:client" type="result" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><jid>polynomdivision@test.server/MU29eEZn</jid></bind></iq>',
ignoreId: true,
),
]);
final conn = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
fakeSocket,
)..setConnectionSettings(
ConnectionSettings(
jid: JID.fromString('polynomdivision@test.server'),
password: 'aaaa',
useDirectTLS: true,
),
);
await conn.registerManagers([
RosterManager(TestingRosterStateManager('', [])),
DiscoManager([]),
]);
await conn.registerFeatureNegotiators([
SaslPlainNegotiator(),
ResourceBindingNegotiator(),
FASTSaslNegotiator()
..fastToken = const FASTToken(
'WXZzciBwYmFmdmZnZiBqdmd1IGp2eXFhcmZm',
'2020-03-12T14:36:15Z',
),
Sasl2Negotiator(
userAgent: const UserAgent(
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
software: 'moxxmpp',
device: "PapaTutuWawa's awesome device",
),
),
]);
final result1 = await conn.connect(
waitUntilLogin: true,
shouldReconnect: false,
enableReconnectOnSuccess: false,
);
expect(result1.isType<NegotiatorError>(), false);
expect(conn.resource, 'MU29eEZn');
expect(fakeSocket.getState(), 4);
final token = conn
.getNegotiatorById<FASTSaslNegotiator>(saslFASTNegotiator)!
.fastToken;
expect(token != null, true);
expect(token!.token, 'ed00e36cb42449a365a306a413f51ffd5ea8');
});
}

View File

@ -35,7 +35,6 @@ Future<bool> testRosterManager(
),
getManagerById: getManagerNullStub,
getNegotiatorById: getNegotiatorNullStub,
isFeatureSupported: (_) => false,
getFullJID: () => JID.fromString('$bareJid/$resource'),
getSocket: () => StubTCPSocket([]),
getConnection: () => XmppConnection(
@ -71,7 +70,7 @@ void main() {
final fakeSocket = StubTCPSocket(
[
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' xml:lang='en'>",
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
@ -90,7 +89,7 @@ void main() {
'<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl" />',
),
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' xml:lang='en'>",
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
@ -140,7 +139,7 @@ void main() {
StreamManagementManager(),
EntityCapabilitiesManager('http://moxxmpp.example'),
]);
conn.registerFeatureNegotiators([
await conn.registerFeatureNegotiators([
SaslPlainNegotiator(),
SaslScramNegotiator(10, '', '', ScramHashType.sha512),
ResourceBindingNegotiator(),
@ -151,13 +150,14 @@ void main() {
waitUntilLogin: true,
);
expect(fakeSocket.getState(), /*6*/ 5);
expect(conn.resource, 'MU29eEZn');
});
test('Test a failed SASL auth', () async {
final fakeSocket = StubTCPSocket(
[
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' xml:lang='en'>",
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
@ -195,7 +195,7 @@ void main() {
DiscoManager([]),
EntityCapabilitiesManager('http://moxxmpp.example'),
]);
conn.registerFeatureNegotiators([
await conn.registerFeatureNegotiators([
SaslPlainNegotiator(),
]);
@ -216,7 +216,7 @@ void main() {
final fakeSocket = StubTCPSocket(
[
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' xml:lang='en'>",
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' from='polynomdivision@test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
@ -254,7 +254,7 @@ void main() {
DiscoManager([]),
EntityCapabilitiesManager('http://moxxmpp.example'),
]);
conn.registerFeatureNegotiators([SaslPlainNegotiator()]);
await conn.registerFeatureNegotiators([SaslPlainNegotiator()]);
conn.asBroadcastStream().listen((event) {
if (event is AuthenticationFailedEvent &&
@ -296,7 +296,6 @@ void main() {
),
getManagerById: getManagerNullStub,
getNegotiatorById: getNegotiatorNullStub,
isFeatureSupported: (_) => false,
getFullJID: () => JID.fromString('some.user@example.server/aaaaa'),
getSocket: () => StubTCPSocket([]),
getConnection: () => XmppConnection(
@ -380,7 +379,7 @@ void main() {
final fakeSocket = StubTCPSocket(
[
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='example.org' xml:lang='en'>",
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='example.org' from='testuser@example.org' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
@ -407,18 +406,17 @@ void main() {
RosterManager(TestingRosterStateManager('', [])),
DiscoManager([]),
]);
conn
..registerFeatureNegotiators([
// SaslPlainNegotiator(),
ResourceBindingNegotiator(),
])
..setConnectionSettings(
ConnectionSettings(
jid: JID.fromString('testuser@example.org'),
password: 'abc123',
useDirectTLS: false,
),
);
await conn.registerFeatureNegotiators([
// SaslPlainNegotiator(),
ResourceBindingNegotiator(),
]);
conn.setConnectionSettings(
ConnectionSettings(
jid: JID.fromString('testuser@example.org'),
password: 'abc123',
useDirectTLS: false,
),
);
final result = await conn.connect(
waitUntilLogin: true,

View File

@ -18,9 +18,10 @@ Future<void> _runTest(String domain) async {
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
socket,
)..registerFeatureNegotiators([
StartTlsNegotiator(),
]);
);
await connection.registerFeatureNegotiators([
StartTlsNegotiator(),
]);
await connection.registerManagers([
DiscoManager([]),
RosterManager(TestingRosterStateManager('', [])),

View File

@ -19,9 +19,10 @@ void main() {
TestingSleepReconnectionPolicy(10),
AlwaysConnectedConnectivityManager(),
TCPSocketWrapper(),
)..registerFeatureNegotiators([
StartTlsNegotiator(),
]);
);
await connection.registerFeatureNegotiators([
StartTlsNegotiator(),
]);
await connection.registerManagers([
DiscoManager([]),
RosterManager(TestingRosterStateManager('', [])),
@ -68,9 +69,10 @@ void main() {
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
TCPSocketWrapper(),
)..registerFeatureNegotiators([
StartTlsNegotiator(),
]);
);
await connection.registerFeatureNegotiators([
StartTlsNegotiator(),
]);
await connection.registerManagers([
DiscoManager([]),
RosterManager(TestingRosterStateManager('', [])),

View File

@ -243,6 +243,8 @@ class TCPSocketWrapper extends BaseSocketWrapper {
if (await _hostPortConnect(host, port)) {
_setupStreams();
return true;
} else {
return false;
}
}
@ -289,17 +291,13 @@ class TCPSocketWrapper extends BaseSocketWrapper {
_eventStream.stream.asBroadcastStream();
@override
void write(String data, {String? redact}) {
void write(String data) {
if (_socket == null) {
_log.severe('Failed to write to socket as _socket is null');
return;
}
if (redact != null) {
_log.finest('**> $redact');
} else {
_log.finest('==> $data');
}
_log.finest('==> $data');
try {
_socket!.write(data);