feat(xep): Implement negotiating PLAIN via SASL2

This commit is contained in:
PapaTutuWawa 2023-03-31 20:53:06 +02:00
parent 2e60e9841e
commit 7ab3f4f0d9
8 changed files with 138 additions and 34 deletions

View File

@ -253,6 +253,19 @@ class XmppConnection {
}
}
// Mark the current connection as authenticated.
void _setAuthenticated() {
_sendEvent(AuthenticationSuccessEvent());
_isAuthenticated = true;
}
/// Remove [feature] from the stream features we are currently negotiating.
void _removeNegotiatingFeature(String feature) {
_streamFeatures.removeWhere((node) {
return node.attributes['xmlns'] == feature;
});
}
/// Register a list of negotiator with the connection.
Future<void> registerFeatureNegotiators(
List<XmppFeatureNegotiatorBase> negotiators) async {
@ -268,6 +281,8 @@ class XmppConnection {
() => _connectionSettings.jid.withResource(_resource),
() => _socket,
() => _isAuthenticated,
_setAuthenticated,
_removeNegotiatingFeature,
),
);
_featureNegotiators[negotiator.id] = negotiator;
@ -900,10 +915,7 @@ class XmppConnection {
_streamFeatures.clear();
_sendStreamHeader();
} else {
_streamFeatures.removeWhere((node) {
return node.attributes['xmlns'] ==
_currentNegotiator!.negotiatingXmlns;
});
_removeNegotiatingFeature(_currentNegotiator!.negotiatingXmlns);
_currentNegotiator = null;
if (_isMandatoryNegotiationDone(_streamFeatures) &&
@ -1024,11 +1036,6 @@ class XmppConnection {
_log.finest('Resetting _serverFeatures');
_serverFeatures.clear();
} else if (event is AuthenticationSuccessEvent) {
_log.finest(
'Received AuthenticationSuccessEvent. Setting _isAuthenticated to true',
);
_isAuthenticated = true;
}
for (final manager in _xmppManagers.values) {

View File

@ -35,6 +35,8 @@ class NegotiatorAttributes {
this.getFullJID,
this.getSocket,
this.isAuthenticated,
this.setAuthenticated,
this.removeNegotiatingFeature,
);
/// Sends the nonza nonza and optionally redacts it in logs if redact is not null.
@ -61,6 +63,13 @@ class NegotiatorAttributes {
/// Returns true if the stream is authenticated. Returns false if not.
final bool Function() isAuthenticated;
/// Sets the authentication state of the connection to true.
final void Function() setAuthenticated;
/// Remove a stream feature from our internal cache. This is useful for when you
/// negotiated a feature for another negotiator, like SASL2.
final void Function(String) removeNegotiatingFeature;
}
abstract class XmppFeatureNegotiatorBase {

View File

@ -11,14 +11,14 @@ import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart';
class SaslPlainAuthNonza extends SaslAuthNonza {
SaslPlainAuthNonza(String username, String password)
SaslPlainAuthNonza(String data)
: super(
'PLAIN',
base64.encode(utf8.encode('\u0000$username\u0000$password')),
data,
);
}
class SaslPlainNegotiator extends SaslNegotiator {
class SaslPlainNegotiator extends Sasl2AuthenticationNegotiator {
SaslPlainNegotiator()
: _authSent = false,
_log = Logger('SaslPlainNegotiator'),
@ -48,10 +48,10 @@ class SaslPlainNegotiator extends SaslNegotiator {
XMLNode nonza,
) async {
if (!_authSent) {
final settings = attributes.getConnectionSettings();
final data = await getRawStep('');
attributes.sendNonza(
SaslPlainAuthNonza(settings.jid.local, settings.password),
redact: SaslPlainAuthNonza('******', '******').toXml(),
SaslPlainAuthNonza(data),
redact: SaslPlainAuthNonza('******').toXml(),
);
_authSent = true;
return const Result(NegotiatorState.ready);
@ -59,6 +59,7 @@ class SaslPlainNegotiator extends SaslNegotiator {
final tag = nonza.tag;
if (tag == 'success') {
await attributes.sendEvent(AuthenticationSuccessEvent());
attributes.setAuthenticated();
return const Result(NegotiatorState.done);
} else {
// We assume it's a <failure/>
@ -84,4 +85,21 @@ class SaslPlainNegotiator extends SaslNegotiator {
.getNegotiatorById<Sasl2Negotiator>(sasl2Negotiator)
?.registerSaslNegotiator(this);
}
@override
Future<String> getRawStep(String input) async {
final settings = attributes.getConnectionSettings();
return base64.encode(
utf8.encode('\u0000${settings.jid.local}\u0000${settings.password}'));
}
@override
Future<void> onSasl2Success(XMLNode response) async {
state = NegotiatorState.done;
}
@override
Future<List<XMLNode>> onSasl2FeaturesReceived(XMLNode sasl2Features) async {
return [];
}
}

View File

@ -299,7 +299,7 @@ class SaslScramNegotiator extends SaslNegotiator {
);
}
await attributes.sendEvent(AuthenticationSuccessEvent());
attributes.setAuthenticated();
return const Result(NegotiatorState.done);
case ScramState.error:
return Result(

View File

@ -1,3 +1,4 @@
import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/negotiators/namespaces.dart';
import 'package:moxxmpp/src/negotiators/negotiator.dart';
@ -7,6 +8,34 @@ import 'package:moxxmpp/src/types/result.dart';
typedef Sasl2FeaturesReceivedCallback = Future<List<XMLNode>> Function(XMLNode);
abstract class Sasl2FeatureNegotiator extends XmppFeatureNegotiatorBase {
Sasl2FeatureNegotiator(
int priority,
bool sendStreamHeaderWhenDone,
String negotiatingXmlns,
String id,
) : super(priority, sendStreamHeaderWhenDone, negotiatingXmlns, 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.
Future<List<XMLNode>> onSasl2FeaturesReceived(XMLNode sasl2Features);
/// Called by the SASL2 negotiator when the SASL2 negotiations are done. [response]
/// is the entire response nonza.
Future<void> onSasl2Success(XMLNode response);
}
abstract class Sasl2AuthenticationNegotiator extends SaslNegotiator
implements Sasl2FeatureNegotiator {
Sasl2AuthenticationNegotiator(int priority, String id, String mechanismName)
: super(priority, id, mechanismName);
/// Perform a SASL step with [input] as the already parsed input data. Returns
/// the base64-encoded response data.
Future<String> getRawStep(String input);
}
class NoSASLMechanismSelectedError extends NegotiatorError {
@override
bool isRecoverable() => false;
@ -58,6 +87,8 @@ class UserAgent {
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
@ -72,28 +103,29 @@ class Sasl2Negotiator extends XmppFeatureNegotiatorBase {
/// 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);
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<SaslNegotiator> _saslNegotiators =
List<SaslNegotiator>.empty(growable: true);
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.
SaslNegotiator? _currentSaslNegotiator;
Sasl2AuthenticationNegotiator? _currentSaslNegotiator;
void registerSaslNegotiator(SaslNegotiator negotiator) {
void registerSaslNegotiator(Sasl2AuthenticationNegotiator negotiator) {
_featureNegotiators.add(negotiator);
_saslNegotiators
..add(negotiator)
..sort((a, b) => b.priority.compareTo(a.priority));
}
void registerFeaturesCallback(Sasl2FeaturesReceivedCallback callback) {
_featureCallbacks.add(callback);
void registerNegotiator(Sasl2FeatureNegotiator negotiator) {
_featureNegotiators.add(negotiator);
}
@override
@ -121,9 +153,9 @@ class Sasl2Negotiator extends XmppFeatureNegotiatorBase {
// Collect additional data by interested negotiators
final children = List<XMLNode>.empty(growable: true);
for (final callback in _featureCallbacks) {
for (final negotiator in _featureNegotiators) {
children.addAll(
await callback(sasl2),
await negotiator.onSasl2FeaturesReceived(sasl2),
);
}
@ -136,16 +168,29 @@ class Sasl2Negotiator extends XmppFeatureNegotiatorBase {
},
children: [
if (userAgent != null) userAgent!.toXml(),
// TODO: Get the initial response
XMLNode(
tag: 'initial-response',
text: await _currentSaslNegotiator!.getRawStep(''),
),
...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
for (final negotiator in _featureNegotiators) {
await negotiator.onSasl2Success(nonza);
}
// We're done
attributes.setAuthenticated();
attributes.removeNegotiatingFeature(saslXmlns);
return const Result(NegotiatorState.done);
}
}
return const Result(NegotiatorState.ready);

View File

@ -57,6 +57,8 @@ void main() {
() => JID.fromString('user@server'),
() => fakeSocket,
() => false,
() {},
(_) {},
),
);
@ -150,6 +152,8 @@ void main() {
() => JID.fromString('user@server'),
() => fakeSocket,
() => false,
() {},
(_) {},
),
);
@ -198,6 +202,8 @@ void main() {
() => JID.fromString('user@server'),
() => fakeSocket,
() => false,
() {},
(_) {},
),
);
@ -236,6 +242,8 @@ void main() {
() => JID.fromString('user@server'),
() => fakeSocket,
() => false,
() {},
(_) {},
),
);
@ -277,6 +285,8 @@ void main() {
() => JID.fromString('user@server'),
() => fakeSocket,
() => false,
() {},
(_) {},
),
);

View File

@ -391,10 +391,10 @@ void main() {
"<enable xmlns='urn:xmpp:sm:3' resume='true' />",
'<enabled xmlns="urn:xmpp:sm:3" id="some-long-sm-id" resume="true" />',
),
// StringExpectation(
// "<presence xmlns='jabber:client' from='polynomdivision@test.server/MU29eEZn'><show>chat</show></presence>",
// '<iq type="result" />',
// ),
StringExpectation(
"<presence xmlns='jabber:client' from='polynomdivision@test.server/MU29eEZn'><show>chat</show></presence>",
'<iq type="result" />',
),
StanzaExpectation(
"<iq to='user@example.com' type='get' id='a' xmlns='jabber:client' />",
"<iq from='user@example.com' type='result' id='a' />",
@ -450,7 +450,7 @@ void main() {
addFrom: StanzaFromType.none,
);
expect(sm.state.s2c, /*2*/ 1);
expect(sm.state.s2c, 2);
});
});

View File

@ -24,11 +24,26 @@ void main() {
<authentication xmlns='urn:xmpp:sasl:2'>
<mechanism>PLAIN</mechanism>
</authentication>
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
<required/>
</bind>
</stream:features>''',
),
StanzaExpectation(
"<authenticate xmlns='urn:xmpp:sasl:2' mechanism='PLAIN'><user-agent id='d4565fa7-4d72-4749-b3d3-740edbf87770'><software>moxxmpp</software><device>PapaTutuWawa's awesome device</device></user-agent><initial-response>AHBvbHlub21kaXZpc2lvbgBhYWFh</initial-response></authenticate>",
'',
'''
<success xmlns='urn:xmpp:sasl:2'>
<authorization-identifier>polynomdivision@test.server</authorization-identifier>
</success>
''',
),
StanzaExpectation(
"<iq xmlns='jabber:client' type='set' id='aaaa'><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind' /></iq>",
'''
'<iq xmlns="jabber:client" type="result" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><jid>polynomdivision@test.server/MU29eEZn</jid></bind></iq>',
''',
adjustId: true,
ignoreId: true,
),
]);
final conn = XmppConnection(