source: node_modules/undici/lib/fetch/util.js

main
Last change on this file was d24f17c, checked in by Aleksandar Panovski <apano77@…>, 15 months ago

Initial commit

  • Property mode set to 100644
File size: 31.2 KB
Line 
1'use strict'
2
3const { redirectStatusSet, referrerPolicySet: referrerPolicyTokens, badPortsSet } = require('./constants')
4const { getGlobalOrigin } = require('./global')
5const { performance } = require('perf_hooks')
6const { isBlobLike, toUSVString, ReadableStreamFrom } = require('../core/util')
7const assert = require('assert')
8const { isUint8Array } = require('util/types')
9
10// https://nodejs.org/api/crypto.html#determining-if-crypto-support-is-unavailable
11/** @type {import('crypto')|undefined} */
12let crypto
13
14try {
15 crypto = require('crypto')
16} catch {
17
18}
19
20function responseURL (response) {
21 // https://fetch.spec.whatwg.org/#responses
22 // A response has an associated URL. It is a pointer to the last URL
23 // in response’s URL list and null if response’s URL list is empty.
24 const urlList = response.urlList
25 const length = urlList.length
26 return length === 0 ? null : urlList[length - 1].toString()
27}
28
29// https://fetch.spec.whatwg.org/#concept-response-location-url
30function responseLocationURL (response, requestFragment) {
31 // 1. If response’s status is not a redirect status, then return null.
32 if (!redirectStatusSet.has(response.status)) {
33 return null
34 }
35
36 // 2. Let location be the result of extracting header list values given
37 // `Location` and response’s header list.
38 let location = response.headersList.get('location')
39
40 // 3. If location is a header value, then set location to the result of
41 // parsing location with response’s URL.
42 if (location !== null && isValidHeaderValue(location)) {
43 location = new URL(location, responseURL(response))
44 }
45
46 // 4. If location is a URL whose fragment is null, then set location’s
47 // fragment to requestFragment.
48 if (location && !location.hash) {
49 location.hash = requestFragment
50 }
51
52 // 5. Return location.
53 return location
54}
55
56/** @returns {URL} */
57function requestCurrentURL (request) {
58 return request.urlList[request.urlList.length - 1]
59}
60
61function requestBadPort (request) {
62 // 1. Let url be request’s current URL.
63 const url = requestCurrentURL(request)
64
65 // 2. If url’s scheme is an HTTP(S) scheme and url’s port is a bad port,
66 // then return blocked.
67 if (urlIsHttpHttpsScheme(url) && badPortsSet.has(url.port)) {
68 return 'blocked'
69 }
70
71 // 3. Return allowed.
72 return 'allowed'
73}
74
75function isErrorLike (object) {
76 return object instanceof Error || (
77 object?.constructor?.name === 'Error' ||
78 object?.constructor?.name === 'DOMException'
79 )
80}
81
82// Check whether |statusText| is a ByteString and
83// matches the Reason-Phrase token production.
84// RFC 2616: https://tools.ietf.org/html/rfc2616
85// RFC 7230: https://tools.ietf.org/html/rfc7230
86// "reason-phrase = *( HTAB / SP / VCHAR / obs-text )"
87// https://github.com/chromium/chromium/blob/94.0.4604.1/third_party/blink/renderer/core/fetch/response.cc#L116
88function isValidReasonPhrase (statusText) {
89 for (let i = 0; i < statusText.length; ++i) {
90 const c = statusText.charCodeAt(i)
91 if (
92 !(
93 (
94 c === 0x09 || // HTAB
95 (c >= 0x20 && c <= 0x7e) || // SP / VCHAR
96 (c >= 0x80 && c <= 0xff)
97 ) // obs-text
98 )
99 ) {
100 return false
101 }
102 }
103 return true
104}
105
106/**
107 * @see https://tools.ietf.org/html/rfc7230#section-3.2.6
108 * @param {number} c
109 */
110function isTokenCharCode (c) {
111 switch (c) {
112 case 0x22:
113 case 0x28:
114 case 0x29:
115 case 0x2c:
116 case 0x2f:
117 case 0x3a:
118 case 0x3b:
119 case 0x3c:
120 case 0x3d:
121 case 0x3e:
122 case 0x3f:
123 case 0x40:
124 case 0x5b:
125 case 0x5c:
126 case 0x5d:
127 case 0x7b:
128 case 0x7d:
129 // DQUOTE and "(),/:;<=>?@[\]{}"
130 return false
131 default:
132 // VCHAR %x21-7E
133 return c >= 0x21 && c <= 0x7e
134 }
135}
136
137/**
138 * @param {string} characters
139 */
140function isValidHTTPToken (characters) {
141 if (characters.length === 0) {
142 return false
143 }
144 for (let i = 0; i < characters.length; ++i) {
145 if (!isTokenCharCode(characters.charCodeAt(i))) {
146 return false
147 }
148 }
149 return true
150}
151
152/**
153 * @see https://fetch.spec.whatwg.org/#header-name
154 * @param {string} potentialValue
155 */
156function isValidHeaderName (potentialValue) {
157 return isValidHTTPToken(potentialValue)
158}
159
160/**
161 * @see https://fetch.spec.whatwg.org/#header-value
162 * @param {string} potentialValue
163 */
164function isValidHeaderValue (potentialValue) {
165 // - Has no leading or trailing HTTP tab or space bytes.
166 // - Contains no 0x00 (NUL) or HTTP newline bytes.
167 if (
168 potentialValue.startsWith('\t') ||
169 potentialValue.startsWith(' ') ||
170 potentialValue.endsWith('\t') ||
171 potentialValue.endsWith(' ')
172 ) {
173 return false
174 }
175
176 if (
177 potentialValue.includes('\0') ||
178 potentialValue.includes('\r') ||
179 potentialValue.includes('\n')
180 ) {
181 return false
182 }
183
184 return true
185}
186
187// https://w3c.github.io/webappsec-referrer-policy/#set-requests-referrer-policy-on-redirect
188function setRequestReferrerPolicyOnRedirect (request, actualResponse) {
189 // Given a request request and a response actualResponse, this algorithm
190 // updates request’s referrer policy according to the Referrer-Policy
191 // header (if any) in actualResponse.
192
193 // 1. Let policy be the result of executing § 8.1 Parse a referrer policy
194 // from a Referrer-Policy header on actualResponse.
195
196 // 8.1 Parse a referrer policy from a Referrer-Policy header
197 // 1. Let policy-tokens be the result of extracting header list values given `Referrer-Policy` and response’s header list.
198 const { headersList } = actualResponse
199 // 2. Let policy be the empty string.
200 // 3. For each token in policy-tokens, if token is a referrer policy and token is not the empty string, then set policy to token.
201 // 4. Return policy.
202 const policyHeader = (headersList.get('referrer-policy') ?? '').split(',')
203
204 // Note: As the referrer-policy can contain multiple policies
205 // separated by comma, we need to loop through all of them
206 // and pick the first valid one.
207 // Ref: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy#specify_a_fallback_policy
208 let policy = ''
209 if (policyHeader.length > 0) {
210 // The right-most policy takes precedence.
211 // The left-most policy is the fallback.
212 for (let i = policyHeader.length; i !== 0; i--) {
213 const token = policyHeader[i - 1].trim()
214 if (referrerPolicyTokens.has(token)) {
215 policy = token
216 break
217 }
218 }
219 }
220
221 // 2. If policy is not the empty string, then set request’s referrer policy to policy.
222 if (policy !== '') {
223 request.referrerPolicy = policy
224 }
225}
226
227// https://fetch.spec.whatwg.org/#cross-origin-resource-policy-check
228function crossOriginResourcePolicyCheck () {
229 // TODO
230 return 'allowed'
231}
232
233// https://fetch.spec.whatwg.org/#concept-cors-check
234function corsCheck () {
235 // TODO
236 return 'success'
237}
238
239// https://fetch.spec.whatwg.org/#concept-tao-check
240function TAOCheck () {
241 // TODO
242 return 'success'
243}
244
245function appendFetchMetadata (httpRequest) {
246 // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-dest-header
247 // TODO
248
249 // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-mode-header
250
251 // 1. Assert: r’s url is a potentially trustworthy URL.
252 // TODO
253
254 // 2. Let header be a Structured Header whose value is a token.
255 let header = null
256
257 // 3. Set header’s value to r’s mode.
258 header = httpRequest.mode
259
260 // 4. Set a structured field value `Sec-Fetch-Mode`/header in r’s header list.
261 httpRequest.headersList.set('sec-fetch-mode', header)
262
263 // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-site-header
264 // TODO
265
266 // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-user-header
267 // TODO
268}
269
270// https://fetch.spec.whatwg.org/#append-a-request-origin-header
271function appendRequestOriginHeader (request) {
272 // 1. Let serializedOrigin be the result of byte-serializing a request origin with request.
273 let serializedOrigin = request.origin
274
275 // 2. If request’s response tainting is "cors" or request’s mode is "websocket", then append (`Origin`, serializedOrigin) to request’s header list.
276 if (request.responseTainting === 'cors' || request.mode === 'websocket') {
277 if (serializedOrigin) {
278 request.headersList.append('origin', serializedOrigin)
279 }
280
281 // 3. Otherwise, if request’s method is neither `GET` nor `HEAD`, then:
282 } else if (request.method !== 'GET' && request.method !== 'HEAD') {
283 // 1. Switch on request’s referrer policy:
284 switch (request.referrerPolicy) {
285 case 'no-referrer':
286 // Set serializedOrigin to `null`.
287 serializedOrigin = null
288 break
289 case 'no-referrer-when-downgrade':
290 case 'strict-origin':
291 case 'strict-origin-when-cross-origin':
292 // If request’s origin is a tuple origin, its scheme is "https", and request’s current URL’s scheme is not "https", then set serializedOrigin to `null`.
293 if (request.origin && urlHasHttpsScheme(request.origin) && !urlHasHttpsScheme(requestCurrentURL(request))) {
294 serializedOrigin = null
295 }
296 break
297 case 'same-origin':
298 // If request’s origin is not same origin with request’s current URL’s origin, then set serializedOrigin to `null`.
299 if (!sameOrigin(request, requestCurrentURL(request))) {
300 serializedOrigin = null
301 }
302 break
303 default:
304 // Do nothing.
305 }
306
307 if (serializedOrigin) {
308 // 2. Append (`Origin`, serializedOrigin) to request’s header list.
309 request.headersList.append('origin', serializedOrigin)
310 }
311 }
312}
313
314function coarsenedSharedCurrentTime (crossOriginIsolatedCapability) {
315 // TODO
316 return performance.now()
317}
318
319// https://fetch.spec.whatwg.org/#create-an-opaque-timing-info
320function createOpaqueTimingInfo (timingInfo) {
321 return {
322 startTime: timingInfo.startTime ?? 0,
323 redirectStartTime: 0,
324 redirectEndTime: 0,
325 postRedirectStartTime: timingInfo.startTime ?? 0,
326 finalServiceWorkerStartTime: 0,
327 finalNetworkResponseStartTime: 0,
328 finalNetworkRequestStartTime: 0,
329 endTime: 0,
330 encodedBodySize: 0,
331 decodedBodySize: 0,
332 finalConnectionTimingInfo: null
333 }
334}
335
336// https://html.spec.whatwg.org/multipage/origin.html#policy-container
337function makePolicyContainer () {
338 // Note: the fetch spec doesn't make use of embedder policy or CSP list
339 return {
340 referrerPolicy: 'strict-origin-when-cross-origin'
341 }
342}
343
344// https://html.spec.whatwg.org/multipage/origin.html#clone-a-policy-container
345function clonePolicyContainer (policyContainer) {
346 return {
347 referrerPolicy: policyContainer.referrerPolicy
348 }
349}
350
351// https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer
352function determineRequestsReferrer (request) {
353 // 1. Let policy be request's referrer policy.
354 const policy = request.referrerPolicy
355
356 // Note: policy cannot (shouldn't) be null or an empty string.
357 assert(policy)
358
359 // 2. Let environment be request’s client.
360
361 let referrerSource = null
362
363 // 3. Switch on request’s referrer:
364 if (request.referrer === 'client') {
365 // Note: node isn't a browser and doesn't implement document/iframes,
366 // so we bypass this step and replace it with our own.
367
368 const globalOrigin = getGlobalOrigin()
369
370 if (!globalOrigin || globalOrigin.origin === 'null') {
371 return 'no-referrer'
372 }
373
374 // note: we need to clone it as it's mutated
375 referrerSource = new URL(globalOrigin)
376 } else if (request.referrer instanceof URL) {
377 // Let referrerSource be request’s referrer.
378 referrerSource = request.referrer
379 }
380
381 // 4. Let request’s referrerURL be the result of stripping referrerSource for
382 // use as a referrer.
383 let referrerURL = stripURLForReferrer(referrerSource)
384
385 // 5. Let referrerOrigin be the result of stripping referrerSource for use as
386 // a referrer, with the origin-only flag set to true.
387 const referrerOrigin = stripURLForReferrer(referrerSource, true)
388
389 // 6. If the result of serializing referrerURL is a string whose length is
390 // greater than 4096, set referrerURL to referrerOrigin.
391 if (referrerURL.toString().length > 4096) {
392 referrerURL = referrerOrigin
393 }
394
395 const areSameOrigin = sameOrigin(request, referrerURL)
396 const isNonPotentiallyTrustWorthy = isURLPotentiallyTrustworthy(referrerURL) &&
397 !isURLPotentiallyTrustworthy(request.url)
398
399 // 8. Execute the switch statements corresponding to the value of policy:
400 switch (policy) {
401 case 'origin': return referrerOrigin != null ? referrerOrigin : stripURLForReferrer(referrerSource, true)
402 case 'unsafe-url': return referrerURL
403 case 'same-origin':
404 return areSameOrigin ? referrerOrigin : 'no-referrer'
405 case 'origin-when-cross-origin':
406 return areSameOrigin ? referrerURL : referrerOrigin
407 case 'strict-origin-when-cross-origin': {
408 const currentURL = requestCurrentURL(request)
409
410 // 1. If the origin of referrerURL and the origin of request’s current
411 // URL are the same, then return referrerURL.
412 if (sameOrigin(referrerURL, currentURL)) {
413 return referrerURL
414 }
415
416 // 2. If referrerURL is a potentially trustworthy URL and request’s
417 // current URL is not a potentially trustworthy URL, then return no
418 // referrer.
419 if (isURLPotentiallyTrustworthy(referrerURL) && !isURLPotentiallyTrustworthy(currentURL)) {
420 return 'no-referrer'
421 }
422
423 // 3. Return referrerOrigin.
424 return referrerOrigin
425 }
426 case 'strict-origin': // eslint-disable-line
427 /**
428 * 1. If referrerURL is a potentially trustworthy URL and
429 * request’s current URL is not a potentially trustworthy URL,
430 * then return no referrer.
431 * 2. Return referrerOrigin
432 */
433 case 'no-referrer-when-downgrade': // eslint-disable-line
434 /**
435 * 1. If referrerURL is a potentially trustworthy URL and
436 * request’s current URL is not a potentially trustworthy URL,
437 * then return no referrer.
438 * 2. Return referrerOrigin
439 */
440
441 default: // eslint-disable-line
442 return isNonPotentiallyTrustWorthy ? 'no-referrer' : referrerOrigin
443 }
444}
445
446/**
447 * @see https://w3c.github.io/webappsec-referrer-policy/#strip-url
448 * @param {URL} url
449 * @param {boolean|undefined} originOnly
450 */
451function stripURLForReferrer (url, originOnly) {
452 // 1. Assert: url is a URL.
453 assert(url instanceof URL)
454
455 // 2. If url’s scheme is a local scheme, then return no referrer.
456 if (url.protocol === 'file:' || url.protocol === 'about:' || url.protocol === 'blank:') {
457 return 'no-referrer'
458 }
459
460 // 3. Set url’s username to the empty string.
461 url.username = ''
462
463 // 4. Set url’s password to the empty string.
464 url.password = ''
465
466 // 5. Set url’s fragment to null.
467 url.hash = ''
468
469 // 6. If the origin-only flag is true, then:
470 if (originOnly) {
471 // 1. Set url’s path to « the empty string ».
472 url.pathname = ''
473
474 // 2. Set url’s query to null.
475 url.search = ''
476 }
477
478 // 7. Return url.
479 return url
480}
481
482function isURLPotentiallyTrustworthy (url) {
483 if (!(url instanceof URL)) {
484 return false
485 }
486
487 // If child of about, return true
488 if (url.href === 'about:blank' || url.href === 'about:srcdoc') {
489 return true
490 }
491
492 // If scheme is data, return true
493 if (url.protocol === 'data:') return true
494
495 // If file, return true
496 if (url.protocol === 'file:') return true
497
498 return isOriginPotentiallyTrustworthy(url.origin)
499
500 function isOriginPotentiallyTrustworthy (origin) {
501 // If origin is explicitly null, return false
502 if (origin == null || origin === 'null') return false
503
504 const originAsURL = new URL(origin)
505
506 // If secure, return true
507 if (originAsURL.protocol === 'https:' || originAsURL.protocol === 'wss:') {
508 return true
509 }
510
511 // If localhost or variants, return true
512 if (/^127(?:\.[0-9]+){0,2}\.[0-9]+$|^\[(?:0*:)*?:?0*1\]$/.test(originAsURL.hostname) ||
513 (originAsURL.hostname === 'localhost' || originAsURL.hostname.includes('localhost.')) ||
514 (originAsURL.hostname.endsWith('.localhost'))) {
515 return true
516 }
517
518 // If any other, return false
519 return false
520 }
521}
522
523/**
524 * @see https://w3c.github.io/webappsec-subresource-integrity/#does-response-match-metadatalist
525 * @param {Uint8Array} bytes
526 * @param {string} metadataList
527 */
528function bytesMatch (bytes, metadataList) {
529 // If node is not built with OpenSSL support, we cannot check
530 // a request's integrity, so allow it by default (the spec will
531 // allow requests if an invalid hash is given, as precedence).
532 /* istanbul ignore if: only if node is built with --without-ssl */
533 if (crypto === undefined) {
534 return true
535 }
536
537 // 1. Let parsedMetadata be the result of parsing metadataList.
538 const parsedMetadata = parseMetadata(metadataList)
539
540 // 2. If parsedMetadata is no metadata, return true.
541 if (parsedMetadata === 'no metadata') {
542 return true
543 }
544
545 // 3. If parsedMetadata is the empty set, return true.
546 if (parsedMetadata.length === 0) {
547 return true
548 }
549
550 // 4. Let metadata be the result of getting the strongest
551 // metadata from parsedMetadata.
552 const list = parsedMetadata.sort((c, d) => d.algo.localeCompare(c.algo))
553 // get the strongest algorithm
554 const strongest = list[0].algo
555 // get all entries that use the strongest algorithm; ignore weaker
556 const metadata = list.filter((item) => item.algo === strongest)
557
558 // 5. For each item in metadata:
559 for (const item of metadata) {
560 // 1. Let algorithm be the alg component of item.
561 const algorithm = item.algo
562
563 // 2. Let expectedValue be the val component of item.
564 let expectedValue = item.hash
565
566 // See https://github.com/web-platform-tests/wpt/commit/e4c5cc7a5e48093220528dfdd1c4012dc3837a0e
567 // "be liberal with padding". This is annoying, and it's not even in the spec.
568
569 if (expectedValue.endsWith('==')) {
570 expectedValue = expectedValue.slice(0, -2)
571 }
572
573 // 3. Let actualValue be the result of applying algorithm to bytes.
574 let actualValue = crypto.createHash(algorithm).update(bytes).digest('base64')
575
576 if (actualValue.endsWith('==')) {
577 actualValue = actualValue.slice(0, -2)
578 }
579
580 // 4. If actualValue is a case-sensitive match for expectedValue,
581 // return true.
582 if (actualValue === expectedValue) {
583 return true
584 }
585
586 let actualBase64URL = crypto.createHash(algorithm).update(bytes).digest('base64url')
587
588 if (actualBase64URL.endsWith('==')) {
589 actualBase64URL = actualBase64URL.slice(0, -2)
590 }
591
592 if (actualBase64URL === expectedValue) {
593 return true
594 }
595 }
596
597 // 6. Return false.
598 return false
599}
600
601// https://w3c.github.io/webappsec-subresource-integrity/#grammardef-hash-with-options
602// https://www.w3.org/TR/CSP2/#source-list-syntax
603// https://www.rfc-editor.org/rfc/rfc5234#appendix-B.1
604const parseHashWithOptions = /((?<algo>sha256|sha384|sha512)-(?<hash>[A-z0-9+/]{1}.*={0,2}))( +[\x21-\x7e]?)?/i
605
606/**
607 * @see https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata
608 * @param {string} metadata
609 */
610function parseMetadata (metadata) {
611 // 1. Let result be the empty set.
612 /** @type {{ algo: string, hash: string }[]} */
613 const result = []
614
615 // 2. Let empty be equal to true.
616 let empty = true
617
618 const supportedHashes = crypto.getHashes()
619
620 // 3. For each token returned by splitting metadata on spaces:
621 for (const token of metadata.split(' ')) {
622 // 1. Set empty to false.
623 empty = false
624
625 // 2. Parse token as a hash-with-options.
626 const parsedToken = parseHashWithOptions.exec(token)
627
628 // 3. If token does not parse, continue to the next token.
629 if (parsedToken === null || parsedToken.groups === undefined) {
630 // Note: Chromium blocks the request at this point, but Firefox
631 // gives a warning that an invalid integrity was given. The
632 // correct behavior is to ignore these, and subsequently not
633 // check the integrity of the resource.
634 continue
635 }
636
637 // 4. Let algorithm be the hash-algo component of token.
638 const algorithm = parsedToken.groups.algo
639
640 // 5. If algorithm is a hash function recognized by the user
641 // agent, add the parsed token to result.
642 if (supportedHashes.includes(algorithm.toLowerCase())) {
643 result.push(parsedToken.groups)
644 }
645 }
646
647 // 4. Return no metadata if empty is true, otherwise return result.
648 if (empty === true) {
649 return 'no metadata'
650 }
651
652 return result
653}
654
655// https://w3c.github.io/webappsec-upgrade-insecure-requests/#upgrade-request
656function tryUpgradeRequestToAPotentiallyTrustworthyURL (request) {
657 // TODO
658}
659
660/**
661 * @link {https://html.spec.whatwg.org/multipage/origin.html#same-origin}
662 * @param {URL} A
663 * @param {URL} B
664 */
665function sameOrigin (A, B) {
666 // 1. If A and B are the same opaque origin, then return true.
667 if (A.origin === B.origin && A.origin === 'null') {
668 return true
669 }
670
671 // 2. If A and B are both tuple origins and their schemes,
672 // hosts, and port are identical, then return true.
673 if (A.protocol === B.protocol && A.hostname === B.hostname && A.port === B.port) {
674 return true
675 }
676
677 // 3. Return false.
678 return false
679}
680
681function createDeferredPromise () {
682 let res
683 let rej
684 const promise = new Promise((resolve, reject) => {
685 res = resolve
686 rej = reject
687 })
688
689 return { promise, resolve: res, reject: rej }
690}
691
692function isAborted (fetchParams) {
693 return fetchParams.controller.state === 'aborted'
694}
695
696function isCancelled (fetchParams) {
697 return fetchParams.controller.state === 'aborted' ||
698 fetchParams.controller.state === 'terminated'
699}
700
701const normalizeMethodRecord = {
702 delete: 'DELETE',
703 DELETE: 'DELETE',
704 get: 'GET',
705 GET: 'GET',
706 head: 'HEAD',
707 HEAD: 'HEAD',
708 options: 'OPTIONS',
709 OPTIONS: 'OPTIONS',
710 post: 'POST',
711 POST: 'POST',
712 put: 'PUT',
713 PUT: 'PUT'
714}
715
716// Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`.
717Object.setPrototypeOf(normalizeMethodRecord, null)
718
719/**
720 * @see https://fetch.spec.whatwg.org/#concept-method-normalize
721 * @param {string} method
722 */
723function normalizeMethod (method) {
724 return normalizeMethodRecord[method.toLowerCase()] ?? method
725}
726
727// https://infra.spec.whatwg.org/#serialize-a-javascript-value-to-a-json-string
728function serializeJavascriptValueToJSONString (value) {
729 // 1. Let result be ? Call(%JSON.stringify%, undefined, « value »).
730 const result = JSON.stringify(value)
731
732 // 2. If result is undefined, then throw a TypeError.
733 if (result === undefined) {
734 throw new TypeError('Value is not JSON serializable')
735 }
736
737 // 3. Assert: result is a string.
738 assert(typeof result === 'string')
739
740 // 4. Return result.
741 return result
742}
743
744// https://tc39.es/ecma262/#sec-%25iteratorprototype%25-object
745const esIteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]()))
746
747/**
748 * @see https://webidl.spec.whatwg.org/#dfn-iterator-prototype-object
749 * @param {() => unknown[]} iterator
750 * @param {string} name name of the instance
751 * @param {'key'|'value'|'key+value'} kind
752 */
753function makeIterator (iterator, name, kind) {
754 const object = {
755 index: 0,
756 kind,
757 target: iterator
758 }
759
760 const i = {
761 next () {
762 // 1. Let interface be the interface for which the iterator prototype object exists.
763
764 // 2. Let thisValue be the this value.
765
766 // 3. Let object be ? ToObject(thisValue).
767
768 // 4. If object is a platform object, then perform a security
769 // check, passing:
770
771 // 5. If object is not a default iterator object for interface,
772 // then throw a TypeError.
773 if (Object.getPrototypeOf(this) !== i) {
774 throw new TypeError(
775 `'next' called on an object that does not implement interface ${name} Iterator.`
776 )
777 }
778
779 // 6. Let index be object’s index.
780 // 7. Let kind be object’s kind.
781 // 8. Let values be object’s target's value pairs to iterate over.
782 const { index, kind, target } = object
783 const values = target()
784
785 // 9. Let len be the length of values.
786 const len = values.length
787
788 // 10. If index is greater than or equal to len, then return
789 // CreateIterResultObject(undefined, true).
790 if (index >= len) {
791 return { value: undefined, done: true }
792 }
793
794 // 11. Let pair be the entry in values at index index.
795 const pair = values[index]
796
797 // 12. Set object’s index to index + 1.
798 object.index = index + 1
799
800 // 13. Return the iterator result for pair and kind.
801 return iteratorResult(pair, kind)
802 },
803 // The class string of an iterator prototype object for a given interface is the
804 // result of concatenating the identifier of the interface and the string " Iterator".
805 [Symbol.toStringTag]: `${name} Iterator`
806 }
807
808 // The [[Prototype]] internal slot of an iterator prototype object must be %IteratorPrototype%.
809 Object.setPrototypeOf(i, esIteratorPrototype)
810 // esIteratorPrototype needs to be the prototype of i
811 // which is the prototype of an empty object. Yes, it's confusing.
812 return Object.setPrototypeOf({}, i)
813}
814
815// https://webidl.spec.whatwg.org/#iterator-result
816function iteratorResult (pair, kind) {
817 let result
818
819 // 1. Let result be a value determined by the value of kind:
820 switch (kind) {
821 case 'key': {
822 // 1. Let idlKey be pair’s key.
823 // 2. Let key be the result of converting idlKey to an
824 // ECMAScript value.
825 // 3. result is key.
826 result = pair[0]
827 break
828 }
829 case 'value': {
830 // 1. Let idlValue be pair’s value.
831 // 2. Let value be the result of converting idlValue to
832 // an ECMAScript value.
833 // 3. result is value.
834 result = pair[1]
835 break
836 }
837 case 'key+value': {
838 // 1. Let idlKey be pair’s key.
839 // 2. Let idlValue be pair’s value.
840 // 3. Let key be the result of converting idlKey to an
841 // ECMAScript value.
842 // 4. Let value be the result of converting idlValue to
843 // an ECMAScript value.
844 // 5. Let array be ! ArrayCreate(2).
845 // 6. Call ! CreateDataProperty(array, "0", key).
846 // 7. Call ! CreateDataProperty(array, "1", value).
847 // 8. result is array.
848 result = pair
849 break
850 }
851 }
852
853 // 2. Return CreateIterResultObject(result, false).
854 return { value: result, done: false }
855}
856
857/**
858 * @see https://fetch.spec.whatwg.org/#body-fully-read
859 */
860async function fullyReadBody (body, processBody, processBodyError) {
861 // 1. If taskDestination is null, then set taskDestination to
862 // the result of starting a new parallel queue.
863
864 // 2. Let successSteps given a byte sequence bytes be to queue a
865 // fetch task to run processBody given bytes, with taskDestination.
866 const successSteps = processBody
867
868 // 3. Let errorSteps be to queue a fetch task to run processBodyError,
869 // with taskDestination.
870 const errorSteps = processBodyError
871
872 // 4. Let reader be the result of getting a reader for body’s stream.
873 // If that threw an exception, then run errorSteps with that
874 // exception and return.
875 let reader
876
877 try {
878 reader = body.stream.getReader()
879 } catch (e) {
880 errorSteps(e)
881 return
882 }
883
884 // 5. Read all bytes from reader, given successSteps and errorSteps.
885 try {
886 const result = await readAllBytes(reader)
887 successSteps(result)
888 } catch (e) {
889 errorSteps(e)
890 }
891}
892
893/** @type {ReadableStream} */
894let ReadableStream = globalThis.ReadableStream
895
896function isReadableStreamLike (stream) {
897 if (!ReadableStream) {
898 ReadableStream = require('stream/web').ReadableStream
899 }
900
901 return stream instanceof ReadableStream || (
902 stream[Symbol.toStringTag] === 'ReadableStream' &&
903 typeof stream.tee === 'function'
904 )
905}
906
907const MAXIMUM_ARGUMENT_LENGTH = 65535
908
909/**
910 * @see https://infra.spec.whatwg.org/#isomorphic-decode
911 * @param {number[]|Uint8Array} input
912 */
913function isomorphicDecode (input) {
914 // 1. To isomorphic decode a byte sequence input, return a string whose code point
915 // length is equal to input’s length and whose code points have the same values
916 // as the values of input’s bytes, in the same order.
917
918 if (input.length < MAXIMUM_ARGUMENT_LENGTH) {
919 return String.fromCharCode(...input)
920 }
921
922 return input.reduce((previous, current) => previous + String.fromCharCode(current), '')
923}
924
925/**
926 * @param {ReadableStreamController<Uint8Array>} controller
927 */
928function readableStreamClose (controller) {
929 try {
930 controller.close()
931 } catch (err) {
932 // TODO: add comment explaining why this error occurs.
933 if (!err.message.includes('Controller is already closed')) {
934 throw err
935 }
936 }
937}
938
939/**
940 * @see https://infra.spec.whatwg.org/#isomorphic-encode
941 * @param {string} input
942 */
943function isomorphicEncode (input) {
944 // 1. Assert: input contains no code points greater than U+00FF.
945 for (let i = 0; i < input.length; i++) {
946 assert(input.charCodeAt(i) <= 0xFF)
947 }
948
949 // 2. Return a byte sequence whose length is equal to input’s code
950 // point length and whose bytes have the same values as the
951 // values of input’s code points, in the same order
952 return input
953}
954
955/**
956 * @see https://streams.spec.whatwg.org/#readablestreamdefaultreader-read-all-bytes
957 * @see https://streams.spec.whatwg.org/#read-loop
958 * @param {ReadableStreamDefaultReader} reader
959 */
960async function readAllBytes (reader) {
961 const bytes = []
962 let byteLength = 0
963
964 while (true) {
965 const { done, value: chunk } = await reader.read()
966
967 if (done) {
968 // 1. Call successSteps with bytes.
969 return Buffer.concat(bytes, byteLength)
970 }
971
972 // 1. If chunk is not a Uint8Array object, call failureSteps
973 // with a TypeError and abort these steps.
974 if (!isUint8Array(chunk)) {
975 throw new TypeError('Received non-Uint8Array chunk')
976 }
977
978 // 2. Append the bytes represented by chunk to bytes.
979 bytes.push(chunk)
980 byteLength += chunk.length
981
982 // 3. Read-loop given reader, bytes, successSteps, and failureSteps.
983 }
984}
985
986/**
987 * @see https://fetch.spec.whatwg.org/#is-local
988 * @param {URL} url
989 */
990function urlIsLocal (url) {
991 assert('protocol' in url) // ensure it's a url object
992
993 const protocol = url.protocol
994
995 return protocol === 'about:' || protocol === 'blob:' || protocol === 'data:'
996}
997
998/**
999 * @param {string|URL} url
1000 */
1001function urlHasHttpsScheme (url) {
1002 if (typeof url === 'string') {
1003 return url.startsWith('https:')
1004 }
1005
1006 return url.protocol === 'https:'
1007}
1008
1009/**
1010 * @see https://fetch.spec.whatwg.org/#http-scheme
1011 * @param {URL} url
1012 */
1013function urlIsHttpHttpsScheme (url) {
1014 assert('protocol' in url) // ensure it's a url object
1015
1016 const protocol = url.protocol
1017
1018 return protocol === 'http:' || protocol === 'https:'
1019}
1020
1021/**
1022 * Fetch supports node >= 16.8.0, but Object.hasOwn was added in v16.9.0.
1023 */
1024const hasOwn = Object.hasOwn || ((dict, key) => Object.prototype.hasOwnProperty.call(dict, key))
1025
1026module.exports = {
1027 isAborted,
1028 isCancelled,
1029 createDeferredPromise,
1030 ReadableStreamFrom,
1031 toUSVString,
1032 tryUpgradeRequestToAPotentiallyTrustworthyURL,
1033 coarsenedSharedCurrentTime,
1034 determineRequestsReferrer,
1035 makePolicyContainer,
1036 clonePolicyContainer,
1037 appendFetchMetadata,
1038 appendRequestOriginHeader,
1039 TAOCheck,
1040 corsCheck,
1041 crossOriginResourcePolicyCheck,
1042 createOpaqueTimingInfo,
1043 setRequestReferrerPolicyOnRedirect,
1044 isValidHTTPToken,
1045 requestBadPort,
1046 requestCurrentURL,
1047 responseURL,
1048 responseLocationURL,
1049 isBlobLike,
1050 isURLPotentiallyTrustworthy,
1051 isValidReasonPhrase,
1052 sameOrigin,
1053 normalizeMethod,
1054 serializeJavascriptValueToJSONString,
1055 makeIterator,
1056 isValidHeaderName,
1057 isValidHeaderValue,
1058 hasOwn,
1059 isErrorLike,
1060 fullyReadBody,
1061 bytesMatch,
1062 isReadableStreamLike,
1063 readableStreamClose,
1064 isomorphicEncode,
1065 isomorphicDecode,
1066 urlIsLocal,
1067 urlHasHttpsScheme,
1068 urlIsHttpHttpsScheme,
1069 readAllBytes,
1070 normalizeMethodRecord
1071}
Note: See TracBrowser for help on using the repository browser.