"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=https://main.vscode-cdn.net/sourcemaps/6a44c352bd24569c417e530095901b649960f9f8/node_modules/@microsoft/dev-tunnels-ssh/sshServerSession.js.map