diff --git a/src/vs/workbench/api/common/shared/webview.ts b/src/vs/workbench/api/common/shared/webview.ts index 9c78a6bf9f1..5a099003c41 100644 --- a/src/vs/workbench/api/common/shared/webview.ts +++ b/src/vs/workbench/api/common/shared/webview.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { CharCode } from 'vs/base/common/charCode'; import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import type * as vscode from 'vscode'; @@ -57,9 +58,27 @@ export function asWebviewUri( return URI.from({ scheme: Schemas.https, - authority: `${resource.scheme}+${resource.authority}.${webviewRootResourceAuthority}`, + authority: `${resource.scheme}+${encodeAuthority(resource.authority)}.${webviewRootResourceAuthority}`, path: resource.path, fragment: resource.fragment, query: resource.query, }); } + +function encodeAuthority(authority: string): string { + return authority.replace(/./g, char => { + const code = char.charCodeAt(0); + if ( + (code >= CharCode.a && code <= CharCode.z) + || (code >= CharCode.A && code <= CharCode.Z) + || (code >= CharCode.Digit0 && code <= CharCode.Digit9) + ) { + return char; + } + return '-' + code.toString(16).padStart(4, '0'); + }); +} + +export function decodeAuthority(authority: string) { + return authority.replace(/-([0-9a-f]{4})/g, (_, code) => String.fromCharCode(parseInt(code, 16))); +} diff --git a/src/vs/workbench/contrib/webview/browser/webviewElement.ts b/src/vs/workbench/contrib/webview/browser/webviewElement.ts index 56cc3957919..5abdd9f3828 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewElement.ts @@ -28,7 +28,7 @@ import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remot import { ITunnelService } from 'vs/platform/remote/common/tunnel'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { WebviewPortMappingManager } from 'vs/platform/webview/common/webviewPortMapping'; -import { asWebviewUri, webviewGenericCspSource, webviewRootResourceAuthority } from 'vs/workbench/api/common/shared/webview'; +import { asWebviewUri, decodeAuthority, webviewGenericCspSource, webviewRootResourceAuthority } from 'vs/workbench/api/common/shared/webview'; import { loadLocalResource, WebviewResourceResponse } from 'vs/workbench/contrib/webview/browser/resourceLoading'; import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/browser/themeing'; import { areWebviewContentOptionsEqual, Webview, WebviewContentOptions, WebviewExtensionDescription, WebviewMessageReceivedEvent, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview'; @@ -243,9 +243,11 @@ export class IFrameWebview extends Disposable implements Webview { this._register(this.on(WebviewMessageChannels.loadResource, (entry: { id: number, path: string, query: string, scheme: string, authority: string, ifNoneMatch?: string }) => { try { + // Restore the authority we previously encoded + const authority = decodeAuthority(entry.authority); const uri = URI.from({ scheme: entry.scheme, - authority: entry.authority, + authority: authority, path: decodeURIComponent(entry.path), // This gets re-encoded query: entry.query ? decodeURIComponent(entry.query) : entry.query, }); diff --git a/src/vs/workbench/test/browser/api/extHostWebview.test.ts b/src/vs/workbench/test/browser/api/extHostWebview.test.ts index b00fe229f84..885fdbd6b0c 100644 --- a/src/vs/workbench/test/browser/api/extHostWebview.test.ts +++ b/src/vs/workbench/test/browser/api/extHostWebview.test.ts @@ -15,12 +15,12 @@ import { NullApiDeprecationService } from 'vs/workbench/api/common/extHostApiDep import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { ExtHostWebviews } from 'vs/workbench/api/common/extHostWebview'; import { ExtHostWebviewPanels } from 'vs/workbench/api/common/extHostWebviewPanels'; -import { webviewResourceBaseHost } from 'vs/workbench/api/common/shared/webview'; +import { decodeAuthority, webviewResourceBaseHost } from 'vs/workbench/api/common/shared/webview'; import { EditorGroupColumn } from 'vs/workbench/services/editor/common/editorGroupColumn'; import type * as vscode from 'vscode'; import { SingleProxyRPCProtocol } from './testRPCProtocol'; -suite('ExtHostWebview', () => { +suite.only('ExtHostWebview', () => { let rpcProtocol: (IExtHostRpcService & IExtHostContext) | undefined; @@ -119,6 +119,29 @@ suite('ExtHostWebview', () => { 'Unix basic' ); }); + + test('asWebviewUri for remote with / and + in name', () => { + const webview = createWebview(rpcProtocol, /* remoteAuthority */ 'remote'); + const authority = 'ssh-remote+localhost=foo/bar'; + + const sourceUri = URI.from({ + scheme: 'vscode-remote', + authority: authority, + path: '/Users/cody/x.png' + }); + + const webviewUri = webview.webview.asWebviewUri(sourceUri); + assert.strictEqual( + webviewUri.toString(), + `https://vscode-remote%2Bssh-002dremote-002blocalhost-003dfoo-002fbar.vscode-resource.vscode-webview.net/Users/cody/x.png`, + 'Check transform'); + + assert.strictEqual( + decodeAuthority(webviewUri.authority), + `vscode-remote+${authority}.vscode-resource.vscode-webview.net`, + 'Check decoded authority' + ); + }); }); function createWebview(rpcProtocol: (IExtHostRpcService & IExtHostContext) | undefined, remoteAuthority: string | undefined) {