"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createComboServer = void 0;
const _ = require("lodash");
const now = require("performance-now");
const net = require("net");
const tls = require("tls");
const semver = require("semver");
const destroyable_server_1 = require("destroyable-server");
const httpolyglot = require("@httptoolkit/httpolyglot");
const read_tls_client_hello_1 = require("read-tls-client-hello");
const tls_1 = require("../util/tls");
const util_1 = require("../util/util");
const socket_util_1 = require("../util/socket-util");
// Hardcore monkey-patching: force TLSSocket to link servername & remoteAddress to
// sockets as soon as they're available, without waiting for the handshake to fully
// complete, so we can easily access them if the handshake fails.
const originalSocketInit = tls.TLSSocket.prototype._init;
tls.TLSSocket.prototype._init = function () {
    originalSocketInit.apply(this, arguments);
    const tlsSocket = this;
    const { _handle } = tlsSocket;
    if (!_handle)
        return;
    const loadSNI = _handle.oncertcb;
    _handle.oncertcb = function (info) {
        tlsSocket.servername = info.servername;
        tlsSocket.initialRemoteAddress = tlsSocket.remoteAddress || // Normal case
            tlsSocket._parent?.remoteAddress || // For early failing sockets
            tlsSocket._handle?._parentWrap?.stream?.remoteAddress; // For HTTP/2 CONNECT
        tlsSocket.initialRemotePort = tlsSocket.remotePort ||
            tlsSocket._parent?.remotePort ||
            tlsSocket._handle?._parentWrap?.stream?.remotePort;
        return loadSNI?.apply(this, arguments);
    };
};
// Takes an established TLS socket, calls the error listener if it's silently closed
function ifTlsDropped(socket, errorCallback) {
    new Promise((resolve, reject) => {
        // If you send data, you trust the TLS connection
        socket.once('data', resolve);
        // If you silently close it very quicky, you probably don't trust us
        socket.once('error', reject);
        socket.once('close', reject);
        socket.once('end', reject);
        // Some clients open later-unused TLS connections for connection pools, preconnect, etc.
        // Even if these are shut later on, that doesn't mean they're are rejected connections.
        // To differentiate the two cases, we consider connections OK after waiting 10x longer
        // than the initial TLS handshake for an unhappy disconnection.
        const timing = socket.__timingInfo;
        const tlsSetupDuration = timing
            ? timing.tlsConnectedTimestamp - (timing.tunnelSetupTimestamp || timing.initialSocketTimestamp)
            : 0;
        const maxTlsRejectionTime = !Object.is(tlsSetupDuration, NaN)
            ? Math.max(tlsSetupDuration * 10, 100) // Ensure a sensible minimum
            : 2000;
        (0, util_1.delay)(maxTlsRejectionTime).then(resolve);
    })
        .then(() => {
        // Mark the socket as having completed TLS setup - this ensures that future
        // errors fire as client errors, not TLS setup errors.
        socket.tlsSetupCompleted = true;
    })
        .catch(() => {
        // If TLS setup was confirmed in any way, we know we don't have a TLS error.
        if (socket.tlsSetupCompleted)
            return;
        // To get here, the socket must have connected & done the TLS handshake, but then
        // closed/ended without ever sending any data. We can fairly confidently assume
        // in that case that it's rejected our certificate.
        errorCallback();
    });
}
function getCauseFromError(error) {
    const cause = (/alert certificate/.test(error.message) ||
        /alert bad certificate/.test(error.message) ||
        error.code === 'ERR_SSL_SSLV3_ALERT_BAD_CERTIFICATE' ||
        /alert unknown ca/.test(error.message))
        // The client explicitly told us it doesn't like the certificate
        ? 'cert-rejected'
        : /no shared cipher/.test(error.message)
            // The client refused to negotiate a cipher. Probably means it didn't like the
            // cert so refused to continue, but it could genuinely not have a shared cipher.
            ? 'no-shared-cipher'
            : (/ECONNRESET/.test(error.message) || error.code === 'ECONNRESET')
                // The client sent no TLS alert, it just hard RST'd the connection
                ? 'reset'
                : error.code === 'ERR_TLS_HANDSHAKE_TIMEOUT'
                    ? 'handshake-timeout'
                    : 'unknown'; // Something else.
    if (cause === 'unknown')
        console.log('Unknown TLS error:', error);
    return cause;
}
function buildTlsError(socket, cause) {
    const eventData = (0, socket_util_1.buildSocketEventData)(socket);
    eventData.failureCause = cause;
    eventData.timingEvents.failureTimestamp = now();
    return eventData;
}
// The low-level server that handles all the sockets & TLS. The server will correctly call the
// given handler for both HTTP & HTTPS direct connections, or connections when used as an
// either HTTP or HTTPS proxy, all on the same port.
async function createComboServer(options, requestListener, tlsClientErrorListener, tlsPassthroughListener) {
    let server;
    if (!options.https) {
        server = httpolyglot.createServer(requestListener);
    }
    else {
        const ca = await (0, tls_1.getCA)(options.https);
        const defaultCert = ca.generateCertificate(options.https.defaultDomain ?? 'localhost');
        const serverProtocolPreferences = options.http2 === true
            ? ['h2', 'http/1.1', 'http 1.1'] // 'http 1.1' is non-standard, but used by https-proxy-agent
            : options.http2 === 'fallback'
                ? ['http/1.1', 'http 1.1', 'h2']
                // options.http2 === false:
                : ['http/1.1', 'http 1.1'];
        const ALPNOption = semver.satisfies(process.version, '>=20.4.0')
            ? {
                // In modern Node (20+), ALPNProtocols will reject unknown protocols. To allow those (so we can
                // at least read the request, and hopefully handle HTTP-like cases - not uncommon) we use the new
                // ALPNCallback feature instead, which lets us dynamically accept unrecognized protocols:
                ALPNCallback: ({ protocols: clientProtocols }) => {
                    const preferredProtocol = serverProtocolPreferences.find(p => clientProtocols.includes(p));
                    // Wherever possible, we tell the client to use our preferred protocol
                    if (preferredProtocol)
                        return preferredProtocol;
                    // If the client only offers protocols that we don't understand, shrug and accept:
                    else
                        return clientProtocols[1];
                }
            } : {
            // In Node versions without ALPNCallback, we just set preferences directly:
            ALPNProtocols: serverProtocolPreferences
        };
        const tlsServer = tls.createServer({
            key: defaultCert.key,
            cert: defaultCert.cert,
            ca: [defaultCert.ca],
            ...ALPNOption,
            SNICallback: (domain, cb) => {
                if (options.debug)
                    console.log(`Generating certificate for ${domain}`);
                try {
                    const generatedCert = ca.generateCertificate(domain);
                    cb(null, tls.createSecureContext({
                        key: generatedCert.key,
                        cert: generatedCert.cert,
                        ca: generatedCert.ca
                    }));
                }
                catch (e) {
                    console.error('Cert generation error', e);
                    cb(e);
                }
            }
        });
        analyzeAndMaybePassThroughTls(tlsServer, options.https.tlsPassthrough ?? [], tlsPassthroughListener);
        server = httpolyglot.createServer(tlsServer, requestListener);
    }
    // In Node v20, this option was added, rejecting all requests with no host header. While that's good, in
    // our case, we want to handle the garbage requests too, so we disable it:
    server._httpServer.requireHostHeader = false;
    server.on('connection', (socket) => {
        socket.__timingInfo = socket.__timingInfo || (0, socket_util_1.buildSocketTimingInfo)();
        // All sockets are initially marked as using unencrypted upstream connections.
        // If TLS is used, this is upgraded to 'true' by secureConnection below.
        socket.__lastHopEncrypted = false;
        // For actual sockets, set NODELAY to avoid any buffering whilst streaming. This is
        // off by default in Node HTTP, but likely to be enabled soon & is default in curl.
        if ('setNoDelay' in socket)
            socket.setNoDelay(true);
    });
    server.on('secureConnection', (socket) => {
        const parentSocket = (0, socket_util_1.getParentSocket)(socket);
        if (parentSocket) {
            // Sometimes wrapper TLS sockets created by the HTTP/2 server don't include the
            // underlying socket details, so it's better to make sure we copy them up.
            copyAddressDetails(parentSocket, socket);
            copyTimingDetails(parentSocket, socket);
            // With TLS metadata, we only propagate directly from parent sockets, not through
            // CONNECT etc - we only want it if the final hop is TLS, previous values don't matter.
            socket.__tlsMetadata ?? (socket.__tlsMetadata = parentSocket.__tlsMetadata);
        }
        else if (!socket.__timingInfo) {
            socket.__timingInfo = (0, socket_util_1.buildSocketTimingInfo)();
        }
        socket.__timingInfo.tlsConnectedTimestamp = now();
        socket.__lastHopEncrypted = true;
        ifTlsDropped(socket, () => {
            tlsClientErrorListener(socket, buildTlsError(socket, 'closed'));
        });
    });
    // Mark HTTP/2 sockets as set up once we receive a first settings frame. This always
    // happens immediately after the connection preface, as long as the connection is OK.
    server.on('session', (session) => {
        session.once('remoteSettings', () => {
            session.socket.tlsSetupCompleted = true;
        });
    });
    server.on('tlsClientError', (error, socket) => {
        tlsClientErrorListener(socket, buildTlsError(socket, getCauseFromError(error)));
    });
    // If the server receives a HTTP/HTTPS CONNECT request, Pretend to tunnel, then just re-handle:
    server.addListener('connect', function (req, resOrSocket) {
        if (resOrSocket instanceof net.Socket) {
            handleH1Connect(req, resOrSocket);
        }
        else {
            handleH2Connect(req, resOrSocket);
        }
    });
    function handleH1Connect(req, socket) {
        // Clients may disconnect at this point (for all sorts of reasons), but here
        // nothing else is listening, so we need to catch errors on the socket:
        socket.once('error', (e) => console.log('Error on client socket', e));
        const connectUrl = req.url || req.headers['host'];
        if (!connectUrl) {
            // If we can't work out where to go, send an error.
            socket.write('HTTP/' + req.httpVersion + ' 400 Bad Request\r\n\r\n', 'utf-8');
            return;
        }
        if (options.debug)
            console.log(`Proxying HTTP/1 CONNECT to ${connectUrl}`);
        socket.write('HTTP/' + req.httpVersion + ' 200 OK\r\n\r\n', 'utf-8', () => {
            socket.__timingInfo.tunnelSetupTimestamp = now();
            socket.__lastHopConnectAddress = connectUrl;
            server.emit('connection', socket);
        });
    }
    function handleH2Connect(req, res) {
        const connectUrl = req.headers[':authority'];
        if (!connectUrl) {
            // If we can't work out where to go, send an error.
            res.writeHead(400, {});
            res.end();
            return;
        }
        if (options.debug)
            console.log(`Proxying HTTP/2 CONNECT to ${connectUrl}`);
        // Send a 200 OK response, and start the tunnel:
        res.writeHead(200, {});
        copyAddressDetails(res.socket, res.stream);
        copyTimingDetails(res.socket, res.stream);
        res.stream.__lastHopConnectAddress = connectUrl;
        // When layering HTTP/2 on JS streams, we have to make sure the JS stream won't autoclose
        // when the other side does, because the upper HTTP/2 layers want to handle shutdown, so
        // they end up trying to write a GOAWAY at the same time as the lower stream shuts down,
        // and we get assertion errors in Node v16.7+.
        if (res.socket.constructor.name.includes('JSStreamSocket')) {
            res.socket.allowHalfOpen = true;
        }
        server.emit('connection', res.stream);
    }
    return (0, destroyable_server_1.makeDestroyable)(server);
}
exports.createComboServer = createComboServer;
const SOCKET_ADDRESS_METADATA_FIELDS = [
    'localAddress',
    'localPort',
    'remoteAddress',
    'remotePort',
    '__lastHopConnectAddress'
];
// Update the target socket(-ish) with the address details from the source socket,
// iff the target has no details of its own.
function copyAddressDetails(source, target) {
    Object.defineProperties(target, _.zipObject(SOCKET_ADDRESS_METADATA_FIELDS, _.range(SOCKET_ADDRESS_METADATA_FIELDS.length).map(() => ({ writable: true }))));
    SOCKET_ADDRESS_METADATA_FIELDS.forEach((fieldName) => {
        if (target[fieldName] === undefined) {
            target[fieldName] = source[fieldName];
        }
    });
}
function copyTimingDetails(source, target) {
    if (!target.__timingInfo) {
        // Clone timing info, don't copy it - child sockets get their own independent timing stats
        target.__timingInfo = Object.assign({}, source.__timingInfo);
    }
}
/**
 * Takes a tls passthrough list (may be empty), and reconfigures a given TLS server so that all
 * client hellos are parsed, matching requests are passed to the given passthrough listener (without
 * continuing setup) and client hello metadata is attached to all sockets.
 */
