Skip to content

Commit

Permalink
electron: only allow browser-window
Browse files Browse the repository at this point in the history
Only allow http request from Electron's own browser-window. Token is
generated within electron-main, which also sets it as a cookie within
browser-windows. Token is passed to the backend via environment
variables. The backend is looking for this specific token to authorize
requests.

Fixes https://bugs.eclipse.org/bugs/show_bug.cgi?id=551747

Signed-off-by: Paul Maréchal <paul.marechal@ericsson.com>
  • Loading branch information
paul-marechal committed Feb 25, 2020
1 parent 4d4f6c2 commit 2af89f6
Show file tree
Hide file tree
Showing 11 changed files with 269 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ function start() {
themeService.loadUserTheme();
const application = container.get(FrontendApplication);
application.start();
return application.start();
}
module.exports = Promise.resolve()${this.compileFrontendModuleImports(frontendModules)}
Expand Down Expand Up @@ -129,10 +129,12 @@ if (process.env.LC_ALL) {
}
process.env.LC_NUMERIC = 'C';
const uuid = require('uuid');
const electron = require('electron');
const { join, resolve } = require('path');
const { fork } = require('child_process');
const { app, dialog, shell, BrowserWindow, ipcMain, Menu, globalShortcut } = electron;
const { ElectronSecurityToken } = require('@theia/core/lib/electron-common/electron-token');
const applicationName = \`${this.pck.props.frontend.config.applicationName}\`;
const isSingleInstance = ${this.pck.props.backend.config.singleInstance === true ? 'true' : 'false'};
Expand All @@ -148,6 +150,10 @@ const nativeKeymap = require('native-keymap');
const Storage = require('electron-store');
const electronStore = new Storage();
const electronSecurityToken = {
value: uuid.v4(),
};
app.on('ready', () => {
if (disallowReloadKeybinding) {
Expand Down Expand Up @@ -300,7 +306,17 @@ app.on('ready', () => {
const loadMainWindow = (port) => {
if (!mainWindow.isDestroyed()) {
mainWindow.loadURL('file://' + join(__dirname, '../../lib/index.html') + '?port=' + port);
mainWindow.webContents.session.cookies.set({
url: \`http://localhost:\${port}/\`,
name: ElectronSecurityToken,
value: JSON.stringify(electronSecurityToken),
}, error => {
if (error) {
console.error(error);
} else {
mainWindow.loadURL('file://' + join(__dirname, '../../lib/index.html') + '?port=' + port);
}
});
}
};
Expand All @@ -322,14 +338,17 @@ app.on('ready', () => {
// We need to distinguish between bundled application and development mode when starting the clusters.
// See: https://github.com/electron/electron/issues/6337#issuecomment-230183287
if (devMode) {
process.env[ElectronSecurityToken] = JSON.stringify(electronSecurityToken);
require(mainPath).then(address => {
loadMainWindow(address.port);
}).catch((error) => {
console.error(error);
app.exit(1);
});
} else {
const cp = fork(mainPath, [], { env: Object.assign({}, process.env) });
const cp = fork(mainPath, [], { env: Object.assign({
[ElectronSecurityToken]: JSON.stringify(electronSecurityToken),
}, process.env) });
cp.on('message', (address) => {
loadMainWindow(address.port);
});
Expand Down
5 changes: 5 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"@primer/octicons-react": "^9.0.0",
"@theia/application-package": "^0.15.0",
"@types/body-parser": "^1.16.4",
"@types/cookie": "^0.3.3",
"@types/express": "^4.16.0",
"@types/fs-extra": "^4.0.2",
"@types/lodash.debounce": "4.0.3",
Expand All @@ -22,6 +23,7 @@
"@types/yargs": "^11.1.0",
"ajv": "^6.5.3",
"body-parser": "^1.17.2",
"cookie": "^0.4.0",
"es6-promise": "^4.2.4",
"express": "^4.16.3",
"file-icons-js": "^1.0.3",
Expand Down Expand Up @@ -61,6 +63,9 @@
"frontend": "lib/browser/keyboard/browser-keyboard-module",
"frontendElectron": "lib/electron-browser/keyboard/electron-keyboard-module",
"backendElectron": "lib/electron-node/keyboard/electron-backend-keyboard-module"
},
{
"backendElectron": "lib/electron-node/token/electron-token-backend-module"
}
],
"keywords": [
Expand Down
26 changes: 26 additions & 0 deletions packages/core/src/electron-common/electron-token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/********************************************************************************
* Copyright (C) 2020 Ericsson and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

/**
* This token is unique the the current running instance. It is used by the backend
* to make sure it is an electron browser window that is connecting to its services.
*
* The identifier is a string, which makes it usable as a key for cookies or similar.
*/
export const ElectronSecurityToken = 'x-theia-electron-token';
export interface ElectronSecurityToken {
value: string;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/********************************************************************************
* Copyright (C) 2020 Ericsson and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import express = require('express');
import { injectable, inject } from 'inversify';
import { BackendApplicationContribution } from '../../node';
import { ElectronTokenValidator } from './electron-token-validator';

/**
* This component contributes a middleware that will refuse all requests that do not include a specific token.
*/
@injectable()
export class ElectronTokenBackendContribution implements BackendApplicationContribution {

@inject(ElectronTokenValidator)
protected readonly tokenValidator: ElectronTokenValidator;

configure(app: express.Application): void {
app.use(this.expressMiddleware.bind(this));
}

/**
* Only allow token-bearers.
*/
protected expressMiddleware(req: express.Request, res: express.Response, next: express.NextFunction): void {
if (this.tokenValidator.allowRequest(req)) {
next();
} else {
console.error(`refused an http request: ${req.connection.remoteAddress}`);
res.sendStatus(403);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/********************************************************************************
* Copyright (C) 2020 Ericsson and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { ContainerModule } from 'inversify';
import { BackendApplicationContribution, MessagingService } from '../../node';
import { MessagingContribution } from '../../node/messaging/messaging-contribution';
import { ElectronSecurityToken } from '../../electron-common/electron-token';
import { ElectronTokenBackendContribution } from './electron-token-backend-contribution';
import { ElectronMessagingContribution } from './electron-token-messaging-contribution';
import { ElectronTokenValidator } from './electron-token-validator';

export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind<Promise<ElectronSecurityToken>>(Promise).toDynamicValue(async () =>
JSON.parse(process.env[ElectronSecurityToken]!)
).inSingletonScope().whenTargetNamed(ElectronSecurityToken);

bind<ElectronTokenValidator>(ElectronTokenValidator).toSelf().inSingletonScope();
bind<ElectronTokenBackendContribution>(ElectronTokenBackendContribution).toSelf().inSingletonScope();
for (const contribution of [ElectronTokenBackendContribution, ElectronTokenValidator]) {
bind<BackendApplicationContribution>(BackendApplicationContribution).toService(contribution);
}

rebind<MessagingContribution>(MessagingService.Identifier).to(ElectronMessagingContribution).inSingletonScope();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/********************************************************************************
* Copyright (C) 2020 Ericsson and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import * as net from 'net';
import * as http from 'http';
import { injectable, inject } from 'inversify';
import { MessagingContribution } from '../../node/messaging/messaging-contribution';
import { ElectronTokenValidator } from './electron-token-validator';

/**
* Override the browser MessagingContribution class to refuse connections that do not include a specific token.
*/
@injectable()
export class ElectronMessagingContribution extends MessagingContribution {

@inject(ElectronTokenValidator)
protected readonly tokenValidator: ElectronTokenValidator;

protected handleHttpUpgrade(request: http.IncomingMessage, socket: net.Socket, head: Buffer): void {
if (this.tokenValidator.allowRequest(request)) {
super.handleHttpUpgrade(request, socket, head);
} else {
console.error(`refused a websocket connection: ${request.connection.remoteAddress}`);
socket.destroy(); // kill connection, client will take that as a "no".
}
}

}
65 changes: 65 additions & 0 deletions packages/core/src/electron-node/token/electron-token-validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/********************************************************************************
* Copyright (C) 2020 Ericsson and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import * as http from 'http';
import * as cookie from 'cookie';
import { injectable, inject, named } from 'inversify';
import { ElectronSecurityToken } from '../../electron-common/electron-token';
import { BackendApplicationContribution } from '../../node';

/**
* On Electron, we want to make sure that only Electron's browser-windows access the backend services.
*/
@injectable()
export class ElectronTokenValidator implements BackendApplicationContribution {

@inject(Promise) @named(ElectronSecurityToken)
protected readonly electronSecurityTokenPromise: Promise<ElectronSecurityToken>;

/**
* `electronSecurityToken` can be undefined while the value is being resolved.
*/
protected electronSecurityToken: ElectronSecurityToken | undefined;

async onStart(): Promise<void> {
this.electronSecurityToken = await this.electronSecurityTokenPromise;
}

allowRequest(request: http.IncomingMessage): boolean {
return this.isTokenValid(this.extractTokenFromRequest(request));
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
isTokenValid(token: any): boolean {
return typeof token === 'object' && token.value === this.electronSecurityToken!.value;
}

/**
* Expects the token to be passed via cookies by default.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected extractTokenFromRequest(request: http.IncomingMessage): any {
const cookieHeader = request.headers.cookie;
if (typeof cookieHeader === 'string') {
const token = cookie.parse(cookieHeader)[ElectronSecurityToken];
if (typeof token === 'string') {
return JSON.parse(token);
}
}
return undefined;
}

}
4 changes: 2 additions & 2 deletions packages/core/src/node/messaging/messaging-backend-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ import { MessagingService } from './messaging-service';
export const messagingBackendModule = new ContainerModule(bind => {
bindContributionProvider(bind, ConnectionContainerModule);
bindContributionProvider(bind, MessagingService.Contribution);
bind(MessagingService.Identifier).to(MessagingContribution).inSingletonScope();
bind(MessagingContribution).toDynamicValue(({ container }) => {
const child = container.createChild();
child.bind(MessagingContainer).toConstantValue(container);
child.bind(MessagingContribution).toSelf();
return child.get(MessagingContribution);
return child.get(MessagingService.Identifier);
}).inSingletonScope();
bind(BackendApplicationContribution).toService(MessagingContribution);
});
20 changes: 16 additions & 4 deletions packages/core/src/node/messaging/messaging-contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import * as ws from 'ws';
import * as url from 'url';
import * as net from 'net';
import * as http from 'http';
import * as https from 'https';
import { injectable, inject, named, postConstruct, interfaces } from 'inversify';
Expand Down Expand Up @@ -46,6 +47,7 @@ export class MessagingContribution implements BackendApplicationContribution, Me
@inject(ContributionProvider) @named(MessagingService.Contribution)
protected readonly contributions: ContributionProvider<MessagingService.Contribution>;

protected webSocketServer: ws.Server | undefined;
protected readonly wsHandlers = new MessagingContribution.ConnectionHandlers<ws>();
protected readonly channelHandlers = new MessagingContribution.ConnectionHandlers<WebSocketChannel>();

Expand Down Expand Up @@ -81,23 +83,24 @@ export class MessagingContribution implements BackendApplicationContribution, Me

protected checkAliveTimeout = 30000;
onStart(server: http.Server | https.Server): void {
const wss = new ws.Server({
server,
this.webSocketServer = new ws.Server({
noServer: true,
perMessageDeflate: {
// don't compress if a message is less than 256kb
threshold: 256 * 1024
}
});
server.on('upgrade', this.handleHttpUpgrade.bind(this));
interface CheckAliveWS extends ws {
alive: boolean;
}
wss.on('connection', (socket: CheckAliveWS, request) => {
this.webSocketServer.on('connection', (socket: CheckAliveWS, request) => {
socket.alive = true;
socket.on('pong', () => socket.alive = true);
this.handleConnection(socket, request);
});
setInterval(() => {
wss.clients.forEach((socket: CheckAliveWS) => {
this.webSocketServer!.clients.forEach((socket: CheckAliveWS) => {
if (socket.alive === false) {
socket.terminate();
return;
Expand All @@ -108,6 +111,15 @@ export class MessagingContribution implements BackendApplicationContribution, Me
}, this.checkAliveTimeout);
}

/**
* Route HTTP upgrade requests to the WebSocket server.
*/
protected handleHttpUpgrade(request: http.IncomingMessage, socket: net.Socket, head: Buffer): void {
this.webSocketServer!.handleUpgrade(request, socket, head, client => {
this.webSocketServer!.emit('connection', client, request);
});
}

protected handleConnection(socket: ws, request: http.IncomingMessage): void {
const pathname = request.url && url.parse(request.url).pathname;
if (pathname && !this.wsHandlers.route(pathname, socket)) {
Expand Down
Loading

0 comments on commit 2af89f6

Please sign in to comment.