agent.js 5.7 KB

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