diff --git a/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts b/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts index ac5ad1585d5..a3f91ee0313 100644 --- a/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts +++ b/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts @@ -241,7 +241,8 @@ class OnAutoForwardedAction extends Disposable { const tunnel = await this.portNumberHeuristicDelay(); this.logService.trace(`ForwardedPorts: (OnAutoForwardedAction) Heuristic chose ${tunnel?.tunnelRemotePort}`); if (tunnel) { - const attributes = (await this.remoteExplorerService.tunnelModel.getAttributes([tunnel.tunnelRemotePort]))?.get(tunnel.tunnelRemotePort)?.onAutoForward; + const allAttributes = await this.remoteExplorerService.tunnelModel.getAttributes([{ port: tunnel.tunnelRemotePort, host: tunnel.tunnelRemoteHost }]); + const attributes = allAttributes?.get(tunnel.tunnelRemotePort)?.onAutoForward; this.logService.trace(`ForwardedPorts: (OnAutoForwardedAction) onAutoForward action is ${attributes}`); switch (attributes) { case OnPortForward.OpenBrowserOnce: { @@ -459,7 +460,7 @@ class OutputAutomaticPortForwarding extends Disposable { if (mapHasAddressLocalhostOrAllInterfaces(this.remoteExplorerService.tunnelModel.detected, localUrl.host, localUrl.port)) { return; } - const attributes = (await this.remoteExplorerService.tunnelModel.getAttributes([localUrl.port]))?.get(localUrl.port); + const attributes = (await this.remoteExplorerService.tunnelModel.getAttributes([localUrl]))?.get(localUrl.port); if (attributes?.onAutoForward === OnPortForward.Ignore) { return; } @@ -587,7 +588,7 @@ class ProcAutomaticPortForwarding extends Disposable { } if (!attributes) { - attributes = await this.remoteExplorerService.tunnelModel.getAttributes(this.remoteExplorerService.tunnelModel.candidates.map(candidate => candidate.port)); + attributes = await this.remoteExplorerService.tunnelModel.getAttributes(this.remoteExplorerService.tunnelModel.candidates); } const portAttributes = attributes?.get(value.port); diff --git a/src/vs/workbench/contrib/remote/common/remote.contribution.ts b/src/vs/workbench/contrib/remote/common/remote.contribution.ts index 2f675d78d7f..18b53894c58 100644 --- a/src/vs/workbench/contrib/remote/common/remote.contribution.ts +++ b/src/vs/workbench/contrib/remote/common/remote.contribution.ts @@ -152,7 +152,7 @@ Registry.as(ConfigurationExtensions.Configuration) patternProperties: { '(^\\d+(\\-\\d+)?$)|(.+)': { type: 'object', - description: localize('remote.portsAttributes.port', "A port, range of ports (ex. \"40000-55000\"), or regular expression (ex. \".+\\\\/server.js\"). For a port number or range, the attributes will apply to that port number or range of port numbers. Attributes which use a regular expression will apply to ports whose associated process command line matches the expression."), + description: localize('remote.portsAttributes.port', "A port, range of ports (ex. \"40000-55000\"), host and port (ex. \"db:1234\"), or regular expression (ex. \".+\\\\/server.js\"). For a port number or range, the attributes will apply to that port number or range of port numbers. Attributes which use a regular expression will apply to ports whose associated process command line matches the expression."), properties: { 'onAutoForward': { type: 'string', diff --git a/src/vs/workbench/services/remote/common/remoteExplorerService.ts b/src/vs/workbench/services/remote/common/remoteExplorerService.ts index 35c57f36303..f97146dc6d1 100644 --- a/src/vs/workbench/services/remote/common/remoteExplorerService.ts +++ b/src/vs/workbench/services/remote/common/remoteExplorerService.ts @@ -193,14 +193,17 @@ export interface Attributes { interface PortRange { start: number, end: number } +interface HostAndPort { host: string, port: number } + interface PortAttributes extends Attributes { - key: number | PortRange | RegExp; + key: number | PortRange | RegExp | HostAndPort; } export class PortsAttributes extends Disposable { private static SETTING = 'remote.portsAttributes'; private static DEFAULTS = 'remote.otherPortsAttributes'; private static RANGE = /^(\d+)\-(\d+)$/; + private static HOST_AND_PORT = /^([a-z0-9\-]+):(\d{1,5})$/; private portsAttributes: PortAttributes[] = []; private defaultPortAttributes: Attributes | undefined; private _onDidChangeAttributes = new Emitter(); @@ -221,8 +224,8 @@ export class PortsAttributes extends Disposable { this._onDidChangeAttributes.fire(); } - getAttributes(port: number, commandLine?: string): Attributes | undefined { - let index = this.findNextIndex(port, commandLine, this.portsAttributes, 0); + getAttributes(port: number, host: string, commandLine?: string): Attributes | undefined { + let index = this.findNextIndex(port, host, commandLine, this.portsAttributes, 0); const attributes: Attributes = { label: undefined, onAutoForward: undefined, @@ -246,7 +249,7 @@ export class PortsAttributes extends Disposable { attributes.requireLocalPort = (attributes.requireLocalPort !== undefined) ? attributes.requireLocalPort : undefined; attributes.protocol = attributes.protocol ?? found.protocol; } - index = this.findNextIndex(port, commandLine, this.portsAttributes, index + 1); + index = this.findNextIndex(port, host, commandLine, this.portsAttributes, index + 1); } if (attributes.onAutoForward !== undefined || attributes.elevateIfNeeded !== undefined || attributes.label !== undefined || attributes.requireLocalPort !== undefined @@ -258,23 +261,32 @@ export class PortsAttributes extends Disposable { return this.getOtherAttributes(); } - private hasStartEnd(value: number | PortRange | RegExp): value is PortRange { + private hasStartEnd(value: number | PortRange | RegExp | HostAndPort): value is PortRange { return ((value).start !== undefined) && ((value).end !== undefined); } - private findNextIndex(port: number, commandLine: string | undefined, attributes: PortAttributes[], fromIndex: number): number { + private hasHostAndPort(value: number | PortRange | RegExp | HostAndPort): value is HostAndPort { + return ((value).host !== undefined) && ((value).port !== undefined) + && isString((value).host) && isNumber((value).port); + } + + private findNextIndex(port: number, host: string, commandLine: string | undefined, attributes: PortAttributes[], fromIndex: number): number { if (fromIndex >= attributes.length) { return -1; } + const shouldUseHost = !isLocalhost(host) && !isAllInterfaces(host); const sliced = attributes.slice(fromIndex); const foundIndex = sliced.findIndex((value) => { if (isNumber(value.key)) { - return value.key === port; + return shouldUseHost ? false : value.key === port; } else if (this.hasStartEnd(value.key)) { - return port >= value.key.start && port <= value.key.end; + return shouldUseHost ? false : (port >= value.key.start && port <= value.key.end); + } else if (this.hasHostAndPort(value.key)) { + return (port === value.key.port) && (host === value.key.host); } else { return commandLine ? value.key.test(commandLine) : false; } + }); return foundIndex >= 0 ? foundIndex + fromIndex : -1; } @@ -291,13 +303,16 @@ export class PortsAttributes extends Disposable { continue; } const setting = (settingValue)[attributesKey]; - let key: number | { start: number, end: number } | RegExp | undefined = undefined; + let key: number | PortRange | RegExp | HostAndPort | undefined = undefined; if (Number(attributesKey)) { key = Number(attributesKey); } else if (isString(attributesKey)) { if (PortsAttributes.RANGE.test(attributesKey)) { - const match = (attributesKey).match(PortsAttributes.RANGE); + const match = attributesKey.match(PortsAttributes.RANGE); key = { start: Number(match![1]), end: Number(match![2]) }; + } else if (PortsAttributes.HOST_AND_PORT.test(attributesKey)) { + const match = attributesKey.match(PortsAttributes.HOST_AND_PORT); + key = { host: match![1], port: Number(match![2]) }; } else { let regTest: RegExp | undefined = undefined; try { @@ -343,6 +358,8 @@ export class PortsAttributes extends Disposable { return item.key; } else if (thisRef.hasStartEnd(item.key)) { return item.key.start; + } else if (thisRef.hasHostAndPort(item.key)) { + return item.key.port; } else { return Number.MAX_VALUE; } @@ -435,7 +452,9 @@ export class TunnelModel extends Disposable { this.forwarded = new Map(); this.remoteTunnels = new Map(); this.tunnelService.tunnels.then(async (tunnels) => { - const attributes = await this.getAttributes(tunnels.map(tunnel => tunnel.tunnelRemotePort)); + const attributes = await this.getAttributes(tunnels.map(tunnel => { + return { port: tunnel.tunnelRemotePort, host: tunnel.tunnelRemoteHost }; + })); for (const tunnel of tunnels) { if (tunnel.localAddress) { const key = makeAddress(tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort); @@ -465,7 +484,7 @@ export class TunnelModel extends Disposable { && !mapHasAddressLocalhostOrAllInterfaces(this.inProgress, tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort) && tunnel.localAddress) { const matchingCandidate = mapHasAddressLocalhostOrAllInterfaces(this._candidates ?? new Map(), tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort); - const attributes = (await this.getAttributes([tunnel.tunnelRemotePort]))?.get(tunnel.tunnelRemotePort); + const attributes = (await this.getAttributes([{ port: tunnel.tunnelRemotePort, host: tunnel.tunnelRemoteHost }]))?.get(tunnel.tunnelRemotePort); this.forwarded.set(key, { remoteHost: tunnel.tunnelRemoteHost, remotePort: tunnel.tunnelRemotePort, @@ -590,7 +609,10 @@ export class TunnelModel extends Disposable { async forward(tunnelProperties: TunnelProperties, attributes?: Attributes | null): Promise { const existingTunnel = mapHasAddressLocalhostOrAllInterfaces(this.forwarded, tunnelProperties.remote.host, tunnelProperties.remote.port); - attributes = attributes ?? ((attributes !== null) ? (await this.getAttributes([tunnelProperties.remote.port]))?.get(tunnelProperties.remote.port) : undefined); + attributes = attributes ?? + ((attributes !== null) + ? (await this.getAttributes([tunnelProperties.remote]))?.get(tunnelProperties.remote.port) + : undefined); const localPort = (tunnelProperties.local !== undefined) ? tunnelProperties.local : tunnelProperties.remote.port; if (!existingTunnel) { @@ -772,7 +794,9 @@ export class TunnelModel extends Disposable { private async updateAttributes() { // If the label changes in the attributes, we should update it. const tunnels = Array.from(this.forwarded.values()); - const allAttributes = await this.getAttributes(tunnels.map(tunnel => tunnel.remotePort), false); + const allAttributes = await this.getAttributes(tunnels.map(tunnel => { + return { port: tunnel.remotePort, host: tunnel.remoteHost }; + }), false); if (!allAttributes) { return; } @@ -797,25 +821,25 @@ export class TunnelModel extends Disposable { } } - async getAttributes(ports: number[], checkProviders: boolean = true): Promise | undefined> { + async getAttributes(forwardedPorts: { host: string, port: number }[], checkProviders: boolean = true): Promise | undefined> { const matchingCandidates: Map = new Map(); const pidToPortsMapping: Map = new Map(); - ports.forEach(port => { - const matchingCandidate = mapHasAddressLocalhostOrAllInterfaces(this._candidates ?? new Map(), LOCALHOST_ADDRESSES[0], port); + forwardedPorts.forEach(forwardedPort => { + const matchingCandidate = mapHasAddressLocalhostOrAllInterfaces(this._candidates ?? new Map(), LOCALHOST_ADDRESSES[0], forwardedPort.port); if (matchingCandidate) { - matchingCandidates.set(port, matchingCandidate); + matchingCandidates.set(forwardedPort.port, matchingCandidate); if (!pidToPortsMapping.has(matchingCandidate.pid)) { pidToPortsMapping.set(matchingCandidate.pid, []); } - pidToPortsMapping.get(matchingCandidate.pid)?.push(port); + pidToPortsMapping.get(matchingCandidate.pid)?.push(forwardedPort.port); } }); const configAttributes: Map = new Map(); - ports.forEach(port => { - const attributes = this.configPortsAttributes.getAttributes(port, matchingCandidates.get(port)?.detail); + forwardedPorts.forEach(forwardedPort => { + const attributes = this.configPortsAttributes.getAttributes(forwardedPort.port, forwardedPort.host, matchingCandidates.get(forwardedPort.port)?.detail); if (attributes) { - configAttributes.set(port, attributes); + configAttributes.set(forwardedPort.port, attributes); } }); if ((this.portAttributesProviders.length === 0) || !checkProviders) { @@ -844,10 +868,10 @@ export class TunnelModel extends Disposable { // Merge. The config wins. const mergedAttributes: Map = new Map(); - ports.forEach(port => { - const config = configAttributes.get(port); - const provider = providedAttributes.get(port); - mergedAttributes.set(port, { + forwardedPorts.forEach(forwardedPorts => { + const config = configAttributes.get(forwardedPorts.port); + const provider = providedAttributes.get(forwardedPorts.port); + mergedAttributes.set(forwardedPorts.port, { elevateIfNeeded: config?.elevateIfNeeded, label: config?.label, onAutoForward: config?.onAutoForward ?? PortsAttributes.providedActionToAction(provider?.autoForwardAction),