"use strict";
//
//  Copyright (c) Microsoft Corporation. All rights reserved.
//
Object.defineProperty(exports, "__esModule", { value: true });
exports.SshClientSession = void 0;
const sshSession_1 = require("./sshSession");
const transportMessages_1 = require("./messages/transportMessages");
const sshAuthenticatingEventArgs_1 = require("./events/sshAuthenticatingEventArgs");
const promiseCompletionSource_1 = require("./util/promiseCompletionSource");
const cancellation_1 = require("./util/cancellation");
const sshSessionConfiguration_1 = require("./sshSessionConfiguration");
const authenticationService_1 = require("./services/authenticationService");
const connectionService_1 = require("./services/connectionService");
const errors_1 = require("./errors");
const trace_1 = require("./trace");
/**
 * The client side of an SSH session. Extends the base `SshSession` class to
 * support client authentication.
 */
class SshClientSession extends sshSession_1.SshSession {
    constructor(config) {
        super(config, true);
        this.serviceRequests = new Map();
        this.clientAuthCompletion = null;
    }
    /**
     * Attempts to authenticate both the server and client.
     *
     * This method must be called only after encrypting the session. It is equivalent
     * to calling both `authenticateServer()` and `authenticateClient()` and waiting on
     * both results.
     *
     * @returns `true` if authentication succeeded, `false` if it failed.
     */
    async authenticate(clientCredentials, cancellation) {
        const serverAuthenticated = await this.authenticateServer(cancellation);
        if (!serverAuthenticated) {
            return false;
        }
        const clientAuthenticated = await this.authenticateClient(clientCredentials, cancellation);
        if (!clientAuthenticated) {
            return false;
        }
        return true;
    }
    /**
     * Triggers server authentication by invoking the `authenticating` event with
     * the verified server host key.
     *
     * This method must be called only after encrypting the session. It does not wait for any
     * further message exchange with the server, since the server host key would have already
     * been obtained during the key-exchange.
     *
     * @returns `true` if authentication succeeded, `false` if it failed.
     */
    async authenticateServer(cancellation) {
        if (!(this.kexService && this.kexService.hostKey)) {
            throw new Error('Encrypt the session before authenticating.');
        }
        try {
            // Raise an Authenticating event that allows handlers to do verification
            // of the host key and return a principal for the server.
            this.principal = await this.raiseAuthenticatingEvent(new sshAuthenticatingEventArgs_1.SshAuthenticatingEventArgs(sshAuthenticatingEventArgs_1.SshAuthenticationType.serverPublicKey, {
                publicKey: this.kexService.hostKey,
            }, cancellation));
        }
        catch (e) {
            if (!(e instanceof Error))
                throw e;
            this.trace(trace_1.TraceLevel.Error, trace_1.SshTraceEventIds.authenticationError, `Error while authenticating server: ${e.message}`, e);
            throw e;
        }
        if (!this.principal) {
            await this.close(transportMessages_1.SshDisconnectReason.hostKeyNotVerifiable, 'Server authentication failed.');
            this.trace(trace_1.TraceLevel.Warning, trace_1.SshTraceEventIds.serverAuthenticationFailed, `${this} server authentication failed.`);
            return false;
        }
        this.trace(trace_1.TraceLevel.Info, trace_1.SshTraceEventIds.sessionAuthenticated, `${this} server authenticated.`);
        return true;
    }
    /* @internal */
    authenticateClient(credentials, callbackOrCancellation, cancellation) {
        if (!credentials) {
            throw new TypeError('A credentials object is required.');
        }
        if (typeof callbackOrCancellation === 'function') {
            return this.authenticateClientWithCompletion(credentials, callbackOrCancellation, cancellation);
        }
        else {
            return new Promise((resolve, reject) => this.authenticateClientWithCompletion(credentials, (err, result) => {
                if (err)
                    reject(err);
                else
                    resolve(result);
            }, callbackOrCancellation));
        }
    }
    async authenticateClientWithCompletion(credentials, callback, cancellation) {
        this.clientAuthCompletion = new promiseCompletionSource_1.PromiseCompletionSource();
        this.clientAuthCompletion.promise.then((result) => callback(undefined, result), (err) => callback(err));
        if (cancellation) {
            if (cancellation.isCancellationRequested)
                throw new cancellation_1.CancellationError();
            cancellation.onCancellationRequested((e) => {
                if (this.clientAuthCompletion) {
                    this.clientAuthCompletion.reject(new cancellation_1.CancellationError());
                }
            });
        }
        let authService = this.getService(authenticationService_1.AuthenticationService);
        if (!authService) {
            const serviceRequestMessage = new transportMessages_1.ServiceRequestMessage();
            serviceRequestMessage.serviceName = authenticationService_1.AuthenticationService.serviceName;
            await this.sendMessage(serviceRequestMessage, cancellation);
            // Assume the service request is accepted, without waiting for an accept message.
            // (If not, the following auth requests will fail anyway.)
            authService = this.activateService(authenticationService_1.AuthenticationService);
        }
        await authService.authenticateClient(credentials, cancellation);
    }
    /* @internal */
    onAuthenticationComplete(success) {
        if (success) {
            this.trace(trace_1.TraceLevel.Info, trace_1.SshTraceEventIds.sessionAuthenticated, `${this} client authenticated.`);
        }
        else {
            this.trace(trace_1.TraceLevel.Warning, trace_1.SshTraceEventIds.clientAuthenticationFailed, `${this} client authentication failed.`);
        }
        if (this.clientAuthCompletion) {
            this.clientAuthCompletion.resolve(success);
            this.clientAuthCompletion = null;
        }
    }
    /**
     * Sends a request for a service and waits for a response.
     *
     * @param serviceName Name of the service to be requested.
     * @param cancellation Optional cancellation token.
     * @returns A promise that resolves when the service request has been accepted.
     *
     * If the server does not accept the service request, it will disconnect the session.
     */
    async requestService(serviceName, cancellation) {
        let sendRequest = false;
        let completion = this.serviceRequests.get(serviceName);
        if (!completion) {
            completion = new promiseCompletionSource_1.PromiseCompletionSource();
            this.serviceRequests.set(serviceName, completion);
            sendRequest = true;
        }
        if (sendRequest) {
            const requestMessage = new transportMessages_1.ServiceRequestMessage();
            requestMessage.serviceName = serviceName;
            await this.sendMessage(requestMessage, cancellation);
        }
        await completion.promise;
    }
    /* @internal */
    async handleServiceAcceptMessage(message, cancellation) {
        const completion = this.serviceRequests.get(message.serviceName);
        completion === null || completion === void 0 ? void 0 : completion.resolve(true);
    }
    async openChannel(channelTypeOrOpenMessageOrCancellation, initialRequestOrCancellation, cancellation) {
        if (!this.connectionService) {
            // Authentication must have been skipped, meaning there was no
            // connection service request sent yet. Send it now, and assume
            // it is accepted without waiting for a response.
            const serviceRequestMessage = new transportMessages_1.ServiceRequestMessage();
            serviceRequestMessage.serviceName = connectionService_1.ConnectionService.serviceName;
            await this.sendMessage(serviceRequestMessage, cancellation);
        }
        return await super.openChannel(channelTypeOrOpenMessageOrCancellation, initialRequestOrCancellation, cancellation);
    }
    /* @internal */
    handleDisconnected() {
        if (this.reconnecting) {
            this.reconnecting = false;
            return false;
        }
        return super.handleDisconnected();
    }
    /**
     * Call instead of `connect()` to reconnect to a prior session instead of connecting
     * a new session.
     * @param stream A new stream that has just (re-) connected to the server.
     * @param cancellation Optional cancellation token.
     * @returns True if reconnect succeeded, false if the server declined the reconnect
     * request or reconnect session validation failed. In the case of a false return value,
     * retrying is unlikely to succeed.
     * @throws {SshConnectionError} There was a problem connecting to or communicating with
     * the server; retrying may still succeed if connectivity is restored.
     * @throws {SshReconnectError} Reconnect failed for some reason other than a communication
     * issue: see the `failureReason` property of the error. Retrying is unlikely to succeed,
     * unless the specific error condition can be addressed.
     */
    async reconnect(stream, cancellation) {
        this.trace(trace_1.TraceLevel.Verbose, trace_1.SshTraceEventIds.clientSessionReconnecting, 'Attempting to reconnect...');
        if (this.isClosed) {
            throw new errors_1.ObjectDisposedError(this);
        }
        else if (this.isConnected) {
            throw new Error('Already connected.');
        }
        if (!this.protocol) {
            throw new Error('The session was never previously connected.');
        }
        if (this.reconnecting) {
            throw new Error('Already reconnecting.');
        }
        this.reconnecting = true;
        try {
            await this.reconnectInternal(stream, cancellation);
        }
        finally {
            this.reconnecting = false;
        }
    }
    async reconnectInternal(stream, cancellation) {
        var _a, _b, _c, _d, _e, _f;
        const previousSessionId = this.sessionId;
        const previousProtocolInstance = this.protocol;
        const previousHostKey = (_a = this.kexService) === null || _a === void 0 ? void 0 : _a.hostKey;
        if (!previousSessionId ||
            !previousProtocolInstance ||
            !this.kexService ||
            !previousHostKey ||
            !((_b = previousProtocolInstance.extensions) === null || _b === void 0 ? void 0 : _b.has(sshSessionConfiguration_1.SshProtocolExtensionNames.sessionReconnect))) {
            throw new Error('Reconnect was not enabled for this session.');
        }
        let newSessionId;
        try {
            // Reconnecting will temporarily create a new session ID.
            this.sessionId = null;
            await this.connect(stream, cancellation);
            if (!this.sessionId || !this.algorithms || !this.algorithms.signer) {
                throw new Error('Session is not encrypted.');
            }
            // Ensure the client is not reconnecting to a different server.
            const newHostKey = this.kexService.hostKey;
            const newHostPublicKey = !newHostKey ? null : await newHostKey.getPublicKeyBytes();
            const previousHostPublicKey = await previousHostKey.getPublicKeyBytes();
            if (!newHostPublicKey ||
                !previousHostPublicKey ||
                !newHostPublicKey.equals(previousHostPublicKey)) {
                const message = 'The server host key is different.';
                this.trace(trace_1.TraceLevel.Error, trace_1.SshTraceEventIds.clientSessionReconnectFailed, `Reconnection failed: ${message}`);
                throw new errors_1.SshReconnectError(message, transportMessages_1.SshReconnectFailureReason.differentServerHostKey);
            }
            newSessionId = this.sessionId;
        }
        catch (e) {
            // Restore the previous protocol instance so reconnect may be attempted again.
            this.protocol = previousProtocolInstance;
            super.handleDisconnected();
            throw e;
        }
        finally {
            // Restore the previous session ID and host key for the reconnected session.
            this.sessionId = previousSessionId;
            this.kexService.hostKey = previousHostKey;
        }
        const reconnectToken = await this.createReconnectToken(previousSessionId, newSessionId);
        const reconnectRequest = new transportMessages_1.SessionReconnectRequestMessage();
        reconnectRequest.requestType = "session-reconnect@microsoft.com" /* ExtensionRequestTypes.sessionReconnect */;
        reconnectRequest.clientReconnectToken = reconnectToken;
        reconnectRequest.lastReceivedSequenceNumber = previousProtocolInstance.lastIncomingSequence;
        reconnectRequest.wantReply = true;
        const response = await this.requestResponse(reconnectRequest, transportMessages_1.SessionReconnectResponseMessage, transportMessages_1.SessionReconnectFailureMessage, cancellation);
        if (response instanceof transportMessages_1.SessionReconnectFailureMessage) {
            const reason = (_c = response.reasonCode) !== null && _c !== void 0 ? _c : transportMessages_1.SshReconnectFailureReason.unknownServerFailure;
            const message = (_d = response.description) !== null && _d !== void 0 ? _d : 'The server rejected the reconnect request.';
            this.trace(trace_1.TraceLevel.Error, trace_1.SshTraceEventIds.clientSessionReconnectFailed, `Reconnection failed: ${message}`);
            // Restore the previous protocol instance so reconnect may be attempted again.
            this.protocol = previousProtocolInstance;
            throw new errors_1.SshReconnectError(message, reason);
        }
        if (!this.verifyReconnectToken(previousSessionId, newSessionId, (_e = response.serverReconnectToken) !== null && _e !== void 0 ? _e : Buffer.alloc(0))) {
            const message = 'The reconnect token provided by the server was invalid.';
            this.trace(trace_1.TraceLevel.Error, trace_1.SshTraceEventIds.clientSessionReconnectFailed, `Reconnection failed: ${message}`);
            throw new errors_1.SshReconnectError(message, transportMessages_1.SshReconnectFailureReason.invalidServerReconnectToken);
        }
        this.trace(trace_1.TraceLevel.Info, trace_1.SshTraceEventIds.clientSessionReconnecting, 'Reconnect request was accepted by the server.');
        // Re-send lost messages.
        const messagesToResend = previousProtocolInstance.getSentMessages(((_f = response.lastReceivedSequenceNumber) !== null && _f !== void 0 ? _f : 0) + 1);
        if (!messagesToResend) {
            const message = 'Client is unable to re-send messages requested by the server.';
            this.trace(trace_1.TraceLevel.Error, trace_1.SshTraceEventIds.clientSessionReconnectFailed, `Reconnection failed: ${message}`);
            throw new errors_1.SshReconnectError(message, transportMessages_1.SshReconnectFailureReason.clientDroppedMessages);
        }
        let count = 0;
        for (const message of messagesToResend) {
            await this.sendMessage(message, cancellation);
            count++;
        }
        // Now the session is fully reconnected!
        previousProtocolInstance.dispose();
        this.metrics.addReconnection();
        this.trace(trace_1.TraceLevel.Info, trace_1.SshTraceEventIds.clientSessionReconnecting, `${this} reconnected. Re-sent ${count} dropped messages.`);
    }
    dispose() {
        if (this.clientAuthCompletion) {
            this.clientAuthCompletion.reject(new errors_1.ObjectDisposedError(this));
        }
        super.dispose();
    }
}
exports.SshClientSession = SshClientSession;
//# sourceMappingURL=https://main.vscode-cdn.net/sourcemaps/8761a5560cfd65fdd19ce7e2bd18dab5c0a4d84e/node_modules/@microsoft/dev-tunnels-ssh/sshClientSession.js.map