feat(xep): Implement inline negotiation
This commit is contained in:
parent
f86dbe6af8
commit
30482c86f0
@ -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,10 +182,12 @@ 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) {
|
||||||
|
if (isInliningPossible(_sasl2Data!, negotiator.negotiatingXmlns)) {
|
||||||
children.addAll(
|
children.addAll(
|
||||||
await negotiator.onSasl2FeaturesReceived(sasl2),
|
await negotiator.onSasl2FeaturesReceived(_sasl2Data!),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Build the authenticate nonza
|
// Build the authenticate nonza
|
||||||
final authenticate = XMLNode.xmlns(
|
final authenticate = XMLNode.xmlns(
|
||||||
@ -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) {
|
||||||
|
if (isInliningPossible(_sasl2Data!, negotiator.negotiatingXmlns)) {
|
||||||
final result = await negotiator.onSasl2Success(nonza);
|
final result = await negotiator.onSasl2Success(nonza);
|
||||||
if (!result.isType<bool>()) {
|
if (!result.isType<bool>()) {
|
||||||
return Result(result.get<NegotiatorError>());
|
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();
|
||||||
}
|
}
|
||||||
|
@ -146,4 +146,6 @@ class XMLNode {
|
|||||||
String innerText() {
|
String innerText() {
|
||||||
return text ?? '';
|
return text ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String? get xmlns => attributes['xmlns'] as String?;
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user