undici 7.0.0-alpha.1 → 7.0.0-alpha.2
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 +2 -2
- package/docs/docs/api/Client.md +1 -1
- package/docs/docs/api/Debug.md +1 -1
- package/docs/docs/api/Dispatcher.md +53 -2
- package/docs/docs/api/MockAgent.md +2 -0
- package/docs/docs/api/MockPool.md +2 -1
- package/docs/docs/api/RetryAgent.md +1 -1
- package/docs/docs/api/RetryHandler.md +1 -1
- package/docs/docs/api/WebSocket.md +45 -3
- package/index.js +6 -2
- package/lib/api/abort-signal.js +2 -0
- package/lib/api/api-pipeline.js +4 -2
- package/lib/api/api-request.js +4 -2
- package/lib/api/api-stream.js +3 -1
- package/lib/api/api-upgrade.js +2 -2
- package/lib/api/readable.js +194 -41
- package/lib/api/util.js +2 -0
- package/lib/core/connect.js +49 -22
- package/lib/core/constants.js +11 -9
- package/lib/core/diagnostics.js +122 -128
- package/lib/core/request.js +4 -4
- package/lib/core/symbols.js +2 -0
- package/lib/core/tree.js +4 -2
- package/lib/core/util.js +220 -39
- package/lib/dispatcher/client-h1.js +299 -60
- package/lib/dispatcher/client-h2.js +1 -1
- package/lib/dispatcher/client.js +24 -7
- package/lib/dispatcher/fixed-queue.js +91 -49
- package/lib/dispatcher/pool-stats.js +2 -0
- package/lib/dispatcher/proxy-agent.js +3 -1
- package/lib/handler/redirect-handler.js +2 -2
- package/lib/handler/retry-handler.js +2 -2
- package/lib/interceptor/dns.js +346 -0
- package/lib/mock/mock-agent.js +5 -8
- package/lib/mock/mock-client.js +7 -2
- package/lib/mock/mock-errors.js +3 -1
- package/lib/mock/mock-interceptor.js +8 -6
- package/lib/mock/mock-pool.js +7 -2
- package/lib/mock/mock-symbols.js +2 -1
- package/lib/mock/mock-utils.js +33 -5
- package/lib/util/timers.js +50 -6
- package/lib/web/cache/cache.js +24 -21
- package/lib/web/cache/cachestorage.js +1 -1
- package/lib/web/cookies/index.js +6 -4
- package/lib/web/fetch/body.js +42 -34
- package/lib/web/fetch/constants.js +35 -26
- package/lib/web/fetch/formdata-parser.js +14 -3
- package/lib/web/fetch/formdata.js +40 -20
- package/lib/web/fetch/headers.js +116 -84
- package/lib/web/fetch/index.js +65 -59
- package/lib/web/fetch/request.js +130 -55
- package/lib/web/fetch/response.js +79 -36
- package/lib/web/fetch/util.js +104 -57
- package/lib/web/fetch/webidl.js +38 -14
- package/lib/web/websocket/connection.js +92 -15
- package/lib/web/websocket/constants.js +2 -3
- package/lib/web/websocket/events.js +4 -2
- package/lib/web/websocket/receiver.js +20 -26
- package/lib/web/websocket/stream/websocketerror.js +83 -0
- package/lib/web/websocket/stream/websocketstream.js +485 -0
- package/lib/web/websocket/util.js +115 -10
- package/lib/web/websocket/websocket.js +45 -170
- package/package.json +6 -6
- package/types/interceptors.d.ts +14 -0
- package/types/mock-agent.d.ts +3 -0
- package/types/readable.d.ts +10 -7
- package/types/webidl.d.ts +24 -4
- package/types/websocket.d.ts +33 -0
- package/lib/mock/pluralizer.js +0 -29
- package/lib/web/cache/symbols.js +0 -5
- package/lib/web/fetch/symbols.js +0 -8
|
@@ -281,7 +281,7 @@ function writeH2 (client, request) {
|
|
|
281
281
|
}
|
|
282
282
|
|
|
283
283
|
// We do not destroy the socket as we can continue using the session
|
|
284
|
-
// the stream
|
|
284
|
+
// the stream gets destroyed and the session remains to create new streams
|
|
285
285
|
util.destroy(body, err)
|
|
286
286
|
}
|
|
287
287
|
|
package/lib/dispatcher/client.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
|
|
3
1
|
'use strict'
|
|
4
2
|
|
|
5
3
|
const assert = require('node:assert')
|
|
@@ -60,6 +58,15 @@ const connectH2 = require('./client-h2.js')
|
|
|
60
58
|
|
|
61
59
|
const kClosedResolve = Symbol('kClosedResolve')
|
|
62
60
|
|
|
61
|
+
const getDefaultNodeMaxHeaderSize = http &&
|
|
62
|
+
http.maxHeaderSize &&
|
|
63
|
+
Number.isInteger(http.maxHeaderSize) &&
|
|
64
|
+
http.maxHeaderSize > 0
|
|
65
|
+
? () => http.maxHeaderSize
|
|
66
|
+
: () => { throw new InvalidArgumentError('http module not available or http.maxHeaderSize invalid') }
|
|
67
|
+
|
|
68
|
+
const noop = () => {}
|
|
69
|
+
|
|
63
70
|
function getPipelining (client) {
|
|
64
71
|
return client[kPipelining] ?? client[kHTTPContext]?.defaultPipelining ?? 1
|
|
65
72
|
}
|
|
@@ -121,8 +128,14 @@ class Client extends DispatcherBase {
|
|
|
121
128
|
throw new InvalidArgumentError('unsupported maxKeepAliveTimeout, use keepAliveMaxTimeout instead')
|
|
122
129
|
}
|
|
123
130
|
|
|
124
|
-
if (maxHeaderSize != null
|
|
125
|
-
|
|
131
|
+
if (maxHeaderSize != null) {
|
|
132
|
+
if (!Number.isInteger(maxHeaderSize) || maxHeaderSize < 1) {
|
|
133
|
+
throw new InvalidArgumentError('invalid maxHeaderSize')
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
// If maxHeaderSize is not provided, use the default value from the http module
|
|
137
|
+
// or if that is not available, throw an error.
|
|
138
|
+
maxHeaderSize = getDefaultNodeMaxHeaderSize()
|
|
126
139
|
}
|
|
127
140
|
|
|
128
141
|
if (socketPath != null && typeof socketPath !== 'string') {
|
|
@@ -202,7 +215,7 @@ class Client extends DispatcherBase {
|
|
|
202
215
|
this[kUrl] = util.parseOrigin(url)
|
|
203
216
|
this[kConnector] = connect
|
|
204
217
|
this[kPipelining] = pipelining != null ? pipelining : 1
|
|
205
|
-
this[kMaxHeadersSize] = maxHeaderSize
|
|
218
|
+
this[kMaxHeadersSize] = maxHeaderSize
|
|
206
219
|
this[kKeepAliveDefaultTimeout] = keepAliveTimeout == null ? 4e3 : keepAliveTimeout
|
|
207
220
|
this[kKeepAliveMaxTimeout] = keepAliveMaxTimeout == null ? 600e3 : keepAliveMaxTimeout
|
|
208
221
|
this[kKeepAliveTimeoutThreshold] = keepAliveTimeoutThreshold == null ? 2e3 : keepAliveTimeoutThreshold
|
|
@@ -361,6 +374,10 @@ function onError (client, err) {
|
|
|
361
374
|
}
|
|
362
375
|
}
|
|
363
376
|
|
|
377
|
+
/**
|
|
378
|
+
* @param {Client} client
|
|
379
|
+
* @returns
|
|
380
|
+
*/
|
|
364
381
|
async function connect (client) {
|
|
365
382
|
assert(!client[kConnecting])
|
|
366
383
|
assert(!client[kHTTPContext])
|
|
@@ -414,7 +431,7 @@ async function connect (client) {
|
|
|
414
431
|
})
|
|
415
432
|
|
|
416
433
|
if (client.destroyed) {
|
|
417
|
-
util.destroy(socket.on('error',
|
|
434
|
+
util.destroy(socket.on('error', noop), new ClientDestroyedError())
|
|
418
435
|
return
|
|
419
436
|
}
|
|
420
437
|
|
|
@@ -425,7 +442,7 @@ async function connect (client) {
|
|
|
425
442
|
? await connectH2(client, socket)
|
|
426
443
|
: await connectH1(client, socket)
|
|
427
444
|
} catch (err) {
|
|
428
|
-
socket.destroy().on('error',
|
|
445
|
+
socket.destroy().on('error', noop)
|
|
429
446
|
throw err
|
|
430
447
|
}
|
|
431
448
|
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
/* eslint-disable */
|
|
2
|
-
|
|
3
1
|
'use strict'
|
|
4
2
|
|
|
5
3
|
// Extracted from node/lib/internal/fixed_queue.js
|
|
6
4
|
|
|
7
5
|
// Currently optimal queue size, tested on V8 6.0 - 6.6. Must be power of two.
|
|
8
|
-
const kSize = 2048
|
|
9
|
-
const kMask = kSize - 1
|
|
6
|
+
const kSize = 2048
|
|
7
|
+
const kMask = kSize - 1
|
|
10
8
|
|
|
11
9
|
// The FixedQueue is implemented as a singly-linked list of fixed-size
|
|
12
10
|
// circular buffers. It looks something like this:
|
|
@@ -17,18 +15,18 @@ const kMask = kSize - 1;
|
|
|
17
15
|
// +-----------+ <-----\ +-----------+ <------\ +-----------+
|
|
18
16
|
// | [null] | \----- | next | \------- | next |
|
|
19
17
|
// +-----------+ +-----------+ +-----------+
|
|
20
|
-
// | item | <-- bottom | item | <-- bottom |
|
|
21
|
-
// | item | | item | |
|
|
22
|
-
// | item | | item | |
|
|
23
|
-
// | item | | item | |
|
|
18
|
+
// | item | <-- bottom | item | <-- bottom | undefined |
|
|
19
|
+
// | item | | item | | undefined |
|
|
20
|
+
// | item | | item | | undefined |
|
|
21
|
+
// | item | | item | | undefined |
|
|
24
22
|
// | item | | item | bottom --> | item |
|
|
25
23
|
// | item | | item | | item |
|
|
26
24
|
// | ... | | ... | | ... |
|
|
27
25
|
// | item | | item | | item |
|
|
28
26
|
// | item | | item | | item |
|
|
29
|
-
// |
|
|
30
|
-
// |
|
|
31
|
-
// |
|
|
27
|
+
// | undefined | <-- top | item | | item |
|
|
28
|
+
// | undefined | | item | | item |
|
|
29
|
+
// | undefined | | undefined | <-- top top --> | undefined |
|
|
32
30
|
// +-----------+ +-----------+ +-----------+
|
|
33
31
|
//
|
|
34
32
|
// Or, if there is only one circular buffer, it looks something
|
|
@@ -40,12 +38,12 @@ const kMask = kSize - 1;
|
|
|
40
38
|
// +-----------+ +-----------+
|
|
41
39
|
// | [null] | | [null] |
|
|
42
40
|
// +-----------+ +-----------+
|
|
43
|
-
// |
|
|
44
|
-
// |
|
|
45
|
-
// | item | <-- bottom top --> |
|
|
46
|
-
// | item | |
|
|
47
|
-
// |
|
|
48
|
-
// |
|
|
41
|
+
// | undefined | | item |
|
|
42
|
+
// | undefined | | item |
|
|
43
|
+
// | item | <-- bottom top --> | undefined |
|
|
44
|
+
// | item | | undefined |
|
|
45
|
+
// | undefined | <-- top bottom --> | item |
|
|
46
|
+
// | undefined | | item |
|
|
49
47
|
// +-----------+ +-----------+
|
|
50
48
|
//
|
|
51
49
|
// Adding a value means moving `top` forward by one, removing means
|
|
@@ -56,62 +54,106 @@ const kMask = kSize - 1;
|
|
|
56
54
|
// `top + 1 === bottom` it's full. This wastes a single space of storage
|
|
57
55
|
// but allows much quicker checks.
|
|
58
56
|
|
|
57
|
+
/**
|
|
58
|
+
* @type {FixedCircularBuffer}
|
|
59
|
+
* @template T
|
|
60
|
+
*/
|
|
59
61
|
class FixedCircularBuffer {
|
|
60
|
-
constructor() {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
this.
|
|
62
|
+
constructor () {
|
|
63
|
+
/**
|
|
64
|
+
* @type {number}
|
|
65
|
+
*/
|
|
66
|
+
this.bottom = 0
|
|
67
|
+
/**
|
|
68
|
+
* @type {number}
|
|
69
|
+
*/
|
|
70
|
+
this.top = 0
|
|
71
|
+
/**
|
|
72
|
+
* @type {Array<T|undefined>}
|
|
73
|
+
*/
|
|
74
|
+
this.list = new Array(kSize).fill(undefined)
|
|
75
|
+
/**
|
|
76
|
+
* @type {T|null}
|
|
77
|
+
*/
|
|
78
|
+
this.next = null
|
|
65
79
|
}
|
|
66
80
|
|
|
67
|
-
|
|
68
|
-
|
|
81
|
+
/**
|
|
82
|
+
* @returns {boolean}
|
|
83
|
+
*/
|
|
84
|
+
isEmpty () {
|
|
85
|
+
return this.top === this.bottom
|
|
69
86
|
}
|
|
70
87
|
|
|
71
|
-
|
|
72
|
-
|
|
88
|
+
/**
|
|
89
|
+
* @returns {boolean}
|
|
90
|
+
*/
|
|
91
|
+
isFull () {
|
|
92
|
+
return ((this.top + 1) & kMask) === this.bottom
|
|
73
93
|
}
|
|
74
94
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
95
|
+
/**
|
|
96
|
+
* @param {T} data
|
|
97
|
+
* @returns {void}
|
|
98
|
+
*/
|
|
99
|
+
push (data) {
|
|
100
|
+
this.list[this.top] = data
|
|
101
|
+
this.top = (this.top + 1) & kMask
|
|
78
102
|
}
|
|
79
103
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
this.list[this.bottom]
|
|
85
|
-
|
|
86
|
-
|
|
104
|
+
/**
|
|
105
|
+
* @returns {T|null}
|
|
106
|
+
*/
|
|
107
|
+
shift () {
|
|
108
|
+
const nextItem = this.list[this.bottom]
|
|
109
|
+
if (nextItem === undefined) { return null }
|
|
110
|
+
this.list[this.bottom] = undefined
|
|
111
|
+
this.bottom = (this.bottom + 1) & kMask
|
|
112
|
+
return nextItem
|
|
87
113
|
}
|
|
88
114
|
}
|
|
89
115
|
|
|
116
|
+
/**
|
|
117
|
+
* @template T
|
|
118
|
+
*/
|
|
90
119
|
module.exports = class FixedQueue {
|
|
91
|
-
constructor() {
|
|
92
|
-
|
|
120
|
+
constructor () {
|
|
121
|
+
/**
|
|
122
|
+
* @type {FixedCircularBuffer<T>}
|
|
123
|
+
*/
|
|
124
|
+
this.head = this.tail = new FixedCircularBuffer()
|
|
93
125
|
}
|
|
94
126
|
|
|
95
|
-
|
|
96
|
-
|
|
127
|
+
/**
|
|
128
|
+
* @returns {boolean}
|
|
129
|
+
*/
|
|
130
|
+
isEmpty () {
|
|
131
|
+
return this.head.isEmpty()
|
|
97
132
|
}
|
|
98
133
|
|
|
99
|
-
|
|
134
|
+
/**
|
|
135
|
+
* @param {T} data
|
|
136
|
+
*/
|
|
137
|
+
push (data) {
|
|
100
138
|
if (this.head.isFull()) {
|
|
101
139
|
// Head is full: Creates a new queue, sets the old queue's `.next` to it,
|
|
102
140
|
// and sets it as the new main queue.
|
|
103
|
-
this.head = this.head.next = new FixedCircularBuffer()
|
|
141
|
+
this.head = this.head.next = new FixedCircularBuffer()
|
|
104
142
|
}
|
|
105
|
-
this.head.push(data)
|
|
143
|
+
this.head.push(data)
|
|
106
144
|
}
|
|
107
145
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
146
|
+
/**
|
|
147
|
+
* @returns {T|null}
|
|
148
|
+
*/
|
|
149
|
+
shift () {
|
|
150
|
+
const tail = this.tail
|
|
151
|
+
const next = tail.shift()
|
|
111
152
|
if (tail.isEmpty() && tail.next !== null) {
|
|
112
153
|
// If there is another queue, it forms the new tail.
|
|
113
|
-
this.tail = tail.next
|
|
154
|
+
this.tail = tail.next
|
|
155
|
+
tail.next = null
|
|
114
156
|
}
|
|
115
|
-
return next
|
|
157
|
+
return next
|
|
116
158
|
}
|
|
117
|
-
}
|
|
159
|
+
}
|
|
@@ -23,6 +23,8 @@ function defaultFactory (origin, opts) {
|
|
|
23
23
|
return new Pool(origin, opts)
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
const noop = () => {}
|
|
27
|
+
|
|
26
28
|
class ProxyAgent extends DispatcherBase {
|
|
27
29
|
constructor (opts) {
|
|
28
30
|
if (!opts || (typeof opts === 'object' && !(opts instanceof URL) && !opts.uri)) {
|
|
@@ -78,7 +80,7 @@ class ProxyAgent extends DispatcherBase {
|
|
|
78
80
|
servername: this[kProxyTls]?.servername || proxyHostname
|
|
79
81
|
})
|
|
80
82
|
if (statusCode !== 200) {
|
|
81
|
-
socket.on('error',
|
|
83
|
+
socket.on('error', noop).destroy()
|
|
82
84
|
callback(new RequestAbortedError(`Proxy response (${statusCode}) !== 200 when HTTP Tunneling`))
|
|
83
85
|
}
|
|
84
86
|
if (opts.protocol !== 'https:') {
|
|
@@ -38,7 +38,7 @@ class RedirectHandler {
|
|
|
38
38
|
throw new InvalidArgumentError('maxRedirections must be a positive number')
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
util.
|
|
41
|
+
util.assertRequestHandler(handler, opts.method, opts.upgrade)
|
|
42
42
|
|
|
43
43
|
this.dispatch = dispatch
|
|
44
44
|
this.location = null
|
|
@@ -146,7 +146,7 @@ class RedirectHandler {
|
|
|
146
146
|
|
|
147
147
|
TLDR: undici always ignores 3xx response bodies.
|
|
148
148
|
|
|
149
|
-
Redirection is used to serve the requested resource from another URL, so it
|
|
149
|
+
Redirection is used to serve the requested resource from another URL, so it assumes that
|
|
150
150
|
no body is generated (and thus can be ignored). Even though generating a body is not prohibited.
|
|
151
151
|
|
|
152
152
|
For status 301, 302, 303, 307 and 308 (the latter from RFC 7238), the specs mention that the body usually
|
|
@@ -194,8 +194,8 @@ class RetryHandler {
|
|
|
194
194
|
|
|
195
195
|
// Only Partial Content 206 supposed to provide Content-Range,
|
|
196
196
|
// any other status code that partially consumed the payload
|
|
197
|
-
// should not be
|
|
198
|
-
// wrongly
|
|
197
|
+
// should not be retried because it would result in downstream
|
|
198
|
+
// wrongly concatenate multiple responses.
|
|
199
199
|
if (statusCode !== 206 && (this.start > 0 || statusCode !== 200)) {
|
|
200
200
|
this.abort(
|
|
201
201
|
new RequestRetryError('server does not support the range header and the payload was partially consumed', statusCode, {
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
const { isIP } = require('node:net')
|
|
3
|
+
const { lookup } = require('node:dns')
|
|
4
|
+
const DecoratorHandler = require('../handler/decorator-handler')
|
|
5
|
+
const { InvalidArgumentError, InformationalError } = require('../core/errors')
|
|
6
|
+
const maxInt = Math.pow(2, 31) - 1
|
|
7
|
+
|
|
8
|
+
class DNSInstance {
|
|
9
|
+
#maxTTL = 0
|
|
10
|
+
#maxItems = 0
|
|
11
|
+
#records = new Map()
|
|
12
|
+
dualStack = true
|
|
13
|
+
affinity = null
|
|
14
|
+
lookup = null
|
|
15
|
+
pick = null
|
|
16
|
+
lastIpFamily = null
|
|
17
|
+
|
|
18
|
+
constructor (opts) {
|
|
19
|
+
this.#maxTTL = opts.maxTTL
|
|
20
|
+
this.#maxItems = opts.maxItems
|
|
21
|
+
this.dualStack = opts.dualStack
|
|
22
|
+
this.affinity = opts.affinity
|
|
23
|
+
this.lookup = opts.lookup ?? this.#defaultLookup
|
|
24
|
+
this.pick = opts.pick ?? this.#defaultPick
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get full () {
|
|
28
|
+
return this.#records.size === this.#maxItems
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
runLookup (origin, opts, cb) {
|
|
32
|
+
const ips = this.#records.get(origin.hostname)
|
|
33
|
+
|
|
34
|
+
// If full, we just return the origin
|
|
35
|
+
if (ips == null && this.full) {
|
|
36
|
+
cb(null, origin.origin)
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const newOpts = {
|
|
41
|
+
affinity: this.affinity,
|
|
42
|
+
dualStack: this.dualStack,
|
|
43
|
+
lookup: this.lookup,
|
|
44
|
+
pick: this.pick,
|
|
45
|
+
...opts.dns,
|
|
46
|
+
maxTTL: this.#maxTTL,
|
|
47
|
+
maxItems: this.#maxItems
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// If no IPs we lookup
|
|
51
|
+
if (ips == null) {
|
|
52
|
+
this.lookup(origin, newOpts, (err, addresses) => {
|
|
53
|
+
if (err || addresses == null || addresses.length === 0) {
|
|
54
|
+
cb(err ?? new InformationalError('No DNS entries found'))
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this.setRecords(origin, addresses)
|
|
59
|
+
const records = this.#records.get(origin.hostname)
|
|
60
|
+
|
|
61
|
+
const ip = this.pick(
|
|
62
|
+
origin,
|
|
63
|
+
records,
|
|
64
|
+
// Only set affinity if dual stack is disabled
|
|
65
|
+
// otherwise let it go through normal flow
|
|
66
|
+
!newOpts.dualStack && newOpts.affinity
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
cb(
|
|
70
|
+
null,
|
|
71
|
+
`${origin.protocol}//${
|
|
72
|
+
ip.family === 6 ? `[${ip.address}]` : ip.address
|
|
73
|
+
}${origin.port === '' ? '' : `:${origin.port}`}`
|
|
74
|
+
)
|
|
75
|
+
})
|
|
76
|
+
} else {
|
|
77
|
+
// If there's IPs we pick
|
|
78
|
+
const ip = this.pick(
|
|
79
|
+
origin,
|
|
80
|
+
ips,
|
|
81
|
+
// Only set affinity if dual stack is disabled
|
|
82
|
+
// otherwise let it go through normal flow
|
|
83
|
+
!newOpts.dualStack && newOpts.affinity
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
// If no IPs we lookup - deleting old records
|
|
87
|
+
if (ip == null) {
|
|
88
|
+
this.#records.delete(origin.hostname)
|
|
89
|
+
this.runLookup(origin, opts, cb)
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
cb(
|
|
94
|
+
null,
|
|
95
|
+
`${origin.protocol}//${
|
|
96
|
+
ip.family === 6 ? `[${ip.address}]` : ip.address
|
|
97
|
+
}${origin.port === '' ? '' : `:${origin.port}`}`
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
#defaultLookup (origin, opts, cb) {
|
|
103
|
+
lookup(
|
|
104
|
+
origin.hostname,
|
|
105
|
+
{ all: true, family: this.dualStack === false ? this.affinity : 0 },
|
|
106
|
+
(err, addresses) => {
|
|
107
|
+
if (err) {
|
|
108
|
+
return cb(err)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const results = new Map()
|
|
112
|
+
|
|
113
|
+
for (const addr of addresses) {
|
|
114
|
+
const record = {
|
|
115
|
+
address: addr.address,
|
|
116
|
+
ttl: opts.maxTTL,
|
|
117
|
+
family: addr.family
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// On linux we found duplicates, we attempt to remove them with
|
|
121
|
+
// the latest record
|
|
122
|
+
results.set(`${record.address}:${record.family}`, record)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
cb(null, results.values())
|
|
126
|
+
}
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
#defaultPick (origin, hostnameRecords, affinity) {
|
|
131
|
+
let ip = null
|
|
132
|
+
const { records, offset = 0 } = hostnameRecords
|
|
133
|
+
let newOffset = 0
|
|
134
|
+
|
|
135
|
+
if (offset === maxInt) {
|
|
136
|
+
newOffset = 0
|
|
137
|
+
} else {
|
|
138
|
+
newOffset = offset + 1
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// We balance between the two IP families
|
|
142
|
+
// If dual-stack disabled, we automatically pick the affinity
|
|
143
|
+
const newIpFamily = (newOffset & 1) === 1 ? 4 : 6
|
|
144
|
+
const family =
|
|
145
|
+
this.dualStack === false
|
|
146
|
+
? records[this.affinity] // If dual-stack is disabled, we pick the default affiniy
|
|
147
|
+
: records[affinity] ?? records[newIpFamily]
|
|
148
|
+
|
|
149
|
+
// If no IPs and we have tried both families or dual stack is disabled, we return null
|
|
150
|
+
if (
|
|
151
|
+
(family == null || family.ips.length === 0) &&
|
|
152
|
+
// eslint-disable-next-line eqeqeq
|
|
153
|
+
(this.dualStack === false || this.lastIpFamily != newIpFamily)
|
|
154
|
+
) {
|
|
155
|
+
return ip
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
family.offset = family.offset ?? 0
|
|
159
|
+
hostnameRecords.offset = newOffset
|
|
160
|
+
|
|
161
|
+
if (family.offset === maxInt) {
|
|
162
|
+
family.offset = 0
|
|
163
|
+
} else {
|
|
164
|
+
family.offset++
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const position = family.offset % family.ips.length
|
|
168
|
+
ip = family.ips[position] ?? null
|
|
169
|
+
|
|
170
|
+
if (ip == null) {
|
|
171
|
+
return ip
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const timestamp = Date.now()
|
|
175
|
+
// Record TTL is already in ms
|
|
176
|
+
if (ip.timestamp != null && timestamp - ip.timestamp > ip.ttl) {
|
|
177
|
+
// We delete expired records
|
|
178
|
+
// It is possible that they have different TTL, so we manage them individually
|
|
179
|
+
family.ips.splice(position, 1)
|
|
180
|
+
return this.pick(origin, hostnameRecords, affinity)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
ip.timestamp = timestamp
|
|
184
|
+
|
|
185
|
+
this.lastIpFamily = newIpFamily
|
|
186
|
+
return ip
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
setRecords (origin, addresses) {
|
|
190
|
+
const records = { records: { 4: null, 6: null } }
|
|
191
|
+
for (const record of addresses) {
|
|
192
|
+
const familyRecords = records.records[record.family] ?? { ips: [] }
|
|
193
|
+
|
|
194
|
+
familyRecords.ips.push(record)
|
|
195
|
+
records.records[record.family] = familyRecords
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
this.#records.set(origin.hostname, records)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
getHandler (meta, opts) {
|
|
202
|
+
return new DNSDispatchHandler(this, meta, opts)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
class DNSDispatchHandler extends DecoratorHandler {
|
|
207
|
+
#state = null
|
|
208
|
+
#opts = null
|
|
209
|
+
#dispatch = null
|
|
210
|
+
#handler = null
|
|
211
|
+
#origin = null
|
|
212
|
+
|
|
213
|
+
constructor (state, { origin, handler, dispatch }, opts) {
|
|
214
|
+
super(handler)
|
|
215
|
+
this.#origin = origin
|
|
216
|
+
this.#handler = handler
|
|
217
|
+
this.#opts = { ...opts }
|
|
218
|
+
this.#state = state
|
|
219
|
+
this.#dispatch = dispatch
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
onError (err) {
|
|
223
|
+
switch (err.code) {
|
|
224
|
+
case 'ETIMEDOUT':
|
|
225
|
+
case 'ECONNREFUSED': {
|
|
226
|
+
if (this.#state.dualStack) {
|
|
227
|
+
// We delete the record and retry
|
|
228
|
+
this.#state.runLookup(this.#origin, this.#opts, (err, newOrigin) => {
|
|
229
|
+
if (err) {
|
|
230
|
+
return this.#handler.onError(err)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const dispatchOpts = {
|
|
234
|
+
...this.#opts,
|
|
235
|
+
origin: newOrigin
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
this.#dispatch(dispatchOpts, this)
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
// if dual-stack disabled, we error out
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
this.#handler.onError(err)
|
|
246
|
+
return
|
|
247
|
+
}
|
|
248
|
+
case 'ENOTFOUND':
|
|
249
|
+
this.#state.deleteRecord(this.#origin)
|
|
250
|
+
// eslint-disable-next-line no-fallthrough
|
|
251
|
+
default:
|
|
252
|
+
this.#handler.onError(err)
|
|
253
|
+
break
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
module.exports = interceptorOpts => {
|
|
259
|
+
if (
|
|
260
|
+
interceptorOpts?.maxTTL != null &&
|
|
261
|
+
(typeof interceptorOpts?.maxTTL !== 'number' || interceptorOpts?.maxTTL < 0)
|
|
262
|
+
) {
|
|
263
|
+
throw new InvalidArgumentError('Invalid maxTTL. Must be a positive number')
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (
|
|
267
|
+
interceptorOpts?.maxItems != null &&
|
|
268
|
+
(typeof interceptorOpts?.maxItems !== 'number' ||
|
|
269
|
+
interceptorOpts?.maxItems < 1)
|
|
270
|
+
) {
|
|
271
|
+
throw new InvalidArgumentError(
|
|
272
|
+
'Invalid maxItems. Must be a positive number and greater than zero'
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (
|
|
277
|
+
interceptorOpts?.affinity != null &&
|
|
278
|
+
interceptorOpts?.affinity !== 4 &&
|
|
279
|
+
interceptorOpts?.affinity !== 6
|
|
280
|
+
) {
|
|
281
|
+
throw new InvalidArgumentError('Invalid affinity. Must be either 4 or 6')
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (
|
|
285
|
+
interceptorOpts?.dualStack != null &&
|
|
286
|
+
typeof interceptorOpts?.dualStack !== 'boolean'
|
|
287
|
+
) {
|
|
288
|
+
throw new InvalidArgumentError('Invalid dualStack. Must be a boolean')
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (
|
|
292
|
+
interceptorOpts?.lookup != null &&
|
|
293
|
+
typeof interceptorOpts?.lookup !== 'function'
|
|
294
|
+
) {
|
|
295
|
+
throw new InvalidArgumentError('Invalid lookup. Must be a function')
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (
|
|
299
|
+
interceptorOpts?.pick != null &&
|
|
300
|
+
typeof interceptorOpts?.pick !== 'function'
|
|
301
|
+
) {
|
|
302
|
+
throw new InvalidArgumentError('Invalid pick. Must be a function')
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const opts = {
|
|
306
|
+
maxTTL: interceptorOpts?.maxTTL ?? 10e3, // Expressed in ms
|
|
307
|
+
lookup: interceptorOpts?.lookup ?? null,
|
|
308
|
+
pick: interceptorOpts?.pick ?? null,
|
|
309
|
+
dualStack: interceptorOpts?.dualStack ?? true,
|
|
310
|
+
affinity: interceptorOpts?.affinity ?? 4,
|
|
311
|
+
maxItems: interceptorOpts?.maxItems ?? Infinity
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const instance = new DNSInstance(opts)
|
|
315
|
+
|
|
316
|
+
return dispatch => {
|
|
317
|
+
return function dnsInterceptor (origDispatchOpts, handler) {
|
|
318
|
+
const origin =
|
|
319
|
+
origDispatchOpts.origin.constructor === URL
|
|
320
|
+
? origDispatchOpts.origin
|
|
321
|
+
: new URL(origDispatchOpts.origin)
|
|
322
|
+
|
|
323
|
+
if (isIP(origin.hostname) !== 0) {
|
|
324
|
+
return dispatch(origDispatchOpts, handler)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
instance.runLookup(origin, origDispatchOpts, (err, newOrigin) => {
|
|
328
|
+
if (err) {
|
|
329
|
+
return handler.onError(err)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const dispatchOpts = {
|
|
333
|
+
...origDispatchOpts,
|
|
334
|
+
origin: newOrigin
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
dispatch(
|
|
338
|
+
dispatchOpts,
|
|
339
|
+
instance.getHandler({ origin, dispatch, handler }, origDispatchOpts)
|
|
340
|
+
)
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
return true
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|