Spaces:
Running
Running
| const net = require('net') | |
| const tls = require('tls') | |
| const { once } = require('events') | |
| const timers = require('timers/promises') | |
| const { normalizeOptions, cacheOptions } = require('./options') | |
| const { getProxy, getProxyAgent, proxyCache } = require('./proxy.js') | |
| const Errors = require('./errors.js') | |
| const { Agent: AgentBase } = require('agent-base') | |
| module.exports = class Agent extends AgentBase { | |
| #options | |
| #timeouts | |
| #proxy | |
| #noProxy | |
| #ProxyAgent | |
| constructor (options = {}) { | |
| const { timeouts, proxy, noProxy, ...normalizedOptions } = normalizeOptions(options) | |
| super(normalizedOptions) | |
| this.#options = normalizedOptions | |
| this.#timeouts = timeouts | |
| if (proxy) { | |
| this.#proxy = new URL(proxy) | |
| this.#noProxy = noProxy | |
| this.#ProxyAgent = getProxyAgent(proxy) | |
| } | |
| } | |
| get proxy () { | |
| return this.#proxy ? { url: this.#proxy } : {} | |
| } | |
| #getProxy (options) { | |
| if (!this.#proxy) { | |
| return | |
| } | |
| const proxy = getProxy(`${options.protocol}//${options.host}:${options.port}`, { | |
| proxy: this.#proxy, | |
| noProxy: this.#noProxy, | |
| }) | |
| if (!proxy) { | |
| return | |
| } | |
| const cacheKey = cacheOptions({ | |
| ...options, | |
| ...this.#options, | |
| timeouts: this.#timeouts, | |
| proxy, | |
| }) | |
| if (proxyCache.has(cacheKey)) { | |
| return proxyCache.get(cacheKey) | |
| } | |
| let ProxyAgent = this.#ProxyAgent | |
| if (Array.isArray(ProxyAgent)) { | |
| ProxyAgent = options.secureEndpoint ? ProxyAgent[1] : ProxyAgent[0] | |
| } | |
| const proxyAgent = new ProxyAgent(proxy, this.#options) | |
| proxyCache.set(cacheKey, proxyAgent) | |
| return proxyAgent | |
| } | |
| // takes an array of promises and races them against the connection timeout | |
| // which will throw the necessary error if it is hit. This will return the | |
| // result of the promise race. | |
| async #timeoutConnection ({ promises, options, timeout }, ac = new AbortController()) { | |
| if (timeout) { | |
| const connectionTimeout = timers.setTimeout(timeout, null, { signal: ac.signal }) | |
| .then(() => { | |
| throw new Errors.ConnectionTimeoutError(`${options.host}:${options.port}`) | |
| }).catch((err) => { | |
| if (err.name === 'AbortError') { | |
| return | |
| } | |
| throw err | |
| }) | |
| promises.push(connectionTimeout) | |
| } | |
| let result | |
| try { | |
| result = await Promise.race(promises) | |
| ac.abort() | |
| } catch (err) { | |
| ac.abort() | |
| throw err | |
| } | |
| return result | |
| } | |
| async connect (request, options) { | |
| // if the connection does not have its own lookup function | |
| // set, then use the one from our options | |
| options.lookup ??= this.#options.lookup | |
| let socket | |
| let timeout = this.#timeouts.connection | |
| const proxy = this.#getProxy(options) | |
| if (proxy) { | |
| // some of the proxies will wait for the socket to fully connect before | |
| // returning so we have to await this while also racing it against the | |
| // connection timeout. | |
| const start = Date.now() | |
| socket = await this.#timeoutConnection({ | |
| options, | |
| timeout, | |
| promises: [proxy.connect(request, options)], | |
| }) | |
| // see how much time proxy.connect took and subtract it from | |
| // the timeout | |
| if (timeout) { | |
| timeout = timeout - (Date.now() - start) | |
| } | |
| } else { | |
| socket = (options.secureEndpoint ? tls : net).connect(options) | |
| } | |
| socket.setKeepAlive(this.keepAlive, this.keepAliveMsecs) | |
| socket.setNoDelay(this.keepAlive) | |
| const abortController = new AbortController() | |
| const { signal } = abortController | |
| const connectPromise = socket[options.secureEndpoint ? 'secureConnecting' : 'connecting'] | |
| ? once(socket, options.secureEndpoint ? 'secureConnect' : 'connect', { signal }) | |
| : Promise.resolve() | |
| await this.#timeoutConnection({ | |
| options, | |
| timeout, | |
| promises: [ | |
| connectPromise, | |
| once(socket, 'error', { signal }).then((err) => { | |
| throw err[0] | |
| }), | |
| ], | |
| }, abortController) | |
| if (this.#timeouts.idle) { | |
| socket.setTimeout(this.#timeouts.idle, () => { | |
| socket.destroy(new Errors.IdleTimeoutError(`${options.host}:${options.port}`)) | |
| }) | |
| } | |
| return socket | |
| } | |
| addRequest (request, options) { | |
| const proxy = this.#getProxy(options) | |
| // it would be better to call proxy.addRequest here but this causes the | |
| // http-proxy-agent to call its super.addRequest which causes the request | |
| // to be added to the agent twice. since we only support 3 agents | |
| // currently (see the required agents in proxy.js) we have manually | |
| // checked that the only public methods we need to call are called in the | |
| // next block. this could change in the future and presumably we would get | |
| // failing tests until we have properly called the necessary methods on | |
| // each of our proxy agents | |
| if (proxy?.setRequestProps) { | |
| proxy.setRequestProps(request, options) | |
| } | |
| request.setHeader('connection', this.keepAlive ? 'keep-alive' : 'close') | |
| if (this.#timeouts.response) { | |
| let responseTimeout | |
| request.once('finish', () => { | |
| setTimeout(() => { | |
| request.destroy(new Errors.ResponseTimeoutError(request, this.#proxy)) | |
| }, this.#timeouts.response) | |
| }) | |
| request.once('response', () => { | |
| clearTimeout(responseTimeout) | |
| }) | |
| } | |
| if (this.#timeouts.transfer) { | |
| let transferTimeout | |
| request.once('response', (res) => { | |
| setTimeout(() => { | |
| res.destroy(new Errors.TransferTimeoutError(request, this.#proxy)) | |
| }, this.#timeouts.transfer) | |
| res.once('close', () => { | |
| clearTimeout(transferTimeout) | |
| }) | |
| }) | |
| } | |
| return super.addRequest(request, options) | |
| } | |
| } | |