Skip to content

Commit 284cbfd

Browse files
committedOct 2, 2023
deps: @npmcli/agent@2.2.0
Fixes #6835
1 parent 68031f2 commit 284cbfd

File tree

9 files changed

+261
-327
lines changed

9 files changed

+261
-327
lines changed
 

‎DEPENDENCIES.md

+1
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,7 @@ graph LR;
606606
npm-registry-fetch-->minizlib;
607607
npm-registry-fetch-->npm-package-arg;
608608
npm-registry-fetch-->proc-log;
609+
npmcli-agent-->agent-base;
609610
npmcli-agent-->http-proxy-agent;
610611
npmcli-agent-->https-proxy-agent;
611612
npmcli-agent-->lru-cache;
+157-154
Original file line numberDiff line numberDiff line change
@@ -1,199 +1,202 @@
11
'use strict'
22

3-
const http = require('http')
4-
const https = require('https')
53
const net = require('net')
64
const tls = require('tls')
75
const { once } = require('events')
8-
const { createTimeout, abortRace, urlify, appendPort, cacheAgent } = require('./util')
6+
const timers = require('timers/promises')
97
const { normalizeOptions, cacheOptions } = require('./options')
10-
const { getProxy, getProxyType, proxyCache } = require('./proxy.js')
8+
const { getProxy, getProxyAgent, proxyCache } = require('./proxy.js')
119
const Errors = require('./errors.js')
10+
const { Agent: AgentBase } = require('agent-base')
1211

