feat(xep): Begin work on SASL2

This commit is contained in:
PapaTutuWawa 2023-03-31 19:02:57 +02:00
parent 52ad9a7ddb
commit 2e60e9841e
12 changed files with 282 additions and 29 deletions

View File

@ -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/negotiator.dart';
export 'package:moxxmpp/src/negotiators/sasl/plain.dart'; export 'package:moxxmpp/src/negotiators/sasl/plain.dart';
export 'package:moxxmpp/src/negotiators/sasl/scram.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/negotiators/starttls.dart';
export 'package:moxxmpp/src/ping.dart'; export 'package:moxxmpp/src/ping.dart';
export 'package:moxxmpp/src/presence.dart'; export 'package:moxxmpp/src/presence.dart';

View File

@ -254,7 +254,8 @@ class XmppConnection {
} }
/// Register a list of negotiator with the connection. /// 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) { for (final negotiator in negotiators) {
_log.finest('Registering ${negotiator.id}'); _log.finest('Registering ${negotiator.id}');
negotiator.register( negotiator.register(
@ -273,6 +274,10 @@ class XmppConnection {
} }
_log.finest('Negotiators registered'); _log.finest('Negotiators registered');
for (final negotiator in _featureNegotiators.values) {
await negotiator.postRegisterCallback();
}
} }
/// Reset all registered negotiators. /// Reset all registered negotiators.
@ -399,7 +404,7 @@ class XmppConnection {
// Close the socket // Close the socket
_socket.close(); _socket.close();
if (!error.isRecoverable()) { if (!error.isRecoverable()) {
// We cannot recover this error // We cannot recover this error
_log.severe( _log.severe(

View File

@ -116,6 +116,9 @@ const omemoBundlesXmlns = 'urn:xmpp:omemo:2:bundles';
// XEP-0385 // XEP-0385
const simsXmlns = 'urn:xmpp:sims:1'; const simsXmlns = 'urn:xmpp:sims:1';
// XEP-0388
const sasl2Xmlns = 'urn:xmpp:sasl:2';
// XEP-0420 // XEP-0420
const sceXmlns = 'urn:xmpp:sce:1'; const sceXmlns = 'urn:xmpp:sce:1';

View File

@ -7,3 +7,4 @@ const rosterNegotiator = 'im.moxxmpp.core.roster';
const resourceBindingNegotiator = 'im.moxxmpp.core.resource'; const resourceBindingNegotiator = 'im.moxxmpp.core.resource';
const streamManagementNegotiator = 'im.moxxmpp.xeps.sm'; const streamManagementNegotiator = 'im.moxxmpp.xeps.sm';
const startTlsNegotiator = 'im.moxxmpp.core.starttls'; const startTlsNegotiator = 'im.moxxmpp.core.starttls';
const sasl2Negotiator = 'org.moxxmpp.sasl.sasl2';

View File

@ -1,3 +1,4 @@
import 'package:meta/meta.dart';
import 'package:moxlib/moxlib.dart'; import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/errors.dart'; import 'package:moxxmpp/src/errors.dart';
import 'package:moxxmpp/src/events.dart'; import 'package:moxxmpp/src/events.dart';
@ -120,5 +121,11 @@ abstract class XmppFeatureNegotiatorBase {
state = NegotiatorState.ready; state = NegotiatorState.ready;
} }
@protected
NegotiatorAttributes get attributes => _attributes; NegotiatorAttributes get attributes => _attributes;
/// Run after all negotiators are registered. Useful for registering callbacks against
/// other negotiators.
@visibleForOverriding
Future<void> postRegisterCallback() async {}
} }

View File

@ -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/errors.dart';
import 'package:moxxmpp/src/negotiators/sasl/negotiator.dart'; import 'package:moxxmpp/src/negotiators/sasl/negotiator.dart';
import 'package:moxxmpp/src/negotiators/sasl/nonza.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/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart'; import 'package:moxxmpp/src/types/result.dart';
@ -76,4 +77,11 @@ class SaslPlainNegotiator extends SaslNegotiator {
super.reset(); super.reset();
} }
@override
Future<void> postRegisterCallback() async {
attributes
.getNegotiatorById<Sasl2Negotiator>(sasl2Negotiator)
?.registerSaslNegotiator(this);
}
} }

View 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();
}
}

View File

