/**
 * Parent/upstream HTTP proxy support.
 *
 * When SRT runs in an environment that requires an HTTP proxy for outbound
 * internet access (e.g. inside a VM on a host behind a corporate proxy),
 * SRT's own proxies must chain through that upstream rather than connecting
 * directly.
 *
 * This module provides:
 *   - config resolution (explicit config -> HTTP_PROXY/HTTPS_PROXY/NO_PROXY env)
 *   - NO_PROXY matching (hostname suffix + CIDR via net.BlockList). Follows
 *     golang.org/x/net/http/httpproxy semantics for suffix matching. Note:
 *     port-specific NO_PROXY entries (e.g. `host:8080`) are matched by host
 *     only; the port is ignored.
 *   - a generic CONNECT-tunnel helper that works over Unix socket, TCP, or TLS
 */
import { BlockList, connect as netConnect, isIP } from 'node:net';
import { connect as tlsConnect } from 'node:tls';
import { URL } from 'node:url';
import { logForDebugging } from '../utils/debug.js';
const CONNECT_TIMEOUT_MS = 30000;
/**
 * Hop-by-hop headers per RFC 7230 §6.1, plus proxy-specific headers that
 * MUST NOT be forwarded to the upstream. `transfer-encoding` is included
 * because we re-frame bodies via Node's client; Content-Length is preserved
 * end-to-end (Node's llhttp already rejects the TE+CL smuggling vector).
 */
const HOP_BY_HOP = new Set([
    'connection',
    'keep-alive',
    'proxy-authenticate',
    'proxy-authorization',
    'proxy-connection',
    'te',
    'trailer',
    'transfer-encoding',
    'upgrade',
]);
/**
 * Resolve the parent proxy config, falling back to the SRT process's own
 * environment. Note: SRT later overwrites HTTP_PROXY etc. in the *sandboxed
 * child's* environment to point at itself — but process.env here reflects the
 * environment SRT itself was launched with, which is what we want.
 */
export function resolveParentProxy(cfg) {
    const http = cfg?.http ?? process.env.HTTP_PROXY ?? process.env.http_proxy ?? undefined;
    const https = cfg?.https ??
        process.env.HTTPS_PROXY ??
        process.env.https_proxy ??
        // Fall back to HTTP_PROXY for HTTPS if HTTPS_PROXY is unset — this is
        // the de-facto behaviour of curl and most tooling.
        http;
    const noProxyRaw = cfg?.noProxy ?? process.env.NO_PROXY ?? process.env.no_proxy ?? '';
    if (!http && !https)
        return undefined;
    const parse = (u) => {
        if (!u)
            return undefined;
        // Accept schemeless `host:port` like curl does, but reject any scheme
        // other than http/https.
        const hasScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(u);
        const withScheme = hasScheme ? u : `http://${u}`;
        try {
            const parsed = new URL(withScheme);
            if ((parsed.protocol !== 'http:' && parsed.protocol !== 'https:') ||
                !parsed.hostname) {
                throw new Error('unsupported scheme or empty host');
            }
            return parsed;
        }
        catch {
            logForDebugging(`Invalid parent proxy URL, ignoring: ${redactUserinfo(u)}`, { level: 'error' });
            return undefined;
        }
    };
    const httpUrl = parse(http);
    const httpsUrl = parse(https);
    // If both parsed to undefined, behave as if no parent proxy was configured
    // rather than returning a husk object that makes callers do bypass checks
    // for nothing.
    if (!httpUrl && !httpsUrl)
        return undefined;
    return { httpUrl, httpsUrl, noProxy: parseNoProxy(noProxyRaw) };
}
function parseNoProxy(raw) {
    const rules = {
        all: false,
        suffixes: [],
        cidr: new BlockList(),
    };
    for (let entry of raw.split(',')) {
        entry = entry.trim();
        if (!entry)
            continue;
        if (entry === '*') {
            rules.all = true;
            continue;
        }
        // CIDR?
        const slash = entry.indexOf('/');
        if (slash !== -1) {
            const ip = entry.slice(0, slash);
            const prefixStr = entry.slice(slash + 1);
            const fam = isIP(ip);
            if (fam && prefixStr !== '' && /^\d+$/.test(prefixStr)) {
                const prefix = Number(prefixStr);
                const max = fam === 6 ? 128 : 32;
                if (prefix >= 0 && prefix <= max) {
                    try {
                        rules.cidr.addSubnet(ip, prefix, fam === 6 ? 'ipv6' : 'ipv4');
                    }
                    catch {
                        // BlockList rejected it — ignore this entry.
                    }
                    continue;
                }
            }
            // malformed CIDR → ignore (do NOT treat as suffix; `/` isn't a valid
            // hostname char)
            continue;
        }
        // Hostname suffix. Normalise: lowercase, strip brackets (handling the
        // `[v6]:port` form), strip leading `*.`, strip a trailing `:port` (unless
        // the entry is an IP literal — IPv6 addresses contain colons).
        let v = entry.toLowerCase();
        const bracketed = /^\[([^\]]+)\](?::\d+)?$/.exec(v);
        if (bracketed)
            v = bracketed[1];
        if (v.startsWith('*.'))
            v = v.slice(1);
        const bareFam = isIP(v);
        if (!bareFam) {
            const colon = v.lastIndexOf(':');
            if (colon !== -1 && /^\d+$/.test(v.slice(colon + 1))) {
                v = v.slice(0, colon);
            }
        }
        else {
            // Bare IP literal — store as an exact-match /32 or /128 CIDR so that
            // lookups go through BlockList rather than string suffix matching.
            try {
                rules.cidr.addAddress(v, bareFam === 6 ? 'ipv6' : 'ipv4');
                continue;
            }
            catch {
                // fall through to suffix push
            }
        }
        rules.suffixes.push(v);
    }
    return rules;
}
/**
 * Returns true if the given host should bypass the parent proxy and connect
 * directly. Always bypasses loopback.
 *
 * NB: the port is not consulted. NO_PROXY entries of the form `host:port` are
 * matched by host only (the port suffix is stripped during parsing).
 */
