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

@@ -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 &amp;
// 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;');
}
@@ -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<string> {
return Promise.resolve(remote.app.getVersion());
async getAppVersion(): Promise<string> {
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<string> {
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 */
}