Skip to content

Commit

Permalink
Merge pull request #7943 from vector-im/dbkr/electron_custom_protocol
Browse files Browse the repository at this point in the history
Electron: Load app from custom protocol
  • Loading branch information
dbkr committed Jan 3, 2019
2 parents baf3df5 + 62a49af commit 9e08551
Show file tree
Hide file tree
Showing 12 changed files with 544 additions and 7 deletions.
86 changes: 82 additions & 4 deletions electron_app/src/electron-main.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,23 @@ const checkSquirrelHooks = require('./squirrelhooks');
if (checkSquirrelHooks()) return;

const argv = require('minimist')(process.argv);
const {app, ipcMain, powerSaveBlocker, BrowserWindow, Menu, autoUpdater} = require('electron');
const {app, ipcMain, powerSaveBlocker, BrowserWindow, Menu, autoUpdater, protocol} = require('electron');
const AutoLaunch = require('auto-launch');
const path = require('path');

const tray = require('./tray');
const vectorMenu = require('./vectormenu');
const webContentsHandler = require('./webcontents-handler');
const updater = require('./updater');
const { migrateFromOldOrigin } = require('./originMigrator');

const windowStateKeeper = require('electron-window-state');

// boolean flag set whilst we are doing one-time origin migration
// We only serve the origin migration script while we're actually
// migrating to mitigate any risk of it being used maliciously.
let migratingOrigin = false;

if (argv['profile']) {
app.setPath('userData', `${app.getPath('userData')}-${argv['profile']}`);
}
Expand Down Expand Up @@ -110,7 +116,7 @@ autoUpdater.on('update-downloaded', (ev, releaseNotes, releaseName, releaseDate,
});
});

