Skip to content

Commit 8e2b304

Browse files
committed
Enable more than just localhost for port forwarding providers
Part of microsoft#81388
1 parent c7ab68c commit 8e2b304

11 files changed

Lines changed: 187 additions & 133 deletions

File tree

src/vs/platform/remote/common/tunnel.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@ export interface ITunnelService {
3333

3434
readonly tunnels: Promise<readonly RemoteTunnel[]>;
3535
readonly onTunnelOpened: Event<RemoteTunnel>;
36-
readonly onTunnelClosed: Event<number>;
36+
readonly onTunnelClosed: Event<{ host: string, port: number }>;
3737

38-
openTunnel(remotePort: number, localPort?: number): Promise<RemoteTunnel> | undefined;
39-
closeTunnel(remotePort: number): Promise<void>;
38+
openTunnel(remoteHost: string | undefined, remotePort: number, localPort?: number): Promise<RemoteTunnel> | undefined;
39+
closeTunnel(remoteHost: string, remotePort: number): Promise<void>;
4040
setTunnelProvider(provider: ITunnelProvider | undefined): IDisposable;
4141
}
4242

src/vs/platform/remote/common/tunnelService.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ export class NoOpTunnelService implements ITunnelService {
1313
public readonly tunnels: Promise<readonly RemoteTunnel[]> = Promise.resolve([]);
1414
private _onTunnelOpened: Emitter<RemoteTunnel> = new Emitter();
1515
public onTunnelOpened: Event<RemoteTunnel> = this._onTunnelOpened.event;
16-
private _onTunnelClosed: Emitter<number> = new Emitter();
17-
public onTunnelClosed: Event<number> = this._onTunnelClosed.event;
18-
openTunnel(_remotePort: number): Promise<RemoteTunnel> | undefined {
16+
private _onTunnelClosed: Emitter<{ host: string, port: number }> = new Emitter();
17+
public onTunnelClosed: Event<{ host: string, port: number }> = this._onTunnelClosed.event;
18+
openTunnel(_remoteHost: string, _remotePort: number): Promise<RemoteTunnel> | undefined {
1919
return undefined;
2020
}
21-
async closeTunnel(_remotePort: number): Promise<void> {
21+
async closeTunnel(_remoteHost: string, _remotePort: number): Promise<void> {
2222
}
2323
setTunnelProvider(provider: ITunnelProvider | undefined): IDisposable {
2424
throw new Error('Method not implemented.');

src/vs/workbench/api/browser/mainThreadTunnelService.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,15 @@ export class MainThreadTunnelService implements MainThreadTunnelServiceShape {
2222
}
2323

2424
async $openTunnel(tunnelOptions: TunnelOptions): Promise<TunnelDto | undefined> {
25-
const tunnel = await this.remoteExplorerService.forward(tunnelOptions.remote.port, tunnelOptions.localPort, tunnelOptions.name);
25+
const tunnel = await this.remoteExplorerService.forward(tunnelOptions.remote, tunnelOptions.localPort, tunnelOptions.name);
2626
if (tunnel) {
2727
return TunnelDto.fromServiceTunnel(tunnel);
2828
}
2929
return undefined;
3030
}
3131

32-
async $closeTunnel(remotePort: number): Promise<void> {
33-
return this.remoteExplorerService.close(remotePort);
32+
async $closeTunnel(remote: { host: string, port: number }): Promise<void> {
33+
return this.remoteExplorerService.close(remote);
3434
}
3535

3636
async $registerCandidateFinder(): Promise<void> {

src/vs/workbench/api/common/extHost.protocol.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -774,7 +774,7 @@ export interface MainThreadWindowShape extends IDisposable {
774774

775775
export interface MainThreadTunnelServiceShape extends IDisposable {
776776
$openTunnel(tunnelOptions: TunnelOptions): Promise<TunnelDto | undefined>;
777-
$closeTunnel(remotePort: number): Promise<void>;
777+
$closeTunnel(remote: { host: string, port: number }): Promise<void>;
778778
$registerCandidateFinder(): Promise<void>;
779779
$setTunnelProvider(): Promise<void>;
780780
}
@@ -1395,7 +1395,7 @@ export interface ExtHostStorageShape {
13951395

13961396

13971397
export interface ExtHostTunnelServiceShape {
1398-
$findCandidatePorts(): Promise<{ port: number, detail: string }[]>;
1398+
$findCandidatePorts(): Promise<{ host: string, port: number, detail: string }[]>;
13991399
$forwardPort(tunnelOptions: TunnelOptions): Promise<TunnelDto> | undefined;
14001400
$closeTunnel(remote: { host: string, port: number }): Promise<void>;
14011401
}

src/vs/workbench/api/common/extHostTunnelService.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export class ExtHostTunnelService implements IExtHostTunnelService {
4848
async makeTunnel(forward: TunnelOptions): Promise<vscode.Tunnel | undefined> {
4949
return undefined;
5050
}
51-
async $findCandidatePorts(): Promise<{ port: number; detail: string; }[]> {
51+
async $findCandidatePorts(): Promise<{ host: string, port: number; detail: string; }[]> {
5252
return [];
5353
}
5454
async setForwardPortProvider(provider: vscode.RemoteAuthorityResolver | undefined): Promise<IDisposable> { return { dispose: () => { } }; }

src/vs/workbench/api/node/extHostTunnelService.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe
5252
const tunnel = await this._proxy.$openTunnel(forward);
5353
if (tunnel) {
5454
const disposableTunnel: vscode.Tunnel = new ExtensionTunnel(tunnel.remote, tunnel.localAddress, () => {
55-
return this._proxy.$closeTunnel(tunnel.remote.port);
55+
return this._proxy.$closeTunnel(tunnel.remote);
5656
});
5757
this._register(disposableTunnel);
5858
return disposableTunnel;
@@ -95,7 +95,7 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe
9595
this._extensionTunnels.set(tunnelOptions.remote.host, new Map());
9696
}
9797
this._extensionTunnels.get(tunnelOptions.remote.host)!.set(tunnelOptions.remote.port, tunnel);
98-
this._register(tunnel.onDispose(() => this._proxy.$closeTunnel(tunnel.remote.port)));
98+
this._register(tunnel.onDispose(() => this._proxy.$closeTunnel(tunnel.remote)));
9999
return Promise.resolve(TunnelDto.fromApiTunnel(tunnel));
100100
});
101101
}
@@ -104,12 +104,12 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe
104104
}
105105

106106

107-
async $findCandidatePorts(): Promise<{ port: number, detail: string }[]> {
107+
async $findCandidatePorts(): Promise<{ host: string, port: number, detail: string }[]> {
108108
if (!isLinux) {
109109
return [];
110110
}
111111

112-
const ports: { port: number, detail: string }[] = [];
112+
const ports: { host: string, port: number, detail: string }[] = [];
113113
const tcp: string = fs.readFileSync('/proc/net/tcp', 'utf8');
114114
const tcp6: string = fs.readFileSync('/proc/net/tcp6', 'utf8');
115115
const procSockets: string = await (new Promise(resolve => {
@@ -150,7 +150,7 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe
150150
connections.filter((connection => socketMap[connection.socket])).forEach(({ socket, ip, port }) => {
151151
const command = processMap[socketMap[socket].pid].cmd;
152152
if (!command.match('.*\.vscode\-server\-[a-zA-Z]+\/bin.*') && (command.indexOf('out/vs/server/main.js') === -1)) {
153-
ports.push({ port, detail: processMap[socketMap[socket].pid].cmd });
153+
ports.push({ host: ip, port, detail: processMap[socketMap[socket].pid].cmd });
154154
}
155155
});
156156

src/vs/workbench/contrib/remote/browser/tunnelView.ts

Lines changed: 40 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel';
2626
import { ActionRunner, IAction } from 'vs/base/common/actions';
2727
import { IMenuService, MenuId, IMenu, MenuRegistry, MenuItemAction } from 'vs/platform/actions/common/actions';
2828
import { createAndFillInContextMenuActions, createAndFillInActionBarActions, ContextAwareMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem';
29-
import { IRemoteExplorerService, TunnelModel } from 'vs/workbench/services/remote/common/remoteExplorerService';
29+
import { IRemoteExplorerService, TunnelModel, MakeAddress } from 'vs/workbench/services/remote/common/remoteExplorerService';
3030
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
3131
import { INotificationService } from 'vs/platform/notification/common/notification';
3232
import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
@@ -105,22 +105,23 @@ export class TunnelViewModel extends Disposable implements ITunnelViewModel {
105105

106106
get forwarded(): TunnelItem[] {
107107
return Array.from(this.model.forwarded.values()).map(tunnel => {
108-
return new TunnelItem(TunnelType.Forwarded, tunnel.remote, tunnel.localAddress, tunnel.closeable, tunnel.name, tunnel.description);
108+
return new TunnelItem(TunnelType.Forwarded, tunnel.remoteHost, tunnel.remotePort, tunnel.localAddress, tunnel.closeable, tunnel.name, tunnel.description);
109109
});
110110
}
111111

112112
get detected(): TunnelItem[] {
113113
return Array.from(this.model.detected.values()).map(tunnel => {
114-
return new TunnelItem(TunnelType.Detected, tunnel.remote, tunnel.localAddress, false, tunnel.name, tunnel.description);
114+
return new TunnelItem(TunnelType.Detected, tunnel.remoteHost, tunnel.remotePort, tunnel.localAddress, false, tunnel.name, tunnel.description);
115115
});
116116
}
117117

118118
get candidates(): Promise<TunnelItem[]> {
119119
return this.model.candidates.then(values => {
120120
const candidates: TunnelItem[] = [];
121121
values.forEach(value => {
122-
if (!this.model.forwarded.has(value.port) && !this.model.detected.has(value.port)) {
123-
candidates.push(new TunnelItem(TunnelType.Candidate, value.port, undefined, false, undefined, value.detail));
122+
const key = MakeAddress(value.host, value.port);
123+
if (!this.model.forwarded.has(key) && !this.model.detected.has(key)) {
124+
candidates.push(new TunnelItem(TunnelType.Candidate, value.host, value.port, undefined, false, undefined, value.detail));
124125
}
125126
});
126127
return candidates;
@@ -185,7 +186,7 @@ class TunnelTreeRenderer extends Disposable implements ITreeRenderer<ITunnelGrou
185186
}
186187

187188
private isTunnelItem(item: ITunnelGroup | ITunnelItem): item is ITunnelItem {
188-
return !!((<ITunnelItem>item).remote);
189+
return !!((<ITunnelItem>item).remotePort);
189190
}
190191

191192
renderElement(element: ITreeNode<ITunnelGroup | ITunnelItem, ITunnelGroup | ITunnelItem>, index: number, templateData: ITunnelTemplateData): void {
@@ -196,15 +197,15 @@ class TunnelTreeRenderer extends Disposable implements ITreeRenderer<ITunnelGrou
196197
templateData.actionBar.clear();
197198
let editableData: IEditableData | undefined;
198199
if (this.isTunnelItem(node)) {
199-
editableData = this.remoteExplorerService.getEditableData(node.remote);
200+
editableData = this.remoteExplorerService.getEditableData(node.remoteHost, node.remotePort);
200201
if (editableData) {
201202
templateData.iconLabel.element.style.display = 'none';
202203
this.renderInputBox(templateData.container, editableData);
203204
} else {
204205
templateData.iconLabel.element.style.display = 'flex';
205206
this.renderTunnel(node, templateData);
206207
}
207-
} else if ((node.tunnelType === TunnelType.Add) && (editableData = this.remoteExplorerService.getEditableData(undefined))) {
208+
} else if ((node.tunnelType === TunnelType.Add) && (editableData = this.remoteExplorerService.getEditableData(undefined, undefined))) {
208209
templateData.iconLabel.element.style.display = 'none';
209210
this.renderInputBox(templateData.container, editableData);
210211
} else {
@@ -338,7 +339,8 @@ interface ITunnelGroup {
338339

339340
interface ITunnelItem {
340341
tunnelType: TunnelType;
341-
remote: number;
342+
remoteHost: string;
343+
remotePort: number;
342344
localAddress?: string;
343345
name?: string;
344346
closeable?: boolean;
@@ -349,7 +351,8 @@ interface ITunnelItem {
349351
class TunnelItem implements ITunnelItem {
350352
constructor(
351353
public tunnelType: TunnelType,
352-
public remote: number,
354+
public remoteHost: string,
355+
public remotePort: number,
353356
public localAddress?: string,
354357
public closeable?: boolean,
355358
public name?: string,
@@ -359,17 +362,17 @@ class TunnelItem implements ITunnelItem {
359362
if (this.name) {
360363
return nls.localize('remote.tunnelsView.forwardedPortLabel0', "{0}", this.name);
361364
} else if (this.localAddress) {
362-
return nls.localize('remote.tunnelsView.forwardedPortLabel2', "{0} to {1}", this.remote, this.localAddress);
365+
return nls.localize('remote.tunnelsView.forwardedPortLabel2', "{0} to {1}", this.remotePort, this.localAddress);
363366
} else {
364-
return nls.localize('remote.tunnelsView.forwardedPortLabel3', "{0} not forwarded", this.remote);
367+
return nls.localize('remote.tunnelsView.forwardedPortLabel3', "{0} not forwarded", this.remotePort);
365368
}
366369
}
367370

368371
get description(): string | undefined {
369372
if (this._description) {
370373
return this._description;
371374
} else if (this.name) {
372-
return nls.localize('remote.tunnelsView.forwardedPortDescription0', "{0} to {1}", this.remote, this.localAddress);
375+
return nls.localize('remote.tunnelsView.forwardedPortDescription0', "{0} to {1}", this.remotePort, this.localAddress);
373376
}
374377
return undefined;
375378
}
@@ -474,7 +477,7 @@ export class TunnelPanel extends ViewPane {
474477
}));
475478

476479
this._register(this.remoteExplorerService.onDidChangeEditable(async e => {
477-
const isEditing = !!this.remoteExplorerService.getEditableData(e);
480+
const isEditing = !!this.remoteExplorerService.getEditableData(e.host, e.port);
478481

479482
if (!isEditing) {
480483
dom.removeClass(treeContainer, 'highlight');
@@ -575,12 +578,12 @@ namespace LabelTunnelAction {
575578
return async (accessor, arg) => {
576579
if (arg instanceof TunnelItem) {
577580
const remoteExplorerService = accessor.get(IRemoteExplorerService);
578-
remoteExplorerService.setEditable(arg.remote, {
581+
remoteExplorerService.setEditable(arg.remoteHost, arg.remotePort, {
579582
onFinish: (value, success) => {
580583
if (success) {
581-
remoteExplorerService.tunnelModel.name(arg.remote, value);
584+
remoteExplorerService.tunnelModel.name(arg.remoteHost, arg.remotePort, value);
582585
}
583-
remoteExplorerService.setEditable(arg.remote, null);
586+
remoteExplorerService.setEditable(arg.remoteHost, arg.remotePort, null);
584587
},
585588
validationMessage: () => null,
586589
placeholder: nls.localize('remote.tunnelsView.labelPlaceholder', "Port label"),
@@ -596,24 +599,32 @@ namespace ForwardPortAction {
596599
export const ID = 'remote.tunnel.forward';
597600
export const LABEL = nls.localize('remote.tunnel.forward', "Forward a Port");
598601

602+
function parseInput(value: string): { host: string, port: number } | undefined {
603+
const matches = value.match(/^([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\:|localhost:)?([0-9]+)$/);
604+
if (!matches) {
605+
return undefined;
606+
}
607+
return { host: matches[1]?.substring(0, matches[1].length - 1) || 'localhost', port: Number(matches[2]) };
608+
}
609+
599610
export function handler(): ICommandHandler {
600611
return async (accessor, arg) => {
601612
const remoteExplorerService = accessor.get(IRemoteExplorerService);
602613
if (arg instanceof TunnelItem) {
603-
remoteExplorerService.tunnelModel.forward(arg.remote);
614+
remoteExplorerService.forward({ host: arg.remoteHost, port: arg.remotePort });
604615
} else {
605616
const viewsService = accessor.get(IViewsService);
606617
await viewsService.openView(TunnelPanel.ID, true);
607-
remoteExplorerService.setEditable(undefined, {
618+
remoteExplorerService.setEditable(undefined, undefined, {
608619
onFinish: (value, success) => {
609-
if (success) {
610-
remoteExplorerService.tunnelModel.forward(Number(value));
620+
let parsed: { host: string, port: number } | undefined;
621+
if (success && (parsed = parseInput(value))) {
622+
remoteExplorerService.forward({ host: parsed.host, port: parsed.port });
611623
}
612-
remoteExplorerService.setEditable(undefined, null);
624+
remoteExplorerService.setEditable(undefined, undefined, null);
613625
},
614626
validationMessage: (value) => {
615-
const asNumber = Number(value);
616-
if ((value === '') || isNaN(asNumber) || (asNumber < 0) || (asNumber > 65535)) {
627+
if (!parseInput(value)) {
617628
return nls.localize('remote.tunnelsView.portNumberValid', "Port number is invalid");
618629
}
619630
return null;
@@ -633,7 +644,7 @@ namespace ClosePortAction {
633644
return async (accessor, arg) => {
634645
if (arg instanceof TunnelItem) {
635646
const remoteExplorerService = accessor.get(IRemoteExplorerService);
636-
await remoteExplorerService.tunnelModel.close(arg.remote);
647+
await remoteExplorerService.close({ host: arg.remoteHost, port: arg.remotePort });
637648
}
638649
};
639650
}
@@ -648,9 +659,10 @@ namespace OpenPortInBrowserAction {
648659
if (arg instanceof TunnelItem) {
649660
const model = accessor.get(IRemoteExplorerService).tunnelModel;
650661
const openerService = accessor.get(IOpenerService);
651-
const tunnel = model.forwarded.has(arg.remote) ? model.forwarded.get(arg.remote) : model.detected.get(arg.remote);
662+
const key = MakeAddress(arg.remoteHost, arg.remotePort);
663+
const tunnel = model.forwarded.get(key) || model.detected.get(key);
652664
let address: string | undefined;
653-
if (tunnel && tunnel.localAddress && (address = model.address(tunnel.remote))) {
665+
if (tunnel && tunnel.localAddress && (address = model.address(tunnel.remoteHost, tunnel.remotePort))) {
654666
return openerService.open(URI.parse('http://' + address));
655667
}
656668
return Promise.resolve();
@@ -668,7 +680,7 @@ namespace CopyAddressAction {
668680
if (arg instanceof TunnelItem) {
669681
const model = accessor.get(IRemoteExplorerService).tunnelModel;
670682
const clipboard = accessor.get(IClipboardService);
671-
const address = model.address(arg.remote);
683+
const address = model.address(arg.remoteHost, arg.remotePort);
672684
if (address) {
673685
await clipboard.writeText(address.toString());
674686
}

src/vs/workbench/contrib/webview/common/portMapping.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export class WebviewPortMappingManager extends Disposable {
6868
if (existing) {
6969
return existing;
7070
}
71-
const tunnel = this.tunnelService.openTunnel(remotePort);
71+
const tunnel = this.tunnelService.openTunnel(undefined, remotePort);
7272
if (tunnel) {
7373
this._tunnels.set(remotePort, tunnel);
7474
}

src/vs/workbench/electron-browser/window.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -453,7 +453,7 @@ export class ElectronWindow extends Disposable {
453453
if (options?.allowTunneling) {
454454
const portMappingRequest = extractLocalHostUriMetaDataForPortMapping(uri);
455455
if (portMappingRequest) {
456-
const tunnel = await this.tunnelService.openTunnel(portMappingRequest.port);
456+
const tunnel = await this.tunnelService.openTunnel(undefined, portMappingRequest.port);
457457
if (tunnel) {
458458
return {
459459
resolved: uri.with({ authority: `127.0.0.1:${tunnel.tunnelLocalPort}` }),

0 commit comments

Comments
 (0)