diff options
| -rw-r--r-- | app/app.android.js | 10 | ||||
| -rw-r--r-- | app/app.js | 10 | ||||
| -rw-r--r-- | app/components/Connect.js | 17 | ||||
| -rw-r--r-- | app/lib/backend.js | 135 | ||||
| -rw-r--r-- | app/lib/jsonrpc-ws-ipc.js | 29 | ||||
| -rw-r--r-- | app/redux/account/actions.js | 6 | ||||
| -rw-r--r-- | app/redux/account/reducers.js | 3 |
7 files changed, 113 insertions, 97 deletions
diff --git a/app/app.android.js b/app/app.android.js index 38bb927a93..d362f25283 100644 --- a/app/app.android.js +++ b/app/app.android.js @@ -8,7 +8,7 @@ import { createMemoryHistory } from 'history'; import makeRoutes from './routes'; import configureStore from './redux/store'; import { log } from './lib/platform'; -import { Backend, BackendError } from './lib/backend'; +import { Backend, NoAccountError } from './lib/backend'; import { DeviceEventEmitter } from 'react-native'; import { MobileAppBridge } from 'NativeModules'; import { Dimensions } from 'react-native'; @@ -31,11 +31,9 @@ DeviceEventEmitter.addListener('com.mullvad.backend-info', async (_event, args) await backend.fetchSecurityState(); await backend.connect(); } catch (e) { - if (e instanceof BackendError) { - if (e.type === 'NO_ACCOUNT') { - log.debug('No user set in the backend, showing window'); - MobileAppBridge.showWindow(); - } + if (e instanceof NoAccountError) { + log.debug('No previously configured account set, showing window'); + MobileAppBridge.showWindow(); } } }); diff --git a/app/app.js b/app/app.js index 055972fdf5..dadc2177c2 100644 --- a/app/app.js +++ b/app/app.js @@ -9,7 +9,7 @@ import { webFrame, ipcRenderer } from 'electron'; import { log } from './lib/platform'; import makeRoutes from './routes'; import configureStore from './redux/store'; -import { Backend, BackendError } from './lib/backend'; +import { Backend, NoAccountError } from './lib/backend'; import { setShutdownHandler } from './shutdown-handler'; @@ -33,11 +33,9 @@ ipcRenderer.on('backend-info', async (_event, args) => { await backend.fetchSecurityState(); await backend.connect(); } catch (e) { - if (e instanceof BackendError) { - if (e.type === 'NO_ACCOUNT') { - log.debug('No user set in the backend, showing window'); - ipcRenderer.send('show-window'); - } + if (e instanceof NoAccountError) { + log.debug('No previously configured account set, showing window'); + ipcRenderer.send('show-window'); } } }); diff --git a/app/components/Connect.js b/app/components/Connect.js index 3fbe04cefa..f4647678f1 100644 --- a/app/components/Connect.js +++ b/app/components/Connect.js @@ -9,7 +9,7 @@ import { TransparentButton, RedTransparentButton, GreenButton, Label } from './s import Accordion from './Accordion'; import styles from './ConnectStyles'; -import { BackendError } from '../lib/backend'; +import { NoCreditError, NoInternetError } from '../lib/backend'; import Map from './Map'; import type { HeaderBarStyle } from './HeaderBar'; @@ -79,15 +79,18 @@ export default class Connect extends Component<ConnectProps, ConnectState> { ); } - renderError(error: BackendError) { + renderError(error: Error) { + const title = error.userFriendlyTitle || 'Something went wrong'; + const message = error.userFriendlyMessage || error.message; + return ( <View style={styles.connect}> <View style={styles.status_icon}> <Img source="icon-fail" height={60} width={60} alt="" /> </View> <View style={styles.status}> - <View style={styles.error_title}>{error.title}</View> - <View style={styles.error_message}>{error.message}</View> + <View style={styles.error_title}>{title}</View> + <View style={styles.error_message}>{message}</View> {error.type === 'NO_CREDIT' ? ( <View> <GreenButton onPress={this.onExternalLink.bind(this, 'purchase')}> @@ -339,16 +342,16 @@ export default class Connect extends Component<ConnectProps, ConnectState> { return classes; } - displayError(): ?BackendError { + displayError(): ?Error { // Offline? if (!this.props.connection.isOnline) { - return new BackendError('NO_INTERNET'); + return new NoInternetError(); } // No credit? const expiry = this.props.accountExpiry; if (expiry && moment(expiry).isSameOrBefore(moment())) { - return new BackendError('NO_CREDIT'); + return new NoCreditError(); } return null; diff --git a/app/lib/backend.js b/app/lib/backend.js index c9f9969af3..c7b4ca53ec 100644 --- a/app/lib/backend.js +++ b/app/lib/backend.js @@ -2,6 +2,7 @@ import { log } from '../lib/platform'; import { IpcFacade, RealIpc } from './ipc-facade'; +import { JsonRpcError, TimeOutError } from './jsonrpc-ws-ipc'; import accountActions from '../redux/account/actions'; import connectionActions from '../redux/connection/actions'; import settingsActions from '../redux/settings/actions'; @@ -11,64 +12,65 @@ import type { ReduxStore } from '../redux/store'; import type { AccountToken, BackendState, RelaySettingsUpdate } from './ipc-facade'; import type { ConnectionState } from '../redux/connection/reducers'; -export type ErrorType = - | 'NO_CREDIT' - | 'NO_INTERNET' - | 'NO_DAEMON' - | 'INVALID_ACCOUNT' - | 'NO_ACCOUNT' - | 'COMMUNICATION_FAILURE' - | 'UNKNOWN_ERROR'; +export class NoCreditError extends Error { + constructor() { + super("Account doesn't have enough credit available for connection"); + } -export class BackendError extends Error { - type: ErrorType; - title: string; - message: string; - cause: ?Error; + get userFriendlyTitle(): string { + return 'Out of time'; + } - constructor(type: ErrorType, cause?: Error) { - super(''); - this.type = type; - this.title = BackendError.localizedTitle(type); - this.message = BackendError.localizedMessage(type, cause); - this.cause = cause; + get userFriendlyMessage(): string { + return 'Buy more time, so you can continue using the internet securely'; } +} - static localizedTitle(type: ErrorType): string { - switch (type) { - case 'NO_CREDIT': - return 'Out of time'; - case 'NO_INTERNET': - return 'Offline'; - default: - return 'Something went wrong'; - } +export class NoInternetError extends Error { + constructor() { + super('Internet connectivity is currently unavailable'); } - static localizedMessage(type: ErrorType, cause: ?Error): string { - // TODO: since instanceof now works, BackendError can be replaced by a set - // of specific error types - switch (type) { - case 'NO_CREDIT': - return 'Buy more time, so you can continue using the internet securely'; - case 'NO_INTERNET': - return 'Your internet connection will be secured when you get back online'; - case 'INVALID_ACCOUNT': - return 'Invalid account number'; - case 'NO_ACCOUNT': - return 'No account was set'; - case 'NO_DAEMON': - return 'Could not connect to the Mullvad daemon'; - case 'COMMUNICATION_FAILURE': - return 'api.mullvad.net is blocked, please check your firewall'; - case 'UNKNOWN_ERROR': { - const message = cause ? ', ' + cause.message : ''; + get userFriendlyTitle(): string { + return 'Offline'; + } - return 'An unknown error occurred' + message; - } - default: - return ''; - } + get userFriendlyMessage(): string { + return 'Your internet connection will be secured when you get back online'; + } +} + +export class NoDaemonError extends Error { + constructor() { + super('Could not connect to Mullvad daemon'); + } +} + +export class InvalidAccountError extends Error { + constructor() { + super('Invalid account number'); + } +} + +export class NoAccountError extends Error { + constructor() { + super('No account was set'); + } +} + +export class CommunicationError extends Error { + constructor() { + super('api.mullvad.net is blocked, please check your firewall'); + } +} + +export class UnknownError extends Error { + constructor(cause: string) { + super(`An unknown error occurred, ${cause}`); + } + + get userFriendlyTitle(): string { + return 'Something went wrong'; } } @@ -186,35 +188,30 @@ export class Backend { } catch (e) { log.error('Failed to log in,', e.message); - const err = this._rpcErrorToBackendError(e); - this._store.dispatch(accountActions.loginFailed(err)); + const error = this._rpcErrorToBackendError(e); + this._store.dispatch(accountActions.loginFailed(error)); } } _rpcErrorToBackendError(e) { - if (e instanceof BackendError) { - return e; - } - - const isJsonRpcError = e.hasOwnProperty('code'); - if (isJsonRpcError) { + if (e instanceof JsonRpcError) { switch (e.code) { case -200: // Account doesn't exist - return new BackendError('INVALID_ACCOUNT'); + return new InvalidAccountError(); case -32603: // Internal error // We treat all internal backend errors as the user cannot reach // api.mullvad.net. This is not always true of course, but it is // true so often that we choose to disregard the other edge cases // for now. - return new BackendError('COMMUNICATION_FAILURE'); + return new CommunicationError(); } + } else if (e instanceof TimeOutError) { + return new CommunicationError(); + } else if (e instanceof NoDaemonError) { + return e; } - if (e.name === 'TimeOutError') { - return new BackendError('COMMUNICATION_FAILURE'); - } - - return new BackendError('UNKNOWN_ERROR', e); + return new UnknownError(e.message); } async autologin() { @@ -227,7 +224,7 @@ export class Backend { const accountToken = await this._ipc.getAccount(); if (!accountToken) { - throw new BackendError('NO_ACCOUNT'); + throw new NoAccountError(); } log.debug('The backend had an account number stored: ', accountToken); @@ -365,7 +362,7 @@ export class Backend { const accountToken = await this._ipc.getAccount(); if (!accountToken) { - throw new BackendError('NO_ACCOUNT'); + throw new NoAccountError(); } const accountData = await ipc.getAccountData(accountToken); @@ -531,7 +528,7 @@ export class Backend { } return this._authenticationPromise; } else { - return Promise.reject(new BackendError('NO_DAEMON')); + return Promise.reject(new NoDaemonError()); } } diff --git a/app/lib/jsonrpc-ws-ipc.js b/app/lib/jsonrpc-ws-ipc.js index ff73f4cc88..84dec96c77 100644 --- a/app/lib/jsonrpc-ws-ipc.js +++ b/app/lib/jsonrpc-ws-ipc.js @@ -11,11 +11,12 @@ export type UnansweredRequest = { message: Object, }; -export type JsonRpcError = { +export type JsonRpcErrorResponse = { type: 'error', payload: { id: string, error: { + code: number, message: string, }, }, @@ -37,7 +38,26 @@ export type JsonRpcSuccess = { result: mixed, }, }; -export type JsonRpcMessage = JsonRpcError | JsonRpcNotification | JsonRpcSuccess; +export type JsonRpcMessage = JsonRpcErrorResponse | JsonRpcNotification | JsonRpcSuccess; + +export class JsonRpcError extends Error { + _code: number; + _details: string; + + constructor(code: number, details: string) { + super(`Remote JSON-RPC error ${code}: ${details}`); + this._code = code; + this._details = details; + } + + get code(): number { + return this._code; + } + + get details(): string { + return this._details; + } +} export class TimeOutError extends Error { jsonRpcMessage: Object; @@ -200,7 +220,7 @@ export default class Ipc { } } - _onReply(message: JsonRpcError | JsonRpcSuccess) { + _onReply(message: JsonRpcErrorResponse | JsonRpcSuccess) { const id = message.payload.id; const request = this._unansweredRequests.get(id); this._unansweredRequests.delete(id); @@ -215,7 +235,8 @@ export default class Ipc { clearTimeout(request.timerId); if (message.type === 'error') { - request.reject(message.payload.error); + const error = message.payload.error; + request.reject(new JsonRpcError(error.code, error.message)); } else { const reply = message.payload.result; request.resolve(reply); diff --git a/app/redux/account/actions.js b/app/redux/account/actions.js index fdb9f44281..c778b111fd 100644 --- a/app/redux/account/actions.js +++ b/app/redux/account/actions.js @@ -1,7 +1,7 @@ // @flow import type { AccountToken } from '../../lib/ipc-facade'; -import type { Backend, BackendError } from '../../lib/backend'; +import type { Backend } from '../../lib/backend'; type StartLoginAction = { type: 'START_LOGIN', @@ -15,7 +15,7 @@ type LoginSuccessfulAction = { type LoginFailedAction = { type: 'LOGIN_FAILED', - error: BackendError, + error: Error, }; type LoggedOutAction = { @@ -65,7 +65,7 @@ function loginSuccessful(expiry: string): LoginSuccessfulAction { }; } -function loginFailed(error: BackendError): LoginFailedAction { +function loginFailed(error: Error): LoginFailedAction { return { type: 'LOGIN_FAILED', error: error, diff --git a/app/redux/account/reducers.js b/app/redux/account/reducers.js index f3485028d4..094864c9a8 100644 --- a/app/redux/account/reducers.js +++ b/app/redux/account/reducers.js @@ -1,7 +1,6 @@ // @flow import type { ReduxAction } from '../store'; -import type { BackendError } from '../../lib/backend'; import type { AccountToken } from '../../lib/ipc-facade'; export type LoginState = 'none' | 'logging in' | 'failed' | 'ok'; @@ -10,7 +9,7 @@ export type AccountReduxState = { accountHistory: Array<AccountToken>, expiry: ?string, // ISO8601 status: LoginState, - error: ?BackendError, + error: ?Error, }; const initialState: AccountReduxState = { |
