undici 8.1.0 → 8.3.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/README.md +8 -5
- package/docs/docs/api/Dispatcher.md +2 -2
- package/docs/docs/api/GlobalInstallation.md +7 -5
- package/docs/docs/api/SnapshotAgent.md +23 -0
- package/lib/api/api-connect.js +1 -1
- package/lib/api/api-pipeline.js +6 -2
- package/lib/api/api-request.js +2 -2
- package/lib/api/api-stream.js +52 -6
- 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/symbols.js +1 -0
- package/lib/core/util.js +30 -5
- package/lib/dispatcher/agent.js +37 -39
- package/lib/dispatcher/balanced-pool.js +21 -23
- package/lib/dispatcher/client-h1.js +93 -34
- package/lib/dispatcher/client-h2.js +602 -270
- package/lib/dispatcher/client.js +3 -1
- package/lib/dispatcher/h2c-client.js +4 -4
- package/lib/dispatcher/pool-base.js +27 -9
- package/lib/dispatcher/pool.js +30 -2
- package/lib/dispatcher/proxy-agent.js +23 -4
- package/lib/dispatcher/round-robin-pool.js +31 -6
- package/lib/dispatcher/socks5-proxy-agent.js +42 -33
- package/lib/handler/cache-handler.js +1 -1
- package/lib/handler/redirect-handler.js +4 -0
- package/lib/handler/retry-handler.js +14 -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/mock/mock-utils.js +3 -1
- package/lib/mock/snapshot-agent.js +2 -0
- package/lib/mock/snapshot-recorder.js +38 -3
- package/lib/util/cache.js +1 -1
- package/lib/web/eventsource/eventsource-stream.js +245 -150
- package/lib/web/fetch/body.js +2 -7
- package/lib/web/fetch/formdata-parser.js +17 -6
- package/lib/web/fetch/formdata.js +21 -2
- package/lib/web/fetch/index.js +40 -28
- package/lib/web/webidl/index.js +5 -5
- package/lib/web/websocket/frame.js +1 -7
- package/lib/web/websocket/stream/websocketstream.js +6 -5
- package/package.json +4 -4
- package/types/client.d.ts +7 -7
- package/types/dispatcher.d.ts +4 -6
- package/types/formdata.d.ts +0 -6
- package/types/header.d.ts +5 -0
- package/types/interceptors.d.ts +1 -1
- package/types/proxy-agent.d.ts +2 -2
- package/types/snapshot-agent.d.ts +4 -0
- package/types/socks5-proxy-agent.d.ts +2 -2
- package/lib/llhttp/.gitkeep +0 -0
package/README.md
CHANGED
|
@@ -200,7 +200,9 @@ await fetch('https://example.com', {
|
|
|
200
200
|
```
|
|
201
201
|
|
|
202
202
|
`install()` replaces the global `fetch`, `Headers`, `Response`, `Request`, and
|
|
203
|
-
`FormData` implementations with undici's versions, so they all match.
|
|
203
|
+
`FormData` implementations with undici's versions, so they all match. It also
|
|
204
|
+
installs undici's `WebSocket`, `CloseEvent`, `ErrorEvent`, `MessageEvent`, and
|
|
205
|
+
`EventSource` globals.
|
|
204
206
|
|
|
205
207
|
Avoid mixing a global `FormData` with `undici.fetch()`, or `undici.FormData`
|
|
206
208
|
with the built-in global `fetch()`.
|
|
@@ -283,12 +285,12 @@ const data2 = await getData();
|
|
|
283
285
|
|
|
284
286
|
## Global Installation
|
|
285
287
|
|
|
286
|
-
Undici provides an `install()` function to add
|
|
288
|
+
Undici provides an `install()` function to add fetch-related and other web API classes to `globalThis`, making them available globally:
|
|
287
289
|
|
|
288
290
|
```js
|
|
289
291
|
import { install } from 'undici'
|
|
290
292
|
|
|
291
|
-
// Install
|
|
293
|
+
// Install undici's global web APIs
|
|
292
294
|
install()
|
|
293
295
|
|
|
294
296
|
// Now you can use fetch classes globally without importing
|
|
@@ -316,8 +318,9 @@ The `install()` function adds the following classes to `globalThis`:
|
|
|
316
318
|
|
|
317
319
|
When you call `install()`, these globals come from the same undici
|
|
318
320
|
implementation. For example, global `fetch` and global `FormData` will both be
|
|
319
|
-
undici's versions,
|
|
320
|
-
through
|
|
321
|
+
undici's versions, and `WebSocket` and `EventSource` will also come from
|
|
322
|
+
undici, which is the recommended setup if you want to use undici through
|
|
323
|
+
globals.
|
|
321
324
|
|
|
322
325
|
This is useful for:
|
|
323
326
|
- Polyfilling environments that don't have fetch
|
|
@@ -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.
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
# Global Installation
|
|
2
2
|
|
|
3
|
-
Undici provides an `install()` function to add
|
|
3
|
+
Undici provides an `install()` function to add fetch-related and other web API classes to `globalThis`, making them available globally without requiring imports.
|
|
4
4
|
|
|
5
5
|
## `install()`
|
|
6
6
|
|
|
7
|
-
Install
|
|
7
|
+
Install undici's global web APIs on `globalThis`.
|
|
8
8
|
|
|
9
9
|
**Example:**
|
|
10
10
|
|
|
11
11
|
```js
|
|
12
12
|
import { install } from 'undici'
|
|
13
13
|
|
|
14
|
-
// Install
|
|
14
|
+
// Install undici's global web APIs
|
|
15
15
|
install()
|
|
16
16
|
|
|
17
17
|
// Now you can use fetch classes globally without importing
|
|
@@ -74,6 +74,8 @@ await fetch('https://example.com', {
|
|
|
74
74
|
|
|
75
75
|
After `install()`, `fetch`, `Headers`, `Response`, `Request`, and `FormData`
|
|
76
76
|
all come from the installed `undici` package, so they work as a matching set.
|
|
77
|
+
`WebSocket`, `CloseEvent`, `ErrorEvent`, `MessageEvent`, and `EventSource`
|
|
78
|
+
also come from the installed `undici` package.
|
|
77
79
|
|
|
78
80
|
If you do not want to install globals, import both from `undici` instead:
|
|
79
81
|
|
|
@@ -135,5 +137,5 @@ test('fetch API test', async () => {
|
|
|
135
137
|
|
|
136
138
|
- The `install()` function overwrites any existing global implementations
|
|
137
139
|
- Classes installed are undici's implementations, not Node.js built-ins
|
|
138
|
-
- This provides access to undici's latest features and performance improvements
|
|
139
|
-
- The global installation persists for the lifetime of the process
|
|
140
|
+
- This provides access to undici's latest fetch, WebSocket, and EventSource features and performance improvements
|
|
141
|
+
- The global installation persists for the lifetime of the process
|
|
@@ -27,7 +27,9 @@ new SnapshotAgent([options])
|
|
|
27
27
|
- **ignoreHeaders** `Array<String>` - Headers to ignore during request matching
|
|
28
28
|
- **excludeHeaders** `Array<String>` - Headers to exclude from snapshots (for security)
|
|
29
29
|
- **matchBody** `Boolean` - Whether to include request body in matching. Default: `true`
|
|
30
|
+
- **normalizeBody** `Function` - Optional function `(body) => string` to normalize the request body before matching (e.g. strip volatile fields like timestamps). Only used when `matchBody` is `true`.
|
|
30
31
|
- **matchQuery** `Boolean` - Whether to include query parameters in matching. Default: `true`
|
|
32
|
+
- **normalizeQuery** `Function` - Optional function `(query: URLSearchParams) => string` to normalize query parameters before matching (e.g. strip volatile params like cache-busters). Only used when `matchQuery` is `true`.
|
|
31
33
|
- **caseSensitive** `Boolean` - Whether header matching is case-sensitive. Default: `false`
|
|
32
34
|
- **shouldRecord** `Function` - Callback to determine if a request should be recorded
|
|
33
35
|
- **shouldPlayback** `Function` - Callback to determine if a request should be played back
|
|
@@ -108,6 +110,27 @@ await agent.saveSnapshots('./custom-snapshots.json')
|
|
|
108
110
|
|
|
109
111
|
## Advanced Configuration
|
|
110
112
|
|
|
113
|
+
### Body Matching
|
|
114
|
+
|
|
115
|
+
By default (`matchBody: true`) the full request body string is included in the snapshot key. Set it to `false` to ignore the body entirely, or use `normalizeBody` to strip volatile fields (like timestamps) before matching:
|
|
116
|
+
|
|
117
|
+
```javascript
|
|
118
|
+
const agent = new SnapshotAgent({
|
|
119
|
+
mode: 'playback',
|
|
120
|
+
snapshotPath: './snapshots.json',
|
|
121
|
+
|
|
122
|
+
// Match on everything except the timestamp field
|
|
123
|
+
normalizeBody: (body) => {
|
|
124
|
+
if (!body) return ''
|
|
125
|
+
const parsed = JSON.parse(String(body))
|
|
126
|
+
delete parsed.timestamp
|
|
127
|
+
return JSON.stringify(parsed)
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`normalizeBody` receives the raw body (`string | Buffer | null | undefined`) and must return a `string`. It runs at both record and playback time so the hash is consistent. Two requests match the same snapshot whenever their normalized strings are identical.
|
|
133
|
+
|
|
111
134
|
### Header Filtering
|
|
112
135
|
|
|
113
136
|
Control which headers are used for request matching and what gets stored in snapshots:
|
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
|
@@ -13,6 +13,7 @@ const {
|
|
|
13
13
|
RequestAbortedError
|
|
14
14
|
} = require('../core/errors')
|
|
15
15
|
const util = require('../core/util')
|
|
16
|
+
const { kBodyUsed } = require('../core/symbols')
|
|
16
17
|
const { addSignal, removeSignal } = require('./abort-signal')
|
|
17
18
|
|
|
18
19
|
function noop () {}
|
|
@@ -24,6 +25,9 @@ class PipelineRequest extends Readable {
|
|
|
24
25
|
super({ autoDestroy: true })
|
|
25
26
|
|
|
26
27
|
this[kResume] = null
|
|
28
|
+
// Pipeline request bodies come from a live writable side and cannot be
|
|
29
|
+
// replayed across redirects or retries, even before any bytes are read.
|
|
30
|
+
this[kBodyUsed] = true
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
_read () {
|
|
@@ -167,7 +171,7 @@ class PipelineHandler extends AsyncResource {
|
|
|
167
171
|
if (this.onInfo) {
|
|
168
172
|
const rawHeaders = controller?.rawHeaders
|
|
169
173
|
const responseHeaders = this.responseHeaders === 'raw'
|
|
170
|
-
?
|
|
174
|
+
? util.parseRawHeaders(rawHeaders)
|
|
171
175
|
: headers
|
|
172
176
|
this.onInfo({ statusCode, headers: responseHeaders })
|
|
173
177
|
}
|
|
@@ -181,7 +185,7 @@ class PipelineHandler extends AsyncResource {
|
|
|
181
185
|
this.handler = null
|
|
182
186
|
const rawHeaders = controller?.rawHeaders
|
|
183
187
|
const responseHeaders = this.responseHeaders === 'raw'
|
|
184
|
-
?
|
|
188
|
+
? util.parseRawHeaders(rawHeaders)
|
|
185
189
|
: headers
|
|
186
190
|
body = this.runInAsyncScope(handler, null, {
|
|
187
191
|
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
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const assert = require('node:assert')
|
|
4
|
-
const { finished } = require('node:stream')
|
|
5
4
|
const { AsyncResource } = require('node:async_hooks')
|
|
6
5
|
const { InvalidArgumentError, InvalidReturnValueError } = require('../core/errors')
|
|
7
6
|
const util = require('../core/util')
|
|
@@ -9,6 +8,54 @@ const { addSignal, removeSignal } = require('./abort-signal')
|
|
|
9
8
|
|
|
10
9
|
function noop () {}
|
|
11
10
|
|
|
11
|
+
function getWritableError (stream) {
|
|
12
|
+
return stream.errored ?? stream.writableErrored ?? stream._writableState?.errored
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function createPrematureCloseError () {
|
|
16
|
+
const err = new Error('Premature close')
|
|
17
|
+
err.code = 'ERR_STREAM_PREMATURE_CLOSE'
|
|
18
|
+
return err
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function trackWritableLifecycle (stream, callback) {
|
|
22
|
+
let done = false
|
|
23
|
+
|
|
24
|
+
const cleanup = () => {
|
|
25
|
+
stream.removeListener('close', onClose)
|
|
26
|
+
stream.removeListener('error', onError)
|
|
27
|
+
stream.removeListener('finish', onFinish)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const finish = (err, fromErrorEvent = false) => {
|
|
31
|
+
if (done) {
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
done = true
|
|
36
|
+
cleanup()
|
|
37
|
+
callback(err, fromErrorEvent)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const onClose = () => {
|
|
41
|
+
const err = getWritableError(stream)
|
|
42
|
+
finish(err ?? (!stream.writableFinished ? createPrematureCloseError() : undefined))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const onError = (err) => finish(err, true)
|
|
46
|
+
const onFinish = () => finish()
|
|
47
|
+
|
|
48
|
+
stream.on('close', onClose)
|
|
49
|
+
stream.on('error', onError)
|
|
50
|
+
stream.on('finish', onFinish)
|
|
51
|
+
|
|
52
|
+
if (stream.closed) {
|
|
53
|
+
process.nextTick(onClose)
|
|
54
|
+
} else if (stream.writableFinished) {
|
|
55
|
+
process.nextTick(onFinish)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
12
59
|
class StreamHandler extends AsyncResource {
|
|
13
60
|
constructor (opts, factory, callback) {
|
|
14
61
|
if (!opts || typeof opts !== 'object') {
|
|
@@ -85,7 +132,7 @@ class StreamHandler extends AsyncResource {
|
|
|
85
132
|
|
|
86
133
|
const rawHeaders = controller?.rawHeaders
|
|
87
134
|
const responseHeaderData = responseHeaders === 'raw'
|
|
88
|
-
?
|
|
135
|
+
? util.parseRawHeaders(rawHeaders)
|
|
89
136
|
: headers
|
|
90
137
|
|
|
91
138
|
if (statusCode < 200) {
|
|
@@ -117,20 +164,19 @@ class StreamHandler extends AsyncResource {
|
|
|
117
164
|
throw new InvalidReturnValueError('expected Writable')
|
|
118
165
|
}
|
|
119
166
|
|
|
120
|
-
|
|
121
|
-
finished(res, { readable: false }, (err) => {
|
|
167
|
+
trackWritableLifecycle(res, (err, fromErrorEvent) => {
|
|
122
168
|
const { callback, res, opaque, trailers, abort } = this
|
|
123
169
|
|
|
124
170
|
this.res = null
|
|
125
171
|
if (err || !res?.readable) {
|
|
126
|
-
util.destroy(res, err)
|
|
172
|
+
util.destroy(res, fromErrorEvent ? undefined : err)
|
|
127
173
|
}
|
|
128
174
|
|
|
129
175
|
this.callback = null
|
|
130
176
|
this.runInAsyncScope(callback, null, err || null, { opaque, trailers })
|
|
131
177
|
|
|
132
178
|
if (err) {
|
|
133
|
-
abort()
|
|
179
|
+
abort(err)
|
|
134
180
|
}
|
|
135
181
|
})
|
|
136
182
|
|
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
|
|