chore(xep): Clean the SASL2 implementation

This commit is contained in:
PapaTutuWawa 2023-04-02 23:06:02 +02:00
parent 68e2a65dcf
commit 7fdd83ea69
14 changed files with 353 additions and 376 deletions

View File

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

View File

@ -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';

View File

@ -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!);

View File

@ -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'),
); );
} }

View File

@ -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';

View File

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

View File

@ -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 {

View File

@ -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

View File

@ -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.

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

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

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

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

View File

@ -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'>",
); );