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
Future<void> handleError(XmppError error) async {
_log.severe('handleError called with ${error.toString()}');
// 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
// try to gracefully disconnect.
@ -390,11 +390,18 @@ class XmppConnection {
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);
} else {
await _setConnectionState(XmppConnectionState.notConnected);
await _sendEvent(
NonRecoverableErrorEvent(error),
);
return;
}
// The error is recoverable
await _setConnectionState(XmppConnectionState.notConnected);
await _reconnectionPolicy.onFailure();
}

View File

@ -1,20 +1,37 @@
import 'package:moxxmpp/src/socket.dart';
/// 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
/// to the server.
class NoConnectionError extends XmppError {}
class NoConnectionError extends XmppError {
@override
bool isRecoverable() => true;
}
/// Returned if a socket error occured
class SocketError extends XmppError {
SocketError(this.event);
final XmppSocketErrorEvent event;
@override
bool isRecoverable() => true;
}
/// Returned if we time out
class TimeoutError extends XmppError {}
class TimeoutError extends XmppError {
@override
bool isRecoverable() => true;
}
/// 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/errors.dart';
import 'package:moxxmpp/src/jid.dart';
import 'package:moxxmpp/src/managers/data.dart';
import 'package:moxxmpp/src/roster/roster.dart';
@ -236,3 +237,12 @@ class OmemoDeviceListUpdatedEvent extends XmppEvent {
final JID jid;
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: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 {
ResourceBindingNegotiator() : super(0, false, bindXmlns, resourceBindingNegotiator);
ResourceBindingNegotiator() : _requestSent = false, super(0, false, bindXmlns, resourceBindingNegotiator);
bool _requestSent;
/// Flag indicating the state of the negotiator:
/// - True: We sent a binding request
/// - False: We have not yet sent the binding request
bool _requestSent = false;
@override
bool matchesFeature(List<XMLNode> features) {

View File

@ -1,3 +1,50 @@
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/>
final error = nonza.children.first.tag;
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));
_scramState = ScramState.error;
return Result(SaslFailedError());
return Result(
SaslError.fromFailure(nonza),
);
}
final challengeBase64 = nonza.innerText();
@ -236,7 +238,9 @@ class SaslScramNegotiator extends SaslNegotiator {
final error = nonza.children.first.tag;
await attributes.sendEvent(AuthenticationFailedEvent(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
@ -246,13 +250,17 @@ class SaslScramNegotiator extends SaslNegotiator {
//final error = nonza.children.first.tag;
//attributes.sendEvent(AuthenticationFailedEvent(error));
_scramState = ScramState.error;
return Result(SaslFailedError());
return Result(
SaslError.fromFailure(nonza),
);
}
await attributes.sendEvent(AuthenticationSuccessEvent());
return const Result(NegotiatorState.done);
case ScramState.error:
return Result(SaslFailedError());
return Result(
SaslError.fromFailure(nonza),
);
}
}

View File

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