feat(xep): Begin work on SASL2
This commit is contained in:
parent
52ad9a7ddb
commit
2e60e9841e
@ -23,6 +23,7 @@ export 'package:moxxmpp/src/negotiators/sasl/errors.dart';
|
||||
export 'package:moxxmpp/src/negotiators/sasl/negotiator.dart';
|
||||
export 'package:moxxmpp/src/negotiators/sasl/plain.dart';
|
||||
export 'package:moxxmpp/src/negotiators/sasl/scram.dart';
|
||||
export 'package:moxxmpp/src/negotiators/sasl2.dart';
|
||||
export 'package:moxxmpp/src/negotiators/starttls.dart';
|
||||
export 'package:moxxmpp/src/ping.dart';
|
||||
export 'package:moxxmpp/src/presence.dart';
|
||||
|
@ -254,7 +254,8 @@ class XmppConnection {
|
||||
}
|
||||
|
||||
/// Register a list of negotiator with the connection.
|
||||
void registerFeatureNegotiators(List<XmppFeatureNegotiatorBase> negotiators) {
|
||||
Future<void> registerFeatureNegotiators(
|
||||
List<XmppFeatureNegotiatorBase> negotiators) async {
|
||||
for (final negotiator in negotiators) {
|
||||
_log.finest('Registering ${negotiator.id}');
|
||||
negotiator.register(
|
||||
@ -273,6 +274,10 @@ class XmppConnection {
|
||||
}
|
||||
|
||||
_log.finest('Negotiators registered');
|
||||
|
||||
for (final negotiator in _featureNegotiators.values) {
|
||||
await negotiator.postRegisterCallback();
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset all registered negotiators.
|
||||
|
@ -116,6 +116,9 @@ const omemoBundlesXmlns = 'urn:xmpp:omemo:2:bundles';
|
||||
// XEP-0385
|
||||
const simsXmlns = 'urn:xmpp:sims:1';
|
||||
|
||||
// XEP-0388
|
||||
const sasl2Xmlns = 'urn:xmpp:sasl:2';
|
||||
|
||||
// XEP-0420
|
||||
const sceXmlns = 'urn:xmpp:sce:1';
|
||||
|
||||
|
@ -7,3 +7,4 @@ const rosterNegotiator = 'im.moxxmpp.core.roster';
|
||||
const resourceBindingNegotiator = 'im.moxxmpp.core.resource';
|
||||
const streamManagementNegotiator = 'im.moxxmpp.xeps.sm';
|
||||
const startTlsNegotiator = 'im.moxxmpp.core.starttls';
|
||||
const sasl2Negotiator = 'org.moxxmpp.sasl.sasl2';
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:moxlib/moxlib.dart';
|
||||
import 'package:moxxmpp/src/errors.dart';
|
||||
import 'package:moxxmpp/src/events.dart';
|
||||
@ -120,5 +121,11 @@ abstract class XmppFeatureNegotiatorBase {
|
||||
state = NegotiatorState.ready;
|
||||
}
|
||||
|
||||
@protected
|
||||
NegotiatorAttributes get attributes => _attributes;
|
||||
|
||||
/// Run after all negotiators are registered. Useful for registering callbacks against
|
||||
/// other negotiators.
|
||||
@visibleForOverriding
|
||||
Future<void> postRegisterCallback() async {}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import 'package:moxxmpp/src/negotiators/negotiator.dart';
|
||||
import 'package:moxxmpp/src/negotiators/sasl/errors.dart';
|
||||
import 'package:moxxmpp/src/negotiators/sasl/negotiator.dart';
|
||||
import 'package:moxxmpp/src/negotiators/sasl/nonza.dart';
|
||||
import 'package:moxxmpp/src/negotiators/sasl2.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
import 'package:moxxmpp/src/types/result.dart';
|
||||
|
||||
@ -76,4 +77,11 @@ class SaslPlainNegotiator extends SaslNegotiator {
|
||||
|
||||
super.reset();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> postRegisterCallback() async {
|
||||
attributes
|
||||
.getNegotiatorById<Sasl2Negotiator>(sasl2Negotiator)
|
||||
?.registerSaslNegotiator(this);
|
||||
}
|
||||
}
|
||||
|
161
packages/moxxmpp/lib/src/negotiators/sasl2.dart
Normal file
161
packages/moxxmpp/lib/src/negotiators/sasl2.dart
Normal file
@ -0,0 +1,161 @@
|
||||
import 'package:moxxmpp/src/namespaces.dart';
|
||||
import 'package:moxxmpp/src/negotiators/namespaces.dart';
|
||||
import 'package:moxxmpp/src/negotiators/negotiator.dart';
|
||||
import 'package:moxxmpp/src/negotiators/sasl/negotiator.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
import 'package:moxxmpp/src/types/result.dart';
|
||||
|
||||
typedef Sasl2FeaturesReceivedCallback = Future<List<XMLNode>> Function(XMLNode);
|
||||
|
||||
class NoSASLMechanismSelectedError extends NegotiatorError {
|
||||
@override
|
||||
bool isRecoverable() => false;
|
||||
}
|
||||
|
||||
/// A data class describing the user agent. See https://dyn.eightysoft.de/final/xep-0388.html#initiation
|
||||
class UserAgent {
|
||||
const UserAgent({
|
||||
this.id,
|
||||
this.software,
|
||||
this.device,
|
||||
});
|
||||
|
||||
/// The identifier of the software/device combo connecting. SHOULD be a UUIDv4.
|
||||
final String? id;
|
||||
|
||||
/// The software's name that's connecting at the moment.
|
||||
final String? software;
|
||||
|
||||
/// The name of the device.
|
||||
final String? device;
|
||||
|
||||
XMLNode toXml() {
|
||||
assert(id != null || software != null || device != null,
|
||||
'A completely empty user agent makes no sense');
|
||||
return XMLNode(
|
||||
tag: 'user-agent',
|
||||
attributes: id != null
|
||||
? {
|
||||
'id': id,
|
||||
}
|
||||
: {},
|
||||
children: [
|
||||
if (software != null)
|
||||
XMLNode(
|
||||
tag: 'software',
|
||||
text: software,
|
||||
),
|
||||
if (device != null)
|
||||
XMLNode(
|
||||
tag: 'device',
|
||||
text: device,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum Sasl2State {
|
||||
// No request has been sent yet.
|
||||
idle,
|
||||
}
|
||||
|
||||
/// A negotiator that implements XEP-0388 SASL2. Alone, it does nothing. Has to be
|
||||
/// registered with other negotiators that register themselves against this one.
|
||||
class Sasl2Negotiator extends XmppFeatureNegotiatorBase {
|
||||
Sasl2Negotiator({
|
||||
this.userAgent,
|
||||
}) : super(100, false, sasl2Xmlns, sasl2Negotiator);
|
||||
|
||||
/// The user agent data that will be sent to the server when authenticating.
|
||||
final UserAgent? userAgent;
|
||||
|
||||
/// List of callbacks that are registered against us. Will be called once we get
|
||||
/// SASL2 features.
|
||||
final List<Sasl2FeaturesReceivedCallback> _featureCallbacks =
|
||||
List<Sasl2FeaturesReceivedCallback>.empty(growable: true);
|
||||
|
||||
/// List of SASL negotiators, sorted by their priority. The higher the priority, the
|
||||
/// lower its index.
|
||||
final List<SaslNegotiator> _saslNegotiators =
|
||||
List<SaslNegotiator>.empty(growable: true);
|
||||
|
||||
/// The state the SASL2 negotiator is currently in.
|
||||
Sasl2State _sasl2State = Sasl2State.idle;
|
||||
|
||||
/// The SASL negotiator that will negotiate authentication.
|
||||
SaslNegotiator? _currentSaslNegotiator;
|
||||
|
||||
void registerSaslNegotiator(SaslNegotiator negotiator) {
|
||||
_saslNegotiators
|
||||
..add(negotiator)
|
||||
..sort((a, b) => b.priority.compareTo(a.priority));
|
||||
}
|
||||
|
||||
void registerFeaturesCallback(Sasl2FeaturesReceivedCallback callback) {
|
||||
_featureCallbacks.add(callback);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<NegotiatorState, NegotiatorError>> negotiate(
|
||||
XMLNode nonza) async {
|
||||
switch (_sasl2State) {
|
||||
case Sasl2State.idle:
|
||||
final sasl2 = nonza.firstTag('authentication', xmlns: sasl2Xmlns)!;
|
||||
final mechanisms = XMLNode.xmlns(
|
||||
tag: 'mechanisms',
|
||||
xmlns: saslXmlns,
|
||||
children: sasl2.children.where((c) => c.tag == 'mechanism').toList(),
|
||||
);
|
||||
for (final negotiator in _saslNegotiators) {
|
||||
if (negotiator.matchesFeature([mechanisms])) {
|
||||
_currentSaslNegotiator = negotiator;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// We must have a SASL negotiator by now
|
||||
if (_currentSaslNegotiator == null) {
|
||||
return Result(NoSASLMechanismSelectedError());
|
||||
}
|
||||
|
||||
// Collect additional data by interested negotiators
|
||||
final children = List<XMLNode>.empty(growable: true);
|
||||
for (final callback in _featureCallbacks) {
|
||||
children.addAll(
|
||||
await callback(sasl2),
|
||||
);
|
||||
}
|
||||
|
||||
// Build the authenticate nonza
|
||||
final authenticate = XMLNode.xmlns(
|
||||
tag: 'authenticate',
|
||||
xmlns: sasl2Xmlns,
|
||||
attributes: {
|
||||
'mechanism': _currentSaslNegotiator!.mechanismName,
|
||||
},
|
||||
children: [
|
||||
if (userAgent != null) userAgent!.toXml(),
|
||||
|
||||
// TODO: Get the initial response
|
||||
XMLNode(
|
||||
tag: 'initial-response',
|
||||
),
|
||||
...children,
|
||||
],
|
||||
);
|
||||
attributes.sendNonza(authenticate);
|
||||
return const Result(NegotiatorState.ready);
|
||||
}
|
||||
|
||||
return const Result(NegotiatorState.ready);
|
||||
}
|
||||
|
||||
@override
|
||||
void reset() {
|
||||
_currentSaslNegotiator = null;
|
||||
_sasl2State = Sasl2State.idle;
|
||||
|
||||
super.reset();
|
||||
}
|
||||
}
|
@ -84,7 +84,7 @@ void main() {
|
||||
DiscoManager([]),
|
||||
EntityCapabilitiesManager('http://moxxmpp.example'),
|
||||
]);
|
||||
conn.registerFeatureNegotiators([
|
||||
await conn.registerFeatureNegotiators([
|
||||
SaslPlainNegotiator(),
|
||||
SaslScramNegotiator(10, '', '', ScramHashType.sha512),
|
||||
ResourceBindingNegotiator(),
|
||||
|
@ -169,12 +169,11 @@ void main() {
|
||||
MessageManager(),
|
||||
RosterManager(TestingRosterStateManager(null, [])),
|
||||
]);
|
||||
connection
|
||||
..registerFeatureNegotiators([
|
||||
await connection.registerFeatureNegotiators([
|
||||
SaslPlainNegotiator(),
|
||||
ResourceBindingNegotiator(),
|
||||
])
|
||||
..setConnectionSettings(TestingManagerHolder.settings);
|
||||
]);
|
||||
connection.setConnectionSettings(TestingManagerHolder.settings);
|
||||
await connection.connect(
|
||||
waitUntilLogin: true,
|
||||
);
|
||||
|
@ -298,7 +298,7 @@ void main() {
|
||||
CarbonsManager()..forceEnable(),
|
||||
EntityCapabilitiesManager('http://moxxmpp.example'),
|
||||
]);
|
||||
conn.registerFeatureNegotiators([
|
||||
await conn.registerFeatureNegotiators([
|
||||
SaslPlainNegotiator(),
|
||||
ResourceBindingNegotiator(),
|
||||
StreamManagementNegotiator(),
|
||||
@ -423,7 +423,7 @@ void main() {
|
||||
CarbonsManager()..forceEnable(),
|
||||
//EntityCapabilitiesManager('http://moxxmpp.example'),
|
||||
]);
|
||||
conn.registerFeatureNegotiators([
|
||||
await conn.registerFeatureNegotiators([
|
||||
SaslPlainNegotiator(),
|
||||
ResourceBindingNegotiator(),
|
||||
StreamManagementNegotiator(),
|
||||
@ -580,7 +580,7 @@ void main() {
|
||||
DiscoManager([]),
|
||||
StreamManagementManager(),
|
||||
]);
|
||||
conn.registerFeatureNegotiators([
|
||||
await conn.registerFeatureNegotiators([
|
||||
SaslPlainNegotiator(),
|
||||
ResourceBindingNegotiator(),
|
||||
StreamManagementNegotiator(),
|
||||
@ -674,7 +674,7 @@ void main() {
|
||||
DiscoManager([]),
|
||||
StreamManagementManager(),
|
||||
]);
|
||||
conn.registerFeatureNegotiators([
|
||||
await conn.registerFeatureNegotiators([
|
||||
SaslPlainNegotiator(),
|
||||
ResourceBindingNegotiator(),
|
||||
StreamManagementNegotiator(),
|
||||
@ -765,7 +765,7 @@ void main() {
|
||||
DiscoManager([]),
|
||||
StreamManagementManager(),
|
||||
]);
|
||||
conn.registerFeatureNegotiators([
|
||||
await conn.registerFeatureNegotiators([
|
||||
SaslPlainNegotiator(),
|
||||
ResourceBindingNegotiator(),
|
||||
StreamManagementNegotiator(),
|
||||
|
69
packages/moxxmpp/test/xeps/xep_0388_test.dart
Normal file
69
packages/moxxmpp/test/xeps/xep_0388_test.dart
Normal file
@ -0,0 +1,69 @@
|
||||
import 'package:moxxmpp/moxxmpp.dart';
|
||||
import 'package:test/test.dart';
|
||||
import '../helpers/logging.dart';
|
||||
import '../helpers/xmpp.dart';
|
||||
|
||||
void main() {
|
||||
initLogger();
|
||||
|
||||
test('Test simple SASL2 negotiation', () 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>
|
||||
</authentication>
|
||||
</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>",
|
||||
'',
|
||||
),
|
||||
]);
|
||||
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(),
|
||||
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<XmppError>(), false);
|
||||
});
|
||||
}
|
@ -140,7 +140,7 @@ void main() {
|
||||
StreamManagementManager(),
|
||||
EntityCapabilitiesManager('http://moxxmpp.example'),
|
||||
]);
|
||||
conn.registerFeatureNegotiators([
|
||||
await conn.registerFeatureNegotiators([
|
||||
SaslPlainNegotiator(),
|
||||
SaslScramNegotiator(10, '', '', ScramHashType.sha512),
|
||||
ResourceBindingNegotiator(),
|
||||
@ -195,7 +195,7 @@ void main() {
|
||||
DiscoManager([]),
|
||||
EntityCapabilitiesManager('http://moxxmpp.example'),
|
||||
]);
|
||||
conn.registerFeatureNegotiators([
|
||||
await conn.registerFeatureNegotiators([
|
||||
SaslPlainNegotiator(),
|
||||
]);
|
||||
|
||||
@ -254,7 +254,7 @@ void main() {
|
||||
DiscoManager([]),
|
||||
EntityCapabilitiesManager('http://moxxmpp.example'),
|
||||
]);
|
||||
conn.registerFeatureNegotiators([SaslPlainNegotiator()]);
|
||||
await conn.registerFeatureNegotiators([SaslPlainNegotiator()]);
|
||||
|
||||
conn.asBroadcastStream().listen((event) {
|
||||
if (event is AuthenticationFailedEvent &&
|
||||
@ -407,12 +407,11 @@ void main() {
|
||||
RosterManager(TestingRosterStateManager('', [])),
|
||||
DiscoManager([]),
|
||||
]);
|
||||
conn
|
||||
..registerFeatureNegotiators([
|
||||
await conn.registerFeatureNegotiators([
|
||||
// SaslPlainNegotiator(),
|
||||
ResourceBindingNegotiator(),
|
||||
])
|
||||
..setConnectionSettings(
|
||||
]);
|
||||
conn.setConnectionSettings(
|
||||
ConnectionSettings(
|
||||
jid: JID.fromString('testuser@example.org'),
|
||||
password: 'abc123',
|
||||
|
Loading…
Reference in New Issue
Block a user