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..edaa687 --- /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. 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/integration_tests/sasl2_test.dart b/packages/moxxmpp/integration_tests/sasl2_test.dart new file mode 100644 index 0000000..4a11042 --- /dev/null +++ b/packages/moxxmpp/integration_tests/sasl2_test.dart @@ -0,0 +1,43 @@ +import 'package:moxxmpp/moxxmpp.dart'; +import 'package:moxxmpp_socket_tcp/moxxmpp_socket_tcp.dart'; +import 'package:test/test.dart'; + +void main() async { + final conn = XmppConnection( + TestingReconnectionPolicy(), + AlwaysConnectedConnectivityManager(), + TCPSocketWrapper(), + )..setConnectionSettings( + ConnectionSettings( + jid: JID.fromString('testuser@localhost'), + password: 'abc123', + useDirectTLS: false, + ), + ); + final csi = CSIManager(); + await csi.setInactive(sendNonza: false); + await conn.registerManagers([ + RosterManager(TestingRosterStateManager('', [])), + DiscoManager([]), + ]); + await conn.registerFeatureNegotiators([ + SaslPlainNegotiator(), + ResourceBindingNegotiator(), + FASTSaslNegotiator(), + 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); +} diff --git a/packages/moxxmpp/lib/src/connection.dart b/packages/moxxmpp/lib/src/connection.dart index 1c5d9af..903a9c5 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, @@ -1037,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(), ); @@ -1156,13 +1158,16 @@ class XmppConnection { } final smManager = getStreamManagementManager(); - String? host; - int? port; + String? host = _connectionSettings.host; + int? 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/negotiators/sasl/plain.dart b/packages/moxxmpp/lib/src/negotiators/sasl/plain.dart index b601f75..9582519 100644 --- a/packages/moxxmpp/lib/src/negotiators/sasl/plain.dart +++ b/packages/moxxmpp/lib/src/negotiators/sasl/plain.dart @@ -8,6 +8,7 @@ import 'package:moxxmpp/src/negotiators/sasl/nonza.dart'; import 'package:moxxmpp/src/negotiators/sasl2.dart'; import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/types/result.dart'; +import 'package:saslprep/saslprep.dart'; class SaslPlainAuthNonza extends SaslAuthNonza { SaslPlainAuthNonza(String data) @@ -86,8 +87,9 @@ class SaslPlainNegotiator extends Sasl2AuthenticationNegotiator { @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${settings.password}'), + utf8.encode('\u0000${settings.jid.local}\u0000${prep}'), ); } diff --git a/packages/moxxmpp/lib/src/negotiators/sasl2.dart b/packages/moxxmpp/lib/src/negotiators/sasl2.dart index bbf481d..d763a4f 100644 --- a/packages/moxxmpp/lib/src/negotiators/sasl2.dart +++ b/packages/moxxmpp/lib/src/negotiators/sasl2.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'package:moxxmpp/src/jid.dart'; import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/negotiators/namespaces.dart'; @@ -245,7 +246,6 @@ class Sasl2Negotiator extends XmppFeatureNegotiatorBase { attributes.sendNonza(authenticate); return const Result(NegotiatorState.ready); case Sasl2State.authenticateSent: - // TODO(PapaTutuWawa): Handle failure if (nonza.tag == 'success') { // Tell the dependent negotiators about the result final negotiators = _featureNegotiators 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_socket_tcp/lib/src/socket.dart b/packages/moxxmpp_socket_tcp/lib/src/socket.dart index 883f16a..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; } }