feat: Don't attempt reconnections when the error is unrecoverable

Fixes #25.
Should fix #24.
This commit is contained in:
PapaTutuWawa 2023-01-28 13:20:16 +01:00
parent 7f294d6632
commit 96d9ce4761
8 changed files with 127 additions and 25 deletions

View File

@ -373,7 +373,7 @@ class XmppConnection {
/// Called when a stream ending error has occurred /// Called when a stream ending error has occurred
Future<void> handleError(XmppError error) async { Future<void> handleError(XmppError error) async {
_log.severe('handleError called with ${error.toString()}'); _log.severe('handleError called with ${error.toString()}');
// Whenever we encounter an error that would trigger a reconnection attempt while // Whenever we encounter an error that would trigger a reconnection attempt while
// the connection result is being awaited, don't attempt a reconnection but instead // the connection result is being awaited, don't attempt a reconnection but instead
// try to gracefully disconnect. // try to gracefully disconnect.
@ -390,11 +390,18 @@ class XmppConnection {
return; return;
} }
if (await _connectivityManager.hasConnection()) { if (!error.isRecoverable()) {
// We cannot recover this error
_log.severe('Since a $error is not recoverable, not attempting a reconnection');
await _setConnectionState(XmppConnectionState.error); await _setConnectionState(XmppConnectionState.error);
} else { await _sendEvent(
await _setConnectionState(XmppConnectionState.notConnected); NonRecoverableErrorEvent(error),
);
return;
} }
// The error is recoverable
await _setConnectionState(XmppConnectionState.notConnected);
await _reconnectionPolicy.onFailure(); await _reconnectionPolicy.onFailure();
} }

View File

@ -1,20 +1,37 @@
import 'package:moxxmpp/src/socket.dart'; import 'package:moxxmpp/src/socket.dart';
/// An internal error class /// An internal error class
abstract class XmppError {} // ignore: one_member_abstracts
abstract class XmppError {
/// Return true if we can recover from the error by attempting a reconnection.
bool isRecoverable();
}
/// Returned if we could not establish a TCP connection /// Returned if we could not establish a TCP connection
/// to the server. /// to the server.
class NoConnectionError extends XmppError {} class NoConnectionError extends XmppError {
@override
bool isRecoverable() => true;
}
/// Returned if a socket error occured /// Returned if a socket error occured
class SocketError extends XmppError { class SocketError extends XmppError {
SocketError(this.event); SocketError(this.event);
final XmppSocketErrorEvent event; final XmppSocketErrorEvent event;
@override
bool isRecoverable() => true;
} }
/// Returned if we time out /// Returned if we time out
class TimeoutError extends XmppError {} class TimeoutError extends XmppError {
@override
bool isRecoverable() => true;
}
/// Returned if we received a stream error /// Returned if we received a stream error
class StreamError extends XmppError {} class StreamError extends XmppError {
// TODO(PapaTutuWawa): Be more precise
@override
bool isRecoverable() => true;
}

View File

@ -1,4 +1,5 @@
import 'package:moxxmpp/src/connection.dart'; import 'package:moxxmpp/src/connection.dart';
import 'package:moxxmpp/src/errors.dart';
import 'package:moxxmpp/src/jid.dart'; import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/data.dart'; import 'package:moxxmpp/src/managers/data.dart';
import 'package:moxxmpp/src/roster/roster.dart'; import 'package:moxxmpp/src/roster/roster.dart';
@ -236,3 +237,12 @@ class OmemoDeviceListUpdatedEvent extends XmppEvent {
final JID jid; final JID jid;
final List<int> deviceList; final List<int> deviceList;
} }
/// Triggered when a reconnection is not performed due to a non-recoverable
/// error.
class NonRecoverableErrorEvent extends XmppEvent {
NonRecoverableErrorEvent(this.error);
/// The error in question.
final XmppError error;
}

View File