function analyzeAndMaybePassThroughTls(server, passthroughList, passthroughListener) {
    const hostnames = passthroughList.map(({ hostname }) => hostname);
    const tlsConnectionListener = server.listeners('connection')[0];
    server.removeListener('connection', tlsConnectionListener);
    server.on('connection', async (socket) => {
        try {
            const helloData = await (0, read_tls_client_hello_1.readTlsClientHello)(socket);
            const [connectHostname, connectPort] = socket.__lastHopConnectAddress?.split(':') ?? [];
            const sniHostname = helloData.serverName;
            socket.__tlsMetadata = {
                sniHostname,
                connectHostname,
                connectPort,
                clientAlpn: helloData.alpnProtocols,
                ja3Fingerprint: (0, read_tls_client_hello_1.calculateJa3FromFingerprintData)(helloData.fingerprintData)
            };
            if (connectHostname && hostnames.includes(connectHostname)) {
                const upstreamPort = connectPort ? parseInt(connectPort, 10) : undefined;
                passthroughListener(socket, connectHostname, upstreamPort);
                return; // Do not continue with TLS
            }
            else if (sniHostname && hostnames.includes(sniHostname)) {
                passthroughListener(socket, sniHostname); // Can't guess the port - not included in SNI
                return; // Do not continue with TLS
            }
        }
        catch (e) {
            if (!(e instanceof read_tls_client_hello_1.NonTlsError)) { // Don't even warn for non-TLS traffic
                console.warn(`TLS client hello data not available for TLS connection from ${socket.remoteAddress ?? 'unknown address'}: ${e.message ?? e}`);
            }
        }
        // Didn't match a passthrough hostname - continue with TLS setup
        tlsConnectionListener.call(server, socket);
    });
}
//# sourceMappingURL=http-combo-server.js.map