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(), "", );