"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getDnsLookupFunction = exports.OVERRIDABLE_REQUEST_PSEUDOHEADERS = exports.getUpstreamTlsOptions = void 0;
exports.getTrustedCAs = getTrustedCAs;
exports.buildOverriddenBody = buildOverriddenBody;
exports.getHostAfterModification = getHostAfterModification;
exports.getH2HeadersAfterModification = getH2HeadersAfterModification;
exports.getContentLengthAfterModification = getContentLengthAfterModification;
exports.shouldUseStrictHttps = shouldUseStrictHttps;
exports.getClientRelativeHostname = getClientRelativeHostname;
const _ = require("lodash");
const fs = require("fs/promises");
const tls = require("tls");
const url = require("url");
const common_tags_1 = require("common-tags");
const cacheable_lookup_1 = require("cacheable-lookup");
const util_1 = require("../util/util");
const buffer_utils_1 = require("../util/buffer-utils");
const socket_util_1 = require("../util/socket-util");
const dns_1 = require("../util/dns");
const request_utils_1 = require("../util/request-utils");
const openssl_compat_1 = require("../util/openssl-compat");
// TLS settings for proxied connections, intended to avoid TLS fingerprint blocking
// issues so far as possible, by closely emulating a Firefox Client Hello:
const NEW_CURVES_SUPPORTED = (0, openssl_compat_1.areFFDHECurvesSupported)(process.versions.openssl);
const SSL_OP_LEGACY_SERVER_CONNECT = 1 << 2;
const SSL_OP_TLSEXT_PADDING = 1 << 4;
const SSL_OP_NO_ENCRYPT_THEN_MAC = 1 << 19;
// All settings are designed to exactly match Firefox v103, since that's a good baseline
// that seems to be widely accepted and is easy to emulate from Node.js.
const getUpstreamTlsOptions = (strictChecks) => ({
    ecdhCurve: [
        'X25519',
        'prime256v1', // N.B. Equivalent to secp256r1
        'secp384r1',
        'secp521r1',
        ...(NEW_CURVES_SUPPORTED
            ? [
                'ffdhe2048',
                'ffdhe3072'
            ] : [])
    ].join(':'),
    sigalgs: [
        'ecdsa_secp256r1_sha256',
        'ecdsa_secp384r1_sha384',
        'ecdsa_secp521r1_sha512',
        'rsa_pss_rsae_sha256',
        'rsa_pss_rsae_sha384',
        'rsa_pss_rsae_sha512',
        'rsa_pkcs1_sha256',
        'rsa_pkcs1_sha384',
        'rsa_pkcs1_sha512',
        'ECDSA+SHA1',
        'rsa_pkcs1_sha1'
    ].join(':'),
    ciphers: [
        'TLS_AES_128_GCM_SHA256',
        'TLS_CHACHA20_POLY1305_SHA256',
        'TLS_AES_256_GCM_SHA384',
        'ECDHE-ECDSA-AES128-GCM-SHA256',
        'ECDHE-RSA-AES128-GCM-SHA256',
        'ECDHE-ECDSA-CHACHA20-POLY1305',
        'ECDHE-RSA-CHACHA20-POLY1305',
        'ECDHE-ECDSA-AES256-GCM-SHA384',
        'ECDHE-RSA-AES256-GCM-SHA384',
        'ECDHE-ECDSA-AES256-SHA',
        'ECDHE-ECDSA-AES128-SHA',
        'ECDHE-RSA-AES128-SHA',
        'ECDHE-RSA-AES256-SHA',
        'AES128-GCM-SHA256',
        'AES256-GCM-SHA384',
        'AES128-SHA',
        'AES256-SHA',
        // This magic cipher is the very obtuse way that OpenSSL downgrades the overall
        // security level to allow various legacy settings, protocols & ciphers:
        ...(!strictChecks
            ? ['@SECLEVEL=0']
            : [])
    ].join(':'),
    secureOptions: strictChecks
        ? SSL_OP_TLSEXT_PADDING | SSL_OP_NO_ENCRYPT_THEN_MAC
        : SSL_OP_TLSEXT_PADDING | SSL_OP_NO_ENCRYPT_THEN_MAC | SSL_OP_LEGACY_SERVER_CONNECT,
    ...{
        // Valid, but not included in Node.js TLS module types:
        requestOSCP: true
    },
    // Allow TLSv1, if !strict:
    minVersion: strictChecks ? tls.DEFAULT_MIN_VERSION : 'TLSv1',
    // Skip certificate validation entirely, if not strict:
    rejectUnauthorized: strictChecks,
});
exports.getUpstreamTlsOptions = getUpstreamTlsOptions;
async function getTrustedCAs(trustedCAs, additionalTrustedCAs) {
    if (trustedCAs && additionalTrustedCAs?.length) {
        throw new Error(`trustedCAs and additionalTrustedCAs options are mutually exclusive`);
    }
    if (trustedCAs) {
        return Promise.all(trustedCAs.map((caDefinition) => getCA(caDefinition)));
    }
    if (additionalTrustedCAs) {
        const CAs = await Promise.all(additionalTrustedCAs.map((caDefinition) => getCA(caDefinition)));
        return tls.rootCertificates.concat(CAs);
    }
}
const getCA = async (caDefinition) => {
    return typeof caDefinition === 'string'
        ? caDefinition
        : 'certPath' in caDefinition
            ? await fs.readFile(caDefinition.certPath, 'utf8')
            // 'cert' in caDefinition
            : caDefinition.cert.toString('utf8');
};
// --- Various helpers for deriving parts of request/response data given partial overrides: ---
/**
 * Takes a callback result and some headers, and returns a ready to send body, using the headers
 * (and potentially modifying them) to match the content type & encoding.
 */
