Don't use data: as the src of the web worker extension host iframe (#108285)

This commit is contained in:
Alex Dima 2020-10-21 20:03:54 +02:00
parent 3b95fb5233
commit b72438c7f3
No known key found for this signature in database
GPG key ID: 6E58D7B045760DA0
8 changed files with 192 additions and 92 deletions

View file

@ -60,6 +60,7 @@ if (args.help) {
' --host Remote host\n' + ' --host Remote host\n' +
' --port Remote/Local port\n' + ' --port Remote/Local port\n' +
' --local_port Local port override\n' + ' --local_port Local port override\n' +
' --secondary-port Secondary port\n' +
' --extension Path of an extension to include\n' + ' --extension Path of an extension to include\n' +
' --github-auth Github authentication token\n' + ' --github-auth Github authentication token\n' +
' --verbose Print out more information\n' + ' --verbose Print out more information\n' +
@ -72,6 +73,7 @@ if (args.help) {
const PORT = args.port || process.env.PORT || 8080; const PORT = args.port || process.env.PORT || 8080;
const LOCAL_PORT = args.local_port || process.env.LOCAL_PORT || PORT; const LOCAL_PORT = args.local_port || process.env.LOCAL_PORT || PORT;
const SECONDARY_PORT = args['secondary-port'] || (parseInt(PORT, 10) + 1);
const SCHEME = args.scheme || process.env.VSCODE_SCHEME || 'http'; const SCHEME = args.scheme || process.env.VSCODE_SCHEME || 'http';
const HOST = args.host || 'localhost'; const HOST = args.host || 'localhost';
const AUTHORITY = process.env.VSCODE_AUTHORITY || `${HOST}:${PORT}`; const AUTHORITY = process.env.VSCODE_AUTHORITY || `${HOST}:${PORT}`;
@ -207,7 +209,11 @@ const commandlineProvidedExtensionsPromise = getCommandlineProvidedExtensionInfo
const mapCallbackUriToRequestId = new Map(); const mapCallbackUriToRequestId = new Map();
const server = http.createServer((req, res) => { /**
* @param req {http.IncomingMessage}
* @param res {http.ServerResponse}
*/
const requestHandler = (req, res) => {
const parsedUrl = url.parse(req.url, true); const parsedUrl = url.parse(req.url, true);
const pathname = parsedUrl.pathname; const pathname = parsedUrl.pathname;
@ -252,20 +258,29 @@ const server = http.createServer((req, res) => {
return serveError(req, res, 500, 'Internal Server Error.'); return serveError(req, res, 500, 'Internal Server Error.');
} }
}); };
const server = http.createServer(requestHandler);
server.listen(LOCAL_PORT, () => { server.listen(LOCAL_PORT, () => {
if (LOCAL_PORT !== PORT) { if (LOCAL_PORT !== PORT) {
console.log(`Operating location at http://0.0.0.0:${LOCAL_PORT}`); console.log(`Operating location at http://0.0.0.0:${LOCAL_PORT}`);
} }
console.log(`Web UI available at ${SCHEME}://${AUTHORITY}`); console.log(`Web UI available at ${SCHEME}://${AUTHORITY}`);
}); });
server.on('error', err => { server.on('error', err => {
console.error(`Error occurred in server:`); console.error(`Error occurred in server:`);
console.error(err); console.error(err);
}); });
const secondaryServer = http.createServer(requestHandler);
secondaryServer.listen(SECONDARY_PORT, () => {
console.log(`Secondary server available at ${SCHEME}://${HOST}:${SECONDARY_PORT}`);
});
secondaryServer.on('error', err => {
console.error(`Error occurred in server:`);
console.error(err);
});
/** /**
* @param {import('http').IncomingMessage} req * @param {import('http').IncomingMessage} req
* @param {import('http').ServerResponse} res * @param {import('http').ServerResponse} res
@ -366,6 +381,7 @@ async function handleRoot(req, res) {
folderUri: folderUri, folderUri: folderUri,
staticExtensions, staticExtensions,
enableSyncByDefault: args['enable-sync'], enableSyncByDefault: args['enable-sync'],
webWorkerExtensionHostIframeSrc: `${SCHEME}://${HOST}:${SECONDARY_PORT}/static/out/vs/workbench/services/extensions/worker/httpWebWorkerExtensionHostIframe.html`
}; };
if (args['wrap-iframe']) { if (args['wrap-iframe']) {
webConfigJSON._wrapWebWorkerExtHostInIframe = true; webConfigJSON._wrapWebWorkerExtHostInIframe = true;

View file

@ -51,6 +51,7 @@ export interface IProductConfiguration {
readonly downloadUrl?: string; readonly downloadUrl?: string;
readonly updateUrl?: string; readonly updateUrl?: string;
readonly webEndpointUrl?: string;
readonly target?: string; readonly target?: string;
readonly settingsSearchBuildId?: number; readonly settingsSearchBuildId?: number;

View file

@ -16,7 +16,6 @@ import { ILabelService } from 'vs/platform/label/common/label';
import { ILogService } from 'vs/platform/log/common/log'; import { ILogService } from 'vs/platform/log/common/log';
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import * as platform from 'vs/base/common/platform'; import * as platform from 'vs/base/common/platform';
import * as browser from 'vs/base/browser/browser';
import * as dom from 'vs/base/browser/dom'; import * as dom from 'vs/base/browser/dom';
import { URI } from 'vs/base/common/uri'; import { URI } from 'vs/base/common/uri';
import { IExtensionHost, ExtensionHostLogFileName, ExtensionHostKind } from 'vs/workbench/services/extensions/common/extensions'; import { IExtensionHost, ExtensionHostLogFileName, ExtensionHostKind } from 'vs/workbench/services/extensions/common/extensions';
@ -28,7 +27,6 @@ import { IOutputChannelRegistry, Extensions } from 'vs/workbench/services/output
import { localize } from 'vs/nls'; import { localize } from 'vs/nls';
import { generateUuid } from 'vs/base/common/uuid'; import { generateUuid } from 'vs/base/common/uuid';
import { canceled, onUnexpectedError } from 'vs/base/common/errors'; import { canceled, onUnexpectedError } from 'vs/base/common/errors';
import { WEB_WORKER_IFRAME } from 'vs/workbench/services/extensions/common/webWorkerIframe';
import { Barrier } from 'vs/base/common/async'; import { Barrier } from 'vs/base/common/async';
import { FileAccess } from 'vs/base/common/network'; import { FileAccess } from 'vs/base/common/network';
@ -81,10 +79,38 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost
return true; return true;
} }
private _webWorkerExtensionHostIframeSrc(): string | null {
if (this._environmentService.options && this._environmentService.options.webWorkerExtensionHostIframeSrc) {
return this._environmentService.options.webWorkerExtensionHostIframeSrc;
}
if (this._productService.webEndpointUrl) {
const forceHTTPS = (location.protocol === 'https:');
let baseUrl = this._productService.webEndpointUrl;
if (this._productService.quality) {
baseUrl += `/${this._productService.quality}`;
}
if (this._productService.commit) {
baseUrl += `/${this._productService.commit}`;
}
return (
forceHTTPS
? `${baseUrl}/out/vs/workbench/services/extensions/worker/httpsWebWorkerExtensionHostIframe.html`
: `${baseUrl}/out/vs/workbench/services/extensions/worker/httpWebWorkerExtensionHostIframe.html`
);
}
return null;
}
public async start(): Promise<IMessagePassingProtocol> { public async start(): Promise<IMessagePassingProtocol> {
if (!this._protocolPromise) { if (!this._protocolPromise) {
if (platform.isWeb && !browser.isSafari && this._wrapInIframe()) { if (platform.isWeb) {
this._protocolPromise = this._startInsideIframe(); const webWorkerExtensionHostIframeSrc = this._webWorkerExtensionHostIframeSrc();
if (webWorkerExtensionHostIframeSrc && this._wrapInIframe()) {
this._protocolPromise = this._startInsideIframe(webWorkerExtensionHostIframeSrc);
} else {
console.warn(`The web worker extension host is started without an iframe sandbox!`);
this._protocolPromise = this._startOutsideIframe();
}
} else { } else {
this._protocolPromise = this._startOutsideIframe(); this._protocolPromise = this._startOutsideIframe();
} }
@ -93,40 +119,41 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost
return this._protocolPromise; return this._protocolPromise;
} }
private async _startInsideIframe(): Promise<IMessagePassingProtocol> { private async _startInsideIframe(webWorkerExtensionHostIframeSrc: string): Promise<IMessagePassingProtocol> {
const emitter = this._register(new Emitter<VSBuffer>()); const emitter = this._register(new Emitter<VSBuffer>());
const iframe = document.createElement('iframe'); const iframe = document.createElement('iframe');
iframe.setAttribute('class', 'web-worker-ext-host-iframe'); iframe.setAttribute('class', 'web-worker-ext-host-iframe');
iframe.setAttribute('sandbox', 'allow-scripts'); iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin');
iframe.style.display = 'none'; iframe.style.display = 'none';
const vscodeWebWorkerExtHostId = generateUuid(); const vscodeWebWorkerExtHostId = generateUuid();
const workerUrl = FileAccess.asBrowserUri('../worker/extensionHostWorkerMain.js', require).toString(true); iframe.setAttribute('src', `${webWorkerExtensionHostIframeSrc}?vscodeWebWorkerExtHostId=${vscodeWebWorkerExtHostId}`);
const workerSrc = getWorkerBootstrapUrl(workerUrl, 'WorkerExtensionHost', true);
const escapeAttribute = (value: string): string => {
return value.replace(/"/g, '&quot;');
};
const forceHTTPS = (location.protocol === 'https:');
const html = `<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-eval' '${WEB_WORKER_IFRAME.sha}' ${forceHTTPS ? 'https:' : 'http: https:'}; worker-src data:; connect-src ${forceHTTPS ? 'https:' : 'http: https:'}" />
<meta id="vscode-worker-src" data-value="${escapeAttribute(workerSrc)}" />
<meta id="vscode-web-worker-ext-host-id" data-value="${escapeAttribute(vscodeWebWorkerExtHostId)}" />
</head>
<body>
<script>${WEB_WORKER_IFRAME.js}</script>
</body>
</html>`;
const iframeContent = `data:text/html;charset=utf-8,${encodeURIComponent(html)}`;
iframe.setAttribute('src', iframeContent);
const timeout = setTimeout(() => {
this._onDidExit.fire([ExtensionHostExitCode.StartTimeout10s, 'The Web Worker Extension Host did not start in 10s']);
}, 10000);
const barrier = new Barrier(); const barrier = new Barrier();
let port!: MessagePort; let port!: MessagePort;
let barrierError: Error | null = null;
let barrierHasError = false;
let startTimeout: any = null;
const rejectBarrier = (exitCode: number, error: Error) => {
barrierError = error;
barrierHasError = true;
onUnexpectedError(barrierError);
clearTimeout(startTimeout);
this._onDidExit.fire([ExtensionHostExitCode.UnexpectedError, barrierError.message]);
barrier.open();
};
const resolveBarrier = (messagePort: MessagePort) => {
port = messagePort;
clearTimeout(startTimeout);
barrier.open();
};
startTimeout = setTimeout(() => {
rejectBarrier(ExtensionHostExitCode.StartTimeout10s, new Error('The Web Worker Extension Host did not start in 10s'));
}, 10000);
this._register(dom.addDisposableListener(window, 'message', (event) => { this._register(dom.addDisposableListener(window, 'message', (event) => {
if (event.source !== iframe.contentWindow) { if (event.source !== iframe.contentWindow) {
@ -141,21 +168,15 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost
err.message = message; err.message = message;
err.name = name; err.name = name;
err.stack = stack; err.stack = stack;
onUnexpectedError(err); return rejectBarrier(ExtensionHostExitCode.UnexpectedError, err);
clearTimeout(timeout);
this._onDidExit.fire([ExtensionHostExitCode.UnexpectedError, err.message]);
return;
} }
const { data } = event.data; const { data } = event.data;
if (barrier.isOpen() || !(data instanceof MessagePort)) { if (barrier.isOpen() || !(data instanceof MessagePort)) {
console.warn('UNEXPECTED message', event); console.warn('UNEXPECTED message', event);
clearTimeout(timeout); const err = new Error('UNEXPECTED message');
this._onDidExit.fire([ExtensionHostExitCode.UnexpectedError, 'UNEXPECTED message']); return rejectBarrier(ExtensionHostExitCode.UnexpectedError, err);
return;
} }
port = data; resolveBarrier(data);
clearTimeout(timeout);
barrier.open();
})); }));
document.body.appendChild(iframe); document.body.appendChild(iframe);
@ -165,6 +186,10 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost
// with the worker extension host // with the worker extension host
await barrier.wait(); await barrier.wait();
if (barrierHasError) {
throw barrierError;
}
port.onmessage = (event) => { port.onmessage = (event) => {
const { data } = event; const { data } = event;
if (!(data instanceof ArrayBuffer)) { if (!(data instanceof ArrayBuffer)) {

View file

@ -1,47 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export const WEB_WORKER_IFRAME = {
sha: 'sha256-r24mDVsMuFEo8ChaY9ppVJKbY3CUM4I12Aw/yscWZbg=',
js: `
(function() {
const workerSrc = document.getElementById('vscode-worker-src').getAttribute('data-value');
const worker = new Worker(workerSrc, { name: 'WorkerExtensionHost' });
const vscodeWebWorkerExtHostId = document.getElementById('vscode-web-worker-ext-host-id').getAttribute('data-value');
worker.onmessage = (event) => {
const { data } = event;
if (!(data instanceof MessagePort)) {
console.warn('Unknown data received', event);
window.parent.postMessage({
vscodeWebWorkerExtHostId,
error: {
name: 'Error',
message: 'Unknown data received',
stack: []
}
}, '*');
return;
}
window.parent.postMessage({
vscodeWebWorkerExtHostId,
data: data
}, '*', [data]);
};
worker.onerror = (event) => {
console.error(event.message, event.error);
window.parent.postMessage({
vscodeWebWorkerExtHostId,
error: {
name: event.error ? event.error.name : '',
message: event.error ? event.error.message : '',
stack: event.error ? event.error.stack : []
}
}, '*');
};
})();
`
};

View file

@ -45,9 +45,9 @@ self.addEventListener = () => console.trace(`'addEventListener' has been blocked
(<any>self)['webkitResolveLocalFileSystemSyncURL'] = undefined; (<any>self)['webkitResolveLocalFileSystemSyncURL'] = undefined;
(<any>self)['webkitResolveLocalFileSystemURL'] = undefined; (<any>self)['webkitResolveLocalFileSystemURL'] = undefined;
if (location.protocol === 'data:') { if ((<any>self).Worker) {
// make sure new Worker(...) always uses data: // make sure new Worker(...) always uses data:
const _Worker = Worker; const _Worker = (<any>self).Worker;
Worker = <any>function (stringUrl: string | URL, options?: WorkerOptions) { Worker = <any>function (stringUrl: string | URL, options?: WorkerOptions) {
const js = `importScripts('${stringUrl}');`; const js = `importScripts('${stringUrl}');`;
options = options || {}; options = options || {};

View file

@ -0,0 +1,50 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; child-src 'self' data:; script-src 'unsafe-eval' 'sha256-O98pkmgtvUCQGVoddaGy891K52PVRnySDRxRszVLPNQ=' http: https:; connect-src http: https:" />
</head>
<body>
<script>
(function() {
const idMatch = document.location.search.match(/\bvscodeWebWorkerExtHostId=([0-9a-f\-]+)/i);
const vscodeWebWorkerExtHostId = idMatch ? idMatch[1] : '';
function sendError(error) {
window.parent.postMessage({
vscodeWebWorkerExtHostId,
error: {
name: error ? error.name : '',
message: error ? error.message : '',
stack: error ? error.stack : []
}
}, '*');
}
try {
const worker = new Worker('extensionHostWorkerMain.js', { name: 'WorkerExtensionHost' });
worker.onmessage = (event) => {
const { data } = event;
if (!(data instanceof MessagePort)) {
console.warn('Unknown data received', event);
sendError({ name: 'Error', message: 'Unknown data received', stack: [] });
return;
}
window.parent.postMessage({
vscodeWebWorkerExtHostId,
data: data
}, '*', [data]);
};
worker.onerror = (event) => {
console.error(event.message, event.error);
sendError(event.error);
};
} catch(err) {
console.error(err);
sendError(err);
}
})();
</script>
</body>
</html>

View file

@ -0,0 +1,50 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; child-src 'self' data:; script-src 'unsafe-eval' 'sha256-O98pkmgtvUCQGVoddaGy891K52PVRnySDRxRszVLPNQ=' https:; connect-src https:" />
</head>
<body>
<script>
(function() {
const idMatch = document.location.search.match(/\bvscodeWebWorkerExtHostId=([0-9a-f\-]+)/i);
const vscodeWebWorkerExtHostId = idMatch ? idMatch[1] : '';
function sendError(error) {
window.parent.postMessage({
vscodeWebWorkerExtHostId,
error: {
name: error ? error.name : '',
message: error ? error.message : '',
stack: error ? error.stack : []
}
}, '*');
}
try {
const worker = new Worker('extensionHostWorkerMain.js', { name: 'WorkerExtensionHost' });
worker.onmessage = (event) => {
const { data } = event;
if (!(data instanceof MessagePort)) {
console.warn('Unknown data received', event);
sendError({ name: 'Error', message: 'Unknown data received', stack: [] });
return;
}
window.parent.postMessage({
vscodeWebWorkerExtHostId,
data: data
}, '*', [data]);
};
worker.onerror = (event) => {
console.error(event.message, event.error);
sendError(event.error);
};
} catch(err) {
console.error(err);
sendError(err);
}
})();
</script>
</body>
</html>

View file

@ -277,6 +277,11 @@ interface IWorkbenchConstructionOptions {
*/ */
readonly webviewEndpoint?: string; readonly webviewEndpoint?: string;
/**
* An URL pointing to the web worker extension host <iframe> src.
*/
readonly webWorkerExtensionHostIframeSrc?: string;
/** /**
* A factory for web sockets. * A factory for web sockets.
*/ */