feat: Rework how the ReconnectionPolicy system works

This commit is contained in:
PapaTutuWawa 2023-01-27 00:14:44 +01:00
parent 1cc266c675
commit bff4a6f707
15 changed files with 501 additions and 292 deletions

View File

@ -1,6 +1,7 @@
library moxxmpp; library moxxmpp;
export 'package:moxxmpp/src/connection.dart'; export 'package:moxxmpp/src/connection.dart';
export 'package:moxxmpp/src/connectivity.dart';
export 'package:moxxmpp/src/errors.dart'; export 'package:moxxmpp/src/errors.dart';
export 'package:moxxmpp/src/events.dart'; export 'package:moxxmpp/src/events.dart';
export 'package:moxxmpp/src/iq.dart'; export 'package:moxxmpp/src/iq.dart';

View File

@ -4,6 +4,7 @@ import 'package:meta/meta.dart';
import 'package:moxlib/moxlib.dart'; import 'package:moxlib/moxlib.dart';
import 'package:moxxmpp/src/awaiter.dart'; import 'package:moxxmpp/src/awaiter.dart';
import 'package:moxxmpp/src/buffer.dart'; import 'package:moxxmpp/src/buffer.dart';
import 'package:moxxmpp/src/connectivity.dart';
import 'package:moxxmpp/src/errors.dart'; import 'package:moxxmpp/src/errors.dart';
import 'package:moxxmpp/src/events.dart'; import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/iq.dart'; import 'package:moxxmpp/src/iq.dart';
@ -94,22 +95,24 @@ class XmppConnectionResult {
class XmppConnection { class XmppConnection {
XmppConnection( XmppConnection(
ReconnectionPolicy reconnectionPolicy, ReconnectionPolicy reconnectionPolicy,
ConnectivityManager connectivityManager,
this._socket, this._socket,
{ {
this.connectionPingDuration = const Duration(minutes: 3), this.connectionPingDuration = const Duration(minutes: 3),
this.connectingTimeout = const Duration(minutes: 2), this.connectingTimeout = const Duration(minutes: 2),
} }
) : _reconnectionPolicy = reconnectionPolicy { ) : _reconnectionPolicy = reconnectionPolicy,
// Allow the reconnection policy to perform reconnections by itself _connectivityManager = connectivityManager {
_reconnectionPolicy.register( // Allow the reconnection policy to perform reconnections by itself
_attemptReconnection, _reconnectionPolicy.register(
_onNetworkConnectionLost, _attemptReconnection,
); _onNetworkConnectionLost,
);
_socketStream = _socket.getDataStream(); _socketStream = _socket.getDataStream();
// TODO(Unknown): Handle on done // TODO(Unknown): Handle on done
_socketStream.transform(_streamBuffer).forEach(handleXmlStream); _socketStream.transform(_streamBuffer).forEach(handleXmlStream);
_socket.getEventStream().listen(_handleSocketEvent); _socket.getEventStream().listen(_handleSocketEvent);
} }
@ -122,13 +125,16 @@ class XmppConnection {
/// The data stream of the socket /// The data stream of the socket
late final Stream<String> _socketStream; late final Stream<String> _socketStream;
/// Connection settings /// Connection settings
late ConnectionSettings _connectionSettings; late ConnectionSettings _connectionSettings;
/// A policy on how to reconnect /// A policy on how to reconnect
final ReconnectionPolicy _reconnectionPolicy; final ReconnectionPolicy _reconnectionPolicy;
/// The class responsible for preventing errors on initial connection due
/// to no network.
final ConnectivityManager _connectivityManager;
/// A helper for handling await semantics with stanzas /// A helper for handling await semantics with stanzas
final StanzaAwaiter _stanzaAwaiter = StanzaAwaiter(); final StanzaAwaiter _stanzaAwaiter = StanzaAwaiter();
@ -378,7 +384,7 @@ class XmppConnection {
// Connect again // Connect again
// ignore: cascade_invocations // ignore: cascade_invocations
_log.finest('Calling connect() from _attemptReconnection'); _log.finest('Calling connect() from _attemptReconnection');
await connect(); await connect(waitForConnection: true);
} }
/// Called when a stream ending error has occurred /// Called when a stream ending error has occurred
@ -401,7 +407,11 @@ class XmppConnection {
return; return;
} }
await _setConnectionState(XmppConnectionState.error); if (await _connectivityManager.hasConnection()) {
await _setConnectionState(XmppConnectionState.error);
} else {
await _setConnectionState(XmppConnectionState.notConnected);
}
await _reconnectionPolicy.onFailure(); await _reconnectionPolicy.onFailure();
} }
@ -832,7 +842,6 @@ class XmppConnection {
if (_isMandatoryNegotiationDone(_streamFeatures) && !_isNegotiationPossible(_streamFeatures)) { if (_isMandatoryNegotiationDone(_streamFeatures) && !_isNegotiationPossible(_streamFeatures)) {
_log.finest('Negotiations done!'); _log.finest('Negotiations done!');
_updateRoutingState(RoutingState.handleStanzas); _updateRoutingState(RoutingState.handleStanzas);
await _reconnectionPolicy.onSuccess();
await _resetIsConnectionRunning(); await _resetIsConnectionRunning();
await _onNegotiationsDone(); await _onNegotiationsDone();
} else { } else {
@ -857,7 +866,6 @@ class XmppConnection {
_log.finest('Negotiations done!'); _log.finest('Negotiations done!');
_updateRoutingState(RoutingState.handleStanzas); _updateRoutingState(RoutingState.handleStanzas);
await _reconnectionPolicy.onSuccess();
await _resetIsConnectionRunning(); await _resetIsConnectionRunning();
await _onNegotiationsDone(); await _onNegotiationsDone();
} else { } else {
@ -875,7 +883,6 @@ class XmppConnection {
_log.finest('Negotiator wants to skip the remaining negotiation... Negotiations (assumed) done!'); _log.finest('Negotiator wants to skip the remaining negotiation... Negotiations (assumed) done!');
_updateRoutingState(RoutingState.handleStanzas); _updateRoutingState(RoutingState.handleStanzas);
await _reconnectionPolicy.onSuccess();
await _resetIsConnectionRunning(); await _resetIsConnectionRunning();
await _onNegotiationsDone(); await _onNegotiationsDone();
break; break;
@ -987,7 +994,7 @@ class XmppConnection {
} }
Future<void> _disconnect({required XmppConnectionState state, bool triggeredByUser = true}) async { Future<void> _disconnect({required XmppConnectionState state, bool triggeredByUser = true}) async {
_reconnectionPolicy.setShouldReconnect(false); await _reconnectionPolicy.setShouldReconnect(false);
if (triggeredByUser) { if (triggeredByUser) {
getPresenceManager().sendUnavailablePresence(); getPresenceManager().sendUnavailablePresence();
@ -1018,17 +1025,21 @@ class XmppConnection {
/// Like [connect] but the Future resolves when the resource binding is either done or /// Like [connect] but the Future resolves when the resource binding is either done or
/// SASL has failed. /// SASL has failed.
Future<XmppConnectionResult> connectAwaitable({ String? lastResource }) async { Future<XmppConnectionResult> connectAwaitable({ String? lastResource, bool waitForConnection = false }) async {
_runPreConnectionAssertions(); _runPreConnectionAssertions();
await _resetIsConnectionRunning(); await _resetIsConnectionRunning();
_connectionCompleter = Completer(); _connectionCompleter = Completer();
_log.finest('Calling connect() from connectAwaitable'); _log.finest('Calling connect() from connectAwaitable');
await connect(lastResource: lastResource); await connect(
lastResource: lastResource,
waitForConnection: waitForConnection,
shouldReconnect: false,
);
return _connectionCompleter!.future; return _connectionCompleter!.future;
} }
/// Start the connection process using the provided connection settings. /// Start the connection process using the provided connection settings.
Future<void> connect({ String? lastResource }) async { Future<void> connect({ String? lastResource, bool waitForConnection = false, bool shouldReconnect = true }) async {
if (_connectionState != XmppConnectionState.notConnected && _connectionState != XmppConnectionState.error) { if (_connectionState != XmppConnectionState.notConnected && _connectionState != XmppConnectionState.error) {
_log.fine('Cancelling this connection attempt as one appears to be already running.'); _log.fine('Cancelling this connection attempt as one appears to be already running.');
return; return;
@ -1036,15 +1047,25 @@ class XmppConnection {
_runPreConnectionAssertions(); _runPreConnectionAssertions();
await _resetIsConnectionRunning(); await _resetIsConnectionRunning();
_reconnectionPolicy.setShouldReconnect(true);
if (lastResource != null) { if (lastResource != null) {
setResource(lastResource); setResource(lastResource);
} }
if (shouldReconnect) {
await _reconnectionPolicy.setShouldReconnect(true);
}
await _reconnectionPolicy.reset(); await _reconnectionPolicy.reset();
await _sendEvent(ConnectingEvent()); await _sendEvent(ConnectingEvent());
// If requested, wait until we have a network connection
if (waitForConnection) {
_log.info('Waiting for okay from connectivityManager');
await _connectivityManager.waitForConnection();
_log.info('Got okay from connectivityManager');
}
final smManager = getStreamManagementManager(); final smManager = getStreamManagementManager();
String? host; String? host;
int? port; int? port;

View File

@ -0,0 +1,18 @@
/// This manager class is responsible to tell the moxxmpp XmppConnection
/// when a connection can be established or not, regarding the network availability.
abstract class ConnectivityManager {
/// Returns true if a network connection is available. If not, returns false.
Future<bool> hasConnection();
/// Returns a future that resolves once we have a network connection.
Future<void> waitForConnection();
}
/// An implementation of [ConnectivityManager] that is always connected.
class AlwaysConnectedConnectivityManager extends ConnectivityManager {
@override
Future<bool> hasConnection() async => true;
@override
Future<void> waitForConnection() async {}
}

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:moxxmpp/src/util/queue.dart';
import 'package:synchronized/synchronized.dart'; import 'package:synchronized/synchronized.dart';
/// A callback function to be called when the connection to the server has been lost. /// A callback function to be called when the connection to the server has been lost.
@ -24,10 +25,16 @@ abstract class ReconnectionPolicy {
bool _shouldAttemptReconnection = false; bool _shouldAttemptReconnection = false;
/// Indicate if a reconnection attempt is currently running. /// Indicate if a reconnection attempt is currently running.
bool _isReconnecting = false; @protected
bool isReconnecting = false;
/// And the corresponding lock /// And the corresponding lock
final Lock _isReconnectingLock = Lock(); @protected
final Lock lock = Lock();
/// The lock for accessing [_shouldAttemptReconnection]
@protected
final Lock shouldReconnectLock = Lock();
/// Called by XmppConnection to register the policy. /// Called by XmppConnection to register the policy.
void register(PerformReconnectFunction performReconnect, ConnectionLostCallback triggerConnectionLost) { void register(PerformReconnectFunction performReconnect, ConnectionLostCallback triggerConnectionLost) {
@ -48,96 +55,121 @@ abstract class ReconnectionPolicy {
/// Caled by the XmppConnection when the reconnection was successful. /// Caled by the XmppConnection when the reconnection was successful.
Future<void> onSuccess(); Future<void> onSuccess();
bool get shouldReconnect => _shouldAttemptReconnection; Future<bool> getShouldReconnect() async {
return shouldReconnectLock.synchronized(() => _shouldAttemptReconnection);
}
/// Set whether a reconnection attempt should be made. /// Set whether a reconnection attempt should be made.
void setShouldReconnect(bool value) { Future<void> setShouldReconnect(bool value) async {
_shouldAttemptReconnection = value; return shouldReconnectLock.synchronized(() => _shouldAttemptReconnection = value);
} }
/// Returns true if the manager is currently triggering a reconnection. If not, returns /// Returns true if the manager is currently triggering a reconnection. If not, returns
/// false. /// false.
Future<bool> isReconnectionRunning() async { Future<bool> isReconnectionRunning() async {
return _isReconnectingLock.synchronized(() => _isReconnecting); return lock.synchronized(() => isReconnecting);
} }
/// Set the _isReconnecting state to [value]. /// Set the isReconnecting state to [value].
@protected @protected
Future<void> setIsReconnecting(bool value) async { Future<void> setIsReconnecting(bool value) async {
await _isReconnectingLock.synchronized(() async { await lock.synchronized(() async {
_isReconnecting = value; isReconnecting = value;
}); });
} }
@protected
Future<bool> testAndSetIsReconnecting() async {
return _isReconnectingLock.synchronized(() {
if (_isReconnecting) {
return false;
} else {
_isReconnecting = true;
return true;
}
});
}
} }
/// A simple reconnection strategy: Make the reconnection delays exponentially longer /// A simple reconnection strategy: Make the reconnection delays exponentially longer
/// for every failed attempt. /// for every failed attempt.
/// NOTE: This ReconnectionPolicy may be broken /// NOTE: This ReconnectionPolicy may be broken
class ExponentialBackoffReconnectionPolicy extends ReconnectionPolicy { class RandomBackoffReconnectionPolicy extends ReconnectionPolicy {
ExponentialBackoffReconnectionPolicy(this._maxBackoffTime) : super(); RandomBackoffReconnectionPolicy(
this._minBackoffTime,
this._maxBackoffTime,
) : assert(_minBackoffTime < _maxBackoffTime, '_minBackoffTime must be smaller than _maxBackoffTime'),
super();
/// The maximum time in seconds that a backoff step should be. /// The maximum time in seconds that a backoff should be.
final int _maxBackoffTime; final int _maxBackoffTime;
/// Amount of consecutive failed reconnections. /// The minimum time in seconds that a backoff should be.
int _counter = 0; final int _minBackoffTime;
/// Backoff timer. /// Backoff timer.
Timer? _timer; Timer? _timer;
final Lock _timerLock = Lock();
/// Logger. /// Logger.
final Logger _log = Logger('ExponentialBackoffReconnectionPolicy'); final Logger _log = Logger('RandomBackoffReconnectionPolicy');
/// Event queue
final AsyncQueue _eventQueue = AsyncQueue();
/// Called when the backoff expired /// Called when the backoff expired
Future<void> _onTimerElapsed() async { Future<void> _onTimerElapsed() async {
final isReconnecting = await isReconnectionRunning(); _log.fine('Timer elapsed. Waiting for lock');
if (shouldReconnect) { await lock.synchronized(() async {
if (!isReconnecting) { _log.fine('Lock aquired');
await setIsReconnecting(true); if (!(await getShouldReconnect())) {
await performReconnect!(); _log.fine('Backoff timer expired but getShouldReconnect() returned false');
} else { return;
// Should never happen.
_log.fine('Backoff timer expired but reconnection is running, so doing nothing.');
} }
}
if (isReconnecting) {
_log.fine('Backoff timer expired but a reconnection is running, so doing nothing.');
return;
}
_log.fine('Triggering reconnect');
isReconnecting = true;
await performReconnect!();
});
await _timerLock.synchronized(() {
_timer?.cancel();
_timer = null;
});
}
Future<void> _reset() async {
_log.finest('Resetting internal state');
await _timerLock.synchronized(() {
_timer?.cancel();
_timer = null;
});
await setIsReconnecting(false);
} }
@override @override
Future<void> reset() async { Future<void> reset() async {
_log.finest('Resetting internal state'); // ignore: unnecessary_lambdas
_counter = 0; await _eventQueue.addJob(() => _reset());
await setIsReconnecting(false); }
if (_timer != null) { Future<void> _onFailure() async {
_timer!.cancel(); final shouldContinue = await _timerLock.synchronized(() {
_timer = null; return _timer == null;
});
if (!shouldContinue) {
_log.finest('_onFailure: Not backing off since _timer is already running');
return;
} }
final seconds = Random().nextInt(_maxBackoffTime - _minBackoffTime) + _minBackoffTime;
_log.finest('Failure occured. Starting random backoff with ${seconds}s');
_timer?.cancel();
_timer = Timer(Duration(seconds: seconds), _onTimerElapsed);
} }
@override @override
Future<void> onFailure() async { Future<void> onFailure() async {
_log.finest('Failure occured. Starting exponential backoff'); // ignore: unnecessary_lambdas
_counter++; await _eventQueue.addJob(() => _onFailure());
if (_timer != null) {
_timer!.cancel();
}
// Wait at max 80 seconds.
final seconds = min(min(pow(2, _counter).toInt(), 80), _maxBackoffTime);
_timer = Timer(Duration(seconds: seconds), _onTimerElapsed);
} }
@override @override

View File

@ -0,0 +1,56 @@
import 'dart:async';
import 'dart:collection';
import 'package:meta/meta.dart';
import 'package:synchronized/synchronized.dart';
/// A job to be submitted to an [AsyncQueue].
typedef AsyncQueueJob = Future<void> Function();
/// A (hopefully) async-safe queue that attempts to force
/// in-order execution of its jobs.
class AsyncQueue {
/// The lock for accessing [AsyncQueue._lock] and [AsyncQueue._running].
final Lock _lock = Lock();
/// The actual job queue.
final Queue<AsyncQueueJob> _queue = Queue<AsyncQueueJob>();
/// Indicates whether we are currently executing a job.
bool _running = false;
@visibleForTesting
Queue<AsyncQueueJob> get queue => _queue;
@visibleForTesting
bool get isRunning => _running;
/// Adds a job [job] to the queue.
Future<void> addJob(AsyncQueueJob job) async {
await _lock.synchronized(() {
_queue.add(job);
if (!_running && _queue.isNotEmpty) {
_running = true;
unawaited(_popJob());
}
});
}
Future<void> clear() async {
await _lock.synchronized(_queue.clear);
}
Future<void> _popJob() async {
final job = _queue.removeFirst();
final future = job();
await future;
await _lock.synchronized(() {
if (_queue.isNotEmpty) {
unawaited(_popJob());
} else {
_running = false;
}
});
}
}

View File

@ -0,0 +1,43 @@
import 'package:moxxmpp/src/util/queue.dart';
import 'package:test/test.dart';
void main() {
test('Test the async queue', () async {
final queue = AsyncQueue();
int future1Finish = 0;
int future2Finish = 0;
int future3Finish = 0;
await queue.addJob(() => Future<void>.delayed(const Duration(seconds: 3), () => future1Finish = DateTime.now().millisecondsSinceEpoch));
await queue.addJob(() => Future<void>.delayed(const Duration(seconds: 3), () => future2Finish = DateTime.now().millisecondsSinceEpoch));
await queue.addJob(() => Future<void>.delayed(const Duration(seconds: 3), () => future3Finish = DateTime.now().millisecondsSinceEpoch));
await Future<void>.delayed(const Duration(seconds: 12));
// The three futures must be done
expect(future1Finish != 0, true);
expect(future2Finish != 0, true);
expect(future3Finish != 0, true);
// The end times of the futures must be ordered (on a timeline)
// |-- future1Finish -- future2Finish -- future3Finish --|
expect(
future1Finish < future2Finish && future1Finish < future3Finish,
true,
);
expect(
future2Finish < future3Finish && future2Finish > future1Finish,
true,
);
expect(
future3Finish > future1Finish && future3Finish > future2Finish,
true,
);
// The queue must be empty at the end
expect(queue.queue.isEmpty, true);
// The queue must not be executing anything at the end
expect(queue.isRunning, false);
});
}

View File

@ -54,8 +54,11 @@ void main() {
], ],
); );
final connection = XmppConnection(TestingReconnectionPolicy(), stubSocket) final connection = XmppConnection(
..registerFeatureNegotiators([ TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
stubSocket,
)..registerFeatureNegotiators([
StubNegotiator1(), StubNegotiator1(),
StubNegotiator2(), StubNegotiator2(),
]) ])

View File

@ -3,14 +3,14 @@ import 'package:test/test.dart';
void main() { void main() {
test('Parsing', () { test('Parsing', () {
const testData = "<x xmlns='jabber:x:data' type='result'><field var='FORM_TYPE' type='hidden'><value>urn:xmpp:dataforms:softwareinfo</value></field><field var='ip_version' type='text-multi' ><value>ipv4</value><value>ipv6</value></field><field var='os'><value>Mac</value></field><field var='os_version'><value>10.5.1</value></field><field var='software'><value>Psi</value></field><field var='software_version'><value>0.11</value></field></x>"; const testData = "<x xmlns='jabber:x:data' type='result'><field var='FORM_TYPE' type='hidden'><value>urn:xmpp:dataforms:softwareinfo</value></field><field var='ip_version' type='text-multi' ><value>ipv4</value><value>ipv6</value></field><field var='os'><value>Mac</value></field><field var='os_version'><value>10.5.1</value></field><field var='software'><value>Psi</value></field><field var='software_version'><value>0.11</value></field></x>";
final form = parseDataForm(XMLNode.fromString(testData)); final form = parseDataForm(XMLNode.fromString(testData));
expect(form.getFieldByVar('FORM_TYPE')?.values.first, 'urn:xmpp:dataforms:softwareinfo'); expect(form.getFieldByVar('FORM_TYPE')?.values.first, 'urn:xmpp:dataforms:softwareinfo');
expect(form.getFieldByVar('ip_version')?.values, [ 'ipv4', 'ipv6' ]); expect(form.getFieldByVar('ip_version')?.values, [ 'ipv4', 'ipv6' ]);
expect(form.getFieldByVar('os')?.values.first, 'Mac'); expect(form.getFieldByVar('os')?.values.first, 'Mac');
expect(form.getFieldByVar('os_version')?.values.first, '10.5.1'); expect(form.getFieldByVar('os_version')?.values.first, '10.5.1');
expect(form.getFieldByVar('software')?.values.first, 'Psi'); expect(form.getFieldByVar('software')?.values.first, 'Psi');
expect(form.getFieldByVar('software_version')?.values.first, '0.11'); expect(form.getFieldByVar('software_version')?.values.first, '0.11');
}); });
} }

View File

@ -65,7 +65,11 @@ void main() {
], ],
); );
final XmppConnection conn = XmppConnection(TestingReconnectionPolicy(), socket: fakeSocket); final XmppConnection conn = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
fakeSocket,
);
conn.setConnectionSettings(ConnectionSettings( conn.setConnectionSettings(ConnectionSettings(
jid: JID.fromString('polynomdivision@test.server'), jid: JID.fromString('polynomdivision@test.server'),
password: 'aaaa', password: 'aaaa',
@ -74,7 +78,7 @@ void main() {
),); ),);
conn.registerManagers([ conn.registerManagers([
PresenceManager('http://moxxmpp.example'), PresenceManager('http://moxxmpp.example'),
RosterManager(), RosterManager(TestingRosterStateManager(null, [])),
DiscoManager(), DiscoManager(),
PingManager(), PingManager(),
]); ]);

View File

@ -4,164 +4,164 @@ import 'package:test/test.dart';
void main() { void main() {
test('Test XEP example', () async { test('Test XEP example', () async {
final data = DiscoInfo( final data = DiscoInfo(
[ [
'http://jabber.org/protocol/caps', 'http://jabber.org/protocol/caps',
'http://jabber.org/protocol/disco#info', 'http://jabber.org/protocol/disco#info',
'http://jabber.org/protocol/disco#items', 'http://jabber.org/protocol/disco#items',
'http://jabber.org/protocol/muc' 'http://jabber.org/protocol/muc'
], ],
[ [
Identity( Identity(
category: 'client', category: 'client',
type: 'pc', type: 'pc',
name: 'Exodus 0.9.1', name: 'Exodus 0.9.1',
) )
], ],
[], [],
JID.fromString('some@user.local/test'), JID.fromString('some@user.local/test'),
); );
final hash = await calculateCapabilityHash(data, Sha1()); final hash = await calculateCapabilityHash(data, Sha1());
expect(hash, 'QgayPKawpkPSDYmwT/WM94uAlu0='); expect(hash, 'QgayPKawpkPSDYmwT/WM94uAlu0=');
}); });
test('Test complex generation example', () async { test('Test complex generation example', () async {
const extDiscoDataString = "<x xmlns='jabber:x:data' type='result'><field var='FORM_TYPE' type='hidden'><value>urn:xmpp:dataforms:softwareinfo</value></field><field var='ip_version' type='text-multi' ><value>ipv4</value><value>ipv6</value></field><field var='os'><value>Mac</value></field><field var='os_version'><value>10.5.1</value></field><field var='software'><value>Psi</value></field><field var='software_version'><value>0.11</value></field></x>"; const extDiscoDataString = "<x xmlns='jabber:x:data' type='result'><field var='FORM_TYPE' type='hidden'><value>urn:xmpp:dataforms:softwareinfo</value></field><field var='ip_version' type='text-multi' ><value>ipv4</value><value>ipv6</value></field><field var='os'><value>Mac</value></field><field var='os_version'><value>10.5.1</value></field><field var='software'><value>Psi</value></field><field var='software_version'><value>0.11</value></field></x>";
final data = DiscoInfo( final data = DiscoInfo(
[ [
'http://jabber.org/protocol/caps', 'http://jabber.org/protocol/caps',
'http://jabber.org/protocol/disco#info', 'http://jabber.org/protocol/disco#info',
'http://jabber.org/protocol/disco#items', 'http://jabber.org/protocol/disco#items',
'http://jabber.org/protocol/muc' 'http://jabber.org/protocol/muc'
], ],
[ [
const Identity( const Identity(
category: 'client', category: 'client',
type: 'pc', type: 'pc',
name: 'Psi 0.11', name: 'Psi 0.11',
lang: 'en', lang: 'en',
), ),
const Identity( const Identity(
category: 'client', category: 'client',
type: 'pc', type: 'pc',
name: 'Ψ 0.11', name: 'Ψ 0.11',
lang: 'el', lang: 'el',
), ),
], ],
[ parseDataForm(XMLNode.fromString(extDiscoDataString)) ], [ parseDataForm(XMLNode.fromString(extDiscoDataString)) ],
JID.fromString('some@user.local/test'), JID.fromString('some@user.local/test'),
); );
final hash = await calculateCapabilityHash(data, Sha1()); final hash = await calculateCapabilityHash(data, Sha1());
expect(hash, 'q07IKJEyjvHSyhy//CH0CxmKi8w='); expect(hash, 'q07IKJEyjvHSyhy//CH0CxmKi8w=');
}); });
test('Test Gajim capability hash computation', () async { test('Test Gajim capability hash computation', () async {
// TODO: This one fails // TODO: This one fails
/* /*
final data = DiscoInfo( final data = DiscoInfo(
features: [ features: [
"http://jabber.org/protocol/bytestreams", "http://jabber.org/protocol/bytestreams",
"http://jabber.org/protocol/muc", "http://jabber.org/protocol/muc",
"http://jabber.org/protocol/commands", "http://jabber.org/protocol/commands",
"http://jabber.org/protocol/disco#info", "http://jabber.org/protocol/disco#info",
"jabber:iq:last", "jabber:iq:last",
"jabber:x:data", "jabber:x:data",
"jabber:x:encrypted", "jabber:x:encrypted",
"urn:xmpp:ping", "urn:xmpp:ping",
"http://jabber.org/protocol/chatstates", "http://jabber.org/protocol/chatstates",
"urn:xmpp:receipts", "urn:xmpp:receipts",
"urn:xmpp:time", "urn:xmpp:time",
"jabber:iq:version", "jabber:iq:version",
"http://jabber.org/protocol/rosterx", "http://jabber.org/protocol/rosterx",
"urn:xmpp:sec-label:0", "urn:xmpp:sec-label:0",
"jabber:x:conference", "jabber:x:conference",
"urn:xmpp:message-correct:0", "urn:xmpp:message-correct:0",
"urn:xmpp:chat-markers:0", "urn:xmpp:chat-markers:0",
"urn:xmpp:eme:0", "urn:xmpp:eme:0",
"http://jabber.org/protocol/xhtml-im", "http://jabber.org/protocol/xhtml-im",
"urn:xmpp:hashes:2", "urn:xmpp:hashes:2",
"urn:xmpp:hash-function-text-names:md5", "urn:xmpp:hash-function-text-names:md5",
"urn:xmpp:hash-function-text-names:sha-1", "urn:xmpp:hash-function-text-names:sha-1",
"urn:xmpp:hash-function-text-names:sha-256", "urn:xmpp:hash-function-text-names:sha-256",
"urn:xmpp:hash-function-text-names:sha-512", "urn:xmpp:hash-function-text-names:sha-512",
"urn:xmpp:hash-function-text-names:sha3-256", "urn:xmpp:hash-function-text-names:sha3-256",
"urn:xmpp:hash-function-text-names:sha3-512", "urn:xmpp:hash-function-text-names:sha3-512",
"urn:xmpp:hash-function-text-names:id-blake2b256", "urn:xmpp:hash-function-text-names:id-blake2b256",
"urn:xmpp:hash-function-text-names:id-blake2b512", "urn:xmpp:hash-function-text-names:id-blake2b512",
"urn:xmpp:jingle:1", "urn:xmpp:jingle:1",
"urn:xmpp:jingle:apps:file-transfer:5", "urn:xmpp:jingle:apps:file-transfer:5",
"urn:xmpp:jingle:security:xtls:0", "urn:xmpp:jingle:security:xtls:0",
"urn:xmpp:jingle:transports:s5b:1", "urn:xmpp:jingle:transports:s5b:1",
"urn:xmpp:jingle:transports:ibb:1", "urn:xmpp:jingle:transports:ibb:1",
"urn:xmpp:avatar:metadata+notify", "urn:xmpp:avatar:metadata+notify",
"urn:xmpp:message-moderate:0", "urn:xmpp:message-moderate:0",
"http://jabber.org/protocol/tune+notify", "http://jabber.org/protocol/tune+notify",
"http://jabber.org/protocol/geoloc+notify", "http://jabber.org/protocol/geoloc+notify",
"http://jabber.org/protocol/nick+notify", "http://jabber.org/protocol/nick+notify",
"eu.siacs.conversations.axolotl.devicelist+notify", "eu.siacs.conversations.axolotl.devicelist+notify",
], ],
identities: [ identities: [
Identity( Identity(
category: "client", category: "client",
type: "pc", type: "pc",
name: "Gajim" name: "Gajim"
) )
] ]
); );
final hash = await calculateCapabilityHash(data, Sha1()); final hash = await calculateCapabilityHash(data, Sha1());
expect(hash, "T7fOZrtBnV8sDA2fFTS59vyOyUs="); expect(hash, "T7fOZrtBnV8sDA2fFTS59vyOyUs=");
*/ */
}); });
test('Test Conversations hash computation', () async { test('Test Conversations hash computation', () async {
final data = DiscoInfo( final data = DiscoInfo(
[ [
'eu.siacs.conversations.axolotl.devicelist+notify', 'eu.siacs.conversations.axolotl.devicelist+notify',
'http://jabber.org/protocol/caps', 'http://jabber.org/protocol/caps',
'http://jabber.org/protocol/chatstates', 'http://jabber.org/protocol/chatstates',
'http://jabber.org/protocol/disco#info', 'http://jabber.org/protocol/disco#info',
'http://jabber.org/protocol/muc', 'http://jabber.org/protocol/muc',
'http://jabber.org/protocol/nick+notify', 'http://jabber.org/protocol/nick+notify',
'jabber:iq:version', 'jabber:iq:version',
'jabber:x:conference', 'jabber:x:conference',
'jabber:x:oob', 'jabber:x:oob',
'storage:bookmarks+notify', 'storage:bookmarks+notify',
'urn:xmpp:avatar:metadata+notify', 'urn:xmpp:avatar:metadata+notify',
'urn:xmpp:chat-markers:0', 'urn:xmpp:chat-markers:0',
'urn:xmpp:jingle-message:0', 'urn:xmpp:jingle-message:0',
'urn:xmpp:jingle:1', 'urn:xmpp:jingle:1',
'urn:xmpp:jingle:apps:dtls:0', 'urn:xmpp:jingle:apps:dtls:0',
'urn:xmpp:jingle:apps:file-transfer:3', 'urn:xmpp:jingle:apps:file-transfer:3',
'urn:xmpp:jingle:apps:file-transfer:4', 'urn:xmpp:jingle:apps:file-transfer:4',
'urn:xmpp:jingle:apps:file-transfer:5', 'urn:xmpp:jingle:apps:file-transfer:5',
'urn:xmpp:jingle:apps:rtp:1', 'urn:xmpp:jingle:apps:rtp:1',
'urn:xmpp:jingle:apps:rtp:audio', 'urn:xmpp:jingle:apps:rtp:audio',
'urn:xmpp:jingle:apps:rtp:video', 'urn:xmpp:jingle:apps:rtp:video',
'urn:xmpp:jingle:jet-omemo:0', 'urn:xmpp:jingle:jet-omemo:0',
'urn:xmpp:jingle:jet:0', 'urn:xmpp:jingle:jet:0',
'urn:xmpp:jingle:transports:ibb:1', 'urn:xmpp:jingle:transports:ibb:1',
'urn:xmpp:jingle:transports:ice-udp:1', 'urn:xmpp:jingle:transports:ice-udp:1',
'urn:xmpp:jingle:transports:s5b:1', 'urn:xmpp:jingle:transports:s5b:1',
'urn:xmpp:message-correct:0', 'urn:xmpp:message-correct:0',
'urn:xmpp:ping', 'urn:xmpp:ping',
'urn:xmpp:receipts', 'urn:xmpp:receipts',
'urn:xmpp:time' 'urn:xmpp:time'
], ],
[ [
Identity( Identity(
category: 'client', category: 'client',
type: 'phone', type: 'phone',
name: 'Conversations', name: 'Conversations',
) )
], ],
[], [],
JID.fromString('user@server.local/test'), JID.fromString('user@server.local/test'),
); );
final hash = await calculateCapabilityHash(data, Sha1()); final hash = await calculateCapabilityHash(data, Sha1());
expect(hash, 'zcIke+Rk13ah4d1pwDG7bEZsVwA='); expect(hash, 'zcIke+Rk13ah4d1pwDG7bEZsVwA=');
}); });
} }

View File

@ -34,7 +34,7 @@ XmppManagerAttributes mkAttributes(void Function(Stanza) callback) {
isFeatureSupported: (_) => false, isFeatureSupported: (_) => false,
getFullJID: () => JID.fromString('hallo@example.server/uwu'), getFullJID: () => JID.fromString('hallo@example.server/uwu'),
getSocket: () => StubTCPSocket(play: []), getSocket: () => StubTCPSocket(play: []),
getConnection: () => XmppConnection(TestingReconnectionPolicy(), StubTCPSocket(play: [])), getConnection: () => XmppConnection(TestingReconnectionPolicy(), AlwaysConnectedConnectivityManager(), StubTCPSocket(play: [])),
getNegotiatorById: getNegotiatorNullStub, getNegotiatorById: getNegotiatorNullStub,
); );
} }
@ -233,7 +233,11 @@ void main() {
] ]
); );
final XmppConnection conn = XmppConnection(TestingReconnectionPolicy(), fakeSocket); final XmppConnection conn = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
fakeSocket,
);
conn.setConnectionSettings(ConnectionSettings( conn.setConnectionSettings(ConnectionSettings(
jid: JID.fromString('polynomdivision@test.server'), jid: JID.fromString('polynomdivision@test.server'),
password: 'aaaa', password: 'aaaa',
@ -355,7 +359,11 @@ void main() {
] ]
); );
final XmppConnection conn = XmppConnection(TestingReconnectionPolicy(), fakeSocket); final XmppConnection conn = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
fakeSocket,
);
conn.setConnectionSettings(ConnectionSettings( conn.setConnectionSettings(ConnectionSettings(
jid: JID.fromString('polynomdivision@test.server'), jid: JID.fromString('polynomdivision@test.server'),
password: 'aaaa', password: 'aaaa',
@ -510,7 +518,11 @@ void main() {
] ]
); );
final XmppConnection conn = XmppConnection(TestingReconnectionPolicy(), fakeSocket); final XmppConnection conn = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
fakeSocket,
);
conn.setConnectionSettings(ConnectionSettings( conn.setConnectionSettings(ConnectionSettings(
jid: JID.fromString('polynomdivision@test.server'), jid: JID.fromString('polynomdivision@test.server'),
password: 'aaaa', password: 'aaaa',
@ -602,7 +614,11 @@ void main() {
] ]
); );
final XmppConnection conn = XmppConnection(TestingReconnectionPolicy(), fakeSocket); final XmppConnection conn = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
fakeSocket,
);
conn.setConnectionSettings(ConnectionSettings( conn.setConnectionSettings(ConnectionSettings(
jid: JID.fromString('polynomdivision@test.server'), jid: JID.fromString('polynomdivision@test.server'),
password: 'aaaa', password: 'aaaa',
@ -694,7 +710,11 @@ void main() {
] ]
); );
final XmppConnection conn = XmppConnection(TestingReconnectionPolicy(), fakeSocket); final XmppConnection conn = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
fakeSocket,
);
conn.setConnectionSettings(ConnectionSettings( conn.setConnectionSettings(ConnectionSettings(
jid: JID.fromString('polynomdivision@test.server'), jid: JID.fromString('polynomdivision@test.server'),
password: 'aaaa', password: 'aaaa',

View File

@ -22,7 +22,7 @@ void main() {
isFeatureSupported: (_) => false, isFeatureSupported: (_) => false,
getFullJID: () => JID.fromString('bob@xmpp.example/uwu'), getFullJID: () => JID.fromString('bob@xmpp.example/uwu'),
getSocket: () => StubTCPSocket(play: []), getSocket: () => StubTCPSocket(play: []),
getConnection: () => XmppConnection(TestingReconnectionPolicy(), StubTCPSocket(play: [])), getConnection: () => XmppConnection(TestingReconnectionPolicy(), AlwaysConnectedConnectivityManager(), StubTCPSocket(play: [])),
getNegotiatorById: getNegotiatorNullStub, getNegotiatorById: getNegotiatorNullStub,
); );
final manager = CarbonsManager(); final manager = CarbonsManager();

View File

@ -50,7 +50,7 @@ void main() {
isFeatureSupported: (_) => false, isFeatureSupported: (_) => false,
getFullJID: () => JID.fromString('some.user@example.server/aaaaa'), getFullJID: () => JID.fromString('some.user@example.server/aaaaa'),
getSocket: () => StubTCPSocket(play: []), getSocket: () => StubTCPSocket(play: []),
getConnection: () => XmppConnection(TestingReconnectionPolicy(), StubTCPSocket(play: [])), getConnection: () => XmppConnection(TestingReconnectionPolicy(), AlwaysConnectedConnectivityManager(), StubTCPSocket(play: [])),
), ),
); );
@ -79,7 +79,7 @@ void main() {
isFeatureSupported: (_) => false, isFeatureSupported: (_) => false,
getFullJID: () => JID.fromString('some.user@example.server/aaaaa'), getFullJID: () => JID.fromString('some.user@example.server/aaaaa'),
getSocket: () => StubTCPSocket(play: []), getSocket: () => StubTCPSocket(play: []),
getConnection: () => XmppConnection(TestingReconnectionPolicy(), StubTCPSocket(play: [])), getConnection: () => XmppConnection(TestingReconnectionPolicy(), AlwaysConnectedConnectivityManager(), StubTCPSocket(play: [])),
), ),
); );

View File

@ -3,52 +3,52 @@ import 'package:test/test.dart';
void main() { void main() {
group('Test the XEP-0363 header preparation', () { group('Test the XEP-0363 header preparation', () {
test('invariance', () { test('invariance', () {
final headers = { final headers = {
'authorization': 'Basic Base64String==', 'authorization': 'Basic Base64String==',
'cookie': 'foo=bar; user=romeo' 'cookie': 'foo=bar; user=romeo'
}; };
expect( expect(
prepareHeaders(headers), prepareHeaders(headers),
headers, headers,
); );
}); });
test('invariance through uppercase', () { test('invariance through uppercase', () {
final headers = { final headers = {
'Authorization': 'Basic Base64String==', 'Authorization': 'Basic Base64String==',
'Cookie': 'foo=bar; user=romeo' 'Cookie': 'foo=bar; user=romeo'
}; };
expect( expect(
prepareHeaders(headers), prepareHeaders(headers),
headers, headers,
); );
}); });
test('remove unspecified headers', () { test('remove unspecified headers', () {
final headers = { final headers = {
'Authorization': 'Basic Base64String==', 'Authorization': 'Basic Base64String==',
'Cookie': 'foo=bar; user=romeo', 'Cookie': 'foo=bar; user=romeo',
'X-Tracking': 'Base64String==' 'X-Tracking': 'Base64String=='
}; };
expect( expect(
prepareHeaders(headers), prepareHeaders(headers),
{ {
'Authorization': 'Basic Base64String==', 'Authorization': 'Basic Base64String==',
'Cookie': 'foo=bar; user=romeo', 'Cookie': 'foo=bar; user=romeo',
} }
); );
}); });
test('remove newlines', () { test('remove newlines', () {
final headers = { final headers = {
'Authorization': '\n\nBasic Base64String==\n\n', 'Authorization': '\n\nBasic Base64String==\n\n',
'\nCookie\r\n': 'foo=bar; user=romeo', '\nCookie\r\n': 'foo=bar; user=romeo',
}; };
expect( expect(
prepareHeaders(headers), prepareHeaders(headers),
{ {
'Authorization': 'Basic Base64String==', 'Authorization': 'Basic Base64String==',
'Cookie': 'foo=bar; user=romeo', 'Cookie': 'foo=bar; user=romeo',
} }
); );
}); });
}); });
} }

View File

@ -25,7 +25,7 @@ Future<bool> testRosterManager(String bareJid, String resource, String stanzaStr
isFeatureSupported: (_) => false, isFeatureSupported: (_) => false,
getFullJID: () => JID.fromString('$bareJid/$resource'), getFullJID: () => JID.fromString('$bareJid/$resource'),
getSocket: () => StubTCPSocket(play: []), getSocket: () => StubTCPSocket(play: []),
getConnection: () => XmppConnection(TestingReconnectionPolicy(), StubTCPSocket(play: [])), getConnection: () => XmppConnection(TestingReconnectionPolicy(), AlwaysConnectedConnectivityManager(), StubTCPSocket(play: [])),
),); ),);
final stanza = Stanza.fromXMLNode(XMLNode.fromString(stanzaString)); final stanza = Stanza.fromXMLNode(XMLNode.fromString(stanzaString));
@ -118,7 +118,10 @@ void main() {
], ],
); );
// TODO: This test is broken since we query the server and enable carbons // TODO: This test is broken since we query the server and enable carbons
final XmppConnection conn = XmppConnection(TestingReconnectionPolicy(), fakeSocket); final XmppConnection conn = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
fakeSocket);
conn.setConnectionSettings(ConnectionSettings( conn.setConnectionSettings(ConnectionSettings(
jid: JID.fromString('polynomdivision@test.server'), jid: JID.fromString('polynomdivision@test.server'),
password: 'aaaa', password: 'aaaa',
@ -172,7 +175,11 @@ void main() {
], ],
); );
var receivedEvent = false; var receivedEvent = false;
final XmppConnection conn = XmppConnection(TestingReconnectionPolicy(), fakeSocket); final XmppConnection conn = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
fakeSocket,
);
conn.setConnectionSettings(ConnectionSettings( conn.setConnectionSettings(ConnectionSettings(
jid: JID.fromString('polynomdivision@test.server'), jid: JID.fromString('polynomdivision@test.server'),
password: 'aaaa', password: 'aaaa',
@ -226,7 +233,11 @@ void main() {
], ],
); );
var receivedEvent = false; var receivedEvent = false;
final XmppConnection conn = XmppConnection(TestingReconnectionPolicy(), fakeSocket); final XmppConnection conn = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
fakeSocket,
);
conn.setConnectionSettings(ConnectionSettings( conn.setConnectionSettings(ConnectionSettings(
jid: JID.fromString('polynomdivision@test.server'), jid: JID.fromString('polynomdivision@test.server'),
password: 'aaaa', password: 'aaaa',
@ -326,7 +337,7 @@ void main() {
isFeatureSupported: (_) => false, isFeatureSupported: (_) => false,
getFullJID: () => JID.fromString('some.user@example.server/aaaaa'), getFullJID: () => JID.fromString('some.user@example.server/aaaaa'),
getSocket: () => StubTCPSocket(play: []), getSocket: () => StubTCPSocket(play: []),
getConnection: () => XmppConnection(TestingReconnectionPolicy(), StubTCPSocket(play: [])), getConnection: () => XmppConnection(TestingReconnectionPolicy(), AlwaysConnectedConnectivityManager(), StubTCPSocket(play: [])),
),); ),);
// NOTE: Based on https://gultsch.de/gajim_roster_push_and_message_interception.html // NOTE: Based on https://gultsch.de/gajim_roster_push_and_message_interception.html