@theia/core 1.73.0-next.7 → 1.73.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -16
- package/i18n/nls.cs.json +268 -36
- package/i18n/nls.de.json +268 -36
- package/i18n/nls.es.json +268 -36
- package/i18n/nls.fr.json +268 -36
- package/i18n/nls.hu.json +268 -36
- package/i18n/nls.it.json +268 -36
- package/i18n/nls.ja.json +268 -36
- package/i18n/nls.json +268 -36
- package/i18n/nls.ko.json +268 -36
- package/i18n/nls.pl.json +268 -36
- package/i18n/nls.pt-br.json +268 -36
- package/i18n/nls.ru.json +268 -36
- package/i18n/nls.tr.json +268 -36
- package/i18n/nls.zh-cn.json +268 -36
- package/i18n/nls.zh-tw.json +268 -36
- package/lib/browser/authentication-service.d.ts +2 -0
- package/lib/browser/authentication-service.d.ts.map +1 -1
- package/lib/browser/authentication-service.js +8 -2
- package/lib/browser/authentication-service.js.map +1 -1
- package/lib/browser/catalog.json +251 -72
- package/lib/browser/frontend-application.d.ts +2 -0
- package/lib/browser/frontend-application.d.ts.map +1 -1
- package/lib/browser/frontend-application.js +15 -9
- package/lib/browser/frontend-application.js.map +1 -1
- package/lib/browser/json-schema-store.d.ts +2 -0
- package/lib/browser/json-schema-store.d.ts.map +1 -1
- package/lib/browser/json-schema-store.js +8 -2
- package/lib/browser/json-schema-store.js.map +1 -1
- package/lib/browser/keybinding.js +2 -1
- package/lib/browser/keybinding.js.map +1 -1
- package/lib/browser/markdown-rendering/markdown.d.ts +1 -1
- package/lib/browser/markdown-rendering/markdown.d.ts.map +1 -1
- package/lib/browser/markdown-rendering/markdown.js +1 -1
- package/lib/browser/markdown-rendering/markdown.js.map +1 -1
- package/lib/browser/menu/composite-menu-node.js +4 -4
- package/lib/browser/menu/composite-menu-node.js.map +1 -1
- package/lib/browser/messaging/ws-connection-source.d.ts.map +1 -1
- package/lib/browser/messaging/ws-connection-source.js +1 -6
- package/lib/browser/messaging/ws-connection-source.js.map +1 -1
- package/lib/browser/preferences/frontend-config-preference-contributions.d.ts +2 -0
- package/lib/browser/preferences/frontend-config-preference-contributions.d.ts.map +1 -1
- package/lib/browser/preferences/frontend-config-preference-contributions.js +7 -1
- package/lib/browser/preferences/frontend-config-preference-contributions.js.map +1 -1
- package/lib/browser/preferences/preference-proxy.spec.js +7 -0
- package/lib/browser/preferences/preference-proxy.spec.js.map +1 -1
- package/lib/browser/preferences/preference-schema-provider.spec.js +3 -0
- package/lib/browser/preferences/preference-schema-provider.spec.js.map +1 -1
- package/lib/browser/preferences/preference-validation-service.d.ts +2 -0
- package/lib/browser/preferences/preference-validation-service.d.ts.map +1 -1
- package/lib/browser/preferences/preference-validation-service.js +11 -5
- package/lib/browser/preferences/preference-validation-service.js.map +1 -1
- package/lib/browser/preferences/preference-validation-service.spec.js +2 -0
- package/lib/browser/preferences/preference-validation-service.spec.js.map +1 -1
- package/lib/browser/secondary-window-handler.d.ts +2 -0
- package/lib/browser/secondary-window-handler.d.ts.map +1 -1
- package/lib/browser/secondary-window-handler.js +12 -6
- package/lib/browser/secondary-window-handler.js.map +1 -1
- package/lib/browser/shell/application-shell.d.ts +2 -0
- package/lib/browser/shell/application-shell.d.ts.map +1 -1
- package/lib/browser/shell/application-shell.js +9 -3
- package/lib/browser/shell/application-shell.js.map +1 -1
- package/lib/browser/shell/tab-bar-toolbar/tab-bar-toolbar-menu-adapters.js +6 -6
- package/lib/browser/shell/tab-bar-toolbar/tab-bar-toolbar-menu-adapters.js.map +1 -1
- package/lib/browser/shell/tab-bar-toolbar/tab-bar-toolbar-registry.js +2 -2
- package/lib/browser/shell/tab-bar-toolbar/tab-bar-toolbar-registry.js.map +1 -1
- package/lib/browser/shell/tab-bar-toolbar/tab-bar-toolbar.spec.js +131 -0
- package/lib/browser/shell/tab-bar-toolbar/tab-bar-toolbar.spec.js.map +1 -1
- package/lib/browser/status-bar/status-bar.d.ts +2 -2
- package/lib/browser/status-bar/status-bar.d.ts.map +1 -1
- package/lib/browser/status-bar/status-bar.js.map +1 -1
- package/lib/browser/test/jsdom.d.ts +4 -0
- package/lib/browser/test/jsdom.d.ts.map +1 -1
- package/lib/browser/test/jsdom.js +18 -0
- package/lib/browser/test/jsdom.js.map +1 -1
- package/lib/browser/tree/tree-container.spec.js +4 -0
- package/lib/browser/tree/tree-container.spec.js.map +1 -1
- package/lib/browser/tree/tree-model.d.ts +1 -0
- package/lib/browser/tree/tree-model.d.ts.map +1 -1
- package/lib/browser/tree/tree-model.js +3 -0
- package/lib/browser/tree/tree-model.js.map +1 -1
- package/lib/browser/tree/tree-view-welcome-widget.d.ts +1 -0
- package/lib/browser/tree/tree-view-welcome-widget.d.ts.map +1 -1
- package/lib/browser/tree/tree-view-welcome-widget.js +8 -6
- package/lib/browser/tree/tree-view-welcome-widget.js.map +1 -1
- package/lib/browser/tree/tree-view-welcome-widget.spec.d.ts +2 -0
- package/lib/browser/tree/tree-view-welcome-widget.spec.d.ts.map +1 -0
- package/lib/browser/tree/tree-view-welcome-widget.spec.js +68 -0
- package/lib/browser/tree/tree-view-welcome-widget.spec.js.map +1 -0
- package/lib/browser/tree/tree-widget.js +2 -2
- package/lib/browser/tree/tree-widget.js.map +1 -1
- package/lib/browser/tree/tree.d.ts +24 -2
- package/lib/browser/tree/tree.d.ts.map +1 -1
- package/lib/browser/tree/tree.js +20 -2
- package/lib/browser/tree/tree.js.map +1 -1
- package/lib/browser/tree/tree.spec.js +27 -0
- package/lib/browser/tree/tree.spec.js.map +1 -1
- package/lib/browser/window/default-window-service.d.ts +2 -1
- package/lib/browser/window/default-window-service.d.ts.map +1 -1
- package/lib/browser/window/default-window-service.js +10 -5
- package/lib/browser/window/default-window-service.js.map +1 -1
- package/lib/browser/window/default-window-service.spec.js +2 -0
- package/lib/browser/window/default-window-service.spec.js.map +1 -1
- package/lib/browser-only/messaging/frontend-only-service-connection-provider.d.ts +2 -0
- package/lib/browser-only/messaging/frontend-only-service-connection-provider.d.ts.map +1 -1
- package/lib/browser-only/messaging/frontend-only-service-connection-provider.js +8 -2
- package/lib/browser-only/messaging/frontend-only-service-connection-provider.js.map +1 -1
- package/lib/common/command.d.ts +2 -0
- package/lib/common/command.d.ts.map +1 -1
- package/lib/common/command.js +10 -4
- package/lib/common/command.js.map +1 -1
- package/lib/common/preferences/injectable-preference-proxy.d.ts +3 -3
- package/lib/common/preferences/injectable-preference-proxy.d.ts.map +1 -1
- package/lib/common/preferences/injectable-preference-proxy.js +6 -1
- package/lib/common/preferences/injectable-preference-proxy.js.map +1 -1
- package/lib/common/preferences/preference-proxy.d.ts.map +1 -1
- package/lib/common/preferences/preference-proxy.js +6 -1
- package/lib/common/preferences/preference-proxy.js.map +1 -1
- package/lib/electron-main/electron-main-application.d.ts.map +1 -1
- package/lib/electron-main/electron-main-application.js +4 -2
- package/lib/electron-main/electron-main-application.js.map +1 -1
- package/lib/electron-node/hosting/electron-ws-origin-validator.d.ts.map +1 -1
- package/lib/electron-node/hosting/electron-ws-origin-validator.js +5 -2
- package/lib/electron-node/hosting/electron-ws-origin-validator.js.map +1 -1
- package/lib/electron-node/hosting/electron-ws-origin-validator.spec.d.ts +2 -0
- package/lib/electron-node/hosting/electron-ws-origin-validator.spec.d.ts.map +1 -0
- package/lib/electron-node/hosting/electron-ws-origin-validator.spec.js +59 -0
- package/lib/electron-node/hosting/electron-ws-origin-validator.spec.js.map +1 -0
- package/lib/electron-node/request/electron-backend-request-service.d.ts +2 -0
- package/lib/electron-node/request/electron-backend-request-service.d.ts.map +1 -1
- package/lib/electron-node/request/electron-backend-request-service.js +7 -1
- package/lib/electron-node/request/electron-backend-request-service.js.map +1 -1
- package/lib/electron-node/token/electron-token-backend-contribution.d.ts +2 -0
- package/lib/electron-node/token/electron-token-backend-contribution.d.ts.map +1 -1
- package/lib/electron-node/token/electron-token-backend-contribution.js +7 -1
- package/lib/electron-node/token/electron-token-backend-contribution.js.map +1 -1
- package/lib/electron-node/token/electron-token-validator.d.ts +2 -0
- package/lib/electron-node/token/electron-token-validator.d.ts.map +1 -1
- package/lib/electron-node/token/electron-token-validator.js +7 -1
- package/lib/electron-node/token/electron-token-validator.js.map +1 -1
- package/lib/node/backend-application-module.d.ts.map +1 -1
- package/lib/node/backend-application-module.js +2 -0
- package/lib/node/backend-application-module.js.map +1 -1
- package/lib/node/backend-application.d.ts +11 -0
- package/lib/node/backend-application.d.ts.map +1 -1
- package/lib/node/backend-application.js +40 -12
- package/lib/node/backend-application.js.map +1 -1
- package/lib/node/backend-application.spec.js +4 -2
- package/lib/node/backend-application.spec.js.map +1 -1
- package/lib/node/hosting/backend-hosting-module.d.ts.map +1 -1
- package/lib/node/hosting/backend-hosting-module.js +7 -0
- package/lib/node/hosting/backend-hosting-module.js.map +1 -1
- package/lib/node/hosting/browser-connection-token.d.ts +46 -0
- package/lib/node/hosting/browser-connection-token.d.ts.map +1 -0
- package/lib/node/hosting/browser-connection-token.js +120 -0
- package/lib/node/hosting/browser-connection-token.js.map +1 -0
- package/lib/node/hosting/browser-connection-token.spec.d.ts +2 -0
- package/lib/node/hosting/browser-connection-token.spec.d.ts.map +1 -0
- package/lib/node/hosting/browser-connection-token.spec.js +128 -0
- package/lib/node/hosting/browser-connection-token.spec.js.map +1 -0
- package/lib/node/hosting/ws-origin-validator.d.ts.map +1 -1
- package/lib/node/hosting/ws-origin-validator.js +21 -4
- package/lib/node/hosting/ws-origin-validator.js.map +1 -1
- package/lib/node/hosting/ws-origin-validator.spec.d.ts +2 -0
- package/lib/node/hosting/ws-origin-validator.spec.d.ts.map +1 -0
- package/lib/node/hosting/ws-origin-validator.spec.js +131 -0
- package/lib/node/hosting/ws-origin-validator.spec.js.map +1 -0
- package/lib/node/index.d.ts +1 -0
- package/lib/node/index.d.ts.map +1 -1
- package/lib/node/index.js +3 -1
- package/lib/node/index.js.map +1 -1
- package/lib/node/logger-cli-contribution.d.ts +6 -0
- package/lib/node/logger-cli-contribution.d.ts.map +1 -1
- package/lib/node/logger-cli-contribution.js +28 -5
- package/lib/node/logger-cli-contribution.js.map +1 -1
- package/lib/node/logger-cli-contribution.spec.js +54 -0
- package/lib/node/logger-cli-contribution.spec.js.map +1 -1
- package/lib/node/messaging/default-messaging-service.d.ts +2 -0
- package/lib/node/messaging/default-messaging-service.d.ts.map +1 -1
- package/lib/node/messaging/default-messaging-service.js +8 -2
- package/lib/node/messaging/default-messaging-service.js.map +1 -1
- package/lib/node/messaging/test/default-messaging-service.spec.js +2 -0
- package/lib/node/messaging/test/default-messaging-service.spec.js.map +1 -1
- package/lib/node/messaging/websocket-endpoint.d.ts +7 -0
- package/lib/node/messaging/websocket-endpoint.d.ts.map +1 -1
- package/lib/node/messaging/websocket-endpoint.js +21 -6
- package/lib/node/messaging/websocket-endpoint.js.map +1 -1
- package/lib/node/process-utils.d.ts +2 -0
- package/lib/node/process-utils.d.ts.map +1 -1
- package/lib/node/process-utils.js +8 -2
- package/lib/node/process-utils.js.map +1 -1
- package/lib/node/process-utils.spec.js +11 -9
- package/lib/node/process-utils.spec.js.map +1 -1
- package/lib/node/request/backend-request-facade.d.ts +19 -0
- package/lib/node/request/backend-request-facade.d.ts.map +1 -1
- package/lib/node/request/backend-request-facade.js +93 -3
- package/lib/node/request/backend-request-facade.js.map +1 -1
- package/lib/node/request/backend-request-facade.spec.d.ts +2 -0
- package/lib/node/request/backend-request-facade.spec.d.ts.map +1 -0
- package/lib/node/request/backend-request-facade.spec.js +171 -0
- package/lib/node/request/backend-request-facade.spec.js.map +1 -0
- package/package.json +29 -20
- package/src/browser/authentication-service.ts +6 -3
- package/src/browser/frontend-application.ts +13 -9
- package/src/browser/json-schema-store.ts +6 -2
- package/src/browser/keybinding.ts +2 -2
- package/src/browser/markdown-rendering/markdown.tsx +2 -2
- package/src/browser/menu/composite-menu-node.ts +4 -4
- package/src/browser/messaging/ws-connection-source.ts +1 -6
- package/src/browser/preferences/frontend-config-preference-contributions.ts +7 -2
- package/src/browser/preferences/preference-proxy.spec.ts +8 -0
- package/src/browser/preferences/preference-schema-provider.spec.ts +5 -0
- package/src/browser/preferences/preference-validation-service.spec.ts +3 -1
- package/src/browser/preferences/preference-validation-service.ts +9 -6
- package/src/browser/secondary-window-handler.ts +11 -7
- package/src/browser/shell/application-shell.ts +8 -4
- package/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar-menu-adapters.tsx +6 -6
- package/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar-registry.ts +2 -2
- package/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar.spec.ts +159 -1
- package/src/browser/status-bar/status-bar.tsx +3 -3
- package/src/browser/test/jsdom.ts +18 -0
- package/src/browser/tree/tree-container.spec.ts +6 -0
- package/src/browser/tree/tree-model.ts +4 -0
- package/src/browser/tree/tree-view-welcome-widget.spec.ts +82 -0
- package/src/browser/tree/tree-view-welcome-widget.tsx +10 -11
- package/src/browser/tree/tree-widget.tsx +2 -2
- package/src/browser/tree/tree.spec.ts +30 -0
- package/src/browser/tree/tree.ts +31 -4
- package/src/browser/window/default-window-service.spec.ts +3 -1
- package/src/browser/window/default-window-service.ts +9 -6
- package/src/browser-only/messaging/frontend-only-service-connection-provider.ts +8 -3
- package/src/common/command.ts +8 -4
- package/src/common/i18n/nls.metadata.json +2659 -374
- package/src/common/preferences/injectable-preference-proxy.ts +10 -4
- package/src/common/preferences/preference-proxy.ts +10 -4
- package/src/electron-main/electron-main-application.ts +4 -2
- package/src/electron-node/hosting/electron-ws-origin-validator.spec.ts +69 -0
- package/src/electron-node/hosting/electron-ws-origin-validator.ts +5 -2
- package/src/electron-node/request/electron-backend-request-service.ts +6 -2
- package/src/electron-node/token/electron-token-backend-contribution.ts +6 -2
- package/src/electron-node/token/electron-token-validator.ts +6 -2
- package/src/node/backend-application-module.ts +4 -2
- package/src/node/backend-application.spec.ts +5 -3
- package/src/node/backend-application.ts +33 -11
- package/src/node/hosting/backend-hosting-module.ts +10 -0
- package/src/node/hosting/browser-connection-token.spec.ts +167 -0
- package/src/node/hosting/browser-connection-token.ts +126 -0
- package/src/node/hosting/ws-origin-validator.spec.ts +193 -0
- package/src/node/hosting/ws-origin-validator.ts +23 -4
- package/src/node/index.ts +1 -0
- package/src/node/logger-cli-contribution.spec.ts +65 -0
- package/src/node/logger-cli-contribution.ts +32 -5
- package/src/node/messaging/default-messaging-service.ts +6 -2
- package/src/node/messaging/test/default-messaging-service.spec.ts +3 -1
- package/src/node/messaging/websocket-endpoint.ts +23 -7
- package/src/node/process-utils.spec.ts +12 -9
- package/src/node/process-utils.ts +7 -3
- package/src/node/request/backend-request-facade.spec.ts +203 -0
- package/src/node/request/backend-request-facade.ts +108 -3
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 STMicroelectronics and others.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { expect } from 'chai';
|
|
18
|
+
import * as http from 'http';
|
|
19
|
+
import {
|
|
20
|
+
BrowserConnectionToken,
|
|
21
|
+
BrowserConnectionTokenBackendContribution,
|
|
22
|
+
BROWSER_TOKEN_COOKIE_NAME,
|
|
23
|
+
createBrowserConnectionToken
|
|
24
|
+
} from './browser-connection-token';
|
|
25
|
+
|
|
26
|
+
describe('BrowserConnectionToken', () => {
|
|
27
|
+
|
|
28
|
+
it('should generate unique tokens', () => {
|
|
29
|
+
const token1 = createBrowserConnectionToken();
|
|
30
|
+
const token2 = createBrowserConnectionToken();
|
|
31
|
+
expect(token1.value).to.not.equal(token2.value);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should generate tokens of sufficient length', () => {
|
|
35
|
+
const token = createBrowserConnectionToken();
|
|
36
|
+
expect(token.value.length).to.be.greaterThan(16);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('BrowserConnectionTokenBackendContribution', () => {
|
|
41
|
+
|
|
42
|
+
const token: BrowserConnectionToken = createBrowserConnectionToken();
|
|
43
|
+
|
|
44
|
+
function createContribution(): BrowserConnectionTokenBackendContribution {
|
|
45
|
+
const contribution = new BrowserConnectionTokenBackendContribution();
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
47
|
+
(contribution as any)['browserConnectionToken'] = token;
|
|
48
|
+
return contribution;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function createRequest(cookieValue?: string): http.IncomingMessage {
|
|
52
|
+
const request = {
|
|
53
|
+
headers: {} as http.IncomingHttpHeaders
|
|
54
|
+
} as http.IncomingMessage;
|
|
55
|
+
if (cookieValue !== undefined) {
|
|
56
|
+
request.headers.cookie = `${BROWSER_TOKEN_COOKIE_NAME}=${cookieValue}`;
|
|
57
|
+
}
|
|
58
|
+
return request;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
describe('allowWsUpgrade (WebSocket validation)', () => {
|
|
62
|
+
|
|
63
|
+
it('should allow WebSocket with valid token cookie', () => {
|
|
64
|
+
const contribution = createContribution();
|
|
65
|
+
expect(contribution.allowWsUpgrade(createRequest(token.value))).to.be.true;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should reject WebSocket with no cookie', () => {
|
|
69
|
+
const contribution = createContribution();
|
|
70
|
+
expect(contribution.allowWsUpgrade(createRequest())).to.be.false;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should reject WebSocket with invalid token', () => {
|
|
74
|
+
const contribution = createContribution();
|
|
75
|
+
expect(contribution.allowWsUpgrade(createRequest('wrong-token'))).to.be.false;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should reject WebSocket with stale token from previous server', () => {
|
|
79
|
+
const contribution = createContribution();
|
|
80
|
+
const staleToken = createBrowserConnectionToken().value;
|
|
81
|
+
expect(contribution.allowWsUpgrade(createRequest(staleToken))).to.be.false;
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should reject WebSocket with empty token', () => {
|
|
85
|
+
const contribution = createContribution();
|
|
86
|
+
expect(contribution.allowWsUpgrade(createRequest(''))).to.be.false;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should handle multiple cookies correctly', () => {
|
|
90
|
+
const contribution = createContribution();
|
|
91
|
+
const request = {
|
|
92
|
+
headers: {
|
|
93
|
+
cookie: `other-cookie=foo; ${BROWSER_TOKEN_COOKIE_NAME}=${token.value}; another=bar`
|
|
94
|
+
} as http.IncomingHttpHeaders
|
|
95
|
+
} as http.IncomingMessage;
|
|
96
|
+
expect(contribution.allowWsUpgrade(request)).to.be.true;
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('expressMiddleware (HTTP cookie flow)', () => {
|
|
101
|
+
|
|
102
|
+
interface CookieCall {
|
|
103
|
+
name: string;
|
|
104
|
+
value: string;
|
|
105
|
+
options: Record<string, unknown>;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
interface MockResponse {
|
|
109
|
+
cookieCalls: CookieCall[];
|
|
110
|
+
nextCalled: boolean;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function callMiddleware(cookieValue?: string): MockResponse {
|
|
114
|
+
const contribution = createContribution();
|
|
115
|
+
const result: MockResponse = { cookieCalls: [], nextCalled: false };
|
|
116
|
+
|
|
117
|
+
const req = {
|
|
118
|
+
headers: {} as Record<string, string | undefined>,
|
|
119
|
+
socket: { remoteAddress: '127.0.0.1' }
|
|
120
|
+
};
|
|
121
|
+
if (cookieValue !== undefined) {
|
|
122
|
+
req.headers.cookie = `${BROWSER_TOKEN_COOKIE_NAME}=${cookieValue}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const res = {
|
|
126
|
+
cookie: (name: string, value: string, options: Record<string, unknown>) => {
|
|
127
|
+
result.cookieCalls.push({ name, value, options });
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const next = (): void => { result.nextCalled = true; };
|
|
132
|
+
|
|
133
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
134
|
+
(contribution as any).expressMiddleware(req, res, next);
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
it('should set cookie and allow request when no cookie present', () => {
|
|
139
|
+
const result = callMiddleware();
|
|
140
|
+
expect(result.nextCalled).to.be.true;
|
|
141
|
+
expect(result.cookieCalls).to.have.length(1);
|
|
142
|
+
expect(result.cookieCalls[0].name).to.equal(BROWSER_TOKEN_COOKIE_NAME);
|
|
143
|
+
expect(result.cookieCalls[0].value).to.equal(token.value);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should set cookie with HttpOnly, SameSite=Strict, and path=/', () => {
|
|
147
|
+
const result = callMiddleware();
|
|
148
|
+
const opts = result.cookieCalls[0].options;
|
|
149
|
+
expect(opts.httpOnly).to.be.true;
|
|
150
|
+
expect(opts.sameSite).to.equal('strict');
|
|
151
|
+
expect(opts.path).to.equal('/');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should allow request with valid cookie without re-setting it', () => {
|
|
155
|
+
const result = callMiddleware(token.value);
|
|
156
|
+
expect(result.nextCalled).to.be.true;
|
|
157
|
+
expect(result.cookieCalls).to.have.length(0);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should refresh stale cookie and allow request', () => {
|
|
161
|
+
const result = callMiddleware('stale-token-from-old-server');
|
|
162
|
+
expect(result.nextCalled).to.be.true;
|
|
163
|
+
expect(result.cookieCalls).to.have.length(1);
|
|
164
|
+
expect(result.cookieCalls[0].value).to.equal(token.value);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 STMicroelectronics and others.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import * as cookie from 'cookie';
|
|
18
|
+
import * as crypto from 'crypto';
|
|
19
|
+
import * as http from 'http';
|
|
20
|
+
import express = require('express');
|
|
21
|
+
import { inject, injectable } from 'inversify';
|
|
22
|
+
import { environment } from '../../common/index';
|
|
23
|
+
import { MaybePromise } from '../../common';
|
|
24
|
+
import { BackendApplicationContribution, EarlyExpressMiddleware } from '../backend-application';
|
|
25
|
+
import { WsRequestValidatorContribution } from '../ws-request-validators';
|
|
26
|
+
import { generateUuid } from '../../common/uuid';
|
|
27
|
+
|
|
28
|
+
export const BrowserConnectionToken = Symbol('BrowserConnectionToken');
|
|
29
|
+
|
|
30
|
+
export const BROWSER_TOKEN_COOKIE_NAME = 'theia-connection-token';
|
|
31
|
+
|
|
32
|
+
export interface BrowserConnectionToken {
|
|
33
|
+
value: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Validates WebSocket and HTTP requests using a cookie-based connection token.
|
|
38
|
+
*
|
|
39
|
+
* In browser deployments, the server generates a random token at startup and sets it
|
|
40
|
+
* as a `SameSite=Strict; HttpOnly` cookie on the first page load. Cross-origin pages
|
|
41
|
+
* cannot obtain or send this cookie, so their requests are rejected.
|
|
42
|
+
*
|
|
43
|
+
* This complements the origin validator: non-browser callers that omit the Origin
|
|
44
|
+
* header (e.g. Node.js scripts) still cannot reach the backend without the cookie.
|
|
45
|
+
*
|
|
46
|
+
* Skipped in Electron deployments (which use their own `ElectronSecurityToken`).
|
|
47
|
+
*/
|
|
48
|
+
@injectable()
|
|
49
|
+
export class BrowserConnectionTokenBackendContribution implements BackendApplicationContribution, WsRequestValidatorContribution {
|
|
50
|
+
|
|
51
|
+
@inject(BrowserConnectionToken)
|
|
52
|
+
protected readonly browserConnectionToken: BrowserConnectionToken;
|
|
53
|
+
|
|
54
|
+
@inject(EarlyExpressMiddleware)
|
|
55
|
+
protected readonly earlyMiddleware: EarlyExpressMiddleware;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Register the cookie middleware during `initialize()` via `EarlyExpressMiddleware`
|
|
59
|
+
* so it runs before `express.static()` (which is registered later during `configure()`).
|
|
60
|
+
* This ensures the browser receives the token cookie on the initial page load.
|
|
61
|
+
*/
|
|
62
|
+
initialize(): void {
|
|
63
|
+
if (environment.electron.is()) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
this.earlyMiddleware.handlers.push((req, res, next) => this.expressMiddleware(req, res, next));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Validate the connection token cookie on WebSocket upgrade requests.
|
|
71
|
+
* Non-browser callers that omit the Origin header (e.g. Node.js scripts)
|
|
72
|
+
* cannot provide the `SameSite=Strict` cookie either, so they are rejected.
|
|
73
|
+
*/
|
|
74
|
+
allowWsUpgrade(request: http.IncomingMessage): MaybePromise<boolean> {
|
|
75
|
+
if (environment.electron.is()) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
const token = this.getTokenFromCookie(request);
|
|
79
|
+
if (token) {
|
|
80
|
+
return this.isTokenValid(token);
|
|
81
|
+
}
|
|
82
|
+
// No cookie: reject. Legitimate browsers always have the cookie
|
|
83
|
+
// because it is set on the initial page load.
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
protected expressMiddleware(req: express.Request, res: express.Response, next: express.NextFunction): void {
|
|
88
|
+
const existing = this.getTokenFromCookie(req);
|
|
89
|
+
if (!existing || !this.isTokenValid(existing)) {
|
|
90
|
+
// No cookie or stale cookie (e.g. after server restart) so (re-)issue it.
|
|
91
|
+
// The browser will use the fresh token on subsequent requests.
|
|
92
|
+
res.cookie(BROWSER_TOKEN_COOKIE_NAME, this.browserConnectionToken.value, {
|
|
93
|
+
httpOnly: true,
|
|
94
|
+
sameSite: 'strict',
|
|
95
|
+
path: '/'
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
next();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
protected getTokenFromCookie(req: http.IncomingMessage): string | undefined {
|
|
102
|
+
const cookieHeader = req.headers.cookie;
|
|
103
|
+
if (cookieHeader) {
|
|
104
|
+
return cookie.parse(cookieHeader)[BROWSER_TOKEN_COOKIE_NAME];
|
|
105
|
+
}
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
protected isTokenValid(token: string): boolean {
|
|
110
|
+
try {
|
|
111
|
+
const received = Buffer.from(token, 'utf8');
|
|
112
|
+
const expected = Buffer.from(this.browserConnectionToken.value, 'utf8');
|
|
113
|
+
return received.byteLength === expected.byteLength && crypto.timingSafeEqual(received, expected);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.error(error);
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Creates a new browser connection token.
|
|
123
|
+
*/
|
|
124
|
+
export function createBrowserConnectionToken(): BrowserConnectionToken {
|
|
125
|
+
return { value: generateUuid() };
|
|
126
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 STMicroelectronics and others.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { expect } from 'chai';
|
|
18
|
+
import * as http from 'http';
|
|
19
|
+
import { WsOriginValidator } from './ws-origin-validator';
|
|
20
|
+
import { BackendApplicationHosts } from './backend-application-hosts';
|
|
21
|
+
|
|
22
|
+
describe('WsOriginValidator', () => {
|
|
23
|
+
|
|
24
|
+
function createValidator(hosts?: string[]): WsOriginValidator {
|
|
25
|
+
const backendHosts = new BackendApplicationHosts();
|
|
26
|
+
if (hosts) {
|
|
27
|
+
for (const h of hosts) {
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
29
|
+
(backendHosts as any)['_hosts'].add(h);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const validator = new WsOriginValidator();
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
34
|
+
(validator as any)['backendApplicationHosts'] = backendHosts;
|
|
35
|
+
return validator;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createRequest(origin?: string, host?: string): http.IncomingMessage {
|
|
39
|
+
const request = {
|
|
40
|
+
headers: {} as http.IncomingHttpHeaders
|
|
41
|
+
} as http.IncomingMessage;
|
|
42
|
+
if (origin !== undefined) {
|
|
43
|
+
request.headers.origin = origin;
|
|
44
|
+
}
|
|
45
|
+
if (host !== undefined) {
|
|
46
|
+
request.headers.host = host;
|
|
47
|
+
}
|
|
48
|
+
return request;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('when THEIA_HOSTS is not set', () => {
|
|
52
|
+
|
|
53
|
+
it('should allow same-origin requests', () => {
|
|
54
|
+
const validator = createValidator();
|
|
55
|
+
expect(validator.allowWsUpgrade(
|
|
56
|
+
createRequest('http://localhost:3000', 'localhost:3000')
|
|
57
|
+
)).to.be.true;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should allow same-origin requests on default ports', () => {
|
|
61
|
+
const validator = createValidator();
|
|
62
|
+
expect(validator.allowWsUpgrade(
|
|
63
|
+
createRequest('http://myhost.com', 'myhost.com')
|
|
64
|
+
)).to.be.true;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should reject cross-origin requests', () => {
|
|
68
|
+
const validator = createValidator();
|
|
69
|
+
expect(validator.allowWsUpgrade(
|
|
70
|
+
createRequest('http://evil.com', 'localhost:3000')
|
|
71
|
+
)).to.be.false;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should allow requests with no Origin header (same-origin polling)', () => {
|
|
75
|
+
const validator = createValidator();
|
|
76
|
+
expect(validator.allowWsUpgrade(
|
|
77
|
+
createRequest(undefined, 'localhost:3000')
|
|
78
|
+
)).to.be.true;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should reject requests with no Host header when Origin is present', () => {
|
|
82
|
+
const validator = createValidator();
|
|
83
|
+
expect(validator.allowWsUpgrade(
|
|
84
|
+
createRequest('http://localhost:3000', undefined)
|
|
85
|
+
)).to.be.false;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should reject requests with malformed Origin', () => {
|
|
89
|
+
const validator = createValidator();
|
|
90
|
+
expect(validator.allowWsUpgrade(
|
|
91
|
+
createRequest('not-a-valid-url', 'localhost:3000')
|
|
92
|
+
)).to.be.false;
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('when THEIA_HOSTS is set', () => {
|
|
97
|
+
|
|
98
|
+
it('should allow requests from a listed host', () => {
|
|
99
|
+
const validator = createValidator(['myhost.com']);
|
|
100
|
+
expect(validator.allowWsUpgrade(
|
|
101
|
+
createRequest('http://myhost.com', 'myhost.com')
|
|
102
|
+
)).to.be.true;
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should allow requests from a listed host with port', () => {
|
|
106
|
+
const validator = createValidator(['myhost.com:8080']);
|
|
107
|
+
expect(validator.allowWsUpgrade(
|
|
108
|
+
createRequest('http://myhost.com:8080', 'myhost.com:8080')
|
|
109
|
+
)).to.be.true;
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should reject requests from an unlisted host', () => {
|
|
113
|
+
const validator = createValidator(['myhost.com']);
|
|
114
|
+
expect(validator.allowWsUpgrade(
|
|
115
|
+
createRequest('http://evil.com', 'myhost.com')
|
|
116
|
+
)).to.be.false;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should allow requests with no Origin header (same-origin polling)', () => {
|
|
120
|
+
const validator = createValidator(['myhost.com']);
|
|
121
|
+
expect(validator.allowWsUpgrade(
|
|
122
|
+
createRequest(undefined, 'myhost.com')
|
|
123
|
+
)).to.be.true;
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should allow when origin host is in the allowlist regardless of Host header', () => {
|
|
127
|
+
const validator = createValidator(['myhost.com']);
|
|
128
|
+
expect(validator.allowWsUpgrade(
|
|
129
|
+
createRequest('http://myhost.com', 'internal-host:3000')
|
|
130
|
+
)).to.be.true;
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should reject malformed Origin even with THEIA_HOSTS', () => {
|
|
134
|
+
const validator = createValidator(['myhost.com']);
|
|
135
|
+
expect(validator.allowWsUpgrade(
|
|
136
|
+
createRequest('not-a-valid-url', 'myhost.com')
|
|
137
|
+
)).to.be.false;
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('edge cases', () => {
|
|
142
|
+
|
|
143
|
+
it('should reject cross-origin with port mismatch', () => {
|
|
144
|
+
const validator = createValidator();
|
|
145
|
+
expect(validator.allowWsUpgrade(
|
|
146
|
+
createRequest('http://localhost:4000', 'localhost:3000')
|
|
147
|
+
)).to.be.false;
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should handle HTTPS origins', () => {
|
|
151
|
+
const validator = createValidator();
|
|
152
|
+
expect(validator.allowWsUpgrade(
|
|
153
|
+
createRequest('https://localhost:3000', 'localhost:3000')
|
|
154
|
+
)).to.be.true;
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should reject Origin: null (opaque origin from sandboxed pages)', () => {
|
|
158
|
+
const validator = createValidator();
|
|
159
|
+
expect(validator.allowWsUpgrade(
|
|
160
|
+
createRequest('null', 'localhost:3000')
|
|
161
|
+
)).to.be.false;
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should allow empty Origin string (treated as absent)', () => {
|
|
165
|
+
const validator = createValidator();
|
|
166
|
+
expect(validator.allowWsUpgrade(
|
|
167
|
+
createRequest('', 'localhost:3000')
|
|
168
|
+
)).to.be.true;
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should handle IPv6 same-origin', () => {
|
|
172
|
+
const validator = createValidator();
|
|
173
|
+
expect(validator.allowWsUpgrade(
|
|
174
|
+
createRequest('http://[::1]:3000', '[::1]:3000')
|
|
175
|
+
)).to.be.true;
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('should strip credentials from Origin when comparing', () => {
|
|
179
|
+
const validator = createValidator();
|
|
180
|
+
// new URL('http://user:pass@localhost:3000').host === 'localhost:3000'
|
|
181
|
+
expect(validator.allowWsUpgrade(
|
|
182
|
+
createRequest('http://user:pass@localhost:3000', 'localhost:3000')
|
|
183
|
+
)).to.be.true;
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should ignore path component in Origin', () => {
|
|
187
|
+
const validator = createValidator();
|
|
188
|
+
expect(validator.allowWsUpgrade(
|
|
189
|
+
createRequest('http://localhost:3000/some/path', 'localhost:3000')
|
|
190
|
+
)).to.be.true;
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
});
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
|
|
17
17
|
import * as http from 'http';
|
|
18
18
|
import { inject, injectable } from 'inversify';
|
|
19
|
-
import * as url from 'url';
|
|
20
19
|
import { WsRequestValidatorContribution } from '../ws-request-validators';
|
|
21
20
|
import { BackendApplicationHosts } from './backend-application-hosts';
|
|
22
21
|
|
|
@@ -27,10 +26,30 @@ export class WsOriginValidator implements WsRequestValidatorContribution {
|
|
|
27
26
|
protected readonly backendApplicationHosts: BackendApplicationHosts;
|
|
28
27
|
|
|
29
28
|
allowWsUpgrade(request: http.IncomingMessage): boolean {
|
|
30
|
-
if (!
|
|
29
|
+
if (!request.headers.origin) {
|
|
30
|
+
// Browsers omit the Origin header for same-origin requests (e.g. Socket.IO polling).
|
|
31
|
+
// Absent Origin is safe: cross-origin browser requests always include it.
|
|
31
32
|
return true;
|
|
32
33
|
}
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
|
|
35
|
+
let originHost: string;
|
|
36
|
+
try {
|
|
37
|
+
originHost = new URL(request.headers.origin).host;
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (this.backendApplicationHosts.hasKnownHosts()) {
|
|
43
|
+
// When THEIA_HOSTS is configured, validate against the explicit allowlist.
|
|
44
|
+
return this.backendApplicationHosts.hosts.has(originHost);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// When THEIA_HOSTS is not configured (the common development/default case),
|
|
48
|
+
// enforce same-origin: the Origin's host must match the request's Host header.
|
|
49
|
+
const hostHeader = request.headers.host;
|
|
50
|
+
if (!hostHeader) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
return originHost === hostHeader;
|
|
35
54
|
}
|
|
36
55
|
}
|
package/src/node/index.ts
CHANGED
|
@@ -257,4 +257,69 @@ describe('log-level-cli-contribution', () => {
|
|
|
257
257
|
world: LogLevel.FATAL,
|
|
258
258
|
});
|
|
259
259
|
});
|
|
260
|
+
|
|
261
|
+
describe('Wildcard Matching', () => {
|
|
262
|
+
|
|
263
|
+
async function setupLogLevels(levels: { [key: string]: string }): Promise<void> {
|
|
264
|
+
const file = track.openSync();
|
|
265
|
+
fs.writeFileSync(file.fd, JSON.stringify({
|
|
266
|
+
defaultLevel: 'info',
|
|
267
|
+
levels
|
|
268
|
+
}));
|
|
269
|
+
fs.fsyncSync(file.fd);
|
|
270
|
+
fs.closeSync(file.fd);
|
|
271
|
+
|
|
272
|
+
const args: yargs.Arguments = await yargs.parse(['--log-config', file.path]);
|
|
273
|
+
await cli.setArguments(args);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
it('should respect exact matches without wildcards', async () => {
|
|
277
|
+
await setupLogLevels({
|
|
278
|
+
'unrelated:Service': 'debug'
|
|
279
|
+
});
|
|
280
|
+
expect(cli.logLevelFor('unrelated:Service')).to.equal(LogLevel.DEBUG);
|
|
281
|
+
expect(cli.logLevelFor('something-else')).to.equal(LogLevel.INFO);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('should match prefix wildcards', async () => {
|
|
285
|
+
await setupLogLevels({
|
|
286
|
+
'ai-core*': 'error'
|
|
287
|
+
});
|
|
288
|
+
expect(cli.logLevelFor('ai-core:SomeOtherService')).to.equal(LogLevel.ERROR);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('should match suffix wildcards', async () => {
|
|
292
|
+
await setupLogLevels({
|
|
293
|
+
'*TokenUsageFrontendServiceImpl': 'warn'
|
|
294
|
+
});
|
|
295
|
+
expect(cli.logLevelFor('my-package:MyTokenUsageFrontendServiceImpl')).to.equal(LogLevel.WARN);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('should match middle wildcards', async () => {
|
|
299
|
+
await setupLogLevels({
|
|
300
|
+
'*ai*:*': 'warn'
|
|
301
|
+
});
|
|
302
|
+
expect(cli.logLevelFor('foo-ai-bar:SomeService')).to.equal(LogLevel.WARN);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('should enforce perfect match first, then last wildcard wins', async () => {
|
|
306
|
+
await setupLogLevels({
|
|
307
|
+
'ai-core*': 'error',
|
|
308
|
+
'*ai*:*': 'warn',
|
|
309
|
+
'ai-core:SpecificService': 'debug'
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
expect(cli.logLevelFor('ai-core:SpecificService')).to.equal(LogLevel.DEBUG);
|
|
313
|
+
|
|
314
|
+
expect(cli.logLevelFor('ai-core:OtherService')).to.equal(LogLevel.WARN);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('should properly escape regex specifics like periods', async () => {
|
|
318
|
+
await setupLogLevels({
|
|
319
|
+
'.*': 'error'
|
|
320
|
+
});
|
|
321
|
+
expect(cli.logLevelFor('anything-else')).to.equal(LogLevel.INFO);
|
|
322
|
+
expect(cli.logLevelFor('.*')).to.equal(LogLevel.ERROR);
|
|
323
|
+
});
|
|
324
|
+
});
|
|
260
325
|
});
|
|
@@ -23,6 +23,7 @@ import { AsyncSubscription, subscribe } from '@parcel/watcher';
|
|
|
23
23
|
import { Event, Emitter } from '../common/event';
|
|
24
24
|
import * as path from 'path';
|
|
25
25
|
import { Disposable, DisposableCollection } from '../common';
|
|
26
|
+
import { escapeRegExpCharacters } from '../common/strings';
|
|
26
27
|
|
|
27
28
|
/** Maps logger names to log levels. */
|
|
28
29
|
export interface LogLevels {
|
|
@@ -38,6 +39,7 @@ export interface LogLevels {
|
|
|
38
39
|
export class LogLevelCliContribution implements CliContribution, Disposable {
|
|
39
40
|
|
|
40
41
|
protected _logLevels: LogLevels = {};
|
|
42
|
+
protected wildcardRegexCache = new Map<string, RegExp>();
|
|
41
43
|
protected asyncSubscriptions: AsyncSubscription[] = [];
|
|
42
44
|
protected toDispose = new DisposableCollection();
|
|
43
45
|
|
|
@@ -190,6 +192,7 @@ export class LogLevelCliContribution implements CliContribution, Disposable {
|
|
|
190
192
|
|
|
191
193
|
this._defaultLogLevel = newDefaultLogLevel;
|
|
192
194
|
this._logLevels = newLogLevels;
|
|
195
|
+
this.wildcardRegexCache.clear();
|
|
193
196
|
|
|
194
197
|
console.log(`Successfully read new log config from ${filename}.`);
|
|
195
198
|
} catch (e) {
|
|
@@ -202,13 +205,23 @@ export class LogLevelCliContribution implements CliContribution, Disposable {
|
|
|
202
205
|
}
|
|
203
206
|
|
|
204
207
|
logLevelFor(loggerName: string): LogLevel {
|
|
205
|
-
|
|
208
|
+
if (loggerName in this._logLevels) {
|
|
209
|
+
return this._logLevels[loggerName];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const keys = Object.keys(this._logLevels);
|
|
213
|
+
for (let i = keys.length - 1; i >= 0; i--) {
|
|
214
|
+
const pattern = keys[i];
|
|
206
215
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
216
|
+
if (pattern.includes('*')) {
|
|
217
|
+
const regex = this.getWildcardRegex(pattern);
|
|
218
|
+
if (regex.test(loggerName)) {
|
|
219
|
+
return this._logLevels[pattern];
|
|
220
|
+
}
|
|
221
|
+
}
|
|
211
222
|
}
|
|
223
|
+
|
|
224
|
+
return this.defaultLogLevel;
|
|
212
225
|
}
|
|
213
226
|
|
|
214
227
|
/**
|
|
@@ -223,4 +236,18 @@ export class LogLevelCliContribution implements CliContribution, Disposable {
|
|
|
223
236
|
|
|
224
237
|
return level;
|
|
225
238
|
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Converts a wildcard string into a strict regular expression and caches it.
|
|
242
|
+
* Example: "ai-core*" -> /^ai-core.*$/
|
|
243
|
+
*/
|
|
244
|
+
protected getWildcardRegex(pattern: string): RegExp {
|
|
245
|
+
let regex = this.wildcardRegexCache.get(pattern);
|
|
246
|
+
if (!regex) {
|
|
247
|
+
const escapedPattern = pattern.split('*').map(escapeRegExpCharacters).join('.*');
|
|
248
|
+
regex = new RegExp(`^${escapedPattern}$`);
|
|
249
|
+
this.wildcardRegexCache.set(pattern, regex);
|
|
250
|
+
}
|
|
251
|
+
return regex;
|
|
252
|
+
}
|
|
226
253
|
}
|