agents.js 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. 'use strict'
  2. const net = require('net')
  3. const tls = require('tls')
  4. const { once } = require('events')
  5. const timers = require('timers/promises')
  6. const { normalizeOptions, cacheOptions } = require('./options')
  7. const { getProxy, getProxyAgent, proxyCache } = require('./proxy.js')
  8. const Errors = require('./errors.js')
  9. const { Agent: AgentBase } = require('agent-base')
  10. module.exports = class Agent extends AgentBase {
  11. #options
  12. #timeouts
  13. #proxy
  14. #noProxy
  15. #ProxyAgent
  16. constructor (options = {}) {
  17. const { timeouts, proxy, noProxy, ...normalizedOptions } = normalizeOptions(options)
  18. super(normalizedOptions)
  19. this.#options = normalizedOptions
  20. this.#timeouts = timeouts
  21. if (proxy) {
  22. this.#proxy = new URL(proxy)
  23. this.#noProxy = noProxy
  24. this.#ProxyAgent = getProxyAgent(proxy)
  25. }
  26. }
  27. get proxy () {
  28. return this.#proxy ? { url: this.#proxy } : {}
  29. }
  30. #getProxy (options) {
  31. if (!this.#proxy) {
  32. return
  33. }
  34. const proxy = getProxy(`${options.protocol}//${options.host}:${options.port}`, {
  35. proxy: this.#proxy,
  36. noProxy: this.#noProxy,
  37. })
  38. if (!proxy) {
  39. return
  40. }
  41. const cacheKey = cacheOptions({
  42. ...options,
  43. ...this.#options,
  44. timeouts: this.#timeouts,
  45. proxy,
  46. })
  47. if (proxyCache.has(cacheKey)) {
  48. return proxyCache.get(cacheKey)
  49. }
  50. let ProxyAgent = this.#ProxyAgent
  51. if (Array.isArray(ProxyAgent)) {
  52. ProxyAgent = this.isSecureEndpoint(options) ? ProxyAgent[1] : ProxyAgent[0]
  53. }
  54. const proxyAgent = new ProxyAgent(proxy, {
  55. ...this.#options,
  56. socketOptions: { family: this.#options.family },
  57. })
  58. proxyCache.set(cacheKey, proxyAgent)
  59. return proxyAgent
  60. }
  61. // takes an array of promises and races them against the connection timeout
  62. // which will throw the necessary error if it is hit. This will return the
  63. // result of the promise race.
  64. async #timeoutConnection ({ promises, options, timeout }, ac = new AbortController()) {
  65. if (timeout) {
  66. const connectionTimeout = timers.setTimeout(timeout, null, { signal: ac.signal })
  67. .then(() => {
  68. throw new Errors.ConnectionTimeoutError(`${options.host}:${options.port}`)
  69. }).catch((err) => {
  70. if (err.name === 'AbortError') {
  71. return
  72. }
  73. throw err
  74. })
  75. promises.push(connectionTimeout)
  76. }
  77. let result
  78. try {
  79. result = await Promise.race(promises)
  80. ac.abort()
  81. } catch (err) {
  82. ac.abort()
  83. throw err
  84. }
  85. return result
  86. }
  87. async connect (request, options) {
  88. // if the connection does not have its own lookup function
  89. // set, then use the one from our options
  90. options.lookup ??= this.#options.lookup
  91. let socket
  92. let timeout = this.#timeouts.connection
  93. const isSecureEndpoint = this.isSecureEndpoint(options)
  94. const proxy = this.#getProxy(options)
  95. if (proxy) {
  96. // some of the proxies will wait for the socket to fully connect before
  97. // returning so we have to await this while also racing it against the
  98. // connection timeout.
  99. const start = Date.now()
  100. socket = await this.#timeoutConnection({
  101. options,
  102. timeout,
  103. promises: [proxy.connect(request, options)],
  104. })
  105. // see how much time proxy.connect took and subtract it from
  106. // the timeout
  107. if (timeout) {
  108. timeout = timeout - (Date.now() - start)
  109. }
  110. } else {
  111. socket = (isSecureEndpoint ? tls : net).connect(options)
  112. }
  113. socket.setKeepAlive(this.keepAlive, this.keepAliveMsecs)
  114. socket.setNoDelay(this.keepAlive)
  115. const abortController = new AbortController()
  116. const { signal } = abortController
  117. const connectPromise = socket[isSecureEndpoint ? 'secureConnecting' : 'connecting']
  118. ? once(socket, isSecureEndpoint ? 'secureConnect' : 'connect', { signal })
  119. : Promise.resolve()
  120. await this.#timeoutConnection({
  121. options,
  122. timeout,
  123. promises: [
  124. connectPromise,
  125. once(socket, 'error', { signal }).then((err) => {
  126. throw err[0]
  127. }),
  128. ],
  129. }, abortController)
  130. if (this.#timeouts.idle) {
  131. socket.setTimeout(this.#timeouts.idle, () => {
  132. socket.destroy(new Errors.IdleTimeoutError(`${options.host}:${options.port}`))
  133. })
  134. }
  135. return socket
  136. }
  137. addRequest (request, options) {
  138. const proxy = this.#getProxy(options)
  139. // it would be better to call proxy.addRequest here but this causes the
  140. // http-proxy-agent to call its super.addRequest which causes the request
  141. // to be added to the agent twice. since we only support 3 agents
  142. // currently (see the required agents in proxy.js) we have manually
  143. // checked that the only public methods we need to call are called in the
  144. // next block. this could change in the future and presumably we would get
  145. // failing tests until we have properly called the necessary methods on
  146. // each of our proxy agents
  147. if (proxy?.setRequestProps) {
  148. proxy.setRequestProps(request, options)
  149. }
  150. request.setHeader('connection', this.keepAlive ? 'keep-alive' : 'close')
  151. if (this.#timeouts.response) {
  152. let responseTimeout
  153. request.once('finish', () => {
  154. setTimeout(() => {
  155. request.destroy(new Errors.ResponseTimeoutError(request, this.#proxy))
  156. }, this.#timeouts.response)
  157. })
  158. request.once('response', () => {
  159. clearTimeout(responseTimeout)
  160. })
  161. }
  162. if (this.#timeouts.transfer) {
  163. let transferTimeout
  164. request.once('response', (res) => {
  165. setTimeout(() => {
  166. res.destroy(new Errors.TransferTimeoutError(request, this.#proxy))
  167. }, this.#timeouts.transfer)
  168. res.once('close', () => {
  169. clearTimeout(transferTimeout)
  170. })
  171. })
  172. }
  173. return super.addRequest(request, options)
  174. }
  175. }