feat(xep): Implement XEP-0114
This commit is contained in:
parent
63c84d9479
commit
f6abf3d5b5
@ -52,5 +52,9 @@ log = {
|
||||
|
||||
pidfile = "/tmp/prosody.pid"
|
||||
|
||||
component_ports = { 8888 }
|
||||
component_interfaces = { '127.0.0.1' }
|
||||
VirtualHost "localhost"
|
||||
|
||||
Component "component.localhost"
|
||||
component_secret = "abc123"
|
47
integration_tests/test/component_test.dart
Normal file
47
integration_tests/test/component_test.dart
Normal file
@ -0,0 +1,47 @@
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:moxxmpp/moxxmpp.dart';
|
||||
import 'package:moxxmpp_socket_tcp/moxxmpp_socket_tcp.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
class TestingTCPSocketWrapper extends TCPSocketWrapper {
|
||||
@override
|
||||
bool onBadCertificate(dynamic certificate, String domain) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
Logger.root.level = Level.ALL;
|
||||
Logger.root.onRecord.listen((record) {
|
||||
// ignore: avoid_print
|
||||
print(
|
||||
'[${record.level.name}] (${record.loggerName}) ${record.time}: ${record.message}',
|
||||
);
|
||||
});
|
||||
|
||||
test('Test connecting to prosody as a component', () async {
|
||||
final conn = XmppConnection(
|
||||
TestingReconnectionPolicy(),
|
||||
AlwaysConnectedConnectivityManager(),
|
||||
ComponentToServerNegotiator(),
|
||||
TestingTCPSocketWrapper(),
|
||||
)..connectionSettings = ConnectionSettings(
|
||||
jid: JID.fromString('component.localhost'),
|
||||
password: 'abc123',
|
||||
useDirectTLS: false,
|
||||
host: '127.0.0.1',
|
||||
port: 8888,
|
||||
);
|
||||
await conn.registerManagers([
|
||||
RosterManager(TestingRosterStateManager('', [])),
|
||||
DiscoManager([]),
|
||||
]);
|
||||
|
||||
final result = await conn.connect(
|
||||
waitUntilLogin: true,
|
||||
shouldReconnect: false,
|
||||
enableReconnectOnSuccess: false,
|
||||
);
|
||||
expect(result.isType<bool>(), true);
|
||||
});
|
||||
}
|
@ -19,20 +19,19 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
test('Test authenticating against Prosody with SASL2, Bind2, and FAST', () async {
|
||||
test('Test authenticating against Prosody with SASL2, Bind2, and FAST',
|
||||
() async {
|
||||
final conn = XmppConnection(
|
||||
TestingReconnectionPolicy(),
|
||||
AlwaysConnectedConnectivityManager(),
|
||||
ClientToServerNegotiator(),
|
||||
TestingTCPSocketWrapper(),
|
||||
)..setConnectionSettings(
|
||||
ConnectionSettings(
|
||||
jid: JID.fromString('testuser@localhost'),
|
||||
password: 'abc123',
|
||||
useDirectTLS: false,
|
||||
|
||||
host: '127.0.0.1',
|
||||
port: 5222,
|
||||
),
|
||||
)..connectionSettings = ConnectionSettings(
|
||||
jid: JID.fromString('testuser@localhost'),
|
||||
password: 'abc123',
|
||||
useDirectTLS: false,
|
||||
host: '127.0.0.1',
|
||||
port: 5222,
|
||||
);
|
||||
final csi = CSIManager();
|
||||
await csi.setInactive(sendNonza: false);
|
||||
@ -61,7 +60,16 @@ void main() {
|
||||
enableReconnectOnSuccess: false,
|
||||
);
|
||||
expect(result.isType<bool>(), true);
|
||||
expect(conn.getNegotiatorById<Sasl2Negotiator>(sasl2Negotiator)!.state, NegotiatorState.done);
|
||||
expect(conn.getNegotiatorById<FASTSaslNegotiator>(saslFASTNegotiator)!.fastToken != null, true,);
|
||||
expect(
|
||||
conn.getNegotiatorById<Sasl2Negotiator>(sasl2Negotiator)!.state,
|
||||
NegotiatorState.done,
|
||||
);
|
||||
expect(
|
||||
conn
|
||||
.getNegotiatorById<FASTSaslNegotiator>(saslFASTNegotiator)!
|
||||
.fastToken !=
|
||||
null,
|
||||
true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -5,6 +5,9 @@ export 'package:moxxmpp/src/connection_errors.dart';
|
||||
export 'package:moxxmpp/src/connectivity.dart';
|
||||
export 'package:moxxmpp/src/errors.dart';
|
||||
export 'package:moxxmpp/src/events.dart';
|
||||
export 'package:moxxmpp/src/handlers/base.dart';
|
||||
export 'package:moxxmpp/src/handlers/client.dart';
|
||||
export 'package:moxxmpp/src/handlers/component.dart';
|
||||
export 'package:moxxmpp/src/iq.dart';
|
||||
export 'package:moxxmpp/src/jid.dart';
|
||||
export 'package:moxxmpp/src/managers/attributes.dart';
|
||||
@ -15,7 +18,6 @@ export 'package:moxxmpp/src/managers/namespaces.dart';
|
||||
export 'package:moxxmpp/src/managers/priorities.dart';
|
||||
export 'package:moxxmpp/src/message.dart';
|
||||
export 'package:moxxmpp/src/namespaces.dart';
|
||||
export 'package:moxxmpp/src/negotiators/handler.dart';
|
||||
export 'package:moxxmpp/src/negotiators/manager.dart';
|
||||
export 'package:moxxmpp/src/negotiators/namespaces.dart';
|
||||
export 'package:moxxmpp/src/negotiators/negotiator.dart';
|
||||
|
@ -8,15 +8,13 @@ import 'package:moxxmpp/src/connection_errors.dart';
|
||||
import 'package:moxxmpp/src/connectivity.dart';
|
||||
import 'package:moxxmpp/src/errors.dart';
|
||||
import 'package:moxxmpp/src/events.dart';
|
||||
import 'package:moxxmpp/src/handlers/base.dart';
|
||||
import 'package:moxxmpp/src/iq.dart';
|
||||
import 'package:moxxmpp/src/jid.dart';
|
||||
import 'package:moxxmpp/src/managers/attributes.dart';
|
||||
import 'package:moxxmpp/src/managers/base.dart';
|
||||
import 'package:moxxmpp/src/managers/data.dart';
|
||||
import 'package:moxxmpp/src/managers/handlers.dart';
|
||||
import 'package:moxxmpp/src/managers/namespaces.dart';
|
||||
import 'package:moxxmpp/src/namespaces.dart';
|
||||
import 'package:moxxmpp/src/negotiators/handler.dart';
|
||||
import 'package:moxxmpp/src/negotiators/namespaces.dart';
|
||||
import 'package:moxxmpp/src/negotiators/negotiator.dart';
|
||||
import 'package:moxxmpp/src/presence.dart';
|
||||
@ -63,23 +61,6 @@ enum StanzaFromType {
|
||||
none,
|
||||
}
|
||||
|
||||
/// Nonza describing the XMPP stream header.
|
||||
class StreamHeaderNonza extends XMLNode {
|
||||
StreamHeaderNonza(JID jid)
|
||||
: super(
|
||||
tag: 'stream:stream',
|
||||
attributes: <String, String>{
|
||||
'xmlns': stanzaXmlns,
|
||||
'version': '1.0',
|
||||
'xmlns:stream': streamXmlns,
|
||||
'to': jid.domain,
|
||||
'from': jid.toBare().toString(),
|
||||
'xml:lang': 'en',
|
||||
},
|
||||
closeTag: false,
|
||||
);
|
||||
}
|
||||
|
||||
/// This class is a connection to the server.
|
||||
class XmppConnection {
|
||||
XmppConnection(
|
||||
@ -99,8 +80,9 @@ class XmppConnection {
|
||||
_negotiationsHandler.register(
|
||||
_onNegotiationsDone,
|
||||
handleError,
|
||||
_sendStreamHeaders,
|
||||
() => _isAuthenticated,
|
||||
sendRawXML,
|
||||
() => connectionSettings,
|
||||
);
|
||||
|
||||
_socketStream = _socket.getDataStream();
|
||||
@ -768,7 +750,7 @@ class XmppConnection {
|
||||
/// Called whenever we receive data that has been parsed as XML.
|
||||
Future<void> handleXmlStream(XmlStreamBufferObject event) async {
|
||||
if (event is XmlStreamBufferHeader) {
|
||||
_negotiationsHandler.setStreamHeaderId(event.attributes['id']);
|
||||
await _negotiationsHandler.negotiate(event);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -799,11 +781,11 @@ class XmppConnection {
|
||||
// prevent this issue.
|
||||
await _negotiationLock.synchronized(() async {
|
||||
if (_routingState != RoutingState.negotiating) {
|
||||
unawaited(handleXmlStream(XmlStreamBufferElement(node)));
|
||||
unawaited(handleXmlStream(event));
|
||||
return;
|
||||
}
|
||||
|
||||
await _negotiationsHandler.negotiate(node);
|
||||
await _negotiationsHandler.negotiate(event);
|
||||
});
|
||||
break;
|
||||
case RoutingState.handleStanzas:
|
||||
@ -833,21 +815,6 @@ class XmppConnection {
|
||||
_eventStreamController.add(event);
|
||||
}
|
||||
|
||||
/// Sends a stream header to the socket.
|
||||
void _sendStreamHeaders() {
|
||||
_socket.write(
|
||||
XMLNode(
|
||||
tag: 'xml',
|
||||
attributes: {'version': '1.0'},
|
||||
closeTag: false,
|
||||
isDeclaration: true,
|
||||
children: [
|
||||
StreamHeaderNonza(connectionSettings.jid),
|
||||
],
|
||||
).toXml(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Attempt to gracefully close the session
|
||||
Future<void> disconnect() async {
|
||||
await _disconnect(state: XmppConnectionState.notConnected);
|
||||
@ -955,7 +922,7 @@ class XmppConnection {
|
||||
await _setConnectionState(XmppConnectionState.connecting);
|
||||
_updateRoutingState(RoutingState.negotiating);
|
||||
_isAuthenticated = false;
|
||||
_sendStreamHeaders();
|
||||
_negotiationsHandler.sendStreamHeader();
|
||||
|
||||
if (waitUntilLogin) {
|
||||
return _connectionCompleter!.future;
|
||||
|
@ -46,3 +46,15 @@ class NoAuthenticatorAvailableError extends XmppConnectionError {
|
||||
@override
|
||||
bool isRecoverable() => false;
|
||||
}
|
||||
|
||||
/// Returned by the negotiation handler if unexpected data has been received
|
||||
class UnexpectedDataError extends XmppConnectionError {
|
||||
@override
|
||||
bool isRecoverable() => false;
|
||||
}
|
||||
|
||||
/// Returned by the ComponentToServerNegotiator if the handshake is not successful.
|
||||
class InvalidHandshakeCredentialsError extends XmppConnectionError {
|
||||
@override
|
||||
bool isRecoverable() => false;
|
||||
}
|
||||
|
121
packages/moxxmpp/lib/src/handlers/base.dart
Normal file
121
packages/moxxmpp/lib/src/handlers/base.dart
Normal file
@ -0,0 +1,121 @@
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:moxxmpp/src/buffer.dart';
|
||||
import 'package:moxxmpp/src/errors.dart';
|
||||
import 'package:moxxmpp/src/events.dart';
|
||||
import 'package:moxxmpp/src/negotiators/negotiator.dart';
|
||||
import 'package:moxxmpp/src/settings.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
|
||||
/// A callback for when the [NegotiationsHandler] is done.
|
||||
typedef NegotiationsDoneCallback = Future<void> Function();
|
||||
|
||||
/// A callback for the case that an error occurs while negotiating.
|
||||
typedef ErrorCallback = Future<void> Function(XmppError);
|
||||
|
||||
/// Return true if the current connection is authenticated. If not, return false.
|
||||
typedef IsAuthenticatedFunction = bool Function();
|
||||
|
||||
/// Send a nonza on the stream
|
||||
typedef SendNonzaFunction = void Function(XMLNode);
|
||||
|
||||
/// Returns the connection settings.
|
||||
typedef GetConnectionSettingsFunction = ConnectionSettings Function();
|
||||
|
||||
/// This class implements the stream feature negotiation for XmppConnection.
|
||||
abstract class NegotiationsHandler {
|
||||
@protected
|
||||
late final Logger log;
|
||||
|
||||
/// Map of all negotiators registered against the handler.
|
||||
@protected
|
||||
final Map<String, XmppFeatureNegotiatorBase> negotiators = {};
|
||||
|
||||
/// Function that is called once the negotiator is done with its stream negotiations.
|
||||
@protected
|
||||
late final NegotiationsDoneCallback onNegotiationsDone;
|
||||
|
||||
/// XmppConnection's handleError method.
|
||||
@protected
|
||||
late final ErrorCallback handleError;
|
||||
|
||||
/// Returns true if the connection is authenticated. If not, returns false.
|
||||
@protected
|
||||
late final IsAuthenticatedFunction isAuthenticated;
|
||||
|
||||
/// Send a nonza over the stream.
|
||||
@protected
|
||||
late final SendNonzaFunction sendNonza;
|
||||
|
||||
/// Get the connection's settings.
|
||||
@protected
|
||||
late final GetConnectionSettingsFunction getConnectionSettings;
|
||||
|
||||
/// The id included in the last stream header.
|
||||
@protected
|
||||
String? streamId;
|
||||
|
||||
/// Set the id of the last stream header.
|
||||
void setStreamHeaderId(String? id) {
|
||||
streamId = id;
|
||||
}
|
||||
|
||||
/// Returns, if registered, a negotiator with id [id].
|
||||
T? getNegotiatorById<T extends XmppFeatureNegotiatorBase>(String id) =>
|
||||
negotiators[id] as T?;
|
||||
|
||||
/// Register the parameters as the corresponding methods in this class. Also
|
||||
/// initializes the logger.
|
||||
void register(
|
||||
NegotiationsDoneCallback onNegotiationsDone,
|
||||
ErrorCallback handleError,
|
||||
IsAuthenticatedFunction isAuthenticated,
|
||||
SendNonzaFunction sendNonza,
|
||||
GetConnectionSettingsFunction getConnectionSettings,
|
||||
) {
|
||||
this.onNegotiationsDone = onNegotiationsDone;
|
||||
this.handleError = handleError;
|
||||
this.isAuthenticated = isAuthenticated;
|
||||
this.sendNonza = sendNonza;
|
||||
this.getConnectionSettings = getConnectionSettings;
|
||||
log = Logger(toString());
|
||||
}
|
||||
|
||||
/// Registers the negotiator [negotiator] against this negotiations handler.
|
||||
void registerNegotiator(XmppFeatureNegotiatorBase negotiator);
|
||||
|
||||
/// Sends the stream header.
|
||||
void sendStreamHeader();
|
||||
|
||||
/// Runs the post-register callback of all negotiators.
|
||||
Future<void> runPostRegisterCallback() async {
|
||||
for (final negotiator in negotiators.values) {
|
||||
await negotiator.postRegisterCallback();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> sendEventToNegotiators(XmppEvent event) async {
|
||||
for (final negotiator in negotiators.values) {
|
||||
await negotiator.onXmppEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove [feature] from the stream features we are currently negotiating.
|
||||
void removeNegotiatingFeature(String feature) {}
|
||||
|
||||
/// Resets all registered negotiators and the negotiation handler.
|
||||
@mustCallSuper
|
||||
void reset() {
|
||||
streamId = null;
|
||||
for (final negotiator in negotiators.values) {
|
||||
negotiator.reset();
|
||||
}
|
||||
}
|
||||
|
||||
/// Called whenever the stream buffer outputs a new event [event].
|
||||
Future<void> negotiate(XmlStreamBufferObject event) async {
|
||||
if (event is XmlStreamBufferHeader) {
|
||||
streamId = event.attributes['id'];
|
||||
}
|
||||
}
|
||||
}
|
@ -1,106 +1,27 @@
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:moxxmpp/src/buffer.dart';
|
||||
import 'package:moxxmpp/src/connection_errors.dart';
|
||||
import 'package:moxxmpp/src/errors.dart';
|
||||
import 'package:moxxmpp/src/events.dart';
|
||||
import 'package:moxxmpp/src/handlers/base.dart';
|
||||
import 'package:moxxmpp/src/jid.dart';
|
||||
import 'package:moxxmpp/src/namespaces.dart';
|
||||
import 'package:moxxmpp/src/negotiators/negotiator.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
|
||||
/// A callback for when the [NegotiationsHandler] is done.
|
||||
typedef NegotiationsDoneCallback = Future<void> Function();
|
||||
|
||||
/// A callback for the case that an error occurs while negotiating.
|
||||
typedef ErrorCallback = Future<void> Function(XmppError);
|
||||
|
||||
/// Trigger stream headers to be sent
|
||||
typedef SendStreamHeadersFunction = void Function();
|
||||
|
||||
/// Return true if the current connection is authenticated. If not, return false.
|
||||
typedef IsAuthenticatedFunction = bool Function();
|
||||
|
||||
/// This class implements the stream feature negotiation for XmppConnection.
|
||||
abstract class NegotiationsHandler {
|
||||
@protected
|
||||
late final Logger log;
|
||||
|
||||
/// Map of all negotiators registered against the handler.
|
||||
@protected
|
||||
final Map<String, XmppFeatureNegotiatorBase> negotiators = {};
|
||||
|
||||
/// Function that is called once the negotiator is done with its stream negotiations.
|
||||
@protected
|
||||
late final NegotiationsDoneCallback onNegotiationsDone;
|
||||
|
||||
/// XmppConnection's handleError method.
|
||||
@protected
|
||||
late final ErrorCallback handleError;
|
||||
|
||||
/// Sends stream headers in the stream.
|
||||
@protected
|
||||
late final SendStreamHeadersFunction sendStreamHeaders;
|
||||
|
||||
/// Returns true if the connection is authenticated. If not, returns false.
|
||||
@protected
|
||||
late final IsAuthenticatedFunction isAuthenticated;
|
||||
|
||||
/// The id included in the last stream header.
|
||||
@protected
|
||||
String? streamId;
|
||||
|
||||
/// Set the id of the last stream header.
|
||||
void setStreamHeaderId(String? id) {
|
||||
streamId = id;
|
||||
}
|
||||
|
||||
/// Returns, if registered, a negotiator with id [id].
|
||||
T? getNegotiatorById<T extends XmppFeatureNegotiatorBase>(String id) =>
|
||||
negotiators[id] as T?;
|
||||
|
||||
/// Register the parameters as the corresponding methods in this class. Also
|
||||
/// initializes the logger.
|
||||
void register(
|
||||
NegotiationsDoneCallback onNegotiationsDone,
|
||||
ErrorCallback handleError,
|
||||
SendStreamHeadersFunction sendStreamHeaders,
|
||||
IsAuthenticatedFunction isAuthenticated,
|
||||
) {
|
||||
this.onNegotiationsDone = onNegotiationsDone;
|
||||
this.handleError = handleError;
|
||||
this.sendStreamHeaders = sendStreamHeaders;
|
||||
this.isAuthenticated = isAuthenticated;
|
||||
log = Logger(toString());
|
||||
}
|
||||
|
||||
/// Registers the negotiator [negotiator] against this negotiations handler.
|
||||
void registerNegotiator(XmppFeatureNegotiatorBase negotiator);
|
||||
|
||||
/// Runs the post-register callback of all negotiators.
|
||||
Future<void> runPostRegisterCallback() async {
|
||||
for (final negotiator in negotiators.values) {
|
||||
await negotiator.postRegisterCallback();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> sendEventToNegotiators(XmppEvent event) async {
|
||||
for (final negotiator in negotiators.values) {
|
||||
await negotiator.onXmppEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove [feature] from the stream features we are currently negotiating.
|
||||
void removeNegotiatingFeature(String feature) {}
|
||||
|
||||
/// Resets all registered negotiators and the negotiation handler.
|
||||
@mustCallSuper
|
||||
void reset() {
|
||||
streamId = null;
|
||||
for (final negotiator in negotiators.values) {
|
||||
negotiator.reset();
|
||||
}
|
||||
}
|
||||
|
||||
/// Called whenever a new nonza [nonza] is received while negotiating.
|
||||
Future<void> negotiate(XMLNode nonza);
|
||||
/// "Nonza" describing the XMPP stream header of a client-to-server connection.
|
||||
class ClientStreamHeaderNonza extends XMLNode {
|
||||
ClientStreamHeaderNonza(JID jid)
|
||||
: super(
|
||||
tag: 'stream:stream',
|
||||
attributes: <String, String>{
|
||||
'xmlns': stanzaXmlns,
|
||||
'version': '1.0',
|
||||
'xmlns:stream': streamXmlns,
|
||||
'to': jid.domain,
|
||||
'from': jid.toBare().toString(),
|
||||
'xml:lang': 'en',
|
||||
},
|
||||
closeTag: false,
|
||||
);
|
||||
}
|
||||
|
||||
/// This class implements the stream feature negotiation for usage in client to server
|
||||
@ -134,6 +55,21 @@ class ClientToServerNegotiator extends NegotiationsHandler {
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void sendStreamHeader() {
|
||||
sendNonza(
|
||||
XMLNode(
|
||||
tag: 'xml',
|
||||
attributes: {'version': '1.0'},
|
||||
closeTag: false,
|
||||
isDeclaration: true,
|
||||
children: [
|
||||
ClientStreamHeaderNonza(getConnectionSettings().jid),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns true if all mandatory features in [features] have been negotiated.
|
||||
/// Otherwise returns false.
|
||||
bool _isMandatoryNegotiationDone(List<XMLNode> features) {
|
||||
@ -219,7 +155,7 @@ class ClientToServerNegotiator extends NegotiationsHandler {
|
||||
if (_currentNegotiator!.sendStreamHeaderWhenDone) {
|
||||
_currentNegotiator = null;
|
||||
_streamFeatures.clear();
|
||||
sendStreamHeaders();
|
||||
sendStreamHeader();
|
||||
} else {
|
||||
removeNegotiatingFeature(_currentNegotiator!.negotiatingXmlns);
|
||||
_currentNegotiator = null;
|
||||
@ -272,14 +208,16 @@ class ClientToServerNegotiator extends NegotiationsHandler {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> negotiate(XMLNode nonza) async {
|
||||
if (nonza.tag == 'stream:features') {
|
||||
// Store the received stream features
|
||||
_streamFeatures
|
||||
..clear()
|
||||
..addAll(nonza.children);
|
||||
}
|
||||
Future<void> negotiate(XmlStreamBufferObject event) async {
|
||||
if (event is XmlStreamBufferElement) {
|
||||
if (event.node.tag == 'stream:features') {
|
||||
// Store the received stream features
|
||||
_streamFeatures
|
||||
..clear()
|
||||
..addAll(event.node.children);
|
||||
}
|
||||
|
||||
await _executeCurrentNegotiator(nonza);
|
||||
await _executeCurrentNegotiator(event.node);
|
||||
}
|
||||
}
|
||||
}
|
117
packages/moxxmpp/lib/src/handlers/component.dart
Normal file
117
packages/moxxmpp/lib/src/handlers/component.dart
Normal file
@ -0,0 +1,117 @@
|
||||
import 'dart:convert';
|
||||
import 'package:cryptography/cryptography.dart';
|
||||
import 'package:hex/hex.dart';
|
||||
import 'package:moxxmpp/src/buffer.dart';
|
||||
import 'package:moxxmpp/src/connection_errors.dart';
|
||||
import 'package:moxxmpp/src/handlers/base.dart';
|
||||
import 'package:moxxmpp/src/jid.dart';
|
||||
import 'package:moxxmpp/src/namespaces.dart';
|
||||
import 'package:moxxmpp/src/negotiators/negotiator.dart';
|
||||
import 'package:moxxmpp/src/stringxml.dart';
|
||||
|
||||
/// Nonza describing the XMPP stream header.
|
||||
class ComponentStreamHeaderNonza extends XMLNode {
|
||||
ComponentStreamHeaderNonza(JID jid)
|
||||
: assert(jid.isBare(), 'Component JID must be bare'),
|
||||
super(
|
||||
tag: 'stream:stream',
|
||||
attributes: <String, String>{
|
||||
'xmlns': componentAcceptXmlns,
|
||||
'xmlns:stream': streamXmlns,
|
||||
'to': jid.domain,
|
||||
},
|
||||
closeTag: false,
|
||||
);
|
||||
}
|
||||
|
||||
/// The states the ComponentToServerNegotiator can be in.
|
||||
enum ComponentToServerState {
|
||||
/// No data has been sent or received yet
|
||||
idle,
|
||||
|
||||
/// Handshake has been sent
|
||||
handshakeSent,
|
||||
}
|
||||
|
||||
/// The ComponentToServerNegotiator is a NegotiationsHandler that allows writing
|
||||
/// components that adhere to XEP-0114.
|
||||
class ComponentToServerNegotiator extends NegotiationsHandler {
|
||||
ComponentToServerNegotiator();
|
||||
|
||||
/// The state the negotiation handler is currently in
|
||||
ComponentToServerState _state = ComponentToServerState.idle;
|
||||
|
||||
@override
|
||||
void registerNegotiator(XmppFeatureNegotiatorBase negotiator) {}
|
||||
|
||||
@override
|
||||
void sendStreamHeader() {
|
||||
sendNonza(
|
||||
XMLNode(
|
||||
tag: 'xml',
|
||||
attributes: {'version': '1.0'},
|
||||
closeTag: false,
|
||||
isDeclaration: true,
|
||||
children: [
|
||||
ComponentStreamHeaderNonza(getConnectionSettings().jid),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> _computeHandshake(String id) async {
|
||||
final secret = getConnectionSettings().password;
|
||||
return HEX.encode(
|
||||
(await Sha1().hash(utf8.encode('$streamId$secret'))).bytes,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> negotiate(XmlStreamBufferObject event) async {
|
||||
switch (_state) {
|
||||
case ComponentToServerState.idle:
|
||||
if (event is XmlStreamBufferHeader) {
|
||||
streamId = event.attributes['id'];
|
||||
assert(
|
||||
streamId != null,
|
||||
'The server must respond with a stream header that contains an id',
|
||||
);
|
||||
|
||||
_state = ComponentToServerState.handshakeSent;
|
||||
sendNonza(
|
||||
XMLNode(
|
||||
tag: 'handshake',
|
||||
text: await _computeHandshake(streamId!),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
log.severe('Unexpected data received');
|
||||
await handleError(UnexpectedDataError());
|
||||
}
|
||||
break;
|
||||
case ComponentToServerState.handshakeSent:
|
||||
if (event is XmlStreamBufferElement) {
|
||||
if (event.node.tag == 'handshake' &&
|
||||
event.node.children.isEmpty &&
|
||||
event.node.attributes.isEmpty) {
|
||||
log.info('Successfully authenticated as component');
|
||||
await onNegotiationsDone();
|
||||
} else {
|
||||
log.warning('Handshake failed');
|
||||
await handleError(InvalidHandshakeCredentialsError());
|
||||
}
|
||||
} else {
|
||||
log.severe('Unexpected data received');
|
||||
await handleError(UnexpectedDataError());
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void reset() {
|
||||
_state = ComponentToServerState.idle;
|
||||
|
||||
super.reset();
|
||||
}
|
||||
}
|
@ -44,6 +44,9 @@ const userAvatarMetadataXmlns = 'urn:xmpp:avatar:metadata';
|
||||
// XEP-0085
|
||||
const chatStateXmlns = 'http://jabber.org/protocol/chatstates';
|
||||
|
||||
// XEP-0114
|
||||
const componentAcceptXmlns = 'jabber:component:accept';
|
||||
|
||||
// XEP-0115
|
||||
const capsXmlns = 'http://jabber.org/protocol/caps';
|
||||
|
||||
|
@ -8,10 +8,19 @@ class ConnectionSettings {
|
||||
this.host,
|
||||
this.port,
|
||||
});
|
||||
|
||||
/// The JID to authenticate as.
|
||||
final JID jid;
|
||||
|
||||
/// The password to use during authentication.
|
||||
final String password;
|
||||
|
||||
/// Directly use TLS while connecting. Only effective if [host] and [port] are null.
|
||||
final bool useDirectTLS;
|
||||
|
||||
/// The host to connect to. Skips DNS resolution if specified.
|
||||
final String? host;
|
||||
|
||||
/// The port to connect to. Skips DNS resolution if specified.
|
||||
final int? port;
|
||||
}
|
||||
|
48
packages/moxxmpp/test/component_test.dart
Normal file
48
packages/moxxmpp/test/component_test.dart
Normal file
@ -0,0 +1,48 @@
|
||||
import 'package:moxxmpp/moxxmpp.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'helpers/logging.dart';
|
||||
import 'helpers/xmpp.dart';
|
||||
|
||||
const exampleXmlns1 = 'im:moxxmpp:example1';
|
||||
const exampleNamespace1 = 'im.moxxmpp.test.example1';
|
||||
const exampleXmlns2 = 'im:moxxmpp:example2';
|
||||
const exampleNamespace2 = 'im.moxxmpp.test.example2';
|
||||
|
||||
void main() {
|
||||
initLogger();
|
||||
|
||||
test('Test connecting as a component', () async {
|
||||
final socket = StubTCPSocket([
|
||||
StringExpectation(
|
||||
"<stream:stream xmlns='jabber:component:accept' xmlns:stream='http://etherx.jabber.org/streams' to='component.example.org'>",
|
||||
'''
|
||||
<stream:stream
|
||||
xmlns:stream='http://etherx.jabber.org/streams'
|
||||
xmlns='jabber:component:accept'
|
||||
from='component.example.org'
|
||||
id='3BF96D32'>'''),
|
||||
StringExpectation(
|
||||
'<handshake>ee8567f3b4c6e315345416b45ca2e47dbe921565</handshake>',
|
||||
'<handshake />',
|
||||
),
|
||||
]);
|
||||
final conn = XmppConnection(
|
||||
TestingReconnectionPolicy(),
|
||||
AlwaysConnectedConnectivityManager(),
|
||||
ComponentToServerNegotiator(),
|
||||
socket,
|
||||
)..connectionSettings = ConnectionSettings(
|
||||
jid: JID.fromString('component.example.org'),
|
||||
password: 'abc123',
|
||||
useDirectTLS: true,
|
||||
);
|
||||
await conn.registerManagers([
|
||||
RosterManager(TestingRosterStateManager('', [])),
|
||||
DiscoManager([]),
|
||||
]);
|
||||
final result = await conn.connect(
|
||||
waitUntilLogin: true,
|
||||
);
|
||||
expect(result.isType<bool>(), true);
|
||||
});
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
import 'dart:async';
|
||||
import 'package:moxxmpp/src/connection.dart';
|
||||
import 'package:moxxmpp/src/connectivity.dart';
|
||||
import 'package:moxxmpp/src/handlers/client.dart';
|
||||
import 'package:moxxmpp/src/jid.dart';
|
||||
import 'package:moxxmpp/src/managers/attributes.dart';
|
||||
import 'package:moxxmpp/src/managers/base.dart';
|
||||
import 'package:moxxmpp/src/negotiators/handler.dart';
|
||||
import 'package:moxxmpp/src/reconnect.dart';
|
||||
import 'package:moxxmpp/src/settings.dart';
|
||||
import 'package:moxxmpp/src/socket.dart';
|
||||
|
@ -1,7 +1,7 @@
|
||||
import 'package:moxxmpp/moxxmpp.dart';
|
||||
import 'package:moxxmpp/src/buffer.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'helpers/logging.dart';
|
||||
//import 'helpers/xmpp.dart';
|
||||
|
||||
const exampleXmlns1 = 'im:moxxmpp:example1';
|
||||
const exampleNamespace1 = 'im.moxxmpp.test.example1';
|
||||
@ -53,8 +53,13 @@ void main() {
|
||||
..register(
|
||||
() async {},
|
||||
(_) async {},
|
||||
() {},
|
||||
() => false,
|
||||
(_) {},
|
||||
() => ConnectionSettings(
|
||||
jid: JID.fromString('test'),
|
||||
password: 'abc123',
|
||||
useDirectTLS: false,
|
||||
),
|
||||
)
|
||||
..registerNegotiator(StubNegotiator1())
|
||||
..registerNegotiator(StubNegotiator2());
|
||||
@ -67,20 +72,27 @@ void main() {
|
||||
..register(
|
||||
() async {},
|
||||
(_) async {},
|
||||
() {},
|
||||
() => false,
|
||||
(_) {},
|
||||
() => ConnectionSettings(
|
||||
jid: JID.fromString('test'),
|
||||
password: 'abc123',
|
||||
useDirectTLS: false,
|
||||
),
|
||||
)
|
||||
..registerNegotiator(StubNegotiator1())
|
||||
..registerNegotiator(StubNegotiator2());
|
||||
await negotiator.runPostRegisterCallback();
|
||||
|
||||
await negotiator.negotiate(
|
||||
XMLNode.fromString(
|
||||
'''
|
||||
XmlStreamBufferElement(
|
||||
XMLNode.fromString(
|
||||
'''
|
||||
<stream:features xmlns="http://etherx.jabber.org/streams">
|
||||
<example1 xmlns="im:moxxmpp:example1" />
|
||||
<example2 xmlns="im:moxxmpp:example2" />
|
||||
</stream:features>''',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
|
@ -23,7 +23,7 @@ void main() {
|
||||
);
|
||||
|
||||
expect(
|
||||
StreamHeaderNonza(JID.fromString('user@uwu.server')).toXml(),
|
||||
ClientStreamHeaderNonza(JID.fromString('user@uwu.server')).toXml(),
|
||||
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='uwu.server' from='user@uwu.server' xml:lang='en'>",
|
||||
);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user