"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildSocketTimingInfo = exports.buildSocketEventData = exports.resetOrDestroy = exports.requireSocketResetSupport = exports.getParentSocket = exports.isSocketLoop = exports.isLocalhostAddress = exports.isLocalIPv6Available = exports.isLocalPortActive = void 0;
const _ = require("lodash");
const now = require("performance-now");
const os = require("os");
const net = require("net");
const tls = require("tls");
const http2 = require("http2");
const util_1 = require("./util");
// Test if a local port for a given interface (IPv4/6) is currently in use
async function isLocalPortActive(interfaceIp, port) {
    if (interfaceIp === '::1' && !exports.isLocalIPv6Available)
        return false;
    return new Promise((resolve) => {
        const server = net.createServer();
        server.listen({
            host: interfaceIp,
            port,
            ipv6Only: interfaceIp === '::1'
        });
        server.once('listening', () => {
            resolve(false);
            server.close(() => { });
        });
        server.once('error', (e) => {
            resolve(true);
        });
    });
}
exports.isLocalPortActive = isLocalPortActive;
// This file imported in browsers etc as it's used in handlers, but none of these methods are used
// directly. It is useful though to guard sections that immediately perform actions:
exports.isLocalIPv6Available = util_1.isNode
    ? _.some(os.networkInterfaces(), (addresses) => _.some(addresses, a => a.address === '::1'))
    : true;
// We need to normalize ips for comparison, because the same ip may be reported as ::ffff:127.0.0.1
// and 127.0.0.1 on the two sides of the connection, for the same ip.
const normalizeIp = (ip) => (ip && ip.startsWith('::ffff:'))
    ? ip.slice('::ffff:'.length)
    : ip;
const isLocalhostAddress = (host) => !!host && ( // Null/undef are something else weird, but not localhost
host === 'localhost' || // Most common
    host.endsWith('.localhost') ||
    host === '::1' || // IPv6
    normalizeIp(host).match(/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/) // 127.0.0.0/8 range
);
exports.isLocalhostAddress = isLocalhostAddress;
// Check whether an incoming socket is the other end of one of our outgoing sockets:
const isSocketLoop = (outgoingSockets, incomingSocket) => 
// We effectively just compare the address & port: if they match, we've almost certainly got a loop.
// I don't think it's generally possible to see the same ip on different interfaces from one process (you need
// ip-netns network namespaces), but if it is, then there's a tiny chance of false positives here. If we have ip X,
// and on another interface somebody else has ip X, and they send a request with the same incoming port as an
// outgoing request we have on the other interface, we'll assume it's a loop. Extremely unlikely imo.
_.some([...outgoingSockets], (outgoingSocket) => {
    if (!outgoingSocket.localAddress || !outgoingSocket.localPort) {
        // It's possible for sockets in outgoingSockets to be closed, in which case these properties
        // will be undefined. If so, we know they're not relevant to loops, so skip entirely.
        return false;
    }
    else {
        return normalizeIp(outgoingSocket.localAddress) === normalizeIp(incomingSocket.remoteAddress) &&
            outgoingSocket.localPort === incomingSocket.remotePort;
    }
});
exports.isSocketLoop = isSocketLoop;
function getParentSocket(socket) {
    return socket._parent || // TLS wrapper
        socket.stream || // SocketWrapper
        socket._handle?._parentWrap?.stream; // HTTP/2 CONNECT'd TLS wrapper
}
exports.getParentSocket = getParentSocket;
const isSocketResetSupported = util_1.isNode
    ? !!net.Socket.prototype.resetAndDestroy
    : false; // Avoid errors in browsers
const requireSocketResetSupport = () => {
    if (!isSocketResetSupported) {
        throw new Error('Connection reset is only supported in Node v16.17+, v18.3.0+, or later');
    }
};
exports.requireSocketResetSupport = requireSocketResetSupport;
const isHttp2Stream = (maybeStream) => 'httpVersion' in maybeStream &&
    maybeStream.httpVersion?.startsWith('2');
/**
 * Reset the socket where possible, or at least destroy it where that's not possible.
 *
 * This has a few cases for different layers of socket & tunneling, designed to
 * simulate a real connection reset as closely as possible. That means, in general,
 * we unwrap the connection as far as possible whilst still only affecting a single
 * request.
 *
 * In practice, we unwrap HTTP/1 & TLS back as far as we can, until we hit either an
 * HTTP/2 stream or a raw TCP connection. We then either send a RST_FRAME or a TCP RST
 * to kill that connection.
 */
function resetOrDestroy(requestOrSocket) {
    let socket = isHttp2Stream(requestOrSocket)
        ? requestOrSocket.stream
        : ('socket' in requestOrSocket && requestOrSocket.socket)
            ? requestOrSocket.socket
            : requestOrSocket;
    while (socket instanceof tls.TLSSocket) {
        socket = getParentSocket(socket);
    }
    if ('rstCode' in socket) {
        // It's an HTTP/2 stream instance - let's kill it here.
        // If it's the innermost stream, i.e. this is the stream of the request we're
        // resetting, then we want to send an internal error. If it's a tunneling
        // stream, then we want to send a CONNECT error:
        const isOuterSocket = socket === requestOrSocket.stream;
        const errorCode = isOuterSocket
            ? http2.constants.NGHTTP2_INTERNAL_ERROR
            : http2.constants.NGHTTP2_CONNECT_ERROR;
        const h2Stream = socket;
        h2Stream.close(errorCode);
    }
    else {
        // Must be a net.Socket then, so we let's reset it for real:
        if (isSocketResetSupported) {
            try {
                socket.resetAndDestroy();
            }
            catch (error) {
                // This could fail in funky ways if the socket is not just the right kind
                // of socket. We should still fail in that case, but it's useful to log
                // some extra data first beforehand, so we can fix this if it ever happens:
                console.warn(`Failed to reset on socket of type ${socket.constructor.name} with parent of type ${getParentSocket(socket)?.constructor.name}`);
                throw error;
            }
        }
        else {
            socket.destroy();
        }
    }
}
exports.resetOrDestroy = resetOrDestroy;
;
function buildSocketEventData(socket) {
    const timingInfo = socket.__timingInfo ||
        socket._parent?.__timingInfo ||
        buildSocketTimingInfo();
    // Attached in passThroughMatchingTls TLS sniffing logic in http-combo-server:
    const tlsMetadata = socket.__tlsMetadata ||
        socket._parent?.__tlsMetadata ||
        {};
    return {
        hostname: socket.servername,
        // These only work because of oncertcb monkeypatch in http-combo-server:
        remoteIpAddress: socket.remoteAddress || // Normal case
            socket._parent?.remoteAddress || // Pre-certCB error, e.g. timeout
            socket.initialRemoteAddress,
        remotePort: socket.remotePort ||
            socket._parent?.remotePort ||
            socket.initialRemotePort,
        tags: [],
        timingEvents: {
            startTime: timingInfo.initialSocket,
            connectTimestamp: timingInfo.initialSocketTimestamp,
            tunnelTimestamp: timingInfo.tunnelSetupTimestamp,
            handshakeTimestamp: timingInfo.tlsConnectedTimestamp
        },
        tlsMetadata
    };
}
exports.buildSocketEventData = buildSocketEventData;
function buildSocketTimingInfo() {
    return { initialSocket: Date.now(), initialSocketTimestamp: now() };
}
exports.buildSocketTimingInfo = buildSocketTimingInfo;
//# sourceMappingURL=socket-util.js.map