@ -84,7 +84,7 @@ void main() {
DiscoManager([]), DiscoManager([]),
EntityCapabilitiesManager('http://moxxmpp.example'), EntityCapabilitiesManager('http://moxxmpp.example'),
]); ]);
conn.registerFeatureNegotiators([ await conn.registerFeatureNegotiators([
SaslPlainNegotiator(), SaslPlainNegotiator(),
SaslScramNegotiator(10, '', '', ScramHashType.sha512), SaslScramNegotiator(10, '', '', ScramHashType.sha512),
ResourceBindingNegotiator(), ResourceBindingNegotiator(),

View File

@ -169,12 +169,11 @@ void main() {
MessageManager(), MessageManager(),
RosterManager(TestingRosterStateManager(null, [])), RosterManager(TestingRosterStateManager(null, [])),
]); ]);
connection await connection.registerFeatureNegotiators([
..registerFeatureNegotiators([ SaslPlainNegotiator(),
SaslPlainNegotiator(), ResourceBindingNegotiator(),
ResourceBindingNegotiator(), ]);
]) connection.setConnectionSettings(TestingManagerHolder.settings);
..setConnectionSettings(TestingManagerHolder.settings);
await connection.connect( await connection.connect(
waitUntilLogin: true, waitUntilLogin: true,
); );

View File

@ -298,7 +298,7 @@ void main() {
CarbonsManager()..forceEnable(), CarbonsManager()..forceEnable(),
EntityCapabilitiesManager('http://moxxmpp.example'), EntityCapabilitiesManager('http://moxxmpp.example'),
]); ]);
conn.registerFeatureNegotiators([ await conn.registerFeatureNegotiators([
SaslPlainNegotiator(), SaslPlainNegotiator(),
ResourceBindingNegotiator(), ResourceBindingNegotiator(),
StreamManagementNegotiator(), StreamManagementNegotiator(),
@ -423,7 +423,7 @@ void main() {
CarbonsManager()..forceEnable(), CarbonsManager()..forceEnable(),
//EntityCapabilitiesManager('http://moxxmpp.example'), //EntityCapabilitiesManager('http://moxxmpp.example'),
]); ]);
conn.registerFeatureNegotiators([ await conn.registerFeatureNegotiators([
SaslPlainNegotiator(), SaslPlainNegotiator(),
ResourceBindingNegotiator(), ResourceBindingNegotiator(),
StreamManagementNegotiator(), StreamManagementNegotiator(),
@ -580,7 +580,7 @@ void main() {
DiscoManager([]), DiscoManager([]),
StreamManagementManager(), StreamManagementManager(),
]); ]);
conn.registerFeatureNegotiators([ await conn.registerFeatureNegotiators([
SaslPlainNegotiator(), SaslPlainNegotiator(),
ResourceBindingNegotiator(), ResourceBindingNegotiator(),
StreamManagementNegotiator(), StreamManagementNegotiator(),
@ -674,7 +674,7 @@ void main() {
DiscoManager([]), DiscoManager([]),
StreamManagementManager(), StreamManagementManager(),
]); ]);
conn.registerFeatureNegotiators([ await conn.registerFeatureNegotiators([
SaslPlainNegotiator(), SaslPlainNegotiator(),
ResourceBindingNegotiator(), ResourceBindingNegotiator(),
StreamManagementNegotiator(), StreamManagementNegotiator(),
@ -765,7 +765,7 @@ void main() {
DiscoManager([]), DiscoManager([]),
StreamManagementManager(), StreamManagementManager(),
]); ]);
conn.registerFeatureNegotiators([ await conn.registerFeatureNegotiators([
SaslPlainNegotiator(), SaslPlainNegotiator(),
ResourceBindingNegotiator(), ResourceBindingNegotiator(),
StreamManagementNegotiator(), StreamManagementNegotiator(),

View 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);
});
}

View File

@ -140,7 +140,7 @@ void main() {
StreamManagementManager(), StreamManagementManager(),
EntityCapabilitiesManager('http://moxxmpp.example'), EntityCapabilitiesManager('http://moxxmpp.example'),
]); ]);
conn.registerFeatureNegotiators([ await conn.registerFeatureNegotiators([
SaslPlainNegotiator(), SaslPlainNegotiator(),
SaslScramNegotiator(10, '', '', ScramHashType.sha512), SaslScramNegotiator(10, '', '', ScramHashType.sha512),
ResourceBindingNegotiator(), ResourceBindingNegotiator(),
@ -195,7 +195,7 @@ void main() {
DiscoManager([]), DiscoManager([]),
EntityCapabilitiesManager('http://moxxmpp.example'), EntityCapabilitiesManager('http://moxxmpp.example'),
]); ]);
conn.registerFeatureNegotiators([ await conn.registerFeatureNegotiators([
SaslPlainNegotiator(), SaslPlainNegotiator(),
]); ]);
@ -254,7 +254,7 @@ void main() {
DiscoManager([]), DiscoManager([]),
EntityCapabilitiesManager('http://moxxmpp.example'), EntityCapabilitiesManager('http://moxxmpp.example'),
]); ]);
conn.registerFeatureNegotiators([SaslPlainNegotiator()]); await conn.registerFeatureNegotiators([SaslPlainNegotiator()]);
conn.asBroadcastStream().listen((event) { conn.asBroadcastStream().listen((event) {
if (event is AuthenticationFailedEvent && if (event is AuthenticationFailedEvent &&
@ -407,18 +407,17 @@ void main() {
RosterManager(TestingRosterStateManager('', [])), RosterManager(TestingRosterStateManager('', [])),
DiscoManager([]), DiscoManager([]),
]); ]);
conn await conn.registerFeatureNegotiators([
..registerFeatureNegotiators([ // SaslPlainNegotiator(),
// SaslPlainNegotiator(), ResourceBindingNegotiator(),
ResourceBindingNegotiator(), ]);
]) conn.setConnectionSettings(
..setConnectionSettings( ConnectionSettings(
ConnectionSettings( jid: JID.fromString('testuser@example.org'),
jid: JID.fromString('testuser@example.org'), password: 'abc123',
password: 'abc123', useDirectTLS: false,
useDirectTLS: false, ),
), );
);
final result = await conn.connect( final result = await conn.connect(
waitUntilLogin: true, waitUntilLogin: true,