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 17, 2020
1 parent cd85908 commit 57a88c6
Show file tree
Hide file tree
Showing 30 changed files with 600 additions and 104 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

<a name="breaking_changes_1.8.0">[Breaking Changes:](#breaking_changes_1.8.0)</a>

- [file-search] Deprecate dependency on `@theia/process` and replaced its usage by node's `child_process` api.
- [file-search] Deprecated dependency on `@theia/process` and replaced its usage by node's `child_process` api.
- [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
57 changes: 27 additions & 30 deletions packages/core/src/electron-main/electron-main-application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,21 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { FrontendApplicationConfig } from '@theia/application-package/lib/application-props';
import { fork, ForkOptions } from 'child_process';
import { app, BrowserWindow, BrowserWindowConstructorOptions, dialog, Event as ElectronEvent, globalShortcut, screen } from 'electron';
import { promises as fs } from 'fs';
import { inject, injectable, named } from 'inversify';
import { session, screen, globalShortcut, app, BrowserWindow, BrowserWindowConstructorOptions, Event as ElectronEvent, dialog } from 'electron';
import { AddressInfo } from 'net';
import * as path from 'path';
import { Argv } from 'yargs';
import { AddressInfo } from 'net';
import { promises as fs } from 'fs';
import { fork, ForkOptions } from 'child_process';
import { FrontendApplicationConfig } from '@theia/application-package/lib/application-props';
import URI from '../common/uri';
import { FileUri } from '../node/file-uri';
import { ContributionProvider } from '../common/contribution-provider';
import { Deferred } from '../common/promise-util';
import { MaybePromise } from '../common/types';
import { ContributionProvider } from '../common/contribution-provider';
import { ElectronSecurityToken } from '../electron-common/electron-token';
import URI from '../common/uri';
import { FileUri } from '../node/file-uri';
import { ElectronSecurityTokenService } from './electron-security-token-service';

const Storage = require('electron-store');
const createYargs: (argv?: string[], cwd?: string) => Argv = require('yargs/yargs');

Expand Down Expand Up @@ -162,16 +163,18 @@ export class ElectronMainApplication {
@inject(ElectronMainApplicationGlobals)
protected readonly globals: ElectronMainApplicationGlobals;

@inject(ElectronSecurityToken)
protected electronSecurityToken: ElectronSecurityToken;

@inject(ElectronMainProcessArgv)
protected processArgv: ElectronMainProcessArgv;

@inject(ElectronSecurityTokenService)
protected electronSecurityTokenService: ElectronSecurityTokenService;

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 @@ -181,9 +184,10 @@ export class ElectronMainApplication {

async start(config: FrontendApplicationConfig): Promise<void> {
this._config = config;
this.prepareEnv();
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 @@ -280,8 +284,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 @@ -402,7 +406,6 @@ export class ElectronMainApplication {
// Otherwise, the forked backend processes will not know that they're serving the electron frontend.
process.env.THEIA_ELECTRON_VERSION = process.versions.electron;
if (noBackendFork) {
process.env[ElectronSecurityToken] = JSON.stringify(this.electronSecurityToken);
// The backend server main file is supposed to export a promise resolving with the port used by the http(s) server.
const address: AddressInfo = await require(this.globals.THEIA_BACKEND_MAIN_PATH);
return address.port;
Expand Down Expand Up @@ -430,21 +433,15 @@ export class ElectronMainApplication {
}

protected async getForkOptions(): Promise<ForkOptions> {
return {
env: {
...process.env,
[ElectronSecurityToken]: JSON.stringify(this.electronSecurityToken),
},
};
return { env: process.env };
}

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 prepareEnv(): void {
this.electronSecurityTokenService.setElectronSecurityTokenEnv(process.env);
}

protected hookApplicationEvents(): void {
Expand Down
39 changes: 39 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,39 @@
/********************************************************************************
* 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
});
}

setElectronSecurityTokenEnv(env: { [key: string]: string | undefined }): void {
env[ElectronSecurityToken] = JSON.stringify(this.electronSecurityToken);
}
}
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 @@ -14,14 +14,15 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

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

/**
* Override the browser MessagingContribution class to refuse connections that do not include a specific token.
* @deprecated since 1.8.0
*/
@injectable()
export class ElectronMessagingContribution extends MessagingContribution {
Expand Down
19 changes: 15 additions & 4 deletions packages/core/src/electron-node/token/electron-token-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,30 @@
* 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 * as crypto from 'crypto';
import { injectable } from 'inversify';
import * as http from 'http';
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;

protected electronSecurityToken: ElectronSecurityToken = this.getToken();
@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
23 changes: 14 additions & 9 deletions packages/core/src/node/backend-application-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,23 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { ContainerModule, decorate, injectable } from 'inversify';
import { ApplicationPackage } from '@theia/application-package';
import { ContainerModule, decorate, injectable } from 'inversify';
import {
bindContributionProvider, MessageService, MessageClient, ConnectionHandler, JsonRpcConnectionHandler,
CommandService, commandServicePath, messageServicePath
bindContributionProvider,
CommandService, commandServicePath, ConnectionHandler, JsonRpcConnectionHandler, MessageClient, MessageService,
messageServicePath
} from '../common';
import { BackendApplication, BackendApplicationContribution, BackendApplicationCliContribution } from './backend-application';
import { CliManager, CliContribution } from './cli';
import { IPCConnectionProvider } from './messaging';
import { applicationPath, ApplicationServer } from '../common/application-protocol';
import { QuickPickService, quickPickServicePath } from '../common/quick-pick-service';
import { envVariablesPath, EnvVariablesServer } from './../common/env-variables';
import { ApplicationServerImpl } from './application-server';
import { ApplicationServer, applicationPath } from '../common/application-protocol';
import { EnvVariablesServer, envVariablesPath } from './../common/env-variables';
import { BackendApplication, BackendApplicationCliContribution, BackendApplicationContribution } from './backend-application';
import { CliContribution, CliManager } from './cli';
import { EnvVariablesServerImpl } from './env-variables';
import { IPCConnectionProvider } from './messaging';
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 +83,7 @@ export const backendApplicationModule = new ContainerModule(bind => {
const { projectPath } = container.get(BackendApplicationCliContribution);
return new ApplicationPackage({ projectPath });
}).inSingletonScope();

bind(WsRequestValidator).toSelf().inSingletonScope();
bindContributionProvider(bind, WsRequestValidatorContribution);
});
Loading

0 comments on commit 57a88c6

Please sign in to comment.