@theia/core
Version:
Theia is a cloud & desktop IDE framework implemented in TypeScript.
120 lines • 5.53 kB
JavaScript
;
// *****************************************************************************
// Copyright (C) 2026 STMicroelectronics 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-only WITH Classpath-exception-2.0
// *****************************************************************************
Object.defineProperty(exports, "__esModule", { value: true });
exports.BrowserConnectionTokenBackendContribution = exports.BROWSER_TOKEN_COOKIE_NAME = exports.BrowserConnectionToken = void 0;
exports.createBrowserConnectionToken = createBrowserConnectionToken;
const tslib_1 = require("tslib");
const cookie = require("cookie");
const crypto = require("crypto");
const inversify_1 = require("inversify");
const index_1 = require("../../common/index");
const backend_application_1 = require("../backend-application");
const uuid_1 = require("../../common/uuid");
exports.BrowserConnectionToken = Symbol('BrowserConnectionToken');
exports.BROWSER_TOKEN_COOKIE_NAME = 'theia-connection-token';
/**
* Validates WebSocket and HTTP requests using a cookie-based connection token.
*
* In browser deployments, the server generates a random token at startup and sets it
* as a `SameSite=Strict; HttpOnly` cookie on the first page load. Cross-origin pages
* cannot obtain or send this cookie, so their requests are rejected.
*
* This complements the origin validator: non-browser callers that omit the Origin
* header (e.g. Node.js scripts) still cannot reach the backend without the cookie.
*
* Skipped in Electron deployments (which use their own `ElectronSecurityToken`).
*/
let BrowserConnectionTokenBackendContribution = class BrowserConnectionTokenBackendContribution {
/**
* Register the cookie middleware during `initialize()` via `EarlyExpressMiddleware`
* so it runs before `express.static()` (which is registered later during `configure()`).
* This ensures the browser receives the token cookie on the initial page load.
*/
initialize() {
if (index_1.environment.electron.is()) {
return;
}
this.earlyMiddleware.handlers.push((req, res, next) => this.expressMiddleware(req, res, next));
}
/**
* Validate the connection token cookie on WebSocket upgrade requests.
* Non-browser callers that omit the Origin header (e.g. Node.js scripts)
* cannot provide the `SameSite=Strict` cookie either, so they are rejected.
*/
allowWsUpgrade(request) {
if (index_1.environment.electron.is()) {
return true;
}
const token = this.getTokenFromCookie(request);
if (token) {
return this.isTokenValid(token);
}
// No cookie: reject. Legitimate browsers always have the cookie
// because it is set on the initial page load.
return false;
}
expressMiddleware(req, res, next) {
const existing = this.getTokenFromCookie(req);
if (!existing || !this.isTokenValid(existing)) {
// No cookie or stale cookie (e.g. after server restart) so (re-)issue it.
// The browser will use the fresh token on subsequent requests.
res.cookie(exports.BROWSER_TOKEN_COOKIE_NAME, this.browserConnectionToken.value, {
httpOnly: true,
sameSite: 'strict',
path: '/'
});
}
next();
}
getTokenFromCookie(req) {
const cookieHeader = req.headers.cookie;
if (cookieHeader) {
return cookie.parse(cookieHeader)[exports.BROWSER_TOKEN_COOKIE_NAME];
}
return undefined;
}
isTokenValid(token) {
try {
const received = Buffer.from(token, 'utf8');
const expected = Buffer.from(this.browserConnectionToken.value, 'utf8');
return received.byteLength === expected.byteLength && crypto.timingSafeEqual(received, expected);
}
catch (error) {
console.error(error);
}
return false;
}
};
exports.BrowserConnectionTokenBackendContribution = BrowserConnectionTokenBackendContribution;
tslib_1.__decorate([
(0, inversify_1.inject)(exports.BrowserConnectionToken),
tslib_1.__metadata("design:type", Object)
], BrowserConnectionTokenBackendContribution.prototype, "browserConnectionToken", void 0);
tslib_1.__decorate([
(0, inversify_1.inject)(backend_application_1.EarlyExpressMiddleware),
tslib_1.__metadata("design:type", backend_application_1.EarlyExpressMiddleware)
], BrowserConnectionTokenBackendContribution.prototype, "earlyMiddleware", void 0);
exports.BrowserConnectionTokenBackendContribution = BrowserConnectionTokenBackendContribution = tslib_1.__decorate([
(0, inversify_1.injectable)()
], BrowserConnectionTokenBackendContribution);
/**
* Creates a new browser connection token.
*/
function createBrowserConnectionToken() {
return { value: (0, uuid_1.generateUuid)() };
}
//# sourceMappingURL=browser-connection-token.js.map