async function buildOverriddenBody(callbackResult, headers) {
    // Raw bodies are easy: use them as is.
    if (callbackResult?.rawBody)
        return callbackResult?.rawBody;
    // In the json/body case, we need to get the body and transform it into a buffer
    // for consistent handling later, and encode it to match the headers.
    let replacementBody;
    if (callbackResult?.json) {
        headers['content-type'] = 'application/json';
        replacementBody = JSON.stringify(callbackResult?.json);
    }
    else {
        replacementBody = callbackResult?.body;
    }
    if (replacementBody === undefined)
        return replacementBody;
    let rawBuffer;
    if ((0, request_utils_1.isMockttpBody)(replacementBody)) {
        // It's our own bodyReader instance. That's not supposed to happen, but
        // it's ok, we just need to use the buffer data instead of the whole object
        rawBuffer = Buffer.from(replacementBody.buffer);
    }
    else if (replacementBody === '') {
        // For empty bodies, it's slightly more convenient if they're truthy
        rawBuffer = Buffer.alloc(0);
    }
    else {
        rawBuffer = (0, buffer_utils_1.asBuffer)(replacementBody);
    }
    return await (0, request_utils_1.encodeBodyBuffer)(rawBuffer, headers);
}
/**
 * If you override some headers, they have implications for the effective URL we send the
 * request to. If you override that and the URL at the same time, it gets complicated.
 *
 * This method calculates the correct header value we should use: prioritising the header
 * value you provide, printing a warning if it's contradictory, or return the URL-inferred
 * value to override the header correctly if you didn't specify.
 */
function deriveUrlLinkedHeader(originalHeaders, replacementHeaders, headerName, expectedValue // The inferred 'correct' value from the URL
) {
    const replacementValue = replacementHeaders?.[headerName];
    if (replacementValue !== undefined) {
        if (replacementValue !== expectedValue && replacementValue === originalHeaders[headerName]) {
            // If you rewrite the URL-based header wrongly, by explicitly setting it to the
            // existing value, we accept it but print a warning. This would be easy to
            // do if you mutate the existing headers, for example, and ignore the host.
            console.warn((0, common_tags_1.oneLine) `
                Passthrough callback overrode the URL and the ${headerName} header
                with mismatched values, which may be a mistake. The URL implies
                ${expectedValue}, whilst the header was set to ${replacementValue}.
            `);
        }
        // Whatever happens, if you explicitly set a value, we use it.
        return replacementValue;
    }
    // If you didn't override the header at all, then we automatically ensure
    // the correct value is set automatically.
    return expectedValue;
}
/**
 * Autocorrect the host header only in the case that if you didn't explicitly
 * override it yourself for some reason (e.g. if you're testing bad behaviour).
 */
function getHostAfterModification(reqUrl, originalHeaders, replacementHeaders) {
    return deriveUrlLinkedHeader(originalHeaders, replacementHeaders, 'host', url.parse(reqUrl).host);
}
exports.OVERRIDABLE_REQUEST_PSEUDOHEADERS = [
    ':authority',
    ':scheme'
];
/**
 * Automatically update the :scheme and :authority headers to match the updated URL,
 * as long as they weren't explicitly overriden themselves, in which case let them
 * be set to any invalid value you like (e.g. to send a request to one server but
 * pretend it was sent to another).
 */
