undici 8.0.3 → 8.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/docs/api/Client.md +2 -0
- package/docs/docs/api/Dispatcher.md +2 -2
- package/lib/api/api-connect.js +1 -1
- package/lib/api/api-pipeline.js +2 -2
- package/lib/api/api-request.js +2 -2
- package/lib/api/api-stream.js +1 -1
- package/lib/api/api-upgrade.js +8 -2
- package/lib/api/readable.js +3 -2
- package/lib/cache/memory-cache-store.js +1 -1
- package/lib/cache/sqlite-cache-store.js +6 -4
- package/lib/core/connect.js +16 -0
- package/lib/core/constants.js +1 -24
- package/lib/core/errors.js +2 -2
- package/lib/core/request.js +17 -2
- package/lib/core/socks5-client.js +24 -9
- package/lib/core/socks5-utils.js +32 -23
- package/lib/core/util.js +28 -3
- package/lib/dispatcher/agent.js +38 -40
- package/lib/dispatcher/balanced-pool.js +21 -23
- package/lib/dispatcher/client-h1.js +34 -16
- package/lib/dispatcher/client-h2.js +400 -147
- package/lib/dispatcher/client.js +3 -2
- package/lib/dispatcher/dispatcher-base.js +18 -0
- package/lib/dispatcher/h2c-client.js +4 -4
- package/lib/dispatcher/pool-base.js +6 -6
- package/lib/dispatcher/pool.js +8 -3
- package/lib/dispatcher/proxy-agent.js +2 -0
- package/lib/dispatcher/round-robin-pool.js +5 -6
- package/lib/dispatcher/socks5-proxy-agent.js +23 -14
- package/lib/handler/cache-handler.js +1 -1
- package/lib/handler/redirect-handler.js +4 -0
- package/lib/interceptor/redirect.js +3 -3
- package/lib/llhttp/llhttp-wasm.js +1 -1
- package/lib/llhttp/llhttp_simd-wasm.js +1 -1
- package/lib/mock/mock-agent.js +8 -8
- package/lib/mock/mock-call-history.js +15 -15
- package/lib/util/cache.js +1 -1
- package/lib/web/eventsource/eventsource-stream.js +245 -150
- package/lib/web/fetch/formdata-parser.js +17 -6
- package/lib/web/fetch/index.js +38 -28
- package/lib/web/webidl/index.js +5 -5
- package/lib/web/websocket/frame.js +1 -7
- package/lib/web/websocket/permessage-deflate.js +13 -31
- package/lib/web/websocket/receiver.js +62 -22
- package/lib/web/websocket/stream/websocketstream.js +6 -5
- package/lib/web/websocket/websocket.js +6 -1
- package/package.json +1 -1
- package/types/client.d.ts +11 -0
- package/types/dispatcher.d.ts +4 -4
- package/types/header.d.ts +5 -0
- package/types/interceptors.d.ts +1 -1
- package/types/proxy-agent.d.ts +2 -2
- package/types/socks5-proxy-agent.d.ts +2 -2
- package/lib/llhttp/.gitkeep +0 -0
package/docs/docs/api/Client.md
CHANGED
|
@@ -24,6 +24,8 @@ Returns: `Client`
|
|
|
24
24
|
* **keepAliveTimeoutThreshold** `number | null` (optional) - Default: `2e3` - A number of milliseconds subtracted from server *keep-alive* hints when overriding `keepAliveTimeout` to account for timing inaccuracies caused by e.g. transport latency. Defaults to 2 seconds.
|
|
25
25
|
* **maxHeaderSize** `number | null` (optional) - Default: `--max-http-header-size` or `16384` - The maximum length of request headers in bytes. Defaults to Node.js' --max-http-header-size or 16KiB.
|
|
26
26
|
* **maxResponseSize** `number | null` (optional) - Default: `-1` - The maximum length of response body in bytes. Set to `-1` to disable.
|
|
27
|
+
* **webSocket** `WebSocketOptions` (optional) - WebSocket-specific configuration options.
|
|
28
|
+
* **maxPayloadSize** `number` (optional) - Default: `134217728` (128 MB) - Maximum allowed payload size in bytes for WebSocket messages. Applied to uncompressed messages, compressed frame payloads, and decompressed (permessage-deflate) messages. Set to 0 to disable the limit.
|
|
27
29
|
* **pipelining** `number | null` (optional) - Default: `1` - The amount of concurrent requests to be sent over the single TCP/TLS connection according to [RFC7230](https://tools.ietf.org/html/rfc7230#section-6.3.2). Carefully consider your workload and environment before enabling concurrent requests as pipelining may reduce performance if used incorrectly. Pipelining is sensitive to network stack settings as well as head of line blocking caused by e.g. long running requests. Set to `0` to disable keep-alive connections.
|
|
28
30
|
* **connect** `ConnectOptions | Function | null` (optional) - Default: `null`.
|
|
29
31
|
* **strictContentLength** `Boolean` (optional) - Default: `true` - Whether to treat request content length mismatches as errors. If true, an error is thrown when the request content-length header doesn't match the length of the request body. **Security Warning:** Disabling this option can expose your application to HTTP Request Smuggling attacks, where mismatched content-length headers cause servers and proxies to interpret request boundaries differently. This can lead to cache poisoning, credential hijacking, and bypassing security controls. Only disable this in controlled environments where you fully trust the request source.
|
|
@@ -1354,10 +1354,10 @@ Emitted when dispatcher is no longer busy.
|
|
|
1354
1354
|
|
|
1355
1355
|
## Parameter: `UndiciHeaders`
|
|
1356
1356
|
|
|
1357
|
-
* `Record<string, string | string[] | undefined> | string[] | Iterable<[string, string | string[] | undefined]> | null`
|
|
1357
|
+
* `Record<string, number | string | string[] | undefined> | string[] | Iterable<[string, string | string[] | undefined]> | null`
|
|
1358
1358
|
|
|
1359
1359
|
Header arguments such as `options.headers` in [`Client.dispatch`](/docs/docs/api/Client.md#clientdispatchoptions-handlers) can be specified in three forms:
|
|
1360
|
-
* As an object specified by the `Record<string, string | string[] | undefined>` (`
|
|
1360
|
+
* As an object specified by the `Record<string, number | string | string[] | undefined>` (`OutgoingHttpHeaders`) type.
|
|
1361
1361
|
* As an array of strings. An array representation of a header list must have an even length, or an `InvalidArgumentError` will be thrown.
|
|
1362
1362
|
* As an iterable that can encompass `Headers`, `Map`, or a custom iterator returning key-value pairs.
|
|
1363
1363
|
Keys are lowercase and values are not modified.
|
package/lib/api/api-connect.js
CHANGED
|
@@ -60,7 +60,7 @@ class ConnectHandler extends AsyncResource {
|
|
|
60
60
|
// Indicates is an HTTP2Session
|
|
61
61
|
if (responseHeaders != null) {
|
|
62
62
|
responseHeaders = this.responseHeaders === 'raw'
|
|
63
|
-
?
|
|
63
|
+
? util.parseRawHeaders(rawHeaders)
|
|
64
64
|
: headers
|
|
65
65
|
}
|
|
66
66
|
|
package/lib/api/api-pipeline.js
CHANGED
|
@@ -167,7 +167,7 @@ class PipelineHandler extends AsyncResource {
|
|
|
167
167
|
if (this.onInfo) {
|
|
168
168
|
const rawHeaders = controller?.rawHeaders
|
|
169
169
|
const responseHeaders = this.responseHeaders === 'raw'
|
|
170
|
-
?
|
|
170
|
+
? util.parseRawHeaders(rawHeaders)
|
|
171
171
|
: headers
|
|
172
172
|
this.onInfo({ statusCode, headers: responseHeaders })
|
|
173
173
|
}
|
|
@@ -181,7 +181,7 @@ class PipelineHandler extends AsyncResource {
|
|
|
181
181
|
this.handler = null
|
|
182
182
|
const rawHeaders = controller?.rawHeaders
|
|
183
183
|
const responseHeaders = this.responseHeaders === 'raw'
|
|
184
|
-
?
|
|
184
|
+
? util.parseRawHeaders(rawHeaders)
|
|
185
185
|
: headers
|
|
186
186
|
body = this.runInAsyncScope(handler, null, {
|
|
187
187
|
statusCode,
|
package/lib/api/api-request.js
CHANGED
|
@@ -21,7 +21,7 @@ class RequestHandler extends AsyncResource {
|
|
|
21
21
|
throw new InvalidArgumentError('invalid callback')
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
if (highWaterMark && (
|
|
24
|
+
if (highWaterMark != null && (!Number.isFinite(highWaterMark) || highWaterMark < 0)) {
|
|
25
25
|
throw new InvalidArgumentError('invalid highWaterMark')
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -92,7 +92,7 @@ class RequestHandler extends AsyncResource {
|
|
|
92
92
|
|
|
93
93
|
const rawHeaders = controller?.rawHeaders
|
|
94
94
|
const responseHeaderData = responseHeaders === 'raw'
|
|
95
|
-
?
|
|
95
|
+
? util.parseRawHeaders(rawHeaders)
|
|
96
96
|
: headers
|
|
97
97
|
|
|
98
98
|
if (statusCode < 200) {
|
package/lib/api/api-stream.js
CHANGED
|
@@ -85,7 +85,7 @@ class StreamHandler extends AsyncResource {
|
|
|
85
85
|
|
|
86
86
|
const rawHeaders = controller?.rawHeaders
|
|
87
87
|
const responseHeaderData = responseHeaders === 'raw'
|
|
88
|
-
?
|
|
88
|
+
? util.parseRawHeaders(rawHeaders)
|
|
89
89
|
: headers
|
|
90
90
|
|
|
91
91
|
if (statusCode < 200) {
|
package/lib/api/api-upgrade.js
CHANGED
|
@@ -51,7 +51,13 @@ class UpgradeHandler extends AsyncResource {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
onRequestUpgrade (controller, statusCode, headers, socket) {
|
|
54
|
-
|
|
54
|
+
const expectedStatusCode = socket[kHTTP2Stream] === true ? 200 : 101
|
|
55
|
+
|
|
56
|
+
if (statusCode !== expectedStatusCode) {
|
|
57
|
+
const socketInfo = socket[kHTTP2Stream] === true ? null : util.getSocketInfo(socket)
|
|
58
|
+
controller.abort(new SocketError('bad upgrade', socketInfo))
|
|
59
|
+
return
|
|
60
|
+
}
|
|
55
61
|
|
|
56
62
|
const { callback, opaque, context } = this
|
|
57
63
|
|
|
@@ -61,7 +67,7 @@ class UpgradeHandler extends AsyncResource {
|
|
|
61
67
|
|
|
62
68
|
const rawHeaders = controller?.rawHeaders
|
|
63
69
|
const responseHeaders = this.responseHeaders === 'raw'
|
|
64
|
-
?
|
|
70
|
+
? util.parseRawHeaders(rawHeaders)
|
|
65
71
|
: headers
|
|
66
72
|
|
|
67
73
|
this.runInAsyncScope(callback, null, null, {
|
package/lib/api/readable.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const assert = require('node:assert')
|
|
4
|
+
const { addAbortListener } = require('node:events')
|
|
4
5
|
const { Readable } = require('node:stream')
|
|
5
6
|
const { RequestAbortedError, NotSupportedError, InvalidArgumentError, AbortError } = require('../core/errors')
|
|
6
7
|
const util = require('../core/util')
|
|
@@ -293,10 +294,10 @@ class BodyReadable extends Readable {
|
|
|
293
294
|
const onAbort = () => {
|
|
294
295
|
this.destroy(signal.reason ?? new AbortError())
|
|
295
296
|
}
|
|
296
|
-
signal
|
|
297
|
+
const abortListener = addAbortListener(signal, onAbort)
|
|
297
298
|
this
|
|
298
299
|
.on('close', function () {
|
|
299
|
-
|
|
300
|
+
abortListener[Symbol.dispose]()
|
|
300
301
|
if (signal.aborted) {
|
|
301
302
|
reject(signal.reason ?? new AbortError())
|
|
302
303
|
} else {
|
|
@@ -173,6 +173,7 @@ module.exports = class SqliteCacheStore {
|
|
|
173
173
|
headers = ?,
|
|
174
174
|
etag = ?,
|
|
175
175
|
cacheControlDirectives = ?,
|
|
176
|
+
vary = ?,
|
|
176
177
|
cachedAt = ?,
|
|
177
178
|
staleAt = ?
|
|
178
179
|
WHERE
|
|
@@ -216,7 +217,7 @@ module.exports = class SqliteCacheStore {
|
|
|
216
217
|
SELECT
|
|
217
218
|
id
|
|
218
219
|
FROM cacheInterceptorV${VERSION}
|
|
219
|
-
ORDER BY cachedAt
|
|
220
|
+
ORDER BY cachedAt ASC
|
|
220
221
|
LIMIT ?
|
|
221
222
|
)
|
|
222
223
|
`)
|
|
@@ -278,12 +279,12 @@ module.exports = class SqliteCacheStore {
|
|
|
278
279
|
value.headers ? JSON.stringify(value.headers) : null,
|
|
279
280
|
value.etag ? value.etag : null,
|
|
280
281
|
value.cacheControlDirectives ? JSON.stringify(value.cacheControlDirectives) : null,
|
|
282
|
+
value.vary ? JSON.stringify(value.vary) : null,
|
|
281
283
|
value.cachedAt,
|
|
282
284
|
value.staleAt,
|
|
283
285
|
existingValue.id
|
|
284
286
|
)
|
|
285
287
|
} else {
|
|
286
|
-
this.#prune()
|
|
287
288
|
// New response, let's insert it
|
|
288
289
|
this.#insertValueQuery.run(
|
|
289
290
|
url,
|
|
@@ -299,6 +300,7 @@ module.exports = class SqliteCacheStore {
|
|
|
299
300
|
value.cachedAt,
|
|
300
301
|
value.staleAt
|
|
301
302
|
)
|
|
303
|
+
this.#prune()
|
|
302
304
|
}
|
|
303
305
|
}
|
|
304
306
|
|
|
@@ -323,7 +325,7 @@ module.exports = class SqliteCacheStore {
|
|
|
323
325
|
write (chunk, encoding, callback) {
|
|
324
326
|
size += chunk.byteLength
|
|
325
327
|
|
|
326
|
-
if (size
|
|
328
|
+
if (size <= store.#maxEntrySize) {
|
|
327
329
|
body.push(chunk)
|
|
328
330
|
} else {
|
|
329
331
|
this.destroy()
|
|
@@ -409,7 +411,7 @@ module.exports = class SqliteCacheStore {
|
|
|
409
411
|
const now = Date.now()
|
|
410
412
|
for (const value of values) {
|
|
411
413
|
if (now >= value.deleteAt && !canBeExpired) {
|
|
412
|
-
|
|
414
|
+
continue
|
|
413
415
|
}
|
|
414
416
|
|
|
415
417
|
let matches = true
|
package/lib/core/connect.js
CHANGED
|
@@ -38,6 +38,22 @@ const SessionCache = class WeakSessionCache {
|
|
|
38
38
|
return
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
if (this._sessionCache.has(sessionKey)) {
|
|
42
|
+
this._sessionCache.delete(sessionKey)
|
|
43
|
+
} else if (this._sessionCache.size >= this._maxCachedSessions) {
|
|
44
|
+
for (const [key, ref] of this._sessionCache) {
|
|
45
|
+
if (ref.deref() === undefined) {
|
|
46
|
+
this._sessionCache.delete(key)
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const oldest = this._sessionCache.keys().next()
|
|
52
|
+
if (!oldest.done) {
|
|
53
|
+
this._sessionCache.delete(oldest.value)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
41
57
|
this._sessionCache.set(sessionKey, new WeakRef(session))
|
|
42
58
|
this._sessionRegistry.register(session, sessionKey)
|
|
43
59
|
}
|
package/lib/core/constants.js
CHANGED
|
@@ -107,28 +107,6 @@ const headerNameLowerCasedRecord = {}
|
|
|
107
107
|
// Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`.
|
|
108
108
|
Object.setPrototypeOf(headerNameLowerCasedRecord, null)
|
|
109
109
|
|
|
110
|
-
/**
|
|
111
|
-
* @type {Record<Lowercase<typeof wellknownHeaderNames[number]>, Buffer>}
|
|
112
|
-
*/
|
|
113
|
-
const wellknownHeaderNameBuffers = {}
|
|
114
|
-
|
|
115
|
-
// Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`.
|
|
116
|
-
Object.setPrototypeOf(wellknownHeaderNameBuffers, null)
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* @param {string} header Lowercased header
|
|
120
|
-
* @returns {Buffer}
|
|
121
|
-
*/
|
|
122
|
-
function getHeaderNameAsBuffer (header) {
|
|
123
|
-
let buffer = wellknownHeaderNameBuffers[header]
|
|
124
|
-
|
|
125
|
-
if (buffer === undefined) {
|
|
126
|
-
buffer = Buffer.from(header)
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return buffer
|
|
130
|
-
}
|
|
131
|
-
|
|
132
110
|
for (let i = 0; i < wellknownHeaderNames.length; ++i) {
|
|
133
111
|
const key = wellknownHeaderNames[i]
|
|
134
112
|
const lowerCasedKey = key.toLowerCase()
|
|
@@ -138,6 +116,5 @@ for (let i = 0; i < wellknownHeaderNames.length; ++i) {
|
|
|
138
116
|
|
|
139
117
|
module.exports = {
|
|
140
118
|
wellknownHeaderNames,
|
|
141
|
-
headerNameLowerCasedRecord
|
|
142
|
-
getHeaderNameAsBuffer
|
|
119
|
+
headerNameLowerCasedRecord
|
|
143
120
|
}
|
package/lib/core/errors.js
CHANGED
|
@@ -163,8 +163,8 @@ class RequestAbortedError extends AbortError {
|
|
|
163
163
|
|
|
164
164
|
const kInformationalError = Symbol.for('undici.error.UND_ERR_INFO')
|
|
165
165
|
class InformationalError extends UndiciError {
|
|
166
|
-
constructor (message) {
|
|
167
|
-
super(message)
|
|
166
|
+
constructor (message, options) {
|
|
167
|
+
super(message, options)
|
|
168
168
|
this.name = 'InformationalError'
|
|
169
169
|
this.message = message || 'Request information'
|
|
170
170
|
this.code = 'UND_ERR_INFO'
|
package/lib/core/request.js
CHANGED
|
@@ -28,6 +28,21 @@ const { headerNameLowerCasedRecord } = require('./constants')
|
|
|
28
28
|
// Verifies that a given path is valid does not contain control chars \x00 to \x20
|
|
29
29
|
const invalidPathRegex = /[^\u0021-\u00ff]/
|
|
30
30
|
|
|
31
|
+
function isValidContentLengthHeaderValue (val) {
|
|
32
|
+
if (typeof val !== 'string' || val.length === 0) {
|
|
33
|
+
return false
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (let i = 0; i < val.length; i++) {
|
|
37
|
+
const charCode = val.charCodeAt(i)
|
|
38
|
+
if (charCode < 48 || charCode > 57) {
|
|
39
|
+
return false
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return true
|
|
44
|
+
}
|
|
45
|
+
|
|
31
46
|
const kHandler = Symbol('handler')
|
|
32
47
|
const kController = Symbol('controller')
|
|
33
48
|
const kResume = Symbol('resume')
|
|
@@ -484,10 +499,10 @@ function processHeader (request, key, val) {
|
|
|
484
499
|
if (request.contentLength !== null) {
|
|
485
500
|
throw new InvalidArgumentError('duplicate content-length header')
|
|
486
501
|
}
|
|
487
|
-
|
|
488
|
-
if (!Number.isFinite(request.contentLength)) {
|
|
502
|
+
if (!isValidContentLengthHeaderValue(val)) {
|
|
489
503
|
throw new InvalidArgumentError('invalid content-length header')
|
|
490
504
|
}
|
|
505
|
+
request.contentLength = parseInt(val, 10)
|
|
491
506
|
} else if (request.contentType === null && headerName === 'content-type') {
|
|
492
507
|
request.contentType = val
|
|
493
508
|
request.headers.push(key, val)
|
|
@@ -7,6 +7,7 @@ const { debuglog } = require('node:util')
|
|
|
7
7
|
const { parseAddress } = require('./socks5-utils')
|
|
8
8
|
|
|
9
9
|
const debug = debuglog('undici:socks5')
|
|
10
|
+
const EMPTY_BUFFER = Buffer.alloc(0)
|
|
10
11
|
|
|
11
12
|
// SOCKS5 constants
|
|
12
13
|
const SOCKS_VERSION = 0x05
|
|
@@ -51,6 +52,7 @@ const STATES = {
|
|
|
51
52
|
INITIAL: 'initial',
|
|
52
53
|
HANDSHAKING: 'handshaking',
|
|
53
54
|
AUTHENTICATING: 'authenticating',
|
|
55
|
+
AUTHENTICATED: 'authenticated',
|
|
54
56
|
CONNECTING: 'connecting',
|
|
55
57
|
CONNECTED: 'connected',
|
|
56
58
|
ERROR: 'error',
|
|
@@ -72,7 +74,10 @@ class Socks5Client extends EventEmitter {
|
|
|
72
74
|
this.socket = socket
|
|
73
75
|
this.options = options
|
|
74
76
|
this.state = STATES.INITIAL
|
|
75
|
-
this.buffer =
|
|
77
|
+
this.buffer = EMPTY_BUFFER
|
|
78
|
+
this.onSocketData = this.onData.bind(this)
|
|
79
|
+
this.onSocketError = this.onError.bind(this)
|
|
80
|
+
this.onSocketClose = this.onClose.bind(this)
|
|
76
81
|
|
|
77
82
|
// Authentication settings
|
|
78
83
|
this.authMethods = []
|
|
@@ -82,9 +87,9 @@ class Socks5Client extends EventEmitter {
|
|
|
82
87
|
this.authMethods.push(AUTH_METHODS.NO_AUTH)
|
|
83
88
|
|
|
84
89
|
// Socket event handlers
|
|
85
|
-
this.socket.on('data', this.
|
|
86
|
-
this.socket.on('error', this.
|
|
87
|
-
this.socket.on('close', this.
|
|
90
|
+
this.socket.on('data', this.onSocketData)
|
|
91
|
+
this.socket.on('error', this.onSocketError)
|
|
92
|
+
this.socket.on('close', this.onSocketClose)
|
|
88
93
|
}
|
|
89
94
|
|
|
90
95
|
/**
|
|
@@ -139,6 +144,11 @@ class Socks5Client extends EventEmitter {
|
|
|
139
144
|
}
|
|
140
145
|
}
|
|
141
146
|
|
|
147
|
+
markAuthenticated () {
|
|
148
|
+
this.state = STATES.AUTHENTICATED
|
|
149
|
+
this.emit('authenticated')
|
|
150
|
+
}
|
|
151
|
+
|
|
142
152
|
/**
|
|
143
153
|
* Start the SOCKS5 handshake
|
|
144
154
|
*/
|
|
@@ -189,7 +199,7 @@ class Socks5Client extends EventEmitter {
|
|
|
189
199
|
debug('server selected auth method', method)
|
|
190
200
|
|
|
191
201
|
if (method === AUTH_METHODS.NO_AUTH) {
|
|
192
|
-
this.
|
|
202
|
+
this.markAuthenticated()
|
|
193
203
|
} else if (method === AUTH_METHODS.USERNAME_PASSWORD) {
|
|
194
204
|
this.state = STATES.AUTHENTICATING
|
|
195
205
|
this.sendAuthRequest()
|
|
@@ -254,7 +264,7 @@ class Socks5Client extends EventEmitter {
|
|
|
254
264
|
|
|
255
265
|
this.buffer = this.buffer.subarray(2)
|
|
256
266
|
debug('authentication successful')
|
|
257
|
-
this.
|
|
267
|
+
this.markAuthenticated()
|
|
258
268
|
}
|
|
259
269
|
|
|
260
270
|
/**
|
|
@@ -263,8 +273,12 @@ class Socks5Client extends EventEmitter {
|
|
|
263
273
|
* @param {number} port - Target port
|
|
264
274
|
*/
|
|
265
275
|
connect (address, port) {
|
|
266
|
-
if (this.state === STATES.CONNECTED) {
|
|
267
|
-
throw new InvalidArgumentError('
|
|
276
|
+
if (this.state === STATES.CONNECTING || this.state === STATES.CONNECTED) {
|
|
277
|
+
throw new InvalidArgumentError('Connection already in progress')
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (this.state !== STATES.AUTHENTICATED) {
|
|
281
|
+
throw new InvalidArgumentError('Client must be authenticated before CONNECT')
|
|
268
282
|
}
|
|
269
283
|
|
|
270
284
|
debug('connecting to', address, port)
|
|
@@ -363,8 +377,9 @@ class Socks5Client extends EventEmitter {
|
|
|
363
377
|
|
|
364
378
|
const boundPort = this.buffer.readUInt16BE(offset)
|
|
365
379
|
|
|
366
|
-
this.buffer =
|
|
380
|
+
this.buffer = EMPTY_BUFFER
|
|
367
381
|
this.state = STATES.CONNECTED
|
|
382
|
+
this.socket.removeListener('data', this.onSocketData)
|
|
368
383
|
|
|
369
384
|
debug('connected, bound address:', boundAddress, 'port:', boundPort)
|
|
370
385
|
this.emit('connected', { address: boundAddress, port: boundPort })
|
package/lib/core/socks5-utils.js
CHANGED
|
@@ -46,34 +46,43 @@ function parseAddress (address) {
|
|
|
46
46
|
*/
|
|
47
47
|
function parseIPv6 (address) {
|
|
48
48
|
const buffer = Buffer.alloc(16)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
let normalizedAddress = address
|
|
50
|
+
|
|
51
|
+
// Expand an embedded IPv4 tail into the last two IPv6 groups.
|
|
52
|
+
if (address.includes('.')) {
|
|
53
|
+
const lastColonIndex = address.lastIndexOf(':')
|
|
54
|
+
const ipv4Part = address.slice(lastColonIndex + 1)
|
|
55
|
+
|
|
56
|
+
if (net.isIPv4(ipv4Part)) {
|
|
57
|
+
const octets = ipv4Part.split('.').map(Number)
|
|
58
|
+
const high = ((octets[0] << 8) | octets[1]).toString(16)
|
|
59
|
+
const low = ((octets[2] << 8) | octets[3]).toString(16)
|
|
60
|
+
normalizedAddress = `${address.slice(0, lastColonIndex)}:${high}:${low}`
|
|
61
|
+
}
|
|
62
|
+
}
|
|
52
63
|
|
|
53
64
|
// Handle compressed notation (::)
|
|
54
|
-
const doubleColonIndex =
|
|
65
|
+
const doubleColonIndex = normalizedAddress.indexOf('::')
|
|
55
66
|
if (doubleColonIndex !== -1) {
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
const before = normalizedAddress.slice(0, doubleColonIndex)
|
|
68
|
+
const after = normalizedAddress.slice(doubleColonIndex + 2)
|
|
69
|
+
const beforeParts = before === '' ? [] : before.split(':')
|
|
70
|
+
const afterParts = after === '' ? [] : after.split(':')
|
|
71
|
+
|
|
72
|
+
let bufferIndex = 0
|
|
73
|
+
for (const part of beforeParts) {
|
|
74
|
+
buffer.writeUInt16BE(parseInt(part, 16), bufferIndex)
|
|
75
|
+
bufferIndex += 2
|
|
76
|
+
}
|
|
77
|
+
bufferIndex = 16 - afterParts.length * 2
|
|
78
|
+
for (const part of afterParts) {
|
|
79
|
+
buffer.writeUInt16BE(parseInt(part, 16), bufferIndex)
|
|
80
|
+
bufferIndex += 2
|
|
69
81
|
}
|
|
70
82
|
} else {
|
|
71
|
-
|
|
72
|
-
for (
|
|
73
|
-
|
|
74
|
-
const value = parseInt(part, 16)
|
|
75
|
-
buffer.writeUInt16BE(value, partIndex * 2)
|
|
76
|
-
partIndex++
|
|
83
|
+
const parts = normalizedAddress.split(':')
|
|
84
|
+
for (let i = 0; i < parts.length; i++) {
|
|
85
|
+
buffer.writeUInt16BE(parseInt(parts[i], 16), i * 2)
|
|
77
86
|
}
|
|
78
87
|
}
|
|
79
88
|
|
package/lib/core/util.js
CHANGED
|
@@ -6,7 +6,7 @@ const { IncomingMessage } = require('node:http')
|
|
|
6
6
|
const stream = require('node:stream')
|
|
7
7
|
const net = require('node:net')
|
|
8
8
|
const { stringify } = require('node:querystring')
|
|
9
|
-
const { EventEmitter: EE } = require('node:events')
|
|
9
|
+
const { EventEmitter: EE, addAbortListener: addAbortListenerNative } = require('node:events')
|
|
10
10
|
const timers = require('../util/timers')
|
|
11
11
|
const { InvalidArgumentError, ConnectTimeoutError } = require('./errors')
|
|
12
12
|
const { headerNameLowerCasedRecord } = require('./constants')
|
|
@@ -464,10 +464,30 @@ function parseHeaders (headers, obj) {
|
|
|
464
464
|
}
|
|
465
465
|
|
|
466
466
|
/**
|
|
467
|
-
* @param {Buffer[]} headers
|
|
467
|
+
* @param {Buffer[] | string[] | Record<string, string | string[]> | null | undefined} headers
|
|
468
468
|
* @returns {string[]}
|
|
469
469
|
*/
|
|
470
470
|
function parseRawHeaders (headers) {
|
|
471
|
+
if (headers == null) {
|
|
472
|
+
return []
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (!Array.isArray(headers)) {
|
|
476
|
+
const rawHeaders = []
|
|
477
|
+
|
|
478
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
479
|
+
if (Array.isArray(value)) {
|
|
480
|
+
for (const entry of value) {
|
|
481
|
+
rawHeaders.push(name, `${entry}`)
|
|
482
|
+
}
|
|
483
|
+
} else {
|
|
484
|
+
rawHeaders.push(name, `${value}`)
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return rawHeaders
|
|
489
|
+
}
|
|
490
|
+
|
|
471
491
|
const headersLength = headers.length
|
|
472
492
|
/**
|
|
473
493
|
* @type {string[]}
|
|
@@ -678,7 +698,12 @@ function isFormDataLike (object) {
|
|
|
678
698
|
}
|
|
679
699
|
|
|
680
700
|
function addAbortListener (signal, listener) {
|
|
681
|
-
if (
|
|
701
|
+
if (signal instanceof AbortSignal) {
|
|
702
|
+
const disposable = addAbortListenerNative(signal, listener)
|
|
703
|
+
return () => disposable[Symbol.dispose]()
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
if (typeof signal.addEventListener === 'function') {
|
|
682
707
|
signal.addEventListener('abort', listener, { once: true })
|
|
683
708
|
return () => signal.removeEventListener('abort', listener)
|
|
684
709
|
}
|
package/lib/dispatcher/agent.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { InvalidArgumentError, MaxOriginsReachedError } = require('../core/errors')
|
|
4
|
-
const { kClients, kRunning, kClose, kDestroy, kDispatch, kUrl } = require('../core/symbols')
|
|
4
|
+
const { kBusy, kClients, kConnected, kRunning, kClose, kDestroy, kDispatch, kUrl } = require('../core/symbols')
|
|
5
5
|
const DispatcherBase = require('./dispatcher-base')
|
|
6
6
|
const Pool = require('./pool')
|
|
7
7
|
const Client = require('./client')
|
|
@@ -35,7 +35,7 @@ class Agent extends DispatcherBase {
|
|
|
35
35
|
throw new InvalidArgumentError('maxOrigins must be a number greater than 0')
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
super()
|
|
38
|
+
super(options)
|
|
39
39
|
|
|
40
40
|
if (connect && typeof connect !== 'function') {
|
|
41
41
|
connect = { ...connect }
|
|
@@ -65,7 +65,7 @@ class Agent extends DispatcherBase {
|
|
|
65
65
|
|
|
66
66
|
get [kRunning] () {
|
|
67
67
|
let ret = 0
|
|
68
|
-
for (const
|
|
68
|
+
for (const dispatcher of this[kClients].values()) {
|
|
69
69
|
ret += dispatcher[kRunning]
|
|
70
70
|
}
|
|
71
71
|
return ret
|
|
@@ -86,54 +86,52 @@ class Agent extends DispatcherBase {
|
|
|
86
86
|
throw new MaxOriginsReachedError()
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
|
|
90
|
-
let dispatcher = result && result.dispatcher
|
|
89
|
+
let dispatcher = this[kClients].get(key)
|
|
91
90
|
if (!dispatcher) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (connected) result.count -= 1
|
|
96
|
-
if (result.count <= 0) {
|
|
97
|
-
this[kClients].delete(key)
|
|
98
|
-
if (!result.dispatcher.destroyed) {
|
|
99
|
-
result.dispatcher.close()
|
|
100
|
-
}
|
|
101
|
-
}
|
|
91
|
+
dispatcher = this[kFactory](opts.origin, allowH2 === false
|
|
92
|
+
? { ...this[kOptions], allowH2: false }
|
|
93
|
+
: this[kOptions])
|
|
102
94
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
95
|
+
const closeClientIfUnused = () => {
|
|
96
|
+
if (this[kClients].get(key) !== dispatcher) {
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (dispatcher[kConnected] > 0 || dispatcher[kBusy]) {
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this[kClients].delete(key)
|
|
105
|
+
if (!dispatcher.destroyed) {
|
|
106
|
+
dispatcher.close()
|
|
107
|
+
}
|
|
110
108
|
|
|
111
|
-
|
|
112
|
-
|
|
109
|
+
let hasOrigin = false
|
|
110
|
+
for (const client of this[kClients].values()) {
|
|
111
|
+
if (client[kUrl].origin === dispatcher[kUrl].origin) {
|
|
112
|
+
hasOrigin = true
|
|
113
|
+
break
|
|
113
114
|
}
|
|
114
115
|
}
|
|
116
|
+
|
|
117
|
+
if (!hasOrigin) {
|
|
118
|
+
this[kOrigins].delete(dispatcher[kUrl].origin)
|
|
119
|
+
}
|
|
115
120
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
: this[kOptions])
|
|
121
|
+
|
|
122
|
+
dispatcher
|
|
119
123
|
.on('drain', this[kOnDrain])
|
|
120
|
-
.on('connect',
|
|
121
|
-
const result = this[kClients].get(key)
|
|
122
|
-
if (result) {
|
|
123
|
-
result.count += 1
|
|
124
|
-
}
|
|
125
|
-
this[kOnConnect](origin, targets)
|
|
126
|
-
})
|
|
124
|
+
.on('connect', this[kOnConnect])
|
|
127
125
|
.on('disconnect', (origin, targets, err) => {
|
|
128
|
-
closeClientIfUnused(
|
|
126
|
+
closeClientIfUnused()
|
|
129
127
|
this[kOnDisconnect](origin, targets, err)
|
|
130
128
|
})
|
|
131
129
|
.on('connectionError', (origin, targets, err) => {
|
|
132
|
-
closeClientIfUnused(
|
|
130
|
+
closeClientIfUnused()
|
|
133
131
|
this[kOnConnectionError](origin, targets, err)
|
|
134
132
|
})
|
|
135
133
|
|
|
136
|
-
this[kClients].set(key,
|
|
134
|
+
this[kClients].set(key, dispatcher)
|
|
137
135
|
this[kOrigins].add(origin)
|
|
138
136
|
}
|
|
139
137
|
|
|
@@ -142,7 +140,7 @@ class Agent extends DispatcherBase {
|
|
|
142
140
|
|
|
143
141
|
[kClose] () {
|
|
144
142
|
const closePromises = []
|
|
145
|
-
for (const
|
|
143
|
+
for (const dispatcher of this[kClients].values()) {
|
|
146
144
|
closePromises.push(dispatcher.close())
|
|
147
145
|
}
|
|
148
146
|
this[kClients].clear()
|
|
@@ -152,7 +150,7 @@ class Agent extends DispatcherBase {
|
|
|
152
150
|
|
|
153
151
|
[kDestroy] (err) {
|
|
154
152
|
const destroyPromises = []
|
|
155
|
-
for (const
|
|
153
|
+
for (const dispatcher of this[kClients].values()) {
|
|
156
154
|
destroyPromises.push(dispatcher.destroy(err))
|
|
157
155
|
}
|
|
158
156
|
this[kClients].clear()
|
|
@@ -162,7 +160,7 @@ class Agent extends DispatcherBase {
|
|
|
162
160
|
|
|
163
161
|
get stats () {
|
|
164
162
|
const allClientStats = {}
|
|
165
|
-
for (const
|
|
163
|
+
for (const dispatcher of this[kClients].values()) {
|
|
166
164
|
if (dispatcher.stats) {
|
|
167
165
|
allClientStats[dispatcher[kUrl].origin] = dispatcher.stats
|
|
168
166
|
}
|