undici 6.27.0 → 6.28.1
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/lib/core/request.js +12 -1
- package/lib/dispatcher/client-h1.js +23 -6
- package/lib/handler/retry-handler.js +63 -3
- package/lib/web/cookies/util.js +79 -9
- package/lib/web/eventsource/eventsource-stream.js +245 -150
- package/lib/web/websocket/connection.js +1 -1
- package/lib/web/websocket/permessage-deflate.js +5 -0
- package/package.json +1 -1
package/lib/core/request.js
CHANGED
|
@@ -350,7 +350,13 @@ function processHeader (request, key, val) {
|
|
|
350
350
|
} else if (typeof val[i] === 'object') {
|
|
351
351
|
throw new InvalidArgumentError(`invalid ${key} header`)
|
|
352
352
|
} else {
|
|
353
|
-
|
|
353
|
+
// Coerce primitives (and reject unsafe coercions such as functions
|
|
354
|
+
// with a crafted toString/Symbol.toPrimitive).
|
|
355
|
+
const str = `${val[i]}`
|
|
356
|
+
if (!isValidHeaderValue(str)) {
|
|
357
|
+
throw new InvalidArgumentError(`invalid ${key} header`)
|
|
358
|
+
}
|
|
359
|
+
arr.push(str)
|
|
354
360
|
}
|
|
355
361
|
}
|
|
356
362
|
val = arr
|
|
@@ -361,7 +367,12 @@ function processHeader (request, key, val) {
|
|
|
361
367
|
} else if (val === null) {
|
|
362
368
|
val = ''
|
|
363
369
|
} else {
|
|
370
|
+
// Coerce primitives (and reject unsafe coercions such as functions
|
|
371
|
+
// with a crafted toString/Symbol.toPrimitive).
|
|
364
372
|
val = `${val}`
|
|
373
|
+
if (!isValidHeaderValue(val)) {
|
|
374
|
+
throw new InvalidArgumentError(`invalid ${key} header`)
|
|
375
|
+
}
|
|
365
376
|
}
|
|
366
377
|
|
|
367
378
|
if (headerName === 'host') {
|
|
@@ -10,6 +10,7 @@ const {
|
|
|
10
10
|
RequestContentLengthMismatchError,
|
|
11
11
|
ResponseContentLengthMismatchError,
|
|
12
12
|
RequestAbortedError,
|
|
13
|
+
InvalidArgumentError,
|
|
13
14
|
HeadersTimeoutError,
|
|
14
15
|
HeadersOverflowError,
|
|
15
16
|
SocketError,
|
|
@@ -875,7 +876,7 @@ async function connectH1 (client, socket) {
|
|
|
875
876
|
|
|
876
877
|
function clearIdleSocketValidation (socket) {
|
|
877
878
|
if (socket[kIdleSocketValidationTimeout]) {
|
|
878
|
-
|
|
879
|
+
clearImmediate(socket[kIdleSocketValidationTimeout])
|
|
879
880
|
socket[kIdleSocketValidationTimeout] = null
|
|
880
881
|
}
|
|
881
882
|
|
|
@@ -884,15 +885,23 @@ function clearIdleSocketValidation (socket) {
|
|
|
884
885
|
|
|
885
886
|
function scheduleIdleSocketValidation (client, socket) {
|
|
886
887
|
socket[kIdleSocketValidation] = 1
|
|
887
|
-
|
|
888
|
+
// Yield to the check phase (after poll) so unsolicited bytes / FIN / RST
|
|
889
|
+
// already pending on this idle keep-alive socket are processed before the
|
|
890
|
+
// next request is written (GHSA-35p6-xmwp-9g52).
|
|
891
|
+
//
|
|
892
|
+
// setTimeout(0) pays Node's ~1ms timer floor on every sequential reuse
|
|
893
|
+
// (#5493). setImmediate avoids that, but an *unref'd* Immediate lets poll
|
|
894
|
+
// block for ~500ms when the event loop is otherwise idle (#5600 / #5606).
|
|
895
|
+
// A ref'd Immediate both keeps the pending request alive and makes poll
|
|
896
|
+
// return immediately — the hybrid those issues asked for.
|
|
897
|
+
socket[kIdleSocketValidationTimeout] = setImmediate(() => {
|
|
888
898
|
socket[kIdleSocketValidationTimeout] = null
|
|
889
899
|
socket[kIdleSocketValidation] = 2
|
|
890
900
|
|
|
891
901
|
if (client[kSocket] === socket && !socket.destroyed) {
|
|
892
902
|
client[kResume]()
|
|
893
903
|
}
|
|
894
|
-
}
|
|
895
|
-
socket[kIdleSocketValidationTimeout].unref?.()
|
|
904
|
+
})
|
|
896
905
|
}
|
|
897
906
|
|
|
898
907
|
/**
|
|
@@ -993,8 +1002,16 @@ function writeH1 (client, request) {
|
|
|
993
1002
|
}
|
|
994
1003
|
body = bodyStream.stream
|
|
995
1004
|
contentLength = bodyStream.length
|
|
996
|
-
} else if (util.isBlobLike(body) && request.contentType == null
|
|
997
|
-
|
|
1005
|
+
} else if (util.isBlobLike(body) && request.contentType == null) {
|
|
1006
|
+
const contentType = body.type
|
|
1007
|
+
if (contentType) {
|
|
1008
|
+
const contentTypeValue = `${contentType}`
|
|
1009
|
+
if (!util.isValidHeaderValue(contentTypeValue)) {
|
|
1010
|
+
util.errorRequest(client, request, new InvalidArgumentError('invalid content-type header'))
|
|
1011
|
+
return false
|
|
1012
|
+
}
|
|
1013
|
+
headers.push('content-type', contentTypeValue)
|
|
1014
|
+
}
|
|
998
1015
|
}
|
|
999
1016
|
|
|
1000
1017
|
if (body && typeof body.read === 'function') {
|
|
@@ -15,6 +15,28 @@ function calculateRetryAfterHeader (retryAfter) {
|
|
|
15
15
|
return new Date(retryAfter).getTime() - current
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
function validatePartialResponseContentLength (headers, range, statusCode, retryCount) {
|
|
19
|
+
const contentLength = headers['content-length']
|
|
20
|
+
if (contentLength == null) {
|
|
21
|
+
return null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!Number.isFinite(range.start) || !Number.isFinite(range.end)) {
|
|
25
|
+
return null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const length = Number(contentLength)
|
|
29
|
+
const expectedLength = range.end - range.start + 1
|
|
30
|
+
if (!Number.isFinite(length) || length !== expectedLength) {
|
|
31
|
+
return new RequestRetryError('Content-Length mismatch', statusCode, {
|
|
32
|
+
headers,
|
|
33
|
+
data: { count: retryCount }
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
|
|
18
40
|
class RetryHandler {
|
|
19
41
|
constructor (opts, handlers) {
|
|
20
42
|
const { retryOptions, ...dispatchOpts } = opts
|
|
@@ -68,6 +90,7 @@ class RetryHandler {
|
|
|
68
90
|
this.end = null
|
|
69
91
|
this.etag = null
|
|
70
92
|
this.resume = null
|
|
93
|
+
this.headersSent = false
|
|
71
94
|
|
|
72
95
|
// Handle possible onConnect duplication
|
|
73
96
|
this.handler.onConnect(reason => {
|
|
@@ -80,6 +103,20 @@ class RetryHandler {
|
|
|
80
103
|
})
|
|
81
104
|
}
|
|
82
105
|
|
|
106
|
+
checkpointResponseEnd (headers, resume) {
|
|
107
|
+
if (this.end == null && this.opts.method !== 'HEAD') {
|
|
108
|
+
const contentLength = headers['content-length']
|
|
109
|
+
this.end = contentLength != null ? Number(contentLength) - 1 : null
|
|
110
|
+
|
|
111
|
+
assert(
|
|
112
|
+
this.end == null || Number.isFinite(this.end),
|
|
113
|
+
'invalid content-length'
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
this.resume = this.end != null ? resume : null
|
|
118
|
+
}
|
|
119
|
+
|
|
83
120
|
onRequestSent () {
|
|
84
121
|
if (this.handler.onRequestSent) {
|
|
85
122
|
this.handler.onRequestSent()
|
|
@@ -169,6 +206,8 @@ class RetryHandler {
|
|
|
169
206
|
|
|
170
207
|
if (statusCode >= 300) {
|
|
171
208
|
if (this.retryOpts.statusCodes.includes(statusCode) === false) {
|
|
209
|
+
this.headersSent = true
|
|
210
|
+
this.checkpointResponseEnd(headers, resume)
|
|
172
211
|
return this.handler.onHeaders(
|
|
173
212
|
statusCode,
|
|
174
213
|
rawHeaders,
|
|
@@ -229,10 +268,23 @@ class RetryHandler {
|
|
|
229
268
|
return false
|
|
230
269
|
}
|
|
231
270
|
|
|
271
|
+
const contentLengthError = validatePartialResponseContentLength(headers, contentRange, statusCode, this.retryCount)
|
|
272
|
+
if (contentLengthError != null) {
|
|
273
|
+
this.abort(contentLengthError)
|
|
274
|
+
return false
|
|
275
|
+
}
|
|
276
|
+
|
|
232
277
|
const { start, size, end = size - 1 } = contentRange
|
|
233
278
|
|
|
234
|
-
|
|
235
|
-
|
|
279
|
+
if (this.start !== start || (this.end != null && this.end !== end)) {
|
|
280
|
+
this.abort(
|
|
281
|
+
new RequestRetryError('Content-Range mismatch', statusCode, {
|
|
282
|
+
headers,
|
|
283
|
+
data: { count: this.retryCount }
|
|
284
|
+
})
|
|
285
|
+
)
|
|
286
|
+
return false
|
|
287
|
+
}
|
|
236
288
|
|
|
237
289
|
this.resume = resume
|
|
238
290
|
return true
|
|
@@ -244,6 +296,7 @@ class RetryHandler {
|
|
|
244
296
|
const range = parseRangeHeader(headers['content-range'])
|
|
245
297
|
|
|
246
298
|
if (range == null) {
|
|
299
|
+
this.headersSent = true
|
|
247
300
|
return this.handler.onHeaders(
|
|
248
301
|
statusCode,
|
|
249
302
|
rawHeaders,
|
|
@@ -252,6 +305,12 @@ class RetryHandler {
|
|
|
252
305
|
)
|
|
253
306
|
}
|
|
254
307
|
|
|
308
|
+
const contentLengthError = validatePartialResponseContentLength(headers, range, statusCode, this.retryCount)
|
|
309
|
+
if (contentLengthError != null) {
|
|
310
|
+
this.abort(contentLengthError)
|
|
311
|
+
return false
|
|
312
|
+
}
|
|
313
|
+
|
|
255
314
|
const { start, size, end = size - 1 } = range
|
|
256
315
|
assert(
|
|
257
316
|
start != null && Number.isFinite(start),
|
|
@@ -276,6 +335,7 @@ class RetryHandler {
|
|
|
276
335
|
)
|
|
277
336
|
|
|
278
337
|
this.resume = resume
|
|
338
|
+
this.headersSent = true
|
|
279
339
|
this.etag = headers.etag != null ? headers.etag : null
|
|
280
340
|
|
|
281
341
|
// Weak etags are not useful for comparison nor cache
|
|
@@ -315,7 +375,7 @@ class RetryHandler {
|
|
|
315
375
|
}
|
|
316
376
|
|
|
317
377
|
onError (err) {
|
|
318
|
-
if (this.aborted || isDisturbed(this.opts.body)) {
|
|
378
|
+
if (this.aborted || isDisturbed(this.opts.body) || (this.headersSent && this.resume == null)) {
|
|
319
379
|
return this.handler.onError(err)
|
|
320
380
|
}
|
|
321
381
|
|
package/lib/web/cookies/util.js
CHANGED
|
@@ -105,7 +105,7 @@ function validateCookiePath (path) {
|
|
|
105
105
|
|
|
106
106
|
if (
|
|
107
107
|
code < 0x20 || // exclude CTLs (0-31)
|
|
108
|
-
code
|
|
108
|
+
code > 0x7E || // exclude DEL and non-ascii
|
|
109
109
|
code === 0x3B // ;
|
|
110
110
|
) {
|
|
111
111
|
throw new Error('Invalid cookie path')
|
|
@@ -114,16 +114,80 @@ function validateCookiePath (path) {
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
/**
|
|
117
|
-
*
|
|
118
|
-
*
|
|
117
|
+
* <let-dig> ::= <letter> | <digit>
|
|
118
|
+
*
|
|
119
|
+
* <letter> ::= any one of the 52 alphabetic characters A through Z in
|
|
120
|
+
* upper case and a through z in lower case
|
|
121
|
+
*
|
|
122
|
+
* <digit> ::= any one of the ten digits 0 through 9r
|
|
123
|
+
*
|
|
124
|
+
* @see https://www.rfc-editor.org/rfc/rfc1034#section-3.5
|
|
125
|
+
* @param {number} code
|
|
126
|
+
*/
|
|
127
|
+
function isLetterOrDigit (code) {
|
|
128
|
+
return (
|
|
129
|
+
(code >= 0x30 && code <= 0x39) || // 0-9
|
|
130
|
+
(code >= 0x41 && code <= 0x5A) || // A-Z
|
|
131
|
+
(code >= 0x61 && code <= 0x7A) // a-z
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Validates a cookie domain against the "preferred name syntax".
|
|
137
|
+
*
|
|
138
|
+
* <domain> ::= <subdomain> | " "
|
|
139
|
+
* <subdomain> ::= <label> | <subdomain> "." <label>
|
|
140
|
+
* <label> ::= <let-dig> [ [ <ldh-str> ] <let-dig> ]
|
|
141
|
+
* <ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
|
|
142
|
+
* <let-dig-hyp> ::= <let-dig> | "-"
|
|
143
|
+
*
|
|
144
|
+
* @see https://www.rfc-editor.org/rfc/rfc1034#section-3.5
|
|
145
|
+
* @see https://www.rfc-editor.org/rfc/rfc1123#section-2.1
|
|
146
|
+
* @see https://www.rfc-editor.org/rfc/rfc1035#section-2.3.4
|
|
119
147
|
* @param {string} domain
|
|
120
148
|
*/
|
|
121
149
|
function validateCookieDomain (domain) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
150
|
+
// <domain> ::= <subdomain> | " "
|
|
151
|
+
if (domain === ' ') {
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (domain.length > 255) {
|
|
156
|
+
throw new Error('Invalid cookie domain')
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
let labelLength = 0
|
|
160
|
+
|
|
161
|
+
for (let i = 0; i < domain.length; ++i) {
|
|
162
|
+
const code = domain.charCodeAt(i)
|
|
163
|
+
|
|
164
|
+
if (code === 0x2E) {
|
|
165
|
+
if (labelLength === 0) {
|
|
166
|
+
throw new Error('Invalid cookie domain')
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (domain.charCodeAt(i - 1) === 0x2D) { // "-"
|
|
170
|
+
throw new Error('Invalid cookie domain')
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
labelLength = 0
|
|
174
|
+
continue
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (labelLength === 0 && !isLetterOrDigit(code)) {
|
|
178
|
+
throw new Error('Invalid cookie domain')
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!isLetterOrDigit(code) && code !== 0x2D) { // "-"
|
|
182
|
+
throw new Error('Invalid cookie domain')
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (++labelLength > 63) {
|
|
186
|
+
throw new Error('Invalid cookie domain')
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (labelLength === 0 || domain.charCodeAt(domain.length - 1) === 0x2D) { // "-"
|
|
127
191
|
throw new Error('Invalid cookie domain')
|
|
128
192
|
}
|
|
129
193
|
}
|
|
@@ -266,7 +330,13 @@ function stringify (cookie) {
|
|
|
266
330
|
|
|
267
331
|
const [key, ...value] = part.split('=')
|
|
268
332
|
|
|
269
|
-
|
|
333
|
+
const trimmedKey = key.trim()
|
|
334
|
+
const joinedValue = value.join('=')
|
|
335
|
+
|
|
336
|
+
validateCookieName(trimmedKey)
|
|
337
|
+
validateCookieValue(joinedValue)
|
|
338
|
+
|
|
339
|
+
out.push(`${trimmedKey}=${joinedValue}`)
|
|
270
340
|
}
|
|
271
341
|
|
|
272
342
|
return out.join('; ')
|
|
@@ -23,6 +23,49 @@ const COLON = 0x3A
|
|
|
23
23
|
*/
|
|
24
24
|
const SPACE = 0x20
|
|
25
25
|
|
|
26
|
+
const DATA = Buffer.from('data')
|
|
27
|
+
const EVENT = Buffer.from('event')
|
|
28
|
+
const ID = Buffer.from('id')
|
|
29
|
+
const RETRY = Buffer.from('retry')
|
|
30
|
+
|
|
31
|
+
function isASCIINumberBytes (buffer, start) {
|
|
32
|
+
if (start >= buffer.length) {
|
|
33
|
+
return false
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (let i = start; i < buffer.length; i++) {
|
|
37
|
+
if (buffer[i] < 0x30 || buffer[i] > 0x39) {
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return true
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isValidLastEventIdBytes (buffer, start) {
|
|
46
|
+
for (let i = start; i < buffer.length; i++) {
|
|
47
|
+
if (buffer[i] === 0x00) {
|
|
48
|
+
return false
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return true
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isFieldName (line, length, field) {
|
|
56
|
+
if (length !== field.length) {
|
|
57
|
+
return false
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (let i = 0; i < length; i++) {
|
|
61
|
+
if (line[i] !== field[i]) {
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return true
|
|
67
|
+
}
|
|
68
|
+
|
|
26
69
|
/**
|
|
27
70
|
* @typedef {object} EventSourceStreamEvent
|
|
28
71
|
* @type {object}
|
|
@@ -63,11 +106,14 @@ class EventSourceStream extends Transform {
|
|
|
63
106
|
eventEndCheck = false
|
|
64
107
|
|
|
65
108
|
/**
|
|
66
|
-
* @type {Buffer}
|
|
109
|
+
* @type {Buffer[]}
|
|
67
110
|
*/
|
|
68
|
-
|
|
111
|
+
chunks = []
|
|
69
112
|
|
|
113
|
+
chunkIndex = 0
|
|
70
114
|
pos = 0
|
|
115
|
+
lineChunkIndex = 0
|
|
116
|
+
linePos = 0
|
|
71
117
|
|
|
72
118
|
event = {
|
|
73
119
|
data: undefined,
|
|
@@ -106,92 +152,20 @@ class EventSourceStream extends Transform {
|
|
|
106
152
|
return
|
|
107
153
|
}
|
|
108
154
|
|
|
109
|
-
|
|
110
|
-
// processing it
|
|
111
|
-
// TODO: Investigate if there is a more performant way to handle
|
|
112
|
-
// incoming chunks
|
|
113
|
-
// see: https://github.com/nodejs/undici/issues/2630
|
|
114
|
-
if (this.buffer) {
|
|
115
|
-
this.buffer = Buffer.concat([this.buffer, chunk])
|
|
116
|
-
} else {
|
|
117
|
-
this.buffer = chunk
|
|
118
|
-
}
|
|
155
|
+
this.chunks.push(chunk)
|
|
119
156
|
|
|
120
157
|
// Strip leading byte-order-mark if we opened the stream and started
|
|
121
158
|
// the processing of the incoming data
|
|
122
159
|
if (this.checkBOM) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (this.buffer[0] === BOM[0]) {
|
|
127
|
-
// If it is, we need to wait for more data
|
|
128
|
-
callback()
|
|
129
|
-
return
|
|
130
|
-
}
|
|
131
|
-
// Set the checkBOM flag to false as we don't need to check for the
|
|
132
|
-
// BOM anymore
|
|
133
|
-
this.checkBOM = false
|
|
134
|
-
|
|
135
|
-
// The buffer only contains one byte so we need to wait for more data
|
|
136
|
-
callback()
|
|
137
|
-
return
|
|
138
|
-
case 2:
|
|
139
|
-
// Check if the first two bytes are the same as the first two bytes
|
|
140
|
-
// of the BOM
|
|
141
|
-
if (
|
|
142
|
-
this.buffer[0] === BOM[0] &&
|
|
143
|
-
this.buffer[1] === BOM[1]
|
|
144
|
-
) {
|
|
145
|
-
// If it is, we need to wait for more data, because the third byte
|
|
146
|
-
// is needed to determine if it is the BOM or not
|
|
147
|
-
callback()
|
|
148
|
-
return
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Set the checkBOM flag to false as we don't need to check for the
|
|
152
|
-
// BOM anymore
|
|
153
|
-
this.checkBOM = false
|
|
154
|
-
break
|
|
155
|
-
case 3:
|
|
156
|
-
// Check if the first three bytes are the same as the first three
|
|
157
|
-
// bytes of the BOM
|
|
158
|
-
if (
|
|
159
|
-
this.buffer[0] === BOM[0] &&
|
|
160
|
-
this.buffer[1] === BOM[1] &&
|
|
161
|
-
this.buffer[2] === BOM[2]
|
|
162
|
-
) {
|
|
163
|
-
// If it is, we can drop the buffered data, as it is only the BOM
|
|
164
|
-
this.buffer = Buffer.alloc(0)
|
|
165
|
-
// Set the checkBOM flag to false as we don't need to check for the
|
|
166
|
-
// BOM anymore
|
|
167
|
-
this.checkBOM = false
|
|
168
|
-
|
|
169
|
-
// Await more data
|
|
170
|
-
callback()
|
|
171
|
-
return
|
|
172
|
-
}
|
|
173
|
-
// If it is not the BOM, we can start processing the data
|
|
174
|
-
this.checkBOM = false
|
|
175
|
-
break
|
|
176
|
-
default:
|
|
177
|
-
// The buffer is longer than 3 bytes, so we can drop the BOM if it is
|
|
178
|
-
// present
|
|
179
|
-
if (
|
|
180
|
-
this.buffer[0] === BOM[0] &&
|
|
181
|
-
this.buffer[1] === BOM[1] &&
|
|
182
|
-
this.buffer[2] === BOM[2]
|
|
183
|
-
) {
|
|
184
|
-
// Remove the BOM from the buffer
|
|
185
|
-
this.buffer = this.buffer.subarray(3)
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Set the checkBOM flag to false as we don't need to check for the
|
|
189
|
-
this.checkBOM = false
|
|
190
|
-
break
|
|
160
|
+
if (this.handleBOM()) {
|
|
161
|
+
callback()
|
|
162
|
+
return
|
|
191
163
|
}
|
|
192
164
|
}
|
|
193
165
|
|
|
194
|
-
while (this.
|
|
166
|
+
while (this.hasCurrentByte()) {
|
|
167
|
+
const byte = this.currentByte()
|
|
168
|
+
|
|
195
169
|
// If the previous line ended with an end-of-line, we need to check
|
|
196
170
|
// if the next character is also an end-of-line.
|
|
197
171
|
if (this.eventEndCheck) {
|
|
@@ -204,10 +178,9 @@ class EventSourceStream extends Transform {
|
|
|
204
178
|
if (this.crlfCheck) {
|
|
205
179
|
// If the current character is a line feed, we can remove it
|
|
206
180
|
// from the buffer and reset the crlfCheck flag
|
|
207
|
-
if (
|
|
208
|
-
this.buffer = this.buffer.subarray(this.pos + 1)
|
|
209
|
-
this.pos = 0
|
|
181
|
+
if (byte === LF) {
|
|
210
182
|
this.crlfCheck = false
|
|
183
|
+
this.consumeCurrentByte()
|
|
211
184
|
|
|
212
185
|
// It is possible that the line feed is not the end of the
|
|
213
186
|
// event. We need to check if the next character is an
|
|
@@ -223,19 +196,17 @@ class EventSourceStream extends Transform {
|
|
|
223
196
|
this.crlfCheck = false
|
|
224
197
|
}
|
|
225
198
|
|
|
226
|
-
if (
|
|
199
|
+
if (byte === LF || byte === CR) {
|
|
227
200
|
// If the current character is a carriage return, we need to
|
|
228
201
|
// set the crlfCheck flag to true, as we need to check if the
|
|
229
202
|
// next character is a line feed so we can remove it from the
|
|
230
203
|
// buffer
|
|
231
|
-
if (
|
|
204
|
+
if (byte === CR) {
|
|
232
205
|
this.crlfCheck = true
|
|
233
206
|
}
|
|
234
207
|
|
|
235
|
-
this.
|
|
236
|
-
this.
|
|
237
|
-
if (
|
|
238
|
-
this.event.data !== undefined || this.event.event || this.event.id || this.event.retry) {
|
|
208
|
+
this.consumeCurrentByte()
|
|
209
|
+
if (this.hasPendingEvent()) {
|
|
239
210
|
this.processEvent(this.event)
|
|
240
211
|
}
|
|
241
212
|
this.clearEvent()
|
|
@@ -249,22 +220,18 @@ class EventSourceStream extends Transform {
|
|
|
249
220
|
|
|
250
221
|
// If the current character is an end-of-line, we can process the
|
|
251
222
|
// line
|
|
252
|
-
if (
|
|
223
|
+
if (byte === LF || byte === CR) {
|
|
253
224
|
// If the current character is a carriage return, we need to
|
|
254
225
|
// set the crlfCheck flag to true, as we need to check if the
|
|
255
226
|
// next character is a line feed
|
|
256
|
-
if (
|
|
227
|
+
if (byte === CR) {
|
|
257
228
|
this.crlfCheck = true
|
|
258
229
|
}
|
|
259
230
|
|
|
260
231
|
// In any case, we can process the line as we reached an
|
|
261
232
|
// end-of-line character
|
|
262
|
-
this.parseLine(this.
|
|
263
|
-
|
|
264
|
-
// Remove the processed line from the buffer
|
|
265
|
-
this.buffer = this.buffer.subarray(this.pos + 1)
|
|
266
|
-
// Reset the position as we removed the processed line from the buffer
|
|
267
|
-
this.pos = 0
|
|
233
|
+
this.parseLine(this.readLine(), this.event)
|
|
234
|
+
this.consumeCurrentByte()
|
|
268
235
|
// A line was processed and this could be the end of the event. We need
|
|
269
236
|
// to check if the next line is empty to determine if the event is
|
|
270
237
|
// finished.
|
|
@@ -272,7 +239,7 @@ class EventSourceStream extends Transform {
|
|
|
272
239
|
continue
|
|
273
240
|
}
|
|
274
241
|
|
|
275
|
-
this.
|
|
242
|
+
this.advanceCursor()
|
|
276
243
|
}
|
|
277
244
|
|
|
278
245
|
callback()
|
|
@@ -297,64 +264,53 @@ class EventSourceStream extends Transform {
|
|
|
297
264
|
return
|
|
298
265
|
}
|
|
299
266
|
|
|
300
|
-
let
|
|
301
|
-
let
|
|
267
|
+
let fieldLength = line.length
|
|
268
|
+
let valueStart = line.length
|
|
302
269
|
|
|
303
270
|
// If the line contains a U+003A COLON character (:)
|
|
304
271
|
if (colonPosition !== -1) {
|
|
305
|
-
|
|
306
|
-
// character (:), and let field be that string.
|
|
307
|
-
// TODO: Investigate if there is a more performant way to extract the
|
|
308
|
-
// field
|
|
309
|
-
// see: https://github.com/nodejs/undici/issues/2630
|
|
310
|
-
field = line.subarray(0, colonPosition).toString('utf8')
|
|
272
|
+
fieldLength = colonPosition
|
|
311
273
|
|
|
312
274
|
// Collect the characters on the line after the first U+003A COLON
|
|
313
275
|
// character (:), and let value be that string.
|
|
314
276
|
// If value starts with a U+0020 SPACE character, remove it from value.
|
|
315
|
-
|
|
277
|
+
valueStart = colonPosition + 1
|
|
316
278
|
if (line[valueStart] === SPACE) {
|
|
317
279
|
++valueStart
|
|
318
280
|
}
|
|
319
|
-
// TODO: Investigate if there is a more performant way to extract the
|
|
320
|
-
// value
|
|
321
|
-
// see: https://github.com/nodejs/undici/issues/2630
|
|
322
|
-
value = line.subarray(valueStart).toString('utf8')
|
|
323
|
-
|
|
324
|
-
// Otherwise, the string is not empty but does not contain a U+003A COLON
|
|
325
|
-
// character (:)
|
|
326
|
-
} else {
|
|
327
|
-
// Process the field using the steps described below, using the whole
|
|
328
|
-
// line as the field name, and the empty string as the field value.
|
|
329
|
-
field = line.toString('utf8')
|
|
330
|
-
value = ''
|
|
331
281
|
}
|
|
332
282
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
283
|
+
if (isFieldName(line, fieldLength, DATA)) {
|
|
284
|
+
const value = line.toString('utf8', valueStart)
|
|
285
|
+
|
|
286
|
+
if (event.data === undefined) {
|
|
287
|
+
event.data = value
|
|
288
|
+
} else {
|
|
289
|
+
event.data += `\n${value}`
|
|
290
|
+
}
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (isFieldName(line, fieldLength, RETRY)) {
|
|
295
|
+
if (isASCIINumberBytes(line, valueStart)) {
|
|
296
|
+
event.retry = line.toString('utf8', valueStart)
|
|
297
|
+
}
|
|
298
|
+
return
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (isFieldName(line, fieldLength, ID)) {
|
|
302
|
+
if (isValidLastEventIdBytes(line, valueStart)) {
|
|
303
|
+
event.id = line.toString('utf8', valueStart)
|
|
304
|
+
}
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (isFieldName(line, fieldLength, EVENT)) {
|
|
309
|
+
const value = line.toString('utf8', valueStart)
|
|
310
|
+
|
|
311
|
+
if (value.length > 0) {
|
|
312
|
+
event.event = value
|
|
313
|
+
}
|
|
358
314
|
}
|
|
359
315
|
}
|
|
360
316
|
|
|
@@ -384,12 +340,151 @@ class EventSourceStream extends Transform {
|
|
|
384
340
|
}
|
|
385
341
|
|
|
386
342
|
clearEvent () {
|
|
387
|
-
this.event =
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
343
|
+
this.event.data = undefined
|
|
344
|
+
this.event.event = undefined
|
|
345
|
+
this.event.id = undefined
|
|
346
|
+
this.event.retry = undefined
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
hasPendingEvent () {
|
|
350
|
+
return this.event.data !== undefined ||
|
|
351
|
+
this.event.event !== undefined ||
|
|
352
|
+
this.event.id !== undefined ||
|
|
353
|
+
this.event.retry !== undefined
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
hasCurrentByte () {
|
|
357
|
+
return this.chunkIndex < this.chunks.length &&
|
|
358
|
+
this.pos < this.chunks[this.chunkIndex].length
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
currentByte () {
|
|
362
|
+
return this.chunks[this.chunkIndex][this.pos]
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
consumeCurrentByte () {
|
|
366
|
+
this.advanceCursor()
|
|
367
|
+
this.syncLineStartToCursor()
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
advanceCursor () {
|
|
371
|
+
this.pos++
|
|
372
|
+
|
|
373
|
+
while (this.chunkIndex < this.chunks.length && this.pos >= this.chunks[this.chunkIndex].length) {
|
|
374
|
+
this.chunkIndex++
|
|
375
|
+
this.pos = 0
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
syncLineStartToCursor () {
|
|
380
|
+
this.lineChunkIndex = this.chunkIndex
|
|
381
|
+
this.linePos = this.pos
|
|
382
|
+
this.dropConsumedChunks()
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
dropConsumedChunks () {
|
|
386
|
+
while (this.lineChunkIndex > 0) {
|
|
387
|
+
this.chunks.shift()
|
|
388
|
+
this.lineChunkIndex--
|
|
389
|
+
this.chunkIndex--
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (this.chunkIndex === this.chunks.length) {
|
|
393
|
+
this.chunks.length = 0
|
|
394
|
+
this.chunkIndex = 0
|
|
395
|
+
this.pos = 0
|
|
396
|
+
this.lineChunkIndex = 0
|
|
397
|
+
this.linePos = 0
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
readLine () {
|
|
402
|
+
if (this.lineChunkIndex === this.chunkIndex) {
|
|
403
|
+
return this.chunks[this.chunkIndex].subarray(this.linePos, this.pos)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const chunks = []
|
|
407
|
+
let length = 0
|
|
408
|
+
|
|
409
|
+
for (let i = this.lineChunkIndex; i <= this.chunkIndex; i++) {
|
|
410
|
+
const chunk = this.chunks[i]
|
|
411
|
+
const start = i === this.lineChunkIndex ? this.linePos : 0
|
|
412
|
+
const end = i === this.chunkIndex ? this.pos : chunk.length
|
|
413
|
+
const slice = chunk.subarray(start, end)
|
|
414
|
+
length += slice.length
|
|
415
|
+
chunks.push(slice)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return Buffer.concat(chunks, length)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
peekBufferedByte (offset) {
|
|
422
|
+
let chunkIndex = this.lineChunkIndex
|
|
423
|
+
let pos = this.linePos
|
|
424
|
+
|
|
425
|
+
while (chunkIndex < this.chunks.length) {
|
|
426
|
+
const chunk = this.chunks[chunkIndex]
|
|
427
|
+
const remaining = chunk.length - pos
|
|
428
|
+
|
|
429
|
+
if (offset < remaining) {
|
|
430
|
+
return chunk[pos + offset]
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
offset -= remaining
|
|
434
|
+
chunkIndex++
|
|
435
|
+
pos = 0
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
discardLeadingBytes (count) {
|
|
440
|
+
while (count > 0 && this.lineChunkIndex < this.chunks.length) {
|
|
441
|
+
const chunk = this.chunks[this.lineChunkIndex]
|
|
442
|
+
const remaining = chunk.length - this.linePos
|
|
443
|
+
|
|
444
|
+
if (count < remaining) {
|
|
445
|
+
this.linePos += count
|
|
446
|
+
count = 0
|
|
447
|
+
} else {
|
|
448
|
+
count -= remaining
|
|
449
|
+
this.lineChunkIndex++
|
|
450
|
+
this.linePos = 0
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
this.chunkIndex = this.lineChunkIndex
|
|
455
|
+
this.pos = this.linePos
|
|
456
|
+
this.dropConsumedChunks()
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
handleBOM () {
|
|
460
|
+
const first = this.peekBufferedByte(0)
|
|
461
|
+
const second = this.peekBufferedByte(1)
|
|
462
|
+
const third = this.peekBufferedByte(2)
|
|
463
|
+
|
|
464
|
+
if (second === undefined) {
|
|
465
|
+
if (first === BOM[0]) {
|
|
466
|
+
return true
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
this.checkBOM = false
|
|
470
|
+
return true
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (third === undefined) {
|
|
474
|
+
if (first === BOM[0] && second === BOM[1]) {
|
|
475
|
+
return true
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
this.checkBOM = false
|
|
479
|
+
return false
|
|
392
480
|
}
|
|
481
|
+
|
|
482
|
+
if (first === BOM[0] && second === BOM[1] && third === BOM[2]) {
|
|
483
|
+
this.discardLeadingBytes(3)
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
this.checkBOM = false
|
|
487
|
+
return !this.hasCurrentByte()
|
|
393
488
|
}
|
|
394
489
|
}
|
|
395
490
|
|
|
@@ -192,7 +192,7 @@ function establishWebSocketConnection (url, protocols, client, ws, onEstablish,
|
|
|
192
192
|
// is specified, the server needs to include the same field and one of
|
|
193
193
|
// the selected subprotocol values in its response for the connection to
|
|
194
194
|
// be established.
|
|
195
|
-
if (!requestProtocols.includes(secProtocol)) {
|
|
195
|
+
if (requestProtocols === null || !requestProtocols.includes(secProtocol)) {
|
|
196
196
|
failWebsocketConnection(ws, 'Protocol was not set in the opening handshake.')
|
|
197
197
|
return
|
|
198
198
|
}
|
|
@@ -63,7 +63,12 @@ class PerMessageDeflate {
|
|
|
63
63
|
|
|
64
64
|
if (this.#maxPayloadSize > 0 && this.#inflate[kLength] > this.#maxPayloadSize) {
|
|
65
65
|
callback(new MessageSizeExceededError())
|
|
66
|
+
// The inflater may still hold buffered input that can emit a late
|
|
67
|
+
// zlib error. Remove the data listener, then deterministically stop
|
|
68
|
+
// the stream so a subsequent 'error' cannot fire without a listener
|
|
69
|
+
// (which would terminate the process as an unhandled error event).
|
|
66
70
|
this.#inflate.removeAllListeners()
|
|
71
|
+
this.#inflate.destroy()
|
|
67
72
|
this.#inflate = null
|
|
68
73
|
return
|
|
69
74
|
}
|