From 24cb05f91bca39330e68ca62cd06bc4c4597d82f Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Sat, 1 Apr 2023 17:38:40 +0200 Subject: [PATCH] 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'); + }); }