From 30482c86f0beb04a213c27249677c58e352b0991 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Sat, 1 Apr 2023 00:47:42 +0200 Subject: [PATCH] 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, + ); + }); }