@ 261105.16
current
'use strict';

const {
  Date,
  Number,
  NumberParseInt,
  Symbol,
  decodeURIComponent,
} = primordials;

const { setUnrefTimeout } = require('internal/timers');
const {
  categoryEnabledChecker,
  trace,
  nodeTraceEventCategory,
  kAsyncBegin,
  kAsyncEnd,
} = require('internal/trace_events');

const { URL } = require('internal/url');
const { Buffer } = require('buffer');
const { isIPv4 } = require('internal/net');
const { ERR_PROXY_INVALID_CONFIG } = require('internal/errors').codes;
let utcCache;
let utcHeaderCache;

function utcDate() {
  if (!utcCache) cache();
  return utcCache;
}

// The complete `Date: ...\r\n` header line, cached alongside utcDate() so
// that the serializer does not re-concatenate it for every response.
function utcDateHeader() {
  utcHeaderCache ||= 'Date: ' + utcDate() + '\r\n';
  return utcHeaderCache;
}

function cache() {
  const d = new Date();
  utcCache = d.toUTCString();
  setUnrefTimeout(resetCache, 1000 - d.getMilliseconds());
}

function resetCache() {
  utcCache = undefined;
  utcHeaderCache = undefined;
}

let traceEventId = 0;

function getNextTraceEventId() {
  return ++traceEventId;
}

const isTraceHTTPEnabled = categoryEnabledChecker('node.http');

const traceEventCategory = nodeTraceEventCategory('node.http');

function traceBegin(...args) {
  trace(kAsyncBegin, traceEventCategory, ...args);
}

function traceEnd(...args) {
  trace(kAsyncEnd, traceEventCategory, ...args);
}

function ipToInt(ip) {
  const octets = ip.split('.');
  let result = 0;
  for (let i = 0; i < octets.length; i++) {
    result = (result << 8) + NumberParseInt(octets[i]);
  }
  // Force unsigned 32-bit result
  return result >>> 0;
}

// There are two factors in play when proxying the request:
// 1. What the request protocol is, that is, whether users are sending it via
//    http.request or https.request, or whether they are sending
//    the request to a https:// URL or a http:// URL. HTTPS requests should be
//    proxied by the proxy specified using the HTTPS_PROXY environment variable.
//    HTTP requests should be proxied by the proxy specified using the HTTP_PROXY
//    environment variable.
// 2. What the proxy protocol is. This depends on the value of the environment variables,
//    for example.
//
// When proxying a HTTP request, the following needs to be done:
// https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.2
// 1. Rewrite the request path to absolute-form.
// 2. Add proxy-connection and proxy-authorization headers appropriately.
//
// When proxying a HTTPS request, the following needs to be done:
// https://datatracker.ietf.org/doc/html/rfc9110#CONNECT
// 1. Send a CONNECT request to the proxy server.
// 2. Wait for 200 connection established response to establish the tunnel.
// 3. Perform TLS handshake with the endpoint through the tunnel.
// 4. Tunnel the request using the established connection.
//
// When the proxy protocol is HTTP, the modified HTTP request can just be sent over
// the TCP socket to the proxy server, and the HTTPS request tunnel can be established
// over the TCP socket to the proxy server.
// When the proxy protocol is HTTPS, the modified request needs to be sent after
// TLS handshake with the proxy server. Same goes to the HTTPS request tunnel establishment.

/**
 * Represents the proxy configuration for an agent. The built-in http and https agent
 * implementation have one of this when they are configured to use a proxy.
 * @property {string} href - Proxy server URL with credentials redacted.
 * @property {string} protocol - Proxy protocol used to talk to the proxy server.
 * @property {string|undefined} auth - proxy-authorization header value, if username or password is provided.
 * @property {Array<string>} bypassList - List of hosts to bypass the proxy.
 * @property {object} proxyConnectionOptions - Options for connecting to the proxy server.
 */
class ProxyConfig {
  constructor(proxyUrl, keepAlive, noProxyList) {
    let parsedURL;
    try {
      parsedURL = new URL(proxyUrl);
    } catch {
      throw new ERR_PROXY_INVALID_CONFIG(`Invalid proxy URL: ${proxyUrl}`);
    }
    const { hostname, port, protocol, username, password } = parsedURL;

    if (username || password) {
      parsedURL.username = '';
      parsedURL.password = '';
      this.href = parsedURL.href;
    } else {
      this.href = proxyUrl;
    }
    this.protocol = protocol;

    if (username || password) {
      // If username or password is provided, prepare the proxy-authorization header.
      const auth = `${decodeURIComponent(username)}:${decodeURIComponent(password)}`;
      this.auth = `Basic ${Buffer.from(auth).toString('base64')}`;
    }
    if (noProxyList) {
      this.bypassList = noProxyList.split(',').map((entry) => entry.trim().toLowerCase());
    } else {
      this.bypassList = []; // No bypass list provided.
    }

    this.proxyConnectionOptions = {
      // The host name comes from parsed URL so if it starts with '[' it must be an IPv6 address
      // ending with ']'. Remove the brackets for net.connect().
      host: hostname[0] === '[' ? hostname.slice(1, -1) : hostname,
      // The port comes from parsed URL so it is either '' or a valid number string.
      port: port ? Number(port) : (protocol === 'https:' ? 443 : 80),
    };
  }

