You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

215 lines
5.7 KiB
JavaScript

'use strict'
const LRU = require('lru-cache')
const url = require('url')
const isLambda = require('is-lambda')
const dns = require('./dns.js')
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,
freeSocketTimeout: 15000,
lookup: dns.getLookup(opts.dns),
}) : new HttpAgent({
maxSockets: agentMaxSockets,
localAddress: opts.localAddress,
timeout: agentTimeout,
freeSocketTimeout: 15000,
lookup: dns.getLookup(opts.dns),
})
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(',').map(n => n.trim())
}
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) {
// our current proxy agents do not support an overridden dns lookup method, so will not
// benefit from the dns cache
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')) {
// socks-proxy-agent uses hostname not host
popts.hostname = popts.host
delete popts.host
return new SocksProxyAgent(popts)
} else {
throw Object.assign(
new Error(`unsupported proxy protocol: '${proxyUrl.protocol}'`),
{
code: 'EUNSUPPORTEDPROXY',
url: proxyUrl.href,
}
)
}
}