@npmcli/agent
Advanced tools
Comparing version 2.0.0 to 2.1.0
@@ -6,39 +6,36 @@ 'use strict' | ||
const defaultOptions = exports.defaultOptions = { | ||
family: undefined, | ||
hints: dns.ADDRCONFIG, | ||
all: false, | ||
verbatim: undefined, | ||
} | ||
// this is a factory so that each request can have its own opts (i.e. ttl) | ||
// while still sharing the cache across all requests | ||
const cache = new LRUCache({ max: 50 }) | ||
const lookupCache = exports.lookupCache = new LRUCache({ max: 50 }) | ||
const getOptions = ({ | ||
family = 0, | ||
hints = dns.ADDRCONFIG, | ||
all = false, | ||
verbatim = undefined, | ||
ttl = 5 * 60 * 1000, | ||
lookup = dns.lookup, | ||
}) => ({ | ||
// hints and lookup are returned since both are top level properties to (net|tls).connect | ||
hints, | ||
lookup: (hostname, ...args) => { | ||
const callback = args.pop() // callback is always last arg | ||
const lookupOptions = args[0] ?? {} | ||
// this is a factory so that each request can have its own opts (i.e. ttl) | ||
// while still sharing the cache across all requests | ||
exports.getLookup = (dnsOptions) => { | ||
return (hostname, options, callback) => { | ||
if (typeof options === 'function') { | ||
callback = options | ||
options = null | ||
} else if (typeof options === 'number') { | ||
options = { family: options } | ||
const options = { | ||
family, | ||
hints, | ||
all, | ||
verbatim, | ||
...(typeof lookupOptions === 'number' ? { family: lookupOptions } : lookupOptions), | ||
} | ||
options = { ...defaultOptions, ...options } | ||
const key = JSON.stringify({ hostname, ...options }) | ||
const key = JSON.stringify({ | ||
hostname, | ||
family: options.family, | ||
hints: options.hints, | ||
all: options.all, | ||
verbatim: options.verbatim, | ||
}) | ||
if (lookupCache.has(key)) { | ||
const [address, family] = lookupCache.get(key) | ||
process.nextTick(callback, null, address, family) | ||
return | ||
if (cache.has(key)) { | ||
const cached = cache.get(key) | ||
return process.nextTick(callback, null, ...cached) | ||
} | ||
dnsOptions.lookup(hostname, options, (err, address, family) => { | ||
lookup(hostname, options, (err, ...result) => { | ||
if (err) { | ||
@@ -48,6 +45,11 @@ return callback(err) | ||
lookupCache.set(key, [address, family], { ttl: dnsOptions.ttl }) | ||
return callback(null, address, family) | ||
cache.set(key, result, { ttl }) | ||
return callback(null, ...result) | ||
}) | ||
} | ||
}, | ||
}) | ||
module.exports = { | ||
cache, | ||
getOptions, | ||
} |
'use strict' | ||
const { appendPort } = require('./util') | ||
class InvalidProxyProtocolError extends Error { | ||
@@ -11,13 +13,5 @@ constructor (url) { | ||
class InvalidProxyResponseError extends Error { | ||
constructor (url, status) { | ||
super(`Invalid status code \`${status}\` connecting to proxy \`${url.host}\``) | ||
this.code = 'EINVALIDRESPONSE' | ||
this.proxy = url | ||
this.status = status | ||
} | ||
} | ||
class ConnectionTimeoutError extends Error { | ||
constructor (host) { | ||
constructor ({ host, port }) { | ||
host = appendPort(host, port) | ||
super(`Timeout connecting to host \`${host}\``) | ||
@@ -30,3 +24,4 @@ this.code = 'ECONNECTIONTIMEOUT' | ||
class IdleTimeoutError extends Error { | ||
constructor (host) { | ||
constructor ({ host, port }) { | ||
host = appendPort(host, port) | ||
super(`Idle timeout reached for host \`${host}\``) | ||
@@ -39,6 +34,6 @@ this.code = 'EIDLETIMEOUT' | ||
class ResponseTimeoutError extends Error { | ||
constructor (proxy, request) { | ||
constructor (request, proxy) { | ||
let msg = 'Response timeout ' | ||
if (proxy.url) { | ||
msg += `from proxy \`${proxy.url.host}\` ` | ||
if (proxy) { | ||
msg += `from proxy \`${proxy.host}\` ` | ||
} | ||
@@ -48,3 +43,3 @@ msg += `connecting to host \`${request.host}\`` | ||
this.code = 'ERESPONSETIMEOUT' | ||
this.proxy = proxy.url | ||
this.proxy = proxy | ||
this.request = request | ||
@@ -55,6 +50,6 @@ } | ||
class TransferTimeoutError extends Error { | ||
constructor (proxy, request) { | ||
constructor (request, proxy) { | ||
let msg = 'Transfer timeout ' | ||
if (proxy.url) { | ||
msg += `from proxy \`${proxy.url.host}\` ` | ||
if (proxy) { | ||
msg += `from proxy \`${proxy.host}\` ` | ||
} | ||
@@ -64,3 +59,3 @@ msg += `for \`${request.host}\`` | ||
this.code = 'ETRANSFERTIMEOUT' | ||
this.proxy = proxy.url | ||
this.proxy = proxy | ||
this.request = request | ||
@@ -72,3 +67,2 @@ } | ||
InvalidProxyProtocolError, | ||
InvalidProxyResponseError, | ||
ConnectionTimeoutError, | ||
@@ -75,0 +69,0 @@ IdleTimeoutError, |
149
lib/index.js
'use strict' | ||
const { normalizeOptions } = require('./util.js') | ||
const HttpAgent = require('./http.js') | ||
const HttpsAgent = require('./https.js') | ||
const { LRUCache } = require('lru-cache') | ||
const { urlify, cacheAgent } = require('./util') | ||
const { normalizeOptions, cacheOptions } = require('./options') | ||
const { getProxy, proxyCache } = require('./proxy.js') | ||
const dns = require('./dns.js') | ||
const { HttpAgent, HttpsAgent } = require('./agents.js') | ||
const AgentCache = new Map() | ||
const agentCache = new LRUCache({ max: 20 }) | ||
const proxyEnv = {} | ||
for (const [key, value] of Object.entries(process.env)) { | ||
const lowerKey = key.toLowerCase() | ||
if (['https_proxy', 'http_proxy', 'proxy', 'no_proxy'].includes(lowerKey)) { | ||
proxyEnv[lowerKey] = value | ||
} | ||
} | ||
const getAgent = (url, options) => { | ||
url = new URL(url) | ||
options = normalizeOptions(options) | ||
const getAgent = (url, { agent: _agent, proxy: _proxy, noProxy, ..._options } = {}) => { | ||
// false has meaning so this can't be a simple truthiness check | ||
if (options.agent != null) { | ||
return options.agent | ||
if (_agent != null) { | ||
return _agent | ||
} | ||
const isHttps = url.protocol === 'https:' | ||
url = urlify(url) | ||
let proxy = options.proxy | ||
if (!proxy) { | ||
proxy = isHttps | ||
? proxyEnv.https_proxy | ||
: (proxyEnv.https_proxy || proxyEnv.http_proxy || proxyEnv.proxy) | ||
} | ||
const secure = url.protocol === 'https:' | ||
const proxy = getProxy(url, { proxy: _proxy, noProxy }) | ||
const options = { ...normalizeOptions(_options), proxy } | ||
if (proxy) { | ||
proxy = new URL(proxy) | ||
let noProxy = options.noProxy || proxyEnv.no_proxy | ||
if (typeof noProxy === 'string') { | ||
noProxy = noProxy.split(',').map((p) => p.trim()) | ||
} | ||
if (noProxy) { | ||
const hostSegments = url.hostname.split('.').reverse() | ||
const matches = noProxy.some((no) => { | ||
const noSegments = no.split('.').filter(Boolean).reverse() | ||
if (!noSegments.length) { | ||
return false | ||
} | ||
for (let i = 0; i < noSegments.length; ++i) { | ||
if (hostSegments[i] !== noSegments[i]) { | ||
return false | ||
} | ||
} | ||
return true | ||
}) | ||
if (matches) { | ||
proxy = '' | ||
} | ||
} | ||
} | ||
const timeouts = [ | ||
options.timeouts.connection || 0, | ||
options.timeouts.idle || 0, | ||
options.timeouts.response || 0, | ||
options.timeouts.transfer || 0, | ||
].join('.') | ||
const maxSockets = options.maxSockets || 15 | ||
let proxyDescriptor = 'proxy:' | ||
if (!proxy) { | ||
proxyDescriptor += 'null' | ||
} else { | ||
proxyDescriptor += `${proxy.protocol}//` | ||
let auth = '' | ||
if (proxy.username) { | ||
auth += proxy.username | ||
} | ||
if (proxy.password) { | ||
auth += `:${proxy.password}` | ||
} | ||
if (auth) { | ||
proxyDescriptor += `${auth}@` | ||
} | ||
proxyDescriptor += proxy.host | ||
} | ||
const key = [ | ||
`https:${isHttps}`, | ||
proxyDescriptor, | ||
`local-address:${options.localAddress || 'null'}`, | ||
`strict-ssl:${isHttps ? options.rejectUnauthorized : 'false'}`, | ||
`ca:${isHttps && options.ca || 'null'}`, | ||
`cert:${isHttps && options.cert || 'null'}`, | ||
`key:${isHttps && options.key || 'null'}`, | ||
`timeouts:${timeouts}`, | ||
`maxSockets:${maxSockets}`, | ||
].join(':') | ||
if (AgentCache.has(key)) { | ||
return AgentCache.get(key) | ||
} | ||
const agentOptions = { | ||
ca: options.ca, | ||
cert: options.cert, | ||
key: options.key, | ||
rejectUnauthorized: options.rejectUnauthorized, | ||
maxSockets, | ||
timeouts: options.timeouts, | ||
localAddress: options.localAddress, | ||
proxy, | ||
} | ||
const agent = isHttps | ||
? new HttpsAgent(agentOptions) | ||
: new HttpAgent(agentOptions) | ||
AgentCache.set(key, agent) | ||
return agent | ||
return cacheAgent({ | ||
key: cacheOptions({ ...options, secure }), | ||
cache: agentCache, | ||
secure, | ||
proxies: [HttpAgent, HttpsAgent], | ||
}, options) | ||
} | ||
@@ -135,2 +36,12 @@ | ||
HttpsAgent, | ||
cache: { | ||
proxy: proxyCache, | ||
agent: agentCache, | ||
dns: dns.cache, | ||
clear: () => { | ||
proxyCache.clear() | ||
agentCache.clear() | ||
dns.cache.clear() | ||
}, | ||
}, | ||
} |
'use strict' | ||
const dns = require('dns') | ||
const timers = require('timers/promises') | ||
const normalizeOptions = (_options) => { | ||
const options = { ..._options } | ||
const createKey = (obj) => { | ||
let key = '' | ||
const sorted = Object.entries(obj).sort((a, b) => a[0] - b[0]) | ||
for (let [k, v] of sorted) { | ||
if (v == null) { | ||
v = 'null' | ||
} else if (v instanceof URL) { | ||
v = v.toString() | ||
} else if (typeof v === 'object') { | ||
v = createKey(v) | ||
} | ||
key += `${k}:${v}:` | ||
} | ||
return key | ||
} | ||
if (typeof options.keepAlive === 'undefined') { | ||
options.keepAlive = true | ||
const createTimeout = (delay, signal) => { | ||
if (!delay) { | ||
return signal ? new Promise(() => {}) : null | ||
} | ||
if (!options.timeouts) { | ||
options.timeouts = {} | ||
if (!signal) { | ||
let timeout | ||
return { | ||
start: (cb) => (timeout = setTimeout(cb, delay)), | ||
clear: () => clearTimeout(timeout), | ||
} | ||
} | ||
if (options.timeout) { | ||
options.timeouts.idle = options.timeout | ||
delete options.timeout | ||
return timers.setTimeout(delay, null, signal) | ||
.then(() => { | ||
throw new Error() | ||
}).catch((err) => { | ||
if (err.name === 'AbortError') { | ||
return | ||
} | ||
throw err | ||
}) | ||
} | ||
const abortRace = async (promises, ac = new AbortController()) => { | ||
let res | ||
try { | ||
res = await Promise.race(promises.map((p) => p(ac))) | ||
ac.abort() | ||
} catch (err) { | ||
ac.abort() | ||
throw err | ||
} | ||
return res | ||
} | ||
options.family = !isNaN(+options.family) ? +options.family : 0 | ||
options.dns = { | ||
ttl: 5 * 60 * 1000, | ||
lookup: dns.lookup, | ||
...options.dns, | ||
const urlify = (url) => typeof url === 'string' ? new URL(url) : url | ||
const appendPort = (host, port) => { | ||
// istanbul ignore next | ||
if (port) { | ||
host += `:${port}` | ||
} | ||
return host | ||
} | ||
return options | ||
const cacheAgent = ({ key, cache, secure, proxies }, ...args) => { | ||
if (cache.has(key)) { | ||
return cache.get(key) | ||
} | ||
const Ctor = (secure ? proxies[1] : proxies[0]) ?? proxies[0] | ||
const agent = new Ctor(...args) | ||
cache.set(key, agent) | ||
return agent | ||
} | ||
module.exports = { | ||
normalizeOptions, | ||
createKey, | ||
createTimeout, | ||
abortRace, | ||
urlify, | ||
cacheAgent, | ||
appendPort, | ||
} |
{ | ||
"name": "@npmcli/agent", | ||
"version": "2.0.0", | ||
"version": "2.1.0", | ||
"description": "the http/https agent used by the npm cli", | ||
@@ -40,2 +40,8 @@ "main": "lib/index.js", | ||
}, | ||
"dependencies": { | ||
"http-proxy-agent": "^7.0.0", | ||
"https-proxy-agent": "^7.0.1", | ||
"lru-cache": "^10.0.1", | ||
"socks-proxy-agent": "^8.0.1" | ||
}, | ||
"devDependencies": { | ||
@@ -58,7 +64,3 @@ "@npmcli/eslint-config": "^4.0.0", | ||
] | ||
}, | ||
"dependencies": { | ||
"lru-cache": "^10.0.1", | ||
"socks": "^2.7.1" | ||
} | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 2 instances in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
8
19357
4
9
524
+ Addedhttp-proxy-agent@^7.0.0
+ Addedhttps-proxy-agent@^7.0.1
+ Addedsocks-proxy-agent@^8.0.1
+ Addedagent-base@7.1.1(transitive)
+ Addeddebug@4.3.7(transitive)
+ Addedhttp-proxy-agent@7.0.2(transitive)
+ Addedhttps-proxy-agent@7.0.5(transitive)
+ Addedms@2.1.3(transitive)
+ Addedsocks-proxy-agent@8.0.4(transitive)
- Removedsocks@^2.7.1