From 8ac37a61cd283bb8b291ebdf35a41b60f5fa4876 Mon Sep 17 00:00:00 2001 From: Khafra <42794878+KhafraDev@users.noreply.github.com> Date: Sun, 16 Oct 2022 15:02:19 -0400 Subject: [PATCH 1/7] wpt: add `header-value-combining.any.js` --- test/wpt/runner/runner/runner.mjs | 6 ++++-- test/wpt/runner/runner/util.mjs | 14 ++++++++++++++ test/wpt/server/server.mjs | 16 +++++++++++++++- test/wpt/status/fetch.status.json | 8 ++++++++ .../api/basic/header-value-combining.any.js | 15 +++++++++++++++ .../resources/header-content-length-twice.asis | 3 +++ .../xhr/resources/header-content-length.asis | 2 ++ test/wpt/tests/xhr/resources/headers-basic.asis | 4 ++++ .../xhr/resources/headers-double-empty.asis | 3 +++ .../xhr/resources/headers-some-are-empty.asis | 7 +++++++ .../xhr/resources/headers-www-authenticate.asis | 4 ++++ 11 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 test/wpt/tests/fetch/api/basic/header-value-combining.any.js create mode 100644 test/wpt/tests/xhr/resources/header-content-length-twice.asis create mode 100644 test/wpt/tests/xhr/resources/header-content-length.asis create mode 100644 test/wpt/tests/xhr/resources/headers-basic.asis create mode 100644 test/wpt/tests/xhr/resources/headers-double-empty.asis create mode 100644 test/wpt/tests/xhr/resources/headers-some-are-empty.asis create mode 100644 test/wpt/tests/xhr/resources/headers-www-authenticate.asis diff --git a/test/wpt/runner/runner/runner.mjs b/test/wpt/runner/runner/runner.mjs index 8bf9f6d304b..97c93db1fef 100644 --- a/test/wpt/runner/runner/runner.mjs +++ b/test/wpt/runner/runner/runner.mjs @@ -3,7 +3,7 @@ import { readdirSync, readFileSync, statSync } from 'node:fs' import { basename, isAbsolute, join, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { Worker } from 'node:worker_threads' -import { parseMeta, handlePipes } from './util.mjs' +import { parseMeta, handlePipes, normalizeName } from './util.mjs' const basePath = fileURLToPath(join(import.meta.url, '../../..')) const testPath = join(basePath, 'tests') @@ -130,7 +130,9 @@ export class WPTRunner extends EventEmitter { if (message.result.status === 1) { this.#stats.failed += 1 - if (allowUnexpectedFailures || (fail && fail.includes(message.result.name))) { + const name = normalizeName(message.result.name) + + if (allowUnexpectedFailures || fail?.includes(name)) { this.#stats.expectedFailures += 1 } else { process.exitCode = 1 diff --git a/test/wpt/runner/runner/util.mjs b/test/wpt/runner/runner/util.mjs index 25d8c7c5adf..8ccf5eb4beb 100644 --- a/test/wpt/runner/runner/util.mjs +++ b/test/wpt/runner/runner/util.mjs @@ -1,4 +1,5 @@ import { exit } from 'node:process' +import { inspect } from 'node:util' /** * Parse the `Meta:` tags sometimes included in tests. @@ -117,3 +118,16 @@ export function handlePipes (code, url) { } }) } + +/** + * Some test names may contain characters that JSON cannot handle. + * @param {string} name + */ +export function normalizeName (name) { + return name.replace(/(\v)/g, (_, match) => { + switch (inspect(match)) { + case `'\\x0B'`: return '\\x0B' + default: return match + } + }) +} diff --git a/test/wpt/server/server.mjs b/test/wpt/server/server.mjs index 7e5b75bdf52..cf610e6f5ce 100644 --- a/test/wpt/server/server.mjs +++ b/test/wpt/server/server.mjs @@ -3,7 +3,7 @@ import { createServer } from 'node:http' import { join } from 'node:path' import process from 'node:process' import { fileURLToPath } from 'node:url' -import { createReadStream } from 'node:fs' +import { createReadStream, readFileSync } from 'node:fs' import { setTimeout as sleep } from 'node:timers/promises' const tests = fileURLToPath(join(import.meta.url, '../../tests')) @@ -204,6 +204,20 @@ const server = createServer(async (req, res) => { res.end('garbage') break } + case '/xhr/resources/headers-www-authenticate.asis': + case '/xhr/resources/headers-some-are-empty.asis': + case '/xhr/resources/headers-basic': + case '/xhr/resources/headers-double-empty.asis': + case '/xhr/resources/header-content-length-twice.asis': + case '/xhr/resources/header-content-length.asis': { + let asis = readFileSync(join(tests, fullUrl.pathname), 'utf-8') + asis = asis.replace(/\n/g, '\r\n') + asis = `${asis}\r\n` + + res.socket.write(asis) + res.end() + break + } default: { res.statusCode = 200 res.end('body') diff --git a/test/wpt/status/fetch.status.json b/test/wpt/status/fetch.status.json index a9c0088b5ef..3f65fad70b1 100644 --- a/test/wpt/status/fetch.status.json +++ b/test/wpt/status/fetch.status.json @@ -54,5 +54,13 @@ "Set content type to the empty string for slice with invalid content type", "Set content type to the empty string for slice with no content type " ] + }, + "header-value-combining.any.js": { + "fail": [ + "response.headers.get('content-length') expects 0, 0", + "response.headers.get('double-trouble') expects , ", + "response.headers.get('foo-test') expects 1, 2, 3", + "response.headers.get('heya') expects , \\x0B\f, 1, , , 2" + ] } } \ No newline at end of file diff --git a/test/wpt/tests/fetch/api/basic/header-value-combining.any.js b/test/wpt/tests/fetch/api/basic/header-value-combining.any.js new file mode 100644 index 00000000000..bb70d87d250 --- /dev/null +++ b/test/wpt/tests/fetch/api/basic/header-value-combining.any.js @@ -0,0 +1,15 @@ +// META: global=window,worker + +[ + ["content-length", "0", "header-content-length"], + ["content-length", "0, 0", "header-content-length-twice"], + ["double-trouble", ", ", "headers-double-empty"], + ["foo-test", "1, 2, 3", "headers-basic"], + ["heya", ", \u000B\u000C, 1, , , 2", "headers-some-are-empty"], + ["www-authenticate", "1, 2, 3, 4", "headers-www-authenticate"], +].forEach(testValues => { + promise_test(async t => { + const response = await fetch("../../../xhr/resources/" + testValues[2] + ".asis"); + assert_equals(response.headers.get(testValues[0]), testValues[1]); + }, "response.headers.get('" + testValues[0] + "') expects " + testValues[1]); +}); diff --git a/test/wpt/tests/xhr/resources/header-content-length-twice.asis b/test/wpt/tests/xhr/resources/header-content-length-twice.asis new file mode 100644 index 00000000000..e3196984c06 --- /dev/null +++ b/test/wpt/tests/xhr/resources/header-content-length-twice.asis @@ -0,0 +1,3 @@ +HTTP/1.0 200 NANANA +CONTENT-LENGTH: 0 +content-length: 0 diff --git a/test/wpt/tests/xhr/resources/header-content-length.asis b/test/wpt/tests/xhr/resources/header-content-length.asis new file mode 100644 index 00000000000..ef7071d7428 --- /dev/null +++ b/test/wpt/tests/xhr/resources/header-content-length.asis @@ -0,0 +1,2 @@ +HTTP/1.0 200 NANANA +CONTENT-LENGTH: 0 diff --git a/test/wpt/tests/xhr/resources/headers-basic.asis b/test/wpt/tests/xhr/resources/headers-basic.asis new file mode 100644 index 00000000000..fe37b1b38e9 --- /dev/null +++ b/test/wpt/tests/xhr/resources/headers-basic.asis @@ -0,0 +1,4 @@ +HTTP/1.1 280 HELLO +foo-test: 1 +foo-test: 2 +foo-test: 3 diff --git a/test/wpt/tests/xhr/resources/headers-double-empty.asis b/test/wpt/tests/xhr/resources/headers-double-empty.asis new file mode 100644 index 00000000000..14304b2b434 --- /dev/null +++ b/test/wpt/tests/xhr/resources/headers-double-empty.asis @@ -0,0 +1,3 @@ +HTTP/1.1 444 HI +double-trouble: +double-trouble: diff --git a/test/wpt/tests/xhr/resources/headers-some-are-empty.asis b/test/wpt/tests/xhr/resources/headers-some-are-empty.asis new file mode 100644 index 00000000000..1783e1a11b4 --- /dev/null +++ b/test/wpt/tests/xhr/resources/headers-some-are-empty.asis @@ -0,0 +1,7 @@ +HTTP/1.0 200 MEH +HEYA: +HEYA: +HEYA: 1 +HEYA: +HEYA: +HEYA: 2 diff --git a/test/wpt/tests/xhr/resources/headers-www-authenticate.asis b/test/wpt/tests/xhr/resources/headers-www-authenticate.asis new file mode 100644 index 00000000000..6f9905ee7a0 --- /dev/null +++ b/test/wpt/tests/xhr/resources/headers-www-authenticate.asis @@ -0,0 +1,4 @@ +HTTP/1.1 280 HELLO +www-authenticate: 1 +www-authenticate: 2 +www-authenticate: 3, 4 From 028c556175eb14944d75408ead77ad9146a1664c Mon Sep 17 00:00:00 2001 From: Khafra <42794878+KhafraDev@users.noreply.github.com> Date: Sun, 16 Oct 2022 16:35:32 -0400 Subject: [PATCH 2/7] wpt: add `http-response-code.any.js` --- test/wpt/runner/runner/util.mjs | 2 +- .../server/routes/network-partition-key.mjs | 111 ++++++++++++++++++ test/wpt/server/server.mjs | 4 + .../fetch/api/basic/http-response-code.any.js | 14 +++ 4 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 test/wpt/server/routes/network-partition-key.mjs create mode 100644 test/wpt/tests/fetch/api/basic/http-response-code.any.js diff --git a/test/wpt/runner/runner/util.mjs b/test/wpt/runner/runner/util.mjs index 8ccf5eb4beb..6c5e5d1cc7e 100644 --- a/test/wpt/runner/runner/util.mjs +++ b/test/wpt/runner/runner/util.mjs @@ -126,7 +126,7 @@ export function handlePipes (code, url) { export function normalizeName (name) { return name.replace(/(\v)/g, (_, match) => { switch (inspect(match)) { - case `'\\x0B'`: return '\\x0B' + case '\'\\x0B\'': return '\\x0B' default: return match } }) diff --git a/test/wpt/server/routes/network-partition-key.mjs b/test/wpt/server/routes/network-partition-key.mjs new file mode 100644 index 00000000000..f1203f7b63d --- /dev/null +++ b/test/wpt/server/routes/network-partition-key.mjs @@ -0,0 +1,111 @@ +const stash = new Map() + +/** + * @see https://github.com/web-platform-tests/wpt/blob/master/fetch/connection-pool/resources/network-partition-key.py + * @param {Parameters[0]} req + * @param {Parameters[1]} res + * @param {URL} url + */ +export function route (req, res, { searchParams, port }) { + res.setHeader('Cache-Control', 'no-store') + + const dispatch = searchParams.get('dispatch') + const uuid = searchParams.get('uuid') + const partitionId = searchParams.get('partition_id') + + if (!uuid || !dispatch || !partitionId) { + res.statusCode = 404 + res.end('Invalid query parameters') + return + } + + let testFailed = false + let requestCount = 0 + let connectionCount = 0 + + if (searchParams.get('nocheck_partition') !== 'True') { + const addressKey = `${req.socket.localAddress}|${port}` + const serverState = stash.get(uuid) ?? { + testFailed: false, + requestCount: 0, + connectionCount: 0 + } + + stash.delete(uuid) + requestCount = serverState.requestCount + 1 + serverState.requestCount = requestCount + + if (Object.hasOwn(serverState, addressKey)) { + if (serverState[addressKey] !== partitionId) { + serverState.testFailed = true + } + } else { + connectionCount = serverState.connectionCount + 1 + serverState.connectionCount = connectionCount + } + + serverState[addressKey] = partitionId + testFailed = serverState.testFailed + stash.set(uuid, serverState) + } + + const origin = req.headers.origin + if (origin) { + res.setHeader('Access-Control-Allow-Origin', origin) + res.setHeader('Access-Control-Allow-Credentials', 'true') + } + + if (req.method === 'OPTIONS') { + return handlePreflight(req, res) + } + + if (dispatch === 'fetch_file') { + res.end() + return + } + + if (dispatch === 'check_partition') { + const status = searchParams.get('status') ?? 200 + + if (testFailed) { + res.statusCode = status + res.end('Multiple partition IDs used on a socket') + return + } + + let body = 'ok' + if (searchParams.get('addcounter')) { + body += `. Request was sent ${requestCount} times. ${connectionCount} connections were created.` + res.statusCode = status + res.end(body) + return + } + } + + if (dispatch === 'clean_up') { + stash.delete(uuid) + res.statusCode = 200 + if (testFailed) { + res.end('Test failed, but cleanup completed.') + } else { + res.end('cleanup complete') + } + + return + } + + res.statusCode = 404 + res.end('Unrecognized dispatch parameter: ' + dispatch) +} + +/** + * @param {Parameters[0]} req + * @param {Parameters[1]} res + */ +function handlePreflight (req, res) { + res.statusCode = 200 + res.setHeader('Access-Control-Allow-Methods', 'GET') + res.setHeader('Access-Control-Allow-Headers', 'header-to-force-cors') + res.setHeader('Access-Control-Max-Age', '86400') + res.end('Preflight request') +} diff --git a/test/wpt/server/server.mjs b/test/wpt/server/server.mjs index cf610e6f5ce..f4d29238e6c 100644 --- a/test/wpt/server/server.mjs +++ b/test/wpt/server/server.mjs @@ -5,6 +5,7 @@ import process from 'node:process' import { fileURLToPath } from 'node:url' import { createReadStream, readFileSync } from 'node:fs' import { setTimeout as sleep } from 'node:timers/promises' +import { route } from './routes/network-partition-key.mjs' const tests = fileURLToPath(join(import.meta.url, '../../tests')) @@ -218,6 +219,9 @@ const server = createServer(async (req, res) => { res.end() break } + case '/fetch/connection-pool/resources/network-partition-key.py': { + return route(req, res, fullUrl) + } default: { res.statusCode = 200 res.end('body') diff --git a/test/wpt/tests/fetch/api/basic/http-response-code.any.js b/test/wpt/tests/fetch/api/basic/http-response-code.any.js new file mode 100644 index 00000000000..1fd312a3e9f --- /dev/null +++ b/test/wpt/tests/fetch/api/basic/http-response-code.any.js @@ -0,0 +1,14 @@ +// META: global=window,worker +// META: script=../resources/utils.js +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js + +promise_test(async (test) => { + const resp = await fetch( + "/fetch/connection-pool/resources/network-partition-key.py?" + + `status=425&uuid=${token()}&partition_id=${get_host_info().ORIGIN}` + + `&dispatch=check_partition&addcounter=true`); + assert_equals(resp.status, 425); + const text = await resp.text(); + assert_equals(text, "ok. Request was sent 1 times. 1 connections were created."); +}, "Fetch on 425 response should not be retried for non TLS early data."); From 2d2af484468eb46bb35ba4853ae75febbf5dfb23 Mon Sep 17 00:00:00 2001 From: Khafra <42794878+KhafraDev@users.noreply.github.com> Date: Sun, 16 Oct 2022 17:58:58 -0400 Subject: [PATCH 3/7] wpt: add `integrity.sub.any.js` --- lib/fetch/util.js | 14 ++-- test/wpt/runner/runner/runner.mjs | 4 +- test/wpt/runner/runner/util.mjs | 2 +- test/wpt/server/server.mjs | 6 ++ test/wpt/status/fetch.status.json | 5 ++ .../fetch/api/basic/integrity.sub.any.js | 77 +++++++++++++++++++ 6 files changed, 100 insertions(+), 8 deletions(-) create mode 100644 test/wpt/tests/fetch/api/basic/integrity.sub.any.js diff --git a/lib/fetch/util.js b/lib/fetch/util.js index 3c318e69962..673652be2d3 100644 --- a/lib/fetch/util.js +++ b/lib/fetch/util.js @@ -532,8 +532,11 @@ function bytesMatch (bytes, metadataList) { // 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)) + const list = parsedMetadata.sort((c, d) => d.algo.localeCompare(c.algo)) + // get the strongest algorithm + const strongest = list[0].algo + // get all entries that use the strongest algorithm; ignore weaker + const metadata = list.filter((item) => item.algo === strongest) // 5. For each item in metadata: for (const item of metadata) { @@ -559,10 +562,9 @@ function bytesMatch (bytes, metadataList) { } // 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 = /((?sha256|sha384|sha512)-(?[A-z0-9+/]{1}.*={1,2}))( +[\x21-\x7e]?)?/i +// https://www.w3.org/TR/CSP2/#source-list-syntax +// https://www.rfc-editor.org/rfc/rfc5234#appendix-B.1 +const parseHashWithOptions = /((?sha256|sha384|sha512)-(?[A-z0-9+/]{1}.*={0,2}))( +[\x21-\x7e]?)?/i /** * @see https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata diff --git a/test/wpt/runner/runner/runner.mjs b/test/wpt/runner/runner/runner.mjs index 97c93db1fef..54ce47ec388 100644 --- a/test/wpt/runner/runner/runner.mjs +++ b/test/wpt/runner/runner/runner.mjs @@ -81,7 +81,9 @@ export class WPTRunner extends EventEmitter { const workerPath = fileURLToPath(join(import.meta.url, '../worker.mjs')) for (const test of this.#files) { - const code = readFileSync(test, 'utf-8') + const code = test.includes('.sub.') + ? handlePipes(readFileSync(test, 'utf-8'), this.#url) + : readFileSync(test, 'utf-8') const meta = this.resolveMeta(code, test) const worker = new Worker(workerPath, { diff --git a/test/wpt/runner/runner/util.mjs b/test/wpt/runner/runner/util.mjs index 6c5e5d1cc7e..ac6d6baa39d 100644 --- a/test/wpt/runner/runner/util.mjs +++ b/test/wpt/runner/runner/util.mjs @@ -110,7 +110,7 @@ export function handlePipes (code, url) { // "The port number of servers, by protocol e.g. {{ports[http][0]}} // for the first (and, depending on setup, possibly only) http server" case 'ports': { - return server.port + return `${server.port}/` } default: { throw new TypeError(`Unknown substitute "${sub}".`) diff --git a/test/wpt/server/server.mjs b/test/wpt/server/server.mjs index f4d29238e6c..9b0244afbfc 100644 --- a/test/wpt/server/server.mjs +++ b/test/wpt/server/server.mjs @@ -31,6 +31,7 @@ const server = createServer(async (req, res) => { const fullUrl = new URL(req.url, `http://localhost:${server.address().port}`) switch (fullUrl.pathname) { + case '/fetch/api/resources/top.txt': case '/mimesniff/mime-types/resources/generated-mime-types.json': case '/mimesniff/mime-types/resources/mime-types.json': case '/interfaces/dom.idl': @@ -222,6 +223,11 @@ const server = createServer(async (req, res) => { case '/fetch/connection-pool/resources/network-partition-key.py': { return route(req, res, fullUrl) } + case '/resources/top.txt': { + return createReadStream(join(tests, 'fetch/api/', fullUrl.pathname)) + .on('end', () => res.end()) + .pipe(res) + } default: { res.statusCode = 200 res.end('body') diff --git a/test/wpt/status/fetch.status.json b/test/wpt/status/fetch.status.json index 3f65fad70b1..c1e4db9cccc 100644 --- a/test/wpt/status/fetch.status.json +++ b/test/wpt/status/fetch.status.json @@ -62,5 +62,10 @@ "response.headers.get('foo-test') expects 1, 2, 3", "response.headers.get('heya') expects , \\x0B\f, 1, , , 2" ] + }, + "integrity.sub.any.js": { + "fail": [ + "Empty string integrity for opaque response" + ] } } \ No newline at end of file diff --git a/test/wpt/tests/fetch/api/basic/integrity.sub.any.js b/test/wpt/tests/fetch/api/basic/integrity.sub.any.js new file mode 100644 index 00000000000..56dbd4909f6 --- /dev/null +++ b/test/wpt/tests/fetch/api/basic/integrity.sub.any.js @@ -0,0 +1,77 @@ +// META: global=window,dedicatedworker,sharedworker +// META: script=../resources/utils.js + +function integrity(desc, url, integrity, initRequestMode, shouldPass) { + var fetchRequestInit = {'integrity': integrity} + if (!!initRequestMode && initRequestMode !== "") { + fetchRequestInit.mode = initRequestMode; + } + + if (shouldPass) { + promise_test(function(test) { + return fetch(url, fetchRequestInit).then(function(resp) { + if (initRequestMode !== "no-cors") { + assert_equals(resp.status, 200, "Response's status is 200"); + } else { + assert_equals(resp.status, 0, "Opaque response's status is 0"); + assert_equals(resp.type, "opaque"); + } + }); + }, desc); + } else { + promise_test(function(test) { + return promise_rejects_js(test, TypeError, fetch(url, fetchRequestInit)); + }, desc); + } +} + +const topSha256 = "sha256-KHIDZcXnR2oBHk9DrAA+5fFiR6JjudYjqoXtMR1zvzk="; +const topSha384 = "sha384-MgZYnnAzPM/MjhqfOIMfQK5qcFvGZsGLzx4Phd7/A8fHTqqLqXqKo8cNzY3xEPTL"; +const topSha512 = "sha512-D6yns0qxG0E7+TwkevZ4Jt5t7Iy3ugmAajG/dlf6Pado1JqTyneKXICDiqFIkLMRExgtvg8PlxbKTkYfRejSOg=="; +const invalidSha256 = "sha256-dKUcPOn/AlUjWIwcHeHNqYXPlvyGiq+2dWOdFcE+24I="; +const invalidSha512 = "sha512-oUceBRNxPxnY60g/VtPCj2syT4wo4EZh2CgYdWy9veW8+OsReTXoh7dizMGZafvx9+QhMS39L/gIkxnPIn41Zg=="; + +const path = dirname(location.pathname) + RESOURCES_DIR + "top.txt"; +const url = path; +const corsUrl = + `http://{{host}}:{{ports[http][1]}}${path}?pipe=header(Access-Control-Allow-Origin,*)`; +const corsUrl2 = `https://{{host}}:{{ports[https][0]}}${path}` + +integrity("Empty string integrity", url, "", /* initRequestMode */ undefined, + /* shouldPass */ true); +integrity("SHA-256 integrity", url, topSha256, /* initRequestMode */ undefined, + /* shouldPass */ true); +integrity("SHA-384 integrity", url, topSha384, /* initRequestMode */ undefined, + /* shouldPass */ true); +integrity("SHA-512 integrity", url, topSha512, /* initRequestMode */ undefined, + /* shouldPass */ true); +integrity("Invalid integrity", url, invalidSha256, + /* initRequestMode */ undefined, /* shouldPass */ false); +integrity("Multiple integrities: valid stronger than invalid", url, + invalidSha256 + " " + topSha384, /* initRequestMode */ undefined, + /* shouldPass */ true); +integrity("Multiple integrities: invalid stronger than valid", + url, invalidSha512 + " " + topSha384, /* initRequestMode */ undefined, + /* shouldPass */ false); +integrity("Multiple integrities: invalid as strong as valid", url, + invalidSha512 + " " + topSha512, /* initRequestMode */ undefined, + /* shouldPass */ true); +integrity("Multiple integrities: both are valid", url, + topSha384 + " " + topSha512, /* initRequestMode */ undefined, + /* shouldPass */ true); +integrity("Multiple integrities: both are invalid", url, + invalidSha256 + " " + invalidSha512, /* initRequestMode */ undefined, + /* shouldPass */ false); +integrity("CORS empty integrity", corsUrl, "", /* initRequestMode */ undefined, + /* shouldPass */ true); +integrity("CORS SHA-512 integrity", corsUrl, topSha512, + /* initRequestMode */ undefined, /* shouldPass */ true); +integrity("CORS invalid integrity", corsUrl, invalidSha512, + /* initRequestMode */ undefined, /* shouldPass */ false); + +integrity("Empty string integrity for opaque response", corsUrl2, "", + /* initRequestMode */ "no-cors", /* shouldPass */ true); +integrity("SHA-* integrity for opaque response", corsUrl2, topSha512, + /* initRequestMode */ "no-cors", /* shouldPass */ false); + +done(); From 0f5a1c4c509c8902750ded02330f0f21c7e2a8a0 Mon Sep 17 00:00:00 2001 From: Khafra <42794878+KhafraDev@users.noreply.github.com> Date: Sun, 16 Oct 2022 19:50:17 -0400 Subject: [PATCH 4/7] wpt: add `response-url.sub.any.js` --- lib/fetch/dataURL.js | 2 +- lib/fetch/request.js | 3 ++- lib/fetch/response.js | 12 ++++-------- test/wpt/runner/runner/util.mjs | 2 +- .../fetch/api/basic/response-url.sub.any.js | 16 ++++++++++++++++ 5 files changed, 24 insertions(+), 11 deletions(-) create mode 100644 test/wpt/tests/fetch/api/basic/response-url.sub.any.js diff --git a/lib/fetch/dataURL.js b/lib/fetch/dataURL.js index db6357e4762..a4269120e55 100644 --- a/lib/fetch/dataURL.js +++ b/lib/fetch/dataURL.js @@ -135,7 +135,7 @@ function URLSerializer (url, excludeFragment = false) { } // 3. Append url’s host, serialized, to output. - output += decodeURIComponent(url.host) + output += decodeURIComponent(url.hostname) // 4. If url’s port is non-null, append U+003A (:) followed by url’s port, // serialized, to output. diff --git a/lib/fetch/request.js b/lib/fetch/request.js index 02e4ae46899..7c3032bd5ed 100644 --- a/lib/fetch/request.js +++ b/lib/fetch/request.js @@ -24,6 +24,7 @@ const { kEnumerableProperty } = util const { kHeaders, kSignal, kState, kGuard, kRealm } = require('./symbols') const { webidl } = require('./webidl') const { getGlobalOrigin } = require('./global') +const { URLSerializer } = require('./dataURL') const { kHeadersList } = require('../core/symbols') const assert = require('assert') @@ -542,7 +543,7 @@ class Request { } // The url getter steps are to return this’s request’s URL, serialized. - return this[kState].url.toString() + return URLSerializer(this[kState].url) } // Returns a Headers object consisting of the headers associated with request. diff --git a/lib/fetch/response.js b/lib/fetch/response.js index 85bd6af7a8b..bb1ab240c9f 100644 --- a/lib/fetch/response.js +++ b/lib/fetch/response.js @@ -22,6 +22,7 @@ const { kState, kHeaders, kGuard, kRealm } = require('./symbols') const { webidl } = require('./webidl') const { FormData } = require('./formdata') const { getGlobalOrigin } = require('./global') +const { URLSerializer } = require('./dataURL') const { kHeadersList } = require('../core/symbols') const assert = require('assert') const { types } = require('util') @@ -192,18 +193,13 @@ class Response { // The url getter steps are to return the empty string if this’s // response’s URL is null; otherwise this’s response’s URL, // serialized with exclude fragment set to true. - let url = responseURL(this[kState]) + const url = this[kState].urlList[0] ?? null - if (url == null) { + if (url === null) { return '' } - if (url.hash) { - url = new URL(url) - url.hash = '' - } - - return url.toString() + return URLSerializer(url, true) } // Returns whether response was obtained through a redirect. diff --git a/test/wpt/runner/runner/util.mjs b/test/wpt/runner/runner/util.mjs index ac6d6baa39d..6c5e5d1cc7e 100644 --- a/test/wpt/runner/runner/util.mjs +++ b/test/wpt/runner/runner/util.mjs @@ -110,7 +110,7 @@ export function handlePipes (code, url) { // "The port number of servers, by protocol e.g. {{ports[http][0]}} // for the first (and, depending on setup, possibly only) http server" case 'ports': { - return `${server.port}/` + return server.port } default: { throw new TypeError(`Unknown substitute "${sub}".`) diff --git a/test/wpt/tests/fetch/api/basic/response-url.sub.any.js b/test/wpt/tests/fetch/api/basic/response-url.sub.any.js new file mode 100644 index 00000000000..0d123c42944 --- /dev/null +++ b/test/wpt/tests/fetch/api/basic/response-url.sub.any.js @@ -0,0 +1,16 @@ +function checkResponseURL(fetchedURL, expectedURL) +{ + promise_test(function() { + return fetch(fetchedURL).then(function(response) { + assert_equals(response.url, expectedURL); + }); + }, "Testing response url getter with " +fetchedURL); +} + +var baseURL = "http://{{host}}:{{ports[http][0]}}"; +checkResponseURL(baseURL + "/ada", baseURL + "/ada"); +checkResponseURL(baseURL + "/#", baseURL + "/"); +checkResponseURL(baseURL + "/#ada", baseURL + "/"); +checkResponseURL(baseURL + "#ada", baseURL + "/"); + +done(); From ba3137b9ccaffb5bbefd15dbd49ca87c07ff99cb Mon Sep 17 00:00:00 2001 From: Khafra <42794878+KhafraDev@users.noreply.github.com> Date: Sun, 16 Oct 2022 19:56:30 -0400 Subject: [PATCH 5/7] wpt: add `scheme-about.any.js` --- lib/fetch/index.js | 18 ++----------- lib/fetch/response.js | 1 - .../tests/fetch/api/basic/scheme-about.any.js | 26 +++++++++++++++++++ 3 files changed, 28 insertions(+), 17 deletions(-) create mode 100644 test/wpt/tests/fetch/api/basic/scheme-about.any.js diff --git a/lib/fetch/index.js b/lib/fetch/index.js index 386c90da1af..c0393939976 100644 --- a/lib/fetch/index.js +++ b/lib/fetch/index.js @@ -753,10 +753,7 @@ async function schemeFetch (fetchParams) { // let request be fetchParams’s request const { request } = fetchParams - const { - protocol: scheme, - pathname: path - } = requestCurrentURL(request) + const { protocol: scheme } = requestCurrentURL(request) // switch on request’s current URL’s scheme, and run the associated steps: switch (scheme) { @@ -764,20 +761,9 @@ async function schemeFetch (fetchParams) { // If request’s current URL’s path is the string "blank", then return a new response // whose status message is `OK`, header list is « (`Content-Type`, `text/html;charset=utf-8`) », // and body is the empty byte sequence. - if (path === 'blank') { - const resp = makeResponse({ - statusText: 'OK', - headersList: [ - ['content-type', 'text/html;charset=utf-8'] - ] - }) - - resp.urlList = [new URL('about:blank')] - return resp - } // Otherwise, return a network error. - return makeNetworkError('invalid path called') + return makeNetworkError('about scheme is not supported') } case 'blob:': { resolveObjectURL = resolveObjectURL || require('buffer').resolveObjectURL diff --git a/lib/fetch/response.js b/lib/fetch/response.js index bb1ab240c9f..7bf195e1ace 100644 --- a/lib/fetch/response.js +++ b/lib/fetch/response.js @@ -5,7 +5,6 @@ const { extractBody, cloneBody, mixinBody } = require('./body') const util = require('../core/util') const { kEnumerableProperty } = util const { - responseURL, isValidReasonPhrase, isCancelled, isAborted, diff --git a/test/wpt/tests/fetch/api/basic/scheme-about.any.js b/test/wpt/tests/fetch/api/basic/scheme-about.any.js new file mode 100644 index 00000000000..9ef44183c17 --- /dev/null +++ b/test/wpt/tests/fetch/api/basic/scheme-about.any.js @@ -0,0 +1,26 @@ +// META: global=window,worker +// META: script=../resources/utils.js + +function checkNetworkError(url, method) { + method = method || "GET"; + const desc = "Fetching " + url.substring(0, 45) + " with method " + method + " is KO" + promise_test(function(test) { + var promise = fetch(url, { method: method }); + return promise_rejects_js(test, TypeError, promise); + }, desc); +} + +checkNetworkError("about:blank", "GET"); +checkNetworkError("about:blank", "PUT"); +checkNetworkError("about:blank", "POST"); +checkNetworkError("about:invalid.com"); +checkNetworkError("about:config"); +checkNetworkError("about:unicorn"); + +promise_test(function(test) { + var promise = fetch("about:blank", { + "method": "GET", + "Range": "bytes=1-10" + }); + return promise_rejects_js(test, TypeError, promise); +}, "Fetching about:blank with range header does not affect behavior"); From b261144c5d2c994d168ce8e7bf141258491e1694 Mon Sep 17 00:00:00 2001 From: Khafra <42794878+KhafraDev@users.noreply.github.com> Date: Sun, 16 Oct 2022 20:48:54 -0400 Subject: [PATCH 6/7] wpt: add `scheme-others.sub.any.js` and fix issues --- lib/fetch/response.js | 4 ++- lib/fetch/util.js | 1 - test/fetch/about-uri.js | 7 +---- test/fetch/integrity.js | 4 +-- test/node-fetch/main.js | 8 ++--- test/node-fetch/response.js | 2 +- .../fetch/api/basic/scheme-others.sub.any.js | 31 +++++++++++++++++++ 7 files changed, 42 insertions(+), 15 deletions(-) create mode 100644 test/wpt/tests/fetch/api/basic/scheme-others.sub.any.js diff --git a/lib/fetch/response.js b/lib/fetch/response.js index 7bf195e1ace..018224bc02c 100644 --- a/lib/fetch/response.js +++ b/lib/fetch/response.js @@ -189,10 +189,12 @@ class Response { throw new TypeError('Illegal invocation') } + const urlList = this[kState].urlList + // The url getter steps are to return the empty string if this’s // response’s URL is null; otherwise this’s response’s URL, // serialized with exclude fragment set to true. - const url = this[kState].urlList[0] ?? null + const url = urlList[urlList.length - 1] ?? null if (url === null) { return '' diff --git a/lib/fetch/util.js b/lib/fetch/util.js index 673652be2d3..cf485b7daf1 100644 --- a/lib/fetch/util.js +++ b/lib/fetch/util.js @@ -547,7 +547,6 @@ function bytesMatch (bytes, metadataList) { 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, diff --git a/test/fetch/about-uri.js b/test/fetch/about-uri.js index 3375a360e8c..ac9cbf2be90 100644 --- a/test/fetch/about-uri.js +++ b/test/fetch/about-uri.js @@ -5,12 +5,7 @@ const { fetch } = require('../..') test('fetching about: uris', async (t) => { t.test('about:blank', async (t) => { - const res = await fetch('about:blank') - - t.equal(res.url, 'about:blank') - t.equal(res.statusText, 'OK') - t.equal(res.headers.get('Content-Type'), 'text/html;charset=utf-8') - t.end() + await t.rejects(fetch('about:blank')) }) t.test('All other about: urls should return an error', async (t) => { diff --git a/test/fetch/integrity.js b/test/fetch/integrity.js index 70802a6aecf..f91f69314ee 100644 --- a/test/fetch/integrity.js +++ b/test/fetch/integrity.js @@ -16,7 +16,7 @@ setGlobalDispatcher(new Agent({ test('request with correct integrity checksum', (t) => { const body = 'Hello world!' - const hash = createHash('sha256').update(body).digest('hex') + const hash = createHash('sha256').update(body).digest('base64') const server = createServer((req, res) => { res.end(body) @@ -58,7 +58,7 @@ test('request with wrong integrity checksum', (t) => { test('request with integrity checksum on encoded body', (t) => { const body = 'Hello world!' - const hash = createHash('sha256').update(body).digest('hex') + const hash = createHash('sha256').update(body).digest('base64') const server = createServer((req, res) => { res.setHeader('content-encoding', 'gzip') diff --git a/test/node-fetch/main.js b/test/node-fetch/main.js index 04f74db9727..f4f967cf67e 100644 --- a/test/node-fetch/main.js +++ b/test/node-fetch/main.js @@ -1532,12 +1532,12 @@ describe('node-fetch', () => { }) }) - it('should keep `?` sign in URL when no params are given', () => { + it('should NOT keep `?` sign in URL when no params are given', () => { const url = `${base}question?` const urlObject = new URL(url) const request = new Request(urlObject) return fetch(request).then(res => { - expect(res.url).to.equal(url) + expect(res.url).to.equal(url.slice(0, -1)) expect(res.ok).to.be.true expect(res.status).to.equal(200) }) @@ -1554,12 +1554,12 @@ describe('node-fetch', () => { }) }) - it('should preserve the hash (#) symbol', () => { + it('should NOT preserve the hash (#) symbol', () => { const url = `${base}question?#` const urlObject = new URL(url) const request = new Request(urlObject) return fetch(request).then(res => { - expect(res.url).to.equal(url) + expect(res.url).to.equal(url.slice(0, -2)) expect(res.ok).to.be.true expect(res.status).to.equal(200) }) diff --git a/test/node-fetch/response.js b/test/node-fetch/response.js index 44f0c1ac437..4bb7c423208 100644 --- a/test/node-fetch/response.js +++ b/test/node-fetch/response.js @@ -121,7 +121,7 @@ describe('Response', () => { status: 346, statusText: 'production' }) - res[kState].urlList = [base] + res[kState].urlList = [new URL(base)] const cl = res.clone() expect(cl.headers.get('a')).to.equal('1') expect(cl.type).to.equal('default') diff --git a/test/wpt/tests/fetch/api/basic/scheme-others.sub.any.js b/test/wpt/tests/fetch/api/basic/scheme-others.sub.any.js new file mode 100644 index 00000000000..550f69c41b5 --- /dev/null +++ b/test/wpt/tests/fetch/api/basic/scheme-others.sub.any.js @@ -0,0 +1,31 @@ +// META: global=window,worker +// META: script=../resources/utils.js + +function checkKoUrl(url, desc) { + if (!desc) + desc = "Fetching " + url.substring(0, 45) + " is KO" + promise_test(function(test) { + var promise = fetch(url); + return promise_rejects_js(test, TypeError, promise); + }, desc); +} + +var urlWithoutScheme = "://{{host}}:{{ports[http][0]}}/"; +checkKoUrl("aaa" + urlWithoutScheme); +checkKoUrl("cap" + urlWithoutScheme); +checkKoUrl("cid" + urlWithoutScheme); +checkKoUrl("dav" + urlWithoutScheme); +checkKoUrl("dict" + urlWithoutScheme); +checkKoUrl("dns" + urlWithoutScheme); +checkKoUrl("geo" + urlWithoutScheme); +checkKoUrl("im" + urlWithoutScheme); +checkKoUrl("imap" + urlWithoutScheme); +checkKoUrl("ipp" + urlWithoutScheme); +checkKoUrl("ldap" + urlWithoutScheme); +checkKoUrl("mailto" + urlWithoutScheme); +checkKoUrl("nfs" + urlWithoutScheme); +checkKoUrl("pop" + urlWithoutScheme); +checkKoUrl("rtsp" + urlWithoutScheme); +checkKoUrl("snmp" + urlWithoutScheme); + +done(); From b730ce30b014422b810d91f7221f1d934a4d2d5a Mon Sep 17 00:00:00 2001 From: Khafra <42794878+KhafraDev@users.noreply.github.com> Date: Sun, 16 Oct 2022 21:22:32 -0400 Subject: [PATCH 7/7] wpt: add flaky test option --- test/wpt/runner/runner/runner.mjs | 6 ++++-- test/wpt/status/fetch.status.json | 4 ++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/test/wpt/runner/runner/runner.mjs b/test/wpt/runner/runner/runner.mjs index 54ce47ec388..93ad51722c2 100644 --- a/test/wpt/runner/runner/runner.mjs +++ b/test/wpt/runner/runner/runner.mjs @@ -124,7 +124,7 @@ export class WPTRunner extends EventEmitter { * Called after a test has succeeded or failed. */ handleIndividualTestCompletion (message, fileName) { - const { fail, allowUnexpectedFailures } = this.#status[fileName] ?? {} + const { fail, allowUnexpectedFailures, flaky } = this.#status[fileName] ?? {} if (message.type === 'result') { this.#stats.completed += 1 @@ -134,7 +134,9 @@ export class WPTRunner extends EventEmitter { const name = normalizeName(message.result.name) - if (allowUnexpectedFailures || fail?.includes(name)) { + if (flaky?.includes(name)) { + this.#stats.expectedFailures += 1 + } else if (allowUnexpectedFailures || fail?.includes(name)) { this.#stats.expectedFailures += 1 } else { process.exitCode = 1 diff --git a/test/wpt/status/fetch.status.json b/test/wpt/status/fetch.status.json index c1e4db9cccc..70d1874ae95 100644 --- a/test/wpt/status/fetch.status.json +++ b/test/wpt/status/fetch.status.json @@ -61,6 +61,10 @@ "response.headers.get('double-trouble') expects , ", "response.headers.get('foo-test') expects 1, 2, 3", "response.headers.get('heya') expects , \\x0B\f, 1, , , 2" + ], + "flaky": [ + "response.headers.get('content-length') expects 0", + "response.headers.get('www-authenticate') expects 1, 2, 3, 4" ] }, "integrity.sub.any.js": {