Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/sessions/contrib/remoteAgentHost/electron-browser/tunnelAgentHostServiceImpl.ts
13401 views
1
/*---------------------------------------------------------------------------------------------
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 { Emitter, Event } from '../../../../base/common/event.js';
7
import { Disposable } from '../../../../base/common/lifecycle.js';
8
import { ProxyChannel } from '../../../../base/parts/ipc/common/ipc.js';
9
import { IAuthenticationService } from '../../../../workbench/services/authentication/common/authentication.js';
10
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
11
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
12
import { ISharedProcessService } from '../../../../platform/ipc/electron-browser/services.js';
13
import { ILogService } from '../../../../platform/log/common/log.js';
14
import { IProductService } from '../../../../platform/product/common/productService.js';
15
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
16
import { IRemoteAgentHostService, RemoteAgentHostEntryType, RemoteAgentHostsEnabledSettingId } from '../../../../platform/agentHost/common/remoteAgentHostService.js';
17
import {
18
ITunnelAgentHostService,
19
TUNNEL_AGENT_HOST_CHANNEL,
20
TunnelAgentHostsSettingId,
21
type ICachedTunnel,
22
type ITunnelAgentHostMainService,
23
type ITunnelInfo,
24
} from '../../../../platform/agentHost/common/tunnelAgentHost.js';
25
import { RemoteAgentHostProtocolClient } from '../../../../platform/agentHost/browser/remoteAgentHostProtocolClient.js';
26
import { TunnelRelayTransport } from '../../../../platform/agentHost/electron-browser/tunnelRelayTransport.js';
27
28
const LOG_PREFIX = '[TunnelAgentHost]';
29
30
/** Storage key for recently used tunnel cache. */
31
const CACHED_TUNNELS_KEY = 'tunnelAgentHost.recentTunnels';
32
33
/**
34
* Renderer-side implementation of {@link ITunnelAgentHostService} that
35
* delegates tunnel SDK operations to the shared process via IPC, then
36
* registers connections with the renderer-local {@link IRemoteAgentHostService}.
37
*/
38
export class TunnelAgentHostService extends Disposable implements ITunnelAgentHostService {
39
declare readonly _serviceBrand: undefined;
40
41
private readonly _mainService: ITunnelAgentHostMainService;
42
43
private readonly _onDidChangeTunnels = this._register(new Emitter<void>());
44
readonly onDidChangeTunnels: Event<void> = this._onDidChangeTunnels.event;
45
46
/** Tracks which auth provider was last used successfully. */
47
private _lastAuthProvider: 'github' | 'microsoft' | undefined;
48
49
constructor(
50
@ISharedProcessService sharedProcessService: ISharedProcessService,
51
@IRemoteAgentHostService private readonly _remoteAgentHostService: IRemoteAgentHostService,
52
@ILogService private readonly _logService: ILogService,
53
@IInstantiationService private readonly _instantiationService: IInstantiationService,
54
@IConfigurationService private readonly _configurationService: IConfigurationService,
55
@IAuthenticationService private readonly _authenticationService: IAuthenticationService,
56
@IProductService private readonly _productService: IProductService,
57
@IStorageService private readonly _storageService: IStorageService,
58
) {
59
super();
60
61
this._mainService = ProxyChannel.toService<ITunnelAgentHostMainService>(
62
sharedProcessService.getChannel(TUNNEL_AGENT_HOST_CHANNEL),
63
);
64
}
65
66
async listTunnels(options?: { silent?: boolean }): Promise<ITunnelInfo[]> {
67
if (!this._configurationService.getValue<boolean>(RemoteAgentHostsEnabledSettingId)) {
68
return [];
69
}
70
71
const silent = options?.silent ?? false;
72
const auth = await this._getToken(silent);
73
if (!auth) {
74
if (silent) {
75
this._logService.debug(`${LOG_PREFIX} No cached token available for silent tunnel enumeration`);
76
} else {
77
this._logService.warn(`${LOG_PREFIX} No auth token available for tunnel enumeration`);
78
}
79
return [];
80
}
81
82
const additionalNames = this._configurationService.getValue<string[]>(TunnelAgentHostsSettingId) ?? [];
83
return this._mainService.listTunnels(auth.token, auth.provider, additionalNames.length > 0 ? additionalNames : undefined);
84
}
85
86
async connect(tunnel: ITunnelInfo, authProvider?: 'github' | 'microsoft'): Promise<void> {
87
const auth = authProvider
88
? await this._getTokenForProvider(authProvider, false)
89
: await this._getToken(false);
90
if (!auth) {
91
throw new Error('No authentication available');
92
}
93
94
this._logService.info(`${LOG_PREFIX} Connecting to tunnel '${tunnel.name}' (${tunnel.tunnelId})`);
95
const result = await this._mainService.connect(auth.token, auth.provider, tunnel.tunnelId, tunnel.clusterId);
96
this._logService.info(`${LOG_PREFIX} Tunnel relay connected, connectionId=${result.connectionId}`);
97
98
// Create relay transport + protocol client, then register with RemoteAgentHostService
99
try {
100
const transport = new TunnelRelayTransport(result.connectionId, this._mainService);
101
const protocolClient = this._instantiationService.createInstance(
102
RemoteAgentHostProtocolClient, result.address, transport,
103
);
104
105
await protocolClient.connect();
106
this._logService.info(`${LOG_PREFIX} Protocol handshake completed with ${result.address}`);
107
108
await this._remoteAgentHostService.addManagedConnection({
109
name: result.name,
110
connectionToken: result.connectionToken,
111
connection: {
112
type: RemoteAgentHostEntryType.Tunnel,
113
tunnelId: tunnel.tunnelId,
114
clusterId: tunnel.clusterId,
115
label: tunnel.name,
116
authProvider: auth.provider,
117
},
118
}, protocolClient);
119
120
this._onDidChangeTunnels.fire();
121
} catch (err) {
122
this._logService.error(`${LOG_PREFIX} Connection setup failed`, err);
123
this._mainService.disconnect(result.connectionId).catch(() => { /* best effort */ });
124
throw err;
125
}
126
}
127
128
async disconnect(address: string): Promise<void> {
129
await this._remoteAgentHostService.removeRemoteAgentHost(address);
130
this._onDidChangeTunnels.fire();
131
}
132
133
/**
134
* Get an auth token, trying cached sessions first (silent),
135
* then prompting interactively if `silent` is false.
136
*/
137
private async _getToken(silent: boolean): Promise<{ token: string; provider: 'github' | 'microsoft' } | undefined> {
138
// Try the last known provider first
139
if (this._lastAuthProvider) {
140
const result = await this._getTokenForProvider(this._lastAuthProvider, silent);
141
if (result) {
142
return result;
143
}
144
}
145
146
// Try both providers silently
147
for (const provider of ['github', 'microsoft'] as const) {
148
if (provider === this._lastAuthProvider) {
149
continue; // Already tried above
150
}
151
const result = await this._getTokenForProvider(provider, true);
152
if (result) {
153
return result;
154
}
155
}
156
157
// If not silent, we would need the caller to prompt for provider selection.
158
// Return undefined — the caller (promptToConnectViaTunnel) handles the interactive flow.
159
return undefined;
160
}
161
162
/**
163
* Get a token for a specific auth provider.
164
* @param provider The auth provider to use.
165
* @param silent If true, only try cached sessions. If false, prompt the user.
166
*/
167
private _getScopesForProvider(provider: 'github' | 'microsoft'): string[] {
168
const config = this._productService.tunnelApplicationConfig?.authenticationProviders;
169
return config?.[provider]?.scopes ?? [];
170
}
171
172
private async _getTokenForProvider(
173
provider: 'github' | 'microsoft',
174
silent: boolean,
175
): Promise<{ token: string; provider: 'github' | 'microsoft' } | undefined> {
176
const providerId = provider;
177
const scopes = this._getScopesForProvider(provider);
178
if (scopes.length === 0) {
179
return undefined;
180
}
181
182
try {
183
// Try exact scope match first
184
let sessions = await this._authenticationService.getSessions(providerId, scopes, {}, true);
185
186
// Fall back: find any session whose scopes are a superset
187
if (sessions.length === 0) {
188
const allSessions = await this._authenticationService.getSessions(providerId, undefined, {}, true);
189
const requestedSet = new Set(scopes);
190
let bestSession: typeof allSessions[number] | undefined;
191
let bestExtra = Infinity;
192
for (const session of allSessions) {
193
const sessionScopes = new Set(session.scopes);
194
let isSuperset = true;
195
for (const scope of requestedSet) {
196
if (!sessionScopes.has(scope)) {
197
isSuperset = false;
198
break;
199
}
200
}
201
if (isSuperset) {
202
const extra = sessionScopes.size - requestedSet.size;
203
if (extra < bestExtra) {
204
bestExtra = extra;
205
bestSession = session;
206
}
207
}
208
}
209
if (bestSession) {
210
sessions = [bestSession];
211
}
212
}
213
214
// Interactive fallback: create a new session
215
if (sessions.length === 0 && !silent) {
216
const session = await this._authenticationService.createSession(providerId, scopes, { activateImmediate: true });
217
sessions = [session];
218
}
219
220
if (sessions.length > 0) {
221
const token = sessions[0].accessToken;
222
if (token) {
223
this._lastAuthProvider = provider;
224
return { token, provider };
225
}
226
}
227
} catch (err) {
228
this._logService.debug(`${LOG_PREFIX} Failed to get ${provider} token: ${err}`);
229
}
230
return undefined;
231
}
232
233
async getAuthProvider(options?: { silent?: boolean }): Promise<'github' | 'microsoft' | undefined> {
234
const result = await this._getToken(options?.silent ?? true);
235
return result?.provider;
236
}
237
238
getCachedTunnels(): ICachedTunnel[] {
239
const raw = this._storageService.get(CACHED_TUNNELS_KEY, StorageScope.APPLICATION);
240
if (!raw) {
241
return [];
242
}
243
try {
244
return JSON.parse(raw);
245
} catch {
246
return [];
247
}
248
}
249
250
cacheTunnel(tunnel: ITunnelInfo, authProvider?: 'github' | 'microsoft'): void {
251
const cached = this.getCachedTunnels();
252
const filtered = cached.filter(t => t.tunnelId !== tunnel.tunnelId);
253
filtered.unshift({
254
tunnelId: tunnel.tunnelId,
255
clusterId: tunnel.clusterId,
256
name: tunnel.name,
257
authProvider,
258
});
259
this._storeCachedTunnels(filtered.slice(0, 20));
260
this._onDidChangeTunnels.fire();
261
}
262
263
removeCachedTunnel(tunnelId: string): void {
264
const cached = this.getCachedTunnels();
265
this._storeCachedTunnels(cached.filter(t => t.tunnelId !== tunnelId));
266
this._onDidChangeTunnels.fire();
267
}
268
269
private _storeCachedTunnels(tunnels: ICachedTunnel[]): void {
270
if (tunnels.length === 0) {
271
this._storageService.remove(CACHED_TUNNELS_KEY, StorageScope.APPLICATION);
272
} else {
273
this._storageService.store(CACHED_TUNNELS_KEY, JSON.stringify(tunnels), StorageScope.APPLICATION, StorageTarget.USER);
274
}
275
}
276
}
277
278