diff options
| -rw-r--r-- | app/app.js | 22 | ||||
| -rw-r--r-- | app/lib/backend.js | 38 | ||||
| -rw-r--r-- | app/lib/ipc-facade.js | 162 | ||||
| -rw-r--r-- | app/lib/jsonrpc-ws-ipc.js | 9 | ||||
| -rw-r--r-- | app/main.js | 9 | ||||
| -rw-r--r-- | test/mocks/ipc.js | 7 |
6 files changed, 142 insertions, 105 deletions
diff --git a/app/app.js b/app/app.js index 452e940a2b..c0682343c3 100644 --- a/app/app.js +++ b/app/app.js @@ -12,7 +12,6 @@ import makeRoutes from './routes'; import configureStore from './redux/store'; import { Backend } from './lib/backend'; -import type { IpcCredentials } from './lib/backend'; import type { ConnectionState } from './redux/connection/reducers'; import type { TrayIconType } from './lib/tray-icon-manager'; @@ -25,19 +24,14 @@ const store = configureStore(initialState, memoryHistory); ////////////////////////////////////////////////////////////////////////// const backend = new Backend(store); ipcRenderer.on('backend-info', (_event, args) => { - const credentials: IpcCredentials = args.credentials; - - backend.setLocation(credentials.connectionString); - backend.authenticate(credentials.sharedSecret) - .then(() => { - backend.sync(); - backend.autologin() - .catch( e => { - if (e.type === 'NO_ACCOUNT') { - log.debug('No user set in the backend, showing window'); - ipcRenderer.send('show-window'); - } - }); + backend.setCredentials(args.credentials); + backend.sync(); + backend.autologin() + .catch( e => { + if (e.type === 'NO_ACCOUNT') { + log.debug('No user set in the backend, showing window'); + ipcRenderer.send('show-window'); + } }); }); ipcRenderer.on('disconnect', () => { diff --git a/app/lib/backend.js b/app/lib/backend.js index ebd5a8dec5..a8b560812f 100644 --- a/app/lib/backend.js +++ b/app/lib/backend.js @@ -9,9 +9,8 @@ import connectionActions from '../redux/connection/actions'; import type { ReduxStore } from '../redux/store'; import { push } from 'react-router-redux'; -import type { BackendState } from './ipc-facade'; +import type { BackendState, RelayEndpoint, IpcCredentials } from './ipc-facade'; import type { ConnectionState } from '../redux/connection/reducers'; -import type { RelayEndpoint } from './ipc-facade'; export type EventType = 'connect' | 'connecting' | 'disconnect' | 'login' | 'logging' | 'logout' | 'updatedIp' | 'updatedLocation' | 'updatedReachability'; export type ErrorType = 'NO_CREDIT' | 'NO_INTERNET' | 'INVALID_ACCOUNT' | 'NO_ACCOUNT'; @@ -66,22 +65,6 @@ 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 @@ -101,28 +84,17 @@ export class Backend { } } - setLocation(loc: string) { - log.info('Got connection info to backend', loc); + setCredentials(credentials: IpcCredentials) { + log.info('Got connection info to backend', credentials); if (this._ipc) { - this._ipc.setConnectionString(loc); + this._ipc.setCredentials(credentials); } else { - this._ipc = new RealIpc(loc); + this._ipc = new RealIpc(credentials); } this._registerIpcListeners(); } - authenticate(sharedSecret: string): Promise<void> { - return this._ipc.authenticate(sharedSecret) - .then(() => { - log.info('Authenticated with backend'); - }) - .catch((e) => { - log.error('Backend authentication failed', e.message); - throw e; - }); - } - sync() { log.info('Syncing with the backend...'); diff --git a/app/lib/ipc-facade.js b/app/lib/ipc-facade.js index 246c864ad0..9e191ddf30 100644 --- a/app/lib/ipc-facade.js +++ b/app/lib/ipc-facade.js @@ -3,6 +3,7 @@ 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'; @@ -31,10 +32,26 @@ 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 { - setConnectionString(string): void, - authenticate(string): Promise<void>, + setCredentials(IpcCredentials): void, getAccountData(AccountToken): Promise<AccountData>, getAccount(): Promise<?AccountToken>, setAccount(accountToken: ?AccountToken): Promise<void>, @@ -50,18 +67,22 @@ export interface IpcFacade { export class RealIpc implements IpcFacade { _ipc: JsonRpcWs; + _credentials: ?IpcCredentials; + _authenticationPromise: ?Promise<void>; - constructor(connectionString: string) { - this._ipc = new JsonRpcWs(connectionString); - } + constructor(credentials: IpcCredentials) { + this._credentials = credentials; + this._ipc = new JsonRpcWs(credentials.connectionString); - setConnectionString(str: string) { - this._ipc.setConnectionString(str); + // force to re-authenticate when connection closed + this._ipc.setCloseConnectionHandler(() => { + this._authenticationPromise = null; + }); } - authenticate(sharedSecret: string): Promise<void> { - return this._ipc.send('auth', sharedSecret) - .then(this._ignoreResponse); + setCredentials(credentials: IpcCredentials) { + this._credentials = credentials; + this._ipc.setConnectionString(credentials.connectionString); } getAccountData(accountToken: AccountToken): Promise<AccountData> { @@ -79,19 +100,23 @@ export class RealIpc implements IpcFacade { } getAccount(): Promise<?AccountToken> { - 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._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); + } + }); + }); } setAccount(accountToken: ?AccountToken): Promise<void> { - return this._ipc.send('set_account', accountToken) - .then(this._ignoreResponse); + return this._ensureAuthenticated().then(() => { + return this._ipc.send('set_account', accountToken) + .then(this._ignoreResponse); + }); } _ignoreResponse(_response: mixed): void { @@ -99,48 +124,60 @@ export class RealIpc implements IpcFacade { } setCustomRelay(relayEndpoint: RelayEndpoint): Promise<void> { - return this._ipc.send('set_custom_relay', [relayEndpoint]) - .then(this._ignoreResponse); + return this._ensureAuthenticated().then(() => { + return this._ipc.send('set_custom_relay', [relayEndpoint]) + .then(this._ignoreResponse); + }); } connect(): Promise<void> { - return this._ipc.send('connect') - .then(this._ignoreResponse); + return this._ensureAuthenticated().then(() => { + return this._ipc.send('connect') + .then(this._ignoreResponse); + }); } disconnect(): Promise<void> { - return this._ipc.send('disconnect') - .then(this._ignoreResponse); + return this._ensureAuthenticated().then(() => { + return this._ipc.send('disconnect') + .then(this._ignoreResponse); + }); } getIp(): Promise<Ip> { - 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._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'); + } + }); + }); } getLocation(): Promise<Location> { - 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._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); + } + }); + }); } getState(): Promise<BackendState> { - return this._ipc.send('get_state') - .then(raw => { - return this._parseBackendState(raw); - }); + return this._ensureAuthenticated().then(() => { + return this._ipc.send('get_state') + .then(raw => { + return this._parseBackendState(raw); + }); + }); } _parseBackendState(raw: mixed): BackendState { @@ -163,10 +200,35 @@ export class RealIpc implements IpcFacade { } registerStateListener(listener: (BackendState) => void) { - this._ipc.on('new_state', (rawEvent) => { - const parsedEvent : BackendState = this._parseBackendState(rawEvent); + this._ensureAuthenticated().then(() => { + 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.')); + } + } + + _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; + }); + } } diff --git a/app/lib/jsonrpc-ws-ipc.js b/app/lib/jsonrpc-ws-ipc.js index 2609d1514c..deefce3d66 100644 --- a/app/lib/jsonrpc-ws-ipc.js +++ b/app/lib/jsonrpc-ws-ipc.js @@ -75,6 +75,7 @@ export default class Ipc { _websocket: WebSocket; _backoff: ReconnectionBackoff; _websocketFactory: (string) => WebSocket; + _closeConnectionHandler: ?() => void; constructor(connectionString: string, websocketFactory: ?(string)=>WebSocket) { this._connectionString = connectionString; @@ -91,6 +92,10 @@ export default class Ipc { this._connectionString = str; } + setCloseConnectionHandler(handler: ?() => void) { + this._closeConnectionHandler = handler; + } + on(event: string, listener: (mixed) => void): Promise<*> { log.info('Adding a listener to', event); @@ -246,6 +251,10 @@ export default class Ipc { }; this._websocket.onclose = () => { + if(this._closeConnectionHandler) { + this._closeConnectionHandler(); + } + const delay = this._backoff.getIncreasedBackoff(); log.warn('The websocket connetion closed, attempting to reconnect it in', delay, 'milliseconds'); setTimeout(() => this._reconnect(), delay); diff --git a/app/main.js b/app/main.js index a18bba32b9..1ca22fc115 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/backend'; +import { parseIpcCredentials } from './lib/ipc-facade'; import type { TrayIconType } from './lib/tray-icon-manager'; @@ -89,6 +89,7 @@ const appDelegate = { browserWindowReady = true; appDelegate._pollForConnectionInfoFile(); }); + ipcMain.on('show-window', () => { appDelegate._showWindow(window, appDelegate._tray); }); @@ -102,6 +103,8 @@ const appDelegate = { // create tray icon on macOS if(isMacOS) { appDelegate._tray = appDelegate._createTray(window); + } else { + appDelegate._showWindow(window, null); } appDelegate._setAppMenu(); @@ -259,6 +262,7 @@ const appDelegate = { resizable: false, maximizable: false, fullscreenable: false, + show: false, webPreferences: { // prevents renderer process code from not running when window is hidden backgroundThrottling: false, @@ -272,8 +276,7 @@ const appDelegate = { options = Object.assign({}, options, { height: contentHeight + 12, // 12 is the size of transparent area around arrow frame: false, - transparent: true, - show: false + transparent: true }); } diff --git a/test/mocks/ipc.js b/test/mocks/ipc.js index fc5dede75e..8f0b82ba37 100644 --- a/test/mocks/ipc.js +++ b/test/mocks/ipc.js @@ -1,5 +1,5 @@ // @flow -import type { IpcFacade, BackendState } from '../../app/lib/ipc-facade'; +import type { IpcFacade, BackendState, IpcCredentials } from '../../app/lib/ipc-facade'; interface MockIpc { sendNewState: (BackendState) => void; @@ -14,10 +14,7 @@ export function newMockIpc() { const mockIpc: IpcFacade & MockIpc = { - setConnectionString: (_str: string) => {}, - authenticate: (_sharedSecret) => { - return new Promise(r => r()); - }, + setCredentials: (_credentials: IpcCredentials) => {}, getAccountData: (accountToken) => { return new Promise(r => r({ accountToken: accountToken, |
