From 2e60e9841e75b53d08484ddbd2ad21f63c458da2 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Fri, 31 Mar 2023 19:02:57 +0200 Subject: [PATCH 01/29] feat(xep): Begin work on SASL2 --- packages/moxxmpp/lib/moxxmpp.dart | 1 + packages/moxxmpp/lib/src/connection.dart | 9 +- packages/moxxmpp/lib/src/namespaces.dart | 3 + .../lib/src/negotiators/namespaces.dart | 1 + .../lib/src/negotiators/negotiator.dart | 7 + .../lib/src/negotiators/sasl/plain.dart | 8 + .../moxxmpp/lib/src/negotiators/sasl2.dart | 161 ++++++++++++++++++ packages/moxxmpp/test/xeps/xep_0030_test.dart | 2 +- packages/moxxmpp/test/xeps/xep_0060_test.dart | 11 +- packages/moxxmpp/test/xeps/xep_0198_test.dart | 10 +- packages/moxxmpp/test/xeps/xep_0388_test.dart | 69 ++++++++ packages/moxxmpp/test/xmpp_test.dart | 29 ++-- 12 files changed, 282 insertions(+), 29 deletions(-) create mode 100644 packages/moxxmpp/lib/src/negotiators/sasl2.dart create mode 100644 packages/moxxmpp/test/xeps/xep_0388_test.dart diff --git a/packages/moxxmpp/lib/moxxmpp.dart b/packages/moxxmpp/lib/moxxmpp.dart index e8583ad..2f74fca 100644 --- a/packages/moxxmpp/lib/moxxmpp.dart +++ b/packages/moxxmpp/lib/moxxmpp.dart @@ -23,6 +23,7 @@ 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/sasl2.dart'; export 'package:moxxmpp/src/negotiators/starttls.dart'; export 'package:moxxmpp/src/ping.dart'; export 'package:moxxmpp/src/presence.dart'; diff --git a/packages/moxxmpp/lib/src/connection.dart b/packages/moxxmpp/lib/src/connection.dart index 1f1f673..6dd47b4 100644 --- a/packages/moxxmpp/lib/src/connection.dart +++ b/packages/moxxmpp/lib/src/connection.dart @@ -254,7 +254,8 @@ class XmppConnection { } /// 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( @@ -273,6 +274,10 @@ class XmppConnection { } _log.finest('Negotiators registered'); + + for (final negotiator in _featureNegotiators.values) { + await negotiator.postRegisterCallback(); + } } /// Reset all registered negotiators. @@ -399,7 +404,7 @@ class XmppConnection { // Close the socket _socket.close(); - + if (!error.isRecoverable()) { // We cannot recover this error _log.severe( diff --git a/packages/moxxmpp/lib/src/namespaces.dart b/packages/moxxmpp/lib/src/namespaces.dart index d49330b..f7a11eb 100644 --- a/packages/moxxmpp/lib/src/namespaces.dart +++ b/packages/moxxmpp/lib/src/namespaces.dart @@ -116,6 +116,9 @@ const omemoBundlesXmlns = 'urn:xmpp:omemo:2:bundles'; // XEP-0385 const simsXmlns = 'urn:xmpp:sims:1'; +// XEP-0388 +const sasl2Xmlns = 'urn:xmpp:sasl:2'; + // XEP-0420 const sceXmlns = 'urn:xmpp:sce:1'; diff --git a/packages/moxxmpp/lib/src/negotiators/namespaces.dart b/packages/moxxmpp/lib/src/negotiators/namespaces.dart index 00595b7..1605329 100644 --- a/packages/moxxmpp/lib/src/negotiators/namespaces.dart +++ b/packages/moxxmpp/lib/src/negotiators/namespaces.dart @@ -7,3 +7,4 @@ 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'; diff --git a/packages/moxxmpp/lib/src/negotiators/negotiator.dart b/packages/moxxmpp/lib/src/negotiators/negotiator.dart index 3594403..a251192 100644 --- a/packages/moxxmpp/lib/src/negotiators/negotiator.dart +++ b/packages/moxxmpp/lib/src/negotiators/negotiator.dart @@ -1,3 +1,4 @@ +import 'package:meta/meta.dart'; import 'package:moxlib/moxlib.dart'; import 'package:moxxmpp/src/errors.dart'; import 'package:moxxmpp/src/events.dart'; @@ -120,5 +121,11 @@ abstract class XmppFeatureNegotiatorBase { state = NegotiatorState.ready; } + @protected NegotiatorAttributes get attributes => _attributes; + + /// Run after all negotiators are registered. Useful for registering callbacks against + /// other negotiators. + @visibleForOverriding + Future postRegisterCallback() async {} } diff --git a/packages/moxxmpp/lib/src/negotiators/sasl/plain.dart b/packages/moxxmpp/lib/src/negotiators/sasl/plain.dart index 8ee4512..eb78979 100644 --- a/packages/moxxmpp/lib/src/negotiators/sasl/plain.dart +++ b/packages/moxxmpp/lib/src/negotiators/sasl/plain.dart @@ -6,6 +6,7 @@ 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/negotiators/sasl2.dart'; import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/types/result.dart'; @@ -76,4 +77,11 @@ class SaslPlainNegotiator extends SaslNegotiator { super.reset(); } + + @override + Future postRegisterCallback() async { + attributes + .getNegotiatorById(sasl2Negotiator) + ?.registerSaslNegotiator(this); + } } diff --git a/packages/moxxmpp/lib/src/negotiators/sasl2.dart b/packages/moxxmpp/lib/src/negotiators/sasl2.dart new file mode 100644 index 0000000..694c7f6 --- /dev/null +++ b/packages/moxxmpp/lib/src/negotiators/sasl2.dart @@ -0,0 +1,161 @@ +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/negotiator.dart'; +import 'package:moxxmpp/src/stringxml.dart'; +import 'package:moxxmpp/src/types/result.dart'; + +typedef Sasl2FeaturesReceivedCallback = Future> Function(XMLNode); + +class NoSASLMechanismSelectedError extends NegotiatorError { + @override + bool isRecoverable() => false; +} + +/// 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, + ), + ], + ); + } +} + +enum Sasl2State { + // No request has been sent yet. + idle, +} + +/// 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 _featureCallbacks = + 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. + SaslNegotiator? _currentSaslNegotiator; + + void registerSaslNegotiator(SaslNegotiator negotiator) { + _saslNegotiators + ..add(negotiator) + ..sort((a, b) => b.priority.compareTo(a.priority)); + } + + void registerFeaturesCallback(Sasl2FeaturesReceivedCallback callback) { + _featureCallbacks.add(callback); + } + + @override + Future> negotiate( + XMLNode nonza) async { + switch (_sasl2State) { + case Sasl2State.idle: + final sasl2 = nonza.firstTag('authentication', xmlns: sasl2Xmlns)!; + final mechanisms = XMLNode.xmlns( + tag: 'mechanisms', + xmlns: saslXmlns, + children: sasl2.children.where((c) => c.tag == 'mechanism').toList(), + ); + for (final negotiator in _saslNegotiators) { + if (negotiator.matchesFeature([mechanisms])) { + _currentSaslNegotiator = negotiator; + break; + } + } + + // We must have a SASL negotiator by now + if (_currentSaslNegotiator == null) { + return Result(NoSASLMechanismSelectedError()); + } + + // Collect additional data by interested negotiators + final children = List.empty(growable: true); + for (final callback in _featureCallbacks) { + children.addAll( + await callback(sasl2), + ); + } + + // Build the authenticate nonza + final authenticate = XMLNode.xmlns( + tag: 'authenticate', + xmlns: sasl2Xmlns, + attributes: { + 'mechanism': _currentSaslNegotiator!.mechanismName, + }, + children: [ + if (userAgent != null) userAgent!.toXml(), + + // TODO: Get the initial response + XMLNode( + tag: 'initial-response', + ), + ...children, + ], + ); + attributes.sendNonza(authenticate); + return const Result(NegotiatorState.ready); + } + + return const Result(NegotiatorState.ready); + } + + @override + void reset() { + _currentSaslNegotiator = null; + _sasl2State = Sasl2State.idle; + + super.reset(); + } +} diff --git a/packages/moxxmpp/test/xeps/xep_0030_test.dart b/packages/moxxmpp/test/xeps/xep_0030_test.dart index 6f15953..4f3b903 100644 --- a/packages/moxxmpp/test/xeps/xep_0030_test.dart +++ b/packages/moxxmpp/test/xeps/xep_0030_test.dart @@ -84,7 +84,7 @@ void main() { DiscoManager([]), EntityCapabilitiesManager('http://moxxmpp.example'), ]); - conn.registerFeatureNegotiators([ + await conn.registerFeatureNegotiators([ SaslPlainNegotiator(), SaslScramNegotiator(10, '', '', ScramHashType.sha512), ResourceBindingNegotiator(), diff --git a/packages/moxxmpp/test/xeps/xep_0060_test.dart b/packages/moxxmpp/test/xeps/xep_0060_test.dart index c52a328..a8c84a1 100644 --- a/packages/moxxmpp/test/xeps/xep_0060_test.dart +++ b/packages/moxxmpp/test/xeps/xep_0060_test.dart @@ -169,12 +169,11 @@ void main() { MessageManager(), RosterManager(TestingRosterStateManager(null, [])), ]); - connection - ..registerFeatureNegotiators([ - SaslPlainNegotiator(), - ResourceBindingNegotiator(), - ]) - ..setConnectionSettings(TestingManagerHolder.settings); + await connection.registerFeatureNegotiators([ + SaslPlainNegotiator(), + ResourceBindingNegotiator(), + ]); + connection.setConnectionSettings(TestingManagerHolder.settings); await connection.connect( waitUntilLogin: true, ); diff --git a/packages/moxxmpp/test/xeps/xep_0198_test.dart b/packages/moxxmpp/test/xeps/xep_0198_test.dart index 66e9349..770fece 100644 --- a/packages/moxxmpp/test/xeps/xep_0198_test.dart +++ b/packages/moxxmpp/test/xeps/xep_0198_test.dart @@ -298,7 +298,7 @@ void main() { CarbonsManager()..forceEnable(), EntityCapabilitiesManager('http://moxxmpp.example'), ]); - conn.registerFeatureNegotiators([ + await conn.registerFeatureNegotiators([ SaslPlainNegotiator(), ResourceBindingNegotiator(), StreamManagementNegotiator(), @@ -423,7 +423,7 @@ void main() { CarbonsManager()..forceEnable(), //EntityCapabilitiesManager('http://moxxmpp.example'), ]); - conn.registerFeatureNegotiators([ + await conn.registerFeatureNegotiators([ SaslPlainNegotiator(), ResourceBindingNegotiator(), StreamManagementNegotiator(), @@ -580,7 +580,7 @@ void main() { DiscoManager([]), StreamManagementManager(), ]); - conn.registerFeatureNegotiators([ + await conn.registerFeatureNegotiators([ SaslPlainNegotiator(), ResourceBindingNegotiator(), StreamManagementNegotiator(), @@ -674,7 +674,7 @@ void main() { DiscoManager([]), StreamManagementManager(), ]); - conn.registerFeatureNegotiators([ + await conn.registerFeatureNegotiators([ SaslPlainNegotiator(), ResourceBindingNegotiator(), StreamManagementNegotiator(), @@ -765,7 +765,7 @@ void main() { DiscoManager([]), StreamManagementManager(), ]); - conn.registerFeatureNegotiators([ + await conn.registerFeatureNegotiators([ SaslPlainNegotiator(), ResourceBindingNegotiator(), StreamManagementNegotiator(), 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..e381aac --- /dev/null +++ b/packages/moxxmpp/test/xeps/xep_0388_test.dart @@ -0,0 +1,69 @@ +import 'package:moxxmpp/moxxmpp.dart'; +import 'package:test/test.dart'; +import '../helpers/logging.dart'; +import '../helpers/xmpp.dart'; + +void main() { + initLogger(); + + test('Test simple SASL2 negotiation', () async { + final fakeSocket = StubTCPSocket([ + StringExpectation( + "", + ''' + + + + PLAIN + + + PLAIN + + ''', + ), + StanzaExpectation( + "moxxmppPapaTutuWawa's awesome deviceAHBvbHlub21kaXZpc2lvbgBhYWFh", + '', + ), + ]); + 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); + }); +} diff --git a/packages/moxxmpp/test/xmpp_test.dart b/packages/moxxmpp/test/xmpp_test.dart index a20b982..829f101 100644 --- a/packages/moxxmpp/test/xmpp_test.dart +++ b/packages/moxxmpp/test/xmpp_test.dart @@ -140,7 +140,7 @@ void main() { StreamManagementManager(), EntityCapabilitiesManager('http://moxxmpp.example'), ]); - conn.registerFeatureNegotiators([ + await conn.registerFeatureNegotiators([ SaslPlainNegotiator(), SaslScramNegotiator(10, '', '', ScramHashType.sha512), ResourceBindingNegotiator(), @@ -195,7 +195,7 @@ void main() { DiscoManager([]), EntityCapabilitiesManager('http://moxxmpp.example'), ]); - conn.registerFeatureNegotiators([ + await conn.registerFeatureNegotiators([ SaslPlainNegotiator(), ]); @@ -254,7 +254,7 @@ void main() { DiscoManager([]), EntityCapabilitiesManager('http://moxxmpp.example'), ]); - conn.registerFeatureNegotiators([SaslPlainNegotiator()]); + await conn.registerFeatureNegotiators([SaslPlainNegotiator()]); conn.asBroadcastStream().listen((event) { if (event is AuthenticationFailedEvent && @@ -407,18 +407,17 @@ void main() { RosterManager(TestingRosterStateManager('', [])), DiscoManager([]), ]); - conn - ..registerFeatureNegotiators([ - // SaslPlainNegotiator(), - ResourceBindingNegotiator(), - ]) - ..setConnectionSettings( - ConnectionSettings( - jid: JID.fromString('testuser@example.org'), - password: 'abc123', - useDirectTLS: false, - ), - ); + await conn.registerFeatureNegotiators([ + // SaslPlainNegotiator(), + ResourceBindingNegotiator(), + ]); + conn.setConnectionSettings( + ConnectionSettings( + jid: JID.fromString('testuser@example.org'), + password: 'abc123', + useDirectTLS: false, + ), + ); final result = await conn.connect( waitUntilLogin: true, From 7ab3f4f0d9fe3a858a254b3e75343c3d1625bb26 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Fri, 31 Mar 2023 20:53:06 +0200 Subject: [PATCH 02/29] feat(xep): Implement negotiating PLAIN via SASL2 --- packages/moxxmpp/lib/src/connection.dart | 25 ++++--- .../lib/src/negotiators/negotiator.dart | 9 +++ .../lib/src/negotiators/sasl/plain.dart | 30 ++++++-- .../lib/src/negotiators/sasl/scram.dart | 2 +- .../moxxmpp/lib/src/negotiators/sasl2.dart | 69 +++++++++++++++---- packages/moxxmpp/test/sasl/scram_test.dart | 10 +++ packages/moxxmpp/test/xeps/xep_0198_test.dart | 10 +-- packages/moxxmpp/test/xeps/xep_0388_test.dart | 17 ++++- 8 files changed, 138 insertions(+), 34 deletions(-) diff --git a/packages/moxxmpp/lib/src/connection.dart b/packages/moxxmpp/lib/src/connection.dart index 6dd47b4..9ed906b 100644 --- a/packages/moxxmpp/lib/src/connection.dart +++ b/packages/moxxmpp/lib/src/connection.dart @@ -253,6 +253,19 @@ 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. Future registerFeatureNegotiators( List negotiators) async { @@ -268,6 +281,8 @@ class XmppConnection { () => _connectionSettings.jid.withResource(_resource), () => _socket, () => _isAuthenticated, + _setAuthenticated, + _removeNegotiatingFeature, ), ); _featureNegotiators[negotiator.id] = negotiator; @@ -900,10 +915,7 @@ class XmppConnection { _streamFeatures.clear(); _sendStreamHeader(); } else { - _streamFeatures.removeWhere((node) { - return node.attributes['xmlns'] == - _currentNegotiator!.negotiatingXmlns; - }); + _removeNegotiatingFeature(_currentNegotiator!.negotiatingXmlns); _currentNegotiator = null; if (_isMandatoryNegotiationDone(_streamFeatures) && @@ -1024,11 +1036,6 @@ class XmppConnection { _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) { diff --git a/packages/moxxmpp/lib/src/negotiators/negotiator.dart b/packages/moxxmpp/lib/src/negotiators/negotiator.dart index a251192..50cc220 100644 --- a/packages/moxxmpp/lib/src/negotiators/negotiator.dart +++ b/packages/moxxmpp/lib/src/negotiators/negotiator.dart @@ -35,6 +35,8 @@ class NegotiatorAttributes { this.getFullJID, this.getSocket, this.isAuthenticated, + this.setAuthenticated, + this.removeNegotiatingFeature, ); /// Sends the nonza nonza and optionally redacts it in logs if redact is not null. @@ -61,6 +63,13 @@ class NegotiatorAttributes { /// Returns true if the stream is authenticated. Returns false if not. final bool Function() isAuthenticated; + + /// 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 { diff --git a/packages/moxxmpp/lib/src/negotiators/sasl/plain.dart b/packages/moxxmpp/lib/src/negotiators/sasl/plain.dart index eb78979..a74d092 100644 --- a/packages/moxxmpp/lib/src/negotiators/sasl/plain.dart +++ b/packages/moxxmpp/lib/src/negotiators/sasl/plain.dart @@ -11,14 +11,14 @@ import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/types/result.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'), @@ -48,10 +48,10 @@ 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), + redact: SaslPlainAuthNonza('******').toXml(), ); _authSent = true; return const Result(NegotiatorState.ready); @@ -59,6 +59,7 @@ class SaslPlainNegotiator extends SaslNegotiator { final tag = nonza.tag; if (tag == 'success') { await attributes.sendEvent(AuthenticationSuccessEvent()); + attributes.setAuthenticated(); return const Result(NegotiatorState.done); } else { // We assume it's a @@ -84,4 +85,21 @@ class SaslPlainNegotiator extends SaslNegotiator { .getNegotiatorById(sasl2Negotiator) ?.registerSaslNegotiator(this); } + + @override + Future getRawStep(String input) async { + final settings = attributes.getConnectionSettings(); + return base64.encode( + utf8.encode('\u0000${settings.jid.local}\u0000${settings.password}')); + } + + @override + Future onSasl2Success(XMLNode response) async { + state = NegotiatorState.done; + } + + @override + Future> onSasl2FeaturesReceived(XMLNode sasl2Features) async { + return []; + } } diff --git a/packages/moxxmpp/lib/src/negotiators/sasl/scram.dart b/packages/moxxmpp/lib/src/negotiators/sasl/scram.dart index 78d64c8..d1be06f 100644 --- a/packages/moxxmpp/lib/src/negotiators/sasl/scram.dart +++ b/packages/moxxmpp/lib/src/negotiators/sasl/scram.dart @@ -299,7 +299,7 @@ class SaslScramNegotiator extends SaslNegotiator { ); } - await attributes.sendEvent(AuthenticationSuccessEvent()); + attributes.setAuthenticated(); return const Result(NegotiatorState.done); case ScramState.error: return Result( diff --git a/packages/moxxmpp/lib/src/negotiators/sasl2.dart b/packages/moxxmpp/lib/src/negotiators/sasl2.dart index 694c7f6..ef981fd 100644 --- a/packages/moxxmpp/lib/src/negotiators/sasl2.dart +++ b/packages/moxxmpp/lib/src/negotiators/sasl2.dart @@ -1,3 +1,4 @@ +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'; @@ -7,6 +8,34 @@ import 'package:moxxmpp/src/types/result.dart'; typedef Sasl2FeaturesReceivedCallback = Future> Function(XMLNode); +abstract class Sasl2FeatureNegotiator extends XmppFeatureNegotiatorBase { + Sasl2FeatureNegotiator( + int priority, + bool sendStreamHeaderWhenDone, + String negotiatingXmlns, + String id, + ) : super(priority, sendStreamHeaderWhenDone, negotiatingXmlns, 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. + Future> onSasl2FeaturesReceived(XMLNode sasl2Features); + + /// Called by the SASL2 negotiator when the SASL2 negotiations are done. [response] + /// is the entire response nonza. + Future onSasl2Success(XMLNode response); +} + +abstract class Sasl2AuthenticationNegotiator extends SaslNegotiator + implements Sasl2FeatureNegotiator { + Sasl2AuthenticationNegotiator(int priority, String id, String mechanismName) + : super(priority, id, mechanismName); + + /// Perform a SASL step with [input] as the already parsed input data. Returns + /// the base64-encoded response data. + Future getRawStep(String input); +} + class NoSASLMechanismSelectedError extends NegotiatorError { @override bool isRecoverable() => false; @@ -58,6 +87,8 @@ class UserAgent { 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 @@ -72,28 +103,29 @@ class Sasl2Negotiator extends XmppFeatureNegotiatorBase { /// List of callbacks that are registered against us. Will be called once we get /// SASL2 features. - final List _featureCallbacks = - List.empty(growable: true); + 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); + 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. - SaslNegotiator? _currentSaslNegotiator; + Sasl2AuthenticationNegotiator? _currentSaslNegotiator; - void registerSaslNegotiator(SaslNegotiator negotiator) { + void registerSaslNegotiator(Sasl2AuthenticationNegotiator negotiator) { + _featureNegotiators.add(negotiator); _saslNegotiators ..add(negotiator) ..sort((a, b) => b.priority.compareTo(a.priority)); } - void registerFeaturesCallback(Sasl2FeaturesReceivedCallback callback) { - _featureCallbacks.add(callback); + void registerNegotiator(Sasl2FeatureNegotiator negotiator) { + _featureNegotiators.add(negotiator); } @override @@ -121,9 +153,9 @@ class Sasl2Negotiator extends XmppFeatureNegotiatorBase { // Collect additional data by interested negotiators final children = List.empty(growable: true); - for (final callback in _featureCallbacks) { + for (final negotiator in _featureNegotiators) { children.addAll( - await callback(sasl2), + await negotiator.onSasl2FeaturesReceived(sasl2), ); } @@ -136,16 +168,29 @@ class Sasl2Negotiator extends XmppFeatureNegotiatorBase { }, children: [ if (userAgent != null) userAgent!.toXml(), - - // TODO: Get the initial response XMLNode( tag: 'initial-response', + text: await _currentSaslNegotiator!.getRawStep(''), ), ...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 + for (final negotiator in _featureNegotiators) { + await negotiator.onSasl2Success(nonza); + } + + // We're done + attributes.setAuthenticated(); + attributes.removeNegotiatingFeature(saslXmlns); + return const Result(NegotiatorState.done); + } } return const Result(NegotiatorState.ready); diff --git a/packages/moxxmpp/test/sasl/scram_test.dart b/packages/moxxmpp/test/sasl/scram_test.dart index 237462f..d3552f7 100644 --- a/packages/moxxmpp/test/sasl/scram_test.dart +++ b/packages/moxxmpp/test/sasl/scram_test.dart @@ -57,6 +57,8 @@ void main() { () => JID.fromString('user@server'), () => fakeSocket, () => false, + () {}, + (_) {}, ), ); @@ -150,6 +152,8 @@ void main() { () => JID.fromString('user@server'), () => fakeSocket, () => false, + () {}, + (_) {}, ), ); @@ -198,6 +202,8 @@ void main() { () => JID.fromString('user@server'), () => fakeSocket, () => false, + () {}, + (_) {}, ), ); @@ -236,6 +242,8 @@ void main() { () => JID.fromString('user@server'), () => fakeSocket, () => false, + () {}, + (_) {}, ), ); @@ -277,6 +285,8 @@ void main() { () => JID.fromString('user@server'), () => fakeSocket, () => false, + () {}, + (_) {}, ), ); diff --git a/packages/moxxmpp/test/xeps/xep_0198_test.dart b/packages/moxxmpp/test/xeps/xep_0198_test.dart index 770fece..bce1f96 100644 --- a/packages/moxxmpp/test/xeps/xep_0198_test.dart +++ b/packages/moxxmpp/test/xeps/xep_0198_test.dart @@ -391,10 +391,10 @@ void main() { "", '', ), - // StringExpectation( - // "chat", - // '', - // ), + StringExpectation( + "chat", + '', + ), StanzaExpectation( "", "", @@ -450,7 +450,7 @@ void main() { addFrom: StanzaFromType.none, ); - expect(sm.state.s2c, /*2*/ 1); + expect(sm.state.s2c, 2); }); }); diff --git a/packages/moxxmpp/test/xeps/xep_0388_test.dart b/packages/moxxmpp/test/xeps/xep_0388_test.dart index e381aac..8cd96e6 100644 --- a/packages/moxxmpp/test/xeps/xep_0388_test.dart +++ b/packages/moxxmpp/test/xeps/xep_0388_test.dart @@ -24,11 +24,26 @@ void main() { PLAIN + + + ''', ), StanzaExpectation( "moxxmppPapaTutuWawa's awesome deviceAHBvbHlub21kaXZpc2lvbgBhYWFh", - '', + ''' + + polynomdivision@test.server + + ''', + ), + StanzaExpectation( + "", + ''' +'polynomdivision@test.server/MU29eEZn', + ''', + adjustId: true, + ignoreId: true, ), ]); final conn = XmppConnection( From 478b5b87708fe5ef3e1e09082f11f761c613fdc9 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Fri, 31 Mar 2023 21:09:16 +0200 Subject: [PATCH 03/29] feat(core): Make SCRAM SASL2 aware --- packages/moxxmpp/lib/src/connection.dart | 3 +- .../lib/src/negotiators/sasl/plain.dart | 5 +- .../lib/src/negotiators/sasl/scram.dart | 62 +++++++++++++------ .../moxxmpp/lib/src/negotiators/sasl2.dart | 27 ++++---- 4 files changed, 61 insertions(+), 36 deletions(-) diff --git a/packages/moxxmpp/lib/src/connection.dart b/packages/moxxmpp/lib/src/connection.dart index 9ed906b..f2ff348 100644 --- a/packages/moxxmpp/lib/src/connection.dart +++ b/packages/moxxmpp/lib/src/connection.dart @@ -268,7 +268,8 @@ class XmppConnection { /// Register a list of negotiator with the connection. Future registerFeatureNegotiators( - List negotiators) async { + List negotiators, + ) async { for (final negotiator in negotiators) { _log.finest('Registering ${negotiator.id}'); negotiator.register( diff --git a/packages/moxxmpp/lib/src/negotiators/sasl/plain.dart b/packages/moxxmpp/lib/src/negotiators/sasl/plain.dart index a74d092..17a54de 100644 --- a/packages/moxxmpp/lib/src/negotiators/sasl/plain.dart +++ b/packages/moxxmpp/lib/src/negotiators/sasl/plain.dart @@ -4,7 +4,6 @@ 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/negotiators/sasl2.dart'; import 'package:moxxmpp/src/stringxml.dart'; @@ -58,7 +57,6 @@ class SaslPlainNegotiator extends Sasl2AuthenticationNegotiator { } else { final tag = nonza.tag; if (tag == 'success') { - await attributes.sendEvent(AuthenticationSuccessEvent()); attributes.setAuthenticated(); return const Result(NegotiatorState.done); } else { @@ -90,7 +88,8 @@ class SaslPlainNegotiator extends Sasl2AuthenticationNegotiator { Future getRawStep(String input) async { final settings = attributes.getConnectionSettings(); return base64.encode( - utf8.encode('\u0000${settings.jid.local}\u0000${settings.password}')); + utf8.encode('\u0000${settings.jid.local}\u0000${settings.password}'), + ); } @override diff --git a/packages/moxxmpp/lib/src/negotiators/sasl/scram.dart b/packages/moxxmpp/lib/src/negotiators/sasl/scram.dart index d1be06f..1273366 100644 --- a/packages/moxxmpp/lib/src/negotiators/sasl/scram.dart +++ b/packages/moxxmpp/lib/src/negotiators/sasl/scram.dart @@ -8,8 +8,8 @@ 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/negotiators/sasl2.dart'; import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/types/result.dart'; import 'package:random_string/random_string.dart'; @@ -95,7 +95,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, @@ -236,20 +236,9 @@ class SaslScramNegotiator extends SaslNegotiator { ) 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(), @@ -266,12 +255,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), + SaslScramResponseNonza(body: await getRawStep(nonza.innerText())), redact: SaslScramResponseNonza(body: '******').toXml(), ); return const Result(NegotiatorState.ready); @@ -314,4 +299,43 @@ 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> onSasl2FeaturesReceived(XMLNode sasl2Features) async { + return []; + } + + @override + Future onSasl2Success(XMLNode response) async { + state = NegotiatorState.done; + } } diff --git a/packages/moxxmpp/lib/src/negotiators/sasl2.dart b/packages/moxxmpp/lib/src/negotiators/sasl2.dart index ef981fd..861b33c 100644 --- a/packages/moxxmpp/lib/src/negotiators/sasl2.dart +++ b/packages/moxxmpp/lib/src/negotiators/sasl2.dart @@ -1,4 +1,3 @@ -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'; @@ -6,15 +5,14 @@ import 'package:moxxmpp/src/negotiators/sasl/negotiator.dart'; import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/types/result.dart'; -typedef Sasl2FeaturesReceivedCallback = Future> Function(XMLNode); - +/// A special type of [XmppFeatureNegotiatorBase] that is aware of SASL2. abstract class Sasl2FeatureNegotiator extends XmppFeatureNegotiatorBase { Sasl2FeatureNegotiator( - int priority, - bool sendStreamHeaderWhenDone, - String negotiatingXmlns, - String id, - ) : super(priority, sendStreamHeaderWhenDone, negotiatingXmlns, id); + 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 @@ -26,10 +24,10 @@ abstract class Sasl2FeatureNegotiator extends XmppFeatureNegotiatorBase { Future onSasl2Success(XMLNode response); } +/// A special type of [SaslNegotiator] that is aware of SASL2. abstract class Sasl2AuthenticationNegotiator extends SaslNegotiator implements Sasl2FeatureNegotiator { - Sasl2AuthenticationNegotiator(int priority, String id, String mechanismName) - : super(priority, id, mechanismName); + Sasl2AuthenticationNegotiator(super.priority, super.id, super.mechanismName); /// Perform a SASL step with [input] as the already parsed input data. Returns /// the base64-encoded response data. @@ -59,8 +57,10 @@ class UserAgent { final String? device; XMLNode toXml() { - assert(id != null || software != null || device != null, - 'A completely empty user agent makes no sense'); + assert( + id != null || software != null || device != null, + 'A completely empty user agent makes no sense', + ); return XMLNode( tag: 'user-agent', attributes: id != null @@ -130,7 +130,8 @@ class Sasl2Negotiator extends XmppFeatureNegotiatorBase { @override Future> negotiate( - XMLNode nonza) async { + XMLNode nonza, + ) async { switch (_sasl2State) { case Sasl2State.idle: final sasl2 = nonza.firstTag('authentication', xmlns: sasl2Xmlns)!; From f86dbe6af88658415c0c3fa02cfc240d2fe7a0d9 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Fri, 31 Mar 2023 23:52:48 +0200 Subject: [PATCH 04/29] feat(core): Verify the server signature with SASL2 --- .../lib/src/negotiators/negotiator.dart | 3 +- .../lib/src/negotiators/sasl/plain.dart | 3 +- .../lib/src/negotiators/sasl/scram.dart | 44 ++++- .../moxxmpp/lib/src/negotiators/sasl2.dart | 16 +- packages/moxxmpp/test/xeps/xep_0388_test.dart | 166 ++++++++++++++++++ 5 files changed, 222 insertions(+), 10 deletions(-) diff --git a/packages/moxxmpp/lib/src/negotiators/negotiator.dart b/packages/moxxmpp/lib/src/negotiators/negotiator.dart index 50cc220..d60c4a6 100644 --- a/packages/moxxmpp/lib/src/negotiators/negotiator.dart +++ b/packages/moxxmpp/lib/src/negotiators/negotiator.dart @@ -134,7 +134,6 @@ abstract class XmppFeatureNegotiatorBase { NegotiatorAttributes get attributes => _attributes; /// Run after all negotiators are registered. Useful for registering callbacks against - /// other negotiators. - @visibleForOverriding + /// other negotiators. By default this function does nothing. Future postRegisterCallback() async {} } diff --git a/packages/moxxmpp/lib/src/negotiators/sasl/plain.dart b/packages/moxxmpp/lib/src/negotiators/sasl/plain.dart index 17a54de..d3e02c2 100644 --- a/packages/moxxmpp/lib/src/negotiators/sasl/plain.dart +++ b/packages/moxxmpp/lib/src/negotiators/sasl/plain.dart @@ -93,8 +93,9 @@ class SaslPlainNegotiator extends Sasl2AuthenticationNegotiator { } @override - Future onSasl2Success(XMLNode response) async { + Future> onSasl2Success(XMLNode response) async { state = NegotiatorState.done; + return const Result(true); } @override diff --git a/packages/moxxmpp/lib/src/negotiators/sasl/scram.dart b/packages/moxxmpp/lib/src/negotiators/sasl/scram.dart index 1273366..360ebf7 100644 --- a/packages/moxxmpp/lib/src/negotiators/sasl/scram.dart +++ b/packages/moxxmpp/lib/src/negotiators/sasl/scram.dart @@ -15,6 +15,18 @@ import 'package:moxxmpp/src/types/result.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 } @@ -230,6 +242,12 @@ class SaslScramNegotiator extends Sasl2AuthenticationNegotiator { return false; } + bool _checkSignature(String base64Signature) { + final signature = + parseKeyValue(utf8.decode(base64.decode(base64Signature))); + return signature['v']! == _serverSignature; + } + @override Future> negotiate( XMLNode nonza, @@ -271,10 +289,7 @@ class SaslScramNegotiator extends Sasl2AuthenticationNegotiator { ); } - // 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)); @@ -329,13 +344,32 @@ class SaslScramNegotiator extends Sasl2AuthenticationNegotiator { } } + @override + Future postRegisterCallback() async { + attributes + .getNegotiatorById(sasl2Negotiator) + ?.registerSaslNegotiator(this); + } + @override Future> onSasl2FeaturesReceived(XMLNode sasl2Features) async { return []; } @override - Future onSasl2Success(XMLNode response) async { + 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/sasl2.dart b/packages/moxxmpp/lib/src/negotiators/sasl2.dart index 861b33c..fdd2139 100644 --- a/packages/moxxmpp/lib/src/negotiators/sasl2.dart +++ b/packages/moxxmpp/lib/src/negotiators/sasl2.dart @@ -21,7 +21,7 @@ abstract class Sasl2FeatureNegotiator extends XmppFeatureNegotiatorBase { /// Called by the SASL2 negotiator when the SASL2 negotiations are done. [response] /// is the entire response nonza. - Future onSasl2Success(XMLNode response); + Future> onSasl2Success(XMLNode response); } /// A special type of [SaslNegotiator] that is aware of SASL2. @@ -184,13 +184,25 @@ class Sasl2Negotiator extends XmppFeatureNegotiatorBase { if (nonza.tag == 'success') { // Tell the dependent negotiators about the result for (final negotiator in _featureNegotiators) { - await negotiator.onSasl2Success(nonza); + final result = await negotiator.onSasl2Success(nonza); + if (!result.isType()) { + return Result(result.get()); + } } // We're done attributes.setAuthenticated(); attributes.removeNegotiatingFeature(saslXmlns); 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); } } diff --git a/packages/moxxmpp/test/xeps/xep_0388_test.dart b/packages/moxxmpp/test/xeps/xep_0388_test.dart index 8cd96e6..544343c 100644 --- a/packages/moxxmpp/test/xeps/xep_0388_test.dart +++ b/packages/moxxmpp/test/xeps/xep_0388_test.dart @@ -74,6 +74,92 @@ void main() { ), ]); + 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, @@ -81,4 +167,84 @@ void main() { ); 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); + }); } From 30482c86f0beb04a213c27249677c58e352b0991 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Sat, 1 Apr 2023 00:47:42 +0200 Subject: [PATCH 05/29] feat(xep): Implement inline negotiation --- .../moxxmpp/lib/src/negotiators/sasl2.dart | 53 ++++- packages/moxxmpp/lib/src/stringxml.dart | 2 + packages/moxxmpp/test/xeps/xep_0388_test.dart | 218 ++++++++++++++++++ 3 files changed, 265 insertions(+), 8 deletions(-) diff --git a/packages/moxxmpp/lib/src/negotiators/sasl2.dart b/packages/moxxmpp/lib/src/negotiators/sasl2.dart index fdd2139..3f3e239 100644 --- a/packages/moxxmpp/lib/src/negotiators/sasl2.dart +++ b/packages/moxxmpp/lib/src/negotiators/sasl2.dart @@ -1,3 +1,4 @@ +import 'package:collection/collection.dart'; import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/negotiators/namespaces.dart'; import 'package:moxxmpp/src/negotiators/negotiator.dart'; @@ -5,6 +6,18 @@ import 'package:moxxmpp/src/negotiators/sasl/negotiator.dart'; import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/types/result.dart'; +bool isInliningPossible(XMLNode nonza, String xmlns) { + assert(nonza.tag == 'authentication', 'Ensure we use the correct nonza'); + assert(nonza.xmlns == sasl2Xmlns, 'Ensure we use the correct nonza'); + final inline = nonza.firstTag('inline'); + if (inline == null) { + return false; + } + + return inline.children.firstWhereOrNull((child) => child.xmlns == xmlns) != + null; +} + /// A special type of [XmppFeatureNegotiatorBase] that is aware of SASL2. abstract class Sasl2FeatureNegotiator extends XmppFeatureNegotiatorBase { Sasl2FeatureNegotiator( @@ -117,6 +130,11 @@ class Sasl2Negotiator extends XmppFeatureNegotiatorBase { /// The SASL negotiator that will negotiate authentication. Sasl2AuthenticationNegotiator? _currentSaslNegotiator; + /// The SASL2 element we received with the stream features. + XMLNode? _sasl2Data; + + /// Register a SASL negotiator so that we can use that SASL implementation during + /// SASL2. void registerSaslNegotiator(Sasl2AuthenticationNegotiator negotiator) { _featureNegotiators.add(negotiator); _saslNegotiators @@ -124,21 +142,30 @@ class Sasl2Negotiator extends XmppFeatureNegotiatorBase { ..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: - final sasl2 = nonza.firstTag('authentication', xmlns: sasl2Xmlns)!; + _sasl2Data = nonza.firstTag('authentication', xmlns: sasl2Xmlns); final mechanisms = XMLNode.xmlns( tag: 'mechanisms', xmlns: saslXmlns, - children: sasl2.children.where((c) => c.tag == 'mechanism').toList(), + children: + _sasl2Data!.children.where((c) => c.tag == 'mechanism').toList(), ); for (final negotiator in _saslNegotiators) { if (negotiator.matchesFeature([mechanisms])) { @@ -155,9 +182,11 @@ class Sasl2Negotiator extends XmppFeatureNegotiatorBase { // Collect additional data by interested negotiators final children = List.empty(growable: true); for (final negotiator in _featureNegotiators) { - children.addAll( - await negotiator.onSasl2FeaturesReceived(sasl2), - ); + if (isInliningPossible(_sasl2Data!, negotiator.negotiatingXmlns)) { + children.addAll( + await negotiator.onSasl2FeaturesReceived(_sasl2Data!), + ); + } } // Build the authenticate nonza @@ -183,12 +212,19 @@ class Sasl2Negotiator extends XmppFeatureNegotiatorBase { case Sasl2State.authenticateSent: if (nonza.tag == 'success') { // Tell the dependent negotiators about the result + // TODO(Unknown): This can be written in a better way for (final negotiator in _featureNegotiators) { - final result = await negotiator.onSasl2Success(nonza); - if (!result.isType()) { - return Result(result.get()); + if (isInliningPossible(_sasl2Data!, negotiator.negotiatingXmlns)) { + final result = await negotiator.onSasl2Success(nonza); + if (!result.isType()) { + return Result(result.get()); + } } } + final result = await _currentSaslNegotiator!.onSasl2Success(nonza); + if (!result.isType()) { + return Result(result.get()); + } // We're done attributes.setAuthenticated(); @@ -213,6 +249,7 @@ class Sasl2Negotiator extends XmppFeatureNegotiatorBase { void reset() { _currentSaslNegotiator = null; _sasl2State = Sasl2State.idle; + _sasl2Data = null; super.reset(); } 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/test/xeps/xep_0388_test.dart b/packages/moxxmpp/test/xeps/xep_0388_test.dart index 544343c..b9af408 100644 --- a/packages/moxxmpp/test/xeps/xep_0388_test.dart +++ b/packages/moxxmpp/test/xeps/xep_0388_test.dart @@ -3,6 +3,53 @@ 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 + Future postRegisterCallback() async { + attributes + .getNegotiatorById(sasl2Negotiator)! + .registerNegotiator(this); + } + + @override + Future> onSasl2FeaturesReceived(XMLNode nonza) async { + if (!isInliningPossible(nonza, 'invalid:example:dont:use')) { + return []; + } + + 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(); @@ -247,4 +294,175 @@ void main() { 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, + ); + }); } From af8bc606d67f087756baa15a21735b41353885d7 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Sat, 1 Apr 2023 00:51:51 +0200 Subject: [PATCH 06/29] feat(xep): Guard against random data in the SASL2 result --- .../moxxmpp/lib/src/negotiators/sasl2.dart | 32 +++++++++++-------- packages/moxxmpp/test/xeps/xep_0388_test.dart | 4 --- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/moxxmpp/lib/src/negotiators/sasl2.dart b/packages/moxxmpp/lib/src/negotiators/sasl2.dart index 3f3e239..e27398b 100644 --- a/packages/moxxmpp/lib/src/negotiators/sasl2.dart +++ b/packages/moxxmpp/lib/src/negotiators/sasl2.dart @@ -6,18 +6,6 @@ import 'package:moxxmpp/src/negotiators/sasl/negotiator.dart'; import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/types/result.dart'; -bool isInliningPossible(XMLNode nonza, String xmlns) { - assert(nonza.tag == 'authentication', 'Ensure we use the correct nonza'); - assert(nonza.xmlns == sasl2Xmlns, 'Ensure we use the correct nonza'); - final inline = nonza.firstTag('inline'); - if (inline == null) { - return false; - } - - return inline.children.firstWhereOrNull((child) => child.xmlns == xmlns) != - null; -} - /// A special type of [XmppFeatureNegotiatorBase] that is aware of SASL2. abstract class Sasl2FeatureNegotiator extends XmppFeatureNegotiatorBase { Sasl2FeatureNegotiator( @@ -30,10 +18,14 @@ abstract class Sasl2FeatureNegotiator extends XmppFeatureNegotiatorBase { /// 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); } @@ -148,6 +140,18 @@ class Sasl2Negotiator extends XmppFeatureNegotiatorBase { _featureNegotiators.add(negotiator); } + /// Returns true, if an item with xmlns of [xmlns] is contained inside [_sasl2Data]'s + /// block. If not, returns false. + bool _isInliningPossible(String xmlns) { + final inline = _sasl2Data!.firstTag('inline'); + if (inline == null) { + return false; + } + + return inline.children.firstWhereOrNull((child) => child.xmlns == xmlns) != + null; + } + @override bool matchesFeature(List features) { // Only do SASL2 when the socket is secure @@ -182,7 +186,7 @@ class Sasl2Negotiator extends XmppFeatureNegotiatorBase { // Collect additional data by interested negotiators final children = List.empty(growable: true); for (final negotiator in _featureNegotiators) { - if (isInliningPossible(_sasl2Data!, negotiator.negotiatingXmlns)) { + if (_isInliningPossible(negotiator.negotiatingXmlns)) { children.addAll( await negotiator.onSasl2FeaturesReceived(_sasl2Data!), ); @@ -214,7 +218,7 @@ class Sasl2Negotiator extends XmppFeatureNegotiatorBase { // Tell the dependent negotiators about the result // TODO(Unknown): This can be written in a better way for (final negotiator in _featureNegotiators) { - if (isInliningPossible(_sasl2Data!, negotiator.negotiatingXmlns)) { + if (_isInliningPossible(negotiator.negotiatingXmlns)) { final result = await negotiator.onSasl2Success(nonza); if (!result.isType()) { return Result(result.get()); diff --git a/packages/moxxmpp/test/xeps/xep_0388_test.dart b/packages/moxxmpp/test/xeps/xep_0388_test.dart index b9af408..0941e8c 100644 --- a/packages/moxxmpp/test/xeps/xep_0388_test.dart +++ b/packages/moxxmpp/test/xeps/xep_0388_test.dart @@ -25,10 +25,6 @@ class ExampleNegotiator extends Sasl2FeatureNegotiator { @override Future> onSasl2FeaturesReceived(XMLNode nonza) async { - if (!isInliningPossible(nonza, 'invalid:example:dont:use')) { - return []; - } - return [ XMLNode.xmlns( tag: 'test-data-request', From f460e5ebe984fc693cd8b33a0bc69e4df890a937 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Sat, 1 Apr 2023 12:28:11 +0200 Subject: [PATCH 07/29] feat(core): Handle less resource binding in the core connection class --- packages/moxxmpp/CHANGELOG.md | 2 ++ packages/moxxmpp/lib/src/connection.dart | 27 ++++++------------- packages/moxxmpp/lib/src/events.dart | 6 +++-- .../moxxmpp/lib/src/managers/attributes.dart | 4 --- packages/moxxmpp/lib/src/managers/base.dart | 24 +++++++++++++++++ .../lib/src/negotiators/negotiator.dart | 5 ++++ .../lib/src/negotiators/resource_binding.dart | 10 +++---- packages/moxxmpp/test/helpers/manager.dart | 1 - packages/moxxmpp/test/sasl/scram_test.dart | 5 ++++ packages/moxxmpp/test/xeps/xep_0198_test.dart | 1 - packages/moxxmpp/test/xeps/xep_0280_test.dart | 1 - packages/moxxmpp/test/xeps/xep_0352_test.dart | 2 -- packages/moxxmpp/test/xmpp_test.dart | 3 +-- 13 files changed, 53 insertions(+), 38 deletions(-) diff --git a/packages/moxxmpp/CHANGELOG.md b/packages/moxxmpp/CHANGELOG.md index 4bf403d..34fa8c7 100644 --- a/packages/moxxmpp/CHANGELOG.md +++ b/packages/moxxmpp/CHANGELOG.md @@ -3,6 +3,8 @@ - **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 ## 0.1.6+1 diff --git a/packages/moxxmpp/lib/src/connection.dart b/packages/moxxmpp/lib/src/connection.dart index f2ff348..0cef5c0 100644 --- a/packages/moxxmpp/lib/src/connection.dart +++ b/packages/moxxmpp/lib/src/connection.dart @@ -133,9 +133,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,6 +147,7 @@ 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; @@ -201,8 +199,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 +217,6 @@ class XmppConnection { sendEvent: _sendEvent, getConnectionSettings: () => _connectionSettings, getManagerById: getManagerById, - isFeatureSupported: _serverFeatures.contains, getFullJID: () => _connectionSettings.jid.withResource(_resource), getSocket: () => _socket, getConnection: () => this, @@ -283,6 +278,7 @@ class XmppConnection { () => _socket, () => _isAuthenticated, _setAuthenticated, + _setResource, _removeNegotiatingFeature, ), ); @@ -678,9 +674,13 @@ class XmppConnection { } /// Sets the resource of the connection - void setResource(String resource) { + 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. @@ -1028,17 +1028,6 @@ 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(); - } - for (final manager in _xmppManagers.values) { await manager.onXmppEvent(event); } @@ -1151,7 +1140,7 @@ class XmppConnection { } if (lastResource != null) { - setResource(lastResource); + _setResource(lastResource, triggerEvent: false); } _enableReconnectOnSuccess = enableReconnectOnSuccess; 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/negotiators/negotiator.dart b/packages/moxxmpp/lib/src/negotiators/negotiator.dart index d60c4a6..9658e95 100644 --- a/packages/moxxmpp/lib/src/negotiators/negotiator.dart +++ b/packages/moxxmpp/lib/src/negotiators/negotiator.dart @@ -36,6 +36,7 @@ class NegotiatorAttributes { this.getSocket, this.isAuthenticated, this.setAuthenticated, + this.setResource, this.removeNegotiatingFeature, ); @@ -64,6 +65,10 @@ 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; diff --git a/packages/moxxmpp/lib/src/negotiators/resource_binding.dart b/packages/moxxmpp/lib/src/negotiators/resource_binding.dart index e60eee0..6c6ffd2 100644 --- a/packages/moxxmpp/lib/src/negotiators/resource_binding.dart +++ b/packages/moxxmpp/lib/src/negotiators/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'; @@ -65,11 +65,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/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/sasl/scram_test.dart b/packages/moxxmpp/test/sasl/scram_test.dart index d3552f7..44596de 100644 --- a/packages/moxxmpp/test/sasl/scram_test.dart +++ b/packages/moxxmpp/test/sasl/scram_test.dart @@ -58,6 +58,7 @@ void main() { () => fakeSocket, () => false, () {}, + (_, {bool triggerEvent = true}) {}, (_) {}, ), ); @@ -153,6 +154,7 @@ void main() { () => fakeSocket, () => false, () {}, + (_, {bool triggerEvent = true}) {}, (_) {}, ), ); @@ -203,6 +205,7 @@ void main() { () => fakeSocket, () => false, () {}, + (_, {bool triggerEvent = true}) {}, (_) {}, ), ); @@ -243,6 +246,7 @@ void main() { () => fakeSocket, () => false, () {}, + (_, {bool triggerEvent = true}) {}, (_) {}, ), ); @@ -286,6 +290,7 @@ void main() { () => fakeSocket, () => false, () {}, + (_, {bool triggerEvent = true}) {}, (_) {}, ), ); diff --git a/packages/moxxmpp/test/xeps/xep_0198_test.dart b/packages/moxxmpp/test/xeps/xep_0198_test.dart index bce1f96..fc985ef 100644 --- a/packages/moxxmpp/test/xeps/xep_0198_test.dart +++ b/packages/moxxmpp/test/xeps/xep_0198_test.dart @@ -63,7 +63,6 @@ XmppManagerAttributes mkAttributes(void Function(Stanza) callback) { password: 'password', useDirectTLS: true, ), - isFeatureSupported: (_) => false, getFullJID: () => JID.fromString('hallo@example.server/uwu'), getSocket: () => StubTCPSocket([]), getConnection: () => XmppConnection( diff --git a/packages/moxxmpp/test/xeps/xep_0280_test.dart b/packages/moxxmpp/test/xeps/xep_0280_test.dart index f22333e..282317e 100644 --- a/packages/moxxmpp/test/xeps/xep_0280_test.dart +++ b/packages/moxxmpp/test/xeps/xep_0280_test.dart @@ -27,7 +27,6 @@ void main() { password: 'password', useDirectTLS: true, ), - isFeatureSupported: (_) => false, getFullJID: () => JID.fromString('bob@xmpp.example/uwu'), getSocket: () => StubTCPSocket([]), getConnection: () => XmppConnection( diff --git a/packages/moxxmpp/test/xeps/xep_0352_test.dart b/packages/moxxmpp/test/xeps/xep_0352_test.dart index da89079..3981296 100644 --- a/packages/moxxmpp/test/xeps/xep_0352_test.dart +++ b/packages/moxxmpp/test/xeps/xep_0352_test.dart @@ -55,7 +55,6 @@ void main() { ), getManagerById: getManagerNullStub, getNegotiatorById: getUnsupportedCSINegotiator, - isFeatureSupported: (_) => false, getFullJID: () => JID.fromString('some.user@example.server/aaaaa'), getSocket: () => StubTCPSocket([]), getConnection: () => XmppConnection( @@ -99,7 +98,6 @@ void main() { ), getManagerById: getManagerNullStub, getNegotiatorById: getSupportedCSINegotiator, - isFeatureSupported: (_) => false, getFullJID: () => JID.fromString('some.user@example.server/aaaaa'), getSocket: () => StubTCPSocket([]), getConnection: () => XmppConnection( diff --git a/packages/moxxmpp/test/xmpp_test.dart b/packages/moxxmpp/test/xmpp_test.dart index 829f101..501932d 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( @@ -151,6 +150,7 @@ void main() { waitUntilLogin: true, ); expect(fakeSocket.getState(), /*6*/ 5); + expect(conn.resource, 'MU29eEZn'); }); test('Test a failed SASL auth', () async { @@ -296,7 +296,6 @@ void main() { ), getManagerById: getManagerNullStub, getNegotiatorById: getNegotiatorNullStub, - isFeatureSupported: (_) => false, getFullJID: () => JID.fromString('some.user@example.server/aaaaa'), getSocket: () => StubTCPSocket([]), getConnection: () => XmppConnection( From 63b7abd6f9d9d46d418d8f15c8784a76f22d00df Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Sat, 1 Apr 2023 12:38:18 +0200 Subject: [PATCH 08/29] fix(core): Prevent resource binding if we already have a resource --- packages/moxxmpp/lib/src/connection.dart | 1 + .../lib/src/negotiators/negotiator.dart | 7 +++++- .../lib/src/negotiators/resource_binding.dart | 7 ++++-- packages/moxxmpp/test/sasl/scram_test.dart | 25 +++++++++++++++++++ 4 files changed, 37 insertions(+), 3 deletions(-) diff --git a/packages/moxxmpp/lib/src/connection.dart b/packages/moxxmpp/lib/src/connection.dart index 0cef5c0..01aea42 100644 --- a/packages/moxxmpp/lib/src/connection.dart +++ b/packages/moxxmpp/lib/src/connection.dart @@ -270,6 +270,7 @@ class XmppConnection { negotiator.register( NegotiatorAttributes( sendRawXML, + () => this, () => _connectionSettings, _sendEvent, getNegotiatorById, diff --git a/packages/moxxmpp/lib/src/negotiators/negotiator.dart b/packages/moxxmpp/lib/src/negotiators/negotiator.dart index 9658e95..534dd28 100644 --- a/packages/moxxmpp/lib/src/negotiators/negotiator.dart +++ b/packages/moxxmpp/lib/src/negotiators/negotiator.dart @@ -1,5 +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'; @@ -28,6 +29,7 @@ abstract class NegotiatorError extends XmppError {} class NegotiatorAttributes { const NegotiatorAttributes( this.sendNonza, + this.getConnection, this.getConnectionSettings, this.sendEvent, this.getNegotiatorById, @@ -46,7 +48,10 @@ class NegotiatorAttributes { /// 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. diff --git a/packages/moxxmpp/lib/src/negotiators/resource_binding.dart b/packages/moxxmpp/lib/src/negotiators/resource_binding.dart index 6c6ffd2..2ebb737 100644 --- a/packages/moxxmpp/lib/src/negotiators/resource_binding.dart +++ b/packages/moxxmpp/lib/src/negotiators/resource_binding.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 diff --git a/packages/moxxmpp/test/sasl/scram_test.dart b/packages/moxxmpp/test/sasl/scram_test.dart index 44596de..b8e3798 100644 --- a/packages/moxxmpp/test/sasl/scram_test.dart +++ b/packages/moxxmpp/test/sasl/scram_test.dart @@ -46,6 +46,11 @@ void main() { )..register( NegotiatorAttributes( (XMLNode _, {String? redact}) {}, + () => XmppConnection( + TestingReconnectionPolicy(), + AlwaysConnectedConnectivityManager(), + fakeSocket, + ), () => ConnectionSettings( jid: JID.fromString('user@server'), password: 'pencil', @@ -142,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', @@ -193,6 +203,11 @@ void main() { )..register( NegotiatorAttributes( (XMLNode _, {String? redact}) {}, + () => XmppConnection( + TestingReconnectionPolicy(), + AlwaysConnectedConnectivityManager(), + StubTCPSocket([]), + ), () => ConnectionSettings( jid: JID.fromString('user@server'), password: 'pencil', @@ -234,6 +249,11 @@ void main() { )..register( NegotiatorAttributes( (XMLNode _, {String? redact}) {}, + () => XmppConnection( + TestingReconnectionPolicy(), + AlwaysConnectedConnectivityManager(), + StubTCPSocket([]), + ), () => ConnectionSettings( jid: JID.fromString('user@server'), password: 'pencil', @@ -278,6 +298,11 @@ void main() { )..register( NegotiatorAttributes( (XMLNode _, {String? redact}) {}, + () => XmppConnection( + TestingReconnectionPolicy(), + AlwaysConnectedConnectivityManager(), + StubTCPSocket([]), + ), () => ConnectionSettings( jid: JID.fromString('user@server'), password: 'pencil', From cf425917cfdbae3a1e50d801db1edd91fdd17927 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Sat, 1 Apr 2023 12:39:15 +0200 Subject: [PATCH 09/29] feat(core): Reset the resource if lastResource is null --- packages/moxxmpp/lib/src/connection.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/moxxmpp/lib/src/connection.dart b/packages/moxxmpp/lib/src/connection.dart index 01aea42..7c9c57c 100644 --- a/packages/moxxmpp/lib/src/connection.dart +++ b/packages/moxxmpp/lib/src/connection.dart @@ -1142,6 +1142,8 @@ class XmppConnection { if (lastResource != null) { _setResource(lastResource, triggerEvent: false); + } else { + _setResource('', triggerEvent: false); } _enableReconnectOnSuccess = enableReconnectOnSuccess; From 564a2379860271e8d75cba55c7202bd1404a6520 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Sat, 1 Apr 2023 12:51:26 +0200 Subject: [PATCH 10/29] feat(xep): Set the resource if SASL2 resulted in a resource --- packages/moxxmpp/lib/src/negotiators/sasl2.dart | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/moxxmpp/lib/src/negotiators/sasl2.dart b/packages/moxxmpp/lib/src/negotiators/sasl2.dart index e27398b..5a4dc6f 100644 --- a/packages/moxxmpp/lib/src/negotiators/sasl2.dart +++ b/packages/moxxmpp/lib/src/negotiators/sasl2.dart @@ -1,4 +1,5 @@ import 'package:collection/collection.dart'; +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'; @@ -233,6 +234,15 @@ class Sasl2Negotiator extends XmppFeatureNegotiatorBase { // 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 From 93581759258ab6f4c1a569f1dd755db5d9947c4f Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Sat, 1 Apr 2023 13:00:35 +0200 Subject: [PATCH 11/29] feat(xep): Inline resource binding with Bind2 --- packages/moxxmpp/lib/moxxmpp.dart | 1 + packages/moxxmpp/lib/src/namespaces.dart | 3 + .../lib/src/negotiators/namespaces.dart | 1 + packages/moxxmpp/lib/src/xeps/xep_0386.dart | 43 ++++++++++ packages/moxxmpp/test/xeps/xep_0386_test.dart | 81 +++++++++++++++++++ 5 files changed, 129 insertions(+) create mode 100644 packages/moxxmpp/lib/src/xeps/xep_0386.dart create mode 100644 packages/moxxmpp/test/xeps/xep_0386_test.dart diff --git a/packages/moxxmpp/lib/moxxmpp.dart b/packages/moxxmpp/lib/moxxmpp.dart index 2f74fca..6c321a7 100644 --- a/packages/moxxmpp/lib/moxxmpp.dart +++ b/packages/moxxmpp/lib/moxxmpp.dart @@ -77,6 +77,7 @@ 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_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/namespaces.dart b/packages/moxxmpp/lib/src/namespaces.dart index f7a11eb..38cf744 100644 --- a/packages/moxxmpp/lib/src/namespaces.dart +++ b/packages/moxxmpp/lib/src/namespaces.dart @@ -116,6 +116,9 @@ 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'; diff --git a/packages/moxxmpp/lib/src/negotiators/namespaces.dart b/packages/moxxmpp/lib/src/negotiators/namespaces.dart index 1605329..755835d 100644 --- a/packages/moxxmpp/lib/src/negotiators/namespaces.dart +++ b/packages/moxxmpp/lib/src/negotiators/namespaces.dart @@ -8,3 +8,4 @@ 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'; 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..7bc40b2 --- /dev/null +++ b/packages/moxxmpp/lib/src/xeps/xep_0386.dart @@ -0,0 +1,43 @@ +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/sasl2.dart'; +import 'package:moxxmpp/src/stringxml.dart'; +import 'package:moxxmpp/src/types/result.dart'; + +/// 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); + + @override + Future> negotiate( + XMLNode nonza, + ) async { + return const Result(NegotiatorState.done); + } + + @override + Future> onSasl2FeaturesReceived(XMLNode sasl2Features) async { + return [ + XMLNode.xmlns( + tag: 'bind', + xmlns: bind2Xmlns, + ), + ]; + } + + @override + Future> onSasl2Success(XMLNode response) async { + attributes.removeNegotiatingFeature(bindXmlns); + + return const Result(true); + } + + @override + Future postRegisterCallback() async { + attributes + .getNegotiatorById(sasl2Negotiator)! + .registerNegotiator(this); + } +} 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..c9aaccf --- /dev/null +++ b/packages/moxxmpp/test/xeps/xep_0386_test.dart @@ -0,0 +1,81 @@ +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'); + }); +} From 89fe8f0a9c10adc2956cd1fa64099c13b6b1091a Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Sat, 1 Apr 2023 13:09:43 +0200 Subject: [PATCH 12/29] feat(core): Make the PresenceManager optional --- packages/moxxmpp/CHANGELOG.md | 1 + packages/moxxmpp/lib/src/connection.dart | 7 ------- packages/moxxmpp/lib/src/presence.dart | 16 ++++++++++++++++ packages/moxxmpp/test/xeps/xep_0198_test.dart | 6 +++--- 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/packages/moxxmpp/CHANGELOG.md b/packages/moxxmpp/CHANGELOG.md index 34fa8c7..071da45 100644 --- a/packages/moxxmpp/CHANGELOG.md +++ b/packages/moxxmpp/CHANGELOG.md @@ -5,6 +5,7 @@ - **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/src/connection.dart b/packages/moxxmpp/lib/src/connection.dart index 7c9c57c..a517826 100644 --- a/packages/moxxmpp/lib/src/connection.dart +++ b/packages/moxxmpp/lib/src/connection.dart @@ -861,9 +861,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 { @@ -1090,10 +1087,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', diff --git a/packages/moxxmpp/lib/src/presence.dart b/packages/moxxmpp/lib/src/presence.dart index a461a85..570d8f7 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,19 @@ 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/test/xeps/xep_0198_test.dart b/packages/moxxmpp/test/xeps/xep_0198_test.dart index fc985ef..6c1dc73 100644 --- a/packages/moxxmpp/test/xeps/xep_0198_test.dart +++ b/packages/moxxmpp/test/xeps/xep_0198_test.dart @@ -431,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 @@ -589,7 +589,7 @@ void main() { waitUntilLogin: true, ); - expect(fakeSocket.getState(), 5); + expect(fakeSocket.getState(), 6); expect(await conn.getConnectionState(), XmppConnectionState.connected); expect( conn @@ -689,7 +689,7 @@ void main() { await conn.connect( waitUntilLogin: true, ); - expect(fakeSocket.getState(), 6); + expect(fakeSocket.getState(), 7); expect(await conn.getConnectionState(), XmppConnectionState.connected); expect( conn From f2fe06104cd034a1ed035afbcded821fe3762a34 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Sat, 1 Apr 2023 13:13:19 +0200 Subject: [PATCH 13/29] fix(core): Fix formatting --- packages/moxxmpp/lib/src/presence.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/moxxmpp/lib/src/presence.dart b/packages/moxxmpp/lib/src/presence.dart index 570d8f7..6dcc962 100644 --- a/packages/moxxmpp/lib/src/presence.dart +++ b/packages/moxxmpp/lib/src/presence.dart @@ -50,7 +50,8 @@ class PresenceManager extends XmppManagerBase { if (event is StreamNegotiationsDoneEvent) { // Send initial presence only when we have not resumed the stream final sm = getAttributes().getNegotiatorById( - streamManagementNegotiator); + streamManagementNegotiator, + ); final isResumed = sm?.isResumed ?? false; if (!isResumed) { unawaited(sendInitialPresence()); From 4e01d32e90e4e57d6d51daaddb0d4c6a2d5b7998 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Sat, 1 Apr 2023 13:15:46 +0200 Subject: [PATCH 14/29] feat(xep): Allow setting a tag when using Bind2 --- packages/moxxmpp/lib/src/xeps/xep_0386.dart | 10 +++ packages/moxxmpp/test/xeps/xep_0386_test.dart | 73 +++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/packages/moxxmpp/lib/src/xeps/xep_0386.dart b/packages/moxxmpp/lib/src/xeps/xep_0386.dart index 7bc40b2..033a82f 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0386.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0386.dart @@ -10,6 +10,9 @@ import 'package:moxxmpp/src/types/result.dart'; class Bind2Negotiator extends Sasl2FeatureNegotiator { Bind2Negotiator() : super(0, false, bind2Xmlns, bind2Negotiator); + /// A tag to sent to the server when requesting Bind2. + String? tag; + @override Future> negotiate( XMLNode nonza, @@ -23,6 +26,13 @@ class Bind2Negotiator extends Sasl2FeatureNegotiator { XMLNode.xmlns( tag: 'bind', xmlns: bind2Xmlns, + children: [ + if (tag != null) + XMLNode( + tag: 'tag', + text: tag, + ), + ], ), ]; } diff --git a/packages/moxxmpp/test/xeps/xep_0386_test.dart b/packages/moxxmpp/test/xeps/xep_0386_test.dart index c9aaccf..b7b5864 100644 --- a/packages/moxxmpp/test/xeps/xep_0386_test.dart +++ b/packages/moxxmpp/test/xeps/xep_0386_test.dart @@ -78,4 +78,77 @@ void main() { 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'); + }); } From 51edb614433dddf0fe782f1d848d42ddfc2cc9af Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Sat, 1 Apr 2023 15:50:13 +0200 Subject: [PATCH 15/29] feat(xep): Implement SASL2 inline stream resumption --- packages/moxxmpp/lib/src/connection.dart | 9 +- .../moxxmpp/lib/src/negotiators/sasl2.dart | 61 +++---- .../lib/src/xeps/xep_0198/negotiator.dart | 154 ++++++++++++++---- packages/moxxmpp/lib/src/xeps/xep_0386.dart | 9 + packages/moxxmpp/test/xeps/xep_0198_test.dart | 94 +++++++++++ packages/moxxmpp/test/xeps/xep_0386_test.dart | 3 +- packages/moxxmpp/test/xeps/xep_0388_test.dart | 9 + 7 files changed, 271 insertions(+), 68 deletions(-) diff --git a/packages/moxxmpp/lib/src/connection.dart b/packages/moxxmpp/lib/src/connection.dart index a517826..c092c57 100644 --- a/packages/moxxmpp/lib/src/connection.dart +++ b/packages/moxxmpp/lib/src/connection.dart @@ -279,7 +279,7 @@ class XmppConnection { () => _socket, () => _isAuthenticated, _setAuthenticated, - _setResource, + setResource, _removeNegotiatingFeature, ), ); @@ -675,7 +675,8 @@ class XmppConnection { } /// Sets the resource of the connection - void _setResource(String resource, {bool triggerEvent = true}) { + @visibleForTesting + void setResource(String resource, {bool triggerEvent = true}) { _log.finest('Updating _resource to $resource'); _resource = resource; @@ -1134,9 +1135,7 @@ class XmppConnection { } if (lastResource != null) { - _setResource(lastResource, triggerEvent: false); - } else { - _setResource('', triggerEvent: false); + setResource(lastResource, triggerEvent: false); } _enableReconnectOnSuccess = enableReconnectOnSuccess; diff --git a/packages/moxxmpp/lib/src/negotiators/sasl2.dart b/packages/moxxmpp/lib/src/negotiators/sasl2.dart index 5a4dc6f..bfb186e 100644 --- a/packages/moxxmpp/lib/src/negotiators/sasl2.dart +++ b/packages/moxxmpp/lib/src/negotiators/sasl2.dart @@ -1,4 +1,3 @@ -import 'package:collection/collection.dart'; import 'package:moxxmpp/src/jid.dart'; import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/negotiators/namespaces.dart'; @@ -28,6 +27,11 @@ abstract class Sasl2FeatureNegotiator extends XmppFeatureNegotiatorBase { /// 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 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. @@ -38,6 +42,11 @@ abstract class Sasl2AuthenticationNegotiator extends SaslNegotiator /// Perform a SASL step with [input] as the already parsed input data. Returns /// the base64-encoded response data. Future getRawStep(String input); + + @override + bool canInlineFeature(List features) { + return true; + } } class NoSASLMechanismSelectedError extends NegotiatorError { @@ -125,6 +134,8 @@ class Sasl2Negotiator extends XmppFeatureNegotiatorBase { /// 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. @@ -141,18 +152,6 @@ class Sasl2Negotiator extends XmppFeatureNegotiatorBase { _featureNegotiators.add(negotiator); } - /// Returns true, if an item with xmlns of [xmlns] is contained inside [_sasl2Data]'s - /// block. If not, returns false. - bool _isInliningPossible(String xmlns) { - final inline = _sasl2Data!.firstTag('inline'); - if (inline == null) { - return false; - } - - return inline.children.firstWhereOrNull((child) => child.xmlns == xmlns) != - null; - } - @override bool matchesFeature(List features) { // Only do SASL2 when the socket is secure @@ -185,12 +184,16 @@ class Sasl2Negotiator extends XmppFeatureNegotiatorBase { } // Collect additional data by interested negotiators + final inline = _sasl2Data!.firstTag('inline'); final children = List.empty(growable: true); - for (final negotiator in _featureNegotiators) { - if (_isInliningPossible(negotiator.negotiatingXmlns)) { - children.addAll( - await negotiator.onSasl2FeaturesReceived(_sasl2Data!), - ); + 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!), + ); + } } } @@ -217,19 +220,18 @@ class Sasl2Negotiator extends XmppFeatureNegotiatorBase { case Sasl2State.authenticateSent: if (nonza.tag == 'success') { // Tell the dependent negotiators about the result - // TODO(Unknown): This can be written in a better way - for (final negotiator in _featureNegotiators) { - if (_isInliningPossible(negotiator.negotiatingXmlns)) { - final result = await negotiator.onSasl2Success(nonza); - if (!result.isType()) { - return Result(result.get()); - } + 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()); } } - final result = await _currentSaslNegotiator!.onSasl2Success(nonza); - if (!result.isType()) { - return Result(result.get()); - } // We're done attributes.setAuthenticated(); @@ -264,6 +266,7 @@ class Sasl2Negotiator extends XmppFeatureNegotiatorBase { _currentSaslNegotiator = null; _sasl2State = Sasl2State.idle; _sasl2Data = null; + _activeSasl2Negotiators.clear(); super.reset(); } diff --git a/packages/moxxmpp/lib/src/xeps/xep_0198/negotiator.dart b/packages/moxxmpp/lib/src/xeps/xep_0198/negotiator.dart index 274f345..7e007f1 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0198/negotiator.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0198/negotiator.dart @@ -1,9 +1,11 @@ +import 'package:collection/collection.dart'; import 'package:logging/logging.dart'; import 'package:moxxmpp/src/events.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/negotiators/negotiator.dart'; +import 'package:moxxmpp/src/negotiators/sasl2.dart'; import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/types/result.dart'; import 'package:moxxmpp/src/xeps/xep_0198/nonzas.dart'; @@ -23,27 +25,51 @@ 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 { 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; + + /// Flag indicating whether the current stream is resumed (true) or not (false). + bool _isResumed = false; + + /// 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; + @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 bool matchesFeature(List features) { final sm = attributes.getManagerById(smManager)!; @@ -53,13 +79,37 @@ 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; + } + @override Future> negotiate( XMLNode nonza, @@ -103,30 +153,14 @@ 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: @@ -165,4 +199,60 @@ class StreamManagementNegotiator extends XmppFeatureNegotiatorBase { super.reset(); } + + @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 [ + XMLNode.xmlns( + tag: 'resume', + xmlns: smXmlns, + attributes: { + 'h': h.toString(), + 'previd': srid, + }, + ), + ]; + } + + @override + Future> onSasl2Success(XMLNode response) async { + final resumed = response.firstTag('resumed', xmlns: smXmlns); + if (resumed == null) { + _log.warning('Inline stream resumption failed'); + await _onStreamResumptionFailed(); + state = NegotiatorState.retryLater; + 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); + } } diff --git a/packages/moxxmpp/lib/src/xeps/xep_0386.dart b/packages/moxxmpp/lib/src/xeps/xep_0386.dart index 033a82f..25deb7a 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0386.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0386.dart @@ -1,3 +1,4 @@ +import 'package:collection/collection.dart'; import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/negotiators/namespaces.dart'; import 'package:moxxmpp/src/negotiators/negotiator.dart'; @@ -37,6 +38,14 @@ class Bind2Negotiator extends Sasl2FeatureNegotiator { ]; } + @override + bool canInlineFeature(List features) { + return features.firstWhereOrNull( + (child) => child.tag == 'bind' && child.xmlns == bind2Xmlns, + ) != + null; + } + @override Future> onSasl2Success(XMLNode response) async { attributes.removeNegotiatingFeature(bindXmlns); diff --git a/packages/moxxmpp/test/xeps/xep_0198_test.dart b/packages/moxxmpp/test/xeps/xep_0198_test.dart index 6c1dc73..a11987d 100644 --- a/packages/moxxmpp/test/xeps/xep_0198_test.dart +++ b/packages/moxxmpp/test/xeps/xep_0198_test.dart @@ -788,4 +788,98 @@ void main() { expect(sm.streamResumed, true); }); }); + + test('Test SASL2 inline stream resumption', () 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(), + 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'); + }); } diff --git a/packages/moxxmpp/test/xeps/xep_0386_test.dart b/packages/moxxmpp/test/xeps/xep_0386_test.dart index b7b5864..ebb70fa 100644 --- a/packages/moxxmpp/test/xeps/xep_0386_test.dart +++ b/packages/moxxmpp/test/xeps/xep_0386_test.dart @@ -132,8 +132,7 @@ void main() { await conn.registerFeatureNegotiators([ SaslPlainNegotiator(), ResourceBindingNegotiator(), - Bind2Negotiator() - ..tag = 'moxxmpp', + Bind2Negotiator()..tag = 'moxxmpp', Sasl2Negotiator( userAgent: const UserAgent( id: 'd4565fa7-4d72-4749-b3d3-740edbf87770', diff --git a/packages/moxxmpp/test/xeps/xep_0388_test.dart b/packages/moxxmpp/test/xeps/xep_0388_test.dart index 0941e8c..1973e45 100644 --- a/packages/moxxmpp/test/xeps/xep_0388_test.dart +++ b/packages/moxxmpp/test/xeps/xep_0388_test.dart @@ -1,3 +1,4 @@ +import 'package:collection/collection.dart'; import 'package:moxxmpp/moxxmpp.dart'; import 'package:test/test.dart'; import '../helpers/logging.dart'; @@ -16,6 +17,14 @@ class ExampleNegotiator extends Sasl2FeatureNegotiator { 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 From 91f763ac2625b5ad47232c8815ecfa1c328c94c4 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Sat, 1 Apr 2023 17:16:29 +0200 Subject: [PATCH 16/29] feat(xep): Allow negotiating SM enabling inline with Bind2 --- .../moxxmpp/lib/src/negotiators/sasl2.dart | 1 + .../lib/src/xeps/xep_0198/negotiator.dart | 24 ++++- packages/moxxmpp/lib/src/xeps/xep_0386.dart | 39 +++++++ packages/moxxmpp/test/xeps/xep_0198_test.dart | 100 ++++++++++++++++++ 4 files changed, 163 insertions(+), 1 deletion(-) diff --git a/packages/moxxmpp/lib/src/negotiators/sasl2.dart b/packages/moxxmpp/lib/src/negotiators/sasl2.dart index bfb186e..f12e61a 100644 --- a/packages/moxxmpp/lib/src/negotiators/sasl2.dart +++ b/packages/moxxmpp/lib/src/negotiators/sasl2.dart @@ -218,6 +218,7 @@ 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/xeps/xep_0198/negotiator.dart b/packages/moxxmpp/lib/src/xeps/xep_0198/negotiator.dart index 7e007f1..62047dd 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0198/negotiator.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0198/negotiator.dart @@ -12,6 +12,7 @@ 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'; enum _StreamManagementNegotiatorState { // We have not done anything yet @@ -25,7 +26,8 @@ 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 Sasl2FeatureNegotiator { +class StreamManagementNegotiator extends Sasl2FeatureNegotiator + implements Bind2FeatureNegotiator { StreamManagementNegotiator() : super(10, false, smXmlns, streamManagementNegotiator); @@ -200,6 +202,22 @@ class StreamManagementNegotiator extends Sasl2FeatureNegotiator { super.reset(); } + @override + Future> onBind2FeaturesReceived( + List bind2Features, + ) async { + if (!bind2Features.contains(smXmlns)) { + return []; + } + + return [ + XMLNode.xmlns( + tag: 'enable', + xmlns: smXmlns, + ), + ]; + } + @override Future> onSasl2FeaturesReceived(XMLNode sasl2Features) async { final inline = sasl2Features.firstTag('inline')!; @@ -231,6 +249,7 @@ class StreamManagementNegotiator extends Sasl2FeatureNegotiator { @override Future> onSasl2Success(XMLNode response) async { + // TODO(PapaTutuWawa): Handle SM failures. final resumed = response.firstTag('resumed', xmlns: smXmlns); if (resumed == null) { _log.warning('Inline stream resumption failed'); @@ -254,5 +273,8 @@ class StreamManagementNegotiator extends Sasl2FeatureNegotiator { attributes .getNegotiatorById(sasl2Negotiator) ?.registerNegotiator(this); + attributes + .getNegotiatorById(bind2Negotiator) + ?.registerNegotiator(this); } } diff --git a/packages/moxxmpp/lib/src/xeps/xep_0386.dart b/packages/moxxmpp/lib/src/xeps/xep_0386.dart index 25deb7a..60691b3 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0386.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0386.dart @@ -6,14 +6,33 @@ import 'package:moxxmpp/src/negotiators/sasl2.dart'; import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/types/result.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 Bind2FeatureNegotiator { + /// 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); +} + /// 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(Bind2FeatureNegotiator negotiator) { + _negotiators.add(negotiator); + } + @override Future> negotiate( XMLNode nonza, @@ -23,6 +42,25 @@ class Bind2Negotiator extends Sasl2FeatureNegotiator { @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', @@ -33,6 +71,7 @@ class Bind2Negotiator extends Sasl2FeatureNegotiator { tag: 'tag', text: tag, ), + ...children, ], ), ]; diff --git a/packages/moxxmpp/test/xeps/xep_0198_test.dart b/packages/moxxmpp/test/xeps/xep_0198_test.dart index a11987d..a0706e5 100644 --- a/packages/moxxmpp/test/xeps/xep_0198_test.dart +++ b/packages/moxxmpp/test/xeps/xep_0198_test.dart @@ -882,4 +882,104 @@ void main() { ); 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(), + 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'); + }); } From 24cb05f91bca39330e68ca62cd06bc4c4597d82f Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Sat, 1 Apr 2023 17:38:40 +0200 Subject: [PATCH 17/29] feat(xep): Handle inline stream enablement with Bind2 --- .../lib/src/xeps/xep_0198/negotiator.dart | 85 ++++++++++----- packages/moxxmpp/test/xeps/xep_0198_test.dart | 102 +++++++++++++++++- 2 files changed, 157 insertions(+), 30 deletions(-) diff --git a/packages/moxxmpp/lib/src/xeps/xep_0198/negotiator.dart b/packages/moxxmpp/lib/src/xeps/xep_0198/negotiator.dart index 62047dd..9c149c3 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0198/negotiator.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0198/negotiator.dart @@ -37,9 +37,15 @@ class StreamManagementNegotiator extends Sasl2FeatureNegotiator /// 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'); @@ -48,8 +54,8 @@ class StreamManagementNegotiator extends Sasl2FeatureNegotiator 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; @override bool canInlineFeature(List features) { @@ -112,6 +118,28 @@ class StreamManagementNegotiator extends Sasl2FeatureNegotiator _isResumed = true; } + 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, @@ -168,25 +196,13 @@ class StreamManagementNegotiator extends Sasl2FeatureNegotiator 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); } } @@ -198,6 +214,8 @@ class StreamManagementNegotiator extends Sasl2FeatureNegotiator _supported = false; _resumeFailed = false; _isResumed = false; + _inlineStreamEnablementRequested = false; + _streamEnablementFailed = false; super.reset(); } @@ -210,11 +228,9 @@ class StreamManagementNegotiator extends Sasl2FeatureNegotiator return []; } + _inlineStreamEnablementRequested = true; return [ - XMLNode.xmlns( - tag: 'enable', - xmlns: smXmlns, - ), + StreamManagementEnableNonza(), ]; } @@ -236,25 +252,36 @@ class StreamManagementNegotiator extends Sasl2FeatureNegotiator } return [ - XMLNode.xmlns( - tag: 'resume', - xmlns: smXmlns, - attributes: { - 'h': h.toString(), - 'previd': srid, - }, + StreamManagementResumeNonza( + srid, + h, ), ]; } @override Future> onSasl2Success(XMLNode response) async { - // TODO(PapaTutuWawa): Handle SM failures. + 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.retryLater; + state = NegotiatorState.done; return const Result(true); } diff --git a/packages/moxxmpp/test/xeps/xep_0198_test.dart b/packages/moxxmpp/test/xeps/xep_0198_test.dart index a0706e5..03e35c7 100644 --- a/packages/moxxmpp/test/xeps/xep_0198_test.dart +++ b/packages/moxxmpp/test/xeps/xep_0198_test.dart @@ -915,7 +915,7 @@ void main() { ''', ), StanzaExpectation( - "moxxmppPapaTutuWawa's awesome deviceAHBvbHlub21kaXZpc2lvbgBhYWFh", + "moxxmppPapaTutuWawa's awesome deviceAHBvbHlub21kaXZpc2lvbgBhYWFh", ''' polynomdivision@test.server @@ -982,4 +982,104 @@ void main() { ); 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'); + }); } From 0033d0eb6e2f84742e095c515fd7913c01aa68c2 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Sat, 1 Apr 2023 21:10:46 +0200 Subject: [PATCH 18/29] feat(xep): Implement FAST --- packages/moxxmpp/lib/moxxmpp.dart | 1 + packages/moxxmpp/lib/src/connection.dart | 16 +- packages/moxxmpp/lib/src/namespaces.dart | 3 + .../lib/src/negotiators/namespaces.dart | 1 + .../lib/src/negotiators/negotiator.dart | 3 + .../lib/src/negotiators/sasl/plain.dart | 3 + .../lib/src/negotiators/sasl/scram.dart | 3 + .../moxxmpp/lib/src/negotiators/sasl2.dart | 36 ++++ .../moxxmpp/lib/src/xeps/staging/fast.dart | 158 +++++++++++++++++ .../lib/src/xeps/xep_0198/negotiator.dart | 22 +++ packages/moxxmpp/test/xeps/xep_0198_test.dart | 4 +- .../moxxmpp/test/xeps/xep_xxxx_fast_test.dart | 163 ++++++++++++++++++ 12 files changed, 403 insertions(+), 10 deletions(-) create mode 100644 packages/moxxmpp/lib/src/xeps/staging/fast.dart create mode 100644 packages/moxxmpp/test/xeps/xep_xxxx_fast_test.dart diff --git a/packages/moxxmpp/lib/moxxmpp.dart b/packages/moxxmpp/lib/moxxmpp.dart index 6c321a7..77322d6 100644 --- a/packages/moxxmpp/lib/moxxmpp.dart +++ b/packages/moxxmpp/lib/moxxmpp.dart @@ -39,6 +39,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'; diff --git a/packages/moxxmpp/lib/src/connection.dart b/packages/moxxmpp/lib/src/connection.dart index c092c57..cc4654e 100644 --- a/packages/moxxmpp/lib/src/connection.dart +++ b/packages/moxxmpp/lib/src/connection.dart @@ -314,13 +314,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. @@ -1030,6 +1025,9 @@ class XmppConnection { for (final manager in _xmppManagers.values) { await manager.onXmppEvent(event); } + for (final negotiator in _featureNegotiators.values) { + await negotiator.onXmppEvent(event); + } _eventStreamController.add(event); } @@ -1068,7 +1066,7 @@ class XmppConnection { await _reconnectionPolicy.setShouldReconnect(false); if (triggeredByUser) { - getPresenceManager().sendUnavailablePresence(); + getPresenceManager()?.sendUnavailablePresence(); } _socket.prepareDisconnect(); @@ -1136,6 +1134,8 @@ class XmppConnection { if (lastResource != null) { setResource(lastResource, triggerEvent: false); + } else { + setResource('', triggerEvent: false); } _enableReconnectOnSuccess = enableReconnectOnSuccess; diff --git a/packages/moxxmpp/lib/src/namespaces.dart b/packages/moxxmpp/lib/src/namespaces.dart index 38cf744..86b69b3 100644 --- a/packages/moxxmpp/lib/src/namespaces.dart +++ b/packages/moxxmpp/lib/src/namespaces.dart @@ -160,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 755835d..a62dd44 100644 --- a/packages/moxxmpp/lib/src/negotiators/namespaces.dart +++ b/packages/moxxmpp/lib/src/negotiators/namespaces.dart @@ -9,3 +9,4 @@ 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'; diff --git a/packages/moxxmpp/lib/src/negotiators/negotiator.dart b/packages/moxxmpp/lib/src/negotiators/negotiator.dart index 534dd28..98c0d4f 100644 --- a/packages/moxxmpp/lib/src/negotiators/negotiator.dart +++ b/packages/moxxmpp/lib/src/negotiators/negotiator.dart @@ -124,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. diff --git a/packages/moxxmpp/lib/src/negotiators/sasl/plain.dart b/packages/moxxmpp/lib/src/negotiators/sasl/plain.dart index d3e02c2..7f3acb9 100644 --- a/packages/moxxmpp/lib/src/negotiators/sasl/plain.dart +++ b/packages/moxxmpp/lib/src/negotiators/sasl/plain.dart @@ -98,6 +98,9 @@ class SaslPlainNegotiator extends Sasl2AuthenticationNegotiator { 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/negotiators/sasl/scram.dart index 360ebf7..61ad667 100644 --- a/packages/moxxmpp/lib/src/negotiators/sasl/scram.dart +++ b/packages/moxxmpp/lib/src/negotiators/sasl/scram.dart @@ -356,6 +356,9 @@ class SaslScramNegotiator extends Sasl2AuthenticationNegotiator { 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 diff --git a/packages/moxxmpp/lib/src/negotiators/sasl2.dart b/packages/moxxmpp/lib/src/negotiators/sasl2.dart index f12e61a..28d01b3 100644 --- a/packages/moxxmpp/lib/src/negotiators/sasl2.dart +++ b/packages/moxxmpp/lib/src/negotiators/sasl2.dart @@ -2,6 +2,7 @@ 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/negotiators/sasl/errors.dart'; import 'package:moxxmpp/src/negotiators/sasl/negotiator.dart'; import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/types/result.dart'; @@ -28,6 +29,10 @@ abstract class Sasl2FeatureNegotiator extends XmppFeatureNegotiatorBase { /// 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. @@ -39,10 +44,26 @@ 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); + void pickForSasl2() { + _pickedForSasl2 = true; + } + + @override + void reset() { + _pickedForSasl2 = false; + + super.reset(); + } + @override bool canInlineFeature(List features) { return true; @@ -174,6 +195,7 @@ class Sasl2Negotiator extends XmppFeatureNegotiatorBase { for (final negotiator in _saslNegotiators) { if (negotiator.matchesFeature([mechanisms])) { _currentSaslNegotiator = negotiator; + _currentSaslNegotiator!.pickForSasl2(); break; } } @@ -256,6 +278,20 @@ class Sasl2Negotiator extends XmppFeatureNegotiatorBase { 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); + } + + return Result( + SaslError.fromFailure(nonza), + ); } } 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..76088b1 --- /dev/null +++ b/packages/moxxmpp/lib/src/xeps/staging/fast.dart @@ -0,0 +1,158 @@ +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/negotiators/sasl2.dart'; +import 'package:moxxmpp/src/stringxml.dart'; +import 'package:moxxmpp/src/types/result.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 + 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 9c149c3..48497f3 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0198/negotiator.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0198/negotiator.dart @@ -1,5 +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'; @@ -57,6 +58,13 @@ class StreamManagementNegotiator extends Sasl2FeatureNegotiator /// 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)!; @@ -78,6 +86,13 @@ class StreamManagementNegotiator extends Sasl2FeatureNegotiator } } + @override + Future onXmppEvent(XmppEvent event) async { + if (event is ResourceBoundEvent) { + _resource = event.resource; + } + } + @override bool matchesFeature(List features) { final sm = attributes.getManagerById(smManager)!; @@ -116,6 +131,13 @@ class StreamManagementNegotiator extends Sasl2FeatureNegotiator _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 { diff --git a/packages/moxxmpp/test/xeps/xep_0198_test.dart b/packages/moxxmpp/test/xeps/xep_0198_test.dart index 03e35c7..d454f64 100644 --- a/packages/moxxmpp/test/xeps/xep_0198_test.dart +++ b/packages/moxxmpp/test/xeps/xep_0198_test.dart @@ -855,7 +855,7 @@ void main() { await conn.registerFeatureNegotiators([ SaslPlainNegotiator(), ResourceBindingNegotiator(), - StreamManagementNegotiator(), + StreamManagementNegotiator()..setResource('test-resource'), Sasl2Negotiator( userAgent: const UserAgent( id: 'd4565fa7-4d72-4749-b3d3-740edbf87770', @@ -954,7 +954,7 @@ void main() { await conn.registerFeatureNegotiators([ SaslPlainNegotiator(), ResourceBindingNegotiator(), - StreamManagementNegotiator(), + StreamManagementNegotiator()..setResource('test-resource'), Bind2Negotiator(), Sasl2Negotiator( userAgent: const UserAgent( 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..b59a22a --- /dev/null +++ b/packages/moxxmpp/test/xeps/xep_xxxx_fast_test.dart @@ -0,0 +1,163 @@ +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); + }); +} From 4a6aa79e56c75342258a9bc337d4eba93626f2bb Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Sat, 1 Apr 2023 21:42:52 +0200 Subject: [PATCH 19/29] fix(xep): When using FAST, fallback to other SASL mechanisms on failure --- .../moxxmpp/lib/src/negotiators/sasl2.dart | 15 +++ .../moxxmpp/lib/src/xeps/staging/fast.dart | 18 ++- .../moxxmpp/test/xeps/xep_xxxx_fast_test.dart | 106 ++++++++++++++++++ 3 files changed, 134 insertions(+), 5 deletions(-) diff --git a/packages/moxxmpp/lib/src/negotiators/sasl2.dart b/packages/moxxmpp/lib/src/negotiators/sasl2.dart index 28d01b3..f260037 100644 --- a/packages/moxxmpp/lib/src/negotiators/sasl2.dart +++ b/packages/moxxmpp/lib/src/negotiators/sasl2.dart @@ -53,10 +53,15 @@ abstract class Sasl2AuthenticationNegotiator extends SaslNegotiator /// 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; @@ -289,6 +294,16 @@ class Sasl2Negotiator extends XmppFeatureNegotiatorBase { 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), ); diff --git a/packages/moxxmpp/lib/src/xeps/staging/fast.dart b/packages/moxxmpp/lib/src/xeps/staging/fast.dart index 76088b1..39d32f2 100644 --- a/packages/moxxmpp/lib/src/xeps/staging/fast.dart +++ b/packages/moxxmpp/lib/src/xeps/staging/fast.dart @@ -30,10 +30,14 @@ class FASTToken { ); 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',); + 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, @@ -80,7 +84,8 @@ class FASTSaslNegotiator extends Sasl2AuthenticationNegotiator { @override bool canInlineFeature(List features) { return features.firstWhereOrNull( - (child) => child.tag == 'fast' && child.xmlns == fastXmlns,) != + (child) => child.tag == 'fast' && child.xmlns == fastXmlns, + ) != null; } @@ -114,6 +119,9 @@ class FASTSaslNegotiator extends Sasl2AuthenticationNegotiator { ); } + @override + bool shouldRetrySasl() => true; + @override Future> onSasl2FeaturesReceived(XMLNode sasl2Features) async { if (fastToken != null && pickedForSasl2) { diff --git a/packages/moxxmpp/test/xeps/xep_xxxx_fast_test.dart b/packages/moxxmpp/test/xeps/xep_xxxx_fast_test.dart index b59a22a..e844527 100644 --- a/packages/moxxmpp/test/xeps/xep_xxxx_fast_test.dart +++ b/packages/moxxmpp/test/xeps/xep_xxxx_fast_test.dart @@ -160,4 +160,110 @@ void main() { 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'); + }); } From fbb495dc2f32360340212ac3ba8e66048c77fd19 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Sat, 1 Apr 2023 23:16:37 +0200 Subject: [PATCH 20/29] feat(xep): Allow inlining CSI --- packages/moxxmpp/lib/src/xeps/xep_0352.dart | 54 +++++++++-- packages/moxxmpp/test/xeps/xep_0352_test.dart | 94 +++++++++++++++++++ 2 files changed, 138 insertions(+), 10 deletions(-) diff --git a/packages/moxxmpp/lib/src/xeps/xep_0352.dart b/packages/moxxmpp/lib/src/xeps/xep_0352.dart index 6ac38c4..1c339df 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 Bind2FeatureNegotiator { CSINegotiator() : super(11, false, csiXmlns, csiNegotiator); /// True if CSI is supported. False otherwise. @@ -40,19 +42,43 @@ class CSINegotiator extends XmppFeatureNegotiatorBase { return const Result(NegotiatorState.done); } + @override + Future> onBind2FeaturesReceived( + List bind2Features, + ) async { + if (!bind2Features.contains(csiXmlns)) { + return []; + } + + final active = attributes.getManagerById(csiManager)!.isActive; + return [ + if (active) CSIActiveNonza() else CSIInactiveNonza(), + ]; + } + @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 +97,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/test/xeps/xep_0352_test.dart b/packages/moxxmpp/test/xeps/xep_0352_test.dart index 3981296..15192b1 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; @@ -111,4 +114,95 @@ 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); + }); } From ce1815d1f36eb2111b5df02f2b79cd5313e429e0 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Sun, 2 Apr 2023 12:43:28 +0200 Subject: [PATCH 21/29] fix(tests): Fix namespace of --- packages/moxxmpp/test/xeps/xep_0198_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/moxxmpp/test/xeps/xep_0198_test.dart b/packages/moxxmpp/test/xeps/xep_0198_test.dart index d454f64..e97826c 100644 --- a/packages/moxxmpp/test/xeps/xep_0198_test.dart +++ b/packages/moxxmpp/test/xeps/xep_0198_test.dart @@ -1020,7 +1020,7 @@ void main() { polynomdivision@test.server/test-resource - + From ec6b5ab7534f00ac6d608204c9a1440cd787dd4d Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Sun, 2 Apr 2023 12:44:09 +0200 Subject: [PATCH 22/29] feat(xep): Allow inline enablement of carbons --- .../lib/src/negotiators/namespaces.dart | 1 + packages/moxxmpp/lib/src/xeps/xep_0280.dart | 95 +++++++++++++++++++ packages/moxxmpp/test/xeps/xep_0280_test.dart | 91 ++++++++++++++++++ 3 files changed, 187 insertions(+) diff --git a/packages/moxxmpp/lib/src/negotiators/namespaces.dart b/packages/moxxmpp/lib/src/negotiators/namespaces.dart index a62dd44..17691ec 100644 --- a/packages/moxxmpp/lib/src/negotiators/namespaces.dart +++ b/packages/moxxmpp/lib/src/negotiators/namespaces.dart @@ -10,3 +10,4 @@ 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/xeps/xep_0280.dart b/packages/moxxmpp/lib/src/xeps/xep_0280.dart index 5a2ad78..f3efc24 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,15 @@ 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/negotiators/negotiator.dart'; +import 'package:moxxmpp/src/negotiators/sasl2.dart'; import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stringxml.dart'; +import 'package:moxxmpp/src/types/result.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 +179,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 +201,82 @@ class CarbonsManager extends XmppManagerBase { ); } } + +class CarbonsNegotiator extends Sasl2FeatureNegotiator + implements Bind2FeatureNegotiator { + CarbonsNegotiator() : super(0, false, 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 + bool canInlineFeature(List features) => true; + + @override + Future> onSasl2FeaturesReceived(XMLNode sasl2Features) async { + return []; + } + + @override + Future> onSasl2Success(XMLNode response) async { + if (_requestedEnablement) { + final enabled = response + .firstTag('bound', xmlns: bind2Xmlns) + ?.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(); + } + } + + return const Result(true); + } + + @override + Future> negotiate( + XMLNode nonza, + ) async { + return const Result(NegotiatorState.done); + } + + @override + Future> onBind2FeaturesReceived( + List bind2Features) async { + if (!bind2Features.contains(carbonsXmlns)) { + return []; + } + + _requestedEnablement = true; + return [ + XMLNode.xmlns( + tag: 'enable', + xmlns: carbonsXmlns, + ), + ]; + } + + @override + Future postRegisterCallback() async { + attributes + .getNegotiatorById(sasl2Negotiator) + ?.registerNegotiator(this); + attributes + .getNegotiatorById(bind2Negotiator) + ?.registerNegotiator(this); + } + + @override + void reset() { + _requestedEnablement = false; + + super.reset(); + } +} diff --git a/packages/moxxmpp/test/xeps/xep_0280_test.dart b/packages/moxxmpp/test/xeps/xep_0280_test.dart index 282317e..c29c751 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( @@ -52,4 +55,92 @@ 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); + }); } From b354ca8d0a9773d8a16f49d50df84b61e8af18e8 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Sun, 2 Apr 2023 13:39:43 +0200 Subject: [PATCH 23/29] feat(xep): Improve the ergonomics of Bind2 negotiators --- .../lib/src/xeps/xep_0198/negotiator.dart | 5 +- packages/moxxmpp/lib/src/xeps/xep_0280.dart | 62 +++++-------------- packages/moxxmpp/lib/src/xeps/xep_0352.dart | 6 +- packages/moxxmpp/lib/src/xeps/xep_0386.dart | 50 +++++++++++++-- packages/moxxmpp/test/xeps/xep_0280_test.dart | 4 +- packages/moxxmpp/test/xeps/xep_0352_test.dart | 4 ++ 6 files changed, 77 insertions(+), 54 deletions(-) diff --git a/packages/moxxmpp/lib/src/xeps/xep_0198/negotiator.dart b/packages/moxxmpp/lib/src/xeps/xep_0198/negotiator.dart index 48497f3..daa00b3 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0198/negotiator.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0198/negotiator.dart @@ -28,7 +28,7 @@ enum _StreamManagementNegotiatorState { /// StreamManagementManager at least once before connecting, if stream resumption /// is wanted. class StreamManagementNegotiator extends Sasl2FeatureNegotiator - implements Bind2FeatureNegotiator { + implements Bind2FeatureNegotiatorInterface { StreamManagementNegotiator() : super(10, false, smXmlns, streamManagementNegotiator); @@ -256,6 +256,9 @@ class StreamManagementNegotiator extends Sasl2FeatureNegotiator ]; } + @override + Future onBind2Success(XMLNode response) async {} + @override Future> onSasl2FeaturesReceived(XMLNode sasl2Features) async { final inline = sasl2Features.firstTag('inline')!; diff --git a/packages/moxxmpp/lib/src/xeps/xep_0280.dart b/packages/moxxmpp/lib/src/xeps/xep_0280.dart index f3efc24..3153dd1 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0280.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0280.dart @@ -9,11 +9,8 @@ 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/negotiators/negotiator.dart'; -import 'package:moxxmpp/src/negotiators/sasl2.dart'; import 'package:moxxmpp/src/stanza.dart'; import 'package:moxxmpp/src/stringxml.dart'; -import 'package:moxxmpp/src/types/result.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'; @@ -202,9 +199,8 @@ class CarbonsManager extends XmppManagerBase { } } -class CarbonsNegotiator extends Sasl2FeatureNegotiator - implements Bind2FeatureNegotiator { - CarbonsNegotiator() : super(0, false, carbonsXmlns, carbonsNegotiator); +class CarbonsNegotiator extends Bind2FeatureNegotiator { + CarbonsNegotiator() : super(0, carbonsXmlns, carbonsNegotiator); /// Flag indicating whether we requested to enable carbons inline (true) or not /// (false). @@ -214,42 +210,26 @@ class CarbonsNegotiator extends Sasl2FeatureNegotiator final Logger _log = Logger('CarbonsNegotiator'); @override - bool canInlineFeature(List features) => true; - - @override - Future> onSasl2FeaturesReceived(XMLNode sasl2Features) async { - return []; - } - - @override - Future> onSasl2Success(XMLNode response) async { - if (_requestedEnablement) { - final enabled = response - .firstTag('bound', xmlns: bind2Xmlns) - ?.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(); - } + Future onBind2Success(XMLNode response) async { + if (!_requestedEnablement) { + return; } - return const Result(true); - } - - @override - Future> negotiate( - XMLNode nonza, - ) async { - return const Result(NegotiatorState.done); + 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 { + List bind2Features, + ) async { if (!bind2Features.contains(carbonsXmlns)) { return []; } @@ -263,16 +243,6 @@ class CarbonsNegotiator extends Sasl2FeatureNegotiator ]; } - @override - Future postRegisterCallback() async { - attributes - .getNegotiatorById(sasl2Negotiator) - ?.registerNegotiator(this); - attributes - .getNegotiatorById(bind2Negotiator) - ?.registerNegotiator(this); - } - @override void reset() { _requestedEnablement = false; diff --git a/packages/moxxmpp/lib/src/xeps/xep_0352.dart b/packages/moxxmpp/lib/src/xeps/xep_0352.dart index 1c339df..718d805 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0352.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0352.dart @@ -25,7 +25,7 @@ class CSIInactiveNonza extends XMLNode { /// A Stub negotiator that is just for "intercepting" the stream feature. class CSINegotiator extends XmppFeatureNegotiatorBase - implements Bind2FeatureNegotiator { + implements Bind2FeatureNegotiatorInterface { CSINegotiator() : super(11, false, csiXmlns, csiNegotiator); /// True if CSI is supported. False otherwise. @@ -50,12 +50,16 @@ class CSINegotiator extends XmppFeatureNegotiatorBase 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; diff --git a/packages/moxxmpp/lib/src/xeps/xep_0386.dart b/packages/moxxmpp/lib/src/xeps/xep_0386.dart index 60691b3..09550d9 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0386.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0386.dart @@ -1,4 +1,5 @@ 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'; @@ -9,10 +10,43 @@ import 'package:moxxmpp/src/types/result.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 Bind2FeatureNegotiator { +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 @@ -21,15 +55,15 @@ 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); + 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(Bind2FeatureNegotiator negotiator) { + void registerNegotiator(Bind2FeatureNegotiatorInterface negotiator) { _negotiators.add(negotiator); } @@ -87,8 +121,14 @@ class Bind2Negotiator extends Sasl2FeatureNegotiator { @override Future> onSasl2Success(XMLNode response) async { - attributes.removeNegotiatingFeature(bindXmlns); + 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); } diff --git a/packages/moxxmpp/test/xeps/xep_0280_test.dart b/packages/moxxmpp/test/xeps/xep_0280_test.dart index c29c751..1f9bf6d 100644 --- a/packages/moxxmpp/test/xeps/xep_0280_test.dart +++ b/packages/moxxmpp/test/xeps/xep_0280_test.dart @@ -141,6 +141,8 @@ void main() { expect(result.isType(), false); expect(conn.resource, 'test-resource'); expect( - conn.getManagerById(carbonsManager)!.isEnabled, true); + 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 15192b1..67b2fbe 100644 --- a/packages/moxxmpp/test/xeps/xep_0352_test.dart +++ b/packages/moxxmpp/test/xeps/xep_0352_test.dart @@ -204,5 +204,9 @@ void main() { ); expect(result.isType(), false); expect(fakeSocket.getState(), 2); + expect( + conn.getNegotiatorById(csiNegotiator)!.isSupported, + true, + ); }); } From 29f041915460f4d9020fb60bbd7ae25958aeba6c Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Sun, 2 Apr 2023 14:38:32 +0200 Subject: [PATCH 24/29] feat(meta): Remove log redaction --- packages/moxxmpp/lib/src/connection.dart | 6 +++--- .../moxxmpp/lib/src/negotiators/negotiator.dart | 2 +- .../moxxmpp/lib/src/negotiators/sasl/plain.dart | 1 - .../moxxmpp/lib/src/negotiators/sasl/scram.dart | 2 -- packages/moxxmpp/lib/src/negotiators/sasl2.dart | 2 +- packages/moxxmpp/lib/src/socket.dart | 5 ++--- .../integration_test/badxmpp_certificate_test.dart | 7 ++++--- .../failure_reconnection_test.dart | 14 ++++++++------ packages/moxxmpp_socket_tcp/lib/src/socket.dart | 8 ++------ 9 files changed, 21 insertions(+), 26 deletions(-) diff --git a/packages/moxxmpp/lib/src/connection.dart b/packages/moxxmpp/lib/src/connection.dart index cc4654e..1c5d9af 100644 --- a/packages/moxxmpp/lib/src/connection.dart +++ b/packages/moxxmpp/lib/src/connection.dart @@ -152,7 +152,7 @@ class XmppConnection { /// 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 @@ -467,10 +467,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. diff --git a/packages/moxxmpp/lib/src/negotiators/negotiator.dart b/packages/moxxmpp/lib/src/negotiators/negotiator.dart index 98c0d4f..600e872 100644 --- a/packages/moxxmpp/lib/src/negotiators/negotiator.dart +++ b/packages/moxxmpp/lib/src/negotiators/negotiator.dart @@ -43,7 +43,7 @@ class NegotiatorAttributes { ); /// 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; diff --git a/packages/moxxmpp/lib/src/negotiators/sasl/plain.dart b/packages/moxxmpp/lib/src/negotiators/sasl/plain.dart index 7f3acb9..b601f75 100644 --- a/packages/moxxmpp/lib/src/negotiators/sasl/plain.dart +++ b/packages/moxxmpp/lib/src/negotiators/sasl/plain.dart @@ -50,7 +50,6 @@ class SaslPlainNegotiator extends Sasl2AuthenticationNegotiator { final data = await getRawStep(''); attributes.sendNonza( SaslPlainAuthNonza(data), - redact: SaslPlainAuthNonza('******').toXml(), ); _authSent = true; return const Result(NegotiatorState.ready); diff --git a/packages/moxxmpp/lib/src/negotiators/sasl/scram.dart b/packages/moxxmpp/lib/src/negotiators/sasl/scram.dart index 61ad667..666f95b 100644 --- a/packages/moxxmpp/lib/src/negotiators/sasl/scram.dart +++ b/packages/moxxmpp/lib/src/negotiators/sasl/scram.dart @@ -259,7 +259,6 @@ class SaslScramNegotiator extends Sasl2AuthenticationNegotiator { body: await getRawStep(''), type: hashType, ), - redact: SaslScramAuthNonza(body: '******', type: hashType).toXml(), ); return const Result(NegotiatorState.ready); case ScramState.initialMessageSent: @@ -275,7 +274,6 @@ class SaslScramNegotiator extends Sasl2AuthenticationNegotiator { attributes.sendNonza( SaslScramResponseNonza(body: await getRawStep(nonza.innerText())), - redact: SaslScramResponseNonza(body: '******').toXml(), ); return const Result(NegotiatorState.ready); case ScramState.challengeResponseSent: diff --git a/packages/moxxmpp/lib/src/negotiators/sasl2.dart b/packages/moxxmpp/lib/src/negotiators/sasl2.dart index f260037..bbf481d 100644 --- a/packages/moxxmpp/lib/src/negotiators/sasl2.dart +++ b/packages/moxxmpp/lib/src/negotiators/sasl2.dart @@ -232,11 +232,11 @@ class Sasl2Negotiator extends XmppFeatureNegotiatorBase { 'mechanism': _currentSaslNegotiator!.mechanismName, }, children: [ - if (userAgent != null) userAgent!.toXml(), XMLNode( tag: 'initial-response', text: await _currentSaslNegotiator!.getRawStep(''), ), + if (userAgent != null) userAgent!.toXml(), ...children, ], ); 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_socket_tcp/integration_test/badxmpp_certificate_test.dart b/packages/moxxmpp_socket_tcp/integration_test/badxmpp_certificate_test.dart index b93d554..6a7f8d9 100644 --- a/packages/moxxmpp_socket_tcp/integration_test/badxmpp_certificate_test.dart +++ b/packages/moxxmpp_socket_tcp/integration_test/badxmpp_certificate_test.dart @@ -18,9 +18,10 @@ Future _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..883f16a 100644 --- a/packages/moxxmpp_socket_tcp/lib/src/socket.dart +++ b/packages/moxxmpp_socket_tcp/lib/src/socket.dart @@ -289,17 +289,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); From d977a74446daf04332ee71f7f72878afc143a287 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Sun, 2 Apr 2023 17:20:14 +0200 Subject: [PATCH 25/29] feat(tests): Add an integration test for SASL2 --- flake.lock | 25 +++++-- flake.nix | 31 ++++++++- integration_tests/.gitignore | 6 ++ integration_tests/README.md | 5 ++ integration_tests/analysis_options.yaml | 1 + integration_tests/certs/localhost.crt | 24 +++++++ integration_tests/certs/localhost.key | 28 ++++++++ integration_tests/prosody.cfg.lua | 56 ++++++++++++++++ integration_tests/pubspec.yaml | 16 +++++ integration_tests/test/sasl2_test.dart | 67 +++++++++++++++++++ .../moxxmpp/integration_tests/sasl2_test.dart | 43 ++++++++++++ packages/moxxmpp/lib/src/connection.dart | 17 +++-- .../lib/src/negotiators/sasl/plain.dart | 4 +- .../moxxmpp/lib/src/negotiators/sasl2.dart | 2 +- packages/moxxmpp/lib/src/settings.dart | 5 ++ .../moxxmpp_socket_tcp/lib/src/socket.dart | 2 + 16 files changed, 318 insertions(+), 14 deletions(-) create mode 100644 integration_tests/.gitignore create mode 100644 integration_tests/README.md create mode 100644 integration_tests/analysis_options.yaml create mode 100644 integration_tests/certs/localhost.crt create mode 100644 integration_tests/certs/localhost.key create mode 100644 integration_tests/prosody.cfg.lua create mode 100644 integration_tests/pubspec.yaml create mode 100644 integration_tests/test/sasl2_test.dart create mode 100644 packages/moxxmpp/integration_tests/sasl2_test.dart 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; } } From 68e2a65dcf72fed1e387d0e9ac47fba15fd2c383 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Sun, 2 Apr 2023 19:49:00 +0200 Subject: [PATCH 26/29] docs(tests): Mention that we need prosody-trunk --- integration_tests/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration_tests/README.md b/integration_tests/README.md index edaa687..0d93bac 100644 --- a/integration_tests/README.md +++ b/integration_tests/README.md @@ -2,4 +2,4 @@ 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. +exists. Note that this currently requires prosody-trunk. From 7fdd83ea6972b3decb367b37f166528579974580 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Sun, 2 Apr 2023 23:06:02 +0200 Subject: [PATCH 27/29] chore(xep): Clean the SASL2 implementation --- .../moxxmpp/integration_tests/sasl2_test.dart | 43 --- packages/moxxmpp/lib/moxxmpp.dart | 4 + packages/moxxmpp/lib/src/connection.dart | 4 +- .../lib/src/negotiators/sasl/plain.dart | 5 +- .../lib/src/negotiators/sasl/scram.dart | 3 +- .../moxxmpp/lib/src/negotiators/sasl2.dart | 324 ------------------ .../moxxmpp/lib/src/xeps/staging/fast.dart | 3 +- .../lib/src/xeps/xep_0198/negotiator.dart | 3 +- packages/moxxmpp/lib/src/xeps/xep_0386.dart | 3 +- .../moxxmpp/lib/src/xeps/xep_0388/errors.dart | 8 + .../lib/src/xeps/xep_0388/negotiators.dart | 72 ++++ .../lib/src/xeps/xep_0388/user_agent.dart | 46 +++ .../lib/src/xeps/xep_0388/xep_0388.dart | 209 +++++++++++ packages/moxxmpp/test/stringxml_test.dart | 2 +- 14 files changed, 353 insertions(+), 376 deletions(-) delete mode 100644 packages/moxxmpp/integration_tests/sasl2_test.dart create mode 100644 packages/moxxmpp/lib/src/xeps/xep_0388/errors.dart create mode 100644 packages/moxxmpp/lib/src/xeps/xep_0388/negotiators.dart create mode 100644 packages/moxxmpp/lib/src/xeps/xep_0388/user_agent.dart create mode 100644 packages/moxxmpp/lib/src/xeps/xep_0388/xep_0388.dart diff --git a/packages/moxxmpp/integration_tests/sasl2_test.dart b/packages/moxxmpp/integration_tests/sasl2_test.dart deleted file mode 100644 index 4a11042..0000000 --- a/packages/moxxmpp/integration_tests/sasl2_test.dart +++ /dev/null @@ -1,43 +0,0 @@ -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/moxxmpp.dart b/packages/moxxmpp/lib/moxxmpp.dart index 77322d6..2a349fc 100644 --- a/packages/moxxmpp/lib/moxxmpp.dart +++ b/packages/moxxmpp/lib/moxxmpp.dart @@ -79,6 +79,10 @@ 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 903a9c5..6ce1f7b 100644 --- a/packages/moxxmpp/lib/src/connection.dart +++ b/packages/moxxmpp/lib/src/connection.dart @@ -1158,8 +1158,8 @@ class XmppConnection { } final smManager = getStreamManagementManager(); - String? host = _connectionSettings.host; - int? port = _connectionSettings.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!); diff --git a/packages/moxxmpp/lib/src/negotiators/sasl/plain.dart b/packages/moxxmpp/lib/src/negotiators/sasl/plain.dart index 9582519..2d0650f 100644 --- a/packages/moxxmpp/lib/src/negotiators/sasl/plain.dart +++ b/packages/moxxmpp/lib/src/negotiators/sasl/plain.dart @@ -5,9 +5,10 @@ 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/nonza.dart'; -import 'package:moxxmpp/src/negotiators/sasl2.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 { @@ -89,7 +90,7 @@ class SaslPlainNegotiator extends Sasl2AuthenticationNegotiator { final settings = attributes.getConnectionSettings(); final prep = Saslprep.saslprep(settings.password); return base64.encode( - utf8.encode('\u0000${settings.jid.local}\u0000${prep}'), + utf8.encode('\u0000${settings.jid.local}\u0000$prep'), ); } diff --git a/packages/moxxmpp/lib/src/negotiators/sasl/scram.dart b/packages/moxxmpp/lib/src/negotiators/sasl/scram.dart index 666f95b..e44d9a7 100644 --- a/packages/moxxmpp/lib/src/negotiators/sasl/scram.dart +++ b/packages/moxxmpp/lib/src/negotiators/sasl/scram.dart @@ -9,9 +9,10 @@ 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/nonza.dart'; -import 'package:moxxmpp/src/negotiators/sasl2.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'; diff --git a/packages/moxxmpp/lib/src/negotiators/sasl2.dart b/packages/moxxmpp/lib/src/negotiators/sasl2.dart index d763a4f..8b13789 100644 --- a/packages/moxxmpp/lib/src/negotiators/sasl2.dart +++ b/packages/moxxmpp/lib/src/negotiators/sasl2.dart @@ -1,325 +1 @@ -import 'dart:convert'; -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/negotiators/sasl/errors.dart'; -import 'package:moxxmpp/src/negotiators/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; - } -} - -class NoSASLMechanismSelectedError extends NegotiatorError { - @override - bool isRecoverable() => false; -} - -/// 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, - ), - ], - ); - } -} - -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/lib/src/xeps/staging/fast.dart b/packages/moxxmpp/lib/src/xeps/staging/fast.dart index 39d32f2..83785ad 100644 --- a/packages/moxxmpp/lib/src/xeps/staging/fast.dart +++ b/packages/moxxmpp/lib/src/xeps/staging/fast.dart @@ -4,9 +4,10 @@ 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/sasl2.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 { diff --git a/packages/moxxmpp/lib/src/xeps/xep_0198/negotiator.dart b/packages/moxxmpp/lib/src/xeps/xep_0198/negotiator.dart index daa00b3..3008370 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0198/negotiator.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0198/negotiator.dart @@ -6,7 +6,6 @@ import 'package:moxxmpp/src/managers/namespaces.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/sasl2.dart'; import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/types/result.dart'; import 'package:moxxmpp/src/xeps/xep_0198/nonzas.dart'; @@ -14,6 +13,8 @@ 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 diff --git a/packages/moxxmpp/lib/src/xeps/xep_0386.dart b/packages/moxxmpp/lib/src/xeps/xep_0386.dart index 09550d9..4f61add 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0386.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0386.dart @@ -3,9 +3,10 @@ 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/negotiators/sasl2.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. 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..4a95a70 --- /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/negotiators/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..18067bc --- /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/negotiators/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/stringxml_test.dart b/packages/moxxmpp/test/stringxml_test.dart index 26a3811..7926d65 100644 --- a/packages/moxxmpp/test/stringxml_test.dart +++ b/packages/moxxmpp/test/stringxml_test.dart @@ -23,7 +23,7 @@ void main() { ); expect( - StreamHeaderNonza('uwu.server').toXml(), + StreamHeaderNonza(JID.fromString('uwu.server')).toXml(), "", ); From 808371b27119a964ada81b3ba2688420ad27ecca Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Sun, 2 Apr 2023 23:23:50 +0200 Subject: [PATCH 28/29] chore(tests): Fix tests --- packages/moxxmpp/test/helpers/xmpp.dart | 4 +-- packages/moxxmpp/test/negotiator_test.dart | 2 +- packages/moxxmpp/test/stringxml_test.dart | 4 +-- packages/moxxmpp/test/xeps/xep_0030_test.dart | 4 +-- packages/moxxmpp/test/xeps/xep_0198_test.dart | 26 +++++++++---------- packages/moxxmpp/test/xeps/xep_0280_test.dart | 2 +- packages/moxxmpp/test/xeps/xep_0352_test.dart | 2 +- packages/moxxmpp/test/xeps/xep_0386_test.dart | 4 +-- packages/moxxmpp/test/xeps/xep_0388_test.dart | 10 +++---- .../moxxmpp/test/xeps/xep_xxxx_fast_test.dart | 6 ++--- packages/moxxmpp/test/xmpp_test.dart | 10 +++---- 11 files changed, 37 insertions(+), 37 deletions(-) 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( - "", + "", ''' ", + "", ''' ", + 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 4f3b903..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( - "", + "", ''' ", + "", ''' ', ), StringExpectation( - "", + "", ''' ", + "", ''' ', ), StringExpectation( - "", + "", ''' ", + "", ''' ', ), StringExpectation( - "", + "", ''' ", + "", ''' ', ), StringExpectation( - "", + "", ''' ", + "", ''' ', ), StringExpectation( - "", + "", ''' ", + "", ''' ", + "", ''' ", + "", ''' ", + "", ''' ", + "", ''' ", + "", ''' ", + "", ''' ", + "", ''' ", + "", ''' ", + "", ''' ", + "", ''' ", + "", ''' ", + "", ''' ", + "", ''' ", + "", ''' ", + "", ''' ', ), StringExpectation( - "", + "", ''' ", + "", ''' ", + "", ''' ", + "", ''' Date: Sun, 2 Apr 2023 23:33:53 +0200 Subject: [PATCH 29/29] chore(core): Refactor RFC6120 implementations --- packages/moxxmpp/lib/moxxmpp.dart | 13 ++++++------- packages/moxxmpp/lib/src/negotiators/sasl2.dart | 1 - .../rfc_6120}/resource_binding.dart | 0 .../{negotiators => rfcs/rfc_6120}/sasl/errors.dart | 0 .../src/{negotiators => rfcs/rfc_6120}/sasl/kv.dart | 0 .../rfc_6120}/sasl/negotiator.dart | 0 .../{negotiators => rfcs/rfc_6120}/sasl/nonza.dart | 0 .../{negotiators => rfcs/rfc_6120}/sasl/plain.dart | 4 ++-- .../{negotiators => rfcs/rfc_6120}/sasl/scram.dart | 6 +++--- .../{negotiators => rfcs/rfc_6120}/starttls.dart | 0 .../moxxmpp/lib/src/xeps/xep_0388/negotiators.dart | 2 +- .../moxxmpp/lib/src/xeps/xep_0388/xep_0388.dart | 2 +- packages/moxxmpp/test/sasl/kv_test.dart | 2 +- 13 files changed, 14 insertions(+), 16 deletions(-) delete mode 100644 packages/moxxmpp/lib/src/negotiators/sasl2.dart rename packages/moxxmpp/lib/src/{negotiators => rfcs/rfc_6120}/resource_binding.dart (100%) rename packages/moxxmpp/lib/src/{negotiators => rfcs/rfc_6120}/sasl/errors.dart (100%) rename packages/moxxmpp/lib/src/{negotiators => rfcs/rfc_6120}/sasl/kv.dart (100%) rename packages/moxxmpp/lib/src/{negotiators => rfcs/rfc_6120}/sasl/negotiator.dart (100%) rename packages/moxxmpp/lib/src/{negotiators => rfcs/rfc_6120}/sasl/nonza.dart (100%) rename packages/moxxmpp/lib/src/{negotiators => rfcs/rfc_6120}/sasl/plain.dart (95%) rename packages/moxxmpp/lib/src/{negotiators => rfcs/rfc_6120}/sasl/scram.dart (98%) rename packages/moxxmpp/lib/src/{negotiators => rfcs/rfc_6120}/starttls.dart (100%) diff --git a/packages/moxxmpp/lib/moxxmpp.dart b/packages/moxxmpp/lib/moxxmpp.dart index 2a349fc..8a9dd9b 100644 --- a/packages/moxxmpp/lib/moxxmpp.dart +++ b/packages/moxxmpp/lib/moxxmpp.dart @@ -18,18 +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/sasl2.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'; diff --git a/packages/moxxmpp/lib/src/negotiators/sasl2.dart b/packages/moxxmpp/lib/src/negotiators/sasl2.dart deleted file mode 100644 index 8b13789..0000000 --- a/packages/moxxmpp/lib/src/negotiators/sasl2.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/packages/moxxmpp/lib/src/negotiators/resource_binding.dart b/packages/moxxmpp/lib/src/rfcs/rfc_6120/resource_binding.dart similarity index 100% rename from packages/moxxmpp/lib/src/negotiators/resource_binding.dart rename to packages/moxxmpp/lib/src/rfcs/rfc_6120/resource_binding.dart 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 95% rename from packages/moxxmpp/lib/src/negotiators/sasl/plain.dart rename to packages/moxxmpp/lib/src/rfcs/rfc_6120/sasl/plain.dart index 2d0650f..573762c 100644 --- a/packages/moxxmpp/lib/src/negotiators/sasl/plain.dart +++ b/packages/moxxmpp/lib/src/rfcs/rfc_6120/sasl/plain.dart @@ -3,8 +3,8 @@ 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/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'; diff --git a/packages/moxxmpp/lib/src/negotiators/sasl/scram.dart b/packages/moxxmpp/lib/src/rfcs/rfc_6120/sasl/scram.dart similarity index 98% rename from packages/moxxmpp/lib/src/negotiators/sasl/scram.dart rename to packages/moxxmpp/lib/src/rfcs/rfc_6120/sasl/scram.dart index e44d9a7..9ea570b 100644 --- a/packages/moxxmpp/lib/src/negotiators/sasl/scram.dart +++ b/packages/moxxmpp/lib/src/rfcs/rfc_6120/sasl/scram.dart @@ -6,9 +6,9 @@ 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/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'; 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/xeps/xep_0388/negotiators.dart b/packages/moxxmpp/lib/src/xeps/xep_0388/negotiators.dart index 4a95a70..948930f 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0388/negotiators.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0388/negotiators.dart @@ -1,5 +1,5 @@ import 'package:moxxmpp/src/negotiators/negotiator.dart'; -import 'package:moxxmpp/src/negotiators/sasl/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'; diff --git a/packages/moxxmpp/lib/src/xeps/xep_0388/xep_0388.dart b/packages/moxxmpp/lib/src/xeps/xep_0388/xep_0388.dart index 18067bc..d28f760 100644 --- a/packages/moxxmpp/lib/src/xeps/xep_0388/xep_0388.dart +++ b/packages/moxxmpp/lib/src/xeps/xep_0388/xep_0388.dart @@ -2,7 +2,7 @@ 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/negotiators/sasl/errors.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'; diff --git a/packages/moxxmpp/test/sasl/kv_test.dart b/packages/moxxmpp/test/sasl/kv_test.dart index 4b601fb..5717ac5 100644 --- a/packages/moxxmpp/test/sasl/kv_test.dart +++ b/packages/moxxmpp/test/sasl/kv_test.dart @@ -1,4 +1,4 @@ -import 'package:moxxmpp/src/negotiators/sasl/kv.dart'; +import 'package:moxxmpp/src/rfcs/rfc_6120/sasl/kv.dart'; import 'package:test/test.dart'; void main() {