diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2018-06-20 14:43:51 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2018-07-03 13:37:54 +0200 |
| commit | 67e82627564f8e4a2d8da4bcf5f0fd00867876bc (patch) | |
| tree | 814494d59045d8b3f02cb310dc7728aa818be509 | |
| parent | be096ee87bb1256b67c3b24b61be77d523aff9cc (diff) | |
| download | mullvadvpn-67e82627564f8e4a2d8da4bcf5f0fd00867876bc.tar.xz mullvadvpn-67e82627564f8e4a2d8da4bcf5f0fd00867876bc.zip | |
Move RPC file related stuff into RpcAddressFile
| -rw-r--r-- | app/app.android.js | 2 | ||||
| -rw-r--r-- | app/app.js | 19 | ||||
| -rw-r--r-- | app/lib/backend.js | 23 | ||||
| -rw-r--r-- | app/lib/rpc-address-file.js | 120 | ||||
| -rw-r--r-- | app/lib/rpc-file-security.js | 36 | ||||
| -rw-r--r-- | app/main.js | 121 |
6 files changed, 166 insertions, 155 deletions
diff --git a/app/app.android.js b/app/app.android.js index d362f25283..dbbb0c1c26 100644 --- a/app/app.android.js +++ b/app/app.android.js @@ -22,7 +22,7 @@ const store = configureStore(initialState, memoryHistory); ////////////////////////////////////////////////////////////////////////// const backend = new Backend(store); -DeviceEventEmitter.addListener('com.mullvad.backend-info', async (_event, args) => { +DeviceEventEmitter.addListener('com.mullvad.daemon-connection', async (_event, args) => { backend.setCredentials(args.credentials); backend.sync(); try { diff --git a/app/app.js b/app/app.js index dadc2177c2..70d1052560 100644 --- a/app/app.js +++ b/app/app.js @@ -10,7 +10,6 @@ import { log } from './lib/platform'; import makeRoutes from './routes'; import configureStore from './redux/store'; import { Backend, NoAccountError } from './lib/backend'; - import { setShutdownHandler } from './shutdown-handler'; import type { ConnectionState } from './redux/connection/reducers'; @@ -20,11 +19,8 @@ const initialState = null; const memoryHistory = createMemoryHistory(); const store = configureStore(initialState, memoryHistory); -////////////////////////////////////////////////////////////////////////// -// Backend -////////////////////////////////////////////////////////////////////////// const backend = new Backend(store); -ipcRenderer.on('backend-info', async (_event, args) => { +ipcRenderer.on('daemon-connection', async (_event, args) => { backend.setCredentials(args.credentials); backend.sync(); try { @@ -40,6 +36,8 @@ ipcRenderer.on('backend-info', async (_event, args) => { } }); +ipcRenderer.send('daemon-connection'); + setShutdownHandler(async () => { log.info('Executing a shutdown handler'); @@ -51,13 +49,6 @@ setShutdownHandler(async () => { } }); -////////////////////////////////////////////////////////////////////////// -////////////////////////////////////////////////////////////////////////// - -////////////////////////////////////////////////////////////////////////// -// Tray icon -////////////////////////////////////////////////////////////////////////// - /** * Get tray icon type based on connection state */ @@ -86,14 +77,10 @@ store.subscribe(updateTrayIcon); // force update tray updateTrayIcon(); -////////////////////////////////////////////////////////////////////////// -////////////////////////////////////////////////////////////////////////// // disable smart pinch. webFrame.setVisualZoomLevelLimits(1, 1); -ipcRenderer.send('on-browser-window-ready'); - export default class App extends Component { render() { return ( diff --git a/app/lib/backend.js b/app/lib/backend.js index b24b495449..39521d8edb 100644 --- a/app/lib/backend.js +++ b/app/lib/backend.js @@ -8,6 +8,7 @@ import connectionActions from '../redux/connection/actions'; import settingsActions from '../redux/settings/actions'; import { push } from 'react-router-redux'; +import type { RpcCredentials } from './rpc-address-file'; import type { ReduxStore } from '../redux/store'; import type { AccountToken, BackendState, RelaySettingsUpdate } from './ipc-facade'; import type { ConnectionState } from '../redux/connection/reducers'; @@ -74,32 +75,16 @@ export class UnknownError 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 !== undefined) { - return { - connectionString, - sharedSecret, - }; - } else { - return null; - } -} - /** * Backend implementation */ export class Backend { _ipc: IpcFacade; - _credentials: ?IpcCredentials; + _credentials: ?RpcCredentials; _authenticationPromise: ?Promise<void>; _store: ReduxStore; - constructor(store: ReduxStore, credentials?: IpcCredentials, ipc: ?IpcFacade) { + constructor(store: ReduxStore, credentials?: RpcCredentials, ipc: ?IpcFacade) { this._store = store; this._credentials = credentials; @@ -116,7 +101,7 @@ export class Backend { } } - setCredentials(credentials: IpcCredentials) { + setCredentials(credentials: RpcCredentials) { log.debug('Got connection info to backend', credentials.connectionString); this._credentials = credentials; diff --git a/app/lib/rpc-address-file.js b/app/lib/rpc-address-file.js new file mode 100644 index 0000000000..5185ddc749 --- /dev/null +++ b/app/lib/rpc-address-file.js @@ -0,0 +1,120 @@ +// @flow + +import fs from 'fs'; +import path from 'path'; +import { promisify } from 'util'; +import { getSystemTemporaryDirectory } from './tempdir'; + +const fsReadFileAsync = promisify(fs.readFile); + +const POLL_INTERVAL = 200; + +const appDirectoryName = 'Mullvad VPN'; + +export type RpcCredentials = { + connectionString: string, + sharedSecret: string, +}; + +export class RpcAddressFile { + _filePath: string; + _pollIntervalId: ?IntervalID; + _pollPromise: ?Promise<void>; + + constructor() { + this._filePath = getRpcAddressFilePath(); + } + + get filePath(): string { + return this._filePath; + } + + poll(): Promise<void> { + let promise = this._pollPromise; + + if (!promise) { + promise = new Promise((resolve, _reject) => { + const timer = setInterval(() => { + fs.exists(this._filePath, (exists) => { + if (exists) { + clearInterval(timer); + resolve(); + + this._pollPromise = null; + } + }); + }, POLL_INTERVAL); + }); + + this._pollPromise = promise; + } + + return promise; + } + + 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}`); + } + } + + async waitUntilExists(): 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'); + } + } +} + +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, appDirectoryName); + 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/app/lib/rpc-file-security.js b/app/lib/rpc-file-security.js deleted file mode 100644 index fa88111c02..0000000000 --- a/app/lib/rpc-file-security.js +++ /dev/null @@ -1,36 +0,0 @@ -// @flow - -import fs from 'fs'; - -export function canTrustRpcAddressFile(path: string): boolean { - const platform = process.platform; - switch (platform) { - case 'win32': - return isOwnedByLocalSystem(path); - case 'darwin': - case 'linux': - return isOwnedAndOnlyWritableByRoot(path); - default: - throw new Error(`Unknown platform: ${platform}`); - } -} - -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/app/main.js b/app/main.js index 64c341dc60..3a10f6808c 100644 --- a/app/main.js +++ b/app/main.js @@ -1,6 +1,5 @@ // @flow import path from 'path'; -import fs from 'fs'; import mkdirp from 'mkdirp'; import { log } from './lib/platform'; import { app, BrowserWindow, ipcMain, Tray, Menu, nativeImage } from 'electron'; @@ -8,8 +7,7 @@ import TrayIconController from './tray-icon-controller'; import WindowController from './window-controller'; import { version } from '../package.json'; import { resolveBin } from './lib/proc'; -import { getSystemTemporaryDirectory } from './lib/tempdir'; -import { canTrustRpcAddressFile } from './lib/rpc-file-security'; +import { RpcAddressFile } from './lib/rpc-address-file'; import { execFile } from 'child_process'; import uuid from 'uuid'; @@ -162,8 +160,43 @@ const ApplicationMain = { }, _registerIpcListeners() { - ipcMain.on('on-browser-window-ready', () => { - this._pollConnectionInfoFile(); + ipcMain.on('daemon-connection', async (event) => { + const addressFile = new RpcAddressFile(); + + log.debug(`Reading the RPC connection info from "${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', { credentials }); + } catch (error) { + log.error(`Cannot parse the RPC address file: ${error.message}`); + return; + } }); ipcMain.on('show-window', () => { @@ -249,84 +282,6 @@ const ApplicationMain = { ); }, - _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, appDirectoryName); - return path.join(appDataDirectory, rpcAddressFileName); - } else { - throw new Error('Missing %ALLUSERSPROFILE% environment variable'); - } - } - default: - return path.join(getSystemTemporaryDirectory(), rpcAddressFileName); - } - }, - - _pollConnectionInfoFile() { - if (this._connectionFilePollInterval) { - log.warn( - 'Attempted to start polling for the RPC connection info file while another polling was already running', - ); - return; - } - - const pollIntervalMs = 200; - const rpcAddressFile = this._getRpcAddressFilePath(); - - this._connectionFilePollInterval = setInterval(() => { - if (fs.existsSync(rpcAddressFile)) { - if (this._connectionFilePollInterval) { - clearInterval(this._connectionFilePollInterval); - this._connectionFilePollInterval = null; - } - - this._sendDaemonConnectionInfo(rpcAddressFile); - } - }, pollIntervalMs); - }, - - _sendDaemonConnectionInfo(rpcAddressFile: string) { - log.debug(`Reading the ipc connection info from "${rpcAddressFile}"`); - - try { - if (!canTrustRpcAddressFile(rpcAddressFile)) { - log.error(`Not trusting the contents of "${rpcAddressFile}".`); - return; - } - } catch (e) { - log.error(`Cannot verify the credibility of RPC address file: ${e.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. - - fs.readFile(rpcAddressFile, 'utf8', (err, data) => { - if (err) { - return log.error('Could not find backend connection info', err); - } - - const credentials = parseIpcCredentials(data); - if (credentials) { - log.debug('Read IPC connection info', credentials.connectionString); - const windowController = this._windowController; - if (windowController) { - windowController.window.webContents.send('backend-info', { credentials }); - } - } else { - log.error('Could not parse IPC credentials.'); - } - }); - }, - async _installDevTools() { const installer = require('electron-devtools-installer'); const extensions = ['REACT_DEVELOPER_TOOLS', 'REDUX_DEVTOOLS']; |
