Skip to content

Commit

Permalink
core,mini-browser: server enhancements
Browse files Browse the repository at this point in the history
# 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 <paul.marechal@ericsson.com>
  • Loading branch information
paul-marechal committed Nov 19, 2020
1 parent e209832 commit 2a9e3d4
Show file tree
Hide file tree
Showing 32 changed files with 614 additions and 53 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
10 changes: 10 additions & 0 deletions packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
Expand Down
31 changes: 16 additions & 15 deletions packages/core/src/electron-main/electron-main-application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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');
Expand Down Expand Up @@ -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<number>();

protected _config: FrontendApplicationConfig;
protected readonly _backendPort = new Deferred<number>();
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.');
Expand All @@ -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();
Expand Down Expand Up @@ -279,8 +285,8 @@ export class ElectronMainApplication {
}

protected async createWindowUri(): Promise<URI> {
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 {
Expand Down Expand Up @@ -418,12 +424,7 @@ export class ElectronMainApplication {
}

protected async attachElectronSecurityToken(port: number): Promise<void> {
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 {
Expand Down
35 changes: 35 additions & 0 deletions packages/core/src/electron-main/electron-security-token-service.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
await session.defaultSession.cookies.set({
url,
name: ElectronSecurityToken,
value: JSON.stringify(this.electronSecurityToken),
httpOnly: true
});
}
}
Original file line number Diff line number Diff line change
@@ -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);
});
Original file line number Diff line number Diff line change
@@ -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://';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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>(ElectronTokenValidator).toSelf().inSingletonScope();

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

rebind<MessagingContribution>(MessagingService.Identifier).to(ElectronMessagingContribution).inSingletonScope();
bind(ElectronTokenBackendContribution).toSelf().inSingletonScope();
bind(BackendApplicationContribution).toService(ElectronTokenBackendContribution);
bind(ElectronTokenValidator).toSelf().inSingletonScope();
bind(WsRequestValidatorContribution).toService(ElectronTokenValidator);
});
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
17 changes: 14 additions & 3 deletions packages/core/src/electron-node/token/electron-token-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean> {
return this.allowRequest(request);
}

/**
* Expects the token to be passed via cookies by default.
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/node/backend-application-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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);
});
57 changes: 57 additions & 0 deletions packages/core/src/node/hosting/backend-application-hosts.ts
Original file line number Diff line number Diff line change
@@ -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<string>();
/**
* 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<string> {
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;
}
}
26 changes: 26 additions & 0 deletions packages/core/src/node/hosting/backend-hosting-module.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
********************************************************************************/

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);
});
Loading

0 comments on commit 2a9e3d4

Please sign in to comment.