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) | |
} | |
} | |