From 19f1489c9284171e5ea3e69121864697a4e97e8a Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 18 Dec 2018 17:42:55 +0000 Subject: [PATCH] Run the Desktop app in a sandbox * Turn off node integration in the electron renderer process * Enable the chromium sandbox to put the renderer into its own process * Expose just the ipc module with a preload script * Introduce a little IPC call wrapper so we can call into the renderer process and await on the result. * Use this in a bunch of places we previously used direct calls to electron modules. * Convert other uses of node, eg. use of process to derive the platform (just look at the user agent) * Strip out the desktopCapturer integration which doesn't appear to have ever worked (probably best to just wait until getDisplayMedia() is available in chrome at this point: https://github.com/vector-im/riot-web/issues/4880). --- electron_app/src/electron-main.js | 106 +++++++---- .../index.js => electron_app/src/preload.js | 16 +- src/vector/index.js | 15 +- src/vector/platform/ElectronPlatform.js | 177 +++++++++--------- src/vector/platform/VectorBasePlatform.js | 19 +- src/vector/platform/WebPlatform.js | 4 + webpack.config.js | 5 - 7 files changed, 198 insertions(+), 144 deletions(-) rename src/vector/platform/index.js => electron_app/src/preload.js (61%) diff --git a/electron_app/src/electron-main.js b/electron_app/src/electron-main.js index 85955392aa..e11b3233ba 100644 --- a/electron_app/src/electron-main.js +++ b/electron_app/src/electron-main.js @@ -2,6 +2,7 @@ Copyright 2016 Aviral Dasgupta Copyright 2016 OpenMarket Ltd Copyright 2017 Michael Telatynski <7t3chguy@gmail.com> +Copyright 2018 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -23,8 +24,9 @@ const checkSquirrelHooks = require('./squirrelhooks'); if (checkSquirrelHooks()) return; const argv = require('minimist')(process.argv); -const {app, ipcMain, powerSaveBlocker, BrowserWindow, Menu} = require('electron'); +const {app, ipcMain, powerSaveBlocker, BrowserWindow, Menu, autoUpdater} = require('electron'); const AutoLaunch = require('auto-launch'); +const path = require('path'); const tray = require('./tray'); const vectorMenu = require('./vectormenu'); @@ -97,6 +99,61 @@ ipcMain.on('app_onAction', function(ev, payload) { } }); +autoUpdater.on('update-downloaded', (ev, releaseNotes, releaseName, releaseDate, updateURL) => { + if (!mainWindow) return; + // forward to renderer + mainWindow.webContents.send('update-downloaded', { + releaseNotes, + releaseName, + releaseDate, + updateURL, + }); +}); + +ipcMain.on('ipcCall', function(ev, payload) { + if (!mainWindow) return; + + const args = payload.args || []; + let ret; + + switch (payload.name) { + case 'getUpdateFeedUrl': + ret = autoUpdater.getFeedURL(); + break; + case 'getAutoLaunchEnabled': + ret = launcher.isEnabled; + break; + case 'setAutoLaunchEnabled': + if (args[0]) { + launcher.enable(); + } else { + launcher.disable(); + } + break; + case 'getAppVersion': + ret = app.getVersion(); + break; + case 'focusWindow': + if (mainWindow.isMinimized()) { + mainWindow.restore(); + } else if (!mainWindow.isVisible()) { + mainWindow.show(); + } else { + mainWindow.focus(); + } + default: + mainWindow.webContents.send('ipcReply', { + id: payload.id, + error: new Error("Unknown IPC Call: "+payload.name), + }); + return; + } + + mainWindow.webContents.send('ipcReply', { + id: payload.id, + reply: ret, + }); +}); app.commandLine.appendSwitch('--enable-usermedia-screen-capturing'); @@ -126,40 +183,6 @@ const launcher = new AutoLaunch({ }, }); -const settings = { - 'auto-launch': { - get: launcher.isEnabled, - set: function(bool) { - if (bool) { - return launcher.enable(); - } else { - return launcher.disable(); - } - }, - }, -}; - -ipcMain.on('settings_get', async function(ev) { - const data = {}; - - try { - await Promise.all(Object.keys(settings).map(async function (setting) { - data[setting] = await settings[setting].get(); - })); - - ev.sender.send('settings', data); - } catch (e) { - console.error(e); - } -}); - -ipcMain.on('settings_set', function(ev, key, value) { - console.log(key, value); - if (settings[key] && settings[key].set) { - settings[key].set(value); - } -}); - app.on('ready', () => { if (argv['devtools']) { try { @@ -191,6 +214,7 @@ app.on('ready', () => { defaultHeight: 768, }); + const preloadScript = path.normalize(`${__dirname}/preload.js`); mainWindow = global.mainWindow = new BrowserWindow({ icon: iconPath, show: false, @@ -200,6 +224,18 @@ app.on('ready', () => { y: mainWindowState.y, width: mainWindowState.width, height: mainWindowState.height, + webPreferences: { + preload: preloadScript, + nodeIntegration: false, + sandbox: true, + enableRemoteModule: false, + // We don't use this: it's useful for the preload script to + // share a context with the main page so we can give select + // objects to the main page. The sandbox option isolates the + // main page from the background script. + contextIsolation: false, + webgl: false, + }, }); mainWindow.loadURL(`file://${__dirname}/../../webapp/index.html`); Menu.setApplicationMenu(vectorMenu); diff --git a/src/vector/platform/index.js b/electron_app/src/preload.js similarity index 61% rename from src/vector/platform/index.js rename to electron_app/src/preload.js index 9071420015..4c926d2145 100644 --- a/src/vector/platform/index.js +++ b/electron_app/src/preload.js @@ -1,8 +1,5 @@ -// @flow - /* -Copyright 2016 Aviral Dasgupta -Copyright 2016 OpenMarket Ltd +Copyright 2018 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,13 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -let Platform = null; +const { ipcRenderer } = require('electron'); -if (window && window.process && window.process && window.process.type === 'renderer') { - // we're running inside electron - Platform = require('./ElectronPlatform'); -} else { - Platform = require('./WebPlatform'); -} +// expose ipcRenderer to the renderer process +window.ipcRenderer = ipcRenderer; -export default Platform; diff --git a/src/vector/index.js b/src/vector/index.js index 32a88f363d..9d05aa347c 100644 --- a/src/vector/index.js +++ b/src/vector/index.js @@ -47,7 +47,9 @@ import * as languageHandler from 'matrix-react-sdk/lib/languageHandler'; import url from 'url'; import {parseQs, parseQsFromFragment} from './url_utils'; -import Platform from './platform'; + +import ElectronPlatform from './platform/ElectronPlatform'; +import WebPlatform from './platform/WebPlatform'; import MatrixClientPeg from 'matrix-react-sdk/lib/MatrixClientPeg'; import SettingsStore from "matrix-react-sdk/lib/settings/SettingsStore"; @@ -219,8 +221,15 @@ async function loadApp() { const fragparts = parseQsFromFragment(window.location); const params = parseQs(window.location); - // set the platform for react sdk (our Platform object automatically picks the right one) - PlatformPeg.set(new Platform()); + // set the platform for react sdk + //if (navigator.userAgent.toLowerCase().indexOf('electron') > 0) { + if (window.ipcRenderer) { + console.log("Using Electron platform"); + PlatformPeg.set(new ElectronPlatform()); + } else { + console.log("Using Web platform"); + PlatformPeg.set(new WebPlatform()); + } // Load the config file. First try to load up a domain-specific config of the // form "config.$domain.json" and if that fails, fall back to config.json. diff --git a/src/vector/platform/ElectronPlatform.js b/src/vector/platform/ElectronPlatform.js index 8b384b20d1..b116cd6370 100644 --- a/src/vector/platform/ElectronPlatform.js +++ b/src/vector/platform/ElectronPlatform.js @@ -3,6 +3,7 @@ /* Copyright 2016 Aviral Dasgupta Copyright 2016 OpenMarket Ltd +Copyright 2018 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -21,44 +22,24 @@ import VectorBasePlatform, {updateCheckStatusEnum} from './VectorBasePlatform'; import dis from 'matrix-react-sdk/lib/dispatcher'; import { _t } from 'matrix-react-sdk/lib/languageHandler'; import Promise from 'bluebird'; -import {remote, ipcRenderer, desktopCapturer} from 'electron'; import rageshake from 'matrix-react-sdk/lib/rageshake/rageshake'; -remote.autoUpdater.on('update-downloaded', onUpdateDownloaded); - -// try to flush the rageshake logs to indexeddb before quit. -ipcRenderer.on('before-quit', function() { - console.log('riot-desktop closing'); - rageshake.flush(); -}); - -function onUpdateDownloaded(ev: Event, releaseNotes: string, ver: string, date: Date, updateURL: string) { - dis.dispatch({ - action: 'new_version', - currentVersion: remote.app.getVersion(), - newVersion: ver, - releaseNotes: releaseNotes, - }); -} - function platformFriendlyName(): string { - console.log(window.process); - switch (window.process.platform) { - case 'darwin': - return 'macOS'; - case 'freebsd': - return 'FreeBSD'; - case 'openbsd': - return 'OpenBSD'; - case 'sunos': - return 'SunOS'; - case 'win32': - return 'Windows'; - default: - // Sorry, Linux users: you get lumped into here, - // but only because Linux's capitalisation is - // normal. We do care about you. - return window.process.platform[0].toUpperCase() + window.process.platform.slice(1); + // used to use window.process but the same info is available here + if (navigator.userAgent.indexOf('Macintosh')) { + return 'macOS'; + } else if (navigator.userAgent.indexOf('FreeBSD')) { + return 'FreeBSD'; + } else if (navigator.userAgent.indexOf('OpenBSD')) { + return 'OpenBSD'; + } else if (navigator.userAgent.indexOf('SunOS')) { + return 'SunOS'; + } else if (navigator.userAgent.indexOf('Windows')) { + return 'Windows'; + } else if (navigator.userAgent.indexOf('Linux')) { + return 'Linux'; + } else { + return 'Unknown'; } } @@ -85,9 +66,11 @@ function getUpdateCheckStatus(status) { export default class ElectronPlatform extends VectorBasePlatform { constructor() { super(); - dis.register(_onAction); - this.updatable = Boolean(remote.autoUpdater.getFeedURL()); + this._pendingIpcCalls = {}; + this._nextIpcCallId = 0; + + dis.register(_onAction); /* IPC Call `check_updates` returns: true if there is an update available @@ -103,10 +86,28 @@ export default class ElectronPlatform extends VectorBasePlatform { this.showUpdateCheck = false; }); + // try to flush the rageshake logs to indexeddb before quit. + ipcRenderer.on('before-quit', function() { + console.log('riot-desktop closing'); + rageshake.flush(); + }); + + ipcRenderer.on('ipcReply', this._onIpcReply.bind(this)); + ipcRenderer.on('update-downloaded', this.onUpdateDownloaded.bind(this)); + this.startUpdateCheck = this.startUpdateCheck.bind(this); this.stopUpdateCheck = this.stopUpdateCheck.bind(this); } + async onUpdateDownloaded(ev, updateInfo) { + dis.dispatch({ + action: 'new_version', + currentVersion: await this.getAppVersion(), + newVersion: updateInfo, + releaseNotes: updateInfo.releaseNotes, + }); + } + getHumanReadableName(): string { return 'Electron Platform'; // no translation required: only used for analytics } @@ -133,7 +134,7 @@ export default class ElectronPlatform extends VectorBasePlatform { // maybe we should pass basic styling (italics, bold, underline) through from MD // we only have to strip out < and > as the spec doesn't include anything about things like & // so we shouldn't assume that all implementations will treat those properly. Very basic tag parsing is done. - if (window.process.platform === 'linux') { + if (navigator.userAgent.indexOf('Linux')) { msg = msg.replace(//g, '>'); } @@ -147,17 +148,13 @@ export default class ElectronPlatform extends VectorBasePlatform { }, ); - notification.onclick = function() { + notification.onclick = () => { dis.dispatch({ action: 'view_room', room_id: room.roomId, }); global.focus(); - const win = remote.getCurrentWindow(); - - if (win.isMinimized()) win.restore(); - else if (!win.isVisible()) win.show(); - else win.focus(); + this._ipcCall('focusWindow'); }; return notification; @@ -171,8 +168,25 @@ export default class ElectronPlatform extends VectorBasePlatform { notif.close(); } - getAppVersion(): Promise { - return Promise.resolve(remote.app.getVersion()); + async getAppVersion(): Promise { + return await this._ipcCall('getAppVersion'); + } + + supportsAutoLaunch() { + return true; + } + + async getAutoLaunchEnabled() { + return await this._ipcCall('getAutoLaunchEnabled'); + } + + async setAutoLaunchEnabled(enabled) { + return await this._ipcCall('setAutoLaunchEnabled', enabled); + } + + async canSelfUpdate(): boolean { + const feedUrl = await this._ipcCall('getUpdateFeedUrl'); + return Boolean(feedUrl); } startUpdateCheck() { @@ -197,52 +211,43 @@ export default class ElectronPlatform extends VectorBasePlatform { return null; } - isElectron(): boolean { return true; } - requestNotificationPermission(): Promise { return Promise.resolve('granted'); } reload() { - remote.getCurrentWebContents().reload(); + // we used to remote to the main process to get it to + // reload the webcontents, but in practice this is unnecessary: + // the normal way works fine. + window.location.reload(false); } - /* BEGIN copied and slightly-modified code - * setupScreenSharingForIframe function from: - * https://github.com/jitsi/jitsi-meet-electron-utils - * Copied directly here to avoid the need for a native electron module for - * 'just a bit of JavaScript' - * NOTE: Apache v2.0 licensed - */ - setupScreenSharingForIframe(iframe: Object) { - iframe.contentWindow.JitsiMeetElectron = { - /** - * Get sources available for screensharing. The callback is invoked - * with an array of DesktopCapturerSources. - * - * @param {Function} callback - The success callback. - * @param {Function} errorCallback - The callback for errors. - * @param {Object} options - Configuration for getting sources. - * @param {Array} options.types - Specify the desktop source types - * to get, with valid sources being "window" and "screen". - * @param {Object} options.thumbnailSize - Specify how big the - * preview images for the sources should be. The valid keys are - * height and width, e.g. { height: number, width: number}. By - * default electron will return images with height and width of - * 150px. - */ - obtainDesktopStreams(callback, errorCallback, options = {}) { - desktopCapturer.getSources(options, - (error, sources) => { - if (error) { - errorCallback(error); - return; - } - - callback(sources); - }); - }, - }; + async _ipcCall(name, ...args) { + const ipcCallId = ++this._nextIpcCallId; + return new Promise((resolve, reject) => { + this._pendingIpcCalls[ipcCallId] = {resolve, reject}; + window.ipcRenderer.send('ipcCall', {id: ipcCallId, name, args}); + // Maybe add a timeout to these? Probably not necessary. + }); + } + + _onIpcReply(ev, payload) { + if (payload.id === undefined) { + console.warn("Ignoring IPC reply with no ID"); + return; + } + + if (this._pendingIpcCalls[payload.id] === undefined) { + console.warn("Unknown IPC payload ID: " + payload.id); + return; + } + + const callbacks = this._pendingIpcCalls[payload.id]; + delete this._pendingIpcCalls[payload.id]; + if (payload.error) { + callbacks.reject(payload.error); + } else { + callbacks.resolve(payload.reply); + } } - /* END of copied and slightly-modified code */ } diff --git a/src/vector/platform/VectorBasePlatform.js b/src/vector/platform/VectorBasePlatform.js index 16b9d17801..7523462cfc 100644 --- a/src/vector/platform/VectorBasePlatform.js +++ b/src/vector/platform/VectorBasePlatform.js @@ -3,6 +3,7 @@ /* Copyright 2016 Aviral Dasgupta Copyright 2016 OpenMarket Ltd +Copyright 2018 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -45,7 +46,6 @@ export default class VectorBasePlatform extends BasePlatform { this.favicon = new Favico({animation: 'none'}); this.showUpdateCheck = false; this._updateFavicon(); - this.updatable = true; this.startUpdateCheck = this.startUpdateCheck.bind(this); this.stopUpdateCheck = this.stopUpdateCheck.bind(this); @@ -88,6 +88,19 @@ export default class VectorBasePlatform extends BasePlatform { this._updateFavicon(); } + supportsAutoLaunch() { + return false; + } + + // XXX: Surely this should be a setting like any other? + async getAutoLaunchEnabled() { + return false; + } + + async setAutoLaunchEnabled(enabled) { + throw new Error("Unimplemented"); + } + /** * Begin update polling, if applicable */ @@ -97,8 +110,8 @@ export default class VectorBasePlatform extends BasePlatform { /** * Whether we can call checkForUpdate on this platform build */ - canSelfUpdate(): boolean { - return this.updatable; + async canSelfUpdate(): boolean { + return false; } startUpdateCheck() { diff --git a/src/vector/platform/WebPlatform.js b/src/vector/platform/WebPlatform.js index 2955b84a3b..d850dd6ddd 100644 --- a/src/vector/platform/WebPlatform.js +++ b/src/vector/platform/WebPlatform.js @@ -142,6 +142,10 @@ export default class WebPlatform extends VectorBasePlatform { setInterval(this.pollForUpdate.bind(this), POKE_RATE_MS); } + async canSelfUpdate(): boolean { + return true; + } + pollForUpdate() { return this._getVersion().then((ver) => { if (this.runningVersion === null) { diff --git a/webpack.config.js b/webpack.config.js index f335aa3c1b..82972adf44 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -108,11 +108,6 @@ module.exports = { "matrix-js-sdk": path.resolve('./node_modules/matrix-js-sdk'), }, }, - externals: { - // Don't try to bundle electron: leave it as a commonjs dependency - // (the 'commonjs' here means it will output a 'require') - "electron": "commonjs electron", - }, plugins: [ new webpack.DefinePlugin({ 'process.env': {