diff --git a/packages/moxxmpp/lib/src/connection.dart b/packages/moxxmpp/lib/src/connection.dart index 3473d35..3fb5ae8 100644 --- a/packages/moxxmpp/lib/src/connection.dart +++ b/packages/moxxmpp/lib/src/connection.dart @@ -373,7 +373,7 @@ class XmppConnection { /// Called when a stream ending error has occurred Future 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(); } diff --git a/packages/moxxmpp/lib/src/errors.dart b/packages/moxxmpp/lib/src/errors.dart index 15dbcb8..6bc458e 100644 --- a/packages/moxxmpp/lib/src/errors.dart +++ b/packages/moxxmpp/lib/src/errors.dart @@ -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; +} diff --git a/packages/moxxmpp/lib/src/events.dart b/packages/moxxmpp/lib/src/events.dart index 3b674bf..82d2f78 100644 --- a/packages/moxxmpp/lib/src/events.dart +++ b/packages/moxxmpp/lib/src/events.dart @@ -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 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; +} diff --git a/packages/moxxmpp/lib/src/negotiators/resource_binding.dart b/packages/moxxmpp/lib/src/negotiators/resource_binding.dart index ec46984..ecc177a 100644 --- a/packages/moxxmpp/lib/src/negotiators/resource_binding.dart +++ b/packages/moxxmpp/lib/src/negotiators/resource_binding.dart @@ -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 features) { diff --git a/packages/moxxmpp/lib/src/negotiators/sasl/errors.dart b/packages/moxxmpp/lib/src/negotiators/sasl/errors.dart index 32348fa..1ffd652 100644 --- a/packages/moxxmpp/lib/src/negotiators/sasl/errors.dart +++ b/packages/moxxmpp/lib/src/negotiators/sasl/errors.dart @@ -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 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 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 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; +} diff --git a/packages/moxxmpp/lib/src/negotiators/sasl/plain.dart b/packages/moxxmpp/lib/src/negotiators/sasl/plain.dart index da40ea6..9e29daa 100644 --- a/packages/moxxmpp/lib/src/negotiators/sasl/plain.dart +++ b/packages/moxxmpp/lib/src/negotiators/sasl/plain.dart @@ -59,7 +59,9 @@ class SaslPlainNegotiator extends SaslNegotiator { // We assume it's a final error = nonza.children.first.tag; await attributes.sendEvent(AuthenticationFailedEvent(error)); - return Result(SaslFailedError()); + return Result( + SaslError.fromFailure(nonza), + ); } } } diff --git a/packages/moxxmpp/lib/src/negotiators/sasl/scram.dart b/packages/moxxmpp/lib/src/negotiators/sasl/scram.dart index b8f5b7d..baafec7 100644 --- a/packages/moxxmpp/lib/src/negotiators/sasl/scram.dart +++ b/packages/moxxmpp/lib/src/negotiators/sasl/scram.dart @@ -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), + ); } } diff --git a/packages/moxxmpp/lib/src/negotiators/starttls.dart b/packages/moxxmpp/lib/src/negotiators/starttls.dart index 0d2df94..5f8735a 100644 --- a/packages/moxxmpp/lib/src/negotiators/starttls.dart +++ b/packages/moxxmpp/lib/src/negotiators/starttls.dart @@ -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> negotiate(XMLNode nonza) async {