feat(xep): Allow negotiating SM enabling inline with Bind2

This commit is contained in:
PapaTutuWawa 2023-04-01 17:16:29 +02:00
parent 51edb61443
commit 91f763ac26
4 changed files with 163 additions and 1 deletions

View File

@ -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

View File

@ -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<List<XMLNode>> onBind2FeaturesReceived(
List<String> bind2Features,
) async {
if (!bind2Features.contains(smXmlns)) {
return [];
}
return [
XMLNode.xmlns(
tag: 'enable',
xmlns: smXmlns,
),
];
}
@override
Future<List<XMLNode>> onSasl2FeaturesReceived(XMLNode sasl2Features) async {
final inline = sasl2Features.firstTag('inline')!;
@ -231,6 +249,7 @@ class StreamManagementNegotiator extends Sasl2FeatureNegotiator {
@override
Future<Result<bool, NegotiatorError>> 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>(sasl2Negotiator)
?.registerNegotiator(this);
attributes
.getNegotiatorById<Bind2Negotiator>(bind2Negotiator)
?.registerNegotiator(this);
}
}

View File

@ -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<List<XMLNode>> onBind2FeaturesReceived(List<String> 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<Bind2FeatureNegotiator> _negotiators =
List<Bind2FeatureNegotiator>.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<Result<NegotiatorState, NegotiatorError>> negotiate(
XMLNode nonza,
@ -23,6 +42,25 @@ class Bind2Negotiator extends Sasl2FeatureNegotiator {
@override
Future<List<XMLNode>> onSasl2FeaturesReceived(XMLNode sasl2Features) async {
final children = List<XMLNode>.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,
],
),
];

View File

@ -882,4 +882,104 @@ void main() {
);
expect(conn.resource, 'test-resource');
});
test('Test 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' /></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>
<resumed xmlns='urn:xmpp:sm:3' h='25' previd='test-prev-id' />
</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,
]);
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<NegotiatorError>(), false);
expect(
sm.state.c2s,
25,
);
expect(
sm.state.s2c,
2,
);
expect(conn.resource, 'test-resource');
});
}