"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;
    }
    async handleDisconnectMessage(message) {
        var _a;
        const description = message.description || 'Received disconnect message.';
        await this.close((_a = message.reasonCode) !== null && _a !== void 0 ? _a : transportMessages_1.SshDisconnectReason.none, description);
    }
    dispose(error) {
        var _a, _b;
        const closedError = error !== null && error !== void 0 ? error : (this.closedError instanceof errors_1.SshConnectionError
            ? this.closedError
            : new errors_1.SshConnectionError(this.constructor.name + ' disposed.'));
        if (!this.disposed) {
            this.trace(trace_1.TraceLevel.Info, trace_1.SshTraceEventIds.sessionClosing, `${this} disposed.`);
            this.disposed = true;
            // Dispose keep-alive timer
            if (this.keepAliveTimer) {
                clearTimeout(this.keepAliveTimer);
                this.keepAliveTimer = undefined;
            }
            this.closedEmitter.fire(new sshSessionClosedEventArgs_1.SshSessionClosedEventArgs(transportMessages_1.SshDisconnectReason.none, closedError.message, closedError));
        }
        // Cancel any pending requests.
        this.invokeRequestHandler(undefined, undefined, closedError);
        this.metrics.close();
        // Dispose the connection service before other services, to ensure
        // channels are disposed before services that work with them.
        (_a = this.connectionService) === null || _a === void 0 ? void 0 : _a.dispose();
        for (const service of this.activatedServices.values()) {
            if (service !== this.connectionService) {
                service.dispose();
            }
        }
        this.activatedServices.clear();
        (_b = this.protocol) === null || _b === void 0 ? void 0 : _b.dispose();
        this.protocol = undefined;
    }
    /* @internal */
    async enableReconnect(cancellation) {
        var _a;
        try {
            // Ensure no other messages are sent in the middle of turning this on.
            await this.blockedMessagesSemaphore.wait();
            // This should not be done during a key-exchange, however that should never
            // be the case since the EnableSessionReconnectRequest is sent in response
            // to an ExtensionInfo message which is sent in response to a NewKeys message.
            // So a key exchange just finished and won't be restarted again soon.
            if ((_a = this.kexService) === null || _a === void 0 ? void 0 : _a.exchanging) {
                this.trace(trace_1.TraceLevel.Error, trace_1.SshTraceEventIds.sessionReconnectInitFailed, 'Failed to initialize session reconnect because a key-exchange was in-progress.');
            }
            else {
                // Send the message indicating reconnect message tracking is starting.
                const enableReconnectMessage = new transportMessages_1.SessionRequestMessage("enable-session-reconnect@microsoft.com" /* ExtensionRequestTypes.enableSessionReconnect */, false);
                await this.protocol.sendMessage(enableReconnectMessage, cancellation);
                // Start using the protocol extensions that include an extra field or two
                // with every sent message.
                if (this.protocol) {
                    this.protocol.outgoingMessagesHaveReconnectInfo = true;
                    this.protocol.outgoingMessagesHaveLatencyInfo = this.protocol.extensions.has(sshSessionConfiguration_1.SshProtocolExtensionNames.sessionLatency);
                }
            }
            this.blockedMessagesSemaphore.release();
        }
        catch (e) {
            // This is not in a finally block because the semaphore must be released before
            // the call to close() which tries to send a message.
            this.blockedMessagesSemaphore.release();
            if (e instanceof Error) {
                await this.close(transportMessages_1.SshDisconnectReason.protocolError, undefined, e);
            }
            throw e;
        }
    }
    /* @internal */
    async createReconnectToken(previousSessionId, newSessionId) {
        // To generate the reconnect token, combine the old session ID and new (re-negotiated)
        // session ID and sign the result using the new negotiated HMAC algorithm and key. This
        // proves that the old (secret) session ID is known while not disclosing it, and also
        // prevents replay attacks.
        const reconnectToken = await this.algorithms.signer.sign(buffer_1.Buffer.concat([previousSessionId, newSessionId]));
        return reconnectToken;
    }
    /* @internal */
    async verifyReconnectToken(previousSessionId, newSessionId, reconnectToken) {
        const result = await this.algorithms.verifier.verify(buffer_1.Buffer.concat([previousSessionId, newSessionId]), reconnectToken);
        return result;
    }
    /**
     * Pipes one SSH session into another, relaying all data between them.
     *
     * Any new channels opened from the remote side of either session will be piped into a
     * new channel in the other session. Any channels opened before connecting the session pipe,
     * or any channels opened from the local side, will not be piped.
     *
     * @param toSession Session to which the current session will be connected via the pipe.
     * @returns A promise that resolves when the sessions are closed.
     */
    pipe(toSession) {
        return pipeExtensions_1.PipeExtensions.pipeSession(this, toSession);
    }
    toString() {
        return this.constructor.name;
    }
}
exports.SshSession = SshSession;
SshSession.localVersion = sshVersionInfo_1.SshVersionInfo.getLocalVersion();
//# sourceMappingURL=https://main.vscode-cdn.net/sourcemaps/6a44c352bd24569c417e530095901b649960f9f8/node_modules/@microsoft/dev-tunnels-ssh/sshSession.js.map