undici 6.6.2 → 6.7.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/README.md +49 -27
- package/docs/{api → docs/api}/DiagnosticsChannel.md +2 -2
- package/docs/{api → docs/api}/Dispatcher.md +39 -3
- package/docs/docs/api/Fetch.md +57 -0
- package/docs/{api → docs/api}/ProxyAgent.md +5 -1
- package/docs/docs/api/RetryAgent.md +45 -0
- package/docs/{api → docs/api}/RetryHandler.md +1 -1
- package/docs/{api → docs/api}/api-lifecycle.md +33 -4
- package/docs/{best-practices → docs/best-practices}/proxy.md +6 -6
- package/index-fetch.js +9 -8
- package/index.js +31 -25
- package/lib/api/api-request.js +1 -1
- package/lib/api/readable.js +12 -9
- package/lib/api/util.js +8 -6
- package/lib/core/request.js +72 -135
- package/lib/core/symbols.js +6 -5
- package/lib/core/tree.js +46 -26
- package/lib/core/util.js +41 -20
- package/lib/{agent.js → dispatcher/agent.js} +4 -4
- package/lib/{balanced-pool.js → dispatcher/balanced-pool.js} +3 -3
- package/lib/dispatcher/client-h1.js +1352 -0
- package/lib/dispatcher/client-h2.js +639 -0
- package/lib/dispatcher/client.js +611 -0
- package/lib/{dispatcher-base.js → dispatcher/dispatcher-base.js} +2 -2
- package/lib/{pool-base.js → dispatcher/pool-base.js} +3 -3
- package/lib/{pool-stats.js → dispatcher/pool-stats.js} +1 -1
- package/lib/{pool.js → dispatcher/pool.js} +4 -4
- package/lib/{proxy-agent.js → dispatcher/proxy-agent.js} +29 -35
- package/lib/dispatcher/retry-agent.js +35 -0
- package/lib/global.js +1 -1
- package/lib/handler/{RetryHandler.js → retry-handler.js} +2 -2
- package/lib/interceptor/{redirectInterceptor.js → redirect-interceptor.js} +1 -1
- package/lib/mock/mock-agent.js +2 -2
- package/lib/mock/mock-client.js +1 -1
- package/lib/mock/mock-interceptor.js +2 -2
- package/lib/mock/mock-pool.js +1 -1
- package/lib/mock/mock-utils.js +6 -4
- package/lib/{cache → web/cache}/cache.js +2 -4
- package/lib/{cache → web/cache}/cachestorage.js +1 -1
- package/lib/web/cache/symbols.js +5 -0
- package/lib/{cache → web/cache}/util.js +5 -9
- package/lib/{cookies → web/cookies}/parse.js +1 -1
- package/lib/{cookies → web/cookies}/util.js +76 -60
- package/lib/{eventsource → web/eventsource}/eventsource.js +2 -6
- package/lib/{fetch → web/fetch}/body.js +56 -175
- package/lib/{fetch/dataURL.js → web/fetch/data-url.js} +5 -2
- package/lib/{compat → web/fetch}/dispatcher-weakref.js +1 -1
- package/lib/{fetch → web/fetch}/file.js +7 -8
- package/lib/web/fetch/formdata-parser.js +488 -0
- package/lib/{fetch → web/fetch}/formdata.js +7 -68
- package/lib/{fetch → web/fetch}/headers.js +99 -71
- package/lib/{fetch → web/fetch}/index.js +33 -25
- package/lib/{fetch → web/fetch}/request.js +15 -7
- package/lib/{fetch → web/fetch}/response.js +3 -3
- package/lib/{fetch → web/fetch}/symbols.js +2 -1
- package/lib/{fetch → web/fetch}/util.js +171 -48
- package/lib/{fetch → web/fetch}/webidl.js +46 -16
- package/lib/{fileapi → web/fileapi}/filereader.js +1 -1
- package/lib/{fileapi → web/fileapi}/util.js +1 -1
- package/lib/{websocket → web/websocket}/connection.js +20 -10
- package/lib/{websocket → web/websocket}/constants.js +7 -0
- package/lib/{websocket → web/websocket}/events.js +1 -1
- package/lib/{websocket → web/websocket}/frame.js +1 -0
- package/lib/{websocket → web/websocket}/receiver.js +9 -16
- package/lib/{websocket → web/websocket}/util.js +37 -23
- package/lib/{websocket → web/websocket}/websocket.js +21 -9
- package/package.json +26 -54
- package/types/dispatcher.d.ts +1 -1
- package/types/fetch.d.ts +20 -21
- package/types/index.d.ts +2 -1
- package/types/retry-agent.d.ts +11 -0
- package/types/webidl.d.ts +6 -1
- package/docs/api/Fetch.md +0 -27
- package/docs/assets/lifecycle-diagram.png +0 -0
- package/lib/cache/symbols.js +0 -5
- package/lib/client.js +0 -2295
- package/lib/llhttp/llhttp.wasm +0 -0
- package/lib/llhttp/llhttp_simd.wasm +0 -0
- /package/docs/{api → docs/api}/Agent.md +0 -0
- /package/docs/{api → docs/api}/BalancedPool.md +0 -0
- /package/docs/{api → docs/api}/CacheStorage.md +0 -0
- /package/docs/{api → docs/api}/Client.md +0 -0
- /package/docs/{api → docs/api}/Connector.md +0 -0
- /package/docs/{api → docs/api}/ContentType.md +0 -0
- /package/docs/{api → docs/api}/Cookies.md +0 -0
- /package/docs/{api → docs/api}/Debug.md +0 -0
- /package/docs/{api → docs/api}/DispatchInterceptor.md +0 -0
- /package/docs/{api → docs/api}/Errors.md +0 -0
- /package/docs/{api → docs/api}/EventSource.md +0 -0
- /package/docs/{api → docs/api}/MockAgent.md +0 -0
- /package/docs/{api → docs/api}/MockClient.md +0 -0
- /package/docs/{api → docs/api}/MockErrors.md +0 -0
- /package/docs/{api → docs/api}/MockPool.md +0 -0
- /package/docs/{api → docs/api}/Pool.md +0 -0
- /package/docs/{api → docs/api}/PoolStats.md +0 -0
- /package/docs/{api → docs/api}/RedirectHandler.md +0 -0
- /package/docs/{api → docs/api}/Util.md +0 -0
- /package/docs/{api → docs/api}/WebSocket.md +0 -0
- /package/docs/{best-practices → docs/best-practices}/client-certificate.md +0 -0
- /package/docs/{best-practices → docs/best-practices}/mocking-request.md +0 -0
- /package/docs/{best-practices → docs/best-practices}/writing-tests.md +0 -0
- /package/lib/{dispatcher.js → dispatcher/dispatcher.js} +0 -0
- /package/lib/{node → dispatcher}/fixed-queue.js +0 -0
- /package/lib/handler/{DecoratorHandler.js → decorator-handler.js} +0 -0
- /package/lib/handler/{RedirectHandler.js → redirect-handler.js} +0 -0
- /package/lib/{timers.js → util/timers.js} +0 -0
- /package/lib/{cookies → web/cookies}/constants.js +0 -0
- /package/lib/{cookies → web/cookies}/index.js +0 -0
- /package/lib/{eventsource → web/eventsource}/eventsource-stream.js +0 -0
- /package/lib/{eventsource → web/eventsource}/util.js +0 -0
- /package/lib/{fetch → web/fetch}/LICENSE +0 -0
- /package/lib/{fetch → web/fetch}/constants.js +0 -0
- /package/lib/{fetch → web/fetch}/global.js +0 -0
- /package/lib/{fileapi → web/fileapi}/encoding.js +0 -0
- /package/lib/{fileapi → web/fileapi}/progressevent.js +0 -0
- /package/lib/{fileapi → web/fileapi}/symbols.js +0 -0
- /package/lib/{websocket → web/websocket}/symbols.js +0 -0
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { webidl } = require('./webidl')
|
|
4
|
+
const { utf8DecodeBytes } = require('./util')
|
|
5
|
+
const { HTTP_TOKEN_CODEPOINTS, isomorphicDecode } = require('./data-url')
|
|
6
|
+
const { isFileLike, File: UndiciFile } = require('./file')
|
|
7
|
+
const { makeEntry } = require('./formdata')
|
|
8
|
+
const assert = require('node:assert')
|
|
9
|
+
const { isAscii, File: NodeFile } = require('node:buffer')
|
|
10
|
+
|
|
11
|
+
const File = globalThis.File ?? NodeFile ?? UndiciFile
|
|
12
|
+
|
|
13
|
+
const formDataNameBuffer = Buffer.from('form-data; name="')
|
|
14
|
+
const filenameBuffer = Buffer.from('; filename')
|
|
15
|
+
const dd = Buffer.from('--')
|
|
16
|
+
const ddcrlf = Buffer.from('--\r\n')
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @see https://andreubotella.github.io/multipart-form-data/#multipart-form-data-boundary
|
|
20
|
+
* @param {string} boundary
|
|
21
|
+
*/
|
|
22
|
+
function validateBoundary (boundary) {
|
|
23
|
+
const length = boundary.length
|
|
24
|
+
|
|
25
|
+
// - its length is greater or equal to 27 and lesser or equal to 70, and
|
|
26
|
+
if (length < 27 || length > 70) {
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// - it is composed by bytes in the ranges 0x30 to 0x39, 0x41 to 0x5A, or
|
|
31
|
+
// 0x61 to 0x7A, inclusive (ASCII alphanumeric), or which are 0x27 ('),
|
|
32
|
+
// 0x2D (-) or 0x5F (_).
|
|
33
|
+
for (let i = 0; i < boundary.length; i++) {
|
|
34
|
+
const cp = boundary.charCodeAt(i)
|
|
35
|
+
|
|
36
|
+
if (!(
|
|
37
|
+
(cp >= 0x30 && cp <= 0x39) ||
|
|
38
|
+
(cp >= 0x41 && cp <= 0x5a) ||
|
|
39
|
+
(cp >= 0x61 && cp <= 0x7a) ||
|
|
40
|
+
cp === 0x27 ||
|
|
41
|
+
cp === 0x2d ||
|
|
42
|
+
cp === 0x5f
|
|
43
|
+
)) {
|
|
44
|
+
return false
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return true
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @see https://andreubotella.github.io/multipart-form-data/#escape-a-multipart-form-data-name
|
|
53
|
+
* @param {string} name
|
|
54
|
+
* @param {string} [encoding='utf-8']
|
|
55
|
+
* @param {boolean} [isFilename=false]
|
|
56
|
+
*/
|
|
57
|
+
function escapeFormDataName (name, encoding = 'utf-8', isFilename = false) {
|
|
58
|
+
// 1. If isFilename is true:
|
|
59
|
+
if (isFilename) {
|
|
60
|
+
// 1.1. Set name to the result of converting name into a scalar value string.
|
|
61
|
+
name = webidl.converters.USVString(name)
|
|
62
|
+
} else {
|
|
63
|
+
// 2. Otherwise:
|
|
64
|
+
|
|
65
|
+
// 2.1. Assert: name is a scalar value string.
|
|
66
|
+
assert(name === webidl.converters.USVString(name))
|
|
67
|
+
|
|
68
|
+
// 2.2. Replace every occurrence of U+000D (CR) not followed by U+000A (LF),
|
|
69
|
+
// and every occurrence of U+000A (LF) not preceded by U+000D (CR), in
|
|
70
|
+
// name, by a string consisting of U+000D (CR) and U+000A (LF).
|
|
71
|
+
name = name.replace(/\r\n?|\r?\n/g, '\r\n')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 3. Let encoded be the result of encoding name with encoding.
|
|
75
|
+
assert(Buffer.isEncoding(encoding))
|
|
76
|
+
|
|
77
|
+
// 4. Replace every 0x0A (LF) bytes in encoded with the byte sequence `%0A`,
|
|
78
|
+
// 0x0D (CR) with `%0D` and 0x22 (") with `%22`.
|
|
79
|
+
name = name
|
|
80
|
+
.replace(/\n/g, '%0A')
|
|
81
|
+
.replace(/\r/g, '%0D')
|
|
82
|
+
.replace(/"/g, '%22')
|
|
83
|
+
|
|
84
|
+
// 5. Return encoded.
|
|
85
|
+
return Buffer.from(name, encoding) // encoded
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @see https://andreubotella.github.io/multipart-form-data/#multipart-form-data-parser
|
|
90
|
+
* @param {Buffer} input
|
|
91
|
+
* @param {ReturnType<import('./data-url')['parseMIMEType']>} mimeType
|
|
92
|
+
*/
|
|
93
|
+
function multipartFormDataParser (input, mimeType) {
|
|
94
|
+
// 1. Assert: mimeType’s essence is "multipart/form-data".
|
|
95
|
+
assert(mimeType !== 'failure' && mimeType.essence === 'multipart/form-data')
|
|
96
|
+
|
|
97
|
+
// 2. If mimeType’s parameters["boundary"] does not exist, return failure.
|
|
98
|
+
// Otherwise, let boundary be the result of UTF-8 decoding mimeType’s
|
|
99
|
+
// parameters["boundary"].
|
|
100
|
+
if (!mimeType.parameters.has('boundary')) {
|
|
101
|
+
return 'failure'
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const boundary = Buffer.from(`--${mimeType.parameters.get('boundary')}`, 'utf8')
|
|
105
|
+
|
|
106
|
+
// 3. Let entry list be an empty entry list.
|
|
107
|
+
const entryList = []
|
|
108
|
+
|
|
109
|
+
// 4. Let position be a pointer to a byte in input, initially pointing at
|
|
110
|
+
// the first byte.
|
|
111
|
+
const position = { position: 0 }
|
|
112
|
+
|
|
113
|
+
// Note: undici addition, allow \r\n before the body.
|
|
114
|
+
if (input[0] === 0x0d && input[1] === 0x0a) {
|
|
115
|
+
position.position += 2
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 5. While true:
|
|
119
|
+
while (true) {
|
|
120
|
+
// 5.1. If position points to a sequence of bytes starting with 0x2D 0x2D
|
|
121
|
+
// (`--`) followed by boundary, advance position by 2 + the length of
|
|
122
|
+
// boundary. Otherwise, return failure.
|
|
123
|
+
// Note: boundary is padded with 2 dashes already, no need to add 2.
|
|
124
|
+
if (input.subarray(position.position, position.position + boundary.length).equals(boundary)) {
|
|
125
|
+
position.position += boundary.length
|
|
126
|
+
} else {
|
|
127
|
+
return 'failure'
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 5.2. If position points to the sequence of bytes 0x2D 0x2D 0x0D 0x0A
|
|
131
|
+
// (`--` followed by CR LF) followed by the end of input, return entry list.
|
|
132
|
+
// Note: a body does NOT need to end with CRLF. It can end with --.
|
|
133
|
+
if (
|
|
134
|
+
(position.position === input.length - 2 && bufferStartsWith(input, dd, position)) ||
|
|
135
|
+
(position.position === input.length - 4 && bufferStartsWith(input, ddcrlf, position))
|
|
136
|
+
) {
|
|
137
|
+
return entryList
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 5.3. If position does not point to a sequence of bytes starting with 0x0D
|
|
141
|
+
// 0x0A (CR LF), return failure.
|
|
142
|
+
if (input[position.position] !== 0x0d || input[position.position + 1] !== 0x0a) {
|
|
143
|
+
return 'failure'
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 5.4. Advance position by 2. (This skips past the newline.)
|
|
147
|
+
position.position += 2
|
|
148
|
+
|
|
149
|
+
// 5.5. Let name, filename and contentType be the result of parsing
|
|
150
|
+
// multipart/form-data headers on input and position, if the result
|
|
151
|
+
// is not failure. Otherwise, return failure.
|
|
152
|
+
const result = parseMultipartFormDataHeaders(input, position)
|
|
153
|
+
|
|
154
|
+
if (result === 'failure') {
|
|
155
|
+
return 'failure'
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let { name, filename, contentType, encoding } = result
|
|
159
|
+
|
|
160
|
+
// 5.6. Advance position by 2. (This skips past the empty line that marks
|
|
161
|
+
// the end of the headers.)
|
|
162
|
+
position.position += 2
|
|
163
|
+
|
|
164
|
+
// 5.7. Let body be the empty byte sequence.
|
|
165
|
+
let body
|
|
166
|
+
|
|
167
|
+
// 5.8. Body loop: While position is not past the end of input:
|
|
168
|
+
// TODO: the steps here are completely wrong
|
|
169
|
+
{
|
|
170
|
+
const boundaryIndex = input.indexOf(boundary.subarray(2), position.position)
|
|
171
|
+
|
|
172
|
+
if (boundaryIndex === -1) {
|
|
173
|
+
return 'failure'
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
body = input.subarray(position.position, boundaryIndex - 4)
|
|
177
|
+
|
|
178
|
+
position.position += body.length
|
|
179
|
+
|
|
180
|
+
// Note: position must be advanced by the body's length before being
|
|
181
|
+
// decoded, otherwise the parsing will fail.
|
|
182
|
+
if (encoding === 'base64') {
|
|
183
|
+
body = Buffer.from(body.toString(), 'base64')
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 5.9. If position does not point to a sequence of bytes starting with
|
|
188
|
+
// 0x0D 0x0A (CR LF), return failure. Otherwise, advance position by 2.
|
|
189
|
+
if (input[position.position] !== 0x0d || input[position.position + 1] !== 0x0a) {
|
|
190
|
+
return 'failure'
|
|
191
|
+
} else {
|
|
192
|
+
position.position += 2
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 5.10. If filename is not null:
|
|
196
|
+
let value
|
|
197
|
+
|
|
198
|
+
if (filename !== null) {
|
|
199
|
+
// 5.10.1. If contentType is null, set contentType to "text/plain".
|
|
200
|
+
contentType ??= 'text/plain'
|
|
201
|
+
|
|
202
|
+
// 5.10.2. If contentType is not an ASCII string, set contentType to the empty string.
|
|
203
|
+
if (!isAscii(Buffer.from(contentType))) {
|
|
204
|
+
contentType = ''
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// 5.10.3. Let value be a new File object with name filename, type contentType, and body body.
|
|
208
|
+
value = new File([body], filename, { type: contentType })
|
|
209
|
+
} else {
|
|
210
|
+
// 5.11. Otherwise:
|
|
211
|
+
|
|
212
|
+
// 5.11.1. Let value be the UTF-8 decoding without BOM of body.
|
|
213
|
+
value = utf8DecodeBytes(Buffer.from(body))
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// 5.12. Assert: name is a scalar value string and value is either a scalar value string or a File object.
|
|
217
|
+
assert(name === webidl.converters.USVString(name))
|
|
218
|
+
assert((typeof value === 'string' && value === webidl.converters.USVString(value)) || isFileLike(value))
|
|
219
|
+
|
|
220
|
+
// 5.13. Create an entry with name and value, and append it to entry list.
|
|
221
|
+
entryList.push(makeEntry(name, value, filename))
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* @see https://andreubotella.github.io/multipart-form-data/#parse-multipart-form-data-headers
|
|
227
|
+
* @param {Buffer} input
|
|
228
|
+
* @param {{ position: number }} position
|
|
229
|
+
*/
|
|
230
|
+
function parseMultipartFormDataHeaders (input, position) {
|
|
231
|
+
// 1. Let name, filename and contentType be null.
|
|
232
|
+
let name = null
|
|
233
|
+
let filename = null
|
|
234
|
+
let contentType = null
|
|
235
|
+
let encoding = null
|
|
236
|
+
|
|
237
|
+
// 2. While true:
|
|
238
|
+
while (true) {
|
|
239
|
+
// 2.1. If position points to a sequence of bytes starting with 0x0D 0x0A (CR LF):
|
|
240
|
+
if (input[position.position] === 0x0d && input[position.position + 1] === 0x0a) {
|
|
241
|
+
// 2.1.1. If name is null, return failure.
|
|
242
|
+
if (name === null) {
|
|
243
|
+
return 'failure'
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// 2.1.2. Return name, filename and contentType.
|
|
247
|
+
return { name, filename, contentType, encoding }
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// 2.2. Let header name be the result of collecting a sequence of bytes that are
|
|
251
|
+
// not 0x0A (LF), 0x0D (CR) or 0x3A (:), given position.
|
|
252
|
+
let headerName = collectASequenceOfBytes(
|
|
253
|
+
(char) => char !== 0x0a && char !== 0x0d && char !== 0x3a,
|
|
254
|
+
input,
|
|
255
|
+
position
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
// 2.3. Remove any HTTP tab or space bytes from the start or end of header name.
|
|
259
|
+
headerName = removeChars(headerName, true, true, (char) => char === 0x9 || char === 0x20)
|
|
260
|
+
|
|
261
|
+
// 2.4. If header name does not match the field-name token production, return failure.
|
|
262
|
+
if (!HTTP_TOKEN_CODEPOINTS.test(headerName.toString())) {
|
|
263
|
+
return 'failure'
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// 2.5. If the byte at position is not 0x3A (:), return failure.
|
|
267
|
+
if (input[position.position] !== 0x3a) {
|
|
268
|
+
return 'failure'
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// 2.6. Advance position by 1.
|
|
272
|
+
position.position++
|
|
273
|
+
|
|
274
|
+
// 2.7. Collect a sequence of bytes that are HTTP tab or space bytes given position.
|
|
275
|
+
// (Do nothing with those bytes.)
|
|
276
|
+
collectASequenceOfBytes(
|
|
277
|
+
(char) => char === 0x20 || char === 0x09,
|
|
278
|
+
input,
|
|
279
|
+
position
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
// 2.8. Byte-lowercase header name and switch on the result:
|
|
283
|
+
switch (new TextDecoder().decode(headerName).toLowerCase()) {
|
|
284
|
+
case 'content-disposition': {
|
|
285
|
+
// 1. Set name and filename to null.
|
|
286
|
+
name = filename = null
|
|
287
|
+
|
|
288
|
+
// 2. If position does not point to a sequence of bytes starting with
|
|
289
|
+
// `form-data; name="`, return failure.
|
|
290
|
+
if (!bufferStartsWith(input, formDataNameBuffer, position)) {
|
|
291
|
+
return 'failure'
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// 3. Advance position so it points at the byte after the next 0x22 (")
|
|
295
|
+
// byte (the one in the sequence of bytes matched above).
|
|
296
|
+
position.position += 17
|
|
297
|
+
|
|
298
|
+
// 4. Set name to the result of parsing a multipart/form-data name given
|
|
299
|
+
// input and position, if the result is not failure. Otherwise, return
|
|
300
|
+
// failure.
|
|
301
|
+
name = parseMultipartFormDataName(input, position)
|
|
302
|
+
|
|
303
|
+
if (name === null) {
|
|
304
|
+
return 'failure'
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// 5. If position points to a sequence of bytes starting with `; filename="`:
|
|
308
|
+
if (bufferStartsWith(input, filenameBuffer, position)) {
|
|
309
|
+
// Note: undici also handles filename*
|
|
310
|
+
let check = position.position + filenameBuffer.length
|
|
311
|
+
|
|
312
|
+
if (input[check] === 0x2a) {
|
|
313
|
+
position.position += 1
|
|
314
|
+
check += 1
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (input[check] !== 0x3d || input[check + 1] !== 0x22) { // ="
|
|
318
|
+
return 'failure'
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// 1. Advance position so it points at the byte after the next 0x22 (") byte
|
|
322
|
+
// (the one in the sequence of bytes matched above).
|
|
323
|
+
position.position += 12
|
|
324
|
+
|
|
325
|
+
// 2. Set filename to the result of parsing a multipart/form-data name given
|
|
326
|
+
// input and position, if the result is not failure. Otherwise, return failure.
|
|
327
|
+
filename = parseMultipartFormDataName(input, position)
|
|
328
|
+
|
|
329
|
+
if (filename === null) {
|
|
330
|
+
return 'failure'
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
break
|
|
335
|
+
}
|
|
336
|
+
case 'content-type': {
|
|
337
|
+
// 1. Let header value be the result of collecting a sequence of bytes that are
|
|
338
|
+
// not 0x0A (LF) or 0x0D (CR), given position.
|
|
339
|
+
let headerValue = collectASequenceOfBytes(
|
|
340
|
+
(char) => char !== 0x0a && char !== 0x0d,
|
|
341
|
+
input,
|
|
342
|
+
position
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
// 2. Remove any HTTP tab or space bytes from the end of header value.
|
|
346
|
+
headerValue = removeChars(headerValue, false, true, (char) => char === 0x9 || char === 0x20)
|
|
347
|
+
|
|
348
|
+
// 3. Set contentType to the isomorphic decoding of header value.
|
|
349
|
+
contentType = isomorphicDecode(headerValue)
|
|
350
|
+
|
|
351
|
+
break
|
|
352
|
+
}
|
|
353
|
+
case 'content-transfer-encoding': {
|
|
354
|
+
let headerValue = collectASequenceOfBytes(
|
|
355
|
+
(char) => char !== 0x0a && char !== 0x0d,
|
|
356
|
+
input,
|
|
357
|
+
position
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
headerValue = removeChars(headerValue, false, true, (char) => char === 0x9 || char === 0x20)
|
|
361
|
+
|
|
362
|
+
encoding = isomorphicDecode(headerValue)
|
|
363
|
+
|
|
364
|
+
break
|
|
365
|
+
}
|
|
366
|
+
default: {
|
|
367
|
+
// Collect a sequence of bytes that are not 0x0A (LF) or 0x0D (CR), given position.
|
|
368
|
+
// (Do nothing with those bytes.)
|
|
369
|
+
collectASequenceOfBytes(
|
|
370
|
+
(char) => char !== 0x0a && char !== 0x0d,
|
|
371
|
+
input,
|
|
372
|
+
position
|
|
373
|
+
)
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// 2.9. If position does not point to a sequence of bytes starting with 0x0D 0x0A
|
|
378
|
+
// (CR LF), return failure. Otherwise, advance position by 2 (past the newline).
|
|
379
|
+
if (input[position.position] !== 0x0d && input[position.position + 1] !== 0x0a) {
|
|
380
|
+
return 'failure'
|
|
381
|
+
} else {
|
|
382
|
+
position.position += 2
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* @see https://andreubotella.github.io/multipart-form-data/#parse-a-multipart-form-data-name
|
|
389
|
+
* @param {Buffer} input
|
|
390
|
+
* @param {{ position: number }} position
|
|
391
|
+
*/
|
|
392
|
+
function parseMultipartFormDataName (input, position) {
|
|
393
|
+
// 1. Assert: The byte at (position - 1) is 0x22 (").
|
|
394
|
+
assert(input[position.position - 1] === 0x22)
|
|
395
|
+
|
|
396
|
+
// 2. Let name be the result of collecting a sequence of bytes that are not 0x0A (LF), 0x0D (CR) or 0x22 ("), given position.
|
|
397
|
+
/** @type {string | Buffer} */
|
|
398
|
+
let name = collectASequenceOfBytes(
|
|
399
|
+
(char) => char !== 0x0a && char !== 0x0d && char !== 0x22,
|
|
400
|
+
input,
|
|
401
|
+
position
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
// 3. If the byte at position is not 0x22 ("), return failure. Otherwise, advance position by 1.
|
|
405
|
+
if (input[position.position] !== 0x22) {
|
|
406
|
+
return null // name could be 'failure'
|
|
407
|
+
} else {
|
|
408
|
+
position.position++
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// 4. Replace any occurrence of the following subsequences in name with the given byte:
|
|
412
|
+
// - `%0A`: 0x0A (LF)
|
|
413
|
+
// - `%0D`: 0x0D (CR)
|
|
414
|
+
// - `%22`: 0x22 (")
|
|
415
|
+
name = new TextDecoder().decode(name)
|
|
416
|
+
.replace(/%0A/ig, '\n')
|
|
417
|
+
.replace(/%0D/ig, '\r')
|
|
418
|
+
.replace(/%22/g, '"')
|
|
419
|
+
|
|
420
|
+
// 5. Return the UTF-8 decoding without BOM of name.
|
|
421
|
+
return name
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* @param {(char: number) => boolean} condition
|
|
426
|
+
* @param {Buffer} input
|
|
427
|
+
* @param {{ position: number }} position
|
|
428
|
+
*/
|
|
429
|
+
function collectASequenceOfBytes (condition, input, position) {
|
|
430
|
+
const result = []
|
|
431
|
+
let index = 0
|
|
432
|
+
|
|
433
|
+
while (position.position < input.length && condition(input[position.position])) {
|
|
434
|
+
result[index++] = input[position.position]
|
|
435
|
+
|
|
436
|
+
position.position++
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return Buffer.from(result, result.length)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* @param {Buffer} buf
|
|
444
|
+
* @param {boolean} leading
|
|
445
|
+
* @param {boolean} trailing
|
|
446
|
+
* @param {(charCode: number) => boolean} predicate
|
|
447
|
+
* @returns {Buffer}
|
|
448
|
+
*/
|
|
449
|
+
function removeChars (buf, leading, trailing, predicate) {
|
|
450
|
+
let lead = 0
|
|
451
|
+
let trail = buf.length - 1
|
|
452
|
+
|
|
453
|
+
if (leading) {
|
|
454
|
+
while (lead < buf.length && predicate(buf[lead])) lead++
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (trailing) {
|
|
458
|
+
while (trail > 0 && predicate(buf[trail])) trail--
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return lead === 0 && trail === buf.length - 1 ? buf : buf.subarray(lead, trail + 1)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Checks if {@param buffer} starts with {@param start}
|
|
466
|
+
* @param {Buffer} buffer
|
|
467
|
+
* @param {Buffer} start
|
|
468
|
+
* @param {{ position: number }} position
|
|
469
|
+
*/
|
|
470
|
+
function bufferStartsWith (buffer, start, position) {
|
|
471
|
+
if (buffer.length < start.length) {
|
|
472
|
+
return false
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
for (let i = 0; i < start.length; i++) {
|
|
476
|
+
if (start[i] !== buffer[position.position + i]) {
|
|
477
|
+
return false
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return true
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
module.exports = {
|
|
485
|
+
multipartFormDataParser,
|
|
486
|
+
validateBoundary,
|
|
487
|
+
escapeFormDataName
|
|
488
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const { isBlobLike,
|
|
3
|
+
const { isBlobLike, iteratorMixin } = require('./util')
|
|
4
4
|
const { kState } = require('./symbols')
|
|
5
|
-
const { kEnumerableProperty } = require('
|
|
5
|
+
const { kEnumerableProperty } = require('../../core/util')
|
|
6
6
|
const { File: UndiciFile, FileLike, isFileLike } = require('./file')
|
|
7
7
|
const { webidl } = require('./webidl')
|
|
8
8
|
const { File: NativeFile } = require('node:buffer')
|
|
@@ -133,7 +133,7 @@ class FormData {
|
|
|
133
133
|
? webidl.converters.Blob(value, { strict: false })
|
|
134
134
|
: webidl.converters.USVString(value)
|
|
135
135
|
filename = arguments.length === 3
|
|
136
|
-
?
|
|
136
|
+
? webidl.converters.USVString(filename)
|
|
137
137
|
: undefined
|
|
138
138
|
|
|
139
139
|
// 2. Let entry be the result of creating an entry with name, value, and
|
|
@@ -154,62 +154,9 @@ class FormData {
|
|
|
154
154
|
this[kState].push(entry)
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
|
-
|
|
158
|
-
entries () {
|
|
159
|
-
webidl.brandCheck(this, FormData)
|
|
160
|
-
|
|
161
|
-
return makeIterator(
|
|
162
|
-
() => this[kState],
|
|
163
|
-
'FormData',
|
|
164
|
-
'key+value',
|
|
165
|
-
'name', 'value'
|
|
166
|
-
)
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
keys () {
|
|
170
|
-
webidl.brandCheck(this, FormData)
|
|
171
|
-
|
|
172
|
-
return makeIterator(
|
|
173
|
-
() => this[kState],
|
|
174
|
-
'FormData',
|
|
175
|
-
'key',
|
|
176
|
-
'name', 'value'
|
|
177
|
-
)
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
values () {
|
|
181
|
-
webidl.brandCheck(this, FormData)
|
|
182
|
-
|
|
183
|
-
return makeIterator(
|
|
184
|
-
() => this[kState],
|
|
185
|
-
'FormData',
|
|
186
|
-
'value',
|
|
187
|
-
'name', 'value'
|
|
188
|
-
)
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* @param {(value: string, key: string, self: FormData) => void} callbackFn
|
|
193
|
-
* @param {unknown} thisArg
|
|
194
|
-
*/
|
|
195
|
-
forEach (callbackFn, thisArg = globalThis) {
|
|
196
|
-
webidl.brandCheck(this, FormData)
|
|
197
|
-
|
|
198
|
-
webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.forEach' })
|
|
199
|
-
|
|
200
|
-
if (typeof callbackFn !== 'function') {
|
|
201
|
-
throw new TypeError(
|
|
202
|
-
"Failed to execute 'forEach' on 'FormData': parameter 1 is not of type 'Function'."
|
|
203
|
-
)
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
for (const [key, value] of this) {
|
|
207
|
-
callbackFn.call(thisArg, value, key, this)
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
157
|
}
|
|
211
158
|
|
|
212
|
-
FormData
|
|
159
|
+
iteratorMixin('FormData', FormData, kState, 'name', 'value')
|
|
213
160
|
|
|
214
161
|
Object.defineProperties(FormData.prototype, {
|
|
215
162
|
append: kEnumerableProperty,
|
|
@@ -218,11 +165,6 @@ Object.defineProperties(FormData.prototype, {
|
|
|
218
165
|
getAll: kEnumerableProperty,
|
|
219
166
|
has: kEnumerableProperty,
|
|
220
167
|
set: kEnumerableProperty,
|
|
221
|
-
entries: kEnumerableProperty,
|
|
222
|
-
keys: kEnumerableProperty,
|
|
223
|
-
values: kEnumerableProperty,
|
|
224
|
-
forEach: kEnumerableProperty,
|
|
225
|
-
[Symbol.iterator]: { enumerable: false },
|
|
226
168
|
[Symbol.toStringTag]: {
|
|
227
169
|
value: 'FormData',
|
|
228
170
|
configurable: true
|
|
@@ -238,15 +180,12 @@ Object.defineProperties(FormData.prototype, {
|
|
|
238
180
|
*/
|
|
239
181
|
function makeEntry (name, value, filename) {
|
|
240
182
|
// 1. Set name to the result of converting name into a scalar value string.
|
|
241
|
-
//
|
|
242
|
-
// with U+FFFD."
|
|
243
|
-
// see: https://nodejs.org/dist/latest-v20.x/docs/api/buffer.html#buftostringencoding-start-end
|
|
244
|
-
name = Buffer.from(name).toString('utf8')
|
|
183
|
+
// Note: This operation was done by the webidl converter USVString.
|
|
245
184
|
|
|
246
185
|
// 2. If value is a string, then set value to the result of converting
|
|
247
186
|
// value into a scalar value string.
|
|
248
187
|
if (typeof value === 'string') {
|
|
249
|
-
|
|
188
|
+
// Note: This operation was done by the webidl converter USVString.
|
|
250
189
|
} else {
|
|
251
190
|
// 3. Otherwise:
|
|
252
191
|
|
|
@@ -277,4 +216,4 @@ function makeEntry (name, value, filename) {
|
|
|
277
216
|
return { name, value }
|
|
278
217
|
}
|
|
279
218
|
|
|
280
|
-
module.exports = { FormData }
|
|
219
|
+
module.exports = { FormData, makeEntry }
|