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
|
@@ -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,
|
|
@@ -107,92 +153,20 @@ class EventSourceStream extends Transform {
|
|
|
107
153
|
return
|
|
108
154
|
}
|
|
109
155
|
|
|
110
|
-
|
|
111
|
-
// processing it
|
|
112
|
-
// TODO: Investigate if there is a more performant way to handle
|
|
113
|
-
// incoming chunks
|
|
114
|
-
// see: https://github.com/nodejs/undici/issues/2630
|
|
115
|
-
if (this.buffer) {
|
|
116
|
-
this.buffer = Buffer.concat([this.buffer, chunk])
|
|
117
|
-
} else {
|
|
118
|
-
this.buffer = chunk
|
|
119
|
-
}
|
|
156
|
+
this.chunks.push(chunk)
|
|
120
157
|
|
|
121
158
|
// Strip leading byte-order-mark if we opened the stream and started
|
|
122
159
|
// the processing of the incoming data
|
|
123
160
|
if (this.checkBOM) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (this.buffer[0] === BOM[0]) {
|
|
128
|
-
// If it is, we need to wait for more data
|
|
129
|
-
callback()
|
|
130
|
-
return
|
|
131
|
-
}
|
|
132
|
-
// Set the checkBOM flag to false as we don't need to check for the
|
|
133
|
-
// BOM anymore
|
|
134
|
-
this.checkBOM = false
|
|
135
|
-
|
|
136
|
-
// The buffer only contains one byte so we need to wait for more data
|
|
137
|
-
callback()
|
|
138
|
-
return
|
|
139
|
-
case 2:
|
|
140
|
-
// Check if the first two bytes are the same as the first two bytes
|
|
141
|
-
// of the BOM
|
|
142
|
-
if (
|
|
143
|
-
this.buffer[0] === BOM[0] &&
|
|
144
|
-
this.buffer[1] === BOM[1]
|
|
145
|
-
) {
|
|
146
|
-
// If it is, we need to wait for more data, because the third byte
|
|
147
|
-
// is needed to determine if it is the BOM or not
|
|
148
|
-
callback()
|
|
149
|
-
return
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Set the checkBOM flag to false as we don't need to check for the
|
|
153
|
-
// BOM anymore
|
|
154
|
-
this.checkBOM = false
|
|
155
|
-
break
|
|
156
|
-
case 3:
|
|
157
|
-
// Check if the first three bytes are the same as the first three
|
|
158
|
-
// bytes of the BOM
|
|
159
|
-
if (
|
|
160
|
-
this.buffer[0] === BOM[0] &&
|
|
161
|
-
this.buffer[1] === BOM[1] &&
|
|
162
|
-
this.buffer[2] === BOM[2]
|
|
163
|
-
) {
|
|
164
|
-
// If it is, we can drop the buffered data, as it is only the BOM
|
|
165
|
-
this.buffer = Buffer.alloc(0)
|
|
166
|
-
// Set the checkBOM flag to false as we don't need to check for the
|
|
167
|
-
// BOM anymore
|
|
168
|
-
this.checkBOM = false
|
|
169
|
-
|
|
170
|
-
// Await more data
|
|
171
|
-
callback()
|
|
172
|
-
return
|
|
173
|
-
}
|
|
174
|
-
// If it is not the BOM, we can start processing the data
|
|
175
|
-
this.checkBOM = false
|
|
176
|
-
break
|
|
177
|
-
default:
|
|
178
|
-
// The buffer is longer than 3 bytes, so we can drop the BOM if it is
|
|
179
|
-
// present
|
|
180
|
-
if (
|
|
181
|
-
this.buffer[0] === BOM[0] &&
|
|
182
|
-
this.buffer[1] === BOM[1] &&
|
|
183
|
-
this.buffer[2] === BOM[2]
|
|
184
|
-
) {
|
|
185
|
-
// Remove the BOM from the buffer
|
|
186
|
-
this.buffer = this.buffer.subarray(3)
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Set the checkBOM flag to false as we don't need to check for the
|
|
190
|
-
this.checkBOM = false
|
|
191
|
-
break
|
|
161
|
+
if (this.handleBOM()) {
|
|
162
|
+
callback()
|
|
163
|
+
return
|
|
192
164
|
}
|
|
193
165
|
}
|
|
194
166
|
|
|
195
|
-
while (this.
|
|
167
|
+
while (this.hasCurrentByte()) {
|
|
168
|
+
const byte = this.currentByte()
|
|
169
|
+
|
|
196
170
|
// If the previous line ended with an end-of-line, we need to check
|
|
197
171
|
// if the next character is also an end-of-line.
|
|
198
172
|
if (this.eventEndCheck) {
|
|
@@ -205,10 +179,9 @@ class EventSourceStream extends Transform {
|
|
|
205
179
|
if (this.crlfCheck) {
|
|
206
180
|
// If the current character is a line feed, we can remove it
|
|
207
181
|
// from the buffer and reset the crlfCheck flag
|
|
208
|
-
if (
|
|
209
|
-
this.buffer = this.buffer.subarray(this.pos + 1)
|
|
210
|
-
this.pos = 0
|
|
182
|
+
if (byte === LF) {
|
|
211
183
|
this.crlfCheck = false
|
|
184
|
+
this.consumeCurrentByte()
|
|
212
185
|
|
|
213
186
|
// It is possible that the line feed is not the end of the
|
|
214
187
|
// event. We need to check if the next character is an
|
|
@@ -224,19 +197,17 @@ class EventSourceStream extends Transform {
|
|
|
224
197
|
this.crlfCheck = false
|
|
225
198
|
}
|
|
226
199
|
|
|
227
|
-
if (
|
|
200
|
+
if (byte === LF || byte === CR) {
|
|
228
201
|
// If the current character is a carriage return, we need to
|
|
229
202
|
// set the crlfCheck flag to true, as we need to check if the
|
|
230
203
|
// next character is a line feed so we can remove it from the
|
|
231
204
|
// buffer
|
|
232
|
-
if (
|
|
205
|
+
if (byte === CR) {
|
|
233
206
|
this.crlfCheck = true
|
|
234
207
|
}
|
|
235
208
|
|
|
236
|
-
this.
|
|
237
|
-
this.
|
|
238
|
-
if (
|
|
239
|
-
this.event.data !== undefined || this.event.event || this.event.id !== undefined || this.event.retry) {
|
|
209
|
+
this.consumeCurrentByte()
|
|
210
|
+
if (this.hasPendingEvent()) {
|
|
240
211
|
this.processEvent(this.event)
|
|
241
212
|
}
|
|
242
213
|
this.clearEvent()
|
|
@@ -250,22 +221,18 @@ class EventSourceStream extends Transform {
|
|
|
250
221
|
|
|
251
222
|
// If the current character is an end-of-line, we can process the
|
|
252
223
|
// line
|
|
253
|
-
if (
|
|
224
|
+
if (byte === LF || byte === CR) {
|
|
254
225
|
// If the current character is a carriage return, we need to
|
|
255
226
|
// set the crlfCheck flag to true, as we need to check if the
|
|
256
227
|
// next character is a line feed
|
|
257
|
-
if (
|
|
228
|
+
if (byte === CR) {
|
|
258
229
|
this.crlfCheck = true
|
|
259
230
|
}
|
|
260
231
|
|
|
261
232
|
// In any case, we can process the line as we reached an
|
|
262
233
|
// end-of-line character
|
|
263
|
-
this.parseLine(this.
|
|
264
|
-
|
|
265
|
-
// Remove the processed line from the buffer
|
|
266
|
-
this.buffer = this.buffer.subarray(this.pos + 1)
|
|
267
|
-
// Reset the position as we removed the processed line from the buffer
|
|
268
|
-
this.pos = 0
|
|
234
|
+
this.parseLine(this.readLine(), this.event)
|
|
235
|
+
this.consumeCurrentByte()
|
|
269
236
|
// A line was processed and this could be the end of the event. We need
|
|
270
237
|
// to check if the next line is empty to determine if the event is
|
|
271
238
|
// finished.
|
|
@@ -273,7 +240,7 @@ class EventSourceStream extends Transform {
|
|
|
273
240
|
continue
|
|
274
241
|
}
|
|
275
242
|
|
|
276
|
-
this.
|
|
243
|
+
this.advanceCursor()
|
|
277
244
|
}
|
|
278
245
|
|
|
279
246
|
callback()
|
|
@@ -298,64 +265,53 @@ class EventSourceStream extends Transform {
|
|
|
298
265
|
return
|
|
299
266
|
}
|
|
300
267
|
|
|
301
|
-
let
|
|
302
|
-
let
|
|
268
|
+
let fieldLength = line.length
|
|
269
|
+
let valueStart = line.length
|
|
303
270
|
|
|
304
271
|
// If the line contains a U+003A COLON character (:)
|
|
305
272
|
if (colonPosition !== -1) {
|
|
306
|
-
|
|
307
|
-
// character (:), and let field be that string.
|
|
308
|
-
// TODO: Investigate if there is a more performant way to extract the
|
|
309
|
-
// field
|
|
310
|
-
// see: https://github.com/nodejs/undici/issues/2630
|
|
311
|
-
field = line.subarray(0, colonPosition).toString('utf8')
|
|
273
|
+
fieldLength = colonPosition
|
|
312
274
|
|
|
313
275
|
// Collect the characters on the line after the first U+003A COLON
|
|
314
276
|
// character (:), and let value be that string.
|
|
315
277
|
// If value starts with a U+0020 SPACE character, remove it from value.
|
|
316
|
-
|
|
278
|
+
valueStart = colonPosition + 1
|
|
317
279
|
if (line[valueStart] === SPACE) {
|
|
318
280
|
++valueStart
|
|
319
281
|
}
|
|
320
|
-
// TODO: Investigate if there is a more performant way to extract the
|
|
321
|
-
// value
|
|
322
|
-
// see: https://github.com/nodejs/undici/issues/2630
|
|
323
|
-
value = line.subarray(valueStart).toString('utf8')
|
|
324
|
-
|
|
325
|
-
// Otherwise, the string is not empty but does not contain a U+003A COLON
|
|
326
|
-
// character (:)
|
|
327
|
-
} else {
|
|
328
|
-
// Process the field using the steps described below, using the whole
|
|
329
|
-
// line as the field name, and the empty string as the field value.
|
|
330
|
-
field = line.toString('utf8')
|
|
331
|
-
value = ''
|
|
332
282
|
}
|
|
333
283
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
284
|
+
if (isFieldName(line, fieldLength, DATA)) {
|
|
285
|
+
const value = line.toString('utf8', valueStart)
|
|
286
|
+
|
|
287
|
+
if (event.data === undefined) {
|
|
288
|
+
event.data = value
|
|
289
|
+
} else {
|
|
290
|
+
event.data += `\n${value}`
|
|
291
|
+
}
|
|
292
|
+
return
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (isFieldName(line, fieldLength, RETRY)) {
|
|
296
|
+
if (isASCIINumberBytes(line, valueStart)) {
|
|
297
|
+
event.retry = line.toString('utf8', valueStart)
|
|
298
|
+
}
|
|
299
|
+
return
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (isFieldName(line, fieldLength, ID)) {
|
|
303
|
+
if (isValidLastEventIdBytes(line, valueStart)) {
|
|
304
|
+
event.id = line.toString('utf8', valueStart)
|
|
305
|
+
}
|
|
306
|
+
return
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (isFieldName(line, fieldLength, EVENT)) {
|
|
310
|
+
const value = line.toString('utf8', valueStart)
|
|
311
|
+
|
|
312
|
+
if (value.length > 0) {
|
|
313
|
+
event.event = value
|
|
314
|
+
}
|
|
359
315
|
}
|
|
360
316
|
}
|
|
361
317
|
|
|
@@ -385,12 +341,151 @@ class EventSourceStream extends Transform {
|
|
|
385
341
|
}
|
|
386
342
|
|
|
387
343
|
clearEvent () {
|
|
388
|
-
this.event =
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
344
|
+
this.event.data = undefined
|
|
345
|
+
this.event.event = undefined
|
|
346
|
+
this.event.id = undefined
|
|
347
|
+
this.event.retry = undefined
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
hasPendingEvent () {
|
|
351
|
+
return this.event.data !== undefined ||
|
|
352
|
+
this.event.event !== undefined ||
|
|
353
|
+
this.event.id !== undefined ||
|
|
354
|
+
this.event.retry !== undefined
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
hasCurrentByte () {
|
|
358
|
+
return this.chunkIndex < this.chunks.length &&
|
|
359
|
+
this.pos < this.chunks[this.chunkIndex].length
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
currentByte () {
|
|
363
|
+
return this.chunks[this.chunkIndex][this.pos]
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
consumeCurrentByte () {
|
|
367
|
+
this.advanceCursor()
|
|
368
|
+
this.syncLineStartToCursor()
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
advanceCursor () {
|
|
372
|
+
this.pos++
|
|
373
|
+
|
|
374
|
+
while (this.chunkIndex < this.chunks.length && this.pos >= this.chunks[this.chunkIndex].length) {
|
|
375
|
+
this.chunkIndex++
|
|
376
|
+
this.pos = 0
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
syncLineStartToCursor () {
|
|
381
|
+
this.lineChunkIndex = this.chunkIndex
|
|
382
|
+
this.linePos = this.pos
|
|
383
|
+
this.dropConsumedChunks()
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
dropConsumedChunks () {
|
|
387
|
+
while (this.lineChunkIndex > 0) {
|
|
388
|
+
this.chunks.shift()
|
|
389
|
+
this.lineChunkIndex--
|
|
390
|
+
this.chunkIndex--
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (this.chunkIndex === this.chunks.length) {
|
|
394
|
+
this.chunks.length = 0
|
|
395
|
+
this.chunkIndex = 0
|
|
396
|
+
this.pos = 0
|
|
397
|
+
this.lineChunkIndex = 0
|
|
398
|
+
this.linePos = 0
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
readLine () {
|
|
403
|
+
if (this.lineChunkIndex === this.chunkIndex) {
|
|
404
|
+
return this.chunks[this.chunkIndex].subarray(this.linePos, this.pos)
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const chunks = []
|
|
408
|
+
let length = 0
|
|
409
|
+
|
|
410
|
+
for (let i = this.lineChunkIndex; i <= this.chunkIndex; i++) {
|
|
411
|
+
const chunk = this.chunks[i]
|
|
412
|
+
const start = i === this.lineChunkIndex ? this.linePos : 0
|
|
413
|
+
const end = i === this.chunkIndex ? this.pos : chunk.length
|
|
414
|
+
const slice = chunk.subarray(start, end)
|
|
415
|
+
length += slice.length
|
|
416
|
+
chunks.push(slice)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return Buffer.concat(chunks, length)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
peekBufferedByte (offset) {
|
|
423
|
+
let chunkIndex = this.lineChunkIndex
|
|
424
|
+
let pos = this.linePos
|
|
425
|
+
|
|
426
|
+
while (chunkIndex < this.chunks.length) {
|
|
427
|
+
const chunk = this.chunks[chunkIndex]
|
|
428
|
+
const remaining = chunk.length - pos
|
|
429
|
+
|
|
430
|
+
if (offset < remaining) {
|
|
431
|
+
return chunk[pos + offset]
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
offset -= remaining
|
|
435
|
+
chunkIndex++
|
|
436
|
+
pos = 0
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
discardLeadingBytes (count) {
|
|
441
|
+
while (count > 0 && this.lineChunkIndex < this.chunks.length) {
|
|
442
|
+
const chunk = this.chunks[this.lineChunkIndex]
|
|
443
|
+
const remaining = chunk.length - this.linePos
|
|
444
|
+
|
|
445
|
+
if (count < remaining) {
|
|
446
|
+
this.linePos += count
|
|
447
|
+
count = 0
|
|
448
|
+
} else {
|
|
449
|
+
count -= remaining
|
|
450
|
+
this.lineChunkIndex++
|
|
451
|
+
this.linePos = 0
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
this.chunkIndex = this.lineChunkIndex
|
|
456
|
+
this.pos = this.linePos
|
|
457
|
+
this.dropConsumedChunks()
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
handleBOM () {
|
|
461
|
+
const first = this.peekBufferedByte(0)
|
|
462
|
+
const second = this.peekBufferedByte(1)
|
|
463
|
+
const third = this.peekBufferedByte(2)
|
|
464
|
+
|
|
465
|
+
if (second === undefined) {
|
|
466
|
+
if (first === BOM[0]) {
|
|
467
|
+
return true
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
this.checkBOM = false
|
|
471
|
+
return true
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (third === undefined) {
|
|
475
|
+
if (first === BOM[0] && second === BOM[1]) {
|
|
476
|
+
return true
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
this.checkBOM = false
|
|
480
|
+
return false
|
|
393
481
|
}
|
|
482
|
+
|
|
483
|
+
if (first === BOM[0] && second === BOM[1] && third === BOM[2]) {
|
|
484
|
+
this.discardLeadingBytes(3)
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
this.checkBOM = false
|
|
488
|
+
return !this.hasCurrentByte()
|
|
394
489
|
}
|
|
395
490
|
}
|
|
396
491
|
|
|
@@ -204,7 +204,7 @@ function multipartFormDataParser (input, mimeType) {
|
|
|
204
204
|
* Parses content-disposition attributes (e.g., name="value" or filename*=utf-8''encoded)
|
|
205
205
|
* @param {Buffer} input
|
|
206
206
|
* @param {{ position: number }} position
|
|
207
|
-
* @returns {{ name: string, value: string }}
|
|
207
|
+
* @returns {{ name: string, value: string, extended: boolean } | null}
|
|
208
208
|
*/
|
|
209
209
|
function parseContentDispositionAttribute (input, position) {
|
|
210
210
|
// Skip leading semicolon and whitespace
|
|
@@ -304,7 +304,7 @@ function parseContentDispositionAttribute (input, position) {
|
|
|
304
304
|
value = decoder.decode(tokenValue)
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
-
return { name: attrNameStr, value }
|
|
307
|
+
return { name: attrNameStr, value, extended: isExtended }
|
|
308
308
|
}
|
|
309
309
|
|
|
310
310
|
/**
|
|
@@ -368,6 +368,9 @@ function parseMultipartFormDataHeaders (input, position) {
|
|
|
368
368
|
switch (bufferToLowerCasedHeaderName(headerName)) {
|
|
369
369
|
case 'content-disposition': {
|
|
370
370
|
name = filename = null
|
|
371
|
+
// Track whether filename was set from the extended (RFC 5987) form so
|
|
372
|
+
// a subsequent legacy `filename` attribute does not override it.
|
|
373
|
+
let filenameIsExtended = false
|
|
371
374
|
|
|
372
375
|
// Collect the disposition type (should be "form-data")
|
|
373
376
|
const dispositionType = collectASequenceOfBytes(
|
|
@@ -383,8 +386,8 @@ function parseMultipartFormDataHeaders (input, position) {
|
|
|
383
386
|
// Parse attributes recursively until CRLF
|
|
384
387
|
while (
|
|
385
388
|
position.position < input.length &&
|
|
386
|
-
input[position.position] !== 0x0d
|
|
387
|
-
input[position.position + 1] !== 0x0a
|
|
389
|
+
(input[position.position] !== 0x0d ||
|
|
390
|
+
input[position.position + 1] !== 0x0a)
|
|
388
391
|
) {
|
|
389
392
|
const attribute = parseContentDispositionAttribute(input, position)
|
|
390
393
|
|
|
@@ -395,7 +398,15 @@ function parseMultipartFormDataHeaders (input, position) {
|
|
|
395
398
|
if (attribute.name === 'name') {
|
|
396
399
|
name = attribute.value
|
|
397
400
|
} else if (attribute.name === 'filename') {
|
|
398
|
-
|
|
401
|
+
// Per RFC 5987 §4.1, when both legacy and extended forms of the
|
|
402
|
+
// same parameter are present, the extended (filename*) form takes
|
|
403
|
+
// precedence regardless of the order they appear in.
|
|
404
|
+
if (attribute.extended) {
|
|
405
|
+
filename = attribute.value
|
|
406
|
+
filenameIsExtended = true
|
|
407
|
+
} else if (!filenameIsExtended) {
|
|
408
|
+
filename = attribute.value
|
|
409
|
+
}
|
|
399
410
|
}
|
|
400
411
|
}
|
|
401
412
|
|
|
@@ -448,7 +459,7 @@ function parseMultipartFormDataHeaders (input, position) {
|
|
|
448
459
|
|
|
449
460
|
// 2.9. If position does not point to a sequence of bytes starting with 0x0D 0x0A
|
|
450
461
|
// (CR LF), return failure. Otherwise, advance position by 2 (past the newline).
|
|
451
|
-
if (input[position.position] !== 0x0d
|
|
462
|
+
if (input[position.position] !== 0x0d || input[position.position + 1] !== 0x0a) {
|
|
452
463
|
throw parsingError('expected CRLF')
|
|
453
464
|
} else {
|
|
454
465
|
position.position += 2
|
package/lib/web/fetch/index.js
CHANGED
|
@@ -75,6 +75,35 @@ const defaultUserAgent = typeof __UNDICI_IS_NODE__ !== 'undefined' || typeof esb
|
|
|
75
75
|
/** @type {import('buffer').resolveObjectURL} */
|
|
76
76
|
let resolveObjectURL
|
|
77
77
|
|
|
78
|
+
function appendHeadersListFromResponseHeaders (headersList, headers, rawHeaders) {
|
|
79
|
+
if (Array.isArray(rawHeaders)) {
|
|
80
|
+
for (let i = 0; i < rawHeaders.length; i += 2) {
|
|
81
|
+
const nameStr = bufferToLowerCasedHeaderName(rawHeaders[i])
|
|
82
|
+
const value = rawHeaders[i + 1]
|
|
83
|
+
|
|
84
|
+
if (Array.isArray(value) && !Buffer.isBuffer(value)) {
|
|
85
|
+
for (const val of value) {
|
|
86
|
+
headersList.append(nameStr, val.toString('latin1'), true)
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
headersList.append(nameStr, value.toString('latin1'), true)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
for (const [name, value] of Object.entries(headers ?? {})) {
|
|
97
|
+
if (Array.isArray(value)) {
|
|
98
|
+
for (const entry of value) {
|
|
99
|
+
headersList.append(name, `${entry}`, true)
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
headersList.append(name, `${value}`, true)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
78
107
|
class Fetch extends EE {
|
|
79
108
|
constructor (dispatcher) {
|
|
80
109
|
super()
|
|
@@ -1025,7 +1054,7 @@ function fetchFinale (fetchParams, response) {
|
|
|
1025
1054
|
let responseStatus = 0
|
|
1026
1055
|
|
|
1027
1056
|
// 7. If fetchParams’s request’s mode is not "navigate" or response’s has-cross-origin-redirects is false:
|
|
1028
|
-
if (fetchParams.request.mode !== '
|
|
1057
|
+
if (fetchParams.request.mode !== 'navigate' || !response.hasCrossOriginRedirects) {
|
|
1029
1058
|
// 1. Set responseStatus to response’s status.
|
|
1030
1059
|
responseStatus = response.status
|
|
1031
1060
|
|
|
@@ -1428,7 +1457,10 @@ async function httpNetworkOrCacheFetch (
|
|
|
1428
1457
|
// 8. If contentLengthHeaderValue is non-null, then append
|
|
1429
1458
|
// `Content-Length`/contentLengthHeaderValue to httpRequest’s header
|
|
1430
1459
|
// list.
|
|
1431
|
-
if (
|
|
1460
|
+
if (
|
|
1461
|
+
contentLengthHeaderValue != null &&
|
|
1462
|
+
!httpRequest.headersList.contains('content-length', true)
|
|
1463
|
+
) {
|
|
1432
1464
|
httpRequest.headersList.append('content-length', contentLengthHeaderValue, true)
|
|
1433
1465
|
}
|
|
1434
1466
|
|
|
@@ -2193,25 +2225,14 @@ async function httpNetworkFetch (
|
|
|
2193
2225
|
timingInfo.finalNetworkResponseStartTime = coarsenedSharedCurrentTime(fetchParams.crossOriginIsolatedCapability)
|
|
2194
2226
|
},
|
|
2195
2227
|
|
|
2196
|
-
onResponseStart (controller, status,
|
|
2228
|
+
onResponseStart (controller, status, headers, statusText) {
|
|
2197
2229
|
if (status < 200) {
|
|
2198
2230
|
return
|
|
2199
2231
|
}
|
|
2200
2232
|
|
|
2201
2233
|
const rawHeaders = controller?.rawHeaders ?? []
|
|
2202
2234
|
const headersList = new HeadersList()
|
|
2203
|
-
|
|
2204
|
-
for (let i = 0; i < rawHeaders.length; i += 2) {
|
|
2205
|
-
const nameStr = bufferToLowerCasedHeaderName(rawHeaders[i])
|
|
2206
|
-
const value = rawHeaders[i + 1]
|
|
2207
|
-
if (Array.isArray(value) && !Buffer.isBuffer(rawHeaders[i + 1])) {
|
|
2208
|
-
for (const val of value) {
|
|
2209
|
-
headersList.append(nameStr, val.toString('latin1'), true)
|
|
2210
|
-
}
|
|
2211
|
-
} else {
|
|
2212
|
-
headersList.append(nameStr, value.toString('latin1'), true)
|
|
2213
|
-
}
|
|
2214
|
-
}
|
|
2235
|
+
appendHeadersListFromResponseHeaders(headersList, headers, rawHeaders)
|
|
2215
2236
|
const location = headersList.get('location', true)
|
|
2216
2237
|
|
|
2217
2238
|
this.body = new Readable({ read: () => controller.resume() })
|
|
@@ -2346,7 +2367,7 @@ async function httpNetworkFetch (
|
|
|
2346
2367
|
reject(error)
|
|
2347
2368
|
},
|
|
2348
2369
|
|
|
2349
|
-
onRequestUpgrade (controller, status,
|
|
2370
|
+
onRequestUpgrade (controller, status, headers, socket) {
|
|
2350
2371
|
// We need to support 200 for websocket over h2 as per RFC-8441
|
|
2351
2372
|
// Absence of session means H1
|
|
2352
2373
|
if ((socket.session != null && status !== 200) || (socket.session == null && status !== 101)) {
|
|
@@ -2355,18 +2376,7 @@ async function httpNetworkFetch (
|
|
|
2355
2376
|
|
|
2356
2377
|
const rawHeaders = controller?.rawHeaders ?? []
|
|
2357
2378
|
const headersList = new HeadersList()
|
|
2358
|
-
|
|
2359
|
-
for (let i = 0; i < rawHeaders.length; i += 2) {
|
|
2360
|
-
const nameStr = bufferToLowerCasedHeaderName(rawHeaders[i])
|
|
2361
|
-
const value = rawHeaders[i + 1]
|
|
2362
|
-
if (Array.isArray(value) && !Buffer.isBuffer(rawHeaders[i + 1])) {
|
|
2363
|
-
for (const val of value) {
|
|
2364
|
-
headersList.append(nameStr, val.toString('latin1'), true)
|
|
2365
|
-
}
|
|
2366
|
-
} else {
|
|
2367
|
-
headersList.append(nameStr, value.toString('latin1'), true)
|
|
2368
|
-
}
|
|
2369
|
-
}
|
|
2379
|
+
appendHeadersListFromResponseHeaders(headersList, headers, rawHeaders)
|
|
2370
2380
|
|
|
2371
2381
|
resolve({
|
|
2372
2382
|
status,
|
package/lib/web/webidl/index.js
CHANGED
|
@@ -188,10 +188,10 @@ webidl.util.ConvertToInt = function (V, bitLength, signedness, flags) {
|
|
|
188
188
|
} else {
|
|
189
189
|
// 3. Otherwise:
|
|
190
190
|
|
|
191
|
-
// 1. Let lowerBound be -2^bitLength − 1.
|
|
192
|
-
lowerBound = Math.pow(
|
|
191
|
+
// 1. Let lowerBound be -2^(bitLength − 1).
|
|
192
|
+
lowerBound = -Math.pow(2, bitLength - 1)
|
|
193
193
|
|
|
194
|
-
// 2. Let upperBound be 2^bitLength − 1 − 1.
|
|
194
|
+
// 2. Let upperBound be 2^(bitLength − 1) − 1.
|
|
195
195
|
upperBound = Math.pow(2, bitLength - 1) - 1
|
|
196
196
|
}
|
|
197
197
|
|
|
@@ -270,9 +270,9 @@ webidl.util.ConvertToInt = function (V, bitLength, signedness, flags) {
|
|
|
270
270
|
// 10. Set x to x modulo 2^bitLength.
|
|
271
271
|
x = x % Math.pow(2, bitLength)
|
|
272
272
|
|
|
273
|
-
// 11. If signedness is "signed" and x ≥ 2^bitLength − 1,
|
|
273
|
+
// 11. If signedness is "signed" and x ≥ 2^(bitLength − 1),
|
|
274
274
|
// then return x − 2^bitLength.
|
|
275
|
-
if (signedness === 'signed' && x >= Math.pow(2, bitLength
|
|
275
|
+
if (signedness === 'signed' && x >= Math.pow(2, bitLength - 1)) {
|
|
276
276
|
return x - Math.pow(2, bitLength)
|
|
277
277
|
}
|
|
278
278
|
|