@microsoft/dev-tunnels-ssh
Version:
SSH library for Dev Tunnels
175 lines • 9.45 kB
JavaScript
"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