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;
export 'package:moxxmpp/src/connection.dart';
export 'package:moxxmpp/src/connectivity.dart';
export 'package:moxxmpp/src/errors.dart';
export 'package:moxxmpp/src/events.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:moxxmpp/src/awaiter.dart';
import 'package:moxxmpp/src/buffer.dart';
import 'package:moxxmpp/src/connectivity.dart';
import 'package:moxxmpp/src/errors.dart';
import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/iq.dart';
@ -94,22 +95,24 @@ class XmppConnectionResult {
class XmppConnection {
XmppConnection(
ReconnectionPolicy reconnectionPolicy,
ConnectivityManager connectivityManager,
this._socket,
{
this.connectionPingDuration = const Duration(minutes: 3),
this.connectingTimeout = const Duration(minutes: 2),
}
) : _reconnectionPolicy = reconnectionPolicy {
// Allow the reconnection policy to perform reconnections by itself
_reconnectionPolicy.register(
_attemptReconnection,
_onNetworkConnectionLost,
);
) : _reconnectionPolicy = reconnectionPolicy,
_connectivityManager = connectivityManager {
// Allow the reconnection policy to perform reconnections by itself
_reconnectionPolicy.register(
_attemptReconnection,
_onNetworkConnectionLost,
);
_socketStream = _socket.getDataStream();
// TODO(Unknown): Handle on done
_socketStream.transform(_streamBuffer).forEach(handleXmlStream);
_socket.getEventStream().listen(_handleSocketEvent);
_socketStream = _socket.getDataStream();
// TODO(Unknown): Handle on done
_socketStream.transform(_streamBuffer).forEach(handleXmlStream);
_socket.getEventStream().listen(_handleSocketEvent);
}
@ -122,13 +125,16 @@ class XmppConnection {
/// The data stream of the socket
late final Stream<String> _socketStream;
/// Connection settings
late ConnectionSettings _connectionSettings;
/// A policy on how to reconnect
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
final StanzaAwaiter _stanzaAwaiter = StanzaAwaiter();
@ -378,7 +384,7 @@ class XmppConnection {
// Connect again
// ignore: cascade_invocations
_log.finest('Calling connect() from _attemptReconnection');
await connect();
await connect(waitForConnection: true);
}
/// Called when a stream ending error has occurred
@ -401,7 +407,11 @@ class XmppConnection {
return;
}
await _setConnectionState(XmppConnectionState.error);
if (await _connectivityManager.hasConnection()) {
await _setConnectionState(XmppConnectionState.error);
} else {
await _setConnectionState(XmppConnectionState.notConnected);
}
await _reconnectionPolicy.onFailure();
}
@ -832,7 +842,6 @@ class XmppConnection {
if (_isMandatoryNegotiationDone(_streamFeatures) && !_isNegotiationPossible(_streamFeatures)) {
_log.finest('Negotiations done!');
_updateRoutingState(RoutingState.handleStanzas);
await _reconnectionPolicy.onSuccess();
await _resetIsConnectionRunning();
await _onNegotiationsDone();
} else {
@ -857,7 +866,6 @@ class XmppConnection {
_log.finest('Negotiations done!');
_updateRoutingState(RoutingState.handleStanzas);
await _reconnectionPolicy.onSuccess();
await _resetIsConnectionRunning();
await _onNegotiationsDone();
} else {
@ -875,7 +883,6 @@ class XmppConnection {
_log.finest('Negotiator wants to skip the remaining negotiation... Negotiations (assumed) done!');
_updateRoutingState(RoutingState.handleStanzas);
await _reconnectionPolicy.onSuccess();
await _resetIsConnectionRunning();
await _onNegotiationsDone();
break;
@ -987,7 +994,7 @@ class XmppConnection {
}
Future<void> _disconnect({required XmppConnectionState state, bool triggeredByUser = true}) async {
_reconnectionPolicy.setShouldReconnect(false);
await _reconnectionPolicy.setShouldReconnect(false);
if (triggeredByUser) {
getPresenceManager().sendUnavailablePresence();
@ -1018,17 +1025,21 @@ class XmppConnection {
/// Like [connect] but the Future resolves when the resource binding is either done or
/// SASL has failed.
Future<XmppConnectionResult> connectAwaitable({ String? lastResource }) async {
Future<XmppConnectionResult> connectAwaitable({ String? lastResource, bool waitForConnection = false }) async {
_runPreConnectionAssertions();
await _resetIsConnectionRunning();
_connectionCompleter = Completer();
_log.finest('Calling connect() from connectAwaitable');
await connect(lastResource: lastResource);
await connect(
lastResource: lastResource,
waitForConnection: waitForConnection,
shouldReconnect: false,
);
return _connectionCompleter!.future;
}
/// 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) {
_log.fine('Cancelling this connection attempt as one appears to be already running.');
return;
@ -1036,15 +1047,25 @@ class XmppConnection {
_runPreConnectionAssertions();
await _resetIsConnectionRunning();
_reconnectionPolicy.setShouldReconnect(true);
if (lastResource != null) {
setResource(lastResource);
}
if (shouldReconnect) {
await _reconnectionPolicy.setShouldReconnect(true);
}
await _reconnectionPolicy.reset();
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();
String? host;
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 'package:logging/logging.dart';
import 'package:meta/meta.dart';
import 'package:moxxmpp/src/util/queue.dart';
import 'package:synchronized/synchronized.dart';
/// 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;
/// Indicate if a reconnection attempt is currently running.
bool _isReconnecting = false;
@protected
bool isReconnecting = false;
/// 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.
void register(PerformReconnectFunction performReconnect, ConnectionLostCallback triggerConnectionLost) {
@ -48,96 +55,121 @@ abstract class ReconnectionPolicy {
/// Caled by the XmppConnection when the reconnection was successful.
Future<void> onSuccess();
bool get shouldReconnect => _shouldAttemptReconnection;
Future<bool> getShouldReconnect() async {
return shouldReconnectLock.synchronized(() => _shouldAttemptReconnection);
}
/// Set whether a reconnection attempt should be made.
void setShouldReconnect(bool value) {
_shouldAttemptReconnection = value;
Future<void> setShouldReconnect(bool value) async {
return shouldReconnectLock.synchronized(() => _shouldAttemptReconnection = value);
}
/// Returns true if the manager is currently triggering a reconnection. If not, returns
/// false.
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
Future<void> setIsReconnecting(bool value) async {
await _isReconnectingLock.synchronized(() async {
_isReconnecting = value;
await lock.synchronized(() async {
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
/// for every failed attempt.
/// NOTE: This ReconnectionPolicy may be broken
class ExponentialBackoffReconnectionPolicy extends ReconnectionPolicy {
ExponentialBackoffReconnectionPolicy(this._maxBackoffTime) : super();
class RandomBackoffReconnectionPolicy extends ReconnectionPolicy {
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;
/// Amount of consecutive failed reconnections.
int _counter = 0;
/// The minimum time in seconds that a backoff should be.
final int _minBackoffTime;
/// Backoff timer.
Timer? _timer;
final Lock _timerLock = Lock();
/// Logger.
final Logger _log = Logger('ExponentialBackoffReconnectionPolicy');
final Logger _log = Logger('RandomBackoffReconnectionPolicy');
/// Event queue
final AsyncQueue _eventQueue = AsyncQueue();
/// Called when the backoff expired
Future<void> _onTimerElapsed() async {
final isReconnecting = await isReconnectionRunning();
if (shouldReconnect) {
if (!isReconnecting) {
await setIsReconnecting(true);
await performReconnect!();
} else {
// Should never happen.
_log.fine('Backoff timer expired but reconnection is running, so doing nothing.');
_log.fine('Timer elapsed. Waiting for lock');
await lock.synchronized(() async {
_log.fine('Lock aquired');
if (!(await getShouldReconnect())) {
_log.fine('Backoff timer expired but getShouldReconnect() returned false');
return;
}
}
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
Future<void> reset() async {
_log.finest('Resetting internal state');
_counter = 0;
await setIsReconnecting(false);
// ignore: unnecessary_lambdas
await _eventQueue.addJob(() => _reset());
}
if (_timer != null) {
_timer!.cancel();
_timer = null;
Future<void> _onFailure() async {
final shouldContinue = await _timerLock.synchronized(() {
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
Future<void> onFailure() async {
_log.finest('Failure occured. Starting exponential backoff');
_counter++;
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);
// ignore: unnecessary_lambdas
await _eventQueue.addJob(() => _onFailure());
}
@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)
..registerFeatureNegotiators([
final connection = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
stubSocket,
)..registerFeatureNegotiators([
StubNegotiator1(),
StubNegotiator2(),
])

View File

@ -3,14 +3,14 @@ import 'package:test/test.dart';
void main() {
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));
expect(form.getFieldByVar('FORM_TYPE')?.values.first, 'urn:xmpp:dataforms:softwareinfo');
expect(form.getFieldByVar('ip_version')?.values, [ 'ipv4', 'ipv6' ]);
expect(form.getFieldByVar('os')?.values.first, 'Mac');
expect(form.getFieldByVar('os_version')?.values.first, '10.5.1');
expect(form.getFieldByVar('software')?.values.first, 'Psi');
expect(form.getFieldByVar('software_version')?.values.first, '0.11');
final form = parseDataForm(XMLNode.fromString(testData));
expect(form.getFieldByVar('FORM_TYPE')?.values.first, 'urn:xmpp:dataforms:softwareinfo');
expect(form.getFieldByVar('ip_version')?.values, [ 'ipv4', 'ipv6' ]);
expect(form.getFieldByVar('os')?.values.first, 'Mac');
expect(form.getFieldByVar('os_version')?.values.first, '10.5.1');
expect(form.getFieldByVar('software')?.values.first, 'Psi');
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(
jid: JID.fromString('polynomdivision@test.server'),
password: 'aaaa',
@ -74,7 +78,7 @@ void main() {
),);
conn.registerManagers([
PresenceManager('http://moxxmpp.example'),
RosterManager(),
RosterManager(TestingRosterStateManager(null, [])),
DiscoManager(),
PingManager(),
]);

View File

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

View File

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

View File

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

View File

@ -50,7 +50,7 @@ void main() {
isFeatureSupported: (_) => false,
getFullJID: () => JID.fromString('some.user@example.server/aaaaa'),
getSocket: () => StubTCPSocket(play: []),
getConnection: () => XmppConnection(TestingReconnectionPolicy(), StubTCPSocket(play: [])),
getConnection: () => XmppConnection(TestingReconnectionPolicy(), AlwaysConnectedConnectivityManager(), StubTCPSocket(play: [])),
),
);
@ -79,7 +79,7 @@ void main() {
isFeatureSupported: (_) => false,
getFullJID: () => JID.fromString('some.user@example.server/aaaaa'),
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() {
group('Test the XEP-0363 header preparation', () {
test('invariance', () {
final headers = {
'authorization': 'Basic Base64String==',
'cookie': 'foo=bar; user=romeo'
};
expect(
prepareHeaders(headers),
headers,
);
});
test('invariance through uppercase', () {
final headers = {
'Authorization': 'Basic Base64String==',
'Cookie': 'foo=bar; user=romeo'
};
expect(
prepareHeaders(headers),
headers,
);
});
test('remove unspecified headers', () {
final headers = {
'Authorization': 'Basic Base64String==',
'Cookie': 'foo=bar; user=romeo',
'X-Tracking': 'Base64String=='
};
expect(
prepareHeaders(headers),
{
'Authorization': 'Basic Base64String==',
'Cookie': 'foo=bar; user=romeo',
}
);
});
test('remove newlines', () {
final headers = {
'Authorization': '\n\nBasic Base64String==\n\n',
'\nCookie\r\n': 'foo=bar; user=romeo',
};
expect(
prepareHeaders(headers),
{
'Authorization': 'Basic Base64String==',
'Cookie': 'foo=bar; user=romeo',
}
);
});
test('invariance', () {
final headers = {
'authorization': 'Basic Base64String==',
'cookie': 'foo=bar; user=romeo'
};
expect(
prepareHeaders(headers),
headers,
);
});
test('invariance through uppercase', () {
final headers = {
'Authorization': 'Basic Base64String==',
'Cookie': 'foo=bar; user=romeo'
};
expect(
prepareHeaders(headers),
headers,
);
});
test('remove unspecified headers', () {
final headers = {
'Authorization': 'Basic Base64String==',
'Cookie': 'foo=bar; user=romeo',
'X-Tracking': 'Base64String=='
};
expect(
prepareHeaders(headers),
{
'Authorization': 'Basic Base64String==',
'Cookie': 'foo=bar; user=romeo',
}
);
});
test('remove newlines', () {
final headers = {
'Authorization': '\n\nBasic Base64String==\n\n',
'\nCookie\r\n': 'foo=bar; user=romeo',
};
expect(
prepareHeaders(headers),
{
'Authorization': 'Basic Base64String==',
'Cookie': 'foo=bar; user=romeo',
}
);
});
});
}

View File

@ -25,7 +25,7 @@ Future<bool> testRosterManager(String bareJid, String resource, String stanzaStr
isFeatureSupported: (_) => false,
getFullJID: () => JID.fromString('$bareJid/$resource'),
getSocket: () => StubTCPSocket(play: []),
getConnection: () => XmppConnection(TestingReconnectionPolicy(), StubTCPSocket(play: [])),
getConnection: () => XmppConnection(TestingReconnectionPolicy(), AlwaysConnectedConnectivityManager(), StubTCPSocket(play: [])),
),);
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
final XmppConnection conn = XmppConnection(TestingReconnectionPolicy(), fakeSocket);
final XmppConnection conn = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
fakeSocket);
conn.setConnectionSettings(ConnectionSettings(
jid: JID.fromString('polynomdivision@test.server'),
password: 'aaaa',
@ -172,7 +175,11 @@ void main() {
],
);
var receivedEvent = false;
final XmppConnection conn = XmppConnection(TestingReconnectionPolicy(), fakeSocket);
final XmppConnection conn = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
fakeSocket,
);
conn.setConnectionSettings(ConnectionSettings(
jid: JID.fromString('polynomdivision@test.server'),
password: 'aaaa',
@ -226,7 +233,11 @@ void main() {
],
);
var receivedEvent = false;
final XmppConnection conn = XmppConnection(TestingReconnectionPolicy(), fakeSocket);
final XmppConnection conn = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
fakeSocket,
);
conn.setConnectionSettings(ConnectionSettings(
jid: JID.fromString('polynomdivision@test.server'),
password: 'aaaa',
@ -326,7 +337,7 @@ void main() {
isFeatureSupported: (_) => false,
getFullJID: () => JID.fromString('some.user@example.server/aaaaa'),
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