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.
|
/// Register a list of negotiator with the connection.
|
||||||
Future<void> registerFeatureNegotiators(
|
Future<void> registerFeatureNegotiators(
|
||||||
List<XmppFeatureNegotiatorBase> negotiators) async {
|
List<XmppFeatureNegotiatorBase> negotiators) async {
|
||||||
@ -268,6 +281,8 @@ class XmppConnection {
|
|||||||
() => _connectionSettings.jid.withResource(_resource),
|
() => _connectionSettings.jid.withResource(_resource),
|
||||||
() => _socket,
|
() => _socket,
|
||||||
() => _isAuthenticated,
|
() => _isAuthenticated,
|
||||||
|
_setAuthenticated,
|
||||||
|
_removeNegotiatingFeature,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
_featureNegotiators[negotiator.id] = negotiator;
|
_featureNegotiators[negotiator.id] = negotiator;
|
||||||
@ -900,10 +915,7 @@ class XmppConnection {
|
|||||||
_streamFeatures.clear();
|
_streamFeatures.clear();
|
||||||
_sendStreamHeader();
|
_sendStreamHeader();
|
||||||
} else {
|
} else {
|
||||||
_streamFeatures.removeWhere((node) {
|
_removeNegotiatingFeature(_currentNegotiator!.negotiatingXmlns);
|
||||||
return node.attributes['xmlns'] ==
|
|
||||||
_currentNegotiator!.negotiatingXmlns;
|
|
||||||
});
|
|
||||||
_currentNegotiator = null;
|
_currentNegotiator = null;
|
||||||
|
|
||||||
if (_isMandatoryNegotiationDone(_streamFeatures) &&
|
if (_isMandatoryNegotiationDone(_streamFeatures) &&
|
||||||
@ -1024,11 +1036,6 @@ class XmppConnection {
|
|||||||
|
|
||||||
_log.finest('Resetting _serverFeatures');
|
_log.finest('Resetting _serverFeatures');
|
||||||
_serverFeatures.clear();
|
_serverFeatures.clear();
|
||||||
} else if (event is AuthenticationSuccessEvent) {
|
|
||||||
_log.finest(
|
|
||||||
'Received AuthenticationSuccessEvent. Setting _isAuthenticated to true',
|
|
||||||
);
|
|
||||||
_isAuthenticated = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (final manager in _xmppManagers.values) {
|
for (final manager in _xmppManagers.values) {
|
||||||
|
@ -35,6 +35,8 @@ class NegotiatorAttributes {
|
|||||||
this.getFullJID,
|
this.getFullJID,
|
||||||
this.getSocket,
|
this.getSocket,
|
||||||
this.isAuthenticated,
|
this.isAuthenticated,
|
||||||
|
this.setAuthenticated,
|
||||||
|
this.removeNegotiatingFeature,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Sends the nonza nonza and optionally redacts it in logs if redact is not null.
|
/// 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.
|
/// Returns true if the stream is authenticated. Returns false if not.
|
||||||
final bool Function() isAuthenticated;
|
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 {
|
abstract class XmppFeatureNegotiatorBase {
|
||||||
|
@ -11,14 +11,14 @@ import 'package:moxxmpp/src/stringxml.dart';
|
|||||||
import 'package:moxxmpp/src/types/result.dart';
|
import 'package:moxxmpp/src/types/result.dart';
|
||||||
|
|
||||||
class SaslPlainAuthNonza extends SaslAuthNonza {
|
class SaslPlainAuthNonza extends SaslAuthNonza {
|
||||||
SaslPlainAuthNonza(String username, String password)
|
SaslPlainAuthNonza(String data)
|
||||||
: super(
|
: super(
|
||||||
'PLAIN',
|
'PLAIN',
|
||||||
base64.encode(utf8.encode('\u0000$username\u0000$password')),
|
data,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class SaslPlainNegotiator extends SaslNegotiator {
|
class SaslPlainNegotiator extends Sasl2AuthenticationNegotiator {
|
||||||
SaslPlainNegotiator()
|
SaslPlainNegotiator()
|
||||||
: _authSent = false,
|
: _authSent = false,
|
||||||
_log = Logger('SaslPlainNegotiator'),
|
_log = Logger('SaslPlainNegotiator'),
|
||||||
@ -48,10 +48,10 @@ class SaslPlainNegotiator extends SaslNegotiator {
|
|||||||
XMLNode nonza,
|
XMLNode nonza,
|
||||||
) async {
|
) async {
|
||||||
if (!_authSent) {
|
if (!_authSent) {
|
||||||
final settings = attributes.getConnectionSettings();
|
final data = await getRawStep('');
|
||||||
attributes.sendNonza(
|
attributes.sendNonza(
|
||||||
SaslPlainAuthNonza(settings.jid.local, settings.password),
|
SaslPlainAuthNonza(data),
|
||||||
redact: SaslPlainAuthNonza('******', '******').toXml(),
|
redact: SaslPlainAuthNonza('******').toXml(),
|
||||||
);
|
);
|
||||||
_authSent = true;
|
_authSent = true;
|
||||||
return const Result(NegotiatorState.ready);
|
return const Result(NegotiatorState.ready);
|
||||||
@ -59,6 +59,7 @@ class SaslPlainNegotiator extends SaslNegotiator {
|
|||||||
final tag = nonza.tag;
|
final tag = nonza.tag;
|
||||||
if (tag == 'success') {
|
if (tag == 'success') {
|
||||||
await attributes.sendEvent(AuthenticationSuccessEvent());
|
await attributes.sendEvent(AuthenticationSuccessEvent());
|
||||||
|
attributes.setAuthenticated();
|
||||||
return const Result(NegotiatorState.done);
|
return const Result(NegotiatorState.done);
|
||||||
} else {
|
} else {
|
||||||
// We assume it's a <failure/>
|
// We assume it's a <failure/>
|
||||||
@ -84,4 +85,21 @@ class SaslPlainNegotiator extends SaslNegotiator {
|
|||||||
.getNegotiatorById<Sasl2Negotiator>(sasl2Negotiator)
|
.getNegotiatorById<Sasl2Negotiator>(sasl2Negotiator)
|
||||||
?.registerSaslNegotiator(this);
|
?.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);
|
return const Result(NegotiatorState.done);
|
||||||
case ScramState.error:
|
case ScramState.error:
|
||||||
return Result(
|
return Result(
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
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';
|
||||||
@ -7,6 +8,34 @@ import 'package:moxxmpp/src/types/result.dart';
|
|||||||
|
|
||||||
typedef Sasl2FeaturesReceivedCallback = Future<List<XMLNode>> Function(XMLNode);
|
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 {
|
class NoSASLMechanismSelectedError extends NegotiatorError {
|
||||||
@override
|
@override
|
||||||
bool isRecoverable() => false;
|
bool isRecoverable() => false;
|
||||||
@ -58,6 +87,8 @@ class UserAgent {
|
|||||||
enum Sasl2State {
|
enum Sasl2State {
|
||||||
// No request has been sent yet.
|
// No request has been sent yet.
|
||||||
idle,
|
idle,
|
||||||
|
// We have sent the <authenticate /> nonza.
|
||||||
|
authenticateSent,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A negotiator that implements XEP-0388 SASL2. Alone, it does nothing. Has to be
|
/// 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
|
/// List of callbacks that are registered against us. Will be called once we get
|
||||||
/// SASL2 features.
|
/// SASL2 features.
|
||||||
final List<Sasl2FeaturesReceivedCallback> _featureCallbacks =
|
final List<Sasl2FeatureNegotiator> _featureNegotiators =
|
||||||
List<Sasl2FeaturesReceivedCallback>.empty(growable: true);
|
List<Sasl2FeatureNegotiator>.empty(growable: true);
|
||||||
|
|
||||||
/// List of SASL negotiators, sorted by their priority. The higher the priority, the
|
/// List of SASL negotiators, sorted by their priority. The higher the priority, the
|
||||||
/// lower its index.
|
/// lower its index.
|
||||||
final List<SaslNegotiator> _saslNegotiators =
|
final List<Sasl2AuthenticationNegotiator> _saslNegotiators =
|
||||||
List<SaslNegotiator>.empty(growable: true);
|
List<Sasl2AuthenticationNegotiator>.empty(growable: true);
|
||||||
|
|
||||||
/// The state the SASL2 negotiator is currently in.
|
/// The state the SASL2 negotiator is currently in.
|
||||||
Sasl2State _sasl2State = Sasl2State.idle;
|
Sasl2State _sasl2State = Sasl2State.idle;
|
||||||
|
|
||||||
/// The SASL negotiator that will negotiate authentication.
|
/// The SASL negotiator that will negotiate authentication.
|
||||||
SaslNegotiator? _currentSaslNegotiator;
|
Sasl2AuthenticationNegotiator? _currentSaslNegotiator;
|
||||||
|
|
||||||
void registerSaslNegotiator(SaslNegotiator negotiator) {
|
void registerSaslNegotiator(Sasl2AuthenticationNegotiator negotiator) {
|
||||||
|
_featureNegotiators.add(negotiator);
|
||||||
_saslNegotiators
|
_saslNegotiators
|
||||||
..add(negotiator)
|
..add(negotiator)
|
||||||
..sort((a, b) => b.priority.compareTo(a.priority));
|
..sort((a, b) => b.priority.compareTo(a.priority));
|
||||||
}
|
}
|
||||||
|
|
||||||
void registerFeaturesCallback(Sasl2FeaturesReceivedCallback callback) {
|
void registerNegotiator(Sasl2FeatureNegotiator negotiator) {
|
||||||
_featureCallbacks.add(callback);
|
_featureNegotiators.add(negotiator);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -121,9 +153,9 @@ class Sasl2Negotiator extends XmppFeatureNegotiatorBase {
|
|||||||
|
|
||||||
// Collect additional data by interested negotiators
|
// Collect additional data by interested negotiators
|
||||||
final children = List<XMLNode>.empty(growable: true);
|
final children = List<XMLNode>.empty(growable: true);
|
||||||
for (final callback in _featureCallbacks) {
|
for (final negotiator in _featureNegotiators) {
|
||||||
children.addAll(
|
children.addAll(
|
||||||
await callback(sasl2),
|
await negotiator.onSasl2FeaturesReceived(sasl2),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,16 +168,29 @@ class Sasl2Negotiator extends XmppFeatureNegotiatorBase {
|
|||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
if (userAgent != null) userAgent!.toXml(),
|
if (userAgent != null) userAgent!.toXml(),
|
||||||
|
|
||||||
// TODO: Get the initial response
|
|
||||||
XMLNode(
|
XMLNode(
|
||||||
tag: 'initial-response',
|
tag: 'initial-response',
|
||||||
|
text: await _currentSaslNegotiator!.getRawStep(''),
|
||||||
),
|
),
|
||||||
...children,
|
...children,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
_sasl2State = Sasl2State.authenticateSent;
|
||||||
attributes.sendNonza(authenticate);
|
attributes.sendNonza(authenticate);
|
||||||
return const Result(NegotiatorState.ready);
|
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);
|
return const Result(NegotiatorState.ready);
|
||||||
|
@ -57,6 +57,8 @@ void main() {
|
|||||||
() => JID.fromString('user@server'),
|
() => JID.fromString('user@server'),
|
||||||
() => fakeSocket,
|
() => fakeSocket,
|
||||||
() => false,
|
() => false,
|
||||||
|
() {},
|
||||||
|
(_) {},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -150,6 +152,8 @@ void main() {
|
|||||||
() => JID.fromString('user@server'),
|
() => JID.fromString('user@server'),
|
||||||
() => fakeSocket,
|
() => fakeSocket,
|
||||||
() => false,
|
() => false,
|
||||||
|
() {},
|
||||||
|
(_) {},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -198,6 +202,8 @@ void main() {
|
|||||||
() => JID.fromString('user@server'),
|
() => JID.fromString('user@server'),
|
||||||
() => fakeSocket,
|
() => fakeSocket,
|
||||||
() => false,
|
() => false,
|
||||||
|
() {},
|
||||||
|
(_) {},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -236,6 +242,8 @@ void main() {
|
|||||||
() => JID.fromString('user@server'),
|
() => JID.fromString('user@server'),
|
||||||
() => fakeSocket,
|
() => fakeSocket,
|
||||||
() => false,
|
() => false,
|
||||||
|
() {},
|
||||||
|
(_) {},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -277,6 +285,8 @@ void main() {
|
|||||||
() => JID.fromString('user@server'),
|
() => JID.fromString('user@server'),
|
||||||
() => fakeSocket,
|
() => fakeSocket,
|
||||||
() => false,
|
() => false,
|
||||||
|
() {},
|
||||||
|
(_) {},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -391,10 +391,10 @@ void main() {
|
|||||||
"<enable xmlns='urn:xmpp:sm:3' resume='true' />",
|
"<enable xmlns='urn:xmpp:sm:3' resume='true' />",
|
||||||
'<enabled xmlns="urn:xmpp:sm:3" id="some-long-sm-id" resume="true" />',
|
'<enabled xmlns="urn:xmpp:sm:3" id="some-long-sm-id" resume="true" />',
|
||||||
),
|
),
|
||||||
// StringExpectation(
|
StringExpectation(
|
||||||
// "<presence xmlns='jabber:client' from='polynomdivision@test.server/MU29eEZn'><show>chat</show></presence>",
|
"<presence xmlns='jabber:client' from='polynomdivision@test.server/MU29eEZn'><show>chat</show></presence>",
|
||||||
// '<iq type="result" />',
|
'<iq type="result" />',
|
||||||
// ),
|
),
|
||||||
StanzaExpectation(
|
StanzaExpectation(
|
||||||
"<iq to='user@example.com' type='get' id='a' xmlns='jabber:client' />",
|
"<iq to='user@example.com' type='get' id='a' xmlns='jabber:client' />",
|
||||||
"<iq from='user@example.com' type='result' id='a' />",
|
"<iq from='user@example.com' type='result' id='a' />",
|
||||||
@ -450,7 +450,7 @@ void main() {
|
|||||||
addFrom: StanzaFromType.none,
|
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'>
|
<authentication xmlns='urn:xmpp:sasl:2'>
|
||||||
<mechanism>PLAIN</mechanism>
|
<mechanism>PLAIN</mechanism>
|
||||||
</authentication>
|
</authentication>
|
||||||
|
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
|
||||||
|
<required/>
|
||||||
|
</bind>
|
||||||
</stream:features>''',
|
</stream:features>''',
|
||||||
),
|
),
|
||||||
StanzaExpectation(
|
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>",
|
"<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(
|
final conn = XmppConnection(
|
||||||
|
Loading…
Reference in New Issue
Block a user