diff options
| author | Erik Larkö <erik@mullvad.net> | 2017-10-13 11:31:57 +0200 |
|---|---|---|
| committer | Erik Larkö <erik@mullvad.net> | 2017-10-13 11:31:57 +0200 |
| commit | 992f7946dbe9004d88a331b97ea9e030701599ea (patch) | |
| tree | 858ef774e0751435f2b039af846cef4d29c37a0c /app | |
| parent | 8f5c5aa9a433a65d8a7f6f6d8e4211f5a82068f5 (diff) | |
| download | mullvadvpn-992f7946dbe9004d88a331b97ea9e030701599ea.tar.xz mullvadvpn-992f7946dbe9004d88a331b97ea9e030701599ea.zip | |
Move the auth flow into backend.js and write tests
Diffstat (limited to 'app')
| -rw-r--r-- | app/lib/backend.js | 291 | ||||
| -rw-r--r-- | app/lib/ipc-facade.js | 161 | ||||
| -rw-r--r-- | app/main.js | 2 |
3 files changed, 241 insertions, 213 deletions
diff --git a/app/lib/backend.js b/app/lib/backend.js index a8b560812f..2e4cc90cc7 100644 --- a/app/lib/backend.js +++ b/app/lib/backend.js @@ -1,6 +1,7 @@ // @flow -import log from 'electron-log'; +//import log from 'electron-log'; +const log = console; import EventEmitter from 'events'; import { servers } from '../config'; import { IpcFacade, RealIpc } from './ipc-facade'; @@ -9,7 +10,7 @@ import connectionActions from '../redux/connection/actions'; import type { ReduxStore } from '../redux/store'; import { push } from 'react-router-redux'; -import type { BackendState, RelayEndpoint, IpcCredentials } from './ipc-facade'; +import type { BackendState, RelayEndpoint } from './ipc-facade'; import type { ConnectionState } from '../redux/connection/reducers'; export type EventType = 'connect' | 'connecting' | 'disconnect' | 'login' | 'logging' | 'logout' | 'updatedIp' | 'updatedLocation' | 'updatedReachability'; @@ -66,31 +67,65 @@ export class BackendError extends Error { } +export type IpcCredentials = { + connectionString: string, + sharedSecret: string, +}; +export function parseIpcCredentials(data: string): ?IpcCredentials { + const [connectionString, sharedSecret] = data.split('\n', 2); + if(connectionString && sharedSecret) { + return { + connectionString, + sharedSecret, + }; + } else { + return null; + } +} + + /** * Backend implementation */ export class Backend { _ipc: IpcFacade; + _credentials: ?IpcCredentials; + _authenticationPromise: ?Promise<void>; _store: ReduxStore; _eventEmitter = new EventEmitter(); - constructor(store: ReduxStore, ipc: ?IpcFacade) { + constructor(store: ReduxStore, credentials?: IpcCredentials, ipc: ?IpcFacade) { this._store = store; + this._credentials = credentials; + + if(ipc) { this._ipc = ipc; + + // force to re-authenticate when connection closed + this._ipc.setCloseConnectionHandler(() => { + this._authenticationPromise = null; + }); + this._registerIpcListeners(); this._startReachability(); } } setCredentials(credentials: IpcCredentials) { - log.info('Got connection info to backend', credentials); + log.info('Got connection info to backend', credentials.connectionString); + this._credentials = credentials; if (this._ipc) { - this._ipc.setCredentials(credentials); + this._credentials = credentials; } else { - this._ipc = new RealIpc(credentials); + this._ipc = new RealIpc(credentials.connectionString); + + // force to re-authenticate when connection closed + this._ipc.setCloseConnectionHandler(() => { + this._authenticationPromise = null; + }); } this._registerIpcListeners(); } @@ -98,27 +133,33 @@ export class Backend { sync() { log.info('Syncing with the backend...'); - this._ipc.getIp() - .then( ip => { - log.info('Got ip', ip); - this._store.dispatch(connectionActions.newPublicIp(ip)); - }) - .catch(e => { - log.info('Failed syncing with the backend,', e.message); + this._ensureAuthenticated() + .then( () => { + this._ipc.getIp() + .then( ip => { + log.info('Got ip', ip); + this._store.dispatch(connectionActions.newPublicIp(ip)); + }) + .catch(e => { + log.info('Failed syncing with the backend,', e.message); + }); }); - this._ipc.getLocation() - .then( location => { - log.info('Got location', location); - const newLocation = { - location: location.latlong, - country: location.country, - city: location.city - }; - this._store.dispatch(connectionActions.newLocation(newLocation)); - }) - .catch(e => { - log.info('Failed getting new location,', e.message); + this._ensureAuthenticated() + .then( () => { + this._ipc.getLocation() + .then( location => { + log.info('Got location', location); + const newLocation = { + location: location.latlong, + country: location.country, + city: location.city + }; + this._store.dispatch(connectionActions.newLocation(newLocation)); + }) + .catch(e => { + log.info('Failed getting new location,', e.message); + }); }); } @@ -131,30 +172,33 @@ export class Backend { this._store.dispatch(accountActions.startLogin(accountToken)); - return this._ipc.getAccountData(accountToken) - .then( response => { - log.info('Account exists', response); + return this._ensureAuthenticated() + .then( () => { + return this._ipc.getAccountData(accountToken) + .then( response => { + log.info('Account exists', response); - return this._ipc.setAccount(accountToken) - .then( () => response ); + return this._ipc.setAccount(accountToken) + .then( () => response ); - }).then( accountData => { - log.info('Log in complete'); + }).then( accountData => { + log.info('Log in complete'); - this._store.dispatch(accountActions.loginSuccessful(accountData.expiry)); + this._store.dispatch(accountActions.loginSuccessful(accountData.expiry)); - // Redirect the user after some time to allow for - // the 'Login Successful' screen to be visible - setTimeout(() => { - this._store.dispatch(push('/connect')); - this.connect(); - }, 1000); - }).catch(e => { - log.error('Failed to log in,', e.message); + // Redirect the user after some time to allow for + // the 'Login Successful' screen to be visible + setTimeout(() => { + this._store.dispatch(push('/connect')); + this.connect(); + }, 1000); + }).catch(e => { + log.error('Failed to log in,', e.message); - // TODO: This is not true. If there is a communication link failure the promise will be rejected too - const err = new BackendError('INVALID_ACCOUNT'); - this._store.dispatch(accountActions.loginFailed(err)); + // TODO: This is not true. If there is a communication link failure the promise will be rejected too + const err = new BackendError('INVALID_ACCOUNT'); + this._store.dispatch(accountActions.loginFailed(err)); + }); }); } @@ -163,49 +207,55 @@ export class Backend { this._store.dispatch(accountActions.startLogin()); - return this._ipc.getAccount() - .then( accountToken => { - if (!accountToken) { - throw new BackendError('NO_ACCOUNT'); - } - log.debug('The backend had an account number stored:', accountToken); - this._store.dispatch(accountActions.startLogin(accountToken)); + return this._ensureAuthenticated() + .then( () => { + return this._ipc.getAccount() + .then( accountToken => { + if (!accountToken) { + throw new BackendError('NO_ACCOUNT'); + } + log.debug('The backend had an account number stored:', accountToken); + this._store.dispatch(accountActions.startLogin(accountToken)); - return this._ipc.getAccountData(accountToken); - }) - .then( accountData => { - log.info('The stored account number still exists', accountData); + return this._ipc.getAccountData(accountToken); + }) + .then( accountData => { + log.info('The stored account number still exists', accountData); - this._store.dispatch(accountActions.loginSuccessful(accountData.expiry)); + this._store.dispatch(accountActions.loginSuccessful(accountData.expiry)); - this._store.dispatch(push('/connect')); - this.connect(); - }) - .catch( e => { - log.warn('Unable to autologin,', e.message); + this._store.dispatch(push('/connect')); + this.connect(); + }) + .catch( e => { + log.warn('Unable to autologin,', e.message); - this._store.dispatch(accountActions.autoLoginFailed()); - this._store.dispatch(push('/')); + this._store.dispatch(accountActions.autoLoginFailed()); + this._store.dispatch(push('/')); - throw e; + throw e; + }); }); } logout() { // @TODO: What does it mean for a logout to be successful or failed? - this._ipc.setAccount(null) - .then(() => { + return this._ensureAuthenticated() + .then( () => { + return this._ipc.setAccount(null) + .then(() => { - this._store.dispatch(accountActions.loggedOut()); + this._store.dispatch(accountActions.loggedOut()); - // disconnect user during logout - return this.disconnect() - .then( () => { - this._store.dispatch(push('/')); + // disconnect user during logout + return this.disconnect() + .then( () => { + this._store.dispatch(push('/')); + }); + }) + .catch(e => { + log.info('Failed to logout,', e.message); }); - }) - .catch(e => { - log.info('Failed to logout,', e.message); }); } @@ -215,28 +265,37 @@ export class Backend { if (relayEndpoint) { this._store.dispatch(connectionActions.connectingTo(relayEndpoint)); - return this._ipc.setCustomRelay(relayEndpoint) + return this._ensureAuthenticated() .then( () => { - return this._ipc.connect(); - }) - .catch(e => { - log.info('Failed connecting to', relayEndpoint.host, '-', e.message); - this._store.dispatch(connectionActions.disconnected()); + return this._ipc.setCustomRelay(relayEndpoint) + .then( () => { + return this._ipc.connect(); + }) + .catch(e => { + log.info('Failed connecting to', relayEndpoint.host, '-', e.message); + this._store.dispatch(connectionActions.disconnected()); + }); }); } else { - return this._ipc.connect() - .catch(e => { - log.info('Failed connecting to the relay set in the backend, ', e.message); - this._store.dispatch(connectionActions.disconnected()); + return this._ensureAuthenticated() + .then( () => { + return this._ipc.connect() + .catch(e => { + log.info('Failed connecting to the relay set in the backend, ', e.message); + this._store.dispatch(connectionActions.disconnected()); + }); }); } } disconnect(): Promise<void> { // @TODO: Failure modes - return this._ipc.disconnect() - .catch(e => { - log.info('Failed to disconnect,', e.message); + return this._ensureAuthenticated() + .then( () => { + return this._ipc.disconnect() + .catch(e => { + log.info('Failed to disconnect,', e.message); + }); }); } @@ -266,24 +325,27 @@ export class Backend { } _registerIpcListeners() { - this._ipc.registerStateListener(newState => { - log.info('Got new state from backend', newState); + return this._ensureAuthenticated() + .then( () => { + return this._ipc.registerStateListener(newState => { + log.info('Got new state from backend', newState); - const newStatus = this._securityStateToConnectionState(newState); - switch(newStatus) { - case 'connecting': - this._store.dispatch(connectionActions.connecting()); - break; - case 'connected': - this._store.dispatch(connectionActions.connected()); - break; - case 'disconnected': - this._store.dispatch(connectionActions.disconnected()); - break; - } + const newStatus = this._securityStateToConnectionState(newState); + switch(newStatus) { + case 'connecting': + this._store.dispatch(connectionActions.connecting()); + break; + case 'connected': + this._store.dispatch(connectionActions.connected()); + break; + case 'disconnected': + this._store.dispatch(connectionActions.disconnected()); + break; + } - this.sync(); - }); + this.sync(); + }); + }); } _securityStateToConnectionState(backendState: BackendState): ConnectionState { @@ -296,4 +358,27 @@ export class Backend { } throw new Error('Unsupported state/target state combination: ' + JSON.stringify(backendState)); } + + _ensureAuthenticated(): Promise<void> { + const credentials = this._credentials; + if(credentials) { + if(!this._authenticationPromise) { + this._authenticationPromise = this._authenticate(credentials.sharedSecret); + } + return this._authenticationPromise; + } else { + return Promise.reject(new Error('Missing authentication credentials.')); + } + } + + _authenticate(sharedSecret: string): Promise<void> { + return this._ipc.auth(sharedSecret) + .then(() => { + log.info('Authenticated with backend'); + }) + .catch((e) => { + log.error('Failed to authenticate with backend: ', e.message); + throw e; + }); + } } diff --git a/app/lib/ipc-facade.js b/app/lib/ipc-facade.js index 9e191ddf30..2d63f4b9ba 100644 --- a/app/lib/ipc-facade.js +++ b/app/lib/ipc-facade.js @@ -3,7 +3,6 @@ import JsonRpcWs, { InvalidReply } from './jsonrpc-ws-ipc'; import { object, string, arrayOf, number } from 'validated/schema'; import { validate } from 'validated/object'; -import log from 'electron-log'; import type { Coordinate2d } from '../types'; @@ -32,26 +31,9 @@ export type RelayEndpoint = { protocol: 'tcp' | 'udp', }; -export type IpcCredentials = { - connectionString: string, - sharedSecret: string, -}; - -export function parseIpcCredentials(data: string): ?IpcCredentials { - const [connectionString, sharedSecret] = data.split('\n', 2); - if(connectionString && sharedSecret) { - return { - connectionString, - sharedSecret, - }; - } else { - return null; - } -} - export interface IpcFacade { - setCredentials(IpcCredentials): void, + setConnectionString(string): void, getAccountData(AccountToken): Promise<AccountData>, getAccount(): Promise<?AccountToken>, setAccount(accountToken: ?AccountToken): Promise<void>, @@ -62,27 +44,20 @@ export interface IpcFacade { getLocation(): Promise<Location>, getState(): Promise<BackendState>, registerStateListener((BackendState) => void): void, + setCloseConnectionHandler(() => void): void, + auth(sharedSecret: string): Promise<void>, } export class RealIpc implements IpcFacade { _ipc: JsonRpcWs; - _credentials: ?IpcCredentials; - _authenticationPromise: ?Promise<void>; - constructor(credentials: IpcCredentials) { - this._credentials = credentials; - this._ipc = new JsonRpcWs(credentials.connectionString); - - // force to re-authenticate when connection closed - this._ipc.setCloseConnectionHandler(() => { - this._authenticationPromise = null; - }); + constructor(connectionString: string) { + this._ipc = new JsonRpcWs(connectionString); } - setCredentials(credentials: IpcCredentials) { - this._credentials = credentials; - this._ipc.setConnectionString(credentials.connectionString); + setConnectionString(str: string) { + this._ipc.setConnectionString(str); } getAccountData(accountToken: AccountToken): Promise<AccountData> { @@ -100,23 +75,19 @@ export class RealIpc implements IpcFacade { } getAccount(): Promise<?AccountToken> { - return this._ensureAuthenticated().then(() => { - return this._ipc.send('get_account') - .then( raw => { - if (raw === undefined || raw === null || typeof raw === 'string') { - return raw; - } else { - throw new InvalidReply(raw); - } - }); - }); + return this._ipc.send('get_account') + .then( raw => { + if (raw === undefined || raw === null || typeof raw === 'string') { + return raw; + } else { + throw new InvalidReply(raw); + } + }); } setAccount(accountToken: ?AccountToken): Promise<void> { - return this._ensureAuthenticated().then(() => { - return this._ipc.send('set_account', accountToken) - .then(this._ignoreResponse); - }); + return this._ipc.send('set_account', accountToken) + .then(this._ignoreResponse); } _ignoreResponse(_response: mixed): void { @@ -124,60 +95,48 @@ export class RealIpc implements IpcFacade { } setCustomRelay(relayEndpoint: RelayEndpoint): Promise<void> { - return this._ensureAuthenticated().then(() => { - return this._ipc.send('set_custom_relay', [relayEndpoint]) - .then(this._ignoreResponse); - }); + return this._ipc.send('set_custom_relay', [relayEndpoint]) + .then(this._ignoreResponse); } connect(): Promise<void> { - return this._ensureAuthenticated().then(() => { - return this._ipc.send('connect') - .then(this._ignoreResponse); - }); + return this._ipc.send('connect') + .then(this._ignoreResponse); } disconnect(): Promise<void> { - return this._ensureAuthenticated().then(() => { - return this._ipc.send('disconnect') - .then(this._ignoreResponse); - }); + return this._ipc.send('disconnect') + .then(this._ignoreResponse); } getIp(): Promise<Ip> { - return this._ensureAuthenticated().then(() => { - return this._ipc.send('get_ip') - .then(raw => { - if (typeof raw === 'string' && raw) { - return raw; - } else { - throw new InvalidReply(raw, 'Expected a string'); - } - }); - }); + return this._ipc.send('get_ip') + .then(raw => { + if (typeof raw === 'string' && raw) { + return raw; + } else { + throw new InvalidReply(raw, 'Expected a string'); + } + }); } getLocation(): Promise<Location> { - return this._ensureAuthenticated().then(() => { - return this._ipc.send('get_location') - .then(raw => { - try { - const validated: any = validate(LocationSchema, raw); - return (validated: Location); - } catch (e) { - throw new InvalidReply(raw, e); - } - }); - }); + return this._ipc.send('get_location') + .then(raw => { + try { + const validated: any = validate(LocationSchema, raw); + return (validated: Location); + } catch (e) { + throw new InvalidReply(raw, e); + } + }); } getState(): Promise<BackendState> { - return this._ensureAuthenticated().then(() => { - return this._ipc.send('get_state') - .then(raw => { - return this._parseBackendState(raw); - }); - }); + return this._ipc.send('get_state') + .then(raw => { + return this._parseBackendState(raw); + }); } _parseBackendState(raw: mixed): BackendState { @@ -200,35 +159,19 @@ export class RealIpc implements IpcFacade { } registerStateListener(listener: (BackendState) => void) { - this._ensureAuthenticated().then(() => { - this._ipc.on('new_state', (rawEvent) => { - const parsedEvent : BackendState = this._parseBackendState(rawEvent); + this._ipc.on('new_state', (rawEvent) => { + const parsedEvent : BackendState = this._parseBackendState(rawEvent); - listener(parsedEvent); - }); + listener(parsedEvent); }); } - _ensureAuthenticated(): Promise<void> { - if(this._credentials) { - const credentials = this._credentials; - if(!this._authenticationPromise) { - this._authenticationPromise = this._authenticate(credentials.sharedSecret); - } - return this._authenticationPromise; - } else { - return Promise.reject(new Error('Missing authentication credentials.')); - } + auth(sharedSecret: string): Promise<void> { + return this._ipc.send('auth', sharedSecret) + .then(this._ignoreResponse); } - _authenticate(sharedSecret: string): Promise<void> { - return this._ipc.send('auth', sharedSecret) - .then(() => { - log.info('Authenticated with backend'); - }) - .catch((e) => { - log.error('Failed to authenticate with backend: ', e.message); - throw e; - }); + setCloseConnectionHandler(handler: () => void) { + console.log('appa', handler); } } diff --git a/app/main.js b/app/main.js index 1ca22fc115..7dcb83f9c6 100644 --- a/app/main.js +++ b/app/main.js @@ -7,7 +7,7 @@ import { app, BrowserWindow, ipcMain, Tray, Menu, nativeImage } from 'electron'; import TrayIconManager from './lib/tray-icon-manager'; import ElectronSudo from 'electron-sudo'; import { version } from '../package.json'; -import { parseIpcCredentials } from './lib/ipc-facade'; +import { parseIpcCredentials } from './lib/backend'; import type { TrayIconType } from './lib/tray-icon-manager'; |