export function shouldBypassParentProxy(resolved, host) {
    const h = stripBrackets(host.toLowerCase().replace(/\.$/, ''));
    // Always bypass loopback — chaining localhost through an upstream proxy is
    // never what you want. Covers the whole 127/8 block and IPv4-mapped forms.
    if (h === 'localhost')
        return true;
    const fam = isIP(h);
    if (fam) {
        if (LOOPBACK.check(h, fam === 6 ? 'ipv6' : 'ipv4'))
            return true;
    }
    if (resolved.noProxy.all)
        return true;
    if (fam) {
        if (resolved.noProxy.cidr.check(h, fam === 6 ? 'ipv6' : 'ipv4'))
            return true;
    }
    for (const v of resolved.noProxy.suffixes) {
        if (v.startsWith('.')) {
            // .example.com matches foo.example.com and example.com
            if (h === v.slice(1) || h.endsWith(v))
                return true;
        }
        else {
            // example.com matches example.com and foo.example.com (golang semantics)
            if (h === v || h.endsWith('.' + v))
                return true;
        }
    }
    return false;
}
const LOOPBACK = (() => {
    const bl = new BlockList();
    bl.addSubnet('127.0.0.0', 8, 'ipv4');
    bl.addAddress('::1', 'ipv6');
    bl.addSubnet('::ffff:127.0.0.0', 104, 'ipv6'); // v4-mapped loopback
    return bl;
})();
/**
 * Pick which parent proxy URL to use for a given destination.
 */
export function selectParentProxyUrl(resolved, opts) {
    if (opts.isHttps)
        return resolved.httpsUrl ?? resolved.httpUrl;
    // For plain HTTP we only fall back to HTTPS_PROXY if it was explicitly set
    // — matches curl's behaviour where HTTP requests go direct if only
    // HTTPS_PROXY is configured.
    return resolved.httpUrl;
}
/**
 * Generic CONNECT-tunnel: dial a proxy transport (unix/tcp/tls), send
 * `CONNECT host:port`, wait for a 2xx, and resolve with the tunnelled socket.
 * Validates destHost to prevent CRLF injection from untrusted callers.
 */
