UNPKG

@microsoft/dev-tunnels-ssh

Version:
983 lines 55.8 kB
"use strict"; // // Copyright (c) Microsoft Corporation. All rights reserved. // Object.defineProperty(exports, "__esModule", { value: true }); exports.SshSession = void 0; const trace_1 = require("./trace"); const buffer_1 = require("buffer"); const vscode_jsonrpc_1 = require("vscode-jsonrpc"); const sshSessionConfiguration_1 = require("./sshSessionConfiguration"); const sshChannel_1 = require("./sshChannel"); const sshVersionInfo_1 = require("./sshVersionInfo"); const sshProtocol_1 = require("./io/sshProtocol"); const keyExchangeService_1 = require("./services/keyExchangeService"); const serviceActivation_1 = require("./services/serviceActivation"); const connectionService_1 = require("./services/connectionService"); const authenticationService_1 = require("./services/authenticationService"); const sshMessage_1 = require("./messages/sshMessage"); const kexMessages_1 = require("./messages/kexMessages"); const connectionMessages_1 = require("./messages/connectionMessages"); const authenticationMessages_1 = require("./messages/authenticationMessages"); const transportMessages_1 = require("./messages/transportMessages"); const sessionMetrics_1 = require("./metrics/sessionMetrics"); const promiseCompletionSource_1 = require("./util/promiseCompletionSource"); const sshSessionClosedEventArgs_1 = require("./events/sshSessionClosedEventArgs"); const sshRequestEventArgs_1 = require("./events/sshRequestEventArgs"); const sshAlgorithms_1 = require("./algorithms/sshAlgorithms"); const cancellation_1 = require("./util/cancellation"); const errors_1 = require("./errors"); const semaphore_1 = require("./util/semaphore"); const pipeExtensions_1 = require("./pipeExtensions"); const queue_1 = require("./util/queue"); const progress_1 = require("./progress"); const sshReportProgressEventArgs_1 = require("./events/sshReportProgressEventArgs"); /** * Allows SSH sessions to keep track of the session number for progress * reporting purposes. */ let sessionCounter = 0; /** * Base class for an SSH server or client connection; coordinates high-level SSH * protocol details and dispatches messages to registered internal services. * Enables opening and accepting `SshChannel` instances. */ class SshSession { get algorithms() { return this.protocol ? this.protocol.algorithms : null; } /** * Gets an object containing claims about the server or client on the * other end of the session, or `null` if the session is not authenticated. * * This property is initially `null` for an unauthenticated session. On * successful authentication, the session Authenticating event handler * provides a Task that returns a principal that is stored here. */ get principal() { return this.principalValue; } /* @internal */ set principal(value) { this.principalValue = value; } constructor(config, isClientSession) { this.config = config; this.remoteVersion = null; this.activatedServices = new Map(); this.connectionService = null; this.requestHandlers = new queue_1.Queue(); this.blockedMessages = []; this.blockedMessagesSemaphore = new semaphore_1.Semaphore(1); this.connected = false; this.disposed = false; this.keepAliveResponseReceived = false; this.keepAliveFailureCount = 0; this.keepAliveSuccessCount = 0; /** * Gets an object that reports current and cumulative measurements about the session. */ this.metrics = new sessionMetrics_1.SessionMetrics(); /* @internal */ this.reconnecting = false; this.sessionId = null; this.principalValue = null; this.authenticatingEmitter = new vscode_jsonrpc_1.Emitter(); /** * Event that is raised when a client or server is requesting authentication. * * See `SshAuthenticationType` for a description of the different authentication * methods and how they map to the event-args object. * * After validating the credentials, the event handler must set the * `SshAuthenticatingEventArgs.authenticationPromise` property to a task that * resolves to a principal object to indicate successful authentication. That principal will * then be associated with the sesssion as the `principal` property. */ this.onAuthenticating = this.authenticatingEmitter.event; this.closedEmitter = new vscode_jsonrpc_1.Emitter(); this.onClosed = this.closedEmitter.event; this.disconnectedEmitter = new vscode_jsonrpc_1.Emitter(); this.onDisconnected = this.disconnectedEmitter.event; this.serviceActivatedEmitter = new vscode_jsonrpc_1.Emitter(); this.onServiceActivated = this.serviceActivatedEmitter.event; this.channelOpeningEmitter = new vscode_jsonrpc_1.Emitter(); this.onChannelOpening = this.channelOpeningEmitter.event; this.requestEmitter = new vscode_jsonrpc_1.Emitter(); this.onRequest = this.requestEmitter.event; this.reportProgressEmitter = new vscode_jsonrpc_1.Emitter(); /** * Event that is raised to report connection progress. * * Apps may use this to provide progress feedback to users during the initial * connection or reconnection process. * * See `Progress` for a description of the different progress events that can be reported. */ this.onReportProgress = this.reportProgressEmitter.event; this.keepAliveFailedEmitter = new vscode_jsonrpc_1.Emitter(); /** * Event that is raised when a keep-alive message response is not received. */ this.onKeepAliveFailed = this.keepAliveFailedEmitter.event; this.keepAliveSucceededEmitter = new vscode_jsonrpc_1.Emitter(); /** * Event that is raised when a keep-alive message response is received. */ this.onKeepAliveSucceeded = this.keepAliveSucceededEmitter.event; /** * Gets or sets a function that handles trace messages associated with the session. * * By default, no messages are traced. To enable tracing, set this property to a function * that routes the message to console.log, a file, or anywhere else. * * @param level Level of message: error, warning, info, or verbose * @param eventId Integer identifier of the event being traced. * @param msg Message (non-localized) describing the event. */ this.trace = (level, eventId, msg, err) => { }; this.isClientSession = isClientSession; this.sessionNumber = ++sessionCounter; if (!config) throw new TypeError('Session configuration is required.'); if (!config.keyExchangeAlgorithms.find((a) => !!a)) { if (config.encryptionAlgorithms.length > 0 && config.encryptionAlgorithms.indexOf(null) < 0) { throw new Error('Encryption requires a key-exchange algorithm to be configured.'); } else if (config.hmacAlgorithms.length > 0 && config.hmacAlgorithms.indexOf(null) < 0) { throw new Error('HMAC requires a key-exchange algorithm to be configured.'); } else if (config.publicKeyAlgorithms.length > 0 && config.publicKeyAlgorithms.indexOf(null) < 0) { throw new Error('Host authentication requires a key-exchange algorithm to be configured.'); } // No key exchange, no encryption, no HMAC. this.kexService = null; this.activateService(connectionService_1.ConnectionService); } else { this.kexService = new keyExchangeService_1.KeyExchangeService(this); } config.onConfigurationChanged(() => { const protocol = this.protocol; if (protocol) { protocol.traceChannelData = config.traceChannelData; } // Restart keep-alive timer if timeout changed if (this.connected && !this.disposed) { this.startKeepAliveTimer(); } }); } get isConnected() { return this.connected; } get isClosed() { return this.disposed; } get services() { return [...this.activatedServices.values()]; } get channels() { var _a, _b; return (_b = (_a = this.connectionService) === null || _a === void 0 ? void 0 : _a.channels) !== null && _b !== void 0 ? _b : []; } get protocolExtensions() { var _a; return ((_a = this.protocol) === null || _a === void 0 ? void 0 : _a.extensions) || null; } /** * Gets an activated service instance by type. * * @returns The service instance, or `null` if the service has not been activated. */ getService(serviceType) { const service = this.activatedServices.get(serviceType); return service ? service : null; } /* @internal */ activateService(serviceTypeOrName) { let serviceType; if (typeof serviceTypeOrName === 'function') { serviceType = serviceTypeOrName; } else { const serviceName = serviceTypeOrName; serviceType = (0, serviceActivation_1.findService)(this.config.services, (a) => a.serviceRequest === serviceName); if (!serviceType) { return null; } } let activatedService = this.activatedServices.get(serviceType); if (!activatedService) { if (!this.config.services.has(serviceType)) { throw new Error(`Service type not configured: ${serviceType.name}`); } const serviceConfig = this.config.services.get(serviceType); activatedService = new serviceType(this, serviceConfig); // This service is maintained in a separate member because it is accessed frequently. if (serviceType === connectionService_1.ConnectionService) { this.connectionService = activatedService; } this.activatedServices.set(serviceType, activatedService); this.serviceActivatedEmitter.fire(activatedService); } return activatedService; } async connect(stream, cancellation) { if (!stream) throw new TypeError('A session stream is required.'); if (this.disposed) throw new errors_1.ObjectDisposedError(this); if (!this.connectPromise) { this.connectPromise = this.doConnect(stream, cancellation); } await this.connectPromise; } async doConnect(stream, cancellation) { this.trace(trace_1.TraceLevel.Info, trace_1.SshTraceEventIds.sessionConnecting, `${this} ${this.reconnecting ? 're' : ''}connecting...`); this.raiseReportProgress(progress_1.Progress.OpeningSshSessionConnection); this.protocol = new sshProtocol_1.SshProtocol(stream, this.config, this.metrics, this.trace); this.protocol.kexService = this.kexService; this.raiseReportProgress(progress_1.Progress.StartingProtocolVersionExchange); await this.exchangeVersions(cancellation); if (this.kexService) { await this.encrypt(cancellation); } else { // When there's no key-exchange service configured, send a key-exchange init message // that specifies "none" for all algorithms. await this.sendMessage(kexMessages_1.KeyExchangeInitMessage.none, cancellation); // When encrypting, the key-exchange step will wait on the version-exchange. // When not encrypting, it must be directly awaited. await (0, cancellation_1.withCancellation)(this.versionExchangePromise, cancellation); this.raiseReportProgress(progress_1.Progress.CompletedProtocolVersionExchange); this.connected = true; } this.processMessages().catch((e) => { this.trace(trace_1.TraceLevel.Error, trace_1.SshTraceEventIds.unknownError, `Unhandled error processing messages: ${e.message}`, e); }); this.startKeepAliveTimer(); this.raiseReportProgress(progress_1.Progress.OpenedSshSessionConnection); } async exchangeVersions(cancellation) { const writePromise = this.protocol.writeProtocolVersion(SshSession.localVersion.toString(), cancellation); const readPromise = this.protocol.readProtocolVersion(cancellation); // Don't wait for and verify the other side's version info yet. // Instead save a promise that can be awaited later. this.versionExchangePromise = readPromise.then(async (remoteVersion) => { this.trace(trace_1.TraceLevel.Info, trace_1.SshTraceEventIds.protocolVersion, `Local version: ${SshSession.localVersion}, remote version: ${remoteVersion}`); let errorMessage; const remoteVersionInfo = sshVersionInfo_1.SshVersionInfo.tryParse(remoteVersion); if (remoteVersionInfo) { this.remoteVersion = remoteVersionInfo; if (remoteVersionInfo.protocolVersion === '2.0') { return; } errorMessage = `Remote SSH version ${this.remoteVersion} is not supported. ` + 'This library only supports SSH v2.0.'; } else { errorMessage = `Could not parse remote SSH version ${remoteVersion}`; } await this.close(transportMessages_1.SshDisconnectReason.protocolVersionNotSupported, errorMessage, new Error(errorMessage)); }); await writePromise; } async encrypt(cancellation) { var _a, _b; const protocol = this.protocol; if (!protocol) throw new errors_1.ObjectDisposedError(this); await protocol.considerReExchange(true, cancellation); // Ensure the protocol version has been received before receiving any messages. await (0, cancellation_1.withCancellation)(this.versionExchangePromise, cancellation); this.raiseReportProgress(progress_1.Progress.CompletedProtocolVersionExchange); this.connected = true; this.raiseReportProgress(progress_1.Progress.StartingKeyExchange); let message = null; while (!this.isClosed && !((_a = this.protocol) === null || _a === void 0 ? void 0 : _a.algorithms) && !(message instanceof transportMessages_1.DisconnectMessage)) { message = await protocol.receiveMessage(cancellation); if (!message) { break; } await this.handleMessage(message, cancellation); } this.raiseReportProgress(progress_1.Progress.CompletedKeyExchange); if (!((_b = this.protocol) === null || _b === void 0 ? void 0 : _b.algorithms)) { throw new errors_1.SshConnectionError('Session closed while encrypting.', transportMessages_1.SshDisconnectReason.connectionLost); } else if (this.protocol.algorithms.cipher) { this.trace(trace_1.TraceLevel.Info, trace_1.SshTraceEventIds.sessionEncrypted, `${this} encrypted.`); } } /* @internal */ async processMessages() { var _a; this.connected = true; while (!this.disposed) { const protocol = this.protocol; if (!protocol) { break; } let message = null; try { message = await protocol.receiveMessage(); } catch (e) { if (!(e instanceof Error)) throw e; let reason = transportMessages_1.SshDisconnectReason.protocolError; if (e instanceof errors_1.SshConnectionError) { reason = (_a = e.reason) !== null && _a !== void 0 ? _a : reason; } else { this.trace(trace_1.TraceLevel.Error, trace_1.SshTraceEventIds.receiveMessageFailed, `Error receiving message: ${e.message}`, e); } await this.close(reason, e.message, e); } if (!message) { await this.close(transportMessages_1.SshDisconnectReason.connectionLost, 'Connection lost.'); break; } let messageSuccess = false; try { await this.handleMessage(message); messageSuccess = true; } catch (e) { if (!(e instanceof Error)) throw e; this.trace(trace_1.TraceLevel.Error, trace_1.SshTraceEventIds.handleMessageFailed, `Error handling ${message}: ${e.message}`, e); await this.close(transportMessages_1.SshDisconnectReason.protocolError, e.message, e); } if (messageSuccess) { this.keepAliveResponseReceived = true; this.startKeepAliveTimer(); } } this.connected = false; } /** * Checks whether the session is in a state that allows requests, such as session requests * and open-channel requests. * * A session with disabled crypto (no key-exchange service) always allows requests. A * session with enabled crypto does not allow requests until the first key-exchange has * completed (algorithms are negotiated). If the negotiated algorithms enabled encryption, * then the session must be authenticated (have a principal) before allowing requests. */ /* @internal */ get canAcceptRequests() { var _a; return (!this.kexService || (!!((_a = this.protocol) === null || _a === void 0 ? void 0 : _a.algorithms) && (!this.protocol.algorithms.cipher || !!this.principal))); } async sendMessage(message, cancellation) { var _a, _b; if (!message) throw new TypeError('Message expected.'); if (cancellation && cancellation.isCancellationRequested) throw new cancellation_1.CancellationError(); const protocol = this.protocol; if (!protocol || this.disposed) { throw new errors_1.ObjectDisposedError(this); } // Delay sending messages if in the middle of a key (re-)exchange. if (this.kexService && this.kexService.exchanging && message.messageType > 4 && (message.messageType < 20 || message.messageType > 49)) { this.blockedMessages.push(message); return; } await this.blockedMessagesSemaphore.wait(cancellation); let result; try { result = await protocol.sendMessage(message, cancellation); this.blockedMessagesSemaphore.release(); } catch (e) { this.blockedMessagesSemaphore.release(); if (e instanceof errors_1.SshConnectionError) { const ce = e; if (ce.reason === transportMessages_1.SshDisconnectReason.connectionLost && ((_a = this.protocolExtensions) === null || _a === void 0 ? void 0 : _a.has(sshSessionConfiguration_1.SshProtocolExtensionNames.sessionReconnect))) { // Connection-lost error when reconnect is enabled. Don't throw an error; // the message will remain in the reconnect message cache and will be re-sent // upon reconnection. return; } } if (!(e instanceof Error)) throw e; this.trace(trace_1.TraceLevel.Error, trace_1.SshTraceEventIds.sendMessageFailed, `Error sending ${message}: ${e.message}`, e); throw e; } if (!result) { // Sending failed due to a closed stream, but don't throw when reconnect is enabled. // In that case the sent message is buffered and will be re-sent after reconnecting. if (!((_b = this.protocolExtensions) === null || _b === void 0 ? void 0 : _b.has(sshSessionConfiguration_1.SshProtocolExtensionNames.sessionReconnect))) { throw new errors_1.SshConnectionError('Session disconnected.', transportMessages_1.SshDisconnectReason.connectionLost); } } } /** * Handles an incoming message. Can be overridden by subclasses to handle additional * message types that are registered via `SshSessionConfiguration.messages`. */ handleMessage(message, cancellation) { var _a; if (message instanceof connectionMessages_1.ConnectionMessage && this.connectionService) { return this.connectionService.handleMessage(message, cancellation); } else if (message instanceof kexMessages_1.NewKeysMessage) { return this.handleNewKeysMessage(message, cancellation); } else if (message instanceof kexMessages_1.KeyExchangeMessage) { return this.handleKeyExchangeMessage(message, cancellation); } else if (message instanceof authenticationMessages_1.AuthenticationMessage) { return (_a = this.getService(authenticationService_1.AuthenticationService)) === null || _a === void 0 ? void 0 : _a.handleMessage(message, cancellation); } else if (message instanceof transportMessages_1.ServiceRequestMessage) { return this.handleServiceRequestMessage(message, cancellation); } else if (message instanceof transportMessages_1.ServiceAcceptMessage) { return this.handleServiceAcceptMessage(message, cancellation); } else if (message instanceof transportMessages_1.SessionRequestMessage) { return this.handleRequestMessage(message, cancellation); } else if (message instanceof transportMessages_1.SessionRequestSuccessMessage) { return this.handleRequestSuccessMessage(message); } else if (message instanceof transportMessages_1.SessionRequestFailureMessage) { return this.handleRequestFailureMessage(message); } else if (message instanceof transportMessages_1.ExtensionInfoMessage) { return this.handleExtensionInfoMessage(message, cancellation); } else if (message instanceof transportMessages_1.DisconnectMessage) { return this.handleDisconnectMessage(message); } else if (message instanceof transportMessages_1.UnimplementedMessage) { return this.handleUnimplementedMessage(message, cancellation); } else if (message instanceof transportMessages_1.DebugMessage) { return this.handleDebugMessage(message); } else if (message instanceof transportMessages_1.IgnoreMessage) { // Do nothing for ignore message return; } else if (message instanceof sshMessage_1.SshMessage) { throw new Error(`Unhandled message type: ${message.constructor.name}`); } else { throw new TypeError('Message argument was ' + (message ? 'invalid type.' : 'null.')); } } /* @internal */ async handleRequestMessage(message, cancellation) { var _a; let result = false; let response = null; if (message.requestType === "initial-channel-request@microsoft.com" /* ExtensionRequestTypes.initialChannelRequest */ && this.config.protocolExtensions.includes(sshSessionConfiguration_1.SshProtocolExtensionNames.openChannelRequest)) { const sessionChannelRequest = message.convertTo(new transportMessages_1.SessionChannelRequestMessage()); const remoteChannelId = sessionChannelRequest.senderChannel; const channel = this.channels.find((c) => c.remoteChannelId === remoteChannelId); if (channel && sessionChannelRequest.request) { sessionChannelRequest.request.wantReply = false; // Avoid redundant reply result = await channel.handleRequest(sessionChannelRequest.request, cancellation); } } else if (message.requestType === "enable-session-reconnect@microsoft.com" /* ExtensionRequestTypes.enableSessionReconnect */ && ((_a = this.config.protocolExtensions) === null || _a === void 0 ? void 0 : _a.includes(sshSessionConfiguration_1.SshProtocolExtensionNames.sessionReconnect))) { if (!this.protocol.incomingMessagesHaveReconnectInfo) { // Starting immediately after this message, all incoming messages include // an extra field or two after the payload. this.protocol.incomingMessagesHaveReconnectInfo = true; this.protocol.incomingMessagesHaveLatencyInfo = this.protocol.extensions.has(sshSessionConfiguration_1.SshProtocolExtensionNames.sessionLatency); result = true; } } else if (message.requestType === "keepalive@openssh.com" /* ExtensionRequestTypes.keepAliveRequest */) { // Handle keep-alive request - always succeed and trace this.trace(trace_1.TraceLevel.Verbose, trace_1.SshTraceEventIds.keepAliveRequestReceived, `${this} Keep-alive request received`); result = true; } else if (!this.canAcceptRequests) { this.trace(trace_1.TraceLevel.Warning, trace_1.SshTraceEventIds.sessionRequestFailed, 'Session request blocked because the session is not yet authenticated.'); result = false; } else { const args = new sshRequestEventArgs_1.SshRequestEventArgs(message.requestType || '', message, this.principal, cancellation); const serviceType = (0, serviceActivation_1.findService)(this.config.services, (a) => a.sessionRequest === message.requestType); if (serviceType) { // A service was configured for activation via this session request type. const service = this.activateService(serviceType); // `onSessionRequest` should really be 'protected internal'. await service.onSessionRequest(args, cancellation); } else { // Raise a request event to let an event listener handle this request. this.raiseSessionRequest(args); } // TODO: do not block requests in TS (similar to CS) // see https://dev.azure.com/devdiv/DevDiv/_git/SSH/commit/0b84a48811e2f015107c73bf4584b6c3b676a6de if (args.responsePromise) { response = await args.responsePromise; result = response instanceof transportMessages_1.SessionRequestSuccessMessage; } else { result = args.isAuthorized || false; } } if (message.wantReply) { if (result) { if (!(response instanceof transportMessages_1.SessionRequestSuccessMessage)) { response = new transportMessages_1.SessionRequestSuccessMessage(); } } else { if (!(response instanceof transportMessages_1.SessionRequestFailureMessage)) { response = new transportMessages_1.SessionRequestFailureMessage(); } } await this.sendMessage(response, cancellation); } } /* @internal */ raiseReportProgress(progress) { const args = new sshReportProgressEventArgs_1.SshReportProgressEventArgs(progress, this.sessionNumber); this.reportProgressEmitter.fire(args); } /* @internal */ raiseSessionRequest(args) { this.requestEmitter.fire(args); } /* @internal */ async handleServiceRequestMessage(message, cancellation) { // Do nothing. Subclasses may override. } /* @internal */ async handleServiceAcceptMessage(message, cancellation) { // Do nothing. Subclasses may override. } async handleKeyExchangeMessage(message, cancellation) { if (this.kexService) { await this.kexService.handleMessage(message, cancellation); } else if (!(message instanceof kexMessages_1.KeyExchangeInitMessage && message.allowsNone)) { // The other side required some security, but it's not configured here. await this.close(transportMessages_1.SshDisconnectReason.keyExchangeFailed, 'Encryption is disabled.'); } } /* @internal */ async handleNewKeysMessage(message, cancellation) { var _a; try { await this.blockedMessagesSemaphore.wait(cancellation); await this.protocol.handleNewKeys(cancellation); if ((_a = this.algorithms) === null || _a === void 0 ? void 0 : _a.isExtensionInfoRequested) { await this.sendExtensionInfo(cancellation); } try { // Send messages that were blocked during key exchange. while (this.blockedMessages.length > 0) { const blockedMessage = this.blockedMessages.shift(); if (!this.protocol) throw new errors_1.ObjectDisposedError(this); await this.protocol.sendMessage(blockedMessage, cancellation); } } catch (e) { if (!(e instanceof Error)) throw e; await this.close(transportMessages_1.SshDisconnectReason.protocolError, undefined, e); } } finally { this.blockedMessagesSemaphore.release(); } } async handleUnimplementedMessage(message, cancellation) { if (message.unimplementedMessageType !== undefined) { // Received a message type that is unimplemented by this side. // Send a reply to inform the other side. await this.sendMessage(message, cancellation); } else { // This is a reply indicating this side previously sent a message type // that is not implemented by the other side. It has already been traced. } } handleDebugMessage(message) { if (message.message) { this.trace(message.alwaysDisplay ? trace_1.TraceLevel.Info : trace_1.TraceLevel.Verbose, trace_1.SshTraceEventIds.debugMessage, message.message); } } startKeepAliveTimer() { // Stop existing timer if (this.keepAliveTimer) { clearTimeout(this.keepAliveTimer); this.keepAliveTimer = undefined; } // Don't start timer if keep-alive is disabled or session is disposed/not connected const timeoutInSeconds = this.config.keepAliveTimeoutInSeconds; if (!timeoutInSeconds || timeoutInSeconds <= 0 || this.disposed || !this.connected) { return; } // Start new timer this.keepAliveTimer = setTimeout(async () => { try { await this.onKeepAliveTimeout(); } catch (error) { // Log error but don't let it crash the session this.trace(trace_1.TraceLevel.Error, trace_1.SshTraceEventIds.unknownError, `Error in keep-alive timeout: ${error instanceof Error ? error.message : error}`, error instanceof Error ? error : undefined); } }, timeoutInSeconds * 1000); } async onKeepAliveTimeout() { // Don't send keep-alive if session is disposed or not connected if (this.disposed || !this.connected) { // Clear the timer to prevent further timeouts if (this.keepAliveTimer) { clearTimeout(this.keepAliveTimer); this.keepAliveTimer = undefined; } return; } // Check if we can send keep-alive requests if (!this.canAcceptRequests) { // Schedule next timeout this.startKeepAliveTimer(); return; } // Check if we received a response to the previous keep-alive request if (!this.keepAliveResponseReceived) { // No response received - this is a failure this.keepAliveSuccessCount = 0; this.keepAliveFailureCount++; this.trace(trace_1.TraceLevel.Warning, trace_1.SshTraceEventIds.keepAliveResponseNotReceived, `${this} Keep-alive response not received (failure #${this.keepAliveFailureCount})`); this.keepAliveFailedEmitter.fire(this.keepAliveFailureCount); } else { // Response was received - success this.keepAliveFailureCount = 0; this.keepAliveSuccessCount++; this.keepAliveSucceededEmitter.fire(this.keepAliveSuccessCount); } // Reset response flag for next cycle this.keepAliveResponseReceived = false; // Send keep-alive request try { const request = new transportMessages_1.SessionRequestMessage(); request.requestType = "keepalive@openssh.com" /* ExtensionRequestTypes.keepAliveRequest */; request.wantReply = true; // Request reply to detect if connection is alive await this.sendMessage(request); } catch (error) { this.keepAliveSuccessCount = 0; this.keepAliveFailureCount++; this.keepAliveFailedEmitter.fire(this.keepAliveFailureCount); } // Schedule next timeout only if still connected and not disposed if (!this.disposed && this.connected) { this.startKeepAliveTimer(); } } /* @internal */ async raiseAuthenticatingEvent(args) { this.trace(trace_1.TraceLevel.Info, trace_1.SshTraceEventIds.sessionAuthenticating, `${this} Authenticating(${args})`); this.raiseReportProgress(progress_1.Progress.StartingSessionAuthentication); this.authenticatingEmitter.fire(args); let authPromise = args.authenticationPromise; if (!authPromise) { authPromise = Promise.resolve(null); } const principal = await authPromise; this.raiseReportProgress(progress_1.Progress.CompletedSessionAuthentication); return principal; } /** * Sends a session request and waits for a response. * * Note if `wantReply` is `false`, this method returns `true` immediately after sending * the request, without waiting for a response. * * @returns The authorization status of the response; if `false`, the other side denied the * request. */ async request(request, cancellation) { if (!request) throw new TypeError('Request is required.'); if (!request.wantReply) { await this.sendMessage(request, cancellation); return true; } const response = await this.requestResponse(request, transportMessages_1.SessionRequestSuccessMessage, transportMessages_1.SessionRequestFailureMessage, cancellation); return response instanceof transportMessages_1.SessionRequestSuccessMessage; } /** * Sends a session request and waits for a specific type of success or failure message. * * @returns The success or failure response message. */ async requestResponse(request, successType, failureType, cancellation) { if (!request) throw new TypeError('Request is required.'); if (!successType) throw new TypeError('Success response type is required.'); if (!failureType) throw new TypeError('Failure response type is required.'); request.wantReply = true; const requestHandler = (err, result) => { var _a, _b; if (err) { requestCompletionSource.reject(err); } else if (requestHandler.isCancelled) { // The completion source was already rejected with a cancellation error. return; } else if (result instanceof transportMessages_1.SessionRequestFailureMessage) { const failure = (_a = result === null || result === void 0 ? void 0 : result.convertTo(new failureType(), true)) !== null && _a !== void 0 ? _a : null; requestCompletionSource.resolve(failure); } else if (result instanceof transportMessages_1.SessionRequestSuccessMessage) { // Make a copy of the response message because the continuation may be // asynchronous; meanwhile the receive buffer will be re-used. const success = (_b = result === null || result === void 0 ? void 0 : result.convertTo(new successType(), true)) !== null && _b !== void 0 ? _b : null; requestCompletionSource.resolve(success); } else { requestCompletionSource.reject(new Error('Unknown response message type.')); } }; const requestCompletionSource = new promiseCompletionSource_1.PromiseCompletionSource(); if (cancellation) { if (cancellation.isCancellationRequested) throw new cancellation_1.CancellationError(); cancellation.onCancellationRequested(() => { requestHandler.isCancelled = true; requestCompletionSource.reject(new cancellation_1.CancellationError()); }); } this.requestHandlers.enqueue(requestHandler); await this.sendMessage(request, cancellation); return await requestCompletionSource.promise; } handleRequestSuccessMessage(message) { this.invokeRequestHandler(message, undefined, undefined); } handleRequestFailureMessage(message) { this.invokeRequestHandler(undefined, message, undefined); } invokeRequestHandler(success, failure, error) { let requestHandler; while ((requestHandler = this.requestHandlers.dequeue())) { requestHandler(error, success !== null && success !== void 0 ? success : failure); // An error is provided if the session is disposing. In that case, // all pending requests should fail with that error. if (!error) { break; } } } async acceptChannel(channelTypeOrCancellation, cancellation) { const channelType = typeof channelTypeOrCancellation === 'string' ? channelTypeOrCancellation : undefined; if (!cancellation && typeof channelTypeOrCancellation === 'object') cancellation = channelTypeOrCancellation; this.activateService(connectionService_1.ConnectionService); // Prepare to accept the channel before connecting. This ensures that if the channel // open request comes in immediately after connecting then the channel won't be missed // in case of a task scheduling delay. const acceptPromise = this.connectionService.acceptChannel(channelType || sshChannel_1.SshChannel.sessionChannelType, cancellation); return await acceptPromise; } async openChannel(channelTypeOrOpenMessageOrCancellation, initialRequestOrCancellation, cancellation) { let openMessage; if (typeof channelTypeOrOpenMessageOrCancellation === 'string' || channelTypeOrOpenMessageOrCancellation === null) { openMessage = new connectionMessages_1.ChannelOpenMessage(); openMessage.channelType = channelTypeOrOpenMessageOrCancellation !== null && channelTypeOrOpenMessageOrCancellation !== void 0 ? channelTypeOrOpenMessageOrCancellation : sshChannel_1.SshChannel.sessionChannelType; } else if (channelTypeOrOpenMessageOrCancellation instanceof connectionMessages_1.ChannelOpenMessage) { openMessage = channelTypeOrOpenMessageOrCancellation; } else { openMessage = new connectionMessages_1.ChannelOpenMessage(); openMessage.channelType = sshChannel_1.SshChannel.sessionChannelType; cancellation = channelTypeOrOpenMessageOrCancellation; } if (initialRequestOrCancellation instanceof connectionMessages_1.ChannelRequestMessage) { return await this.openChannelWithInitialRequest(openMessage, initialRequestOrCancellation, cancellation); } else if (!cancellation && initialRequestOrCancellation !== null) { cancellation = initialRequestOrCancellation; } this.activateService(connectionService_1.ConnectionService); const completionSource = new promiseCompletionSource_1.PromiseCompletionSource(); await this.connectionService.openChannel(openMessage, completionSource, cancellation); return await completionSource.promise; } async openChannelWithInitialRequest(openMessage, initialRequest, cancellation) { var _a; this.activateService(connectionService_1.ConnectionService); const completionSource = new promiseCompletionSource_1.PromiseCompletionSource(); const channelId = await this.connectionService.openChannel(openMessage, completionSource, cancellation); if (cancellation) { if (cancellation.isCancellationRequested) throw new cancellation_1.CancellationError(); cancellation.onCancellationRequested(() => completionSource.reject(new cancellation_1.CancellationError())); } let channel; let requestResult; const isExtensionSupported = this.config.protocolExtensions.includes(sshSessionConfiguration_1.SshProtocolExtensionNames.openChannelRequest) && ((_a = this.protocolExtensions) === null || _a === void 0 ? void 0 : _a.has(sshSessionConfiguration_1.SshProtocolExtensionNames.openChannelRequest)); if (isExtensionSupported === false) { // The local or remote side definitely doesn't support this extension. Just send a // normal channel request after waiting for the channel open confirmation. channel = await completionSource.promise; requestResult = await channel.request(initialRequest, cancellation); } else { // The remote side does or might support this extension. If uncertain then a reply // is required. const wantReply = initialRequest.wantReply || isExtensionSupported === undefined; // Send the initial channel request message BEFORE waiting for the // channel open confirmation. const sessionRequest = new transportMessages_1.SessionChannelRequestMessage(); sessionRequest.requestType = "initial-channel-request@microsoft.com" /* ExtensionRequestTypes.initialChannelRequest */; sessionRequest.senderChannel = channelId; sessionRequest.request = initialRequest; sessionRequest.wantReply = wantReply; const requestPromise = this.request(sessionRequest, cancellation); // Wait for the channel open confirmation. channel = await completionSource.promise; if (!wantReply) { requestResult = true; } else { // Wait for the response to the initial channel request. requestResult = await requestPromise; if (!requestResult && isExtensionSupported === undefined) { // The initial request failed. This could be because the other side doesn't // support the initial-request extension or because the request was denied. // Try sending the request again as a regular channel request. requestResult = await channel.request(initialRequest); } } } if (!requestResult) { // The regular request still failed, so close the channel and throw. await channel.close(); throw new Error('The initial channel request was denied.'); } return channel; } /* @internal */ async handleChannelOpening(args, cancellation, resolveService = true) { if (resolveService) { const serviceType = (0, serviceActivation_1.findService)(this.config.services, (a) => a.channelType === args.channel.channelType && !a.channelRequest); if (serviceType) { // A service was configured for activation via this channel type. const service = this.activateService(serviceType); // `onChannelOpening` should really be 'protected internal'. await service.onChannelOpening(args, cancellation); return; } } args.cancellation = cancellation !== null && cancellation !== void 0 ? cancellation : vscode_jsonrpc_1.CancellationToken.None; this.channelOpeningEmitter.fire(args); } /* @internal */ async sendExtensionInfo(cancellation) { if (!this.protocol) return; const message = new transportMessages_1.ExtensionInfoMessage(); message.extensionInfo = {}; for (const extensionName of this.config.protocolExtensions) { if (extensionName === sshSessionConfiguration_1.SshProtocolExtensionNames.serverSignatureAlgorithms) { // Send the list of enabled host key signature algorithms. const publicKeyAlgorithms = Array.from(new Set((0, sshAlgorithms_1.algorithmNames)(this.config.publicKeyAlgorithms))).join(','); message.extensionInfo[extensionName] = publicKeyAlgorithms; } else { message.extensionInfo[extensionName] = ''; } } await this.protocol.sendMessage(message, cancellation); } async handleExtensionInfoMessage(message, cancellation) { if (!this.protocol) { return; } this.protocol.extensions = new Map(); const proposedExtensions = message.extensionInfo; if (!proposedExtensions) { return; } for (const extensionName of this.config.protocolExtensions) { const proposedExtension = message.extensionInfo[extensionName]; if (typeof proposedExtension === 'string') { this.protocol.extensions.set(extensionName, proposedExtension); } } if (this.protocol.extensions.has(sshSessionConfiguration_1.SshProtocolExtensionNames.sessionReconnect)) { await this.enableReconnect(cancellation); } } async close(reason, message, error) { var _a, _b, _c; if (this.disposed || !this.connected) { return; } this.connected = false; this.trace(trace_1.TraceLevel.Info, trace_1.SshTraceEventIds.sessionClosing, `${this} Close(${transportMessages_1.SshDisconnectReason[reason]}, "${message || ''}")`); if (reason !== transportMessages_1.SshDisconnectReason.connectionLost) { try { const disconnectMessage = new transportMessages_1.DisconnectMessage(); disconnectMessage.reasonCode = reason; disconnectMessage.description = message || ''; await ((_a = this.protocol) === null || _a === void 0 ? void 0 : _a.sendMessage(disconnectMessage)); } catch (e) { // Already disconnected. } } else if (this.handleDisconnected()) { // Keep the session in a disconnected (but not closed) state. (_b = this.protocol) === null || _b === void 0 ? void 0 : _b.dispose(); this.trace(trace_1.TraceLevel.Info, trace_1.SshTraceEventIds.sessionDisconnected, `${this} disconnected.`); this.disconnectedEmitter.fire(); return; } this.disposed = true; this.closedError = error; error = error !== null && error !== void 0 ? error : new errors_1.SshConnectionError(message, reason); if (error) { (_c = this.connectionService) === null || _c === void 0 ? void 0 : _c.close(error); } this.closedEmitter.fire(new sshSessionClosedEventArgs_1.SshSessionClosedEventArgs(reason, message || 'Disconnected.', error)); this.dispose(); } /* @internal */ handleDisconnected() { var _a, _b; this.connectPromise = undefined; (_a = this.kexService) === null || _a === void 0 ? void 0 : _a.abortKeyExchange(); if (!((_b = this.protocolExtensions) === null || _b === void 0 ? void 0 : _b.has(sshSessionConfiguration_1.SshProtocolExtensionNames.sessionReconnect))) { return false; } return true; }