1 | 'use strict'
|
---|
2 |
|
---|
3 | const assert = require('assert')
|
---|
4 | const { kDestroyed, kBodyUsed } = require('./symbols')
|
---|
5 | const { IncomingMessage } = require('http')
|
---|
6 | const stream = require('stream')
|
---|
7 | const net = require('net')
|
---|
8 | const { InvalidArgumentError } = require('./errors')
|
---|
9 | const { Blob } = require('buffer')
|
---|
10 | const nodeUtil = require('util')
|
---|
11 | const { stringify } = require('querystring')
|
---|
12 |
|
---|
13 | const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(v => Number(v))
|
---|
14 |
|
---|
15 | function nop () {}
|
---|
16 |
|
---|
17 | function isStream (obj) {
|
---|
18 | return obj && typeof obj === 'object' && typeof obj.pipe === 'function' && typeof obj.on === 'function'
|
---|
19 | }
|
---|
20 |
|
---|
21 | // based on https://github.com/node-fetch/fetch-blob/blob/8ab587d34080de94140b54f07168451e7d0b655e/index.js#L229-L241 (MIT License)
|
---|
22 | function isBlobLike (object) {
|
---|
23 | return (Blob && object instanceof Blob) || (
|
---|
24 | object &&
|
---|
25 | typeof object === 'object' &&
|
---|
26 | (typeof object.stream === 'function' ||
|
---|
27 | typeof object.arrayBuffer === 'function') &&
|
---|
28 | /^(Blob|File)$/.test(object[Symbol.toStringTag])
|
---|
29 | )
|
---|
30 | }
|
---|
31 |
|
---|
32 | function buildURL (url, queryParams) {
|
---|
33 | if (url.includes('?') || url.includes('#')) {
|
---|
34 | throw new Error('Query params cannot be passed when url already contains "?" or "#".')
|
---|
35 | }
|
---|
36 |
|
---|
37 | const stringified = stringify(queryParams)
|
---|
38 |
|
---|
39 | if (stringified) {
|
---|
40 | url += '?' + stringified
|
---|
41 | }
|
---|
42 |
|
---|
43 | return url
|
---|
44 | }
|
---|
45 |
|
---|
46 | function parseURL (url) {
|
---|
47 | if (typeof url === 'string') {
|
---|
48 | url = new URL(url)
|
---|
49 |
|
---|
50 | if (!/^https?:/.test(url.origin || url.protocol)) {
|
---|
51 | throw new InvalidArgumentError('Invalid URL protocol: the URL must start with `http:` or `https:`.')
|
---|
52 | }
|
---|
53 |
|
---|
54 | return url
|
---|
55 | }
|
---|
56 |
|
---|
57 | if (!url || typeof url !== 'object') {
|
---|
58 | throw new InvalidArgumentError('Invalid URL: The URL argument must be a non-null object.')
|
---|
59 | }
|
---|
60 |
|
---|
61 | if (!/^https?:/.test(url.origin || url.protocol)) {
|
---|
62 | throw new InvalidArgumentError('Invalid URL protocol: the URL must start with `http:` or `https:`.')
|
---|
63 | }
|
---|
64 |
|
---|
65 | if (!(url instanceof URL)) {
|
---|
66 | if (url.port != null && url.port !== '' && !Number.isFinite(parseInt(url.port))) {
|
---|
67 | throw new InvalidArgumentError('Invalid URL: port must be a valid integer or a string representation of an integer.')
|
---|
68 | }
|
---|
69 |
|
---|
70 | if (url.path != null && typeof url.path !== 'string') {
|
---|
71 | throw new InvalidArgumentError('Invalid URL path: the path must be a string or null/undefined.')
|
---|
72 | }
|
---|
73 |
|
---|
74 | if (url.pathname != null && typeof url.pathname !== 'string') {
|
---|
75 | throw new InvalidArgumentError('Invalid URL pathname: the pathname must be a string or null/undefined.')
|
---|
76 | }
|
---|
77 |
|
---|
78 | if (url.hostname != null && typeof url.hostname !== 'string') {
|
---|
79 | throw new InvalidArgumentError('Invalid URL hostname: the hostname must be a string or null/undefined.')
|
---|
80 | }
|
---|
81 |
|
---|
82 | if (url.origin != null && typeof url.origin !== 'string') {
|
---|
83 | throw new InvalidArgumentError('Invalid URL origin: the origin must be a string or null/undefined.')
|
---|
84 | }
|
---|
85 |
|
---|
86 | const port = url.port != null
|
---|
87 | ? url.port
|
---|
88 | : (url.protocol === 'https:' ? 443 : 80)
|
---|
89 | let origin = url.origin != null
|
---|
90 | ? url.origin
|
---|
91 | : `${url.protocol}//${url.hostname}:${port}`
|
---|
92 | let path = url.path != null
|
---|
93 | ? url.path
|
---|
94 | : `${url.pathname || ''}${url.search || ''}`
|
---|
95 |
|
---|
96 | if (origin.endsWith('/')) {
|
---|
97 | origin = origin.substring(0, origin.length - 1)
|
---|
98 | }
|
---|
99 |
|
---|
100 | if (path && !path.startsWith('/')) {
|
---|
101 | path = `/${path}`
|
---|
102 | }
|
---|
103 | // new URL(path, origin) is unsafe when `path` contains an absolute URL
|
---|
104 | // From https://developer.mozilla.org/en-US/docs/Web/API/URL/URL:
|
---|
105 | // If first parameter is a relative URL, second param is required, and will be used as the base URL.
|
---|
106 | // If first parameter is an absolute URL, a given second param will be ignored.
|
---|
107 | url = new URL(origin + path)
|
---|
108 | }
|
---|
109 |
|
---|
110 | return url
|
---|
111 | }
|
---|
112 |
|
---|
113 | function parseOrigin (url) {
|
---|
114 | url = parseURL(url)
|
---|
115 |
|
---|
116 | if (url.pathname !== '/' || url.search || url.hash) {
|
---|
117 | throw new InvalidArgumentError('invalid url')
|
---|
118 | }
|
---|
119 |
|
---|
120 | return url
|
---|
121 | }
|
---|
122 |
|
---|
123 | function getHostname (host) {
|
---|
124 | if (host[0] === '[') {
|
---|
125 | const idx = host.indexOf(']')
|
---|
126 |
|
---|
127 | assert(idx !== -1)
|
---|
128 | return host.substring(1, idx)
|
---|
129 | }
|
---|
130 |
|
---|
131 | const idx = host.indexOf(':')
|
---|
132 | if (idx === -1) return host
|
---|
133 |
|
---|
134 | return host.substring(0, idx)
|
---|
135 | }
|
---|
136 |
|
---|
137 | // IP addresses are not valid server names per RFC6066
|
---|
138 | // > Currently, the only server names supported are DNS hostnames
|
---|
139 | function getServerName (host) {
|
---|
140 | if (!host) {
|
---|
141 | return null
|
---|
142 | }
|
---|
143 |
|
---|
144 | assert.strictEqual(typeof host, 'string')
|
---|
145 |
|
---|
146 | const servername = getHostname(host)
|
---|
147 | if (net.isIP(servername)) {
|
---|
148 | return ''
|
---|
149 | }
|
---|
150 |
|
---|
151 | return servername
|
---|
152 | }
|
---|
153 |
|
---|
154 | function deepClone (obj) {
|
---|
155 | return JSON.parse(JSON.stringify(obj))
|
---|
156 | }
|
---|
157 |
|
---|
158 | function isAsyncIterable (obj) {
|
---|
159 | return !!(obj != null && typeof obj[Symbol.asyncIterator] === 'function')
|
---|
160 | }
|
---|
161 |
|
---|
162 | function isIterable (obj) {
|
---|
163 | return !!(obj != null && (typeof obj[Symbol.iterator] === 'function' || typeof obj[Symbol.asyncIterator] === 'function'))
|
---|
164 | }
|
---|
165 |
|
---|
166 | function bodyLength (body) {
|
---|
167 | if (body == null) {
|
---|
168 | return 0
|
---|
169 | } else if (isStream(body)) {
|
---|
170 | const state = body._readableState
|
---|
171 | return state && state.objectMode === false && state.ended === true && Number.isFinite(state.length)
|
---|
172 | ? state.length
|
---|
173 | : null
|
---|
174 | } else if (isBlobLike(body)) {
|
---|
175 | return body.size != null ? body.size : null
|
---|
176 | } else if (isBuffer(body)) {
|
---|
177 | return body.byteLength
|
---|
178 | }
|
---|
179 |
|
---|
180 | return null
|
---|
181 | }
|
---|
182 |
|
---|
183 | function isDestroyed (stream) {
|
---|
184 | return !stream || !!(stream.destroyed || stream[kDestroyed])
|
---|
185 | }
|
---|
186 |
|
---|
187 | function isReadableAborted (stream) {
|
---|
188 | const state = stream && stream._readableState
|
---|
189 | return isDestroyed(stream) && state && !state.endEmitted
|
---|
190 | }
|
---|
191 |
|
---|
192 | function destroy (stream, err) {
|
---|
193 | if (stream == null || !isStream(stream) || isDestroyed(stream)) {
|
---|
194 | return
|
---|
195 | }
|
---|
196 |
|
---|
197 | if (typeof stream.destroy === 'function') {
|
---|
198 | if (Object.getPrototypeOf(stream).constructor === IncomingMessage) {
|
---|
199 | // See: https://github.com/nodejs/node/pull/38505/files
|
---|
200 | stream.socket = null
|
---|
201 | }
|
---|
202 |
|
---|
203 | stream.destroy(err)
|
---|
204 | } else if (err) {
|
---|
205 | process.nextTick((stream, err) => {
|
---|
206 | stream.emit('error', err)
|
---|
207 | }, stream, err)
|
---|
208 | }
|
---|
209 |
|
---|
210 | if (stream.destroyed !== true) {
|
---|
211 | stream[kDestroyed] = true
|
---|
212 | }
|
---|
213 | }
|
---|
214 |
|
---|
215 | const KEEPALIVE_TIMEOUT_EXPR = /timeout=(\d+)/
|
---|
216 | function parseKeepAliveTimeout (val) {
|
---|
217 | const m = val.toString().match(KEEPALIVE_TIMEOUT_EXPR)
|
---|
218 | return m ? parseInt(m[1], 10) * 1000 : null
|
---|
219 | }
|
---|
220 |
|
---|
221 | function parseHeaders (headers, obj = {}) {
|
---|
222 | // For H2 support
|
---|
223 | if (!Array.isArray(headers)) return headers
|
---|
224 |
|
---|
225 | for (let i = 0; i < headers.length; i += 2) {
|
---|
226 | const key = headers[i].toString().toLowerCase()
|
---|
227 | let val = obj[key]
|
---|
228 |
|
---|
229 | if (!val) {
|
---|
230 | if (Array.isArray(headers[i + 1])) {
|
---|
231 | obj[key] = headers[i + 1].map(x => x.toString('utf8'))
|
---|
232 | } else {
|
---|
233 | obj[key] = headers[i + 1].toString('utf8')
|
---|
234 | }
|
---|
235 | } else {
|
---|
236 | if (!Array.isArray(val)) {
|
---|
237 | val = [val]
|
---|
238 | obj[key] = val
|
---|
239 | }
|
---|
240 | val.push(headers[i + 1].toString('utf8'))
|
---|
241 | }
|
---|
242 | }
|
---|
243 |
|
---|
244 | // See https://github.com/nodejs/node/pull/46528
|
---|
245 | if ('content-length' in obj && 'content-disposition' in obj) {
|
---|
246 | obj['content-disposition'] = Buffer.from(obj['content-disposition']).toString('latin1')
|
---|
247 | }
|
---|
248 |
|
---|
249 | return obj
|
---|
250 | }
|
---|
251 |
|
---|
252 | function parseRawHeaders (headers) {
|
---|
253 | const ret = []
|
---|
254 | let hasContentLength = false
|
---|
255 | let contentDispositionIdx = -1
|
---|
256 |
|
---|
257 | for (let n = 0; n < headers.length; n += 2) {
|
---|
258 | const key = headers[n + 0].toString()
|
---|
259 | const val = headers[n + 1].toString('utf8')
|
---|
260 |
|
---|
261 | if (key.length === 14 && (key === 'content-length' || key.toLowerCase() === 'content-length')) {
|
---|
262 | ret.push(key, val)
|
---|
263 | hasContentLength = true
|
---|
264 | } else if (key.length === 19 && (key === 'content-disposition' || key.toLowerCase() === 'content-disposition')) {
|
---|
265 | contentDispositionIdx = ret.push(key, val) - 1
|
---|
266 | } else {
|
---|
267 | ret.push(key, val)
|
---|
268 | }
|
---|
269 | }
|
---|
270 |
|
---|
271 | // See https://github.com/nodejs/node/pull/46528
|
---|
272 | if (hasContentLength && contentDispositionIdx !== -1) {
|
---|
273 | ret[contentDispositionIdx] = Buffer.from(ret[contentDispositionIdx]).toString('latin1')
|
---|
274 | }
|
---|
275 |
|
---|
276 | return ret
|
---|
277 | }
|
---|
278 |
|
---|
279 | function isBuffer (buffer) {
|
---|
280 | // See, https://github.com/mcollina/undici/pull/319
|
---|
281 | return buffer instanceof Uint8Array || Buffer.isBuffer(buffer)
|
---|
282 | }
|
---|
283 |
|
---|
284 | function validateHandler (handler, method, upgrade) {
|
---|
285 | if (!handler || typeof handler !== 'object') {
|
---|
286 | throw new InvalidArgumentError('handler must be an object')
|
---|
287 | }
|
---|
288 |
|
---|
289 | if (typeof handler.onConnect !== 'function') {
|
---|
290 | throw new InvalidArgumentError('invalid onConnect method')
|
---|
291 | }
|
---|
292 |
|
---|
293 | if (typeof handler.onError !== 'function') {
|
---|
294 | throw new InvalidArgumentError('invalid onError method')
|
---|
295 | }
|
---|
296 |
|
---|
297 | if (typeof handler.onBodySent !== 'function' && handler.onBodySent !== undefined) {
|
---|
298 | throw new InvalidArgumentError('invalid onBodySent method')
|
---|
299 | }
|
---|
300 |
|
---|
301 | if (upgrade || method === 'CONNECT') {
|
---|
302 | if (typeof handler.onUpgrade !== 'function') {
|
---|
303 | throw new InvalidArgumentError('invalid onUpgrade method')
|
---|
304 | }
|
---|
305 | } else {
|
---|
306 | if (typeof handler.onHeaders !== 'function') {
|
---|
307 | throw new InvalidArgumentError('invalid onHeaders method')
|
---|
308 | }
|
---|
309 |
|
---|
310 | if (typeof handler.onData !== 'function') {
|
---|
311 | throw new InvalidArgumentError('invalid onData method')
|
---|
312 | }
|
---|
313 |
|
---|
314 | if (typeof handler.onComplete !== 'function') {
|
---|
315 | throw new InvalidArgumentError('invalid onComplete method')
|
---|
316 | }
|
---|
317 | }
|
---|
318 | }
|
---|
319 |
|
---|
320 | // A body is disturbed if it has been read from and it cannot
|
---|
321 | // be re-used without losing state or data.
|
---|
322 | function isDisturbed (body) {
|
---|
323 | return !!(body && (
|
---|
324 | stream.isDisturbed
|
---|
325 | ? stream.isDisturbed(body) || body[kBodyUsed] // TODO (fix): Why is body[kBodyUsed] needed?
|
---|
326 | : body[kBodyUsed] ||
|
---|
327 | body.readableDidRead ||
|
---|
328 | (body._readableState && body._readableState.dataEmitted) ||
|
---|
329 | isReadableAborted(body)
|
---|
330 | ))
|
---|
331 | }
|
---|
332 |
|
---|
333 | function isErrored (body) {
|
---|
334 | return !!(body && (
|
---|
335 | stream.isErrored
|
---|
336 | ? stream.isErrored(body)
|
---|
337 | : /state: 'errored'/.test(nodeUtil.inspect(body)
|
---|
338 | )))
|
---|
339 | }
|
---|
340 |
|
---|
341 | function isReadable (body) {
|
---|
342 | return !!(body && (
|
---|
343 | stream.isReadable
|
---|
344 | ? stream.isReadable(body)
|
---|
345 | : /state: 'readable'/.test(nodeUtil.inspect(body)
|
---|
346 | )))
|
---|
347 | }
|
---|
348 |
|
---|
349 | function getSocketInfo (socket) {
|
---|
350 | return {
|
---|
351 | localAddress: socket.localAddress,
|
---|
352 | localPort: socket.localPort,
|
---|
353 | remoteAddress: socket.remoteAddress,
|
---|
354 | remotePort: socket.remotePort,
|
---|
355 | remoteFamily: socket.remoteFamily,
|
---|
356 | timeout: socket.timeout,
|
---|
357 | bytesWritten: socket.bytesWritten,
|
---|
358 | bytesRead: socket.bytesRead
|
---|
359 | }
|
---|
360 | }
|
---|
361 |
|
---|
362 | async function * convertIterableToBuffer (iterable) {
|
---|
363 | for await (const chunk of iterable) {
|
---|
364 | yield Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
|
---|
365 | }
|
---|
366 | }
|
---|
367 |
|
---|
368 | let ReadableStream
|
---|
369 | function ReadableStreamFrom (iterable) {
|
---|
370 | if (!ReadableStream) {
|
---|
371 | ReadableStream = require('stream/web').ReadableStream
|
---|
372 | }
|
---|
373 |
|
---|
374 | if (ReadableStream.from) {
|
---|
375 | return ReadableStream.from(convertIterableToBuffer(iterable))
|
---|
376 | }
|
---|
377 |
|
---|
378 | let iterator
|
---|
379 | return new ReadableStream(
|
---|
380 | {
|
---|
381 | async start () {
|
---|
382 | iterator = iterable[Symbol.asyncIterator]()
|
---|
383 | },
|
---|
384 | async pull (controller) {
|
---|
385 | const { done, value } = await iterator.next()
|
---|
386 | if (done) {
|
---|
387 | queueMicrotask(() => {
|
---|
388 | controller.close()
|
---|
389 | })
|
---|
390 | } else {
|
---|
391 | const buf = Buffer.isBuffer(value) ? value : Buffer.from(value)
|
---|
392 | controller.enqueue(new Uint8Array(buf))
|
---|
393 | }
|
---|
394 | return controller.desiredSize > 0
|
---|
395 | },
|
---|
396 | async cancel (reason) {
|
---|
397 | await iterator.return()
|
---|
398 | }
|
---|
399 | },
|
---|
400 | 0
|
---|
401 | )
|
---|
402 | }
|
---|
403 |
|
---|
404 | // The chunk should be a FormData instance and contains
|
---|
405 | // all the required methods.
|
---|
406 | function isFormDataLike (object) {
|
---|
407 | return (
|
---|
408 | object &&
|
---|
409 | typeof object === 'object' &&
|
---|
410 | typeof object.append === 'function' &&
|
---|
411 | typeof object.delete === 'function' &&
|
---|
412 | typeof object.get === 'function' &&
|
---|
413 | typeof object.getAll === 'function' &&
|
---|
414 | typeof object.has === 'function' &&
|
---|
415 | typeof object.set === 'function' &&
|
---|
416 | object[Symbol.toStringTag] === 'FormData'
|
---|
417 | )
|
---|
418 | }
|
---|
419 |
|
---|
420 | function throwIfAborted (signal) {
|
---|
421 | if (!signal) { return }
|
---|
422 | if (typeof signal.throwIfAborted === 'function') {
|
---|
423 | signal.throwIfAborted()
|
---|
424 | } else {
|
---|
425 | if (signal.aborted) {
|
---|
426 | // DOMException not available < v17.0.0
|
---|
427 | const err = new Error('The operation was aborted')
|
---|
428 | err.name = 'AbortError'
|
---|
429 | throw err
|
---|
430 | }
|
---|
431 | }
|
---|
432 | }
|
---|
433 |
|
---|
434 | function addAbortListener (signal, listener) {
|
---|
435 | if ('addEventListener' in signal) {
|
---|
436 | signal.addEventListener('abort', listener, { once: true })
|
---|
437 | return () => signal.removeEventListener('abort', listener)
|
---|
438 | }
|
---|
439 | signal.addListener('abort', listener)
|
---|
440 | return () => signal.removeListener('abort', listener)
|
---|
441 | }
|
---|
442 |
|
---|
443 | const hasToWellFormed = !!String.prototype.toWellFormed
|
---|
444 |
|
---|
445 | /**
|
---|
446 | * @param {string} val
|
---|
447 | */
|
---|
448 | function toUSVString (val) {
|
---|
449 | if (hasToWellFormed) {
|
---|
450 | return `${val}`.toWellFormed()
|
---|
451 | } else if (nodeUtil.toUSVString) {
|
---|
452 | return nodeUtil.toUSVString(val)
|
---|
453 | }
|
---|
454 |
|
---|
455 | return `${val}`
|
---|
456 | }
|
---|
457 |
|
---|
458 | // Parsed accordingly to RFC 9110
|
---|
459 | // https://www.rfc-editor.org/rfc/rfc9110#field.content-range
|
---|
460 | function parseRangeHeader (range) {
|
---|
461 | if (range == null || range === '') return { start: 0, end: null, size: null }
|
---|
462 |
|
---|
463 | const m = range ? range.match(/^bytes (\d+)-(\d+)\/(\d+)?$/) : null
|
---|
464 | return m
|
---|
465 | ? {
|
---|
466 | start: parseInt(m[1]),
|
---|
467 | end: m[2] ? parseInt(m[2]) : null,
|
---|
468 | size: m[3] ? parseInt(m[3]) : null
|
---|
469 | }
|
---|
470 | : null
|
---|
471 | }
|
---|
472 |
|
---|
473 | const kEnumerableProperty = Object.create(null)
|
---|
474 | kEnumerableProperty.enumerable = true
|
---|
475 |
|
---|
476 | module.exports = {
|
---|
477 | kEnumerableProperty,
|
---|
478 | nop,
|
---|
479 | isDisturbed,
|
---|
480 | isErrored,
|
---|
481 | isReadable,
|
---|
482 | toUSVString,
|
---|
483 | isReadableAborted,
|
---|
484 | isBlobLike,
|
---|
485 | parseOrigin,
|
---|
486 | parseURL,
|
---|
487 | getServerName,
|
---|
488 | isStream,
|
---|
489 | isIterable,
|
---|
490 | isAsyncIterable,
|
---|
491 | isDestroyed,
|
---|
492 | parseRawHeaders,
|
---|
493 | parseHeaders,
|
---|
494 | parseKeepAliveTimeout,
|
---|
495 | destroy,
|
---|
496 | bodyLength,
|
---|
497 | deepClone,
|
---|
498 | ReadableStreamFrom,
|
---|
499 | isBuffer,
|
---|
500 | validateHandler,
|
---|
501 | getSocketInfo,
|
---|
502 | isFormDataLike,
|
---|
503 | buildURL,
|
---|
504 | throwIfAborted,
|
---|
505 | addAbortListener,
|
---|
506 | parseRangeHeader,
|
---|
507 | nodeMajor,
|
---|
508 | nodeMinor,
|
---|
509 | nodeHasAutoSelectFamily: nodeMajor > 18 || (nodeMajor === 18 && nodeMinor >= 13),
|
---|
510 | safeHTTPMethods: ['GET', 'HEAD', 'OPTIONS', 'TRACE']
|
---|
511 | }
|
---|