diff --git a/extensions/configuration-editing/schemas/attachContainer.schema.json b/extensions/configuration-editing/schemas/attachContainer.schema.json index 96c03edabd8e9..9cdd3a5f6d36c 100644 --- a/extensions/configuration-editing/schemas/attachContainer.schema.json +++ b/extensions/configuration-editing/schemas/attachContainer.schema.json @@ -59,6 +59,14 @@ "type": "boolean", "markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.", "default": false + }, + "protocol": { + "type": "string", + "enum": [ + "http", + "https" + ], + "description": "The protocol to use when forwarding this port." } }, "default": { @@ -85,7 +93,13 @@ "properties": { "onAutoForward": { "type": "string", - "enum": ["notify", "openBrowser", "openPreview", "silent", "ignore"], + "enum": [ + "notify", + "openBrowser", + "openPreview", + "silent", + "ignore" + ], "enumDescriptions": [ "Shows a notification when a port is automatically forwarded.", "Opens the browser when the port is automatically forwarded. Depending on your settings, this could open an embedded browser.", @@ -110,9 +124,23 @@ "type": "boolean", "markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.", "default": false + }, + "protocol": { + "type": "string", + "enum": [ + "http", + "https" + ], + "description": "The protocol to use when forwarding this port." } }, - "defaultSnippets": [{ "body": { "onAutoForward": "ignore" } }], + "defaultSnippets": [ + { + "body": { + "onAutoForward": "ignore" + } + } + ], "markdownDescription": "Set default properties that are applied to all ports that don't get properties from the setting `remote.portsAttributes`. For example:\n\n```\n{\n \"onAutoForward\": \"ignore\"\n}\n```", "additionalProperties": false }, diff --git a/extensions/configuration-editing/schemas/devContainer.schema.generated.json b/extensions/configuration-editing/schemas/devContainer.schema.generated.json index 58d810ff4d612..89718de7ada1a 100644 --- a/extensions/configuration-editing/schemas/devContainer.schema.generated.json +++ b/extensions/configuration-editing/schemas/devContainer.schema.generated.json @@ -167,6 +167,14 @@ "type": "boolean", "markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.", "default": false + }, + "protocol": { + "type": "string", + "enum": [ + "http", + "https" + ], + "description": "The protocol to use when forwarding this port." } }, "default": { @@ -225,6 +233,14 @@ "type": "boolean", "markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.", "default": false + }, + "protocol": { + "type": "string", + "enum": [ + "http", + "https" + ], + "description": "The protocol to use when forwarding this port." } }, "defaultSnippets": [ @@ -476,6 +492,14 @@ "type": "boolean", "markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.", "default": false + }, + "protocol": { + "type": "string", + "enum": [ + "http", + "https" + ], + "description": "The protocol to use when forwarding this port." } }, "default": { @@ -534,6 +558,14 @@ "type": "boolean", "markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.", "default": false + }, + "protocol": { + "type": "string", + "enum": [ + "http", + "https" + ], + "description": "The protocol to use when forwarding this port." } }, "defaultSnippets": [ @@ -761,6 +793,14 @@ "type": "boolean", "markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.", "default": false + }, + "protocol": { + "type": "string", + "enum": [ + "http", + "https" + ], + "description": "The protocol to use when forwarding this port." } }, "default": { @@ -819,6 +859,14 @@ "type": "boolean", "markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.", "default": false + }, + "protocol": { + "type": "string", + "enum": [ + "http", + "https" + ], + "description": "The protocol to use when forwarding this port." } }, "defaultSnippets": [ @@ -1012,6 +1060,14 @@ "type": "boolean", "markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.", "default": false + }, + "protocol": { + "type": "string", + "enum": [ + "http", + "https" + ], + "description": "The protocol to use when forwarding this port." } }, "default": { @@ -1070,6 +1126,14 @@ "type": "boolean", "markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.", "default": false + }, + "protocol": { + "type": "string", + "enum": [ + "http", + "https" + ], + "description": "The protocol to use when forwarding this port." } }, "defaultSnippets": [ @@ -1232,6 +1296,14 @@ "type": "boolean", "markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.", "default": false + }, + "protocol": { + "type": "string", + "enum": [ + "http", + "https" + ], + "description": "The protocol to use when forwarding this port." } }, "default": { @@ -1290,6 +1362,14 @@ "type": "boolean", "markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.", "default": false + }, + "protocol": { + "type": "string", + "enum": [ + "http", + "https" + ], + "description": "The protocol to use when forwarding this port." } }, "defaultSnippets": [ @@ -1379,4 +1459,4 @@ "additionalProperties": false } ] -} +} \ No newline at end of file diff --git a/extensions/configuration-editing/schemas/devContainer.schema.src.json b/extensions/configuration-editing/schemas/devContainer.schema.src.json index 8c5dbce1ac195..1412545910a2b 100644 --- a/extensions/configuration-editing/schemas/devContainer.schema.src.json +++ b/extensions/configuration-editing/schemas/devContainer.schema.src.json @@ -73,6 +73,11 @@ "type": "boolean", "markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.", "default": false + }, + "protocol": { + "type": "string", + "enum": ["http", "https"], + "description": "The protocol to use when forwarding this port." } }, "default": { @@ -130,6 +135,11 @@ "type": "boolean", "markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.", "default": false + }, + "protocol": { + "type": "string", + "enum": ["http", "https"], + "description": "The protocol to use when forwarding this port." } }, "defaultSnippets": [ diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index e84b9c74737bf..adb05e765875b 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -543,6 +543,7 @@ class TunnelItem implements ITunnelItem { tunnel.source ?? (tunnel.userForwarded ? nls.localize('tunnel.user', "User Forwarded") : (type === TunnelType.Detected ? nls.localize('tunnel.staticallyForwarded', "Statically Forwarded") : nls.localize('tunnel.automatic', "Auto Forwarded"))), !!tunnel.hasRunningProcess, + tunnel.localUri, tunnel.localAddress, tunnel.localPort, closeable === undefined ? tunnel.closeable : closeable, @@ -559,6 +560,7 @@ class TunnelItem implements ITunnelItem { public remotePort: number, public source: string, public hasRunningProcess: boolean, + public localUri?: URI, public localAddress?: string, public localPort?: number, public closeable?: boolean, @@ -1167,12 +1169,8 @@ export namespace OpenPortInBrowserAction { export function run(model: TunnelModel, openerService: IOpenerService, key: string) { const tunnel = model.forwarded.get(key) || model.detected.get(key); - let address: string | undefined; - if (tunnel && tunnel.localAddress && (address = model.address(tunnel.remoteHost, tunnel.remotePort))) { - if (!address.startsWith('http')) { - address = `http://${address}`; - } - return openerService.open(URI.parse(address), { allowContributedOpeners: false }); + if (tunnel) { + return openerService.open(tunnel.localUri, { allowContributedOpeners: false }); } return Promise.resolve(); } @@ -1201,18 +1199,13 @@ export namespace OpenPortInPreviewAction { export async function run(model: TunnelModel, openerService: IOpenerService, externalOpenerService: IExternalUriOpenerService, key: string) { const tunnel = model.forwarded.get(key) || model.detected.get(key); - let address: string | undefined; - if (tunnel && tunnel.localAddress && (address = model.address(tunnel.remoteHost, tunnel.remotePort))) { - if (!address.startsWith('http')) { - address = `http://${address}`; - } - const uri = URI.parse(address); + if (tunnel) { const sourceUri = URI.parse(`http://${tunnel.remoteHost}:${tunnel.remotePort}`); - const opener = await externalOpenerService.getOpener(uri, { sourceUri }, new CancellationTokenSource().token); + const opener = await externalOpenerService.getOpener(tunnel.localUri, { sourceUri }, new CancellationTokenSource().token); if (opener) { - return opener.openExternalUri(uri, { sourceUri }, new CancellationTokenSource().token); + return opener.openExternalUri(tunnel.localUri, { sourceUri }, new CancellationTokenSource().token); } - return openerService.open(uri); + return openerService.open(tunnel.localUri); } return Promise.resolve(); } diff --git a/src/vs/workbench/contrib/remote/common/remote.contribution.ts b/src/vs/workbench/contrib/remote/common/remote.contribution.ts index 9dd984dacf82b..15db29208f6c6 100644 --- a/src/vs/workbench/contrib/remote/common/remote.contribution.ts +++ b/src/vs/workbench/contrib/remote/common/remote.contribution.ts @@ -183,6 +183,11 @@ Registry.as(ConfigurationExtensions.Configuration) type: 'boolean', markdownDescription: localize('remote.portsAttributes.requireLocalPort', "When true, a modal dialog will show if the chosen local port isn't used for forwarding."), default: false + }, + 'protocol': { + type: 'string', + enum: ['http', 'https'], + description: localize('remote.portsAttributes.protocol', "The protocol to use when forwarding this port.") } }, default: { @@ -226,6 +231,11 @@ Registry.as(ConfigurationExtensions.Configuration) type: 'boolean', markdownDescription: localize('remote.portsAttributes.requireLocalPort', "When true, a modal dialog will show if the chosen local port isn't used for forwarding."), default: false + }, + 'protocol': { + type: 'string', + enum: ['http', 'https'], + description: localize('remote.portsAttributes.protocol', "The protocol to use when forwarding this port.") } }, defaultSnippets: [{ body: { onAutoForward: 'ignore' } }], diff --git a/src/vs/workbench/services/remote/common/remoteExplorerService.ts b/src/vs/workbench/services/remote/common/remoteExplorerService.ts index 6b4bff2b8a14b..3fb24b9fff0d6 100644 --- a/src/vs/workbench/services/remote/common/remoteExplorerService.ts +++ b/src/vs/workbench/services/remote/common/remoteExplorerService.ts @@ -24,6 +24,7 @@ import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { flatten } from 'vs/base/common/arrays'; import Severity from 'vs/base/common/severity'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { URI } from 'vs/base/common/uri'; export const IRemoteExplorerService = createDecorator('remoteExplorerService'); export const REMOTE_EXPLORER_TYPE_KEY: string = 'remote.explorerType'; @@ -53,6 +54,7 @@ export interface ITunnelItem { remoteHost: string; remotePort: number; localAddress?: string; + localUri?: URI; localPort?: number; name?: string; closeable?: boolean; @@ -74,6 +76,7 @@ export interface Tunnel { remoteHost: string; remotePort: number; localAddress: string; + localUri: URI; localPort?: number; name?: string; closeable?: boolean; @@ -149,6 +152,7 @@ export interface Attributes { onAutoForward: OnPortForward | undefined, elevateIfNeeded: boolean | undefined; requireLocalPort: boolean | undefined; + protocol: string | undefined; } interface PortRange { start: number, end: number } @@ -187,7 +191,8 @@ export class PortsAttributes extends Disposable { label: undefined, onAutoForward: undefined, elevateIfNeeded: undefined, - requireLocalPort: undefined + requireLocalPort: undefined, + protocol: undefined }; while (index >= 0) { const found = this.portsAttributes[index]; @@ -196,17 +201,20 @@ export class PortsAttributes extends Disposable { attributes.elevateIfNeeded = (found.elevateIfNeeded !== undefined) ? found.elevateIfNeeded : attributes.elevateIfNeeded; attributes.label = found.label ?? attributes.label; attributes.requireLocalPort = found.requireLocalPort; + attributes.protocol = found.protocol; } else { // It's a range or regex, which means that if the attribute is already set, we keep it attributes.onAutoForward = attributes.onAutoForward ?? found.onAutoForward; attributes.elevateIfNeeded = (attributes.elevateIfNeeded !== undefined) ? attributes.elevateIfNeeded : found.elevateIfNeeded; attributes.label = attributes.label ?? found.label; attributes.requireLocalPort = (attributes.requireLocalPort !== undefined) ? attributes.requireLocalPort : undefined; + attributes.protocol = attributes.protocol ?? found.protocol; } index = this.findNextIndex(port, commandLine, this.portsAttributes, index + 1); } if (attributes.onAutoForward !== undefined || attributes.elevateIfNeeded !== undefined - || attributes.label !== undefined || attributes.requireLocalPort !== undefined) { + || attributes.label !== undefined || attributes.requireLocalPort !== undefined + || attributes.protocol !== undefined) { return attributes; } @@ -274,7 +282,8 @@ export class PortsAttributes extends Disposable { elevateIfNeeded: setting.elevateIfNeeded, onAutoForward: setting.onAutoForward, label: setting.label, - requireLocalPort: setting.requireLocalPort + requireLocalPort: setting.requireLocalPort, + protocol: setting.protocol }); } @@ -284,7 +293,8 @@ export class PortsAttributes extends Disposable { elevateIfNeeded: defaults.elevateIfNeeded, label: defaults.label, onAutoForward: defaults.onAutoForward, - requireLocalPort: defaults.requireLocalPort + requireLocalPort: defaults.requireLocalPort, + protocol: defaults.protocol }; } @@ -365,8 +375,9 @@ export class TunnelModel extends Disposable { this._register(this.configPortsAttributes.onDidChangeAttributes(this.updateAttributes, this)); this.forwarded = new Map(); this.remoteTunnels = new Map(); - this.tunnelService.tunnels.then(tunnels => { - tunnels.forEach(tunnel => { + this.tunnelService.tunnels.then(async (tunnels) => { + const attributes = await this.getAttributes(tunnels.map(tunnel => tunnel.tunnelRemotePort)); + for (const tunnel of tunnels) { if (tunnel.localAddress) { const key = makeAddress(tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort); const matchingCandidate = mapHasAddressLocalhostOrAllInterfaces(this._candidates ?? new Map(), tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort); @@ -374,6 +385,7 @@ export class TunnelModel extends Disposable { remotePort: tunnel.tunnelRemotePort, remoteHost: tunnel.tunnelRemoteHost, localAddress: tunnel.localAddress, + localUri: await this.makeLocalUri(tunnel.localAddress, attributes?.get(tunnel.tunnelRemotePort)), localPort: tunnel.tunnelLocalPort, runningProcess: matchingCandidate?.detail, hasRunningProcess: !!matchingCandidate, @@ -383,7 +395,7 @@ export class TunnelModel extends Disposable { }); this.remoteTunnels.set(key, tunnel); } - }); + } }); this.detected = new Map(); @@ -395,6 +407,7 @@ export class TunnelModel extends Disposable { remoteHost: tunnel.tunnelRemoteHost, remotePort: tunnel.tunnelRemotePort, localAddress: tunnel.localAddress, + localUri: await this.makeLocalUri(tunnel.localAddress, (await this.getAttributes([tunnel.tunnelRemotePort]))?.get(tunnel.tunnelRemotePort)), localPort: tunnel.tunnelLocalPort, closeable: true, runningProcess: matchingCandidate?.detail, @@ -418,6 +431,14 @@ export class TunnelModel extends Disposable { })); } + private makeLocalUri(localAddress: string, attributes?: Attributes) { + if (localAddress.startsWith('http')) { + return URI.parse(localAddress); + } + const protocol = attributes?.protocol ?? 'http'; + return URI.parse(`${protocol}://${localAddress}`); + } + private makeTunnelPrivacy(isPublic: boolean) { return isPublic ? TunnelPrivacy.Public : this.tunnelService.canMakePublic ? TunnelPrivacy.Private : TunnelPrivacy.ConstantPrivate; } @@ -490,9 +511,9 @@ export class TunnelModel extends Disposable { return this.dialogService.show(Severity.Info, mismatchString, [nls.localize('remote.localPortMismatch.Ok', "Ok")]); } - async forward(remote: { host: string, port: number }, local?: number, name?: string, source?: string, elevateIfNeeded?: boolean, isPublic?: boolean, restore: boolean = true): Promise { + async forward(remote: { host: string, port: number }, local?: number, name?: string, source?: string, elevateIfNeeded?: boolean, isPublic?: boolean, restore: boolean = true, attributes?: Attributes): Promise { const existingTunnel = mapHasAddressLocalhostOrAllInterfaces(this.forwarded, remote.host, remote.port); - const attributes = (await this.getAttributes([remote.port]))?.get(remote.port); + attributes = attributes ?? (await this.getAttributes([remote.port]))?.get(remote.port); const localPort = (local !== undefined) ? local : remote.port; if (!existingTunnel) { @@ -511,6 +532,7 @@ export class TunnelModel extends Disposable { name: attributes?.label ?? name, closeable: true, localAddress: tunnel.localAddress, + localUri: await this.makeLocalUri(tunnel.localAddress, attributes), runningProcess: matchingCandidate?.detail, hasRunningProcess: !!matchingCandidate, pid: matchingCandidate?.pid, @@ -530,6 +552,9 @@ export class TunnelModel extends Disposable { if (attributes?.label ?? name) { existingTunnel.name = attributes?.label ?? name; } + if (attributes?.protocol) { + existingTunnel.localUri = this.makeLocalUri(existingTunnel.localAddress, attributes); + } this._onForwardPort.fire(); return mapHasAddressLocalhostOrAllInterfaces(this.remoteTunnels, remote.host, remote.port); } @@ -564,12 +589,14 @@ export class TunnelModel extends Disposable { addEnvironmentTunnels(tunnels: TunnelDescription[] | undefined): void { if (tunnels) { - tunnels.forEach(tunnel => { + for (const tunnel of tunnels) { const matchingCandidate = mapHasAddressLocalhostOrAllInterfaces(this._candidates ?? new Map(), tunnel.remoteAddress.host, tunnel.remoteAddress.port); + const localAddress = typeof tunnel.localAddress === 'string' ? tunnel.localAddress : makeAddress(tunnel.localAddress.host, tunnel.localAddress.port); this.detected.set(makeAddress(tunnel.remoteAddress.host, tunnel.remoteAddress.port), { remoteHost: tunnel.remoteAddress.host, remotePort: tunnel.remoteAddress.port, - localAddress: typeof tunnel.localAddress === 'string' ? tunnel.localAddress : makeAddress(tunnel.localAddress.host, tunnel.localAddress.port), + localAddress: localAddress, + localUri: this.makeLocalUri(localAddress), closeable: false, runningProcess: matchingCandidate?.detail, hasRunningProcess: !!matchingCandidate, @@ -577,7 +604,7 @@ export class TunnelModel extends Disposable { privacy: TunnelPrivacy.ConstantPrivate, userForwarded: false }); - }); + } } this._environmentTunnelsSet = true; this._onEnvironmentTunnelsSet.fire(); @@ -654,11 +681,22 @@ export class TunnelModel extends Disposable { private async updateAttributes() { // If the label changes in the attributes, we should update it. - for (let forwarded of this.forwarded.values()) { - const attributes = (await this.getAttributes([forwarded.remotePort], false))?.get(forwarded.remotePort); - if (attributes && attributes.label && attributes.label !== forwarded.name) { + const tunnels = Array.from(this.forwarded.values()); + const allAttributes = await this.getAttributes(tunnels.map(tunnel => tunnel.remotePort), false); + if (!allAttributes) { + return; + } + for (const forwarded of tunnels) { + const attributes = allAttributes.get(forwarded.remotePort); + if (!attributes) { + continue; + } + if (attributes.label && attributes.label !== forwarded.name) { await this.name(forwarded.remoteHost, forwarded.remotePort, attributes.label); } + if (attributes.protocol && attributes.protocol !== forwarded.localUri.scheme) { + await this.forward({ host: forwarded.remoteHost, port: forwarded.remotePort }, forwarded.localPort, forwarded.name, forwarded.source, undefined, undefined, undefined, attributes); + } } } @@ -716,7 +754,8 @@ export class TunnelModel extends Disposable { elevateIfNeeded: config?.elevateIfNeeded, label: config?.label, onAutoForward: config?.onAutoForward ?? PortsAttributes.providedActionToAction(provider?.autoForwardAction), - requireLocalPort: config?.requireLocalPort + requireLocalPort: config?.requireLocalPort, + protocol: config?.protocol }); });