From d5190cbac6465080b79730ab0ad5cb6adf4f3b30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20Mar=C3=A9chal?= Date: Thu, 12 Nov 2020 17:25:45 -0500 Subject: [PATCH] core,mini-browser: server enhancements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # mini-browser: serve on separate origin The mini-browser currently hosts its services on the same origin than Theia's main origin. This commit makes the `mini-browser` serve on its own origin: `mini-browser.{{hostname}}` by default. Can be configured with a `THEIA_MINI_BROWSER_HOST_PATTERN` environment variable. # core: validate ws upgrade origin Hosting the `mini-browser` on its own origin prevents cross-origin requests from happening easily, but WebSockets don't benefit from the same protection. We need to allow/disallow upgrade requests in the backend application ourselves. This means that in order to know who to refuse, we need to know where we are hosted. This is done by specifying the `THEIA_HOSTS` environment variable. If left empty, no check will be done. But if set, nothing besides what is written in this var will be allowed. See `BackendApplicationHosts` to get the hosts extracted from this var. Note that the latter is not set when running Electron, since there's no need to deal with arbitrary origins, everything should be local. No check is done on the HTTP(s) endpoints because we'll rely on the browser's SOP and CORS policies to take effect. A new contribution point is added: `WsRequestValidatorContribution`. An extender can implement this contribution to allow or not WebSocket connections. Internally used to filter origins and Electron's token as well as checking the origin of requests to prevent some type of XSS. Signed-off-by: Paul Maréchal --- CHANGELOG.md | 1 + packages/core/README.md | 10 ++++ packages/core/package.json | 4 ++ .../electron-main-application-module.ts | 2 + .../electron-main-application.ts | 31 +++++----- .../electron-security-token-service.ts | 35 ++++++++++++ .../electron-backend-hosting-module.ts | 24 ++++++++ .../hosting/electron-ws-origin-validator.ts | 29 ++++++++++ .../token/electron-token-backend-module.ts | 15 ++--- .../electron-token-messaging-contribution.ts | 1 + .../token/electron-token-validator.ts | 17 +++++- .../src/node/backend-application-module.ts | 4 ++ .../node/hosting/backend-application-hosts.ts | 57 +++++++++++++++++++ .../node/hosting/backend-hosting-module.ts | 26 +++++++++ .../src/node/hosting/ws-origin-validator.ts | 36 ++++++++++++ .../node/messaging/messaging-contribution.ts | 21 ++++++- .../core/src/node/ws-request-validators.ts | 56 ++++++++++++++++++ packages/mini-browser/README.md | 7 +++ packages/mini-browser/compile.tsconfig.json | 3 +- packages/mini-browser/package.json | 6 +- .../src/browser/mini-browser-content.ts | 16 +++++- .../src/browser/mini-browser-environment.ts | 43 ++++++++++++++ .../browser/mini-browser-frontend-module.ts | 2 + .../src/browser/mini-browser-open-handler.ts | 10 ++-- .../src/common/mini-browser-endpoint.ts | 26 +++++++++ ...mini-browser-electron-main-contribution.ts | 42 ++++++++++++++ .../mini-browser-electron-main-module.ts | 24 ++++++++ .../src/node/mini-browser-backend-module.ts | 4 ++ .../src/node/mini-browser-endpoint.ts | 27 +++++++-- .../src/node/mini-browser-ws-validator.ts | 46 +++++++++++++++ .../main/node/plugin-ext-backend-module.ts | 2 + .../src/main/node/plugin-service.ts | 39 ++++++++++--- 32 files changed, 613 insertions(+), 53 deletions(-) create mode 100644 packages/core/src/electron-main/electron-security-token-service.ts create mode 100644 packages/core/src/electron-node/hosting/electron-backend-hosting-module.ts create mode 100644 packages/core/src/electron-node/hosting/electron-ws-origin-validator.ts create mode 100644 packages/core/src/node/hosting/backend-application-hosts.ts create mode 100644 packages/core/src/node/hosting/backend-hosting-module.ts create mode 100644 packages/core/src/node/hosting/ws-origin-validator.ts create mode 100644 packages/core/src/node/ws-request-validators.ts create mode 100644 packages/mini-browser/src/browser/mini-browser-environment.ts create mode 100644 packages/mini-browser/src/common/mini-browser-endpoint.ts create mode 100644 packages/mini-browser/src/electron-main/mini-browser-electron-main-contribution.ts create mode 100644 packages/mini-browser/src/electron-main/mini-browser-electron-main-module.ts create mode 100644 packages/mini-browser/src/node/mini-browser-ws-validator.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 4716142a42989..5f8bf3a146fd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - [file-search] Deprecate dependency on `@theia/process` and replaced its usage by node's `child_process` api. - [electron] Removed `attachWillPreventUnload` method from the Electron main application. The `confirmExit` logic is handled on the frontend. [#8732](https://github.com/eclipse-theia/theia/pull/8732) +- [core] Deprecated `ElectronMessagingContribution`, token validation is now done in `ElectronTokenValidator` as a `WsRequestValidatorContribution`. ## v1.7.0 - 29/10/2020 diff --git a/packages/core/README.md b/packages/core/README.md index 46bc11cad4362..3634d2e6d342b 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -84,6 +84,16 @@ root INFO [nsfw-watcher: 10734] Started watching: /Users/captain.future/git/thei ``` Where `root` is the name of the logger and `INFO` is the log level. These are optionally followed by the name of a child process and the process ID. +## Environment Variables + +- `THEIA_HOSTS` + - A comma-separated list of hosts expected to resolve to the current application. + - e.g: `theia.app.com,some.other.domain:3000` + - The port number is important if your application is not hosted on either `80` or `443`. + - If possible, you should set this environment variable: + - When not set, Theia will allow any origin to access the WebSocket services. + - When set, Theia will only allow the origins defined in this environment variable. + ## Additional Information - [API documentation for `@theia/core`](https://eclipse-theia.github.io/theia/docs/next/modules/core.html) diff --git a/packages/core/package.json b/packages/core/package.json index ae30aa6771f18..f1dab92898f8d 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -74,6 +74,10 @@ { "frontendElectron": "lib/electron-browser/token/electron-token-frontend-module", "backendElectron": "lib/electron-node/token/electron-token-backend-module" + }, + { + "backend": "lib/node/hosting/backend-hosting-module", + "backendElectron": "lib/electron-node/hosting/electron-backend-hosting-module" } ], "keywords": [ diff --git a/packages/core/src/electron-main/electron-main-application-module.ts b/packages/core/src/electron-main/electron-main-application-module.ts index 6590fddc2eb45..eb9db9abef77f 100644 --- a/packages/core/src/electron-main/electron-main-application-module.ts +++ b/packages/core/src/electron-main/electron-main-application-module.ts @@ -25,6 +25,7 @@ import { ElectronMainWindowServiceImpl } from './electron-main-window-service-im import { ElectronMessagingContribution } from './messaging/electron-messaging-contribution'; import { ElectronMessagingService } from './messaging/electron-messaging-service'; import { ElectronConnectionHandler } from '../electron-common/messaging/electron-connection-handler'; +import { ElectronSecurityTokenService } from './electron-security-token-service'; const electronSecurityToken: ElectronSecurityToken = { value: v4() }; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -34,6 +35,7 @@ export default new ContainerModule(bind => { bind(ElectronMainApplication).toSelf().inSingletonScope(); bind(ElectronMessagingContribution).toSelf().inSingletonScope(); bind(ElectronSecurityToken).toConstantValue(electronSecurityToken); + bind(ElectronSecurityTokenService).toSelf().inSingletonScope(); bindContributionProvider(bind, ElectronConnectionHandler); bindContributionProvider(bind, ElectronMessagingService.Contribution); diff --git a/packages/core/src/electron-main/electron-main-application.ts b/packages/core/src/electron-main/electron-main-application.ts index dc7304c35c8e8..30ccd59777bdf 100644 --- a/packages/core/src/electron-main/electron-main-application.ts +++ b/packages/core/src/electron-main/electron-main-application.ts @@ -15,7 +15,7 @@ ********************************************************************************/ import { inject, injectable, named } from 'inversify'; -import { session, screen, globalShortcut, app, BrowserWindow, BrowserWindowConstructorOptions, Event as ElectronEvent } from 'electron'; +import { screen, globalShortcut, app, BrowserWindow, BrowserWindowConstructorOptions, Event as ElectronEvent } from 'electron'; import * as path from 'path'; import { Argv } from 'yargs'; import { AddressInfo } from 'net'; @@ -27,6 +27,7 @@ import { FileUri } from '../node/file-uri'; import { Deferred } from '../common/promise-util'; import { MaybePromise } from '../common/types'; import { ContributionProvider } from '../common/contribution-provider'; +import { ElectronSecurityTokenService } from './electron-security-token-service'; import { ElectronSecurityToken } from '../electron-common/electron-token'; const Storage = require('electron-store'); const createYargs: (argv?: string[], cwd?: string) => Argv = require('yargs/yargs'); @@ -162,16 +163,21 @@ export class ElectronMainApplication { @inject(ElectronMainApplicationGlobals) protected readonly globals: ElectronMainApplicationGlobals; - @inject(ElectronSecurityToken) - protected electronSecurityToken: ElectronSecurityToken; - @inject(ElectronMainProcessArgv) protected processArgv: ElectronMainProcessArgv; + @inject(ElectronSecurityTokenService) + protected electronSecurityTokenService: ElectronSecurityTokenService; + + @inject(ElectronSecurityToken) + protected readonly electronSecurityToken: ElectronSecurityToken; + protected readonly electronStore = new Storage(); - protected readonly backendPort = new Deferred(); - protected _config: FrontendApplicationConfig; + protected readonly _backendPort = new Deferred(); + readonly backendPort = this._backendPort.promise; + + protected _config: FrontendApplicationConfig | undefined; get config(): FrontendApplicationConfig { if (!this._config) { throw new Error('You have to start the application first.'); @@ -183,7 +189,7 @@ export class ElectronMainApplication { this._config = config; this.hookApplicationEvents(); const port = await this.startBackend(); - this.backendPort.resolve(port); + this._backendPort.resolve(port); await app.whenReady(); await this.attachElectronSecurityToken(port); await this.startContributions(); @@ -279,8 +285,8 @@ export class ElectronMainApplication { } protected async createWindowUri(): Promise { - const port = await this.backendPort.promise; - return FileUri.create(this.globals.THEIA_FRONTEND_HTML_PATH).withQuery(`port=${port}`); + return FileUri.create(this.globals.THEIA_FRONTEND_HTML_PATH) + .withQuery(`port=${await this.backendPort}`); } protected getDefaultWindowState(): BrowserWindowConstructorOptions { @@ -418,12 +424,7 @@ export class ElectronMainApplication { } protected async attachElectronSecurityToken(port: number): Promise { - session.defaultSession.cookies.set({ - url: `http://localhost:${port}/`, - name: ElectronSecurityToken, - value: JSON.stringify(this.electronSecurityToken), - httpOnly: true - }); + await this.electronSecurityTokenService.setElectronSecurityTokenCookie(`http://localhost:${port}`); } protected hookApplicationEvents(): void { diff --git a/packages/core/src/electron-main/electron-security-token-service.ts b/packages/core/src/electron-main/electron-security-token-service.ts new file mode 100644 index 0000000000000..534de8fd993bc --- /dev/null +++ b/packages/core/src/electron-main/electron-security-token-service.ts @@ -0,0 +1,35 @@ +/******************************************************************************** + * 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 { session } from 'electron'; +import { inject, injectable } from 'inversify'; +import { ElectronSecurityToken } from '../electron-common/electron-token'; + +@injectable() +export class ElectronSecurityTokenService { + + @inject(ElectronSecurityToken) + protected readonly electronSecurityToken: ElectronSecurityToken; + + async setElectronSecurityTokenCookie(url: string): Promise { + await session.defaultSession.cookies.set({ + url, + name: ElectronSecurityToken, + value: JSON.stringify(this.electronSecurityToken), + httpOnly: true + }); + } +} diff --git a/packages/core/src/electron-node/hosting/electron-backend-hosting-module.ts b/packages/core/src/electron-node/hosting/electron-backend-hosting-module.ts new file mode 100644 index 0000000000000..e033512968e5e --- /dev/null +++ b/packages/core/src/electron-node/hosting/electron-backend-hosting-module.ts @@ -0,0 +1,24 @@ +/******************************************************************************** + * 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 { WsRequestValidatorContribution } from '../../node/ws-request-validators'; +import { ElectronWsOriginValidator } from './electron-ws-origin-validator'; + +export default new ContainerModule(bind => { + bind(ElectronWsOriginValidator).toSelf().inSingletonScope(); + bind(WsRequestValidatorContribution).toService(ElectronWsOriginValidator); +}); diff --git a/packages/core/src/electron-node/hosting/electron-ws-origin-validator.ts b/packages/core/src/electron-node/hosting/electron-ws-origin-validator.ts new file mode 100644 index 0000000000000..5fe8987c8d85f --- /dev/null +++ b/packages/core/src/electron-node/hosting/electron-ws-origin-validator.ts @@ -0,0 +1,29 @@ +/******************************************************************************** + * 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 { injectable } from 'inversify'; +import { WsRequestValidatorContribution } from '../../node/ws-request-validators'; + +@injectable() +export class ElectronWsOriginValidator implements WsRequestValidatorContribution { + + allowWsUpgrade(request: http.IncomingMessage): boolean { + // On Electron the main page is served from the `file` protocol. + // We don't expect the requests to come from anywhere else. + return request.headers.origin === 'file://'; + } +} 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 index 073d9e12703e9..1850f6b4c0be8 100644 --- a/packages/core/src/electron-node/token/electron-token-backend-module.ts +++ b/packages/core/src/electron-node/token/electron-token-backend-module.ts @@ -15,17 +15,14 @@ ********************************************************************************/ import { ContainerModule } from 'inversify'; -import { BackendApplicationContribution, MessagingService } from '../../node'; -import { MessagingContribution } from '../../node/messaging/messaging-contribution'; +import { BackendApplicationContribution } from '../../node'; +import { WsRequestValidatorContribution } from '../../node/ws-request-validators'; 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).toSelf().inSingletonScope(); - - bind(ElectronTokenBackendContribution).toSelf().inSingletonScope(); - bind(BackendApplicationContribution).toService(ElectronTokenBackendContribution); - - rebind(MessagingService.Identifier).to(ElectronMessagingContribution).inSingletonScope(); + bind(ElectronTokenBackendContribution).toSelf().inSingletonScope(); + bind(BackendApplicationContribution).toService(ElectronTokenBackendContribution); + bind(ElectronTokenValidator).toSelf().inSingletonScope(); + bind(WsRequestValidatorContribution).toService(ElectronTokenValidator); }); diff --git a/packages/core/src/electron-node/token/electron-token-messaging-contribution.ts b/packages/core/src/electron-node/token/electron-token-messaging-contribution.ts index 1e28440d2ce53..97876c46469fa 100644 --- a/packages/core/src/electron-node/token/electron-token-messaging-contribution.ts +++ b/packages/core/src/electron-node/token/electron-token-messaging-contribution.ts @@ -22,6 +22,7 @@ import { ElectronTokenValidator } from './electron-token-validator'; /** * Override the browser MessagingContribution class to refuse connections that do not include a specific token. + * @deprecated since 1.8.0 */ @injectable() export class ElectronMessagingContribution extends MessagingContribution { diff --git a/packages/core/src/electron-node/token/electron-token-validator.ts b/packages/core/src/electron-node/token/electron-token-validator.ts index 20f69aceb5235..3fd9d4d9c6623 100644 --- a/packages/core/src/electron-node/token/electron-token-validator.ts +++ b/packages/core/src/electron-node/token/electron-token-validator.ts @@ -17,16 +17,27 @@ import * as http from 'http'; import * as cookie from 'cookie'; import * as crypto from 'crypto'; -import { injectable } from 'inversify'; +import { injectable, postConstruct } from 'inversify'; +import { MaybePromise } from '../../common'; import { ElectronSecurityToken } from '../../electron-common/electron-token'; +import { WsRequestValidatorContribution } from '../../node/ws-request-validators'; /** * On Electron, we want to make sure that only Electron's browser-windows access the backend services. */ @injectable() -export class ElectronTokenValidator { +export class ElectronTokenValidator implements WsRequestValidatorContribution { - protected electronSecurityToken: ElectronSecurityToken = this.getToken(); + protected electronSecurityToken: ElectronSecurityToken; + + @postConstruct() + protected postConstruct(): void { + this.electronSecurityToken = this.getToken(); + } + + allowWsUpgrade(request: http.IncomingMessage): MaybePromise { + return this.allowRequest(request); + } /** * Expects the token to be passed via cookies by default. diff --git a/packages/core/src/node/backend-application-module.ts b/packages/core/src/node/backend-application-module.ts index 0a2b36edb0376..41d43532205d6 100644 --- a/packages/core/src/node/backend-application-module.ts +++ b/packages/core/src/node/backend-application-module.ts @@ -29,6 +29,7 @@ import { EnvVariablesServer, envVariablesPath } from './../common/env-variables' import { EnvVariablesServerImpl } from './env-variables'; import { ConnectionContainerModule } from './messaging/connection-container-module'; import { QuickPickService, quickPickServicePath } from '../common/quick-pick-service'; +import { WsRequestValidator, WsRequestValidatorContribution } from './ws-request-validators'; decorate(injectable(), ApplicationPackage); @@ -81,4 +82,7 @@ export const backendApplicationModule = new ContainerModule(bind => { const { projectPath } = container.get(BackendApplicationCliContribution); return new ApplicationPackage({ projectPath }); }).inSingletonScope(); + + bind(WsRequestValidator).toSelf().inSingletonScope(); + bindContributionProvider(bind, WsRequestValidatorContribution); }); diff --git a/packages/core/src/node/hosting/backend-application-hosts.ts b/packages/core/src/node/hosting/backend-application-hosts.ts new file mode 100644 index 0000000000000..341697a05c489 --- /dev/null +++ b/packages/core/src/node/hosting/backend-application-hosts.ts @@ -0,0 +1,57 @@ +/******************************************************************************** + * 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 { injectable, postConstruct } from 'inversify'; + +/** + * **Important: This component is not bound on Electron.** + * + * Component handling the different hosts the Theia backend should be reachable at. + * + * Hosts should be set through the `THEIA_HOSTS` environment variable as a comma-separated list of hosts. + * + * If you do not set this variable, we'll consider that we don't know where the application is hosted at. + */ +@injectable() +export class BackendApplicationHosts { + + protected readonly _hosts = new Set(); + /** + * Set of domains that the application is supposed to be reachable at. + * If the set is empty it means that we don't know where we are hosted. + * You can check for this with `.areHostsKnown()`. + */ + get hosts(): ReadonlySet { + return this._hosts; + } + + @postConstruct() + protected postConstruct(): void { + const theiaHostsEnv = process.env['THEIA_HOSTS']; + if (theiaHostsEnv) { + theiaHostsEnv.split(',').forEach(host => { + const trimmed = host.trim(); + if (trimmed.length > 0) { + this._hosts.add(trimmed); + } + }); + } + } + + areHostsKnown(): boolean { + return this._hosts.size > 0; + } +} diff --git a/packages/core/src/node/hosting/backend-hosting-module.ts b/packages/core/src/node/hosting/backend-hosting-module.ts new file mode 100644 index 0000000000000..d123587f94368 --- /dev/null +++ b/packages/core/src/node/hosting/backend-hosting-module.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 + ********************************************************************************/ + +import { ContainerModule } from 'inversify'; +import { WsRequestValidatorContribution } from '../ws-request-validators'; +import { BackendApplicationHosts } from './backend-application-hosts'; +import { WsOriginValidator } from './ws-origin-validator'; + +export default new ContainerModule(bind => { + bind(BackendApplicationHosts).toSelf().inSingletonScope(); + bind(WsOriginValidator).toSelf().inSingletonScope(); + bind(WsRequestValidatorContribution).toService(WsOriginValidator); +}); diff --git a/packages/core/src/node/hosting/ws-origin-validator.ts b/packages/core/src/node/hosting/ws-origin-validator.ts new file mode 100644 index 0000000000000..6304a08bb2a91 --- /dev/null +++ b/packages/core/src/node/hosting/ws-origin-validator.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 * as http from 'http'; +import { inject, injectable } from 'inversify'; +import * as url from 'url'; +import { WsRequestValidatorContribution } from '../ws-request-validators'; +import { BackendApplicationHosts } from './backend-application-hosts'; + +@injectable() +export class WsOriginValidator implements WsRequestValidatorContribution { + + @inject(BackendApplicationHosts) + protected readonly backendApplicationHosts: BackendApplicationHosts; + + allowWsUpgrade(request: http.IncomingMessage): boolean { + if (!this.backendApplicationHosts.areHostsKnown() || !request.headers.origin) { + return true; + } + const origin = url.parse(request.headers.origin); + return this.backendApplicationHosts.hosts.has(origin.host!); + } +} diff --git a/packages/core/src/node/messaging/messaging-contribution.ts b/packages/core/src/node/messaging/messaging-contribution.ts index 6d56b3a1b5f46..354ae68dd5ec2 100644 --- a/packages/core/src/node/messaging/messaging-contribution.ts +++ b/packages/core/src/node/messaging/messaging-contribution.ts @@ -30,8 +30,8 @@ import { BackendApplicationContribution } from '../backend-application'; import { MessagingService, WebSocketChannelConnection } from './messaging-service'; import { ConsoleLogger } from './logger'; import { ConnectionContainerModule } from './connection-container-module'; - import Route = require('route-parser'); +import { WsRequestValidator } from '../ws-request-validators'; export const MessagingContainer = Symbol('MessagingContainer'); @@ -47,6 +47,9 @@ export class MessagingContribution implements BackendApplicationContribution, Me @inject(ContributionProvider) @named(MessagingService.Contribution) protected readonly contributions: ContributionProvider; + @inject(WsRequestValidator) + protected readonly wsRequestValidator: WsRequestValidator; + protected webSocketServer: ws.Server | undefined; protected readonly wsHandlers = new MessagingContribution.ConnectionHandlers(); protected readonly channelHandlers = new MessagingContribution.ConnectionHandlers(); @@ -115,8 +118,20 @@ export class MessagingContribution implements BackendApplicationContribution, Me * 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); + this.wsRequestValidator.allowWsUpgrade(request).then(allowed => { + if (allowed) { + this.webSocketServer!.handleUpgrade(request, socket, head, client => { + this.webSocketServer!.emit('connection', client, request); + }); + } else { + console.error(`refused a websocket connection: ${request.connection.remoteAddress}`); + socket.write('HTTP/1.1 403 Forbidden\n\n'); + socket.destroy(); + } + }, error => { + console.error(error); + socket.write('HTTP/1.1 500 Internal Error\n\n'); + socket.destroy(); }); } diff --git a/packages/core/src/node/ws-request-validators.ts b/packages/core/src/node/ws-request-validators.ts new file mode 100644 index 0000000000000..51fb24f391503 --- /dev/null +++ b/packages/core/src/node/ws-request-validators.ts @@ -0,0 +1,56 @@ +/******************************************************************************** + * 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 { inject, injectable, named } from 'inversify'; +import * as http from 'http'; +import { ContributionProvider, MaybePromise } from '../common'; + +/** + * Bind components to this symbol to filter WebSocket connections. + */ +export const WsRequestValidatorContribution = Symbol('RequestValidatorContribution'); +export interface WsRequestValidatorContribution { + /** + * Return `false` to prevent the protocol upgrade from going through, blocking the WebSocket connection. + * + * @param request The HTTP connection upgrade request received by the server. + */ + allowWsUpgrade(request: http.IncomingMessage): MaybePromise; +} + +/** + * Central handler of `WsRequestValidatorContribution`. + */ +@injectable() +export class WsRequestValidator { + + @inject(ContributionProvider) @named(WsRequestValidatorContribution) + protected readonly requestValidators: ContributionProvider; + + /** + * Ask all bound `WsRequestValidatorContributions` if the WebSocket connection should be allowed or not. + */ + async allowWsUpgrade(request: http.IncomingMessage): Promise { + return new Promise(async resolve => { + await Promise.all(Array.from(this.requestValidators.getContributions(), async validator => { + if (!await validator.allowWsUpgrade(request)) { + resolve(false); // bail on first refusal + } + })); + resolve(true); + }); + } +} diff --git a/packages/mini-browser/README.md b/packages/mini-browser/README.md index 9f5b33767ff67..3eae4da864344 100644 --- a/packages/mini-browser/README.md +++ b/packages/mini-browser/README.md @@ -14,6 +14,13 @@ The `@theia/mini-browser` extension provides a browser widget with the corresponding backend endpoints. +### Environment Variables + +- `THEIA_MINI_BROWSER_HOST_PATTERN` + - A string pattern possibly containing `{{hostname}}` which will be replaced. This is the host for which the `mini-browser` will serve. + - It is a good practice to host the `mini-browser` handlers on a sub-domain as it is more secure. + - Defaults to `mini-browser.{{hostname}}`. + ## Additional Information - [API documentation for `@theia/mini-browser`](https://eclipse-theia.github.io/theia/docs/next/modules/mini_browser.html) diff --git a/packages/mini-browser/compile.tsconfig.json b/packages/mini-browser/compile.tsconfig.json index 8202bdd8795b7..d3e8f1dc24e40 100644 --- a/packages/mini-browser/compile.tsconfig.json +++ b/packages/mini-browser/compile.tsconfig.json @@ -3,8 +3,7 @@ "compilerOptions": { "composite": true, "rootDir": "src", - "outDir": "lib", - "baseUrl": "." + "outDir": "lib" }, "include": [ "src" diff --git a/packages/mini-browser/package.json b/packages/mini-browser/package.json index c34891324f24c..dbde3da2448e4 100644 --- a/packages/mini-browser/package.json +++ b/packages/mini-browser/package.json @@ -7,7 +7,8 @@ "@theia/filesystem": "^1.7.0", "@types/mime-types": "^2.1.0", "mime-types": "^2.1.18", - "pdfobject": "^2.0.201604172" + "pdfobject": "^2.0.201604172", + "vhost": "^3.0.2" }, "publishConfig": { "access": "public" @@ -15,7 +16,8 @@ "theiaExtensions": [ { "backend": "lib/node/mini-browser-backend-module", - "frontend": "lib/browser/mini-browser-frontend-module" + "frontend": "lib/browser/mini-browser-frontend-module", + "electronMain": "lib/electron-main/mini-browser-electron-main-module" } ], "keywords": [ diff --git a/packages/mini-browser/src/browser/mini-browser-content.ts b/packages/mini-browser/src/browser/mini-browser-content.ts index e705ee5ae511a..37dbf8486dd3d 100644 --- a/packages/mini-browser/src/browser/mini-browser-content.ts +++ b/packages/mini-browser/src/browser/mini-browser-content.ts @@ -32,6 +32,7 @@ import debounce = require('lodash.debounce'); import { MiniBrowserContentStyle } from './mini-browser-content-style'; import { FileService } from '@theia/filesystem/lib/browser/file-service'; import { FileChangesEvent, FileChangeType } from '@theia/filesystem/lib/common/files'; +import { MiniBrowserEnvironment } from './mini-browser-environment'; /** * Initializer properties for the embedded browser widget. @@ -180,6 +181,9 @@ export class MiniBrowserContent extends BaseWidget { @inject(FileService) protected readonly fileService: FileService; + @inject(MiniBrowserEnvironment) + protected readonly miniBrowserEnvironment: MiniBrowserEnvironment; + protected readonly submitInputEmitter = new Emitter(); protected readonly navigateBackEmitter = new Emitter(); protected readonly navigateForwardEmitter = new Emitter(); @@ -511,8 +515,16 @@ export class MiniBrowserContent extends BaseWidget { return element; } - protected mapLocation(location: string): Promise { - return this.locationMapper.map(location); + protected async mapLocation(location: string): Promise { + const [hostPattern, mappedLocation] = await Promise.all([ + this.miniBrowserEnvironment.hostPattern, + this.locationMapper.map(location), + ]); + // The `LocationMapper` will most likely resolve something on Theia's main host. + // But we need to re-route those to the virtual host where the mini-browser accepts requests. + const url = new URL(mappedLocation); + url.host = hostPattern.replace('{{hostname}}', url.host); + return url.toString(); } protected setInput(value: string): void { diff --git a/packages/mini-browser/src/browser/mini-browser-environment.ts b/packages/mini-browser/src/browser/mini-browser-environment.ts new file mode 100644 index 0000000000000..f2f75cf6eaaf3 --- /dev/null +++ b/packages/mini-browser/src/browser/mini-browser-environment.ts @@ -0,0 +1,43 @@ +/******************************************************************************** + * 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 { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; +import { Deferred } from '@theia/core/lib/common/promise-util'; +import { inject, injectable, postConstruct } from 'inversify'; +import { MiniBrowserEndpoint } from '../common/mini-browser-endpoint'; + +/** + * Fetch values from the backend's environment. + */ +@injectable() +export class MiniBrowserEnvironment { + + protected readonly deferredHostPattern = new Deferred(); + /** + * The mini-browser host pattern as set in the backend's environment. + */ + readonly hostPattern = this.deferredHostPattern.promise; + + @inject(EnvVariablesServer) + protected readonly environment: EnvVariablesServer; + + @postConstruct() + protected postConstruct(): void { + this.environment.getValue(MiniBrowserEndpoint.HOST_PATTERN_ENV).then(envVar => { + this.deferredHostPattern.resolve(envVar?.value || MiniBrowserEndpoint.HOST_PATTERN_DEFAULT); + }); + } +} diff --git a/packages/mini-browser/src/browser/mini-browser-frontend-module.ts b/packages/mini-browser/src/browser/mini-browser-frontend-module.ts index f3788c6c3fe34..f32c3115cb480 100644 --- a/packages/mini-browser/src/browser/mini-browser-frontend-module.ts +++ b/packages/mini-browser/src/browser/mini-browser-frontend-module.ts @@ -39,6 +39,7 @@ import { LocationMapper, LocationWithoutSchemeMapper, } from './location-mapper-service'; +import { MiniBrowserEnvironment } from './mini-browser-environment'; export default new ContainerModule(bind => { @@ -62,6 +63,7 @@ export default new ContainerModule(bind => { } })).inSingletonScope(); + bind(MiniBrowserEnvironment).toSelf().inSingletonScope(); bind(MiniBrowserOpenHandler).toSelf().inSingletonScope(); bind(OpenHandler).toService(MiniBrowserOpenHandler); bind(FrontendApplicationContribution).toService(MiniBrowserOpenHandler); diff --git a/packages/mini-browser/src/browser/mini-browser-open-handler.ts b/packages/mini-browser/src/browser/mini-browser-open-handler.ts index 95df96ff5d865..f0d6b61ab7488 100644 --- a/packages/mini-browser/src/browser/mini-browser-open-handler.ts +++ b/packages/mini-browser/src/browser/mini-browser-open-handler.ts @@ -91,10 +91,12 @@ export class MiniBrowserOpenHandler extends NavigatableWidgetOpenHandler (await this.miniBrowserService.supportedFileExtensions()).forEach(entry => { - const { extension, priority } = entry; - this.supportedExtensions.set(extension, priority); - }))(); + this.miniBrowserService.supportedFileExtensions().then(entries => { + entries.forEach(entry => { + const { extension, priority } = entry; + this.supportedExtensions.set(extension, priority); + }); + }); } canHandle(uri: URI): number { diff --git a/packages/mini-browser/src/common/mini-browser-endpoint.ts b/packages/mini-browser/src/common/mini-browser-endpoint.ts new file mode 100644 index 0000000000000..8ac9b306f12e9 --- /dev/null +++ b/packages/mini-browser/src/common/mini-browser-endpoint.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 + ********************************************************************************/ + +/** + * The mini-browser can now serve content on its own host/origin. + * + * The virtual host can be configured with this `THEIA_MINI_BROWSER_HOST_PATTERN` + * environment variable. Only a `{{hostname}}` variable is allowed and will be replaced. + */ +export namespace MiniBrowserEndpoint { + export const HOST_PATTERN_ENV = 'THEIA_MINI_BROWSER_HOST_PATTERN'; + export const HOST_PATTERN_DEFAULT = 'mini-browser.{{hostname}}'; +} diff --git a/packages/mini-browser/src/electron-main/mini-browser-electron-main-contribution.ts b/packages/mini-browser/src/electron-main/mini-browser-electron-main-contribution.ts new file mode 100644 index 0000000000000..e8b6c60ad360a --- /dev/null +++ b/packages/mini-browser/src/electron-main/mini-browser-electron-main-contribution.ts @@ -0,0 +1,42 @@ +/******************************************************************************** + * 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 { ElectronMainApplication, ElectronMainApplicationContribution } from '@theia/core/lib/electron-main/electron-main-application'; +import { ElectronSecurityTokenService } from '@theia/core/lib/electron-main/electron-security-token-service'; +import { inject, injectable } from 'inversify'; +import { MiniBrowserEndpoint } from '../common/mini-browser-endpoint'; + +/** + * Since the mini-browser might serve content from a new origin, + * we need to attach the ElectronSecurityToken for the Electron + * backend to accept HTTP requests. + */ +@injectable() +export class MiniBrowserElectronMainContribution implements ElectronMainApplicationContribution { + + @inject(ElectronSecurityTokenService) + protected readonly electronSecurityTokenService: ElectronSecurityTokenService; + + async onStart(app: ElectronMainApplication): Promise { + const url = this.getMiniBrowserEndpoint(await app.backendPort); + await this.electronSecurityTokenService.setElectronSecurityTokenCookie(url); + } + + protected getMiniBrowserEndpoint(port: number): string { + const pattern = process.env[MiniBrowserEndpoint.HOST_PATTERN_ENV] ?? MiniBrowserEndpoint.HOST_PATTERN_DEFAULT; + return 'http://' + pattern.replace('{{hostname}}', `localhost:${port}`); + } +} diff --git a/packages/mini-browser/src/electron-main/mini-browser-electron-main-module.ts b/packages/mini-browser/src/electron-main/mini-browser-electron-main-module.ts new file mode 100644 index 0000000000000..200e352f34d4d --- /dev/null +++ b/packages/mini-browser/src/electron-main/mini-browser-electron-main-module.ts @@ -0,0 +1,24 @@ +/******************************************************************************** + * 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 { ElectronMainApplicationContribution } from '@theia/core/lib/electron-main/electron-main-application'; +import { ContainerModule } from 'inversify'; +import { MiniBrowserElectronMainContribution } from './mini-browser-electron-main-contribution'; + +export default new ContainerModule(bind => { + bind(MiniBrowserElectronMainContribution).toSelf().inSingletonScope(); + bind(ElectronMainApplicationContribution).toService(MiniBrowserElectronMainContribution); +}); diff --git a/packages/mini-browser/src/node/mini-browser-backend-module.ts b/packages/mini-browser/src/node/mini-browser-backend-module.ts index 45b6a697e4ef7..5be14fa758d5b 100644 --- a/packages/mini-browser/src/node/mini-browser-backend-module.ts +++ b/packages/mini-browser/src/node/mini-browser-backend-module.ts @@ -20,10 +20,14 @@ import { BackendApplicationContribution } from '@theia/core/lib/node/backend-app import { ConnectionHandler, JsonRpcConnectionHandler } from '@theia/core/lib/common'; import { MiniBrowserService, MiniBrowserServicePath } from '../common/mini-browser-service'; import { MiniBrowserEndpoint, MiniBrowserEndpointHandler, HtmlHandler, ImageHandler, PdfHandler, SvgHandler } from './mini-browser-endpoint'; +import { WsRequestValidatorContribution } from '@theia/core/lib/node/ws-request-validators'; +import { MiniBrowserWsRequestValidator } from './mini-browser-ws-validator'; export default new ContainerModule(bind => { bind(MiniBrowserEndpoint).toSelf().inSingletonScope(); bind(BackendApplicationContribution).toService(MiniBrowserEndpoint); + bind(MiniBrowserWsRequestValidator).toSelf().inSingletonScope(); + bind(WsRequestValidatorContribution).toService(MiniBrowserWsRequestValidator); bind(MiniBrowserService).toService(MiniBrowserEndpoint); bind(ConnectionHandler).toDynamicValue(context => new JsonRpcConnectionHandler(MiniBrowserServicePath, () => context.container.get(MiniBrowserService))).inSingletonScope(); bindContributionProvider(bind, MiniBrowserEndpointHandler); diff --git a/packages/mini-browser/src/node/mini-browser-endpoint.ts b/packages/mini-browser/src/node/mini-browser-endpoint.ts index 101a922503a3f..6d2a921573afa 100644 --- a/packages/mini-browser/src/node/mini-browser-endpoint.ts +++ b/packages/mini-browser/src/node/mini-browser-endpoint.ts @@ -18,13 +18,14 @@ import * as fs from 'fs-extra'; import { lookup } from 'mime-types'; import { injectable, inject, named } from 'inversify'; import { Application, Request, Response } from 'express'; -import URI from '@theia/core/lib/common/uri'; import { FileUri } from '@theia/core/lib/node/file-uri'; import { ILogger } from '@theia/core/lib/common/logger'; import { MaybePromise } from '@theia/core/lib/common/types'; import { ContributionProvider } from '@theia/core/lib/common/contribution-provider'; import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application'; import { MiniBrowserService } from '../common/mini-browser-service'; +const vhost = require('vhost'); +import express = require('express'); /** * The return type of the `FileSystem#resolveContent` method. @@ -86,11 +87,11 @@ export class MiniBrowserEndpoint implements BackendApplicationContribution, Mini protected readonly handlers: Map = new Map(); configure(app: Application): void { - app.get(`${MiniBrowserEndpoint.HANDLE_PATH}*`, async (request, response) => this.response(await this.getUri(request), response)); + this.attachRequestHandler(app); } async onStart(): Promise { - for (const handler of this.getContributions()) { + await Promise.all(Array.from(this.getContributions(), async handler => { const extensions = await handler.supportedExtensions(); for (const extension of (Array.isArray(extensions) ? extensions : [extensions]).map(e => e.toLocaleLowerCase())) { const existingHandler = this.handlers.get(extension); @@ -98,11 +99,18 @@ export class MiniBrowserEndpoint implements BackendApplicationContribution, Mini this.handlers.set(extension, handler); } } - } + })); } async supportedFileExtensions(): Promise[]> { - return Array.from(this.handlers.entries()).map(([extension, handler]) => ({ extension, priority: handler.priority() })); + return Array.from(this.handlers.entries(), ([extension, handler]) => ({ extension, priority: handler.priority() })); + } + + protected attachRequestHandler(app: Application): void { + const miniBrowserApp = express(); + miniBrowserApp.get(`${MiniBrowserEndpoint.HANDLE_PATH}*`, async (request, response) => this.response(await this.getUri(request), response)); + // We'll only answer on requests for which the host matches `THEIA_MINI_BROWSER_HOST_PATTERN`: + app.use(vhost(this.getVirtualHostRegExp(), miniBrowserApp)); } protected async response(uri: string, response: Response): Promise { @@ -135,7 +143,7 @@ export class MiniBrowserEndpoint implements BackendApplicationContribution, Mini protected getUri(request: Request): MaybePromise { const decodedPath = request.path.substr(MiniBrowserEndpoint.HANDLE_PATH.length); - return new URI(FileUri.create(decodedPath).toString(true)).toString(true); + return FileUri.create(decodedPath).toString(true); } protected async readContent(uri: string): Promise { @@ -186,6 +194,13 @@ export class MiniBrowserEndpoint implements BackendApplicationContribution, Mini }; } + protected getVirtualHostRegExp(): RegExp { + const pattern = process.env[MiniBrowserEndpointNS.HOST_PATTERN_ENV] ?? MiniBrowserEndpointNS.HOST_PATTERN_DEFAULT; + const vhostRe = pattern + .replace('.', '\\.') + .replace('{{hostname}}', '.+'); + return new RegExp(vhostRe, 'i'); + } } // See `EditorManager#canHandle`. diff --git a/packages/mini-browser/src/node/mini-browser-ws-validator.ts b/packages/mini-browser/src/node/mini-browser-ws-validator.ts new file mode 100644 index 0000000000000..9efad636b1200 --- /dev/null +++ b/packages/mini-browser/src/node/mini-browser-ws-validator.ts @@ -0,0 +1,46 @@ +/******************************************************************************** + * 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 { WsRequestValidatorContribution } from '@theia/core/lib/node/ws-request-validators'; +import * as http from 'http'; +import { injectable, postConstruct } from 'inversify'; +import { MiniBrowserEndpoint } from '../common/mini-browser-endpoint'; + +/** + * Prevents explicit WebSocket connections from the mini-browser virtual host. + */ +@injectable() +export class MiniBrowserWsRequestValidator implements WsRequestValidatorContribution { + + protected miniBrowserHostRe: RegExp; + + @postConstruct() + protected postConstruct(): void { + const pattern = process.env[MiniBrowserEndpoint.HOST_PATTERN_ENV] || MiniBrowserEndpoint.HOST_PATTERN_DEFAULT; + const vhostRe = pattern + .replace('.', '\\.') + .replace('{{hostname}}', '.+'); + this.miniBrowserHostRe = new RegExp(vhostRe, 'i'); + } + + async allowWsUpgrade(request: http.IncomingMessage): Promise { + const origin = request.headers.origin; + if (origin && this.miniBrowserHostRe.test(origin)) { + return false; + } + return true; + } +} diff --git a/packages/plugin-ext/src/main/node/plugin-ext-backend-module.ts b/packages/plugin-ext/src/main/node/plugin-ext-backend-module.ts index 440e5f138f032..4b97bce1a32f2 100644 --- a/packages/plugin-ext/src/main/node/plugin-ext-backend-module.ts +++ b/packages/plugin-ext/src/main/node/plugin-ext-backend-module.ts @@ -36,10 +36,12 @@ import { PluginServerHandler } from './plugin-server-handler'; import { PluginCliContribution } from './plugin-cli-contribution'; import { PluginTheiaEnvironment } from '../common/plugin-theia-environment'; import { PluginTheiaDeployerParticipant } from './plugin-theia-deployer-participant'; +import { WsRequestValidatorContribution } from '@theia/core/src/node/ws-request-validators'; export function bindMainBackend(bind: interfaces.Bind): void { bind(PluginApiContribution).toSelf().inSingletonScope(); bind(BackendApplicationContribution).toService(PluginApiContribution); + bind(WsRequestValidatorContribution).toService(PluginApiContribution); bindContributionProvider(bind, PluginDeployerParticipant); bind(PluginDeployer).to(PluginDeployerImpl).inSingletonScope(); diff --git a/packages/plugin-ext/src/main/node/plugin-service.ts b/packages/plugin-ext/src/main/node/plugin-service.ts index f98aefde36300..9d0433cd47d90 100644 --- a/packages/plugin-ext/src/main/node/plugin-service.ts +++ b/packages/plugin-ext/src/main/node/plugin-service.ts @@ -14,20 +14,33 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ +import * as http from 'http'; import * as path from 'path'; +import * as url from 'url'; import connect = require('connect'); import serveStatic = require('serve-static'); const vhost = require('vhost'); import * as express from 'express'; import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application'; -import { injectable } from 'inversify'; +import { injectable, postConstruct } from 'inversify'; import { WebviewExternalEndpoint } from '../common/webview-protocol'; import { environment } from '@theia/application-package/lib/environment'; +import { WsRequestValidatorContribution } from '@theia/core/lib/node/ws-request-validators'; +import { MaybePromise } from '@theia/core/lib/common'; const pluginPath = (process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE) + './theia/plugins/'; @injectable() -export class PluginApiContribution implements BackendApplicationContribution { +export class PluginApiContribution implements BackendApplicationContribution, WsRequestValidatorContribution { + + protected _webviewExternalEndpointRegExp: RegExp; + + @postConstruct() + protected postConstruct(): void { + const webviewExternalEndpoint = this.webviewExternalEndpoint(); + console.log(`Configuring to accept webviews on '${webviewExternalEndpoint}' hostname.`); + this._webviewExternalEndpointRegExp = new RegExp(webviewExternalEndpoint, 'i'); + } configure(app: express.Application): void { app.get('/plugin/:path(*)', (req, res) => { @@ -37,11 +50,23 @@ export class PluginApiContribution implements BackendApplicationContribution { const webviewApp = connect(); webviewApp.use('/webview', serveStatic(path.join(__dirname, '../../../src/main/browser/webview/pre'))); - const webviewExternalEndpoint = this.webviewExternalEndpoint(); - console.log(`Configuring to accept webviews on '${webviewExternalEndpoint}' hostname.`); - app.use(vhost(new RegExp(webviewExternalEndpoint, 'i'), webviewApp)); + app.use(vhost(this._webviewExternalEndpointRegExp, webviewApp)); + } + + allowWsUpgrade(request: http.IncomingMessage): MaybePromise { + if (request.headers.origin) { + const origin = url.parse(request.headers.origin); + if (origin.host && this._webviewExternalEndpointRegExp.test(origin.host)) { + // If the origin comes from the WebViews, refuse: + return false; + } + } + return true; } + /** + * Returns a RegExp pattern matching the expected WebView endpoint's host. + */ protected webviewExternalEndpoint(): string { let endpointPattern; if (environment.electron.is()) { @@ -49,8 +74,8 @@ export class PluginApiContribution implements BackendApplicationContribution { } else { endpointPattern = process.env[WebviewExternalEndpoint.pattern] || WebviewExternalEndpoint.defaultPattern; } - return endpointPattern + return `^${endpointPattern .replace('{{uuid}}', '.+') - .replace('{{hostname}}', '.+'); + .replace('{{hostname}}', '.+')}$`; } }