function getH2HeadersAfterModification(reqUrl, originalHeaders, replacementHeaders) {
    const parsedUrl = url.parse(reqUrl);
    return {
        ':scheme': deriveUrlLinkedHeader(originalHeaders, replacementHeaders, ':scheme', parsedUrl.protocol.slice(0, -1)),
        ':authority': deriveUrlLinkedHeader(originalHeaders, replacementHeaders, ':authority', parsedUrl.host)
    };
}
// Helper to handle content-length nicely for you when rewriting requests with callbacks
function getContentLengthAfterModification(body, originalHeaders, replacementHeaders, mismatchAllowed = false) {
    // If there was a content-length header, it might now be wrong, and it's annoying
    // to need to set your own content-length override when you just want to change
    // the body. To help out, if you override the body but don't explicitly override
    // the (now invalid) content-length, then we fix it for you.
    if (!_.has(originalHeaders, 'content-length')) {
        // Nothing to override - use the replacement value, or undefined
        return (replacementHeaders || {})['content-length'];
    }
    if (!replacementHeaders) {
        // There was a length set, and you've provided a body but not changed it.
        // You probably just want to send this body and have it work correctly,
        // so we should fix the content length for you automatically.
        return (0, util_1.byteLength)(body).toString();
    }
    // There was a content length before, and you're replacing the headers entirely
    const lengthOverride = replacementHeaders['content-length']?.toString();
    // If you're setting the content-length to the same as the origin headers, even
    // though that's the wrong value, it *might* be that you're just extending the
    // existing headers, and you're doing this by accident (we can't tell for sure).
    // We use invalid content-length as instructed, but print a warning just in case.
    if (lengthOverride === originalHeaders['content-length'] &&
        lengthOverride !== (0, util_1.byteLength)(body).toString() &&
        !mismatchAllowed // Set for HEAD responses
    ) {
        console.warn((0, common_tags_1.oneLine) `
            Passthrough modifications overrode the body and the content-length header
            with mismatched values, which may be a mistake. The body contains
            ${(0, util_1.byteLength)(body)} bytes, whilst the header was set to ${lengthOverride}.
        `);
    }
    return lengthOverride;
}
// Function to check if we should skip https errors for the current hostname and port,
// based on the given config
function shouldUseStrictHttps(hostname, port, ignoreHostHttpsErrors) {
    let skipHttpsErrors = false;
    if (ignoreHostHttpsErrors === true) {
        // Ignore cert errors if `ignoreHostHttpsErrors` is set to true, or
        skipHttpsErrors = true;
    }
    else if (Array.isArray(ignoreHostHttpsErrors) && (
    // if the whole hostname or host+port is whitelisted
    _.includes(ignoreHostHttpsErrors, hostname) ||
        _.includes(ignoreHostHttpsErrors, `${hostname}:${port}`))) {
        skipHttpsErrors = true;
    }
    return !skipHttpsErrors;
}
exports.getDnsLookupFunction = _.memoize((lookupOptions) => {
    if (!lookupOptions) {
        // By default, use 10s caching of hostnames, just to reduce the delay from
        // endlessly 10ms query delay for 'localhost' with every request.
        return new dns_1.CachedDns(10000).lookup;
    }
    else {
        // Or if options are provided, use those to configure advanced DNS cases:
        const cacheableLookup = new cacheable_lookup_1.default({
            maxTtl: lookupOptions.maxTtl,
            errorTtl: lookupOptions.errorTtl,
            // As little caching of "use the fallback server" as possible:
            fallbackDuration: 0
        });
        if (lookupOptions.servers) {
            cacheableLookup.servers = lookupOptions.servers;
        }
        return cacheableLookup.lookup;
    }
});
async function getClientRelativeHostname(hostname, remoteIp, lookupFn) {
    if (!hostname || !remoteIp || (0, socket_util_1.isLocalhostAddress)(remoteIp))
        return hostname;
    // Otherwise, we have a request from a different machine (or Docker container/VM/etc) and we need
    // to make sure that 'localhost' means _that_ machine, not ourselves.
    // This check must be run before req modifications. If a modification changes the address to localhost,
    // then presumably it really does mean *this* localhost.
    if (
    // If the hostname is a known localhost address, we're done:
    (0, socket_util_1.isLocalhostAddress)(hostname) ||
        // Otherwise, we look up the IP, so we can accurately check for localhost-bound requests. This adds a little
        // delay, but since it's cached we save the equivalent delay in request lookup later, so it should be
        // effectively free. We ignore errors to delegate unresolvable etc to request processing later.
        (0, socket_util_1.isLocalhostAddress)(await (0, dns_1.dnsLookup)(lookupFn, hostname).catch(() => null))) {
        return (0, socket_util_1.normalizeIP)(remoteIp);
        // Note that we just redirect - we don't update the host header. From the POV of the target, it's still
        // 'localhost' traffic that should appear identical to normal.
    }
    else {
        return hostname;
    }
}
//# sourceMappingURL=passthrough-handling.js.map