Path: blob/main/src/vs/sessions/contrib/tunnelHost/electron-browser/tunnelHostService.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 { joinPath } from '../../../../base/common/resources.js';6import { Emitter, Event } from '../../../../base/common/event.js';7import { Disposable } from '../../../../base/common/lifecycle.js';8import { ProxyChannel } from '../../../../base/parts/ipc/common/ipc.js';9import { IAuthenticationService } from '../../../../workbench/services/authentication/common/authentication.js';10import { IEnvironmentService } from '../../../../platform/environment/common/environment.js';11import { ISharedProcessService } from '../../../../platform/ipc/electron-browser/services.js';12import { ILogger, ILoggerService } from '../../../../platform/log/common/log.js';13import { IProductService } from '../../../../platform/product/common/productService.js';14import { localize } from '../../../../nls.js';15import {16ITunnelAgentHostHostingService,17TUNNEL_HOST_CHANNEL,18TUNNEL_HOST_LOG_ID,19type ITunnelHostInfo,20type TunnelHostStatus,21} from '../../../../platform/agentHost/common/tunnelAgentHost.js';22import { IAgentHostService } from '../../../../platform/agentHost/common/agentService.js';23import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';24import { ITunnelHostService } from '../common/tunnelHost.js';2526export const CONFIGURATION_KEY_MICROSOFT_AUTH = 'remote.tunnels.access.enableMicrosoftAuth';27export const SHOW_TUNNEL_HOST_OUTPUT_ID = 'sessions.tunnelHost.showOutput';2829export class TunnelHostService extends Disposable implements ITunnelHostService {30declare readonly _serviceBrand: undefined;3132private readonly _mainService: ITunnelAgentHostHostingService;33private readonly _logger: ILogger;3435private readonly _onDidChangeStatus = this._register(new Emitter<void>());36readonly onDidChangeStatus: Event<void> = this._onDidChangeStatus.event;3738private _isSharing = false;39private _isConnecting = false;40private _sharingInfo: ITunnelHostInfo | undefined;4142/** Tracks which auth provider was last used successfully. */43private _lastAuthProvider: 'github' | 'microsoft' | undefined;4445constructor(46@ISharedProcessService sharedProcessService: ISharedProcessService,47@IAuthenticationService private readonly _authenticationService: IAuthenticationService,48@IProductService private readonly _productService: IProductService,49@IAgentHostService private readonly _agentHostService: IAgentHostService,50@IConfigurationService private readonly _configurationService: IConfigurationService,51@ILoggerService loggerService: ILoggerService,52@IEnvironmentService environmentService: IEnvironmentService,53) {54super();5556// Register a renderer-side logger so that the output channel57// created in the shared process is visible in the workbench UI58this._logger = this._register(loggerService.createLogger(59joinPath(environmentService.logsHome, `${TUNNEL_HOST_LOG_ID}.log`),60{ id: TUNNEL_HOST_LOG_ID, name: localize('tunnelHost.outputChannel', "Remote Connections") },61));6263this._mainService = ProxyChannel.toService<ITunnelAgentHostHostingService>(64sharedProcessService.getChannel(TUNNEL_HOST_CHANNEL),65);6667// Listen for status changes from the shared process68this._register(this._mainService.onDidChangeStatus((status: TunnelHostStatus) => {69this._isSharing = status.active;70this._sharingInfo = status.active ? status.info : undefined;71this._onDidChangeStatus.fire();72}));7374// Restore status on construction75this._mainService.getStatus().then(status => {76this._isSharing = status.active;77this._sharingInfo = status.active ? status.info : undefined;78if (status.active) {79this._onDidChangeStatus.fire();80}81});82}8384get isSharing(): boolean {85return this._isSharing;86}8788get isConnecting(): boolean {89return this._isConnecting;90}9192get sharingInfo(): ITunnelHostInfo | undefined {93return this._sharingInfo;94}9596async startSharing(): Promise<void> {97this._isConnecting = true;98this._onDidChangeStatus.fire();99100try {101const auth = await this._getToken(false);102if (!auth) {103this._logger.warn(`No auth token available for tunnel hosting`);104throw new Error(localize('tunnelHost.noAuth', "No authentication token available. Please sign in and try again."));105}106107this._logger.info(`Starting tunnel hosting...`);108109const socketInfo = await this._agentHostService.startWebSocketServer();110const info = await this._mainService.startHosting(auth.token, auth.provider, socketInfo);111this._isSharing = true;112this._sharingInfo = info;113} finally {114this._isConnecting = false;115this._onDidChangeStatus.fire();116}117}118119async stopSharing(): Promise<void> {120this._logger.info(`Stopping tunnel hosting...`);121await this._mainService.stopHosting();122this._isSharing = false;123this._sharingInfo = undefined;124this._onDidChangeStatus.fire();125}126127// ---- Auth helpers (reused from TunnelAgentHostService) -------------------128129private _getEnabledProviders(): readonly ('github' | 'microsoft')[] {130const microsoftEnabled = this._configurationService.getValue<boolean>(CONFIGURATION_KEY_MICROSOFT_AUTH);131return microsoftEnabled ? ['microsoft', 'github'] : ['github'];132}133134private async _getToken(silent: boolean): Promise<{ token: string; provider: 'github' | 'microsoft' } | undefined> {135const enabledProviders = this._getEnabledProviders();136137// Try the last known provider first138if (this._lastAuthProvider && enabledProviders.includes(this._lastAuthProvider)) {139const result = await this._getTokenForProvider(this._lastAuthProvider, silent);140if (result) {141return result;142}143}144145// Try enabled providers silently146for (const provider of enabledProviders) {147if (provider === this._lastAuthProvider) {148continue;149}150const result = await this._getTokenForProvider(provider, true);151if (result) {152return result;153}154}155156// If not silent, try interactively with each enabled provider157if (!silent) {158for (const provider of enabledProviders) {159const result = await this._getTokenForProvider(provider, false);160if (result) {161return result;162}163}164}165166return undefined;167}168169private _getScopesForProvider(provider: 'github' | 'microsoft'): string[] {170const config = this._productService.tunnelApplicationConfig?.authenticationProviders;171return config?.[provider]?.scopes ?? [];172}173174private async _getTokenForProvider(175provider: 'github' | 'microsoft',176silent: boolean,177): Promise<{ token: string; provider: 'github' | 'microsoft' } | undefined> {178const scopes = this._getScopesForProvider(provider);179if (scopes.length === 0) {180return undefined;181}182183try {184// Try exact scope match first185let sessions = await this._authenticationService.getSessions(provider, scopes, {}, true);186187// Fall back: find any session whose scopes are a superset188if (sessions.length === 0) {189const allSessions = await this._authenticationService.getSessions(provider, undefined, {}, true);190const requestedSet = new Set(scopes);191let bestSession: typeof allSessions[number] | undefined;192let bestExtra = Infinity;193for (const session of allSessions) {194const sessionScopes = new Set(session.scopes);195let isSuperset = true;196for (const scope of requestedSet) {197if (!sessionScopes.has(scope)) {198isSuperset = false;199break;200}201}202if (isSuperset) {203const extra = sessionScopes.size - requestedSet.size;204if (extra < bestExtra) {205bestExtra = extra;206bestSession = session;207}208}209}210if (bestSession) {211sessions = [bestSession];212}213}214215// Interactive fallback: create a new session216if (sessions.length === 0 && !silent) {217const session = await this._authenticationService.createSession(provider, scopes, { activateImmediate: true });218sessions = [session];219}220221if (sessions.length > 0) {222const token = sessions[0].accessToken;223if (token) {224this._lastAuthProvider = provider;225return { token, provider };226}227}228} catch (err) {229this._logger.debug(`Failed to get ${provider} token: ${err}`);230}231return undefined;232}233234override dispose(): void {235// Best-effort cleanup — stop hosting when the window closes236if (this._isSharing) {237this.stopSharing().catch(() => { /* ignore */ });238}239super.dispose();240}241}242243244