Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/sessions/contrib/tunnelHost/electron-browser/tunnelHostService.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 { joinPath } from '../../../../base/common/resources.js';
7
import { Emitter, Event } from '../../../../base/common/event.js';
8
import { Disposable } from '../../../../base/common/lifecycle.js';
9
import { ProxyChannel } from '../../../../base/parts/ipc/common/ipc.js';
10
import { IAuthenticationService } from '../../../../workbench/services/authentication/common/authentication.js';
11
import { IEnvironmentService } from '../../../../platform/environment/common/environment.js';
12
import { ISharedProcessService } from '../../../../platform/ipc/electron-browser/services.js';
13
import { ILogger, ILoggerService } from '../../../../platform/log/common/log.js';
14
import { IProductService } from '../../../../platform/product/common/productService.js';
15
import { localize } from '../../../../nls.js';
16
import {
17
ITunnelAgentHostHostingService,
18
TUNNEL_HOST_CHANNEL,
19
TUNNEL_HOST_LOG_ID,
20
type ITunnelHostInfo,
21
type TunnelHostStatus,
22
} from '../../../../platform/agentHost/common/tunnelAgentHost.js';
23
import { IAgentHostService } from '../../../../platform/agentHost/common/agentService.js';
24
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
25
import { ITunnelHostService } from '../common/tunnelHost.js';
26
27
export const CONFIGURATION_KEY_MICROSOFT_AUTH = 'remote.tunnels.access.enableMicrosoftAuth';
28
export const SHOW_TUNNEL_HOST_OUTPUT_ID = 'sessions.tunnelHost.showOutput';
29
30
export class TunnelHostService extends Disposable implements ITunnelHostService {
31
declare readonly _serviceBrand: undefined;
32
33
private readonly _mainService: ITunnelAgentHostHostingService;
34
private readonly _logger: ILogger;
35
36
private readonly _onDidChangeStatus = this._register(new Emitter<void>());
37
readonly onDidChangeStatus: Event<void> = this._onDidChangeStatus.event;
38
39
private _isSharing = false;
40
private _isConnecting = false;
41
private _sharingInfo: ITunnelHostInfo | undefined;
42
43
/** Tracks which auth provider was last used successfully. */
44
private _lastAuthProvider: 'github' | 'microsoft' | undefined;
45
46
constructor(
47
@ISharedProcessService sharedProcessService: ISharedProcessService,
48
@IAuthenticationService private readonly _authenticationService: IAuthenticationService,
49
@IProductService private readonly _productService: IProductService,
50
@IAgentHostService private readonly _agentHostService: IAgentHostService,
51
@IConfigurationService private readonly _configurationService: IConfigurationService,
52
@ILoggerService loggerService: ILoggerService,
53
@IEnvironmentService environmentService: IEnvironmentService,
54
) {
55
super();
56
57
// Register a renderer-side logger so that the output channel
58
// created in the shared process is visible in the workbench UI
59
this._logger = this._register(loggerService.createLogger(
60
joinPath(environmentService.logsHome, `${TUNNEL_HOST_LOG_ID}.log`),
61
{ id: TUNNEL_HOST_LOG_ID, name: localize('tunnelHost.outputChannel', "Remote Connections") },
62
));
63
64
this._mainService = ProxyChannel.toService<ITunnelAgentHostHostingService>(
65
sharedProcessService.getChannel(TUNNEL_HOST_CHANNEL),
66
);
67
68
// Listen for status changes from the shared process
69
this._register(this._mainService.onDidChangeStatus((status: TunnelHostStatus) => {
70
this._isSharing = status.active;
71
this._sharingInfo = status.active ? status.info : undefined;
72
this._onDidChangeStatus.fire();
73
}));
74
75
// Restore status on construction
76
this._mainService.getStatus().then(status => {
77
this._isSharing = status.active;
78
this._sharingInfo = status.active ? status.info : undefined;
79
if (status.active) {
80
this._onDidChangeStatus.fire();
81
}
82
});
83
}
84
85
get isSharing(): boolean {
86
return this._isSharing;
87
}
88
89
get isConnecting(): boolean {
90
return this._isConnecting;
91
}
92
93
get sharingInfo(): ITunnelHostInfo | undefined {
94
return this._sharingInfo;
95
}
96
97
async startSharing(): Promise<void> {
98
this._isConnecting = true;
99
this._onDidChangeStatus.fire();
100
101
try {
102
const auth = await this._getToken(false);
103
if (!auth) {
104
this._logger.warn(`No auth token available for tunnel hosting`);
105
throw new Error(localize('tunnelHost.noAuth', "No authentication token available. Please sign in and try again."));
106
}
107
108
this._logger.info(`Starting tunnel hosting...`);
109
110
const socketInfo = await this._agentHostService.startWebSocketServer();
111
const info = await this._mainService.startHosting(auth.token, auth.provider, socketInfo);
112
this._isSharing = true;
113
this._sharingInfo = info;
114
} finally {
115
this._isConnecting = false;
116
this._onDidChangeStatus.fire();
117
}
118
}
119
120
async stopSharing(): Promise<void> {
121
this._logger.info(`Stopping tunnel hosting...`);
122
await this._mainService.stopHosting();
123
this._isSharing = false;
124
this._sharingInfo = undefined;
125
this._onDidChangeStatus.fire();
126
}
127
128
// ---- Auth helpers (reused from TunnelAgentHostService) -------------------
129
130
private _getEnabledProviders(): readonly ('github' | 'microsoft')[] {
131
const microsoftEnabled = this._configurationService.getValue<boolean>(CONFIGURATION_KEY_MICROSOFT_AUTH);
132
return microsoftEnabled ? ['microsoft', 'github'] : ['github'];
133
}
134
135
private async _getToken(silent: boolean): Promise<{ token: string; provider: 'github' | 'microsoft' } | undefined> {
136
const enabledProviders = this._getEnabledProviders();
137
138
// Try the last known provider first
139
if (this._lastAuthProvider && enabledProviders.includes(this._lastAuthProvider)) {
140
const result = await this._getTokenForProvider(this._lastAuthProvider, silent);
141
if (result) {
142
return result;
143
}
144
}
145
146
// Try enabled providers silently
147
for (const provider of enabledProviders) {
148
if (provider === this._lastAuthProvider) {
149
continue;
150
}
151
const result = await this._getTokenForProvider(provider, true);
152
if (result) {
153
return result;
154
}
155
}
156
157
// If not silent, try interactively with each enabled provider
158
if (!silent) {
159
for (const provider of enabledProviders) {
160
const result = await this._getTokenForProvider(provider, false);
161
if (result) {
162
return result;
163
}
164
}
165
}
166
167
return undefined;
168
}
169
170
private _getScopesForProvider(provider: 'github' | 'microsoft'): string[] {
171
const config = this._productService.tunnelApplicationConfig?.authenticationProviders;
172
return config?.[provider]?.scopes ?? [];
173
}
174
175
private async _getTokenForProvider(
176
provider: 'github' | 'microsoft',
177
silent: boolean,
178
): Promise<{ token: string; provider: 'github' | 'microsoft' } | undefined> {
179
const scopes = this._getScopesForProvider(provider);
180
if (scopes.length === 0) {
181
return undefined;
182
}
183
184
try {
185
// Try exact scope match first
186
let sessions = await this._authenticationService.getSessions(provider, scopes, {}, true);
187
188
// Fall back: find any session whose scopes are a superset
189
if (sessions.length === 0) {
190
const allSessions = await this._authenticationService.getSessions(provider, undefined, {}, true);
191
const requestedSet = new Set(scopes);
192
let bestSession: typeof allSessions[number] | undefined;
193
let bestExtra = Infinity;
194
for (const session of allSessions) {
195
const sessionScopes = new Set(session.scopes);
196
let isSuperset = true;
197
for (const scope of requestedSet) {
198
if (!sessionScopes.has(scope)) {
199
isSuperset = false;
200
break;
201
}
202
}
203
if (isSuperset) {
204
const extra = sessionScopes.size - requestedSet.size;
205
if (extra < bestExtra) {
206
bestExtra = extra;
207
bestSession = session;
208
}
209
}
210
}
211
if (bestSession) {
212
sessions = [bestSession];
213
}
214
}
215
216
// Interactive fallback: create a new session
217
if (sessions.length === 0 && !silent) {
218
const session = await this._authenticationService.createSession(provider, scopes, { activateImmediate: true });
219
sessions = [session];
220
}
221
222
if (sessions.length > 0) {
223
const token = sessions[0].accessToken;
224
if (token) {
225
this._lastAuthProvider = provider;
226
return { token, provider };
227
}
228
}
229
} catch (err) {
230
this._logger.debug(`Failed to get ${provider} token: ${err}`);
231
}
232
return undefined;
233
}
234
235
override dispose(): void {
236
// Best-effort cleanup — stop hosting when the window closes
237
if (this._isSharing) {
238
this.stopSharing().catch(() => { /* ignore */ });
239
}
240
super.dispose();
241
}
242
}
243
244