| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 8a921fe commit 0c4953c
13 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -18,7 +18,7 @@ Returns: `Client` | |||
| 18 | 18 | ### Parameter: `ClientOptions` | |
| 19 | 19 | ||
| 20 | 20 | * **bodyTimeout** `number | null` (optional) - Default: `30e3` - The timeout after which a request will time out, in milliseconds. Monitors time between receiving body data. Use `0` to disable it entirely. Defaults to 30 seconds. | |
| 21 | - * **headersTimeout** `number | null` (optional) - Default: `30e3` - The amount of time the parser will wait to receive the complete HTTP headers. Defaults to 30 seconds. | ||
| 21 | + * **headersTimeout** `number | null` (optional) - Default: `30e3` - The amount of time the parser will wait to receive the complete HTTP headers while not sending the request. Defaults to 30 seconds. | ||
| 22 | 22 | * **keepAliveMaxTimeout** `number | null` (optional) - Default: `600e3` - The maximum allowed `keepAliveTimeout` when overridden by *keep-alive* hints from the server. Defaults to 10 minutes. | |
| 23 | 23 | * **keepAliveTimeout** `number | null` (optional) - Default: `4e3` - The timeout after which a socket without active requests will time out. Monitors time between activity on a connected socket. This value may be overridden by *keep-alive* hints from the server. See [MDN: HTTP - Headers - Keep-Alive directives](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Keep-Alive#directives) for more details. Defaults to 4 seconds. | |
| 24 | 24 | * **keepAliveTimeoutThreshold** `number | null` (optional) - Default: `1e3` - A number subtracted from server *keep-alive* hints when overriding `keepAliveTimeout` to account for timing inaccuracies caused by e.g. transport latency. Defaults to 1 second. | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -199,7 +199,7 @@ Returns: `Boolean` - `false` if dispatcher is busy and further dispatch calls wo | |||
| 199 | 199 | * **blocking** `boolean` (optional) - Default: `false` - Whether the response is expected to take a long time and would end up blocking the pipeline. When this is set to `true` further pipelining will be avoided on the same connection until headers have been received. | |
| 200 | 200 | * **upgrade** `string | null` (optional) - Default: `null` - Upgrade the request. Should be used to specify the kind of upgrade i.e. `'Websocket'`. | |
| 201 | 201 | * **bodyTimeout** `number | null` (optional) - The timeout after which a request will time out, in milliseconds. Monitors time between receiving body data. Use `0` to disable it entirely. Defaults to 30 seconds. | |
| 202 | - * **headersTimeout** `number | null` (optional) - The amount of time the parser will wait to receive the complete HTTP headers. Defaults to 30 seconds. | ||
| 202 | + * **headersTimeout** `number | null` (optional) - The amount of time the parser will wait to receive the complete HTTP headers while not sending the request. Defaults to 30 seconds. | ||
| 203 | 203 | * **throwOnError** `boolean` (optional) - Default: `false` - Whether Undici should throw an error upon receiving a 4xx or 5xx response from the server. | |
| 204 | 204 | ||
| 205 | 205 | #### Parameter: `DispatchHandler` | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -93,7 +93,7 @@ module.exports = class BodyReadable extends Readable { | |||
| 93 | 93 | } | |
| 94 | 94 | ||
| 95 | 95 | push (chunk) { | |
| 96 | - if (this[kConsume] && chunk !== null) { | ||
| 96 | + if (this[kConsume] && chunk !== null && this.readableLength === 0) { | ||
| 97 | 97 | consumePush(this[kConsume], chunk) | |
| 98 | 98 | return this[kReading] ? super.push(chunk) : true | |
| 99 | 99 | } | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -889,8 +889,10 @@ function onParserTimeout (parser) { | |||
| 889 | 889 | ||
| 890 | 890 | /* istanbul ignore else */ | |
| 891 | 891 | if (timeoutType === TIMEOUT_HEADERS) { | |
| 892 | - assert(!parser.paused, 'cannot be paused while waiting for headers') | ||
| 893 | - util.destroy(socket, new HeadersTimeoutError()) | ||
| 892 | + if (!socket[kWriting] || socket.writableNeedDrain || client[kRunning] > 1) { | ||
| 893 | + assert(!parser.paused, 'cannot be paused while waiting for headers') | ||
| 894 | + util.destroy(socket, new HeadersTimeoutError()) | ||
| 895 | + } | ||
| 894 | 896 | } else if (timeoutType === TIMEOUT_BODY) { | |
| 895 | 897 | if (!parser.paused) { | |
| 896 | 898 | util.destroy(socket, new BodyTimeoutError()) | |
@@ -1641,7 +1643,18 @@ class AsyncWriter { | |||
| 1641 | 1643 | this.bytesWritten += len | |
| 1642 | 1644 | ||
| 1643 | 1645 | const ret = socket.write(chunk) | |
| 1646 | + | ||
| 1644 | 1647 | request.onBodySent(chunk) | |
| 1648 | + | ||
| 1649 | + if (!ret) { | ||
| 1650 | + if (socket[kParser].timeout && socket[kParser].timeoutType === TIMEOUT_HEADERS) { | ||
| 1651 | + // istanbul ignore else: only for jest | ||
| 1652 | + if (socket[kParser].timeout.refresh) { | ||
| 1653 | + socket[kParser].timeout.refresh() | ||
| 1654 | + } | ||
| 1655 | + } | ||
| 1656 | + } | ||
| 1657 | + | ||
| 1645 | 1658 | return ret | |
| 1646 | 1659 | } | |
| 1647 | 1660 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -244,7 +244,11 @@ function parseHeaders (headers, obj = {}) { | |||
| 244 | 244 | const key = headers[i].toString().toLowerCase() | |
| 245 | 245 | let val = obj[key] | |
| 246 | 246 | if (!val) { | |
| 247 | - obj[key] = headers[i + 1].toString() | ||
| 247 | + if (Array.isArray(headers[i + 1])) { | ||
| 248 | + obj[key] = headers[i + 1] | ||
| 249 | + } else { | ||
| 250 | + obj[key] = headers[i + 1].toString() | ||
| 251 | + } | ||
| 248 | 252 | } else { | |
| 249 | 253 | if (!Array.isArray(val)) { | |
| 250 | 254 | val = [val] | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -57,16 +57,16 @@ function extractBody (object, keepalive = false) { | |||
| 57 | 57 | ||
| 58 | 58 | // Set Content-Type to `application/x-www-form-urlencoded;charset=UTF-8`. | |
| 59 | 59 | contentType = 'application/x-www-form-urlencoded;charset=UTF-8' | |
| 60 | - } else if (isArrayBuffer(object) || ArrayBuffer.isView(object)) { | ||
| 61 | - // BufferSource | ||
| 60 | + } else if (isArrayBuffer(object)) { | ||
| 61 | + // BufferSource/ArrayBuffer | ||
| 62 | 62 | ||
| 63 | - if (object instanceof DataView) { | ||
| 64 | - // TODO: Blob doesn't seem to work with DataView? | ||
| 65 | - object = object.buffer | ||
| 66 | - } | ||
| 63 | + // Set source to a copy of the bytes held by object. | ||
| 64 | + source = new Uint8Array(object.slice()) | ||
| 65 | + } else if (ArrayBuffer.isView(object)) { | ||
| 66 | + // BufferSource/ArrayBufferView | ||
| 67 | 67 | ||
| 68 | 68 | // Set source to a copy of the bytes held by object. | |
| 69 | - source = new Uint8Array(object) | ||
| 69 | + source = new Uint8Array(object.buffer.slice(object.byteOffset, object.byteOffset + object.byteLength)) | ||
| 70 | 70 | } else if (util.isFormDataLike(object)) { | |
| 71 | 71 | const boundary = '----formdata-undici-' + Math.random() | |
| 72 | 72 | const prefix = `--${boundary}\r\nContent-Disposition: form-data` | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -278,7 +278,9 @@ function processBlobParts (parts, options) { | |||
| 278 | 278 | if (!element.buffer) { // ArrayBuffer | |
| 279 | 279 | bytes.push(new Uint8Array(element)) | |
| 280 | 280 | } else { | |
| 281 | - bytes.push(element.buffer) | ||
| 281 | + bytes.push( | ||
| 282 | + new Uint8Array(element.buffer, element.byteOffset, element.byteLength) | ||
| 283 | + ) | ||
| 282 | 284 | } | |
| 283 | 285 | } else if (isBlobLike(element)) { | |
| 284 | 286 | // 3. If element is a Blob, append the bytes it represents | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -13,7 +13,7 @@ const { Headers } = require('./headers') | |||
| 13 | 13 | const { Request, makeRequest } = require('./request') | |
| 14 | 14 | const zlib = require('zlib') | |
| 15 | 15 | const { | |
| 16 | - matchRequestIntegrity, | ||
| 16 | + bytesMatch, | ||
| 17 | 17 | makePolicyContainer, | |
| 18 | 18 | clonePolicyContainer, | |
| 19 | 19 | requestBadPort, | |
@@ -34,7 +34,8 @@ const { | |||
| 34 | 34 | sameOrigin, | |
| 35 | 35 | isCancelled, | |
| 36 | 36 | isAborted, | |
| 37 | - isErrorLike | ||
| 37 | + isErrorLike, | ||
| 38 | + fullyReadBody | ||
| 38 | 39 | } = require('./util') | |
| 39 | 40 | const { kState, kHeaders, kGuard, kRealm } = require('./symbols') | |
| 40 | 41 | const assert = require('assert') | |
@@ -724,7 +725,7 @@ async function mainFetch (fetchParams, recursive = false) { | |||
| 724 | 725 | const processBody = (bytes) => { | |
| 725 | 726 | // 1. If bytes do not match request’s integrity metadata, | |
| 726 | 727 | // then run processBodyError and abort these steps. [SRI] | |
| 727 | - if (!matchRequestIntegrity(request, bytes)) { | ||
| 728 | + if (!bytesMatch(bytes, request.integrity)) { | ||
| 728 | 729 | processBodyError('integrity mismatch') | |
| 729 | 730 | return | |
| 730 | 731 | } | |
@@ -738,11 +739,7 @@ async function mainFetch (fetchParams, recursive = false) { | |||
| 738 | 739 | } | |
| 739 | 740 | ||
| 740 | 741 | // 4. Fully read response’s body given processBody and processBodyError. | |
| 741 | - try { | ||
| 742 | - processBody(await response.arrayBuffer()) | ||
| 743 | - } catch (err) { | ||
| 744 | - processBodyError(err) | ||
| 745 | - } | ||
| 742 | + await fullyReadBody(response.body, processBody, processBodyError) | ||
| 746 | 743 | } else { | |
| 747 | 744 | // 21. Otherwise, run fetch finale given fetchParams and response. | |
| 748 | 745 | fetchFinale(fetchParams, response) | |
@@ -974,11 +971,7 @@ async function fetchFinale (fetchParams, response) { | |||
| 974 | 971 | } else { | |
| 975 | 972 | // 4. Otherwise, fully read response’s body given processBody, processBodyError, | |
| 976 | 973 | // and fetchParams’s task destination. | |
| 977 | - try { | ||
| 978 | - processBody(await response.body.stream.arrayBuffer()) | ||
| 979 | - } catch (err) { | ||
| 980 | - processBodyError(err) | ||
| 981 | - } | ||
| 974 | + await fullyReadBody(response.body, processBody, processBodyError) | ||
| 982 | 975 | } | |
| 983 | 976 | } | |
| 984 | 977 | } | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -4,6 +4,7 @@ | |||
| 4 | 4 | ||
| 5 | 5 | const { extractBody, mixinBody, cloneBody } = require('./body') | |
| 6 | 6 | const { Headers, fill: fillHeaders, HeadersList } = require('./headers') | |
| 7 | + const { FinalizationRegistry } = require('../compat/dispatcher-weakref')() | ||
| 7 | 8 | const util = require('../core/util') | |
| 8 | 9 | const { | |
| 9 | 10 | isValidHTTPToken, | |
@@ -914,7 +915,10 @@ webidl.converters.RequestInit = webidl.dictionaryConverter([ | |||
| 914 | 915 | { | |
| 915 | 916 | key: 'signal', | |
| 916 | 917 | converter: webidl.nullableConverter( | |
| 917 | - webidl.converters.AbortSignal | ||
| 918 | + (signal) => webidl.converters.AbortSignal( | ||
| 919 | + signal, | ||
| 920 | + { strict: false } | ||
| 921 | + ) | ||
| 918 | 922 | ) | |
| 919 | 923 | }, | |
| 920 | 924 | { | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -4,9 +4,20 @@ const { redirectStatus } = require('./constants') | |||
| 4 | 4 | const { performance } = require('perf_hooks') | |
| 5 | 5 | const { isBlobLike, toUSVString, ReadableStreamFrom } = require('../core/util') | |
| 6 | 6 | const assert = require('assert') | |
| 7 | + const { isUint8Array } = require('util/types') | ||
| 7 | 8 | ||
| 8 | 9 | let File | |
| 9 | 10 | ||
| 11 | + // https://nodejs.org/api/crypto.html#determining-if-crypto-support-is-unavailable | ||
| 12 | + /** @type {import('crypto')|undefined} */ | ||
| 13 | + let crypto | ||
| 14 | + | ||
| 15 | + try { | ||
| 16 | + crypto = require('crypto') | ||
| 17 | + } catch { | ||
| 18 | + | ||
| 19 | + } | ||
| 20 | + | ||
| 10 | 21 | // https://fetch.spec.whatwg.org/#block-bad-port | |
| 11 | 22 | const badPorts = [ | |
| 12 | 23 | '1', '7', '9', '11', '13', '15', '17', '19', '20', '21', '22', '23', '25', '37', '42', '43', '53', '69', '77', '79', | |
@@ -339,10 +350,116 @@ function determineRequestsReferrer (request) { | |||
| 339 | 350 | return 'no-referrer' | |
| 340 | 351 | } | |
| 341 | 352 | ||
| 342 | - function matchRequestIntegrity (request, bytes) { | ||
| 353 | + /** | ||
| 354 | + * @see https://w3c.github.io/webappsec-subresource-integrity/#does-response-match-metadatalist | ||
| 355 | + * @param {Uint8Array} bytes | ||
| 356 | + * @param {string} metadataList | ||
| 357 | + */ | ||
| 358 | + function bytesMatch (bytes, metadataList) { | ||
| 359 | + // If node is not built with OpenSSL support, we cannot check | ||
| 360 | + // a request's integrity, so allow it by default (the spec will | ||
| 361 | + // allow requests if an invalid hash is given, as precedence). | ||
| 362 | + /* istanbul ignore if: only if node is built with --without-ssl */ | ||
| 363 | + if (crypto === undefined) { | ||
| 364 | + return true | ||
| 365 | + } | ||
| 366 | + | ||
| 367 | + // 1. Let parsedMetadata be the result of parsing metadataList. | ||
| 368 | + const parsedMetadata = parseMetadata(metadataList) | ||
| 369 | + | ||
| 370 | + // 2. If parsedMetadata is no metadata, return true. | ||
| 371 | + if (parsedMetadata === 'no metadata') { | ||
| 372 | + return true | ||
| 373 | + } | ||
| 374 | + | ||
| 375 | + // 3. If parsedMetadata is the empty set, return true. | ||
| 376 | + if (parsedMetadata.length === 0) { | ||
| 377 | + return true | ||
| 378 | + } | ||
| 379 | + | ||
| 380 | + // 4. Let metadata be the result of getting the strongest | ||
| 381 | + // metadata from parsedMetadata. | ||
| 382 | + // Note: this will only work for SHA- algorithms and it's lazy *at best*. | ||
| 383 | + const metadata = parsedMetadata.sort((c, d) => d.algo.localeCompare(c.algo)) | ||
| 384 | + | ||
| 385 | + // 5. For each item in metadata: | ||
| 386 | + for (const item of metadata) { | ||
| 387 | + // 1. Let algorithm be the alg component of item. | ||
| 388 | + const algorithm = item.algo | ||
| 389 | + | ||
| 390 | + // 2. Let expectedValue be the val component of item. | ||
| 391 | + const expectedValue = item.hash | ||
| 392 | + | ||
| 393 | + // 3. Let actualValue be the result of applying algorithm to bytes. | ||
| 394 | + // Note: "applying algorithm to bytes" converts the result to base64 | ||
| 395 | + const actualValue = crypto.createHash(algorithm).update(bytes).digest('base64') | ||
| 396 | + | ||
| 397 | + // 4. If actualValue is a case-sensitive match for expectedValue, | ||
| 398 | + // return true. | ||
| 399 | + if (actualValue === expectedValue) { | ||
| 400 | + return true | ||
| 401 | + } | ||
| 402 | + } | ||
| 403 | + | ||
| 404 | + // 6. Return false. | ||
| 343 | 405 | return false | |
| 344 | 406 | } | |
| 345 | 407 | ||
| 408 | + // https://w3c.github.io/webappsec-subresource-integrity/#grammardef-hash-with-options | ||
| 409 | + // hash-algo is defined in Content Security Policy 2 Section 4.2 | ||
| 410 | + // base64-value is similary defined there | ||
| 411 | + // VCHAR is defined https://www.rfc-editor.org/rfc/rfc5234#appendix-B.1 | ||
| 412 | + const parseHashWithOptions = /((?<algo>sha256|sha384|sha512)-(?<hash>[A-z0-9+/]{1}.*={1,2}))( +[\x21-\x7e]?)?/i | ||
| 413 | + | ||
| 414 | + /** | ||
| 415 | + * @see https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata | ||
| 416 | + * @param {string} metadata | ||
| 417 | + */ | ||
| 418 | + function parseMetadata (metadata) { | ||
| 419 | + // 1. Let result be the empty set. | ||
| 420 | + /** @type {{ algo: string, hash: string }[]} */ | ||
| 421 | + const result = [] | ||
| 422 | + | ||
| 423 | + // 2. Let empty be equal to true. | ||
| 424 | + let empty = true | ||
| 425 | + | ||
| 426 | + const supportedHashes = crypto.getHashes() | ||
| 427 | + | ||
| 428 | + // 3. For each token returned by splitting metadata on spaces: | ||
| 429 | + for (const token of metadata.split(' ')) { | ||
| 430 | + // 1. Set empty to false. | ||
| 431 | + empty = false | ||
| 432 | + | ||
| 433 | + // 2. Parse token as a hash-with-options. | ||
| 434 | + const parsedToken = parseHashWithOptions.exec(token) | ||
| 435 | + | ||
| 436 | + // 3. If token does not parse, continue to the next token. | ||
| 437 | + if (parsedToken === null || parsedToken.groups === undefined) { | ||
| 438 | + // Note: Chromium blocks the request at this point, but Firefox | ||
| 439 | + // gives a warning that an invalid integrity was given. The | ||
| 440 | + // correct behavior is to ignore these, and subsequently not | ||
| 441 | + // check the integrity of the resource. | ||
| 442 | + continue | ||
| 443 | + } | ||
| 444 | + | ||
| 445 | + // 4. Let algorithm be the hash-algo component of token. | ||
| 446 | + const algorithm = parsedToken.groups.algo | ||
| 447 | + | ||
| 448 | + // 5. If algorithm is a hash function recognized by the user | ||
| 449 | + // agent, add the parsed token to result. | ||
| 450 | + if (supportedHashes.includes(algorithm.toLowerCase())) { | ||
| 451 | + result.push(parsedToken.groups) | ||
| 452 | + } | ||
| 453 | + } | ||
| 454 | + | ||
| 455 | + // 4. Return no metadata if empty is true, otherwise return result. | ||
| 456 | + if (empty === true) { | ||
| 457 | + return 'no metadata' | ||
| 458 | + } | ||
| 459 | + | ||
| 460 | + return result | ||
| 461 | + } | ||
| 462 | + | ||
| 346 | 463 | // https://w3c.github.io/webappsec-upgrade-insecure-requests/#upgrade-request | |
| 347 | 464 | function tryUpgradeRequestToAPotentiallyTrustworthyURL (request) { | |
| 348 | 465 | // TODO | |
@@ -438,6 +555,53 @@ function makeIterator (iterator, name) { | |||
| 438 | 555 | return Object.setPrototypeOf({}, i) | |
| 439 | 556 | } | |
| 440 | 557 | ||
| 558 | + /** | ||
| 559 | + * @see https://fetch.spec.whatwg.org/#body-fully-read | ||
| 560 | + */ | ||
| 561 | + async function fullyReadBody (body, processBody, processBodyError) { | ||
| 562 | + // 1. If taskDestination is null, then set taskDestination to | ||
| 563 | + // the result of starting a new parallel queue. | ||
| 564 | + | ||
| 565 | + // 2. Let promise be the result of fully reading body as promise | ||
| 566 | + // given body. | ||
| 567 | + try { | ||
| 568 | + /** @type {Uint8Array[]} */ | ||
| 569 | + const chunks = [] | ||
| 570 | + let length = 0 | ||
| 571 | + | ||
| 572 | + const reader = body.stream.getReader() | ||
| 573 | + | ||
| 574 | + while (true) { | ||
| 575 | + const { done, value } = await reader.read() | ||
| 576 | + | ||
| 577 | + if (done === true) { | ||
| 578 | + break | ||
| 579 | + } | ||
| 580 | + | ||
| 581 | + // read-loop chunk steps | ||
| 582 | + assert(isUint8Array(value)) | ||
| 583 | + | ||
| 584 | + chunks.push(value) | ||
| 585 | + length += value.byteLength | ||
| 586 | + } | ||
| 587 | + | ||
| 588 | + // 3. Let fulfilledSteps given a byte sequence bytes be to queue | ||
| 589 | + // a fetch task to run processBody given bytes, with | ||
| 590 | + // taskDestination. | ||
| 591 | + const fulfilledSteps = (bytes) => queueMicrotask(() => { | ||
| 592 | + processBody(bytes) | ||
| 593 | + }) | ||
| 594 | + | ||
| 595 | + fulfilledSteps(Buffer.concat(chunks, length)) | ||
| 596 | + } catch (err) { | ||
| 597 | + // 4. Let rejectedSteps be to queue a fetch task to run | ||
| 598 | + // processBodyError, with taskDestination. | ||
| 599 | + queueMicrotask(() => processBodyError(err)) | ||
| 600 | + } | ||
| 601 | + | ||
| 602 | + // 5. React to promise with fulfilledSteps and rejectedSteps. | ||
| 603 | + } | ||
| 604 | + | ||
| 441 | 605 | /** | |
| 442 | 606 | * Fetch supports node >= 16.8.0, but Object.hasOwn was added in v16.9.0. | |
| 443 | 607 | */ | |
@@ -451,7 +615,6 @@ module.exports = { | |||
| 451 | 615 | toUSVString, | |
| 452 | 616 | tryUpgradeRequestToAPotentiallyTrustworthyURL, | |
| 453 | 617 | coarsenedSharedCurrentTime, | |
| 454 | - matchRequestIntegrity, | ||
| 455 | 618 | determineRequestsReferrer, | |
| 456 | 619 | makePolicyContainer, | |
| 457 | 620 | clonePolicyContainer, | |
@@ -477,5 +640,7 @@ module.exports = { | |||
| 477 | 640 | isValidHeaderName, | |
| 478 | 641 | isValidHeaderValue, | |
| 479 | 642 | hasOwn, | |
| 480 | - isErrorLike | ||
| 643 | + isErrorLike, | ||
| 644 | + fullyReadBody, | ||
| 645 | + bytesMatch | ||
| 481 | 646 | } | |
| Back | FazBrowse Home | New Git URL |
0 commit comments