"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.connectThroughAgent = connectThroughAgent;
exports.createSocks5Tunnel = createSocks5Tunnel;
const events_1 = require("events");
const proxy_options_1 = require("./proxy-options");
const agent_1 = require("./agent");
const server_1 = __importDefault(require("@mongodb-js/socksv5/lib/server"));
const None_1 = __importDefault(require("@mongodb-js/socksv5/lib/auth/None"));
const UserPassword_1 = __importDefault(require("@mongodb-js/socksv5/lib/auth/UserPassword"));
const util_1 = require("util");
const net_1 = require("net");
const crypto_1 = __importDefault(require("crypto"));
const randomBytes = (0, util_1.promisify)(crypto_1.default.randomBytes);
function getTunnelOptions(config) {
    return {
        proxyHost: '127.0.0.1',
        proxyPort: 0,
        proxyUsername: undefined,
        proxyPassword: undefined,
        ...config,
    };
}
function createFakeHttpClientRequest(dstAddr, dstPort, overrideProtocol) {
    const headers = Object.create(null);
    headers.host = `${(0, net_1.isIPv6)(dstAddr) ? `[${dstAddr}]` : dstAddr}:${dstPort}`;
    headers.upgrade = 'websocket';
    return Object.assign(new events_1.EventEmitter().setMaxListeners(Infinity), {
        host: headers.host,
        protocol: 'http',
        method: 'GET',
        path: '/',
        getHeader(name) {
            return headers[name];
        },
        setHeader(name, value) {
            headers[name] = value;
        },
        _implicitHeader() {
        },
        overrideProtocol,
    });
}
async function connectThroughAgent({ dstAddr, dstPort, agent, overrideProtocol, }) {
    const channel = await new Promise((resolve, reject) => {
        const req = createFakeHttpClientRequest(dstAddr, dstPort, overrideProtocol);
        req.on('error', reject);
        const done = (error, sock) => {
            req.off('error', reject);
            if (error)
                reject(error);
            else if (sock)
                resolve(sock);
            else
                reject(new Error('Received neither error object nor socket from agent.createSocket()'));
        };
        req.onSocket = (sock, err) => {
            done(err, sock);
        };
        agent.createSocket(req, {
            host: dstAddr,
            port: dstPort,
        }, (err, sock) => {
            done(err, sock);
        });
    });
    if (!channel)
        throw new Error(`Could not create channel to ${dstAddr}:${dstPort}`);
    return channel;
}
class Socks5Server extends events_1.EventEmitter {
    logger = new events_1.EventEmitter().setMaxListeners(Infinity);
    agent;
    server;
    serverListen;
    serverClose;
    connections = new Set();
    rawConfig;
    generateCredentials;
    overrideProtocol;
    closed = true;
    agentInitialized = false;
    agentInitPromise;
    constructor(agent, tunnelOptions, generateCredentials, overrideProtocol) {
        super();
        this.setMaxListeners(Infinity);
        this.agent = agent;
        this.generateCredentials = generateCredentials;
        this.overrideProtocol = overrideProtocol;
        if (agent.logger)
            this.logger = agent.logger;
        agent.on?.('error', (err) => this.emit('forwardingError', err));
        this.rawConfig = getTunnelOptions(tunnelOptions);
        this.server = server_1.default.createServer(this.socks5Request.bind(this));
        this.serverListen = (0, util_1.promisify)(this.server.listen.bind(this.server));
        this.serverClose = (0, util_1.promisify)(this.server.close.bind(this.server));
        for (const eventName of ['close', 'error', 'listening']) {
            this.server.on(eventName, this.emit.bind(this, eventName));
        }
    }
    get config() {
        const serverAddress = this.server.address();
        return {
            ...this.rawConfig,
            proxyPort: (typeof serverAddress !== 'string' && serverAddress?.port) ||
                this.rawConfig.proxyPort,
        };
    }
    async listen() {
        this.closed = false;
        if (this.generateCredentials) {
            const credentialsSource = await randomBytes(64);
            this.rawConfig = {
                ...this.rawConfig,
                proxyUsername: credentialsSource.slice(0, 32).toString('base64url'),
                proxyPassword: credentialsSource.slice(32).toString('base64url'),
            };
        }
        if (this.rawConfig.proxyUsername) {
            this.server.useAuth((0, UserPassword_1.default)((user, pass, cb) => {
                const success = this.rawConfig.proxyUsername === user &&
                    this.rawConfig.proxyPassword === pass;
                this.logger.emit('socks5:authentication-complete', { success });
                queueMicrotask(() => cb(success));
            }));
        }
        else {
            this.logger.emit('socks5:skip-auth-setup');
            this.server.useAuth((0, None_1.default)());
        }
        const { proxyHost, proxyPort } = this.rawConfig;
        this.logger.emit('socks5:start-listening', { proxyHost, proxyPort });
        const listeningPromise = this.serverListen(proxyPort, proxyHost);
        try {
            await Promise.all([
                listeningPromise,
                (0, events_1.once)(this, 'listening'),
                this.ensureAgentInitialized(),
            ]);
            this.agentInitialized = true;
        }
        catch (err) {
            await this.close();
            throw err;
        }
    }
    async ensureAgentInitialized() {
        if (this.agentInitialized) {
            return;
        }
        if (this.agentInitPromise) {
            return this.agentInitPromise;
        }
        if (this.closed) {
            throw new Error('Disconnected.');
        }
        try {
            await (this.agentInitPromise = this.agent.initialize?.());
        }
        catch (err) {
            this.emit('forwardingError', err);
            this.logger.emit('socks5:forwarding-error', {
                error: err?.stack ?? String(err),
            });
            delete this.agentInitPromise;
            await this.serverClose();
            throw err;
        }
        delete this.agentInitPromise;
        this.agentInitialized = true;
        this.logger.emit('socks5:agent-initialized');
    }
    async closeOpenConnections() {
        const waitForClose = [];
        for (const socket of this.connections) {
            waitForClose.push((0, events_1.once)(socket, 'close'));
            socket.destroy();
        }
        await Promise.all(waitForClose);
        this.connections.clear();
    }
    async close() {
        if (this.closed)
            return;
        this.closed = true;
        this.logger.emit('socks5:closing-tunnel');
        const [maybeError] = await Promise.all([
            this.serverClose().catch((e) => e),
            this.agent.destroy?.(),
            this.closeOpenConnections(),
        ]);
        if (maybeError &&
            !('code' in maybeError && maybeError.code === 'ERR_SERVER_NOT_RUNNING')) {
            throw maybeError;
        }
    }
    async forwardOut(dstAddr, dstPort) {
        return await connectThroughAgent({
            dstAddr,
            dstPort,
            agent: this.agent,
            overrideProtocol: this.overrideProtocol,
        });
    }
    async socks5Request(info, accept, deny) {
        const { srcAddr, srcPort, dstAddr, dstPort } = info;
        const logMetadata = { srcAddr, srcPort, dstAddr, dstPort };
        this.logger.emit('socks5:got-forwarding-request', { ...logMetadata });
        let socket = null;
        try {
            await this.ensureAgentInitialized();
            const channel = await this.forwardOut(dstAddr, dstPort);
            this.logger.emit('socks5:accepted-forwarding-request', {
                ...logMetadata,
            });
            socket = accept(true);
            this.connections.add(socket);
            const forwardingErrorHandler = (err) => {
                if (!socket?.writableEnded)
                    socket?.end();
                if (!channel?.writableEnded)
                    channel?.end();
                err.origin ??= 'connection';
                this.logger.emit('socks5:forwarding-error', {
                    ...logMetadata,
                    error: String(err.stack),
                });
                this.emit('forwardingError', err);
            };
            channel.on('error', forwardingErrorHandler);
            socket.on('error', forwardingErrorHandler);
            socket.once('close', () => {
                if (!channel?.destroyed)
                    channel.destroy();
                this.logger.emit('socks5:forwarded-socket-closed', { ...logMetadata });
                this.connections.delete(socket);
            });
            channel.once('close', () => {
                if (!socket?.destroyed)
                    socket?.destroy();
            });
            socket.pipe(channel).pipe(socket);
        }
        catch (err) {
            this.emit('socks5:failed-forwarding-request', {
                ...logMetadata,
                error: String(err.stack),
            });
            this.emit('forwardingError', err);
            deny();
            if (socket) {
                err.origin = 'ssh-client';
                socket.destroy(err);
            }
        }
    }
}
class ExistingTunnel extends events_1.EventEmitter {
    logger = new events_1.EventEmitter().setMaxListeners(Infinity);
    config;
    constructor(config) {
        super();
        this.setMaxListeners(Infinity);
        this.config = config;
    }
    async listen() {
    }
    async close() {
    }
}
function createSocks5Tunnel(proxyOptions, tunnelOptions, target) {
    const socks5OnlyProxyOptions = (0, proxy_options_1.getSocks5OnlyProxyOptions)(('proxyOptions' in proxyOptions
        ? proxyOptions.proxyOptions
        : proxyOptions), target);
    if (socks5OnlyProxyOptions) {
        return new ExistingTunnel(socks5OnlyProxyOptions);
    }
    const agent = (0, agent_1.useOrCreateAgent)(proxyOptions, target, true);
    if (!agent)
        return undefined;
    let generateCredentials = false;
    if (tunnelOptions === 'generate-credentials') {
        tunnelOptions = {};
        generateCredentials = true;
    }
    return new Socks5Server(agent, { ...tunnelOptions }, generateCredentials, target ? new URL(target).protocol : undefined);
}
//# sourceMappingURL=socks5.js.map