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