diff --git a/dev-packages/application-manager/src/generator/frontend-generator.ts b/dev-packages/application-manager/src/generator/frontend-generator.ts index c386866d803f3..6556fa4d3fc88 100644 --- a/dev-packages/application-manager/src/generator/frontend-generator.ts +++ b/dev-packages/application-manager/src/generator/frontend-generator.ts @@ -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)} @@ -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'}; @@ -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) { @@ -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); + } + }); } }; @@ -322,6 +338,7 @@ 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) => { @@ -329,7 +346,9 @@ app.on('ready', () => { 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); }); diff --git a/packages/core/package.json b/packages/core/package.json index fd784c7afe611..4d0016d4574d6 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -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", @@ -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", @@ -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": [ diff --git a/packages/core/src/browser/messaging/ws-connection-provider.ts b/packages/core/src/browser/messaging/ws-connection-provider.ts index d2a9b8a881444..40a5bb58c2aac 100644 --- a/packages/core/src/browser/messaging/ws-connection-provider.ts +++ b/packages/core/src/browser/messaging/ws-connection-provider.ts @@ -77,7 +77,7 @@ export class WebSocketConnectionProvider { } this.onIncomingMessageActivityEmitter.fire(undefined); }; - this.socket = socket; + (this.socket as ReconnectingWebSocket) = socket; } /** diff --git a/packages/core/src/electron-common/electron-token.ts b/packages/core/src/electron-common/electron-token.ts new file mode 100644 index 0000000000000..4ebde485a14a7 --- /dev/null +++ b/packages/core/src/electron-common/electron-token.ts @@ -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; +}; diff --git a/packages/core/src/electron-node/token/electron-token-backend-contribution.ts b/packages/core/src/electron-node/token/electron-token-backend-contribution.ts new file mode 100644 index 0000000000000..67a74e4806ada --- /dev/null +++ b/packages/core/src/electron-node/token/electron-token-backend-contribution.ts @@ -0,0 +1,74 @@ +/******************************************************************************** + * 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 ws from 'ws'; +import * as http from 'http'; +import express = require('express'); +import { injectable, inject } from 'inversify'; +import { BackendApplicationContribution } from '../../node'; +import { MessagingContribution } from '../../node/messaging/messaging-contribution'; +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); + } + } + +} + +/** + * 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; + + /** + * The Browser API doesn't allow us to define custom headers. + * Then, in order to authorize a connect, we will expect the first message to contain a token. + */ + protected handleConnection(socket: ws, request: http.IncomingMessage): void { + if (this.tokenValidator.allowRequest(request)) { + super.handleConnection(socket, request); + } else { + console.error(`refused a websocket connection: ${request.connection.remoteAddress}`); + socket.close(); + } + } + +} diff --git a/packages/core/src/electron-node/token/electron-token-backend-module.ts b/packages/core/src/electron-node/token/electron-token-backend-module.ts new file mode 100644 index 0000000000000..0134ef2ebecb2 --- /dev/null +++ b/packages/core/src/electron-node/token/electron-token-backend-module.ts @@ -0,0 +1,36 @@ +/******************************************************************************** + * 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 { MessagingContribution } from '../../node/messaging/messaging-contribution'; +import { ElectronSecurityToken } from '../../electron-common/electron-token'; +import { ElectronMessagingContribution, ElectronTokenBackendContribution } from './electron-token-backend-contribution'; +import { BackendApplicationContribution, MessagingService } from '../../node'; +import { ElectronTokenValidator } from './electron-token-validator'; + +export default new ContainerModule((bind, unbind, isBound, rebind) => { + bind>(Promise).toDynamicValue(async () => + JSON.parse(process.env[ElectronSecurityToken]!) + ).inSingletonScope().whenTargetNamed(ElectronSecurityToken); + + bind(ElectronTokenValidator).toSelf().inSingletonScope(); + bind(ElectronTokenBackendContribution).toSelf().inSingletonScope(); + for (const contribution of [ElectronTokenBackendContribution, ElectronTokenValidator]) { + bind(BackendApplicationContribution).toService(contribution); + } + + rebind(MessagingService.Identifier).to(ElectronMessagingContribution).inSingletonScope(); +}); diff --git a/packages/core/src/electron-node/token/electron-token-validator.ts b/packages/core/src/electron-node/token/electron-token-validator.ts new file mode 100644 index 0000000000000..c1a4158f91af8 --- /dev/null +++ b/packages/core/src/electron-node/token/electron-token-validator.ts @@ -0,0 +1,66 @@ +/******************************************************************************** + * 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 windows access the backend services. + */ +@injectable() +export class ElectronTokenValidator implements BackendApplicationContribution { + + @inject(Promise) @named(ElectronSecurityToken) + protected readonly electronSecurityTokenPromise: Promise; + + /** + * `electronSecurityToken` can be undefined while the value is being resolved. + */ + protected electronSecurityToken: ElectronSecurityToken | undefined; + + async onStart(): Promise { + this.electronSecurityToken = await this.electronSecurityTokenPromise; + } + + allowRequest(request: http.IncomingMessage): boolean { + const token = this.extractTokenFromRequest(request); + return typeof token !== 'undefined' && this.isTokenValid(token); + } + + // 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; + } + +} diff --git a/packages/core/src/node/messaging/messaging-backend-module.ts b/packages/core/src/node/messaging/messaging-backend-module.ts index e0edd7fd854d8..99ccf7d1fb245 100644 --- a/packages/core/src/node/messaging/messaging-backend-module.ts +++ b/packages/core/src/node/messaging/messaging-backend-module.ts @@ -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); }); diff --git a/packages/core/src/node/messaging/messaging-service.ts b/packages/core/src/node/messaging/messaging-service.ts index 919cc505f576f..68dd461abe828 100644 --- a/packages/core/src/node/messaging/messaging-service.ts +++ b/packages/core/src/node/messaging/messaging-service.ts @@ -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 } diff --git a/yarn.lock b/yarn.lock index 92fc8d6638686..7780ca583e36e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1098,6 +1098,11 @@ dependencies: "@types/node" "*" +"@types/cookie@^0.3.3": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.3.3.tgz#85bc74ba782fb7aa3a514d11767832b0e3bc6803" + integrity sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow== + "@types/decompress@^4.2.2": version "4.2.3" resolved "https://registry.yarnpkg.com/@types/decompress/-/decompress-4.2.3.tgz#98eed48af80001038aa05690b2094915f296fe65" @@ -4152,7 +4157,7 @@ cookie-signature@1.0.6: resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= -cookie@0.4.0: +cookie@0.4.0, cookie@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==