export function openConnectTunnel(opts) {
    const { destHost, destPort } = opts;
    // CRLF-injection guard: destHost may originate from an untrusted SOCKS5
    // DOMAINNAME field. Reject anything that isn't a plain hostname or IP.
    const bare = stripBrackets(destHost);
    if (!isValidHost(bare)) {
        return Promise.reject(new Error(`Invalid destination host for CONNECT: ${JSON.stringify(destHost)}`));
    }
    if (!Number.isInteger(destPort) || destPort < 1 || destPort > 65535) {
        return Promise.reject(new Error(`Invalid destination port: ${destPort}`));
    }
    const authority = isIP(bare) === 6 ? `[${bare}]:${destPort}` : `${bare}:${destPort}`;
    return new Promise((resolve, reject) => {
        const sock = opts.dial();
        let settled = false;
        const fail = (err) => {
            if (settled)
                return;
            settled = true;
            sock.destroy();
            reject(err);
        };
        const onClose = () => fail(new Error('Proxy closed during CONNECT handshake'));
        sock.setTimeout(opts.timeoutMs ?? CONNECT_TIMEOUT_MS, () => fail(new Error('CONNECT handshake timed out')));
        sock.once('error', fail);
        sock.once('close', onClose);
        sock.once(opts.readyEvent, () => {
            sock.write(`CONNECT ${authority} HTTP/1.1\r\n` +
                `Host: ${authority}\r\n` +
                (opts.authHeader
                    ? `Proxy-Authorization: ${opts.authHeader}\r\n`
                    : '') +
                '\r\n');
            let buf = '';
            const onData = (chunk) => {
                buf += chunk.toString('latin1');
                const end = buf.indexOf('\r\n\r\n');
                if (end === -1) {
                    // Cap header size to avoid unbounded buffering on a misbehaving proxy.
                    if (buf.length > 16 * 1024)
                        fail(new Error('CONNECT response header too large'));
                    return;
                }
                // Pause before detaching the data listener so the stream stops
                // flowing — otherwise the unshift below (or any bytes arriving
                // between now and the caller's pipe()) would be dropped.
                sock.pause();
                sock.removeListener('data', onData);
                const statusLine = buf.slice(0, buf.indexOf('\r\n'));
                if (!/^HTTP\/1\.[01] 2\d\d(?:\s|$)/.test(statusLine)) {
                    return fail(new Error(`Proxy refused CONNECT: ${statusLine.trim()}`));
                }
                // Re-emit any bytes that arrived after the header terminator.
                const rest = buf.slice(end + 4);
                if (rest.length)
                    sock.unshift(Buffer.from(rest, 'latin1'));
                settled = true;
                sock.setTimeout(0);
                sock.removeListener('error', fail);
                sock.removeListener('close', onClose);
                resolve(sock);
            };
            sock.on('data', onData);
        });
    });
}
/**
 * Open a CONNECT tunnel through a parent HTTP(S) proxy specified by URL.
 * Thin wrapper around openConnectTunnel that dials TCP or TLS based on the
 * proxy URL scheme.
 */
export function connectViaParentProxy(proxyUrl, destHost, destPort) {
    const proxyHost = stripBrackets(proxyUrl.hostname);
    const proxyPort = Number(proxyUrl.port) || (proxyUrl.protocol === 'https:' ? 443 : 80);
    const useTls = proxyUrl.protocol === 'https:';
    return openConnectTunnel({
        destHost,
        destPort,
        authHeader: proxyAuthHeader(proxyUrl),
        readyEvent: useTls ? 'secureConnect' : 'connect',
        dial: () => useTls
            ? tlsConnect({
                host: proxyHost,
                port: proxyPort,
                // SNI must be a hostname, never an IP literal (RFC 6066 §3).
                ...(isIP(proxyHost) ? {} : { servername: proxyHost }),
            })
            : netConnect(proxyPort, proxyHost),
    });
}
// ---------------------------------------------------------------------------
// Utilities
// ---------------------------------------------------------------------------
export function proxyAuthHeader(proxyUrl) {
    if (!proxyUrl.username && !proxyUrl.password)
        return undefined;
    try {
        const creds = `${decodeURIComponent(proxyUrl.username)}:${decodeURIComponent(proxyUrl.password)}`;
        return `Basic ${Buffer.from(creds).toString('base64')}`;
    }
    catch {
        // Malformed percent-encoding in userinfo — fall back to raw values
        // rather than throwing synchronously into the caller.
        const creds = `${proxyUrl.username}:${proxyUrl.password}`;
        return `Basic ${Buffer.from(creds).toString('base64')}`;
    }
}
/**
 * Strip hop-by-hop and proxy-specific headers before forwarding upstream.
 * Also strips any headers named in the incoming `Connection` header, per
 * RFC 7230 §6.1.
 */
