agent.js 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. 'use strict'
  2. const LRU = require('lru-cache')
  3. const url = require('url')
  4. const isLambda = require('is-lambda')
  5. const AGENT_CACHE = new LRU({ max: 50 })
  6. const HttpAgent = require('agentkeepalive')
  7. const HttpsAgent = HttpAgent.HttpsAgent
  8. module.exports = getAgent
  9. const getAgentTimeout = timeout =>
  10. typeof timeout !== 'number' || !timeout ? 0 : timeout + 1
  11. const getMaxSockets = maxSockets => maxSockets || 15
  12. function getAgent (uri, opts) {
  13. const parsedUri = new url.URL(typeof uri === 'string' ? uri : uri.url)
  14. const isHttps = parsedUri.protocol === 'https:'
  15. const pxuri = getProxyUri(parsedUri.href, opts)
  16. // If opts.timeout is zero, set the agentTimeout to zero as well. A timeout
  17. // of zero disables the timeout behavior (OS limits still apply). Else, if
  18. // opts.timeout is a non-zero value, set it to timeout + 1, to ensure that
  19. // the node-fetch-npm timeout will always fire first, giving us more
  20. // consistent errors.
  21. const agentTimeout = getAgentTimeout(opts.timeout)
  22. const agentMaxSockets = getMaxSockets(opts.maxSockets)
  23. const key = [
  24. `https:${isHttps}`,
  25. pxuri
  26. ? `proxy:${pxuri.protocol}//${pxuri.host}:${pxuri.port}`
  27. : '>no-proxy<',
  28. `local-address:${opts.localAddress || '>no-local-address<'}`,
  29. `strict-ssl:${isHttps ? opts.rejectUnauthorized : '>no-strict-ssl<'}`,
  30. `ca:${(isHttps && opts.ca) || '>no-ca<'}`,
  31. `cert:${(isHttps && opts.cert) || '>no-cert<'}`,
  32. `key:${(isHttps && opts.key) || '>no-key<'}`,
  33. `timeout:${agentTimeout}`,
  34. `maxSockets:${agentMaxSockets}`,
  35. ].join(':')
  36. if (opts.agent != null) { // `agent: false` has special behavior!
  37. return opts.agent
  38. }
  39. // keep alive in AWS lambda makes no sense
  40. const lambdaAgent = !isLambda ? null
  41. : isHttps ? require('https').globalAgent
  42. : require('http').globalAgent
  43. if (isLambda && !pxuri)
  44. return lambdaAgent
  45. if (AGENT_CACHE.peek(key))
  46. return AGENT_CACHE.get(key)
  47. if (pxuri) {
  48. const pxopts = isLambda ? {
  49. ...opts,
  50. agent: lambdaAgent,
  51. } : opts
  52. const proxy = getProxy(pxuri, pxopts, isHttps)
  53. AGENT_CACHE.set(key, proxy)
  54. return proxy
  55. }
  56. const agent = isHttps ? new HttpsAgent({
  57. maxSockets: agentMaxSockets,
  58. ca: opts.ca,
  59. cert: opts.cert,
  60. key: opts.key,
  61. localAddress: opts.localAddress,
  62. rejectUnauthorized: opts.rejectUnauthorized,
  63. timeout: agentTimeout,
  64. }) : new HttpAgent({
  65. maxSockets: agentMaxSockets,
  66. localAddress: opts.localAddress,
  67. timeout: agentTimeout,
  68. })
  69. AGENT_CACHE.set(key, agent)
  70. return agent
  71. }
  72. function checkNoProxy (uri, opts) {
  73. const host = new url.URL(uri).hostname.split('.').reverse()
  74. let noproxy = (opts.noProxy || getProcessEnv('no_proxy'))
  75. if (typeof noproxy === 'string')
  76. noproxy = noproxy.split(/\s*,\s*/g)
  77. return noproxy && noproxy.some(no => {
  78. const noParts = no.split('.').filter(x => x).reverse()
  79. if (!noParts.length)
  80. return false
  81. for (let i = 0; i < noParts.length; i++) {
  82. if (host[i] !== noParts[i])
  83. return false
  84. }
  85. return true
  86. })
  87. }
  88. module.exports.getProcessEnv = getProcessEnv
  89. function getProcessEnv (env) {
  90. if (!env)
  91. return
  92. let value
  93. if (Array.isArray(env)) {
  94. for (const e of env) {
  95. value = process.env[e] ||
  96. process.env[e.toUpperCase()] ||
  97. process.env[e.toLowerCase()]
  98. if (typeof value !== 'undefined')
  99. break
  100. }
  101. }
  102. if (typeof env === 'string') {
  103. value = process.env[env] ||
  104. process.env[env.toUpperCase()] ||
  105. process.env[env.toLowerCase()]
  106. }
  107. return value
  108. }
  109. module.exports.getProxyUri = getProxyUri
  110. function getProxyUri (uri, opts) {
  111. const protocol = new url.URL(uri).protocol
  112. const proxy = opts.proxy ||
  113. (
  114. protocol === 'https:' &&
  115. getProcessEnv('https_proxy')
  116. ) ||
  117. (
  118. protocol === 'http:' &&
  119. getProcessEnv(['https_proxy', 'http_proxy', 'proxy'])
  120. )
  121. if (!proxy)
  122. return null
  123. const parsedProxy = (typeof proxy === 'string') ? new url.URL(proxy) : proxy
  124. return !checkNoProxy(uri, opts) && parsedProxy
  125. }
  126. const getAuth = u =>
  127. u.username && u.password ? decodeURIComponent(`${u.username}:${u.password}`)
  128. : u.username ? decodeURIComponent(u.username)
  129. : null
  130. const getPath = u => u.pathname + u.search + u.hash
  131. const HttpProxyAgent = require('http-proxy-agent')
  132. const HttpsProxyAgent = require('https-proxy-agent')
  133. const SocksProxyAgent = require('socks-proxy-agent')
  134. module.exports.getProxy = getProxy
  135. function getProxy (proxyUrl, opts, isHttps) {
  136. const popts = {
  137. host: proxyUrl.hostname,
  138. port: proxyUrl.port,
  139. protocol: proxyUrl.protocol,
  140. path: getPath(proxyUrl),
  141. auth: getAuth(proxyUrl),
  142. ca: opts.ca,
  143. cert: opts.cert,
  144. key: opts.key,
  145. timeout: getAgentTimeout(opts.timeout),
  146. localAddress: opts.localAddress,
  147. maxSockets: getMaxSockets(opts.maxSockets),
  148. rejectUnauthorized: opts.rejectUnauthorized,
  149. }
  150. if (proxyUrl.protocol === 'http:' || proxyUrl.protocol === 'https:') {
  151. if (!isHttps)
  152. return new HttpProxyAgent(popts)
  153. else
  154. return new HttpsProxyAgent(popts)
  155. } else if (proxyUrl.protocol.startsWith('socks'))
  156. return new SocksProxyAgent(popts)
  157. else {
  158. throw Object.assign(
  159. new Error(`unsupported proxy protocol: '${proxyUrl.protocol}'`),
  160. {
  161. url: proxyUrl.href,
  162. }
  163. )
  164. }
  165. }