feat(xep): Handle inline stream enablement with Bind2

This commit is contained in:
PapaTutuWawa 2023-04-01 17:38:40 +02:00
parent 91f763ac26
commit 24cb05f91b
2 changed files with 157 additions and 30 deletions

View File

@ -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<XMLNode> features) {
@ -112,6 +118,28 @@ class StreamManagementNegotiator extends Sasl2FeatureNegotiator
_isResumed = true;
}
Future<void> _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<Result<NegotiatorState, NegotiatorError>> 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 <failed />
_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<Result<bool, NegotiatorError>> 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);
}

View File

@ -915,7 +915,7 @@ void main() {
</stream:features>''',
),
StanzaExpectation(
"<authenticate xmlns='urn:xmpp:sasl:2' mechanism='PLAIN'><user-agent id='d4565fa7-4d72-4749-b3d3-740edbf87770'><software>moxxmpp</software><device>PapaTutuWawa's awesome device</device></user-agent><initial-response>AHBvbHlub21kaXZpc2lvbgBhYWFh</initial-response><bind xmlns='urn:xmpp:bind:0'><enable xmlns='urn:xmpp:sm:3' /></bind><resume xmlns='urn:xmpp:sm:3' previd='test-prev-id' h='2' /></authenticate>",
"<authenticate xmlns='urn:xmpp:sasl:2' mechanism='PLAIN'><user-agent id='d4565fa7-4d72-4749-b3d3-740edbf87770'><software>moxxmpp</software><device>PapaTutuWawa's awesome device</device></user-agent><initial-response>AHBvbHlub21kaXZpc2lvbgBhYWFh</initial-response><bind xmlns='urn:xmpp:bind:0'><enable xmlns='urn:xmpp:sm:3' resume='true' /></bind><resume xmlns='urn:xmpp:sm:3' previd='test-prev-id' h='2' /></authenticate>",
'''
<success xmlns='urn:xmpp:sasl:2'>
<authorization-identifier>polynomdivision@test.server</authorization-identifier>
@ -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(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
<mechanism>PLAIN</mechanism>
</mechanisms>
<authentication xmlns='urn:xmpp:sasl:2'>
<mechanism>PLAIN</mechanism>
<inline>
<resume xmlns="urn:xmpp:sm:3" />
<bind xmlns="urn:xmpp:bind:0">
<inline>
<feature var="urn:xmpp:sm:3" />
</inline>
</bind>
</inline>
</authentication>
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
<required/>
</bind>
</stream:features>''',
),
StanzaExpectation(
"<authenticate xmlns='urn:xmpp:sasl:2' mechanism='PLAIN'><user-agent id='d4565fa7-4d72-4749-b3d3-740edbf87770'><software>moxxmpp</software><device>PapaTutuWawa's awesome device</device></user-agent><initial-response>AHBvbHlub21kaXZpc2lvbgBhYWFh</initial-response><bind xmlns='urn:xmpp:bind:0'><enable xmlns='urn:xmpp:sm:3' resume='true' /></bind><resume xmlns='urn:xmpp:sm:3' previd='test-prev-id' h='2' /></authenticate>",
'''
<success xmlns='urn:xmpp:sasl:2'>
<authorization-identifier>polynomdivision@test.server/test-resource</authorization-identifier>
<failed xmlns='urn:xmpp:sm:3' />
<bound xmlns='urn:xmpp:sm:3'>
<failed xmlns='urn:xmpp:sm:3' />
</bound>
</success>
''',
),
]);
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<NegotiatorError>(), false);
expect(smn.isResumed, false);
expect(smn.resumeFailed, true);
expect(smn.streamEnablementFailed, true);
expect(conn.resource, 'test-resource');
});
}