diff options
| author | Emīls Piņķis <emils@mullvad.net> | 2018-08-29 11:20:57 +0100 |
|---|---|---|
| committer | Emīls Piņķis <emils@mullvad.net> | 2018-08-29 16:28:17 +0100 |
| commit | cff2be6a76823fa250df6b3a69a796093634dbc6 (patch) | |
| tree | 366dd4be601188cb3ce2fee621a5d2500d9e7a30 /gui | |
| parent | 0dd43272e4473dbd01ef39ab8bfbb454c47487da (diff) | |
| download | mullvadvpn-cff2be6a76823fa250df6b3a69a796093634dbc6.tar.xz mullvadvpn-cff2be6a76823fa250df6b3a69a796093634dbc6.zip | |
Adjust frontend to use IPC rather than WebSockets
Diffstat (limited to 'gui')
| -rw-r--r-- | gui/packages/desktop/package.json | 4 | ||||
| -rw-r--r-- | gui/packages/desktop/src/common/types.js | 6 | ||||
| -rw-r--r-- | gui/packages/desktop/src/main/index.js | 40 | ||||
| -rw-r--r-- | gui/packages/desktop/src/main/rpc-address-file.js | 112 | ||||
| -rw-r--r-- | gui/packages/desktop/src/main/tempdir.js | 20 | ||||
| -rw-r--r-- | gui/packages/desktop/src/renderer/app.js | 41 | ||||
| -rw-r--r-- | gui/packages/desktop/src/renderer/lib/daemon-rpc.js | 13 | ||||
| -rw-r--r-- | gui/packages/desktop/src/renderer/lib/jsonrpc-client.js (renamed from gui/packages/desktop/src/renderer/lib/jsonrpc-transport.js) | 205 | ||||
| -rw-r--r-- | gui/packages/desktop/test/jsonrpc-transport.spec.js | 11 | ||||
| -rw-r--r-- | gui/yarn.lock | 19 |
10 files changed, 203 insertions, 268 deletions
diff --git a/gui/packages/desktop/package.json b/gui/packages/desktop/package.json index 98b6ab3c85..70ef5cc11c 100644 --- a/gui/packages/desktop/package.json +++ b/gui/packages/desktop/package.json @@ -13,6 +13,7 @@ "license": "GPL-3.0", "dependencies": { "@mullvad/components": "0.1.0", + "JSONStream": "^1.3.4", "babel-runtime": "^6.26.0", "connected-react-router": "^4.3.0", "d3-geo-projection": "^2.3.2", @@ -33,8 +34,7 @@ "validated": "^1.3.0" }, "optionalDependencies": { - "nseventmonitor": "https://github.com/mullvad/NSEventMonitor.git#0.0.9", - "windows-security": "https://github.com/mullvad/windows-security.git#0.0.5" + "nseventmonitor": "https://github.com/mullvad/NSEventMonitor.git#0.0.9" }, "devDependencies": { "babel-cli": "^6.26.0", diff --git a/gui/packages/desktop/src/common/types.js b/gui/packages/desktop/src/common/types.js deleted file mode 100644 index f9ede46014..0000000000 --- a/gui/packages/desktop/src/common/types.js +++ /dev/null @@ -1,6 +0,0 @@ -// @flow - -export type RpcCredentials = { - connectionString: string, - sharedSecret: string, -}; diff --git a/gui/packages/desktop/src/main/index.js b/gui/packages/desktop/src/main/index.js index 34eb88247a..c9067a0146 100644 --- a/gui/packages/desktop/src/main/index.js +++ b/gui/packages/desktop/src/main/index.js @@ -10,7 +10,6 @@ import { app, screen, BrowserWindow, ipcMain, Tray, Menu, nativeImage } from 'el import TrayIconController from './tray-icon-controller'; import WindowController from './window-controller'; -import RpcAddressFile from './rpc-address-file'; import ShutdownCoordinator from './shutdown-coordinator'; import { resolveBin } from './proc'; @@ -191,45 +190,6 @@ const ApplicationMain = { }, _registerIpcListeners() { - ipcMain.on('discover-daemon-connection', async (event) => { - const addressFile = new RpcAddressFile(); - - log.debug(`Waiting for RPC address file: "${addressFile.filePath}"`); - - try { - await addressFile.waitUntilExists(); - } catch (error) { - log.error(`Cannot finish polling the RPC address file: ${error.message}`); - return; - } - - try { - if (!addressFile.isTrusted()) { - log.error(`Cannot verify the credibility of RPC address file`); - return; - } - } catch (error) { - log.error(`An error occurred during the credibility check: ${error.message}`); - return; - } - - // There is a race condition here where the owner and permissions of - // the file can change in the time between we validate the owner and - // permissions and read the contents of the file. We deem the chance - // of that to be small enough to ignore. - - try { - const credentials = await addressFile.parse(); - - log.debug('Read RPC connection info', credentials.connectionString); - - event.sender.send('daemon-connection-ready', credentials); - } catch (error) { - log.error(`Cannot parse the RPC address file: ${error.message}`); - return; - } - }); - ipcMain.on('show-window', () => { const windowController = this._windowController; if (windowController) { diff --git a/gui/packages/desktop/src/main/rpc-address-file.js b/gui/packages/desktop/src/main/rpc-address-file.js deleted file mode 100644 index 9c46165058..0000000000 --- a/gui/packages/desktop/src/main/rpc-address-file.js +++ /dev/null @@ -1,112 +0,0 @@ -// @flow - -import fs from 'fs'; -import path from 'path'; -import { app } from 'electron'; -import { promisify } from 'util'; -import { getSystemTemporaryDirectory } from './tempdir'; - -import type { RpcCredentials } from '../common/types'; - -const fsReadFileAsync = promisify(fs.readFile); - -const POLL_INTERVAL = 200; - -export default class RpcAddressFile { - _filePath = getRpcAddressFilePath(); - _pollIntervalId: ?IntervalID; - _pollPromise: ?Promise<void>; - - get filePath(): string { - return this._filePath; - } - - waitUntilExists(): Promise<void> { - let promise = this._pollPromise; - - if (!promise) { - promise = new Promise((resolve) => { - const timer = setInterval(() => { - fs.exists(this._filePath, (exists) => { - if (exists) { - clearInterval(timer); - resolve(); - - this._pollPromise = null; - } - }); - }, POLL_INTERVAL); - }); - - this._pollPromise = promise; - } - - return promise; - } - - async parse(): Promise<RpcCredentials> { - const data = await fsReadFileAsync(this._filePath, 'utf8'); - const [connectionString, sharedSecret] = data.split('\n', 2); - - if (connectionString && sharedSecret !== undefined) { - return { - connectionString, - sharedSecret, - }; - } else { - throw new Error('Cannot parse the RPC address file'); - } - } - - isTrusted() { - const filePath = this._filePath; - switch (process.platform) { - case 'win32': - return isOwnedByLocalSystem(filePath); - case 'darwin': - case 'linux': - return isOwnedAndOnlyWritableByRoot(filePath); - default: - throw new Error(`Unknown platform: ${process.platform}`); - } - } -} - -function getRpcAddressFilePath() { - const rpcAddressFileName = '.mullvad_rpc_address'; - - switch (process.platform) { - case 'win32': { - // Windows: %ALLUSERSPROFILE%\{appname} - const programDataDirectory = process.env.ALLUSERSPROFILE; - if (programDataDirectory) { - const appDataDirectory = path.join(programDataDirectory, app.getName()); - return path.join(appDataDirectory, rpcAddressFileName); - } else { - throw new Error('Missing %ALLUSERSPROFILE% environment variable'); - } - } - default: - return path.join(getSystemTemporaryDirectory(), rpcAddressFileName); - } -} - -function isOwnedAndOnlyWritableByRoot(path: string): boolean { - const stat = fs.statSync(path); - const isOwnedByRoot = stat.uid === 0; - const isOnlyWritableByOwner = (stat.mode & parseInt('022', 8)) === 0; - - return isOwnedByRoot && isOnlyWritableByOwner; -} - -function isOwnedByLocalSystem(path: string): boolean { - // $FlowFixMe: this module is only available on Windows - const winsec = require('windows-security'); - const ownerSid = winsec.getFileOwnerSid(path, null); - const isWellKnownSid = winsec.isWellKnownSid( - ownerSid, - winsec.WellKnownSid.BuiltinAdministratorsSid, - ); - - return isWellKnownSid; -} diff --git a/gui/packages/desktop/src/main/tempdir.js b/gui/packages/desktop/src/main/tempdir.js deleted file mode 100644 index 938448949a..0000000000 --- a/gui/packages/desktop/src/main/tempdir.js +++ /dev/null @@ -1,20 +0,0 @@ -// @flow -import path from 'path'; - -export function getSystemTemporaryDirectory() { - switch (process.platform) { - case 'win32': { - const windowsPath = process.env.windir; - if (windowsPath) { - return path.join(windowsPath, 'Temp'); - } else { - throw new Error('Missing windir in environment variables.'); - } - } - case 'darwin': - case 'linux': - return '/tmp'; - default: - throw new Error(`Not implemented for ${process.platform}`); - } -} diff --git a/gui/packages/desktop/src/renderer/app.js b/gui/packages/desktop/src/renderer/app.js index ad886f963c..e0374f05d5 100644 --- a/gui/packages/desktop/src/renderer/app.js +++ b/gui/packages/desktop/src/renderer/app.js @@ -26,7 +26,6 @@ import settingsActions from './redux/settings/actions'; import versionActions from './redux/version/actions'; import daemonActions from './redux/daemon/actions'; -import type { RpcCredentials } from '../common/types'; import type { DaemonRpcProtocol, AccountData, @@ -41,7 +40,6 @@ export default class AppRenderer { _notificationController = new NotificationController(); _daemonRpc: DaemonRpcProtocol = new DaemonRpc(); _reconnectBackoff = new ReconnectionBackoff(); - _credentials: ?RpcCredentials; _openConnectionObserver: ?DaemonConnectionObserver; _closeConnectionObserver: ?DaemonConnectionObserver; _memoryHistory = createMemoryHistory(); @@ -418,16 +416,7 @@ export default class AppRenderer { } 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); + this._daemonRpc.connect({ path: getIpcPath() }); } async _onOpenConnection() { @@ -437,17 +426,6 @@ export default class AppRenderer { // reset the reconnect backoff when connection established. 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}`); - } - // attempt to restore the session try { await this._restoreSession(); @@ -508,15 +486,6 @@ export default class AppRenderer { } } - _requestCredentials(): Promise<RpcCredentials> { - return new Promise((resolve) => { - ipcRenderer.once('daemon-connection-ready', (_event, credentials: RpcCredentials) => { - resolve(credentials); - }); - ipcRenderer.send('discover-daemon-connection'); - }); - } - async _subscribeStateListener() { await this._daemonRpc.subscribeStateListener((newState, error) => { if (error) { @@ -655,3 +624,11 @@ class AccountDataState { } } } + +const getIpcPath = (): string => { + if (process.platform === 'win32') { + return '//./pipe/Mullvad VPN'; + } else { + return '/var/run/mullvad-vpn'; + } +}; diff --git a/gui/packages/desktop/src/renderer/lib/daemon-rpc.js b/gui/packages/desktop/src/renderer/lib/daemon-rpc.js index 3f3d3af9c1..c403e3fa92 100644 --- a/gui/packages/desktop/src/renderer/lib/daemon-rpc.js +++ b/gui/packages/desktop/src/renderer/lib/daemon-rpc.js @@ -1,9 +1,10 @@ // @flow -import JsonRpcTransport, { +import JsonRpcClient, { RemoteError as JsonRpcRemoteError, TimeOutError as JsonRpcTimeOutError, -} from './jsonrpc-transport'; + SocketTransport, +} from './jsonrpc-client'; import { CommunicationError, InvalidAccountError, NoDaemonError } from '../errors'; import { @@ -222,7 +223,7 @@ const AppVersionInfoSchema = object({ }); export interface DaemonRpcProtocol { - connect(string): void; + connect({ path: string }): void; disconnect(): void; getAccountData(AccountToken): Promise<AccountData>; getRelayLocations(): Promise<RelayList>; @@ -268,14 +269,14 @@ export type ConnectionObserver = { }; export class DaemonRpc implements DaemonRpcProtocol { - _transport = new JsonRpcTransport(); + _transport = new JsonRpcClient(new SocketTransport()); async authenticate(sharedSecret: string): Promise<void> { await this._transport.send('auth', sharedSecret); } - connect(connectionString: string) { - this._transport.connect(connectionString); + connect(connectionParams: { path: string }) { + this._transport.connect(connectionParams); } disconnect() { diff --git a/gui/packages/desktop/src/renderer/lib/jsonrpc-transport.js b/gui/packages/desktop/src/renderer/lib/jsonrpc-client.js index e5c81e6b0f..cf779c83be 100644 --- a/gui/packages/desktop/src/renderer/lib/jsonrpc-transport.js +++ b/gui/packages/desktop/src/renderer/lib/jsonrpc-client.js @@ -4,6 +4,8 @@ import { EventEmitter } from 'events'; import log from 'electron-log'; import jsonrpc from 'jsonrpc-lite'; import uuid from 'uuid'; +import net from 'net'; +import JSONStream from 'JSONStream'; export type UnansweredRequest = { resolve: (mixed) => void, @@ -90,11 +92,11 @@ export class SubscriptionError extends Error { } } -export class ConnectionError extends Error { +export class WebSocketError extends Error { _code: number; constructor(code: number) { - super(ConnectionError.reason(code)); + super(WebSocketError.reason(code)); this._code = code; } @@ -118,34 +120,39 @@ export class ConnectionError extends Error { } } +export class TransportError extends Error { + constructor(reason: string) { + super(reason); + } +} + const DEFAULT_TIMEOUT_MILLIS = 5000; -export default class JsonRpcTransport extends EventEmitter { +export default class JsonRpcClient<T> extends EventEmitter { _unansweredRequests: Map<string, UnansweredRequest> = new Map(); _subscriptions: Map<string | number, (mixed) => void> = new Map(); - _webSocket: ?WebSocket; - _websocketFactory: (string) => WebSocket; + _transport: Transport<T>; - constructor(websocketFactory: ?(string) => WebSocket) { + constructor(transport: Transport<T>) { super(); - this._websocketFactory = - websocketFactory || ((connectionString) => new WebSocket(connectionString)); + + this._transport = transport; } /// Connect websocket - connect(connectionString: string): Promise<void> { + connect(connectionParams: T): Promise<void> { return new Promise((resolve, reject) => { this.disconnect(); - log.info('Connecting to websocket', connectionString); - - const webSocket = this._websocketFactory(connectionString); + log.info('Connecting to transport with params', connectionParams); // A flag used to determine if Promise was resolved. let isPromiseResolved = false; - webSocket.onopen = () => { - log.info('Websocket is connected'); + const transport = this._transport; + + transport.onOpen = () => { + log.info('Transport is connected'); this.emit('open'); // Resolve the Promise @@ -153,40 +160,30 @@ export default class JsonRpcTransport extends EventEmitter { isPromiseResolved = true; }; - webSocket.onmessage = (event) => { - const data = event.data; - if (typeof data === 'string') { - this._onMessage(data); - } else { - log.error('Got invalid reply from the server', event); - } + transport.onMessage = (obj) => { + this._onMessage(obj); }; - webSocket.onclose = (event) => { - log.info(`The websocket connection closed with code: ${event.code}`); - + transport.onClose = (error: ?Error) => { // Remove all subscriptions since they are connection based this._subscriptions.clear(); - // 1000 is a code used for normal connection closure. - const connectionError = event.code === 1000 ? null : new ConnectionError(event.code); - - this.emit('close', connectionError); + this.emit('close', error); // Prevent rejecting a previously resolved Promise. if (!isPromiseResolved) { - reject(connectionError); + reject(error); } }; + transport.connect(connectionParams); - this._webSocket = webSocket; + this._transport = transport; }); } disconnect() { - if (this._webSocket) { - this._webSocket.close(); - this._webSocket = null; + if (this._transport) { + this._transport.close(); } } @@ -211,8 +208,8 @@ export default class JsonRpcTransport extends EventEmitter { send(action: string, data: mixed, timeout: number = DEFAULT_TIMEOUT_MILLIS): Promise<mixed> { return new Promise((resolve, reject) => { - const webSocket = this._webSocket; - if (!webSocket) { + const transport = this._transport; + if (!transport) { reject(new Error('Websocket is not connected.')); return; } @@ -230,7 +227,7 @@ export default class JsonRpcTransport extends EventEmitter { try { log.silly('Sending message', id, action); - webSocket.send(JSON.stringify(message)); + transport.send(JSON.stringify(message)); } catch (error) { log.error(`Failed sending RPC message "${action}": ${error.message}`); @@ -272,9 +269,14 @@ export default class JsonRpcTransport extends EventEmitter { } } - _onMessage(message: string) { - const result = jsonrpc.parse(message); - const messages = Array.isArray(result) ? result : [result]; + _onMessage(obj: Object) { + let messages = []; + try { + const message = jsonrpc.parseObject(obj); + messages = Array.isArray(message) ? message : [message]; + } catch (error) { + log.error(`Failed to parse JSON-RPC message: ${error} for object`); + } for (const message of messages) { if (message.type === 'notification') { @@ -319,3 +321,128 @@ export default class JsonRpcTransport extends EventEmitter { } } } + +interface Transport<T> { + close(): void; + onOpen: (event: Event) => void; + onMessage: (Object) => void; + onClose: (error: ?Error) => void; + send(message: string): void; + connect(params: T): void; +} + +export class WebsocketTransport implements Transport<string> { + ws: ?WebSocket; + onOpen: (event: Event) => void; + onMessage: (Object) => void; + onClose: (error: ?Error) => void; + + constructor(ws: ?WebSocket) { + this.ws = ws; + this.onOpen = () => {}; + this.onMessage = () => {}; + this.onClose = () => {}; + } + + close() { + if (this.ws) this.ws.close(); + } + + send(msg: string) { + if (this.ws) { + this.ws.send(msg); + } + } + + connect(params: string): void { + if (this.ws) { + this.ws.close(); + } + this.ws = new WebSocket(params); + this.ws.onopen = this.onOpen; + this.ws.onmessage = (event) => { + try { + const data = event.data; + if (typeof data === 'string') { + const msg = JSON.parse(data); + this.onMessage(msg); + } else { + throw event; + } + } catch (error) { + log.error('Got invalid reply from server: ', error); + } + }; + + this.ws.onclose = (event) => { + log.info(`The websocket connection closed with code: ${event.code}`); + const error = event.code === 1000 ? null : new WebSocketError(event.code); + this.onClose(error); + }; + } +} + +// Given the correct parameters, this transport supports named pipes/unix +// domain sockets, and also TCP/UDP sockets +export class SocketTransport implements Transport<{ path: string }> { + connection: ?net.Socket; + onMessage: (message: Object) => void; + onClose: (error: ?Error) => void; + onOpen: (event: Event) => void; + + constructor() { + this.connection = null; + this.onMessage = () => {}; + this.onClose = () => {}; + this.onOpen = () => {}; + } + + _connect(options: { path: string }) { + const connection = new net.Socket(); + connection.on('error', (err) => { + this.onClose(err); + this.close(); + }); + + connection.on('connect', (event) => { + this.connection = connection; + this.onOpen(event); + }); + + const jsonStream = JSONStream.parse(); + + connection.pipe(jsonStream); + + jsonStream.on('data', this.onMessage); + + jsonStream.on('error', (err) => { + this.onClose(err); + this.close(); + }); + + connection.connect(options); + } + + close() { + try { + if (this.connection) { + this.connection.end(); + } + } catch (error) { + log.error('failed to close the connection: ', error); + } + this.connection = null; + } + + send(msg: string) { + if (this.connection) { + this.connection.write(msg); + } else { + throw new TransportError('Socket not connected'); + } + } + + connect(options: { path: string }): void { + this._connect(options); + } +} diff --git a/gui/packages/desktop/test/jsonrpc-transport.spec.js b/gui/packages/desktop/test/jsonrpc-transport.spec.js index 5e600f2204..ea07e060b8 100644 --- a/gui/packages/desktop/test/jsonrpc-transport.spec.js +++ b/gui/packages/desktop/test/jsonrpc-transport.spec.js @@ -1,15 +1,18 @@ // @flow import jsonrpc from 'jsonrpc-lite'; -import { Server, WebSocket as MockWebSocket } from 'mock-socket'; -import JsonRpcTransport, { TimeOutError } from '../src/renderer/lib/jsonrpc-transport'; +import { Server } from 'mock-socket'; +import JsonRpcClient, { + WebsocketTransport, + TimeOutError, +} from '../src/renderer/lib/jsonrpc-client'; describe('JSON RPC transport', () => { const WEBSOCKET_URL = 'ws://localhost:8080'; - let server: Server, transport: JsonRpcTransport; + let server: Server, transport: JsonRpcClient<string>; beforeEach(() => { server = new Server(WEBSOCKET_URL); - transport = new JsonRpcTransport((url) => new MockWebSocket(url)); + transport = new JsonRpcClient(new WebsocketTransport()); }); afterEach(() => { diff --git a/gui/yarn.lock b/gui/yarn.lock index cded790c63..102a3b710e 100644 --- a/gui/yarn.lock +++ b/gui/yarn.lock @@ -684,6 +684,13 @@ version "2.3.3" resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-2.3.3.tgz#7f226d67d654ec9070e755f46daebf014628e9d9" +JSONStream@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.4.tgz#615bb2adb0cd34c8f4c447b5f6512fa1d8f16a2e" + dependencies: + jsonparse "^1.2.0" + through ">=2.2.7 <3" + abab@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.0.tgz#aba0ab4c5eee2d4c79d3487d85450fb2376ebb0f" @@ -4625,6 +4632,10 @@ jsonify@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" +jsonparse@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" + jsonrpc-lite@^1.2.3: version "1.3.1" resolved "https://registry.yarnpkg.com/jsonrpc-lite/-/jsonrpc-lite-1.3.1.tgz#5c33086071793a0806e6c96e7c1ae92f4460ac50" @@ -7251,7 +7262,7 @@ through2@~0.2.3: readable-stream "~1.1.9" xtend "~2.1.1" -through@2, through@^2.3.6, through@^2.3.8, through@~2.3, through@~2.3.1: +through@2, "through@>=2.2.7 <3", through@^2.3.6, through@^2.3.8, through@~2.3, through@~2.3.1: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" @@ -7728,12 +7739,6 @@ window-size@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.2.0.tgz#b4315bb4214a3d7058ebeee892e13fa24d98b075" -"windows-security@https://github.com/mullvad/windows-security.git#0.0.5": - version "0.0.5" - resolved "https://github.com/mullvad/windows-security.git#311fcfeb137e9e10b25929b0615f9188e118004f" - dependencies: - node-pre-gyp "^0.10.0" - wordwrap@^1.0.0, wordwrap@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" |
