"use strict";
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
Object.defineProperty(exports, "__esModule", { value: true });
exports.RelayConnectionError = exports.RelayErrorType = exports.isNode = exports.SshHelpers = exports.BrowserWebSocketRelayError = void 0;
const ssh = require("@microsoft/dev-tunnels-ssh");
const websocket_1 = require("websocket");
/**
 * Error class for errors connecting to a web socket in non-node (browser) context.
 * There is no status code or underlying network error info in the browser context.
 */
class BrowserWebSocketRelayError extends Error {
    constructor(message) {
        super(message);
    }
}
exports.BrowserWebSocketRelayError = BrowserWebSocketRelayError;
/**
 * Ssh connection helper
 */
class SshHelpers {
    /**
     * Open a connection to the relay uri depending on the running environment.
     * @param relayUri
     * @param protocols
     * @param headers
     * @param clientConfig
     * @returns
     */
    static openConnection(relayUri, protocols, headers, clientConfig) {
        if ((0, exports.isNode)()) {
            return SshHelpers.nodeSshStreamFactory(relayUri, protocols, headers, clientConfig);
        }
        return SshHelpers.webSshStreamFactory(new WebSocket(relayUri, protocols));
    }
    /**
     * Creates a client SSH session with standard configuration for tunnels.
     * @param configure Optional callback for additional session configuration.
     * @returns The created SSH session.
     */
    static createSshClientSession(configure) {
        return SshHelpers.createSshSession((config) => {
            if (configure)
                configure(config);
            return new ssh.SshClientSession(config);
        });
    }
    /**
     * Creates a SSH server session with standard configuration for tunnels.
     * @param reconnectableSessions Optional list that tracks reconnectable sessions.
     * @param configure Optional callback for additional session configuration.
     * @returns The created SSH session.
     */
    static createSshServerSession(reconnectableSessions, configure) {
        return SshHelpers.createSshSession((config) => {
            if (configure)
                configure(config);
            return new ssh.SshServerSession(config, reconnectableSessions);
        });
    }
    /**
     * Create a websocketStream from a connection.
     * @param connection
     * @returns
     */
    static createWebSocketStreamAdapter(connection) {
        return new ssh.WebSocketStream(new WebsocketStreamAdapter(connection));
    }
    /**
     * Set up a web Ssh stream factory.
     * @param socket
     * @returns
     */
    static webSshStreamFactory(socket) {
        socket.binaryType = 'arraybuffer';
        return new Promise((resolve, reject) => {
            const relayError = 'Failed to connect to relay url';
            socket.onopen = () => {
                resolve(new ssh.WebSocketStream(socket));
            };
            socket.onerror = (e) => {
                // Note: as per web socket guidance https://websockets.spec.whatwg.org/#eventdef-websocket-error,
                // the user agents must not convey extended error information including the cases where the server
                // didn't complete the opening handshake (e.g. because it was not a WebSocket server).
                // So we cannot obtain the response status code.
                // Note: When the socket is connected and an error occurs then `onclose` event occurs after `onerror`.
                // However, the promise is already rejected by `onerror` and we loose this information, hence the
                // timeout helps to give us some info in this scenario.
                setTimeout(() => reject(new BrowserWebSocketRelayError(relayError)), 100);
            };
            socket.onclose = (e) => {
                if (e.code !== 1000) {
                    reject(new BrowserWebSocketRelayError(`${relayError} Code: ${e.code} Reason: ${e.reason}`));
                }
            };
        });
    }
    static createSshSession(factoryCallback) {
        const config = new ssh.SshSessionConfiguration();
        config.keyExchangeAlgorithms.splice(0);
        config.keyExchangeAlgorithms.push(ssh.SshAlgorithms.keyExchange.ecdhNistp384Sha384);
        config.keyExchangeAlgorithms.push(ssh.SshAlgorithms.keyExchange.ecdhNistp256Sha256);
        config.keyExchangeAlgorithms.push(ssh.SshAlgorithms.keyExchange.dhGroup14Sha256);
        return factoryCallback(config);
    }
    static nodeSshStreamFactory(relayUri, protocols, headers, clientConfig) {
        const client = new websocket_1.client(clientConfig);
        return new Promise((resolve, reject) => {
            client.on('connect', (connection) => {
                resolve(new ssh.WebSocketStream(new WebsocketStreamAdapter(connection)));
            });
            // If the server responds but doesn't properly upgrade the connection to web socket, WebSocketClient fires 'httpResponse' event.
            // TODO: Return ProblemDetails from TunnelRelay service
            client.on('httpResponse', ({ statusCode, statusMessage }) => {
                var _a;
                const errorContext = (_a = webSocketClientContexts.find((c) => c.statusCode === statusCode)) !== null && _a !== void 0 ? _a : {
                    statusCode,
                    errorType: RelayErrorType.ServerError,
                    error: `relayConnectionError Server responded with a non-101 status: ${statusCode} ${statusMessage}`,
                };
                reject(new RelayConnectionError(`error.${errorContext.error}`, errorContext));
            });
            // All other failure cases - cannot connect and get the response, or the web socket handshake failed.
            client.on('connectFailed', ({ message }) => {
                var _a;
                if (message && message.startsWith('Error: ')) {
                    message = message.substr(7);
                }
                const errorContext = (_a = webSocketClientContexts.find((c) => c.regex && c.regex.test(message))) !== null && _a !== void 0 ? _a : {
                    // Other errors are most likely connectivity issues.
                    // The original error message may have additional helpful details.
                    errorType: RelayErrorType.ServerError,
                    error: `relayConnectionError ${message}`,
                };
                reject(new RelayConnectionError(`error.${errorContext.error}`, errorContext));
            });
            client.connect(relayUri, protocols, undefined, headers);
        });
    }
}
exports.SshHelpers = SshHelpers;
/**
 * Partially adapts a Node websocket connection object to the browser websocket API,
 * enough so that it can be used as an SSH stream.
 */
