src/vs/workbench/services/remote/common/tunnelModel.ts

1042 LOC · 270 covered · 772 uncovered · 47 ranges · 7 concepts · 1 introducers · 6 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the filesrc/vs/platform/remote/common/managedSocket.ts · 145 LOCcommon/managedSocket.tssrc/vs/platform/remote/common/remoteAgentConnection.ts · 839 LOCcommon/remoteAgentConnec…src/vs/platform/remote/common/remoteSocketFactoryService.ts · 60 LOCcommon/remoteSocketFacto…src/vs/platform/sign/common/sign.ts · 22 LOCcommon/sign.tssrc/vs/platform/tunnel/node/tunnelService.ts · 265 LOCnode/tunnelService.tssrc/vs/workbench/api/node/extHostTunnelService.ts · 419 LOCnode/extHostTunnelServic…extHostTunnelService.ts ×3 · 22 introduced LOCextHostTunnelService.ts …extHostTunnelService.ts ×2 · 21 introduced LOCextHostTunnelService.ts …extHostTunnelService.ts ×4 · 38 introduced LOCextHostTunnelService.ts …extHostTunnelService.ts ×2 · 23 introduced LOCextHostTunnelService.ts …extHostTunnelService.ts ×1 · 9 introduced LOCextHostTunnelService.ts …extHostTunnelService.ts ×1 · 17 introduced LOCextHostTunnelService.ts …remoteAgentConnection.ts ×53 · 823 introduced LOCremoteAgentConnection.ts…extHostTunnelService.test|title=ExtHostTunnelService findPorts|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostTunnelService.test|title=ExtHostTunnelService findPorts|occurrence=1extHostTunnelService.tes…extHostTunnelService.test|title=ExtHostTunnelService getSockets|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostTunnelService.test|title=ExtHostTunnelService getSockets|occurrence=1extHostTunnelService.tes…extHostTunnelService.test|title=ExtHostTunnelService loadConnectionTable|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostTunnelService.test|title=ExtHostTunnelService loadConnectionTable|occurrence=1extHostTunnelService.tes…extHostTunnelService.test|title=ExtHostTunnelService loadListeningPorts|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostTunnelService.test|title=ExtHostTunnelService loadListeningPorts|occurrence=1extHostTunnelService.tes…extHostTunnelService.test|title=ExtHostTunnelService parseIpAddress|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostTunnelService.test|title=ExtHostTunnelService parseIpAddress|occurrence=1extHostTunnelService.tes…extHostTunnelService.test|title=ExtHostTunnelService tryFindRootPorts|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/workbench/api/test/node/extHostTunnelService.test|title=ExtHostTunnelService tryFindRootPorts|occurrence=1extHostTunnelService.tes…Focused file · src/vs/workbench/services/remote/common/tunnelModel.ts · 1042 LOCcommon/tunnelModel.ts

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- remoteAgentConnection.ts ×53
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import * as nls from '../../../../nls.js';
7 > import { debounce } from '../../../../base/common/decorators.js';
8 > import { Emitter, Event } from '../../../../base/common/event.js';
9 > import { hash } from '../../../../base/common/hash.js';
10 > import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';
11 > import { URI } from '../../../../base/common/uri.js';
12 > import { ConfigurationTarget, IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
13 > import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';
14 > import { ILogService } from '../../../../platform/log/common/log.js';
15 > import { IAddressProvider } from '../../../../platform/remote/common/remoteAgentConnection.js';
16 > import { IRemoteAuthorityResolverService, TunnelDescription } from '../../../../platform/remote/common/remoteAuthorityResolver.js';
17 > import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
18 > import { RemoteTunnel, ITunnelService, TunnelProtocol, TunnelPrivacyId, LOCALHOST_ADDRESSES, ProvidedPortAttributes, PortAttributesProvider, isLocalhost, isAllInterfaces, ProvidedOnAutoForward, ALL_INTERFACES_ADDRESSES } from '../../../../platform/tunnel/common/tunnel.js';
19 > import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
20 > import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService.js';
21 > import { IExtensionService } from '../../extensions/common/extensions.js';
22 > import { CancellationToken } from '../../../../base/common/cancellation.js';
23 > import { isNumber, isObject, isString } from '../../../../base/common/types.js';
24 > import { deepClone } from '../../../../base/common/objects.js';
25 > import { IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
26 >
27 > const MISMATCH_LOCAL_PORT_COOLDOWN = 10 * 1000; // 10 seconds
28 > const TUNNELS_TO_RESTORE = 'remote.tunnels.toRestore';
29 > const TUNNELS_TO_RESTORE_EXPIRATION = 'remote.tunnels.toRestoreExpiration';
30 > const RESTORE_EXPIRATION_TIME = 1000 * 60 * 60 * 24 * 14; // 2 weeks
31 > export const ACTIVATION_EVENT = 'onTunnel';
32 > export const forwardedPortsFeaturesEnabled = new RawContextKey<boolean>('forwardedPortsViewEnabled', false, nls.localize('tunnel.forwardedPortsViewEnabled', "Whether the Ports view is enabled."));
33 > export const forwardedPortsViewEnabled = new RawContextKey<boolean>('forwardedPortsViewOnlyEnabled', false, nls.localize('tunnel.forwardedPortsViewEnabled', "Whether the Ports view is enabled."));
34 >
35 > export interface RestorableTunnel {
36 > remoteHost: string;
37 > remotePort: number;
38 > localAddress: string;
39 > localUri: URI;
40 > protocol: TunnelProtocol;
41 > localPort?: number;
42 > name?: string;
43 > source: {
44 > source: TunnelSource;
45 > description: string;
46 > };
47 > }
48 >
49 > export interface Tunnel {
50 > remoteHost: string;
51 > remotePort: number;
52 > localAddress: string;
53 > localUri: URI;
54 > protocol: TunnelProtocol;
55 > localPort?: number;
56 > name?: string;
57 > closeable?: boolean;
58 > privacy: TunnelPrivacyId | string;
59 > runningProcess: string | undefined;
60 > hasRunningProcess?: boolean;
61 > pid: number | undefined;
62 > source: {
63 > source: TunnelSource;
64 > description: string;
65 > };
66 > }
67 >
68 > export function parseAddress(address: string): { host: string; port: number } | undefined {
69 const matches = address.match(/^([a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*:)?([0-9]+)$/);
70 if (!matches) {
71 return undefined;
72 }
73 return { host: matches[1]?.substring(0, matches[1].length - 1) || 'localhost', port: Number(matches[2]) };
74 }
76 > export enum TunnelCloseReason {
77 > Other = 'Other',
78 > User = 'User',
79 > AutoForwardEnd = 'AutoForwardEnd',
80 > }
81 >
82 > export enum TunnelSource {
83 > User,
84 > Auto,
85 > Extension
86 > }
87 >
88 > export const UserTunnelSource = {
89 > source: TunnelSource.User,
90 > description: nls.localize('tunnel.source.user', "User Forwarded")
91 > };
92 > export const AutoTunnelSource = {
93 > source: TunnelSource.Auto,
94 > description: nls.localize('tunnel.source.auto', "Auto Forwarded")
95 > };
96 >
97 > export function mapHasAddress<T>(map: Map<string, T>, host: string, port: number): T | undefined {
98 const initialAddress = map.get(makeAddress(host, port));
99 if (initialAddress) {
100 return initialAddress;
101 }
102
103 if (isLocalhost(host)) {
104 // Do localhost checks
105 for (const testHost of LOCALHOST_ADDRESSES) {
106 const testAddress = makeAddress(testHost, port);
107 if (map.has(testAddress)) {
108 return map.get(testAddress);
109 }
110 }
111 } else if (isAllInterfaces(host)) {
112 // Do all interfaces checks
113 for (const testHost of ALL_INTERFACES_ADDRESSES) {
114 const testAddress = makeAddress(testHost, port);
115 if (map.has(testAddress)) {
116 return map.get(testAddress);
117 }
118 }
119 }
120
121 return undefined;
122 }
124 > export function mapHasAddressLocalhostOrAllInterfaces<T>(map: Map<string, T>, host: string, port: number): T | undefined {
125 const originalAddress = mapHasAddress(map, host, port);
126 if (originalAddress) {
127 return originalAddress;
128 }
129 const otherHost = isAllInterfaces(host) ? 'localhost' : (isLocalhost(host) ? '0.0.0.0' : undefined);
130 if (otherHost) {
131 return mapHasAddress(map, otherHost, port);
132 }
133 return undefined;
134 }
136 >
137 > export function makeAddress(host: string, port: number): string {
138 return host + ':' + port;
139 }
141 > export interface TunnelProperties {
142 > remote: { host: string; port: number };
143 > local?: number;
144 > name?: string;
145 > source?: {
146 > source: TunnelSource;
147 > description: string;
148 > };
149 > elevateIfNeeded?: boolean;
150 > privacy?: string;
151 > }
152 >
153 > export interface CandidatePort {
154 > host: string;
155 > port: number;
156 > detail?: string;
157 > pid?: number;
158 > }
159 >
160 > interface PortAttributes extends Attributes {
161 > key: number | PortRange | RegExp | HostAndPort;
162 > }
163 >
164 > export enum OnPortForward {
165 > Notify = 'notify',
166 > OpenBrowser = 'openBrowser',
167 > OpenBrowserOnce = 'openBrowserOnce',
168 > OpenPreview = 'openPreview',
169 > Silent = 'silent',
170 > Ignore = 'ignore'
171 > }
172 >
173 > export interface Attributes {
174 > label: string | undefined;
175 > onAutoForward: OnPortForward | undefined;
176 > elevateIfNeeded: boolean | undefined;
177 > requireLocalPort: boolean | undefined;
178 > protocol: TunnelProtocol | undefined;
179 > }
180 >
181 > interface PortRange { start: number; end: number }
182 >
183 > interface HostAndPort { host: string; port: number }
184 >
185 > export function isCandidatePort(candidate: any): candidate is CandidatePort {
186 return candidate && 'host' in candidate && typeof candidate.host === 'string'
187 && 'port' in candidate && typeof candidate.port === 'number'
188 && (!('detail' in candidate) || typeof candidate.detail === 'string')
189 && (!('pid' in candidate) || typeof candidate.pid === 'string');
190 }
192 > export class PortsAttributes extends Disposable {
193 > private static SETTING = 'remote.portsAttributes';
194 > private static DEFAULTS = 'remote.otherPortsAttributes';
195 > private static RANGE = /^(\d+)\-(\d+)$/;
196 > private static HOST_AND_PORT = /^([a-z0-9\-]+):(\d{1,5})$/;
197 > private portsAttributes: PortAttributes[] = [];
198 > private defaultPortAttributes: Attributes | undefined;
199 > private _onDidChangeAttributes = this._register(new Emitter<void>());
200 > public readonly onDidChangeAttributes = this._onDidChangeAttributes.event;
201 >
202 > constructor(private readonly configurationService: IConfigurationService) {
203 super();
204 this._register(configurationService.onDidChangeConfiguration(e => {
205 if (e.affectsConfiguration(PortsAttributes.SETTING) || e.affectsConfiguration(PortsAttributes.DEFAULTS)) {
206 this.updateAttributes();
207 }
208 }));
209 this.updateAttributes();
210 }
212 > private updateAttributes() {
213 this.portsAttributes = this.readSetting();
214 this._onDidChangeAttributes.fire();
215 }
217 > getAttributes(port: number, host: string, commandLine?: string): Attributes | undefined {
218 let index = this.findNextIndex(port, host, commandLine, this.portsAttributes, 0);
219 const attributes: Attributes = {
220 label: undefined,
221 onAutoForward: undefined,
222 elevateIfNeeded: undefined,
223 requireLocalPort: undefined,
224 protocol: undefined
225 };
226 while (index >= 0) {
227 const found = this.portsAttributes[index];
228 if (found.key === port) {
229 attributes.onAutoForward = found.onAutoForward ?? attributes.onAutoForward;
230 attributes.elevateIfNeeded = (found.elevateIfNeeded !== undefined) ? found.elevateIfNeeded : attributes.elevateIfNeeded;
231 attributes.label = found.label ?? attributes.label;
232 attributes.requireLocalPort = found.requireLocalPort;
233 attributes.protocol = found.protocol;
234 } else {
235 // It's a range or regex, which means that if the attribute is already set, we keep it
236 attributes.onAutoForward = attributes.onAutoForward ?? found.onAutoForward;
237 attributes.elevateIfNeeded = (attributes.elevateIfNeeded !== undefined) ? attributes.elevateIfNeeded : found.elevateIfNeeded;
238 attributes.label = attributes.label ?? found.label;
239 attributes.requireLocalPort = (attributes.requireLocalPort !== undefined) ? attributes.requireLocalPort : undefined;
240 attributes.protocol = attributes.protocol ?? found.protocol;
241 }
242 index = this.findNextIndex(port, host, commandLine, this.portsAttributes, index + 1);
243 }
244 if (attributes.onAutoForward !== undefined || attributes.elevateIfNeeded !== undefined
245 || attributes.label !== undefined || attributes.requireLocalPort !== undefined
246 || attributes.protocol !== undefined) {
247 return attributes;
248 }
249
250 // If we find no matches, then use the other port attributes.
251 return this.getOtherAttributes();
252 }
254 > private hasStartEnd(value: number | PortRange | RegExp | HostAndPort): value is PortRange {
255 return (value as Partial<PortRange>).start !== undefined && (value as Partial<PortRange>).end !== undefined;
256 }
258 > private hasHostAndPort(value: number | PortRange | RegExp | HostAndPort): value is HostAndPort {
259 return ((value as Partial<HostAndPort>).host !== undefined) && ((value as Partial<HostAndPort>).port !== undefined)
260 && isString((value as Partial<HostAndPort>).host) && isNumber((value as Partial<HostAndPort>).port);
261 }
263 > private findNextIndex(port: number, host: string, commandLine: string | undefined, attributes: PortAttributes[], fromIndex: number): number {
264 if (fromIndex >= attributes.length) {
265 return -1;
266 }
267 const shouldUseHost = !isLocalhost(host) && !isAllInterfaces(host);
268 const sliced = attributes.slice(fromIndex);
269 const foundIndex = sliced.findIndex((value) => {
270 if (isNumber(value.key)) {
271 return shouldUseHost ? false : value.key === port;
272 } else if (this.hasStartEnd(value.key)) {
273 return shouldUseHost ? false : (port >= value.key.start && port <= value.key.end);
274 } else if (this.hasHostAndPort(value.key)) {
275 return (port === value.key.port) && (host === value.key.host);
276 } else {
277 return commandLine ? value.key.test(commandLine) : false;
278 }
279
280 });
281 return foundIndex >= 0 ? foundIndex + fromIndex : -1;
282 }
284 > private readSetting(): PortAttributes[] {
285 const settingValue = this.configurationService.getValue(PortsAttributes.SETTING);
286 if (!settingValue || !isObject(settingValue)) {
287 return [];
288 }
289
290 const attributes: PortAttributes[] = [];
291 for (const attributesKey in settingValue) {
292 if (attributesKey === undefined) {
293 continue;
294 }
295 const setting = (settingValue as Record<string, PortAttributes>)[attributesKey];
296 let key: number | PortRange | RegExp | HostAndPort | undefined = undefined;
297 if (Number(attributesKey)) {
298 key = Number(attributesKey);
299 } else if (isString(attributesKey)) {
300 if (PortsAttributes.RANGE.test(attributesKey)) {
301 const match = attributesKey.match(PortsAttributes.RANGE);
302 key = { start: Number(match![1]), end: Number(match![2]) };
303 } else if (PortsAttributes.HOST_AND_PORT.test(attributesKey)) {
304 const match = attributesKey.match(PortsAttributes.HOST_AND_PORT);
305 key = { host: match![1], port: Number(match![2]) };
306 } else {
307 let regTest: RegExp | undefined = undefined;
308 try {
309 regTest = RegExp(attributesKey);
310 } catch (e) {
311 // The user entered an invalid regular expression.
312 }
313 if (regTest) {
314 key = regTest;
315 }
316 }
317 }
318 if (!key) {
319 continue;
320 }
321 attributes.push({
322 key: key,
323 elevateIfNeeded: setting.elevateIfNeeded,
324 onAutoForward: setting.onAutoForward,
325 label: setting.label,
326 requireLocalPort: setting.requireLocalPort,
327 protocol: setting.protocol
328 });
329 }
330
331 const defaults = this.configurationService.getValue(PortsAttributes.DEFAULTS) as Partial<Attributes> | undefined;
332 if (defaults) {
333 this.defaultPortAttributes = {
334 elevateIfNeeded: defaults.elevateIfNeeded,
335 label: defaults.label,
336 onAutoForward: defaults.onAutoForward,
337 requireLocalPort: defaults.requireLocalPort,
338 protocol: defaults.protocol
339 };
340 }
341
342 return this.sortAttributes(attributes);
343 }
345 > private sortAttributes(attributes: PortAttributes[]): PortAttributes[] {
346 function getVal(item: PortAttributes, thisRef: PortsAttributes) {
347 if (isNumber(item.key)) {
348 return item.key;
349 } else if (thisRef.hasStartEnd(item.key)) {
350 return item.key.start;
351 } else if (thisRef.hasHostAndPort(item.key)) {
352 return item.key.port;
353 } else {
354 return Number.MAX_VALUE;
355 }
356 }
357
358 return attributes.sort((a, b) => {
359 return getVal(a, this) - getVal(b, this);
360 });
361 }
363 > private getOtherAttributes() {
364 return this.defaultPortAttributes;
365 }
367 > static providedActionToAction(providedAction: ProvidedOnAutoForward | undefined) {
368 switch (providedAction) {
369 case ProvidedOnAutoForward.Notify: return OnPortForward.Notify;
370 case ProvidedOnAutoForward.OpenBrowser: return OnPortForward.OpenBrowser;
371 case ProvidedOnAutoForward.OpenBrowserOnce: return OnPortForward.OpenBrowserOnce;
372 case ProvidedOnAutoForward.OpenPreview: return OnPortForward.OpenPreview;
373 case ProvidedOnAutoForward.Silent: return OnPortForward.Silent;
374 case ProvidedOnAutoForward.Ignore: return OnPortForward.Ignore;
375 default: return undefined;
376 }
377 }
379 > public async addAttributes(port: number, attributes: Partial<Attributes>, target: ConfigurationTarget) {
380 const settingValue = this.configurationService.inspect(PortsAttributes.SETTING);
381 const remoteValue: any = settingValue.userRemoteValue;
382 let newRemoteValue: any;
383 if (!remoteValue || !isObject(remoteValue)) {
384 newRemoteValue = {};
385 } else {
386 newRemoteValue = deepClone(remoteValue);
387 }
388
389 if (!newRemoteValue[`${port}`]) {
390 newRemoteValue[`${port}`] = {};
391 }
392 for (const attribute in attributes) {
393 newRemoteValue[`${port}`][attribute] = (attributes as Record<string, unknown>)[attribute];
394 }
395
396 return this.configurationService.updateValue(PortsAttributes.SETTING, newRemoteValue, target);
397 }
399 >
400 > export class TunnelModel extends Disposable {
401 > readonly forwarded: Map<string, Tunnel>;
402 > private readonly inProgress: Map<string, true> = new Map();
403 > readonly detected: Map<string, Tunnel>;
404 > private remoteTunnels: Map<string, RemoteTunnel>;
405 > private _onForwardPort = this._register(new Emitter<Tunnel | void>());
406 > public onForwardPort = this._onForwardPort.event;
407 > private _onClosePort = this._register(new Emitter<{ host: string; port: number }>());
408 > public onClosePort = this._onClosePort.event;
409 > private _onPortName = this._register(new Emitter<{ host: string; port: number }>());
410 > public onPortName = this._onPortName.event;
411 > private _candidates: Map<string, CandidatePort> | undefined;
412 > private _onCandidatesChanged = this._register(new Emitter<Map<string, { host: string; port: number }>>());
413 > // onCandidateChanged returns the removed candidates
414 > public onCandidatesChanged = this._onCandidatesChanged.event;
415 > private _candidateFilter: ((candidates: CandidatePort[]) => Promise<CandidatePort[]>) | undefined;
416 > private tunnelRestoreValue: Promise<string | undefined>;
417 > private _onEnvironmentTunnelsSet = this._register(new Emitter<void>());
418 > public onEnvironmentTunnelsSet = this._onEnvironmentTunnelsSet.event;
419 > private _environmentTunnelsSet: boolean = false;
420 > public readonly configPortsAttributes: PortsAttributes;
421 > private restoreListener: DisposableStore | undefined = undefined;
422 > private knownPortsRestoreValue: string | undefined;
423 > private restoreComplete = false;
424 > private onRestoreComplete = this._register(new Emitter<void>());
425 > private unrestoredExtensionTunnels: Map<string, RestorableTunnel> = new Map();
426 > private sessionCachedProperties: Map<string, Partial<TunnelProperties>> = new Map();
427 >
428 > private portAttributesProviders: PortAttributesProvider[] = [];
429 >
430 > constructor(
431 @ITunnelService private readonly tunnelService: ITunnelService,
432 @IStorageService private readonly storageService: IStorageService,
433 @IConfigurationService private readonly configurationService: IConfigurationService,
434 @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
435 @IRemoteAuthorityResolverService private readonly remoteAuthorityResolverService: IRemoteAuthorityResolverService,
436 @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
437 @ILogService private readonly logService: ILogService,
438 @IDialogService private readonly dialogService: IDialogService,
439 @IExtensionService private readonly extensionService: IExtensionService,
440 @IContextKeyService private readonly contextKeyService: IContextKeyService
441 ) {
442 super();
443 this.configPortsAttributes = new PortsAttributes(configurationService);
444 this.tunnelRestoreValue = this.getTunnelRestoreValue();
445 this._register(this.configPortsAttributes.onDidChangeAttributes(this.updateAttributes, this));
446 this.forwarded = new Map();
447 this.remoteTunnels = new Map();
448 this.tunnelService.tunnels.then(async (tunnels) => {
449 const attributes = await this.getAttributes(tunnels.map(tunnel => {
450 return { port: tunnel.tunnelRemotePort, host: tunnel.tunnelRemoteHost };
451 }));
452 for (const tunnel of tunnels) {
453 if (tunnel.localAddress) {
454 const key = makeAddress(tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort);
455 const matchingCandidate = mapHasAddressLocalhostOrAllInterfaces(this._candidates ?? new Map(), tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort);
456 this.forwarded.set(key, {
457 remotePort: tunnel.tunnelRemotePort,
458 remoteHost: tunnel.tunnelRemoteHost,
459 localAddress: tunnel.localAddress,
460 protocol: attributes?.get(tunnel.tunnelRemotePort)?.protocol ?? TunnelProtocol.Http,
461 localUri: await this.makeLocalUri(tunnel.localAddress, attributes?.get(tunnel.tunnelRemotePort)),
462 localPort: tunnel.tunnelLocalPort,
463 name: attributes?.get(tunnel.tunnelRemotePort)?.label,
464 runningProcess: matchingCandidate?.detail,
465 hasRunningProcess: !!matchingCandidate,
466 pid: matchingCandidate?.pid,
467 privacy: tunnel.privacy,
468 source: UserTunnelSource,
469 });
470 this.remoteTunnels.set(key, tunnel);
471 }
472 }
473 });
474
475 this.detected = new Map();
476 this._register(this.tunnelService.onTunnelOpened(async (tunnel) => {
477 const key = makeAddress(tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort);
478 if (!mapHasAddressLocalhostOrAllInterfaces(this.forwarded, tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort)
479 && !mapHasAddressLocalhostOrAllInterfaces(this.detected, tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort)
480 && !mapHasAddressLocalhostOrAllInterfaces(this.inProgress, tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort)
481 && tunnel.localAddress) {
482 const matchingCandidate = mapHasAddressLocalhostOrAllInterfaces(this._candidates ?? new Map(), tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort);
483 const attributes = (await this.getAttributes([{ port: tunnel.tunnelRemotePort, host: tunnel.tunnelRemoteHost }]))?.get(tunnel.tunnelRemotePort);
484 this.forwarded.set(key, {
485 remoteHost: tunnel.tunnelRemoteHost,
486 remotePort: tunnel.tunnelRemotePort,
487 localAddress: tunnel.localAddress,
488 protocol: attributes?.protocol ?? TunnelProtocol.Http,
489 localUri: await this.makeLocalUri(tunnel.localAddress, attributes),
490 localPort: tunnel.tunnelLocalPort,
491 name: attributes?.label,
492 closeable: true,
493 runningProcess: matchingCandidate?.detail,
494 hasRunningProcess: !!matchingCandidate,
495 pid: matchingCandidate?.pid,
496 privacy: tunnel.privacy,
497 source: UserTunnelSource,
498 });
499 }
500 await this.storeForwarded();
501 this.checkExtensionActivationEvents(true);
502 this.remoteTunnels.set(key, tunnel);
503 this._onForwardPort.fire(this.forwarded.get(key)!);
504 }));
505 this._register(this.tunnelService.onTunnelClosed(address => {
506 return this.onTunnelClosed(address, TunnelCloseReason.Other);
507 }));
508 this.checkExtensionActivationEvents(false);
509 }
511 > private extensionHasActivationEvent() {
512 if (this.extensionService.extensions.find(extension => extension.activationEvents?.includes(ACTIVATION_EVENT))) {
513 this.contextKeyService.createKey(forwardedPortsViewEnabled.key, true);
514 return true;
515 }
516 return false;
517 }
519 > private hasCheckedExtensionsOnTunnelOpened = false;
520 > private checkExtensionActivationEvents(tunnelOpened: boolean) {
521 if (this.hasCheckedExtensionsOnTunnelOpened) {
522 return;
523 }
524 if (tunnelOpened) {
525 this.hasCheckedExtensionsOnTunnelOpened = true;
526 }
527 const hasRemote = this.environmentService.remoteAuthority !== undefined;
528 if (hasRemote && !tunnelOpened) {
529 // We don't activate extensions on startup if there is a remote
530 return;
531 }
532 if (this.extensionHasActivationEvent()) {
533 return;
534 }
535
536 const activationDisposable = this._register(this.extensionService.onDidRegisterExtensions(() => {
537 if (this.extensionHasActivationEvent()) {
538 activationDisposable.dispose();
539 }
540 }));
541 }
543 > private async onTunnelClosed(address: { host: string; port: number }, reason: TunnelCloseReason) {
544 const key = makeAddress(address.host, address.port);
545 if (this.forwarded.delete(key)) {
546 await this.storeForwarded();
547 this._onClosePort.fire(address);
548 }
549 }
551 > private makeLocalUri(localAddress: string, attributes?: Attributes) {
552 if (localAddress.startsWith('http')) {
553 return URI.parse(localAddress);
554 }
555 const protocol = attributes?.protocol ?? 'http';
556 return URI.parse(`${protocol}://${localAddress}`);
557 }
559 > private async addStorageKeyPostfix(prefix: string): Promise<string | undefined> {
560 const workspace = this.workspaceContextService.getWorkspace();
561 const workspaceHash = workspace.configuration ? hash(workspace.configuration.path) : (workspace.folders.length > 0 ? hash(workspace.folders[0].uri.path) : undefined);
562 if (workspaceHash === undefined) {
563 this.logService.debug('Could not get workspace hash for forwarded ports storage key.');
564 return undefined;
565 }
566 return `${prefix}.${this.environmentService.remoteAuthority}.${workspaceHash}`;
567 }
569 > private async getTunnelRestoreStorageKey(): Promise<string | undefined> {
570 return this.addStorageKeyPostfix(TUNNELS_TO_RESTORE);
571 }
573 > private async getRestoreExpirationStorageKey(): Promise<string | undefined> {
574 return this.addStorageKeyPostfix(TUNNELS_TO_RESTORE_EXPIRATION);
575 }
577 > private async getTunnelRestoreValue(): Promise<string | undefined> {
578 const deprecatedValue = this.storageService.get(TUNNELS_TO_RESTORE, StorageScope.WORKSPACE);
579 if (deprecatedValue) {
580 this.storageService.remove(TUNNELS_TO_RESTORE, StorageScope.WORKSPACE);
581 await this.storeForwarded();
582 return deprecatedValue;
583 }
584 const storageKey = await this.getTunnelRestoreStorageKey();
585 if (!storageKey) {
586 return undefined;
587 }
588 return this.storageService.get(storageKey, StorageScope.PROFILE);
589 }
591 > async restoreForwarded() {
592 this.cleanupExpiredTunnelsForRestore();
593 if (this.configurationService.getValue('remote.restoreForwardedPorts')) {
594 const tunnelRestoreValue = await this.tunnelRestoreValue;
595 if (tunnelRestoreValue && (tunnelRestoreValue !== this.knownPortsRestoreValue)) {
596 const tunnels = <RestorableTunnel[] | undefined>JSON.parse(tunnelRestoreValue) ?? [];
597 this.logService.trace(`ForwardedPorts: (TunnelModel) restoring ports ${tunnels.map(tunnel => tunnel.remotePort).join(', ')}`);
598 for (const tunnel of tunnels) {
599 const alreadyForwarded = mapHasAddressLocalhostOrAllInterfaces(this.detected, tunnel.remoteHost, tunnel.remotePort);
600 // Extension forwarded ports should only be updated, not restored.
601 if ((tunnel.source.source !== TunnelSource.Extension && !alreadyForwarded) || (tunnel.source.source === TunnelSource.Extension && alreadyForwarded)) {
602 await this.doForward({
603 remote: { host: tunnel.remoteHost, port: tunnel.remotePort },
604 local: tunnel.localPort,
605 name: tunnel.name,
606 elevateIfNeeded: true,
607 source: tunnel.source
608 });
609 } else if (tunnel.source.source === TunnelSource.Extension && !alreadyForwarded) {
610 this.unrestoredExtensionTunnels.set(makeAddress(tunnel.remoteHost, tunnel.remotePort), tunnel);
611 }
612 }
613 }
614 }
615
616 this.restoreComplete = true;
617 this.onRestoreComplete.fire();
618
619 if (!this.restoreListener) {
620 // It's possible that at restore time the value hasn't synced.
621 const key = await this.getTunnelRestoreStorageKey();
622 this.restoreListener = this._register(new DisposableStore());
623 this.restoreListener.add(this.storageService.onDidChangeValue(StorageScope.PROFILE, undefined, this.restoreListener)(async (e) => {
624 if (e.key === key) {
625 this.tunnelRestoreValue = Promise.resolve(this.storageService.get(key, StorageScope.PROFILE));
626 await this.restoreForwarded();
627 }
628 }));
629 }
630 }
632 > private cleanupExpiredTunnelsForRestore() {
633 const keys = this.storageService.keys(StorageScope.PROFILE, StorageTarget.USER).filter(key => key.startsWith(TUNNELS_TO_RESTORE_EXPIRATION));
634 for (const key of keys) {
635 const expiration = this.storageService.getNumber(key, StorageScope.PROFILE);
636 if (expiration && expiration < Date.now()) {
637 this.tunnelRestoreValue = Promise.resolve(undefined);
638 const storageKey = key.replace(TUNNELS_TO_RESTORE_EXPIRATION, TUNNELS_TO_RESTORE);
639 this.storageService.remove(key, StorageScope.PROFILE);
640 this.storageService.remove(storageKey, StorageScope.PROFILE);
641 }
642 }
643 }
645 > @debounce(1000)
646 > private async storeForwarded() {
647 if (this.configurationService.getValue('remote.restoreForwardedPorts')) {
648 const forwarded = Array.from(this.forwarded.values());
649 const restorableTunnels: RestorableTunnel[] = forwarded.map(tunnel => {
650 return {
651 remoteHost: tunnel.remoteHost,
652 remotePort: tunnel.remotePort,
653 localPort: tunnel.localPort,
654 name: tunnel.name,
655 localAddress: tunnel.localAddress,
656 localUri: tunnel.localUri,
657 protocol: tunnel.protocol,
658 source: tunnel.source,
659 };
660 });
661 let valueToStore: string | undefined;
662 if (forwarded.length > 0) {
663 valueToStore = JSON.stringify(restorableTunnels);
664 }
665
666 const key = await this.getTunnelRestoreStorageKey();
667 const expirationKey = await this.getRestoreExpirationStorageKey();
668 if (!valueToStore && key && expirationKey) {
669 this.storageService.remove(key, StorageScope.PROFILE);
670 this.storageService.remove(expirationKey, StorageScope.PROFILE);
671 } else if ((valueToStore !== this.knownPortsRestoreValue) && key && expirationKey) {
672 this.storageService.store(key, valueToStore, StorageScope.PROFILE, StorageTarget.USER);
673 this.storageService.store(expirationKey, Date.now() + RESTORE_EXPIRATION_TIME, StorageScope.PROFILE, StorageTarget.USER);
674 }
675 this.knownPortsRestoreValue = valueToStore;
676 }
677 }
679 > private mismatchCooldown = new Date();
680 > private async showPortMismatchModalIfNeeded(tunnel: RemoteTunnel, expectedLocal: number, attributes: Attributes | undefined) {
681 if (!tunnel.tunnelLocalPort || !attributes?.requireLocalPort) {
682 return;
683 }
684 if (tunnel.tunnelLocalPort === expectedLocal) {
685 return;
686 }
687
688 const newCooldown = new Date();
689 if ((this.mismatchCooldown.getTime() + MISMATCH_LOCAL_PORT_COOLDOWN) > newCooldown.getTime()) {
690 return;
691 }
692 this.mismatchCooldown = newCooldown;
693 const mismatchString = nls.localize('remote.localPortMismatch.single', "Local port {0} could not be used for forwarding to remote port {1}.\n\nThis usually happens when there is already another process using local port {0}.\n\nPort number {2} has been used instead.",
694 expectedLocal, tunnel.tunnelRemotePort, tunnel.tunnelLocalPort);
695 return this.dialogService.info(mismatchString);
696 }
698 > async forward(tunnelProperties: TunnelProperties, attributes?: Attributes | null): Promise<RemoteTunnel | string | undefined> {
699 if (!this.restoreComplete && this.environmentService.remoteAuthority) {
700 await Event.toPromise(this.onRestoreComplete.event);
701 }
702 return this.doForward(tunnelProperties, attributes);
703 }
705 > private async doForward(tunnelProperties: TunnelProperties, attributes?: Attributes | null): Promise<RemoteTunnel | string | undefined> {
706 await this.extensionService.activateByEvent(ACTIVATION_EVENT);
707
708 const existingTunnel = mapHasAddressLocalhostOrAllInterfaces(this.forwarded, tunnelProperties.remote.host, tunnelProperties.remote.port);
709 attributes = attributes ??
710 ((attributes !== null)
711 ? (await this.getAttributes([tunnelProperties.remote]))?.get(tunnelProperties.remote.port)
712 : undefined);
713 const localPort = (tunnelProperties.local !== undefined) ? tunnelProperties.local : tunnelProperties.remote.port;
714 let noTunnelValue: string | undefined;
715 if (!existingTunnel) {
716 const authority = this.environmentService.remoteAuthority;
717 const addressProvider: IAddressProvider | undefined = authority ? {
718 getAddress: async () => { return (await this.remoteAuthorityResolverService.resolveAuthority(authority)).authority; }
719 } : undefined;
720
721 const key = makeAddress(tunnelProperties.remote.host, tunnelProperties.remote.port);
722 this.inProgress.set(key, true);
723 tunnelProperties = this.mergeCachedAndUnrestoredProperties(key, tunnelProperties);
724
725 const tunnel = await this.tunnelService.openTunnel(addressProvider, tunnelProperties.remote.host, tunnelProperties.remote.port, undefined, localPort, (!tunnelProperties.elevateIfNeeded) ? attributes?.elevateIfNeeded : tunnelProperties.elevateIfNeeded, tunnelProperties.privacy, attributes?.protocol);
726 if (typeof tunnel === 'string') {
727 // There was an error while creating the tunnel.
728 noTunnelValue = tunnel;
729 } else if (tunnel && tunnel.localAddress) {
730 const matchingCandidate = mapHasAddressLocalhostOrAllInterfaces<CandidatePort>(this._candidates ?? new Map(), tunnelProperties.remote.host, tunnelProperties.remote.port);
731 const protocol = (tunnel.protocol ?
732 ((tunnel.protocol === TunnelProtocol.Https) ? TunnelProtocol.Https : TunnelProtocol.Http)
733 : (attributes?.protocol ?? TunnelProtocol.Http));
734 const newForward: Tunnel = {
735 remoteHost: tunnel.tunnelRemoteHost,
736 remotePort: tunnel.tunnelRemotePort,
737 localPort: tunnel.tunnelLocalPort,
738 name: attributes?.label ?? tunnelProperties.name,
739 closeable: true,
740 localAddress: tunnel.localAddress,
741 protocol,
742 localUri: await this.makeLocalUri(tunnel.localAddress, attributes),
743 runningProcess: matchingCandidate?.detail,
744 hasRunningProcess: !!matchingCandidate,
745 pid: matchingCandidate?.pid,
746 source: tunnelProperties.source ?? UserTunnelSource,
747 privacy: tunnel.privacy,
748 };
749 this.forwarded.set(key, newForward);
750 this.remoteTunnels.set(key, tunnel);
751 this.inProgress.delete(key);
752 await this.storeForwarded();
753 await this.showPortMismatchModalIfNeeded(tunnel, localPort, attributes);
754 this._onForwardPort.fire(newForward);
755 return tunnel;
756 }
757 this.inProgress.delete(key);
758 } else {
759 return this.mergeAttributesIntoExistingTunnel(existingTunnel, tunnelProperties, attributes);
760 }
761
762 return noTunnelValue;
763 }
765 > private mergeCachedAndUnrestoredProperties(key: string, tunnelProperties: TunnelProperties): TunnelProperties {
766 const map = this.unrestoredExtensionTunnels.has(key) ? this.unrestoredExtensionTunnels : (this.sessionCachedProperties.has(key) ? this.sessionCachedProperties : undefined);
767 if (map) {
768 const updateProps = map.get(key)!;
769 map.delete(key);
770 if (updateProps) {
771 tunnelProperties.name = updateProps.name ?? tunnelProperties.name;
772 tunnelProperties.local = (('local' in updateProps) ? updateProps.local : (('localPort' in updateProps) ? updateProps.localPort : undefined)) ?? tunnelProperties.local;
773 tunnelProperties.privacy = tunnelProperties.privacy;
774 }
775 }
776 return tunnelProperties;
777 }
779 > private async mergeAttributesIntoExistingTunnel(existingTunnel: Tunnel, tunnelProperties: TunnelProperties, attributes: Attributes | undefined) {
780 const newName = attributes?.label ?? tunnelProperties.name;
781 enum MergedAttributeAction {
782 None = 0,
783 Fire = 1,
784 Reopen = 2
785 }
786 let mergedAction = MergedAttributeAction.None;
787 if (newName !== existingTunnel.name) {
788 existingTunnel.name = newName;
789 mergedAction = MergedAttributeAction.Fire;
790 }
791 // Source of existing tunnel wins so that original source is maintained
792 if ((attributes?.protocol || (existingTunnel.protocol !== TunnelProtocol.Http)) && (attributes?.protocol !== existingTunnel.protocol)) {
793 tunnelProperties.source = existingTunnel.source;
794 mergedAction = MergedAttributeAction.Reopen;
795 }
796 // New privacy value wins
797 if (tunnelProperties.privacy && (existingTunnel.privacy !== tunnelProperties.privacy)) {
798 mergedAction = MergedAttributeAction.Reopen;
799 }
800 switch (mergedAction) {
801 case MergedAttributeAction.Fire: {
802 this._onForwardPort.fire();
803 break;
804 }
805 case MergedAttributeAction.Reopen: {
806 await this.close(existingTunnel.remoteHost, existingTunnel.remotePort, TunnelCloseReason.User);
807 await this.doForward(tunnelProperties, attributes);
808 }
809 }
810
811 return mapHasAddressLocalhostOrAllInterfaces(this.remoteTunnels, tunnelProperties.remote.host, tunnelProperties.remote.port);
812 }
814 > async name(host: string, port: number, name: string) {
815 const existingForwarded = mapHasAddressLocalhostOrAllInterfaces(this.forwarded, host, port);
816 const key = makeAddress(host, port);
817 if (existingForwarded) {
818 existingForwarded.name = name;
819 await this.storeForwarded();
820 this._onPortName.fire({ host, port });
821 return;
822 } else if (this.detected.has(key)) {
823 this.detected.get(key)!.name = name;
824 this._onPortName.fire({ host, port });
825 }
826 }
828 > async close(host: string, port: number, reason: TunnelCloseReason): Promise<void> {
829 const key = makeAddress(host, port);
830 const oldTunnel = this.forwarded.get(key)!;
831 if ((reason === TunnelCloseReason.AutoForwardEnd) && oldTunnel && (oldTunnel.source.source === TunnelSource.Auto)) {
832 this.sessionCachedProperties.set(key, {
833 local: oldTunnel.localPort,
834 name: oldTunnel.name,
835 privacy: oldTunnel.privacy,
836 });
837 }
838 await this.tunnelService.closeTunnel(host, port);
839 return this.onTunnelClosed({ host, port }, reason);
840 }
842 > address(host: string, port: number): string | undefined {
843 const key = makeAddress(host, port);
844 return (this.forwarded.get(key) || this.detected.get(key))?.localAddress;
845 }
847 > public get environmentTunnelsSet(): boolean {
848 return this._environmentTunnelsSet;
849 }
851 > addEnvironmentTunnels(tunnels: TunnelDescription[] | undefined): void {
852 if (tunnels) {
853 for (const tunnel of tunnels) {
854 const matchingCandidate = mapHasAddressLocalhostOrAllInterfaces(this._candidates ?? new Map(), tunnel.remoteAddress.host, tunnel.remoteAddress.port);
855 const localAddress = typeof tunnel.localAddress === 'string' ? tunnel.localAddress : makeAddress(tunnel.localAddress.host, tunnel.localAddress.port);
856 this.detected.set(makeAddress(tunnel.remoteAddress.host, tunnel.remoteAddress.port), {
857 remoteHost: tunnel.remoteAddress.host,
858 remotePort: tunnel.remoteAddress.port,
859 localAddress: localAddress,
860 protocol: TunnelProtocol.Http,
861 localUri: this.makeLocalUri(localAddress),
862 closeable: false,
863 runningProcess: matchingCandidate?.detail,
864 hasRunningProcess: !!matchingCandidate,
865 pid: matchingCandidate?.pid,
866 privacy: TunnelPrivacyId.ConstantPrivate,
867 source: {
868 source: TunnelSource.Extension,
869 description: nls.localize('tunnel.staticallyForwarded', "Statically Forwarded")
870 }
871 });
872 this.tunnelService.setEnvironmentTunnel(tunnel.remoteAddress.host, tunnel.remoteAddress.port, localAddress, TunnelPrivacyId.ConstantPrivate, TunnelProtocol.Http);
873 }
874 }
875 this._environmentTunnelsSet = true;
876 this._onEnvironmentTunnelsSet.fire();
877 this._onForwardPort.fire();
878 }
880 > setCandidateFilter(filter: ((candidates: CandidatePort[]) => Promise<CandidatePort[]>) | undefined): void {
881 this._candidateFilter = filter;
882 }
884 > async setCandidates(candidates: CandidatePort[]) {
885 let processedCandidates = candidates;
886 if (this._candidateFilter) {
887 // When an extension provides a filter, we do the filtering on the extension host before the candidates are set here.
888 // However, when the filter doesn't come from an extension we filter here.
889 processedCandidates = await this._candidateFilter(candidates);
890 }
891 const removedCandidates = this.updateInResponseToCandidates(processedCandidates);
892 this.logService.trace(`ForwardedPorts: (TunnelModel) removed candidates ${Array.from(removedCandidates.values()).map(candidate => candidate.port).join(', ')}`);
893 this._onCandidatesChanged.fire(removedCandidates);
894 }
896 > // Returns removed candidates
897 > private updateInResponseToCandidates(candidates: CandidatePort[]): Map<string, { host: string; port: number }> {
898 const removedCandidates = this._candidates ?? new Map();
899 const candidatesMap = new Map();
900 this._candidates = candidatesMap;
901 candidates.forEach(value => {
902 const addressKey = makeAddress(value.host, value.port);
903 candidatesMap.set(addressKey, {
904 host: value.host,
905 port: value.port,
906 detail: value.detail,
907 pid: value.pid
908 });
909 removedCandidates.delete(addressKey);
910 const forwardedValue = mapHasAddressLocalhostOrAllInterfaces(this.forwarded, value.host, value.port);
911 if (forwardedValue) {
912 forwardedValue.runningProcess = value.detail;
913 forwardedValue.hasRunningProcess = true;
914 forwardedValue.pid = value.pid;
915 }
916 });
917 removedCandidates.forEach((_value, key) => {
918 const parsedAddress = parseAddress(key);
919 if (!parsedAddress) {
920 return;
921 }
922 const forwardedValue = mapHasAddressLocalhostOrAllInterfaces(this.forwarded, parsedAddress.host, parsedAddress.port);
923 if (forwardedValue) {
924 forwardedValue.runningProcess = undefined;
925 forwardedValue.hasRunningProcess = false;
926 forwardedValue.pid = undefined;
927 }
928 const detectedValue = mapHasAddressLocalhostOrAllInterfaces(this.detected, parsedAddress.host, parsedAddress.port);
929 if (detectedValue) {
930 detectedValue.runningProcess = undefined;
931 detectedValue.hasRunningProcess = false;
932 detectedValue.pid = undefined;
933 }
934 });
935 return removedCandidates;
936 }
938 > get candidates(): CandidatePort[] {
939 return this._candidates ? Array.from(this._candidates.values()) : [];
940 }
942 > get candidatesOrUndefined(): CandidatePort[] | undefined {
943 return this._candidates ? this.candidates : undefined;
944 }
946 > private async updateAttributes() {
947 // If the label changes in the attributes, we should update it.
948 const tunnels = Array.from(this.forwarded.values());
949 const allAttributes = await this.getAttributes(tunnels.map(tunnel => {
950 return { port: tunnel.remotePort, host: tunnel.remoteHost };
951 }), false);
952 if (!allAttributes) {
953 return;
954 }
955 for (const forwarded of tunnels) {
956 const attributes = allAttributes.get(forwarded.remotePort);
957 if ((attributes?.protocol || (forwarded.protocol !== TunnelProtocol.Http)) && (attributes?.protocol !== forwarded.protocol)) {
958 await this.doForward({
959 remote: { host: forwarded.remoteHost, port: forwarded.remotePort },
960 local: forwarded.localPort,
961 name: forwarded.name,
962 source: forwarded.source
963 }, attributes);
964 }
965
966 if (!attributes) {
967 continue;
968 }
969 if (attributes.label && attributes.label !== forwarded.name) {
970 await this.name(forwarded.remoteHost, forwarded.remotePort, attributes.label);
971 }
972
973 }
974 }
976 > async getAttributes(forwardedPorts: { host: string; port: number }[], checkProviders: boolean = true): Promise<Map<number, Attributes> | undefined> {
977 const matchingCandidates: Map<number, CandidatePort> = new Map();
978 const pidToPortsMapping: Map<number | undefined, number[]> = new Map();
979 forwardedPorts.forEach(forwardedPort => {
980 const matchingCandidate = mapHasAddressLocalhostOrAllInterfaces<CandidatePort>(this._candidates ?? new Map(), LOCALHOST_ADDRESSES[0], forwardedPort.port) ?? forwardedPort;
981 if (matchingCandidate) {
982 matchingCandidates.set(forwardedPort.port, matchingCandidate);
983 const pid = isCandidatePort(matchingCandidate) ? matchingCandidate.pid : undefined;
984 if (!pidToPortsMapping.has(pid)) {
985 pidToPortsMapping.set(pid, []);
986 }
987 pidToPortsMapping.get(pid)?.push(forwardedPort.port);
988 }
989 });
990
991 const configAttributes: Map<number, Attributes> = new Map();
992 forwardedPorts.forEach(forwardedPort => {
993 const attributes = this.configPortsAttributes.getAttributes(forwardedPort.port, forwardedPort.host, matchingCandidates.get(forwardedPort.port)?.detail);
994 if (attributes) {
995 configAttributes.set(forwardedPort.port, attributes);
996 }
997 });
998 if ((this.portAttributesProviders.length === 0) || !checkProviders) {
999 return (configAttributes.size > 0) ? configAttributes : undefined;
1000 }
1001
1002 // Group calls to provide attributes by pid.
1003 const allProviderResults = await Promise.all(this.portAttributesProviders.flatMap(provider => {
1004 return Array.from(pidToPortsMapping.entries()).map(entry => {
1005 const portGroup = entry[1];
1006 const matchingCandidate = matchingCandidates.get(portGroup[0]);
1007 return provider.providePortAttributes(portGroup,
1008 matchingCandidate?.pid, matchingCandidate?.detail, CancellationToken.None);
1009 });
1010 }));
1011 const providedAttributes: Map<number, ProvidedPortAttributes> = new Map();
1012 allProviderResults.forEach(attributes => attributes.forEach(attribute => {
1013 if (attribute) {
1014 providedAttributes.set(attribute.port, attribute);
1015 }
1016 }));
1017
1018 if (!configAttributes && !providedAttributes) {
1019 return undefined;
1020 }
1021
1022 // Merge. The config wins.
1023 const mergedAttributes: Map<number, Attributes> = new Map();
1024 forwardedPorts.forEach(forwardedPorts => {
1025 const config = configAttributes.get(forwardedPorts.port);
1026 const provider = providedAttributes.get(forwardedPorts.port);
1027 mergedAttributes.set(forwardedPorts.port, {
1028 elevateIfNeeded: config?.elevateIfNeeded,
1029 label: config?.label,
1030 onAutoForward: config?.onAutoForward ?? PortsAttributes.providedActionToAction(provider?.autoForwardAction),
1031 requireLocalPort: config?.requireLocalPort,
1032 protocol: config?.protocol
1033 });
1034 });
1035
1036 return mergedAttributes;
1037 }
1039 > addAttributesProvider(provider: PortAttributesProvider) {
1040 this.portAttributesProviders.push(provider);
1041 }