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