agent.js 5.24 KB
'use strict'
const LRU = require('lru-cache')
const url = require('url')
const isLambda = require('is-lambda')

const AGENT_CACHE = new LRU({ max: 50 })
const HttpAgent = require('agentkeepalive')
const HttpsAgent = HttpAgent.HttpsAgent

module.exports = getAgent

const getAgentTimeout = timeout =>
  typeof timeout !== 'number' || !timeout ? 0 : timeout + 1

const getMaxSockets = maxSockets => maxSockets || 15

function getAgent (uri, opts) {
  const parsedUri = new url.URL(typeof uri === 'string' ? uri : uri.url)
  const isHttps = parsedUri.protocol === 'https:'
  const pxuri = getProxyUri(parsedUri.href, opts)

  // If opts.timeout is zero, set the agentTimeout to zero as well. A timeout
  // of zero disables the timeout behavior (OS limits still apply). Else, if
  // opts.timeout is a non-zero value, set it to timeout + 1, to ensure that
  // the node-fetch-npm timeout will always fire first, giving us more
  // consistent errors.
  const agentTimeout = getAgentTimeout(opts.timeout)
  const agentMaxSockets = getMaxSockets(opts.maxSockets)

  const key = [
    `https:${isHttps}`,
    pxuri
      ? `proxy:${pxuri.protocol}//${pxuri.host}:${pxuri.port}`
      : '>no-proxy<',
    `local-address:${opts.localAddress || '>no-local-address<'}`,
    `strict-ssl:${isHttps ? opts.rejectUnauthorized : '>no-strict-ssl<'}`,
    `ca:${(isHttps && opts.ca) || '>no-ca<'}`,
    `cert:${(isHttps && opts.cert) || '>no-cert<'}`,
    `key:${(isHttps && opts.key) || '>no-key<'}`,
    `timeout:${agentTimeout}`,
    `maxSockets:${agentMaxSockets}`,
  ].join(':')

  if (opts.agent != null) { // `agent: false` has special behavior!
    return opts.agent
  }

  // keep alive in AWS lambda makes no sense
  const lambdaAgent = !isLambda ? null
    : isHttps ? require('https').globalAgent
    : require('http').globalAgent

  if (isLambda && !pxuri)
    return lambdaAgent

  if (AGENT_CACHE.peek(key))
    return AGENT_CACHE.get(key)

  if (pxuri) {
    const pxopts = isLambda ? {
      ...opts,
      agent: lambdaAgent,
    } : opts
    const proxy = getProxy(pxuri, pxopts, isHttps)
    AGENT_CACHE.set(key, proxy)
    return proxy
  }

  const agent = isHttps ? new HttpsAgent({
    maxSockets: agentMaxSockets,
    ca: opts.ca,
    cert: opts.cert,
    key: opts.key,
    localAddress: opts.localAddress,
    rejectUnauthorized: opts.rejectUnauthorized,
    timeout: agentTimeout,
  }) : new HttpAgent({
    maxSockets: agentMaxSockets,
    localAddress: opts.localAddress,
    timeout: agentTimeout,
  })
  AGENT_CACHE.set(key, agent)
  return agent
}

function checkNoProxy (uri, opts) {
  const host = new url.URL(uri).hostname.split('.').reverse()
  let noproxy = (opts.noProxy || getProcessEnv('no_proxy'))
  if (typeof noproxy === 'string')
    noproxy = noproxy.split(/\s*,\s*/g)

  return noproxy && noproxy.some(no => {
    const noParts = no.split('.').filter(x => x).reverse()
    if (!noParts.length)
      return false
    for (let i = 0; i < noParts.length; i++) {
      if (host[i] !== noParts[i])
        return false
    }
    return true
  })
}

module.exports.getProcessEnv = getProcessEnv

function getProcessEnv (env) {
  if (!env)
    return

  let value

  if (Array.isArray(env)) {
    for (const e of env) {
      value = process.env[e] ||
        process.env[e.toUpperCase()] ||
        process.env[e.toLowerCase()]
      if (typeof value !== 'undefined')
        break
    }
  }

  if (typeof env === 'string') {
    value = process.env[env] ||
      process.env[env.toUpperCase()] ||
      process.env[env.toLowerCase()]
  }

  return value
}

module.exports.getProxyUri = getProxyUri
function getProxyUri (uri, opts) {
  const protocol = new url.URL(uri).protocol

  const proxy = opts.proxy ||
    (
      protocol === 'https:' &&
      getProcessEnv('https_proxy')
    ) ||
    (
      protocol === 'http:' &&
      getProcessEnv(['https_proxy', 'http_proxy', 'proxy'])
    )
  if (!proxy)
    return null

  const parsedProxy = (typeof proxy === 'string') ? new url.URL(proxy) : proxy

  return !checkNoProxy(uri, opts) && parsedProxy
}

const getAuth = u =>
  u.username && u.password ? decodeURIComponent(`${u.username}:${u.password}`)
  : u.username ? decodeURIComponent(u.username)
  : null

const getPath = u => u.pathname + u.search + u.hash

const HttpProxyAgent = require('http-proxy-agent')
const HttpsProxyAgent = require('https-proxy-agent')
const SocksProxyAgent = require('socks-proxy-agent')
module.exports.getProxy = getProxy
function getProxy (proxyUrl, opts, isHttps) {
  const popts = {
    host: proxyUrl.hostname,
    port: proxyUrl.port,
    protocol: proxyUrl.protocol,
    path: getPath(proxyUrl),
    auth: getAuth(proxyUrl),
    ca: opts.ca,
    cert: opts.cert,
    key: opts.key,
    timeout: getAgentTimeout(opts.timeout),
    localAddress: opts.localAddress,
    maxSockets: getMaxSockets(opts.maxSockets),
    rejectUnauthorized: opts.rejectUnauthorized,
  }

  if (proxyUrl.protocol === 'http:' || proxyUrl.protocol === 'https:') {
    if (!isHttps)
      return new HttpProxyAgent(popts)
    else
      return new HttpsProxyAgent(popts)
  } else if (proxyUrl.protocol.startsWith('socks'))
    return new SocksProxyAgent(popts)
  else {
    throw Object.assign(
      new Error(`unsupported proxy protocol: '${proxyUrl.protocol}'`),
      {
        url: proxyUrl.href,
      }
    )
  }
}