class WebsocketStreamAdapter {
    constructor(connection) {
        this.connection = connection;
    }
    get protocol() {
        return this.connection.protocol;
    }
    set onmessage(messageHandler) {
        if (messageHandler) {
            this.connection.on('message', (message) => {
                // This assumes all messages are binary.
                messageHandler({ data: message.binaryData });
            });
        }
        else {
            // Removing event handlers is not implemented.
        }
    }
    set onclose(closeHandler) {
        if (closeHandler) {
            this.connection.on('close', (code, reason) => {
                closeHandler({ code, reason, wasClean: !(code || reason) });
            });
        }
        else {
            // Removing event handlers is not implemented.
        }
    }
    send(data) {
        if (Buffer.isBuffer(data)) {
            this.connection.sendBytes(data);
        }
        else {
            this.connection.sendBytes(Buffer.from(data));
        }
    }
    close(code, reason) {
        if (code || reason) {
            this.connection.drop(code, reason);
        }
        else {
            this.connection.close();
        }
    }
}
/**
 * Helper function to check the running environment.
 */
const isNode = () => typeof process !== 'undefined' &&
    typeof process.release !== 'undefined' &&
    process.release.name === 'node';
exports.isNode = isNode;
/**
 * Type of relay connection error types.
 */
var RelayErrorType;
(function (RelayErrorType) {
    RelayErrorType[RelayErrorType["ConnectionError"] = 1] = "ConnectionError";
    RelayErrorType[RelayErrorType["Unauthorized"] = 2] = "Unauthorized";
    /**
     * @deprecated This relay error type is not used.
     */
    RelayErrorType[RelayErrorType["EndpointNotFound"] = 3] = "EndpointNotFound";
    /**
     * @deprecated This relay error type is not used.
     */
    RelayErrorType[RelayErrorType["ListenerOffline"] = 4] = "ListenerOffline";
    RelayErrorType[RelayErrorType["ServerError"] = 5] = "ServerError";
    RelayErrorType[RelayErrorType["TunnelPortNotFound"] = 6] = "TunnelPortNotFound";
    RelayErrorType[RelayErrorType["TooManyRequests"] = 7] = "TooManyRequests";
    RelayErrorType[RelayErrorType["ServiceUnavailable"] = 8] = "ServiceUnavailable";
    RelayErrorType[RelayErrorType["BadGateway"] = 9] = "BadGateway";
})(RelayErrorType = exports.RelayErrorType || (exports.RelayErrorType = {}));
/**
 * Error used when a connection to the tunnel relay failed.
 */
class RelayConnectionError extends Error {
    constructor(message, errorContext) {
        super(message);
        this.errorContext = errorContext;
    }
}
exports.RelayConnectionError = RelayConnectionError;
/**
 * Web socket client error contexts.
 */
// TODO: Return ProblemDetails from TunnelRelay service.
const webSocketClientContexts = [
    {
        regex: /status: 401/,
        statusCode: 401,
        error: 'relayClientUnauthorized',
        errorType: RelayErrorType.Unauthorized,
    },
    {
        regex: /status: 403/,
        statusCode: 403,
        error: 'relayClientForbidden',
        errorType: RelayErrorType.Unauthorized,
    },
    {
        regex: /status: 404/,
        statusCode: 404,
        error: 'tunnelPortNotFound',
        errorType: RelayErrorType.TunnelPortNotFound,
    },
    {
        regex: /status: 429/,
        statusCode: 429,
        error: 'tooManyRequests',
        errorType: RelayErrorType.TooManyRequests,
    },
    {
        regex: /status: 500/,
        statusCode: 500,
        error: 'relayServerError',
        errorType: RelayErrorType.ServerError,
    },
    {
        regex: /status: 502/,
        statusCode: 502,
        error: 'badGateway',
        errorType: RelayErrorType.BadGateway,
    },
    {
        regex: /status: 503/,
        statusCode: 503,
        error: 'serviceUnavailable',
        errorType: RelayErrorType.ServiceUnavailable,
    },
];
//# sourceMappingURL=https://main.vscode-cdn.net/sourcemaps/6a44c352bd24569c417e530095901b649960f9f8/node_modules/@microsoft/dev-tunnels-connections/sshHelpers.js.map