"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.requireSocketResetSupport = exports.isSocketLoop = exports.isLocalhostAddress = exports.normalizeIP = exports.isLocalIPv6Available = void 0; exports.isLocalPortActive = isLocalPortActive; exports.getParentSocket = getParentSocket; exports.resetOrDestroy = resetOrDestroy; exports.buildSocketEventData = buildSocketEventData; exports.buildSocketTimingInfo = buildSocketTimingInfo; 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); }); }); } // 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 some cases (especially comparisons), 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; exports.normalizeIP = normalizeIP; const isLocalhostAddress = (host) => !!host && ( // Null/undef are something else weird, but not localhost host === 'localhost' || // Most common host.endsWith('.localhost') || host === '::1' || // IPv6 (0, exports.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 (0, exports.normalizeIP)(outgoingSocket.localAddress) === (0, exports.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 } 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) ? requestOrSocket.stream : ('socket' in requestOrSocket && requestOrSocket.socket) ? requestOrSocket.socket : requestOrSocket; while (socket instanceof tls.TLSSocket) { const parent = getParentSocket(socket); if (!parent) break; // Not clear why, but it seems in some cases we run out of parents here socket = parent; } 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(); } } } ; 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, // Recorded by certCB monkeypatch remotePort: socket.remotePort || socket._parent?.remotePort || socket.initialRemotePort, tags: [], timingEvents: { startTime: timingInfo.initialSocket, connectTimestamp: timingInfo.initialSocketTimestamp, tunnelTimestamp: timingInfo.tunnelSetupTimestamp, handshakeTimestamp: timingInfo.tlsConnectedTimestamp }, tlsMetadata }; } function buildSocketTimingInfo() { return { initialSocket: Date.now(), initialSocketTimestamp: now() }; } //# sourceMappingURL=socket-util.js.map