UNPKG

undici

Version:

An HTTP/1.1 client, written from scratch for Node.js

191 lines (163 loc) • 6.74 kB
'use strict' const DispatcherBase = require('./dispatcher-base') const { kClose, kDestroy, kClosed, kDestroyed, kDispatch, kNoProxyAgent, kHttpProxyAgent, kHttpsProxyAgent } = require('../core/symbols') const ProxyAgent = require('./proxy-agent') const Agent = require('./agent') const DEFAULT_PORTS = { 'http:': 80, 'https:': 443 } class EnvHttpProxyAgent extends DispatcherBase { #noProxyValue = null #noProxyEntries = null #opts = null constructor (opts = {}) { super(opts) this.#opts = opts const { httpProxy, httpsProxy, noProxy, ...agentOpts } = opts this[kNoProxyAgent] = new Agent(agentOpts) const HTTP_PROXY = httpProxy ?? process.env.http_proxy ?? process.env.HTTP_PROXY if (HTTP_PROXY) { this[kHttpProxyAgent] = new ProxyAgent({ ...agentOpts, uri: HTTP_PROXY }) } else { this[kHttpProxyAgent] = this[kNoProxyAgent] } const HTTPS_PROXY = httpsProxy ?? process.env.https_proxy ?? process.env.HTTPS_PROXY if (HTTPS_PROXY) { this[kHttpsProxyAgent] = new ProxyAgent({ ...agentOpts, uri: HTTPS_PROXY }) } else { this[kHttpsProxyAgent] = this[kHttpProxyAgent] } this.#parseNoProxy() } [kDispatch] (opts, handler) { const url = new URL(opts.origin) const agent = this.#getProxyAgentForUrl(url) return agent.dispatch(opts, handler) } [kClose] () { return Promise.all([ this[kNoProxyAgent].close(), !this[kHttpProxyAgent][kClosed] && this[kHttpProxyAgent].close(), !this[kHttpsProxyAgent][kClosed] && this[kHttpsProxyAgent].close() ]) } [kDestroy] (err) { return Promise.all([ this[kNoProxyAgent].destroy(err), !this[kHttpProxyAgent][kDestroyed] && this[kHttpProxyAgent].destroy(err), !this[kHttpsProxyAgent][kDestroyed] && this[kHttpsProxyAgent].destroy(err) ]) } #getProxyAgentForUrl (url) { let { protocol, host: hostname, port } = url // Remove the port suffix (e.g. ":8080") and then strip surrounding // brackets from IPv6 literals (e.g. "[::1]" -> "::1") so that the // result matches the unbracketed form stored by #parseNoProxy. hostname = hostname.replace(/:\d*$/, '').replace(/^\[(.+)\]$/, '$1').toLowerCase() // Drop a trailing dot: it only marks the fully qualified form of a domain // name ("example.com." and "example.com" are the same name, RFC 1034 root // label). This runs on every dispatch, so it is a charCode check rather // than a third regex. `length > 1` leaves the degenerate host "." alone. if (hostname.length > 1 && hostname.charCodeAt(hostname.length - 1) === 46) { hostname = hostname.slice(0, -1) } port = Number.parseInt(port, 10) || DEFAULT_PORTS[protocol] || 0 if (!this.#shouldProxy(hostname, port)) { return this[kNoProxyAgent] } if (protocol === 'https:') { return this[kHttpsProxyAgent] } return this[kHttpProxyAgent] } #shouldProxy (hostname, port) { if (this.#noProxyChanged) { this.#parseNoProxy() } if (this.#noProxyEntries.length === 0) { return true // Always proxy if NO_PROXY is not set or empty. } for (let i = 0; i < this.#noProxyEntries.length; i++) { const entry = this.#noProxyEntries[i] // A bare `*` entry matches all hosts regardless of its position or the // surrounding whitespace (e.g. ` * ` or `none.invalid,*`). If a port is // attached (`*:80`) it only bypasses that port. if (entry.hostname === '*') { if (entry.port && entry.port !== port) { continue } return false // Never proxy if a wildcard entry is present. } if (entry.port && entry.port !== port) { continue // Skip if ports don't match. } // Don't proxy if the hostname is equal with the no_proxy host. A // `*.example.com` wildcard matches subdomains only, not the apex // `example.com`, so exact matches are skipped for wildcard entries. if (!entry.wildcard && hostname === entry.hostname) { return false } // Don't proxy if the hostname is the subdomain of the no_proxy host. // Reference - https://github.com/denoland/deno/blob/6fbce91e40cc07fc6da74068e5cc56fdd40f7b4c/ext/fetch/proxy.rs#L485 if (hostname.slice(-(entry.hostname.length + 1)) === `.${entry.hostname}`) { return false } } return true } #parseNoProxy () { const noProxyValue = this.#opts.noProxy ?? this.#noProxyEnv const noProxySplit = noProxyValue.split(/[,\s]/) const noProxyEntries = [] for (let i = 0; i < noProxySplit.length; i++) { const entry = noProxySplit[i] if (!entry) { continue } // An IPv6 entry with a port must be bracketed: [::1]:443. // A bare IPv6 address like ::1 contains colons that must not be // confused with a host:port separator, so we handle it separately. let hostname, port const ipv6WithPort = entry.match(/^\[(.+)\]:(\d+)$/) if (ipv6WithPort) { hostname = ipv6WithPort[1] port = Number.parseInt(ipv6WithPort[2], 10) } else { // Bracketed IPv6 without port, or plain hostname[:port], or bare IPv6. // Strip optional brackets first. const unbracketed = entry.replace(/^\[(.+)\]$/, '$1') // A bare IPv6 address contains multiple colons; a hostname:port entry // has exactly one colon followed by digits. Only attempt host:port // splitting when that is unambiguously the case. const colonCount = (unbracketed.match(/:/g) || []).length const parsed = colonCount === 1 && unbracketed.match(/^(.+):(\d+)$/) hostname = parsed ? parsed[1] : unbracketed port = parsed ? Number.parseInt(parsed[2], 10) : 0 } // A leading `*` marks a subdomain wildcard (`*.example.com`), distinct // from a plain or leading-dot suffix (`example.com` / `.example.com`) // which also matches the apex. `*.example.com` must only match // subdomains, never the apex `example.com` itself. const wildcard = entry.charCodeAt(0) === 42 /* '*' */ noProxyEntries.push({ // strip leading dot or asterisk with dot, and any trailing dot hostname: hostname.replace(/^\*?\./, '').replace(/^(.+)\.$/, '$1').toLowerCase(), port, wildcard }) } this.#noProxyValue = noProxyValue this.#noProxyEntries = noProxyEntries } get #noProxyChanged () { if (this.#opts.noProxy !== undefined) { return false } return this.#noProxyValue !== this.#noProxyEnv } get #noProxyEnv () { return process.env.no_proxy ?? process.env.NO_PROXY ?? '' } } module.exports = EnvHttpProxyAgent