  // See: https://about.gitlab.com/blog/we-need-to-talk-no-proxy
  // TODO(joyeecheung): share code with undici.
  shouldUseProxy(hostname, port) {
    const bypassList = this.bypassList;
    if (this.bypassList.length === 0) {
      return true; // No bypass list, always use the proxy.
    }

    const host = hostname.toLowerCase();
    const hostWithPort = port ? `${host}:${port}` : host;

    for (let i = 0; i < bypassList.length; i++) {
      const entry = bypassList[i];

      if (entry === '*') return false;  // * bypasses all hosts.
      if (entry === host || entry === hostWithPort) return false;  // Matching host and host:port

      // Follow curl's behavior: strip leading dot before matching suffixes.
      if (entry[0] === '.') {
        const suffix = entry.substring(1);
        if (host === suffix || (host.endsWith(suffix) && host[host.length - suffix.length - 1] === '.')) return false;
      }

      // Handle wildcards like *.example.com
      if (entry.startsWith('*.') && host.endsWith(entry.substring(1))) return false;

      // Handle IP ranges (simple format like 192.168.1.0-192.168.1.255)
      // TODO(joyeecheung): support IPv6.
      if (entry.includes('-') && isIPv4(host)) {
        let { 0: startIP, 1: endIP } = entry.split('-');
        startIP = startIP.trim();
        endIP = endIP.trim();
        if (startIP && endIP && isIPv4(startIP) && isIPv4(endIP)) {
          const hostInt = ipToInt(host);
          const startInt = ipToInt(startIP);
          const endInt = ipToInt(endIP);
          if (hostInt >= startInt && hostInt <= endInt) return false;
        }
      }

      // It might be useful to support CIDR notation, but it's not so widely supported
      // in other tools as a de-facto standard to follow, so we don't implement it for now.
    }

    return true; // If no matches found, use the proxy.
  }
}

function parseProxyUrl(env, protocol) {
  // Get the proxy url - following the most popular convention, lower case takes precedence.
  // See https://about.gitlab.com/blog/we-need-to-talk-no-proxy/#http_proxy-and-https_proxy
  const proxyUrl = (protocol === 'https:') ?
    (env.https_proxy || env.HTTPS_PROXY) : (env.http_proxy || env.HTTP_PROXY);
  // No proxy settings from the environment, ignore.
  if (!proxyUrl) {
    return null;
  }

  if (proxyUrl.includes('\r') || proxyUrl.includes('\n')) {
    throw new ERR_PROXY_INVALID_CONFIG(`Invalid proxy URL: ${proxyUrl}`);
  }

  return proxyUrl;
}

function parseProxyConfigFromEnv(env, protocol, keepAlive) {
  // We only support proxying for HTTP and HTTPS requests.
  if (protocol !== 'http:' && protocol !== 'https:') {
    return null;
  }

  const proxyUrl = parseProxyUrl(env, protocol);
  if (proxyUrl === null) {
    return null;
  }

  // Only http:// and https:// proxies are supported.
  // Ignore instead of throw, in case other protocols are supposed to be
  // handled by the user land.
  if (!proxyUrl.startsWith('http://') && !proxyUrl.startsWith('https://')) {
    return null;
  }

  const noProxyList = env.no_proxy || env.NO_PROXY;
  return new ProxyConfig(proxyUrl, keepAlive, noProxyList);
}

/**
 * @param {ProxyConfig} proxyConfig
 * @param {object} reqOptions
 * @returns {boolean}
 */
function checkShouldUseProxy(proxyConfig, reqOptions) {
  if (!proxyConfig) {
    return false;
  }
  if (reqOptions.socketPath) {
    // If socketPath is set, the endpoint is a Unix domain socket, which can't
    // be proxied.
    return false;
  }
  return proxyConfig.shouldUseProxy(reqOptions.host || 'localhost', reqOptions.port);
}

function filterEnvForProxies(env) {
  return {
    __proto__: null,
    http_proxy: env.http_proxy,
    HTTP_PROXY: env.HTTP_PROXY,
    https_proxy: env.https_proxy,
    HTTPS_PROXY: env.HTTPS_PROXY,
    no_proxy: env.no_proxy,
    NO_PROXY: env.NO_PROXY,
  };
}

function getGlobalAgent(proxyEnv, Agent) {
  return new Agent({
    keepAlive: true, scheduling: 'lifo', timeout: 5000,
    proxyEnv,
  });
}

module.exports = {
  kOutHeaders: Symbol('kOutHeaders'),
  kNeedDrain: Symbol('kNeedDrain'),
  kProxyConfig: Symbol('kProxyConfig'),
  kWaitForProxyTunnel: Symbol('kWaitForProxyTunnel'),
  checkShouldUseProxy,
  parseProxyConfigFromEnv,
  utcDate,
  utcDateHeader,
  traceBegin,
  traceEnd,
  getNextTraceEventId,
  isTraceHTTPEnabled,
  filterEnvForProxies,
  getGlobalAgent,
  parseProxyUrl,
};