Skip to content

Commit f43a175

Browse files
committed
Wrap web worker extension host in an iframe to get a different origin
1 parent 532c7b1 commit f43a175

5 files changed

Lines changed: 226 additions & 57 deletions

File tree

resources/serverless/code-web.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,9 @@ async function handleBuiltInExtension(req, res, parsedUrl) {
254254
if (!filePath) {
255255
return serveError(req, res, 400, `Bad request.`);
256256
}
257-
return serveFile(req, res, filePath);
257+
return serveFile(req, res, filePath, {
258+
'Access-Control-Allow-Origin': '*'
259+
});
258260
}
259261

260262
/**

src/vs/base/worker/defaultWorkerFactory.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@ function getWorker(workerId: string, label: string): Worker | Promise<Worker> {
2828
}
2929

3030
// ESM-comment-begin
31-
export function getWorkerBootstrapUrl(scriptPath: string, label: string): string {
32-
if (/^(http:)|(https:)|(file:)/.test(scriptPath)) {
31+
export function getWorkerBootstrapUrl(scriptPath: string, label: string, forceDataUri: boolean = false): string {
32+
if (forceDataUri || /^(http:)|(https:)|(file:)/.test(scriptPath)) {
3333
const currentUrl = String(window.location);
3434
const currentOrigin = currentUrl.substr(0, currentUrl.length - window.location.hash.length - window.location.search.length - window.location.pathname.length);
35-
if (scriptPath.substring(0, currentOrigin.length) !== currentOrigin) {
35+
if (forceDataUri || scriptPath.substring(0, currentOrigin.length) !== currentOrigin) {
3636
// this is the cross-origin case
3737
// i.e. the webpage is running at a different origin than where the scripts are loaded from
3838
const myPath = 'vs/base/worker/defaultWorkerFactory.js';

src/vs/workbench/api/common/extHostExtensionService.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import * as nls from 'vs/nls';
77
import * as path from 'vs/base/common/path';
8+
import * as platform from 'vs/base/common/platform';
89
import { originalFSPath, joinPath } from 'vs/base/common/resources';
910
import { Barrier, timeout } from 'vs/base/common/async';
1011
import { dispose, toDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle';
@@ -383,7 +384,14 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme
383384
subscriptions: [],
384385
get extensionUri() { return extensionDescription.extensionLocation; },
385386
get extensionPath() { return extensionDescription.extensionLocation.fsPath; },
386-
asAbsolutePath(relativePath: string) { return path.join(extensionDescription.extensionLocation.fsPath, relativePath); },
387+
asAbsolutePath(relativePath: string) {
388+
if (platform.isWeb) {
389+
// web worker
390+
return URI.joinPath(extensionDescription.extensionLocation, relativePath).toString();
391+
} else {
392+
return path.join(extensionDescription.extensionLocation.fsPath, relativePath);
393+
}
394+
},
387395
get storagePath() { return that._storagePath.workspaceValue(extensionDescription)?.fsPath; },
388396
get globalStoragePath() { return that._storagePath.globalValue(extensionDescription).fsPath; },
389397
get logPath() { return path.join(that._initData.logsLocation.fsPath, extensionDescription.identifier.value); },

src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts

Lines changed: 199 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import { getWorkerBootstrapUrl } from 'vs/base/worker/defaultWorkerFactory';
77
import { Emitter, Event } from 'vs/base/common/event';
8-
import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
8+
import { toDisposable, Disposable } from 'vs/base/common/lifecycle';
99
import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc';
1010
import { VSBuffer } from 'vs/base/common/buffer';
1111
import { createMessageOfType, MessageType, isMessageOfType } from 'vs/workbench/services/extensions/common/extensionHostProtocol';
@@ -16,6 +16,7 @@ import { ILabelService } from 'vs/platform/label/common/label';
1616
import { ILogService } from 'vs/platform/log/common/log';
1717
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
1818
import * as platform from 'vs/base/common/platform';
19+
import * as dom from 'vs/base/browser/dom';
1920
import { URI } from 'vs/base/common/uri';
2021
import { IExtensionHost, ExtensionHostLogFileName, ExtensionHostKind } from 'vs/workbench/services/extensions/common/extensions';
2122
import { IProductService } from 'vs/platform/product/common/productService';
@@ -24,6 +25,10 @@ import { joinPath } from 'vs/base/common/resources';
2425
import { Registry } from 'vs/platform/registry/common/platform';
2526
import { IOutputChannelRegistry, Extensions } from 'vs/workbench/services/output/common/output';
2627
import { localize } from 'vs/nls';
28+
import { generateUuid } from 'vs/base/common/uuid';
29+
import { canceled, onUnexpectedError } from 'vs/base/common/errors';
30+
31+
const WRAP_IN_IFRAME = true;
2732

2833
export interface IWebWorkerExtensionHostInitData {
2934
readonly autoStart: boolean;
@@ -34,17 +39,17 @@ export interface IWebWorkerExtensionHostDataProvider {
3439
getInitData(): Promise<IWebWorkerExtensionHostInitData>;
3540
}
3641

37-
export class WebWorkerExtensionHost implements IExtensionHost {
42+
export class WebWorkerExtensionHost extends Disposable implements IExtensionHost {
3843

3944
public readonly kind = ExtensionHostKind.LocalWebWorker;
4045
public readonly remoteAuthority = null;
4146

42-
private _toDispose = new DisposableStore();
43-
private _isTerminating: boolean = false;
44-
private _protocol?: IMessagePassingProtocol;
47+
private readonly _onDidExit = this._register(new Emitter<[number, string | null]>());
48+
public readonly onExit: Event<[number, string | null]> = this._onDidExit.event;
4549

46-
private readonly _onDidExit = new Emitter<[number, string | null]>();
47-
readonly onExit: Event<[number, string | null]> = this._onDidExit.event;
50+
private _isTerminating: boolean;
51+
private _protocolPromise: Promise<IMessagePassingProtocol> | null;
52+
private _protocol: IMessagePassingProtocol | null;
4853

4954
private readonly _extensionHostLogsLocation: URI;
5055
private readonly _extensionHostLogFile: URI;
@@ -58,76 +63,218 @@ export class WebWorkerExtensionHost implements IExtensionHost {
5863
@IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService,
5964
@IProductService private readonly _productService: IProductService,
6065
) {
66+
super();
67+
this._isTerminating = false;
68+
this._protocolPromise = null;
69+
this._protocol = null;
6170
this._extensionHostLogsLocation = URI.file(this._environmentService.logsPath).with({ scheme: this._environmentService.logFile.scheme });
6271
this._extensionHostLogFile = joinPath(this._extensionHostLogsLocation, `${ExtensionHostLogFileName}.log`);
6372
}
6473

65-
async start(): Promise<IMessagePassingProtocol> {
74+
public async start(): Promise<IMessagePassingProtocol> {
75+
if (!this._protocolPromise) {
76+
if (WRAP_IN_IFRAME) {
77+
this._protocolPromise = this._startInsideIframe();
78+
} else {
79+
this._protocolPromise = this._startOutsideIframe();
80+
}
81+
this._protocolPromise.then(protocol => this._protocol = protocol);
82+
}
83+
return this._protocolPromise;
84+
}
6685

67-
if (!this._protocol) {
86+
private _startInsideIframe(): Promise<IMessagePassingProtocol> {
87+
const emitter = this._register(new Emitter<VSBuffer>());
6888

69-
const emitter = new Emitter<VSBuffer>();
89+
const iframe = document.createElement('iframe');
90+
iframe.setAttribute('class', 'web-worker-ext-host-iframe');
91+
iframe.setAttribute('sandbox', 'allow-scripts');
92+
iframe.style.display = 'none';
7093

71-
const url = getWorkerBootstrapUrl(require.toUrl('../worker/extensionHostWorkerMain.js'), 'WorkerExtensionHost');
72-
const worker = new Worker(url, { name: 'WorkerExtensionHost' });
94+
const nonce = generateUuid();
95+
const vscodeWebWorkerExtHostId = generateUuid();
96+
const workerUrl = require.toUrl('../worker/extensionHostWorkerMain.js');
7397

74-
worker.onmessage = (event) => {
75-
const { data } = event;
76-
if (!(data instanceof ArrayBuffer)) {
77-
console.warn('UNKNOWN data received', data);
78-
this._onDidExit.fire([77, 'UNKNOWN data received']);
79-
return;
98+
const js = `
99+
(function() {
100+
const workerUrl = "${getWorkerBootstrapUrl(workerUrl, 'WorkerExtensionHost', true)}";
101+
const worker = new Worker(workerUrl, { name: 'WorkerExtensionHost' });
102+
const vscodeWebWorkerExtHostId = '${vscodeWebWorkerExtHostId}';
103+
104+
worker.onmessage = (event) => {
105+
const { data } = event;
106+
if (!(data instanceof ArrayBuffer)) {
107+
console.warn('Unknown data received', data);
108+
window.parent.postMessage({
109+
vscodeWebWorkerExtHostId,
110+
error: {
111+
name: 'Error',
112+
message: 'Unknown data received',
113+
stack: []
80114
}
115+
}, '*');
116+
return;
117+
}
118+
window.parent.postMessage({
119+
vscodeWebWorkerExtHostId,
120+
data: data
121+
}, '*', [data]);
122+
};
123+
124+
worker.onerror = (event) => {
125+
console.error(event.message, event.error);
126+
window.parent.postMessage({
127+
vscodeWebWorkerExtHostId,
128+
error: {
129+
name: event.error.name,
130+
message: event.error.message,
131+
stack: event.error.stack
132+
}
133+
}, '*');
134+
};
135+
136+
window.addEventListener('message', function(event) {
137+
if (event.source !== window.parent) {
138+
return;
139+
}
140+
if (event.data.vscodeWebWorkerExtHostId !== vscodeWebWorkerExtHostId) {
141+
return;
142+
}
143+
worker.postMessage(event.data.data, [event.data.data]);
144+
}, false);
145+
})();
146+
`;
147+
let sourcesOrigin = location.origin;
148+
if (/^(http:)|(https:)|(file:)/.test(workerUrl)) {
149+
sourcesOrigin = new URL(workerUrl).origin;
150+
}
81151

82-
emitter.fire(VSBuffer.wrap(new Uint8Array(data, 0, data.byteLength)));
83-
};
152+
const html = `<!DOCTYPE html>
153+
<html>
154+
<head>
155+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'nonce-${nonce}' 'unsafe-eval' ${sourcesOrigin} https://*.gallerycdn.vsassets.io; worker-src data:; connect-src ${sourcesOrigin} https://*.gallerycdn.vsassets.io" />
156+
</head>
157+
<body>
158+
<script nonce="${nonce}">${js}</script>
159+
</body>
160+
</html>`;
161+
const iframeContent = `data:text/html;charset=utf-8,${encodeURIComponent(html)}`;
162+
iframe.setAttribute('src', iframeContent);
84163

85-
worker.onerror = (event) => {
86-
console.error(event.message, event.error);
87-
this._onDidExit.fire([81, event.message || event.error]);
88-
};
164+
this._register(dom.addDisposableListener(window, 'message', (event) => {
165+
if (event.source !== iframe.contentWindow) {
166+
return;
167+
}
168+
if (event.data.vscodeWebWorkerExtHostId !== vscodeWebWorkerExtHostId) {
169+
return;
170+
}
171+
if (event.data.error) {
172+
const { name, message, stack } = event.data.error;
173+
const err = new Error();
174+
err.message = message;
175+
err.name = name;
176+
err.stack = stack;
177+
onUnexpectedError(err);
178+
this._onDidExit.fire([18, err.message]);
179+
return;
180+
}
181+
const { data } = event.data;
182+
if (!(data instanceof ArrayBuffer)) {
183+
console.warn('UNKNOWN data received', data);
184+
this._onDidExit.fire([77, 'UNKNOWN data received']);
185+
return;
186+
}
187+
emitter.fire(VSBuffer.wrap(new Uint8Array(data, 0, data.byteLength)));
188+
}));
89189

90-
// keep for cleanup
91-
this._toDispose.add(emitter);
92-
this._toDispose.add(toDisposable(() => worker.terminate()));
190+
const protocol: IMessagePassingProtocol = {
191+
onMessage: emitter.event,
192+
send: vsbuf => {
193+
const data = vsbuf.buffer.buffer.slice(vsbuf.buffer.byteOffset, vsbuf.buffer.byteOffset + vsbuf.buffer.byteLength);
194+
iframe.contentWindow!.postMessage({
195+
vscodeWebWorkerExtHostId,
196+
data: data
197+
}, '*', [data]);
198+
}
199+
};
93200

94-
const protocol: IMessagePassingProtocol = {
95-
onMessage: emitter.event,
96-
send: vsbuf => {
97-
const data = vsbuf.buffer.buffer.slice(vsbuf.buffer.byteOffset, vsbuf.buffer.byteOffset + vsbuf.buffer.byteLength);
98-
worker.postMessage(data, [data]);
99-
}
100-
};
201+
document.body.appendChild(iframe);
202+
this._register(toDisposable(() => iframe.remove()));
203+
204+
return this._performHandshake(protocol);
205+
}
101206

102-
// extension host handshake happens below
103-
// (1) <== wait for: Ready
104-
// (2) ==> send: init data
105-
// (3) <== wait for: Initialized
207+
private _startOutsideIframe(): Promise<IMessagePassingProtocol> {
208+
const emitter = new Emitter<VSBuffer>();
106209

107-
await Event.toPromise(Event.filter(protocol.onMessage, msg => isMessageOfType(msg, MessageType.Ready)));
108-
protocol.send(VSBuffer.fromString(JSON.stringify(await this._createExtHostInitData())));
109-
await Event.toPromise(Event.filter(protocol.onMessage, msg => isMessageOfType(msg, MessageType.Initialized)));
210+
const url = getWorkerBootstrapUrl(require.toUrl('../worker/extensionHostWorkerMain.js'), 'WorkerExtensionHost');
211+
const worker = new Worker(url, { name: 'WorkerExtensionHost' });
110212

111-
// Register log channel for web worker exthost log
112-
Registry.as<IOutputChannelRegistry>(Extensions.OutputChannels).registerChannel({ id: 'webWorkerExtHostLog', label: localize('name', "Worker Extension Host"), file: this._extensionHostLogFile, log: true });
213+
worker.onmessage = (event) => {
214+
const { data } = event;
215+
if (!(data instanceof ArrayBuffer)) {
216+
console.warn('UNKNOWN data received', data);
217+
this._onDidExit.fire([77, 'UNKNOWN data received']);
218+
return;
219+
}
113220

114-
this._protocol = protocol;
115-
}
116-
return this._protocol;
221+
emitter.fire(VSBuffer.wrap(new Uint8Array(data, 0, data.byteLength)));
222+
};
223+
224+
worker.onerror = (event) => {
225+
console.error(event.message, event.error);
226+
this._onDidExit.fire([81, event.message || event.error]);
227+
};
117228

229+
// keep for cleanup
230+
this._register(emitter);
231+
this._register(toDisposable(() => worker.terminate()));
232+
233+
const protocol: IMessagePassingProtocol = {
234+
onMessage: emitter.event,
235+
send: vsbuf => {
236+
const data = vsbuf.buffer.buffer.slice(vsbuf.buffer.byteOffset, vsbuf.buffer.byteOffset + vsbuf.buffer.byteLength);
237+
worker.postMessage(data, [data]);
238+
}
239+
};
240+
241+
return this._performHandshake(protocol);
118242
}
119243

120-
dispose(): void {
121-
if (!this._protocol) {
122-
this._toDispose.dispose();
123-
return;
244+
private async _performHandshake(protocol: IMessagePassingProtocol): Promise<IMessagePassingProtocol> {
245+
// extension host handshake happens below
246+
// (1) <== wait for: Ready
247+
// (2) ==> send: init data
248+
// (3) <== wait for: Initialized
249+
250+
await Event.toPromise(Event.filter(protocol.onMessage, msg => isMessageOfType(msg, MessageType.Ready)));
251+
if (this._isTerminating) {
252+
throw canceled();
124253
}
254+
protocol.send(VSBuffer.fromString(JSON.stringify(await this._createExtHostInitData())));
255+
if (this._isTerminating) {
256+
throw canceled();
257+
}
258+
await Event.toPromise(Event.filter(protocol.onMessage, msg => isMessageOfType(msg, MessageType.Initialized)));
259+
if (this._isTerminating) {
260+
throw canceled();
261+
}
262+
263+
// Register log channel for web worker exthost log
264+
Registry.as<IOutputChannelRegistry>(Extensions.OutputChannels).registerChannel({ id: 'webWorkerExtHostLog', label: localize('name', "Worker Extension Host"), file: this._extensionHostLogFile, log: true });
265+
266+
return protocol;
267+
}
268+
269+
public dispose(): void {
125270
if (this._isTerminating) {
126271
return;
127272
}
128273
this._isTerminating = true;
129-
this._protocol.send(createMessageOfType(MessageType.Terminate));
130-
setTimeout(() => this._toDispose.dispose(), 10 * 1000);
274+
if (this._protocol) {
275+
this._protocol.send(createMessageOfType(MessageType.Terminate));
276+
}
277+
super.dispose();
131278
}
132279

133280
getInspectPort(): number | undefined {

src/vs/workbench/services/extensions/worker/extensionHostWorker.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { isMessageOfType, MessageType, createMessageOfType } from 'vs/workbench/
1010
import { IInitData } from 'vs/workbench/api/common/extHost.protocol';
1111
import { ExtensionHostMain } from 'vs/workbench/services/extensions/common/extensionHostMain';
1212
import { IHostUtils } from 'vs/workbench/api/common/extHostExtensionService';
13+
import * as path from 'vs/base/common/path';
1314

1415
import 'vs/workbench/api/common/extHost.common.services';
1516
import 'vs/workbench/api/worker/extHost.worker.services';
@@ -35,6 +36,17 @@ self.postMessage = () => console.trace(`'postMessage' has been blocked`);
3536
const nativeAddEventLister = addEventListener.bind(self);
3637
self.addEventLister = () => console.trace(`'addEventListener' has been blocked`);
3738

39+
if (location.protocol === 'data:') {
40+
// make sure new Worker(...) always uses data:
41+
const _Worker = Worker;
42+
Worker = <any>function (stringUrl: string | URL, options?: WorkerOptions) {
43+
const js = `importScripts('${stringUrl}');`;
44+
options = options || {};
45+
options.name = options.name || path.basename(stringUrl.toString());
46+
return new _Worker(`data:text/javascript;charset=utf-8,${encodeURIComponent(js)}`, options);
47+
};
48+
}
49+
3850
//#endregion ---
3951

4052
const hostUtil = new class implements IHostUtils {

0 commit comments

Comments
 (0)