codekingpro/portable-devtools
114k
1'use strict'
2
3const net = require('net')
4const tls = require('tls')
5const { once } = require('events')
6const timers = require('timers/promises')
7const { normalizeOptions, cacheOptions } = require('./options')
8const { getProxy, getProxyAgent, proxyCache } = require('./proxy.js')
9const Errors = require('./errors.js')
10const { Agent: AgentBase } = require('agent-base')
11
12module.exports = class Agent extends AgentBase {
13 #options
14 #timeouts
15 #proxy
16 #noProxy
17 #ProxyAgent
18
19 constructor (options = {}) {
20 const { timeouts, proxy, noProxy, ...normalizedOptions } = normalizeOptions(options)
21
22 super(normalizedOptions)
23
24 this.#options = normalizedOptions
25 this.#timeouts = timeouts
26
27 if (proxy) {
28 this.#proxy = new URL(proxy)
29 this.#noProxy = noProxy
30 this.#ProxyAgent = getProxyAgent(proxy)
31 }
32 }
33
34 get proxy () {
35 return this.#proxy ? { url: this.#proxy } : {}
36 }
37
38 #getProxy (options) {
39 if (!this.#proxy) {
40 return
41 }
42
43 const proxy = getProxy(`${options.protocol}//${options.host}:${options.port}`, {
44 proxy: this.#proxy,
45 noProxy: this.#noProxy,
46 })
47
48 if (!proxy) {
49 return
50 }
51
52 const cacheKey = cacheOptions({
53 ...options,
54 ...this.#options,
55 timeouts: this.#timeouts,
56 proxy,
57 })
58
59 if (proxyCache.has(cacheKey)) {
60 return proxyCache.get(cacheKey)
61 }
62
63 let ProxyAgent = this.#ProxyAgent
64 if (Array.isArray(ProxyAgent)) {
65 ProxyAgent = this.isSecureEndpoint(options) ? ProxyAgent[1] : ProxyAgent[0]
66 }
67
68 const proxyAgent = new ProxyAgent(proxy, {
69 ...this.#options,
70 socketOptions: { family: this.#options.family },
71 })
72 proxyCache.set(cacheKey, proxyAgent)
73
74 return proxyAgent
75 }
76
77 // takes an array of promises and races them against the connection timeout
78 // which will throw the necessary error if it is hit. This will return the
79 // result of the promise race.
80 async #timeoutConnection ({ promises, options, timeout }, ac = new AbortController()) {
81 if (timeout) {
82 const connectionTimeout = timers.setTimeout(timeout, null, { signal: ac.signal })
83 .then(() => {
84 throw new Errors.ConnectionTimeoutError(`${options.host}:${options.port}`)
85 }).catch((err) => {
86 if (err.name === 'AbortError') {
87 return
88 }
89 throw err
90 })
91 promises.push(connectionTimeout)
92 }
93
94 let result
95 try {
96 result = await Promise.race(promises)
97 ac.abort()
98 } catch (err) {
99 ac.abort()
100 throw err
101 }
102 return result
103 }
104
105 async connect (request, options) {
106 // if the connection does not have its own lookup function
107 // set, then use the one from our options
108 options.lookup ??= this.#options.lookup
109
110 let socket
111 let timeout = this.#timeouts.connection
112 const isSecureEndpoint = this.isSecureEndpoint(options)
113
114 const proxy = this.#getProxy(options)
115 if (proxy) {
116 // some of the proxies will wait for the socket to fully connect before
117 // returning so we have to await this while also racing it against the
118 // connection timeout.
119 const start = Date.now()
120 socket = await this.#timeoutConnection({
121 options,
122 timeout,
123 promises: [proxy.connect(request, options)],
124 })
125 // see how much time proxy.connect took and subtract it from
126 // the timeout
127 if (timeout) {
128 timeout = timeout - (Date.now() - start)
129 }
130 } else {
131 socket = (isSecureEndpoint ? tls : net).connect(options)
132 }
133
134 socket.setKeepAlive(this.keepAlive, this.keepAliveMsecs)
135 socket.setNoDelay(this.keepAlive)
136
137 const abortController = new AbortController()
138 const { signal } = abortController
139
140 const connectPromise = socket[isSecureEndpoint ? 'secureConnecting' : 'connecting']
141 ? once(socket, isSecureEndpoint ? 'secureConnect' : 'connect', { signal })
142 : Promise.resolve()
143
144 await this.#timeoutConnection({
145 options,
146 timeout,
147 promises: [
148 connectPromise,
149 once(socket, 'error', { signal }).then((err) => {
150 throw err[0]
151 }),
152 ],
153 }, abortController)
154
155 if (this.#timeouts.idle) {
156 socket.setTimeout(this.#timeouts.idle, () => {
157 socket.destroy(new Errors.IdleTimeoutError(`${options.host}:${options.port}`))
158 })
159 }
160
161 return socket
162 }
163
164 addRequest (request, options) {
165 const proxy = this.#getProxy(options)
166 // it would be better to call proxy.addRequest here but this causes the
167 // http-proxy-agent to call its super.addRequest which causes the request
168 // to be added to the agent twice. since we only support 3 agents
169 // currently (see the required agents in proxy.js) we have manually
170 // checked that the only public methods we need to call are called in the
171 // next block. this could change in the future and presumably we would get
172 // failing tests until we have properly called the necessary methods on
173 // each of our proxy agents
174 if (proxy?.setRequestProps) {
175 proxy.setRequestProps(request, options)
176 }
177
178 request.setHeader('connection', this.keepAlive ? 'keep-alive' : 'close')
179
180 if (this.#timeouts.response) {
181 let responseTimeout
182 request.once('finish', () => {
183 setTimeout(() => {
184 request.destroy(new Errors.ResponseTimeoutError(request, this.#proxy))
185 }, this.#timeouts.response)
186 })
187 request.once('response', () => {
188 clearTimeout(responseTimeout)
189 })
190 }
191
192 if (this.#timeouts.transfer) {
193 let transferTimeout
194 request.once('response', (res) => {
195 setTimeout(() => {
196 res.destroy(new Errors.TransferTimeoutError(request, this.#proxy))
197 }, this.#timeouts.transfer)
198 res.once('close', () => {
199 clearTimeout(transferTimeout)
200 })
201 })
202 }
203
204 return super.addRequest(request, options)
205 }
206}
207 