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).
This commit is contained in:
David Baker 2018-12-18 17:42:55 +00:00
parent df155293b1
commit 19f1489c92
7 changed files with 198 additions and 144 deletions

View file

@ -2,6 +2,7 @@
Copyright 2016 Aviral Dasgupta Copyright 2016 Aviral Dasgupta
Copyright 2016 OpenMarket Ltd Copyright 2016 OpenMarket Ltd
Copyright 2017 Michael Telatynski <7t3chguy@gmail.com> Copyright 2017 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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; if (checkSquirrelHooks()) return;
const argv = require('minimist')(process.argv); 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 AutoLaunch = require('auto-launch');
const path = require('path');
const tray = require('./tray'); const tray = require('./tray');
const vectorMenu = require('./vectormenu'); 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'); 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', () => { app.on('ready', () => {
if (argv['devtools']) { if (argv['devtools']) {
try { try {
@ -191,6 +214,7 @@ app.on('ready', () => {
defaultHeight: 768, defaultHeight: 768,
}); });
const preloadScript = path.normalize(`${__dirname}/preload.js`);
mainWindow = global.mainWindow = new BrowserWindow({ mainWindow = global.mainWindow = new BrowserWindow({
icon: iconPath, icon: iconPath,
show: false, show: false,
@ -200,6 +224,18 @@ app.on('ready', () => {
y: mainWindowState.y, y: mainWindowState.y,
width: mainWindowState.width, width: mainWindowState.width,
height: mainWindowState.height, 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`); mainWindow.loadURL(`file://${__dirname}/../../webapp/index.html`);
Menu.setApplicationMenu(vectorMenu); Menu.setApplicationMenu(vectorMenu);

View file

@ -1,8 +1,5 @@
// @flow
/* /*
Copyright 2016 Aviral Dasgupta Copyright 2018 New Vector Ltd
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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. limitations under the License.
*/ */
let Platform = null; const { ipcRenderer } = require('electron');
if (window && window.process && window.process && window.process.type === 'renderer') { // expose ipcRenderer to the renderer process
// we're running inside electron window.ipcRenderer = ipcRenderer;
Platform = require('./ElectronPlatform');
} else {
Platform = require('./WebPlatform');
}
export default Platform;

View file

@ -47,7 +47,9 @@ import * as languageHandler from 'matrix-react-sdk/lib/languageHandler';
import url from 'url'; import url from 'url';
import {parseQs, parseQsFromFragment} from './url_utils'; 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 MatrixClientPeg from 'matrix-react-sdk/lib/MatrixClientPeg';
import SettingsStore from "matrix-react-sdk/lib/settings/SettingsStore"; import SettingsStore from "matrix-react-sdk/lib/settings/SettingsStore";
@ -219,8 +221,15 @@ async function loadApp() {
const fragparts = parseQsFromFragment(window.location); const fragparts = parseQsFromFragment(window.location);
const params = parseQs(window.location); const params = parseQs(window.location);
// set the platform for react sdk (our Platform object automatically picks the right one) // set the platform for react sdk
PlatformPeg.set(new Platform()); //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 // 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. // form "config.$domain.json" and if that fails, fall back to config.json.

View file

@ -3,6 +3,7 @@
/* /*
Copyright 2016 Aviral Dasgupta Copyright 2016 Aviral Dasgupta
Copyright 2016 OpenMarket Ltd Copyright 2016 OpenMarket Ltd
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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 dis from 'matrix-react-sdk/lib/dispatcher';
import { _t } from 'matrix-react-sdk/lib/languageHandler'; import { _t } from 'matrix-react-sdk/lib/languageHandler';
import Promise from 'bluebird'; import Promise from 'bluebird';
import {remote, ipcRenderer, desktopCapturer} from 'electron';
import rageshake from 'matrix-react-sdk/lib/rageshake/rageshake'; 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 { function platformFriendlyName(): string {
console.log(window.process); // used to use window.process but the same info is available here
switch (window.process.platform) { if (navigator.userAgent.indexOf('Macintosh')) {
case 'darwin':
return 'macOS'; return 'macOS';
case 'freebsd': } else if (navigator.userAgent.indexOf('FreeBSD')) {
return 'FreeBSD'; return 'FreeBSD';
case 'openbsd': } else if (navigator.userAgent.indexOf('OpenBSD')) {
return 'OpenBSD'; return 'OpenBSD';
case 'sunos': } else if (navigator.userAgent.indexOf('SunOS')) {
return 'SunOS'; return 'SunOS';
case 'win32': } else if (navigator.userAgent.indexOf('Windows')) {
return 'Windows'; return 'Windows';
default: } else if (navigator.userAgent.indexOf('Linux')) {
// Sorry, Linux users: you get lumped into here, return 'Linux';
// but only because Linux's capitalisation is } else {
// normal. We do care about you. return 'Unknown';
return window.process.platform[0].toUpperCase() + window.process.platform.slice(1);
} }
} }
@ -85,9 +66,11 @@ function getUpdateCheckStatus(status) {
export default class ElectronPlatform extends VectorBasePlatform { export default class ElectronPlatform extends VectorBasePlatform {
constructor() { constructor() {
super(); super();
dis.register(_onAction);
this.updatable = Boolean(remote.autoUpdater.getFeedURL());
this._pendingIpcCalls = {};
this._nextIpcCallId = 0;
dis.register(_onAction);
/* /*
IPC Call `check_updates` returns: IPC Call `check_updates` returns:
true if there is an update available true if there is an update available
@ -103,10 +86,28 @@ export default class ElectronPlatform extends VectorBasePlatform {
this.showUpdateCheck = false; 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.startUpdateCheck = this.startUpdateCheck.bind(this);
this.stopUpdateCheck = this.stopUpdateCheck.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 { getHumanReadableName(): string {
return 'Electron Platform'; // no translation required: only used for analytics 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 // 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 &amp; // we only have to strip out < and > as the spec doesn't include anything about things like &amp;
// so we shouldn't assume that all implementations will treat those properly. Very basic tag parsing is done. // 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, '&lt;').replace(/>/g, '&gt;'); msg = msg.replace(/</g, '&lt;').replace(/>/g, '&gt;');
} }
@ -147,17 +148,13 @@ export default class ElectronPlatform extends VectorBasePlatform {
}, },
); );
notification.onclick = function() { notification.onclick = () => {
dis.dispatch({ dis.dispatch({
action: 'view_room', action: 'view_room',
room_id: room.roomId, room_id: room.roomId,
}); });
global.focus(); global.focus();
const win = remote.getCurrentWindow(); this._ipcCall('focusWindow');
if (win.isMinimized()) win.restore();
else if (!win.isVisible()) win.show();
else win.focus();
}; };
return notification; return notification;
@ -171,8 +168,25 @@ export default class ElectronPlatform extends VectorBasePlatform {
notif.close(); notif.close();
} }
getAppVersion(): Promise<string> { async getAppVersion(): Promise<string> {
return Promise.resolve(remote.app.getVersion()); 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() { startUpdateCheck() {
@ -197,52 +211,43 @@ export default class ElectronPlatform extends VectorBasePlatform {
return null; return null;
} }
isElectron(): boolean { return true; }
requestNotificationPermission(): Promise<string> { requestNotificationPermission(): Promise<string> {
return Promise.resolve('granted'); return Promise.resolve('granted');
} }
reload() { 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 async _ipcCall(name, ...args) {
* setupScreenSharingForIframe function from: const ipcCallId = ++this._nextIpcCallId;
* https://github.com/jitsi/jitsi-meet-electron-utils return new Promise((resolve, reject) => {
* Copied directly here to avoid the need for a native electron module for this._pendingIpcCalls[ipcCallId] = {resolve, reject};
* 'just a bit of JavaScript' window.ipcRenderer.send('ipcCall', {id: ipcCallId, name, args});
* NOTE: Apache v2.0 licensed // Maybe add a timeout to these? Probably not necessary.
*/ });
setupScreenSharingForIframe(iframe: Object) { }
iframe.contentWindow.JitsiMeetElectron = {
/** _onIpcReply(ev, payload) {
* Get sources available for screensharing. The callback is invoked if (payload.id === undefined) {
* with an array of DesktopCapturerSources. console.warn("Ignoring IPC reply with no ID");
*
* @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; return;
} }
callback(sources); 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 */
} }

View file

@ -3,6 +3,7 @@
/* /*
Copyright 2016 Aviral Dasgupta Copyright 2016 Aviral Dasgupta
Copyright 2016 OpenMarket Ltd Copyright 2016 OpenMarket Ltd
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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.favicon = new Favico({animation: 'none'});
this.showUpdateCheck = false; this.showUpdateCheck = false;
this._updateFavicon(); this._updateFavicon();
this.updatable = true;
this.startUpdateCheck = this.startUpdateCheck.bind(this); this.startUpdateCheck = this.startUpdateCheck.bind(this);
this.stopUpdateCheck = this.stopUpdateCheck.bind(this); this.stopUpdateCheck = this.stopUpdateCheck.bind(this);
@ -88,6 +88,19 @@ export default class VectorBasePlatform extends BasePlatform {
this._updateFavicon(); 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 * Begin update polling, if applicable
*/ */
@ -97,8 +110,8 @@ export default class VectorBasePlatform extends BasePlatform {
/** /**
* Whether we can call checkForUpdate on this platform build * Whether we can call checkForUpdate on this platform build
*/ */
canSelfUpdate(): boolean { async canSelfUpdate(): boolean {
return this.updatable; return false;
} }
startUpdateCheck() { startUpdateCheck() {

View file

@ -142,6 +142,10 @@ export default class WebPlatform extends VectorBasePlatform {
setInterval(this.pollForUpdate.bind(this), POKE_RATE_MS); setInterval(this.pollForUpdate.bind(this), POKE_RATE_MS);
} }
async canSelfUpdate(): boolean {
return true;
}
pollForUpdate() { pollForUpdate() {
return this._getVersion().then((ver) => { return this._getVersion().then((ver) => {
if (this.runningVersion === null) { if (this.runningVersion === null) {

View file

@ -108,11 +108,6 @@ module.exports = {
"matrix-js-sdk": path.resolve('./node_modules/matrix-js-sdk'), "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: [ plugins: [
new webpack.DefinePlugin({ new webpack.DefinePlugin({
'process.env': { 'process.env': {