diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2022-11-30 10:37:14 +0100 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2022-11-30 10:37:14 +0100 |
| commit | 4bf5b1c5df0776d18e7a31f40c0d840e508b4b22 (patch) | |
| tree | 86b0897032d465ec70b878ee336443171455cd4f | |
| parent | d424bdbedeb1c8e44b693eac02d169e11b7ac92a (diff) | |
| parent | 4fe3a01bc392ee1392e23f424987a01733a9a57e (diff) | |
| download | mullvadvpn-4bf5b1c5df0776d18e7a31f40c0d840e508b4b22.tar.xz mullvadvpn-4bf5b1c5df0776d18e7a31f40c0d840e508b4b22.zip | |
Merge branch 'improve-tunnel-state-error-handling'
| -rw-r--r-- | CHANGELOG.md | 1 | ||||
| -rw-r--r-- | gui/src/main/daemon-rpc.ts | 115 | ||||
| -rw-r--r-- | gui/src/main/index.ts | 1 | ||||
| -rw-r--r-- | gui/src/main/notification-controller.ts | 3 | ||||
| -rw-r--r-- | gui/src/main/user-interface.ts | 2 | ||||
| -rw-r--r-- | gui/src/renderer/app.tsx | 2 | ||||
| -rw-r--r-- | gui/src/renderer/components/Connect.tsx | 2 | ||||
| -rw-r--r-- | gui/src/renderer/components/HeaderBar.tsx | 2 | ||||
| -rw-r--r-- | gui/src/renderer/components/MainView.tsx | 11 | ||||
| -rw-r--r-- | gui/src/renderer/components/NotificationArea.tsx | 2 | ||||
| -rw-r--r-- | gui/src/renderer/components/TunnelControl.tsx | 2 | ||||
| -rw-r--r-- | gui/src/renderer/redux/connection/actions.ts | 6 | ||||
| -rw-r--r-- | gui/src/shared/auth-failure.ts | 81 | ||||
| -rw-r--r-- | gui/src/shared/daemon-rpc-types.ts | 84 | ||||
| -rw-r--r-- | gui/src/shared/notifications/error.ts | 104 | ||||
| -rw-r--r-- | gui/test/e2e/mocked/tunnel-state.spec.ts | 6 | ||||
| -rw-r--r-- | gui/test/unit/auth-failure.spec.ts | 27 |
17 files changed, 213 insertions, 238 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 533044c4dd..f4e5f871fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -136,6 +136,7 @@ Identical to android/2022.2-beta2 except for updated translations. - Fix 'mullvad status -v' to include the port of the endpoint when connecting over TCP. - Check whether the device is valid when reconnecting from the error state. - Stop reconnecting when the account has run out of time. +- Show out of time view after failing to connect due to being out of time. #### Windows - Only use the most recent list of apps to split when resuming from hibernation/sleep if applying diff --git a/gui/src/main/daemon-rpc.ts b/gui/src/main/daemon-rpc.ts index a6136bc6f3..9898a1d2c2 100644 --- a/gui/src/main/daemon-rpc.ts +++ b/gui/src/main/daemon-rpc.ts @@ -10,6 +10,7 @@ import { promisify } from 'util'; import { AccountToken, AfterDisconnect, + AuthFailedError, BridgeSettings, BridgeState, ConnectionConfig, @@ -18,15 +19,16 @@ import { DeviceEvent, DeviceState, EndpointObfuscationType, + ErrorState, ErrorStateCause, FirewallPolicyError, + FirewallPolicyErrorType, IAccountData, IAppVersionInfo, IBridgeConstraints, IDevice, IDeviceRemoval, IDnsOptions, - IErrorState, ILocation, IObfuscationEndpoint, IOpenVpnConstraints, @@ -875,71 +877,104 @@ function convertFromTunnelState(tunnelState: grpcTypes.TunnelState): TunnelState } } -function convertFromTunnelStateError(state: grpcTypes.ErrorState.AsObject): IErrorState { - return { - ...state, - cause: convertFromTunnelStateErrorCause(state.cause, state), - blockFailure: state.blockingError - ? convertFromFirewallPolicyError(state.blockingError) - : undefined, +function convertFromTunnelStateError(state: grpcTypes.ErrorState.AsObject): ErrorState { + const baseError = { + blockingError: state.blockingError && convertFromBlockingError(state.blockingError), }; -} -function convertFromTunnelStateErrorCause( - cause: grpcTypes.ErrorState.Cause, - state: grpcTypes.ErrorState.AsObject, -): ErrorStateCause { - switch (cause) { + switch (state.cause) { + case grpcTypes.ErrorState.Cause.AUTH_FAILED: + return { + ...baseError, + cause: ErrorStateCause.authFailed, + authFailedError: convertFromAuthFailedError(state.authFailedError), + }; + case grpcTypes.ErrorState.Cause.TUNNEL_PARAMETER_ERROR: + return { + ...baseError, + cause: ErrorStateCause.tunnelParameterError, + parameterError: convertFromParameterError(state.parameterError), + }; + case grpcTypes.ErrorState.Cause.SET_FIREWALL_POLICY_ERROR: + return { + ...baseError, + cause: ErrorStateCause.setFirewallPolicyError, + policyError: convertFromBlockingError(state.policyError!), + }; + case grpcTypes.ErrorState.Cause.IS_OFFLINE: - return { reason: 'is_offline' }; + return { + ...baseError, + cause: ErrorStateCause.isOffline, + }; case grpcTypes.ErrorState.Cause.SET_DNS_ERROR: - return { reason: 'set_dns_error' }; + return { + ...baseError, + cause: ErrorStateCause.setDnsError, + }; case grpcTypes.ErrorState.Cause.IPV6_UNAVAILABLE: - return { reason: 'ipv6_unavailable' }; - case grpcTypes.ErrorState.Cause.START_TUNNEL_ERROR: - return { reason: 'start_tunnel_error' }; - case grpcTypes.ErrorState.Cause.SET_FIREWALL_POLICY_ERROR: return { - reason: 'set_firewall_policy_error', - details: convertFromFirewallPolicyError(state.policyError!), + ...baseError, + cause: ErrorStateCause.ipv6Unavailable, }; - case grpcTypes.ErrorState.Cause.AUTH_FAILED: - return { reason: 'auth_failed', details: state.authFailReason }; - case grpcTypes.ErrorState.Cause.TUNNEL_PARAMETER_ERROR: { - const parameterErrorMap: Record< - grpcTypes.ErrorState.GenerationError, - TunnelParameterError - > = { - [grpcTypes.ErrorState.GenerationError.NO_MATCHING_RELAY]: 'no_matching_relay', - [grpcTypes.ErrorState.GenerationError.NO_MATCHING_BRIDGE_RELAY]: 'no_matching_bridge_relay', - [grpcTypes.ErrorState.GenerationError.NO_WIREGUARD_KEY]: 'no_wireguard_key', - [grpcTypes.ErrorState.GenerationError.CUSTOM_TUNNEL_HOST_RESOLUTION_ERROR]: - 'custom_tunnel_host_resultion_error', + case grpcTypes.ErrorState.Cause.START_TUNNEL_ERROR: + return { + ...baseError, + cause: ErrorStateCause.startTunnelError, }; - return { reason: 'tunnel_parameter_error', details: parameterErrorMap[state.parameterError] }; - } case grpcTypes.ErrorState.Cause.SPLIT_TUNNEL_ERROR: - return { reason: 'split_tunnel_error' }; + return { + ...baseError, + cause: ErrorStateCause.splitTunnelError, + }; case grpcTypes.ErrorState.Cause.VPN_PERMISSION_DENIED: // VPN_PERMISSION_DENIED is only ever created on Android throw invalidErrorStateCause; } } -function convertFromFirewallPolicyError( +function convertFromBlockingError( error: grpcTypes.ErrorState.FirewallPolicyError.AsObject, ): FirewallPolicyError { switch (error.type) { case grpcTypes.ErrorState.FirewallPolicyError.ErrorType.GENERIC: - return { reason: 'generic' }; + return { type: FirewallPolicyErrorType.generic }; case grpcTypes.ErrorState.FirewallPolicyError.ErrorType.LOCKED: { const pid = error.lockPid; const name = error.lockName; - return { reason: 'locked', details: pid && name ? { pid, name } : undefined }; + return { type: FirewallPolicyErrorType.locked, pid, name }; } } } +function convertFromAuthFailedError(error: grpcTypes.ErrorState.AuthFailedError): AuthFailedError { + switch (error) { + case grpcTypes.ErrorState.AuthFailedError.UNKNOWN: + return AuthFailedError.unknown; + case grpcTypes.ErrorState.AuthFailedError.INVALID_ACCOUNT: + return AuthFailedError.invalidAccount; + case grpcTypes.ErrorState.AuthFailedError.EXPIRED_ACCOUNT: + return AuthFailedError.expiredAccount; + case grpcTypes.ErrorState.AuthFailedError.TOO_MANY_CONNECTIONS: + return AuthFailedError.tooManyConnections; + } +} + +function convertFromParameterError( + error: grpcTypes.ErrorState.GenerationError, +): TunnelParameterError { + switch (error) { + case grpcTypes.ErrorState.GenerationError.NO_MATCHING_RELAY: + return TunnelParameterError.noMatchingRelay; + case grpcTypes.ErrorState.GenerationError.NO_MATCHING_BRIDGE_RELAY: + return TunnelParameterError.noMatchingBridgeRelay; + case grpcTypes.ErrorState.GenerationError.NO_WIREGUARD_KEY: + return TunnelParameterError.noWireguardKey; + case grpcTypes.ErrorState.GenerationError.CUSTOM_TUNNEL_HOST_RESOLUTION_ERROR: + return TunnelParameterError.customTunnelHostResolutionError; + } +} + function convertFromTunnelStateRelayInfo( state: grpcTypes.TunnelStateRelayInfo.AsObject, ): ITunnelStateRelayInfo | undefined { diff --git a/gui/src/main/index.ts b/gui/src/main/index.ts index 320185abd8..06796a7be1 100644 --- a/gui/src/main/index.ts +++ b/gui/src/main/index.ts @@ -934,7 +934,6 @@ class ApplicationMain this.settings.splitTunnel.enableExclusions && this.settings.splitTunnel.appsList.length > 0, this.userInterface?.isWindowVisible() ?? false, this.settings.gui.enableSystemNotifications, - this.account.accountData?.expiry, ); IpcMainEventChannel.tunnel.notify?.(tunnelState); diff --git a/gui/src/main/notification-controller.ts b/gui/src/main/notification-controller.ts index 06a05366a7..3ea65fa5a0 100644 --- a/gui/src/main/notification-controller.ts +++ b/gui/src/main/notification-controller.ts @@ -56,14 +56,13 @@ export default class NotificationController { hasExcludedApps: boolean, isWindowVisible: boolean, areSystemNotificationsEnabled: boolean, - accountExpiry?: string, ) { const notificationProviders: SystemNotificationProvider[] = [ new ConnectingNotificationProvider({ tunnelState, reconnecting: this.reconnecting }), new ConnectedNotificationProvider(tunnelState), new ReconnectingNotificationProvider(tunnelState), new DisconnectedNotificationProvider({ tunnelState, blockWhenDisconnected }), - new ErrorNotificationProvider({ tunnelState, accountExpiry, hasExcludedApps }), + new ErrorNotificationProvider({ tunnelState, hasExcludedApps }), ]; const notificationProvider = notificationProviders.find((notification) => diff --git a/gui/src/main/user-interface.ts b/gui/src/main/user-interface.ts index 2c1ac03f27..9870c89702 100644 --- a/gui/src/main/user-interface.ts +++ b/gui/src/main/user-interface.ts @@ -653,7 +653,7 @@ export default class UserInterface implements WindowControllerDelegate { return 'securing'; case 'error': - if (!tunnelState.details.blockFailure) { + if (!tunnelState.details.blockingError) { return 'securing'; } else { return 'unsecured'; diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx index ae7cdec881..e78462041b 100644 --- a/gui/src/renderer/app.tsx +++ b/gui/src/renderer/app.tsx @@ -754,7 +754,7 @@ export default class AppRenderer { break; case 'error': - actions.updateBlockState(!tunnelState.details.blockFailure); + actions.updateBlockState(!tunnelState.details.blockingError); break; } } diff --git a/gui/src/renderer/components/Connect.tsx b/gui/src/renderer/components/Connect.tsx index 492fcc3fc9..79de3c5242 100644 --- a/gui/src/renderer/components/Connect.tsx +++ b/gui/src/renderer/components/Connect.tsx @@ -97,7 +97,7 @@ export default function Connect() { case 'connected': return MarkerStyle.secure; case 'error': - return !connection.status.details.blockFailure ? MarkerStyle.secure : MarkerStyle.unsecure; + return !connection.status.details.blockingError ? MarkerStyle.secure : MarkerStyle.unsecure; case 'disconnected': return MarkerStyle.unsecure; case 'disconnecting': diff --git a/gui/src/renderer/components/HeaderBar.tsx b/gui/src/renderer/components/HeaderBar.tsx index 3cf96df6fc..7c436e3c16 100644 --- a/gui/src/renderer/components/HeaderBar.tsx +++ b/gui/src/renderer/components/HeaderBar.tsx @@ -139,7 +139,7 @@ export function calculateHeaderBarStyle(tunnelState: TunnelState): HeaderBarStyl case 'connected': return HeaderBarStyle.success; case 'error': - return !tunnelState.details.blockFailure ? HeaderBarStyle.success : HeaderBarStyle.error; + return !tunnelState.details.blockingError ? HeaderBarStyle.success : HeaderBarStyle.error; case 'disconnecting': switch (tunnelState.details) { case 'block': diff --git a/gui/src/renderer/components/MainView.tsx b/gui/src/renderer/components/MainView.tsx index cc2419ddfd..983a33d376 100644 --- a/gui/src/renderer/components/MainView.tsx +++ b/gui/src/renderer/components/MainView.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from 'react'; import { hasExpired } from '../../shared/account-expiry'; +import { AuthFailedError, ErrorStateCause } from '../../shared/daemon-rpc-types'; import Connect from '../components/Connect'; import { useHistory } from '../lib/history'; import { RoutePath } from '../lib/routes'; @@ -16,6 +17,7 @@ export default function MainView() { const isNewAccount = useSelector( (state) => state.account.status.type === 'ok' && state.account.status.method === 'new_account', ); + const tunnelState = useSelector((state) => state.connection.status); const [showAccountExpired, setShowAccountExpired] = useState<ExpiryData>(() => isNewAccount || accountHasExpired ? { show: true, expiry: accountExpiry } : { show: false }, @@ -23,8 +25,11 @@ export default function MainView() { useEffect(() => { if ( - accountHasExpired && - (!showAccountExpired.show || showAccountExpired.expiry !== accountExpiry) + (!showAccountExpired.show || showAccountExpired.expiry !== accountExpiry) && + (accountHasExpired || + (tunnelState.state === 'error' && + tunnelState.details.cause === ErrorStateCause.authFailed && + tunnelState.details.authFailedError === AuthFailedError.expiredAccount)) ) { setShowAccountExpired({ show: true, expiry: accountExpiry }); } else if ( @@ -34,7 +39,7 @@ export default function MainView() { ) { history.push(RoutePath.timeAdded); } - }, [showAccountExpired, accountHasExpired]); + }, [showAccountExpired, accountHasExpired, tunnelState.state]); if (showAccountExpired.show) { return <ExpiredAccountErrorView />; diff --git a/gui/src/renderer/components/NotificationArea.tsx b/gui/src/renderer/components/NotificationArea.tsx index acd1aaf2df..317cc1e923 100644 --- a/gui/src/renderer/components/NotificationArea.tsx +++ b/gui/src/renderer/components/NotificationArea.tsx @@ -51,7 +51,7 @@ export default function NotificationArea(props: IProps) { blockWhenDisconnected, hasExcludedApps, }), - new ErrorNotificationProvider({ tunnelState, accountExpiry, hasExcludedApps }), + new ErrorNotificationProvider({ tunnelState, hasExcludedApps }), new InconsistentVersionNotificationProvider({ consistent: version.consistent }), new UnsupportedVersionNotificationProvider(version), ]; diff --git a/gui/src/renderer/components/TunnelControl.tsx b/gui/src/renderer/components/TunnelControl.tsx index db0503cb0a..c9fd568a3f 100644 --- a/gui/src/renderer/components/TunnelControl.tsx +++ b/gui/src/renderer/components/TunnelControl.tsx @@ -139,7 +139,7 @@ export default class TunnelControl extends React.Component<ITunnelControlProps> case 'error': if ( this.props.tunnelState.state === 'error' && - this.props.tunnelState.details.blockFailure + this.props.tunnelState.details.blockingError ) { return ( <Wrapper> diff --git a/gui/src/renderer/redux/connection/actions.ts b/gui/src/renderer/redux/connection/actions.ts index 20c63d433d..35af568fb2 100644 --- a/gui/src/renderer/redux/connection/actions.ts +++ b/gui/src/renderer/redux/connection/actions.ts @@ -1,6 +1,6 @@ import { AfterDisconnect, - IErrorState, + ErrorState, ILocation, ITunnelStateRelayInfo, } from '../../../shared/daemon-rpc-types'; @@ -26,7 +26,7 @@ interface IDisconnectingAction { interface IBlockedAction { type: 'TUNNEL_ERROR'; - errorState: IErrorState; + errorState: ErrorState; } interface INewLocationAction { @@ -75,7 +75,7 @@ function disconnecting(afterDisconnect: AfterDisconnect): IDisconnectingAction { }; } -function blocked(errorState: IErrorState): IBlockedAction { +function blocked(errorState: ErrorState): IBlockedAction { return { type: 'TUNNEL_ERROR', errorState, diff --git a/gui/src/shared/auth-failure.ts b/gui/src/shared/auth-failure.ts deleted file mode 100644 index f8db50d82e..0000000000 --- a/gui/src/shared/auth-failure.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { messages } from './gettext'; - -export enum AuthFailureKind { - invalidAccount, - expiredAccount, - tooManyConnections, - unknown, -} - -interface IAuthFailure { - kind: AuthFailureKind; - message: string; -} - -export function parseAuthFailure(rawFailureMessage?: string): IAuthFailure { - if (rawFailureMessage) { - const results = /^\[(\w+)\]\s*(.*)$/.exec(rawFailureMessage); - - if (results && results.length === 3) { - const kind = parseRawFailureKind(results[1]); - const message = kind === AuthFailureKind.unknown ? results[2] : messageForFailureKind(kind); - - return { - kind, - message, - }; - } else { - return { - kind: AuthFailureKind.unknown, - message: rawFailureMessage, - }; - } - } else { - return { - kind: AuthFailureKind.unknown, - message: messageForFailureKind(AuthFailureKind.unknown), - }; - } -} - -function parseRawFailureKind(failureId: string): AuthFailureKind { - // These strings should match up with mullvad-types/src/auth_failed.rs - switch (failureId) { - case 'INVALID_ACCOUNT': - return AuthFailureKind.invalidAccount; - - case 'EXPIRED_ACCOUNT': - return AuthFailureKind.expiredAccount; - - case 'TOO_MANY_CONNECTIONS': - return AuthFailureKind.tooManyConnections; - - default: - return AuthFailureKind.unknown; - } -} - -function messageForFailureKind(kind: AuthFailureKind): string { - switch (kind) { - case AuthFailureKind.invalidAccount: - return messages.pgettext( - 'auth-failure', - 'You are logged in with an invalid account number. Please log out and try another one.', - ); - - case AuthFailureKind.expiredAccount: - return messages.pgettext('auth-failure', 'Blocking internet: account is out of time'); - - case AuthFailureKind.tooManyConnections: - return messages.pgettext( - 'auth-failure', - 'Too many simultaneous connections on this account. Disconnect another device or try connecting again shortly.', - ); - - case AuthFailureKind.unknown: - return messages.pgettext( - 'auth-failure', - 'Unable to authenticate account. Please contact support.', - ); - } -} diff --git a/gui/src/shared/daemon-rpc-types.ts b/gui/src/shared/daemon-rpc-types.ts index 735bb91224..4a5f27d98e 100644 --- a/gui/src/shared/daemon-rpc-types.ts +++ b/gui/src/shared/daemon-rpc-types.ts @@ -17,34 +17,69 @@ export interface ILocation { provider?: string; } +export enum FirewallPolicyErrorType { + generic, + locked, +} + export type FirewallPolicyError = - | { reason: 'generic' } + | { type: FirewallPolicyErrorType.generic } | { - reason: 'locked'; - details?: { - name: string; - pid: number; - }; + type: FirewallPolicyErrorType.locked; + name: string; + pid: number; }; -export type TunnelParameterError = - | 'no_matching_relay' - | 'no_matching_bridge_relay' - | 'no_wireguard_key' - | 'custom_tunnel_host_resultion_error'; +export enum ErrorStateCause { + authFailed, + ipv6Unavailable, + setFirewallPolicyError, + setDnsError, + startTunnelError, + tunnelParameterError, + isOffline, + splitTunnelError, +} + +export enum AuthFailedError { + unknown, + invalidAccount, + expiredAccount, + tooManyConnections, +} + +export enum TunnelParameterError { + noMatchingRelay, + noMatchingBridgeRelay, + noWireguardKey, + customTunnelHostResolutionError, +} -export type ErrorStateCause = +export type ErrorState = | { - reason: - | 'ipv6_unavailable' - | 'set_dns_error' - | 'start_tunnel_error' - | 'is_offline' - | 'split_tunnel_error'; + cause: + | ErrorStateCause.ipv6Unavailable + | ErrorStateCause.setDnsError + | ErrorStateCause.startTunnelError + | ErrorStateCause.isOffline + | ErrorStateCause.splitTunnelError; + blockingError?: FirewallPolicyError; } - | { reason: 'set_firewall_policy_error'; details: FirewallPolicyError } - | { reason: 'tunnel_parameter_error'; details: TunnelParameterError } - | { reason: 'auth_failed'; details?: string }; + | { + cause: ErrorStateCause.authFailed; + blockingError?: FirewallPolicyError; + authFailedError: AuthFailedError; + } + | { + cause: ErrorStateCause.tunnelParameterError; + blockingError?: FirewallPolicyError; + parameterError: TunnelParameterError; + } + | { + cause: ErrorStateCause.setFirewallPolicyError; + blockingError?: FirewallPolicyError; + policyError: FirewallPolicyError; + }; export type AfterDisconnect = 'nothing' | 'block' | 'reconnect'; @@ -134,12 +169,7 @@ export type TunnelState = | { state: 'connecting'; details?: ITunnelStateRelayInfo } | { state: 'connected'; details: ITunnelStateRelayInfo } | { state: 'disconnecting'; details: AfterDisconnect } - | { state: 'error'; details: IErrorState }; - -export interface IErrorState { - blockFailure?: FirewallPolicyError; - cause: ErrorStateCause; -} + | { state: 'error'; details: ErrorState }; export type RelayLocation = | { hostname: [string, string, string] } diff --git a/gui/src/shared/notifications/error.ts b/gui/src/shared/notifications/error.ts index c901c5de67..08462fa6db 100644 --- a/gui/src/shared/notifications/error.ts +++ b/gui/src/shared/notifications/error.ts @@ -1,9 +1,13 @@ import { sprintf } from 'sprintf-js'; import { strings } from '../../config.json'; -import { hasExpired } from '../account-expiry'; -import { AuthFailureKind, parseAuthFailure } from '../auth-failure'; -import { IErrorState, TunnelParameterError, TunnelState } from '../daemon-rpc-types'; +import { + AuthFailedError, + ErrorState, + ErrorStateCause, + TunnelParameterError, + TunnelState, +} from '../daemon-rpc-types'; import { messages } from '../gettext'; import { InAppNotification, @@ -13,7 +17,6 @@ import { interface ErrorNotificationContext { tunnelState: TunnelState; - accountExpiry?: string; hasExcludedApps: boolean; } @@ -25,8 +28,8 @@ export class ErrorNotificationProvider public getSystemNotification() { if (this.context.tunnelState.state === 'error') { - let message = getMessage(this.context.tunnelState.details, this.context.accountExpiry); - if (!this.context.tunnelState.details.blockFailure && this.context.hasExcludedApps) { + let message = getMessage(this.context.tunnelState.details); + if (!this.context.tunnelState.details.blockingError && this.context.hasExcludedApps) { message = `${message} ${sprintf( messages.pgettext( 'notifications', @@ -38,7 +41,7 @@ export class ErrorNotificationProvider return { message, - critical: !!this.context.tunnelState.details.blockFailure, + critical: !!this.context.tunnelState.details.blockingError, }; } else { return undefined; @@ -47,8 +50,8 @@ export class ErrorNotificationProvider public getInAppNotification(): InAppNotification | undefined { if (this.context.tunnelState.state === 'error') { - let subtitle = getMessage(this.context.tunnelState.details, this.context.accountExpiry); - if (!this.context.tunnelState.details.blockFailure && this.context.hasExcludedApps) { + let subtitle = getMessage(this.context.tunnelState.details); + if (!this.context.tunnelState.details.blockingError && this.context.hasExcludedApps) { subtitle = `${subtitle} ${sprintf( messages.pgettext( 'notifications', @@ -60,10 +63,12 @@ export class ErrorNotificationProvider return { indicator: - this.context.tunnelState.details.cause.reason === 'is_offline' ? 'warning' : 'error', - title: !this.context.tunnelState.details.blockFailure - ? messages.pgettext('in-app-notifications', 'BLOCKING INTERNET') - : messages.pgettext('in-app-notifications', 'NETWORK TRAFFIC MIGHT BE LEAKING'), + this.context.tunnelState.details.cause === ErrorStateCause.isOffline + ? 'warning' + : 'error', + title: this.context.tunnelState.details.blockingError + ? messages.pgettext('in-app-notifications', 'NETWORK TRAFFIC MIGHT BE LEAKING') + : messages.pgettext('in-app-notifications', 'BLOCKING INTERNET'), subtitle, }; } else { @@ -72,9 +77,9 @@ export class ErrorNotificationProvider } } -function getMessage(errorDetails: IErrorState, accountExpiry?: string): string { - if (errorDetails.blockFailure) { - if (errorDetails.cause.reason === 'set_firewall_policy_error') { +function getMessage(errorState: ErrorState): string { + if (errorState.blockingError) { + if (errorState.cause === ErrorStateCause.setFirewallPolicyError) { switch (process.platform ?? window.env.platform) { case 'win32': return messages.pgettext( @@ -94,28 +99,37 @@ function getMessage(errorDetails: IErrorState, accountExpiry?: string): string { 'Unable to block all network traffic. Please troubleshoot or contact support.', ); } else { - switch (errorDetails.cause.reason) { - case 'auth_failed': { - const authFailure = parseAuthFailure(errorDetails.cause.details); - if ( - authFailure.kind === AuthFailureKind.unknown && - accountExpiry && - hasExpired(accountExpiry) - ) { - return messages.pgettext( - 'auth-failure', - 'You are logged in with an invalid account number. Please log out and try another one.', - ); - } else { - return authFailure.message; + switch (errorState.cause) { + case ErrorStateCause.authFailed: + switch (errorState.authFailedError) { + case AuthFailedError.invalidAccount: + return messages.pgettext( + 'auth-failure', + 'You are logged in with an invalid account number. Please log out and try another one.', + ); + + case AuthFailedError.expiredAccount: + return messages.pgettext('auth-failure', 'Blocking internet: account is out of time'); + + case AuthFailedError.tooManyConnections: + return messages.pgettext( + 'auth-failure', + 'Too many simultaneous connections on this account. Disconnect another device or try connecting again shortly.', + ); + + case AuthFailedError.unknown: + default: + return messages.pgettext( + 'auth-failure', + 'Unable to authenticate account. Please contact support.', + ); } - } - case 'ipv6_unavailable': + case ErrorStateCause.ipv6Unavailable: return messages.pgettext( 'notifications', 'Could not configure IPv6. Disable it in the app or enable it on your device.', ); - case 'set_firewall_policy_error': + case ErrorStateCause.setFirewallPolicyError: switch (process.platform ?? window.env.platform) { case 'win32': return messages.pgettext( @@ -130,24 +144,24 @@ function getMessage(errorDetails: IErrorState, accountExpiry?: string): string { default: return messages.pgettext('notifications', 'Unable to apply firewall rules.'); } - case 'set_dns_error': + case ErrorStateCause.setDnsError: return messages.pgettext( 'notifications', 'Unable to set system DNS server. Please contact support.', ); - case 'start_tunnel_error': + case ErrorStateCause.startTunnelError: return messages.pgettext( 'notifications', 'Unable to start tunnel connection. Please contact support.', ); - case 'tunnel_parameter_error': - return getTunnelParameterMessage(errorDetails.cause.details); - case 'is_offline': + case ErrorStateCause.tunnelParameterError: + return getTunnelParameterMessage(errorState.parameterError); + case ErrorStateCause.isOffline: return messages.pgettext( 'notifications', 'Your device is offline. The tunnel will automatically connect once your device is back online.', ); - case 'split_tunnel_error': + case ErrorStateCause.splitTunnelError: return messages.pgettext( 'notifications', 'Unable to communicate with Mullvad kernel driver. Try reconnecting or contact support.', @@ -156,16 +170,16 @@ function getMessage(errorDetails: IErrorState, accountExpiry?: string): string { } } -function getTunnelParameterMessage(err: TunnelParameterError): string { - switch (err) { +function getTunnelParameterMessage(error: TunnelParameterError): string { + switch (error) { /// TODO: once bridge constraints can be set, add a more descriptive error message - case 'no_matching_bridge_relay': - case 'no_matching_relay': + case TunnelParameterError.noMatchingBridgeRelay: + case TunnelParameterError.noMatchingRelay: return messages.pgettext( 'notifications', 'No servers in your selected location match your settings.', ); - case 'no_wireguard_key': + case TunnelParameterError.noWireguardKey: return sprintf( // TRANSLATORS: Available placeholders: // TRANSLATORS: %(wireguard)s - will be replaced with "WireGuard" @@ -175,7 +189,7 @@ function getTunnelParameterMessage(err: TunnelParameterError): string { ), { wireguard: strings.wireguard }, ); - case 'custom_tunnel_host_resultion_error': + case TunnelParameterError.customTunnelHostResolutionError: return messages.pgettext( 'notifications', 'Unable to resolve host of custom tunnel. Try changing your settings.', diff --git a/gui/test/e2e/mocked/tunnel-state.spec.ts b/gui/test/e2e/mocked/tunnel-state.spec.ts index c1f0317e24..77ee715103 100644 --- a/gui/test/e2e/mocked/tunnel-state.spec.ts +++ b/gui/test/e2e/mocked/tunnel-state.spec.ts @@ -2,9 +2,9 @@ import { expect, test } from '@playwright/test'; import { Page } from 'playwright'; import { colors } from '../../../src/config.json'; -import { ILocation, ITunnelEndpoint, TunnelState } from '../../../src/shared/daemon-rpc-types'; -import { getBackgroundColor, getColor } from '../utils'; import { startAppWithMocking, MockIpcHandle, SendMockIpcResponse } from './mocked-utils'; +import { ErrorStateCause, ILocation, ITunnelEndpoint, TunnelState } from '../../../src/shared/daemon-rpc-types'; +import { getBackgroundColor, getColor } from '../utils'; const UNSECURED_COLOR = colors.red; const SECURE_COLOR = colors.green; @@ -161,7 +161,7 @@ test('App should show error tunnel state', async () => { await sendMockIpcResponse<TunnelState>({ channel: 'tunnel-', - response: { state: 'error', details: { cause: { reason: 'is_offline' } } }, + response: { state: 'error', details: { cause: ErrorStateCause.isOffline } }, }); const statusLabel = getLabel(); diff --git a/gui/test/unit/auth-failure.spec.ts b/gui/test/unit/auth-failure.spec.ts deleted file mode 100644 index 61aa3bce61..0000000000 --- a/gui/test/unit/auth-failure.spec.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { expect } from 'chai'; -import { it, describe } from 'mocha'; -import { parseAuthFailure, AuthFailureKind } from '../../src/shared/auth-failure'; - -describe('auth_failed parsing', () => { - it('invalid line parsing works', () => { - const authFailure = parseAuthFailure('invalid auth_failed message'); - expect(authFailure.kind).to.be.equal(AuthFailureKind.unknown); - expect(authFailure.message).to.be.equal('invalid auth_failed message'); - }); - - it('valid unknown works', () => { - const authFailure = parseAuthFailure('[valid_unknown] Message'); - expect(authFailure.kind).to.be.equal(AuthFailureKind.unknown); - expect(authFailure.message).to.be.equal('Message'); - }); - - it('valid known works', () => { - const authFailure = parseAuthFailure('[INVALID_ACCOUNT] Invalid account'); - expect(authFailure.kind).to.be.equal(AuthFailureKind.invalidAccount); - }); - - it('empty message works', () => { - const authFailure = parseAuthFailure('[INVALID_ACCOUNT]'); - expect(authFailure.kind).to.be.equal(AuthFailureKind.invalidAccount); - }); -}); |
