Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

electron: only allow browser-window #7205

Merged
merged 1 commit into from
Feb 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
akosyakov marked this conversation as resolved.
Show resolved Hide resolved
}
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,48 @@
/********************************************************************************
* 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 an Express 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;
paul-marechal marked this conversation as resolved.
Show resolved Hide resolved

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,31 @@
/********************************************************************************
* 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 { 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<ElectronTokenValidator>(ElectronTokenValidator).toSelf().inSingletonScope();

bind<ElectronTokenBackendContribution>(ElectronTokenBackendContribution).toSelf().inSingletonScope();
bind<BackendApplicationContribution>(BackendApplicationContribution).toService(ElectronTokenBackendContribution);

rebind<MessagingContribution>(MessagingService.Identifier).to(ElectronMessagingContribution).inSingletonScope();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/********************************************************************************
* 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;

/**
* Only allow token-bearers.
*/
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".
}
}

}
55 changes: 55 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,55 @@
/********************************************************************************
* 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 } from 'inversify';
import { ElectronSecurityToken } from '../../electron-common/electron-token';

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

protected electronSecurityToken: ElectronSecurityToken = this.getToken();

/**
* Expects the token to be passed via cookies by default.
*/
allowRequest(request: http.IncomingMessage): boolean {
const cookieHeader = request.headers.cookie;
if (typeof cookieHeader === 'string') {
const token = cookie.parse(cookieHeader)[ElectronSecurityToken];
if (typeof token === 'string') {
return this.isTokenValid(JSON.parse(token));
}
}
return false;
}

isTokenValid(token: ElectronSecurityToken): boolean {
return typeof token === 'object' && token.value === this.electronSecurityToken!.value;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer a constant-time comparison algorithm here to prevent timing attacks (e.g. crypto.timingSafeEqual https://nodejs.org/api/crypto.html#crypto_crypto_timingsafeequal_a_b)

}

/**
* Returns the token to compare to when authorizing requests.
*/
protected getToken(): ElectronSecurityToken {
return JSON.parse(process.env[ElectronSecurityToken]!);
}

}
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));
akosyakov marked this conversation as resolved.
Show resolved Hide resolved
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
2 changes: 2 additions & 0 deletions packages/core/src/node/messaging/messaging-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ export interface MessagingService {
ws(path: string, callback: (params: MessagingService.PathParams, socket: ws) => void): void;
}
export namespace MessagingService {
/** Inversify container identifier for the `MessagingService` component. */
export const Identifier = Symbol('MessagingService');
export interface PathParams {
[name: string]: string
}
Expand Down
Loading