Add remote-authority to webview uri

This commit is contained in:
Matt Bierner 2021-05-11 17:30:59 -07:00
parent 927e791753
commit b847eb35e7
No known key found for this signature in database
GPG key ID: 099C331567E11888
9 changed files with 77 additions and 121 deletions

View file

@ -148,7 +148,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
const extHostNotebookKernels = rpcProtocol.set(ExtHostContext.ExtHostNotebookKernels, new ExtHostNotebookKernels(rpcProtocol, initData, extHostNotebook));
const extHostEditors = rpcProtocol.set(ExtHostContext.ExtHostEditors, new ExtHostEditors(rpcProtocol, extHostDocumentsAndEditors));
const extHostTreeViews = rpcProtocol.set(ExtHostContext.ExtHostTreeViews, new ExtHostTreeViews(rpcProtocol.getProxy(MainContext.MainThreadTreeViews), extHostCommands, extHostLogService));
const extHostEditorInsets = rpcProtocol.set(ExtHostContext.ExtHostEditorInsets, new ExtHostEditorInsets(rpcProtocol.getProxy(MainContext.MainThreadEditorInsets), extHostEditors, initData.environment));
const extHostEditorInsets = rpcProtocol.set(ExtHostContext.ExtHostEditorInsets, new ExtHostEditorInsets(rpcProtocol.getProxy(MainContext.MainThreadEditorInsets), extHostEditors, { ...initData.environment, remote: initData.remote }));
const extHostDiagnostics = rpcProtocol.set(ExtHostContext.ExtHostDiagnostics, new ExtHostDiagnostics(rpcProtocol, extHostLogService));
const extHostLanguageFeatures = rpcProtocol.set(ExtHostContext.ExtHostLanguageFeatures, new ExtHostLanguageFeatures(rpcProtocol, uriTransformer, extHostDocuments, extHostCommands, extHostDiagnostics, extHostLogService, extHostApiDeprecation));
const extHostFileSystem = rpcProtocol.set(ExtHostContext.ExtHostFileSystem, new ExtHostFileSystem(rpcProtocol, extHostLanguageFeatures));
@ -161,7 +161,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
const extHostTheming = rpcProtocol.set(ExtHostContext.ExtHostTheming, new ExtHostTheming(rpcProtocol));
const extHostAuthentication = rpcProtocol.set(ExtHostContext.ExtHostAuthentication, new ExtHostAuthentication(rpcProtocol));
const extHostTimeline = rpcProtocol.set(ExtHostContext.ExtHostTimeline, new ExtHostTimeline(rpcProtocol, extHostCommands));
const extHostWebviews = rpcProtocol.set(ExtHostContext.ExtHostWebviews, new ExtHostWebviews(rpcProtocol, initData.environment, extHostWorkspace, extHostLogService, extHostApiDeprecation));
const extHostWebviews = rpcProtocol.set(ExtHostContext.ExtHostWebviews, new ExtHostWebviews(rpcProtocol, { ...initData.environment, remote: initData.remote }, extHostWorkspace, extHostLogService, extHostApiDeprecation));
const extHostWebviewPanels = rpcProtocol.set(ExtHostContext.ExtHostWebviewPanels, new ExtHostWebviewPanels(rpcProtocol, extHostWebviews, extHostWorkspace));
const extHostCustomEditors = rpcProtocol.set(ExtHostContext.ExtHostCustomEditors, new ExtHostCustomEditors(rpcProtocol, extHostDocuments, extensionStoragePaths, extHostWebviews, extHostWebviewPanels));
const extHostWebviewViews = rpcProtocol.set(ExtHostContext.ExtHostWebviewViews, new ExtHostWebviewViews(rpcProtocol, extHostWebviews));

View file

