From 07f2fecd98a1d1df5ea4331af3c6dd1d472bc76a Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Wed, 10 Apr 2019 11:46:54 +0200 Subject: [PATCH] More webview ports --- src/vs/platform/remote/common/tunnel.ts | 21 +++++ src/vs/platform/remote/node/tunnelService.ts | 21 +++++ .../workbench/api/browser/mainThreadWindow.ts | 52 ++++++++++- .../browser/nodeless.simpleservices.ts | 14 +++ .../electron-browser/webviewElement.ts | 89 ++++++++++++++++--- src/vs/workbench/workbench.main.ts | 2 + 6 files changed, 183 insertions(+), 16 deletions(-) create mode 100644 src/vs/platform/remote/common/tunnel.ts create mode 100644 src/vs/platform/remote/node/tunnelService.ts diff --git a/src/vs/platform/remote/common/tunnel.ts b/src/vs/platform/remote/common/tunnel.ts new file mode 100644 index 00000000000..d92f3e45d91 --- /dev/null +++ b/src/vs/platform/remote/common/tunnel.ts @@ -0,0 +1,21 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; + +export const ITunnelService = createDecorator('tunnelService'); + +export interface RemoteTunnel { + readonly tunnelRemotePort: number; + readonly tunnelLocalPort: number; + + dispose(): void; +} + +export interface ITunnelService { + _serviceBrand: any; + + openTunnel(remotePort: number): Promise | undefined; +} diff --git a/src/vs/platform/remote/node/tunnelService.ts b/src/vs/platform/remote/node/tunnelService.ts new file mode 100644 index 00000000000..2b1e8a1a04f --- /dev/null +++ b/src/vs/platform/remote/node/tunnelService.ts @@ -0,0 +1,21 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ITunnelService, RemoteTunnel } from 'vs/platform/remote/common/tunnel'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; + +export class TunnelService implements ITunnelService { + _serviceBrand: any; + + public constructor( + ) { + } + + openTunnel(remotePort: number): Promise | undefined { + return undefined; + } +} + +registerSingleton(ITunnelService, TunnelService); diff --git a/src/vs/workbench/api/browser/mainThreadWindow.ts b/src/vs/workbench/api/browser/mainThreadWindow.ts index 85b76f7e64d..7f99842116d 100644 --- a/src/vs/workbench/api/browser/mainThreadWindow.ts +++ b/src/vs/workbench/api/browser/mainThreadWindow.ts @@ -9,17 +9,22 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { IWindowService, IWindowsService } from 'vs/platform/windows/common/windows'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { ExtHostContext, ExtHostWindowShape, IExtHostContext, MainContext, MainThreadWindowShape } from '../common/extHost.protocol'; +import { ITunnelService, RemoteTunnel } from 'vs/platform/remote/common/tunnel'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @extHostNamedCustomer(MainContext.MainThreadWindow) export class MainThreadWindow implements MainThreadWindowShape { private readonly proxy: ExtHostWindowShape; private disposables: IDisposable[] = []; + private readonly _tunnels = new Map>(); constructor( extHostContext: IExtHostContext, @IWindowService private readonly windowService: IWindowService, - @IWindowsService private readonly windowsService: IWindowsService + @IWindowsService private readonly windowsService: IWindowsService, + @ITunnelService private readonly tunnelService: ITunnelService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService ) { this.proxy = extHostContext.getProxy(ExtHostContext.ExtHostWindow); @@ -29,13 +34,54 @@ export class MainThreadWindow implements MainThreadWindowShape { dispose(): void { this.disposables = dispose(this.disposables); + + for (const tunnel of this._tunnels.values()) { + tunnel.then(tunnel => tunnel.dispose()); + } + this._tunnels.clear(); } $getWindowVisibility(): Promise { return this.windowService.isFocused(); } - $openUri(uri: UriComponents): Promise { - return this.windowsService.openExternal(URI.revive(uri).toString(true)); + async $openUri(uriComponent: UriComponents): Promise { + const uri = URI.revive(uriComponent); + if (!!this.environmentService.configuration.remoteAuthority) { + if (uri.scheme === 'http' || uri.scheme === 'https') { + const port = this.getLocalhostPort(uri); + if (typeof port === 'number') { + const tunnel = await this.getOrCreateTunnel(port); + if (tunnel) { + const tunneledUrl = uri.toString().replace( + new RegExp(`^${uri.scheme}://localhost:${port}/`), + `${uri.scheme}://localhost:${tunnel.tunnelLocalPort}/`); + return this.windowsService.openExternal(tunneledUrl); + } + } + } + } + + return this.windowsService.openExternal(uri.toString(true)); + } + + private getLocalhostPort(uri: URI): number | undefined { + const match = /^localhost:(\d+)$/.exec(uri.authority); + if (match) { + return +match[1]; + } + return undefined; + } + + private getOrCreateTunnel(remotePort: number): Promise | undefined { + const existing = this._tunnels.get(remotePort); + if (existing) { + return existing; + } + const tunnel = this.tunnelService.openTunnel(remotePort); + if (tunnel) { + this._tunnels.set(remotePort, tunnel); + } + return tunnel; } } diff --git a/src/vs/workbench/browser/nodeless.simpleservices.ts b/src/vs/workbench/browser/nodeless.simpleservices.ts index be406d6967b..83d1c2abddc 100644 --- a/src/vs/workbench/browser/nodeless.simpleservices.ts +++ b/src/vs/workbench/browser/nodeless.simpleservices.ts @@ -57,6 +57,7 @@ import { ITextResourcePropertiesService } from 'vs/editor/common/services/resour import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { Color, RGBA } from 'vs/base/common/color'; +import { ITunnelService } from 'vs/platform/remote/common/tunnel'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; export const workspaceResource = URI.from({ @@ -1434,4 +1435,17 @@ export class SimpleWorkspacesService implements IWorkspacesService { registerSingleton(IWorkspacesService, SimpleWorkspacesService); +//#endregion + +//#region remote + +class SimpleTunnelService implements ITunnelService { + _serviceBrand: any; + openTunnel(remotePort: number) { + return undefined; + } +} + +registerSingleton(ITunnelService, SimpleTunnelService); + //#endregion \ No newline at end of file diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts index 08f4b2c5147..562ed18cf67 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts @@ -17,14 +17,36 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import * as colorRegistry from 'vs/platform/theme/common/colorRegistry'; import { DARK, ITheme, IThemeService, LIGHT } from 'vs/platform/theme/common/themeService'; import { registerFileProtocol, WebviewProtocol } from 'vs/workbench/contrib/webview/electron-browser/webviewProtocols'; -import { areWebviewInputOptionsEqual } from '../browser/webviewEditorService'; -import { WebviewFindWidget } from '../browser/webviewFindWidget'; +import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { areWebviewInputOptionsEqual } from '../browser/webviewEditorService'; +import { WebviewFindWidget } from '../browser/webviewFindWidget'; import { WebviewContentOptions, WebviewPortMapping, WebviewOptions, Webview } from 'vs/workbench/contrib/webview/common/webview'; +import { ITunnelService, RemoteTunnel } from 'vs/platform/remote/common/tunnel'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEditorOptions, EDITOR_FONT_DEFAULTS } from 'vs/editor/common/config/editorOptions'; +export interface WebviewPortMapping { + readonly port: number; + readonly resolvedPort: number; +} + +export interface WebviewOptions { + readonly allowSvgs?: boolean; + readonly extension?: { + readonly location: URI; + readonly id?: ExtensionIdentifier; + }; + readonly enableFindWidget?: boolean; +} + +export interface WebviewContentOptions { + readonly allowScripts?: boolean; + readonly svgWhiteList?: string[]; + readonly localResourceRoots?: ReadonlyArray; + readonly portMappings?: ReadonlyArray; +} interface IKeydownEvent { key: string; @@ -126,11 +148,15 @@ class WebviewProtocolProvider extends Disposable { class WebviewPortMappingProvider extends Disposable { + private readonly _tunnels = new Map>(); + constructor( session: WebviewSession, + extensionLocation: URI | undefined, mappings: () => ReadonlyArray, + private readonly tunnelService: ITunnelService, extensionId: ExtensionIdentifier | undefined, - @ITelemetryService telemetryService: ITelemetryService, + @ITelemetryService telemetryService: ITelemetryService ) { super(); @@ -148,7 +174,7 @@ class WebviewPortMappingProvider extends Disposable { hasLogged = true; /* __GDPR__ - "webview.accessLocalhost" : { + "webview.accessLocalhost" : { "extension" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } } */ @@ -157,12 +183,25 @@ class WebviewPortMappingProvider extends Disposable { const port = +localhostMatch[1]; for (const mapping of mappings()) { - if (mapping.port === port && mapping.port !== mapping.resolvedPort) { - return { - redirectURL: details.url.replace( - new RegExp(`^${uri.scheme}://localhost:${mapping.port}/`), - `${uri.scheme}://localhost:${mapping.resolvedPort}/`) - }; + if (mapping.port === port) { + if (extensionLocation && extensionLocation.scheme === REMOTE_HOST_SCHEME) { + const tunnel = await this.getOrCreateTunnel(mapping.resolvedPort); + if (tunnel) { + return { + redirectURL: details.url.replace( + new RegExp(`^${uri.scheme}://localhost:${mapping.port}/`), + `${uri.scheme}://localhost:${tunnel.tunnelLocalPort}/`) + }; + } + } + + if (mapping.port !== mapping.resolvedPort) { + return { + redirectURL: details.url.replace( + new RegExp(`^${uri.scheme}://localhost:${mapping.port}/`), + `${uri.scheme}://localhost:${mapping.resolvedPort}/`) + }; + } } } } @@ -170,6 +209,27 @@ class WebviewPortMappingProvider extends Disposable { return undefined; }); } + + dispose() { + super.dispose(); + + for (const tunnel of this._tunnels.values()) { + tunnel.then(tunnel => tunnel.dispose()); + } + this._tunnels.clear(); + } + + private getOrCreateTunnel(remotePort: number): Promise | undefined { + const existing = this._tunnels.get(remotePort); + if (existing) { + return existing; + } + const tunnel = this.tunnelService.openTunnel(remotePort); + if (tunnel) { + this._tunnels.set(remotePort, tunnel); + } + return tunnel; + } } class SvgBlocker extends Disposable { @@ -314,6 +374,7 @@ export class WebviewElement extends Disposable implements Webview { @IThemeService themeService: IThemeService, @IEnvironmentService environmentService: IEnvironmentService, @IFileService fileService: IFileService, + @ITunnelService tunnelService: ITunnelService, @ITelemetryService telemetryService: ITelemetryService, @IConfigurationService private readonly _configurationService: IConfigurationService, ) { @@ -354,10 +415,12 @@ export class WebviewElement extends Disposable implements Webview { this._register(new WebviewPortMappingProvider( session, - () => (this._contentOptions.portMappings || []), + _options.extension ? _options.extension.location : undefined, + () => (this._contentOptions.portMappings || [{ port: 3000, resolvedPort: 4000 }]), + tunnelService, _options.extension ? _options.extension.id : undefined, - telemetryService)); - + telemetryService + )); if (!this._options.allowSvgs) { const svgBlocker = this._register(new SvgBlocker(session, this._contentOptions)); diff --git a/src/vs/workbench/workbench.main.ts b/src/vs/workbench/workbench.main.ts index 227ae1f662c..ebf9cbe1aa2 100644 --- a/src/vs/workbench/workbench.main.ts +++ b/src/vs/workbench/workbench.main.ts @@ -88,6 +88,8 @@ import { MenubarService } from 'vs/platform/menubar/electron-browser/menubarServ import { IURLService } from 'vs/platform/url/common/url'; import { RelayURLService } from 'vs/platform/url/electron-browser/urlService'; +import 'vs/platform/remote/node/tunnelService'; + import 'vs/workbench/services/bulkEdit/browser/bulkEditService'; import 'vs/workbench/services/integrity/node/integrityService'; import 'vs/workbench/services/keybinding/common/keybindingEditing';