summaryrefslogtreecommitdiffhomepage
path: root/app/lib
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2018-07-05 16:19:24 +0200
committerAndrej Mihajlov <and@mullvad.net>2018-07-05 16:19:24 +0200
commitf5628a7a1a185e5d71cbeb8d1c3d56bd87ff30b5 (patch)
treeda38bcd09eaca01af7a5b4fd1adf041cf47aa8ef /app/lib
parente288920bec3000b6cb0da7e16b95c27e4e34be6c (diff)
parent09adf211269ff66eb53af007289765fc53e59b15 (diff)
downloadmullvadvpn-f5628a7a1a185e5d71cbeb8d1c3d56bd87ff30b5.tar.xz
mullvadvpn-f5628a7a1a185e5d71cbeb8d1c3d56bd87ff30b5.zip
Merge branch 'remove-backend'
Diffstat (limited to 'app/lib')
-rw-r--r--app/lib/backend.js631
-rw-r--r--app/lib/reconnection-backoff.js26
2 files changed, 26 insertions, 631 deletions
diff --git a/app/lib/backend.js b/app/lib/backend.js
deleted file mode 100644
index 3e128da0b1..0000000000
--- a/app/lib/backend.js
+++ /dev/null
@@ -1,631 +0,0 @@
-// @flow
-
-import { ipcRenderer } from 'electron';
-import { bindActionCreators } from 'redux';
-import { push as pushHistory } from 'react-router-redux';
-import {
- RemoteError as JsonRpcTransportRemoteError,
- TimeOutError as JsonRpcTransportTimeOutError,
-} from './jsonrpc-transport';
-import accountActions from '../redux/account/actions';
-import connectionActions from '../redux/connection/actions';
-import settingsActions from '../redux/settings/actions';
-import { log } from '../lib/platform';
-
-import type { RpcCredentials as OriginalRpcCredentials } from './rpc-address-file';
-import type {
- DaemonRpcProtocol,
- ConnectionObserver as DaemonConnectionObserver,
-} from './daemon-rpc';
-import type { ReduxStore } from '../redux/store';
-import type { AccountToken, BackendState, RelaySettingsUpdate } from './daemon-rpc';
-import type { ConnectionState } from '../redux/connection/reducers';
-
-export class NoCreditError extends Error {
- constructor() {
- super("Account doesn't have enough credit available for connection");
- }
-
- get userFriendlyTitle(): string {
- return 'Out of time';
- }
-
- get userFriendlyMessage(): string {
- return 'Buy more time, so you can continue using the internet securely';
- }
-}
-
-export class NoInternetError extends Error {
- constructor() {
- super('Internet connectivity is currently unavailable');
- }
-
- get userFriendlyTitle(): string {
- return 'Offline';
- }
-
- 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';
- }
-}
-
-export class CredentialsRequestError extends Error {
- _reason: Error;
-
- constructor(reason: Error) {
- super('Failed to request the RPC credentials');
- this._reason = reason;
- }
-
- get reason(): Error {
- return this._reason;
- }
-}
-
-export type RpcCredentials = OriginalRpcCredentials;
-export interface RpcCredentialsProvider {
- request(): Promise<RpcCredentials>;
-}
-
-/**
- * Backend implementation
- */
-export class Backend {
- _daemonRpc: DaemonRpcProtocol;
- _credentialsProvider: RpcCredentialsProvider;
- _reconnectBackoff = new ReconnectionBackoff();
- _credentials: ?RpcCredentials;
- _openConnectionObserver: ?DaemonConnectionObserver;
- _closeConnectionObserver: ?DaemonConnectionObserver;
- _reduxActions: *;
-
- constructor(
- store: ReduxStore,
- rpc: DaemonRpcProtocol,
- credentialsProvider: RpcCredentialsProvider,
- ) {
- this._daemonRpc = rpc;
- this._credentialsProvider = credentialsProvider;
-
- this._reduxActions = {
- account: bindActionCreators(accountActions, store.dispatch),
- connection: bindActionCreators(connectionActions, store.dispatch),
- settings: bindActionCreators(settingsActions, store.dispatch),
- history: bindActionCreators({ push: pushHistory }, store.dispatch),
- };
-
- this._openConnectionObserver = rpc.addOpenConnectionObserver(() => {
- this._onOpenConnection();
- });
-
- this._closeConnectionObserver = rpc.addCloseConnectionObserver((error) => {
- this._onCloseConnection(error);
- });
-
- this._setupReachability();
- }
-
- dispose() {
- const openConnectionObserver = this._openConnectionObserver;
- const closeConnectionObserver = this._closeConnectionObserver;
-
- if (openConnectionObserver) {
- openConnectionObserver.unsubscribe();
- this._openConnectionObserver = null;
- }
-
- if (closeConnectionObserver) {
- closeConnectionObserver.unsubscribe();
- this._closeConnectionObserver = null;
- }
- }
-
- connect() {
- this._connectToDaemon();
- }
-
- disconnect() {
- this._daemonRpc.disconnect();
- }
-
- async login(accountToken: AccountToken) {
- const actions = this._reduxActions;
- actions.account.startLogin(accountToken);
-
- log.debug('Attempting to login');
-
- try {
- const accountData = await this._daemonRpc.getAccountData(accountToken);
- await this._daemonRpc.setAccount(accountToken);
-
- actions.account.loginSuccessful(accountData.expiry);
-
- // Redirect the user after some time to allow for
- // the 'Login Successful' screen to be visible
- setTimeout(() => {
- actions.history.push('/connect');
- log.debug('Autoconnecting...');
- this.connectTunnel();
- }, 1000);
- } catch (error) {
- log.error('Failed to log in,', error.message);
-
- actions.account.loginFailed(this._rpcErrorToBackendError(error));
- }
- }
-
- async autologin() {
- const actions = this._reduxActions;
- actions.account.startLogin();
-
- log.debug('Attempting to log in automatically');
-
- try {
- const accountToken = await this._daemonRpc.getAccount();
- if (!accountToken) {
- throw new NoAccountError();
- }
-
- log.debug(`The backend had an account number stored: ${accountToken}`);
- actions.account.startLogin(accountToken);
-
- const accountData = await this._daemonRpc.getAccountData(accountToken);
- log.debug('The stored account number still exists:', accountData);
-
- actions.account.loginSuccessful(accountData.expiry);
- actions.history.push('/connect');
- } catch (e) {
- log.warn('Unable to autologin,', e.message);
-
- actions.account.autoLoginFailed();
- actions.history.push('/');
-
- throw e;
- }
- }
-
- async logout() {
- const actions = this._reduxActions;
-
- try {
- await Promise.all([
- this.disconnectTunnel(),
- this._daemonRpc.setAccount(null),
- this.fetchAccountHistory(),
- ]);
- actions.account.loggedOut();
- actions.history.push('/');
- } catch (e) {
- log.info('Failed to logout: ', e.message);
- }
- }
-
- async connectTunnel() {
- const actions = this._reduxActions;
-
- try {
- const currentState = await this._daemonRpc.getState();
- if (currentState.state === 'secured') {
- log.debug('Refusing to connect as connection is already secured');
- actions.connection.connected();
- } else {
- actions.connection.connecting();
- await this._daemonRpc.connectTunnel();
- }
- } catch (error) {
- actions.connection.disconnected();
- throw error;
- }
- }
-
- disconnectTunnel() {
- return this._daemonRpc.disconnectTunnel();
- }
-
- updateRelaySettings(relaySettings: RelaySettingsUpdate) {
- return this._daemonRpc.updateRelaySettings(relaySettings);
- }
-
- async fetchRelaySettings() {
- const actions = this._reduxActions;
- const relaySettings = await this._daemonRpc.getRelaySettings();
-
- log.debug('Got relay settings from backend', JSON.stringify(relaySettings));
-
- if (relaySettings.normal) {
- const payload = {};
- const normal = relaySettings.normal;
- const tunnel = normal.tunnel;
- const location = normal.location;
-
- if (location === 'any') {
- payload.location = 'any';
- } else {
- payload.location = location.only;
- }
-
- if (tunnel === 'any') {
- payload.port = 'any';
- payload.protocol = 'any';
- } else {
- const { port, protocol } = tunnel.only.openvpn;
- payload.port = port === 'any' ? port : port.only;
- payload.protocol = protocol === 'any' ? protocol : protocol.only;
- }
-
- actions.settings.updateRelay({
- normal: payload,
- });
- } else if (relaySettings.custom_tunnel_endpoint) {
- const custom_tunnel_endpoint = relaySettings.custom_tunnel_endpoint;
- const {
- host,
- tunnel: {
- openvpn: { port, protocol },
- },
- } = custom_tunnel_endpoint;
-
- actions.settings.updateRelay({
- custom_tunnel_endpoint: {
- host,
- port,
- protocol,
- },
- });
- }
- }
-
- async updateAccountExpiry() {
- const actions = this._reduxActions;
- try {
- const accountToken = await this._daemonRpc.getAccount();
- if (!accountToken) {
- throw new NoAccountError();
- }
- const accountData = await this._daemonRpc.getAccountData(accountToken);
- actions.account.updateAccountExpiry(accountData.expiry);
- } catch (e) {
- log.error(`Failed to update account expiry: ${e.message}`);
- }
- }
-
- async removeAccountFromHistory(accountToken: AccountToken): Promise<void> {
- await this._daemonRpc.removeAccountFromHistory(accountToken);
- await this.fetchAccountHistory();
- }
-
- async fetchAccountHistory(): Promise<void> {
- const actions = this._reduxActions;
-
- const accountHistory = await this._daemonRpc.getAccountHistory();
- actions.account.updateAccountHistory(accountHistory);
- }
-
- async fetchRelayLocations() {
- const actions = this._reduxActions;
- const locations = await this._daemonRpc.getRelayLocations();
-
- log.info('Got relay locations');
-
- const storedLocations = locations.countries.map((country) => ({
- name: country.name,
- code: country.code,
- hasActiveRelays: country.cities.some((city) => city.has_active_relays),
- cities: country.cities.map((city) => ({
- name: city.name,
- code: city.code,
- latitude: city.latitude,
- longitude: city.longitude,
- hasActiveRelays: city.has_active_relays,
- })),
- }));
-
- actions.settings.updateRelayLocations(storedLocations);
- }
-
- async fetchLocation() {
- const actions = this._reduxActions;
- const location = await this._daemonRpc.getLocation();
-
- log.info('Got location from daemon');
-
- const locationUpdate = {
- ip: location.ip,
- country: location.country,
- city: location.city,
- latitude: location.latitude,
- longitude: location.longitude,
- mullvadExitIp: location.mullvad_exit_ip,
- };
-
- actions.connection.newLocation(locationUpdate);
- }
-
- async setAllowLan(allowLan: boolean) {
- const actions = this._reduxActions;
- await this._daemonRpc.setAllowLan(allowLan);
- actions.settings.updateAllowLan(allowLan);
- }
-
- async fetchAllowLan() {
- const actions = this._reduxActions;
- const allowLan = await this._daemonRpc.getAllowLan();
- actions.settings.updateAllowLan(allowLan);
- }
-
- async fetchSecurityState() {
- const securityState = await this._daemonRpc.getState();
- const connectionState = this._securityStateToConnectionState(securityState);
- this._updateConnectionState(connectionState);
- }
-
- async _requestCredentials(): Promise<RpcCredentials> {
- try {
- return await this._credentialsProvider.request();
- } catch (providerError) {
- throw new CredentialsRequestError(providerError);
- }
- }
-
- async _connectToDaemon(): Promise<void> {
- let credentials;
- try {
- credentials = await this._requestCredentials();
- } catch (error) {
- log.error(`Cannot request the RPC credentials: ${error.message}`);
- return;
- }
-
- this._credentials = credentials;
- this._daemonRpc.connect(credentials.connectionString);
- }
-
- async _onOpenConnection() {
- this._reconnectBackoff.reset();
-
- // authenticate once connected
- const credentials = this._credentials;
- try {
- if (!credentials) {
- throw new Error('Credentials cannot be unset after connection is established.');
- }
- await this._authenticate(credentials.sharedSecret);
- } catch (error) {
- log.error(`Cannot authenticate: ${error.message}`);
- }
-
- // autologin
- try {
- await this.autologin();
- } catch (error) {
- if (error instanceof NoAccountError) {
- log.debug('No previously configured account set, showing window');
- ipcRenderer.send('show-window');
- } else {
- log.error(`Failed to autologin: ${error.message}`);
- }
- }
-
- // make sure to re-subscribe to state notifications when connection is re-established.
- try {
- await this._subscribeStateListener();
- } catch (error) {
- log.error(`Cannot subscribe for RPC notifications: ${error.message}`);
- }
-
- // fetch initial state
- try {
- await this._fetchInitialState();
- } catch (error) {
- log.error(`Cannot fetch initial state: ${error.message}`);
- }
-
- // auto connect the tunnel
- try {
- await this.connectTunnel();
- } catch (error) {
- log.error(`Cannot autoconnect the tunnel: ${error.message}`);
- }
- }
-
- async _onCloseConnection(error: ?Error) {
- if (error) {
- log.debug(`Lost connection to daemon: ${error.message}`);
-
- const recover = async () => {
- try {
- await this.connect();
- } catch (error) {
- log.error(`Failed to reconnect: ${error.message}`);
- }
- };
-
- this._reconnectBackoff.attempt(() => {
- recover();
- });
- }
- }
-
- /**
- * Start reachability monitoring for online/offline detection
- * This is currently done via HTML5 APIs but will be replaced later
- * with proper backend integration.
- */
- _setupReachability() {
- const actions = this._reduxActions;
-
- window.addEventListener('online', () => {
- actions.connection.online();
- });
- window.addEventListener('offline', () => {
- actions.connection.offline();
- });
-
- if (navigator.onLine) {
- actions.connection.online();
- } else {
- actions.connection.offline();
- }
- }
-
- async _subscribeStateListener() {
- await this._daemonRpc.subscribeStateListener((newState, error) => {
- if (error) {
- log.error(`Received an error when processing the incoming state change: ${error.message}`);
- }
-
- if (newState) {
- const connectionState = this._securityStateToConnectionState(newState);
-
- log.debug(
- `Got new state from backend {state: ${newState.state}, target_state: ${
- newState.target_state
- }}, translated to '${connectionState}'`,
- );
-
- this._updateConnectionState(connectionState);
- this._refreshStateOnChange();
- }
- });
- }
-
- async _fetchInitialState() {
- return Promise.all([
- this.fetchSecurityState(),
- this.fetchRelaySettings(),
- this.fetchRelayLocations(),
- this.fetchAllowLan(),
- this.fetchLocation(),
- this.fetchAccountHistory(),
- ]);
- }
-
- async _refreshStateOnChange() {
- try {
- await this.fetchLocation();
- } catch (error) {
- log.error(`Failed to fetch the location: ${error.message}`);
- }
- }
-
- _securityStateToConnectionState(backendState: BackendState): ConnectionState {
- if (backendState.state === 'unsecured' && backendState.target_state === 'secured') {
- return 'connecting';
- } else if (backendState.state === 'secured' && backendState.target_state === 'secured') {
- return 'connected';
- } else if (backendState.target_state === 'unsecured') {
- return 'disconnected';
- }
- throw new Error('Unsupported state/target state combination: ' + JSON.stringify(backendState));
- }
-
- _updateConnectionState(connectionState: ConnectionState) {
- const actions = this._reduxActions;
- switch (connectionState) {
- case 'connecting':
- actions.connection.connecting();
- break;
- case 'connected':
- actions.connection.connected();
- break;
- case 'disconnected':
- actions.connection.disconnected();
- break;
- }
- }
-
- _rpcErrorToBackendError(e) {
- if (e instanceof JsonRpcTransportRemoteError) {
- switch (e.code) {
- case -200: // Account doesn't exist
- 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 CommunicationError();
- }
- } else if (e instanceof JsonRpcTransportTimeOutError) {
- return new CommunicationError();
- } else if (e instanceof NoDaemonError) {
- return e;
- }
-
- return new UnknownError(e.message);
- }
-
- async _authenticate(sharedSecret: string) {
- try {
- await this._daemonRpc.authenticate(sharedSecret);
- log.info('Authenticated with backend');
- } catch (e) {
- log.error(`Failed to authenticate with backend: ${e.message}`);
- throw e;
- }
- }
-}
-
-/*
- * Used to calculate the time to wait before reconnecting
- * the websocket.
- *
- * It uses a linear backoff function that goes from 500ms
- * to 3000ms
- */
-class ReconnectionBackoff {
- _attempt: number;
-
- constructor() {
- this._attempt = 0;
- }
-
- attempt(handler: () => void) {
- setTimeout(handler, this._getIncreasedBackoff());
- }
-
- reset() {
- this._attempt = 0;
- }
-
- _getIncreasedBackoff() {
- if (this._attempt < 6) {
- this._attempt++;
- }
- return this._attempt * 500;
- }
-}
diff --git a/app/lib/reconnection-backoff.js b/app/lib/reconnection-backoff.js
new file mode 100644
index 0000000000..1cd8203dac
--- /dev/null
+++ b/app/lib/reconnection-backoff.js
@@ -0,0 +1,26 @@
+/*
+ * Used to calculate the time to wait before reconnecting to the daemon.
+ * It uses a linear backoff function that goes from 500ms to 3000ms.
+ */
+export default class ReconnectionBackoff {
+ _attempt: number;
+
+ constructor() {
+ this._attempt = 0;
+ }
+
+ attempt(handler: () => void) {
+ setTimeout(handler, this._getIncreasedBackoff());
+ }
+
+ reset() {
+ this._attempt = 0;
+ }
+
+ _getIncreasedBackoff() {
+ if (this._attempt < 6) {
+ this._attempt++;
+ }
+ return this._attempt * 500;
+ }
+}