feat(xep): Implement negotiating PLAIN via SASL2
This commit is contained in:
parent
2e60e9841e
commit
7ab3f4f0d9
@ -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) {
|
||||
|
@ -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 {
|
||||
|
@ -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 [];
|
||||
}
|
||||
}
|
||||
|
@ -299,7 +299,7 @@ class SaslScramNegotiator extends SaslNegotiator {
|
||||
);
|
||||
}
|
||||
|
||||
await attributes.sendEvent(AuthenticationSuccessEvent());
|
||||
attributes.setAuthenticated();
|
||||
return const Result(NegotiatorState.done);
|
||||
case ScramState.error:
|
||||
return Result(
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
() {},
|
||||
(_) {},
|
||||
),
|
||||
);
|
||||
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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(
|
||||
|
Loading…
Reference in New Issue
Block a user