chore(xep): Clean the SASL2 implementation
This commit is contained in:
parent
68e2a65dcf
commit
7fdd83ea69
@ -1,43 +0,0 @@
|
|||||||
import 'package:moxxmpp/moxxmpp.dart';
|
|
||||||
import 'package:moxxmpp_socket_tcp/moxxmpp_socket_tcp.dart';
|
|
||||||
import 'package:test/test.dart';
|
|
||||||
|
|
||||||
void main() async {
|
|
||||||
final conn = XmppConnection(
|
|
||||||
TestingReconnectionPolicy(),
|
|
||||||
AlwaysConnectedConnectivityManager(),
|
|
||||||
TCPSocketWrapper(),
|
|
||||||
)..setConnectionSettings(
|
|
||||||
ConnectionSettings(
|
|
||||||
jid: JID.fromString('testuser@localhost'),
|
|
||||||
password: 'abc123',
|
|
||||||
useDirectTLS: false,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
final csi = CSIManager();
|
|
||||||
await csi.setInactive(sendNonza: false);
|
|
||||||
await conn.registerManagers([
|
|
||||||
RosterManager(TestingRosterStateManager('', [])),
|
|
||||||
DiscoManager([]),
|
|
||||||
]);
|
|
||||||
await conn.registerFeatureNegotiators([
|
|
||||||
SaslPlainNegotiator(),
|
|
||||||
ResourceBindingNegotiator(),
|
|
||||||
FASTSaslNegotiator(),
|
|
||||||
Bind2Negotiator(),
|
|
||||||
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);
|
|
||||||
}
|
|
@ -79,6 +79,10 @@ export 'package:moxxmpp/src/xeps/xep_0384/types.dart';
|
|||||||
export 'package:moxxmpp/src/xeps/xep_0384/xep_0384.dart';
|
export 'package:moxxmpp/src/xeps/xep_0384/xep_0384.dart';
|
||||||
export 'package:moxxmpp/src/xeps/xep_0385.dart';
|
export 'package:moxxmpp/src/xeps/xep_0385.dart';
|
||||||
export 'package:moxxmpp/src/xeps/xep_0386.dart';
|
export 'package:moxxmpp/src/xeps/xep_0386.dart';
|
||||||
|
export 'package:moxxmpp/src/xeps/xep_0388/errors.dart';
|
||||||
|
export 'package:moxxmpp/src/xeps/xep_0388/negotiators.dart';
|
||||||
|
export 'package:moxxmpp/src/xeps/xep_0388/user_agent.dart';
|
||||||
|
export 'package:moxxmpp/src/xeps/xep_0388/xep_0388.dart';
|
||||||
export 'package:moxxmpp/src/xeps/xep_0414.dart';
|
export 'package:moxxmpp/src/xeps/xep_0414.dart';
|
||||||
export 'package:moxxmpp/src/xeps/xep_0424.dart';
|
export 'package:moxxmpp/src/xeps/xep_0424.dart';
|
||||||
export 'package:moxxmpp/src/xeps/xep_0444.dart';
|
export 'package:moxxmpp/src/xeps/xep_0444.dart';
|
||||||
|
@ -1158,8 +1158,8 @@ class XmppConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final smManager = getStreamManagementManager();
|
final smManager = getStreamManagementManager();
|
||||||
String? host = _connectionSettings.host;
|
var host = _connectionSettings.host;
|
||||||
int? port = _connectionSettings.port;
|
var port = _connectionSettings.port;
|
||||||
if (smManager?.state.streamResumptionLocation != null) {
|
if (smManager?.state.streamResumptionLocation != null) {
|
||||||
// TODO(Unknown): Maybe wrap this in a try catch?
|
// TODO(Unknown): Maybe wrap this in a try catch?
|
||||||
final parsed = Uri.parse(smManager!.state.streamResumptionLocation!);
|
final parsed = Uri.parse(smManager!.state.streamResumptionLocation!);
|
||||||
|
@ -5,9 +5,10 @@ import 'package:moxxmpp/src/negotiators/namespaces.dart';
|
|||||||
import 'package:moxxmpp/src/negotiators/negotiator.dart';
|
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/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';
|
||||||
|
import 'package:moxxmpp/src/xeps/xep_0388/negotiators.dart';
|
||||||
|
import 'package:moxxmpp/src/xeps/xep_0388/xep_0388.dart';
|
||||||
import 'package:saslprep/saslprep.dart';
|
import 'package:saslprep/saslprep.dart';
|
||||||
|
|
||||||
class SaslPlainAuthNonza extends SaslAuthNonza {
|
class SaslPlainAuthNonza extends SaslAuthNonza {
|
||||||
@ -89,7 +90,7 @@ class SaslPlainNegotiator extends Sasl2AuthenticationNegotiator {
|
|||||||
final settings = attributes.getConnectionSettings();
|
final settings = attributes.getConnectionSettings();
|
||||||
final prep = Saslprep.saslprep(settings.password);
|
final prep = Saslprep.saslprep(settings.password);
|
||||||
return base64.encode(
|
return base64.encode(
|
||||||
utf8.encode('\u0000${settings.jid.local}\u0000${prep}'),
|
utf8.encode('\u0000${settings.jid.local}\u0000$prep'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,9 +9,10 @@ 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/kv.dart';
|
import 'package:moxxmpp/src/negotiators/sasl/kv.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';
|
||||||
|
import 'package:moxxmpp/src/xeps/xep_0388/negotiators.dart';
|
||||||
|
import 'package:moxxmpp/src/xeps/xep_0388/xep_0388.dart';
|
||||||
import 'package:random_string/random_string.dart';
|
import 'package:random_string/random_string.dart';
|
||||||
import 'package:saslprep/saslprep.dart';
|
import 'package:saslprep/saslprep.dart';
|
||||||
|
|
||||||
|
@ -1,325 +1 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'package:moxxmpp/src/jid.dart';
|
|
||||||
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/errors.dart';
|
|
||||||
import 'package:moxxmpp/src/negotiators/sasl/negotiator.dart';
|
|
||||||
import 'package:moxxmpp/src/stringxml.dart';
|
|
||||||
import 'package:moxxmpp/src/types/result.dart';
|
|
||||||
|
|
||||||
/// A special type of [XmppFeatureNegotiatorBase] that is aware of SASL2.
|
|
||||||
abstract class Sasl2FeatureNegotiator extends XmppFeatureNegotiatorBase {
|
|
||||||
Sasl2FeatureNegotiator(
|
|
||||||
super.priority,
|
|
||||||
super.sendStreamHeaderWhenDone,
|
|
||||||
super.negotiatingXmlns,
|
|
||||||
super.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Called by the SASL2 negotiator when we received the SASL2 stream features
|
|
||||||
/// [sasl2Features]. The return value is a list of XML elements that should be
|
|
||||||
/// added to the SASL2 <authenticate /> nonza.
|
|
||||||
/// This method is only called when the <inline /> element contains an item with
|
|
||||||
/// xmlns equal to [negotiatingXmlns].
|
|
||||||
Future<List<XMLNode>> onSasl2FeaturesReceived(XMLNode sasl2Features);
|
|
||||||
|
|
||||||
/// Called by the SASL2 negotiator when the SASL2 negotiations are done. [response]
|
|
||||||
/// is the entire response nonza.
|
|
||||||
/// This method is only called when the previous <inline /> element contains an
|
|
||||||
/// item with xmlns equal to [negotiatingXmlns].
|
|
||||||
Future<Result<bool, NegotiatorError>> onSasl2Success(XMLNode response);
|
|
||||||
|
|
||||||
/// Called by the SASL2 negotiator when the SASL2 negotiations have failed. [response]
|
|
||||||
/// is the entire response nonza.
|
|
||||||
Future<void> onSasl2Failure(XMLNode response) async {}
|
|
||||||
|
|
||||||
/// Called by the SASL2 negotiator to find out whether the negotiator is willing
|
|
||||||
/// to inline a feature. [features] is the list of elements inside the <inline />
|
|
||||||
/// element.
|
|
||||||
bool canInlineFeature(List<XMLNode> features);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A special type of [SaslNegotiator] that is aware of SASL2.
|
|
||||||
abstract class Sasl2AuthenticationNegotiator extends SaslNegotiator
|
|
||||||
implements Sasl2FeatureNegotiator {
|
|
||||||
Sasl2AuthenticationNegotiator(super.priority, super.id, super.mechanismName);
|
|
||||||
|
|
||||||
/// Flag indicating whether this negotiator was chosen during SASL2 as the SASL
|
|
||||||
/// negotiator to use.
|
|
||||||
bool _pickedForSasl2 = false;
|
|
||||||
bool get pickedForSasl2 => _pickedForSasl2;
|
|
||||||
|
|
||||||
/// Perform a SASL step with [input] as the already parsed input data. Returns
|
|
||||||
/// the base64-encoded response data.
|
|
||||||
Future<String> getRawStep(String input);
|
|
||||||
|
|
||||||
/// Tells the negotiator that it has been selected as the SASL negotiator for SASL2.
|
|
||||||
void pickForSasl2() {
|
|
||||||
_pickedForSasl2 = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// When SASL2 fails, should we retry (true) or just fail (false).
|
|
||||||
/// Defaults to just returning false.
|
|
||||||
bool shouldRetrySasl() => false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void reset() {
|
|
||||||
_pickedForSasl2 = false;
|
|
||||||
|
|
||||||
super.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool canInlineFeature(List<XMLNode> features) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
// We have sent the <authenticate /> nonza.
|
|
||||||
authenticateSent,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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<Sasl2FeatureNegotiator> _featureNegotiators =
|
|
||||||
List<Sasl2FeatureNegotiator>.empty(growable: true);
|
|
||||||
|
|
||||||
/// List of SASL negotiators, sorted by their priority. The higher the priority, the
|
|
||||||
/// lower its index.
|
|
||||||
final List<Sasl2AuthenticationNegotiator> _saslNegotiators =
|
|
||||||
List<Sasl2AuthenticationNegotiator>.empty(growable: true);
|
|
||||||
|
|
||||||
/// The state the SASL2 negotiator is currently in.
|
|
||||||
Sasl2State _sasl2State = Sasl2State.idle;
|
|
||||||
|
|
||||||
/// The SASL negotiator that will negotiate authentication.
|
|
||||||
Sasl2AuthenticationNegotiator? _currentSaslNegotiator;
|
|
||||||
|
|
||||||
/// The SASL2 <authentication /> element we received with the stream features.
|
|
||||||
XMLNode? _sasl2Data;
|
|
||||||
final List<String> _activeSasl2Negotiators =
|
|
||||||
List<String>.empty(growable: true);
|
|
||||||
|
|
||||||
/// Register a SASL negotiator so that we can use that SASL implementation during
|
|
||||||
/// SASL2.
|
|
||||||
void registerSaslNegotiator(Sasl2AuthenticationNegotiator negotiator) {
|
|
||||||
_featureNegotiators.add(negotiator);
|
|
||||||
_saslNegotiators
|
|
||||||
..add(negotiator)
|
|
||||||
..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<XMLNode> features) {
|
|
||||||
// Only do SASL2 when the socket is secure
|
|
||||||
return attributes.getSocket().isSecure() && super.matchesFeature(features);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<Result<NegotiatorState, NegotiatorError>> negotiate(
|
|
||||||
XMLNode nonza,
|
|
||||||
) async {
|
|
||||||
switch (_sasl2State) {
|
|
||||||
case Sasl2State.idle:
|
|
||||||
_sasl2Data = nonza.firstTag('authentication', xmlns: sasl2Xmlns);
|
|
||||||
final mechanisms = XMLNode.xmlns(
|
|
||||||
tag: 'mechanisms',
|
|
||||||
xmlns: saslXmlns,
|
|
||||||
children:
|
|
||||||
_sasl2Data!.children.where((c) => c.tag == 'mechanism').toList(),
|
|
||||||
);
|
|
||||||
for (final negotiator in _saslNegotiators) {
|
|
||||||
if (negotiator.matchesFeature([mechanisms])) {
|
|
||||||
_currentSaslNegotiator = negotiator;
|
|
||||||
_currentSaslNegotiator!.pickForSasl2();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We must have a SASL negotiator by now
|
|
||||||
if (_currentSaslNegotiator == null) {
|
|
||||||
return Result(NoSASLMechanismSelectedError());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect additional data by interested negotiators
|
|
||||||
final inline = _sasl2Data!.firstTag('inline');
|
|
||||||
final children = List<XMLNode>.empty(growable: true);
|
|
||||||
if (inline != null && inline.children.isNotEmpty) {
|
|
||||||
for (final negotiator in _featureNegotiators) {
|
|
||||||
if (negotiator.canInlineFeature(inline.children)) {
|
|
||||||
_activeSasl2Negotiators.add(negotiator.id);
|
|
||||||
children.addAll(
|
|
||||||
await negotiator.onSasl2FeaturesReceived(_sasl2Data!),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build the authenticate nonza
|
|
||||||
final authenticate = XMLNode.xmlns(
|
|
||||||
tag: 'authenticate',
|
|
||||||
xmlns: sasl2Xmlns,
|
|
||||||
attributes: {
|
|
||||||
'mechanism': _currentSaslNegotiator!.mechanismName,
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
XMLNode(
|
|
||||||
tag: 'initial-response',
|
|
||||||
text: await _currentSaslNegotiator!.getRawStep(''),
|
|
||||||
),
|
|
||||||
if (userAgent != null) userAgent!.toXml(),
|
|
||||||
...children,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
_sasl2State = Sasl2State.authenticateSent;
|
|
||||||
attributes.sendNonza(authenticate);
|
|
||||||
return const Result(NegotiatorState.ready);
|
|
||||||
case Sasl2State.authenticateSent:
|
|
||||||
if (nonza.tag == 'success') {
|
|
||||||
// Tell the dependent negotiators about the result
|
|
||||||
final negotiators = _featureNegotiators
|
|
||||||
.where(
|
|
||||||
(negotiator) => _activeSasl2Negotiators.contains(negotiator.id),
|
|
||||||
)
|
|
||||||
.toList()
|
|
||||||
..add(_currentSaslNegotiator!);
|
|
||||||
for (final negotiator in negotiators) {
|
|
||||||
final result = await negotiator.onSasl2Success(nonza);
|
|
||||||
if (!result.isType<bool>()) {
|
|
||||||
return Result(result.get<NegotiatorError>());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We're done
|
|
||||||
attributes.setAuthenticated();
|
|
||||||
attributes.removeNegotiatingFeature(saslXmlns);
|
|
||||||
|
|
||||||
// Check if we also received a resource with the SASL2 success
|
|
||||||
final jid = JID.fromString(
|
|
||||||
nonza.firstTag('authorization-identifier')!.innerText(),
|
|
||||||
);
|
|
||||||
if (!jid.isBare()) {
|
|
||||||
attributes.setResource(jid.resource);
|
|
||||||
}
|
|
||||||
|
|
||||||
return const Result(NegotiatorState.done);
|
|
||||||
} else if (nonza.tag == 'challenge') {
|
|
||||||
// We still have to negotiate
|
|
||||||
final challenge = nonza.innerText();
|
|
||||||
final response = XMLNode.xmlns(
|
|
||||||
tag: 'response',
|
|
||||||
xmlns: sasl2Xmlns,
|
|
||||||
text: await _currentSaslNegotiator!.getRawStep(challenge),
|
|
||||||
);
|
|
||||||
attributes.sendNonza(response);
|
|
||||||
} else if (nonza.tag == 'failure') {
|
|
||||||
final negotiators = _featureNegotiators
|
|
||||||
.where(
|
|
||||||
(negotiator) => _activeSasl2Negotiators.contains(negotiator.id),
|
|
||||||
)
|
|
||||||
.toList()
|
|
||||||
..add(_currentSaslNegotiator!);
|
|
||||||
for (final negotiator in negotiators) {
|
|
||||||
await negotiator.onSasl2Failure(nonza);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we should retry and, if we should, reset the current
|
|
||||||
// negotiator, this negotiator, and retry.
|
|
||||||
if (_currentSaslNegotiator!.shouldRetrySasl()) {
|
|
||||||
_currentSaslNegotiator!.reset();
|
|
||||||
reset();
|
|
||||||
return const Result(
|
|
||||||
NegotiatorState.retryLater,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Result(
|
|
||||||
SaslError.fromFailure(nonza),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return const Result(NegotiatorState.ready);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void reset() {
|
|
||||||
_currentSaslNegotiator = null;
|
|
||||||
_sasl2State = Sasl2State.idle;
|
|
||||||
_sasl2Data = null;
|
|
||||||
_activeSasl2Negotiators.clear();
|
|
||||||
|
|
||||||
super.reset();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -4,9 +4,10 @@ import 'package:moxxmpp/src/events.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';
|
||||||
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';
|
||||||
|
import 'package:moxxmpp/src/xeps/xep_0388/negotiators.dart';
|
||||||
|
import 'package:moxxmpp/src/xeps/xep_0388/xep_0388.dart';
|
||||||
|
|
||||||
/// This event is triggered whenever a new FAST token is received.
|
/// This event is triggered whenever a new FAST token is received.
|
||||||
class NewFASTTokenReceivedEvent extends XmppEvent {
|
class NewFASTTokenReceivedEvent extends XmppEvent {
|
||||||
|
@ -6,7 +6,6 @@ import 'package:moxxmpp/src/managers/namespaces.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';
|
||||||
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';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0198/nonzas.dart';
|
import 'package:moxxmpp/src/xeps/xep_0198/nonzas.dart';
|
||||||
@ -14,6 +13,8 @@ import 'package:moxxmpp/src/xeps/xep_0198/state.dart';
|
|||||||
import 'package:moxxmpp/src/xeps/xep_0198/xep_0198.dart';
|
import 'package:moxxmpp/src/xeps/xep_0198/xep_0198.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0352.dart';
|
import 'package:moxxmpp/src/xeps/xep_0352.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0386.dart';
|
import 'package:moxxmpp/src/xeps/xep_0386.dart';
|
||||||
|
import 'package:moxxmpp/src/xeps/xep_0388/negotiators.dart';
|
||||||
|
import 'package:moxxmpp/src/xeps/xep_0388/xep_0388.dart';
|
||||||
|
|
||||||
enum _StreamManagementNegotiatorState {
|
enum _StreamManagementNegotiatorState {
|
||||||
// We have not done anything yet
|
// We have not done anything yet
|
||||||
|
@ -3,9 +3,10 @@ import 'package:meta/meta.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';
|
||||||
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';
|
||||||
|
import 'package:moxxmpp/src/xeps/xep_0388/negotiators.dart';
|
||||||
|
import 'package:moxxmpp/src/xeps/xep_0388/xep_0388.dart';
|
||||||
|
|
||||||
/// An interface that allows registering against Bind2's feature list in order to
|
/// An interface that allows registering against Bind2's feature list in order to
|
||||||
/// negotiate features inline with Bind2.
|
/// negotiate features inline with Bind2.
|
||||||
|
8
packages/moxxmpp/lib/src/xeps/xep_0388/errors.dart
Normal file
8
packages/moxxmpp/lib/src/xeps/xep_0388/errors.dart
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import 'package:moxxmpp/src/negotiators/negotiator.dart';
|
||||||
|
|
||||||
|
/// Triggered by the SASL2 negotiator when no SASL mechanism was chosen during
|
||||||
|
/// negotiation.
|
||||||
|
class NoSASLMechanismSelectedError extends NegotiatorError {
|
||||||
|
@override
|
||||||
|
bool isRecoverable() => false;
|
||||||
|
}
|
72
packages/moxxmpp/lib/src/xeps/xep_0388/negotiators.dart
Normal file
72
packages/moxxmpp/lib/src/xeps/xep_0388/negotiators.dart
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
/// A special type of [XmppFeatureNegotiatorBase] that is aware of SASL2.
|
||||||
|
abstract class Sasl2FeatureNegotiator extends XmppFeatureNegotiatorBase {
|
||||||
|
Sasl2FeatureNegotiator(
|
||||||
|
super.priority,
|
||||||
|
super.sendStreamHeaderWhenDone,
|
||||||
|
super.negotiatingXmlns,
|
||||||
|
super.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Called by the SASL2 negotiator when we received the SASL2 stream features
|
||||||
|
/// [sasl2Features]. The return value is a list of XML elements that should be
|
||||||
|
/// added to the SASL2 <authenticate /> nonza.
|
||||||
|
/// This method is only called when the <inline /> element contains an item with
|
||||||
|
/// xmlns equal to [negotiatingXmlns].
|
||||||
|
Future<List<XMLNode>> onSasl2FeaturesReceived(XMLNode sasl2Features);
|
||||||
|
|
||||||
|
/// Called by the SASL2 negotiator when the SASL2 negotiations are done. [response]
|
||||||
|
/// is the entire response nonza.
|
||||||
|
/// This method is only called when the previous <inline /> element contains an
|
||||||
|
/// item with xmlns equal to [negotiatingXmlns].
|
||||||
|
Future<Result<bool, NegotiatorError>> onSasl2Success(XMLNode response);
|
||||||
|
|
||||||
|
/// Called by the SASL2 negotiator when the SASL2 negotiations have failed. [response]
|
||||||
|
/// is the entire response nonza.
|
||||||
|
Future<void> onSasl2Failure(XMLNode response) async {}
|
||||||
|
|
||||||
|
/// Called by the SASL2 negotiator to find out whether the negotiator is willing
|
||||||
|
/// to inline a feature. [features] is the list of elements inside the <inline />
|
||||||
|
/// element.
|
||||||
|
bool canInlineFeature(List<XMLNode> features);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A special type of [SaslNegotiator] that is aware of SASL2.
|
||||||
|
abstract class Sasl2AuthenticationNegotiator extends SaslNegotiator
|
||||||
|
implements Sasl2FeatureNegotiator {
|
||||||
|
Sasl2AuthenticationNegotiator(super.priority, super.id, super.mechanismName);
|
||||||
|
|
||||||
|
/// Flag indicating whether this negotiator was chosen during SASL2 as the SASL
|
||||||
|
/// negotiator to use.
|
||||||
|
bool _pickedForSasl2 = false;
|
||||||
|
bool get pickedForSasl2 => _pickedForSasl2;
|
||||||
|
|
||||||
|
/// Perform a SASL step with [input] as the already parsed input data. Returns
|
||||||
|
/// the base64-encoded response data.
|
||||||
|
Future<String> getRawStep(String input);
|
||||||
|
|
||||||
|
/// Tells the negotiator that it has been selected as the SASL negotiator for SASL2.
|
||||||
|
void pickForSasl2() {
|
||||||
|
_pickedForSasl2 = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// When SASL2 fails, should we retry (true) or just fail (false).
|
||||||
|
/// Defaults to just returning false.
|
||||||
|
bool shouldRetrySasl() => false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void reset() {
|
||||||
|
_pickedForSasl2 = false;
|
||||||
|
|
||||||
|
super.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool canInlineFeature(List<XMLNode> features) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
46
packages/moxxmpp/lib/src/xeps/xep_0388/user_agent.dart
Normal file
46
packages/moxxmpp/lib/src/xeps/xep_0388/user_agent.dart
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import 'package:moxxmpp/src/stringxml.dart';
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
209
packages/moxxmpp/lib/src/xeps/xep_0388/xep_0388.dart
Normal file
209
packages/moxxmpp/lib/src/xeps/xep_0388/xep_0388.dart
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
import 'package:moxxmpp/src/jid.dart';
|
||||||
|
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/errors.dart';
|
||||||
|
import 'package:moxxmpp/src/stringxml.dart';
|
||||||
|
import 'package:moxxmpp/src/types/result.dart';
|
||||||
|
import 'package:moxxmpp/src/xeps/xep_0388/errors.dart';
|
||||||
|
import 'package:moxxmpp/src/xeps/xep_0388/negotiators.dart';
|
||||||
|
import 'package:moxxmpp/src/xeps/xep_0388/user_agent.dart';
|
||||||
|
|
||||||
|
/// The state of the SASL2 negotiation
|
||||||
|
enum Sasl2State {
|
||||||
|
// No request has been sent yet.
|
||||||
|
idle,
|
||||||
|
// We have sent the <authenticate /> nonza.
|
||||||
|
authenticateSent,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<Sasl2FeatureNegotiator> _featureNegotiators =
|
||||||
|
List<Sasl2FeatureNegotiator>.empty(growable: true);
|
||||||
|
|
||||||
|
/// List of SASL negotiators, sorted by their priority. The higher the priority, the
|
||||||
|
/// lower its index.
|
||||||
|
final List<Sasl2AuthenticationNegotiator> _saslNegotiators =
|
||||||
|
List<Sasl2AuthenticationNegotiator>.empty(growable: true);
|
||||||
|
|
||||||
|
/// The state the SASL2 negotiator is currently in.
|
||||||
|
Sasl2State _sasl2State = Sasl2State.idle;
|
||||||
|
|
||||||
|
/// The SASL negotiator that will negotiate authentication.
|
||||||
|
Sasl2AuthenticationNegotiator? _currentSaslNegotiator;
|
||||||
|
|
||||||
|
/// The SASL2 <authentication /> element we received with the stream features.
|
||||||
|
XMLNode? _sasl2Data;
|
||||||
|
final List<String> _activeSasl2Negotiators =
|
||||||
|
List<String>.empty(growable: true);
|
||||||
|
|
||||||
|
/// Register a SASL negotiator so that we can use that SASL implementation during
|
||||||
|
/// SASL2.
|
||||||
|
void registerSaslNegotiator(Sasl2AuthenticationNegotiator negotiator) {
|
||||||
|
_featureNegotiators.add(negotiator);
|
||||||
|
_saslNegotiators
|
||||||
|
..add(negotiator)
|
||||||
|
..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<XMLNode> features) {
|
||||||
|
// Only do SASL2 when the socket is secure
|
||||||
|
return attributes.getSocket().isSecure() && super.matchesFeature(features);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Result<NegotiatorState, NegotiatorError>> negotiate(
|
||||||
|
XMLNode nonza,
|
||||||
|
) async {
|
||||||
|
switch (_sasl2State) {
|
||||||
|
case Sasl2State.idle:
|
||||||
|
_sasl2Data = nonza.firstTag('authentication', xmlns: sasl2Xmlns);
|
||||||
|
final mechanisms = XMLNode.xmlns(
|
||||||
|
tag: 'mechanisms',
|
||||||
|
xmlns: saslXmlns,
|
||||||
|
children:
|
||||||
|
_sasl2Data!.children.where((c) => c.tag == 'mechanism').toList(),
|
||||||
|
);
|
||||||
|
for (final negotiator in _saslNegotiators) {
|
||||||
|
if (negotiator.matchesFeature([mechanisms])) {
|
||||||
|
_currentSaslNegotiator = negotiator;
|
||||||
|
_currentSaslNegotiator!.pickForSasl2();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We must have a SASL negotiator by now
|
||||||
|
if (_currentSaslNegotiator == null) {
|
||||||
|
return Result(NoSASLMechanismSelectedError());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect additional data by interested negotiators
|
||||||
|
final inline = _sasl2Data!.firstTag('inline');
|
||||||
|
final children = List<XMLNode>.empty(growable: true);
|
||||||
|
if (inline != null && inline.children.isNotEmpty) {
|
||||||
|
for (final negotiator in _featureNegotiators) {
|
||||||
|
if (negotiator.canInlineFeature(inline.children)) {
|
||||||
|
_activeSasl2Negotiators.add(negotiator.id);
|
||||||
|
children.addAll(
|
||||||
|
await negotiator.onSasl2FeaturesReceived(_sasl2Data!),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the authenticate nonza
|
||||||
|
final authenticate = XMLNode.xmlns(
|
||||||
|
tag: 'authenticate',
|
||||||
|
xmlns: sasl2Xmlns,
|
||||||
|
attributes: {
|
||||||
|
'mechanism': _currentSaslNegotiator!.mechanismName,
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
XMLNode(
|
||||||
|
tag: 'initial-response',
|
||||||
|
text: await _currentSaslNegotiator!.getRawStep(''),
|
||||||
|
),
|
||||||
|
if (userAgent != null) userAgent!.toXml(),
|
||||||
|
...children,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
_sasl2State = Sasl2State.authenticateSent;
|
||||||
|
attributes.sendNonza(authenticate);
|
||||||
|
return const Result(NegotiatorState.ready);
|
||||||
|
case Sasl2State.authenticateSent:
|
||||||
|
if (nonza.tag == 'success') {
|
||||||
|
// Tell the dependent negotiators about the result
|
||||||
|
final negotiators = _featureNegotiators
|
||||||
|
.where(
|
||||||
|
(negotiator) => _activeSasl2Negotiators.contains(negotiator.id),
|
||||||
|
)
|
||||||
|
.toList()
|
||||||
|
..add(_currentSaslNegotiator!);
|
||||||
|
for (final negotiator in negotiators) {
|
||||||
|
final result = await negotiator.onSasl2Success(nonza);
|
||||||
|
if (!result.isType<bool>()) {
|
||||||
|
return Result(result.get<NegotiatorError>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're done
|
||||||
|
attributes.setAuthenticated();
|
||||||
|
attributes.removeNegotiatingFeature(saslXmlns);
|
||||||
|
|
||||||
|
// Check if we also received a resource with the SASL2 success
|
||||||
|
final jid = JID.fromString(
|
||||||
|
nonza.firstTag('authorization-identifier')!.innerText(),
|
||||||
|
);
|
||||||
|
if (!jid.isBare()) {
|
||||||
|
attributes.setResource(jid.resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
return const Result(NegotiatorState.done);
|
||||||
|
} else if (nonza.tag == 'challenge') {
|
||||||
|
// We still have to negotiate
|
||||||
|
final challenge = nonza.innerText();
|
||||||
|
final response = XMLNode.xmlns(
|
||||||
|
tag: 'response',
|
||||||
|
xmlns: sasl2Xmlns,
|
||||||
|
text: await _currentSaslNegotiator!.getRawStep(challenge),
|
||||||
|
);
|
||||||
|
attributes.sendNonza(response);
|
||||||
|
} else if (nonza.tag == 'failure') {
|
||||||
|
final negotiators = _featureNegotiators
|
||||||
|
.where(
|
||||||
|
(negotiator) => _activeSasl2Negotiators.contains(negotiator.id),
|
||||||
|
)
|
||||||
|
.toList()
|
||||||
|
..add(_currentSaslNegotiator!);
|
||||||
|
for (final negotiator in negotiators) {
|
||||||
|
await negotiator.onSasl2Failure(nonza);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we should retry and, if we should, reset the current
|
||||||
|
// negotiator, this negotiator, and retry.
|
||||||
|
if (_currentSaslNegotiator!.shouldRetrySasl()) {
|
||||||
|
_currentSaslNegotiator!.reset();
|
||||||
|
reset();
|
||||||
|
return const Result(
|
||||||
|
NegotiatorState.retryLater,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result(
|
||||||
|
SaslError.fromFailure(nonza),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return const Result(NegotiatorState.ready);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void reset() {
|
||||||
|
_currentSaslNegotiator = null;
|
||||||
|
_sasl2State = Sasl2State.idle;
|
||||||
|
_sasl2Data = null;
|
||||||
|
_activeSasl2Negotiators.clear();
|
||||||
|
|
||||||
|
super.reset();
|
||||||
|
}
|
||||||
|
}
|
@ -23,7 +23,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
StreamHeaderNonza('uwu.server').toXml(),
|
StreamHeaderNonza(JID.fromString('uwu.server')).toXml(),
|
||||||
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='uwu.server' xml:lang='en'>",
|
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='uwu.server' xml:lang='en'>",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user