From b4e4bd16421612b9f48fba8ad374be1d10e283e9 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Wed, 13 Jan 2021 12:29:07 +0100 Subject: [PATCH] Allow tunnel providers to support making a tunnel public --- src/vs/platform/remote/common/tunnel.ts | 45 ++++++++--- src/vs/platform/remote/node/tunnelService.ts | 14 +--- src/vs/vscode.proposed.d.ts | 3 + .../api/browser/mainThreadTunnelService.ts | 1 + .../api/common/extHostTunnelService.ts | 12 ++- .../api/node/extHostTunnelService.ts | 3 +- .../contrib/remote/browser/tunnelView.ts | 76 ++++++++++++++++++- .../contrib/remote/common/tunnelFactory.ts | 3 +- .../sandbox.simpleservices.ts | 7 +- .../remote/browser/tunnelServiceImpl.ts | 13 +--- .../remote/common/remoteExplorerService.ts | 36 ++++++--- src/vs/workbench/workbench.web.api.ts | 4 + 12 files changed, 166 insertions(+), 51 deletions(-) diff --git a/src/vs/platform/remote/common/tunnel.ts b/src/vs/platform/remote/common/tunnel.ts index 7a38ca6e9fa..f8a71a1f9be 100644 --- a/src/vs/platform/remote/common/tunnel.ts +++ b/src/vs/platform/remote/common/tunnel.ts @@ -18,13 +18,15 @@ export interface RemoteTunnel { readonly tunnelRemoteHost: string; readonly tunnelLocalPort?: number; readonly localAddress: string; + readonly public: boolean; dispose(silent?: boolean): Promise; } export interface TunnelOptions { - remoteAddress: { port: number, host: string }; + remoteAddress: { port: number, host: string; }; localAddressPort?: number; label?: string; + public?: boolean; } export interface TunnelCreationOptions { @@ -33,6 +35,7 @@ export interface TunnelCreationOptions { export interface TunnelProviderFeatures { elevation: boolean; + public: boolean; } export interface ITunnelProvider { @@ -43,17 +46,18 @@ export interface ITunnelService { readonly _serviceBrand: undefined; readonly tunnels: Promise; + readonly canMakePublic: boolean; readonly onTunnelOpened: Event; - readonly onTunnelClosed: Event<{ host: string, port: number }>; + readonly onTunnelClosed: Event<{ host: string, port: number; }>; readonly canElevate: boolean; canTunnel(uri: URI): boolean; - openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number, elevateIfNeeded?: boolean): Promise | undefined; + openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number, elevateIfNeeded?: boolean, isPublic?: boolean): Promise | undefined; closeTunnel(remoteHost: string, remotePort: number): Promise; setTunnelProvider(provider: ITunnelProvider | undefined, features: TunnelProviderFeatures): IDisposable; } -export function extractLocalHostUriMetaDataForPortMapping(uri: URI): { address: string, port: number } | undefined { +export function extractLocalHostUriMetaDataForPortMapping(uri: URI): { address: string, port: number; } | undefined { if (uri.scheme !== 'http' && uri.scheme !== 'https') { return undefined; } @@ -90,11 +94,12 @@ export abstract class AbstractTunnelService implements ITunnelService { private _onTunnelOpened: Emitter = new Emitter(); public onTunnelOpened: Event = this._onTunnelOpened.event; - private _onTunnelClosed: Emitter<{ host: string, port: number }> = new Emitter(); - public onTunnelClosed: Event<{ host: string, port: number }> = this._onTunnelClosed.event; - protected readonly _tunnels = new Map }>>(); + private _onTunnelClosed: Emitter<{ host: string, port: number; }> = new Emitter(); + public onTunnelClosed: Event<{ host: string, port: number; }> = this._onTunnelClosed.event; + protected readonly _tunnels = new Map; }>>(); protected _tunnelProvider: ITunnelProvider | undefined; protected _canElevate: boolean = false; + private _canMakePublic: boolean = false; public constructor( @ILogService protected readonly logService: ILogService @@ -105,14 +110,18 @@ export abstract class AbstractTunnelService implements ITunnelService { if (!provider) { // clear features this._canElevate = false; + this._canMakePublic = false; return { dispose: () => { } }; } this._canElevate = features.elevation; + this._canMakePublic = features.public; return { dispose: () => { this._tunnelProvider = undefined; + this._canElevate = false; + this._canMakePublic = false; } }; } @@ -121,6 +130,10 @@ export abstract class AbstractTunnelService implements ITunnelService { return this._canElevate; } + public get canMakePublic() { + return this._canMakePublic; + } + public get tunnels(): Promise { return new Promise(async (resolve) => { const tunnels: RemoteTunnel[] = []; @@ -148,7 +161,7 @@ export abstract class AbstractTunnelService implements ITunnelService { this._tunnels.clear(); } - openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number, elevateIfNeeded: boolean = false): Promise | undefined { + openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number, elevateIfNeeded: boolean = false, isPublic: boolean = false): Promise | undefined { if (!addressProvider) { return undefined; } @@ -157,7 +170,7 @@ export abstract class AbstractTunnelService implements ITunnelService { remoteHost = 'localhost'; } - const resolvedTunnel = this.retainOrCreateTunnel(addressProvider, remoteHost, remotePort, localPort, elevateIfNeeded); + const resolvedTunnel = this.retainOrCreateTunnel(addressProvider, remoteHost, remotePort, localPort, elevateIfNeeded, isPublic); if (!resolvedTunnel) { return resolvedTunnel; } @@ -182,6 +195,7 @@ export abstract class AbstractTunnelService implements ITunnelService { tunnelRemoteHost: tunnel.tunnelRemoteHost, tunnelLocalPort: tunnel.tunnelLocalPort, localAddress: tunnel.localAddress, + public: tunnel.public, dispose: async () => { const existingHost = this._tunnels.get(tunnel.tunnelRemoteHost); if (existingHost) { @@ -261,7 +275,18 @@ export abstract class AbstractTunnelService implements ITunnelService { return !!extractLocalHostUriMetaDataForPortMapping(uri); } - protected abstract retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean): Promise | undefined; + protected abstract retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, isPublic: boolean): Promise | undefined; + + protected createWithProvider(tunnelProvider: ITunnelProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, isPublic: boolean): Promise | undefined { + const preferredLocalPort = localPort === undefined ? remotePort : localPort; + const creationInfo = { elevationRequired: elevateIfNeeded ? isPortPrivileged(preferredLocalPort) : false }; + const tunnelOptions: TunnelOptions = { remoteAddress: { host: remoteHost, port: remotePort }, localAddressPort: localPort, public: isPublic }; + const tunnel = tunnelProvider.forwardPort(tunnelOptions, creationInfo); + if (tunnel) { + this.addTunnelToMap(remoteHost, remotePort, tunnel); + } + return tunnel; + } } diff --git a/src/vs/platform/remote/node/tunnelService.ts b/src/vs/platform/remote/node/tunnelService.ts index 5080db52375..4deae02504a 100644 --- a/src/vs/platform/remote/node/tunnelService.ts +++ b/src/vs/platform/remote/node/tunnelService.ts @@ -11,7 +11,7 @@ import { NodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; import { ILogService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; import { connectRemoteAgentTunnel, IConnectionOptions, IAddressProvider, ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection'; -import { AbstractTunnelService, isPortPrivileged, RemoteTunnel } from 'vs/platform/remote/common/tunnel'; +import { AbstractTunnelService, RemoteTunnel } from 'vs/platform/remote/common/tunnel'; import { nodeSocketFactory } from 'vs/platform/remote/node/nodeSocketFactory'; import { ISignService } from 'vs/platform/sign/common/sign'; @@ -26,6 +26,7 @@ class NodeRemoteTunnel extends Disposable implements RemoteTunnel { public tunnelLocalPort!: number; public tunnelRemoteHost: string; public localAddress!: string; + public readonly public = false; private readonly _options: IConnectionOptions; private readonly _server: net.Server; @@ -139,7 +140,7 @@ export class BaseTunnelService extends AbstractTunnelService { super(logService); } - protected retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean): Promise | undefined { + protected retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, isPublic: boolean): Promise | undefined { const existing = this.getTunnelFromMap(remoteHost, remotePort); if (existing) { ++existing.refcount; @@ -147,14 +148,7 @@ export class BaseTunnelService extends AbstractTunnelService { } if (this._tunnelProvider) { - const preferredLocalPort = localPort === undefined ? remotePort : localPort; - const creationInfo = { elevationRequired: elevateIfNeeded ? isPortPrivileged(preferredLocalPort) : false }; - const tunnelOptions = { remoteAddress: { host: remoteHost, port: remotePort }, localAddressPort: localPort }; - const tunnel = this._tunnelProvider.forwardPort(tunnelOptions, creationInfo); - if (tunnel) { - this.addTunnelToMap(remoteHost, remotePort, tunnel); - } - return tunnel; + return this.createWithProvider(this._tunnelProvider, remoteHost, remotePort, localPort, elevateIfNeeded, isPublic); } else { const options: IConnectionOptions = { commit: this.productService.commit, diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index ec22ca81c20..113d4621efe 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -154,12 +154,14 @@ declare module 'vscode' { // The desired local port. If this port can't be used, then another will be chosen. localAddressPort?: number; label?: string; + public?: boolean; } export interface TunnelDescription { remoteAddress: { port: number, host: string; }; //The complete local address(ex. localhost:1234) localAddress: { port: number, host: string; } | string; + public?: boolean; } export interface Tunnel extends TunnelDescription { @@ -221,6 +223,7 @@ declare module 'vscode' { */ tunnelFeatures?: { elevation: boolean; + public: boolean; }; } diff --git a/src/vs/workbench/api/browser/mainThreadTunnelService.ts b/src/vs/workbench/api/browser/mainThreadTunnelService.ts index afa43e93fc2..85d5e71f8f2 100644 --- a/src/vs/workbench/api/browser/mainThreadTunnelService.ts +++ b/src/vs/workbench/api/browser/mainThreadTunnelService.ts @@ -66,6 +66,7 @@ export class MainThreadTunnelService extends Disposable implements MainThreadTun tunnelRemoteHost: tunnel.remoteAddress.host, localAddress: typeof tunnel.localAddress === 'string' ? tunnel.localAddress : makeAddress(tunnel.localAddress.host, tunnel.localAddress.port), tunnelLocalPort: typeof tunnel.localAddress !== 'string' ? tunnel.localAddress.port : undefined, + public: tunnel.public, dispose: async (silent?: boolean) => { return this._proxy.$closeTunnel({ host: tunnel.remoteAddress.host, port: tunnel.remoteAddress.port }, silent); } diff --git a/src/vs/workbench/api/common/extHostTunnelService.ts b/src/vs/workbench/api/common/extHostTunnelService.ts index ba25e0cc357..53dc677784d 100644 --- a/src/vs/workbench/api/common/extHostTunnelService.ts +++ b/src/vs/workbench/api/common/extHostTunnelService.ts @@ -15,14 +15,22 @@ import { IExtensionDescription } from 'vs/platform/extensions/common/extensions' export interface TunnelDto { remoteAddress: { port: number, host: string }; localAddress: { port: number, host: string } | string; + public: boolean; } export namespace TunnelDto { export function fromApiTunnel(tunnel: vscode.Tunnel): TunnelDto { - return { remoteAddress: tunnel.remoteAddress, localAddress: tunnel.localAddress }; + return { remoteAddress: tunnel.remoteAddress, localAddress: tunnel.localAddress, public: !!tunnel.public }; } export function fromServiceTunnel(tunnel: RemoteTunnel): TunnelDto { - return { remoteAddress: { host: tunnel.tunnelRemoteHost, port: tunnel.tunnelRemotePort }, localAddress: tunnel.localAddress }; + return { + remoteAddress: { + host: tunnel.tunnelRemoteHost, + port: tunnel.tunnelRemotePort + }, + localAddress: tunnel.localAddress, + public: tunnel.public + }; } } diff --git a/src/vs/workbench/api/node/extHostTunnelService.ts b/src/vs/workbench/api/node/extHostTunnelService.ts index e5610491a5b..9074abc3fa2 100644 --- a/src/vs/workbench/api/node/extHostTunnelService.ts +++ b/src/vs/workbench/api/node/extHostTunnelService.ts @@ -196,7 +196,8 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe if (provider.tunnelFactory) { this._forwardPortProvider = provider.tunnelFactory; await this._proxy.$setTunnelProvider(provider.tunnelFeatures ?? { - elevation: false + elevation: false, + public: false }); } } else { diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index 3cd8e109498..129a3329f29 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -26,7 +26,7 @@ import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { ActionRunner, IAction } from 'vs/base/common/actions'; import { IMenuService, MenuId, IMenu, MenuRegistry, MenuItemAction, ILocalizedString, SubmenuItemAction, Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { createAndFillInContextMenuActions, createAndFillInActionBarActions, MenuEntryActionViewItem, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { IRemoteExplorerService, TunnelModel, makeAddress, TunnelType, ITunnelItem, Tunnel, mapHasAddressLocalhostOrAllInterfaces, TUNNEL_VIEW_ID, parseAddress, CandidatePort } from 'vs/workbench/services/remote/common/remoteExplorerService'; +import { IRemoteExplorerService, TunnelModel, makeAddress, TunnelType, ITunnelItem, Tunnel, mapHasAddressLocalhostOrAllInterfaces, TUNNEL_VIEW_ID, parseAddress, CandidatePort, TunnelPrivacy } from 'vs/workbench/services/remote/common/remoteExplorerService'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; @@ -410,7 +410,17 @@ interface ITunnelGroup { class TunnelItem implements ITunnelItem { static createFromTunnel(tunnel: Tunnel, type: TunnelType = TunnelType.Forwarded, closeable?: boolean) { - return new TunnelItem(type, tunnel.remoteHost, tunnel.remotePort, tunnel.localAddress, tunnel.localPort, closeable === undefined ? tunnel.closeable : closeable, tunnel.name, tunnel.runningProcess, tunnel.source, tunnel.pid); + return new TunnelItem(type, + tunnel.remoteHost, + tunnel.remotePort, + tunnel.localAddress, + tunnel.localPort, + closeable === undefined ? tunnel.closeable : closeable, + tunnel.name, + tunnel.runningProcess, + tunnel.source, + tunnel.pid, + tunnel.privacy); } constructor( @@ -423,7 +433,8 @@ class TunnelItem implements ITunnelItem { public name?: string, private runningProcess?: string, private source?: string, - private pid?: number + private pid?: number, + public privacy?: TunnelPrivacy, ) { } get label(): string { if (this.name) { @@ -511,6 +522,7 @@ class TunnelItem implements ITunnelItem { export const TunnelTypeContextKey = new RawContextKey('tunnelType', TunnelType.Add); export const TunnelCloseableContextKey = new RawContextKey('tunnelCloseable', false); +const TunnelPrivacyContextKey = new RawContextKey('tunnelPrivacy', undefined); const TunnelViewFocusContextKey = new RawContextKey('tunnelViewFocus', false); const TunnelViewSelectionKeyName = 'tunnelViewSelection'; const TunnelViewSelectionContextKey = new RawContextKey(TunnelViewSelectionKeyName, undefined); @@ -524,6 +536,7 @@ export class TunnelPanel extends ViewPane { private tree!: TunnelDataTree; private tunnelTypeContext: IContextKey; private tunnelCloseableContext: IContextKey; + private tunnelPrivacyContext: IContextKey; private tunnelViewFocusContext: IContextKey; private tunnelViewSelectionContext: IContextKey; private portChangableContextKey: IContextKey; @@ -552,6 +565,7 @@ export class TunnelPanel extends ViewPane { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); this.tunnelTypeContext = TunnelTypeContextKey.bindTo(contextKeyService); this.tunnelCloseableContext = TunnelCloseableContextKey.bindTo(contextKeyService); + this.tunnelPrivacyContext = TunnelPrivacyContextKey.bindTo(contextKeyService); this.tunnelViewFocusContext = TunnelViewFocusContextKey.bindTo(contextKeyService); this.tunnelViewSelectionContext = TunnelViewSelectionContextKey.bindTo(contextKeyService); this.portChangableContextKey = PortChangableContextKey.bindTo(contextKeyService); @@ -679,11 +693,13 @@ export class TunnelPanel extends ViewPane { this.tunnelViewSelectionContext.set(item); this.tunnelTypeContext.set(item.tunnelType); this.tunnelCloseableContext.set(!!item.closeable); + this.tunnelPrivacyContext.set(item.privacy); this.portChangableContextKey.set(!!item.localPort); } else { this.tunnelTypeContext.reset(); this.tunnelViewSelectionContext.reset(); this.tunnelCloseableContext.reset(); + this.tunnelPrivacyContext.reset(); this.portChangableContextKey.reset(); } } @@ -702,10 +718,12 @@ export class TunnelPanel extends ViewPane { this.tree!.setFocus([node]); this.tunnelTypeContext.set(node.tunnelType); this.tunnelCloseableContext.set(!!node.closeable); + this.tunnelPrivacyContext.set(node.privacy); this.portChangableContextKey.set(!!node.localPort); } else { this.tunnelTypeContext.set(TunnelType.Add); this.tunnelCloseableContext.set(false); + this.tunnelPrivacyContext.set(undefined); this.portChangableContextKey.set(false); } @@ -1073,6 +1091,36 @@ namespace ChangeLocalPortAction { } } +namespace MakePortPublicAction { + export const ID = 'remote.tunnel.makePublic'; + export const LABEL = nls.localize('remote.tunnel.makePublic', "Make Public"); + + export function handler(): ICommandHandler { + return async (accessor, arg) => { + if (arg instanceof TunnelItem) { + const remoteExplorerService = accessor.get(IRemoteExplorerService); + await remoteExplorerService.close({ host: arg.remoteHost, port: arg.remotePort }); + return remoteExplorerService.forward({ host: arg.remoteHost, port: arg.remotePort }, arg.localPort, arg.name, undefined, true, true); + } + }; + } +} + +namespace MakePortPrivateAction { + export const ID = 'remote.tunnel.makePrivate'; + export const LABEL = nls.localize('remote.tunnel.makePrivate', "Make Private"); + + export function handler(): ICommandHandler { + return async (accessor, arg) => { + if (arg instanceof TunnelItem) { + const remoteExplorerService = accessor.get(IRemoteExplorerService); + await remoteExplorerService.close({ host: arg.remoteHost, port: arg.remotePort }); + return remoteExplorerService.forward({ host: arg.remoteHost, port: arg.remotePort }, arg.localPort, arg.name, undefined, true, false); + } + }; + } +} + const tunnelViewCommandsWeightBonus = 10; // give our commands a little bit more weight over other default list/tree commands KeybindingsRegistry.registerCommandAndKeybindingRule({ @@ -1110,6 +1158,8 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ }); CommandsRegistry.registerCommand(CopyAddressAction.COMMANDPALETTE_ID, CopyAddressAction.commandPaletteHandler()); CommandsRegistry.registerCommand(ChangeLocalPortAction.ID, ChangeLocalPortAction.handler()); +CommandsRegistry.registerCommand(MakePortPublicAction.ID, MakePortPublicAction.handler()); +CommandsRegistry.registerCommand(MakePortPrivateAction.ID, MakePortPrivateAction.handler()); MenuRegistry.appendMenuItem(MenuId.CommandPalette, ({ command: { @@ -1188,6 +1238,24 @@ MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({ MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({ group: '1_manage', order: 0, + command: { + id: MakePortPublicAction.ID, + title: MakePortPublicAction.LABEL, + }, + when: TunnelPrivacyContextKey.isEqualTo(TunnelPrivacy.Private) +})); +MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({ + group: '1_manage', + order: 0, + command: { + id: MakePortPrivateAction.ID, + title: MakePortPrivateAction.LABEL, + }, + when: TunnelPrivacyContextKey.isEqualTo(TunnelPrivacy.Public) +})); +MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({ + group: '1_manage', + order: 1, command: { id: ChangeLocalPortAction.ID, title: ChangeLocalPortAction.LABEL, @@ -1205,7 +1273,7 @@ MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({ })); MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({ group: '1_manage', - order: 1, + order: 2, command: { id: ClosePortAction.INLINE_ID, title: ClosePortAction.LABEL, diff --git a/src/vs/workbench/contrib/remote/common/tunnelFactory.ts b/src/vs/workbench/contrib/remote/common/tunnelFactory.ts index 801f30bca12..20ee629634f 100644 --- a/src/vs/workbench/contrib/remote/common/tunnelFactory.ts +++ b/src/vs/workbench/contrib/remote/common/tunnelFactory.ts @@ -36,13 +36,14 @@ export class TunnelFactoryContribution extends Disposable implements IWorkbenchC // The tunnel factory may give us an inaccessible local address. // To make sure this doesn't happen, resolve the uri immediately. localAddress: (await openerService.resolveExternalUri(URI.parse(localAddress))).resolved.toString(), + public: !!tunnel.public, dispose: async () => { await tunnel.dispose; } }; resolve(remoteTunnel); }); }); } - }, environmentService.options?.tunnelProvider?.features ?? { elevation: false })); + }, environmentService.options?.tunnelProvider?.features ?? { elevation: false, public: false })); remoteExplorerService.setTunnelInformation(undefined); } } diff --git a/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts b/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts index 13dc420cdd8..18285df120c 100644 --- a/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts +++ b/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts @@ -31,7 +31,7 @@ import { IWebviewService, WebviewContentOptions, WebviewElement, WebviewExtensio import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { AbstractTextFileService } from 'vs/workbench/services/textfile/browser/textFileService'; import { IExtensionManagementServer, IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -import { ITunnelProvider, ITunnelService, RemoteTunnel } from 'vs/platform/remote/common/tunnel'; +import { ITunnelProvider, ITunnelService, RemoteTunnel, TunnelProviderFeatures } from 'vs/platform/remote/common/tunnel'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { IManualSyncTask, IResourcePreview, ISyncResourceHandle, ISyncTask, IUserDataAutoSyncService, IUserDataSyncService, IUserDataSyncStore, IUserDataSyncStoreManagementService, SyncResource, SyncStatus, UserDataSyncStoreType } from 'vs/platform/userDataSync/common/userDataSync'; import { IUserDataSyncAccount, IUserDataSyncAccountService } from 'vs/platform/userDataSync/common/userDataSyncAccount'; @@ -560,14 +560,15 @@ class SimpleTunnelService implements ITunnelService { tunnels: Promise = Promise.resolve([]); canElevate: boolean = false; - + canMakePublic = false; onTunnelOpened = Event.None; onTunnelClosed = Event.None; canTunnel(uri: URI): boolean { return false; } openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number): Promise | undefined { return undefined; } + async changeTunnelPrivacy(remoteHost: string, remotePort: number, isPublic: boolean): Promise { return undefined; } async closeTunnel(remoteHost: string, remotePort: number): Promise { } - setTunnelProvider(provider: ITunnelProvider | undefined): IDisposable { return Disposable.None; } + setTunnelProvider(provider: ITunnelProvider | undefined, features: TunnelProviderFeatures): IDisposable { return Disposable.None; } } registerSingleton(ITunnelService, SimpleTunnelService); diff --git a/src/vs/workbench/services/remote/browser/tunnelServiceImpl.ts b/src/vs/workbench/services/remote/browser/tunnelServiceImpl.ts index 7917fbc5a1e..e652d1ded49 100644 --- a/src/vs/workbench/services/remote/browser/tunnelServiceImpl.ts +++ b/src/vs/workbench/services/remote/browser/tunnelServiceImpl.ts @@ -6,7 +6,7 @@ import { URI } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; import { IAddressProvider } from 'vs/platform/remote/common/remoteAgentConnection'; -import { AbstractTunnelService, isPortPrivileged, RemoteTunnel } from 'vs/platform/remote/common/tunnel'; +import { AbstractTunnelService, RemoteTunnel } from 'vs/platform/remote/common/tunnel'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; export class TunnelService extends AbstractTunnelService { @@ -17,7 +17,7 @@ export class TunnelService extends AbstractTunnelService { super(logService); } - protected retainOrCreateTunnel(_addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean): Promise | undefined { + protected retainOrCreateTunnel(_addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort: number | undefined, elevateIfNeeded: boolean, isPublic: boolean): Promise | undefined { const existing = this.getTunnelFromMap(remoteHost, remotePort); if (existing) { ++existing.refcount; @@ -25,14 +25,7 @@ export class TunnelService extends AbstractTunnelService { } if (this._tunnelProvider) { - const preferredLocalPort = localPort === undefined ? remotePort : localPort; - const tunnelOptions = { remoteAddress: { host: remoteHost, port: remotePort }, localAddressPort: localPort }; - const creationInfo = { elevationRequired: elevateIfNeeded ? isPortPrivileged(preferredLocalPort) : false }; - const tunnel = this._tunnelProvider.forwardPort(tunnelOptions, creationInfo); - if (tunnel) { - this.addTunnelToMap(remoteHost, remotePort, tunnel); - } - return tunnel; + this.createWithProvider(this._tunnelProvider, remoteHost, remotePort, localPort, elevateIfNeeded, isPublic); } return undefined; } diff --git a/src/vs/workbench/services/remote/common/remoteExplorerService.ts b/src/vs/workbench/services/remote/common/remoteExplorerService.ts index 63a0c592487..46b638c4797 100644 --- a/src/vs/workbench/services/remote/common/remoteExplorerService.ts +++ b/src/vs/workbench/services/remote/common/remoteExplorerService.ts @@ -29,6 +29,12 @@ export enum TunnelType { Add = 'Add' } +export enum TunnelPrivacy { + ConstantPrivate = 'ConstantPrivate', // private, and changing is unsupported + Private = 'Private', + Public = 'Public' +} + export interface ITunnelItem { tunnelType: TunnelType; remoteHost: string; @@ -37,6 +43,7 @@ export interface ITunnelItem { localPort?: number; name?: string; closeable?: boolean; + privacy?: TunnelPrivacy; description?: string; wideDescription?: string; readonly icon?: ThemeIcon; @@ -50,6 +57,7 @@ export interface Tunnel { localPort?: number; name?: string; closeable?: boolean; + privacy: TunnelPrivacy; runningProcess: string | undefined; pid: number | undefined; source?: string; @@ -252,7 +260,8 @@ export class TunnelModel extends Disposable { localAddress: tunnel.localAddress, localPort: tunnel.tunnelLocalPort, runningProcess: matchingCandidate?.detail, - pid: matchingCandidate?.pid + pid: matchingCandidate?.pid, + privacy: this.makeTunnelPrivacy(tunnel.public) }); this.remoteTunnels.set(key, tunnel); } @@ -271,7 +280,8 @@ export class TunnelModel extends Disposable { localPort: tunnel.tunnelLocalPort, closeable: true, runningProcess: matchingCandidate?.detail, - pid: matchingCandidate?.pid + pid: matchingCandidate?.pid, + privacy: this.makeTunnelPrivacy(tunnel.public) }); } this.storeForwarded(); @@ -288,12 +298,16 @@ export class TunnelModel extends Disposable { })); } + private makeTunnelPrivacy(isPublic: boolean) { + return isPublic ? TunnelPrivacy.Public : this.tunnelService.canMakePublic ? TunnelPrivacy.Private : TunnelPrivacy.ConstantPrivate; + } + async restoreForwarded() { if (this.configurationService.getValue('remote.restoreForwardedPorts')) { if (this.tunnelRestoreValue) { (JSON.parse(this.tunnelRestoreValue))?.forEach(tunnel => { if (!mapHasAddressLocalhostOrAllInterfaces(this.detected, tunnel.remoteHost, tunnel.remotePort)) { - this.forward({ host: tunnel.remoteHost, port: tunnel.remotePort }, tunnel.localPort, tunnel.name); + this.forward({ host: tunnel.remoteHost, port: tunnel.remotePort }, tunnel.localPort, tunnel.name, undefined, undefined, tunnel.privacy === TunnelPrivacy.Public); } }); } @@ -306,7 +320,7 @@ export class TunnelModel extends Disposable { } } - async forward(remote: { host: string, port: number }, local?: number, name?: string, source?: string, elevateIfNeeded?: boolean): Promise { + async forward(remote: { host: string, port: number }, local?: number, name?: string, source?: string, elevateIfNeeded?: boolean, isPublic?: boolean): Promise { const existingTunnel = mapHasAddressLocalhostOrAllInterfaces(this.forwarded, remote.host, remote.port); if (!existingTunnel) { const authority = this.environmentService.remoteAuthority; @@ -316,7 +330,7 @@ export class TunnelModel extends Disposable { const attributes = this.portsAttributes.getAttributes(local !== undefined ? local : remote.port); - const tunnel = await this.tunnelService.openTunnel(addressProvider, remote.host, remote.port, local, (!elevateIfNeeded) ? attributes?.elevateIfNeeded : elevateIfNeeded); + const tunnel = await this.tunnelService.openTunnel(addressProvider, remote.host, remote.port, local, (!elevateIfNeeded) ? attributes?.elevateIfNeeded : elevateIfNeeded, isPublic); if (tunnel && tunnel.localAddress) { const matchingCandidate = mapHasAddressLocalhostOrAllInterfaces(this._candidates ?? new Map(), remote.host, remote.port); const newForward: Tunnel = { @@ -328,7 +342,8 @@ export class TunnelModel extends Disposable { localAddress: tunnel.localAddress, runningProcess: matchingCandidate?.detail, pid: matchingCandidate?.pid, - source + source, + privacy: this.makeTunnelPrivacy(tunnel.public) }; const key = makeAddress(remote.host, remote.port); this.forwarded.set(key, newForward); @@ -380,7 +395,8 @@ export class TunnelModel extends Disposable { localAddress: typeof tunnel.localAddress === 'string' ? tunnel.localAddress : makeAddress(tunnel.localAddress.host, tunnel.localAddress.port), closeable: false, runningProcess: matchingCandidate?.detail, - pid: matchingCandidate?.pid + pid: matchingCandidate?.pid, + privacy: TunnelPrivacy.ConstantPrivate }); }); } @@ -469,7 +485,7 @@ export interface IRemoteExplorerService { onDidChangeEditable: Event; setEditable(tunnelItem: ITunnelItem | undefined, data: IEditableData | null): void; getEditableData(tunnelItem: ITunnelItem | undefined): IEditableData | undefined; - forward(remote: { host: string, port: number }, localPort?: number, name?: string, source?: string, elevateIfNeeded?: boolean): Promise; + forward(remote: { host: string, port: number }, localPort?: number, name?: string, source?: string, elevateIfNeeded?: boolean, isPublic?: boolean): Promise; close(remote: { host: string, port: number }): Promise; setTunnelInformation(tunnelInformation: TunnelInformation | undefined): void; setCandidateFilter(filter: ((candidates: CandidatePort[]) => Promise) | undefined): IDisposable; @@ -522,8 +538,8 @@ class RemoteExplorerService implements IRemoteExplorerService { return this._tunnelModel; } - forward(remote: { host: string, port: number }, local?: number, name?: string, source?: string, elevateIfNeeded?: boolean): Promise { - return this.tunnelModel.forward(remote, local, name, source, elevateIfNeeded); + forward(remote: { host: string, port: number }, local?: number, name?: string, source?: string, elevateIfNeeded?: boolean, isPublic?: boolean): Promise { + return this.tunnelModel.forward(remote, local, name, source, elevateIfNeeded, isPublic); } close(remote: { host: string, port: number }): Promise { diff --git a/src/vs/workbench/workbench.web.api.ts b/src/vs/workbench/workbench.web.api.ts index 5d2040fa465..0a82541e0b2 100644 --- a/src/vs/workbench/workbench.web.api.ts +++ b/src/vs/workbench/workbench.web.api.ts @@ -70,6 +70,8 @@ interface ITunnelOptions { localAddressPort?: number; label?: string; + + public?: boolean; } export interface TunnelCreationOptions { @@ -87,6 +89,8 @@ interface ITunnel { */ localAddress: string; + public?: boolean; + /** * Implementers of Tunnel should fire onDidDispose when dispose is called. */ -- GitLab