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(