diff --git a/flake.lock b/flake.lock index e80d64b..7ddaa21 100644 --- a/flake.lock +++ b/flake.lock @@ -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" } } }, diff --git a/flake.nix b/flake.nix index 3652774..446533a 100644 --- a/flake.nix +++ b/flake.nix @@ -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"; diff --git a/integration_tests/.gitignore b/integration_tests/.gitignore new file mode 100644 index 0000000..3c8a157 --- /dev/null +++ b/integration_tests/.gitignore @@ -0,0 +1,6 @@ +# Files and directories created by pub. +.dart_tool/ +.packages + +# Conventional directory for build output. +build/ diff --git a/integration_tests/README.md b/integration_tests/README.md new file mode 100644 index 0000000..0d93bac --- /dev/null +++ b/integration_tests/README.md @@ -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. diff --git a/integration_tests/analysis_options.yaml b/integration_tests/analysis_options.yaml new file mode 100644 index 0000000..5e2133e --- /dev/null +++ b/integration_tests/analysis_options.yaml @@ -0,0 +1 @@ +include: ../analysis_options.yaml diff --git a/integration_tests/certs/localhost.crt b/integration_tests/certs/localhost.crt new file mode 100644 index 0000000..5f27228 --- /dev/null +++ b/integration_tests/certs/localhost.crt @@ -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----- diff --git a/integration_tests/certs/localhost.key b/integration_tests/certs/localhost.key new file mode 100644 index 0000000..f068dca --- /dev/null +++ b/integration_tests/certs/localhost.key @@ -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----- diff --git a/integration_tests/prosody.cfg.lua b/integration_tests/prosody.cfg.lua new file mode 100644 index 0000000..29f1977 --- /dev/null +++ b/integration_tests/prosody.cfg.lua @@ -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" + diff --git a/integration_tests/pubspec.yaml b/integration_tests/pubspec.yaml new file mode 100644 index 0000000..cb6a38e --- /dev/null +++ b/integration_tests/pubspec.yaml @@ -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 diff --git a/integration_tests/test/sasl2_test.dart b/integration_tests/test/sasl2_test.dart new file mode 100644 index 0000000..edd278f --- /dev/null +++ b/integration_tests/test/sasl2_test.dart @@ -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(), true); + expect(conn.getNegotiatorById(sasl2Negotiator)!.state, NegotiatorState.done); + expect(conn.getNegotiatorById(saslFASTNegotiator)!.fastToken != null, true,); + }); +} diff --git a/packages/moxxmpp/CHANGELOG.md b/packages/moxxmpp/CHANGELOG.md index 4bf403d..071da45 100644 --- a/packages/moxxmpp/CHANGELOG.md +++ b/packages/moxxmpp/CHANGELOG.md @@ -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 diff --git a/packages/moxxmpp/lib/moxxmpp.dart b/packages/moxxmpp/lib/moxxmpp.dart index e8583ad..8a9dd9b 100644 --- a/packages/moxxmpp/lib/moxxmpp.dart +++ b/packages/moxxmpp/lib/moxxmpp.dart @@ -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'; diff --git a/packages/moxxmpp/lib/src/connection.dart b/packages/moxxmpp/lib/src/connection.dart index 1f1f673..6ce1f7b 100644 --- a/packages/moxxmpp/lib/src/connection.dart +++ b/packages/moxxmpp/lib/src/connection.dart @@ -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: { '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 _xmppManagers = {}; - /// Disco info we got after binding a resource (xmlns) - final List _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 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 negotiators) { + Future registerFeatureNegotiators( + List 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 _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 _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: {'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( diff --git a/packages/moxxmpp/lib/src/events.dart b/packages/moxxmpp/lib/src/events.dart index 9a8f9ea..44bbc87 100644 --- a/packages/moxxmpp/lib/src/events.dart +++ b/packages/moxxmpp/lib/src/events.dart @@ -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; } diff --git a/packages/moxxmpp/lib/src/managers/attributes.dart b/packages/moxxmpp/lib/src/managers/attributes.dart index 9143d58..508c7a8 100644 --- a/packages/moxxmpp/lib/src/managers/attributes.dart +++ b/packages/moxxmpp/lib/src/managers/attributes.dart @@ -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(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; diff --git a/packages/moxxmpp/lib/src/managers/base.dart b/packages/moxxmpp/lib/src/managers/base.dart index 8d0ebb1..8170675 100644 --- a/packages/moxxmpp/lib/src/managers/base.dart +++ b/packages/moxxmpp/lib/src/managers/base.dart @@ -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 isFeatureSupported(String xmlns) async { + final dm = _managerAttributes.getManagerById(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()) { + return false; + } + + return result.get().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. diff --git a/packages/moxxmpp/lib/src/namespaces.dart b/packages/moxxmpp/lib/src/namespaces.dart index d49330b..86b69b3 100644 --- a/packages/moxxmpp/lib/src/namespaces.dart +++ b/packages/moxxmpp/lib/src/namespaces.dart @@ -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'; diff --git a/packages/moxxmpp/lib/src/negotiators/namespaces.dart b/packages/moxxmpp/lib/src/negotiators/namespaces.dart index 00595b7..17691ec 100644 --- a/packages/moxxmpp/lib/src/negotiators/namespaces.dart +++ b/packages/moxxmpp/lib/src/negotiators/namespaces.dart @@ -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'; diff --git a/packages/moxxmpp/lib/src/negotiators/negotiator.dart b/packages/moxxmpp/lib/src/negotiators/negotiator.dart index 3594403..600e872 100644 --- a/packages/moxxmpp/lib/src/negotiators/negotiator.dart +++ b/packages/moxxmpp/lib/src/negotiators/negotiator.dart @@ -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 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 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 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 postRegisterCallback() async {} } diff --git a/packages/moxxmpp/lib/src/presence.dart b/packages/moxxmpp/lib/src/presence.dart index a461a85..6dcc962 100644 --- a/packages/moxxmpp/lib/src/presence.dart +++ b/packages/moxxmpp/lib/src/presence.dart @@ -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 onXmppEvent(XmppEvent event) async { + if (event is StreamNegotiationsDoneEvent) { + // Send initial presence only when we have not resumed the stream + final sm = getAttributes().getNegotiatorById( + streamManagementNegotiator, + ); + final isResumed = sm?.isResumed ?? false; + if (!isResumed) { + unawaited(sendInitialPresence()); + } + } + } + Future _onPresence( Stanza presence, StanzaHandlerData state, diff --git a/packages/moxxmpp/lib/src/negotiators/resource_binding.dart b/packages/moxxmpp/lib/src/rfcs/rfc_6120/resource_binding.dart similarity index 82% rename from packages/moxxmpp/lib/src/negotiators/resource_binding.dart rename to packages/moxxmpp/lib/src/rfcs/rfc_6120/resource_binding.dart index e60eee0..2ebb737 100644 --- a/packages/moxxmpp/lib/src/negotiators/resource_binding.dart +++ b/packages/moxxmpp/lib/src/rfcs/rfc_6120/resource_binding.dart @@ -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); } } diff --git a/packages/moxxmpp/lib/src/negotiators/sasl/errors.dart b/packages/moxxmpp/lib/src/rfcs/rfc_6120/sasl/errors.dart similarity index 100% rename from packages/moxxmpp/lib/src/negotiators/sasl/errors.dart rename to packages/moxxmpp/lib/src/rfcs/rfc_6120/sasl/errors.dart diff --git a/packages/moxxmpp/lib/src/negotiators/sasl/kv.dart b/packages/moxxmpp/lib/src/rfcs/rfc_6120/sasl/kv.dart similarity index 100% rename from packages/moxxmpp/lib/src/negotiators/sasl/kv.dart rename to packages/moxxmpp/lib/src/rfcs/rfc_6120/sasl/kv.dart diff --git a/packages/moxxmpp/lib/src/negotiators/sasl/negotiator.dart b/packages/moxxmpp/lib/src/rfcs/rfc_6120/sasl/negotiator.dart similarity index 100% rename from packages/moxxmpp/lib/src/negotiators/sasl/negotiator.dart rename to packages/moxxmpp/lib/src/rfcs/rfc_6120/sasl/negotiator.dart diff --git a/packages/moxxmpp/lib/src/negotiators/sasl/nonza.dart b/packages/moxxmpp/lib/src/rfcs/rfc_6120/sasl/nonza.dart similarity index 100% rename from packages/moxxmpp/lib/src/negotiators/sasl/nonza.dart rename to packages/moxxmpp/lib/src/rfcs/rfc_6120/sasl/nonza.dart diff --git a/packages/moxxmpp/lib/src/negotiators/sasl/plain.dart b/packages/moxxmpp/lib/src/rfcs/rfc_6120/sasl/plain.dart similarity index 55% rename from packages/moxxmpp/lib/src/negotiators/sasl/plain.dart rename to packages/moxxmpp/lib/src/rfcs/rfc_6120/sasl/plain.dart index 8ee4512..573762c 100644 --- a/packages/moxxmpp/lib/src/negotiators/sasl/plain.dart +++ b/packages/moxxmpp/lib/src/rfcs/rfc_6120/sasl/plain.dart @@ -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 @@ -76,4 +77,34 @@ class SaslPlainNegotiator extends SaslNegotiator { super.reset(); } + + @override + Future postRegisterCallback() async { + attributes + .getNegotiatorById(sasl2Negotiator) + ?.registerSaslNegotiator(this); + } + + @override + Future 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> onSasl2Success(XMLNode response) async { + state = NegotiatorState.done; + return const Result(true); + } + + @override + Future onSasl2Failure(XMLNode response) async {} + + @override + Future> onSasl2FeaturesReceived(XMLNode sasl2Features) async { + return []; + } } diff --git a/packages/moxxmpp/lib/src/negotiators/sasl/scram.dart b/packages/moxxmpp/lib/src/rfcs/rfc_6120/sasl/scram.dart similarity index 79% rename from packages/moxxmpp/lib/src/negotiators/sasl/scram.dart rename to packages/moxxmpp/lib/src/rfcs/rfc_6120/sasl/scram.dart index 78d64c8..9ea570b 100644 --- a/packages/moxxmpp/lib/src/negotiators/sasl/scram.dart +++ b/packages/moxxmpp/lib/src/rfcs/rfc_6120/sasl/scram.dart @@ -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> 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 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 postRegisterCallback() async { + attributes + .getNegotiatorById(sasl2Negotiator) + ?.registerSaslNegotiator(this); + } + + @override + Future> onSasl2FeaturesReceived(XMLNode sasl2Features) async { + return []; + } + + @override + Future onSasl2Failure(XMLNode response) async {} + + @override + Future> 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); + } } diff --git a/packages/moxxmpp/lib/src/negotiators/starttls.dart b/packages/moxxmpp/lib/src/rfcs/rfc_6120/starttls.dart similarity index 100% rename from packages/moxxmpp/lib/src/negotiators/starttls.dart rename to packages/moxxmpp/lib/src/rfcs/rfc_6120/starttls.dart diff --git a/packages/moxxmpp/lib/src/settings.dart b/packages/moxxmpp/lib/src/settings.dart index 4368b14..177672b 100644 --- a/packages/moxxmpp/lib/src/settings.dart +++ b/packages/moxxmpp/lib/src/settings.dart @@ -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; } diff --git a/packages/moxxmpp/lib/src/socket.dart b/packages/moxxmpp/lib/src/socket.dart index 4999087..d98b523 100644 --- a/packages/moxxmpp/lib/src/socket.dart +++ b/packages/moxxmpp/lib/src/socket.dart @@ -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 diff --git a/packages/moxxmpp/lib/src/stringxml.dart b/packages/moxxmpp/lib/src/stringxml.dart index 7fda088..7fa20c8 100644 --- a/packages/moxxmpp/lib/src/stringxml.dart +++ b/packages/moxxmpp/lib/src/stringxml.dart @@ -146,4 +146,6 @@ class XMLNode { String innerText() { return text ?? ''; } + + String? get xmlns => attributes['xmlns'] as String?; } diff --git a/packages/moxxmpp/lib/src/xeps/staging/fast.dart b/packages/moxxmpp/lib/src/xeps/staging/fast.dart new file mode 100644 index 0000000..83785ad --- /dev/null +++ b/packages/moxxmpp/lib/src/xeps/staging/fast.dart @@ -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 element', + ); + assert( + token.xmlns == fastXmlns, + 'Token can only be deserialised from a 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 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 features) { + return features.firstWhereOrNull( + (child) => child.tag == 'fast' && child.xmlns == fastXmlns, + ) != + null; + } + + @override + Future> negotiate( + XMLNode nonza, + ) async { + // TODO(Unknown): Is FAST supposed to work without SASL2? + return const Result(NegotiatorState.done); + } + + @override + Future> 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 onSasl2Failure(XMLNode response) async { + fastToken = null; + await attributes.sendEvent( + InvalidateFASTTokenEvent(), + ); + } + + @override + bool shouldRetrySasl() => true; + + @override + Future> 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 getRawStep(String input) async { + return fastToken!.token; + } + + @override + Future postRegisterCallback() async { + attributes + .getNegotiatorById(sasl2Negotiator) + ?.registerSaslNegotiator(this); + } +} diff --git a/packages/moxxmpp/lib/src/xeps/xep_0198/negotiator.dart b/packages/moxxmpp/lib/src/xeps/xep_0198/negotiator.dart index 274f345..3008370 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0198/negotiator.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0198/negotiator.dart @@ -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 features) { + final sm = attributes.getManagerById(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 onXmppEvent(XmppEvent event) async { + if (event is ResourceBoundEvent) { + _resource = event.resource; + } + } @override bool matchesFeature(List 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 _onStreamResumptionFailed() async { + await attributes.sendEvent(StreamResumeFailedEvent()); + final sm = attributes.getManagerById(smManager)!; + + // We have to do this because we otherwise get a stanza stuck in the queue, + // thus spamming the server on every nonza we receive. + // ignore: cascade_invocations + await sm.setState(StreamManagementState(0, 0)); + await sm.commitState(); + + _resumeFailed = true; + _isResumed = false; + _state = _StreamManagementNegotiatorState.ready; + } + + Future _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 _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> 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 _log.info( 'Stream resumption failed. Expected , got ${nonza.tag}, Proceeding with new stream...', ); - await attributes.sendEvent(StreamResumeFailedEvent()); - final sm = - attributes.getManagerById(smManager)!; - - // We have to do this because we otherwise get a stanza stuck in the queue, - // thus spamming the server on every 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 _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> onBind2FeaturesReceived( + List bind2Features, + ) async { + if (!bind2Features.contains(smXmlns)) { + return []; + } + + _inlineStreamEnablementRequested = true; + return [ + StreamManagementEnableNonza(), + ]; + } + + @override + Future onBind2Success(XMLNode response) async {} + + @override + Future> onSasl2FeaturesReceived(XMLNode sasl2Features) async { + final inline = sasl2Features.firstTag('inline')!; + final resume = inline.firstTag('resume', xmlns: smXmlns); + + if (resume == null) { + return []; + } + + final sm = attributes.getManagerById(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> 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 postRegisterCallback() async { + attributes + .getNegotiatorById(sasl2Negotiator) + ?.registerNegotiator(this); + attributes + .getNegotiatorById(bind2Negotiator) + ?.registerNegotiator(this); + } } diff --git a/packages/moxxmpp/lib/src/xeps/xep_0280.dart b/packages/moxxmpp/lib/src/xeps/xep_0280.dart index 5a2ad78..3153dd1 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0280.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0280.dart @@ -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 onBind2Success(XMLNode response) async { + if (!_requestedEnablement) { + return; + } + + final enabled = response.firstTag('enabled', xmlns: carbonsXmlns); + final cm = attributes.getManagerById(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> onBind2FeaturesReceived( + List bind2Features, + ) async { + if (!bind2Features.contains(carbonsXmlns)) { + return []; + } + + _requestedEnablement = true; + return [ + XMLNode.xmlns( + tag: 'enable', + xmlns: carbonsXmlns, + ), + ]; + } + + @override + void reset() { + _requestedEnablement = false; + + super.reset(); + } +} diff --git a/packages/moxxmpp/lib/src/xeps/xep_0352.dart b/packages/moxxmpp/lib/src/xeps/xep_0352.dart index 6ac38c4..718d805 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0352.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0352.dart @@ -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> onBind2FeaturesReceived( + List bind2Features, + ) async { + if (!bind2Features.contains(csiXmlns)) { + return []; + } + + _supported = true; + final active = attributes.getManagerById(csiManager)!.isActive; + return [ + if (active) CSIActiveNonza() else CSIInactiveNonza(), + ]; + } + + @override + Future onBind2Success(XMLNode response) async {} + @override void reset() { _supported = false; super.reset(); } + + @override + Future postRegisterCallback() async { + attributes + .getNegotiatorById(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 isSupported() async { @@ -71,23 +101,31 @@ class CSIManager extends XmppManagerBase { } } - /// Tells the server to top optimizing traffic - Future 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 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 setInactive() async { + /// If [sendNonza] is false, then no nonza is sent. This is useful + /// for setting up the CSI manager for Bind2. + Future 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()); + } } } } diff --git a/packages/moxxmpp/lib/src/xeps/xep_0386.dart b/packages/moxxmpp/lib/src/xeps/xep_0386.dart new file mode 100644 index 0000000..4f61add --- /dev/null +++ b/packages/moxxmpp/lib/src/xeps/xep_0386.dart @@ -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> onBind2FeaturesReceived(List bind2Features); + + /// Called by the Bind2 negotiator when Bind2 results are received. + Future 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 features) => false; + + @override + Future> negotiate( + XMLNode nonza, + ) async { + return const Result(NegotiatorState.done); + } + + @mustCallSuper + @override + Future postRegisterCallback() async { + attributes + .getNegotiatorById(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 _negotiators = + List.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> negotiate( + XMLNode nonza, + ) async { + return const Result(NegotiatorState.done); + } + + @override + Future> onSasl2FeaturesReceived(XMLNode sasl2Features) async { + final children = List.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 features) { + return features.firstWhereOrNull( + (child) => child.tag == 'bind' && child.xmlns == bind2Xmlns, + ) != + null; + } + + @override + Future> 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 postRegisterCallback() async { + attributes + .getNegotiatorById(sasl2Negotiator)! + .registerNegotiator(this); + } +} diff --git a/packages/moxxmpp/lib/src/xeps/xep_0388/errors.dart b/packages/moxxmpp/lib/src/xeps/xep_0388/errors.dart new file mode 100644 index 0000000..74c500e --- /dev/null +++ b/packages/moxxmpp/lib/src/xeps/xep_0388/errors.dart @@ -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; +} diff --git a/packages/moxxmpp/lib/src/xeps/xep_0388/negotiators.dart b/packages/moxxmpp/lib/src/xeps/xep_0388/negotiators.dart new file mode 100644 index 0000000..948930f --- /dev/null +++ b/packages/moxxmpp/lib/src/xeps/xep_0388/negotiators.dart @@ -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 nonza. + /// This method is only called when the element contains an item with + /// xmlns equal to [negotiatingXmlns]. + Future> 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 element contains an + /// item with xmlns equal to [negotiatingXmlns]. + Future> onSasl2Success(XMLNode response); + + /// Called by the SASL2 negotiator when the SASL2 negotiations have failed. [response] + /// is the entire response nonza. + Future 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 + /// element. + bool canInlineFeature(List 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 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 features) { + return true; + } +} diff --git a/packages/moxxmpp/lib/src/xeps/xep_0388/user_agent.dart b/packages/moxxmpp/lib/src/xeps/xep_0388/user_agent.dart new file mode 100644 index 0000000..3d12c4f --- /dev/null +++ b/packages/moxxmpp/lib/src/xeps/xep_0388/user_agent.dart @@ -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, + ), + ], + ); + } +} diff --git a/packages/moxxmpp/lib/src/xeps/xep_0388/xep_0388.dart b/packages/moxxmpp/lib/src/xeps/xep_0388/xep_0388.dart new file mode 100644 index 0000000..d28f760 --- /dev/null +++ b/packages/moxxmpp/lib/src/xeps/xep_0388/xep_0388.dart @@ -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 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 _featureNegotiators = + List.empty(growable: true); + + /// List of SASL negotiators, sorted by their priority. The higher the priority, the + /// lower its index. + final List _saslNegotiators = + List.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 element we received with the stream features. + XMLNode? _sasl2Data; + final List _activeSasl2Negotiators = + List.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 features) { + // Only do SASL2 when the socket is secure + return attributes.getSocket().isSecure() && super.matchesFeature(features); + } + + @override + Future> 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.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()) { + return Result(result.get()); + } + } + + // 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(); + } +} diff --git a/packages/moxxmpp/test/helpers/manager.dart b/packages/moxxmpp/test/helpers/manager.dart index 17706ea..a530cce 100644 --- a/packages/moxxmpp/test/helpers/manager.dart +++ b/packages/moxxmpp/test/helpers/manager.dart @@ -56,7 +56,6 @@ class TestingManagerHolder { sendNonza: (_) {}, sendEvent: (_) {}, getSocket: () => _socket, - isFeatureSupported: (_) => false, getNegotiatorById: getNegotiatorNullStub, getFullJID: () => jid, getManagerById: _getManagerById, diff --git a/packages/moxxmpp/test/helpers/xmpp.dart b/packages/moxxmpp/test/helpers/xmpp.dart index c096fb7..3833b8b 100644 --- a/packages/moxxmpp/test/helpers/xmpp.dart +++ b/packages/moxxmpp/test/helpers/xmpp.dart @@ -58,7 +58,7 @@ List buildAuthenticatedPlay(ConnectionSettings settings) { ); return [ StringExpectation( - "", + "", ''' buildAuthenticatedPlay(ConnectionSettings settings) { '', ), StringExpectation( - "", + "", ''' ", + "", ''' 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}) {}, + (_) {}, ), ); diff --git a/packages/moxxmpp/test/stringxml_test.dart b/packages/moxxmpp/test/stringxml_test.dart index 26a3811..41aeb1c 100644 --- a/packages/moxxmpp/test/stringxml_test.dart +++ b/packages/moxxmpp/test/stringxml_test.dart @@ -23,8 +23,8 @@ void main() { ); expect( - StreamHeaderNonza('uwu.server').toXml(), - "", + StreamHeaderNonza(JID.fromString('user@uwu.server')).toXml(), + "", ); expect( diff --git a/packages/moxxmpp/test/xeps/xep_0030_test.dart b/packages/moxxmpp/test/xeps/xep_0030_test.dart index 6f15953..f4cba2c 100644 --- a/packages/moxxmpp/test/xeps/xep_0030_test.dart +++ b/packages/moxxmpp/test/xeps/xep_0030_test.dart @@ -12,7 +12,7 @@ void main() { final fakeSocket = StubTCPSocket( [ StringExpectation( - "", + "", ''' ', ), StringExpectation( - "", + "", ''' false, getFullJID: () => JID.fromString('hallo@example.server/uwu'), getSocket: () => StubTCPSocket([]), getConnection: () => XmppConnection( @@ -228,7 +227,7 @@ void main() { () async { final fakeSocket = StubTCPSocket([ StringExpectation( - "", + "", ''' ', ), StringExpectation( - "", + "", ''' ", + "", ''' ', ), StringExpectation( - "", + "", ''' ", '', ), - // StringExpectation( - // "chat", - // '', - // ), + StringExpectation( + "chat", + '', + ), StanzaExpectation( "", "", @@ -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( - "", + "", ''' ', ), StringExpectation( - "", + "", ''' ", + "", ''' ', ), StringExpectation( - "", + "", ''' ", + "", ''' ', ), StringExpectation( - "", + "", ''' ", + ''' + + + + PLAIN + + + PLAIN + + + + + + + + ''', + ), + StanzaExpectation( + "moxxmppPapaTutuWawa's awesome deviceAHBvbHlub21kaXZpc2lvbgBhYWFh", + ''' + + polynomdivision@test.server + + + ''', + ), + ]); + 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(), 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( + "", + ''' + + + + PLAIN + + + PLAIN + + + + + + + + + + + + + ''', + ), + StanzaExpectation( + "moxxmppPapaTutuWawa's awesome deviceAHBvbHlub21kaXZpc2lvbgBhYWFh", + ''' + + polynomdivision@test.server + + + ''', + ), + ]); + 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(), 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( + "", + ''' + + + + PLAIN + + + PLAIN + + + + + + + + + + + + + ''', + ), + StanzaExpectation( + "moxxmppPapaTutuWawa's awesome deviceAHBvbHlub21kaXZpc2lvbgBhYWFh", + ''' + + polynomdivision@test.server/test-resource + + + + + + ''', + ), + ]); + 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(), false); + + expect(smn.isResumed, false); + expect(smn.resumeFailed, true); + expect(smn.streamEnablementFailed, true); + expect(conn.resource, 'test-resource'); + }); } diff --git a/packages/moxxmpp/test/xeps/xep_0280_test.dart b/packages/moxxmpp/test/xeps/xep_0280_test.dart index f22333e..94aa37f 100644 --- a/packages/moxxmpp/test/xeps/xep_0280_test.dart +++ b/packages/moxxmpp/test/xeps/xep_0280_test.dart @@ -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( + "", + ''' + + + + PLAIN + + + PLAIN + + + + + + + + + + + + + + ''', + ), + StanzaExpectation( + "moxxmppPapaTutuWawa's awesome deviceAHBvbHlub21kaXZpc2lvbgBhYWFh", + ''' + + polynomdivision@test.server/test-resource + + + + + ''', + ), + ]); + 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(), false); + expect(conn.resource, 'test-resource'); + expect( + conn.getManagerById(carbonsManager)!.isEnabled, + true, + ); + }); } diff --git a/packages/moxxmpp/test/xeps/xep_0352_test.dart b/packages/moxxmpp/test/xeps/xep_0352_test.dart index da89079..f0248cf 100644 --- a/packages/moxxmpp/test/xeps/xep_0352_test.dart +++ b/packages/moxxmpp/test/xeps/xep_0352_test.dart @@ -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(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( + "", + ''' + + + + PLAIN + + + PLAIN + + + + + + + + + + + + ''', + ), + StanzaExpectation( + ''' + + + moxxmpp + PapaTutuWawa's awesome device + + AHBvbHlub21kaXZpc2lvbgBhYWFh + + + +''', + ''' + + polynomdivision@test.server/test-resource + + ''', + ), + ]); + 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(), false); + expect(fakeSocket.getState(), 2); + expect( + conn.getNegotiatorById(csiNegotiator)!.isSupported, + true, + ); + }); } diff --git a/packages/moxxmpp/test/xeps/xep_0386_test.dart b/packages/moxxmpp/test/xeps/xep_0386_test.dart new file mode 100644 index 0000000..2c49225 --- /dev/null +++ b/packages/moxxmpp/test/xeps/xep_0386_test.dart @@ -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( + "", + ''' + + + + PLAIN + + + PLAIN + + + + + + + + ''', + ), + StanzaExpectation( + "moxxmppPapaTutuWawa's awesome deviceAHBvbHlub21kaXZpc2lvbgBhYWFh", + ''' + + polynomdivision@test.server/random.resource + + ''', + ), + ]); + 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(), false); + expect(conn.resource, 'random.resource'); + }); + + test('Test simple Bind2 negotiation with a provided tag', () async { + final fakeSocket = StubTCPSocket([ + StringExpectation( + "", + ''' + + + + PLAIN + + + PLAIN + + + + + + + + ''', + ), + StanzaExpectation( + "moxxmppPapaTutuWawa's awesome deviceAHBvbHlub21kaXZpc2lvbgBhYWFhmoxxmpp", + ''' + + polynomdivision@test.server/moxxmpp.random.resource + + ''', + ), + ]); + 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(), false); + expect(conn.resource, 'moxxmpp.random.resource'); + }); +} diff --git a/packages/moxxmpp/test/xeps/xep_0388_test.dart b/packages/moxxmpp/test/xeps/xep_0388_test.dart new file mode 100644 index 0000000..11908e9 --- /dev/null +++ b/packages/moxxmpp/test/xeps/xep_0388_test.dart @@ -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> negotiate( + XMLNode nonza, + ) async { + return const Result(NegotiatorState.done); + } + + @override + bool canInlineFeature(List features) { + return features.firstWhereOrNull( + (child) => child.xmlns == 'invalid:example:dont:use', + ) != + null; + } + + @override + Future postRegisterCallback() async { + attributes + .getNegotiatorById(sasl2Negotiator)! + .registerNegotiator(this); + } + + @override + Future> onSasl2FeaturesReceived(XMLNode nonza) async { + return [ + XMLNode.xmlns( + tag: 'test-data-request', + xmlns: 'invalid:example:dont:use', + ), + ]; + } + + @override + Future> 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( + "", + ''' + + + + PLAIN + + + PLAIN + + + + + ''', + ), + StanzaExpectation( + "moxxmppPapaTutuWawa's awesome deviceAHBvbHlub21kaXZpc2lvbgBhYWFh", + ''' + + polynomdivision@test.server + + ''', + ), + StanzaExpectation( + "", + ''' +'polynomdivision@test.server/MU29eEZn', + ''', + 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(), false); + }); + + test('Test SCRAM-SHA-1 SASL2 negotiation with a valid signature', () async { + final fakeSocket = StubTCPSocket([ + StringExpectation( + "", + ''' + + + + PLAIN + SCRAM-SHA-256 + + + PLAIN + SCRAM-SHA-256 + + + + + ''', + ), + StanzaExpectation( + "moxxmppPapaTutuWawa's awesome devicebiwsbj11c2VyLHI9ck9wck5HZndFYmVSV2diTkVrcU8=", + ''' +cj1yT3ByTkdmd0ViZVJXZ2JORWtxTyVodllEcFdVYTJSYVRDQWZ1eEZJbGopaE5sRiRrMCxzPVcyMlphSjBTTlk3c29Fc1VFamI2Z1E9PSxpPTQwOTY= + ''', + ), + StanzaExpectation( + 'Yz1iaXdzLHI9ck9wck5HZndFYmVSV2diTkVrcU8laHZZRHBXVWEyUmFUQ0FmdXhGSWxqKWhObEYkazAscD1kSHpiWmFwV0lrNGpVaE4rVXRlOXl0YWc5empmTUhnc3FtbWl6N0FuZFZRPQ==', + 'dj02cnJpVFJCaTIzV3BSUi93dHVwK21NaFVaVW4vZEI1bkxUSlJzamw5NUc0PQ==user@server', + ), + StanzaExpectation( + "", + ''' +'polynomdivision@test.server/MU29eEZn', + ''', + 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(), false); + }); + + test('Test SCRAM-SHA-1 SASL2 negotiation with an invalid signature', + () async { + final fakeSocket = StubTCPSocket([ + StringExpectation( + "", + ''' + + + + PLAIN + SCRAM-SHA-256 + + + PLAIN + SCRAM-SHA-256 + + + + + ''', + ), + StanzaExpectation( + "moxxmppPapaTutuWawa's awesome devicebiwsbj11c2VyLHI9ck9wck5HZndFYmVSV2diTkVrcU8=", + ''' +cj1yT3ByTkdmd0ViZVJXZ2JORWtxTyVodllEcFdVYTJSYVRDQWZ1eEZJbGopaE5sRiRrMCxzPVcyMlphSjBTTlk3c29Fc1VFamI2Z1E9PSxpPTQwOTY= + ''', + ), + StanzaExpectation( + 'Yz1iaXdzLHI9ck9wck5HZndFYmVSV2diTkVrcU8laHZZRHBXVWEyUmFUQ0FmdXhGSWxqKWhObEYkazAscD1kSHpiWmFwV0lrNGpVaE4rVXRlOXl0YWc5empmTUhnc3FtbWl6N0FuZFZRPQ==', + 'dj1zbUY5cHFWOFM3c3VBb1pXamE0ZEpSa0ZzS1E9user@server', + ), + ]); + 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(), true); + expect(result.get() is InvalidServerSignatureError, true); + }); + + test('Test simple SASL2 inlining', () async { + final fakeSocket = StubTCPSocket([ + StringExpectation( + "", + ''' + + + + PLAIN + + + PLAIN + + + + + + + + ''', + ), + StanzaExpectation( + "moxxmppPapaTutuWawa's awesome deviceAHBvbHlub21kaXZpc2lvbgBhYWFh", + ''' + + polynomdivision@test.server + Hello World + + ''', + ), + StanzaExpectation( + "", + ''' +'polynomdivision@test.server/MU29eEZn', + ''', + 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(), false); + + expect( + conn.getNegotiatorById('testNegotiator')!.value, + 'Hello World', + ); + }); + + test('Test simple SASL2 inlining 2', () async { + final fakeSocket = StubTCPSocket([ + StringExpectation( + "", + ''' + + + + PLAIN + + + PLAIN + + + + + + + + ''', + ), + StanzaExpectation( + "moxxmppPapaTutuWawa's awesome deviceAHBvbHlub21kaXZpc2lvbgBhYWFh", + ''' + + polynomdivision@test.server + + ''', + ), + StanzaExpectation( + "", + ''' +'polynomdivision@test.server/MU29eEZn', + ''', + 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(), false); + + expect( + conn.getNegotiatorById('testNegotiator')!.value, + null, + ); + }); +} diff --git a/packages/moxxmpp/test/xeps/xep_xxxx_fast_test.dart b/packages/moxxmpp/test/xeps/xep_xxxx_fast_test.dart new file mode 100644 index 0000000..5ea1634 --- /dev/null +++ b/packages/moxxmpp/test/xeps/xep_xxxx_fast_test.dart @@ -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( + "", + ''' + + + + PLAIN + HT-SHA-256-NONE + + + PLAIN + HT-SHA-256-NONE + HT-SHA-256-ENDP + + + + HT-SHA-256-NONE + HT-SHA-256-ENDP + + + + + + + ''', + ), + StanzaExpectation( + "moxxmppPapaTutuWawa's awesome deviceAHBvbHlub21kaXZpc2lvbgBhYWFh", + ''' + + polynomdivision@test.server + + + ''', + ), + StanzaExpectation( + '', + 'polynomdivision@test.server/MU29eEZn', + ignoreId: true, + ), + StringExpectation( + '', + '', + ), + StringExpectation( + "", + ''' + + + + PLAIN + HT-SHA-256-NONE + + + PLAIN + HT-SHA-256-NONE + HT-SHA-256-ENDP + + + + HT-SHA-256-NONE + HT-SHA-256-ENDP + + + + + + + ''', + ), + StanzaExpectation( + "moxxmppPapaTutuWawa's awesome deviceWXZzciBwYmFmdmZnZiBqdmd1IGp2eXFhcmZm", + ''' + + polynomdivision@test.server + + ''', + ), + StanzaExpectation( + '', + 'polynomdivision@test.server/MU29eEZn', + 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(), false); + expect(conn.resource, 'MU29eEZn'); + expect(fakeSocket.getState(), 3); + + final token = conn + .getNegotiatorById(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(), false); + expect(conn.resource, 'MU29eEZn'); + expect(fakeSocket.getState(), 7); + }); + + test('Test failed FAST authentication with a token', () async { + final fakeSocket = StubTCPSocket([ + StringExpectation( + "", + ''' + + + + PLAIN + HT-SHA-256-NONE + + + PLAIN + HT-SHA-256-NONE + HT-SHA-256-ENDP + + + + HT-SHA-256-NONE + HT-SHA-256-ENDP + + + + + + + ''', + ), + StanzaExpectation( + "moxxmppPapaTutuWawa's awesome deviceWXZzciBwYmFmdmZnZiBqdmd1IGp2eXFhcmZm", + ''' + + + + ''', + ), + StanzaExpectation( + "moxxmppPapaTutuWawa's awesome deviceAHBvbHlub21kaXZpc2lvbgBhYWFh", + ''' + + polynomdivision@test.server + + + ''', + ), + StanzaExpectation( + '', + 'polynomdivision@test.server/MU29eEZn', + 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(), false); + expect(conn.resource, 'MU29eEZn'); + expect(fakeSocket.getState(), 4); + + final token = conn + .getNegotiatorById(saslFASTNegotiator)! + .fastToken; + expect(token != null, true); + expect(token!.token, 'ed00e36cb42449a365a306a413f51ffd5ea8'); + }); +} diff --git a/packages/moxxmpp/test/xmpp_test.dart b/packages/moxxmpp/test/xmpp_test.dart index a20b982..b979105 100644 --- a/packages/moxxmpp/test/xmpp_test.dart +++ b/packages/moxxmpp/test/xmpp_test.dart @@ -35,7 +35,6 @@ Future 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( - "", + "", ''' ', ), StringExpectation( - "", + "", ''' ", + "", ''' ", + "", ''' false, getFullJID: () => JID.fromString('some.user@example.server/aaaaa'), getSocket: () => StubTCPSocket([]), getConnection: () => XmppConnection( @@ -380,7 +379,7 @@ void main() { final fakeSocket = StubTCPSocket( [ StringExpectation( - "", + "", ''' _runTest(String domain) async { TestingReconnectionPolicy(), AlwaysConnectedConnectivityManager(), socket, - )..registerFeatureNegotiators([ - StartTlsNegotiator(), - ]); + ); + await connection.registerFeatureNegotiators([ + StartTlsNegotiator(), + ]); await connection.registerManagers([ DiscoManager([]), RosterManager(TestingRosterStateManager('', [])), diff --git a/packages/moxxmpp_socket_tcp/integration_test/failure_reconnection_test.dart b/packages/moxxmpp_socket_tcp/integration_test/failure_reconnection_test.dart index e701de1..5ede85d 100644 --- a/packages/moxxmpp_socket_tcp/integration_test/failure_reconnection_test.dart +++ b/packages/moxxmpp_socket_tcp/integration_test/failure_reconnection_test.dart @@ -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('', [])), diff --git a/packages/moxxmpp_socket_tcp/lib/src/socket.dart b/packages/moxxmpp_socket_tcp/lib/src/socket.dart index 49af453..7922bdc 100644 --- a/packages/moxxmpp_socket_tcp/lib/src/socket.dart +++ b/packages/moxxmpp_socket_tcp/lib/src/socket.dart @@ -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);