"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.SSHAgent = void 0;
const agent_base_1 = require("agent-base");
const events_1 = __importStar(require("events"));
const fs_1 = require("fs");
const util_1 = require("util");
const tls_1 = require("tls");
const crypto_1 = require("crypto");
function ssh2() {
    if ((0, crypto_1.getFips)()) {
        throw new Error('devtools-proxy-support: Using `ssh2` features in FIPS mode is currently not available');
    }
    return require('ssh2');
}
class SSHAgent extends agent_base_1.Agent {
    logger;
    proxyOptions;
    url;
    sshClient;
    connected = false;
    connectingPromise;
    closed = false;
    forwardOut;
    constructor(options, logger) {
        super();
        this.on?.('error', () => {
        });
        this.logger = logger ?? new events_1.default().setMaxListeners(Infinity);
        this.proxyOptions = options;
        this.url = new URL(options.proxy ?? '');
        this.sshClient = new (ssh2().Client)();
        this.sshClient.on('close', () => {
            this.logger.emit('ssh:client-closed');
            this.connected = false;
        });
        this.forwardOut = (0, util_1.promisify)(this.sshClient.forwardOut.bind(this.sshClient));
    }
    async initialize() {
        if (this.connected) {
            return;
        }
        if (this.connectingPromise) {
            return this.connectingPromise;
        }
        if (this.closed) {
            throw new Error('Disconnected.');
        }
        const sshConnectConfig = {
            readyTimeout: 20000,
            keepaliveInterval: 20000,
            host: decodeURIComponent(this.url.hostname),
            port: +this.url.port || 22,
            username: decodeURIComponent(this.url.username) || undefined,
            password: decodeURIComponent(this.url.password) || undefined,
            privateKey: this.proxyOptions.sshOptions?.identityKeyFile
                ? await fs_1.promises.readFile(this.proxyOptions.sshOptions.identityKeyFile)
                : undefined,
            passphrase: this.proxyOptions.sshOptions?.identityKeyPassphrase,
        };
        this.logger.emit('ssh:establishing-conection', {
            host: sshConnectConfig.host,
            port: sshConnectConfig.port,
            password: !!sshConnectConfig.password,
            privateKey: !!sshConnectConfig.privateKey,
            passphrase: !!sshConnectConfig.passphrase,
        });
        this.connectingPromise = Promise.race([
            (0, events_1.once)(this.sshClient, 'error').then(([err]) => {
                throw err;
            }),
            (() => {
                const waitForReady = (0, events_1.once)(this.sshClient, 'ready').then(() => undefined);
                this.sshClient.connect(sshConnectConfig);
                return waitForReady;
            })(),
        ]);
        try {
            await this.connectingPromise;
        }
        catch (err) {
            this.emit?.('error', err);
            this.logger.emit('ssh:failed-connection', {
                error: err?.stack ?? String(err),
            });
            delete this.connectingPromise;
            throw err;
        }
        delete this.connectingPromise;
        this.connected = true;
        this.logger.emit('ssh:established-connection');
    }
    async connect(req, connectOpts) {
        return await this._connect(req, connectOpts);
    }
    async _connect(req, connectOpts, retriesLeft = 1) {
        let host = '';
        try {
            host = connectOpts.host || req.getHeader('host');
            const url = new URL(req.path, `tcp://${host}:${connectOpts.port}`);
            await this.initialize();
            let sock = await this.forwardOut('127.0.0.1', 0, url.hostname, +url.port);
            sock.setTimeout ??= function () {
                return this;
            };
            if (connectOpts.secureEndpoint) {
                sock = (0, tls_1.connect)({
                    ...this.proxyOptions,
                    ...connectOpts,
                    socket: sock,
                });
            }
            return sock;
        }
        catch (err) {
            const retryableError = /Not connected|Channel open failure/.test(err.message);
            this.logger.emit('ssh:failed-forward', {
                host,
                error: String(err.stack),
                retryableError,
                retriesLeft,
            });
            if (retryableError) {
                this.connected = false;
                if (retriesLeft > 0) {
                    await this.initialize();
                    return await this._connect(req, connectOpts, retriesLeft - 1);
                }
            }
            throw err;
        }
    }
    destroy() {
        this.closed = true;
        this.sshClient.end();
    }
    async interruptForTesting() {
        this.sshClient.end();
        await (0, events_1.once)(this.sshClient, 'close');
    }
}
exports.SSHAgent = SSHAgent;
//# sourceMappingURL=ssh.js.map