export function stripHopByHop(h) {
    const extra = new Set();
    const connHeader = h.connection;
    if (connHeader) {
        for (const tok of String(connHeader).split(',')) {
            extra.add(tok.trim().toLowerCase());
        }
    }
    const out = {};
    for (const [k, v] of Object.entries(h)) {
        const lk = k.toLowerCase();
        if (!HOP_BY_HOP.has(lk) && !extra.has(lk))
            out[k] = v;
    }
    return out;
}
/** Remove surrounding square brackets from an IPv6 literal. */
export function stripBrackets(host) {
    return host.startsWith('[') && host.endsWith(']') ? host.slice(1, -1) : host;
}
/** Redact userinfo from a URL for safe logging. */
export function redactUrl(u) {
    if (!u)
        return '-';
    if (!u.username && !u.password)
        return u.href;
    const c = new URL(u.href);
    c.username = '***';
    c.password = '***';
    return c.href;
}
function redactUserinfo(raw) {
    // Best-effort redaction for unparseable URLs.
    return raw.replace(/\/\/[^@/]*@/, '//***:***@');
}
/**
 * Hostname validation: accepts DNS names and IP literals (without zone IDs).
 * Primary purpose is to block control characters (CRLF injection, null-byte
 * DNS truncation) and zone-identifier allowlist bypasses from reaching the
 * wire or the allowlist matcher.
 *
 * IPv6 zone IDs (`fe80::1%eth0`) are rejected because `isIP` accepts a very
 * permissive zone charset including dots — `::ffff:1.2.3.4%x.allowed.com`
 * would pass `isIP`, pass a `.endsWith('.allowed.com')` wildcard check, and
 * then connect to 1.2.3.4 when the OS discards the bogus scope.
 */
export function isValidHost(h) {
    if (!h || h.length > 255)
        return false;
    const bare = stripBrackets(h);
    // Reject zone identifiers outright (see doc comment).
    if (bare.includes('%'))
        return false;
    if (isIP(bare))
        return true;
    // DNS label charset. Underscore is permitted for compatibility with real-
    // world DNS records (_dmarc, _acme-challenge, etc.).
    return /^[A-Za-z0-9._-]+$/.test(bare);
}
/**
 * Canonicalize a host string via the WHATWG URL parser so that string
 * comparisons in the allowlist agree with what `net.connect()`/`getaddrinfo()`
 * will actually dial. This normalizes:
 *   - inet_aton shorthand (`127.1` → `127.0.0.1`, `2130706433` → `127.0.0.1`)
 *   - hex/octal octets (`0x7f.0.0.1` → `127.0.0.1`)
 *   - IPv6 compression (`0:0:0:0:0:0:0:1` → `::1`)
 *   - trailing dots, case, brackets
 *
 * Returns undefined if the input is not a valid URL host.
 */
export function canonicalizeHost(h) {
    try {
        const bare = stripBrackets(h);
        // WHATWG URL rejects zone IDs and most garbage; it normalizes inet_aton
        // forms and IPv6 compression. It does NOT strip trailing dots or IPv6
        // brackets from the output, so we do that ourselves.
        const bracketed = isIP(bare) === 6 ? `[${bare}]` : bare;
        const out = new URL(`http://${bracketed}/`).hostname;
        return stripBrackets(out).replace(/\.$/, '');
    }
    catch {
        return undefined;
    }
}
/**
 * Dial `host:port` directly with a bounded timeout. Shared by the HTTP and
 * SOCKS direct-connect paths so they get the same timeout behaviour as the
 * CONNECT-tunnelled paths.
 */
export function dialDirect(host, port, timeoutMs = CONNECT_TIMEOUT_MS) {
    return new Promise((resolve, reject) => {
        const s = netConnect(port, host);
        let settled = false;
        const done = (err) => {
            if (settled)
                return;
            settled = true;
            s.setTimeout(0);
            if (err) {
                s.destroy();
                reject(err);
            }
            else {
                resolve(s);
            }
        };
        s.setTimeout(timeoutMs, () => done(new Error('connect timed out')));
        s.once('connect', () => done());
        s.once('error', done);
        s.once('close', () => done(new Error('socket closed before connect')));
    });
}//# sourceMappingURL=https://github.com/VSCodium/sourcemaps/releases/download/stable-824c4c46a288b839f13b24022655329c2aeb9f81/node_modules-@vscode-sandbox-runtime-dist-sandbox-parent-proxy.js.map