@ -184,7 +184,7 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape {
return that._proxy.$postMessage(handle, editor && that._extHostNotebook.getIdByEditor(editor), message);
},
asWebviewUri(uri: URI) {
return asWebviewUri(that._initData.environment, String(handle), uri);
return asWebviewUri({ ...that._initData.environment, remote: that._initData.remote }, String(handle), uri);
},
// --- priority
updateNotebookAffinity(notebook, priority) {

View file

@ -10,21 +10,29 @@ export interface WebviewInitData {
readonly isExtensionDevelopmentDebug: boolean;
readonly webviewResourceRoot: string;
readonly webviewCspSource: string;
readonly remote: { readonly authority: string | undefined };
}
/**
* Construct a uri that can load resources inside a webview
*
* We encode the resource component of the uri so that on the main thread
* we know where to load the resource from (remote or truly local):
*
* ```txt
* /remote-authority?/scheme/resource-authority/path...
* ```
*/
export function asWebviewUri(
initData: WebviewInitData,
uuid: string,
resource: vscode.Uri,
): vscode.Uri {
const uri = initData.webviewResourceRoot
// Make sure we preserve the scheme of the resource but convert it into a normal path segment
// The scheme is important as we need to know if we are requesting a local or a remote resource.
.replace('{{resource}}', resource.scheme + withoutScheme(resource))
.replace('{{resource}}', (initData.remote.authority ?? '') + '/' + resource.scheme + '/' + encodeURIComponent(resource.authority) + resource.path)
.replace('{{uuid}}', uuid);
return URI.parse(uri);
}
function withoutScheme(resource: vscode.Uri): string {
return resource.toString().replace(/^\S+?:/, '');
return URI.parse(uri).with({
fragment: resource.fragment,
query: resource.query,
});
}

View file

@ -24,6 +24,7 @@ import { IFileService } from 'vs/platform/files/common/files';
import { IOpenerService, matchesScheme } from 'vs/platform/opener/common/opener';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { asWebviewUri } from 'vs/workbench/api/common/shared/webview';
import { CellEditState, ICellOutputViewModel, ICommonCellInfo, ICommonNotebookEditor, IDisplayOutputLayoutUpdateRequest, IDisplayOutputViewModel, IGenericCellViewModel, IInsetRenderOutput, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { preloadsScriptStr } from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads';
import { transformWebviewThemeVars } from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewThemeMapping';
@ -31,7 +32,6 @@ import { MarkdownCellViewModel } from 'vs/workbench/contrib/notebook/browser/vie
import { INotebookKernel, INotebookRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService';
import { IWebviewService, WebviewContentPurpose, WebviewElement } from 'vs/workbench/contrib/webview/browser/webview';
import { asWebviewUri } from 'vs/workbench/contrib/webview/common/webviewUri';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
interface BaseToWebviewMessage {
@ -731,13 +731,13 @@ export class BackLayerWebView<T extends ICommonCellInfo> extends Disposable {
if (!subRenderers.has(renderer.dependsOn)) {
subRenderers.set(renderer.dependsOn, []);
}
const entryPoint = asWebviewUri(this.environmentService, this.id, renderer.entrypoint);
const entryPoint = this.asWebviewUri(renderer.entrypoint);
subRenderers.get(renderer.dependsOn)!.push({ entrypoint: entryPoint.toString(true) });
}
}
return topLevelMarkdownRenderers.map((renderer) => {
const src = asWebviewUri(this.environmentService, this.id, renderer.entrypoint);
const src = this.asWebviewUri(renderer.entrypoint);
return {
entrypoint: src.toString(),
dependencies: subRenderers.get(renderer.id) || [],
@ -745,6 +745,15 @@ export class BackLayerWebView<T extends ICommonCellInfo> extends Disposable {
});
}
private asWebviewUri(uri: URI) {
return asWebviewUri({
isExtensionDevelopmentDebug: this.environmentService.isExtensionDevelopment,
webviewCspSource: this.environmentService.webviewCspSource,
webviewResourceRoot: this.environmentService.webviewResourceRoot,
remote: { authority: undefined } // TODO
}, this.id, uri);
}
postKernelMessage(message: any) {
this._sendMessageToWebview({
__vscode_notebook_message: true,
@ -775,11 +784,11 @@ export class BackLayerWebView<T extends ICommonCellInfo> extends Disposable {
resolveFunc = resolve;
});
const baseUrl = asWebviewUri(this.environmentService, this.id, dirname(this.documentUri));
const baseUrl = this.asWebviewUri(dirname(this.documentUri));
if (!isWeb) {
const loaderUri = FileAccess.asFileUri('vs/loader.js', require);
const loader = asWebviewUri(this.environmentService, this.id, loaderUri);
const loader = this.asWebviewUri(loaderUri);
coreDependencies = `<script src="${loader}"></script><script>
var requirejs = (function() {
@ -1566,7 +1575,7 @@ var requirejs = (function() {
const resources: IPreloadResource[] = [];
for (const preload of kernel.preloadUris) {
const uri = this.environmentService.isExtensionDevelopment && (preload.scheme === 'http' || preload.scheme === 'https')
? preload : asWebviewUri(this.environmentService, this.id, preload);
? preload : this.asWebviewUri(preload);
if (!this._preloadsCache.has(uri.toString())) {
resources.push({ uri: uri.toString(), originalUri: preload.toString(), source: 'kernel' });
@ -1592,7 +1601,7 @@ var requirejs = (function() {
for (const rendererInfo of renderers) {
extensionLocations.push(rendererInfo.extensionLocation);
for (const preload of [rendererInfo.entrypoint, ...rendererInfo.preloads]) {
const uri = asWebviewUri(this.environmentService, this.id, preload);
const uri = this.asWebviewUri(preload);
const resource: IPreloadResource = {
uri: uri.toString(),
originalUri: preload.toString(),

View file

@ -10,6 +10,7 @@ import { streamToBuffer } from 'vs/base/common/buffer';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { Emitter } from 'vs/base/common/event';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
import { URI } from 'vs/base/common/uri';
import { localize } from 'vs/nls';
import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem';
@ -243,17 +244,19 @@ export abstract class BaseWebview<T extends HTMLElement> extends Disposable {
this._register(this.on(WebviewMessageChannels.loadResource, (entry: { id: number, path: string, query: string, ifNoneMatch?: string }) => {
const rawPath = entry.path;
const uri = URI.parse(rawPath.replace(/^\/([\w\-]+)(\/{1,2})/, (_: string, scheme: string, sep: string) => {
if (sep.length === 1) {
return `${scheme}:///`; // Add empty authority.
} else {
return `${scheme}://`; // Url has own authority.
}
})).with({
// ext-authority / scheme / path-authority / ...path
const match = rawPath.match(/^\/([^\/]*)\/([^\/]*)\/([^\/]*)(\/.+)$/);
if (!match) {
throw new Error('Could not parse resource url');
}
const [_, remoteAuthority, scheme, pathAuthority, paths] = match;
const uri = URI.parse(`${scheme}://${decodeURIComponent(pathAuthority)}${paths}`).with({
query: decodeURIComponent(entry.query),
});
this.loadResource(entry.id, rawPath, uri, entry.ifNoneMatch);
this.loadResource(entry.id, rawPath, uri, decodeURIComponent(remoteAuthority), entry.ifNoneMatch);
}));
this._register(this.on(WebviewMessageChannels.loadLocalhost, (entry: any) => {
@ -377,18 +380,13 @@ export abstract class BaseWebview<T extends HTMLElement> extends Disposable {
protected abstract get webviewResourceEndpoint(): string;
private rewriteVsCodeResourceUrls(value: string): string {
const remoteAuthority = this.extension?.location.scheme === Schemas.vscodeRemote ? this.extension.location.authority : '';
return value
.replace(/(["'])(?:vscode-resource):(\/\/([^\s\/'"]+?)(?=\/))?([^\s'"]+?)(["'])/gi, (match, startQuote, _1, scheme, path, endQuote) => {
if (scheme) {
return `${startQuote}${this.webviewResourceEndpoint}/vscode-resource/${scheme}${path}${endQuote}`;
}
return `${startQuote}${this.webviewResourceEndpoint}/vscode-resource/file${path}${endQuote}`;
.replace(/(["'])(?:vscode-resource):(\/\/([^\s\/'"]+?)(?=\/))?([^\s'"]+?)(["'])/gi, (_match, startQuote, _1, scheme, path, endQuote) => {
return `${startQuote}${this.webviewResourceEndpoint}/vscode-resource/${remoteAuthority}/${scheme ?? 'file'}/${path}${endQuote}`;
})
.replace(/(["'])(?:vscode-webview-resource):(\/\/[^\s\/'"]+\/([^\s\/'"]+?)(?=\/))?([^\s'"]+?)(["'])/gi, (match, startQuote, _1, scheme, path, endQuote) => {
if (scheme) {
return `${startQuote}${this.webviewResourceEndpoint}/vscode-resource/${scheme}${path}${endQuote}`;
}
return `${startQuote}${this.webviewResourceEndpoint}/vscode-resource/file${path}${endQuote}`;
.replace(/(["'])(?:vscode-webview-resource):(\/\/[^\s\/'"]+\/([^\s\/'"]+?)(?=\/))?([^\s'"]+?)(["'])/gi, (_match, startQuote, _1, scheme, path, endQuote) => {
return `${startQuote}${this.webviewResourceEndpoint}/vscode-resource/${remoteAuthority}/${scheme ?? 'file'}/${path}${endQuote}`;
});
}
@ -514,16 +512,16 @@ export abstract class BaseWebview<T extends HTMLElement> extends Disposable {
}
}
private async loadResource(id: number, requestPath: string, uri: URI, ifNoneMatch: string | undefined) {
private async loadResource(id: number, requestPath: string, uri: URI, remoteAuthority: string | undefined, ifNoneMatch: string | undefined) {
try {
const remoteAuthority = this._environmentService.remoteAuthority;
const remoteConnectionData = remoteAuthority ? this._remoteAuthorityResolverService.getConnectionData(remoteAuthority) : null;
const result = await loadLocalResource(uri, ifNoneMatch, {
extensionLocation: this.extension?.location,
roots: this.content.options.localResourceRoots || [],
remoteConnectionData,
useRootAuthority: this.content.options.useRootAuthority
useRootAuthority: this.content.options.useRootAuthority,
remoteAuthority: remoteAuthority,
}, this._fileService, this._requestService, this._logService, this._resourceLoadingCts.token);
switch (result.type) {

View file

@ -47,9 +47,9 @@ export async function loadLocalResource(
requestUri: URI,
ifNoneMatch: string | undefined,
options: {
extensionLocation: URI | undefined;
roots: ReadonlyArray<URI>;
remoteConnectionData?: IRemoteConnectionData | null;
remoteAuthority: string | undefined;
useRootAuthority?: boolean;
},
fileService: IFileService,
@ -59,7 +59,7 @@ export async function loadLocalResource(
): Promise<WebviewResourceResponse.StreamResponse> {
logService.debug(`loadLocalResource - being. requestUri=${requestUri}`);
const resourceToLoad = getResourceToLoad(requestUri, options.roots, options.extensionLocation, options.useRootAuthority);
const resourceToLoad = getResourceToLoad(requestUri, options.roots, options.remoteAuthority, options.useRootAuthority);
logService.debug(`loadLocalResource - found resource to load. requestUri=${requestUri}, resourceToLoad=${resourceToLoad}`);
@ -118,24 +118,24 @@ export async function loadLocalResource(
function getResourceToLoad(
requestUri: URI,
roots: ReadonlyArray<URI>,
extensionLocation: URI | undefined,
remoteAuthority: string | undefined,
useRootAuthority: boolean | undefined
): URI | undefined {
for (const root of roots) {
if (containsResource(root, requestUri)) {
return normalizeResourcePath(requestUri, extensionLocation, useRootAuthority ? root.authority : undefined);
return normalizeResourcePath(requestUri, remoteAuthority, useRootAuthority ? root.authority : undefined);
}
}
return undefined;
}
function normalizeResourcePath(resource: URI, extensionLocation: URI | undefined, useRemoteAuthority: string | undefined): URI {
// If we are loading a file resource from a webview created by a remote extension, rewrite the uri to go remote
if (useRemoteAuthority || (resource.scheme === Schemas.file && extensionLocation?.scheme === Schemas.vscodeRemote)) {
function normalizeResourcePath(resource: URI, remoteAuthority: string | undefined, useRemoteAuthority: string | undefined): URI {
// If the uri was from a remote authority, make we go to the remote to load it
if (useRemoteAuthority || (remoteAuthority && resource.scheme === Schemas.file)) {
return URI.from({
scheme: Schemas.vscodeRemote,
authority: useRemoteAuthority ?? extensionLocation!.authority,
authority: useRemoteAuthority ?? remoteAuthority,
path: '/vscode-resource',
query: JSON.stringify({
requestResourcePath: resource.path

View file

@ -1,25 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vs/base/common/uri';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
export function asWebviewUri(
environmentService: IWorkbenchEnvironmentService,
uuid: string,
resource: URI,
): URI {
const uri = environmentService.webviewResourceRoot
// Make sure we preserve the scheme of the resource but convert it into a normal path segment
// The scheme is important as we need to know if we are requesting a local or a remote resource.
.replace('{{resource}}', resource.scheme + withoutScheme(resource))
.replace('{{uuid}}', uuid);
return URI.parse(uri);
}
function withoutScheme(resource: URI): string {
return resource.toString().replace(/^\S+?:/, '');
}

View file

@ -56,9 +56,9 @@ import { generateTokensCSSForColorMap } from 'vs/editor/common/modes/supports/to
import { ResourceMap } from 'vs/base/common/map';
import { IFileService } from 'vs/platform/files/common/files';
import { joinPath } from 'vs/base/common/resources';
import { asWebviewUri } from 'vs/workbench/contrib/webview/common/webviewUri';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { asWebviewUri } from 'vs/workbench/api/common/shared/webview';
const SLIDE_TRANSITION_TIME_MS = 250;
const configurationKey = 'workbench.startupEditor';
@ -486,7 +486,11 @@ export class GettingStartedPage extends EditorPane {
if (src.startsWith('https://')) { return `src="${src}"`; }
const path = joinPath(base, src);
const transformed = asWebviewUri(this.environmentService, this.webviewID, path).toString();
const transformed = asWebviewUri({
isExtensionDevelopmentDebug: this.environmentService.isExtensionDevelopment,
...this.environmentService,
remote: { authority: undefined },
}, this.webviewID, path).toString();
return `src="${transformed}"`;
});

View file

@ -34,6 +34,7 @@ suite('ExtHostWebview', () => {
webviewCspSource: '',
webviewResourceRoot: '',
isExtensionDevelopmentDebug: false,
remote: { authority: undefined },
}, undefined, new NullLogService(), NullApiDeprecationService);
const extHostWebviewPanels = new ExtHostWebviewPanels(rpcProtocol!, extHostWebviews, undefined);
@ -78,53 +79,14 @@ suite('ExtHostWebview', () => {
assert.strictEqual(lastInvokedDeserializer, serializerB);
});
test('asWebviewUri for desktop vscode-resource scheme', () => {
const extHostWebviews = new ExtHostWebviews(rpcProtocol!, {
webviewCspSource: '',
webviewResourceRoot: 'vscode-resource://{{resource}}',
isExtensionDevelopmentDebug: false,
}, undefined, new NullLogService(), NullApiDeprecationService);
const extHostWebviewPanels = new ExtHostWebviewPanels(rpcProtocol!, extHostWebviews, undefined);
const webview = extHostWebviewPanels.createWebviewPanel({} as any, 'type', 'title', 1, {});
assert.strictEqual(
webview.webview.asWebviewUri(URI.parse('file:///Users/codey/file.html')).toString(),
'vscode-resource://file///Users/codey/file.html',
'Unix basic'
);
assert.strictEqual(
webview.webview.asWebviewUri(URI.parse('file:///Users/codey/file.html#frag')).toString(),
'vscode-resource://file///Users/codey/file.html#frag',
'Unix should preserve fragment'
);
assert.strictEqual(
webview.webview.asWebviewUri(URI.parse('file:///Users/codey/f%20ile.html')).toString(),
'vscode-resource://file///Users/codey/f%20ile.html',
'Unix with encoding'
);
assert.strictEqual(
webview.webview.asWebviewUri(URI.parse('file://localhost/Users/codey/file.html')).toString(),
'vscode-resource://file//localhost/Users/codey/file.html',
'Unix should preserve authority'
);
assert.strictEqual(
webview.webview.asWebviewUri(URI.parse('file:///c:/codey/file.txt')).toString(),
'vscode-resource://file///c%3A/codey/file.txt',
'Windows C drive'
);
});
test('asWebviewUri for web endpoint', () => {
const extHostWebviews = new ExtHostWebviews(rpcProtocol!, {
webviewCspSource: '',
webviewResourceRoot: `https://{{uuid}}.webview.contoso.com/commit/{{resource}}`,
isExtensionDevelopmentDebug: false,
remote: {
authority: 'remote'
},
}, undefined, new NullLogService(), NullApiDeprecationService);
const extHostWebviewPanels = new ExtHostWebviewPanels(rpcProtocol!, extHostWebviews, undefined);
@ -137,31 +99,31 @@ suite('ExtHostWebview', () => {
assert.strictEqual(
stripEndpointUuid(webview.webview.asWebviewUri(URI.parse('file:///Users/codey/file.html')).toString()),
'webview.contoso.com/commit/file///Users/codey/file.html',
'webview.contoso.com/commit/remote/file//Users/codey/file.html',
'Unix basic'
);
assert.strictEqual(
stripEndpointUuid(webview.webview.asWebviewUri(URI.parse('file:///Users/codey/file.html#frag')).toString()),
'webview.contoso.com/commit/file///Users/codey/file.html#frag',
'webview.contoso.com/commit/remote/file//Users/codey/file.html#frag',
'Unix should preserve fragment'
);
assert.strictEqual(
stripEndpointUuid(webview.webview.asWebviewUri(URI.parse('file:///Users/codey/f%20ile.html')).toString()),
'webview.contoso.com/commit/file///Users/codey/f%20ile.html',
'webview.contoso.com/commit/remote/file//Users/codey/f%20ile.html',
'Unix with encoding'
);
assert.strictEqual(
stripEndpointUuid(webview.webview.asWebviewUri(URI.parse('file://localhost/Users/codey/file.html')).toString()),
'webview.contoso.com/commit/file//localhost/Users/codey/file.html',
'webview.contoso.com/commit/remote/file/localhost/Users/codey/file.html',
'Unix should preserve authority'
);
assert.strictEqual(
stripEndpointUuid(webview.webview.asWebviewUri(URI.parse('file:///c:/codey/file.txt')).toString()),
'webview.contoso.com/commit/file///c%3A/codey/file.txt',
'webview.contoso.com/commit/remote/file//c%3A/codey/file.txt',
'Windows C drive'
);
});