ipcMain.on('ipcCall', function(ev, payload) {
ipcMain.on('ipcCall', async function(ev, payload) {
if (!mainWindow) return;

const args = payload.args || [];
Expand Down Expand Up @@ -141,10 +147,15 @@ ipcMain.on('ipcCall', function(ev, payload) {
} else {
mainWindow.focus();
}
case 'origin_migrate':
migratingOrigin = true;
await migrateFromOldOrigin();
migratingOrigin = false;
break;
default:
mainWindow.webContents.send('ipcReply', {
id: payload.id,
error: new Error("Unknown IPC Call: "+payload.name),
error: "Unknown IPC Call: " + payload.name,
});
return;
}
Expand All @@ -171,6 +182,13 @@ const launcher = new AutoLaunch({
},
});

// Register the scheme the app is served from as 'standard'
// which allows things like relative URLs and IndexedDB to
// work.
// Also mark it as secure (ie. accessing resources from this
// protocol and HTTPS won't trigger mixed content warnings).
protocol.registerStandardSchemes(['vector'], {secure: true});

app.on('ready', () => {
if (argv['devtools']) {
try {
Expand All @@ -186,6 +204,66 @@ app.on('ready', () => {
}
}

protocol.registerFileProtocol('vector', (request, callback) => {
if (request.method !== 'GET') {
callback({error: -322}); // METHOD_NOT_SUPPORTED from chromium/src/net/base/net_error_list.h
return null;
}

const parsedUrl = new URL(request.url);
if (parsedUrl.protocol !== 'vector:') {
callback({error: -302}); // UNKNOWN_URL_SCHEME
return;
}
if (parsedUrl.host !== 'vector') {
callback({error: -105}); // NAME_NOT_RESOLVED
return;
}

const target = parsedUrl.pathname.split('/');

// path starts with a '/'
if (target[0] !== '') {
callback({error: -6}); // FILE_NOT_FOUND
return;
}

if (target[target.length - 1] == '') {
target[target.length - 1] = 'index.html';
}

let baseDir;
// first part of the path determines where we serve from
if (migratingOrigin && target[1] === 'origin_migrator_dest') {
// the origin migrator destination page
// (only the destination script needs to come from the
// custom protocol: the source part is loaded from a
// file:// as that's the origin we're migrating from).
baseDir = __dirname + "/../../origin_migrator/dest";
} else if (target[1] === 'webapp') {
baseDir = __dirname + "/../../webapp";
} else {
callback({error: -6}); // FILE_NOT_FOUND
return;
}

// Normalise the base dir and the target path separately, then make sure
// the target path isn't trying to back out beyond its root
baseDir = path.normalize(baseDir);

const relTarget = path.normalize(path.join(...target.slice(2)));
if (relTarget.startsWith('..')) {
callback({error: -6}); // FILE_NOT_FOUND
return;
}
const absTarget = path.join(baseDir, relTarget);

callback({
path: absTarget,
});
}, (error) => {
if (error) console.error('Failed to register protocol')
});

if (vectorConfig['update_base_url']) {
console.log(`Starting auto update with base URL: ${vectorConfig['update_base_url']}`);
Expand Down Expand Up @@ -225,7 +303,7 @@ app.on('ready', () => {
webgl: false,
},
});
mainWindow.loadURL(`file://${__dirname}/../../webapp/index.html`);
mainWindow.loadURL('vector://vector/webapp/');
Menu.setApplicationMenu(vectorMenu);

// explicitly hide because setApplicationMenu on Linux otherwise shows...
Expand Down
62 changes: 62 additions & 0 deletions electron_app/src/originMigrator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
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.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

const { BrowserWindow, ipcMain } = require('electron');
const path = require('path');

async function migrateFromOldOrigin() {
console.log("Attempting to migrate data between origins");

// We can use the same preload script: we just need ipcRenderer exposed
const preloadScript = path.normalize(`${__dirname}/preload.js`);
await new Promise(resolve => {
const migrateWindow = new BrowserWindow({
show: false,
webPreferences: {
preload: preloadScript,
nodeIntegration: false,
sandbox: true,
enableRemoteModule: false,
webgl: false,
},
});
ipcMain.on('origin_migration_complete', (e, success, sentSummary, storedSummary) => {
if (success) {
console.log("Origin migration completed successfully!");
} else {
console.error("Origin migration failed!");
}
console.error("Data sent", sentSummary);
console.error("Data stored", storedSummary);
migrateWindow.close();
resolve();
});
ipcMain.on('origin_migration_nodata', (e) => {
console.log("No session to migrate from old origin");
migrateWindow.close();
resolve();
});
// Normalise the path because in the distribution, __dirname will be inside the
// electron asar.
const sourcePagePath = path.normalize(__dirname + '/../../origin_migrator/source.html');
console.log("Loading path: " + sourcePagePath);
migrateWindow.loadURL('file://' + sourcePagePath);
});
}

module.exports = {
migrateFromOldOrigin,
};
10 changes: 9 additions & 1 deletion electron_app/src/preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,16 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

const { ipcRenderer } = require('electron');
const { ipcRenderer, webFrame } = require('electron');

// expose ipcRenderer to the renderer process
window.ipcRenderer = ipcRenderer;

// Allow the fetch API to load resources from this
// protocol: this is necessary to load olm.wasm.
// (Also mark it a secure although we've already
// done this in the main process).
webFrame.registerURLSchemeAsPrivileged('vector', {
secure: true,
supportFetchAPI: true,
});
19 changes: 19 additions & 0 deletions origin_migrator/dest/browser-matrix.min.js

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions origin_migrator/dest/dest.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<html>
<body>
<script src="browser-matrix.min.js"></script>
<script src="dest.js"></script>
</body>
</html>
125 changes: 125 additions & 0 deletions origin_migrator/dest/dest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
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.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

const SOURCE_ORIGIN = 'file://';

const IndexedDBCryptoStore = window.matrixcs.IndexedDBCryptoStore;
const cryptoStore = new IndexedDBCryptoStore(window.indexedDB, 'matrix-js-sdk:crypto');

let accountStored = 0;
let sessionsStored = 0;
let inboundGroupSessionsStored = 0;
let deviceDataStored = 0;
let roomsStored = 0;
let localStorageKeysStored = 0;

const promises = [];

async function onMessage(e) {
if (e.origin !== SOURCE_ORIGIN) return;

const data = e.data.data; // bleh, naming clash
switch (e.data.cmd) {
case 'init':
// start with clean stores before we migrate data in
window.localStorage.clear();
await cryptoStore.deleteAllData();

e.source.postMessage({
cmd: 'initOK',
}, SOURCE_ORIGIN);
break;
case 'storeAccount':
promises.push(cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
cryptoStore.storeAccount(txn, data);
},
).then(() => {
++accountStored;
}));
break;
case 'storeSessions':
promises.push(cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_SESSIONS],
(txn) => {
for (const sess of data) {
cryptoStore.storeEndToEndSession(sess.deviceKey, sess.sessionId, sess, txn);
}
},
).then(() => {
sessionsStored += data.length;
}));
break;
case 'storeInboundGroupSessions':
promises.push(cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS],
(txn) => {
for (const sess of data) {
cryptoStore.addEndToEndInboundGroupSession(
sess.senderKey, sess.sessionId, sess.sessionData, txn,
);
}
},
).then(() => {
inboundGroupSessionsStored += data.length;
}));
break;
case 'storeDeviceData':
promises.push(cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_DEVICE_DATA],
(txn) => {
cryptoStore.storeEndToEndDeviceData(data, txn);
},
).then(() => {
++deviceDataStored;
}));
break;
case 'storeRooms':
promises.push(cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_ROOMS],
(txn) => {
for (const [roomId, roomInfo] of Object.entries(data)) {
cryptoStore.storeEndToEndRoom(roomId, roomInfo, txn);
}
},
).then(() => {
++roomsStored;
}));
break;
case 'storeLocalStorage':
window.localStorage.setItem(data.key, data.val);
++localStorageKeysStored;
break;
case 'getSummary':
await Promise.all(promises);
e.source.postMessage({
cmd: 'summary',
data: {
accountStored,
sessionsStored,
inboundGroupSessionsStored,
deviceDataStored,
roomsStored,
localStorageKeysStored,
},
}, SOURCE_ORIGIN);
break;
}
}

window.addEventListener('message', onMessage);

7 changes: 7 additions & 0 deletions origin_migrator/source.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<html>
<body>
<script src="dest/browser-matrix.min.js"></script>
<script src="source.js"></script>
<iframe name="dest" src="vector://vector/origin_migrator_dest/dest.html" onload="doMigrate()"></iframe>
</body>
</html>
Loading

0 comments on commit 9e08551

Please sign in to comment.