diff --git a/src/vs/platform/remote/common/tunnel.ts b/src/vs/platform/remote/common/tunnel.ts index fc98af96bcd86514c42d8ed7cc057fa5a0bec359..52dfacbc403dec655fd4923d20d97daf9a5618c0 100644 --- a/src/vs/platform/remote/common/tunnel.ts +++ b/src/vs/platform/remote/common/tunnel.ts @@ -56,6 +56,14 @@ export function extractLocalHostUriMetaDataForPortMapping(uri: URI): { address: }; } +export function isLocalhost(host: string): boolean { + return host === 'localhost' || host === '127.0.0.1'; +} + +function getOtherLocalhost(host: string): string | undefined { + return (host === 'localhost') ? '127.0.0.1' : ((host === '127.0.0.1') ? 'localhost' : undefined); +} + export abstract class AbstractTunnelService implements ITunnelService { declare readonly _serviceBrand: undefined; @@ -105,7 +113,7 @@ export abstract class AbstractTunnelService implements ITunnelService { return undefined; } - if (!remoteHost || (remoteHost === '127.0.0.1')) { + if (!remoteHost) { remoteHost = 'localhost'; } @@ -172,13 +180,29 @@ export abstract class AbstractTunnelService implements ITunnelService { this._tunnels.get(remoteHost)!.set(remotePort, { refcount: 1, value: tunnel }); } + protected getTunnelFromMap(remoteHost: string, remotePort: number): { refcount: number, readonly value: Promise } | undefined { + const otherLocalhost = getOtherLocalhost(remoteHost); + let portMap: Map }> | undefined; + if (otherLocalhost) { + const firstMap = this._tunnels.get(remoteHost); + const secondMap = this._tunnels.get(otherLocalhost); + if (firstMap && secondMap) { + portMap = new Map([...Array.from(firstMap.entries()), ...Array.from(secondMap.entries())]); + } else { + portMap = firstMap ?? secondMap; + } + } else { + portMap = this._tunnels.get(remoteHost); + } + return portMap ? portMap.get(remotePort) : undefined; + } + protected abstract retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort?: number): Promise | undefined; } export class TunnelService extends AbstractTunnelService { protected retainOrCreateTunnel(_addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort?: number | undefined): Promise | undefined { - const portMap = this._tunnels.get(remoteHost); - const existing = portMap ? portMap.get(remotePort) : undefined; + const existing = this.getTunnelFromMap(remoteHost, remotePort); if (existing) { ++existing.refcount; return existing.value; diff --git a/src/vs/platform/remote/node/tunnelService.ts b/src/vs/platform/remote/node/tunnelService.ts index 983401be4178c936422de586368257fcf740fd59..c8d84bdeff4b823fa53bbf508b90c629c34658b0 100644 --- a/src/vs/platform/remote/node/tunnelService.ts +++ b/src/vs/platform/remote/node/tunnelService.ts @@ -86,7 +86,7 @@ class NodeRemoteTunnel extends Disposable implements RemoteTunnel { this.tunnelLocalPort = address.port; await this._barrier.wait(); - this.localAddress = 'localhost:' + address.port; + this.localAddress = `${this.tunnelRemoteHost === '127.0.0.1' ? '127.0.0.1' : 'localhost'}:${address.port}`; return this; } @@ -132,8 +132,7 @@ export class TunnelService extends AbstractTunnelService { } protected retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort?: number): Promise | undefined { - const portMap = this._tunnels.get(remoteHost); - const existing = portMap ? portMap.get(remotePort) : undefined; + const existing = this.getTunnelFromMap(remoteHost, remotePort); if (existing) { ++existing.refcount; return existing.value; diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index 32683bf841816aee208b9e14734ad4af36c3c3aa..3b398442e308404a56b5d3e049484f3e18eddc4b 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -37,7 +37,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { URI } from 'vs/base/common/uri'; -import { RemoteTunnel } from 'vs/platform/remote/common/tunnel'; +import { isLocalhost, RemoteTunnel } from 'vs/platform/remote/common/tunnel'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -163,18 +163,34 @@ export class TunnelViewModel extends Disposable implements ITunnelViewModel { }); } + private mapHasTunnel(map: Map, host: string, port: number): boolean { + if (!isLocalhost(host)) { + return map.has(MakeAddress(host, port)); + } + + const stringAddress = MakeAddress('localhost', port); + if (map.has(stringAddress)) { + return true; + } + const numberAddress = MakeAddress('127.0.0.1', port); + if (map.has(numberAddress)) { + return true; + } + return false; + } + get candidates(): TunnelItem[] { const candidates: TunnelItem[] = []; this._candidates.forEach(value => { - let key = MakeAddress(value.host, value.port); - if (!this.model.forwarded.has(key) && !this.model.detected.has(key)) { + if (!this.mapHasTunnel(this.model.forwarded, value.host, value.port) && + !this.mapHasTunnel(this.model.detected, value.host, value.port)) { // The host:port hasn't been forwarded or detected. However, if the candidate is 0.0.0.0, // also check that the port hasn't already been forwarded with localhost, and vice versa. // For example: no need to show 0.0.0.0:3000 as a candidate if localhost:3000 is already forwarded. const otherHost = value.host === '0.0.0.0' ? 'localhost' : (value.host === 'localhost' ? '0.0.0.0' : undefined); if (otherHost) { - key = MakeAddress(otherHost, value.port); - if (this.model.forwarded.has(key) || this.model.detected.has(key)) { + if (this.mapHasTunnel(this.model.forwarded, otherHost, value.port) || + this.mapHasTunnel(this.model.detected, otherHost, value.port)) { return; } } @@ -411,11 +427,11 @@ class TunnelItem implements ITunnelItem { get label(): string { if (this.name) { return nls.localize('remote.tunnelsView.forwardedPortLabel0', "{0}", this.name); - } else if (this.localAddress && (this.remoteHost !== 'localhost')) { + } else if (this.localAddress && !isLocalhost(this.remoteHost)) { return nls.localize('remote.tunnelsView.forwardedPortLabel2', "{0}:{1} \u2192 {2}", this.remoteHost, this.remotePort, this.localAddress); } else if (this.localAddress) { return nls.localize('remote.tunnelsView.forwardedPortLabel3', "{0} \u2192 {1}", this.remotePort, this.localAddress); - } else if (this.remoteHost !== 'localhost') { + } else if (!isLocalhost(this.remoteHost)) { return nls.localize('remote.tunnelsView.forwardedPortLabel4', "{0}:{1}", this.remoteHost, this.remotePort); } else { return nls.localize('remote.tunnelsView.forwardedPortLabel5', "{0}", this.remotePort); diff --git a/src/vs/workbench/contrib/remote/browser/urlFinder.ts b/src/vs/workbench/contrib/remote/browser/urlFinder.ts index e72dc1fdeb0dbc381bd4d10eefa9b6d2d952bf6a..d0ee06fd48bcefd05e37b827dd5388245b749fbc 100644 --- a/src/vs/workbench/contrib/remote/browser/urlFinder.ts +++ b/src/vs/workbench/contrib/remote/browser/urlFinder.ts @@ -60,7 +60,7 @@ export class UrlFinder extends Disposable { if (!isNaN(port) && Number.isInteger(port) && port > 0 && port <= 65535) { // normalize the host name let host = serverUrl.hostname; - if (host !== '0.0.0.0') { + if (host !== '0.0.0.0' && host !== '127.0.0.1') { host = 'localhost'; } this._onDidMatchLocalUrl.fire({ port, host }); diff --git a/src/vs/workbench/electron-browser/window.ts b/src/vs/workbench/electron-browser/window.ts index 17ace9f6abeb033c258c1970bd70e1ec662d0fde..3749645b62c4d05f05c4f21e131395a1d5fb4129 100644 --- a/src/vs/workbench/electron-browser/window.ts +++ b/src/vs/workbench/electron-browser/window.ts @@ -451,10 +451,10 @@ export class NativeWindow extends Disposable { return (await this.remoteAuthorityResolverService.resolveAuthority(remoteAuthority)).authority; } } : undefined; - const tunnel = await this.tunnelService.openTunnel(addressProvider, undefined, portMappingRequest.port); + const tunnel = await this.tunnelService.openTunnel(addressProvider, portMappingRequest.address, portMappingRequest.port); if (tunnel) { return { - resolved: uri.with({ authority: `127.0.0.1:${tunnel.tunnelLocalPort}` }), + resolved: uri.with({ authority: tunnel.localAddress }), dispose: () => tunnel.dispose(), }; } diff --git a/src/vs/workbench/services/remote/common/remoteExplorerService.ts b/src/vs/workbench/services/remote/common/remoteExplorerService.ts index a8232c97d5987786b7e9aa1c32d925db3e155ec1..99a8e1489c20fe021a393c21e2e67a74a3cf868f 100644 --- a/src/vs/workbench/services/remote/common/remoteExplorerService.ts +++ b/src/vs/workbench/services/remote/common/remoteExplorerService.ts @@ -48,15 +48,8 @@ export interface Tunnel { closeable?: boolean; } -function ToLocalHost(host: string): string { - if (host === '127.0.0.1') { - host = 'localhost'; - } - return host; -} - export function MakeAddress(host: string, port: number): string { - return ToLocalHost(host) + ':' + port; + return host + ':' + port; } export class TunnelModel extends Disposable { @@ -218,7 +211,7 @@ export class TunnelModel extends Disposable { const nullIndex = value.detail.indexOf('\0'); const detail = value.detail.substr(0, nullIndex > 0 ? nullIndex : value.detail.length).trim(); return { - host: ToLocalHost(value.host), + host: value.host, port: value.port, detail };