Try to encode authority for asWebviewUri

Fixes #123494

This change attempts to address issues where `asWebviewUri` could create an valid uri but an invalid http uri. This issue appears to be when the host contains a forbidden character (even if it is percent encoded): https://url.spec.whatwg.org/#forbidden-host-code-point

Having one of these characters in the host causes the url to become invalid, resulting in network requests never being made

To fix this, I've added a very simple (poor) encoding mechanism for the authority. I went with my own mechanism over something like base64 because base64 can output `/` and it's also not easy to use across both node and browsers. I also considered base62 and punycode, but both of these would be best to pull in libraries for
This commit is contained in:
Matt Bierner 2021-08-07 00:19:57 -07:00
parent 813c0b2178
commit 649dd18019
No known key found for this signature in database
GPG key ID: 099C331567E11888
3 changed files with 49 additions and 5 deletions

View file

@ -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)));
}

View file

@ -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,
});

View file

@ -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) {