Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: nodejs/undici
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v5.9.1
Choose a base ref
...
head repository: nodejs/undici
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v5.10.0
Choose a head ref
  • 8 commits
  • 15 files changed
  • 7 contributors

Commits on Aug 18, 2022

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    d83c1e2 View commit details
  2. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    e46d8bf View commit details

Commits on Aug 19, 2022

  1. fix: support ArrayBufferView and ArrayBuffer as body (#1584)

    * fix: support `ArrayBufferView` and `ArrayBuffer` as body
    
    * test: ArrayBufferView as Request.body
    LiviaMedeiros authored Aug 19, 2022

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    8549a94 View commit details
  2. fix: allow third party AbortControllers (#1609)

    * fix: allow third party AbortControllers
    
    * fix: lint
    
    Co-authored-by: Matthew Aitken <matthew@Matthews-MacBook-Air.local>
    KhafraDev and Matthew Aitken authored Aug 19, 2022

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    417e7ab View commit details

Commits on Aug 22, 2022

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    21fdda5 View commit details

Commits on Aug 23, 2022

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    5cb0bac View commit details
  2. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    b0f0f31 View commit details
  3. Bumped v5.10.0

    Signed-off-by: Matteo Collina <hello@matteocollina.com>
    mcollina committed Aug 23, 2022

    Verified

    This commit was signed with the committer’s verified signature.
    mcollina Matteo Collina
    Copy the full SHA
    6a87bfb View commit details
2 changes: 1 addition & 1 deletion lib/api/readable.js
Original file line number Diff line number Diff line change
@@ -93,7 +93,7 @@ module.exports = class BodyReadable extends Readable {
}

push (chunk) {
if (this[kConsume] && chunk !== null) {
if (this[kConsume] && chunk !== null && this.readableLength === 0) {
consumePush(this[kConsume], chunk)
return this[kReading] ? super.push(chunk) : true
}
14 changes: 7 additions & 7 deletions lib/fetch/body.js
Original file line number Diff line number Diff line change
@@ -57,16 +57,16 @@ function extractBody (object, keepalive = false) {

// Set Content-Type to `application/x-www-form-urlencoded;charset=UTF-8`.
contentType = 'application/x-www-form-urlencoded;charset=UTF-8'
} else if (isArrayBuffer(object) || ArrayBuffer.isView(object)) {
// BufferSource
} else if (isArrayBuffer(object)) {
// BufferSource/ArrayBuffer

if (object instanceof DataView) {
// TODO: Blob doesn't seem to work with DataView?
object = object.buffer
}
// Set source to a copy of the bytes held by object.
source = new Uint8Array(object.slice())
} else if (ArrayBuffer.isView(object)) {
// BufferSource/ArrayBufferView

// Set source to a copy of the bytes held by object.
source = new Uint8Array(object)
source = new Uint8Array(object.buffer.slice(object.byteOffset, object.byteOffset + object.byteLength))
} else if (util.isFormDataLike(object)) {
const boundary = '----formdata-undici-' + Math.random()
const prefix = `--${boundary}\r\nContent-Disposition: form-data`
4 changes: 2 additions & 2 deletions lib/fetch/index.js
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@ const { Headers } = require('./headers')
const { Request, makeRequest } = require('./request')
const zlib = require('zlib')
const {
matchRequestIntegrity,
bytesMatch,
makePolicyContainer,
clonePolicyContainer,
requestBadPort,
@@ -725,7 +725,7 @@ async function mainFetch (fetchParams, recursive = false) {
const processBody = (bytes) => {
// 1. If bytes do not match request’s integrity metadata,
// then run processBodyError and abort these steps. [SRI]
if (!matchRequestIntegrity(request, bytes)) {
if (!bytesMatch(bytes, request.integrity)) {
processBodyError('integrity mismatch')
return
}
6 changes: 5 additions & 1 deletion lib/fetch/request.js
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@

const { extractBody, mixinBody, cloneBody } = require('./body')
const { Headers, fill: fillHeaders, HeadersList } = require('./headers')
const { FinalizationRegistry } = require('../compat/dispatcher-weakref')()
const util = require('../core/util')
const {
isValidHTTPToken,
@@ -914,7 +915,10 @@ webidl.converters.RequestInit = webidl.dictionaryConverter([
{
key: 'signal',
converter: webidl.nullableConverter(
webidl.converters.AbortSignal
(signal) => webidl.converters.AbortSignal(
signal,
{ strict: false }
)
)
},
{
126 changes: 120 additions & 6 deletions lib/fetch/util.js
Original file line number Diff line number Diff line change
@@ -5,10 +5,19 @@ const { performance } = require('perf_hooks')
const { isBlobLike, toUSVString, ReadableStreamFrom } = require('../core/util')
const assert = require('assert')
const { isUint8Array } = require('util/types')
const { createHash } = require('crypto')

let File

// https://nodejs.org/api/crypto.html#determining-if-crypto-support-is-unavailable
/** @type {import('crypto')|undefined} */
let crypto

try {
crypto = require('crypto')
} catch {

}

// https://fetch.spec.whatwg.org/#block-bad-port
const badPorts = [
'1', '7', '9', '11', '13', '15', '17', '19', '20', '21', '22', '23', '25', '37', '42', '43', '53', '69', '77', '79',
@@ -341,9 +350,114 @@ function determineRequestsReferrer (request) {
return 'no-referrer'
}

function matchRequestIntegrity (request, bytes) {
const [algo, expectedHashValue] = request.integrity.split('-', 2)
return createHash(algo).update(bytes).digest('hex') === expectedHashValue
/**
* @see https://w3c.github.io/webappsec-subresource-integrity/#does-response-match-metadatalist
* @param {Uint8Array} bytes
* @param {string} metadataList
*/
function bytesMatch (bytes, metadataList) {
// If node is not built with OpenSSL support, we cannot check
// a request's integrity, so allow it by default (the spec will
// allow requests if an invalid hash is given, as precedence).
/* istanbul ignore if: only if node is built with --without-ssl */
if (crypto === undefined) {
return true
}

// 1. Let parsedMetadata be the result of parsing metadataList.
const parsedMetadata = parseMetadata(metadataList)

// 2. If parsedMetadata is no metadata, return true.
if (parsedMetadata === 'no metadata') {
return true
}

// 3. If parsedMetadata is the empty set, return true.
if (parsedMetadata.length === 0) {
return true
}

// 4. Let metadata be the result of getting the strongest
// metadata from parsedMetadata.
// Note: this will only work for SHA- algorithms and it's lazy *at best*.
const metadata = parsedMetadata.sort((c, d) => d.algo.localeCompare(c.algo))

// 5. For each item in metadata:
for (const item of metadata) {
// 1. Let algorithm be the alg component of item.
const algorithm = item.algo

// 2. Let expectedValue be the val component of item.
const expectedValue = item.hash

// 3. Let actualValue be the result of applying algorithm to bytes.
// Note: "applying algorithm to bytes" converts the result to base64
const actualValue = crypto.createHash(algorithm).update(bytes).digest('base64')

// 4. If actualValue is a case-sensitive match for expectedValue,
// return true.
if (actualValue === expectedValue) {
return true
}
}

// 6. Return false.
return false
}

// https://w3c.github.io/webappsec-subresource-integrity/#grammardef-hash-with-options
// hash-algo is defined in Content Security Policy 2 Section 4.2
// base64-value is similary defined there
// VCHAR is defined https://www.rfc-editor.org/rfc/rfc5234#appendix-B.1
const parseHashWithOptions = /((?<algo>sha256|sha384|sha512)-(?<hash>[A-z0-9+/]{1}.*={1,2}))( +[\x21-\x7e]?)?/i

/**
* @see https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata
* @param {string} metadata
*/
function parseMetadata (metadata) {
// 1. Let result be the empty set.
/** @type {{ algo: string, hash: string }[]} */
const result = []

// 2. Let empty be equal to true.
let empty = true

const supportedHashes = crypto.getHashes()

// 3. For each token returned by splitting metadata on spaces:
for (const token of metadata.split(' ')) {
// 1. Set empty to false.
empty = false

// 2. Parse token as a hash-with-options.
const parsedToken = parseHashWithOptions.exec(token)

// 3. If token does not parse, continue to the next token.
if (parsedToken === null || parsedToken.groups === undefined) {
// Note: Chromium blocks the request at this point, but Firefox
// gives a warning that an invalid integrity was given. The
// correct behavior is to ignore these, and subsequently not
// check the integrity of the resource.
continue
}

// 4. Let algorithm be the hash-algo component of token.
const algorithm = parsedToken.groups.algo

// 5. If algorithm is a hash function recognized by the user
// agent, add the parsed token to result.
if (supportedHashes.includes(algorithm.toLowerCase())) {
result.push(parsedToken.groups)
}
}

// 4. Return no metadata if empty is true, otherwise return result.
if (empty === true) {
return 'no metadata'
}

return result
}

// https://w3c.github.io/webappsec-upgrade-insecure-requests/#upgrade-request
@@ -501,7 +615,6 @@ module.exports = {
toUSVString,
tryUpgradeRequestToAPotentiallyTrustworthyURL,
coarsenedSharedCurrentTime,
matchRequestIntegrity,
determineRequestsReferrer,
makePolicyContainer,
clonePolicyContainer,
@@ -528,5 +641,6 @@ module.exports = {
isValidHeaderValue,
hasOwn,
isErrorLike,
fullyReadBody
fullyReadBody,
bytesMatch
}
21 changes: 19 additions & 2 deletions lib/mock/mock-utils.js
Original file line number Diff line number Diff line change
@@ -85,6 +85,22 @@ function matchHeaders (mockDispatch, headers) {
return true
}

function safeUrl (path) {
if (typeof path !== 'string') {
return path
}

const pathSegments = path.split('?')

if (pathSegments.length !== 2) {
return path
}

const qp = new URLSearchParams(pathSegments.pop())
qp.sort()
return [...pathSegments, qp.toString()].join('?')
}

function matchKey (mockDispatch, { path, method, body, headers }) {
const pathMatch = matchValue(mockDispatch.path, path)
const methodMatch = matchValue(mockDispatch.method, method)
@@ -104,10 +120,11 @@ function getResponseData (data) {
}

function getMockDispatch (mockDispatches, key) {
const resolvedPath = key.query ? buildURL(key.path, key.query) : key.path
const basePath = key.query ? buildURL(key.path, key.query) : key.path
const resolvedPath = typeof basePath === 'string' ? safeUrl(basePath) : basePath

// Match path
let matchedMockDispatches = mockDispatches.filter(({ consumed }) => !consumed).filter(({ path }) => matchValue(path, resolvedPath))
let matchedMockDispatches = mockDispatches.filter(({ consumed }) => !consumed).filter(({ path }) => matchValue(safeUrl(path), resolvedPath))
if (matchedMockDispatches.length === 0) {
throw new MockNotMatchedError(`Mock dispatch not matched for path '${resolvedPath}'`)
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "undici",
"version": "5.9.1",
"version": "5.10.0",
"description": "An HTTP/1.1 client, written from scratch for Node.js",
"homepage": "https://undici.nodejs.org",
"bugs": {
30 changes: 30 additions & 0 deletions test/fetch/abort.js
Original file line number Diff line number Diff line change
@@ -7,6 +7,8 @@ const { once } = require('events')
const { ReadableStream } = require('stream/web')
const { DOMException } = require('../../lib/fetch/constants')

const { AbortController: NPMAbortController } = require('abort-controller')

/* global AbortController */

test('parallel fetch with the same AbortController works as expected', async (t) => {
@@ -106,3 +108,31 @@ test('Readable stream synchronously cancels with AbortError if aborted before re

t.end()
})

test('Allow the usage of custom implementation of AbortController', async (t) => {
const body = {
fixes: 1605
}

const server = createServer((req, res) => {
res.statusCode = 200
res.end(JSON.stringify(body))
})

t.teardown(server.close.bind(server))

server.listen(0)
await once(server, 'listening')

const controller = new NPMAbortController()
const signal = controller.signal
controller.abort()

try {
await fetch(`http://localhost:${server.address().port}`, {
signal
})
} catch (e) {
t.equal(e.code, DOMException.ABORT_ERR)
}
})
Loading