@ -8,12 +8,20 @@ import 'package:moxxmpp/src/types/result.dart';
import 'package:moxxmpp/src/xeps/xep_0198/xep_0198.dart'; import 'package:moxxmpp/src/xeps/xep_0198/xep_0198.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
class ResourceBindingFailedError extends NegotiatorError {} class ResourceBindingFailedError extends NegotiatorError {
@override
bool isRecoverable() => true;
}
/// A negotiator that implements resource binding against a random server-provided
/// resource.
class ResourceBindingNegotiator extends XmppFeatureNegotiatorBase { class ResourceBindingNegotiator extends XmppFeatureNegotiatorBase {
ResourceBindingNegotiator() : super(0, false, bindXmlns, resourceBindingNegotiator);
ResourceBindingNegotiator() : _requestSent = false, super(0, false, bindXmlns, resourceBindingNegotiator); /// Flag indicating the state of the negotiator:
bool _requestSent; /// - True: We sent a binding request
/// - False: We have not yet sent the binding request
bool _requestSent = false;
@override @override
bool matchesFeature(List<XMLNode> features) { bool matchesFeature(List<XMLNode> features) {

View File

@ -1,3 +1,50 @@
import 'package:moxxmpp/src/negotiators/negotiator.dart'; import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/stringxml.dart';
class SaslFailedError extends NegotiatorError {} abstract class SaslError extends NegotiatorError {
static SaslError fromFailure(XMLNode failure) {
XMLNode? error;
for (final child in failure.children) {
if (child.tag == 'text') continue;
error = child;
break;
}
switch (error?.tag) {
case 'credentials-expired': return SaslCredentialsExpiredError();
case 'not-authorized': return SaslNotAuthorizedError();
case 'account-disabled': return SaslAccountDisabledError();
}
return SaslUnspecifiedError();
}
}
/// Triggered when the server returned us a <not-authorized /> failure during SASL
/// (https://xmpp.org/rfcs/rfc6120.html#sasl-errors-not-authorized).
class SaslNotAuthorizedError extends SaslError {
@override
bool isRecoverable() => false;
}
/// Triggered when the server returned us a <credentials-expired /> failure during SASL
/// (https://xmpp.org/rfcs/rfc6120.html#sasl-errors-credentials-expired).
class SaslCredentialsExpiredError extends SaslError {
@override
bool isRecoverable() => false;
}
/// Triggered when the server returned us a <account-disabled /> failure during SASL
/// (https://xmpp.org/rfcs/rfc6120.html#sasl-errors-account-disabled).
class SaslAccountDisabledError extends SaslError {
@override
bool isRecoverable() => false;
}
/// An unspecified SASL error, i.e. everything not matched by any more precise erorr
/// class.
class SaslUnspecifiedError extends SaslError {
@override
bool isRecoverable() => true;
}

View File

@ -59,7 +59,9 @@ class SaslPlainNegotiator extends SaslNegotiator {
// We assume it's a <failure/> // We assume it's a <failure/>
final error = nonza.children.first.tag; final error = nonza.children.first.tag;
await attributes.sendEvent(AuthenticationFailedEvent(error)); await attributes.sendEvent(AuthenticationFailedEvent(error));
return Result(SaslFailedError()); return Result(
SaslError.fromFailure(nonza),
);
} }
} }
} }

View File

@ -218,7 +218,9 @@ class SaslScramNegotiator extends SaslNegotiator {
await attributes.sendEvent(AuthenticationFailedEvent(error)); await attributes.sendEvent(AuthenticationFailedEvent(error));
_scramState = ScramState.error; _scramState = ScramState.error;
return Result(SaslFailedError()); return Result(
SaslError.fromFailure(nonza),
);
} }
final challengeBase64 = nonza.innerText(); final challengeBase64 = nonza.innerText();
@ -236,7 +238,9 @@ class SaslScramNegotiator extends SaslNegotiator {
final error = nonza.children.first.tag; final error = nonza.children.first.tag;
await attributes.sendEvent(AuthenticationFailedEvent(error)); await attributes.sendEvent(AuthenticationFailedEvent(error));
_scramState = ScramState.error; _scramState = ScramState.error;
return Result(SaslFailedError()); return Result(
SaslError.fromFailure(nonza),
);
} }
// NOTE: This assumes that the string is always "v=..." and contains no other parameters // NOTE: This assumes that the string is always "v=..." and contains no other parameters
@ -246,13 +250,17 @@ class SaslScramNegotiator extends SaslNegotiator {
//final error = nonza.children.first.tag; //final error = nonza.children.first.tag;
//attributes.sendEvent(AuthenticationFailedEvent(error)); //attributes.sendEvent(AuthenticationFailedEvent(error));
_scramState = ScramState.error; _scramState = ScramState.error;
return Result(SaslFailedError()); return Result(
SaslError.fromFailure(nonza),
);
} }
await attributes.sendEvent(AuthenticationSuccessEvent()); await attributes.sendEvent(AuthenticationSuccessEvent());
return const Result(NegotiatorState.done); return const Result(NegotiatorState.done);
case ScramState.error: case ScramState.error:
return Result(SaslFailedError()); return Result(
SaslError.fromFailure(nonza),
);
} }
} }

View File

@ -10,7 +10,10 @@ enum _StartTlsState {
requested requested
} }
class StartTLSFailedError extends NegotiatorError {} class StartTLSFailedError extends NegotiatorError {
@override
bool isRecoverable() => true;
}
class StartTLSNonza extends XMLNode { class StartTLSNonza extends XMLNode {
StartTLSNonza() : super.xmlns( StartTLSNonza() : super.xmlns(
@ -19,15 +22,15 @@ class StartTLSNonza extends XMLNode {
); );
} }
/// A negotiator implementing StartTLS.
class StartTlsNegotiator extends XmppFeatureNegotiatorBase { class StartTlsNegotiator extends XmppFeatureNegotiatorBase {
StartTlsNegotiator() : super(10, true, startTlsXmlns, startTlsNegotiator);
StartTlsNegotiator()
: _state = _StartTlsState.ready,
_log = Logger('StartTlsNegotiator'),
super(10, true, startTlsXmlns, startTlsNegotiator);
_StartTlsState _state;
final Logger _log; /// The state of the negotiator.
_StartTlsState _state = _StartTlsState.ready;
/// Logger.
final Logger _log = Logger('StartTlsNegotiator');
@override @override
Future<Result<NegotiatorState, NegotiatorError>> negotiate(XMLNode nonza) async { Future<Result<NegotiatorState, NegotiatorError>> negotiate(XMLNode nonza) async {