Path: blob/main/src/vs/sessions/contrib/remoteAgentHost/electron-browser/tunnelAgentHostServiceImpl.ts
13401 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Emitter, Event } from '../../../../base/common/event.js';6import { Disposable } from '../../../../base/common/lifecycle.js';7import { ProxyChannel } from '../../../../base/parts/ipc/common/ipc.js';8import { IAuthenticationService } from '../../../../workbench/services/authentication/common/authentication.js';9import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';10import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';11import { ISharedProcessService } from '../../../../platform/ipc/electron-browser/services.js';12import { ILogService } from '../../../../platform/log/common/log.js';13import { IProductService } from '../../../../platform/product/common/productService.js';14import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';15import { IRemoteAgentHostService, RemoteAgentHostEntryType, RemoteAgentHostsEnabledSettingId } from '../../../../platform/agentHost/common/remoteAgentHostService.js';16import {17ITunnelAgentHostService,18TUNNEL_AGENT_HOST_CHANNEL,19TunnelAgentHostsSettingId,20type ICachedTunnel,21type ITunnelAgentHostMainService,22type ITunnelInfo,23} from '../../../../platform/agentHost/common/tunnelAgentHost.js';24import { RemoteAgentHostProtocolClient } from '../../../../platform/agentHost/browser/remoteAgentHostProtocolClient.js';25import { TunnelRelayTransport } from '../../../../platform/agentHost/electron-browser/tunnelRelayTransport.js';2627const LOG_PREFIX = '[TunnelAgentHost]';2829/** Storage key for recently used tunnel cache. */30const CACHED_TUNNELS_KEY = 'tunnelAgentHost.recentTunnels';3132/**33* Renderer-side implementation of {@link ITunnelAgentHostService} that34* delegates tunnel SDK operations to the shared process via IPC, then35* registers connections with the renderer-local {@link IRemoteAgentHostService}.36*/37export class TunnelAgentHostService extends Disposable implements ITunnelAgentHostService {38declare readonly _serviceBrand: undefined;3940private readonly _mainService: ITunnelAgentHostMainService;4142private readonly _onDidChangeTunnels = this._register(new Emitter<void>());43readonly onDidChangeTunnels: Event<void> = this._onDidChangeTunnels.event;4445/** Tracks which auth provider was last used successfully. */46private _lastAuthProvider: 'github' | 'microsoft' | undefined;4748constructor(49@ISharedProcessService sharedProcessService: ISharedProcessService,50@IRemoteAgentHostService private readonly _remoteAgentHostService: IRemoteAgentHostService,51@ILogService private readonly _logService: ILogService,52@IInstantiationService private readonly _instantiationService: IInstantiationService,53@IConfigurationService private readonly _configurationService: IConfigurationService,54@IAuthenticationService private readonly _authenticationService: IAuthenticationService,55@IProductService private readonly _productService: IProductService,56@IStorageService private readonly _storageService: IStorageService,57) {58super();5960this._mainService = ProxyChannel.toService<ITunnelAgentHostMainService>(61sharedProcessService.getChannel(TUNNEL_AGENT_HOST_CHANNEL),62);63}6465async listTunnels(options?: { silent?: boolean }): Promise<ITunnelInfo[]> {66if (!this._configurationService.getValue<boolean>(RemoteAgentHostsEnabledSettingId)) {67return [];68}6970const silent = options?.silent ?? false;71const auth = await this._getToken(silent);72if (!auth) {73if (silent) {74this._logService.debug(`${LOG_PREFIX} No cached token available for silent tunnel enumeration`);75} else {76this._logService.warn(`${LOG_PREFIX} No auth token available for tunnel enumeration`);77}78return [];79}8081const additionalNames = this._configurationService.getValue<string[]>(TunnelAgentHostsSettingId) ?? [];82return this._mainService.listTunnels(auth.token, auth.provider, additionalNames.length > 0 ? additionalNames : undefined);83}8485async connect(tunnel: ITunnelInfo, authProvider?: 'github' | 'microsoft'): Promise<void> {86const auth = authProvider87? await this._getTokenForProvider(authProvider, false)88: await this._getToken(false);89if (!auth) {90throw new Error('No authentication available');91}9293this._logService.info(`${LOG_PREFIX} Connecting to tunnel '${tunnel.name}' (${tunnel.tunnelId})`);94const result = await this._mainService.connect(auth.token, auth.provider, tunnel.tunnelId, tunnel.clusterId);95this._logService.info(`${LOG_PREFIX} Tunnel relay connected, connectionId=${result.connectionId}`);9697// Create relay transport + protocol client, then register with RemoteAgentHostService98try {99const transport = new TunnelRelayTransport(result.connectionId, this._mainService);100const protocolClient = this._instantiationService.createInstance(101RemoteAgentHostProtocolClient, result.address, transport,102);103104await protocolClient.connect();105this._logService.info(`${LOG_PREFIX} Protocol handshake completed with ${result.address}`);106107await this._remoteAgentHostService.addManagedConnection({108name: result.name,109connectionToken: result.connectionToken,110connection: {111type: RemoteAgentHostEntryType.Tunnel,112tunnelId: tunnel.tunnelId,113clusterId: tunnel.clusterId,114label: tunnel.name,115authProvider: auth.provider,116},117}, protocolClient);118119this._onDidChangeTunnels.fire();120} catch (err) {121this._logService.error(`${LOG_PREFIX} Connection setup failed`, err);122this._mainService.disconnect(result.connectionId).catch(() => { /* best effort */ });123throw err;124}125}126127async disconnect(address: string): Promise<void> {128await this._remoteAgentHostService.removeRemoteAgentHost(address);129this._onDidChangeTunnels.fire();130}131132/**133* Get an auth token, trying cached sessions first (silent),134* then prompting interactively if `silent` is false.135*/136private async _getToken(silent: boolean): Promise<{ token: string; provider: 'github' | 'microsoft' } | undefined> {137// Try the last known provider first138if (this._lastAuthProvider) {139const result = await this._getTokenForProvider(this._lastAuthProvider, silent);140if (result) {141return result;142}143}144145// Try both providers silently146for (const provider of ['github', 'microsoft'] as const) {147if (provider === this._lastAuthProvider) {148continue; // Already tried above149}150const result = await this._getTokenForProvider(provider, true);151if (result) {152return result;153}154}155156// If not silent, we would need the caller to prompt for provider selection.157// Return undefined — the caller (promptToConnectViaTunnel) handles the interactive flow.158return undefined;159}160161/**162* Get a token for a specific auth provider.163* @param provider The auth provider to use.164* @param silent If true, only try cached sessions. If false, prompt the user.165*/166private _getScopesForProvider(provider: 'github' | 'microsoft'): string[] {167const config = this._productService.tunnelApplicationConfig?.authenticationProviders;168return config?.[provider]?.scopes ?? [];169}170171private async _getTokenForProvider(172provider: 'github' | 'microsoft',173silent: boolean,174): Promise<{ token: string; provider: 'github' | 'microsoft' } | undefined> {175const providerId = provider;176const scopes = this._getScopesForProvider(provider);177if (scopes.length === 0) {178return undefined;179}180181try {182// Try exact scope match first183let sessions = await this._authenticationService.getSessions(providerId, scopes, {}, true);184185// Fall back: find any session whose scopes are a superset186if (sessions.length === 0) {187const allSessions = await this._authenticationService.getSessions(providerId, undefined, {}, true);188const requestedSet = new Set(scopes);189let bestSession: typeof allSessions[number] | undefined;190let bestExtra = Infinity;191for (const session of allSessions) {192const sessionScopes = new Set(session.scopes);193let isSuperset = true;194for (const scope of requestedSet) {195if (!sessionScopes.has(scope)) {196isSuperset = false;197break;198}199}200if (isSuperset) {201const extra = sessionScopes.size - requestedSet.size;202if (extra < bestExtra) {203bestExtra = extra;204bestSession = session;205}206}207}208if (bestSession) {209sessions = [bestSession];210}211}212213// Interactive fallback: create a new session214if (sessions.length === 0 && !silent) {215const session = await this._authenticationService.createSession(providerId, scopes, { activateImmediate: true });216sessions = [session];217}218219if (sessions.length > 0) {220const token = sessions[0].accessToken;221if (token) {222this._lastAuthProvider = provider;223return { token, provider };224}225}226} catch (err) {227this._logService.debug(`${LOG_PREFIX} Failed to get ${provider} token: ${err}`);228}229return undefined;230}231232async getAuthProvider(options?: { silent?: boolean }): Promise<'github' | 'microsoft' | undefined> {233const result = await this._getToken(options?.silent ?? true);234return result?.provider;235}236237getCachedTunnels(): ICachedTunnel[] {238const raw = this._storageService.get(CACHED_TUNNELS_KEY, StorageScope.APPLICATION);239if (!raw) {240return [];241}242try {243return JSON.parse(raw);244} catch {245return [];246}247}248249cacheTunnel(tunnel: ITunnelInfo, authProvider?: 'github' | 'microsoft'): void {250const cached = this.getCachedTunnels();251const filtered = cached.filter(t => t.tunnelId !== tunnel.tunnelId);252filtered.unshift({253tunnelId: tunnel.tunnelId,254clusterId: tunnel.clusterId,255name: tunnel.name,256authProvider,257});258this._storeCachedTunnels(filtered.slice(0, 20));259this._onDidChangeTunnels.fire();260}261262removeCachedTunnel(tunnelId: string): void {263const cached = this.getCachedTunnels();264this._storeCachedTunnels(cached.filter(t => t.tunnelId !== tunnelId));265this._onDidChangeTunnels.fire();266}267268private _storeCachedTunnels(tunnels: ICachedTunnel[]): void {269if (tunnels.length === 0) {270this._storageService.remove(CACHED_TUNNELS_KEY, StorageScope.APPLICATION);271} else {272this._storageService.store(CACHED_TUNNELS_KEY, JSON.stringify(tunnels), StorageScope.APPLICATION, StorageTarget.USER);273}274}275}276277278