13-
const createAgent = (base, name) => {
14-
const SECURE = base === https
15-
const SOCKET_TYPE = SECURE ? tls : net
12+
module.exports = class Agent extends AgentBase {
13+
#options
14+
#timeouts
15+
#proxy
16+
#noProxy
17+
#ProxyAgent
1618

17-
const agent = class extends base.Agent {
18-
#options
19-
#timeouts
20-
#proxy
21-
#socket
19+
constructor (options = {}) {
20+
const { timeouts, proxy, noProxy, ...normalizedOptions } = normalizeOptions(options)
2221

23-
constructor (_options) {
24-
const { timeouts, proxy, noProxy, ...options } = normalizeOptions(_options)
22+
super(normalizedOptions)
2523

26-
super(options)
24+
this.#options = normalizedOptions
25+
this.#timeouts = timeouts
2726

28-
this.#options = options
29-
this.#timeouts = timeouts
30-
this.#proxy = proxy ? { proxies: getProxyType(proxy), proxy: urlify(proxy), noProxy } : null
27+
if (proxy) {
28+
this.#proxy = new URL(proxy)
29+
this.#noProxy = noProxy
30+
this.#ProxyAgent = getProxyAgent(proxy)
3131
}
32+
}
3233

33-
get proxy () {
34-
return this.#proxy ? { url: this.#proxy.proxy } : {}
35-
}
34+
get proxy () {
35+
return this.#proxy ? { url: this.#proxy } : {}
36+
}
3637

37-
#getProxy (options) {
38-
const proxy = this.#proxy
39-
? getProxy(appendPort(`${options.protocol}//${options.host}`, options.port), this.#proxy)
40-
: null
38+
#getProxy (options) {
39+
if (!this.#proxy) {
40+
return
41+
}
4142

42-
if (!proxy) {
43-
return
44-
}
43+
const proxy = getProxy(`${options.protocol}//${options.host}:${options.port}`, {
44+
proxy: this.#proxy,
45+
noProxy: this.#noProxy,
46+
})
4547

46-
return cacheAgent({
47-
key: cacheOptions({
48-
...options,
49-
...this.#options,
50-
secure: SECURE,
51-
timeouts: this.#timeouts,
52-
proxy,
53-
}),
54-
cache: proxyCache,
55-
secure: SECURE,
56-
proxies: this.#proxy.proxies,
57-
}, proxy, this.#options)
48+
if (!proxy) {
49+
return
5850
}
5951

60-
#setKeepAlive (socket) {
61-
socket.setKeepAlive(this.keepAlive, this.keepAliveMsecs)
62-
socket.setNoDelay(this.keepAlive)
63-
}
52+
const cacheKey = cacheOptions({
53+
...options,
54+
...this.#options,
55+
timeouts: this.#timeouts,
56+
proxy,
57+
})
6458

65-
#setIdleTimeout (socket, options) {
66-
if (this.#timeouts.idle) {
67-
socket.setTimeout(this.#timeouts.idle, () => {
68-
socket.destroy(new Errors.IdleTimeoutError(options))
69-
})
70-
}
59+
if (proxyCache.has(cacheKey)) {
60+
return proxyCache.get(cacheKey)
7161
}
7262

73-
async #proxyConnect (proxy, request, options) {
74-
// socks-proxy-agent accepts a dns lookup function
75-
options.lookup ??= this.#options.lookup
76-
77-
// all the proxy agents use this secureEndpoint option to determine
78-
// if the proxy should connect over tls or not. we can set it based
79-
// on if the HttpAgent or HttpsAgent is used.
80-
options.secureEndpoint = SECURE
63+
let ProxyAgent = this.#ProxyAgent
64+
if (Array.isArray(ProxyAgent)) {
65+
ProxyAgent = options.secureEndpoint ? ProxyAgent[1] : ProxyAgent[0]
66+
}
8167

82-
const socket = await abortRace([
83-
(ac) => createTimeout(this.#timeouts.connection, ac).catch(() => {
84-
throw new Errors.ConnectionTimeoutError(options)
85-
}),
86-
(ac) => proxy.connect(request, options).then((s) => {
87-
this.#setKeepAlive(s)
68+
const proxyAgent = new ProxyAgent(proxy, this.#options)
69+
proxyCache.set(cacheKey, proxyAgent)
8870

89-
const connectEvent = SECURE ? 'secureConnect' : 'connect'
90-
const connectingEvent = SECURE ? 'secureConnecting' : 'connecting'
71+
return proxyAgent
72+
}
9173

92-
if (!s[connectingEvent]) {
93-
return s
74+
// takes an array of promises and races them against the connection timeout
75+
// which will throw the necessary error if it is hit. This will return the
76+
// result of the promise race.
77+
async #timeoutConnection ({ promises, options, timeout }, ac = new AbortController()) {
78+
if (timeout) {
79+
const connectionTimeout = timers.setTimeout(timeout, null, { signal: ac.signal })
80+
.then(() => {
81+
throw new Errors.ConnectionTimeoutError(`${options.host}:${options.port}`)
82+
}).catch((err) => {
83+
if (err.name === 'AbortError') {
84+
return
9485
}
86+
throw err
87+
})
88+
promises.push(connectionTimeout)
89+
}
9590

96-
return abortRace([
97-
() => once(s, 'error', ac).then((err) => {
98-
throw err
99-
}),
100-
() => once(s, connectEvent, ac).then(() => s),
101-
], ac)
102-
}),
103-
])
104-
105-
this.#setIdleTimeout(socket, options)
106-
107-
return socket
91+
let result
92+
try {
93+
result = await Promise.race(promises)
94+
ac.abort()
95+
} catch (err) {
96+
ac.abort()
97+
throw err
10898
}
99+
return result
100+
}
109101

110-
async connect (request, options) {
111-
const proxy = this.#getProxy(options)
112-
if (proxy) {
113-
return this.#proxyConnect(proxy, request, options)
102+
async connect (request, options) {
103+
// if the connection does not have its own lookup function
104+
// set, then use the one from our options
105+
options.lookup ??= this.#options.lookup
106+
107+
let socket
108+
let timeout = this.#timeouts.connection
109+
110+
const proxy = this.#getProxy(options)
111+
if (proxy) {
112+
// some of the proxies will wait for the socket to fully connect before
113+
// returning so we have to await this while also racing it against the
114+
// connection timeout.
115+
const start = Date.now()
116+
socket = await this.#timeoutConnection({
117+
options,
118+
timeout,
119+
promises: [proxy.connect(request, options)],
120+
})
121+
// see how much time proxy.connect took and subtract it from
122+
// the timeout
123+
if (timeout) {
124+
timeout = timeout - (Date.now() - start)
114125
}
126+
} else {
127+
socket = (options.secureEndpoint ? tls : net).connect(options)
128+
}
115129

116-
const socket = SOCKET_TYPE.connect(options)
130+
socket.setKeepAlive(this.keepAlive, this.keepAliveMsecs)
131+
socket.setNoDelay(this.keepAlive)
117132

118-
this.#setKeepAlive(socket)
133+
const abortController = new AbortController()
134+
const { signal } = abortController
119135

120-
await abortRace([
121-
(s) => createTimeout(this.#timeouts.connection, s).catch(() => {
122-
throw new Errors.ConnectionTimeoutError(options)
123-
}),
124-
(s) => once(socket, 'error', s).then((err) => {
125-
throw err
126-
}),
127-
(s) => once(socket, 'connect', s),
128-
])
136+
const connectPromise = socket[options.secureEndpoint ? 'secureConnecting' : 'connecting']
137+
? once(socket, options.secureEndpoint ? 'secureConnect' : 'connect', { signal })
138+
: Promise.resolve()
129139

130-
this.#setIdleTimeout(socket, options)
140+
await this.#timeoutConnection({
141+
options,
142+
timeout,
143+
promises: [
144+
connectPromise,
145+
once(socket, 'error', { signal }).then((err) => {
146+
throw err[0]
147+
}),
148+
],
149+
}, abortController)
131150

132-
return socket
151+
if (this.#timeouts.idle) {
152+
socket.setTimeout(this.#timeouts.idle, () => {
153+
socket.destroy(new Errors.IdleTimeoutError(`${options.host}:${options.port}`))
154+
})
133155
}
134156

