mirror of
https://github.com/microsoft/vscode.git
synced 2026-07-18 09:53:23 +01:00
Add experimental service-worker based loading of webview content
## Problem We use a custom `vscode-resource` protocol to control access to local resources inside of webviews. This will not work on the web, but we still would prefer a way to intercept webview requests from the main client ## Proposed solution Move webviews into their own origin and register a service worker on this origin. This service worker can talk with the outer iframe of our webview. When a request for a resource comes in to the service worker: * In the service worker, add the request to a map and post a message back to the client saying we want to load this resource * The outer iframe gets the message from the sercice worker and forwards it to our main process * This process handles the message and use the normal file system api to read the resource (also restricting which files can be read) * We post the result back into the inner iframe which fowards it back to the service worker * The service worker now resolves the pending request. The prototype version in this change works but does not correctly handle multiple clients existing at the same time (plus probably a lot of other edge cases too)
This commit is contained in:
@@ -17,7 +17,8 @@
|
||||
folderUri: '{{FOLDER}}',
|
||||
workspaceUri: '{{WORKSPACE}}',
|
||||
userDataUri: '{{USER_DATA}}',
|
||||
authority: '{{AUTHORITY}}'
|
||||
authority: '{{AUTHORITY}}',
|
||||
webviewEndpoint: '{{WEBVIEW_ENDPOINT}}'
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -151,4 +151,6 @@ export interface IEnvironmentService {
|
||||
|
||||
driverHandle?: string;
|
||||
driverVerbose: boolean;
|
||||
|
||||
webviewEndpoint?: string;
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ interface IWindowConfiguration {
|
||||
userDataUri: URI;
|
||||
folderUri?: URI;
|
||||
workspaceUri?: URI;
|
||||
webviewEndpoint?: string;
|
||||
}
|
||||
|
||||
class CodeRendererMain extends Disposable {
|
||||
@@ -149,6 +150,7 @@ class CodeRendererMain extends Disposable {
|
||||
port: null,
|
||||
break: false
|
||||
};
|
||||
environmentService.webviewEndpoint = this.configuration.webviewEndpoint;
|
||||
|
||||
return environmentService;
|
||||
}
|
||||
@@ -189,6 +191,7 @@ export interface IWindowConfigurationContents {
|
||||
userDataUri: UriComponents;
|
||||
folderUri?: UriComponents;
|
||||
workspaceUri?: UriComponents;
|
||||
webviewEndpoint?: string;
|
||||
}
|
||||
|
||||
export function main(windowConfigurationContents: IWindowConfigurationContents): Promise<void> {
|
||||
@@ -196,7 +199,8 @@ export function main(windowConfigurationContents: IWindowConfigurationContents):
|
||||
userDataUri: URI.revive(windowConfigurationContents.userDataUri),
|
||||
remoteAuthority: windowConfigurationContents.authority,
|
||||
folderUri: windowConfigurationContents.folderUri ? URI.revive(windowConfigurationContents.folderUri) : undefined,
|
||||
workspaceUri: windowConfigurationContents.workspaceUri ? URI.revive(windowConfigurationContents.workspaceUri) : undefined
|
||||
workspaceUri: windowConfigurationContents.workspaceUri ? URI.revive(windowConfigurationContents.workspaceUri) : undefined,
|
||||
webviewEndpoint: windowConfigurationContents.webviewEndpoint
|
||||
};
|
||||
|
||||
const renderer = new CodeRendererMain(windowConfiguration);
|
||||
|
||||
@@ -228,6 +228,7 @@ export class SimpleWorkbenchEnvironmentService implements IWorkbenchEnvironmentS
|
||||
disableCrashReporter: boolean;
|
||||
driverHandle?: string;
|
||||
driverVerbose: boolean;
|
||||
webviewEndpoint?: string;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -118,6 +118,28 @@
|
||||
initialScrollProgress: undefined
|
||||
};
|
||||
|
||||
// Service worker for resource loading
|
||||
const FAKE_LOAD = !!navigator.serviceWorker;
|
||||
if (navigator.serviceWorker) {
|
||||
navigator.serviceWorker.register('service-worker.js');
|
||||
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
registration.active.postMessage('ping');
|
||||
|
||||
host.onMessage('loaded-resource', event => {
|
||||
registration.active.postMessage({ channel: 'loaded-resource', data: event.data.args });
|
||||
});
|
||||
});
|
||||
|
||||
navigator.serviceWorker.addEventListener('message', event => {
|
||||
switch (event.data.channel) {
|
||||
case 'load-resource':
|
||||
host.postMessage('load-resource', { path: event.data.path });
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLDocument?} document
|
||||
* @param {HTMLElement?} body
|
||||
@@ -337,15 +359,30 @@
|
||||
newFrame.setAttribute('id', 'pending-frame');
|
||||
newFrame.setAttribute('frameborder', '0');
|
||||
newFrame.setAttribute('sandbox', options.allowScripts ? 'allow-scripts allow-forms allow-same-origin' : 'allow-same-origin');
|
||||
if (FAKE_LOAD) {
|
||||
// We should just be able to use srcdoc, but I wasn't
|
||||
// seeing the service worker applying properly.
|
||||
// Fake load an empty on the correct origin and then write real html
|
||||
// into it to get around this.
|
||||
newFrame.src = '/fake.html';
|
||||
}
|
||||
newFrame.style.cssText = 'display: block; margin: 0; overflow: hidden; position: absolute; width: 100%; height: 100%; visibility: hidden';
|
||||
document.body.appendChild(newFrame);
|
||||
|
||||
// write new content onto iframe
|
||||
newFrame.contentDocument.open('text/html', 'replace');
|
||||
if (!FAKE_LOAD) {
|
||||
// write new content onto iframe
|
||||
newFrame.contentDocument.open();
|
||||
}
|
||||
|
||||
newFrame.contentWindow.addEventListener('keydown', handleInnerKeydown);
|
||||
|
||||
newFrame.contentWindow.addEventListener('DOMContentLoaded', e => {
|
||||
if (FAKE_LOAD) {
|
||||
newFrame.contentDocument.open();
|
||||
newFrame.contentDocument.write('<!DOCTYPE html>');
|
||||
newFrame.contentDocument.write(newDocument.documentElement.innerHTML);
|
||||
newFrame.contentDocument.close();
|
||||
}
|
||||
const contentDocument = e.target ? (/** @type {HTMLDocument} */ (e.target)) : undefined;
|
||||
if (contentDocument) {
|
||||
applyStyles(contentDocument, contentDocument.body);
|
||||
@@ -414,9 +451,11 @@
|
||||
|
||||
// set DOCTYPE for newDocument explicitly as DOMParser.parseFromString strips it off
|
||||
// and DOCTYPE is needed in the iframe to ensure that the user agent stylesheet is correctly overridden
|
||||
newFrame.contentDocument.write('<!DOCTYPE html>');
|
||||
newFrame.contentDocument.write(newDocument.documentElement.innerHTML);
|
||||
newFrame.contentDocument.close();
|
||||
if (!FAKE_LOAD) {
|
||||
newFrame.contentDocument.write('<!DOCTYPE html>');
|
||||
newFrame.contentDocument.write(newDocument.documentElement.innerHTML);
|
||||
newFrame.contentDocument.close();
|
||||
}
|
||||
|
||||
host.postMessage('did-set-content', undefined);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
// Listen for messages from clients.
|
||||
const resolvedPaths = new Map();
|
||||
|
||||
self.addEventListener('message', (event) => {
|
||||
switch (event.data.channel) {
|
||||
case 'loaded-resource':
|
||||
{
|
||||
const data = event.data.data;
|
||||
const target = resolvedPaths.get(data.path);
|
||||
if (!target) {
|
||||
console.log('Loaded unknown resource', data.path);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.status === 200) {
|
||||
target.resolve(new Response(data.data, {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': data.mime },
|
||||
}).clone());
|
||||
} else {
|
||||
target.resolve(new Response('Not Found', {
|
||||
status: 404,
|
||||
}).clone());
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
var clients;
|
||||
const resourceRoot = '/vscode-resource';
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
const requestUrl = new URL(event.request.url);
|
||||
|
||||
if (!requestUrl.pathname.startsWith(resourceRoot + '/')) {
|
||||
return event.respondWith(fetch(event.request));
|
||||
}
|
||||
|
||||
event.respondWith((async () => {
|
||||
const resourcePath = requestUrl.pathname.replace(resourceRoot, '');
|
||||
|
||||
const existing = resolvedPaths.get(resourcePath);
|
||||
if (existing) {
|
||||
return existing.promise.then(r => r.clone());
|
||||
}
|
||||
|
||||
const allClients = await clients.matchAll({
|
||||
includeUncontrolled: true
|
||||
});
|
||||
|
||||
for (const client of allClients) {
|
||||
const clientUrl = new URL(client.url);
|
||||
if (clientUrl.pathname === '/') {
|
||||
client.postMessage({
|
||||
channel: 'load-resource',
|
||||
path: resourcePath
|
||||
});
|
||||
|
||||
if (resolvedPaths.has(resourcePath)) {
|
||||
// Someone else added it in the mean time
|
||||
return resolvedPaths.get(resolvedPaths).promise;
|
||||
}
|
||||
|
||||
let resolve;
|
||||
const promise = new Promise(r => resolve = r);
|
||||
resolvedPaths.set(resourcePath, { resolve, promise });
|
||||
return promise.then(r => r.clone());
|
||||
}
|
||||
}
|
||||
})());
|
||||
});
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
event.waitUntil(self.skipWaiting()); // Activate worker immediately
|
||||
});
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(self.clients.claim()); // Become available to all pages
|
||||
});
|
||||
@@ -16,6 +16,7 @@ import { areWebviewInputOptionsEqual } from 'vs/workbench/contrib/webview/browse
|
||||
import { addDisposableListener, addClass } from 'vs/base/browser/dom';
|
||||
import { getWebviewThemeData } from 'vs/workbench/contrib/webview/common/themeing';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { loadLocalResource } from 'vs/workbench/contrib/webview/common/resourceLoader';
|
||||
|
||||
interface WebviewContent {
|
||||
readonly html: string;
|
||||
@@ -34,12 +35,12 @@ export class IFrameWebview extends Disposable implements Webview {
|
||||
private readonly id: string;
|
||||
|
||||
constructor(
|
||||
_options: WebviewOptions,
|
||||
private _options: WebviewOptions,
|
||||
contentOptions: WebviewContentOptions,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@IEnvironmentService environmentService: IEnvironmentService,
|
||||
@IFileService fileService: IFileService,
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@IConfigurationService private readonly _configurationService: IConfigurationService,
|
||||
) {
|
||||
@@ -55,7 +56,7 @@ export class IFrameWebview extends Disposable implements Webview {
|
||||
this.element = document.createElement('iframe');
|
||||
this.element.sandbox.add('allow-scripts');
|
||||
this.element.sandbox.add('allow-same-origin');
|
||||
this.element.setAttribute('src', `/src/vs/workbench/contrib/webview/browser/pre/index.html?id=${this.id}`);
|
||||
this.element.setAttribute('src', `${environmentService.webviewEndpoint}?id=${this.id}`); // TODO: get this from env service
|
||||
this.element.style.border = 'none';
|
||||
this.element.style.width = '100%';
|
||||
this.element.style.height = '100%';
|
||||
@@ -108,6 +109,13 @@ export class IFrameWebview extends Disposable implements Webview {
|
||||
this.handleFocusChange(false);
|
||||
return;
|
||||
|
||||
case 'load-resource':
|
||||
{
|
||||
const path = e.data.data.path;
|
||||
const uri = URI.file(path);
|
||||
this.loadResource(uri);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -262,5 +270,28 @@ export class IFrameWebview extends Disposable implements Webview {
|
||||
const { styles, activeTheme } = getWebviewThemeData(theme, this._configurationService);
|
||||
this._send('styles', { styles, activeTheme });
|
||||
}
|
||||
|
||||
private async loadResource(uri: URI) {
|
||||
try {
|
||||
const result = await loadLocalResource(uri, this.fileService, this._options.extension ? this._options.extension.location : undefined,
|
||||
() => (this.content.options.localResourceRoots || []));
|
||||
|
||||
if (result.type === 'success') {
|
||||
return this._send('loaded-resource', {
|
||||
status: 200,
|
||||
path: uri.path,
|
||||
mime: result.mimeType,
|
||||
data: result.data.buffer
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
|
||||
return this._send('loaded-resource', {
|
||||
status: 404,
|
||||
path: uri.path
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user