"use strict";
//
//  Copyright (c) Microsoft Corporation. All rights reserved.
//
Object.defineProperty(exports, "__esModule", { value: true });
exports.SecureStream = void 0;
const vscode_jsonrpc_1 = require("vscode-jsonrpc");
const streams_1 = require("./streams");
const sshStream_1 = require("./sshStream");
const sshSessionConfiguration_1 = require("./sshSessionConfiguration");
const transportMessages_1 = require("./messages/transportMessages");
const trace_1 = require("./trace");
const sshClientSession_1 = require("./sshClientSession");
const sshServerSession_1 = require("./sshServerSession");
const errors_1 = require("./errors");
const stream_1 = require("stream");
const promiseCompletionSource_1 = require("./util/promiseCompletionSource");
/**
 * Establishes an end-to-end encrypted two-way authenticated data stream over an underlying
 * transport stream, using the SSH protocol but providing simplified interface that is limited to
 * a single duplex stream (channel).
 *
 * This class is a complement to `MultiChannelStream`, which provides only the channel-multiplexing
 * functions of SSH.
 *
 * To establish a secure connection, the two sides first establish an insecure transport stream
 * over a pipe, socket, or anything else. Then they encrypt and authenticate the connection
 * before beginning to send and receive data.
 */
class SecureStream extends stream_1.Duplex {
    /**
     * Creates a new encrypted and authenticated stream over an underlying transport stream.
     * @param transportStream Stream that is used to multiplex all the channels.
     * @param credentials Client or server credentials for authenticating the secure connection.
     * @param reconnectableSessions Optional parameter that enables the stream to be reconnected
     * with a new transport stream after a temporary disconnection. For a stream client it is
     * a boolean value; for a stream server it must be an array.
     */
    constructor(transportStream, credentials, reconnectableSessions) {
        super({
            write(chunk, encoding, cb) {
                this.connectCompletion.promise.then((stream) => {
                    if (!stream) {
                        cb(new errors_1.ObjectDisposedError('SecureStream'));
                    }
                    else {
                        // eslint-disable-next-line no-underscore-dangle
                        stream._write(chunk, encoding, cb);
                    }
                }, cb);
            },
            writev(chunks, cb) {
                this.connectCompletion.promise.then((stream) => {
                    if (!stream) {
                        cb(new errors_1.ObjectDisposedError('SecureStream'));
                    }
                    else {
                        // eslint-disable-next-line no-underscore-dangle
                        stream._writev(chunks, cb);
                    }
                }, cb);
            },
            final(cb) {
                this.connectCompletion.promise.then((stream) => {
                    if (!stream) {
                        cb(new errors_1.ObjectDisposedError('SecureStream'));
                    }
                    else {
                        // eslint-disable-next-line no-underscore-dangle
                        stream._final(cb);
                    }
                }, cb);
            },
            read(size) {
                this.connectCompletion.promise.then((stream) => {
                    if (!stream) {
                        this.push(null); // EOF
                    }
                    else {
                        // eslint-disable-next-line no-underscore-dangle
                        stream._read(size);
                    }
                }, (e) => {
                    // The error will be thrown from the connect() method.
                });
            },
        });
        this.transportStream = transportStream;
        this.clientCredentials = null;
        this.serverCredentials = null;
        this.connectCompletion = new promiseCompletionSource_1.PromiseCompletionSource();
        this.disposed = false;
        this.disposables = [];
        this.disconnectedEmitter = new vscode_jsonrpc_1.Emitter();
        this.onDisconnected = this.disconnectedEmitter.event;
        this.closedEmitter = new vscode_jsonrpc_1.Emitter();
        this.onClosed = this.closedEmitter.event;
        if (!transportStream)
            throw new TypeError('A transport stream is required.');
        if (!credentials)
            throw new TypeError('Client or server credentials are required.');
        const sessionConfig = new sshSessionConfiguration_1.SshSessionConfiguration(true);
        if (reconnectableSessions) {
            sessionConfig.protocolExtensions.push(sshSessionConfiguration_1.SshProtocolExtensionNames.sessionReconnect);
        }
        if ('username' in credentials) {
            if (typeof reconnectableSessions !== 'undefined' &&
                typeof reconnectableSessions !== 'boolean') {
                throw new TypeError('SecureStream client reconnectable sessions must be a boolean.');
            }
            this.clientCredentials = credentials;
            this.session = new sshClientSession_1.SshClientSession(sessionConfig);
        }
        else if (credentials.publicKeys) {
            if (typeof reconnectableSessions !== 'undefined' &&
                !Array.isArray(reconnectableSessions)) {
                throw new TypeError('SecureStream server reconnectable sessions must be an array.');
            }
            this.serverCredentials = credentials;
            this.session = new sshServerSession_1.SshServerSession(sessionConfig, reconnectableSessions);
        }
        else {
            throw new TypeError('Client or server credentials are required.');
        }
        this.session.onDisconnected(this.onSessionDisconnected, this, this.disposables);
        this.session.onClosed(this.onSessionClosed, this, this.disposables);
    }
    get trace() {
        return this.session.trace;
    }
    set trace(trace) {
        this.session.trace = trace;
    }
    get isClosed() {
        return this.disposed || this.session.isClosed;
    }
    onAuthenticating(listener, thisArgs, disposables) {
        return this.session.onAuthenticating(listener, thisArgs, disposables);
    }
    /**
     * Initiates the SSH session over the transport stream by exchanging initial messages with the
     * remote peer. Waits for the protocol version exchange and key exchange. Additional message
     * processing is kicked off as a background promise chain.
     * @param cancellation optional cancellation token.
     */
    async connect(cancellation) {
        let sessionConnected = false;
        try {
            if (this.serverCredentials) {
                const serverSession = this.session;
                serverSession.credentials = this.serverCredentials;
            }
            let stream = this.transportStream;
            if (stream instanceof stream_1.Duplex) {
                stream = new streams_1.NodeStream(stream);
            }
            await this.session.connect(stream, cancellation);
            sessionConnected = true;
            let channel = null;
            if (this.clientCredentials) {
                const clientSession = this.session;
                if (!(await clientSession.authenticateServer(cancellation))) {
                    throw new errors_1.SshConnectionError('Server authentication failed.', transportMessages_1.SshDisconnectReason.hostKeyNotVerifiable);
                }
                if (!(await clientSession.authenticateClient(this.clientCredentials, cancellation))) {
                    throw new errors_1.SshConnectionError('Client authentication failed.', transportMessages_1.SshDisconnectReason.noMoreAuthMethodsAvailable);
                }
                channel = await this.session.openChannel(cancellation);
            }
            else {
                channel = await this.session.acceptChannel(cancellation);
            }
            this.stream = this.createStream(channel);
            // Do not forward the 'readable' event because adding a listener causes a read.
            this.stream.on('data', (data) => this.emit('data', data));
            this.stream.on('end', () => this.emit('end'));
            this.stream.on('close', () => this.emit('close'));
            this.stream.on('error', () => this.emit('error'));
            channel.onClosed(() => this.dispose());
            this.connectCompletion.resolve(this.stream);
        }
        catch (e) {
            if (!(e instanceof Error))
                throw e;
            if (e instanceof errors_1.ObjectDisposedError && this.session.isClosed) {
                // The session was closed while waiting for the channel.
                // This can happen in reconnect scenarios.
                this.connectCompletion.resolve(null);
            }
            else {
                let disconnectReason = e instanceof errors_1.SshConnectionError ? e.reason : undefined;
                disconnectReason !== null && disconnectReason !== void 0 ? disconnectReason : (disconnectReason = transportMessages_1.SshDisconnectReason.protocolError);
                await this.session.close(disconnectReason, e.message, e);
                this.connectCompletion.reject(e);
                throw e;
            }
        }
    }
    /**
     * Re-initiates the SSH session over a NEW transport stream by exchanging initial messages
     * with the remote server. Waits for the secure reconnect handshake to complete. Additional
     * message processing is kicked off as a background task chain.
     *
     * Applies only to a secure stream client. (The secure stream server handles reconnections
     * automatically during the session handshake.)
     */
    async reconnect(transportStream, cancellation) {
        if (!(this.session instanceof sshClientSession_1.SshClientSession)) {
            throw new Error('Cannot reconnect SecureStream server.');
        }
        if (!transportStream)
            throw new TypeError('A transport stream is required.');
        this.transportStream = transportStream;
        let stream = this.transportStream;
        if (stream instanceof stream_1.Duplex) {
            stream = new streams_1.NodeStream(stream);
        }
        await this.session.reconnect(stream, cancellation);
    }
    /**
     * Creates a stream instance for a channel. May be overridden to create a `SshStream` subclass.
     */
    createStream(channel) {
        return new sshStream_1.SshStream(channel);
    }
    dispose() {
        if (!this.disposed) {
            const sessionWasConnected = this.session.isConnected || this.session.isClosed;
            this.disposed = true;
            this.session.dispose();
            this.unsubscribe();
            try {
                // If the session did not connect yet, it doesn't know about the stream and
                // won't dispose it. So dispose it here.
                if (!sessionWasConnected && this.transportStream) {
                    if (this.transportStream instanceof stream_1.Duplex) {
                        this.transportStream.end();
                        this.transportStream.destroy();
                    }
                    else {
                        this.transportStream.close().catch((e) => {
                            this.trace(trace_1.TraceLevel.Error, trace_1.SshTraceEventIds.streamCloseError, `Error closing transport stream: ${e.message}`, e);
                        });
                    }
                }
            }
            catch (e) {
                if (!(e instanceof Error))
                    throw e;
                this.trace(trace_1.TraceLevel.Error, trace_1.SshTraceEventIds.streamCloseError, `Error closing transport stream: ${e.message}`, e);
            }
        }
    }
    async close() {
        if (!this.disposed) {
            this.disposed = true;
            await this.session.close(transportMessages_1.SshDisconnectReason.none, this.session.constructor.name + ' disposed.');
            this.session.dispose();
            this.unsubscribe();
            if (this.transportStream instanceof stream_1.Duplex) {
                await new Promise((resolve) => {
                    this.transportStream.end(resolve);
                });
            }
            else {
                await this.transportStream.close();
            }
        }
    }
    onSessionDisconnected() {
        this.disconnectedEmitter.fire();
    }
    onSessionClosed(e) {
        this.unsubscribe();
        this.closedEmitter.fire(e);
    }
    unsubscribe() {
        this.disposables.forEach((d) => d.dispose());
        this.disposables = [];
    }
}
exports.SecureStream = SecureStream;
//# sourceMappingURL=https://github.com/VSCodium/sourcemaps/releases/download/stable-824c4c46a288b839f13b24022655329c2aeb9f81/node_modules-@microsoft-dev-tunnels-ssh-secureStream.js.map