135-
addRequest (request, options) {
136-
const proxy = this.#getProxy(options)
137-
// it would be better to call proxy.addRequest here but this causes the
138-
// http-proxy-agent to call its super.addRequest which causes the request
139-
// to be added to the agent twice. since we only support 3 agents
140-
// currently (see the required agents in proxy.js) we have manually
141-
// checked that the only public methods we need to call are called in the
142-
// next block. this could change in the future and presumably we would get
143-
// failing tests until we have properly called the necessary methods on
144-
// each of our proxy agents
145-
if (proxy?.setRequestProps) {
146-
proxy.setRequestProps(request, options)
147-
}
148-
149-
request.setHeader('connection', this.keepAlive ? 'keep-alive' : 'close')
150-
151-
const responseTimeout = createTimeout(this.#timeouts.response)
152-
if (responseTimeout) {
153-
request.once('finish', () => {
154-
responseTimeout.start(() => {
155-
request.destroy(new Errors.ResponseTimeoutError(request, this.proxy?.url))
156-
})
157-
})
158-
request.once('response', () => {
159-
responseTimeout.clear()
160-
})
161-
}
162-
163-
const transferTimeout = createTimeout(this.#timeouts.transfer)
164-
if (transferTimeout) {
165-
request.once('response', (res) => {
166-
transferTimeout.start(() => {
167-
res.destroy(new Errors.TransferTimeoutError(request, this.proxy?.url))
168-
})
169-
res.once('close', () => {
170-
transferTimeout.clear()
171-
})
172-
})
173-
}
157+
return socket
158+
}
174159

175-
return super.addRequest(request, options)
160+
addRequest (request, options) {
161+
const proxy = this.#getProxy(options)
162+
// it would be better to call proxy.addRequest here but this causes the
163+
// http-proxy-agent to call its super.addRequest which causes the request
164+
// to be added to the agent twice. since we only support 3 agents
165+
// currently (see the required agents in proxy.js) we have manually
166+
// checked that the only public methods we need to call are called in the
167+
// next block. this could change in the future and presumably we would get
168+
// failing tests until we have properly called the necessary methods on
169+
// each of our proxy agents
170+
if (proxy?.setRequestProps) {
171+
proxy.setRequestProps(request, options)
176172
}
177173

178-
createSocket (req, options, cb) {
179-
return Promise.resolve()
180-
.then(() => this.connect(req, options))
181-
.then((socket) => {
182-
this.#socket = socket
183-
return super.createSocket(req, options, cb)
184-
}, cb)
174+
request.setHeader('connection', this.keepAlive ? 'keep-alive' : 'close')
175+
176+
if (this.#timeouts.response) {
177+
let responseTimeout
178+
request.once('finish', () => {
179+
setTimeout(() => {
180+
request.destroy(new Errors.ResponseTimeoutError(request, this.#proxy))
181+
}, this.#timeouts.response)
182+
})
183+
request.once('response', () => {
184+
clearTimeout(responseTimeout)
185+
})
185186
}
186187

187-
createConnection () {
188-
return this.#socket
188+
if (this.#timeouts.transfer) {
189+
let transferTimeout
190+
request.once('response', (res) => {
191+
setTimeout(() => {
192+
res.destroy(new Errors.TransferTimeoutError(request, this.#proxy))
193+
}, this.#timeouts.transfer)
194+
res.once('close', () => {
195+
clearTimeout(transferTimeout)
196+
})
197+
})
189198
}
190-
}
191199

192-
Object.defineProperty(agent, 'name', { value: name })
193-
return agent
194-
}
195-
196-
module.exports = {
197-
HttpAgent: createAgent(http, 'HttpAgent'),
198-
HttpsAgent: createAgent(https, 'HttpsAgent'),
200+
return super.addRequest(request, options)
201+
}
199202
}

‎node_modules/@npmcli/agent/lib/errors.js

+2-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
'use strict'
22

3-
const { appendPort } = require('./util')
4-
53
class InvalidProxyProtocolError extends Error {
64
constructor (url) {
75
super(`Invalid protocol \`${url.protocol}\` connecting to proxy \`${url.host}\``)
@@ -11,17 +9,15 @@ class InvalidProxyProtocolError extends Error {
119
}
1210

1311
class ConnectionTimeoutError extends Error {
14-
constructor ({ host, port }) {
15-
host = appendPort(host, port)
12+
constructor (host) {
1613
super(`Timeout connecting to host \`${host}\``)
1714
this.code = 'ECONNECTIONTIMEOUT'
1815
this.host = host
1916
}
2017
}
2118

2219
class IdleTimeoutError extends Error {
23-
constructor ({ host, port }) {
24-
host = appendPort(host, port)
20+
constructor (host) {
2521
super(`Idle timeout reached for host \`${host}\``)
2622
this.code = 'EIDLETIMEOUT'
2723
this.host = host

‎node_modules/@npmcli/agent/lib/index.js

+27-17
Original file line numberDiff line numberDiff line change

‎node_modules/@npmcli/agent/lib/options.js

+38-26
Original file line numberDiff line numberDiff line change

‎node_modules/@npmcli/agent/lib/proxy.js

+25-25
Original file line numberDiff line numberDiff line change

‎node_modules/@npmcli/agent/lib/util.js

-84
This file was deleted.

‎node_modules/@npmcli/agent/package.json

+7-12
Original file line numberDiff line numberDiff line change

‎package-lock.json

+4-3
Original file line numberDiff line numberDiff line change

0 commit comments

Comments
 (0)
Please sign in to comment.