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'); + }); }