feat(xep): Implement inline negotiation

This commit is contained in:
PapaTutuWawa 2023-04-01 00:47:42 +02:00
parent f86dbe6af8
commit 30482c86f0
3 changed files with 265 additions and 8 deletions

View File

@ -1,3 +1,4 @@
import 'package:collection/collection.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/negotiators/namespaces.dart'; import 'package:moxxmpp/src/negotiators/namespaces.dart';
import 'package:moxxmpp/src/negotiators/negotiator.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/stringxml.dart';
import 'package:moxxmpp/src/types/result.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. /// A special type of [XmppFeatureNegotiatorBase] that is aware of SASL2.
abstract class Sasl2FeatureNegotiator extends XmppFeatureNegotiatorBase { abstract class Sasl2FeatureNegotiator extends XmppFeatureNegotiatorBase {
Sasl2FeatureNegotiator( Sasl2FeatureNegotiator(
@ -117,6 +130,11 @@ class Sasl2Negotiator extends XmppFeatureNegotiatorBase {
/// The SASL negotiator that will negotiate authentication. /// The SASL negotiator that will negotiate authentication.
Sasl2AuthenticationNegotiator? _currentSaslNegotiator; Sasl2AuthenticationNegotiator? _currentSaslNegotiator;
/// The SASL2 <authentication /> 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) { void registerSaslNegotiator(Sasl2AuthenticationNegotiator negotiator) {
_featureNegotiators.add(negotiator); _featureNegotiators.add(negotiator);
_saslNegotiators _saslNegotiators
@ -124,21 +142,30 @@ class Sasl2Negotiator extends XmppFeatureNegotiatorBase {
..sort((a, b) => b.priority.compareTo(a.priority)); ..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) { void registerNegotiator(Sasl2FeatureNegotiator negotiator) {
_featureNegotiators.add(negotiator); _featureNegotiators.add(negotiator);
} }
@override
bool matchesFeature(List<XMLNode> features) {
// Only do SASL2 when the socket is secure
return attributes.getSocket().isSecure() && super.matchesFeature(features);
}
@override @override
Future<Result<NegotiatorState, NegotiatorError>> negotiate( Future<Result<NegotiatorState, NegotiatorError>> negotiate(
XMLNode nonza, XMLNode nonza,
) async { ) async {
switch (_sasl2State) { switch (_sasl2State) {
case Sasl2State.idle: case Sasl2State.idle:
final sasl2 = nonza.firstTag('authentication', xmlns: sasl2Xmlns)!; _sasl2Data = nonza.firstTag('authentication', xmlns: sasl2Xmlns);
final mechanisms = XMLNode.xmlns( final mechanisms = XMLNode.xmlns(
tag: 'mechanisms', tag: 'mechanisms',
xmlns: saslXmlns, 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) { for (final negotiator in _saslNegotiators) {
if (negotiator.matchesFeature([mechanisms])) { if (negotiator.matchesFeature([mechanisms])) {
@ -155,9 +182,11 @@ class Sasl2Negotiator extends XmppFeatureNegotiatorBase {
// Collect additional data by interested negotiators // Collect additional data by interested negotiators
final children = List<XMLNode>.empty(growable: true); final children = List<XMLNode>.empty(growable: true);
for (final negotiator in _featureNegotiators) { for (final negotiator in _featureNegotiators) {
children.addAll( if (isInliningPossible(_sasl2Data!, negotiator.negotiatingXmlns)) {
await negotiator.onSasl2FeaturesReceived(sasl2), children.addAll(
); await negotiator.onSasl2FeaturesReceived(_sasl2Data!),
);
}
} }
// Build the authenticate nonza // Build the authenticate nonza
@ -183,12 +212,19 @@ class Sasl2Negotiator extends XmppFeatureNegotiatorBase {
case Sasl2State.authenticateSent: case Sasl2State.authenticateSent:
if (nonza.tag == 'success') { if (nonza.tag == 'success') {
// Tell the dependent negotiators about the result // Tell the dependent negotiators about the result
// TODO(Unknown): This can be written in a better way
for (final negotiator in _featureNegotiators) { for (final negotiator in _featureNegotiators) {
final result = await negotiator.onSasl2Success(nonza); if (isInliningPossible(_sasl2Data!, negotiator.negotiatingXmlns)) {
if (!result.isType<bool>()) { final result = await negotiator.onSasl2Success(nonza);
return Result(result.get<NegotiatorError>()); if (!result.isType<bool>()) {
return Result(result.get<NegotiatorError>());
}
} }
} }
final result = await _currentSaslNegotiator!.onSasl2Success(nonza);
if (!result.isType<bool>()) {
return Result(result.get<NegotiatorError>());
}
// We're done // We're done
attributes.setAuthenticated(); attributes.setAuthenticated();
@ -213,6 +249,7 @@ class Sasl2Negotiator extends XmppFeatureNegotiatorBase {
void reset() { void reset() {
_currentSaslNegotiator = null; _currentSaslNegotiator = null;
_sasl2State = Sasl2State.idle; _sasl2State = Sasl2State.idle;
_sasl2Data = null;
super.reset(); super.reset();
} }

View File

@ -146,4 +146,6 @@ class XMLNode {
String innerText() { String innerText() {
return text ?? ''; return text ?? '';
} }
String? get xmlns => attributes['xmlns'] as String?;
} }

View File

@ -3,6 +3,53 @@ import 'package:test/test.dart';
import '../helpers/logging.dart'; import '../helpers/logging.dart';
import '../helpers/xmpp.dart'; import '../helpers/xmpp.dart';
class ExampleNegotiator extends Sasl2FeatureNegotiator {
ExampleNegotiator()
: super(0, false, 'invalid:example:dont:use', 'testNegotiator');
String? value;
@override
Future<Result<NegotiatorState, NegotiatorError>> negotiate(
XMLNode nonza,
) async {
return const Result(NegotiatorState.done);
}
@override
Future<void> postRegisterCallback() async {
attributes
.getNegotiatorById<Sasl2Negotiator>(sasl2Negotiator)!
.registerNegotiator(this);
}
@override
Future<List<XMLNode>> 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<Result<bool, NegotiatorError>> 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() { void main() {
initLogger(); initLogger();
@ -247,4 +294,175 @@ void main() {
expect(result.isType<NegotiatorError>(), true); expect(result.isType<NegotiatorError>(), true);
expect(result.get<NegotiatorError>() is InvalidServerSignatureError, true); expect(result.get<NegotiatorError>() is InvalidServerSignatureError, true);
}); });
test('Test simple SASL2 inlining', () 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>
<test-feature xmlns="invalid:example:dont:use" />
</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><test-data-request xmlns='invalid:example:dont:use' /></authenticate>",
'''
<success xmlns='urn:xmpp:sasl:2'>
<authorization-identifier>polynomdivision@test.server</authorization-identifier>
<test-data xmlns='invalid:example:dont:use'>Hello World</test-data>
</success>
''',
),
StanzaExpectation(
"<iq xmlns='jabber:client' type='set' id='aaaa'><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind' /></iq>",
'''
'<iq xmlns="jabber:client" type="result" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><jid>polynomdivision@test.server/MU29eEZn</jid></bind></iq>',
''',
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<NegotiatorError>(), false);
expect(
conn.getNegotiatorById<ExampleNegotiator>('testNegotiator')!.value,
'Hello World',
);
});
test('Test simple SASL2 inlining 2', () 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" />
</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></authenticate>",
'''
<success xmlns='urn:xmpp:sasl:2'>
<authorization-identifier>polynomdivision@test.server</authorization-identifier>
</success>
''',
),
StanzaExpectation(
"<iq xmlns='jabber:client' type='set' id='aaaa'><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind' /></iq>",
'''
'<iq xmlns="jabber:client" type="result" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><jid>polynomdivision@test.server/MU29eEZn</jid></bind></iq>',
''',
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<NegotiatorError>(), false);
expect(
conn.getNegotiatorById<ExampleNegotiator>('testNegotiator')!.value,
null,
);
});
} }