UNPKG

@microsoft/dev-tunnels-ssh

Version:
175 lines 9.45 kB
"use strict"; // // Copyright (c) Microsoft Corporation. All rights reserved. // Object.defineProperty(exports, "__esModule", { value: true }); exports.SshServerSession = void 0; const sshSession_1 = require("./sshSession"); const vscode_jsonrpc_1 = require("vscode-jsonrpc"); const transportMessages_1 = require("./messages/transportMessages"); const errors_1 = require("./errors"); const sshSessionConfiguration_1 = require("./sshSessionConfiguration"); const trace_1 = require("./trace"); /** * The server side of an SSH session. Extends the base `SshSession` class * to support host authentication. */ class SshServerSession extends sshSession_1.SshSession { constructor(config, reconnectableSessions) { super(config, false); this.clientAuthenticatedEmitter = new vscode_jsonrpc_1.Emitter(); this.onClientAuthenticated = this.clientAuthenticatedEmitter.event; this.reconnectedEmitter = new vscode_jsonrpc_1.Emitter(); this.onReconnected = this.reconnectedEmitter.event; /** * Gets or sets credentials and/or credential callbacks for authenticating the session. */ this.credentials = { publicKeys: [] }; const enableReconnect = config.protocolExtensions.includes(sshSessionConfiguration_1.SshProtocolExtensionNames.sessionReconnect); if (enableReconnect && !reconnectableSessions) { throw new Error('When reconnect is enabled, server sessions require a reference to a ' + 'shared collection to track reconnectable sessions.'); } else if (!enableReconnect && reconnectableSessions) { throw new Error('When reconnect is not enabled, the reconnectable sessions collection ' + 'is not applicable.'); } this.reconnectableSessions = reconnectableSessions; } /* @internal */ async handleServiceRequestMessage(message, cancellation) { const service = this.activateService(message.serviceName); if (service) { const acceptMessage = new transportMessages_1.ServiceAcceptMessage(); acceptMessage.serviceName = message.serviceName; await this.sendMessage(acceptMessage, cancellation); } else { throw new errors_1.SshConnectionError(`Service "${message.serviceName}" not available.`, transportMessages_1.SshDisconnectReason.serviceNotAvailable); } } /* @internal */ async handleRequestMessage(message, cancellation) { var _a; if (message.requestType === "session-reconnect@microsoft.com" /* ExtensionRequestTypes.sessionReconnect */ && ((_a = this.config.protocolExtensions) === null || _a === void 0 ? void 0 : _a.includes(sshSessionConfiguration_1.SshProtocolExtensionNames.sessionReconnect))) { const reconnectRequest = message.convertTo(new transportMessages_1.SessionReconnectRequestMessage()); await this.reconnect(reconnectRequest, cancellation); // reconnect() handles sending the response message. return; } await super.handleRequestMessage(message, cancellation); } /* @internal */ handleClientAuthenticated() { this.clientAuthenticatedEmitter.fire(); } /* @internal */ async enableReconnect(cancellation) { await super.enableReconnect(cancellation); if (!this.reconnectableSessions.includes(this)) { this.reconnectableSessions.push(this); } } /* @internal */ handleDisconnected() { if (this.reconnecting) { // Prevent closing the session while reconnecting. return true; } return super.handleDisconnected(); } /** * Attempts to reconnect the client to a disconnected server session. * * If reconnection is successful, the current server session is disposed because the client * gets reconnected to a different server session. */ /* @internal */ async reconnect(reconnectRequest, cancellation) { var _a, _b, _c; if (!this.reconnectableSessions) { throw new Error('Disconnected sessions collection ' + 'should have been initialized when reconnect is enabled.'); } // Try to find the requested server session in the list of available disconnected // server sessions, by validating the reconnect token. let reconnectSession; for (const reconnectableSession of this.reconnectableSessions) { if (reconnectableSession !== this && (await this.verifyReconnectToken(reconnectableSession.sessionId, this.sessionId, (_a = reconnectRequest.clientReconnectToken) !== null && _a !== void 0 ? _a : Buffer.alloc(0)))) { reconnectSession = reconnectableSession; this.reconnectableSessions.splice(this.reconnectableSessions.indexOf(reconnectSession), 1); break; } } if (!reconnectSession || reconnectSession.isClosed) { const message = 'Requested reconnect session was not found or ' + 'the reconnect token provided by the client was invalid.'; this.trace(trace_1.TraceLevel.Error, trace_1.SshTraceEventIds.serverSessionReconnectFailed, `Reconnect failed: ${message}`); const failure = new transportMessages_1.SessionReconnectFailureMessage(); failure.reasonCode = transportMessages_1.SshReconnectFailureReason.sessionNotFound; failure.description = message; await this.sendMessage(failure, cancellation); return; } const messagesToResend = reconnectSession.protocol.getSentMessages(((_b = reconnectRequest.lastReceivedSequenceNumber) !== null && _b !== void 0 ? _b : 0) + 1); if (!messagesToResend) { // Messages are not available from requested sequence number. // Restore the current session protocol and put the old session back in the collection. this.reconnectableSessions.push(reconnectSession); const message = 'Server is unable to re-send messages requested by the client.'; this.trace(trace_1.TraceLevel.Error, trace_1.SshTraceEventIds.serverSessionReconnectFailed, `Reconnect failed: ${message}`); const failure = new transportMessages_1.SessionReconnectFailureMessage(); failure.reasonCode = transportMessages_1.SshReconnectFailureReason.serverDroppedMessages; failure.description = message; await this.sendMessage(failure, cancellation); return; } const responseMessage = new transportMessages_1.SessionReconnectResponseMessage(); responseMessage.serverReconnectToken = await this.createReconnectToken(reconnectSession.sessionId, this.sessionId); responseMessage.lastReceivedSequenceNumber = reconnectSession.protocol.lastIncomingSequence; await this.sendMessage(responseMessage, cancellation); try { reconnectSession.reconnecting = true; // Ensure the old connection is disconnected before switching over to the new one. (_c = reconnectSession.protocol) === null || _c === void 0 ? void 0 : _c.dispose(); while (reconnectSession.isConnected) { await new Promise((resolve) => setTimeout(() => resolve(), 5)); } // Move this session's protocol instance over to the reconnected session. reconnectSession.protocol = this.protocol; reconnectSession.protocol.kexService = reconnectSession.kexService; this.protocol = undefined; // Re-send the lost messages that the client requested. for (const message of messagesToResend) { await reconnectSession.sendMessage(message, cancellation); } // Now this server session is invalid because the client reconnected to another one. this.dispose(new errors_1.SshConnectionError('Reconnected.', transportMessages_1.SshDisconnectReason.none)); } finally { reconnectSession.reconnecting = false; } this.reconnectableSessions.push(reconnectSession); reconnectSession.metrics.addReconnection(); // Restart the message loop for the reconnected session. reconnectSession.processMessages().catch((e) => { this.trace(trace_1.TraceLevel.Error, trace_1.SshTraceEventIds.unknownError, `Unhandled error processing messages: ${e.message}`, e); }); this.trace(trace_1.TraceLevel.Info, trace_1.SshTraceEventIds.serverSessionReconnecting, `${reconnectSession} reconnected. Re-sent ${messagesToResend.length} dropped messages.`); // Notify event listeners about the successful reconnection. reconnectSession.reconnectedEmitter.fire(); } dispose(error) { if (this.reconnectableSessions) { const index = this.reconnectableSessions.indexOf(this); if (index >= 0) { this.reconnectableSessions.splice(index, 1); } } super.dispose(error); } } exports.SshServerSession = SshServerSession; //# sourceMappingURL=sshServerSession.js.map