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