From f04f9fabad2972febea7b2c8e24b217327ca9a38 Mon Sep 17 00:00:00 2001 From: andrewfecenko Date: Mon, 13 Mar 2023 10:11:29 -0500 Subject: [PATCH 001/259] Fix typo in kPipelining symbol (#2005) Co-authored-by: Andrew Fecenko --- lib/core/symbols.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/core/symbols.js b/lib/core/symbols.js index 6d6b62919e6..c852107a72a 100644 --- a/lib/core/symbols.js +++ b/lib/core/symbols.js @@ -41,7 +41,7 @@ module.exports = { kClient: Symbol('client'), kParser: Symbol('parser'), kOnDestroyed: Symbol('destroy callbacks'), - kPipelining: Symbol('pipelinig'), + kPipelining: Symbol('pipelining'), kSocket: Symbol('socket'), kHostHeader: Symbol('host header'), kConnector: Symbol('connector'), From a64eb26aea8a68cf41185f28493d17b9ac6aa501 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Mon, 13 Mar 2023 18:36:04 +0100 Subject: [PATCH 002/259] fix(fetch): remove `undefined` error cause (#2006) We have checked on the line before that `isError` is falsy, so the cause is always set to `undefined`, which is not very useful. --- lib/fetch/response.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/fetch/response.js b/lib/fetch/response.js index 09732114e7a..ff06bfb47d0 100644 --- a/lib/fetch/response.js +++ b/lib/fetch/response.js @@ -348,9 +348,7 @@ function makeNetworkError (reason) { status: 0, error: isError ? reason - : new Error(reason ? String(reason) : reason, { - cause: isError ? reason : undefined - }), + : new Error(reason ? String(reason) : reason), aborted: reason && reason.name === 'AbortError' }) } From ebf0b45cd75de2eb68d9b1970a8e278f8c95a1c3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Mar 2023 23:09:37 +0000 Subject: [PATCH 003/259] chore(deps-dev): bump tsd from 0.25.0 to 0.27.0 (#2007) Bumps [tsd](https://github.com/SamVerschueren/tsd) from 0.25.0 to 0.27.0. - [Release notes](https://github.com/SamVerschueren/tsd/releases) - [Commits](https://github.com/SamVerschueren/tsd/compare/v0.25.0...v0.27.0) --- updated-dependencies: - dependency-name: tsd dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 39be1736f06..e772f487ffd 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,7 @@ "standard": "^17.0.0", "table": "^6.8.0", "tap": "^16.1.0", - "tsd": "^0.25.0", + "tsd": "^0.27.0", "typescript": "^4.9.5", "wait-on": "^6.0.0", "ws": "^8.11.0" From 6ec8f4e5e60eae373947481908f43b71acb1ff79 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Mar 2023 23:22:23 +0000 Subject: [PATCH 004/259] build(deps-dev): bump wait-on from 6.0.1 to 7.0.1 (#1820) Bumps [wait-on](https://github.com/jeffbski/wait-on) from 6.0.1 to 7.0.1. - [Release notes](https://github.com/jeffbski/wait-on/releases) - [Commits](https://github.com/jeffbski/wait-on/compare/v6.0.1...v7.0.1) --- updated-dependencies: - dependency-name: wait-on dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e772f487ffd..90d1d568748 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,7 @@ "tap": "^16.1.0", "tsd": "^0.27.0", "typescript": "^4.9.5", - "wait-on": "^6.0.0", + "wait-on": "^7.0.1", "ws": "^8.11.0" }, "engines": { From 4e1e0d07d0261e2a7c951ca4544f0c41b75076c9 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 14 Mar 2023 15:52:00 +0100 Subject: [PATCH 005/259] fix(wpt): set global META_TITLE for the runner (#2008) --- test/wpt/runner/worker.mjs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/wpt/runner/worker.mjs b/test/wpt/runner/worker.mjs index 918d44659fc..0b323b4c972 100644 --- a/test/wpt/runner/worker.mjs +++ b/test/wpt/runner/worker.mjs @@ -97,6 +97,10 @@ runInThisContext(` globalThis.Window = Object.getPrototypeOf(globalThis).constructor `) +if (meta.title) { + runInThisContext(`globalThis.META_TITLE = "${meta.title}"`) +} + const harness = readFileSync(join(basePath, '/resources/testharness.js'), 'utf-8') runInThisContext(harness) From 6fd7477078b22f1c5fd7741c9d4d7aada1c8d75e Mon Sep 17 00:00:00 2001 From: Khafra Date: Fri, 17 Mar 2023 04:30:49 -0400 Subject: [PATCH 006/259] fix: issue 2009 (#2013) --- lib/fetch/index.js | 7 ++++++- test/fetch/issue-2009.js | 28 ++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 test/fetch/issue-2009.js diff --git a/lib/fetch/index.js b/lib/fetch/index.js index e3834a7f1c6..cc5090c556f 100644 --- a/lib/fetch/index.js +++ b/lib/fetch/index.js @@ -1845,6 +1845,7 @@ async function httpNetworkFetch ( // 4. Set bytes to the result of handling content codings given // codings and bytes. let bytes + let isFailure try { const { done, value } = await fetchParams.controller.next() @@ -1859,6 +1860,10 @@ async function httpNetworkFetch ( bytes = undefined } else { bytes = err + + // err may be propagated from the result of calling readablestream.cancel, + // which might not be an error. https://github.com/nodejs/undici/issues/2009 + isFailure = true } } @@ -1878,7 +1883,7 @@ async function httpNetworkFetch ( timingInfo.decodedBodySize += bytes?.byteLength ?? 0 // 6. If bytes is failure, then terminate fetchParams’s controller. - if (isErrorLike(bytes)) { + if (isFailure) { fetchParams.controller.terminate(bytes) return } diff --git a/test/fetch/issue-2009.js b/test/fetch/issue-2009.js new file mode 100644 index 00000000000..0b7b3e9812e --- /dev/null +++ b/test/fetch/issue-2009.js @@ -0,0 +1,28 @@ +'use strict' + +const { test } = require('tap') +const { fetch } = require('../..') +const { createServer } = require('http') +const { once } = require('events') + +test('issue 2009', async (t) => { + const server = createServer((req, res) => { + res.setHeader('a', 'b') + res.flushHeaders() + + res.socket.end() + }).listen(0) + + t.teardown(server.close.bind(server)) + await once(server, 'listening') + + for (let i = 0; i < 10; i++) { + await t.resolves( + fetch(`http://localhost:${server.address().port}`).then( + async (resp) => { + await resp.body.cancel('Some message') + } + ) + ) + } +}) From 7276126945c52cf7ec61460b36d19f882e1bab82 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Mar 2023 23:16:00 +0000 Subject: [PATCH 007/259] build(deps-dev): bump typescript from 4.9.5 to 5.0.2 (#2018) Bumps [typescript](https://github.com/Microsoft/TypeScript) from 4.9.5 to 5.0.2. - [Release notes](https://github.com/Microsoft/TypeScript/releases) - [Commits](https://github.com/Microsoft/TypeScript/compare/v4.9.5...v5.0.2) --- updated-dependencies: - dependency-name: typescript dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 90d1d568748..e019ebc7d1e 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "table": "^6.8.0", "tap": "^16.1.0", "tsd": "^0.27.0", - "typescript": "^4.9.5", + "typescript": "^5.0.2", "wait-on": "^7.0.1", "ws": "^8.11.0" }, From e6fc80f809d1217814c044f52ed40ef13f21e43c Mon Sep 17 00:00:00 2001 From: Rishabh Bhandari <41220684+RishabhKodes@users.noreply.github.com> Date: Wed, 22 Mar 2023 14:08:37 +0530 Subject: [PATCH 008/259] added descriptive error messages for URL parser (#2016) * added descriptive error messages Signed-off-by: Rishabh Bhandari * updating error msgs in tests Signed-off-by: Rishabh Bhandari * updated error messages Signed-off-by: Rishabh Bhandari --------- Signed-off-by: Rishabh Bhandari --- lib/core/util.js | 16 ++++++++-------- test/client-errors.js | 8 ++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/core/util.js b/lib/core/util.js index ab94bcfe51c..3939f0d25e5 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -48,38 +48,38 @@ function parseURL (url) { url = new URL(url) if (!/^https?:/.test(url.origin || url.protocol)) { - throw new InvalidArgumentError('invalid protocol') + throw new InvalidArgumentError('Invalid URL protocol: the URL must start with `http:` or `https:`.') } return url } if (!url || typeof url !== 'object') { - throw new InvalidArgumentError('invalid url') + throw new InvalidArgumentError('Invalid URL: The URL argument must be a non-null object.') } if (url.port != null && url.port !== '' && !Number.isFinite(parseInt(url.port))) { - throw new InvalidArgumentError('invalid port') + throw new InvalidArgumentError('Invalid URL: port must be a valid integer or a string representation of an integer.') } if (url.path != null && typeof url.path !== 'string') { - throw new InvalidArgumentError('invalid path') + throw new InvalidArgumentError('Invalid URL path: the path must be a string or null/undefined.') } if (url.pathname != null && typeof url.pathname !== 'string') { - throw new InvalidArgumentError('invalid pathname') + throw new InvalidArgumentError('Invalid URL pathname: the pathname must be a string or null/undefined.') } if (url.hostname != null && typeof url.hostname !== 'string') { - throw new InvalidArgumentError('invalid hostname') + throw new InvalidArgumentError('Invalid URL hostname: the hostname must be a string or null/undefined.') } if (url.origin != null && typeof url.origin !== 'string') { - throw new InvalidArgumentError('invalid origin') + throw new InvalidArgumentError('Invalid URL origin: the origin must be a string or null/undefined.') } if (!/^https?:/.test(url.origin || url.protocol)) { - throw new InvalidArgumentError('invalid protocol') + throw new InvalidArgumentError('Invalid URL protocol: the URL must start with `http:` or `https:`.') } if (!(url instanceof URL)) { diff --git a/test/client-errors.js b/test/client-errors.js index fc6e534c824..936841fbb15 100644 --- a/test/client-errors.js +++ b/test/client-errors.js @@ -260,7 +260,7 @@ test('invalid options throws', (t) => { t.fail() } catch (err) { t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'invalid port') + t.equal(err.message, 'Invalid URL: port must be a valid integer or a string representation of an integer.') } try { @@ -364,7 +364,7 @@ test('invalid options throws', (t) => { t.fail() } catch (err) { t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'invalid protocol') + t.equal(err.message, 'Invalid URL protocol: the URL must start with `http:` or `https:`.') } try { @@ -374,7 +374,7 @@ test('invalid options throws', (t) => { t.fail() } catch (err) { t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'invalid hostname') + t.equal(err.message, 'Invalid URL hostname: the hostname must be a string or null/undefined.') } try { @@ -392,7 +392,7 @@ test('invalid options throws', (t) => { t.fail() } catch (err) { t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'invalid url') + t.equal(err.message, 'Invalid URL: The URL argument must be a non-null object.') } try { From f0271d46cbdc47faebfc28579b54431292e09087 Mon Sep 17 00:00:00 2001 From: Khafra Date: Sat, 25 Mar 2023 14:20:50 -0400 Subject: [PATCH 009/259] fix(fetch): remove content-length header on redirect (#2022) Fixes https://github.com/nodejs/undici/issues/2021 --- lib/fetch/constants.js | 8 +++++++- test/fetch/issue-2021.js | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 test/fetch/issue-2021.js diff --git a/lib/fetch/constants.js b/lib/fetch/constants.js index bc90a031cde..a5294a994fb 100644 --- a/lib/fetch/constants.js +++ b/lib/fetch/constants.js @@ -48,11 +48,17 @@ const requestCache = [ 'only-if-cached' ] +// https://fetch.spec.whatwg.org/#request-body-header-name const requestBodyHeader = [ 'content-encoding', 'content-language', 'content-location', - 'content-type' + 'content-type', + // See https://github.com/nodejs/undici/issues/2021 + // 'Content-Length' is a forbidden header name, which is typically + // removed in the Headers implementation. However, undici doesn't + // filter out headers, so we add it here. + 'content-length' ] // https://fetch.spec.whatwg.org/#enumdef-requestduplex diff --git a/test/fetch/issue-2021.js b/test/fetch/issue-2021.js new file mode 100644 index 00000000000..cd28a7165d7 --- /dev/null +++ b/test/fetch/issue-2021.js @@ -0,0 +1,32 @@ +'use strict' + +const { test } = require('tap') +const { once } = require('events') +const { createServer } = require('http') +const { fetch } = require('../..') + +// https://github.com/nodejs/undici/issues/2021 +test('content-length header is removed on redirect', async (t) => { + const server = createServer((req, res) => { + if (req.url === '/redirect') { + res.writeHead(302, { Location: '/redirect2' }) + res.end() + return + } + + res.end() + }).listen(0).unref() + + t.teardown(server.close.bind(server)) + await once(server, 'listening') + + const body = 'a+b+c' + + await t.resolves(fetch(`http://localhost:${server.address().port}/redirect`, { + method: 'POST', + body, + headers: { + 'content-length': Buffer.byteLength(body) + } + })) +}) From a9ef50944e917695b1789ad62fc5e1bb9451d483 Mon Sep 17 00:00:00 2001 From: Michele Azzolari Date: Tue, 28 Mar 2023 20:08:34 +0200 Subject: [PATCH 010/259] fix(fetch): remove assertion on request.body.source on redirect (#2027) (#2028) Fixes https://github.com/nodejs/undici/issues/2027 --- lib/fetch/index.js | 2 +- test/fetch/redirect.js | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/lib/fetch/index.js b/lib/fetch/index.js index cc5090c556f..b45ed74cf02 100644 --- a/lib/fetch/index.js +++ b/lib/fetch/index.js @@ -1205,7 +1205,7 @@ async function httpRedirectFetch (fetchParams, response) { // 14. If request’s body is non-null, then set request’s body to the first return // value of safely extracting request’s body’s source. if (request.body != null) { - assert(request.body.source) + assert(request.body.source != null) request.body = safelyExtractBody(request.body.source)[0] } diff --git a/test/fetch/redirect.js b/test/fetch/redirect.js index c43ee482133..7e3681b3d23 100644 --- a/test/fetch/redirect.js +++ b/test/fetch/redirect.js @@ -27,3 +27,24 @@ test('Redirecting with a body does not cancel the current request - #1776', asyn t.equal(await resp.text(), '/redirect/') t.ok(resp.redirected) }) + +test('Redirecting with an empty body does not throw an error - #2027', async (t) => { + const server = createServer((req, res) => { + if (req.url === '/redirect') { + res.statusCode = 307 + res.setHeader('location', '/redirect/') + res.write('Moved Permanently') + res.end() + return + } + res.write(req.url) + res.end() + }).listen(0) + + t.teardown(server.close.bind(server)) + await once(server, 'listening') + + const resp = await fetch(`http://localhost:${server.address().port}/redirect`, { method: 'PUT', body: '' }) + t.equal(await resp.text(), '/redirect/') + t.ok(resp.redirected) +}) From dc8d11183fb95aa93d296bde3e21501856849367 Mon Sep 17 00:00:00 2001 From: Khafra Date: Wed, 29 Mar 2023 15:57:31 -0400 Subject: [PATCH 011/259] fix: skip failing test in node >= v19.8 (#2034) --- test/fetch/client-fetch.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/fetch/client-fetch.js b/test/fetch/client-fetch.js index 7a0cb550f54..3ce058ee9ae 100644 --- a/test/fetch/client-fetch.js +++ b/test/fetch/client-fetch.js @@ -8,6 +8,7 @@ const { ReadableStream } = require('stream/web') const { Blob } = require('buffer') const { fetch, Response, Request, FormData, File } = require('../..') const { Client, setGlobalDispatcher, Agent } = require('../..') +const { nodeMajor, nodeMinor } = require('../../lib/core/util') const nodeFetch = require('../../index-fetch') const { once } = require('events') const { gzipSync } = require('zlib') @@ -199,7 +200,9 @@ test('multipart formdata not base64', async (t) => { t.equal(text, 'example\ntext file') }) -test('multipart formdata base64', (t) => { +// TODO(@KhafraDev): re-enable this test once the issue is fixed +// See https://github.com/nodejs/node/issues/47301 +test('multipart formdata base64', { skip: nodeMajor >= 19 && nodeMinor >= 8 }, (t) => { t.plan(1) // Example form data with base64 encoding From 9013a23f90c1da16422f637ff111f75f22ee9a38 Mon Sep 17 00:00:00 2001 From: Khafra Date: Fri, 31 Mar 2023 15:45:10 -0400 Subject: [PATCH 012/259] fetch: treat content-encoding as case-insensitive & remove x-deflate (#2037) --- lib/fetch/index.js | 9 ++++++--- test/fetch/encoding.js | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 test/fetch/encoding.js diff --git a/lib/fetch/index.js b/lib/fetch/index.js index b45ed74cf02..584b0a2e65b 100644 --- a/lib/fetch/index.js +++ b/lib/fetch/index.js @@ -1984,7 +1984,9 @@ async function httpNetworkFetch ( const val = headersList[n + 1].toString('latin1') if (key.toLowerCase() === 'content-encoding') { - codings = val.split(',').map((x) => x.trim()) + // https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1 + // "All content-coding values are case-insensitive..." + codings = val.toLowerCase().split(',').map((x) => x.trim()) } else if (key.toLowerCase() === 'location') { location = val } @@ -2003,9 +2005,10 @@ async function httpNetworkFetch ( // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding if (request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status) && !willFollow) { for (const coding of codings) { - if (/(x-)?gzip/.test(coding)) { + // https://www.rfc-editor.org/rfc/rfc9112.html#section-7.2 + if (coding === 'x-gzip' || coding === 'gzip') { decoders.push(zlib.createGunzip()) - } else if (/(x-)?deflate/.test(coding)) { + } else if (coding === 'deflate') { decoders.push(zlib.createInflate()) } else if (coding === 'br') { decoders.push(zlib.createBrotliDecompress()) diff --git a/test/fetch/encoding.js b/test/fetch/encoding.js new file mode 100644 index 00000000000..63d22f8b5e4 --- /dev/null +++ b/test/fetch/encoding.js @@ -0,0 +1,33 @@ +'use strict' + +const { test } = require('tap') +const { createServer } = require('http') +const { once } = require('events') +const { fetch } = require('../..') +const { createBrotliCompress, createGzip } = require('zlib') + +test('content-encoding header is case-iNsENsITIve', async (t) => { + const contentCodings = 'GZiP, bR' + const text = 'Hello, World!' + + const server = createServer((req, res) => { + const gzip = createGzip() + const brotli = createBrotliCompress() + + res.setHeader('Content-Encoding', contentCodings) + res.setHeader('Content-Type', 'text/plain') + + brotli.pipe(gzip).pipe(res) + + brotli.write(text) + brotli.end() + }).listen(0) + + t.teardown(server.close.bind(server)) + await once(server, 'listening') + + const response = await fetch(`http://localhost:${server.address().port}`) + + t.equal(await response.text(), text) + t.equal(response.headers.get('content-encoding'), contentCodings) +}) From 7ae1779d0524157bc123bb5403be86ec111f461a Mon Sep 17 00:00:00 2001 From: Khafra Date: Sat, 1 Apr 2023 00:23:34 -0400 Subject: [PATCH 013/259] perf(fetch): use string comparisons for url schemes (#2038) --- lib/fetch/index.js | 18 +++++++++--------- lib/fetch/util.js | 44 +++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 50 insertions(+), 12 deletions(-) diff --git a/lib/fetch/index.js b/lib/fetch/index.js index 584b0a2e65b..0b2e3394322 100644 --- a/lib/fetch/index.js +++ b/lib/fetch/index.js @@ -37,7 +37,10 @@ const { isErrorLike, fullyReadBody, readableStreamClose, - isomorphicEncode + isomorphicEncode, + urlIsLocal, + urlIsHttpHttpsScheme, + urlHasHttpsScheme } = require('./util') const { kState, kHeaders, kGuard, kRealm, kHeadersCaseInsensitive } = require('./symbols') const assert = require('assert') @@ -272,7 +275,7 @@ function finalizeAndReportTiming (response, initiatorType = 'other') { let cacheState = response.cacheState // 6. If originalURL’s scheme is not an HTTP(S) scheme, then return. - if (!/^https?:/.test(originalURL.protocol)) { + if (!urlIsHttpHttpsScheme(originalURL)) { return } @@ -530,10 +533,7 @@ async function mainFetch (fetchParams, recursive = false) { // 3. If request’s local-URLs-only flag is set and request’s current URL is // not local, then set response to a network error. - if ( - request.localURLsOnly && - !/^(about|blob|data):/.test(requestCurrentURL(request).protocol) - ) { + if (request.localURLsOnly && !urlIsLocal(requestCurrentURL(request))) { response = makeNetworkError('local URLs only') } @@ -623,7 +623,7 @@ async function mainFetch (fetchParams, recursive = false) { } // request’s current URL’s scheme is not an HTTP(S) scheme - if (!/^https?:/.test(requestCurrentURL(request).protocol)) { + if (!urlIsHttpHttpsScheme(requestCurrentURL(request))) { // Return a network error. return makeNetworkError('URL scheme must be a HTTP(S) scheme') } @@ -1130,7 +1130,7 @@ async function httpRedirectFetch (fetchParams, response) { // 6. If locationURL’s scheme is not an HTTP(S) scheme, then return a network // error. - if (!/^https?:/.test(locationURL.protocol)) { + if (!urlIsHttpHttpsScheme(locationURL)) { return makeNetworkError('URL scheme must be a HTTP(S) scheme') } @@ -1399,7 +1399,7 @@ async function httpNetworkOrCacheFetch ( // header if httpRequest’s header list contains that header’s name. // TODO: https://github.com/whatwg/fetch/issues/1285#issuecomment-896560129 if (!httpRequest.headersList.contains('accept-encoding')) { - if (/^https:/.test(requestCurrentURL(httpRequest).protocol)) { + if (urlHasHttpsScheme(requestCurrentURL(httpRequest))) { httpRequest.headersList.append('accept-encoding', 'br, gzip, deflate') } else { httpRequest.headersList.append('accept-encoding', 'gzip, deflate') diff --git a/lib/fetch/util.js b/lib/fetch/util.js index 2d8977f17ba..4e44d9012af 100644 --- a/lib/fetch/util.js +++ b/lib/fetch/util.js @@ -64,7 +64,7 @@ function requestBadPort (request) { // 2. If url’s scheme is an HTTP(S) scheme and url’s port is a bad port, // then return blocked. - if (/^https?:/.test(url.protocol) && badPorts.includes(url.port)) { + if (urlIsHttpHttpsScheme(url) && badPorts.includes(url.port)) { return 'blocked' } @@ -285,7 +285,7 @@ function appendRequestOriginHeader (request) { case 'strict-origin': case 'strict-origin-when-cross-origin': // 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`. - if (/^https:/.test(request.origin) && !/^https:/.test(requestCurrentURL(request))) { + if (request.origin && urlHasHttpsScheme(request.origin) && !urlHasHttpsScheme(requestCurrentURL(request))) { serializedOrigin = null } break @@ -944,6 +944,41 @@ async function readAllBytes (reader, successSteps, failureSteps) { } } +/** + * @see https://fetch.spec.whatwg.org/#is-local + * @param {URL} url + */ +function urlIsLocal (url) { + assert('protocol' in url) // ensure it's a url object + + const protocol = url.protocol + + return protocol === 'about:' || protocol === 'blob:' || protocol === 'data:' +} + +/** + * @param {string|URL} url + */ +function urlHasHttpsScheme (url) { + if (typeof url === 'string') { + return url.startsWith('https:') + } + + return url.protocol === 'https:' +} + +/** + * @see https://fetch.spec.whatwg.org/#http-scheme + * @param {URL} url + */ +function urlIsHttpHttpsScheme (url) { + assert('protocol' in url) // ensure it's a url object + + const protocol = url.protocol + + return protocol === 'http:' || protocol === 'https:' +} + /** * Fetch supports node >= 16.8.0, but Object.hasOwn was added in v16.9.0. */ @@ -988,5 +1023,8 @@ module.exports = { isReadableStreamLike, readableStreamClose, isomorphicEncode, - isomorphicDecode + isomorphicDecode, + urlIsLocal, + urlHasHttpsScheme, + urlIsHttpHttpsScheme } From 549e7ed2e71d8cb2548162ad3e321c7b45af1fdb Mon Sep 17 00:00:00 2001 From: Khafra Date: Sat, 1 Apr 2023 03:42:24 -0400 Subject: [PATCH 014/259] util: replace util.toUSVString with String.prototype.toWellFormed (#2036) --- lib/core/util.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/core/util.js b/lib/core/util.js index 3939f0d25e5..bfee97946cd 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -409,6 +409,21 @@ function throwIfAborted (signal) { } } +const hasToWellFormed = !!String.prototype.toWellFormed + +/** + * @param {string} val + */ +function toUSVString (val) { + if (hasToWellFormed) { + return `${val}`.toWellFormed() + } else if (nodeUtil.toUSVString) { + return nodeUtil.toUSVString(val) + } + + return `${val}` +} + const kEnumerableProperty = Object.create(null) kEnumerableProperty.enumerable = true @@ -418,7 +433,7 @@ module.exports = { isDisturbed, isErrored, isReadable, - toUSVString: nodeUtil.toUSVString || ((val) => `${val}`), + toUSVString, isReadableAborted, isBlobLike, parseOrigin, From a6d147400f2f13fe8d18b18912042ecacc2d0518 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 1 Apr 2023 23:06:36 +0000 Subject: [PATCH 015/259] build(deps): bump github/codeql-action from 2.2.4 to 2.2.9 (#2039) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2.2.4 to 2.2.9. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/17573ee1cc1b9d061760f3a006fc4aac4f944fd5...04df1262e6247151b5ac09cd2c303ac36ad3f62b) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/scorecard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index ab9c3e40b34..5490130c491 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -51,6 +51,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2.2.4 + uses: github/codeql-action/upload-sarif@04df1262e6247151b5ac09cd2c303ac36ad3f62b # v2.2.9 with: sarif_file: results.sarif From 3d21d220fc3340d02d91108a84bf3222d6eef688 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Apr 2023 23:23:33 +0000 Subject: [PATCH 016/259] build(deps-dev): bump concurrently from 7.6.0 to 8.0.1 (#2041) Bumps [concurrently](https://github.com/open-cli-tools/concurrently) from 7.6.0 to 8.0.1. - [Release notes](https://github.com/open-cli-tools/concurrently/releases) - [Commits](https://github.com/open-cli-tools/concurrently/compare/v7.6.0...v8.0.1) --- updated-dependencies: - dependency-name: concurrently dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e019ebc7d1e..1406e338cb5 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "chai-as-promised": "^7.1.1", "chai-iterator": "^3.0.2", "chai-string": "^1.5.0", - "concurrently": "^7.1.0", + "concurrently": "^8.0.1", "cronometro": "^1.0.5", "delay": "^5.0.0", "dns-packet": "^5.4.0", From 5f3b8e16a9aa33fe72d640fe271d8e7a5748321a Mon Sep 17 00:00:00 2001 From: Yagiz Nizipli Date: Tue, 4 Apr 2023 08:44:02 -0400 Subject: [PATCH 017/259] Small performance improvements (#2044) * perf: improve null and undefined checks * perf: improve formdata delete operation --- lib/fetch/formdata.js | 9 +-------- lib/fetch/request.js | 6 +++--- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/lib/fetch/formdata.js b/lib/fetch/formdata.js index a957a80e02a..5975e26c1a0 100644 --- a/lib/fetch/formdata.js +++ b/lib/fetch/formdata.js @@ -61,14 +61,7 @@ class FormData { // The delete(name) method steps are to remove all entries whose name // is name from this’s entry list. - const next = [] - for (const entry of this[kState]) { - if (entry.name !== name) { - next.push(entry) - } - } - - this[kState] = next + this[kState] = this[kState].filter(entry => entry.name !== name) } get (name) { diff --git a/lib/fetch/request.js b/lib/fetch/request.js index 080a5d7bfa3..f3d63cca016 100644 --- a/lib/fetch/request.js +++ b/lib/fetch/request.js @@ -128,7 +128,7 @@ class Request { } // 10. If init["window"] exists and is non-null, then throw a TypeError. - if (init.window !== undefined && init.window != null) { + if (init.window != null) { throw new TypeError(`'window' option '${window}' must be null`) } @@ -427,7 +427,7 @@ class Request { // non-null, and request’s method is `GET` or `HEAD`, then throw a // TypeError. if ( - ((init.body !== undefined && init.body != null) || inputBody != null) && + (init.body != null || inputBody != null) && (request.method === 'GET' || request.method === 'HEAD') ) { throw new TypeError('Request with GET/HEAD method cannot have body.') @@ -437,7 +437,7 @@ class Request { let initBody = null // 36. If init["body"] exists and is non-null, then: - if (init.body !== undefined && init.body != null) { + if (init.body != null) { // 1. Let Content-Type be null. // 2. Set initBody and Content-Type to the result of extracting // init["body"], with keepalive set to request’s keepalive. From eceaf9adc564b887bb550e2ea6ca5797ff3b851a Mon Sep 17 00:00:00 2001 From: Darryl Pogue Date: Tue, 4 Apr 2023 16:10:43 -0700 Subject: [PATCH 018/259] fix(types): Add missing Blob import (#2047) This stops TypeScript from complaining: error TS2304: Cannot find name 'Blob'. --- types/websocket.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/types/websocket.d.ts b/types/websocket.d.ts index dadd8013c1e..7524cbda6c4 100644 --- a/types/websocket.d.ts +++ b/types/websocket.d.ts @@ -1,5 +1,6 @@ /// +import type { Blob } from 'buffer' import type { MessagePort } from 'worker_threads' import { EventTarget, From 816dcaa5bb39c477a66b8abb497b5cddcd9cdda4 Mon Sep 17 00:00:00 2001 From: Khafra Date: Wed, 5 Apr 2023 00:34:51 -0400 Subject: [PATCH 019/259] fix: set window option properly (#2048) * fix: set window option properly * Update lib/fetch/request.js Co-authored-by: Robert Nagy * Update lib/fetch/request.js Co-authored-by: Robert Nagy * add test --------- Co-authored-by: Robert Nagy --- lib/fetch/request.js | 2 +- test/fetch/407-statuscode-window-null.js | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 test/fetch/407-statuscode-window-null.js diff --git a/lib/fetch/request.js b/lib/fetch/request.js index f3d63cca016..d6263fd2d43 100644 --- a/lib/fetch/request.js +++ b/lib/fetch/request.js @@ -133,7 +133,7 @@ class Request { } // 11. If init["window"] exists, then set window to "no-window". - if (init.window !== undefined) { + if ('window' in init) { window = 'no-window' } diff --git a/test/fetch/407-statuscode-window-null.js b/test/fetch/407-statuscode-window-null.js new file mode 100644 index 00000000000..e22554fac15 --- /dev/null +++ b/test/fetch/407-statuscode-window-null.js @@ -0,0 +1,20 @@ +'use strict' + +const { fetch } = require('../..') +const { createServer } = require('http') +const { once } = require('events') +const { test } = require('tap') + +test('Receiving a 407 status code w/ a window option present should reject', async (t) => { + const server = createServer((req, res) => { + res.statusCode = 407 + res.end() + }).listen(0) + + t.teardown(server.close.bind(server)) + await once(server, 'listening') + + // if init.window exists, the spec tells us to set request.window to 'no-window', + // which later causes the request to be rejected if the status code is 407 + await t.rejects(fetch(`http://localhost:${server.address().port}`, { window: null })) +}) From a1846e510cd6b4d9dd473bc96c3619d684b04a8c Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Sat, 8 Apr 2023 17:53:51 +0200 Subject: [PATCH 020/259] fetch: fix leak (#2049) --- lib/fetch/request.js | 17 +++++++++++++--- package.json | 2 +- test/client-keep-alive.js | 2 +- test/fetch/fetch-leak.js | 43 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 5 deletions(-) create mode 100644 test/fetch/fetch-leak.js diff --git a/lib/fetch/request.js b/lib/fetch/request.js index d6263fd2d43..32c84bf7e6b 100644 --- a/lib/fetch/request.js +++ b/lib/fetch/request.js @@ -34,6 +34,7 @@ const { setMaxListeners, getEventListeners, defaultMaxListeners } = require('eve let TransformStream = globalThis.TransformStream const kInit = Symbol('init') +const kAbortController = Symbol('abortController') const requestFinalizer = new FinalizationRegistry(({ signal, abort }) => { signal.removeEventListener('abort', abort) @@ -354,12 +355,22 @@ class Request { if (signal.aborted) { ac.abort(signal.reason) } else { + // Keep a strong ref to ac while request object + // is alive. This is needed to prevent AbortController + // from being prematurely garbage collected. + // See, https://github.com/nodejs/undici/issues/1926. + this[kAbortController] = ac + + const acRef = new WeakRef(ac) const abort = function () { - ac.abort(this.reason) + const ac = acRef.deref() + if (ac !== undefined) { + ac.abort(this.reason) + } } // Third-party AbortControllers may not work with these. - // See https://github.com/nodejs/undici/pull/1910#issuecomment-1464495619 + // See, https://github.com/nodejs/undici/pull/1910#issuecomment-1464495619. try { if (getEventListeners(signal, 'abort').length >= defaultMaxListeners) { setMaxListeners(100, signal) @@ -367,7 +378,7 @@ class Request { } catch {} signal.addEventListener('abort', abort, { once: true }) - requestFinalizer.register(this, { signal, abort }) + requestFinalizer.register(ac, { signal, abort }) } } diff --git a/package.json b/package.json index 1406e338cb5..d64ce0b9c2f 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "test": "npm run test:tap && npm run test:node-fetch && npm run test:fetch && npm run test:cookies && npm run test:wpt && npm run test:websocket && npm run test:jest && tsd", "test:cookies": "node scripts/verifyVersion 16 || tap test/cookie/*.js", "test:node-fetch": "node scripts/verifyVersion.js 16 || mocha test/node-fetch", - "test:fetch": "node scripts/verifyVersion.js 16 || (npm run build:node && tap test/fetch/*.js && tap test/webidl/*.js)", + "test:fetch": "node scripts/verifyVersion.js 16 || (npm run build:node && tap --expose-gc test/fetch/*.js && tap test/webidl/*.js)", "test:jest": "node scripts/verifyVersion.js 14 || jest", "test:tap": "tap test/*.js test/diagnostics-channel/*.js", "test:tdd": "tap test/*.js test/diagnostics-channel/*.js -w", diff --git a/test/client-keep-alive.js b/test/client-keep-alive.js index e752995f1f9..968bc50e89f 100644 --- a/test/client-keep-alive.js +++ b/test/client-keep-alive.js @@ -32,7 +32,7 @@ test('keep-alive header', (t) => { body.on('end', () => { const timeout = setTimeout(() => { t.fail() - }, 3e3) + }, 4e3) client.on('disconnect', () => { t.pass() clearTimeout(timeout) diff --git a/test/fetch/fetch-leak.js b/test/fetch/fetch-leak.js new file mode 100644 index 00000000000..e7a260208e3 --- /dev/null +++ b/test/fetch/fetch-leak.js @@ -0,0 +1,43 @@ +'use strict' + +const { test } = require('tap') +const { fetch } = require('../..') +const { createServer } = require('http') + +test('do not leak', (t) => { + t.plan(1) + + const server = createServer((req, res) => { + res.end() + }) + t.teardown(server.close.bind(server)) + + let url + let done = false + server.listen(0, function attack () { + if (done) { + return + } + url ??= new URL(`http://127.0.0.1:${server.address().port}`) + const controller = new AbortController() + fetch(url, { signal: controller.signal }) + .then(res => res.arrayBuffer()) + .then(attack) + }) + + let prev = Infinity + let count = 0 + const interval = setInterval(() => { + done = true + global.gc() + const next = process.memoryUsage().heapUsed + if (next <= prev) { + t.pass() + } else if (count++ > 10) { + t.fail() + } else { + prev = next + } + }, 1e3) + t.teardown(() => clearInterval(interval)) +}) From 2d9441733c231da8b70f31c39eb08a234a42e4bf Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Sat, 8 Apr 2023 18:15:00 +0200 Subject: [PATCH 021/259] 5.21.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d64ce0b9c2f..79aa9b2758c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "5.21.0", + "version": "5.21.1", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { From fb84aac5d428b060b5933fdb4eb91a1a1bdb9395 Mon Sep 17 00:00:00 2001 From: Khafra Date: Sat, 8 Apr 2023 12:53:43 -0400 Subject: [PATCH 022/259] Content disposition parsing (#2051) --- lib/core/util.js | 37 +++++++++++++++++++++++++------------ test/issue-1903.js | 3 ++- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/lib/core/util.js b/lib/core/util.js index bfee97946cd..6f247d22a52 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -222,40 +222,53 @@ function parseHeaders (headers, obj = {}) { const key = headers[i].toString().toLowerCase() let val = obj[key] - const encoding = key.length === 19 && key === 'content-disposition' - ? 'latin1' - : 'utf8' - if (!val) { if (Array.isArray(headers[i + 1])) { obj[key] = headers[i + 1] } else { - obj[key] = headers[i + 1].toString(encoding) + obj[key] = headers[i + 1].toString('utf8') } } else { if (!Array.isArray(val)) { val = [val] obj[key] = val } - val.push(headers[i + 1].toString(encoding)) + val.push(headers[i + 1].toString('utf8')) } } + + // See https://github.com/nodejs/node/pull/46528 + if ('content-length' in obj && 'content-disposition' in obj) { + obj['content-disposition'] = Buffer.from(obj['content-disposition']).toString('latin1') + } + return obj } function parseRawHeaders (headers) { const ret = [] + let hasContentLength = false + let contentDispositionIdx = -1 + for (let n = 0; n < headers.length; n += 2) { const key = headers[n + 0].toString() + const val = headers[n + 1].toString('utf8') - const encoding = key.length === 19 && key.toLowerCase() === 'content-disposition' - ? 'latin1' - : 'utf8' - - const val = headers[n + 1].toString(encoding) + if (key.length === 14 && (key === 'content-length' || key.toLowerCase() === 'content-length')) { + ret.push(key, val) + hasContentLength = true + } else if (key.length === 19 && (key === 'content-disposition' || key.toLowerCase() === 'content-disposition')) { + contentDispositionIdx = ret.push(key, val) - 1 + } else { + ret.push(key, val) + } + } - ret.push(key, val) + // See https://github.com/nodejs/node/pull/46528 + if (hasContentLength && contentDispositionIdx !== -1) { + ret[contentDispositionIdx] = Buffer.from(ret[contentDispositionIdx]).toString('latin1') } + return ret } diff --git a/test/issue-1903.js b/test/issue-1903.js index 8478b2d53dd..99eb6f60517 100644 --- a/test/issue-1903.js +++ b/test/issue-1903.js @@ -3,6 +3,7 @@ const { createServer } = require('http') const { test } = require('tap') const { request } = require('..') +const { nodeMajor } = require('../lib/core/util') function createPromise () { const result = {} @@ -12,7 +13,7 @@ function createPromise () { return result } -test('should parse content-disposition consistencely', async (t) => { +test('should parse content-disposition consistently', { skip: nodeMajor >= 19 }, async (t) => { t.plan(5) // create promise to allow server spinup in parallel From 0562e9bf2625296bf28dc5e8d93099e4d996bc67 Mon Sep 17 00:00:00 2001 From: Khafra Date: Sat, 8 Apr 2023 13:39:37 -0400 Subject: [PATCH 023/259] fix: clear set-cookie headers (#2052) --- lib/fetch/headers.js | 1 + test/fetch/request.js | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/lib/fetch/headers.js b/lib/fetch/headers.js index 5093ef8726f..b42a5edeaab 100644 --- a/lib/fetch/headers.js +++ b/lib/fetch/headers.js @@ -95,6 +95,7 @@ class HeadersList { clear () { this[kHeadersMap].clear() this[kHeadersSortedMap] = null + this.cookies = null } // https://fetch.spec.whatwg.org/#concept-header-list-append diff --git a/test/fetch/request.js b/test/fetch/request.js index 29d88b2ec66..cd32adc7d6f 100644 --- a/test/fetch/request.js +++ b/test/fetch/request.js @@ -460,3 +460,19 @@ test('constructing Request with third party FormData body', async (t) => { t.equal(contentType[0], 'multipart/form-data; boundary') t.ok((await req.text()).startsWith(`--${contentType[1]}`)) }) + +// https://github.com/nodejs/undici/issues/2050 +test('set-cookie headers get cleared when passing a Request as first param', (t) => { + const req1 = new Request('http://localhost', { + headers: { + 'set-cookie': 'a=1' + } + }) + + t.same([...req1.headers], [['set-cookie', 'a=1']]) + const req2 = new Request(req1, { headers: {} }) + + t.same([...req2.headers], []) + t.same(req2.headers.getSetCookie(), []) + t.end() +}) From b20405e54a7b69eca58cab70a43d8cdbab511468 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Sun, 9 Apr 2023 06:51:00 +0200 Subject: [PATCH 024/259] 5.21.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 79aa9b2758c..f0eb6897186 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "5.21.1", + "version": "5.21.2", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { From 31884e6ece20f3a5a425688995c8081ac01d2d4f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 9 Apr 2023 07:10:20 +0200 Subject: [PATCH 025/259] build(deps-dev): bump tsd from 0.27.0 to 0.28.1 (#2042) Bumps [tsd](https://github.com/SamVerschueren/tsd) from 0.27.0 to 0.28.1. - [Release notes](https://github.com/SamVerschueren/tsd/releases) - [Commits](https://github.com/SamVerschueren/tsd/compare/v0.27.0...v0.28.1) --- updated-dependencies: - dependency-name: tsd dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f0eb6897186..636d52d9ce8 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,7 @@ "standard": "^17.0.0", "table": "^6.8.0", "tap": "^16.1.0", - "tsd": "^0.27.0", + "tsd": "^0.28.1", "typescript": "^5.0.2", "wait-on": "^7.0.1", "ws": "^8.11.0" From 8c07892cd8418847fbca37b92a4075c53fa40df8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 9 Apr 2023 07:10:37 +0200 Subject: [PATCH 026/259] build(deps): bump ossf/scorecard-action from 2.1.2 to 2.1.3 (#2040) Bumps [ossf/scorecard-action](https://github.com/ossf/scorecard-action) from 2.1.2 to 2.1.3. - [Release notes](https://github.com/ossf/scorecard-action/releases) - [Changelog](https://github.com/ossf/scorecard-action/blob/main/RELEASE.md) - [Commits](https://github.com/ossf/scorecard-action/compare/e38b1902ae4f44df626f11ba0734b14fb91f8f86...80e868c13c90f172d68d1f4501dee99e2479f7af) --- updated-dependencies: - dependency-name: ossf/scorecard-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/scorecard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 5490130c491..a6affe5f2b0 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -34,7 +34,7 @@ jobs: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@e38b1902ae4f44df626f11ba0734b14fb91f8f86 # v2.1.2 + uses: ossf/scorecard-action@80e868c13c90f172d68d1f4501dee99e2479f7af # v2.1.3 with: results_file: results.sarif results_format: sarif From fa4ba00cc1d595ae4488abe1ae3a45929808cecb Mon Sep 17 00:00:00 2001 From: Khafra Date: Sun, 9 Apr 2023 12:23:40 -0400 Subject: [PATCH 027/259] fix: handle opaque origin in sameOrigin (#2053) * fix: handle opaque origin in sameOrigin * fix: skip tsd on node 12 --- lib/fetch/util.js | 4 +++- package.json | 4 ++-- test/fetch/util.js | 10 ++++++++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/fetch/util.js b/lib/fetch/util.js index 4e44d9012af..23023262d14 100644 --- a/lib/fetch/util.js +++ b/lib/fetch/util.js @@ -638,7 +638,9 @@ function tryUpgradeRequestToAPotentiallyTrustworthyURL (request) { */ function sameOrigin (A, B) { // 1. If A and B are the same opaque origin, then return true. - // "opaque origin" is an internal value we cannot access, ignore. + if (A.origin === B.origin && A.origin === 'null') { + return true + } // 2. If A and B are both tuple origins and their schemes, // hosts, and port are identical, then return true. diff --git a/package.json b/package.json index 636d52d9ce8..85f44d0614d 100644 --- a/package.json +++ b/package.json @@ -46,14 +46,14 @@ "build:wasm": "node build/wasm.js --docker", "lint": "standard | snazzy", "lint:fix": "standard --fix | snazzy", - "test": "npm run test:tap && npm run test:node-fetch && npm run test:fetch && npm run test:cookies && npm run test:wpt && npm run test:websocket && npm run test:jest && tsd", + "test": "npm run test:tap && npm run test:node-fetch && npm run test:fetch && npm run test:cookies && npm run test:wpt && npm run test:websocket && npm run test:jest && npm run test:typescript", "test:cookies": "node scripts/verifyVersion 16 || tap test/cookie/*.js", "test:node-fetch": "node scripts/verifyVersion.js 16 || mocha test/node-fetch", "test:fetch": "node scripts/verifyVersion.js 16 || (npm run build:node && tap --expose-gc test/fetch/*.js && tap test/webidl/*.js)", "test:jest": "node scripts/verifyVersion.js 14 || jest", "test:tap": "tap test/*.js test/diagnostics-channel/*.js", "test:tdd": "tap test/*.js test/diagnostics-channel/*.js -w", - "test:typescript": "tsd && tsc test/imports/undici-import.ts", + "test:typescript": "node scripts/verifyVersion.js 14 || tsd", "test:websocket": "node scripts/verifyVersion.js 18 || tap test/websocket/*.js", "test:wpt": "node scripts/verifyVersion 18 || (node test/wpt/start-fetch.mjs && node test/wpt/start-FileAPI.mjs && node test/wpt/start-mimesniff.mjs && node test/wpt/start-xhr.mjs && node --no-warnings test/wpt/start-websockets.mjs)", "coverage": "nyc --reporter=text --reporter=html npm run test", diff --git a/test/fetch/util.js b/test/fetch/util.js index 3a6e63b6b2e..02b75bc7783 100644 --- a/test/fetch/util.js +++ b/test/fetch/util.js @@ -111,6 +111,16 @@ test('sameOrigin', (t) => { t.end() }) + t.test('file:// urls', (t) => { + // urls with opaque origins should return true + + const a = new URL('file:///C:/undici') + const b = new URL('file:///var/undici') + + t.ok(util.sameOrigin(a, b)) + t.end() + }) + t.end() }) From dfaec78f7a29f07bb043f9006ed0ceb0d5220b55 Mon Sep 17 00:00:00 2001 From: Khafra Date: Sun, 9 Apr 2023 13:20:19 -0400 Subject: [PATCH 028/259] test: add typescript import test back (#2054) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 85f44d0614d..912b86a5234 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "test:jest": "node scripts/verifyVersion.js 14 || jest", "test:tap": "tap test/*.js test/diagnostics-channel/*.js", "test:tdd": "tap test/*.js test/diagnostics-channel/*.js -w", - "test:typescript": "node scripts/verifyVersion.js 14 || tsd", + "test:typescript": "node scripts/verifyVersion.js 14 || tsd && tsc --skipLibCheck test/imports/undici-import.ts", "test:websocket": "node scripts/verifyVersion.js 18 || tap test/websocket/*.js", "test:wpt": "node scripts/verifyVersion 18 || (node test/wpt/start-fetch.mjs && node test/wpt/start-FileAPI.mjs && node test/wpt/start-mimesniff.mjs && node test/wpt/start-xhr.mjs && node --no-warnings test/wpt/start-websockets.mjs)", "coverage": "nyc --reporter=text --reporter=html npm run test", From 5ca10aa5bb4be3099c4c52e990c064a3a33a421e Mon Sep 17 00:00:00 2001 From: Khafra Date: Wed, 12 Apr 2023 10:53:08 -0400 Subject: [PATCH 029/259] fix: use getMaxListeners when available (#2063) --- lib/fetch/request.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/fetch/request.js b/lib/fetch/request.js index 32c84bf7e6b..5c836e4e558 100644 --- a/lib/fetch/request.js +++ b/lib/fetch/request.js @@ -29,7 +29,7 @@ const { getGlobalOrigin } = require('./global') const { URLSerializer } = require('./dataURL') const { kHeadersList } = require('../core/symbols') const assert = require('assert') -const { setMaxListeners, getEventListeners, defaultMaxListeners } = require('events') +const { getMaxListeners, setMaxListeners, getEventListeners, defaultMaxListeners } = require('events') let TransformStream = globalThis.TransformStream @@ -372,7 +372,11 @@ class Request { // Third-party AbortControllers may not work with these. // See, https://github.com/nodejs/undici/pull/1910#issuecomment-1464495619. try { - if (getEventListeners(signal, 'abort').length >= defaultMaxListeners) { + // If the max amount of listeners is equal to the default, increase it + // This is only available in node >= v19.9.0 + if (typeof getMaxListeners === 'function' && getMaxListeners(signal) === defaultMaxListeners) { + setMaxListeners(100, signal) + } else if (getEventListeners(signal, 'abort').length >= defaultMaxListeners) { setMaxListeners(100, signal) } } catch {} From ded60f65772371b52d575e602a82551905bbeda8 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Thu, 13 Apr 2023 09:38:02 +0200 Subject: [PATCH 030/259] feat: allow overriding hwm (#2057) --- lib/api/api-request.js | 11 ++++++++--- lib/api/readable.js | 9 +++++++-- test/client-request.js | 23 +++++++++++++++++++++++ test/readable.test.js | 2 +- types/dispatcher.d.ts | 2 ++ 5 files changed, 41 insertions(+), 6 deletions(-) diff --git a/lib/api/api-request.js b/lib/api/api-request.js index b4674878d2e..bbb80cc96e7 100644 --- a/lib/api/api-request.js +++ b/lib/api/api-request.js @@ -16,13 +16,17 @@ class RequestHandler extends AsyncResource { throw new InvalidArgumentError('invalid opts') } - const { signal, method, opaque, body, onInfo, responseHeaders, throwOnError } = opts + const { signal, method, opaque, body, onInfo, responseHeaders, throwOnError, highWaterMark } = opts try { if (typeof callback !== 'function') { throw new InvalidArgumentError('invalid callback') } + if (highWaterMark && (typeof highWaterMark !== 'number' || highWaterMark < 0)) { + throw new InvalidArgumentError('invalid highWaterMark') + } + if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') { throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget') } @@ -53,6 +57,7 @@ class RequestHandler extends AsyncResource { this.context = null this.onInfo = onInfo || null this.throwOnError = throwOnError + this.highWaterMark = highWaterMark if (util.isStream(body)) { body.on('error', (err) => { @@ -73,7 +78,7 @@ class RequestHandler extends AsyncResource { } onHeaders (statusCode, rawHeaders, resume, statusMessage) { - const { callback, opaque, abort, context } = this + const { callback, opaque, abort, context, highWaterMark } = this if (statusCode < 200) { if (this.onInfo) { @@ -85,7 +90,7 @@ class RequestHandler extends AsyncResource { const parsedHeaders = util.parseHeaders(rawHeaders) const contentType = parsedHeaders['content-type'] - const body = new Readable(resume, abort, contentType) + const body = new Readable({ resume, abort, contentType, highWaterMark }) this.callback = null this.res = body diff --git a/lib/api/readable.js b/lib/api/readable.js index a184e8eb51b..398a75ba8bb 100644 --- a/lib/api/readable.js +++ b/lib/api/readable.js @@ -17,11 +17,16 @@ const kAbort = Symbol('abort') const kContentType = Symbol('kContentType') module.exports = class BodyReadable extends Readable { - constructor (resume, abort, contentType = '') { + constructor ({ + resume, + abort, + contentType = '', + highWaterMark = 64 * 1024 // Same as nodejs fs streams. + }) { super({ autoDestroy: true, read: resume, - highWaterMark: 64 * 1024 // Same as nodejs fs streams. + highWaterMark }) this._readableState.dataEmitted = false diff --git a/test/client-request.js b/test/client-request.js index 2d73e9892ee..626db41187a 100644 --- a/test/client-request.js +++ b/test/client-request.js @@ -75,6 +75,29 @@ test('request dump with abort signal', (t) => { }) }) +test('request hwm', (t) => { + t.plan(2) + const server = createServer((req, res) => { + res.write('hello') + }) + t.teardown(server.close.bind(server)) + + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`) + t.teardown(client.destroy.bind(client)) + + client.request({ + path: '/', + method: 'GET', + highWaterMark: 1000 + }, (err, { body }) => { + t.error(err) + t.same(body.readableHighWaterMark, 1000) + body.dump() + }) + }) +}) + test('request abort before headers', (t) => { t.plan(6) diff --git a/test/readable.test.js b/test/readable.test.js index da329939896..3f4f7939f94 100644 --- a/test/readable.test.js +++ b/test/readable.test.js @@ -8,7 +8,7 @@ test('avoid body reordering', async function (t) { } function abort () { } - const r = new Readable(resume, abort) + const r = new Readable({ resume, abort }) r.push(Buffer.from('hello')) diff --git a/types/dispatcher.d.ts b/types/dispatcher.d.ts index c6b0c8875eb..412520386f3 100644 --- a/types/dispatcher.d.ts +++ b/types/dispatcher.d.ts @@ -142,6 +142,8 @@ declare namespace Dispatcher { onInfo?: (info: { statusCode: number, headers: Record }) => void; /** Default: `null` */ responseHeader?: 'raw' | null; + /** Default: `64 KiB` */ + highWaterMark?: number; } export interface PipelineOptions extends RequestOptions { /** `true` if the `handler` will return an object stream. Default: `false` */ From 8349d265029c5131b7d00f99484e01937209f192 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Thu, 13 Apr 2023 09:38:22 +0200 Subject: [PATCH 031/259] fix: there is no sync connector (#2059) --- types/connector.d.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/types/connector.d.ts b/types/connector.d.ts index d53d7952b5c..847284a1f2b 100644 --- a/types/connector.d.ts +++ b/types/connector.d.ts @@ -27,13 +27,7 @@ declare namespace buildConnector { export type Callback = (...args: CallbackArgs) => void type CallbackArgs = [null, Socket | TLSSocket] | [Error, null] - export type connector = connectorAsync | connectorSync - - interface connectorSync { - (options: buildConnector.Options): Socket | TLSSocket - } - - interface connectorAsync { + export interface connector { (options: buildConnector.Options, callback: buildConnector.Callback): void } } From 25c3230229aaf0ab7db783b484dcba6c1991dbce Mon Sep 17 00:00:00 2001 From: Khafra Date: Thu, 13 Apr 2023 03:38:51 -0400 Subject: [PATCH 032/259] fix: rename .wasm to -wasm to appease jest (#2064) * fix: rename .wasm to -wasm to appease jest * fix: use local server for redirect node-fetch test --- build/wasm.js | 4 ++-- lib/client.js | 6 +++--- lib/llhttp/{llhttp.wasm.js => llhttp-wasm.js} | 0 lib/llhttp/{llhttp_simd.wasm.js => llhttp_simd-wasm.js} | 0 test/node-fetch/main.js | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) rename lib/llhttp/{llhttp.wasm.js => llhttp-wasm.js} (100%) rename lib/llhttp/{llhttp_simd.wasm.js => llhttp_simd-wasm.js} (100%) diff --git a/build/wasm.js b/build/wasm.js index 98de2b38c5d..48ec53cb0b4 100644 --- a/build/wasm.js +++ b/build/wasm.js @@ -46,7 +46,7 @@ execSync(`${WASI_ROOT}/bin/clang \ const base64Wasm = readFileSync(join(WASM_OUT, 'llhttp.wasm')).toString('base64') writeFileSync( - join(WASM_OUT, 'llhttp.wasm.js'), + join(WASM_OUT, 'llhttp-wasm.js'), `module.exports = "${base64Wasm}";\n` ) @@ -74,6 +74,6 @@ execSync(`${WASI_ROOT}/bin/clang \ const base64WasmSimd = readFileSync(join(WASM_OUT, 'llhttp_simd.wasm')).toString('base64') writeFileSync( - join(WASM_OUT, 'llhttp_simd.wasm.js'), + join(WASM_OUT, 'llhttp_simd-wasm.js'), `module.exports = "${base64WasmSimd}";\n` ) diff --git a/lib/client.js b/lib/client.js index b230c368dab..269d6e1a607 100644 --- a/lib/client.js +++ b/lib/client.js @@ -359,11 +359,11 @@ const createRedirectInterceptor = require('./interceptor/redirectInterceptor') const EMPTY_BUF = Buffer.alloc(0) async function lazyllhttp () { - const llhttpWasmData = process.env.JEST_WORKER_ID ? require('./llhttp/llhttp.wasm.js') : undefined + const llhttpWasmData = process.env.JEST_WORKER_ID ? require('./llhttp/llhttp-wasm.js') : undefined let mod try { - mod = await WebAssembly.compile(Buffer.from(require('./llhttp/llhttp_simd.wasm.js'), 'base64')) + mod = await WebAssembly.compile(Buffer.from(require('./llhttp/llhttp_simd-wasm.js'), 'base64')) } catch (e) { /* istanbul ignore next */ @@ -371,7 +371,7 @@ async function lazyllhttp () { // being enabled, but the occurring of this other error // * https://github.com/emscripten-core/emscripten/issues/11495 // got me to remove that check to avoid breaking Node 12. - mod = await WebAssembly.compile(Buffer.from(llhttpWasmData || require('./llhttp/llhttp.wasm.js'), 'base64')) + mod = await WebAssembly.compile(Buffer.from(llhttpWasmData || require('./llhttp/llhttp-wasm.js'), 'base64')) } return await WebAssembly.instantiate(mod, { diff --git a/lib/llhttp/llhttp.wasm.js b/lib/llhttp/llhttp-wasm.js similarity index 100% rename from lib/llhttp/llhttp.wasm.js rename to lib/llhttp/llhttp-wasm.js diff --git a/lib/llhttp/llhttp_simd.wasm.js b/lib/llhttp/llhttp_simd-wasm.js similarity index 100% rename from lib/llhttp/llhttp_simd.wasm.js rename to lib/llhttp/llhttp_simd-wasm.js diff --git a/test/node-fetch/main.js b/test/node-fetch/main.js index ab3ee9f70ae..8dc20f2ce64 100644 --- a/test/node-fetch/main.js +++ b/test/node-fetch/main.js @@ -1648,7 +1648,7 @@ describe('node-fetch', () => { it('should allow manual redirect handling', function () { this.timeout(5000) - const url = 'https://httpbin.org/status/302' + const url = `${base}redirect/302` const options = { redirect: 'manual' } @@ -1656,7 +1656,7 @@ describe('node-fetch', () => { expect(res.status).to.equal(302) expect(res.url).to.equal(url) expect(res.type).to.equal('basic') - expect(res.headers.get('Location')).to.equal('/redirect/1') + expect(res.headers.get('Location')).to.equal('/inspect') expect(res.ok).to.be.false }) }) From 9aa639bee764aa66f593db9335aeaad60e76be1a Mon Sep 17 00:00:00 2001 From: Khafra Date: Fri, 14 Apr 2023 02:09:52 -0400 Subject: [PATCH 033/259] fix: set content-length when using FormData body w/ request (#2066) * fix: set content-length when using FormData body w/ request * import FormData --- lib/core/request.js | 1 + test/issue-2065.js | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 test/issue-2065.js diff --git a/lib/core/request.js b/lib/core/request.js index c82159e5ba0..6c9a24d5d59 100644 --- a/lib/core/request.js +++ b/lib/core/request.js @@ -182,6 +182,7 @@ class Request { this.headers += `content-type: ${contentType}\r\n` } this.body = bodyStream.stream + this.contentLength = bodyStream.length } else if (util.isBlobLike(body) && this.contentType == null && body.type) { this.contentType = body.type this.headers += `content-type: ${body.type}\r\n` diff --git a/test/issue-2065.js b/test/issue-2065.js new file mode 100644 index 00000000000..84183f0fb7c --- /dev/null +++ b/test/issue-2065.js @@ -0,0 +1,30 @@ +'use strict' + +const { test, skip } = require('tap') +const { nodeMajor, nodeMinor } = require('../lib/core/util') +const { createServer } = require('http') +const { once } = require('events') +const { File, FormData, request } = require('..') + +if (nodeMajor < 16 || (nodeMajor === 16 && nodeMinor < 8)) { + skip('FormData is not available in node < v16.8.0') + process.exit() +} + +test('undici.request with a FormData body should set content-length header', async (t) => { + const server = createServer((req, res) => { + t.ok(req.headers['content-length']) + res.end() + }).listen(0) + + t.teardown(server.close.bind(server)) + await once(server, 'listening') + + const body = new FormData() + body.set('file', new File(['abc'], 'abc.txt')) + + await request(`http://localhost:${server.address().port}`, { + method: 'POST', + body + }) +}) From 6eb954c664902d1e0d7cdb7fd25cba69f0c80433 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Fri, 14 Apr 2023 16:39:16 +0200 Subject: [PATCH 034/259] refactor: unify error body handling (#2060) * refactor: unify error body handling * fixup * fixup * fixup --- lib/api/api-request.js | 58 ++++++----------------- lib/api/api-stream.js | 103 ++++++++++++++++++----------------------- lib/api/util.js | 46 ++++++++++++++++++ test/client-stream.js | 2 +- 4 files changed, 108 insertions(+), 101 deletions(-) create mode 100644 lib/api/util.js diff --git a/lib/api/api-request.js b/lib/api/api-request.js index bbb80cc96e7..71d7e926b4c 100644 --- a/lib/api/api-request.js +++ b/lib/api/api-request.js @@ -3,10 +3,10 @@ const Readable = require('./readable') const { InvalidArgumentError, - RequestAbortedError, - ResponseStatusCodeError + RequestAbortedError } = require('../core/errors') const util = require('../core/util') +const { getResolveErrorBodyCallback } = require('./util') const { AsyncResource } = require('async_hooks') const { addSignal, removeSignal } = require('./abort-signal') @@ -78,40 +78,39 @@ class RequestHandler extends AsyncResource { } onHeaders (statusCode, rawHeaders, resume, statusMessage) { - const { callback, opaque, abort, context, highWaterMark } = this + const { callback, opaque, abort, context, responseHeaders, highWaterMark } = this + + const headers = responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) if (statusCode < 200) { if (this.onInfo) { - const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) this.onInfo({ statusCode, headers }) } return } - const parsedHeaders = util.parseHeaders(rawHeaders) + const parsedHeaders = responseHeaders === 'raw' ? util.parseHeaders(rawHeaders) : headers const contentType = parsedHeaders['content-type'] const body = new Readable({ resume, abort, contentType, highWaterMark }) this.callback = null this.res = body - const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) if (callback !== null) { if (this.throwOnError && statusCode >= 400) { this.runInAsyncScope(getResolveErrorBodyCallback, null, { callback, body, contentType, statusCode, statusMessage, headers } ) - return + } else { + this.runInAsyncScope(callback, null, null, { + statusCode, + headers, + trailers: this.trailers, + opaque, + body, + context + }) } - - this.runInAsyncScope(callback, null, null, { - statusCode, - headers, - trailers: this.trailers, - opaque, - body, - context - }) } } @@ -158,33 +157,6 @@ class RequestHandler extends AsyncResource { } } -async function getResolveErrorBodyCallback ({ callback, body, contentType, statusCode, statusMessage, headers }) { - if (statusCode === 204 || !contentType) { - body.dump() - process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers)) - return - } - - try { - if (contentType.startsWith('application/json')) { - const payload = await body.json() - process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers, payload)) - return - } - - if (contentType.startsWith('text/')) { - const payload = await body.text() - process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers, payload)) - return - } - } catch (err) { - // Process in a fallback if error - } - - body.dump() - process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers)) -} - function request (opts, callback) { if (callback === undefined) { return new Promise((resolve, reject) => { diff --git a/lib/api/api-stream.js b/lib/api/api-stream.js index 7560a2e6505..3a8e71a5730 100644 --- a/lib/api/api-stream.js +++ b/lib/api/api-stream.js @@ -4,10 +4,10 @@ const { finished, PassThrough } = require('stream') const { InvalidArgumentError, InvalidReturnValueError, - RequestAbortedError, - ResponseStatusCodeError + RequestAbortedError } = require('../core/errors') const util = require('../core/util') +const { getResolveErrorBodyCallback } = require('./util') const { AsyncResource } = require('async_hooks') const { addSignal, removeSignal } = require('./abort-signal') @@ -79,77 +79,66 @@ class StreamHandler extends AsyncResource { } onHeaders (statusCode, rawHeaders, resume, statusMessage) { - const { factory, opaque, context, callback } = this + const { factory, opaque, context, callback, responseHeaders } = this + + const headers = responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) if (statusCode < 200) { if (this.onInfo) { - const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) this.onInfo({ statusCode, headers }) } return } this.factory = null - const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) - const res = this.runInAsyncScope(factory, null, { - statusCode, - headers, - opaque, - context - }) - if (this.throwOnError && statusCode >= 400) { - const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) - const chunks = [] - const pt = new PassThrough() - pt - .on('data', (chunk) => chunks.push(chunk)) - .on('end', () => { - const payload = Buffer.concat(chunks).toString('utf8') - this.runInAsyncScope( - callback, - null, - new ResponseStatusCodeError( - `Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, - statusCode, - headers, - payload - ) - ) - }) - .on('error', (err) => { - this.onError(err) - }) - this.res = pt - return - } + let res - if ( - !res || - typeof res.write !== 'function' || - typeof res.end !== 'function' || - typeof res.on !== 'function' - ) { - throw new InvalidReturnValueError('expected Writable') - } + if (this.throwOnError && statusCode >= 400) { + const parsedHeaders = responseHeaders === 'raw' ? util.parseHeaders(rawHeaders) : headers + const contentType = parsedHeaders['content-type'] + res = new PassThrough() - res.on('drain', resume) - // TODO: Avoid finished. It registers an unnecessary amount of listeners. - finished(res, { readable: false }, (err) => { - const { callback, res, opaque, trailers, abort } = this + this.callback = null + this.runInAsyncScope(getResolveErrorBodyCallback, null, + { callback, body: res, contentType, statusCode, statusMessage, headers } + ) + } else { + res = this.runInAsyncScope(factory, null, { + statusCode, + headers, + opaque, + context + }) - this.res = null - if (err || !res.readable) { - util.destroy(res, err) + if ( + !res || + typeof res.write !== 'function' || + typeof res.end !== 'function' || + typeof res.on !== 'function' + ) { + throw new InvalidReturnValueError('expected Writable') } - this.callback = null - this.runInAsyncScope(callback, null, err || null, { opaque, trailers }) + // TODO: Avoid finished. It registers an unnecessary amount of listeners. + finished(res, { readable: false }, (err) => { + const { callback, res, opaque, trailers, abort } = this - if (err) { - abort() - } - }) + this.res = null + if (err || !res.readable) { + util.destroy(res, err) + } + + this.callback = null + this.runInAsyncScope(callback, null, err || null, { opaque, trailers }) + + if (err) { + abort() + } + }) + } + + res.on('drain', resume) this.res = res diff --git a/lib/api/util.js b/lib/api/util.js new file mode 100644 index 00000000000..bffd70279a4 --- /dev/null +++ b/lib/api/util.js @@ -0,0 +1,46 @@ +const assert = require('assert') +const { + ResponseStatusCodeError +} = require('../core/errors') +const { toUSVString } = require('../core/util') + +async function getResolveErrorBodyCallback ({ callback, body, contentType, statusCode, statusMessage, headers }) { + assert(body) + + let chunks = [] + let limit = 0 + + for await (const chunk of body) { + chunks.push(chunk) + limit += chunk.length + if (limit > 128 * 1024) { + chunks = null + break + } + } + + if (statusCode === 204 || !contentType || !chunks) { + process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers)) + return + } + + try { + if (contentType.startsWith('application/json')) { + const payload = JSON.parse(toUSVString(Buffer.concat(chunks))) + process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers, payload)) + return + } + + if (contentType.startsWith('text/')) { + const payload = toUSVString(Buffer.concat(chunks)) + process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers, payload)) + return + } + } catch (err) { + // Process in a fallback if error + } + + process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers)) +} + +module.exports = { getResolveErrorBodyCallback } diff --git a/test/client-stream.js b/test/client-stream.js index e67727b74c7..a230c443b67 100644 --- a/test/client-stream.js +++ b/test/client-stream.js @@ -786,7 +786,7 @@ test('stream legacy needDrain', (t) => { }) }) - test('steam throwOnError', (t) => { + test('stream throwOnError', (t) => { t.plan(2) const errStatusCode = 500 From 2ff2e1d0eb0e6f27eee103515520181ce9f6b895 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Fri, 14 Apr 2023 16:39:25 +0200 Subject: [PATCH 035/259] fix: close and destroy overlap (#2068) * fix: close and destroy overlap * fixup --- lib/client.js | 8 +++++--- lib/dispatcher-base.js | 5 +++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/client.js b/lib/client.js index 269d6e1a607..370e89b89cf 100644 --- a/lib/client.js +++ b/lib/client.js @@ -320,7 +320,7 @@ class Client extends DispatcherBase { async [kClose] () { return new Promise((resolve) => { if (!this[kSize]) { - this.destroy(resolve) + resolve(null) } else { this[kClosedResolve] = resolve } @@ -337,6 +337,7 @@ class Client extends DispatcherBase { const callback = () => { if (this[kClosedResolve]) { + // TODO (fix): Should we error here with ClientDestroyedError? this[kClosedResolve]() this[kClosedResolve] = null } @@ -1186,8 +1187,9 @@ function _resume (client, sync) { return } - if (client.closed && !client[kSize]) { - client.destroy() + if (client[kClosedResolve] && !client[kSize]) { + client[kClosedResolve]() + client[kClosedResolve] = null return } diff --git a/lib/dispatcher-base.js b/lib/dispatcher-base.js index 14a5c0acd70..5c0220b5b33 100644 --- a/lib/dispatcher-base.js +++ b/lib/dispatcher-base.js @@ -19,7 +19,7 @@ class DispatcherBase extends Dispatcher { super() this[kDestroyed] = false - this[kOnDestroyed] = [] + this[kOnDestroyed] = null this[kClosed] = false this[kOnClosed] = [] } @@ -127,6 +127,7 @@ class DispatcherBase extends Dispatcher { } this[kDestroyed] = true + this[kOnDestroyed] = this[kOnDestroyed] || [] this[kOnDestroyed].push(callback) const onDestroyed = () => { @@ -167,7 +168,7 @@ class DispatcherBase extends Dispatcher { throw new InvalidArgumentError('opts must be an object.') } - if (this[kDestroyed]) { + if (this[kDestroyed] || this[kOnDestroyed]) { throw new ClientDestroyedError() } From 852bf6ecb7fb9ed02987994af2f0e22c4599acff Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Fri, 14 Apr 2023 17:16:17 +0200 Subject: [PATCH 036/259] remove node 12 from test matrix (#2069) --- .github/workflows/nodejs.yml | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index b676ba08207..984d7be3b47 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -9,7 +9,7 @@ on: [push, pull_request] jobs: build: name: Test - uses: pkgjs/action/.github/workflows/node-test.yaml@v0.1 + uses: pkgjs/action/.github/workflows/node-test.yaml@v0.1.6 with: runs-on: ubuntu-latest, windows-latest test-command: npm run coverage:ci diff --git a/package.json b/package.json index 912b86a5234..463a40f1e23 100644 --- a/package.json +++ b/package.json @@ -104,7 +104,7 @@ "ws": "^8.11.0" }, "engines": { - "node": ">=12.18" + "node": ">=14.0" }, "standard": { "env": [ From 3e9191513d0f6db4421640e4b50b773dc3a02eaa Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Fri, 14 Apr 2023 17:25:25 +0200 Subject: [PATCH 037/259] fix: don't leak socket if client is destroyed while connecting (#2058) * fix: don't leak socket if client is destroyed will connecting * Update types/connector.d.ts --- lib/client.js | 12 +++++++++++- test/connect-abort.js | 28 ++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 test/connect-abort.js diff --git a/lib/client.js b/lib/client.js index 370e89b89cf..e630212fa79 100644 --- a/lib/client.js +++ b/lib/client.js @@ -21,7 +21,8 @@ const { InformationalError, BodyTimeoutError, HTTPParserError, - ResponseExceededMaxSizeError + ResponseExceededMaxSizeError, + ClientDestroyedError } = require('./core/errors') const buildConnector = require('./core/connect') const { @@ -1083,6 +1084,11 @@ async function connect (client) { }) }) + if (client.destroyed) { + util.destroy(socket.on('error', () => {}), new ClientDestroyedError()) + return + } + if (!llhttpInstance) { llhttpInstance = await llhttpPromise llhttpPromise = null @@ -1125,6 +1131,10 @@ async function connect (client) { } client.emit('connect', client[kUrl], [client]) } catch (err) { + if (client.destroyed) { + return + } + client[kConnecting] = false if (channels.connectError.hasSubscribers) { diff --git a/test/connect-abort.js b/test/connect-abort.js new file mode 100644 index 00000000000..6eb36243866 --- /dev/null +++ b/test/connect-abort.js @@ -0,0 +1,28 @@ +'use strict' + +const { test } = require('tap') +const { Client } = require('..') +const { PassThrough } = require('stream') + +test(t => { + t.plan(2) + + const client = new Client('http://localhost:1234', { + connect: (_, cb) => { + client.destroy() + cb(null, new PassThrough({ + destroy (err, cb) { + t.same(err?.name, 'ClientDestroyedError') + cb(null) + } + })) + } + }) + + client.request({ + path: '/', + method: 'GET' + }, (err, data) => { + t.same(err?.name, 'ClientDestroyedError') + }) +}) From 97154f5ae50897c92e9493f1a6da6fd85f6ec047 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Fri, 14 Apr 2023 17:34:56 +0200 Subject: [PATCH 038/259] fix: flaky leak test (#2070) * fix: flaky leak test * fixuP * fixup --- test/client-keep-alive.js | 2 +- test/connect-timeout.js | 4 ++-- test/fetch/fetch-leak.js | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/test/client-keep-alive.js b/test/client-keep-alive.js index 968bc50e89f..393807be9db 100644 --- a/test/client-keep-alive.js +++ b/test/client-keep-alive.js @@ -142,7 +142,7 @@ test('keep-alive header no postfix', (t) => { body.on('end', () => { const timeout = setTimeout(() => { t.fail() - }, 3e3) + }, 4e3) client.on('disconnect', () => { t.pass() clearTimeout(timeout) diff --git a/test/connect-timeout.js b/test/connect-timeout.js index b2af6a3965d..98ed1922979 100644 --- a/test/connect-timeout.js +++ b/test/connect-timeout.js @@ -16,8 +16,8 @@ test('priotorise socket errors over timeouts', (t) => { t.equal(err.code, 'ENOTFOUND') }) - // block for 1001ms which is enough for the dns lookup to complete and TO to fire - sleep(connectTimeout + 1) + // block for 2s which is enough for the dns lookup to complete and TO to fire + sleep(connectTimeout * 2) }) // never connect diff --git a/test/fetch/fetch-leak.js b/test/fetch/fetch-leak.js index e7a260208e3..b8e6b16709f 100644 --- a/test/fetch/fetch-leak.js +++ b/test/fetch/fetch-leak.js @@ -22,6 +22,7 @@ test('do not leak', (t) => { const controller = new AbortController() fetch(url, { signal: controller.signal }) .then(res => res.arrayBuffer()) + .catch(() => {}) .then(attack) }) @@ -33,7 +34,7 @@ test('do not leak', (t) => { const next = process.memoryUsage().heapUsed if (next <= prev) { t.pass() - } else if (count++ > 10) { + } else if (count++ > 20) { t.fail() } else { prev = next From 3b3ee201762c36f8a7d1163bcfb4b043a8516631 Mon Sep 17 00:00:00 2001 From: Khafra Date: Sun, 16 Apr 2023 00:48:29 -0400 Subject: [PATCH 039/259] test: update wpts (#2073) --- CONTRIBUTING.md | 1 - lib/fetch/headers.js | 1 + test/wpt/runner/runner.mjs | 7 +- test/wpt/status/fetch.status.json | 15 +- test/wpt/tests/.azure-pipelines.yml | 20 +- test/wpt/tests/common/rendering-utils.js | 8 +- .../security-features/tools/generate.py | 2 - .../security-features/tools/spec_validator.py | 2 - .../common/security-features/tools/util.py | 2 - .../tests/fetch/api/basic/keepalive.any.js | 39 +++ test/wpt/tests/fetch/api/basic/keepalive.html | 106 -------- .../fetch/api/basic/response-null-body.any.js | 7 + .../fetch/api/redirect/redirect-count.any.js | 71 ++--- .../api/redirect/redirect-keepalive.any.js | 56 ++++ .../api/redirect/redirect-location.any.js | 85 +++--- .../fetch/api/redirect/redirect-origin.any.js | 84 +++--- .../fetch/api/request/request-headers.any.js | 1 + .../fetch/api/resources/keepalive-helper.js | 79 ++++++ .../fetch/api/resources/keepalive-iframe.html | 18 +- .../resources/keepalive-redirect-iframe.html | 23 ++ ...ow.html => keepalive-redirect-window.html} | 0 .../api/response/many-empty-chunks-crash.html | 14 + .../content-type/multipart-malformed.any.js | 22 ++ .../META.yml | 0 .../fetch/local-network-access/README.md | 10 + ...fetch-from-treat-as-public.https.window.js | 0 .../fetch.https.window.js | 0 .../fetch.window.js | 0 .../iframe.tentative.https.window.js | 0 .../iframe.tentative.window.js | 0 ...ed-content-fetch.tentative.https.window.js | 92 +++---- .../nested-worker.https.window.js | 0 .../nested-worker.window.js | 0 .../preflight-cache.https.window.js | 0 .../redirect.https.window.js | 245 ++++++++++++++++++ .../resources/executor.html | 0 .../resources/fetcher.html | 0 .../resources/fetcher.js | 0 .../resources/iframed.html | 0 .../resources/iframer.html | 0 .../resources/preflight.py | 2 +- .../resources/service-worker-bridge.html | 0 .../resources/service-worker.js | 0 .../resources/shared-fetcher.js | 0 .../resources/shared-worker-blob-fetcher.html | 0 .../resources/shared-worker-fetcher.html | 0 .../resources/socket-opener.html | 0 .../resources/support.sub.js | 25 +- .../resources/worker-blob-fetcher.html | 0 .../resources/worker-fetcher.html | 0 .../resources/worker-fetcher.js | 0 .../resources/xhr-sender.html | 0 ...ce-worker-background-fetch.https.window.js | 0 .../service-worker-fetch.https.window.js | 0 .../service-worker-update.https.window.js | 0 .../service-worker.https.window.js | 0 .../shared-worker-blob-fetch.https.window.js | 0 .../shared-worker-blob-fetch.window.js | 0 .../shared-worker-fetch.https.window.js | 0 .../shared-worker-fetch.window.js | 0 .../shared-worker.https.window.js | 0 .../shared-worker.window.js | 0 .../websocket.https.window.js | 0 .../websocket.window.js | 0 .../worker-blob-fetch.window.js | 0 .../worker-fetch.https.window.js | 0 .../worker-fetch.window.js | 0 .../worker.https.window.js | 0 .../worker.window.js | 0 .../xhr-from-treat-as-public.https.window.js | 0 .../xhr.https.window.js | 0 .../xhr.window.js | 0 .../fetch/orb/resources/data_non_ascii.json | 1 + .../js-unlabeled-utf16-without-bom.json | Bin 0 -> 70 bytes .../fetch/orb/resources/script-iso-8559-1.js | 4 + .../fetch/orb/resources/script-utf16-bom.js | Bin 0 -> 92 bytes .../orb/resources/script-utf16-without-bom.js | Bin 0 -> 90 bytes .../orb/tentative/known-mime-type.sub.any.js | 25 ++ ...pt-utf16-without-bom-hint-charset.sub.html | 22 ++ .../fetch/private-network-access/README.md | 10 - test/wpt/tests/fetch/range/blob.any.js | 187 +++++++------ .../interfaces/EXT_clip_cull_distance.idl | 20 -- test/wpt/tests/interfaces/FedCM.idl | 62 ++--- .../interfaces/WEBGL_clip_cull_distance.idl | 20 ++ .../interfaces/WEBGL_provoking_vertex.idl | 13 + test/wpt/tests/interfaces/anchors.idl | 2 + test/wpt/tests/interfaces/badging.idl | 10 +- test/wpt/tests/interfaces/compression.idl | 10 +- .../wpt/tests/interfaces/compute-pressure.idl | 7 +- test/wpt/tests/interfaces/contact-picker.idl | 2 +- test/wpt/tests/interfaces/cookie-store.idl | 4 +- test/wpt/tests/interfaces/css-cascade-6.idl | 10 + test/wpt/tests/interfaces/css-contain.idl | 6 +- test/wpt/tests/interfaces/css-typed-om.idl | 4 +- .../tests/interfaces/css-view-transitions.idl | 8 +- test/wpt/tests/interfaces/device-posture.idl | 3 +- test/wpt/tests/interfaces/dom.idl | 2 +- test/wpt/tests/interfaces/edit-context.idl | 40 ++- test/wpt/tests/interfaces/fenced-frame.idl | 54 ++++ test/wpt/tests/interfaces/fetch.idl | 1 + test/wpt/tests/interfaces/fs.idl | 4 - .../tests/interfaces/gamepad-extensions.idl | 2 +- test/wpt/tests/interfaces/gpc-spec.idl | 10 + test/wpt/tests/interfaces/html.idl | 32 ++- .../tests/interfaces/mediacapture-streams.idl | 10 +- test/wpt/tests/interfaces/navigation-api.idl | 158 ----------- test/wpt/tests/interfaces/notifications.idl | 1 - .../tests/interfaces/orientation-event.idl | 4 - .../tests/interfaces/permissions-policy.idl | 1 - test/wpt/tests/interfaces/priority-hints.idl | 20 -- .../interfaces/requestStorageAccessFor.idl | 12 + test/wpt/tests/interfaces/resource-timing.idl | 1 + .../tests/interfaces/scroll-animations.idl | 1 + .../interfaces/scroll-to-text-fragment.idl | 2 +- .../secure-payment-confirmation.idl | 11 - test/wpt/tests/interfaces/selection-api.idl | 3 +- test/wpt/tests/interfaces/trust-token-api.idl | 29 +++ test/wpt/tests/interfaces/turtledove.idl | 109 ++++++++ test/wpt/tests/interfaces/ua-client-hints.idl | 7 +- test/wpt/tests/interfaces/url.idl | 4 + test/wpt/tests/interfaces/wasm-js-api.idl | 12 +- .../wpt/tests/interfaces/web-animations-2.idl | 11 + test/wpt/tests/interfaces/web-animations.idl | 1 - test/wpt/tests/interfaces/web-bluetooth.idl | 1 + test/wpt/tests/interfaces/webauthn.idl | 42 +-- .../webcodecs-av1-codec-registration.idl | 12 + .../webcodecs-vp9-codec-registration.idl | 12 + test/wpt/tests/interfaces/webcodecs.idl | 16 +- test/wpt/tests/interfaces/webgpu.idl | 68 ++--- test/wpt/tests/interfaces/webidl.idl | 2 - test/wpt/tests/interfaces/webnn.idl | 25 +- .../interfaces/webrtc-encoded-transform.idl | 1 + test/wpt/tests/interfaces/webrtc-stats.idl | 2 +- test/wpt/tests/interfaces/webrtc.idl | 55 ++-- test/wpt/tests/interfaces/webtransport.idl | 20 +- test/wpt/tests/interfaces/webxr.idl | 1 + ...ow-placement.idl => window-management.idl} | 2 +- test/wpt/tests/interfaces/xhr.idl | 2 +- test/wpt/tests/lint.ignore | 13 +- .../chromium/mock-battery-monitor.headers | 1 + .../chromium/mock-battery-monitor.js | 61 +++++ .../chromium/mock-pressure-service.js | 43 +-- .../tests/resources/chromium/mock-subapps.js | 7 +- .../resources/chromium/webusb-child-test.js | 9 +- .../tests/resources/chromium/webxr-test.js | 64 ++--- .../declarative-shadow-dom-polyfill.js | 21 +- test/wpt/tests/resources/test/tox.ini | 2 +- test/wpt/tests/resources/testdriver.js | 115 +++++--- test/wpt/tests/resources/testharness.js | 4 +- test/wpt/tests/wpt | 4 +- 150 files changed, 1662 insertions(+), 952 deletions(-) create mode 100644 test/wpt/tests/fetch/api/basic/keepalive.any.js delete mode 100644 test/wpt/tests/fetch/api/basic/keepalive.html create mode 100644 test/wpt/tests/fetch/api/redirect/redirect-keepalive.any.js create mode 100644 test/wpt/tests/fetch/api/resources/keepalive-helper.js create mode 100644 test/wpt/tests/fetch/api/resources/keepalive-redirect-iframe.html rename test/wpt/tests/fetch/api/resources/{keepalive-window.html => keepalive-redirect-window.html} (100%) create mode 100644 test/wpt/tests/fetch/api/response/many-empty-chunks-crash.html create mode 100644 test/wpt/tests/fetch/content-type/multipart-malformed.any.js rename test/wpt/tests/fetch/{private-network-access => local-network-access}/META.yml (100%) create mode 100644 test/wpt/tests/fetch/local-network-access/README.md rename test/wpt/tests/fetch/{private-network-access => local-network-access}/fetch-from-treat-as-public.https.window.js (100%) rename test/wpt/tests/fetch/{private-network-access => local-network-access}/fetch.https.window.js (100%) rename test/wpt/tests/fetch/{private-network-access => local-network-access}/fetch.window.js (100%) rename test/wpt/tests/fetch/{private-network-access => local-network-access}/iframe.tentative.https.window.js (100%) rename test/wpt/tests/fetch/{private-network-access => local-network-access}/iframe.tentative.window.js (100%) rename test/wpt/tests/fetch/{private-network-access => local-network-access}/mixed-content-fetch.tentative.https.window.js (77%) rename test/wpt/tests/fetch/{private-network-access => local-network-access}/nested-worker.https.window.js (100%) rename test/wpt/tests/fetch/{private-network-access => local-network-access}/nested-worker.window.js (100%) rename test/wpt/tests/fetch/{private-network-access => local-network-access}/preflight-cache.https.window.js (100%) rename test/wpt/tests/fetch/{private-network-access => local-network-access}/redirect.https.window.js (50%) rename test/wpt/tests/fetch/{private-network-access => local-network-access}/resources/executor.html (100%) rename test/wpt/tests/fetch/{private-network-access => local-network-access}/resources/fetcher.html (100%) rename test/wpt/tests/fetch/{private-network-access => local-network-access}/resources/fetcher.js (100%) rename test/wpt/tests/fetch/{private-network-access => local-network-access}/resources/iframed.html (100%) rename test/wpt/tests/fetch/{private-network-access => local-network-access}/resources/iframer.html (100%) rename test/wpt/tests/fetch/{private-network-access => local-network-access}/resources/preflight.py (98%) rename test/wpt/tests/fetch/{private-network-access => local-network-access}/resources/service-worker-bridge.html (100%) rename test/wpt/tests/fetch/{private-network-access => local-network-access}/resources/service-worker.js (100%) rename test/wpt/tests/fetch/{private-network-access => local-network-access}/resources/shared-fetcher.js (100%) rename test/wpt/tests/fetch/{private-network-access => local-network-access}/resources/shared-worker-blob-fetcher.html (100%) rename test/wpt/tests/fetch/{private-network-access => local-network-access}/resources/shared-worker-fetcher.html (100%) rename test/wpt/tests/fetch/{private-network-access => local-network-access}/resources/socket-opener.html (100%) rename test/wpt/tests/fetch/{private-network-access => local-network-access}/resources/support.sub.js (97%) rename test/wpt/tests/fetch/{private-network-access => local-network-access}/resources/worker-blob-fetcher.html (100%) rename test/wpt/tests/fetch/{private-network-access => local-network-access}/resources/worker-fetcher.html (100%) rename test/wpt/tests/fetch/{private-network-access => local-network-access}/resources/worker-fetcher.js (100%) rename test/wpt/tests/fetch/{private-network-access => local-network-access}/resources/xhr-sender.html (100%) rename test/wpt/tests/fetch/{private-network-access => local-network-access}/service-worker-background-fetch.https.window.js (100%) rename test/wpt/tests/fetch/{private-network-access => local-network-access}/service-worker-fetch.https.window.js (100%) rename test/wpt/tests/fetch/{private-network-access => local-network-access}/service-worker-update.https.window.js (100%) rename test/wpt/tests/fetch/{private-network-access => local-network-access}/service-worker.https.window.js (100%) rename test/wpt/tests/fetch/{private-network-access => local-network-access}/shared-worker-blob-fetch.https.window.js (100%) rename test/wpt/tests/fetch/{private-network-access => local-network-access}/shared-worker-blob-fetch.window.js (100%) rename test/wpt/tests/fetch/{private-network-access => local-network-access}/shared-worker-fetch.https.window.js (100%) rename test/wpt/tests/fetch/{private-network-access => local-network-access}/shared-worker-fetch.window.js (100%) rename test/wpt/tests/fetch/{private-network-access => local-network-access}/shared-worker.https.window.js (100%) rename test/wpt/tests/fetch/{private-network-access => local-network-access}/shared-worker.window.js (100%) rename test/wpt/tests/fetch/{private-network-access => local-network-access}/websocket.https.window.js (100%) rename test/wpt/tests/fetch/{private-network-access => local-network-access}/websocket.window.js (100%) rename test/wpt/tests/fetch/{private-network-access => local-network-access}/worker-blob-fetch.window.js (100%) rename test/wpt/tests/fetch/{private-network-access => local-network-access}/worker-fetch.https.window.js (100%) rename test/wpt/tests/fetch/{private-network-access => local-network-access}/worker-fetch.window.js (100%) rename test/wpt/tests/fetch/{private-network-access => local-network-access}/worker.https.window.js (100%) rename test/wpt/tests/fetch/{private-network-access => local-network-access}/worker.window.js (100%) rename test/wpt/tests/fetch/{private-network-access => local-network-access}/xhr-from-treat-as-public.https.window.js (100%) rename test/wpt/tests/fetch/{private-network-access => local-network-access}/xhr.https.window.js (100%) rename test/wpt/tests/fetch/{private-network-access => local-network-access}/xhr.window.js (100%) create mode 100644 test/wpt/tests/fetch/orb/resources/data_non_ascii.json create mode 100644 test/wpt/tests/fetch/orb/resources/js-unlabeled-utf16-without-bom.json create mode 100644 test/wpt/tests/fetch/orb/resources/script-iso-8559-1.js create mode 100644 test/wpt/tests/fetch/orb/resources/script-utf16-bom.js create mode 100644 test/wpt/tests/fetch/orb/resources/script-utf16-without-bom.js create mode 100644 test/wpt/tests/fetch/orb/tentative/script-utf16-without-bom-hint-charset.sub.html delete mode 100644 test/wpt/tests/fetch/private-network-access/README.md delete mode 100644 test/wpt/tests/interfaces/EXT_clip_cull_distance.idl create mode 100644 test/wpt/tests/interfaces/WEBGL_clip_cull_distance.idl create mode 100644 test/wpt/tests/interfaces/WEBGL_provoking_vertex.idl create mode 100644 test/wpt/tests/interfaces/css-cascade-6.idl create mode 100644 test/wpt/tests/interfaces/fenced-frame.idl create mode 100644 test/wpt/tests/interfaces/gpc-spec.idl delete mode 100644 test/wpt/tests/interfaces/navigation-api.idl delete mode 100644 test/wpt/tests/interfaces/priority-hints.idl create mode 100644 test/wpt/tests/interfaces/requestStorageAccessFor.idl create mode 100644 test/wpt/tests/interfaces/trust-token-api.idl create mode 100644 test/wpt/tests/interfaces/turtledove.idl create mode 100644 test/wpt/tests/interfaces/webcodecs-av1-codec-registration.idl create mode 100644 test/wpt/tests/interfaces/webcodecs-vp9-codec-registration.idl rename test/wpt/tests/interfaces/{window-placement.idl => window-management.idl} (93%) create mode 100644 test/wpt/tests/resources/chromium/mock-battery-monitor.headers create mode 100644 test/wpt/tests/resources/chromium/mock-battery-monitor.js diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cf7cb6932a4..17286753f08 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -99,7 +99,6 @@ git sparse-checkout add /resources git sparse-checkout add /common git sparse-checkout add /mimesniff git sparse-checkout add /interfaces -git sparse-checkout add /resources ``` diff --git a/lib/fetch/headers.js b/lib/fetch/headers.js index b42a5edeaab..264ee9518ad 100644 --- a/lib/fetch/headers.js +++ b/lib/fetch/headers.js @@ -516,6 +516,7 @@ Object.defineProperties(Headers.prototype, { get: kEnumerableProperty, has: kEnumerableProperty, set: kEnumerableProperty, + getSetCookie: kEnumerableProperty, keys: kEnumerableProperty, values: kEnumerableProperty, entries: kEnumerableProperty, diff --git a/test/wpt/runner/runner.mjs b/test/wpt/runner/runner.mjs index 000f69424f2..5979730d70a 100644 --- a/test/wpt/runner/runner.mjs +++ b/test/wpt/runner/runner.mjs @@ -99,8 +99,9 @@ export class WPTRunner extends EventEmitter { } this.once('completion', () => { - for (const exception of this.#uncaughtExceptions) { - console.log(colors(`Uncaught exception: ${exception.stack}`, 'red')) + for (const { error, test } of this.#uncaughtExceptions) { + console.log(colors(`Uncaught exception in "${test}":`, 'red')) + console.log(colors(`${error.stack}`, 'red')) console.log('='.repeat(96)) } }) @@ -211,7 +212,7 @@ export class WPTRunner extends EventEmitter { } else if (message.type === 'completion') { this.handleTestCompletion(worker) } else if (message.type === 'error') { - this.#uncaughtExceptions.push(message.error) + this.#uncaughtExceptions.push({ error: message.error, test }) this.#stats.failed += 1 this.#stats.success -= 1 } diff --git a/test/wpt/status/fetch.status.json b/test/wpt/status/fetch.status.json index c98604603da..cf814114752 100644 --- a/test/wpt/status/fetch.status.json +++ b/test/wpt/status/fetch.status.json @@ -37,6 +37,10 @@ "Empty string integrity for opaque response" ] }, + "keepalive.any.js": { + "note": "document is not defined", + "skip": true + }, "mode-no-cors.sub.any.js": { "note": "undici doesn't implement CORs", "skip": true @@ -205,6 +209,10 @@ "redirect response with empty Location, manual mode" ] }, + "redirect-keepalive.any.js": { + "note": "document is not defined", + "skip": true + }, "redirect-location-escape.tentative.any.js": { "note": "TODO(@KhafraDev): crashes runner", "skip": true @@ -226,7 +234,12 @@ "Redirect 307 in \"manual\" mode with data location", "Redirect 308 in \"manual\" mode without location", "Redirect 308 in \"manual\" mode with invalid location", - "Redirect 308 in \"manual\" mode with data location" + "Redirect 308 in \"manual\" mode with data location", + "Redirect 301 in \"manual\" mode with valid location", + "Redirect 302 in \"manual\" mode with valid location", + "Redirect 303 in \"manual\" mode with valid location", + "Redirect 307 in \"manual\" mode with valid location", + "Redirect 308 in \"manual\" mode with valid location" ] }, "redirect-method.any.js": { diff --git a/test/wpt/tests/.azure-pipelines.yml b/test/wpt/tests/.azure-pipelines.yml index 3f824f68e0c..aacd78815ad 100644 --- a/test/wpt/tests/.azure-pipelines.yml +++ b/test/wpt/tests/.azure-pipelines.yml @@ -233,7 +233,7 @@ jobs: toxenv: py310 - job: tools_unittest_win_py37 - displayName: 'tools/ unittests: Windows + Python 3.6' + displayName: 'tools/ unittests: Windows + Python 3.7' dependsOn: decision condition: dependencies.decision.outputs['test_jobs.tools_unittest'] pool: @@ -243,7 +243,7 @@ jobs: steps: - task: UsePythonVersion@0 inputs: - versionSpec: '3.6' + versionSpec: '3.7' addToPath: false - template: tools/ci/azure/checkout.yml - template: tools/ci/azure/tox_pytest.yml @@ -268,8 +268,8 @@ jobs: directory: tools/ toxenv: py310 -- job: wptrunner_unittest_win_py36 - displayName: 'tools/wptrunner/ unittests: Windows + Python 3.6' +- job: wptrunner_unittest_win_py37 + displayName: 'tools/wptrunner/ unittests: Windows + Python 3.7' dependsOn: decision condition: dependencies.decision.outputs['test_jobs.wptrunner_unittest'] pool: @@ -277,13 +277,13 @@ jobs: steps: - task: UsePythonVersion@0 inputs: - versionSpec: '3.6' + versionSpec: '3.7' addToPath: false - template: tools/ci/azure/checkout.yml - template: tools/ci/azure/tox_pytest.yml parameters: directory: tools/wptrunner/ - toxenv: py36 + toxenv: py37 - job: wptrunner_unittest_win_py310 displayName: 'tools/wptrunner/ unittests: Windows + Python 3.10' @@ -302,8 +302,8 @@ jobs: directory: tools/wptrunner/ toxenv: py310 -- job: wpt_integration_win_py36 - displayName: 'tools/wpt/ tests: Windows + Python 3.6' +- job: wpt_integration_win_py37 + displayName: 'tools/wpt/ tests: Windows + Python 3.7' dependsOn: decision condition: dependencies.decision.outputs['test_jobs.wpt_integration'] pool: @@ -312,7 +312,7 @@ jobs: # full checkout required - task: UsePythonVersion@0 inputs: - versionSpec: '3.6' + versionSpec: '3.7' # currently just using the outdated Chrome/Firefox on the VM rather than # figuring out how to install Chrome Dev channel on Windows # - template: tools/ci/azure/install_chrome.yml @@ -322,7 +322,7 @@ jobs: - template: tools/ci/azure/tox_pytest.yml parameters: directory: tools/wpt/ - toxenv: py36 + toxenv: py37 - job: wpt_integration_win_py310 displayName: 'tools/wpt/ tests: Windows + Python 3.10' diff --git a/test/wpt/tests/common/rendering-utils.js b/test/wpt/tests/common/rendering-utils.js index 46283bd5d07..8027cd5f848 100644 --- a/test/wpt/tests/common/rendering-utils.js +++ b/test/wpt/tests/common/rendering-utils.js @@ -7,12 +7,14 @@ */ function waitForAtLeastOneFrame() { return new Promise(resolve => { - // Different web engines work slightly different on this area but waiting - // for two requestAnimationFrames() to happen, one after another, should be + // Different web engines work slightly different on this area but 1) waiting + // for two requestAnimationFrames() to happen one after another and 2) + // adding a step_timeout(0) to guarantee events have finished should be // sufficient to ensure at least one frame has been generated anywhere. + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1785615 window.requestAnimationFrame(() => { window.requestAnimationFrame(() => { - resolve(); + setTimeout(resolve, 0); }); }); }); diff --git a/test/wpt/tests/common/security-features/tools/generate.py b/test/wpt/tests/common/security-features/tools/generate.py index 176e0ebbebc..409b4f195ff 100644 --- a/test/wpt/tests/common/security-features/tools/generate.py +++ b/test/wpt/tests/common/security-features/tools/generate.py @@ -1,7 +1,5 @@ #!/usr/bin/env python3 -from __future__ import print_function - import argparse import collections import copy diff --git a/test/wpt/tests/common/security-features/tools/spec_validator.py b/test/wpt/tests/common/security-features/tools/spec_validator.py index 3ac3f530169..f8a1390ef0d 100644 --- a/test/wpt/tests/common/security-features/tools/spec_validator.py +++ b/test/wpt/tests/common/security-features/tools/spec_validator.py @@ -1,7 +1,5 @@ #!/usr/bin/env python3 -from __future__ import print_function - import json, sys diff --git a/test/wpt/tests/common/security-features/tools/util.py b/test/wpt/tests/common/security-features/tools/util.py index 72541c78142..5da06f9d51e 100644 --- a/test/wpt/tests/common/security-features/tools/util.py +++ b/test/wpt/tests/common/security-features/tools/util.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import os, sys, json, json5, re import collections diff --git a/test/wpt/tests/fetch/api/basic/keepalive.any.js b/test/wpt/tests/fetch/api/basic/keepalive.any.js new file mode 100644 index 00000000000..047f1ed2348 --- /dev/null +++ b/test/wpt/tests/fetch/api/basic/keepalive.any.js @@ -0,0 +1,39 @@ +// META: global=window +// META: title=Fetch API: keepalive handling +// META: script=/resources/testharness.js +// META: script=/resources/testharnessreport.js +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=../resources/keepalive-helper.js + +'use strict'; + +const { + HTTP_NOTSAMESITE_ORIGIN, + HTTP_REMOTE_ORIGIN, + HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT +} = get_host_info(); + +for (const method of ['GET', 'POST']) { + promise_test(async (test) => { + const token1 = token(); + const iframe = document.createElement('iframe'); + iframe.src = getKeepAliveIframeUrl(token1, method); + document.body.appendChild(iframe); + await iframeLoaded(iframe); + assert_equals(await getTokenFromMessage(), token1); + iframe.remove(); + + assertStashedTokenAsync(`simple ${method} request: no payload`, token1); + }, `simple ${method} request: no payload; setting up`); +} + +promise_test(async (test) => { + const w = window.open(`${ + HTTP_NOTSAMESITE_ORIGIN}/fetch/api/resources/keepalive-redirect-window.html`); + const token = await getTokenFromMessage(); + w.close(); + + assertStashedTokenAsync( + 'keepalive in onunload in nested frame in another window', token); +}, 'keepalive in onunload in nested frame in another window; setting up'); diff --git a/test/wpt/tests/fetch/api/basic/keepalive.html b/test/wpt/tests/fetch/api/basic/keepalive.html deleted file mode 100644 index 36d156bba43..00000000000 --- a/test/wpt/tests/fetch/api/basic/keepalive.html +++ /dev/null @@ -1,106 +0,0 @@ - - - -Fetch API: keepalive handling - - - - - - - diff --git a/test/wpt/tests/fetch/api/basic/response-null-body.any.js b/test/wpt/tests/fetch/api/basic/response-null-body.any.js index 7824a200067..bb058926572 100644 --- a/test/wpt/tests/fetch/api/basic/response-null-body.any.js +++ b/test/wpt/tests/fetch/api/basic/response-null-body.any.js @@ -29,3 +29,10 @@ promise_test(async () => { const text = await resp.text(); assert_equals(text, "", "null bodies result in empty text"); }, `Response.body is null for responses with method=HEAD`); + +promise_test(async (t) => { + const integrity = "sha384-UT6f7WCFp32YJnp1is4l/ZYnOeQKpE8xjmdkLOwZ3nIP+tmT2aMRFQGJomjVf5cE"; + const url = `${RESOURCES_DIR}status.py?code=204&content=hello-world`; + const promise = fetch(url, { method: "GET", integrity }); + promise_rejects_js(t, TypeError, promise); +}, "Null body status with subresource integrity should abort"); diff --git a/test/wpt/tests/fetch/api/redirect/redirect-count.any.js b/test/wpt/tests/fetch/api/redirect/redirect-count.any.js index dda5d7f5290..420f9c0dfcb 100644 --- a/test/wpt/tests/fetch/api/redirect/redirect-count.any.js +++ b/test/wpt/tests/fetch/api/redirect/redirect-count.any.js @@ -3,40 +3,49 @@ // META: script=/common/utils.js // META: timeout=long -function redirectCount(desc, redirectUrl, redirectLocation, redirectStatus, maxCount, shouldPass) { - var uuid_token = token(); - - var urlParameters = "?token=" + uuid_token + "&max_age=0"; - urlParameters += "&redirect_status=" + redirectStatus; - urlParameters += "&max_count=" + maxCount; - if (redirectLocation) - urlParameters += "&location=" + encodeURIComponent(redirectLocation); - - var url = redirectUrl; - var requestInit = {"redirect": "follow"}; - - promise_test(function(test) { - return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) { - assert_equals(resp.status, 200, "Clean stash response's status is 200"); - - if (!shouldPass) - return promise_rejects_js(test, TypeError, fetch(url + urlParameters, requestInit)); - - return fetch(url + urlParameters, requestInit).then(function(resp) { - assert_equals(resp.status, 200, "Response's status is 200"); - return resp.text(); - }).then(function(body) { - assert_equals(body, maxCount.toString(), "Redirected " + maxCount + " times"); - }); - }); +/** + * Fetches a target that returns response with HTTP status code `statusCode` to + * redirect `maxCount` times. + */ +function redirectCountTest(maxCount, {statusCode, shouldPass = true} = {}) { + const desc = `Redirect ${statusCode} ${maxCount} times`; + + const fromUrl = `${RESOURCES_DIR}redirect.py`; + const toUrl = fromUrl; + const token1 = token(); + const url = `${fromUrl}?token=${token1}` + + `&max_age=0` + + `&redirect_status=${statusCode}` + + `&max_count=${maxCount}` + + `&location=${encodeURIComponent(toUrl)}`; + + const requestInit = {'redirect': 'follow'}; + + promise_test((test) => { + return fetch(`${RESOURCES_DIR}clean-stash.py?token=${token1}`) + .then((resp) => { + assert_equals( + resp.status, 200, 'Clean stash response\'s status is 200'); + + if (!shouldPass) + return promise_rejects_js(test, TypeError, fetch(url, requestInit)); + + return fetch(url, requestInit) + .then((resp) => { + assert_equals(resp.status, 200, 'Response\'s status is 200'); + return resp.text(); + }) + .then((body) => { + assert_equals( + body, maxCount.toString(), `Redirected ${maxCount} times`); + }); + }); }, desc); } -var redirUrl = RESOURCES_DIR + "redirect.py"; - -for (var statusCode of [301, 302, 303, 307, 308]) { - redirectCount("Redirect " + statusCode + " 20 times", redirUrl, redirUrl, statusCode, 20, true); - redirectCount("Redirect " + statusCode + " 21 times", redirUrl, redirUrl, statusCode, 21, false); +for (const statusCode of [301, 302, 303, 307, 308]) { + redirectCountTest(20, {statusCode}); + redirectCountTest(21, {statusCode, shouldPass: false}); } done(); diff --git a/test/wpt/tests/fetch/api/redirect/redirect-keepalive.any.js b/test/wpt/tests/fetch/api/redirect/redirect-keepalive.any.js new file mode 100644 index 00000000000..9f7cca7dbf7 --- /dev/null +++ b/test/wpt/tests/fetch/api/redirect/redirect-keepalive.any.js @@ -0,0 +1,56 @@ +// META: global=window +// META: title=Fetch API: keepalive handling +// META: script=/resources/testharness.js +// META: script=/resources/testharnessreport.js +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=../resources/keepalive-helper.js + +'use strict'; + +const { + HTTP_NOTSAMESITE_ORIGIN, + HTTP_REMOTE_ORIGIN, + HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT +} = get_host_info(); + +promise_test(async (test) => { + const token1 = token(); + const iframe = document.createElement('iframe'); + iframe.src = getKeepAliveAndRedirectIframeUrl( + token1, '', '', /*withPreflight=*/ false); + document.body.appendChild(iframe); + await iframeLoaded(iframe); + assert_equals(await getTokenFromMessage(), token1); + iframe.remove(); + + assertStashedTokenAsync('same-origin redirect', token1); +}, 'same-origin redirect; setting up'); + +promise_test(async (test) => { + const token1 = token(); + const iframe = document.createElement('iframe'); + iframe.src = getKeepAliveAndRedirectIframeUrl( + token1, HTTP_REMOTE_ORIGIN, HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT, + /*withPreflight=*/ false); + document.body.appendChild(iframe); + await iframeLoaded(iframe); + assert_equals(await getTokenFromMessage(), token1); + iframe.remove(); + + assertStashedTokenAsync('cross-origin redirect', token1); +}, 'cross-origin redirect; setting up'); + +promise_test(async (test) => { + const token1 = token(); + const iframe = document.createElement('iframe'); + iframe.src = getKeepAliveAndRedirectIframeUrl( + token1, HTTP_REMOTE_ORIGIN, HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT, + /*withPreflight=*/ true); + document.body.appendChild(iframe); + await iframeLoaded(iframe); + assert_equals(await getTokenFromMessage(), token1); + iframe.remove(); + + assertStashedTokenAsync('cross-origin redirect with preflight', token1); +}, 'cross-origin redirect with preflight; setting up'); diff --git a/test/wpt/tests/fetch/api/redirect/redirect-location.any.js b/test/wpt/tests/fetch/api/redirect/redirect-location.any.js index 5cb6cc280c4..3d483bdcd49 100644 --- a/test/wpt/tests/fetch/api/redirect/redirect-location.any.js +++ b/test/wpt/tests/fetch/api/redirect/redirect-location.any.js @@ -1,48 +1,73 @@ // META: global=window,worker // META: script=../resources/utils.js -function redirectLocation(desc, redirectUrl, redirectLocation, redirectStatus, redirectMode, shouldPass) { - var url = redirectUrl; - var urlParameters = "?redirect_status=" + redirectStatus; - if (redirectLocation) - urlParameters += "&location=" + encodeURIComponent(redirectLocation); - - var requestInit = {"redirect": redirectMode}; - - promise_test(function(test) { - if (redirectMode === "error" || !shouldPass) - return promise_rejects_js(test, TypeError, fetch(url + urlParameters, requestInit)); - if (redirectMode === "manual") - return fetch(url + urlParameters, requestInit).then(function(resp) { +const VALID_URL = 'top.txt'; +const INVALID_URL = 'invalidurl:'; +const DATA_URL = 'data:text/plain;base64,cmVzcG9uc2UncyBib2R5'; + +/** + * A test to fetch a URL that returns response redirecting to `toUrl` with + * `status` as its HTTP status code. `expectStatus` can be set to test the + * status code in fetch's Promise response. + */ +function redirectLocationTest(toUrlDesc, { + toUrl = undefined, + status, + expectStatus = undefined, + mode, + shouldPass = true +} = {}) { + toUrlDesc = toUrl ? `with ${toUrlDesc}` : `without`; + const desc = `Redirect ${status} in "${mode}" mode ${toUrlDesc} location`; + const url = `${RESOURCES_DIR}redirect.py?redirect_status=${status}` + + (toUrl ? `&location=${encodeURIComponent(toUrl)}` : ''); + const requestInit = {'redirect': mode}; + if (!expectStatus) + expectStatus = status; + + promise_test((test) => { + if (mode === 'error' || !shouldPass) + return promise_rejects_js(test, TypeError, fetch(url, requestInit)); + if (mode === 'manual') + return fetch(url, requestInit).then((resp) => { assert_equals(resp.status, 0, "Response's status is 0"); assert_equals(resp.type, "opaqueredirect", "Response's type is opaqueredirect"); - assert_equals(resp.statusText, "", "Response's statusText is \"\""); + assert_equals(resp.statusText, '', `Response's statusText is ""`); assert_true(resp.headers.entries().next().done, "Headers should be empty"); }); - if (redirectMode === "follow") - return fetch(url + urlParameters, requestInit).then(function(resp) { - assert_equals(resp.status, redirectStatus, "Response's status is " + redirectStatus); + if (mode === 'follow') + return fetch(url, requestInit).then((resp) => { + assert_equals( + resp.status, expectStatus, `Response's status is ${expectStatus}`); }); - assert_unreached(redirectMode + " is not a valid redirect mode"); + assert_unreached(`${mode} is not a valid redirect mode`); }, desc); } -var redirUrl = RESOURCES_DIR + "redirect.py"; -var locationUrl = "top.txt"; -var invalidLocationUrl = "invalidurl:"; -var dataLocationUrl = "data:,data%20url"; // FIXME: We may want to mix redirect-mode and cors-mode. -// FIXME: Add tests for "error" redirect-mode. -for (var statusCode of [301, 302, 303, 307, 308]) { - redirectLocation("Redirect " + statusCode + " in \"follow\" mode without location", redirUrl, undefined, statusCode, "follow", true); - redirectLocation("Redirect " + statusCode + " in \"manual\" mode without location", redirUrl, undefined, statusCode, "manual", true); +for (const status of [301, 302, 303, 307, 308]) { + redirectLocationTest('without location', {status, mode: 'follow'}); + redirectLocationTest('without location', {status, mode: 'manual'}); + // FIXME: Add tests for "error" redirect-mode without location. + + // When succeeded, `follow` mode should have followed all redirects. + redirectLocationTest( + 'valid', {toUrl: VALID_URL, status, expectStatus: 200, mode: 'follow'}); + redirectLocationTest('valid', {toUrl: VALID_URL, status, mode: 'manual'}); + redirectLocationTest('valid', {toUrl: VALID_URL, status, mode: 'error'}); - redirectLocation("Redirect " + statusCode + " in \"follow\" mode with invalid location", redirUrl, invalidLocationUrl, statusCode, "follow", false); - redirectLocation("Redirect " + statusCode + " in \"manual\" mode with invalid location", redirUrl, invalidLocationUrl, statusCode, "manual", true); + redirectLocationTest( + 'invalid', + {toUrl: INVALID_URL, status, mode: 'follow', shouldPass: false}); + redirectLocationTest('invalid', {toUrl: INVALID_URL, status, mode: 'manual'}); + redirectLocationTest('invalid', {toUrl: INVALID_URL, status, mode: 'error'}); - redirectLocation("Redirect " + statusCode + " in \"follow\" mode with data location", redirUrl, dataLocationUrl, statusCode, "follow", false); - redirectLocation("Redirect " + statusCode + " in \"manual\" mode with data location", redirUrl, dataLocationUrl, statusCode, "manual", true); + redirectLocationTest( + 'data', {toUrl: DATA_URL, status, mode: 'follow', shouldPass: false}); + // FIXME: Should this pass? + redirectLocationTest('data', {toUrl: DATA_URL, status, mode: 'manual'}); + redirectLocationTest('data', {toUrl: DATA_URL, status, mode: 'error'}); } done(); diff --git a/test/wpt/tests/fetch/api/redirect/redirect-origin.any.js b/test/wpt/tests/fetch/api/redirect/redirect-origin.any.js index b81b91601a8..6001c509b1d 100644 --- a/test/wpt/tests/fetch/api/redirect/redirect-origin.any.js +++ b/test/wpt/tests/fetch/api/redirect/redirect-origin.any.js @@ -2,41 +2,67 @@ // META: script=../resources/utils.js // META: script=/common/get-host-info.sub.js -function testOriginAfterRedirection(desc, method, redirectUrl, redirectLocation, redirectStatus, expectedOrigin) { - var uuid_token = token(); - var url = redirectUrl; - var urlParameters = "?token=" + uuid_token + "&max_age=0"; - urlParameters += "&redirect_status=" + redirectStatus; - urlParameters += "&location=" + encodeURIComponent(redirectLocation); +const { + HTTP_ORIGIN, + HTTP_REMOTE_ORIGIN, +} = get_host_info(); - var requestInit = {"mode": "cors", "redirect": "follow"}; +/** + * Fetches `fromUrl` with 'cors' and 'follow' modes that returns response to + * redirect to `toUrl`. + */ +function testOriginAfterRedirection( + desc, method, fromUrl, toUrl, statusCode, expectedOrigin) { + desc = `[${method}] Redirect ${statusCode} ${desc}`; + const token1 = token(); + const url = `${fromUrl}?token=${token1}&max_age=0` + + `&redirect_status=${statusCode}` + + `&location=${encodeURIComponent(toUrl)}`; - promise_test(function(test) { - return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) { - assert_equals(resp.status, 200, "Clean stash response's status is 200"); - return fetch(url + urlParameters, requestInit).then(function(response) { - assert_equals(response.status, 200, "Inspect header response's status is 200"); - assert_equals(response.headers.get("x-request-origin"), expectedOrigin, "Check origin header"); - }); + const requestInit = {method, 'mode': 'cors', 'redirect': 'follow'}; + + promise_test(function(test) { + return fetch(`${RESOURCES_DIR}clean-stash.py?token=${token1}`) + .then((cleanResponse) => { + assert_equals( + cleanResponse.status, 200, + `Clean stash response's status is 200`); + return fetch(url, requestInit).then((redirectResponse) => { + assert_equals( + redirectResponse.status, 200, + `Inspect header response's status is 200`); + assert_equals( + redirectResponse.headers.get('x-request-origin'), + expectedOrigin, 'Check origin header'); + }); }); - }, desc); + }, desc); } -var redirectUrl = RESOURCES_DIR + "redirect.py"; -var corsRedirectUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "redirect.py"; -var locationUrl = get_host_info().HTTP_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?headers=origin"; -var corsLocationUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?cors&headers=origin"; - -for (var code of [301, 302, 303, 307, 308]) { - testOriginAfterRedirection("Same origin to same origin redirection " + code, 'GET', redirectUrl, locationUrl, code, null); - testOriginAfterRedirection("Same origin to other origin redirection " + code, 'GET', redirectUrl, corsLocationUrl, code, get_host_info().HTTP_ORIGIN); - testOriginAfterRedirection("Other origin to other origin redirection " + code, 'GET', corsRedirectUrl, corsLocationUrl, code, get_host_info().HTTP_ORIGIN); - testOriginAfterRedirection("Other origin to same origin redirection " + code, 'GET', corsRedirectUrl, locationUrl + "&cors", code, "null"); +const FROM_URL = `${RESOURCES_DIR}redirect.py`; +const CORS_FROM_URL = + `${HTTP_REMOTE_ORIGIN}${dirname(location.pathname)}${FROM_URL}`; +const TO_URL = `${HTTP_ORIGIN}${dirname(location.pathname)}${ + RESOURCES_DIR}inspect-headers.py?headers=origin`; +const CORS_TO_URL = `${HTTP_REMOTE_ORIGIN}${dirname(location.pathname)}${ + RESOURCES_DIR}inspect-headers.py?cors&headers=origin`; - testOriginAfterRedirection("Same origin to same origin redirection[POST] " + code, 'POST', redirectUrl, locationUrl, code, null); - testOriginAfterRedirection("Same origin to other origin redirection[POST] " + code, 'POST', redirectUrl, corsLocationUrl, code, get_host_info().HTTP_ORIGIN); - testOriginAfterRedirection("Other origin to other origin redirection[POST] " + code, 'POST', corsRedirectUrl, corsLocationUrl, code, get_host_info().HTTP_ORIGIN); - testOriginAfterRedirection("Other origin to same origin redirection[POST] " + code, 'POST', corsRedirectUrl, locationUrl + "&cors", code, "null"); +for (const statusCode of [301, 302, 303, 307, 308]) { + for (const method of ['GET', 'POST']) { + testOriginAfterRedirection( + 'Same origin to same origin', method, FROM_URL, TO_URL, statusCode, + null); + testOriginAfterRedirection( + 'Same origin to other origin', method, FROM_URL, CORS_TO_URL, + statusCode, HTTP_ORIGIN); + testOriginAfterRedirection( + 'Other origin to other origin', method, CORS_FROM_URL, CORS_TO_URL, + statusCode, HTTP_ORIGIN); + // TODO(crbug.com/1432059): Fix broken tests. + testOriginAfterRedirection( + 'Other origin to same origin', method, CORS_FROM_URL, `${TO_URL}&cors`, + statusCode, 'null'); + } } done(); diff --git a/test/wpt/tests/fetch/api/request/request-headers.any.js b/test/wpt/tests/fetch/api/request/request-headers.any.js index 6de5dc16e96..22925e01b69 100644 --- a/test/wpt/tests/fetch/api/request/request-headers.any.js +++ b/test/wpt/tests/fetch/api/request/request-headers.any.js @@ -9,6 +9,7 @@ var validRequestHeaders = [ ["sec", "OK"], ["secb", "OK"], ["Set-Cookie2", "OK"], + ["User-Agent", "OK"], ]; var invalidRequestHeaders = [ ["Accept-Charset", "KO"], diff --git a/test/wpt/tests/fetch/api/resources/keepalive-helper.js b/test/wpt/tests/fetch/api/resources/keepalive-helper.js new file mode 100644 index 00000000000..42f20ac30af --- /dev/null +++ b/test/wpt/tests/fetch/api/resources/keepalive-helper.js @@ -0,0 +1,79 @@ +// Utility functions to help testing keepalive requests. + +// Returns a different-site URL to an iframe that loads a keepalive URL. +// +// The keepalive URL points to a target that stores `token`. The token will then +// be posted back to parent document. +// `method` defaults to GET. +// `sendOnPagehide` to tell if request should be sent on pagehide instead. +function getKeepAliveIframeUrl(token, method, sendOnPagehide = false) { + const https = location.protocol.startsWith('https'); + const frameOrigin = + get_host_info()[https ? 'HTTPS_NOTSAMESITE_ORIGIN' : 'HTTP_NOTSAMESITE_ORIGIN']; + return `${frameOrigin}/fetch/api/resources/keepalive-iframe.html?` + + `token=${token}&` + + `method=${method}&` + + `sendOnPagehide=${sendOnPagehide}`; +} + +// Returns a different-site URL to an iframe that loads a keepalive URL. +// +// By default, the keepalive URL points to a target that redirects to another +// same-origin destination storing `token`. The token will then be posted back +// to parent document. +// +// The URL redirects can be customized from `origin1` to `origin2` if provided. +// Sets `withPreflight` to true to get URL enabling preflight. +function getKeepAliveAndRedirectIframeUrl( + token, origin1, origin2, withPreflight) { + const https = location.protocol.startsWith('https'); + const frameOrigin = + get_host_info()[https ? 'HTTPS_NOTSAMESITE_ORIGIN' : 'HTTP_NOTSAMESITE_ORIGIN']; + return `${frameOrigin}/fetch/api/resources/keepalive-redirect-iframe.html?` + + `token=${token}&` + + `origin1=${origin1}&` + + `origin2=${origin2}&` + (withPreflight ? `with-headers` : ``); +} + +async function iframeLoaded(iframe) { + return new Promise((resolve) => iframe.addEventListener('load', resolve)); +} + +// Obtains the token from the message posted by iframe after loading +// `getKeepAliveAndRedirectIframeUrl()`. +async function getTokenFromMessage() { + return new Promise((resolve) => { + window.addEventListener('message', (event) => { + resolve(event.data); + }, {once: true}); + }); +} + +// Tells if `token` has been stored in the server. +async function queryToken(token) { + const response = await fetch(`../resources/stash-take.py?key=${token}`); + const json = await response.json(); + return json; +} + +// In order to parallelize the work, we are going to have an async_test +// for the rest of the work. Note that we want the serialized behavior +// for the steps so far, so we don't want to make the entire test case +// an async_test. +function assertStashedTokenAsync(testName, token) { + async_test((test) => { + new Promise((resolve) => test.step_timeout(resolve, 3000)) + .then(() => { + return queryToken(token); + }) + .then((result) => { + assert_equals(result, 'on'); + }) + .then(() => { + test.done(); + }) + .catch(test.step_func((e) => { + assert_unreached(e); + })); + }, testName); +} diff --git a/test/wpt/tests/fetch/api/resources/keepalive-iframe.html b/test/wpt/tests/fetch/api/resources/keepalive-iframe.html index 47de0da7790..ac00f3a331a 100644 --- a/test/wpt/tests/fetch/api/resources/keepalive-iframe.html +++ b/test/wpt/tests/fetch/api/resources/keepalive-iframe.html @@ -1,24 +1,14 @@ - - diff --git a/test/wpt/tests/fetch/api/resources/keepalive-redirect-iframe.html b/test/wpt/tests/fetch/api/resources/keepalive-redirect-iframe.html new file mode 100644 index 00000000000..fdee00f3124 --- /dev/null +++ b/test/wpt/tests/fetch/api/resources/keepalive-redirect-iframe.html @@ -0,0 +1,23 @@ + + + + + diff --git a/test/wpt/tests/fetch/api/resources/keepalive-window.html b/test/wpt/tests/fetch/api/resources/keepalive-redirect-window.html similarity index 100% rename from test/wpt/tests/fetch/api/resources/keepalive-window.html rename to test/wpt/tests/fetch/api/resources/keepalive-redirect-window.html diff --git a/test/wpt/tests/fetch/api/response/many-empty-chunks-crash.html b/test/wpt/tests/fetch/api/response/many-empty-chunks-crash.html new file mode 100644 index 00000000000..fe5e7d4c075 --- /dev/null +++ b/test/wpt/tests/fetch/api/response/many-empty-chunks-crash.html @@ -0,0 +1,14 @@ + + + diff --git a/test/wpt/tests/fetch/content-type/multipart-malformed.any.js b/test/wpt/tests/fetch/content-type/multipart-malformed.any.js new file mode 100644 index 00000000000..9de0edc24ac --- /dev/null +++ b/test/wpt/tests/fetch/content-type/multipart-malformed.any.js @@ -0,0 +1,22 @@ +// This is a repro for Chromium issue https://crbug.com/1412007. +promise_test(t => { + const form_string = + "--Boundary_with_capital_letters\r\n" + + "Content-Type: application/json\r\n" + + 'Content-Disposition: form-data; name="does_this_work"\r\n' + + "\r\n" + + 'YES\r\n' + + "--Boundary_with_capital_letters-Random junk"; + + const r = new Response(new Blob([form_string]), { + headers: [ + [ + "Content-Type", + "multipart/form-data; boundary=Boundary_with_capital_letters", + ], + ], + }); + + return promise_rejects_js(t, TypeError, r.formData(), + "form data should fail to parse"); +}, "Invalid form data should not crash the browser"); diff --git a/test/wpt/tests/fetch/private-network-access/META.yml b/test/wpt/tests/fetch/local-network-access/META.yml similarity index 100% rename from test/wpt/tests/fetch/private-network-access/META.yml rename to test/wpt/tests/fetch/local-network-access/META.yml diff --git a/test/wpt/tests/fetch/local-network-access/README.md b/test/wpt/tests/fetch/local-network-access/README.md new file mode 100644 index 00000000000..8995e3d7ef6 --- /dev/null +++ b/test/wpt/tests/fetch/local-network-access/README.md @@ -0,0 +1,10 @@ +# Local Network Access tests + +This directory contains tests for Local Network Access' integration with +the Fetch specification. + +See also: + +* [The specification](https://wicg.github.io/local-network-access/) +* [The repository](https://github.com/WICG/local-network-access/) +* [Open issues](https://github.com/WICG/local-network-access/issues/) diff --git a/test/wpt/tests/fetch/private-network-access/fetch-from-treat-as-public.https.window.js b/test/wpt/tests/fetch/local-network-access/fetch-from-treat-as-public.https.window.js similarity index 100% rename from test/wpt/tests/fetch/private-network-access/fetch-from-treat-as-public.https.window.js rename to test/wpt/tests/fetch/local-network-access/fetch-from-treat-as-public.https.window.js diff --git a/test/wpt/tests/fetch/private-network-access/fetch.https.window.js b/test/wpt/tests/fetch/local-network-access/fetch.https.window.js similarity index 100% rename from test/wpt/tests/fetch/private-network-access/fetch.https.window.js rename to test/wpt/tests/fetch/local-network-access/fetch.https.window.js diff --git a/test/wpt/tests/fetch/private-network-access/fetch.window.js b/test/wpt/tests/fetch/local-network-access/fetch.window.js similarity index 100% rename from test/wpt/tests/fetch/private-network-access/fetch.window.js rename to test/wpt/tests/fetch/local-network-access/fetch.window.js diff --git a/test/wpt/tests/fetch/private-network-access/iframe.tentative.https.window.js b/test/wpt/tests/fetch/local-network-access/iframe.tentative.https.window.js similarity index 100% rename from test/wpt/tests/fetch/private-network-access/iframe.tentative.https.window.js rename to test/wpt/tests/fetch/local-network-access/iframe.tentative.https.window.js diff --git a/test/wpt/tests/fetch/private-network-access/iframe.tentative.window.js b/test/wpt/tests/fetch/local-network-access/iframe.tentative.window.js similarity index 100% rename from test/wpt/tests/fetch/private-network-access/iframe.tentative.window.js rename to test/wpt/tests/fetch/local-network-access/iframe.tentative.window.js diff --git a/test/wpt/tests/fetch/private-network-access/mixed-content-fetch.tentative.https.window.js b/test/wpt/tests/fetch/local-network-access/mixed-content-fetch.tentative.https.window.js similarity index 77% rename from test/wpt/tests/fetch/private-network-access/mixed-content-fetch.tentative.https.window.js rename to test/wpt/tests/fetch/local-network-access/mixed-content-fetch.tentative.https.window.js index 54485dc7047..6f7d765617c 100644 --- a/test/wpt/tests/fetch/private-network-access/mixed-content-fetch.tentative.https.window.js +++ b/test/wpt/tests/fetch/local-network-access/mixed-content-fetch.tentative.https.window.js @@ -1,10 +1,10 @@ // META: script=/common/utils.js // META: script=resources/support.sub.js // -// Spec: https://wicg.github.io/private-network-access +// Spec: https://wicg.github.io/local-network-access // // These tests verify that secure contexts can fetch non-secure subresources -// from more private address spaces, avoiding mixed context checks, as long as +// from more local address spaces, avoiding mixed context checks, as long as // they specify a valid `targetAddressSpace` fetch option that matches the // target server's address space. @@ -16,9 +16,9 @@ setup(() => { // Given `addressSpace`, returns the other three possible IP address spaces. function otherAddressSpaces(addressSpace) { switch (addressSpace) { - case "local": return ["unknown", "private", "public"]; - case "private": return ["unknown", "local", "public"]; - case "public": return ["unknown", "local", "private"]; + case "loopback": return ["unknown", "local", "public"]; + case "local": return ["unknown", "loopback", "public"]; + case "public": return ["unknown", "loopback", "local"]; } } @@ -169,64 +169,66 @@ function makeNoBypassTests({ source, target }) { }, fetchOptions: { targetAddressSpace: correctAddressSpace }, expected: FetchTestResult.FAILURE, - }), prefix + 'not a private network request.'); + }), prefix + 'not a local network request.'); } -// Source: local secure context. +// Source: loopback secure context. // -// Fetches to the local and private address spaces cannot use +// Fetches to the loopback and local address spaces cannot use // `targetAddressSpace` to bypass mixed content, as they are not otherwise -// blocked by Private Network Access. +// blocked by Local Network Access. -makeNoBypassTests({ source: "local", target: "local" }); -makeNoBypassTests({ source: "local", target: "private" }); -makeNoBypassTests({ source: "local", target: "public" }); +makeNoBypassTests({ source: "loopback", target: "loopback" }); +makeNoBypassTests({ source: "loopback", target: "local" }); +makeNoBypassTests({ source: "loopback", target: "public" }); -// Source: private secure context. +// Source: local secure context. // -// Fetches to the local address space requires the right `targetAddressSpace` +// Fetches to the loopback address space requires the right `targetAddressSpace` // option, as well as a successful preflight response carrying a PNA-specific // header. // -// Fetches to the private address space cannot use `targetAddressSpace` to -// bypass mixed content, as they are not otherwise blocked by Private Network +// Fetches to the local address space cannot use `targetAddressSpace` to +// bypass mixed content, as they are not otherwise blocked by Local Network // Access. -makeTests({ source: "private", target: "local" }); +makeTests({ source: "local", target: "loopback" }); -makeNoBypassTests({ source: "private", target: "private" }); -makeNoBypassTests({ source: "private", target: "public" }); +makeNoBypassTests({ source: "local", target: "local" }); +makeNoBypassTests({ source: "local", target: "public" }); // Source: public secure context. // -// Fetches to the local and private address spaces require the right +// Fetches to the loopback and local address spaces require the right // `targetAddressSpace` option, as well as a successful preflight response // carrying a PNA-specific header. +makeTests({ source: "public", target: "loopback" }); makeTests({ source: "public", target: "local" }); -makeTests({ source: "public", target: "private" }); makeNoBypassTests({ source: "public", target: "public" }); -// These tests verify that documents fetched from the `local` address space yet -// carrying the `treat-as-public-address` CSP directive are treated as if they -// had been fetched from the `public` address space. +// These tests verify that documents fetched from the `loopback` address space +// yet carrying the `treat-as-public-address` CSP directive are treated as if +// they had been fetched from the `public` address space. -promise_test_parallel(t => fetchTest(t, { - source: { - server: Server.HTTPS_LOCAL, - treatAsPublic: true, - }, - target: { - server: Server.HTTP_LOCAL, - behavior: { - preflight: PreflightBehavior.optionalSuccess(token()), - response: ResponseBehavior.allowCrossOrigin(), - }, - }, - fetchOptions: { targetAddressSpace: "private" }, - expected: FetchTestResult.FAILURE, -}), 'https-treat-as-public to http-local: wrong targetAddressSpace "private".'); +promise_test_parallel( + t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTP_LOCAL, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + fetchOptions: {targetAddressSpace: 'local'}, + expected: FetchTestResult.FAILURE, + }), + 'https-treat-as-public to http-loopback: wrong targetAddressSpace "local".'); promise_test_parallel(t => fetchTest(t, { source: { @@ -240,9 +242,9 @@ promise_test_parallel(t => fetchTest(t, { response: ResponseBehavior.allowCrossOrigin(), }, }, - fetchOptions: { targetAddressSpace: "local" }, + fetchOptions: { targetAddressSpace: "loopback" }, expected: FetchTestResult.SUCCESS, -}), "https-treat-as-public to http-local: success."); +}), "https-treat-as-public to http-loopback: success."); promise_test_parallel(t => fetchTest(t, { source: { @@ -256,9 +258,9 @@ promise_test_parallel(t => fetchTest(t, { response: ResponseBehavior.allowCrossOrigin(), }, }, - fetchOptions: { targetAddressSpace: "local" }, + fetchOptions: { targetAddressSpace: "loopback" }, expected: FetchTestResult.FAILURE, -}), 'https-treat-as-public to http-private: wrong targetAddressSpace "local".'); +}), 'https-treat-as-public to http-local: wrong targetAddressSpace "loopback".'); promise_test_parallel(t => fetchTest(t, { source: { @@ -272,6 +274,6 @@ promise_test_parallel(t => fetchTest(t, { response: ResponseBehavior.allowCrossOrigin(), }, }, - fetchOptions: { targetAddressSpace: "private" }, + fetchOptions: { targetAddressSpace: "local" }, expected: FetchTestResult.SUCCESS, -}), "https-treat-as-public to http-private: success."); +}), "https-treat-as-public to http-local: success."); diff --git a/test/wpt/tests/fetch/private-network-access/nested-worker.https.window.js b/test/wpt/tests/fetch/local-network-access/nested-worker.https.window.js similarity index 100% rename from test/wpt/tests/fetch/private-network-access/nested-worker.https.window.js rename to test/wpt/tests/fetch/local-network-access/nested-worker.https.window.js diff --git a/test/wpt/tests/fetch/private-network-access/nested-worker.window.js b/test/wpt/tests/fetch/local-network-access/nested-worker.window.js similarity index 100% rename from test/wpt/tests/fetch/private-network-access/nested-worker.window.js rename to test/wpt/tests/fetch/local-network-access/nested-worker.window.js diff --git a/test/wpt/tests/fetch/private-network-access/preflight-cache.https.window.js b/test/wpt/tests/fetch/local-network-access/preflight-cache.https.window.js similarity index 100% rename from test/wpt/tests/fetch/private-network-access/preflight-cache.https.window.js rename to test/wpt/tests/fetch/local-network-access/preflight-cache.https.window.js diff --git a/test/wpt/tests/fetch/private-network-access/redirect.https.window.js b/test/wpt/tests/fetch/local-network-access/redirect.https.window.js similarity index 50% rename from test/wpt/tests/fetch/private-network-access/redirect.https.window.js rename to test/wpt/tests/fetch/local-network-access/redirect.https.window.js index fe004d929de..edbd5a19fbc 100644 --- a/test/wpt/tests/fetch/private-network-access/redirect.https.window.js +++ b/test/wpt/tests/fetch/local-network-access/redirect.https.window.js @@ -230,3 +230,248 @@ promise_test(t => fetchTest(t, { fetchOptions: { mode: "no-cors" }, expected: FetchTestResult.OPAQUE, }), "public to private to local: no-cors success."); + +// treat-as-public -> local -> private + +// Request 1 (treat-as-public -> local): preflight required. +// Request 2 (treat-as-public -> private): preflight required. + +// This verifies that PNA checks are applied to every step in a redirect chain. + +promise_test(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + redirect: preflightUrl({ + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }), + } + }, + expected: FetchTestResult.FAILURE, +}), "treat-as-public to local to private: failed first preflight."); + +promise_test(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + redirect: preflightUrl({ + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.noPnaHeader(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }), + } + }, + expected: FetchTestResult.FAILURE, +}), "treat-as-public to local to private: failed second preflight."); + +promise_test(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + redirect: preflightUrl({ + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }), + } + }, + expected: FetchTestResult.SUCCESS, +}), "treat-as-public to local to private: success."); + +promise_test(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + redirect: preflightUrl({ + server: Server.HTTPS_PRIVATE, + behavior: { preflight: PreflightBehavior.success(token()) }, + }), + } + }, + fetchOptions: { mode: "no-cors" }, + expected: FetchTestResult.FAILURE, +}), "treat-as-public to local to private: no-cors failed first preflight."); + +promise_test(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + redirect: preflightUrl({ server: Server.HTTPS_PRIVATE }), + } + }, + fetchOptions: { mode: "no-cors" }, + expected: FetchTestResult.FAILURE, +}), "treat-as-public to local to private: no-cors failed second preflight."); + +promise_test(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + redirect: preflightUrl({ + server: Server.HTTPS_PRIVATE, + behavior: { preflight: PreflightBehavior.success(token()) }, + }), + } + }, + fetchOptions: { mode: "no-cors" }, + expected: FetchTestResult.OPAQUE, +}), "treat-as-public to local to private: no-cors success."); + +// treat-as-public -> private -> local + +// Request 1 (treat-as-public -> private): preflight required. +// Request 2 (treat-as-public -> local): preflight required. + +// This verifies that PNA checks are applied to every step in a redirect chain. + +promise_test(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.noPnaHeader(token()), + response: ResponseBehavior.allowCrossOrigin(), + redirect: preflightUrl({ + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }), + } + }, + expected: FetchTestResult.FAILURE, +}), "treat-as-public to private to local: failed first preflight."); + +promise_test(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + redirect: preflightUrl({ + server: Server.HTTPS_LOCAL, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }), + } + }, + expected: FetchTestResult.FAILURE, +}), "treat-as-public to private to local: failed second preflight."); + +promise_test(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + redirect: preflightUrl({ + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }), + } + }, + expected: FetchTestResult.SUCCESS, +}), "treat-as-public to private to local: success."); + +promise_test(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + redirect: preflightUrl({ + server: Server.HTTPS_LOCAL, + behavior: { preflight: PreflightBehavior.success(token()) }, + }), + } + }, + fetchOptions: { mode: "no-cors" }, + expected: FetchTestResult.FAILURE, +}), "treat-as-public to private to local: no-cors failed first preflight."); + +promise_test(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.success(token()), + redirect: preflightUrl({ server: Server.HTTPS_LOCAL }), + } + }, + fetchOptions: { mode: "no-cors" }, + expected: FetchTestResult.FAILURE, +}), "treat-as-public to private to local: no-cors failed second preflight."); + +promise_test(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.success(token()), + redirect: preflightUrl({ + server: Server.HTTPS_LOCAL, + behavior: { preflight: PreflightBehavior.success(token()) }, + }), + } + }, + fetchOptions: { mode: "no-cors" }, + expected: FetchTestResult.OPAQUE, +}), "treat-as-public to private to local: no-cors success."); diff --git a/test/wpt/tests/fetch/private-network-access/resources/executor.html b/test/wpt/tests/fetch/local-network-access/resources/executor.html similarity index 100% rename from test/wpt/tests/fetch/private-network-access/resources/executor.html rename to test/wpt/tests/fetch/local-network-access/resources/executor.html diff --git a/test/wpt/tests/fetch/private-network-access/resources/fetcher.html b/test/wpt/tests/fetch/local-network-access/resources/fetcher.html similarity index 100% rename from test/wpt/tests/fetch/private-network-access/resources/fetcher.html rename to test/wpt/tests/fetch/local-network-access/resources/fetcher.html diff --git a/test/wpt/tests/fetch/private-network-access/resources/fetcher.js b/test/wpt/tests/fetch/local-network-access/resources/fetcher.js similarity index 100% rename from test/wpt/tests/fetch/private-network-access/resources/fetcher.js rename to test/wpt/tests/fetch/local-network-access/resources/fetcher.js diff --git a/test/wpt/tests/fetch/private-network-access/resources/iframed.html b/test/wpt/tests/fetch/local-network-access/resources/iframed.html similarity index 100% rename from test/wpt/tests/fetch/private-network-access/resources/iframed.html rename to test/wpt/tests/fetch/local-network-access/resources/iframed.html diff --git a/test/wpt/tests/fetch/private-network-access/resources/iframer.html b/test/wpt/tests/fetch/local-network-access/resources/iframer.html similarity index 100% rename from test/wpt/tests/fetch/private-network-access/resources/iframer.html rename to test/wpt/tests/fetch/local-network-access/resources/iframer.html diff --git a/test/wpt/tests/fetch/private-network-access/resources/preflight.py b/test/wpt/tests/fetch/local-network-access/resources/preflight.py similarity index 98% rename from test/wpt/tests/fetch/private-network-access/resources/preflight.py rename to test/wpt/tests/fetch/local-network-access/resources/preflight.py index 4aefee6f40d..4b0bfefd4d6 100644 --- a/test/wpt/tests/fetch/private-network-access/resources/preflight.py +++ b/test/wpt/tests/fetch/local-network-access/resources/preflight.py @@ -140,7 +140,7 @@ def _handle_final_request(request, response): if uuid is not None: if (request.server.stash.take(uuid) is None and not _is_preflight_optional(request)): - return (405, [], "no preflight received for {}".format(uuid)) + return (405, [], "no preflight received") request.server.stash.put(uuid, "final") mode = request.GET.get(b"final-headers") diff --git a/test/wpt/tests/fetch/private-network-access/resources/service-worker-bridge.html b/test/wpt/tests/fetch/local-network-access/resources/service-worker-bridge.html similarity index 100% rename from test/wpt/tests/fetch/private-network-access/resources/service-worker-bridge.html rename to test/wpt/tests/fetch/local-network-access/resources/service-worker-bridge.html diff --git a/test/wpt/tests/fetch/private-network-access/resources/service-worker.js b/test/wpt/tests/fetch/local-network-access/resources/service-worker.js similarity index 100% rename from test/wpt/tests/fetch/private-network-access/resources/service-worker.js rename to test/wpt/tests/fetch/local-network-access/resources/service-worker.js diff --git a/test/wpt/tests/fetch/private-network-access/resources/shared-fetcher.js b/test/wpt/tests/fetch/local-network-access/resources/shared-fetcher.js similarity index 100% rename from test/wpt/tests/fetch/private-network-access/resources/shared-fetcher.js rename to test/wpt/tests/fetch/local-network-access/resources/shared-fetcher.js diff --git a/test/wpt/tests/fetch/private-network-access/resources/shared-worker-blob-fetcher.html b/test/wpt/tests/fetch/local-network-access/resources/shared-worker-blob-fetcher.html similarity index 100% rename from test/wpt/tests/fetch/private-network-access/resources/shared-worker-blob-fetcher.html rename to test/wpt/tests/fetch/local-network-access/resources/shared-worker-blob-fetcher.html diff --git a/test/wpt/tests/fetch/private-network-access/resources/shared-worker-fetcher.html b/test/wpt/tests/fetch/local-network-access/resources/shared-worker-fetcher.html similarity index 100% rename from test/wpt/tests/fetch/private-network-access/resources/shared-worker-fetcher.html rename to test/wpt/tests/fetch/local-network-access/resources/shared-worker-fetcher.html diff --git a/test/wpt/tests/fetch/private-network-access/resources/socket-opener.html b/test/wpt/tests/fetch/local-network-access/resources/socket-opener.html similarity index 100% rename from test/wpt/tests/fetch/private-network-access/resources/socket-opener.html rename to test/wpt/tests/fetch/local-network-access/resources/socket-opener.html diff --git a/test/wpt/tests/fetch/private-network-access/resources/support.sub.js b/test/wpt/tests/fetch/local-network-access/resources/support.sub.js similarity index 97% rename from test/wpt/tests/fetch/private-network-access/resources/support.sub.js rename to test/wpt/tests/fetch/local-network-access/resources/support.sub.js index 210184ec4b6..0cf3d2532e7 100644 --- a/test/wpt/tests/fetch/private-network-access/resources/support.sub.js +++ b/test/wpt/tests/fetch/local-network-access/resources/support.sub.js @@ -75,20 +75,20 @@ async function postMessageAndAwaitReply(target, message) { // Maps protocol (without the trailing colon) and address space to port. const SERVER_PORTS = { "http": { - "local": {{ports[http][0]}}, - "private": {{ports[http-private][0]}}, + "loopback": {{ports[http][0]}}, + "local": {{ports[http-private][0]}}, "public": {{ports[http-public][0]}}, }, "https": { - "local": {{ports[https][0]}}, - "private": {{ports[https-private][0]}}, + "loopback": {{ports[https][0]}}, + "local": {{ports[https-private][0]}}, "public": {{ports[https-public][0]}}, }, "ws": { - "local": {{ports[ws][0]}}, + "loopback": {{ports[ws][0]}}, }, "wss": { - "local": {{ports[wss][0]}}, + "loopback": {{ports[wss][0]}}, }, }; @@ -126,14 +126,14 @@ class Server { }; } - static HTTP_LOCAL = Server.get("http", "local"); - static HTTP_PRIVATE = Server.get("http", "private"); + static HTTP_LOCAL = Server.get("http", "loopback"); + static HTTP_PRIVATE = Server.get("http", "local"); static HTTP_PUBLIC = Server.get("http", "public"); - static HTTPS_LOCAL = Server.get("https", "local"); - static HTTPS_PRIVATE = Server.get("https", "private"); + static HTTPS_LOCAL = Server.get("https", "loopback"); + static HTTPS_PRIVATE = Server.get("https", "local"); static HTTPS_PUBLIC = Server.get("https", "public"); - static WS_LOCAL = Server.get("ws", "local"); - static WSS_LOCAL = Server.get("wss", "local"); + static WS_LOCAL = Server.get("ws", "loopback"); + static WSS_LOCAL = Server.get("wss", "loopback"); }; // Resolves a URL relative to the current location, returning an absolute URL. @@ -205,6 +205,7 @@ function sourceResolveOptions({ server, treatAsPublic }) { // - `response`: The result of calling one of `ResponseBehavior`'s methods. // - `redirect`: A URL to which the target should redirect GET requests. function preflightUrl({ server, behavior }) { + assert_not_equals(server, undefined, 'server'); const options = {...server}; if (behavior) { const { preflight, response, redirect } = behavior; diff --git a/test/wpt/tests/fetch/private-network-access/resources/worker-blob-fetcher.html b/test/wpt/tests/fetch/local-network-access/resources/worker-blob-fetcher.html similarity index 100% rename from test/wpt/tests/fetch/private-network-access/resources/worker-blob-fetcher.html rename to test/wpt/tests/fetch/local-network-access/resources/worker-blob-fetcher.html diff --git a/test/wpt/tests/fetch/private-network-access/resources/worker-fetcher.html b/test/wpt/tests/fetch/local-network-access/resources/worker-fetcher.html similarity index 100% rename from test/wpt/tests/fetch/private-network-access/resources/worker-fetcher.html rename to test/wpt/tests/fetch/local-network-access/resources/worker-fetcher.html diff --git a/test/wpt/tests/fetch/private-network-access/resources/worker-fetcher.js b/test/wpt/tests/fetch/local-network-access/resources/worker-fetcher.js similarity index 100% rename from test/wpt/tests/fetch/private-network-access/resources/worker-fetcher.js rename to test/wpt/tests/fetch/local-network-access/resources/worker-fetcher.js diff --git a/test/wpt/tests/fetch/private-network-access/resources/xhr-sender.html b/test/wpt/tests/fetch/local-network-access/resources/xhr-sender.html similarity index 100% rename from test/wpt/tests/fetch/private-network-access/resources/xhr-sender.html rename to test/wpt/tests/fetch/local-network-access/resources/xhr-sender.html diff --git a/test/wpt/tests/fetch/private-network-access/service-worker-background-fetch.https.window.js b/test/wpt/tests/fetch/local-network-access/service-worker-background-fetch.https.window.js similarity index 100% rename from test/wpt/tests/fetch/private-network-access/service-worker-background-fetch.https.window.js rename to test/wpt/tests/fetch/local-network-access/service-worker-background-fetch.https.window.js diff --git a/test/wpt/tests/fetch/private-network-access/service-worker-fetch.https.window.js b/test/wpt/tests/fetch/local-network-access/service-worker-fetch.https.window.js similarity index 100% rename from test/wpt/tests/fetch/private-network-access/service-worker-fetch.https.window.js rename to test/wpt/tests/fetch/local-network-access/service-worker-fetch.https.window.js diff --git a/test/wpt/tests/fetch/private-network-access/service-worker-update.https.window.js b/test/wpt/tests/fetch/local-network-access/service-worker-update.https.window.js similarity index 100% rename from test/wpt/tests/fetch/private-network-access/service-worker-update.https.window.js rename to test/wpt/tests/fetch/local-network-access/service-worker-update.https.window.js diff --git a/test/wpt/tests/fetch/private-network-access/service-worker.https.window.js b/test/wpt/tests/fetch/local-network-access/service-worker.https.window.js similarity index 100% rename from test/wpt/tests/fetch/private-network-access/service-worker.https.window.js rename to test/wpt/tests/fetch/local-network-access/service-worker.https.window.js diff --git a/test/wpt/tests/fetch/private-network-access/shared-worker-blob-fetch.https.window.js b/test/wpt/tests/fetch/local-network-access/shared-worker-blob-fetch.https.window.js similarity index 100% rename from test/wpt/tests/fetch/private-network-access/shared-worker-blob-fetch.https.window.js rename to test/wpt/tests/fetch/local-network-access/shared-worker-blob-fetch.https.window.js diff --git a/test/wpt/tests/fetch/private-network-access/shared-worker-blob-fetch.window.js b/test/wpt/tests/fetch/local-network-access/shared-worker-blob-fetch.window.js similarity index 100% rename from test/wpt/tests/fetch/private-network-access/shared-worker-blob-fetch.window.js rename to test/wpt/tests/fetch/local-network-access/shared-worker-blob-fetch.window.js diff --git a/test/wpt/tests/fetch/private-network-access/shared-worker-fetch.https.window.js b/test/wpt/tests/fetch/local-network-access/shared-worker-fetch.https.window.js similarity index 100% rename from test/wpt/tests/fetch/private-network-access/shared-worker-fetch.https.window.js rename to test/wpt/tests/fetch/local-network-access/shared-worker-fetch.https.window.js diff --git a/test/wpt/tests/fetch/private-network-access/shared-worker-fetch.window.js b/test/wpt/tests/fetch/local-network-access/shared-worker-fetch.window.js similarity index 100% rename from test/wpt/tests/fetch/private-network-access/shared-worker-fetch.window.js rename to test/wpt/tests/fetch/local-network-access/shared-worker-fetch.window.js diff --git a/test/wpt/tests/fetch/private-network-access/shared-worker.https.window.js b/test/wpt/tests/fetch/local-network-access/shared-worker.https.window.js similarity index 100% rename from test/wpt/tests/fetch/private-network-access/shared-worker.https.window.js rename to test/wpt/tests/fetch/local-network-access/shared-worker.https.window.js diff --git a/test/wpt/tests/fetch/private-network-access/shared-worker.window.js b/test/wpt/tests/fetch/local-network-access/shared-worker.window.js similarity index 100% rename from test/wpt/tests/fetch/private-network-access/shared-worker.window.js rename to test/wpt/tests/fetch/local-network-access/shared-worker.window.js diff --git a/test/wpt/tests/fetch/private-network-access/websocket.https.window.js b/test/wpt/tests/fetch/local-network-access/websocket.https.window.js similarity index 100% rename from test/wpt/tests/fetch/private-network-access/websocket.https.window.js rename to test/wpt/tests/fetch/local-network-access/websocket.https.window.js diff --git a/test/wpt/tests/fetch/private-network-access/websocket.window.js b/test/wpt/tests/fetch/local-network-access/websocket.window.js similarity index 100% rename from test/wpt/tests/fetch/private-network-access/websocket.window.js rename to test/wpt/tests/fetch/local-network-access/websocket.window.js diff --git a/test/wpt/tests/fetch/private-network-access/worker-blob-fetch.window.js b/test/wpt/tests/fetch/local-network-access/worker-blob-fetch.window.js similarity index 100% rename from test/wpt/tests/fetch/private-network-access/worker-blob-fetch.window.js rename to test/wpt/tests/fetch/local-network-access/worker-blob-fetch.window.js diff --git a/test/wpt/tests/fetch/private-network-access/worker-fetch.https.window.js b/test/wpt/tests/fetch/local-network-access/worker-fetch.https.window.js similarity index 100% rename from test/wpt/tests/fetch/private-network-access/worker-fetch.https.window.js rename to test/wpt/tests/fetch/local-network-access/worker-fetch.https.window.js diff --git a/test/wpt/tests/fetch/private-network-access/worker-fetch.window.js b/test/wpt/tests/fetch/local-network-access/worker-fetch.window.js similarity index 100% rename from test/wpt/tests/fetch/private-network-access/worker-fetch.window.js rename to test/wpt/tests/fetch/local-network-access/worker-fetch.window.js diff --git a/test/wpt/tests/fetch/private-network-access/worker.https.window.js b/test/wpt/tests/fetch/local-network-access/worker.https.window.js similarity index 100% rename from test/wpt/tests/fetch/private-network-access/worker.https.window.js rename to test/wpt/tests/fetch/local-network-access/worker.https.window.js diff --git a/test/wpt/tests/fetch/private-network-access/worker.window.js b/test/wpt/tests/fetch/local-network-access/worker.window.js similarity index 100% rename from test/wpt/tests/fetch/private-network-access/worker.window.js rename to test/wpt/tests/fetch/local-network-access/worker.window.js diff --git a/test/wpt/tests/fetch/private-network-access/xhr-from-treat-as-public.https.window.js b/test/wpt/tests/fetch/local-network-access/xhr-from-treat-as-public.https.window.js similarity index 100% rename from test/wpt/tests/fetch/private-network-access/xhr-from-treat-as-public.https.window.js rename to test/wpt/tests/fetch/local-network-access/xhr-from-treat-as-public.https.window.js diff --git a/test/wpt/tests/fetch/private-network-access/xhr.https.window.js b/test/wpt/tests/fetch/local-network-access/xhr.https.window.js similarity index 100% rename from test/wpt/tests/fetch/private-network-access/xhr.https.window.js rename to test/wpt/tests/fetch/local-network-access/xhr.https.window.js diff --git a/test/wpt/tests/fetch/private-network-access/xhr.window.js b/test/wpt/tests/fetch/local-network-access/xhr.window.js similarity index 100% rename from test/wpt/tests/fetch/private-network-access/xhr.window.js rename to test/wpt/tests/fetch/local-network-access/xhr.window.js diff --git a/test/wpt/tests/fetch/orb/resources/data_non_ascii.json b/test/wpt/tests/fetch/orb/resources/data_non_ascii.json new file mode 100644 index 00000000000..64566c50c14 --- /dev/null +++ b/test/wpt/tests/fetch/orb/resources/data_non_ascii.json @@ -0,0 +1 @@ +["你好"] diff --git a/test/wpt/tests/fetch/orb/resources/js-unlabeled-utf16-without-bom.json b/test/wpt/tests/fetch/orb/resources/js-unlabeled-utf16-without-bom.json new file mode 100644 index 0000000000000000000000000000000000000000..157a8f5430862e504c1225de69b998b114f5c289 GIT binary patch literal 70 zcmXSC$YjW4NMXolC}+@P$Y4lhC}xOfNM)!1;$((Wh7us10u(6*@``|J3xFaD47NaA N0_2whWvv;w7y$N*4KDxy literal 0 HcmV?d00001 diff --git a/test/wpt/tests/fetch/orb/resources/script-iso-8559-1.js b/test/wpt/tests/fetch/orb/resources/script-iso-8559-1.js new file mode 100644 index 00000000000..3bccb6af93e --- /dev/null +++ b/test/wpt/tests/fetch/orb/resources/script-iso-8559-1.js @@ -0,0 +1,4 @@ +"use strict"; +function fn() { + return "An"; +} diff --git a/test/wpt/tests/fetch/orb/resources/script-utf16-bom.js b/test/wpt/tests/fetch/orb/resources/script-utf16-bom.js new file mode 100644 index 0000000000000000000000000000000000000000..16b76e9d5e431fd9c363b0b1a15ba095e2a9fa11 GIT binary patch literal 92 zcmezWPl=(Fp_n0+K> + promise_rejects_js( + t, + TypeError, + fetchORB(`${path}/data_non_ascii.json`, null, contentType("application/json")) + ), + "ORB should block opaque application/json which contains non ascii characters" +); + promise_test(async () => { fetchORB(`${path}/image.png`, null, contentType("image/png")); }, "ORB shouldn't block opaque image/png"); @@ -49,3 +59,18 @@ promise_test(async () => { promise_test(async () => { await fetchORB(`${path}/script.js`, null, contentType("text/javascript")); }, "ORB shouldn't block opaque text/javascript"); + +// Test javascript validation can correctly decode the content with BOM. +promise_test(async () => { + await fetchORB(`${path}/script-utf16-bom.js`, null, contentType("application/json")); +}, "ORB shouldn't block opaque text/javascript (utf16 encoded with BOM)"); + +// Test javascript validation can correctly decode the content with the http charset hint. +promise_test(async () => { + await fetchORB(`${path}/script-utf16-without-bom.js`, null, contentType("application/json; charset=utf-16")); +}, "ORB shouldn't block opaque text/javascript (utf16 encoded without BOM but charset is provided in content-type)"); + +// Test javascript validation can correctly decode the content for iso-8559-1 (fallback decoder in Firefox). +promise_test(async () => { + await fetchORB(`${path}/script-iso-8559-1.js`, null, contentType("application/json")); +}, "ORB shouldn't block opaque text/javascript (iso-8559-1 encoded)"); diff --git a/test/wpt/tests/fetch/orb/tentative/script-utf16-without-bom-hint-charset.sub.html b/test/wpt/tests/fetch/orb/tentative/script-utf16-without-bom-hint-charset.sub.html new file mode 100644 index 00000000000..b15f976a669 --- /dev/null +++ b/test/wpt/tests/fetch/orb/tentative/script-utf16-without-bom-hint-charset.sub.html @@ -0,0 +1,22 @@ + + + + +
+ + + + + + + diff --git a/test/wpt/tests/fetch/private-network-access/README.md b/test/wpt/tests/fetch/private-network-access/README.md deleted file mode 100644 index a69aab48723..00000000000 --- a/test/wpt/tests/fetch/private-network-access/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# Private Network Access tests - -This directory contains tests for Private Network Access' integration with -the Fetch specification. - -See also: - -* [The specification](https://wicg.github.io/private-network-access/) -* [The repository](https://github.com/WICG/private-network-access/) -* [Open issues](https://github.com/WICG/private-network-access/issues/) diff --git a/test/wpt/tests/fetch/range/blob.any.js b/test/wpt/tests/fetch/range/blob.any.js index f3eb313b34b..1db3b248f6c 100644 --- a/test/wpt/tests/fetch/range/blob.any.js +++ b/test/wpt/tests/fetch/range/blob.any.js @@ -46,152 +46,179 @@ const supportedBlobRange = [ content_range: "bytes 4-12/13", result: "much here", }, + { + name: "Blob content with short content and a range end matching content length", + data: ["Not much here"], + type: "text/plain", + range: "bytes=4-13", + content_length: 9, + content_range: "bytes 4-12/13", + result: "much here", + }, + { + name: "Blob range with whitespace before and after hyphen", + data: ["Valid whitespace #1"], + type: "text/plain", + range: "bytes=5 - 10", + content_length: 6, + content_range: "bytes 5-10/19", + result: " white", + }, + { + name: "Blob range with whitespace after hyphen", + data: ["Valid whitespace #2"], + type: "text/plain", + range: "bytes=-\t 5", + content_length: 5, + content_range: "bytes 14-18/19", + result: "ce #2", + }, + { + name: "Blob range with whitespace around equals sign", + data: ["Valid whitespace #3"], + type: "text/plain", + range: "bytes \t =\t 6-", + content_length: 13, + content_range: "bytes 6-18/19", + result: "whitespace #3", + }, ]; const unsupportedBlobRange = [ + { + name: "Blob range with no value", + data: ["Blob range should have a value"], + type: "text/plain", + range: "", + }, + { + name: "Blob range with incorrect range header", + data: ["A"], + type: "text/plain", + range: "byte=0-" + }, + { + name: "Blob range with incorrect range header #2", + data: ["A"], + type: "text/plain", + range: "bytes" + }, + { + name: "Blob range with incorrect range header #3", + data: ["A"], + type: "text/plain", + range: "bytes\t \t" + }, { name: "Blob range request with multiple range values", data: ["Multiple ranges are not currently supported"], type: "text/plain", - headers: { - "Range": "bytes=0-5,15-" - } + range: "bytes=0-5,15-", }, { name: "Blob range request with multiple range values and whitespace", data: ["Multiple ranges are not currently supported"], type: "text/plain", - headers: { - "Range": "bytes=0-5, 15-" - } + range: "bytes=0-5, 15-", }, { name: "Blob range request with trailing comma", data: ["Range with invalid trailing comma"], type: "text/plain", - headers: { - "Range": "bytes=0-5," - } + range: "bytes=0-5,", }, { name: "Blob range with no start or end", data: ["Range with no start or end"], type: "text/plain", - headers: { - "Range": "bytes=-" - } - }, - { - name: "Blob range with invalid whitespace in range #1", - data: ["Invalid whitespace #1"], - type: "text/plain", - headers: { - "Range": "bytes=5 - 10" - } - }, - { - name: "Blob range with invalid whitespace in range #2", - data: ["Invalid whitespace #2"], - type: "text/plain", - headers: { - "Range": "bytes=-\t 5" - } + range: "bytes=-", }, { name: "Blob range request with short range end", data: ["Range end should be greater than range start"], - type: "text/plain" , - headers: { - "Range": "bytes=10-5" - } + type: "text/plain", + range: "bytes=10-5", }, { name: "Blob range start should be an ASCII digit", data: ["Range start must be an ASCII digit"], - type: "text/plain" , - headers: { - "Range": "bytes=x-5" - } + type: "text/plain", + range: "bytes=x-5", }, { name: "Blob range should have a dash", data: ["Blob range should have a dash"], - type: "text/plain" , - headers: { - "Range": "bytes=5" - } + type: "text/plain", + range: "bytes=5", }, { name: "Blob range end should be an ASCII digit", data: ["Range end must be an ASCII digit"], - type: "text/plain" , - headers: { - "Range": "bytes=5-x" - } + type: "text/plain", + range: "bytes=5-x", }, { name: "Blob range should include '-'", data: ["Range end must include '-'"], - type: "text/plain" , - headers: { - "Range": "bytes=x" - } + type: "text/plain", + range: "bytes=x", }, { name: "Blob range should include '='", data: ["Range end must include '='"], - type: "text/plain" , - headers: { - "Range": "bytes 5-" - } + type: "text/plain", + range: "bytes 5-", }, { name: "Blob range should include 'bytes='", data: ["Range end must include 'bytes='"], - type: "text/plain" , - headers: { - "Range": "5-" - } + type: "text/plain", + range: "5-", }, { name: "Blob content with short content and a large range start", data: ["Not much here"], type: "text/plain", - headers: { - "Range": "bytes=100000-", - } + range: "bytes=100000-", + }, + { + name: "Blob content with short content and a range start matching the content length", + data: ["Not much here"], + type: "text/plain", + range: "bytes=13-", }, ]; - supportedBlobRange.forEach(({ name, data, type, range, content_length, content_range, result }) => { - promise_test(async () => { - let blob = new Blob(data, { "type" : type }); - return fetch(URL.createObjectURL(blob), { - "method": "GET", + promise_test(async t => { + const blob = new Blob(data, { "type" : type }); + const blobURL = URL.createObjectURL(blob); + t.add_cleanup(() => URL.revokeObjectURL(blobURL)); + const resp = await fetch(blobURL, { "headers": { "Range": range } - }).then(function(resp) { - assert_equals(resp.status, 206, "HTTP status is 206"); - assert_equals(resp.type, "basic", "response type is basic"); - assert_equals(resp.headers.get("Content-Type"), type, "Content-Type is " + resp.headers.get("Content-Type")); - assert_equals(resp.headers.get("Content-Length"), content_length.toString(), "Content-Length is " + resp.headers.get("Content-Length")); - assert_equals(resp.headers.get("Content-Range"), content_range, "Content-Range is " + resp.headers.get("Content-Range")); - return resp.text(); - }).then(function(text) { - assert_equals(text, result, "Response's body is correct"); }); + assert_equals(resp.status, 206, "HTTP status is 206"); + assert_equals(resp.type, "basic", "response type is basic"); + assert_equals(resp.headers.get("Content-Type"), type, "Content-Type is " + resp.headers.get("Content-Type")); + assert_equals(resp.headers.get("Content-Length"), content_length.toString(), "Content-Length is " + resp.headers.get("Content-Length")); + assert_equals(resp.headers.get("Content-Range"), content_range, "Content-Range is " + resp.headers.get("Content-Range")); + const text = await resp.text(); + assert_equals(text, result, "Response's body is correct"); }, name); }); -unsupportedBlobRange.forEach(({ name, data, type, headers }) => { - promise_test(function(test) { - let blob = new Blob(data, { "type" : type }); - let promise = fetch(URL.createObjectURL(blob), { - "method": "GET", - "headers": headers, +unsupportedBlobRange.forEach(({ name, data, type, range }) => { + promise_test(t => { + const blob = new Blob(data, { "type" : type }); + const blobURL = URL.createObjectURL(blob); + t.add_cleanup(() => URL.revokeObjectURL(blobURL)); + const promise = fetch(blobURL, { + "headers": { + "Range": range + } }); - return promise_rejects_js(test, TypeError, promise); + return promise_rejects_js(t, TypeError, promise); }, name); }); diff --git a/test/wpt/tests/interfaces/EXT_clip_cull_distance.idl b/test/wpt/tests/interfaces/EXT_clip_cull_distance.idl deleted file mode 100644 index 18d1c02a11a..00000000000 --- a/test/wpt/tests/interfaces/EXT_clip_cull_distance.idl +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATED CONTENT - DO NOT EDIT -// Content was automatically extracted by Reffy into webref -// (https://github.com/w3c/webref) -// Source: WebGL EXT_clip_cull_distance Extension Draft Specification (https://registry.khronos.org/webgl/extensions/EXT_clip_cull_distance/) - -[Exposed=(Window,Worker), LegacyNoInterfaceObject] -interface EXT_clip_cull_distance { - const GLenum MAX_CLIP_DISTANCES_EXT = 0x0D32; - const GLenum MAX_CULL_DISTANCES_EXT = 0x82F9; - const GLenum MAX_COMBINED_CLIP_AND_CULL_DISTANCES_EXT = 0x82FA; - - const GLenum CLIP_DISTANCE0_EXT = 0x3000; - const GLenum CLIP_DISTANCE1_EXT = 0x3001; - const GLenum CLIP_DISTANCE2_EXT = 0x3002; - const GLenum CLIP_DISTANCE3_EXT = 0x3003; - const GLenum CLIP_DISTANCE4_EXT = 0x3004; - const GLenum CLIP_DISTANCE5_EXT = 0x3005; - const GLenum CLIP_DISTANCE6_EXT = 0x3006; - const GLenum CLIP_DISTANCE7_EXT = 0x3007; -}; diff --git a/test/wpt/tests/interfaces/FedCM.idl b/test/wpt/tests/interfaces/FedCM.idl index b3ddb54e0c6..8de87e88b05 100644 --- a/test/wpt/tests/interfaces/FedCM.idl +++ b/test/wpt/tests/interfaces/FedCM.idl @@ -3,6 +3,25 @@ // (https://github.com/w3c/webref) // Source: Federated Credential Management API (https://fedidcg.github.io/FedCM/) +[Exposed=Window, SecureContext] +interface IdentityCredential : Credential { + readonly attribute USVString? token; +}; + +partial dictionary CredentialRequestOptions { + IdentityCredentialRequestOptions identity; +}; + +dictionary IdentityCredentialRequestOptions { + sequence providers; +}; + +dictionary IdentityProviderConfig { + required USVString configURL; + required USVString clientId; + USVString nonce; +}; + dictionary IdentityProviderWellKnown { required sequence provider_urls; }; @@ -16,6 +35,7 @@ dictionary IdentityProviderBranding { USVString background_color; USVString color; sequence icons; + USVString name; }; dictionary IdentityProviderAPIConfig { @@ -30,52 +50,18 @@ dictionary IdentityProviderAccount { required USVString name; required USVString email; USVString given_name; + USVString picture; sequence approved_clients; }; dictionary IdentityProviderAccountList { sequence accounts; }; -dictionary IdentityProviderClientMetadata { - USVString privacy_policy_url; - USVString terms_of_service_url; -}; - dictionary IdentityProviderToken { required USVString token; }; -[Exposed=Window, SecureContext] -interface IdentityCredential : Credential { - readonly attribute USVString? token; -}; - -partial dictionary CredentialRequestOptions { - IdentityCredentialRequestOptions identity; -}; - -dictionary IdentityCredentialRequestOptions { - sequence providers; -}; - -dictionary IdentityProviderConfig { - required USVString configURL; - required USVString clientId; - USVString nonce; -}; - -dictionary IdentityCredentialLogoutRPsRequest { - required USVString url; - required USVString accountId; -}; - -[Exposed=Window, SecureContext] -partial interface IdentityCredential { - static Promise logoutRPs(sequence logoutRequests); -}; - -[Exposed=Window, SecureContext] -interface IdentityProvider { - static undefined login(); - static undefined logout(); +dictionary IdentityProviderClientMetadata { + USVString privacy_policy_url; + USVString terms_of_service_url; }; diff --git a/test/wpt/tests/interfaces/WEBGL_clip_cull_distance.idl b/test/wpt/tests/interfaces/WEBGL_clip_cull_distance.idl new file mode 100644 index 00000000000..46fa921fd53 --- /dev/null +++ b/test/wpt/tests/interfaces/WEBGL_clip_cull_distance.idl @@ -0,0 +1,20 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: WebGL WEBGL_clip_cull_distance Extension Draft Specification (https://registry.khronos.org/webgl/extensions/WEBGL_clip_cull_distance/) + +[Exposed=(Window,Worker), LegacyNoInterfaceObject] +interface WEBGL_clip_cull_distance { + const GLenum MAX_CLIP_DISTANCES_WEBGL = 0x0D32; + const GLenum MAX_CULL_DISTANCES_WEBGL = 0x82F9; + const GLenum MAX_COMBINED_CLIP_AND_CULL_DISTANCES_WEBGL = 0x82FA; + + const GLenum CLIP_DISTANCE0_WEBGL = 0x3000; + const GLenum CLIP_DISTANCE1_WEBGL = 0x3001; + const GLenum CLIP_DISTANCE2_WEBGL = 0x3002; + const GLenum CLIP_DISTANCE3_WEBGL = 0x3003; + const GLenum CLIP_DISTANCE4_WEBGL = 0x3004; + const GLenum CLIP_DISTANCE5_WEBGL = 0x3005; + const GLenum CLIP_DISTANCE6_WEBGL = 0x3006; + const GLenum CLIP_DISTANCE7_WEBGL = 0x3007; +}; diff --git a/test/wpt/tests/interfaces/WEBGL_provoking_vertex.idl b/test/wpt/tests/interfaces/WEBGL_provoking_vertex.idl new file mode 100644 index 00000000000..035e1d233d6 --- /dev/null +++ b/test/wpt/tests/interfaces/WEBGL_provoking_vertex.idl @@ -0,0 +1,13 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: WebGL WEBGL_provoking_vertex Extension Specification (https://registry.khronos.org/webgl/extensions/WEBGL_provoking_vertex/) + +[Exposed=(Window,Worker), LegacyNoInterfaceObject] +interface WEBGL_provoking_vertex { + const GLenum FIRST_VERTEX_CONVENTION_WEBGL = 0x8E4D; + const GLenum LAST_VERTEX_CONVENTION_WEBGL = 0x8E4E; // default + const GLenum PROVOKING_VERTEX_WEBGL = 0x8E4F; + + undefined provokingVertexWEBGL(GLenum provokeMode); +}; diff --git a/test/wpt/tests/interfaces/anchors.idl b/test/wpt/tests/interfaces/anchors.idl index 5aa9395f09f..d8c5aa69a87 100644 --- a/test/wpt/tests/interfaces/anchors.idl +++ b/test/wpt/tests/interfaces/anchors.idl @@ -17,6 +17,8 @@ partial interface XRFrame { }; partial interface XRSession { + readonly attribute FrozenArray persistentAnchors; + Promise restorePersistentAnchor(DOMString uuid); Promise deletePersistentAnchor(DOMString uuid); }; diff --git a/test/wpt/tests/interfaces/badging.idl b/test/wpt/tests/interfaces/badging.idl index f34dfa7e04a..8b401e057ad 100644 --- a/test/wpt/tests/interfaces/badging.idl +++ b/test/wpt/tests/interfaces/badging.idl @@ -3,15 +3,11 @@ // (https://github.com/w3c/webref) // Source: Badging API (https://w3c.github.io/badging/) -[SecureContext] -partial interface Navigator { - Promise setClientBadge(optional [EnforceRange] unsigned long long contents); - Promise clearClientBadge(); -}; - [SecureContext] interface mixin NavigatorBadge { - Promise setAppBadge(optional [EnforceRange] unsigned long long contents); + Promise setAppBadge( + optional [EnforceRange] unsigned long long contents + ); Promise clearAppBadge(); }; diff --git a/test/wpt/tests/interfaces/compression.idl b/test/wpt/tests/interfaces/compression.idl index 88be302a4a6..7525d7c9847 100644 --- a/test/wpt/tests/interfaces/compression.idl +++ b/test/wpt/tests/interfaces/compression.idl @@ -3,14 +3,20 @@ // (https://github.com/w3c/webref) // Source: Compression Streams (https://wicg.github.io/compression/) +enum CompressionFormat { + "deflate", + "deflate-raw", + "gzip", +}; + [Exposed=*] interface CompressionStream { - constructor(DOMString format); + constructor(CompressionFormat format); }; CompressionStream includes GenericTransformStream; [Exposed=*] interface DecompressionStream { - constructor(DOMString format); + constructor(CompressionFormat format); }; DecompressionStream includes GenericTransformStream; diff --git a/test/wpt/tests/interfaces/compute-pressure.idl b/test/wpt/tests/interfaces/compute-pressure.idl index 42ff4f207ab..3e35dc4ee2d 100644 --- a/test/wpt/tests/interfaces/compute-pressure.idl +++ b/test/wpt/tests/interfaces/compute-pressure.idl @@ -3,17 +3,15 @@ // (https://github.com/w3c/webref) // Source: Compute Pressure Level 1 (https://w3c.github.io/compute-pressure/) -enum PressureState { "nominal", "fair", "serious", "critical" }; +enum PressureSource { "thermals", "cpu" }; -enum PressureFactor { "thermal", "power-supply" }; +enum PressureState { "nominal", "fair", "serious", "critical" }; callback PressureUpdateCallback = undefined ( sequence changes, PressureObserver observer ); -enum PressureSource { "cpu" }; - [Exposed=(DedicatedWorker,SharedWorker,Window), SecureContext] interface PressureObserver { constructor(PressureUpdateCallback callback, optional PressureObserverOptions options = {}); @@ -30,7 +28,6 @@ interface PressureObserver { interface PressureRecord { readonly attribute PressureSource source; readonly attribute PressureState state; - readonly attribute FrozenArray factors; readonly attribute DOMHighResTimeStamp time; [Default] object toJSON(); }; diff --git a/test/wpt/tests/interfaces/contact-picker.idl b/test/wpt/tests/interfaces/contact-picker.idl index aece81664e4..0119d0e2ce6 100644 --- a/test/wpt/tests/interfaces/contact-picker.idl +++ b/test/wpt/tests/interfaces/contact-picker.idl @@ -1,7 +1,7 @@ // GENERATED CONTENT - DO NOT EDIT // Content was automatically extracted by Reffy into webref // (https://github.com/w3c/webref) -// Source: Contact Picker API (https://w3c.github.io/contact-picker/spec/) +// Source: Contact Picker API (https://w3c.github.io/contact-picker/) [Exposed=Window] partial interface Navigator { diff --git a/test/wpt/tests/interfaces/cookie-store.idl b/test/wpt/tests/interfaces/cookie-store.idl index 72ef3f8c824..f44b4c63546 100644 --- a/test/wpt/tests/interfaces/cookie-store.idl +++ b/test/wpt/tests/interfaces/cookie-store.idl @@ -36,7 +36,7 @@ enum CookieSameSite { dictionary CookieInit { required USVString name; required USVString value; - EpochTimeStamp? expires = null; + DOMHighResTimeStamp? expires = null; USVString? domain = null; USVString path = "/"; CookieSameSite sameSite = "strict"; @@ -53,7 +53,7 @@ dictionary CookieListItem { USVString value; USVString? domain; USVString path; - EpochTimeStamp? expires; + DOMHighResTimeStamp? expires; boolean secure; CookieSameSite sameSite; }; diff --git a/test/wpt/tests/interfaces/css-cascade-6.idl b/test/wpt/tests/interfaces/css-cascade-6.idl new file mode 100644 index 00000000000..37cdfb82930 --- /dev/null +++ b/test/wpt/tests/interfaces/css-cascade-6.idl @@ -0,0 +1,10 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: CSS Cascading and Inheritance Level 6 (https://drafts.csswg.org/css-cascade-6/) + +[Exposed=Window] +interface CSSScopeRule : CSSGroupingRule { + readonly attribute CSSOMString start; + readonly attribute CSSOMString end; +}; diff --git a/test/wpt/tests/interfaces/css-contain.idl b/test/wpt/tests/interfaces/css-contain.idl index 6b29119617a..be2137a4a6a 100644 --- a/test/wpt/tests/interfaces/css-contain.idl +++ b/test/wpt/tests/interfaces/css-contain.idl @@ -4,10 +4,10 @@ // Source: CSS Containment Module Level 2 (https://drafts.csswg.org/css-contain-2/) [Exposed=Window] -interface ContentVisibilityAutoStateChangedEvent : Event { - constructor(DOMString type, optional ContentVisibilityAutoStateChangedEventInit eventInitDict = {}); +interface ContentVisibilityAutoStateChangeEvent : Event { + constructor(DOMString type, optional ContentVisibilityAutoStateChangeEventInit eventInitDict = {}); readonly attribute boolean skipped; }; -dictionary ContentVisibilityAutoStateChangedEventInit : EventInit { +dictionary ContentVisibilityAutoStateChangeEventInit : EventInit { boolean skipped = false; }; diff --git a/test/wpt/tests/interfaces/css-typed-om.idl b/test/wpt/tests/interfaces/css-typed-om.idl index 595a424e014..0df6a0327a2 100644 --- a/test/wpt/tests/interfaces/css-typed-om.idl +++ b/test/wpt/tests/interfaces/css-typed-om.idl @@ -13,9 +13,7 @@ interface CSSStyleValue { [Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)] interface StylePropertyMapReadOnly { iterable>; - any get(USVString property); - /* 'any' means (undefined or CSSStyleValue) here, - see https://github.com/heycam/webidl/issues/60 */ + (undefined or CSSStyleValue) get(USVString property); sequence getAll(USVString property); boolean has(USVString property); readonly attribute unsigned long size; diff --git a/test/wpt/tests/interfaces/css-view-transitions.idl b/test/wpt/tests/interfaces/css-view-transitions.idl index cf9cb8927e4..745eb1d9691 100644 --- a/test/wpt/tests/interfaces/css-view-transitions.idl +++ b/test/wpt/tests/interfaces/css-view-transitions.idl @@ -4,15 +4,15 @@ // Source: CSS View Transitions Module Level 1 (https://drafts.csswg.org/css-view-transitions-1/) partial interface Document { - ViewTransition startViewTransition(optional UpdateCallback? callback = null); + ViewTransition startViewTransition(optional UpdateCallback? updateCallback = null); }; callback UpdateCallback = Promise (); [Exposed=Window] interface ViewTransition { - undefined skipTransition(); - readonly attribute Promise finished; - readonly attribute Promise ready; readonly attribute Promise updateCallbackDone; + readonly attribute Promise ready; + readonly attribute Promise finished; + undefined skipTransition(); }; diff --git a/test/wpt/tests/interfaces/device-posture.idl b/test/wpt/tests/interfaces/device-posture.idl index ba8f9f51f02..0f1dded6313 100644 --- a/test/wpt/tests/interfaces/device-posture.idl +++ b/test/wpt/tests/interfaces/device-posture.idl @@ -16,6 +16,5 @@ interface DevicePosture : EventTarget { enum DevicePostureType { "continuous", - "folded", - "folded-over" + "folded" }; diff --git a/test/wpt/tests/interfaces/dom.idl b/test/wpt/tests/interfaces/dom.idl index 96acfc6a717..c5b5c94dbcc 100644 --- a/test/wpt/tests/interfaces/dom.idl +++ b/test/wpt/tests/interfaces/dom.idl @@ -618,7 +618,7 @@ callback interface XPathNSResolver { interface mixin XPathEvaluatorBase { [NewObject] XPathExpression createExpression(DOMString expression, optional XPathNSResolver? resolver = null); - XPathNSResolver createNSResolver(Node nodeResolver); + Node createNSResolver(Node nodeResolver); // legacy // XPathResult.ANY_TYPE = 0 XPathResult evaluate(DOMString expression, Node contextNode, optional XPathNSResolver? resolver = null, optional unsigned short type = 0, optional XPathResult? result = null); }; diff --git a/test/wpt/tests/interfaces/edit-context.idl b/test/wpt/tests/interfaces/edit-context.idl index f5e60bd0d02..91d8af2ea68 100644 --- a/test/wpt/tests/interfaces/edit-context.idl +++ b/test/wpt/tests/interfaces/edit-context.idl @@ -3,7 +3,7 @@ // (https://github.com/w3c/webref) // Source: EditContext API (https://w3c.github.io/edit-context/) -partial interface Element { +partial interface HTMLElement { attribute EditContext? editContext; }; @@ -20,8 +20,8 @@ interface EditContext : EventTarget { undefined updateText(unsigned long rangeStart, unsigned long rangeEnd, DOMString text); undefined updateSelection(unsigned long start, unsigned long end); - undefined updateControlBound(DOMRect controlBound); - undefined updateSelectionBound(DOMRect selectionBound); + undefined updateControlBounds(DOMRect controlBounds); + undefined updateSelectionBounds(DOMRect selectionBounds); undefined updateCharacterBounds(unsigned long rangeStart, sequence characterBounds); sequence attachedElements(); @@ -31,9 +31,9 @@ interface EditContext : EventTarget { readonly attribute unsigned long selectionEnd; readonly attribute unsigned long compositionRangeStart; readonly attribute unsigned long compositionRangeEnd; - readonly attribute boolean isInComposition; - readonly attribute DOMRect controlBound; - readonly attribute DOMRect selectionBound; + readonly attribute boolean isComposing; + readonly attribute DOMRect controlBounds; + readonly attribute DOMRect selectionBounds; readonly attribute unsigned long characterBoundsRangeStart; sequence characterBounds(); @@ -44,7 +44,7 @@ interface EditContext : EventTarget { attribute EventHandler oncompositionend; }; -dictionary TextUpdateEventInit { +dictionary TextUpdateEventInit : EventInit { unsigned long updateRangeStart; unsigned long updateRangeEnd; DOMString text; @@ -56,7 +56,7 @@ dictionary TextUpdateEventInit { [Exposed=Window] interface TextUpdateEvent : Event { - constructor(optional TextUpdateEventInit options = {}); + constructor(DOMString type, optional TextUpdateEventInit options = {}); readonly attribute unsigned long updateRangeStart; readonly attribute unsigned long updateRangeEnd; readonly attribute DOMString text; @@ -79,35 +79,33 @@ dictionary TextFormatInit { [Exposed=Window] interface TextFormat { constructor(optional TextFormatInit options = {}); - attribute unsigned long rangeStart; - attribute unsigned long rangeEnd; - attribute DOMString textColor; - attribute DOMString backgroundColor; - attribute DOMString underlineStyle; - attribute DOMString underlineThickness; - attribute DOMString underlineColor; + readonly attribute unsigned long rangeStart; + readonly attribute unsigned long rangeEnd; + readonly attribute DOMString textColor; + readonly attribute DOMString backgroundColor; + readonly attribute DOMString underlineStyle; + readonly attribute DOMString underlineThickness; + readonly attribute DOMString underlineColor; }; -dictionary TextFormatUpdateEventInit { +dictionary TextFormatUpdateEventInit : EventInit { sequence textFormats; }; [Exposed=Window] interface TextFormatUpdateEvent : Event { - constructor(optional TextFormatUpdateEventInit options = {}); - + constructor(DOMString type, optional TextFormatUpdateEventInit options = {}); sequence getTextFormats(); }; -dictionary CharacterBoundsUpdateEventInit { +dictionary CharacterBoundsUpdateEventInit : EventInit { unsigned long rangeStart; unsigned long rangeEnd; }; [Exposed=Window] interface CharacterBoundsUpdateEvent : Event { - constructor(optional CharacterBoundsUpdateEventInit options = {}); - + constructor(DOMString type, optional CharacterBoundsUpdateEventInit options = {}); readonly attribute unsigned long rangeStart; readonly attribute unsigned long rangeEnd; }; diff --git a/test/wpt/tests/interfaces/fenced-frame.idl b/test/wpt/tests/interfaces/fenced-frame.idl new file mode 100644 index 00000000000..2869b95e6bb --- /dev/null +++ b/test/wpt/tests/interfaces/fenced-frame.idl @@ -0,0 +1,54 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: Fenced frame (https://wicg.github.io/fenced-frame/) + +[Exposed=Window] +interface HTMLFencedFrameElement : HTMLElement { + [HTMLConstructor] constructor(); + + [CEReactions] attribute FencedFrameConfig? config; + [CEReactions] attribute DOMString width; + [CEReactions] attribute DOMString height; +}; + +enum OpaqueProperty {"opaque"}; + +typedef (unsigned long or OpaqueProperty) FencedFrameConfigSize; +typedef (USVString or OpaqueProperty) FencedFrameConfigURL; + +[Exposed=Window] +interface FencedFrameConfig { + constructor(USVString url); + readonly attribute FencedFrameConfigURL? url; + readonly attribute FencedFrameConfigSize? width; + readonly attribute FencedFrameConfigSize? height; +}; + +enum FenceReportingDestination { + "buyer", + "seller", + "component-seller", + "direct-seller", + "shared-storage-select-url", +}; + +dictionary FenceEvent { + required DOMString eventType; + required DOMString eventData; + required sequence destination; +}; + +typedef (FenceEvent or DOMString) ReportEventType; + +[Exposed=Window] +interface Fence { + undefined reportEvent(ReportEventType event); + undefined setReportEventDataForAutomaticBeacons(FenceEvent event); + sequence getNestedConfigs(); +}; + +partial interface Window { + // Collection of fenced frame APIs + readonly attribute Fence? fence; +}; diff --git a/test/wpt/tests/interfaces/fetch.idl b/test/wpt/tests/interfaces/fetch.idl index 866d09d31bd..81a5e69aee5 100644 --- a/test/wpt/tests/interfaces/fetch.idl +++ b/test/wpt/tests/interfaces/fetch.idl @@ -12,6 +12,7 @@ interface Headers { undefined append(ByteString name, ByteString value); undefined delete(ByteString name); ByteString? get(ByteString name); + sequence getSetCookie(); boolean has(ByteString name); undefined set(ByteString name, ByteString value); iterable; diff --git a/test/wpt/tests/interfaces/fs.idl b/test/wpt/tests/interfaces/fs.idl index e341ab387d9..e2474132abf 100644 --- a/test/wpt/tests/interfaces/fs.idl +++ b/test/wpt/tests/interfaces/fs.idl @@ -15,7 +15,6 @@ interface FileSystemHandle { Promise isSameEntry(FileSystemHandle other); }; - dictionary FileSystemCreateWritableOptions { boolean keepExistingData = false; }; @@ -27,7 +26,6 @@ interface FileSystemFileHandle : FileSystemHandle { [Exposed=DedicatedWorker] Promise createSyncAccessHandle(); }; - dictionary FileSystemGetFileOptions { boolean create = false; }; @@ -51,7 +49,6 @@ interface FileSystemDirectoryHandle : FileSystemHandle { Promise?> resolve(FileSystemHandle possibleDescendant); }; - enum WriteCommandType { "write", "seek", @@ -73,7 +70,6 @@ interface FileSystemWritableFileStream : WritableStream { Promise seek(unsigned long long position); Promise truncate(unsigned long long size); }; - dictionary FileSystemReadWriteOptions { [EnforceRange] unsigned long long at; }; diff --git a/test/wpt/tests/interfaces/gamepad-extensions.idl b/test/wpt/tests/interfaces/gamepad-extensions.idl index ddfc0d9c06a..d7d750654fd 100644 --- a/test/wpt/tests/interfaces/gamepad-extensions.idl +++ b/test/wpt/tests/interfaces/gamepad-extensions.idl @@ -67,5 +67,5 @@ partial interface Gamepad { readonly attribute FrozenArray hapticActuators; readonly attribute GamepadPose? pose; readonly attribute FrozenArray? touchEvents; - [SameObject] readonly attribute GamepadHapticActuator vibrationActuator; + [SameObject] readonly attribute GamepadHapticActuator? vibrationActuator; }; diff --git a/test/wpt/tests/interfaces/gpc-spec.idl b/test/wpt/tests/interfaces/gpc-spec.idl new file mode 100644 index 00000000000..0e9a063c54c --- /dev/null +++ b/test/wpt/tests/interfaces/gpc-spec.idl @@ -0,0 +1,10 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: Global Privacy Control (GPC) (https://privacycg.github.io/gpc-spec/) + +interface mixin GlobalPrivacyControl { + readonly attribute boolean globalPrivacyControl; +}; +Navigator includes GlobalPrivacyControl; +WorkerNavigator includes GlobalPrivacyControl; diff --git a/test/wpt/tests/interfaces/html.idl b/test/wpt/tests/interfaces/html.idl index e4752f079ce..33d4de0db97 100644 --- a/test/wpt/tests/interfaces/html.idl +++ b/test/wpt/tests/interfaces/html.idl @@ -127,6 +127,12 @@ interface HTMLElement : Element { [CEReactions] attribute [LegacyNullToEmptyString] DOMString outerText; ElementInternals attachInternals(); + + // The popover API + undefined showPopover(); + undefined hidePopover(); + undefined togglePopover(optional boolean force); + [CEReactions] attribute DOMString? popover; }; HTMLElement includes GlobalEventHandlers; @@ -202,6 +208,7 @@ interface HTMLLinkElement : HTMLElement { [CEReactions] attribute DOMString referrerPolicy; [SameObject, PutForwards=value] readonly attribute DOMTokenList blocking; [CEReactions] attribute boolean disabled; + [CEReactions] attribute DOMString fetchPriority; // also has obsolete members }; @@ -430,6 +437,7 @@ interface HTMLImageElement : HTMLElement { [CEReactions] attribute DOMString referrerPolicy; [CEReactions] attribute DOMString decoding; [CEReactions] attribute DOMString loading; + [CEReactions] attribute DOMString fetchPriority; Promise decode(); @@ -929,6 +937,7 @@ interface HTMLInputElement : HTMLElement { // also has obsolete members }; +HTMLInputElement includes PopoverInvokerElement; [Exposed=Window] interface HTMLButtonElement : HTMLElement { @@ -954,6 +963,7 @@ interface HTMLButtonElement : HTMLElement { readonly attribute NodeList labels; }; +HTMLButtonElement includes PopoverInvokerElement; [Exposed=Window] interface HTMLSelectElement : HTMLElement { @@ -1214,6 +1224,7 @@ interface HTMLScriptElement : HTMLElement { [CEReactions] attribute DOMString integrity; [CEReactions] attribute DOMString referrerPolicy; [SameObject, PutForwards=value] readonly attribute DOMTokenList blocking; + [CEReactions] attribute DOMString fetchPriority; static boolean supports(DOMString type); @@ -1704,6 +1715,23 @@ dictionary DragEventInit : MouseEventInit { DataTransfer? dataTransfer = null; }; +interface mixin PopoverInvokerElement { + [CEReactions] attribute Element? popoverTargetElement; + [CEReactions] attribute DOMString popoverTargetAction; +}; + +[Exposed=Window] +interface ToggleEvent : Event { + constructor(DOMString type, optional ToggleEventInit eventInitDict = {}); + readonly attribute DOMString oldState; + readonly attribute DOMString newState; +}; + +dictionary ToggleEventInit : EventInit { + DOMString oldState = ""; + DOMString newState = ""; +}; + [Global=Window, Exposed=Window, LegacyUnenumerableNamedProperties] @@ -1737,10 +1765,11 @@ interface Window : EventTarget { [Replaceable] readonly attribute WindowProxy? parent; readonly attribute Element? frameElement; WindowProxy? open(optional USVString url = "", optional DOMString target = "_blank", optional [LegacyNullToEmptyString] DOMString features = ""); - getter object (DOMString name); + // Since this is the global object, the IDL named getter adds a NamedPropertiesObject exotic // object on the prototype chain. Indeed, this does not make the global object an exotic object. // Indexed access is taken care of by the WindowProxy exotic object. + getter object (DOMString name); // the user agent readonly attribute Navigator navigator; @@ -1893,6 +1922,7 @@ interface mixin GlobalEventHandlers { attribute EventHandler onauxclick; attribute EventHandler onbeforeinput; attribute EventHandler onbeforematch; + attribute EventHandler onbeforetoggle; attribute EventHandler onblur; attribute EventHandler oncancel; attribute EventHandler oncanplay; diff --git a/test/wpt/tests/interfaces/mediacapture-streams.idl b/test/wpt/tests/interfaces/mediacapture-streams.idl index 5b41f3fa653..3197ff73689 100644 --- a/test/wpt/tests/interfaces/mediacapture-streams.idl +++ b/test/wpt/tests/interfaces/mediacapture-streams.idl @@ -104,19 +104,19 @@ dictionary MediaTrackConstraintSet { }; dictionary MediaTrackSettings { - long width; - long height; + unsigned long width; + unsigned long height; double aspectRatio; double frameRate; DOMString facingMode; DOMString resizeMode; - long sampleRate; - long sampleSize; + unsigned long sampleRate; + unsigned long sampleSize; boolean echoCancellation; boolean autoGainControl; boolean noiseSuppression; double latency; - long channelCount; + unsigned long channelCount; DOMString deviceId; DOMString groupId; }; diff --git a/test/wpt/tests/interfaces/navigation-api.idl b/test/wpt/tests/interfaces/navigation-api.idl deleted file mode 100644 index 11178518292..00000000000 --- a/test/wpt/tests/interfaces/navigation-api.idl +++ /dev/null @@ -1,158 +0,0 @@ -// GENERATED CONTENT - DO NOT EDIT -// Content was automatically extracted by Reffy into webref -// (https://github.com/w3c/webref) -// Source: Navigation API (https://wicg.github.io/navigation-api/) - -partial interface Window { - [Replaceable] readonly attribute Navigation navigation; -}; - -[Exposed=Window] -interface Navigation : EventTarget { - sequence entries(); - readonly attribute NavigationHistoryEntry? currentEntry; - undefined updateCurrentEntry(NavigationUpdateCurrentEntryOptions options); - readonly attribute NavigationTransition? transition; - - readonly attribute boolean canGoBack; - readonly attribute boolean canGoForward; - - NavigationResult navigate(USVString url, optional NavigationNavigateOptions options = {}); - NavigationResult reload(optional NavigationReloadOptions options = {}); - - NavigationResult traverseTo(DOMString key, optional NavigationOptions options = {}); - NavigationResult back(optional NavigationOptions options = {}); - NavigationResult forward(optional NavigationOptions options = {}); - - attribute EventHandler onnavigate; - attribute EventHandler onnavigatesuccess; - attribute EventHandler onnavigateerror; - attribute EventHandler oncurrententrychange; -}; - -dictionary NavigationUpdateCurrentEntryOptions { - required any state; -}; - -dictionary NavigationOptions { - any info; -}; - -dictionary NavigationNavigateOptions : NavigationOptions { - any state; - NavigationHistoryBehavior history = "auto"; -}; - -dictionary NavigationReloadOptions : NavigationOptions { - any state; -}; - -dictionary NavigationResult { - Promise committed; - Promise finished; -}; - -enum NavigationHistoryBehavior { - "auto", - "push", - "replace" -}; - -[Exposed=Window] -interface NavigationCurrentEntryChangeEvent : Event { - constructor(DOMString type, NavigationCurrentEntryChangeEventInit eventInit); - - readonly attribute NavigationType? navigationType; - readonly attribute NavigationHistoryEntry from; -}; - -dictionary NavigationCurrentEntryChangeEventInit : EventInit { - NavigationType? navigationType = null; - required NavigationHistoryEntry destination; -}; - -[Exposed=Window] -interface NavigationTransition { - readonly attribute NavigationType navigationType; - readonly attribute NavigationHistoryEntry from; - readonly attribute Promise finished; -}; - -[Exposed=Window] -interface NavigateEvent : Event { - constructor(DOMString type, NavigateEventInit eventInit); - - readonly attribute NavigationType navigationType; - readonly attribute NavigationDestination destination; - readonly attribute boolean canIntercept; - readonly attribute boolean userInitiated; - readonly attribute boolean hashChange; - readonly attribute AbortSignal signal; - readonly attribute FormData? formData; - readonly attribute DOMString? downloadRequest; - readonly attribute any info; - - undefined intercept(optional NavigationInterceptOptions options = {}); - undefined scroll(); -}; - -dictionary NavigateEventInit : EventInit { - NavigationType navigationType = "push"; - required NavigationDestination destination; - boolean canIntercept = false; - boolean userInitiated = false; - boolean hashChange = false; - required AbortSignal signal; - FormData? formData = null; - DOMString? downloadRequest = null; - any info; -}; - -dictionary NavigationInterceptOptions { - NavigationInterceptHandler handler; - NavigationFocusReset focusReset; - NavigationScrollBehavior scroll; -}; - -enum NavigationFocusReset { - "after-transition", - "manual" -}; - -enum NavigationScrollBehavior { - "after-transition", - "manual" -}; - -callback NavigationInterceptHandler = Promise (); - -enum NavigationType { - "reload", - "push", - "replace", - "traverse" -}; - -[Exposed=Window] -interface NavigationDestination { - readonly attribute USVString url; - readonly attribute DOMString? key; - readonly attribute DOMString? id; - readonly attribute long long index; - readonly attribute boolean sameDocument; - - any getState(); -}; - -[Exposed=Window] -interface NavigationHistoryEntry : EventTarget { - readonly attribute USVString? url; - readonly attribute DOMString key; - readonly attribute DOMString id; - readonly attribute long long index; - readonly attribute boolean sameDocument; - - any getState(); - - attribute EventHandler ondispose; -}; diff --git a/test/wpt/tests/interfaces/notifications.idl b/test/wpt/tests/interfaces/notifications.idl index 2be22213d78..bfcfa2e66af 100644 --- a/test/wpt/tests/interfaces/notifications.idl +++ b/test/wpt/tests/interfaces/notifications.idl @@ -72,7 +72,6 @@ dictionary NotificationAction { }; callback NotificationPermissionCallback = undefined (NotificationPermission permission); - dictionary GetNotificationOptions { DOMString tag = ""; }; diff --git a/test/wpt/tests/interfaces/orientation-event.idl b/test/wpt/tests/interfaces/orientation-event.idl index f6ff96f0e01..a93d4658830 100644 --- a/test/wpt/tests/interfaces/orientation-event.idl +++ b/test/wpt/tests/interfaces/orientation-event.idl @@ -29,10 +29,6 @@ partial interface Window { [SecureContext] attribute EventHandler ondeviceorientationabsolute; }; -partial interface Window { - attribute EventHandler oncompassneedscalibration; -}; - partial interface Window { [SecureContext] attribute EventHandler ondevicemotion; }; diff --git a/test/wpt/tests/interfaces/permissions-policy.idl b/test/wpt/tests/interfaces/permissions-policy.idl index 16945e3a9b7..a789d41738c 100644 --- a/test/wpt/tests/interfaces/permissions-policy.idl +++ b/test/wpt/tests/interfaces/permissions-policy.idl @@ -18,7 +18,6 @@ partial interface Document { partial interface HTMLIFrameElement { [SameObject] readonly attribute PermissionsPolicy permissionsPolicy; }; - [Exposed=Window] interface PermissionsPolicyViolationReportBody : ReportBody { readonly attribute DOMString featureId; diff --git a/test/wpt/tests/interfaces/priority-hints.idl b/test/wpt/tests/interfaces/priority-hints.idl deleted file mode 100644 index cbff6a5222d..00000000000 --- a/test/wpt/tests/interfaces/priority-hints.idl +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATED CONTENT - DO NOT EDIT -// Content was automatically extracted by Reffy into webref -// (https://github.com/w3c/webref) -// Source: Priority Hints (https://wicg.github.io/priority-hints/) - -partial interface HTMLImageElement { - [CEReactions] attribute DOMString fetchPriority; -}; - -partial interface HTMLLinkElement { - [CEReactions] attribute DOMString fetchPriority; -}; - -partial interface HTMLScriptElement { - [CEReactions] attribute DOMString fetchPriority; -}; - -partial interface HTMLIFrameElement { - [CEReactions] attribute DOMString fetchPriority; -}; diff --git a/test/wpt/tests/interfaces/requestStorageAccessFor.idl b/test/wpt/tests/interfaces/requestStorageAccessFor.idl new file mode 100644 index 00000000000..adca77a2b03 --- /dev/null +++ b/test/wpt/tests/interfaces/requestStorageAccessFor.idl @@ -0,0 +1,12 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: requestStorageAccessFor API (https://privacycg.github.io/requestStorageAccessFor/) + +partial interface Document { + Promise requestStorageAccessFor(USVString requestedOrigin); +}; + +dictionary TopLevelStorageAccessPermissionDescriptor : PermissionDescriptor { + USVString requestedOrigin = ""; +}; diff --git a/test/wpt/tests/interfaces/resource-timing.idl b/test/wpt/tests/interfaces/resource-timing.idl index 242df0bd804..151e5d46d84 100644 --- a/test/wpt/tests/interfaces/resource-timing.idl +++ b/test/wpt/tests/interfaces/resource-timing.idl @@ -6,6 +6,7 @@ [Exposed=(Window,Worker)] interface PerformanceResourceTiming : PerformanceEntry { readonly attribute DOMString initiatorType; + readonly attribute DOMString deliveryType; readonly attribute ByteString nextHopProtocol; readonly attribute DOMHighResTimeStamp workerStart; readonly attribute DOMHighResTimeStamp redirectStart; diff --git a/test/wpt/tests/interfaces/scroll-animations.idl b/test/wpt/tests/interfaces/scroll-animations.idl index 1fd52dece19..14215509c9f 100644 --- a/test/wpt/tests/interfaces/scroll-animations.idl +++ b/test/wpt/tests/interfaces/scroll-animations.idl @@ -25,6 +25,7 @@ interface ScrollTimeline : AnimationTimeline { dictionary ViewTimelineOptions { Element subject; ScrollAxis axis = "block"; + (DOMString or sequence<(CSSNumericValue or CSSKeywordValue)>) inset = "auto"; }; [Exposed=Window] diff --git a/test/wpt/tests/interfaces/scroll-to-text-fragment.idl b/test/wpt/tests/interfaces/scroll-to-text-fragment.idl index ce571ddd694..be7bf73968e 100644 --- a/test/wpt/tests/interfaces/scroll-to-text-fragment.idl +++ b/test/wpt/tests/interfaces/scroll-to-text-fragment.idl @@ -1,7 +1,7 @@ // GENERATED CONTENT - DO NOT EDIT // Content was automatically extracted by Reffy into webref // (https://github.com/w3c/webref) -// Source: Text Fragments (https://wicg.github.io/scroll-to-text-fragment/) +// Source: URL Fragment Text Directives (https://wicg.github.io/scroll-to-text-fragment/) [Exposed=Window] interface FragmentDirective { diff --git a/test/wpt/tests/interfaces/secure-payment-confirmation.idl b/test/wpt/tests/interfaces/secure-payment-confirmation.idl index 6b6083b165e..9061b243477 100644 --- a/test/wpt/tests/interfaces/secure-payment-confirmation.idl +++ b/test/wpt/tests/interfaces/secure-payment-confirmation.idl @@ -15,7 +15,6 @@ dictionary SecurePaymentConfirmationRequest { sequence locale; boolean showOptOut; }; - partial dictionary AuthenticationExtensionsClientInputs { AuthenticationExtensionsPaymentInputs payment; }; @@ -31,11 +30,9 @@ dictionary AuthenticationExtensionsPaymentInputs { PaymentCurrencyAmount total; PaymentCredentialInstrument instrument; }; - dictionary CollectedClientPaymentData : CollectedClientData { required CollectedClientAdditionalPaymentData payment; }; - dictionary CollectedClientAdditionalPaymentData { required USVString rpId; required USVString topOrigin; @@ -44,16 +41,8 @@ dictionary CollectedClientAdditionalPaymentData { required PaymentCurrencyAmount total; required PaymentCredentialInstrument instrument; }; - dictionary PaymentCredentialInstrument { required USVString displayName; required USVString icon; boolean iconMustBeShown = true; }; - -enum TransactionAutomationMode { - "none", - "autoAccept", - "autoReject", - "autoOptOut" -}; diff --git a/test/wpt/tests/interfaces/selection-api.idl b/test/wpt/tests/interfaces/selection-api.idl index c1eed2a9cf2..a84536ab0f5 100644 --- a/test/wpt/tests/interfaces/selection-api.idl +++ b/test/wpt/tests/interfaces/selection-api.idl @@ -12,12 +12,13 @@ interface Selection { readonly attribute boolean isCollapsed; readonly attribute unsigned long rangeCount; readonly attribute DOMString type; + readonly attribute DOMString direction; Range getRangeAt(unsigned long index); undefined addRange(Range range); undefined removeRange(Range range); undefined removeAllRanges(); undefined empty(); - StaticRange getComposedRange(ShadowRoot... shadowRoots); + sequence getComposedRanges(ShadowRoot... shadowRoots); undefined collapse(Node? node, optional unsigned long offset = 0); undefined setPosition(Node? node, optional unsigned long offset = 0); undefined collapseToStart(); diff --git a/test/wpt/tests/interfaces/trust-token-api.idl b/test/wpt/tests/interfaces/trust-token-api.idl new file mode 100644 index 00000000000..ee339590827 --- /dev/null +++ b/test/wpt/tests/interfaces/trust-token-api.idl @@ -0,0 +1,29 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: Private State Token API (https://wicg.github.io/trust-token-api/) + +enum RefreshPolicy { "none", "refresh" }; + +enum TokenType { "private-state-token" }; + +enum TokenVersion { "1" }; + +enum OperationType { "token-request", "send-redemption-record", "token-redemption" }; + +dictionary PrivateToken { + required TokenType type; + required TokenVersion version; + required OperationType operation; + RefreshPolicy refreshPolicy = "none"; + sequence issuers; +}; + +partial dictionary RequestInit { + PrivateToken privateToken; +}; + +partial interface Document { + Promise hasPrivateTokens(USVString issuer, USVString type); + Promise hasRedemptionRecord(USVString issuer, USVString type); +}; diff --git a/test/wpt/tests/interfaces/turtledove.idl b/test/wpt/tests/interfaces/turtledove.idl new file mode 100644 index 00000000000..cd81a3d87ef --- /dev/null +++ b/test/wpt/tests/interfaces/turtledove.idl @@ -0,0 +1,109 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: FLEDGE (https://wicg.github.io/turtledove/) + +[SecureContext] +partial interface Navigator { + Promise joinAdInterestGroup(AuctionAdInterestGroup group, double durationSeconds); +}; + +dictionary AuctionAd { + required USVString renderURL; + any metadata; +}; + +dictionary AuctionAdInterestGroup { + required USVString owner; + required USVString name; + + double priority = 0.0; + boolean enableBiddingSignalsPrioritization = false; + record priorityVector; + record prioritySignalsOverrides; + + DOMString executionMode = "compatibility"; + USVString biddingLogicURL; + USVString biddingWasmHelperURL; + USVString dailyUpdateURL; + USVString trustedBiddingSignalsURL; + sequence trustedBiddingSignalsKeys; + any userBiddingSignals; + sequence ads; + sequence adComponents; +}; + +[SecureContext] +partial interface Navigator { + Promise leaveAdInterestGroup(AuctionAdInterestGroupKey group); +}; + +dictionary AuctionAdInterestGroupKey { + required USVString owner; + required USVString name; +}; + +[SecureContext] +partial interface Navigator { + Promise runAdAuction(AuctionAdConfig config); +}; + +dictionary AuctionAdConfig { + required USVString seller; + required USVString decisionLogicURL; + USVString trustedScoringSignalsURL; + sequence interestGroupBuyers; + any auctionSignals; + any sellerSignals; + USVString directFromSellerSignals; + unsigned long long sellerTimeout; + unsigned short sellerExperimentGroupId; + record perBuyerSignals; + record perBuyerTimeouts; + record perBuyerGroupLimits; + record perBuyerExperimentGroupIds; + record> perBuyerPrioritySignals; + sequence componentAuctions = []; + AbortSignal? signal; +}; + +[Exposed=InterestGroupBiddingScriptRunnerGlobalScope, +Global=InterestGroupBiddingScriptRunnerGlobalScope] +interface InterestGroupBiddingScriptRunnerGlobalScope { + boolean setBid(); + boolean setBid(GenerateBidOutput generateBidOutput); + undefined setPriority(double priority); + undefined setPrioritySignalsOverride(DOMString key, double priority); +}; + +[Exposed=InterestGroupScoringScriptRunnerGlobalScope, +Global=InterestGroupScoringScriptRunnerGlobalScope] +interface InterestGroupScoringScriptRunnerGlobalScope { +}; + +[Exposed=InterestGroupReportingScriptRunnerGlobalScope, +Global=InterestGroupReportingScriptRunnerGlobalScope] +interface InterestGroupReportingScriptRunnerGlobalScope { + undefined sendReportTo(DOMString url); +}; + +dictionary AdRender { + required DOMString url; + required DOMString width; + required DOMString height; +}; + +dictionary GenerateBidOutput { + required double bid; + required (DOMString or AdRender) adRender; + DOMString ad; + sequence<(DOMString or AdRender)> adComponents; + double adCost; + double modelingSignals; + boolean allowComponentAuction = false; +}; + +[SecureContext] +partial interface Navigator { + undefined updateAdInterestGroups(); +}; diff --git a/test/wpt/tests/interfaces/ua-client-hints.idl b/test/wpt/tests/interfaces/ua-client-hints.idl index f5070a0e090..c69714b9734 100644 --- a/test/wpt/tests/interfaces/ua-client-hints.idl +++ b/test/wpt/tests/interfaces/ua-client-hints.idl @@ -9,16 +9,17 @@ dictionary NavigatorUABrandVersion { }; dictionary UADataValues { - sequence brands; - boolean mobile; DOMString architecture; DOMString bitness; + sequence brands; + DOMString formFactor; + sequence fullVersionList; DOMString model; + boolean mobile; DOMString platform; DOMString platformVersion; DOMString uaFullVersion; // deprecated in favor of fullVersionList boolean wow64; - sequence fullVersionList; }; dictionary UALowEntropyJSON { diff --git a/test/wpt/tests/interfaces/url.idl b/test/wpt/tests/interfaces/url.idl index 360c9adcfa1..6549e45f419 100644 --- a/test/wpt/tests/interfaces/url.idl +++ b/test/wpt/tests/interfaces/url.idl @@ -8,6 +8,8 @@ interface URL { constructor(USVString url, optional USVString base); + static boolean canParse(USVString url, optional USVString base); + stringifier attribute USVString href; readonly attribute USVString origin; attribute USVString protocol; @@ -28,6 +30,8 @@ interface URL { interface URLSearchParams { constructor(optional (sequence> or record or USVString) init = ""); + readonly attribute unsigned long size; + undefined append(USVString name, USVString value); undefined delete(USVString name); USVString? get(USVString name); diff --git a/test/wpt/tests/interfaces/wasm-js-api.idl b/test/wpt/tests/interfaces/wasm-js-api.idl index 141af90fd2d..0d4384251df 100644 --- a/test/wpt/tests/interfaces/wasm-js-api.idl +++ b/test/wpt/tests/interfaces/wasm-js-api.idl @@ -8,7 +8,7 @@ dictionary WebAssemblyInstantiatedSource { required Instance instance; }; -[Exposed=(Window,Worker,Worklet)] +[Exposed=*] namespace WebAssembly { boolean validate(BufferSource bytes); Promise compile(BufferSource bytes); @@ -39,7 +39,7 @@ dictionary ModuleImportDescriptor { required ImportExportKind kind; }; -[LegacyNamespace=WebAssembly, Exposed=(Window,Worker,Worklet)] +[LegacyNamespace=WebAssembly, Exposed=*] interface Module { constructor(BufferSource bytes); static sequence exports(Module moduleObject); @@ -47,7 +47,7 @@ interface Module { static sequence customSections(Module moduleObject, DOMString sectionName); }; -[LegacyNamespace=WebAssembly, Exposed=(Window,Worker,Worklet)] +[LegacyNamespace=WebAssembly, Exposed=*] interface Instance { constructor(Module module, optional object importObject); readonly attribute object exports; @@ -58,7 +58,7 @@ dictionary MemoryDescriptor { [EnforceRange] unsigned long maximum; }; -[LegacyNamespace=WebAssembly, Exposed=(Window,Worker,Worklet)] +[LegacyNamespace=WebAssembly, Exposed=*] interface Memory { constructor(MemoryDescriptor descriptor); unsigned long grow([EnforceRange] unsigned long delta); @@ -78,7 +78,7 @@ dictionary TableDescriptor { [EnforceRange] unsigned long maximum; }; -[LegacyNamespace=WebAssembly, Exposed=(Window,Worker,Worklet)] +[LegacyNamespace=WebAssembly, Exposed=*] interface Table { constructor(TableDescriptor descriptor, optional any value); unsigned long grow([EnforceRange] unsigned long delta, optional any value); @@ -102,7 +102,7 @@ dictionary GlobalDescriptor { boolean mutable = false; }; -[LegacyNamespace=WebAssembly, Exposed=(Window,Worker,Worklet)] +[LegacyNamespace=WebAssembly, Exposed=*] interface Global { constructor(GlobalDescriptor descriptor, optional any v); any valueOf(); diff --git a/test/wpt/tests/interfaces/web-animations-2.idl b/test/wpt/tests/interfaces/web-animations-2.idl index 61df2cd1518..f9f68a0d49a 100644 --- a/test/wpt/tests/interfaces/web-animations-2.idl +++ b/test/wpt/tests/interfaces/web-animations-2.idl @@ -5,6 +5,7 @@ [Exposed=Window] partial interface AnimationTimeline { + readonly attribute CSSNumberish? currentTime; readonly attribute CSSNumberish? duration; Animation play (optional AnimationEffect? effect = null); }; @@ -88,6 +89,16 @@ callback EffectCallback = undefined (double? progress, (Element or CSSPseudoElement) currentTarget, Animation animation); +dictionary TimelineRangeOffset { + CSSOMString? rangeName; + CSSNumericValue offset; +}; + +partial dictionary KeyframeAnimationOptions { + (TimelineRangeOffset or CSSNumericValue or CSSKeywordValue or DOMString) rangeStart = "normal"; + (TimelineRangeOffset or CSSNumericValue or CSSKeywordValue or DOMString) rangeEnd = "normal"; +}; + [Exposed=Window] interface AnimationPlaybackEvent : Event { constructor(DOMString type, optional AnimationPlaybackEventInit diff --git a/test/wpt/tests/interfaces/web-animations.idl b/test/wpt/tests/interfaces/web-animations.idl index ed612302a47..956d700f49b 100644 --- a/test/wpt/tests/interfaces/web-animations.idl +++ b/test/wpt/tests/interfaces/web-animations.idl @@ -5,7 +5,6 @@ [Exposed=Window] interface AnimationTimeline { - readonly attribute double? currentTime; }; dictionary DocumentTimelineOptions { diff --git a/test/wpt/tests/interfaces/web-bluetooth.idl b/test/wpt/tests/interfaces/web-bluetooth.idl index 21c237040eb..5d461194a83 100644 --- a/test/wpt/tests/interfaces/web-bluetooth.idl +++ b/test/wpt/tests/interfaces/web-bluetooth.idl @@ -26,6 +26,7 @@ dictionary BluetoothLEScanFilterInit { dictionary RequestDeviceOptions { sequence filters; + sequence exclusionFilters; sequence optionalServices = []; sequence optionalManufacturerData = []; boolean acceptAllDevices = false; diff --git a/test/wpt/tests/interfaces/webauthn.idl b/test/wpt/tests/interfaces/webauthn.idl index ef27322312f..58a9e285232 100644 --- a/test/wpt/tests/interfaces/webauthn.idl +++ b/test/wpt/tests/interfaces/webauthn.idl @@ -12,7 +12,6 @@ interface PublicKeyCredential : Credential { static Promise isConditionalMediationAvailable(); PublicKeyCredentialJSON toJSON(); }; - typedef DOMString Base64URLString; typedef (RegistrationResponseJSON or AuthenticationResponseJSON) PublicKeyCredentialJSON; @@ -49,19 +48,15 @@ dictionary AuthenticatorAssertionResponseJSON { dictionary AuthenticationExtensionsClientOutputsJSON { }; - partial dictionary CredentialCreationOptions { PublicKeyCredentialCreationOptions publicKey; }; - partial dictionary CredentialRequestOptions { PublicKeyCredentialRequestOptions publicKey; }; - partial interface PublicKeyCredential { static Promise isUserVerifyingPlatformAuthenticatorAvailable(); }; - partial interface PublicKeyCredential { static PublicKeyCredentialCreationOptions parseCreationOptionsFromJSON(PublicKeyCredentialCreationOptionsJSON options); }; @@ -92,7 +87,6 @@ dictionary PublicKeyCredentialDescriptorJSON { dictionary AuthenticationExtensionsClientInputsJSON { }; - partial interface PublicKeyCredential { static PublicKeyCredentialRequestOptions parseRequestOptionsFromJSON(PublicKeyCredentialRequestOptionsJSON options); }; @@ -105,12 +99,10 @@ dictionary PublicKeyCredentialRequestOptionsJSON { DOMString userVerification = "preferred"; AuthenticationExtensionsClientInputsJSON extensions; }; - [SecureContext, Exposed=Window] interface AuthenticatorResponse { [SameObject] readonly attribute ArrayBuffer clientDataJSON; }; - [SecureContext, Exposed=Window] interface AuthenticatorAttestationResponse : AuthenticatorResponse { [SameObject] readonly attribute ArrayBuffer attestationObject; @@ -119,7 +111,6 @@ interface AuthenticatorAttestationResponse : AuthenticatorResponse { ArrayBuffer? getPublicKey(); COSEAlgorithmIdentifier getPublicKeyAlgorithm(); }; - [SecureContext, Exposed=Window] interface AuthenticatorAssertionResponse : AuthenticatorResponse { [SameObject] readonly attribute ArrayBuffer authenticatorData; @@ -127,12 +118,10 @@ interface AuthenticatorAssertionResponse : AuthenticatorResponse { [SameObject] readonly attribute ArrayBuffer? userHandle; [SameObject] readonly attribute ArrayBuffer? attestationObject; }; - dictionary PublicKeyCredentialParameters { required DOMString type; required COSEAlgorithmIdentifier alg; }; - dictionary PublicKeyCredentialCreationOptions { required PublicKeyCredentialRpEntity rp; required PublicKeyCredentialUserEntity user; @@ -147,45 +136,37 @@ dictionary PublicKeyCredentialCreationOptions { sequence attestationFormats = []; AuthenticationExtensionsClientInputs extensions; }; - dictionary PublicKeyCredentialEntity { required DOMString name; }; - dictionary PublicKeyCredentialRpEntity : PublicKeyCredentialEntity { DOMString id; }; - dictionary PublicKeyCredentialUserEntity : PublicKeyCredentialEntity { required BufferSource id; required DOMString displayName; }; - dictionary AuthenticatorSelectionCriteria { DOMString authenticatorAttachment; DOMString residentKey; boolean requireResidentKey = false; DOMString userVerification = "preferred"; }; - enum AuthenticatorAttachment { "platform", "cross-platform" }; - enum ResidentKeyRequirement { "discouraged", "preferred", "required" }; - enum AttestationConveyancePreference { "none", "indirect", "direct", "enterprise" }; - dictionary PublicKeyCredentialRequestOptions { required BufferSource challenge; unsigned long timeout; @@ -196,17 +177,15 @@ dictionary PublicKeyCredentialRequestOptions { sequence attestationFormats = []; AuthenticationExtensionsClientInputs extensions; }; - dictionary AuthenticationExtensionsClientInputs { }; - dictionary AuthenticationExtensionsClientOutputs { }; - dictionary CollectedClientData { required DOMString type; required DOMString challenge; required DOMString origin; + DOMString topOrigin; boolean crossOrigin; }; @@ -216,17 +195,14 @@ dictionary TokenBinding { }; enum TokenBindingStatus { "present", "supported" }; - enum PublicKeyCredentialType { "public-key" }; - dictionary PublicKeyCredentialDescriptor { required DOMString type; required BufferSource id; sequence transports; }; - enum AuthenticatorTransport { "usb", "nfc", @@ -234,35 +210,27 @@ enum AuthenticatorTransport { "hybrid", "internal" }; - typedef long COSEAlgorithmIdentifier; - enum UserVerificationRequirement { "required", "preferred", "discouraged" }; - partial dictionary AuthenticationExtensionsClientInputs { USVString appid; }; - partial dictionary AuthenticationExtensionsClientOutputs { boolean appid; }; - partial dictionary AuthenticationExtensionsClientInputs { USVString appidExclude; }; - partial dictionary AuthenticationExtensionsClientOutputs { boolean appidExclude; }; - partial dictionary AuthenticationExtensionsClientInputs { boolean credProps; }; - dictionary CredentialPropertiesOutput { boolean rk; }; @@ -270,10 +238,9 @@ dictionary CredentialPropertiesOutput { partial dictionary AuthenticationExtensionsClientOutputs { CredentialPropertiesOutput credProps; }; - dictionary AuthenticationExtensionsPRFValues { - required ArrayBuffer first; - ArrayBuffer second; + required BufferSource first; + BufferSource second; }; dictionary AuthenticationExtensionsPRFInputs { @@ -322,14 +289,12 @@ dictionary AuthenticationExtensionsLargeBlobOutputs { partial dictionary AuthenticationExtensionsClientInputs { boolean uvm; }; - typedef sequence UvmEntry; typedef sequence UvmEntries; partial dictionary AuthenticationExtensionsClientOutputs { UvmEntries uvm; }; - dictionary AuthenticationExtensionsDevicePublicKeyInputs { DOMString attestation = "none"; sequence attestationFormats = []; @@ -340,7 +305,6 @@ partial dictionary AuthenticationExtensionsClientInputs { }; dictionary AuthenticationExtensionsDevicePublicKeyOutputs { - ArrayBuffer authenticatorOutput; ArrayBuffer signature; }; diff --git a/test/wpt/tests/interfaces/webcodecs-av1-codec-registration.idl b/test/wpt/tests/interfaces/webcodecs-av1-codec-registration.idl new file mode 100644 index 00000000000..00e4493d3c0 --- /dev/null +++ b/test/wpt/tests/interfaces/webcodecs-av1-codec-registration.idl @@ -0,0 +1,12 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: AV1 WebCodecs Registration (https://w3c.github.io/webcodecs/av1_codec_registration.html) + +partial dictionary VideoEncoderEncodeOptions { + VideoEncoderEncodeOptionsForAv1 av1; +}; + +dictionary VideoEncoderEncodeOptionsForAv1 { + unsigned short? quantizer; +}; diff --git a/test/wpt/tests/interfaces/webcodecs-vp9-codec-registration.idl b/test/wpt/tests/interfaces/webcodecs-vp9-codec-registration.idl new file mode 100644 index 00000000000..aca64a78961 --- /dev/null +++ b/test/wpt/tests/interfaces/webcodecs-vp9-codec-registration.idl @@ -0,0 +1,12 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: VP9 WebCodecs Registration (https://w3c.github.io/webcodecs/vp9_codec_registration.html) + +partial dictionary VideoEncoderEncodeOptions { + VideoEncoderEncodeOptionsForVp9 vp9; +}; + +dictionary VideoEncoderEncodeOptionsForVp9 { + unsigned short? quantizer; +}; diff --git a/test/wpt/tests/interfaces/webcodecs.idl b/test/wpt/tests/interfaces/webcodecs.idl index 68df48584bd..77649029db6 100644 --- a/test/wpt/tests/interfaces/webcodecs.idl +++ b/test/wpt/tests/interfaces/webcodecs.idl @@ -4,7 +4,7 @@ // Source: WebCodecs (https://w3c.github.io/webcodecs/) [Exposed=(Window,DedicatedWorker), SecureContext] -interface AudioDecoder { +interface AudioDecoder : EventTarget { constructor(AudioDecoderInit init); readonly attribute CodecState state; @@ -28,7 +28,7 @@ dictionary AudioDecoderInit { callback AudioDataOutputCallback = undefined(AudioData output); [Exposed=(Window,DedicatedWorker), SecureContext] -interface VideoDecoder { +interface VideoDecoder : EventTarget { constructor(VideoDecoderInit init); readonly attribute CodecState state; @@ -52,7 +52,7 @@ dictionary VideoDecoderInit { callback VideoFrameOutputCallback = undefined(VideoFrame output); [Exposed=(Window,DedicatedWorker), SecureContext] -interface AudioEncoder { +interface AudioEncoder : EventTarget { constructor(AudioEncoderInit init); readonly attribute CodecState state; @@ -82,7 +82,7 @@ dictionary EncodedAudioChunkMetadata { }; [Exposed=(Window,DedicatedWorker), SecureContext] -interface VideoEncoder { +interface VideoEncoder : EventTarget { constructor(VideoEncoderInit init); readonly attribute CodecState state; @@ -174,7 +174,7 @@ dictionary VideoEncoderConfig { HardwareAcceleration hardwareAcceleration = "no-preference"; AlphaOption alpha = "discard"; DOMString scalabilityMode; - BitrateMode bitrateMode = "variable"; + VideoEncoderBitrateMode bitrateMode = "variable"; LatencyMode latencyMode = "quality"; }; @@ -198,6 +198,12 @@ dictionary VideoEncoderEncodeOptions { boolean keyFrame = false; }; +enum VideoEncoderBitrateMode { + "constant", + "variable", + "quantizer" +}; + enum CodecState { "unconfigured", "configured", diff --git a/test/wpt/tests/interfaces/webgpu.idl b/test/wpt/tests/interfaces/webgpu.idl index b00e2374e58..284327a5789 100644 --- a/test/wpt/tests/interfaces/webgpu.idl +++ b/test/wpt/tests/interfaces/webgpu.idl @@ -26,6 +26,7 @@ interface GPUSupportedLimits { readonly attribute unsigned long maxStorageBuffersPerShaderStage; readonly attribute unsigned long maxStorageTexturesPerShaderStage; readonly attribute unsigned long maxUniformBuffersPerShaderStage; + readonly attribute unsigned long maxFragmentCombinedOutputResources; readonly attribute unsigned long long maxUniformBufferBindingSize; readonly attribute unsigned long long maxStorageBufferBindingSize; readonly attribute unsigned long minUniformBufferOffsetAlignment; @@ -51,6 +52,11 @@ interface GPUSupportedFeatures { readonly setlike; }; +[Exposed=(Window, DedicatedWorker), SecureContext] +interface WGSLLanguageFeatures { + readonly setlike; +}; + [Exposed=(Window, DedicatedWorker), SecureContext] interface GPUAdapterInfo { readonly attribute DOMString vendor; @@ -69,6 +75,7 @@ WorkerNavigator includes NavigatorGPU; interface GPU { Promise requestAdapter(optional GPURequestAdapterOptions options = {}); GPUTextureFormat getPreferredCanvasFormat(); + [SameObject] readonly attribute WGSLLanguageFeatures wgslLanguageFeatures; }; dictionary GPURequestAdapterOptions { @@ -106,7 +113,9 @@ enum GPUFeatureName { "timestamp-query", "indirect-first-instance", "shader-f16", - "rg11b10ufloat-renderable" + "rg11b10ufloat-renderable", + "bgra8unorm-storage", + "float32-filterable" }; [Exposed=(Window, DedicatedWorker), SecureContext] @@ -168,7 +177,7 @@ dictionary GPUBufferDescriptor : GPUObjectDescriptorBase { }; typedef [EnforceRange] unsigned long GPUBufferUsageFlags; -[Exposed=(Window, DedicatedWorker)] +[Exposed=(Window, DedicatedWorker), SecureContext] namespace GPUBufferUsage { const GPUFlagsConstant MAP_READ = 0x0001; const GPUFlagsConstant MAP_WRITE = 0x0002; @@ -183,7 +192,7 @@ namespace GPUBufferUsage { }; typedef [EnforceRange] unsigned long GPUMapModeFlags; -[Exposed=(Window, DedicatedWorker)] +[Exposed=(Window, DedicatedWorker), SecureContext] namespace GPUMapMode { const GPUFlagsConstant READ = 0x0001; const GPUFlagsConstant WRITE = 0x0002; @@ -223,7 +232,7 @@ enum GPUTextureDimension { }; typedef [EnforceRange] unsigned long GPUTextureUsageFlags; -[Exposed=(Window, DedicatedWorker)] +[Exposed=(Window, DedicatedWorker), SecureContext] namespace GPUTextureUsage { const GPUFlagsConstant COPY_SRC = 0x01; const GPUFlagsConstant COPY_DST = 0x02; @@ -384,7 +393,6 @@ enum GPUTextureFormat { [Exposed=(Window, DedicatedWorker), SecureContext] interface GPUExternalTexture { - readonly attribute boolean expired; }; GPUExternalTexture includes GPUObjectBase; @@ -459,7 +467,7 @@ dictionary GPUBindGroupLayoutEntry { }; typedef [EnforceRange] unsigned long GPUShaderStageFlags; -[Exposed=(Window, DedicatedWorker)] +[Exposed=(Window, DedicatedWorker), SecureContext] namespace GPUShaderStage { const GPUFlagsConstant VERTEX = 0x1; const GPUFlagsConstant FRAGMENT = 0x2; @@ -549,7 +557,7 @@ dictionary GPUPipelineLayoutDescriptor : GPUObjectDescriptorBase { [Exposed=(Window, DedicatedWorker), SecureContext] interface GPUShaderModule { - Promise compilationInfo(); + Promise getCompilationInfo(); }; GPUShaderModule includes GPUObjectBase; @@ -586,7 +594,7 @@ interface GPUCompilationInfo { [Exposed=(Window, DedicatedWorker), SecureContext, Serializable] interface GPUPipelineError : DOMException { - constructor(DOMString message, GPUPipelineErrorInit options); + constructor((DOMString or undefined) message, GPUPipelineErrorInit options); readonly attribute GPUPipelineErrorReason reason; }; @@ -695,7 +703,7 @@ dictionary GPUBlendState { }; typedef [EnforceRange] unsigned long GPUColorWriteFlags; -[Exposed=(Window, DedicatedWorker)] +[Exposed=(Window, DedicatedWorker), SecureContext] namespace GPUColorWrite { const GPUFlagsConstant RED = 0x1; const GPUFlagsConstant GREEN = 0x2; @@ -737,8 +745,8 @@ enum GPUBlendOperation { dictionary GPUDepthStencilState { required GPUTextureFormat format; - boolean depthWriteEnabled = false; - GPUCompareFunction depthCompare = "always"; + required boolean depthWriteEnabled; + required GPUCompareFunction depthCompare; GPUStencilFaceState stencilFront = {}; GPUStencilFaceState stencilBack = {}; @@ -829,34 +837,11 @@ dictionary GPUVertexAttribute { required GPUIndex32 shaderLocation; }; -dictionary GPUImageDataLayout { - GPUSize64 offset = 0; - GPUSize32 bytesPerRow; - GPUSize32 rowsPerImage; -}; - -dictionary GPUImageCopyBuffer : GPUImageDataLayout { - required GPUBuffer buffer; -}; - -dictionary GPUImageCopyTexture { - required GPUTexture texture; - GPUIntegerCoordinate mipLevel = 0; - GPUOrigin3D origin = {}; - GPUTextureAspect aspect = "all"; -}; - -dictionary GPUImageCopyTextureTagged : GPUImageCopyTexture { - PredefinedColorSpace colorSpace = "srgb"; - boolean premultipliedAlpha = false; -}; - -dictionary GPUImageCopyExternalImage { - required (ImageBitmap or HTMLVideoElement or HTMLCanvasElement or OffscreenCanvas) source; - GPUOrigin2D origin = {}; - boolean flipY = false; -}; - +dictionary GPUImageDataLayout { GPUSize64 offset = 0; GPUSize32 bytesPerRow; GPUSize32 rowsPerImage;}; +dictionary GPUImageCopyBuffer : GPUImageDataLayout { required GPUBuffer buffer;}; +dictionary GPUImageCopyTexture { required GPUTexture texture; GPUIntegerCoordinate mipLevel = 0; GPUOrigin3D origin = {}; GPUTextureAspect aspect = "all";}; +dictionary GPUImageCopyTextureTagged : GPUImageCopyTexture { PredefinedColorSpace colorSpace = "srgb"; boolean premultipliedAlpha = false;}; +dictionary GPUImageCopyExternalImage { required (ImageBitmap or HTMLVideoElement or HTMLCanvasElement or OffscreenCanvas) source; GPUOrigin2D origin = {}; boolean flipY = false;}; [Exposed=(Window, DedicatedWorker), SecureContext] interface GPUCommandBuffer { }; @@ -1021,7 +1006,7 @@ dictionary GPURenderPassColorAttachment { dictionary GPURenderPassDepthStencilAttachment { required GPUTextureView view; - float depthClearValue = 0; + float depthClearValue; GPULoadOp depthLoadOp; GPUStoreOp depthStoreOp; boolean depthReadOnly = false; @@ -1161,12 +1146,13 @@ dictionary GPUCanvasConfiguration { }; enum GPUDeviceLostReason { + "unknown", "destroyed" }; [Exposed=(Window, DedicatedWorker), SecureContext] interface GPUDeviceLostInfo { - readonly attribute (GPUDeviceLostReason or undefined) reason; + readonly attribute GPUDeviceLostReason reason; readonly attribute DOMString message; }; diff --git a/test/wpt/tests/interfaces/webidl.idl b/test/wpt/tests/interfaces/webidl.idl index 9993673361a..7271f19dfa6 100644 --- a/test/wpt/tests/interfaces/webidl.idl +++ b/test/wpt/tests/interfaces/webidl.idl @@ -46,5 +46,3 @@ interface DOMException { // but see below note about ECMAScript binding callback Function = any (any... arguments); callback VoidFunction = undefined (); - -typedef unsigned long long DOMTimeStamp; diff --git a/test/wpt/tests/interfaces/webnn.idl b/test/wpt/tests/interfaces/webnn.idl index 820c79e9666..2c2ab35e909 100644 --- a/test/wpt/tests/interfaces/webnn.idl +++ b/test/wpt/tests/interfaces/webnn.idl @@ -146,7 +146,7 @@ interface MLGraphBuilder { dictionary MLBatchNormalizationOptions { MLOperand scale; MLOperand bias; - long axis = 1; + unsigned long axis = 1; float epsilon = 1e-5; MLActivation activation; }; @@ -167,7 +167,7 @@ partial interface MLGraphBuilder { }; partial interface MLGraphBuilder { - MLOperand concat(sequence inputs, long axis); + MLOperand concat(sequence inputs, unsigned long axis); }; enum MLConv2dFilterOperandLayout { @@ -409,7 +409,10 @@ dictionary MLPadOptions { }; partial interface MLGraphBuilder { - MLOperand pad(MLOperand input, MLOperand padding, optional MLPadOptions options = {}); + MLOperand pad(MLOperand input, + sequence beginningPadding, + sequence endingPadding, + optional MLPadOptions options = {}); }; enum MLRoundingType { @@ -434,8 +437,12 @@ partial interface MLGraphBuilder { MLOperand maxPool2d(MLOperand input, optional MLPool2dOptions options = {}); }; +partial interface MLGraphBuilder { + MLOperand prelu(MLOperand x, MLOperand slope); +}; + dictionary MLReduceOptions { - sequence axes = null; + sequence axes = null; boolean keepDimensions = false; }; @@ -466,7 +473,7 @@ dictionary MLResample2dOptions { MLInterpolationMode mode = "nearest-neighbor"; sequence scales; sequence sizes; - sequence axes; + sequence axes; }; partial interface MLGraphBuilder { @@ -483,7 +490,7 @@ partial interface MLGraphBuilder { }; dictionary MLSliceOptions { - sequence axes; + sequence axes; }; partial interface MLGraphBuilder { @@ -511,7 +518,7 @@ partial interface MLGraphBuilder { }; dictionary MLSplitOptions { - long axis = 0; + unsigned long axis = 0; }; partial interface MLGraphBuilder { @@ -521,7 +528,7 @@ partial interface MLGraphBuilder { }; dictionary MLSqueezeOptions { - sequence axes; + sequence axes; }; partial interface MLGraphBuilder { @@ -534,7 +541,7 @@ partial interface MLGraphBuilder { }; dictionary MLTransposeOptions { - sequence permutation; + sequence permutation; }; partial interface MLGraphBuilder { diff --git a/test/wpt/tests/interfaces/webrtc-encoded-transform.idl b/test/wpt/tests/interfaces/webrtc-encoded-transform.idl index 13f3999db12..e48f1080c41 100644 --- a/test/wpt/tests/interfaces/webrtc-encoded-transform.idl +++ b/test/wpt/tests/interfaces/webrtc-encoded-transform.idl @@ -89,6 +89,7 @@ dictionary RTCEncodedAudioFrameMetadata { unsigned long synchronizationSource; octet payloadType; sequence contributingSources; + short sequenceNumber; }; [Exposed=(Window,DedicatedWorker)] diff --git a/test/wpt/tests/interfaces/webrtc-stats.idl b/test/wpt/tests/interfaces/webrtc-stats.idl index f1dca4a923d..7e820a26df4 100644 --- a/test/wpt/tests/interfaces/webrtc-stats.idl +++ b/test/wpt/tests/interfaces/webrtc-stats.idl @@ -105,7 +105,7 @@ dictionary RTCRemoteInboundRtpStreamStats : RTCReceivedRtpStreamStats { }; dictionary RTCSentRtpStreamStats : RTCRtpStreamStats { - unsigned long packetsSent; + unsigned long long packetsSent; unsigned long long bytesSent; }; diff --git a/test/wpt/tests/interfaces/webrtc.idl b/test/wpt/tests/interfaces/webrtc.idl index d631169f46f..578cbe92974 100644 --- a/test/wpt/tests/interfaces/webrtc.idl +++ b/test/wpt/tests/interfaces/webrtc.idl @@ -1,7 +1,7 @@ // GENERATED CONTENT - DO NOT EDIT // Content was automatically extracted by Reffy into webref // (https://github.com/w3c/webref) -// Source: WebRTC 1.0: Real-Time Communication Between Browsers (https://w3c.github.io/webrtc-pc/) +// Source: WebRTC: Real-Time Communication in Browsers (https://w3c.github.io/webrtc-pc/) dictionary RTCConfiguration { sequence iceServers = []; @@ -51,29 +51,29 @@ enum RTCSignalingState { }; enum RTCIceGatheringState { -"new", -"gathering", -"complete" - }; + "new", + "gathering", + "complete" +}; enum RTCPeerConnectionState { -"closed", -"failed", -"disconnected", -"new", -"connecting", -"connected" - }; + "closed", + "failed", + "disconnected", + "new", + "connecting", + "connected" +}; enum RTCIceConnectionState { -"closed", -"failed", -"disconnected", -"new", -"checking", -"completed", -"connected" - }; + "closed", + "failed", + "disconnected", + "new", + "checking", + "completed", + "connected" +}; [Exposed=Window] interface RTCPeerConnection : EventTarget { @@ -332,28 +332,27 @@ dictionary RTCRtpHeaderExtensionParameters { boolean encrypted = false; }; -dictionary RTCRtpCodecParameters { - required octet payloadType; +dictionary RTCRtpCodec { required DOMString mimeType; required unsigned long clockRate; unsigned short channels; DOMString sdpFmtpLine; }; +dictionary RTCRtpCodecParameters : RTCRtpCodec { + required octet payloadType; +}; + dictionary RTCRtpCapabilities { required sequence codecs; required sequence headerExtensions; }; -dictionary RTCRtpCodecCapability { - required DOMString mimeType; - required unsigned long clockRate; - unsigned short channels; - DOMString sdpFmtpLine; +dictionary RTCRtpCodecCapability : RTCRtpCodec { }; dictionary RTCRtpHeaderExtensionCapability { - DOMString uri; + required DOMString uri; }; [Exposed=Window] diff --git a/test/wpt/tests/interfaces/webtransport.idl b/test/wpt/tests/interfaces/webtransport.idl index ba705807f12..2bea483e1b9 100644 --- a/test/wpt/tests/interfaces/webtransport.idl +++ b/test/wpt/tests/interfaces/webtransport.idl @@ -9,10 +9,10 @@ interface WebTransportDatagramDuplexStream { readonly attribute WritableStream writable; readonly attribute unsigned long maxDatagramSize; - attribute double? incomingMaxAge; - attribute double? outgoingMaxAge; - attribute long incomingHighWaterMark; - attribute long outgoingHighWaterMark; + attribute unrestricted double incomingMaxAge; + attribute unrestricted double outgoingMaxAge; + attribute unrestricted double incomingHighWaterMark; + attribute unrestricted double outgoingHighWaterMark; }; [Exposed=(Window,Worker), SecureContext] @@ -24,6 +24,7 @@ interface WebTransport { readonly attribute WebTransportReliabilityMode reliability; readonly attribute WebTransportCongestionControl congestionControl; readonly attribute Promise closed; + readonly attribute Promise draining; undefined close(optional WebTransportCloseInfo closeInfo = {}); readonly attribute WebTransportDatagramDuplexStream datagrams; @@ -85,6 +86,7 @@ dictionary WebTransportStats { DOMHighResTimeStamp rttVariation; DOMHighResTimeStamp minRtt; WebTransportDatagramStats datagrams; + unsigned long long? estimatedSendRate; }; dictionary WebTransportDatagramStats { @@ -123,17 +125,17 @@ interface WebTransportBidirectionalStream { readonly attribute WebTransportSendStream writable; }; -[Exposed=(Window,Worker), SecureContext] +[Exposed=(Window,Worker), Serializable, SecureContext] interface WebTransportError : DOMException { - constructor(optional WebTransportErrorInit init = {}); + constructor(optional DOMString message = "", optional WebTransportErrorOptions options = {}); readonly attribute WebTransportErrorSource source; readonly attribute octet? streamErrorCode; }; -dictionary WebTransportErrorInit { - [Clamp] octet streamErrorCode; - DOMString message; +dictionary WebTransportErrorOptions { + WebTransportErrorSource source = "stream"; + [Clamp] octet? streamErrorCode = null; }; enum WebTransportErrorSource { diff --git a/test/wpt/tests/interfaces/webxr.idl b/test/wpt/tests/interfaces/webxr.idl index 939da53364d..de2b04691b3 100644 --- a/test/wpt/tests/interfaces/webxr.idl +++ b/test/wpt/tests/interfaces/webxr.idl @@ -41,6 +41,7 @@ enum XRVisibilityState { [SameObject] readonly attribute XRRenderState renderState; [SameObject] readonly attribute XRInputSourceArray inputSources; readonly attribute FrozenArray enabledFeatures; + readonly attribute boolean isSystemKeyboardSupported; // Methods undefined updateRenderState(optional XRRenderStateInit state = {}); diff --git a/test/wpt/tests/interfaces/window-placement.idl b/test/wpt/tests/interfaces/window-management.idl similarity index 93% rename from test/wpt/tests/interfaces/window-placement.idl rename to test/wpt/tests/interfaces/window-management.idl index ac86c1d502a..527c41deb6c 100644 --- a/test/wpt/tests/interfaces/window-placement.idl +++ b/test/wpt/tests/interfaces/window-management.idl @@ -1,7 +1,7 @@ // GENERATED CONTENT - DO NOT EDIT // Content was automatically extracted by Reffy into webref // (https://github.com/w3c/webref) -// Source: Multi-Screen Window Placement (https://w3c.github.io/window-placement/) +// Source: Window Management (https://w3c.github.io/window-management/) partial interface Screen /* : EventTarget */ { [SecureContext] diff --git a/test/wpt/tests/interfaces/xhr.idl b/test/wpt/tests/interfaces/xhr.idl index 3abd09ce5ed..b4c27c8aca9 100644 --- a/test/wpt/tests/interfaces/xhr.idl +++ b/test/wpt/tests/interfaces/xhr.idl @@ -70,7 +70,7 @@ typedef (File or USVString) FormDataEntryValue; [Exposed=(Window,Worker)] interface FormData { - constructor(optional HTMLFormElement form); + constructor(optional HTMLFormElement form, optional HTMLElement? submitter = null); undefined append(USVString name, USVString value); undefined append(USVString name, Blob blobValue, optional USVString filename); diff --git a/test/wpt/tests/lint.ignore b/test/wpt/tests/lint.ignore index cddc3203b67..056bbd0c230 100644 --- a/test/wpt/tests/lint.ignore +++ b/test/wpt/tests/lint.ignore @@ -215,6 +215,8 @@ SET TIMEOUT: resize-observer/resources/iframe.html SET TIMEOUT: resource-timing/resources/nested-contexts.js SET TIMEOUT: reporting/resources/first-csp-report.https.sub.html SET TIMEOUT: reporting/resources/second-csp-report.https.sub.html +SET TIMEOUT: scheduler/tentative/yield/yield-inherit-across-promises.any.js +SET TIMEOUT: scheduler/tentative/yield/yield-priority-timers.any.js SET TIMEOUT: secure-contexts/basic-popup-and-iframe-tests.https.js SET TIMEOUT: service-workers/cache-storage/cache-abort.https.any.js SET TIMEOUT: service-workers/service-worker/activation.https.html @@ -357,6 +359,7 @@ SET TIMEOUT: speculation-rules/prerender/resources/media-autoplay-attribute.html SET TIMEOUT: speculation-rules/prerender/resources/media-play.html SET TIMEOUT: html/browsers/browsing-the-web/back-forward-cache/timers.html SET TIMEOUT: dom/abort/crashtests/timeout-close.html +SET TIMEOUT: common/rendering-utils.js # setTimeout use in reftests SET TIMEOUT: acid/acid3/test.html @@ -755,6 +758,7 @@ MISSING DEPENDENCY: resources/chromium/webusb-test.js MISSING DEPENDENCY: resources/chromium/fake-serial.js MISSING DEPENDENCY: resources/chromium/fake-hid.js MISSING DEPENDENCY: resources/chromium/generic_sensor_mocks.js +MISSING DEPENDENCY: resources/chromium/mock-battery-monitor.js MISSING DEPENDENCY: resources/chromium/mock-barcodedetection.js MISSING DEPENDENCY: resources/chromium/mock-direct-sockets.js MISSING DEPENDENCY: resources/chromium/mock-facedetection.js @@ -779,6 +783,8 @@ AHEM SYSTEM FONT: css/css-font-loading/fontface-size-adjust-descriptor-ref.html AHEM SYSTEM FONT: css/css-fonts/ascent-descent-override.html AHEM SYSTEM FONT: css/css-fonts/font-size-adjust-012.html AHEM SYSTEM FONT: css/css-fonts/font-size-adjust-012-ref.html +AHEM SYSTEM FONT: css/css-fonts/font-size-adjust-013.html +AHEM SYSTEM FONT: css/css-fonts/font-size-adjust-013-ref.html AHEM SYSTEM FONT: css/css-fonts/line-gap-override.html AHEM SYSTEM FONT: html/dom/render-blocking/remove-attr-unblocks-rendering.optional.html AHEM SYSTEM FONT: html/dom/render-blocking/remove-element-unblocks-rendering.optional.html @@ -790,7 +796,6 @@ AHEM SYSTEM FONT: infrastructure/assumptions/ahem.html # Existing crashtests using testharness TESTHARNESS-IN-OTHER-TYPE: accessibility/crashtests/computed-node-checked.html -TESTHARNESS-IN-OTHER-TYPE: html/canvas/element/manual/wide-gamut-canvas/imagedata-no-color-settings-crash.html TESTHARNESS-IN-OTHER-TYPE: css/CSS2/floats-clear/adjoining-float-new-fc-crash.html TESTHARNESS-IN-OTHER-TYPE: css/CSS2/floats/floats-saturated-position-crash.html TESTHARNESS-IN-OTHER-TYPE: css/CSS2/linebox/video-needs-layout-crash.html @@ -815,6 +820,7 @@ TESTHARNESS-IN-OTHER-TYPE: css/css-text/white-space/pre-with-whitespace-crash.ht TESTHARNESS-IN-OTHER-TYPE: css/css-writing-modes/bidi-inline-fragment-crash.html TESTHARNESS-IN-OTHER-TYPE: dom/svg-insert-crash.html TESTHARNESS-IN-OTHER-TYPE: editing/run/first-letter-crossing-engine-boundary-crash.html +TESTHARNESS-IN-OTHER-TYPE: html/canvas/element/manual/wide-gamut-canvas/imagedata-no-color-settings-crash.html TESTHARNESS-IN-OTHER-TYPE: html/semantics/embedded-content/the-object-element/block-object-with-ruby-crash.html TESTHARNESS-IN-OTHER-TYPE: html/semantics/forms/the-input-element/time-datalist-crash.html TESTHARNESS-IN-OTHER-TYPE: html/semantics/forms/the-input-element/type-change-file-to-text-crash.html @@ -832,8 +838,9 @@ TESTHARNESS-IN-OTHER-TYPE: svg/svg-in-svg/svg-in-svg-circular-filter-reference-c # Adding the testharnessreport.js script causes the test to never complete. MISSING-TESTHARNESSREPORT: accessibility/crashtests/computed-node-checked.html -PRINT STATEMENT: webdriver/tests/print/printcmd.py -PRINT STATEMENT: webdriver/tests/print/user_prompts.py +PRINT STATEMENT: webdriver/tests/print/* +PRINT STATEMENT: webdriver/tests/bidi/browsing_context/print/* +PRINT STATEMENT: webdriver/tests/support/fixtures_bidi.py DUPLICATE-BASENAME-PATH: acid/acid3/empty.html DUPLICATE-BASENAME-PATH: acid/acid3/empty.xml diff --git a/test/wpt/tests/resources/chromium/mock-battery-monitor.headers b/test/wpt/tests/resources/chromium/mock-battery-monitor.headers new file mode 100644 index 00000000000..6805c323df5 --- /dev/null +++ b/test/wpt/tests/resources/chromium/mock-battery-monitor.headers @@ -0,0 +1 @@ +Content-Type: text/javascript; charset=utf-8 diff --git a/test/wpt/tests/resources/chromium/mock-battery-monitor.js b/test/wpt/tests/resources/chromium/mock-battery-monitor.js new file mode 100644 index 00000000000..8fa27bc56a1 --- /dev/null +++ b/test/wpt/tests/resources/chromium/mock-battery-monitor.js @@ -0,0 +1,61 @@ +import {BatteryMonitor, BatteryMonitorReceiver} from '/gen/services/device/public/mojom/battery_monitor.mojom.m.js'; + +class MockBatteryMonitor { + constructor() { + this.receiver_ = new BatteryMonitorReceiver(this); + this.interceptor_ = + new MojoInterfaceInterceptor(BatteryMonitor.$interfaceName); + this.interceptor_.oninterfacerequest = e => + this.receiver_.$.bindHandle(e.handle); + this.reset(); + } + + start() { + this.interceptor_.start(); + } + + stop() { + this.interceptor_.stop(); + } + + reset() { + this.pendingRequests_ = []; + this.status_ = null; + this.lastKnownStatus_ = null; + } + + queryNextStatus() { + const result = new Promise(resolve => this.pendingRequests_.push(resolve)); + this.runCallbacks_(); + return result; + } + + setBatteryStatus(charging, chargingTime, dischargingTime, level) { + this.status_ = {charging, chargingTime, dischargingTime, level}; + this.lastKnownStatus_ = this.status_; + this.runCallbacks_(); + } + + verifyBatteryStatus(manager) { + assert_not_equals(manager, undefined); + assert_not_equals(this.lastKnownStatus_, null); + assert_equals(manager.charging, this.lastKnownStatus_.charging); + assert_equals(manager.chargingTime, this.lastKnownStatus_.chargingTime); + assert_equals( + manager.dischargingTime, this.lastKnownStatus_.dischargingTime); + assert_equals(manager.level, this.lastKnownStatus_.level); + } + + runCallbacks_() { + if (!this.status_ || !this.pendingRequests_.length) + return; + + let result = {status: this.status_}; + while (this.pendingRequests_.length) { + this.pendingRequests_.pop()(result); + } + this.status_ = null; + } +} + +export const mockBatteryMonitor = new MockBatteryMonitor(); diff --git a/test/wpt/tests/resources/chromium/mock-pressure-service.js b/test/wpt/tests/resources/chromium/mock-pressure-service.js index 91efe529f2a..21811ed52da 100644 --- a/test/wpt/tests/resources/chromium/mock-pressure-service.js +++ b/test/wpt/tests/resources/chromium/mock-pressure-service.js @@ -1,5 +1,5 @@ import {PressureManager, PressureManagerReceiver, PressureStatus} from '/gen/services/device/public/mojom/pressure_manager.mojom.m.js' -import {PressureFactor, PressureState} from '/gen/services/device/public/mojom/pressure_update.mojom.m.js' +import {PressureFactor, PressureSource, PressureState} from '/gen/services/device/public/mojom/pressure_update.mojom.m.js' class MockPressureService { constructor() { @@ -9,11 +9,8 @@ class MockPressureService { this.interceptor_.oninterfacerequest = e => { this.receiver_.$.bindHandle(e.handle); }; - this.receiver_.onConnectionError.addListener(() => { - this.stopPlatformCollector(); - this.observer_ = null; - }); this.reset(); + this.mojomSourceType_ = new Map([['cpu', PressureSource.kCpu]]); this.mojomStateType_ = new Map([ ['nominal', PressureState.kNominal], ['fair', PressureState.kFair], ['serious', PressureState.kSerious], ['critical', PressureState.kCritical] @@ -40,22 +37,27 @@ class MockPressureService { } reset() { - this.observer_ = null; + this.observers_ = []; this.pressureUpdate_ = null; this.pressureServiceReadingTimerId_ = null; this.pressureStatus_ = PressureStatus.kOk; this.updatesDelivered_ = 0; } - async addClient(observer) { - if (this.observer_ !== null) - throw new Error('BindObserver() has already been called'); + async addClient(observer, source) { + if (this.observers_.indexOf(observer) >= 0) + throw new Error('addClient() has already been called'); - this.observer_ = observer; - this.observer_.onConnectionError.addListener(() => { - this.stopPlatformCollector(); - this.observer_ = null; + // TODO(crbug.com/1342184): Consider other sources. + // For now, "cpu" is the only source. + if (source !== PressureSource.kCpu) + throw new Error('Call addClient() with a wrong PressureSource'); + + observer.onConnectionError.addListener(() => { + // Remove this observer from observer array. + this.observers_.splice(this.observers_.indexOf(observer), 1); }); + this.observers_.push(observer); return {status: this.pressureStatus_}; } @@ -83,20 +85,21 @@ class MockPressureService { const epochDeltaInMs = unixEpoch - windowsEpoch; const timeout = (1 / sampleRate) * 1000; - this.pressureServiceReadingTimerId_ = window.setInterval(() => { - if (this.pressureUpdate_ === null || this.observer_ === null) + this.pressureServiceReadingTimerId_ = self.setInterval(() => { + if (this.pressureUpdate_ === null || this.observers_.length === 0) return; this.pressureUpdate_.timestamp = { internalValue: BigInt((new Date().getTime() + epochDeltaInMs) * 1000) }; - this.observer_.onPressureUpdated(this.pressureUpdate_); + for (let observer of this.observers_) + observer.onPressureUpdated(this.pressureUpdate_); this.updatesDelivered_++; }, timeout); } stopPlatformCollector() { if (this.pressureServiceReadingTimerId_ != null) { - window.clearInterval(this.pressureServiceReadingTimerId_); + self.clearInterval(this.pressureServiceReadingTimerId_); this.pressureServiceReadingTimerId_ = null; } this.updatesDelivered_ = 0; @@ -106,7 +109,10 @@ class MockPressureService { return this.updatesDelivered_; } - setPressureUpdate(state, factors) { + setPressureUpdate(source, state, factors) { + if (!this.mojomSourceType_.has(source)) + throw new Error(`PressureSource '${source}' is invalid`); + if (!this.mojomStateType_.has(state)) throw new Error(`PressureState '${state}' is invalid`); @@ -120,6 +126,7 @@ class MockPressureService { } this.pressureUpdate_ = { + source: this.mojomSourceType_.get(source), state: this.mojomStateType_.get(state), factors: pressureFactors, }; diff --git a/test/wpt/tests/resources/chromium/mock-subapps.js b/test/wpt/tests/resources/chromium/mock-subapps.js index b63e97b8c4d..b81936713b1 100644 --- a/test/wpt/tests/resources/chromium/mock-subapps.js +++ b/test/wpt/tests/resources/chromium/mock-subapps.js @@ -37,7 +37,7 @@ self.SubAppsServiceTest = (() => { remove() { return Promise.resolve({ - result: testInternal.serviceResultCode, + result: testInternal.removeCallReturnValue, }); } } @@ -48,6 +48,7 @@ self.SubAppsServiceTest = (() => { serviceResultCode: 0, addCallReturnValue: [], listCallReturnValue: [], + removeCallReturnValue: [], } class SubAppsServiceTestChromium { @@ -55,7 +56,7 @@ self.SubAppsServiceTest = (() => { Object.freeze(this); // Make it immutable. } - initialize(service_result_code, add_call_return_value, list_call_return_value) { + initialize(service_result_code, add_call_return_value, list_call_return_value, remove_call_return_value) { if (!testInternal.initialized) { testInternal = { mockSubAppsService: new MockSubAppsService(), @@ -63,6 +64,7 @@ self.SubAppsServiceTest = (() => { serviceResultCode: service_result_code, addCallReturnValue: add_call_return_value, listCallReturnValue: list_call_return_value, + removeCallReturnValue: remove_call_return_value, }; }; } @@ -76,6 +78,7 @@ self.SubAppsServiceTest = (() => { serviceResultCode: 0, addCallReturnValue: [], listCallReturnValue: [], + removeCallReturnValue: [], }; await new Promise(resolve => setTimeout(resolve, 0)); } diff --git a/test/wpt/tests/resources/chromium/webusb-child-test.js b/test/wpt/tests/resources/chromium/webusb-child-test.js index add04fa5825..21412f66b0a 100644 --- a/test/wpt/tests/resources/chromium/webusb-child-test.js +++ b/test/wpt/tests/resources/chromium/webusb-child-test.js @@ -25,7 +25,14 @@ // Wait for a call to GetDevices() to ensure that the interface // handles are forwarded to the parent context. - await navigator.usb.getDevices(); + try { + await navigator.usb.getDevices(); + } catch (e) { + // This can happen in case of, for example, testing usb disallowed + // iframe. + console.error(`getDevices() throws error: ${e.name}: ${e.message}`); + } + messageChannel.port1.postMessage({ type: 'Complete' }); } }; diff --git a/test/wpt/tests/resources/chromium/webxr-test.js b/test/wpt/tests/resources/chromium/webxr-test.js index db61e1ce4d6..ab2c6faa0ee 100644 --- a/test/wpt/tests/resources/chromium/webxr-test.js +++ b/test/wpt/tests/resources/chromium/webxr-test.js @@ -1,4 +1,5 @@ import * as vrMojom from '/gen/device/vr/public/mojom/vr_service.mojom.m.js'; +import * as xrSessionMojom from '/gen/device/vr/public/mojom/xr_session.mojom.m.js'; import {GamepadHand, GamepadMapping} from '/gen/device/gamepad/public/mojom/gamepad.mojom.m.js'; // This polyfill library implements the WebXR Test API as specified here: @@ -322,25 +323,25 @@ class MockRuntime { // Mapping from string feature names to the corresponding mojo types. // This is exposed as a member for extensibility. static _featureToMojoMap = { - 'viewer': vrMojom.XRSessionFeature.REF_SPACE_VIEWER, - 'local': vrMojom.XRSessionFeature.REF_SPACE_LOCAL, - 'local-floor': vrMojom.XRSessionFeature.REF_SPACE_LOCAL_FLOOR, - 'bounded-floor': vrMojom.XRSessionFeature.REF_SPACE_BOUNDED_FLOOR, - 'unbounded': vrMojom.XRSessionFeature.REF_SPACE_UNBOUNDED, - 'hit-test': vrMojom.XRSessionFeature.HIT_TEST, - 'dom-overlay': vrMojom.XRSessionFeature.DOM_OVERLAY, - 'light-estimation': vrMojom.XRSessionFeature.LIGHT_ESTIMATION, - 'anchors': vrMojom.XRSessionFeature.ANCHORS, - 'depth-sensing': vrMojom.XRSessionFeature.DEPTH, - 'secondary-views': vrMojom.XRSessionFeature.SECONDARY_VIEWS, - 'camera-access': vrMojom.XRSessionFeature.CAMERA_ACCESS, - 'layers': vrMojom.XRSessionFeature.LAYERS, + 'viewer': xrSessionMojom.XRSessionFeature.REF_SPACE_VIEWER, + 'local': xrSessionMojom.XRSessionFeature.REF_SPACE_LOCAL, + 'local-floor': xrSessionMojom.XRSessionFeature.REF_SPACE_LOCAL_FLOOR, + 'bounded-floor': xrSessionMojom.XRSessionFeature.REF_SPACE_BOUNDED_FLOOR, + 'unbounded': xrSessionMojom.XRSessionFeature.REF_SPACE_UNBOUNDED, + 'hit-test': xrSessionMojom.XRSessionFeature.HIT_TEST, + 'dom-overlay': xrSessionMojom.XRSessionFeature.DOM_OVERLAY, + 'light-estimation': xrSessionMojom.XRSessionFeature.LIGHT_ESTIMATION, + 'anchors': xrSessionMojom.XRSessionFeature.ANCHORS, + 'depth-sensing': xrSessionMojom.XRSessionFeature.DEPTH, + 'secondary-views': xrSessionMojom.XRSessionFeature.SECONDARY_VIEWS, + 'camera-access': xrSessionMojom.XRSessionFeature.CAMERA_ACCESS, + 'layers': xrSessionMojom.XRSessionFeature.LAYERS, }; static _sessionModeToMojoMap = { - "inline": vrMojom.XRSessionMode.kInline, - "immersive-vr": vrMojom.XRSessionMode.kImmersiveVr, - "immersive-ar": vrMojom.XRSessionMode.kImmersiveAr, + "inline": xrSessionMojom.XRSessionMode.kInline, + "immersive-vr": xrSessionMojom.XRSessionMode.kImmersiveVr, + "immersive-ar": xrSessionMojom.XRSessionMode.kImmersiveAr, }; static _environmentBlendModeToMojoMap = { @@ -700,10 +701,10 @@ class MockRuntime { if (blendMode in MockRuntime._environmentBlendModeToMojoMap) { return MockRuntime._environmentBlendModeToMojoMap[blendMode]; } else { - if (this.supportedModes_.includes(vrMojom.XRSessionMode.kImmersiveAr)) { + if (this.supportedModes_.includes(xrSessionMojom.XRSessionMode.kImmersiveAr)) { return vrMojom.XREnvironmentBlendMode.kAdditive; } else if (this.supportedModes_.includes( - vrMojom.XRSessionMode.kImmersiveVr)) { + xrSessionMojom.XRSessionMode.kImmersiveVr)) { return vrMojom.XREnvironmentBlendMode.kOpaque; } } @@ -852,7 +853,7 @@ class MockRuntime { if (feature in MockRuntime._featureToMojoMap) { return MockRuntime._featureToMojoMap[feature]; } else { - return vrMojom.XRSessionFeature.INVALID; + return xrSessionMojom.XRSessionFeature.INVALID; } } @@ -860,7 +861,7 @@ class MockRuntime { for (let i = 0; i < supportedFeatures.length; i++) { const feature = convertFeatureToMojom(supportedFeatures[i]); - if (feature !== vrMojom.XRSessionFeature.INVALID) { + if (feature !== xrSessionMojom.XRSessionFeature.INVALID) { this.supportedFeatures_.push(feature); } } @@ -917,7 +918,7 @@ class MockRuntime { this.primaryViews_[i].mojoFromView = this._getMojoFromViewerWithOffset(this.primaryViews_[i].viewOffset); } - if (this.enabledFeatures_.includes(vrMojom.XRSessionFeature.SECONDARY_VIEWS)) { + if (this.enabledFeatures_.includes(xrSessionMojom.XRSessionFeature.SECONDARY_VIEWS)) { for (let i = 0; i < this.secondaryViews_.length; i++) { this.secondaryViews_[i].mojoFromView = this._getMojoFromViewerWithOffset(this.secondaryViews_[i].viewOffset); @@ -960,7 +961,7 @@ class MockRuntime { resolve({frameData}); }; - if(this.sessionOptions_.mode == vrMojom.XRSessionMode.kInline) { + if(this.sessionOptions_.mode == xrSessionMojom.XRSessionMode.kInline) { // Inline sessions should not have a delay introduced since it causes them // to miss a vsync blink-side and delays propagation of changes that happened // within a rAFcb by one frame (e.g. setViewerOrigin() calls would take 2 frames @@ -988,7 +989,7 @@ class MockRuntime { // XREnvironmentIntegrationProvider implementation: subscribeToHitTest(nativeOriginInformation, entityTypes, ray) { - if (!this.supportedModes_.includes(vrMojom.XRSessionMode.kImmersiveAr)) { + if (!this.supportedModes_.includes(xrSessionMojom.XRSessionMode.kImmersiveAr)) { // Reject outside of AR. return Promise.resolve({ result : vrMojom.SubscribeToHitTestResult.FAILURE_GENERIC, @@ -1029,7 +1030,7 @@ class MockRuntime { } subscribeToHitTestForTransientInput(profileName, entityTypes, ray){ - if (!this.supportedModes_.includes(vrMojom.XRSessionMode.kImmersiveAr)) { + if (!this.supportedModes_.includes(xrSessionMojom.XRSessionMode.kImmersiveAr)) { // Reject outside of AR. return Promise.resolve({ result : vrMojom.SubscribeToHitTestResult.FAILURE_GENERIC, @@ -1215,7 +1216,7 @@ class MockRuntime { defaultFramebufferScale: this.defaultFramebufferScale_, supportsViewportScaling: true, depthConfiguration: - enabled_features.includes(vrMojom.XRSessionFeature.DEPTH) ? { + enabled_features.includes(xrSessionMojom.XRSessionFeature.DEPTH) ? { depthUsage: vrMojom.XRDepthUsage.kCPUOptimized, depthDataFormat: vrMojom.XRDepthDataFormat.kLuminanceAlpha, } : null, @@ -1234,8 +1235,8 @@ class MockRuntime { _runtimeSupportsSession(options) { let result = this.supportedModes_.includes(options.mode); - if (options.requiredFeatures.includes(vrMojom.XRSessionFeature.DEPTH) - || options.optionalFeatures.includes(vrMojom.XRSessionFeature.DEPTH)) { + if (options.requiredFeatures.includes(xrSessionMojom.XRSessionFeature.DEPTH) + || options.optionalFeatures.includes(xrSessionMojom.XRSessionFeature.DEPTH)) { result &= options.depthOptions.usagePreferences.includes(vrMojom.XRDepthUsage.kCPUOptimized); result &= options.depthOptions.dataFormatPreferences.includes(vrMojom.XRDepthDataFormat.kLuminanceAlpha); } @@ -1273,7 +1274,7 @@ class MockRuntime { // Modifies passed in frameData to add anchor information. _calculateAnchorInformation(frameData) { - if (!this.supportedModes_.includes(vrMojom.XRSessionMode.kImmersiveAr)) { + if (!this.supportedModes_.includes(xrSessionMojom.XRSessionMode.kImmersiveAr)) { return; } @@ -1301,11 +1302,11 @@ class MockRuntime { // Modifies passed in frameData to add anchor information. _calculateDepthInformation(frameData) { - if (!this.supportedModes_.includes(vrMojom.XRSessionMode.kImmersiveAr)) { + if (!this.supportedModes_.includes(xrSessionMojom.XRSessionMode.kImmersiveAr)) { return; } - if (!this.enabledFeatures_.includes(vrMojom.XRSessionFeature.DEPTH)) { + if (!this.enabledFeatures_.includes(xrSessionMojom.XRSessionFeature.DEPTH)) { return; } @@ -1349,7 +1350,7 @@ class MockRuntime { // Modifies passed in frameData to add hit test results. _calculateHitTestResults(frameData) { - if (!this.supportedModes_.includes(vrMojom.XRSessionMode.kImmersiveAr)) { + if (!this.supportedModes_.includes(xrSessionMojom.XRSessionMode.kImmersiveAr)) { return; } @@ -1965,6 +1966,7 @@ class MockXRInputSource { timestamp: 0n, axes: [], buttons: [], + touchEvents: [], mapping: GamepadMapping.GamepadMappingStandard, displayId: 0, }; diff --git a/test/wpt/tests/resources/declarative-shadow-dom-polyfill.js b/test/wpt/tests/resources/declarative-shadow-dom-polyfill.js index dfb0a1e053c..99a3e911eb6 100644 --- a/test/wpt/tests/resources/declarative-shadow-dom-polyfill.js +++ b/test/wpt/tests/resources/declarative-shadow-dom-polyfill.js @@ -1,14 +1,23 @@ /* - * Polyfill for attaching shadow trees for declarative Shadow DOM for implementations that do not support - * declarative Shadow DOM. + * Polyfill for attaching shadow trees for declarative Shadow DOM for + * implementations that do not support declarative Shadow DOM. + * + * Note: this polyfill will feature-detect the native feature, and do nothing + * if supported. + * + * See: https://github.com/whatwg/html/pull/5465 + * + * root: The root of the subtree in which to upgrade shadow roots * - * root: The root of the subtree to perform the attachments in */ function polyfill_declarative_shadow_dom(root) { - root.querySelectorAll("template[shadowroot]").forEach(template => { - const mode = template.getAttribute("shadowroot"); - const shadowRoot = template.parentNode.attachShadow({ mode }); + if (HTMLTemplateElement.prototype.hasOwnProperty('shadowRootMode')) + return; + root.querySelectorAll("template[shadowrootmode]").forEach(template => { + const mode = template.getAttribute("shadowrootmode"); + const delegatesFocus = template.hasAttribute("shadowrootdelegatesfocus"); + const shadowRoot = template.parentNode.attachShadow({ mode, delegatesFocus }); shadowRoot.appendChild(template.content); template.remove(); polyfill_declarative_shadow_dom(shadowRoot); diff --git a/test/wpt/tests/resources/test/tox.ini b/test/wpt/tests/resources/test/tox.ini index cc07e3b028c..4fbeb67fb52 100644 --- a/test/wpt/tests/resources/test/tox.ini +++ b/test/wpt/tests/resources/test/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py36,py37,py38,py39,py310 +envlist = py37,py38,py39,py310 skipsdist=True [testenv] diff --git a/test/wpt/tests/resources/testdriver.js b/test/wpt/tests/resources/testdriver.js index 76ae2834fdf..446b033b0a4 100644 --- a/test/wpt/tests/resources/testdriver.js +++ b/test/wpt/tests/resources/testdriver.js @@ -220,6 +220,40 @@ return cookie; }, + /** + * Get Computed Label for an element. + * + * This matches the behaviour of the + * `Get Computed Label + * `_ + * WebDriver command. + * + * @param {Element} element + * @returns {Promise} fulfilled after the computed label is returned, or + * rejected in the cases the WebDriver command errors + */ + get_computed_label: async function(element) { + let label = await window.test_driver_internal.get_computed_label(element); + return label; + }, + + /** + * Get Computed Role for an element. + * + * This matches the behaviour of the + * `Get Computed Label + * `_ + * WebDriver command. + * + * @param {Element} element + * @returns {Promise} fulfilled after the computed role is returned, or + * rejected in the cases the WebDriver command errors + */ + get_computed_role: async function(element) { + let role = await window.test_driver_internal.get_computed_role(element); + return role; + }, + /** * Send keys to an element. * @@ -657,9 +691,9 @@ */ in_automation: false, - click: function(element, coords) { + async click(element, coords) { if (this.in_automation) { - return Promise.reject(new Error('Not implemented')); + throw new Error("click() is not implemented by testdriver-vendor.js"); } return new Promise(function(resolve, reject) { @@ -667,21 +701,21 @@ }); }, - delete_all_cookies: function(context=null) { - return Promise.reject(new Error("unimplemented")); + async delete_all_cookies(context=null) { + throw new Error("delete_all_cookies() is not implemented by testdriver-vendor.js"); }, - get_all_cookies: function(context=null) { - return Promise.reject(new Error("unimplemented")); + async get_all_cookies(context=null) { + throw new Error("get_all_cookies() is not implemented by testdriver-vendor.js"); }, - get_named_cookie: function(name, context=null) { - return Promise.reject(new Error("unimplemented")); + async get_named_cookie(name, context=null) { + throw new Error("get_named_cookie() is not implemented by testdriver-vendor.js"); }, - send_keys: function(element, keys) { + async send_keys(element, keys) { if (this.in_automation) { - return Promise.reject(new Error('Not implemented')); + throw new Error("send_keys() is not implemented by testdriver-vendor.js"); } return new Promise(function(resolve, reject) { @@ -711,65 +745,64 @@ }); }, - freeze: function(context=null) { - return Promise.reject(new Error("unimplemented")); + async freeze(context=null) { + throw new Error("freeze() is not implemented by testdriver-vendor.js"); }, - minimize_window: function(context=null) { - return Promise.reject(new Error("unimplemented")); + async minimize_window(context=null) { + throw new Error("minimize_window() is not implemented by testdriver-vendor.js"); }, - set_window_rect: function(rect, context=null) { - return Promise.reject(new Error("unimplemented")); + async set_window_rect(rect, context=null) { + throw new Error("set_window_rect() is not implemented by testdriver-vendor.js"); }, - action_sequence: function(actions, context=null) { - return Promise.reject(new Error("unimplemented")); + async action_sequence(actions, context=null) { + throw new Error("action_sequence() is not implemented by testdriver-vendor.js"); }, - generate_test_report: function(message, context=null) { - return Promise.reject(new Error("unimplemented")); + async generate_test_report(message, context=null) { + throw new Error("generate_test_report() is not implemented by testdriver-vendor.js"); }, - - set_permission: function(permission_params, context=null) { - return Promise.reject(new Error("unimplemented")); + async set_permission(permission_params, context=null) { + throw new Error("set_permission() is not implemented by testdriver-vendor.js"); }, - add_virtual_authenticator: function(config, context=null) { - return Promise.reject(new Error("unimplemented")); + async add_virtual_authenticator(config, context=null) { + throw new Error("add_virtual_authenticator() is not implemented by testdriver-vendor.js"); }, - remove_virtual_authenticator: function(authenticator_id, context=null) { - return Promise.reject(new Error("unimplemented")); + async remove_virtual_authenticator(authenticator_id, context=null) { + throw new Error("remove_virtual_authenticator() is not implemented by testdriver-vendor.js"); }, - add_credential: function(authenticator_id, credential, context=null) { - return Promise.reject(new Error("unimplemented")); + async add_credential(authenticator_id, credential, context=null) { + throw new Error("add_credential() is not implemented by testdriver-vendor.js"); }, - get_credentials: function(authenticator_id, context=null) { - return Promise.reject(new Error("unimplemented")); + async get_credentials(authenticator_id, context=null) { + throw new Error("get_credentials() is not implemented by testdriver-vendor.js"); }, - remove_credential: function(authenticator_id, credential_id, context=null) { - return Promise.reject(new Error("unimplemented")); + async remove_credential(authenticator_id, credential_id, context=null) { + throw new Error("remove_credential() is not implemented by testdriver-vendor.js"); }, - remove_all_credentials: function(authenticator_id, context=null) { - return Promise.reject(new Error("unimplemented")); + async remove_all_credentials(authenticator_id, context=null) { + throw new Error("remove_all_credentials() is not implemented by testdriver-vendor.js"); }, - set_user_verified: function(authenticator_id, uv, context=null) { - return Promise.reject(new Error("unimplemented")); + async set_user_verified(authenticator_id, uv, context=null) { + throw new Error("set_user_verified() is not implemented by testdriver-vendor.js"); }, - set_storage_access: function(origin, embedding_origin, blocked, context=null) { - return Promise.reject(new Error("unimplemented")); + async set_storage_access(origin, embedding_origin, blocked, context=null) { + throw new Error("set_storage_access() is not implemented by testdriver-vendor.js"); }, - set_spc_transaction_mode: function(mode, context=null) { - return Promise.reject(new Error("unimplemented")); + async set_spc_transaction_mode(mode, context=null) { + throw new Error("set_spc_transaction_mode() is not implemented by testdriver-vendor.js"); }, }; diff --git a/test/wpt/tests/resources/testharness.js b/test/wpt/tests/resources/testharness.js index bed115a99ce..112790bb1ee 100644 --- a/test/wpt/tests/resources/testharness.js +++ b/test/wpt/tests/resources/testharness.js @@ -494,7 +494,7 @@ ShellTestEnvironment.prototype.next_default_test_name = function() { var suffix = this.name_counter > 0 ? " " + this.name_counter : ""; this.name_counter++; - return "Untitled" + suffix; + return get_title() + suffix; }; ShellTestEnvironment.prototype.on_new_harness_properties = function() {}; @@ -4754,7 +4754,7 @@ if ('META_TITLE' in global_scope && META_TITLE) { return META_TITLE; } - if ('location' in global_scope) { + if ('location' in global_scope && 'pathname' in location) { return location.pathname.substring(location.pathname.lastIndexOf('/') + 1, location.pathname.indexOf('.')); } return "Untitled"; diff --git a/test/wpt/tests/wpt b/test/wpt/tests/wpt index b0e415d8443..e0abacd85da 100644 --- a/test/wpt/tests/wpt +++ b/test/wpt/tests/wpt @@ -2,8 +2,8 @@ if __name__ == "__main__": import sys - if sys.version_info < (3, 6): - sys.stderr.write("wpt requires Python 3.6 or higher\n") + if sys.version_info < (3, 7): + sys.stderr.write("wpt requires Python 3.7 or higher\n") sys.exit(1) from tools.wpt import wpt From 472c40e4f6fb3c7a9e489605057debd81b75acdb Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Sun, 16 Apr 2023 17:44:24 +0200 Subject: [PATCH 040/259] perf: latin1 (#2075) --- lib/client.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/client.js b/lib/client.js index e630212fa79..688df9e6156 100644 --- a/lib/client.js +++ b/lib/client.js @@ -1451,17 +1451,17 @@ function write (client, request) { /* istanbul ignore else: assertion */ if (!body) { if (contentLength === 0) { - socket.write(`${header}content-length: 0\r\n\r\n`, 'ascii') + socket.write(`${header}content-length: 0\r\n\r\n`, 'latin1') } else { assert(contentLength === null, 'no body must not have content length') - socket.write(`${header}\r\n`, 'ascii') + socket.write(`${header}\r\n`, 'latin1') } request.onRequestSent() } else if (util.isBuffer(body)) { assert(contentLength === body.byteLength, 'buffer body must have content length') socket.cork() - socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'ascii') + socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1') socket.write(body) socket.uncork() request.onBodySent(body) @@ -1576,7 +1576,7 @@ async function writeBlob ({ body, client, request, socket, contentLength, header const buffer = Buffer.from(await body.arrayBuffer()) socket.cork() - socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'ascii') + socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1') socket.write(buffer) socket.uncork() @@ -1688,14 +1688,14 @@ class AsyncWriter { } if (contentLength === null) { - socket.write(`${header}transfer-encoding: chunked\r\n`, 'ascii') + socket.write(`${header}transfer-encoding: chunked\r\n`, 'latin1') } else { - socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'ascii') + socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1') } } if (contentLength === null) { - socket.write(`\r\n${len.toString(16)}\r\n`, 'ascii') + socket.write(`\r\n${len.toString(16)}\r\n`, 'latin1') } this.bytesWritten += len @@ -1739,12 +1739,12 @@ class AsyncWriter { // no Transfer-Encoding is sent and the request method defines a meaning // for an enclosed payload body. - socket.write(`${header}content-length: 0\r\n\r\n`, 'ascii') + socket.write(`${header}content-length: 0\r\n\r\n`, 'latin1') } else { - socket.write(`${header}\r\n`, 'ascii') + socket.write(`${header}\r\n`, 'latin1') } } else if (contentLength === null) { - socket.write('\r\n0\r\n\r\n', 'ascii') + socket.write('\r\n0\r\n\r\n', 'latin1') } if (contentLength !== null && bytesWritten !== contentLength) { From 9041e9fed85a69426242dc4ed7aaa7dea4b289d2 Mon Sep 17 00:00:00 2001 From: Khafra Date: Wed, 19 Apr 2023 00:33:37 -0400 Subject: [PATCH 041/259] fix: mock fetch headers shouldn't be an array (#2080) --- lib/fetch/headers.js | 15 ++++++++------- lib/fetch/index.js | 4 ++-- lib/fetch/symbols.js | 3 +-- test/issue-2078.js | 30 ++++++++++++++++++++++++++++++ 4 files changed, 41 insertions(+), 11 deletions(-) create mode 100644 test/issue-2078.js diff --git a/lib/fetch/headers.js b/lib/fetch/headers.js index 264ee9518ad..aa5e73e5d27 100644 --- a/lib/fetch/headers.js +++ b/lib/fetch/headers.js @@ -3,7 +3,7 @@ 'use strict' const { kHeadersList } = require('../core/symbols') -const { kGuard, kHeadersCaseInsensitive } = require('./symbols') +const { kGuard } = require('./symbols') const { kEnumerableProperty } = require('../core/util') const { makeIterator, @@ -173,15 +173,16 @@ class HeadersList { } } - get [kHeadersCaseInsensitive] () { - /** @type {string[]} */ - const flatList = [] + get entries () { + const headers = {} - for (const { name, value } of this[kHeadersMap].values()) { - flatList.push(name, value) + if (this[kHeadersMap].size) { + for (const { name, value } of this[kHeadersMap].values()) { + headers[name] = value + } } - return flatList + return headers } } diff --git a/lib/fetch/index.js b/lib/fetch/index.js index 0b2e3394322..f3016c60dde 100644 --- a/lib/fetch/index.js +++ b/lib/fetch/index.js @@ -42,7 +42,7 @@ const { urlIsHttpHttpsScheme, urlHasHttpsScheme } = require('./util') -const { kState, kHeaders, kGuard, kRealm, kHeadersCaseInsensitive } = require('./symbols') +const { kState, kHeaders, kGuard, kRealm } = require('./symbols') const assert = require('assert') const { safelyExtractBody } = require('./body') const { @@ -1950,7 +1950,7 @@ async function httpNetworkFetch ( origin: url.origin, method: request.method, body: fetchParams.controller.dispatcher.isMockActive ? request.body && request.body.source : body, - headers: request.headersList[kHeadersCaseInsensitive], + headers: request.headersList.entries, maxRedirections: 0, upgrade: request.mode === 'websocket' ? 'websocket' : undefined }, diff --git a/lib/fetch/symbols.js b/lib/fetch/symbols.js index e841ac730a7..0b947d55bad 100644 --- a/lib/fetch/symbols.js +++ b/lib/fetch/symbols.js @@ -6,6 +6,5 @@ module.exports = { kSignal: Symbol('signal'), kState: Symbol('state'), kGuard: Symbol('guard'), - kRealm: Symbol('realm'), - kHeadersCaseInsensitive: Symbol('headers case insensitive') + kRealm: Symbol('realm') } diff --git a/test/issue-2078.js b/test/issue-2078.js new file mode 100644 index 00000000000..d3aa868ef43 --- /dev/null +++ b/test/issue-2078.js @@ -0,0 +1,30 @@ +'use strict' + +const { test, skip } = require('tap') +const { nodeMajor, nodeMinor } = require('../lib/core/util') +const { MockAgent, getGlobalDispatcher, setGlobalDispatcher, fetch } = require('..') + +if (nodeMajor < 16 || (nodeMajor === 16 && nodeMinor < 8)) { + skip('fetch is not supported in node < v16.8.0') + process.exit() +} + +test('MockPool.reply headers are an object, not an array - issue #2078', async (t) => { + const global = getGlobalDispatcher() + const mockAgent = new MockAgent() + const mockPool = mockAgent.get('http://localhost') + + t.teardown(() => setGlobalDispatcher(global)) + setGlobalDispatcher(mockAgent) + + mockPool.intercept({ + path: '/foo', + method: 'GET' + }).reply((options) => { + t.ok(!Array.isArray(options.headers)) + + return { statusCode: 200 } + }) + + await t.resolves(fetch('http://localhost/foo')) +}) From 6870d5b4c58e976f99a16c0ec71051d4b7e2e628 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Thu, 20 Apr 2023 17:05:53 +0200 Subject: [PATCH 042/259] 5.22.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 463a40f1e23..481a6c88108 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "5.21.2", + "version": "5.22.0", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { From 6197e9350b5942e5ea3f378014713ac8e2f8f319 Mon Sep 17 00:00:00 2001 From: Khafra Date: Thu, 20 Apr 2023 14:18:50 -0400 Subject: [PATCH 043/259] Cache storage (#2076) * feat: implement basic outlines * lint * webidl cachestorage * cache: implement CacheStorage methods * cache: implement Cache.prototype.delete, partially? * cache: implement Cache.prototype.add/All, partially? * cache: implement Cache.prototype.put, partially? * cache: implement Cache.prototype.match/All, partially? * test: add cache-storage wpts * cache: re-do CacheStorage implementation * cache: remove unneeded utility * cache: fix put, matchAll, addAll * cache: fix multiple tests * cache: implement Cache.prototype.keys * cache: fix more bugs * cache: skip more tests * cache: fix more bugs, skip all failing tests --- lib/cache/cache.js | 834 +++++++++++++ lib/cache/cachestorage.js | 133 +++ lib/cache/symbols.js | 5 + lib/cache/util.js | 49 + lib/fetch/response.js | 3 +- lib/fetch/util.js | 3 +- lib/fetch/webidl.js | 7 + test/wpt/runner/worker.mjs | 15 + test/wpt/server/server.mjs | 17 +- test/wpt/start-cacheStorage.mjs | 26 + .../service-workers/cache-storage.status.json | 54 + test/wpt/tests/service-workers/META.yml | 6 + .../service-workers/cache-storage/META.yml | 3 + .../cache-storage/cache-abort.https.any.js | 81 ++ .../cache-storage/cache-add.https.any.js | 368 ++++++ .../cache-storage/cache-delete.https.any.js | 164 +++ ...s-attributes-for-service-worker.https.html | 75 ++ .../cache-storage/cache-keys.https.any.js | 212 ++++ .../cache-storage/cache-match.https.any.js | 437 +++++++ .../cache-storage/cache-matchAll.https.any.js | 244 ++++ .../cache-storage/cache-put.https.any.js | 411 +++++++ .../cache-storage-buckets.https.any.js | 64 + .../cache-storage-keys.https.any.js | 35 + .../cache-storage-match.https.any.js | 245 ++++ .../cache-storage/cache-storage.https.any.js | 239 ++++ .../cache-storage/common.https.window.js | 44 + .../cache-response-clone.https.html | 17 + .../cache-storage/credentials.https.html | 46 + .../cross-partition.https.tentative.html | 269 +++++ .../cache-storage/resources/blank.html | 2 + ...ache-keys-attributes-for-service-worker.js | 22 + .../cache-storage/resources/common-worker.js | 15 + .../resources/credentials-iframe.html | 38 + .../resources/credentials-worker.js | 59 + .../cache-storage/resources/fetch-status.py | 2 + .../cache-storage/resources/iframe.html | 18 + .../cache-storage/resources/simple.txt | 1 + .../cache-storage/resources/test-helpers.js | 272 +++++ .../cache-storage/resources/vary.py | 25 + .../sandboxed-iframes.https.html | 66 ++ .../service-workers/idlharness.https.any.js | 53 + .../Service-Worker-Allowed-header.https.html | 88 ++ .../ServiceWorkerGlobalScope/close.https.html | 11 + ...dable-message-event-constructor.https.html | 10 + .../extendable-message-event.https.html | 226 ++++ .../fetch-on-the-right-interface.https.any.js | 14 + .../isSecureContext.https.html | 32 + .../isSecureContext.serviceworker.js | 5 + .../postmessage.https.html | 83 ++ .../registration-attribute.https.html | 107 ++ .../resources/close-worker.js | 5 + .../resources/error-worker.js | 12 + ...ndable-message-event-constructor-worker.js | 197 ++++ ...xtendable-message-event-loopback-worker.js | 36 + .../extendable-message-event-ping-worker.js | 23 + .../extendable-message-event-pong-worker.js | 18 + .../extendable-message-event-utils.js | 78 ++ .../extendable-message-event-worker.js | 5 + .../resources/postmessage-loopback-worker.js | 15 + .../resources/postmessage-ping-worker.js | 15 + .../resources/postmessage-pong-worker.js | 4 + .../registration-attribute-newer-worker.js | 33 + .../registration-attribute-worker.js | 139 +++ .../unregister-controlling-worker.html | 0 .../resources/unregister-worker.js | 25 + .../resources/update-worker.js | 22 + .../resources/update-worker.py | 16 + .../service-worker-error-event.https.html | 31 + .../unregister.https.html | 139 +++ .../update.https.html | 48 + .../about-blank-replacement.https.html | 181 +++ ...vent-after-install-state-change.https.html | 33 + .../activation-after-registration.https.html | 28 + .../service-worker/activation.https.html | 168 +++ .../service-worker/active.https.html | 50 + ...claim-affect-other-registration.https.html | 136 +++ .../service-worker/claim-fetch.https.html | 90 ++ .../claim-not-using-registration.https.html | 131 +++ .../claim-shared-worker-fetch.https.html | 71 ++ .../claim-using-registration.https.html | 103 ++ .../claim-with-redirect.https.html | 59 + .../claim-worker-fetch.https.html | 83 ++ .../service-worker/client-id.https.html | 60 + .../service-worker/client-navigate.https.html | 107 ++ .../client-url-of-blob-url-worker.https.html | 29 + .../clients-get-client-types.https.html | 108 ++ .../clients-get-cross-origin.https.html | 69 ++ .../clients-get-resultingClientId.https.html | 177 +++ .../service-worker/clients-get.https.html | 154 +++ ...lients-matchall-blob-url-worker.https.html | 85 ++ .../clients-matchall-client-types.https.html | 92 ++ ...ients-matchall-exact-controller.https.html | 67 ++ .../clients-matchall-frozen.https.html | 64 + ...s-matchall-include-uncontrolled.https.html | 117 ++ .../clients-matchall-on-evaluation.https.html | 24 + .../clients-matchall-order.https.html | 427 +++++++ .../clients-matchall.https.html | 50 + .../controller-on-disconnect.https.html | 40 + .../controller-on-load.https.html | 46 + .../controller-on-reload.https.html | 58 + ...ler-with-no-fetch-event-handler.https.html | 56 + .../service-worker/credentials.https.html | 100 ++ .../service-worker/data-iframe.html | 25 + .../data-transfer-files.https.html | 41 + ...ker-service-worker-interception.https.html | 40 + .../detached-context.https.html | 141 +++ ...-and-object-are-not-intercepted.https.html | 104 ++ ...xtendable-event-async-waituntil.https.html | 120 ++ .../extendable-event-waituntil.https.html | 140 +++ .../fetch-audio-tainting.https.html | 47 + ...ch-canvas-tainting-double-write.https.html | 57 + ...tch-canvas-tainting-image-cache.https.html | 16 + .../fetch-canvas-tainting-image.https.html | 16 + ...tch-canvas-tainting-video-cache.https.html | 17 + ...inting-video-with-range-request.https.html | 92 ++ .../fetch-canvas-tainting-video.https.html | 17 + ...fetch-cors-exposed-header-names.https.html | 30 + .../service-worker/fetch-cors-xhr.https.html | 49 + .../service-worker/fetch-csp.https.html | 138 +++ .../service-worker/fetch-error.https.html | 33 + .../fetch-event-add-async.https.html | 11 + ...nt-after-navigation-within-page.https.html | 71 ++ .../fetch-event-async-respond-with.https.html | 73 ++ .../fetch-event-handled.https.html | 86 ++ ...tory-backward-navigation-manual.https.html | 8 + ...story-forward-navigation-manual.https.html | 8 + ...reload-iframe-navigation-manual.https.html | 31 + ...ent-is-reload-navigation-manual.https.html | 8 + .../fetch-event-network-error.https.html | 44 + .../fetch-event-redirect.https.html | 1038 +++++++++++++++++ .../fetch-event-referrer-policy.https.html | 274 +++++ ...tch-event-respond-with-argument.https.html | 44 + ...spond-with-body-loaded-in-chunk.https.html | 24 + ...nt-respond-with-custom-response.https.html | 82 ++ ...ent-respond-with-partial-stream.https.html | 62 + ...pond-with-readable-stream-chunk.https.html | 23 + ...nt-respond-with-readable-stream.https.html | 88 ++ ...esponse-body-with-invalid-chunk.https.html | 46 + ...-respond-with-stops-propagation.https.html | 37 + ...event-throws-after-respond-with.https.html | 37 + .../fetch-event-within-sw-manual.https.html | 122 ++ .../fetch-event-within-sw.https.html | 53 + .../service-worker/fetch-event.https.h2.html | 112 ++ .../service-worker/fetch-event.https.html | 1000 ++++++++++++++++ .../fetch-frame-resource.https.html | 236 ++++ .../fetch-header-visibility.https.html | 54 + .../fetch-mixed-content-to-inscope.https.html | 21 + ...fetch-mixed-content-to-outscope.https.html | 21 + .../fetch-request-css-base-url.https.html | 87 ++ .../fetch-request-css-cross-origin.https.html | 81 ++ .../fetch-request-css-images.https.html | 214 ++++ .../fetch-request-fallback.https.html | 282 +++++ ...ch-request-no-freshness-headers.https.html | 55 + .../fetch-request-redirect.https.html | 385 ++++++ .../fetch-request-resources.https.html | 302 +++++ ...tch-request-xhr-sync-error.https.window.js | 19 + ...etch-request-xhr-sync-on-worker.https.html | 41 + .../fetch-request-xhr-sync.https.html | 53 + .../fetch-request-xhr.https.html | 75 ++ .../fetch-response-taint.https.html | 223 ++++ .../fetch-response-xhr.https.html | 50 + .../fetch-waits-for-activate.https.html | 128 ++ .../service-worker/getregistration.https.html | 108 ++ .../getregistrations.https.html | 134 +++ .../global-serviceworker.https.any.js | 53 + .../service-worker/historical.https.any.js | 5 + ...-to-https-redirect-and-register.https.html | 49 + ...mutable-prototype-serviceworker.https.html | 23 + .../import-scripts-cross-origin.https.html | 18 + .../import-scripts-data-url.https.html | 18 + .../import-scripts-mime-types.https.html | 30 + .../import-scripts-redirect.https.html | 55 + .../import-scripts-resource-map.https.html | 34 + .../import-scripts-updated-flag.https.html | 83 ++ .../service-worker/indexeddb.https.html | 78 ++ .../install-event-type.https.html | 30 + .../service-worker/installing.https.html | 48 + .../interface-requirements-sw.https.html | 16 + .../invalid-blobtype.https.html | 40 + .../service-worker/invalid-header.https.html | 39 + .../iso-latin1-header.https.html | 40 + .../local-url-inherit-controller.https.html | 115 ++ .../service-worker/mime-sniffing.https.html | 24 + .../multi-globals/current/current.https.html | 2 + .../multi-globals/current/test-sw.js | 1 + .../incumbent/incumbent.https.html | 20 + .../multi-globals/incumbent/test-sw.js | 1 + .../relevant/relevant.https.html | 2 + .../multi-globals/relevant/test-sw.js | 1 + .../service-worker/multi-globals/test-sw.js | 1 + .../multi-globals/url-parsing.https.html | 73 ++ .../service-worker/multipart-image.https.html | 68 ++ .../multiple-register.https.html | 117 ++ .../service-worker/multiple-update.https.html | 94 ++ .../service-worker/navigate-window.https.html | 151 +++ .../navigation-headers.https.html | 819 +++++++++++++ .../broken-chunked-encoding.https.html | 42 + .../chunked-encoding.https.html | 25 + .../empty-preload-response-body.https.html | 25 + .../navigation-preload/get-state.https.html | 217 ++++ .../navigationPreload.https.html | 20 + .../navigation-preload/redirect.https.html | 93 ++ .../request-headers.https.html | 41 + .../resource-timing.https.html | 92 ++ .../broken-chunked-encoding-scope.asis | 6 + .../broken-chunked-encoding-worker.js | 11 + .../resources/chunked-encoding-scope.py | 19 + .../resources/chunked-encoding-worker.js | 8 + .../navigation-preload/resources/cookie.py | 20 + .../empty-preload-response-body-scope.html | 0 .../empty-preload-response-body-worker.js | 15 + .../resources/get-state-worker.js | 21 + .../navigation-preload/resources/helpers.js | 5 + .../resources/navigation-preload-worker.js | 3 + .../resources/redirect-redirected.html | 3 + .../resources/redirect-scope.py | 38 + .../resources/redirect-worker.js | 35 + .../resources/request-headers-scope.py | 14 + .../resources/request-headers-worker.js | 10 + .../resources/resource-timing-scope.py | 19 + .../resources/resource-timing-worker.js | 37 + .../resources/samesite-iframe.html | 10 + .../resources/samesite-sw-helper.html | 34 + .../resources/samesite-worker.js | 8 + .../resources/wait-for-activate-worker.js | 40 + .../samesite-cookies.https.html | 61 + .../samesite-iframe.https.html | 67 ++ .../navigation-redirect-body.https.html | 53 + .../navigation-redirect-resolution.https.html | 58 + .../navigation-redirect-to-http.https.html | 25 + .../navigation-redirect.https.html | 846 ++++++++++++++ .../navigation-sets-cookie.https.html | 133 +++ .../navigation-timing-extended.https.html | 55 + .../navigation-timing.https.html | 76 ++ .../nested-blob-url-workers.https.html | 42 + .../next-hop-protocol.https.html | 49 + .../no-dynamic-import-in-module.any.js | 7 + .../service-worker/no-dynamic-import.any.js | 3 + .../onactivate-script-error.https.html | 74 ++ .../oninstall-script-error.https.html | 72 ++ .../opaque-response-preloaded.https.html | 50 + .../service-worker/opaque-script.https.html | 71 ++ .../partitioned-claim.tentative.https.html | 74 ++ ...oned-getRegistrations.tentative.https.html | 99 ++ .../partitioned-matchAll.tentative.https.html | 65 ++ .../partitioned.tentative.https.html | 188 +++ .../performance-timeline.https.html | 49 + .../postmessage-blob-url.https.html | 33 + ...sage-from-waiting-serviceworker.https.html | 50 + .../postmessage-msgport-to-client.https.html | 43 + ...message-to-client-message-queue.https.html | 212 ++++ .../postmessage-to-client.https.html | 42 + .../service-worker/postmessage.https.html | 202 ++++ .../service-worker/ready.https.window.js | 223 ++++ .../redirected-response.https.html | 471 ++++++++ .../service-worker/referer.https.html | 40 + .../referrer-policy-header.https.html | 67 ++ .../referrer-toplevel-script-fetch.https.html | 64 + .../register-closed-window.https.html | 35 + .../register-default-scope.https.html | 69 ++ ...same-scope-different-script-url.https.html | 233 ++++ ...-wait-forever-in-install-worker.https.html | 57 + .../registration-basic.https.html | 39 + .../registration-end-to-end.https.html | 88 ++ .../registration-events.https.html | 42 + .../registration-iframe.https.html | 116 ++ .../registration-mime-types.https.html | 10 + .../registration-schedule-job.https.html | 107 ++ ...tion-scope-module-static-import.https.html | 41 + .../registration-scope.https.html | 9 + .../registration-script-module.https.html | 13 + .../registration-script-url.https.html | 9 + .../registration-script.https.html | 12 + .../registration-security-error.https.html | 9 + ...ation-service-worker-attributes.https.html | 72 ++ .../registration-updateviacache.https.html | 204 ++++ .../service-worker/rejections.https.html | 21 + .../request-end-to-end.https.html | 40 + .../resource-timing-bodySize.https.html | 55 + .../resource-timing-cross-origin.https.html | 46 + .../resource-timing-fetch-variants.https.html | 121 ++ .../resource-timing.sub.https.html | 150 +++ .../service-worker/resources/404.py | 5 + ...eplacement-blank-dynamic-nested-frame.html | 21 + ...-blank-replacement-blank-nested-frame.html | 21 + .../about-blank-replacement-frame.py | 31 + .../about-blank-replacement-ping-frame.py | 49 + .../about-blank-replacement-popup-frame.py | 32 + ...blank-replacement-srcdoc-nested-frame.html | 22 + ...replacement-uncontrolled-nested-frame.html | 22 + .../about-blank-replacement-worker.js | 95 ++ .../resources/basic-module-2.js | 1 + .../service-worker/resources/basic-module.js | 1 + .../service-worker/resources/blank.html | 2 + .../bytecheck-worker-imported-script.py | 20 + .../resources/bytecheck-worker.py | 38 + .../claim-blob-url-worker-fetch-iframe.html | 21 + .../claim-nested-worker-fetch-iframe.html | 16 + ...claim-nested-worker-fetch-parent-worker.js | 12 + .../claim-shared-worker-fetch-iframe.html | 13 + .../claim-shared-worker-fetch-worker.js | 8 + .../resources/claim-with-redirect-iframe.html | 48 + .../resources/claim-worker-fetch-iframe.html | 13 + .../resources/claim-worker-fetch-worker.js | 5 + .../service-worker/resources/claim-worker.js | 19 + .../resources/classic-worker.js | 1 + .../resources/client-id-worker.js | 27 + .../resources/client-navigate-frame.html | 12 + .../resources/client-navigate-worker.js | 92 ++ .../resources/client-navigated-frame.html | 3 + .../client-url-of-blob-url-worker.html | 26 + .../client-url-of-blob-url-worker.js | 10 + .../resources/clients-frame-freeze.html | 15 + .../clients-get-client-types-frame-worker.js | 11 + .../clients-get-client-types-frame.html | 17 + .../clients-get-client-types-shared-worker.js | 10 + .../clients-get-client-types-worker.js | 11 + .../clients-get-cross-origin-frame.html | 50 + .../resources/clients-get-frame.html | 12 + .../resources/clients-get-other-origin.html | 64 + .../clients-get-resultingClientId-worker.js | 60 + .../resources/clients-get-worker.js | 41 + .../clients-matchall-blob-url-worker.html | 20 + ...-matchall-client-types-dedicated-worker.js | 3 + .../clients-matchall-client-types-iframe.html | 8 + ...nts-matchall-client-types-shared-worker.js | 4 + .../clients-matchall-on-evaluation-worker.js | 11 + .../resources/clients-matchall-worker.js | 40 + .../resources/cors-approved.txt | 1 + .../resources/cors-approved.txt.headers | 3 + .../service-worker/resources/cors-denied.txt | 2 + .../resources/create-blob-url-worker.js | 22 + .../resources/create-out-of-scope-worker.html | 19 + .../service-worker/resources/echo-content.py | 16 + .../resources/echo-cookie-worker.py | 24 + .../echo-message-to-source-worker.js | 3 + ...d-and-object-are-not-intercepted-worker.js | 14 + ...embed-image-is-not-intercepted-iframe.html | 21 + .../embed-is-not-intercepted-iframe.html | 17 + ...-navigation-is-not-intercepted-iframe.html | 23 + .../embedded-content-from-server.html | 6 + .../embedded-content-from-service-worker.html | 7 + .../resources/empty-but-slow-worker.js | 8 + .../service-worker/resources/empty-worker.js | 1 + .../service-worker/resources/empty.h2.js | 0 .../service-worker/resources/empty.html | 6 + .../service-worker/resources/empty.js | 0 .../enable-client-message-queue.html | 39 + .../resources/end-to-end-worker.js | 7 + .../service-worker/resources/events-worker.js | 12 + .../extendable-event-async-waituntil.js | 210 ++++ .../resources/extendable-event-waituntil.js | 87 ++ .../resources/fail-on-fetch-worker.js | 5 + .../resources/fetch-access-control-login.html | 16 + .../resources/fetch-access-control.py | 109 ++ ...tch-canvas-tainting-double-write-worker.js | 7 + .../fetch-canvas-tainting-iframe.html | 70 ++ .../resources/fetch-canvas-tainting-tests.js | 241 ++++ .../fetch-cors-exposed-header-names-worker.js | 3 + .../resources/fetch-cors-xhr-iframe.html | 170 +++ .../resources/fetch-csp-iframe.html | 16 + .../fetch-csp-iframe.html.sub.headers | 1 + .../resources/fetch-error-worker.js | 22 + .../resources/fetch-event-add-async-worker.js | 6 + ...t-after-navigation-within-page-iframe.html | 22 + .../fetch-event-async-respond-with-worker.js | 66 ++ .../resources/fetch-event-handled-worker.js | 37 + ...event-network-error-controllee-iframe.html | 60 + .../fetch-event-network-error-worker.js | 49 + .../fetch-event-network-fallback-worker.js | 3 + ...ch-event-respond-with-argument-iframe.html | 55 + ...etch-event-respond-with-argument-worker.js | 14 + ...espond-with-body-loaded-in-chunk-worker.js | 7 + ...ent-respond-with-custom-response-worker.js | 45 + ...vent-respond-with-partial-stream-worker.js | 28 + ...spond-with-readable-stream-chunk-worker.js | 40 + ...ent-respond-with-readable-stream-worker.js | 81 ++ ...sponse-body-with-invalid-chunk-iframe.html | 15 + ...response-body-with-invalid-chunk-worker.js | 12 + ...t-respond-with-stops-propagation-worker.js | 15 + .../resources/fetch-event-test-worker.js | 224 ++++ .../resources/fetch-event-within-sw-worker.js | 48 + .../fetch-header-visibility-iframe.html | 66 ++ ...xed-content-iframe-inscope-to-inscope.html | 71 ++ ...ed-content-iframe-inscope-to-outscope.html | 80 ++ .../resources/fetch-mixed-content-iframe.html | 71 ++ .../fetch-request-css-base-url-iframe.html | 20 + .../fetch-request-css-base-url-style.css | 1 + .../fetch-request-css-base-url-worker.js | 45 + ...uest-css-cross-origin-mime-check-cross.css | 1 + ...est-css-cross-origin-mime-check-cross.html | 1 + ...st-css-cross-origin-mime-check-iframe.html | 17 + ...quest-css-cross-origin-mime-check-same.css | 1 + ...uest-css-cross-origin-mime-check-same.html | 1 + ...equest-css-cross-origin-read-contents.html | 15 + .../fetch-request-css-cross-origin-worker.js | 65 ++ .../fetch-request-fallback-iframe.html | 32 + .../fetch-request-fallback-worker.js | 13 + .../fetch-request-html-imports-iframe.html | 13 + .../fetch-request-html-imports-worker.js | 30 + ...h-request-no-freshness-headers-iframe.html | 1 + ...tch-request-no-freshness-headers-script.py | 6 + ...tch-request-no-freshness-headers-worker.js | 18 + .../fetch-request-redirect-iframe.html | 35 + .../fetch-request-resources-iframe.https.html | 87 ++ .../fetch-request-resources-worker.js | 26 + .../fetch-request-xhr-iframe.https.html | 208 ++++ .../fetch-request-xhr-sync-error-worker.js | 19 + .../fetch-request-xhr-sync-iframe.html | 13 + ...fetch-request-xhr-sync-on-worker-worker.js | 41 + .../fetch-request-xhr-sync-worker.js | 7 + .../resources/fetch-request-xhr-worker.js | 22 + .../fetch-response-taint-iframe.html | 2 + .../fetch-response-xhr-iframe.https.html | 53 + .../resources/fetch-response-xhr-worker.js | 12 + .../resources/fetch-response.html | 29 + .../resources/fetch-response.js | 35 + .../fetch-rewrite-worker-referrer-policy.js | 4 + ...-rewrite-worker-referrer-policy.js.headers | 2 + .../resources/fetch-rewrite-worker.js | 166 +++ .../resources/fetch-rewrite-worker.js.headers | 2 + .../resources/fetch-variants-worker.js | 35 + .../fetch-waits-for-activate-worker.js | 31 + .../service-worker/resources/form-poster.html | 13 + .../resources/frame-for-getregistrations.html | 19 + .../resources/get-resultingClientId-worker.js | 107 ++ ...to-https-redirect-and-register-iframe.html | 25 + .../resources/iframe-with-fetch-variants.html | 14 + .../resources/iframe-with-image.html | 2 + .../immutable-prototype-serviceworker.js | 19 + .../import-echo-cookie-worker-module.py | 6 + .../resources/import-echo-cookie-worker.js | 1 + .../resources/import-mime-type-worker.py | 10 + .../resources/import-relative.xsl | 5 + ...pts-404-after-update-plus-update-worker.js | 8 + .../import-scripts-404-after-update.js | 6 + .../resources/import-scripts-404.js | 1 + .../import-scripts-cross-origin-worker.sub.js | 1 + .../import-scripts-data-url-worker.js | 1 + ...import-scripts-diff-resource-map-worker.js | 10 + .../resources/import-scripts-echo.py | 6 + .../resources/import-scripts-get.py | 6 + .../import-scripts-mime-types-worker.js | 49 + .../import-scripts-redirect-import.js | 1 + ...-scripts-redirect-on-second-time-worker.js | 7 + .../import-scripts-redirect-worker.js | 1 + .../import-scripts-resource-map-worker.js | 15 + .../import-scripts-updated-flag-worker.js | 31 + .../resources/import-scripts-version.py | 17 + .../resources/imported-classic-script.js | 1 + .../resources/imported-module-script.js | 1 + .../resources/indexeddb-worker.js | 57 + .../resources/install-event-type-worker.js | 9 + .../resources/install-worker.html | 22 + .../interface-requirements-worker.sub.js | 59 + .../invalid-blobtype-iframe.https.html | 28 + .../resources/invalid-blobtype-worker.js | 10 + .../invalid-chunked-encoding-with-flush.py | 9 + .../resources/invalid-chunked-encoding.py | 2 + .../invalid-header-iframe.https.html | 25 + .../resources/invalid-header-worker.js | 12 + .../resources/iso-latin1-header-iframe.html | 23 + .../resources/iso-latin1-header-worker.js | 12 + .../service-worker/resources/load_worker.js | 29 + .../service-worker/resources/loaded.html | 9 + .../local-url-inherit-controller-frame.html | 130 +++ .../local-url-inherit-controller-worker.js | 5 + .../resources/location-setter.html | 10 + .../resources/malformed-http-response.asis | 1 + .../resources/malformed-worker.py | 14 + .../resources/message-vs-microtask.html | 18 + .../resources/mime-sniffing-worker.js | 9 + .../resources/mime-type-worker.py | 4 + .../resources/mint-new-worker.py | 27 + .../service-worker/resources/module-worker.js | 1 + .../resources/multipart-image-iframe.html | 19 + .../resources/multipart-image-worker.js | 21 + .../resources/multipart-image.py | 23 + .../resources/navigate-window-worker.js | 21 + .../resources/navigation-headers-server.py | 19 + .../navigation-redirect-body-worker.js | 11 + .../resources/navigation-redirect-body.py | 11 + .../navigation-redirect-other-origin.html | 89 ++ .../navigation-redirect-out-scope.py | 22 + .../resources/navigation-redirect-scope1.py | 22 + .../resources/navigation-redirect-scope2.py | 22 + .../navigation-redirect-to-http-iframe.html | 42 + .../navigation-redirect-to-http-worker.js | 22 + .../navigation-timing-worker-extended.js | 22 + .../resources/navigation-timing-worker.js | 15 + ...d-blob-url-worker-created-from-worker.html | 16 + .../resources/nested-blob-url-workers.html | 38 + .../resources/nested-iframe-parent.html | 5 + .../resources/nested-parent.html | 18 + ...d-worker-created-from-blob-url-worker.html | 33 + .../resources/nested_load_worker.js | 23 + .../resources/no-dynamic-import.js | 18 + .../resources/notification_icon.py | 11 + ...bject-image-is-not-intercepted-iframe.html | 21 + .../object-is-not-intercepted-iframe.html | 18 + ...-navigation-is-not-intercepted-iframe.html | 24 + ...te-throw-error-from-nested-event-worker.js | 13 + ...activate-throw-error-then-cancel-worker.js | 3 + ...throw-error-then-prevent-default-worker.js | 7 + ...e-throw-error-with-empty-onerror-worker.js | 2 + .../onactivate-throw-error-worker.js | 7 + .../resources/onactivate-waituntil-forever.js | 8 + .../resources/onfetch-waituntil-forever.js | 10 + ...ll-throw-error-from-nested-event-worker.js | 12 + ...ninstall-throw-error-then-cancel-worker.js | 3 + ...throw-error-then-prevent-default-worker.js | 7 + ...l-throw-error-with-empty-onerror-worker.js | 2 + .../resources/oninstall-throw-error-worker.js | 7 + .../resources/oninstall-waituntil-forever.js | 8 + .../oninstall-waituntil-throw-error-worker.js | 5 + .../resources/onparse-infiniteloop-worker.js | 8 + .../opaque-response-being-preloaded-xhr.html | 33 + .../opaque-response-preloaded-worker.js | 12 + .../opaque-response-preloaded-xhr.html | 35 + .../resources/opaque-script-frame.html | 21 + .../resources/opaque-script-large.js | 41 + .../resources/opaque-script-small.js | 3 + .../resources/opaque-script-sw.js | 37 + .../service-worker/resources/other.html | 3 + .../override_assert_object_equals.js | 58 + ...rtitioned-service-worker-iframe-claim.html | 59 + ...ed-service-worker-nested-iframe-child.html | 44 + ...d-service-worker-nested-iframe-parent.html | 30 + ...r-third-party-iframe-getRegistrations.html | 40 + ...ce-worker-third-party-iframe-matchAll.html | 27 + ...ned-service-worker-third-party-iframe.html | 36 + ...ned-service-worker-third-party-window.html | 41 + .../resources/partitioned-storage-sw.js | 81 ++ .../resources/partitioned-utils.js | 110 ++ .../resources/pass-through-worker.js | 3 + .../service-worker/resources/pass.txt | 1 + .../resources/performance-timeline-worker.js | 62 + .../resources/postmessage-blob-url.js | 5 + ...message-dictionary-transferables-worker.js | 24 + .../resources/postmessage-echo-worker.js | 3 + .../resources/postmessage-fetched-text.js | 5 + .../postmessage-msgport-to-client-worker.js | 19 + .../resources/postmessage-on-load-worker.js | 9 + .../resources/postmessage-to-client-worker.js | 10 + .../postmessage-transferables-worker.js | 24 + .../resources/postmessage-worker.js | 19 + ...nge-request-to-different-origins-worker.js | 40 + ...equest-with-different-cors-modes-worker.js | 60 + .../resources/redirect-worker.js | 145 +++ .../service-worker/resources/redirect.py | 27 + .../resources/referer-iframe.html | 39 + .../resources/referrer-policy-iframe.html | 32 + .../register-closed-window-iframe.html | 19 + .../resources/register-iframe.html | 4 + .../resources/register-rewrite-worker.html | 32 + .../registration-tests-mime-types.js | 96 ++ .../resources/registration-tests-scope.js | 120 ++ .../registration-tests-script-url.js | 82 ++ .../resources/registration-tests-script.js | 121 ++ .../registration-tests-security-error.js | 78 ++ .../resources/registration-worker.js | 1 + .../resources/reject-install-worker.js | 3 + .../resources/reply-to-message.html | 7 + .../resources/request-end-to-end-worker.js | 34 + .../resources/request-headers.py | 8 + .../resources/resource-timing-iframe.sub.html | 10 + .../resources/resource-timing-worker.js | 12 + .../resources/respond-then-throw-worker.js | 40 + ...nd-with-body-accessed-response-iframe.html | 20 + ...pond-with-body-accessed-response-worker.js | 93 ++ .../respond-with-body-accessed-response.jsonp | 1 + .../resources/sample-worker-interceptor.js | 62 + .../service-worker/resources/sample.html | 2 + .../service-worker/resources/sample.js | 1 + .../service-worker/resources/sample.txt | 1 + .../sandboxed-iframe-fetch-event-iframe.html | 63 + .../sandboxed-iframe-fetch-event-iframe.py | 18 + .../sandboxed-iframe-fetch-event-worker.js | 20 + ...iframe-navigator-serviceworker-iframe.html | 25 + ...ule-worker-importing-redirect-to-scope2.js | 1 + .../scope1/module-worker-importing-scope2.js | 1 + .../resources/scope1/redirect.py | 6 + .../resources/scope2/import-scripts-echo.py | 6 + .../scope2/imported-module-script.js | 4 + .../resources/scope2/simple.txt | 1 + .../worker_interception_redirect_webworker.py | 6 + .../secure-context-service-worker.js | 21 + .../resources/secure-context/sender.html | 1 + .../resources/secure-context/window.html | 15 + .../resources/service-worker-csp-worker.py | 183 +++ .../resources/service-worker-header.py | 20 + ...rker-interception-dynamic-import-worker.js | 1 + ...vice-worker-interception-network-worker.js | 1 + ...vice-worker-interception-service-worker.js | 9 + ...orker-interception-static-import-worker.js | 1 + .../service-worker/resources/silence.oga | Bin 0 -> 12983 bytes .../resources/simple-intercept-worker.js | 5 + .../simple-intercept-worker.js.headers | 1 + .../service-worker/resources/simple.html | 3 + .../service-worker/resources/simple.txt | 1 + .../skip-waiting-installed-worker.js | 33 + .../resources/skip-waiting-worker.js | 21 + .../service-worker/resources/square.png | Bin 0 -> 18299 bytes .../resources/square.png.sub.headers | 2 + .../resources/stalling-service-worker.js | 54 + .../resources/subdir/blank.html | 2 + .../resources/subdir/import-scripts-echo.py | 6 + .../resources/subdir/simple.txt | 1 + .../worker_interception_redirect_webworker.py | 6 + .../service-worker/resources/success.py | 8 + .../svg-target-reftest-001-frame.html | 3 + .../resources/svg-target-reftest-001.html | 5 + .../resources/svg-target-reftest-frame.html | 2 + .../resources/test-helpers.sub.js | 300 +++++ .../resources/test-request-headers-worker.js | 10 + .../resources/test-request-headers-worker.py | 21 + .../resources/test-request-mode-worker.js | 10 + .../resources/test-request-mode-worker.py | 22 + .../resources/testharness-helpers.js | 136 +++ .../service-worker/resources/trickle.py | 14 + .../resources/type-check-worker.js | 10 + .../resources/unregister-controller-page.html | 16 + .../unregister-immediately-helpers.js | 19 + .../resources/unregister-rewrite-worker.html | 18 + .../resources/update-claim-worker.py | 24 + .../update-during-installation-worker.js | 61 + .../update-during-installation-worker.py | 11 + .../resources/update-fetch-worker.py | 18 + .../update-max-aged-worker-imported-script.py | 14 + .../resources/update-max-aged-worker.py | 30 + ...-missing-import-scripts-imported-worker.py | 9 + ...date-missing-import-scripts-main-worker.py | 15 + .../resources/update-nocookie-worker.py | 14 + .../resources/update-recovery-worker.py | 25 + .../update-registration-with-type.py | 33 + ...update-smaller-body-after-update-worker.js | 1 + ...pdate-smaller-body-before-update-worker.js | 2 + .../resources/update-worker-from-file.py | 33 + .../service-worker/resources/update-worker.py | 62 + .../update/update-after-oneday.https.html | 8 + .../service-worker/resources/update_shell.py | 32 + .../service-worker/resources/vtt-frame.html | 6 + .../wait-forever-in-install-worker.js | 12 + .../resources/websocket-worker.js | 35 + .../service-worker/resources/websocket.js | 7 + .../resources/window-opener.html | 17 + .../resources/windowclient-navigate-worker.js | 75 ++ .../resources/worker-client-id-worker.js | 25 + .../resources/worker-fetching-cross-origin.js | 12 + ...ker-interception-redirect-serviceworker.js | 53 + .../worker-interception-redirect-webworker.js | 56 + .../resources/worker-load-interceptor.js | 16 + .../resources/worker-testharness.js | 49 + .../worker_interception_redirect_webworker.py | 20 + .../resources/xhr-content-length-worker.js | 22 + .../service-worker/resources/xhr-iframe.html | 23 + .../resources/xhr-response-url-worker.js | 32 + .../resources/xsl-base-url-iframe.xml | 5 + .../resources/xsl-base-url-worker.js | 12 + .../service-worker/resources/xslt-pass.xsl | 11 + ...ond-with-body-accessed-response.https.html | 54 + .../same-site-cookies.https.html | 496 ++++++++ .../sandboxed-iframe-fetch-event.https.html | 536 +++++++++ ...-iframe-navigator-serviceworker.https.html | 120 ++ .../service-worker/secure-context.https.html | 57 + .../service-worker-csp-connect.https.html | 10 + .../service-worker-csp-default.https.html | 10 + .../service-worker-csp-script.https.html | 10 + .../service-worker-header.https.html | 23 + ...worker-message-event-historical.https.html | 45 + .../serviceworkerobject-scripturl.https.html | 26 + .../skip-waiting-installed.https.html | 70 ++ ...skip-waiting-using-registration.https.html | 66 ++ .../skip-waiting-without-client.https.html | 12 + ...ting-without-using-registration.https.html | 44 + .../service-worker/skip-waiting.https.html | 58 + .../service-worker/state.https.html | 74 ++ .../svg-target-reftest.https.html | 28 + .../service-worker/synced-state.https.html | 93 ++ .../uncontrolled-page.https.html | 39 + .../unregister-controller.https.html | 108 ++ ...er-immediately-before-installed.https.html | 57 + ...iately-during-extendable-events.https.html | 50 + .../unregister-immediately.https.html | 134 +++ ...gister-then-register-new-script.https.html | 136 +++ .../unregister-then-register.https.html | 107 ++ .../service-worker/unregister.https.html | 40 + ...te-after-navigation-fetch-event.https.html | 91 ++ ...pdate-after-navigation-redirect.https.html | 74 ++ .../update-after-oneday.https.html | 51 + .../update-bytecheck-cors-import.https.html | 92 ++ .../update-bytecheck.https.html | 92 ++ .../update-import-scripts.https.html | 135 +++ .../update-missing-import-scripts.https.html | 33 + .../update-module-request-mode.https.html | 45 + ...update-no-cache-request-headers.https.html | 48 + .../update-not-allowed.https.html | 140 +++ .../update-on-navigation.https.html | 20 + .../service-worker/update-recovery.https.html | 73 ++ .../update-registration-with-type.https.html | 208 ++++ .../service-worker/update-result.https.html | 23 + .../service-worker/update.https.html | 164 +++ .../service-worker/waiting.https.html | 47 + .../websocket-in-service-worker.https.html | 27 + .../service-worker/websocket.https.html | 45 + .../webvtt-cross-origin.https.html | 175 +++ .../windowclient-navigate.https.html | 190 +++ .../worker-client-id.https.html | 58 + ...boxed-iframe-by-csp-fetch-event.https.html | 132 +++ .../worker-interception-redirect.https.html | 212 ++++ .../worker-interception.https.html | 244 ++++ .../xhr-content-length.https.window.js | 55 + .../xhr-response-url.https.html | 103 ++ .../service-worker/xsl-base-url.https.html | 32 + test/wpt/tests/storage/META.yml | 4 + test/wpt/tests/storage/README.md | 7 + test/wpt/tests/storage/buckets/META.yml | 5 + ...kets_storage_policy.tentative.https.any.js | 21 + .../tests/storage/buckets/resources/util.js | 15 + .../storage/estimate-indexeddb.https.any.js | 101 ++ .../storage/estimate-parallel.https.any.js | 13 + ...sage-details-caches.https.tentative.any.js | 20 + ...e-details-indexeddb.https.tentative.any.js | 59 + ...-service-workers.https.tentative.window.js | 38 + ...imate-usage-details.https.tentative.any.js | 12 + test/wpt/tests/storage/helpers.js | 46 + .../wpt/tests/storage/idlharness.https.any.js | 18 + .../storage/opaque-origin.https.window.js | 80 ++ ...ge-details-caches.tentative.https.sub.html | 74 ++ ...details-indexeddb.tentative.https.sub.html | 84 ++ ...s-service-workers.tentative.https.sub.html | 88 ++ .../storage/permission-query.https.any.js | 10 + .../persist-permission-manual.https.html | 27 + test/wpt/tests/storage/persisted.https.any.js | 14 + ...ge-in-detached-iframe.tentative.https.html | 21 + ...ate-usage-details-caches-helper-frame.html | 30 + ...-usage-details-indexeddb-helper-frame.html | 28 + ...-details-service-workers-helper-frame.html | 30 + test/wpt/tests/storage/resources/worker.js | 3 + .../storagemanager-estimate.https.any.js | 60 + .../storagemanager-persist.https.window.js | 10 + .../storagemanager-persist.https.worker.js | 8 + .../storagemanager-persisted.https.any.js | 10 + types/cache.d.ts | 29 + types/webidl.d.ts | 2 + 745 files changed, 40858 insertions(+), 3 deletions(-) create mode 100644 lib/cache/cache.js create mode 100644 lib/cache/cachestorage.js create mode 100644 lib/cache/symbols.js create mode 100644 lib/cache/util.js create mode 100644 test/wpt/start-cacheStorage.mjs create mode 100644 test/wpt/status/service-workers/cache-storage.status.json create mode 100644 test/wpt/tests/service-workers/META.yml create mode 100644 test/wpt/tests/service-workers/cache-storage/META.yml create mode 100644 test/wpt/tests/service-workers/cache-storage/cache-abort.https.any.js create mode 100644 test/wpt/tests/service-workers/cache-storage/cache-add.https.any.js create mode 100644 test/wpt/tests/service-workers/cache-storage/cache-delete.https.any.js create mode 100644 test/wpt/tests/service-workers/cache-storage/cache-keys-attributes-for-service-worker.https.html create mode 100644 test/wpt/tests/service-workers/cache-storage/cache-keys.https.any.js create mode 100644 test/wpt/tests/service-workers/cache-storage/cache-match.https.any.js create mode 100644 test/wpt/tests/service-workers/cache-storage/cache-matchAll.https.any.js create mode 100644 test/wpt/tests/service-workers/cache-storage/cache-put.https.any.js create mode 100644 test/wpt/tests/service-workers/cache-storage/cache-storage-buckets.https.any.js create mode 100644 test/wpt/tests/service-workers/cache-storage/cache-storage-keys.https.any.js create mode 100644 test/wpt/tests/service-workers/cache-storage/cache-storage-match.https.any.js create mode 100644 test/wpt/tests/service-workers/cache-storage/cache-storage.https.any.js create mode 100644 test/wpt/tests/service-workers/cache-storage/common.https.window.js create mode 100644 test/wpt/tests/service-workers/cache-storage/crashtests/cache-response-clone.https.html create mode 100644 test/wpt/tests/service-workers/cache-storage/credentials.https.html create mode 100644 test/wpt/tests/service-workers/cache-storage/cross-partition.https.tentative.html create mode 100644 test/wpt/tests/service-workers/cache-storage/resources/blank.html create mode 100644 test/wpt/tests/service-workers/cache-storage/resources/cache-keys-attributes-for-service-worker.js create mode 100644 test/wpt/tests/service-workers/cache-storage/resources/common-worker.js create mode 100644 test/wpt/tests/service-workers/cache-storage/resources/credentials-iframe.html create mode 100644 test/wpt/tests/service-workers/cache-storage/resources/credentials-worker.js create mode 100644 test/wpt/tests/service-workers/cache-storage/resources/fetch-status.py create mode 100644 test/wpt/tests/service-workers/cache-storage/resources/iframe.html create mode 100644 test/wpt/tests/service-workers/cache-storage/resources/simple.txt create mode 100644 test/wpt/tests/service-workers/cache-storage/resources/test-helpers.js create mode 100644 test/wpt/tests/service-workers/cache-storage/resources/vary.py create mode 100644 test/wpt/tests/service-workers/cache-storage/sandboxed-iframes.https.html create mode 100644 test/wpt/tests/service-workers/idlharness.https.any.js create mode 100644 test/wpt/tests/service-workers/service-worker/Service-Worker-Allowed-header.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/close.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/extendable-message-event-constructor.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/extendable-message-event.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/fetch-on-the-right-interface.https.any.js create mode 100644 test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/isSecureContext.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/isSecureContext.serviceworker.js create mode 100644 test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/postmessage.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/registration-attribute.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/close-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/error-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-constructor-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-loopback-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-ping-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-pong-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-utils.js create mode 100644 test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-loopback-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-ping-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-pong-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/registration-attribute-newer-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/registration-attribute-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/unregister-controlling-worker.html create mode 100644 test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/unregister-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/update-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/update-worker.py create mode 100644 test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/service-worker-error-event.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/unregister.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/update.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/about-blank-replacement.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/activate-event-after-install-state-change.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/activation-after-registration.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/activation.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/active.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/claim-affect-other-registration.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/claim-fetch.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/claim-not-using-registration.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/claim-shared-worker-fetch.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/claim-using-registration.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/claim-with-redirect.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/claim-worker-fetch.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/client-id.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/client-navigate.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/client-url-of-blob-url-worker.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/clients-get-client-types.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/clients-get-cross-origin.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/clients-get-resultingClientId.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/clients-get.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/clients-matchall-blob-url-worker.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/clients-matchall-client-types.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/clients-matchall-exact-controller.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/clients-matchall-frozen.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/clients-matchall-include-uncontrolled.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/clients-matchall-on-evaluation.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/clients-matchall-order.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/clients-matchall.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/controller-on-disconnect.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/controller-on-load.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/controller-on-reload.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/controller-with-no-fetch-event-handler.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/credentials.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/data-iframe.html create mode 100644 test/wpt/tests/service-workers/service-worker/data-transfer-files.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/dedicated-worker-service-worker-interception.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/detached-context.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/embed-and-object-are-not-intercepted.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/extendable-event-async-waituntil.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/extendable-event-waituntil.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-audio-tainting.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-double-write.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-image-cache.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-image.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-video-cache.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-video-with-range-request.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-video.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-cors-exposed-header-names.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-cors-xhr.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-csp.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-error.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-event-add-async.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-event-after-navigation-within-page.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-event-async-respond-with.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-event-handled.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-event-is-history-backward-navigation-manual.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-event-is-history-forward-navigation-manual.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-event-is-reload-iframe-navigation-manual.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-event-is-reload-navigation-manual.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-event-network-error.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-event-redirect.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-event-referrer-policy.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-argument.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-body-loaded-in-chunk.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-custom-response.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-partial-stream.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-readable-stream-chunk.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-readable-stream.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-response-body-with-invalid-chunk.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-stops-propagation.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-event-throws-after-respond-with.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-event-within-sw-manual.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-event-within-sw.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-event.https.h2.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-event.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-frame-resource.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-header-visibility.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-mixed-content-to-inscope.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-mixed-content-to-outscope.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-request-css-base-url.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-request-css-cross-origin.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-request-css-images.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-request-fallback.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-request-no-freshness-headers.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-request-redirect.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-request-resources.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-request-xhr-sync-error.https.window.js create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-request-xhr-sync-on-worker.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-request-xhr-sync.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-request-xhr.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-response-taint.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-response-xhr.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/fetch-waits-for-activate.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/getregistration.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/getregistrations.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/global-serviceworker.https.any.js create mode 100644 test/wpt/tests/service-workers/service-worker/historical.https.any.js create mode 100644 test/wpt/tests/service-workers/service-worker/http-to-https-redirect-and-register.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/immutable-prototype-serviceworker.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/import-scripts-cross-origin.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/import-scripts-data-url.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/import-scripts-mime-types.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/import-scripts-redirect.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/import-scripts-resource-map.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/import-scripts-updated-flag.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/indexeddb.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/install-event-type.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/installing.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/interface-requirements-sw.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/invalid-blobtype.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/invalid-header.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/iso-latin1-header.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/local-url-inherit-controller.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/mime-sniffing.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/multi-globals/current/current.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/multi-globals/current/test-sw.js create mode 100644 test/wpt/tests/service-workers/service-worker/multi-globals/incumbent/incumbent.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/multi-globals/incumbent/test-sw.js create mode 100644 test/wpt/tests/service-workers/service-worker/multi-globals/relevant/relevant.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/multi-globals/relevant/test-sw.js create mode 100644 test/wpt/tests/service-workers/service-worker/multi-globals/test-sw.js create mode 100644 test/wpt/tests/service-workers/service-worker/multi-globals/url-parsing.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/multipart-image.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/multiple-register.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/multiple-update.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/navigate-window.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/navigation-headers.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/navigation-preload/broken-chunked-encoding.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/navigation-preload/chunked-encoding.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/navigation-preload/empty-preload-response-body.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/navigation-preload/get-state.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/navigation-preload/navigationPreload.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/navigation-preload/redirect.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/navigation-preload/request-headers.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/navigation-preload/resource-timing.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/navigation-preload/resources/broken-chunked-encoding-scope.asis create mode 100644 test/wpt/tests/service-workers/service-worker/navigation-preload/resources/broken-chunked-encoding-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/navigation-preload/resources/chunked-encoding-scope.py create mode 100644 test/wpt/tests/service-workers/service-worker/navigation-preload/resources/chunked-encoding-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/navigation-preload/resources/cookie.py create mode 100644 test/wpt/tests/service-workers/service-worker/navigation-preload/resources/empty-preload-response-body-scope.html create mode 100644 test/wpt/tests/service-workers/service-worker/navigation-preload/resources/empty-preload-response-body-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/navigation-preload/resources/get-state-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/navigation-preload/resources/helpers.js create mode 100644 test/wpt/tests/service-workers/service-worker/navigation-preload/resources/navigation-preload-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/navigation-preload/resources/redirect-redirected.html create mode 100644 test/wpt/tests/service-workers/service-worker/navigation-preload/resources/redirect-scope.py create mode 100644 test/wpt/tests/service-workers/service-worker/navigation-preload/resources/redirect-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/navigation-preload/resources/request-headers-scope.py create mode 100644 test/wpt/tests/service-workers/service-worker/navigation-preload/resources/request-headers-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/navigation-preload/resources/resource-timing-scope.py create mode 100644 test/wpt/tests/service-workers/service-worker/navigation-preload/resources/resource-timing-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/navigation-preload/resources/samesite-iframe.html create mode 100644 test/wpt/tests/service-workers/service-worker/navigation-preload/resources/samesite-sw-helper.html create mode 100644 test/wpt/tests/service-workers/service-worker/navigation-preload/resources/samesite-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/navigation-preload/resources/wait-for-activate-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/navigation-preload/samesite-cookies.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/navigation-preload/samesite-iframe.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/navigation-redirect-body.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/navigation-redirect-resolution.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/navigation-redirect-to-http.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/navigation-redirect.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/navigation-sets-cookie.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/navigation-timing-extended.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/navigation-timing.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/nested-blob-url-workers.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/next-hop-protocol.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/no-dynamic-import-in-module.any.js create mode 100644 test/wpt/tests/service-workers/service-worker/no-dynamic-import.any.js create mode 100644 test/wpt/tests/service-workers/service-worker/onactivate-script-error.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/oninstall-script-error.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/opaque-response-preloaded.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/opaque-script.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/partitioned-claim.tentative.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/partitioned-getRegistrations.tentative.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/partitioned-matchAll.tentative.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/partitioned.tentative.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/performance-timeline.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/postmessage-blob-url.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/postmessage-from-waiting-serviceworker.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/postmessage-msgport-to-client.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/postmessage-to-client-message-queue.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/postmessage-to-client.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/postmessage.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/ready.https.window.js create mode 100644 test/wpt/tests/service-workers/service-worker/redirected-response.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/referer.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/referrer-policy-header.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/referrer-toplevel-script-fetch.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/register-closed-window.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/register-default-scope.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/register-same-scope-different-script-url.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/register-wait-forever-in-install-worker.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/registration-basic.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/registration-end-to-end.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/registration-events.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/registration-iframe.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/registration-mime-types.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/registration-schedule-job.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/registration-scope-module-static-import.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/registration-scope.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/registration-script-module.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/registration-script-url.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/registration-script.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/registration-security-error.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/registration-service-worker-attributes.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/registration-updateviacache.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/rejections.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/request-end-to-end.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/resource-timing-bodySize.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/resource-timing-cross-origin.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/resource-timing-fetch-variants.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/resource-timing.sub.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/404.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-blank-dynamic-nested-frame.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-blank-nested-frame.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-frame.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-ping-frame.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-popup-frame.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-srcdoc-nested-frame.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-uncontrolled-nested-frame.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/basic-module-2.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/basic-module.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/blank.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/bytecheck-worker-imported-script.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/bytecheck-worker.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/claim-blob-url-worker-fetch-iframe.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/claim-nested-worker-fetch-iframe.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/claim-nested-worker-fetch-parent-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/claim-shared-worker-fetch-iframe.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/claim-shared-worker-fetch-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/claim-with-redirect-iframe.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/claim-worker-fetch-iframe.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/claim-worker-fetch-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/claim-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/classic-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/client-id-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/client-navigate-frame.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/client-navigate-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/client-navigated-frame.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/client-url-of-blob-url-worker.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/client-url-of-blob-url-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/clients-frame-freeze.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/clients-get-client-types-frame-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/clients-get-client-types-frame.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/clients-get-client-types-shared-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/clients-get-client-types-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/clients-get-cross-origin-frame.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/clients-get-frame.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/clients-get-other-origin.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/clients-get-resultingClientId-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/clients-get-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/clients-matchall-blob-url-worker.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/clients-matchall-client-types-dedicated-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/clients-matchall-client-types-iframe.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/clients-matchall-client-types-shared-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/clients-matchall-on-evaluation-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/clients-matchall-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/cors-approved.txt create mode 100644 test/wpt/tests/service-workers/service-worker/resources/cors-approved.txt.headers create mode 100644 test/wpt/tests/service-workers/service-worker/resources/cors-denied.txt create mode 100644 test/wpt/tests/service-workers/service-worker/resources/create-blob-url-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/create-out-of-scope-worker.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/echo-content.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/echo-cookie-worker.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/echo-message-to-source-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/embed-and-object-are-not-intercepted-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/embed-image-is-not-intercepted-iframe.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/embed-is-not-intercepted-iframe.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/embed-navigation-is-not-intercepted-iframe.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/embedded-content-from-server.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/embedded-content-from-service-worker.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/empty-but-slow-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/empty-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/empty.h2.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/empty.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/empty.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/enable-client-message-queue.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/end-to-end-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/events-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/extendable-event-async-waituntil.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/extendable-event-waituntil.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fail-on-fetch-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-access-control-login.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-access-control.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-canvas-tainting-double-write-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-canvas-tainting-iframe.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-canvas-tainting-tests.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-cors-exposed-header-names-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-cors-xhr-iframe.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-csp-iframe.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-csp-iframe.html.sub.headers create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-error-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-event-add-async-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-event-after-navigation-within-page-iframe.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-event-async-respond-with-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-event-handled-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-event-network-error-controllee-iframe.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-event-network-error-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-event-network-fallback-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-argument-iframe.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-argument-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-body-loaded-in-chunk-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-custom-response-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-partial-stream-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-readable-stream-chunk-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-readable-stream-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-response-body-with-invalid-chunk-iframe.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-response-body-with-invalid-chunk-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-stops-propagation-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-event-test-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-event-within-sw-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-header-visibility-iframe.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-mixed-content-iframe-inscope-to-inscope.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-mixed-content-iframe-inscope-to-outscope.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-mixed-content-iframe.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-base-url-iframe.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-base-url-style.css create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-base-url-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-cross.css create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-cross.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-iframe.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-same.css create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-same.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-read-contents.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-request-fallback-iframe.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-request-fallback-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-request-html-imports-iframe.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-request-html-imports-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-iframe.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-script.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-request-redirect-iframe.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-request-resources-iframe.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-request-resources-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-iframe.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-error-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-iframe.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-on-worker-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-response-taint-iframe.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-response-xhr-iframe.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-response-xhr-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-response.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-response.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-rewrite-worker-referrer-policy.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-rewrite-worker-referrer-policy.js.headers create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-rewrite-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-rewrite-worker.js.headers create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-variants-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/fetch-waits-for-activate-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/form-poster.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/frame-for-getregistrations.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/get-resultingClientId-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/http-to-https-redirect-and-register-iframe.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/iframe-with-fetch-variants.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/iframe-with-image.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/immutable-prototype-serviceworker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/import-echo-cookie-worker-module.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/import-echo-cookie-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/import-mime-type-worker.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/import-relative.xsl create mode 100644 test/wpt/tests/service-workers/service-worker/resources/import-scripts-404-after-update-plus-update-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/import-scripts-404-after-update.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/import-scripts-404.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/import-scripts-cross-origin-worker.sub.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/import-scripts-data-url-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/import-scripts-diff-resource-map-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/import-scripts-echo.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/import-scripts-get.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/import-scripts-mime-types-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/import-scripts-redirect-import.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/import-scripts-redirect-on-second-time-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/import-scripts-redirect-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/import-scripts-resource-map-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/import-scripts-updated-flag-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/import-scripts-version.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/imported-classic-script.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/imported-module-script.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/indexeddb-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/install-event-type-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/install-worker.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/interface-requirements-worker.sub.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/invalid-blobtype-iframe.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/invalid-blobtype-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/invalid-chunked-encoding-with-flush.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/invalid-chunked-encoding.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/invalid-header-iframe.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/invalid-header-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/iso-latin1-header-iframe.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/iso-latin1-header-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/load_worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/loaded.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/local-url-inherit-controller-frame.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/local-url-inherit-controller-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/location-setter.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/malformed-http-response.asis create mode 100644 test/wpt/tests/service-workers/service-worker/resources/malformed-worker.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/message-vs-microtask.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/mime-sniffing-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/mime-type-worker.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/mint-new-worker.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/module-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/multipart-image-iframe.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/multipart-image-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/multipart-image.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/navigate-window-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/navigation-headers-server.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-body-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-body.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-other-origin.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-out-scope.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-scope1.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-scope2.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-to-http-iframe.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-to-http-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/navigation-timing-worker-extended.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/navigation-timing-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/nested-blob-url-worker-created-from-worker.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/nested-blob-url-workers.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/nested-iframe-parent.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/nested-parent.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/nested-worker-created-from-blob-url-worker.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/nested_load_worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/no-dynamic-import.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/notification_icon.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/object-image-is-not-intercepted-iframe.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/object-is-not-intercepted-iframe.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/object-navigation-is-not-intercepted-iframe.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/onactivate-throw-error-from-nested-event-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/onactivate-throw-error-then-cancel-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/onactivate-throw-error-then-prevent-default-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/onactivate-throw-error-with-empty-onerror-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/onactivate-throw-error-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/onactivate-waituntil-forever.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/onfetch-waituntil-forever.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/oninstall-throw-error-from-nested-event-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/oninstall-throw-error-then-cancel-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/oninstall-throw-error-then-prevent-default-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/oninstall-throw-error-with-empty-onerror-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/oninstall-throw-error-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/oninstall-waituntil-forever.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/oninstall-waituntil-throw-error-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/onparse-infiniteloop-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/opaque-response-being-preloaded-xhr.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/opaque-response-preloaded-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/opaque-response-preloaded-xhr.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/opaque-script-frame.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/opaque-script-large.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/opaque-script-small.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/opaque-script-sw.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/other.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/override_assert_object_equals.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-iframe-claim.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-nested-iframe-child.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-nested-iframe-parent.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe-getRegistrations.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe-matchAll.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-window.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/partitioned-storage-sw.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/partitioned-utils.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/pass-through-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/pass.txt create mode 100644 test/wpt/tests/service-workers/service-worker/resources/performance-timeline-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/postmessage-blob-url.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/postmessage-dictionary-transferables-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/postmessage-echo-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/postmessage-fetched-text.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/postmessage-msgport-to-client-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/postmessage-on-load-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/postmessage-to-client-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/postmessage-transferables-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/postmessage-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/range-request-to-different-origins-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/range-request-with-different-cors-modes-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/redirect-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/redirect.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/referer-iframe.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/referrer-policy-iframe.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/register-closed-window-iframe.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/register-iframe.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/register-rewrite-worker.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/registration-tests-mime-types.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/registration-tests-scope.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/registration-tests-script-url.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/registration-tests-script.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/registration-tests-security-error.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/registration-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/reject-install-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/reply-to-message.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/request-end-to-end-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/request-headers.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/resource-timing-iframe.sub.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/resource-timing-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/respond-then-throw-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/respond-with-body-accessed-response-iframe.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/respond-with-body-accessed-response-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/respond-with-body-accessed-response.jsonp create mode 100644 test/wpt/tests/service-workers/service-worker/resources/sample-worker-interceptor.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/sample.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/sample.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/sample.txt create mode 100644 test/wpt/tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-iframe.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-iframe.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/sandboxed-iframe-navigator-serviceworker-iframe.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/scope1/module-worker-importing-redirect-to-scope2.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/scope1/module-worker-importing-scope2.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/scope1/redirect.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/scope2/import-scripts-echo.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/scope2/imported-module-script.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/scope2/simple.txt create mode 100644 test/wpt/tests/service-workers/service-worker/resources/scope2/worker_interception_redirect_webworker.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/secure-context-service-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/secure-context/sender.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/secure-context/window.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/service-worker-csp-worker.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/service-worker-header.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/service-worker-interception-dynamic-import-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/service-worker-interception-network-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/service-worker-interception-service-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/service-worker-interception-static-import-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/silence.oga create mode 100644 test/wpt/tests/service-workers/service-worker/resources/simple-intercept-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/simple-intercept-worker.js.headers create mode 100644 test/wpt/tests/service-workers/service-worker/resources/simple.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/simple.txt create mode 100644 test/wpt/tests/service-workers/service-worker/resources/skip-waiting-installed-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/skip-waiting-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/square.png create mode 100644 test/wpt/tests/service-workers/service-worker/resources/square.png.sub.headers create mode 100644 test/wpt/tests/service-workers/service-worker/resources/stalling-service-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/subdir/blank.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/subdir/import-scripts-echo.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/subdir/simple.txt create mode 100644 test/wpt/tests/service-workers/service-worker/resources/subdir/worker_interception_redirect_webworker.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/success.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/svg-target-reftest-001-frame.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/svg-target-reftest-001.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/svg-target-reftest-frame.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/test-helpers.sub.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/test-request-headers-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/test-request-headers-worker.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/test-request-mode-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/test-request-mode-worker.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/testharness-helpers.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/trickle.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/type-check-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/unregister-controller-page.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/unregister-immediately-helpers.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/unregister-rewrite-worker.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/update-claim-worker.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/update-during-installation-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/update-during-installation-worker.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/update-fetch-worker.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/update-max-aged-worker-imported-script.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/update-max-aged-worker.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/update-missing-import-scripts-imported-worker.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/update-missing-import-scripts-main-worker.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/update-nocookie-worker.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/update-recovery-worker.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/update-registration-with-type.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/update-smaller-body-after-update-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/update-smaller-body-before-update-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/update-worker-from-file.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/update-worker.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/update/update-after-oneday.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/update_shell.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/vtt-frame.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/wait-forever-in-install-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/websocket-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/websocket.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/window-opener.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/windowclient-navigate-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/worker-client-id-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/worker-fetching-cross-origin.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/worker-interception-redirect-serviceworker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/worker-interception-redirect-webworker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/worker-load-interceptor.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/worker-testharness.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/worker_interception_redirect_webworker.py create mode 100644 test/wpt/tests/service-workers/service-worker/resources/xhr-content-length-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/xhr-iframe.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/xhr-response-url-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/xsl-base-url-iframe.xml create mode 100644 test/wpt/tests/service-workers/service-worker/resources/xsl-base-url-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/xslt-pass.xsl create mode 100644 test/wpt/tests/service-workers/service-worker/respond-with-body-accessed-response.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/same-site-cookies.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/sandboxed-iframe-fetch-event.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/sandboxed-iframe-navigator-serviceworker.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/secure-context.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/service-worker-csp-connect.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/service-worker-csp-default.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/service-worker-csp-script.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/service-worker-header.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/serviceworker-message-event-historical.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/serviceworkerobject-scripturl.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/skip-waiting-installed.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/skip-waiting-using-registration.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/skip-waiting-without-client.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/skip-waiting-without-using-registration.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/skip-waiting.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/state.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/svg-target-reftest.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/synced-state.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/uncontrolled-page.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/unregister-controller.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/unregister-immediately-before-installed.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/unregister-immediately-during-extendable-events.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/unregister-immediately.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/unregister-then-register-new-script.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/unregister-then-register.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/unregister.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/update-after-navigation-fetch-event.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/update-after-navigation-redirect.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/update-after-oneday.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/update-bytecheck-cors-import.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/update-bytecheck.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/update-import-scripts.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/update-missing-import-scripts.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/update-module-request-mode.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/update-no-cache-request-headers.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/update-not-allowed.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/update-on-navigation.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/update-recovery.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/update-registration-with-type.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/update-result.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/update.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/waiting.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/websocket-in-service-worker.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/websocket.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/webvtt-cross-origin.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/windowclient-navigate.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/worker-client-id.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/worker-in-sandboxed-iframe-by-csp-fetch-event.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/worker-interception-redirect.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/worker-interception.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/xhr-content-length.https.window.js create mode 100644 test/wpt/tests/service-workers/service-worker/xhr-response-url.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/xsl-base-url.https.html create mode 100644 test/wpt/tests/storage/META.yml create mode 100644 test/wpt/tests/storage/README.md create mode 100644 test/wpt/tests/storage/buckets/META.yml create mode 100644 test/wpt/tests/storage/buckets/buckets_storage_policy.tentative.https.any.js create mode 100644 test/wpt/tests/storage/buckets/resources/util.js create mode 100644 test/wpt/tests/storage/estimate-indexeddb.https.any.js create mode 100644 test/wpt/tests/storage/estimate-parallel.https.any.js create mode 100644 test/wpt/tests/storage/estimate-usage-details-caches.https.tentative.any.js create mode 100644 test/wpt/tests/storage/estimate-usage-details-indexeddb.https.tentative.any.js create mode 100644 test/wpt/tests/storage/estimate-usage-details-service-workers.https.tentative.window.js create mode 100644 test/wpt/tests/storage/estimate-usage-details.https.tentative.any.js create mode 100644 test/wpt/tests/storage/helpers.js create mode 100644 test/wpt/tests/storage/idlharness.https.any.js create mode 100644 test/wpt/tests/storage/opaque-origin.https.window.js create mode 100644 test/wpt/tests/storage/partitioned-estimate-usage-details-caches.tentative.https.sub.html create mode 100644 test/wpt/tests/storage/partitioned-estimate-usage-details-indexeddb.tentative.https.sub.html create mode 100644 test/wpt/tests/storage/partitioned-estimate-usage-details-service-workers.tentative.https.sub.html create mode 100644 test/wpt/tests/storage/permission-query.https.any.js create mode 100644 test/wpt/tests/storage/persist-permission-manual.https.html create mode 100644 test/wpt/tests/storage/persisted.https.any.js create mode 100644 test/wpt/tests/storage/quotachange-in-detached-iframe.tentative.https.html create mode 100644 test/wpt/tests/storage/resources/partitioned-estimate-usage-details-caches-helper-frame.html create mode 100644 test/wpt/tests/storage/resources/partitioned-estimate-usage-details-indexeddb-helper-frame.html create mode 100644 test/wpt/tests/storage/resources/partitioned-estimate-usage-details-service-workers-helper-frame.html create mode 100644 test/wpt/tests/storage/resources/worker.js create mode 100644 test/wpt/tests/storage/storagemanager-estimate.https.any.js create mode 100644 test/wpt/tests/storage/storagemanager-persist.https.window.js create mode 100644 test/wpt/tests/storage/storagemanager-persist.https.worker.js create mode 100644 test/wpt/tests/storage/storagemanager-persisted.https.any.js create mode 100644 types/cache.d.ts diff --git a/lib/cache/cache.js b/lib/cache/cache.js new file mode 100644 index 00000000000..8838e31c5f3 --- /dev/null +++ b/lib/cache/cache.js @@ -0,0 +1,834 @@ +'use strict' + +const { kConstruct } = require('./symbols') +const { urlEquals, fieldValues: getFieldValues } = require('./util') +const { kEnumerableProperty, isDisturbed } = require('../core/util') +const { kHeadersList } = require('../core/symbols') +const { webidl } = require('../fetch/webidl') +const { Response, cloneResponse } = require('../fetch/response') +const { Request } = require('../fetch/request') +const { kState, kHeaders, kGuard, kRealm } = require('../fetch/symbols') +const { fetching } = require('../fetch/index') +const { urlIsHttpHttpsScheme, createDeferredPromise, readAllBytes } = require('../fetch/util') +const assert = require('assert') +const { getGlobalDispatcher } = require('../global') + +/** + * @see https://w3c.github.io/ServiceWorker/#dfn-cache-batch-operation + * @typedef {Object} CacheBatchOperation + * @property {'delete' | 'put'} type + * @property {any} request + * @property {any} response + * @property {import('../../types/cache').CacheQueryOptions} options + */ + +/** + * @see https://w3c.github.io/ServiceWorker/#dfn-request-response-list + * @typedef {[any, any][]} requestResponseList + */ + +class Cache { + /** + * @see https://w3c.github.io/ServiceWorker/#dfn-relevant-request-response-list + * @type {requestResponseList} + */ + #relevantRequestResponseList = [] + + constructor () { + if (arguments[0] !== kConstruct) { + webidl.illegalConstructor() + } + + this.#relevantRequestResponseList = arguments[1] + } + + async match (request, options = {}) { + webidl.brandCheck(this, Cache) + webidl.argumentLengthCheck(arguments, 1, { header: 'Cache.match' }) + + request = webidl.converters.RequestInfo(request) + options = webidl.converters.CacheQueryOptions(options) + + const p = await this.matchAll(request, options) + + if (p.length === 0) { + return + } + + return p[0] + } + + async matchAll (request = undefined, options = {}) { + webidl.brandCheck(this, Cache) + + if (request !== undefined) request = webidl.converters.RequestInfo(request) + options = webidl.converters.CacheQueryOptions(options) + + // 1. + let r = null + + // 2. + if (request !== undefined) { + if (request instanceof Request) { + // 2.1.1 + r = request[kState] + + // 2.1.2 + if (r.method !== 'GET' && !options.ignoreMethod) { + return [] + } + } else if (typeof request === 'string') { + // 2.2.1 + r = new Request(request)[kState] + } + } + + // 5. + // 5.1 + const responses = [] + + // 5.2 + if (request === undefined) { + // 5.2.1 + for (const requestResponse of this.#relevantRequestResponseList) { + responses.push(requestResponse[1]) + } + } else { // 5.3 + // 5.3.1 + const requestResponses = this.#queryCache(r, options) + + // 5.3.2 + for (const requestResponse of requestResponses) { + responses.push(requestResponse[1]) + } + } + + // 5.4 + // We don't implement CORs so we don't need to loop over the responses, yay! + + // 5.5.1 + const responseList = [] + + // 5.5.2 + for (const response of responses) { + // 5.5.2.1 + const responseObject = new Response(response.body?.source ?? null) + const body = responseObject[kState].body + responseObject[kState] = response + responseObject[kState].body = body + responseObject[kHeaders][kHeadersList] = response.headersList + responseObject[kHeaders][kGuard] = 'immutable' + + responseList.push(responseObject) + } + + // 6. + return Object.freeze(responseList) + } + + async add (request) { + webidl.brandCheck(this, Cache) + webidl.argumentLengthCheck(arguments, 1, { header: 'Cache.add' }) + + request = webidl.converters.RequestInfo(request) + + // 1. + const requests = [request] + + // 2. + const responseArrayPromise = this.addAll(requests) + + // 3. + return await responseArrayPromise + } + + async addAll (requests) { + webidl.brandCheck(this, Cache) + webidl.argumentLengthCheck(arguments, 1, { header: 'Cache.addAll' }) + + requests = webidl.converters['sequence'](requests) + + // 1. + const responsePromises = [] + + // 2. + const requestList = [] + + // 3. + for (const request of requests) { + if (typeof request === 'string') { + continue + } + + // 3.1 + const r = request[kState] + + // 3.2 + if (!urlIsHttpHttpsScheme(r.url) || r.method !== 'GET') { + throw webidl.errors.exception({ + header: 'Cache.addAll', + message: 'Expected http/s scheme when method is not GET.' + }) + } + } + + // 4. + /** @type {ReturnType[]} */ + const fetchControllers = [] + + // 5. + for (const request of requests) { + // 5.1 + const r = new Request(request)[kState] + + // 5.2 + if (!urlIsHttpHttpsScheme(r.url)) { + throw webidl.errors.exception({ + header: 'Cache.addAll', + message: 'Expected http/s scheme.' + }) + } + + // 5.4 + r.initiator = 'fetch' + r.destination = 'subresource' + + // 5.5 + requestList.push(r) + + // 5.6 + const responsePromise = createDeferredPromise() + + // 5.7 + fetchControllers.push(fetching({ + request: r, + dispatcher: getGlobalDispatcher(), + processResponse (response) { + // 1. + if (response.type === 'error' || response.status === 206 || response.status < 200 || response.status > 299) { + responsePromise.reject(webidl.errors.exception({ + header: 'Cache.addAll', + message: 'Received an invalid status code or the request failed.' + })) + } else if (response.headersList.contains('vary')) { // 2. + // 2.1 + const fieldValues = getFieldValues(response.headersList.get('vary')) + + // 2.2 + for (const fieldValue of fieldValues) { + // 2.2.1 + if (fieldValue === '*') { + responsePromise.reject(webidl.errors.exception({ + header: 'Cache.addAll', + message: 'invalid vary field value' + })) + + for (const controller of fetchControllers) { + controller.abort() + } + + return + } + } + } + }, + processResponseEndOfBody (response) { + // 1. + if (response.aborted) { + responsePromise.reject(new DOMException('aborted', 'AbortError')) + return + } + + // 2. + responsePromise.resolve(response) + } + })) + + // 5.8 + responsePromises.push(responsePromise.promise) + } + + // 6. + const p = Promise.all(responsePromises) + + // 7. + const responses = await p + + // 7.1 + const operations = [] + + // 7.2 + let index = 0 + + // 7.3 + for (const response of responses) { + // 7.3.1 + /** @type {CacheBatchOperation} */ + const operation = { + type: 'put', // 7.3.2 + request: requestList[index], // 7.3.3 + response // 7.3.4 + } + + operations.push(operation) // 7.3.5 + + index++ // 7.3.6 + } + + // 7.5 + const cacheJobPromise = createDeferredPromise() + + // 7.6.1 + let errorData = null + + // 7.6.2 + try { + this.#batchCacheOperations(operations) + } catch (e) { + errorData = e + } + + // 7.6.3 + queueMicrotask(() => { + // 7.6.3.1 + if (errorData === null) { + cacheJobPromise.resolve(undefined) + } else { + // 7.6.3.2 + cacheJobPromise.reject(errorData) + } + }) + + // 7.7 + return cacheJobPromise.promise + } + + async put (request, response) { + webidl.brandCheck(this, Cache) + webidl.argumentLengthCheck(arguments, 2, { header: 'Cache.put' }) + + request = webidl.converters.RequestInfo(request) + response = webidl.converters.Response(response) + + // 1. + let innerRequest = null + + // 2. + if (typeof request !== 'string') { + innerRequest = request[kState] + } else { // 3. + innerRequest = new Request(request)[kState] + } + + // 4. + if (!urlIsHttpHttpsScheme(innerRequest.url) || innerRequest.method !== 'GET') { + throw webidl.errors.exception({ + header: 'Cache.put', + message: 'Expected an http/s scheme when method is not GET' + }) + } + + // 5. + const innerResponse = response[kState] + + // 6. + if (innerResponse.status === 206) { + throw webidl.errors.exception({ + header: 'Cache.put', + message: 'Got 206 status' + }) + } + + // 7. + if (innerResponse.headersList.contains('vary')) { + // 7.1. + const fieldValues = getFieldValues(innerResponse.headersList.get('vary')) + + // 7.2. + for (const fieldValue of fieldValues) { + // 7.2.1 + if (fieldValue === '*') { + throw webidl.errors.exception({ + header: 'Cache.put', + message: 'Got * vary field value' + }) + } + } + } + + // 8. + if (innerResponse.body && (isDisturbed(innerResponse.body.stream) || innerResponse.body.stream.locked)) { + throw webidl.errors.exception({ + header: 'Cache.put', + message: 'Response body is locked or disturbed' + }) + } + + // 9. + const clonedResponse = cloneResponse(innerResponse) + + // 10. + const bodyReadPromise = createDeferredPromise() + + // 11. + if (innerResponse.body != null) { + // 11.1 + const stream = innerResponse.body.stream + + // 11.2 + const reader = stream.getReader() + + // 11.3 + readAllBytes( + reader, + (bytes) => bodyReadPromise.resolve(bytes), + (error) => bodyReadPromise.reject(error) + ) + } else { + bodyReadPromise.resolve(undefined) + } + + // 12. + /** @type {CacheBatchOperation[]} */ + const operations = [] + + // 13. + /** @type {CacheBatchOperation} */ + const operation = { + type: 'put', // 14. + request: innerRequest, // 15. + response: clonedResponse // 16. + } + + // 17. + operations.push(operation) + + // 19. + const bytes = await bodyReadPromise.promise + + if (clonedResponse.body != null) { + clonedResponse.body.source = bytes + } + + // 19.1 + const cacheJobPromise = createDeferredPromise() + + // 19.2.1 + let errorData = null + + // 19.2.2 + try { + this.#batchCacheOperations(operations) + } catch (e) { + errorData = e + } + + // 19.2.3 + queueMicrotask(() => { + // 19.2.3.1 + if (errorData === null) { + cacheJobPromise.resolve() + } else { // 19.2.3.2 + cacheJobPromise.reject(errorData) + } + }) + + return cacheJobPromise.promise + } + + async delete (request, options = {}) { + webidl.brandCheck(this, Cache) + webidl.argumentLengthCheck(arguments, 1, { header: 'Cache.delete' }) + + request = webidl.converters.RequestInfo(request) + options = webidl.converters.CacheQueryOptions(options) + + /** + * @type {Request} + */ + let r = null + + if (request instanceof Request) { + r = request[kState] + + if (r.method !== 'GET' && !options.ignoreMethod) { + return false + } + } else { + assert(typeof request === 'string') + + r = new Request(r)[kState] + } + + /** @type {CacheBatchOperation[]} */ + const operations = [] + + /** @type {CacheBatchOperation} */ + const operation = { + type: 'delete', + request: r, + options + } + + operations.push(operation) + + const cacheJobPromise = createDeferredPromise() + + let errorData = null + let requestResponses + + try { + requestResponses = this.#batchCacheOperations(operations) + } catch (e) { + errorData = e + } + + queueMicrotask(() => { + if (errorData === null) { + cacheJobPromise.resolve(!!requestResponses?.length) + } else { + cacheJobPromise.reject(errorData) + } + }) + + return cacheJobPromise.promise + } + + /** + * @see https://w3c.github.io/ServiceWorker/#dom-cache-keys + * @param {any} request + * @param {import('../../types/cache').CacheQueryOptions} options + * @returns {readonly Request[]} + */ + async keys (request = undefined, options = {}) { + webidl.brandCheck(this, Cache) + + if (request !== undefined) request = webidl.converters.RequestInfo(request) + options = webidl.converters.CacheQueryOptions(options) + + // 1. + let r = null + + // 2. + if (request !== undefined) { + // 2.1 + if (request instanceof Request) { + // 2.1.1 + r = request[kState] + + // 2.1.2 + if (r.method !== 'GET' && !options.ignoreMethod) { + return [] + } + } else if (typeof request === 'string') { // 2.2 + r = new Request(request)[kState] + } + } + + // 4. + const promise = createDeferredPromise() + + // 5. + // 5.1 + const requests = [] + + // 5.2 + if (request === undefined) { + // 5.2.1 + for (const requestResponse of this.#relevantRequestResponseList) { + // 5.2.1.1 + requests.push(requestResponse[0]) + } + } else { // 5.3 + // 5.3.1 + const requestResponses = this.#queryCache(r, options) + + // 5.3.2 + for (const requestResponse of requestResponses) { + // 5.3.2.1 + requests.push(requestResponse[0]) + } + } + + // 5.4 + queueMicrotask(() => { + // 5.4.1 + const requestList = [] + + // 5.4.2 + for (const request of requests) { + const requestObject = new Request('https://a') + requestObject[kState] = request + requestObject[kHeaders][kHeadersList] = request.headersList + requestObject[kHeaders][kGuard] = 'immutable' + requestObject[kRealm] = request.client + + // 5.4.2.1 + requestList.push(requestObject) + } + + // 5.4.3 + promise.resolve(Object.freeze(requestList)) + }) + + return promise.promise + } + + /** + * @see https://w3c.github.io/ServiceWorker/#batch-cache-operations + * @param {CacheBatchOperation[]} operations + * @returns {requestResponseList} + */ + #batchCacheOperations (operations) { + // 1. + const cache = this.#relevantRequestResponseList + + // 2. + const backupCache = [...cache] + + // 3. + const addedItems = [] + + // 4.1 + const resultList = [] + + try { + // 4.2 + for (const operation of operations) { + // 4.2.1 + if (operation.type !== 'delete' && operation.type !== 'put') { + throw webidl.errors.exception({ + header: 'Cache.#batchCacheOperations', + message: 'operation type does not match "delete" or "put"' + }) + } + + // 4.2.2 + if (operation.type === 'delete' && operation.response != null) { + throw webidl.errors.exception({ + header: 'Cache.#batchCacheOperations', + message: 'delete operation should not have an associated response' + }) + } + + // 4.2.3 + if (this.#queryCache(operation.request, operation.options, addedItems).length) { + throw new DOMException('???', 'InvalidStateError') + } + + // 4.2.4 + let requestResponses + + // 4.2.5 + if (operation.type === 'delete') { + // 4.2.5.1 + requestResponses = this.#queryCache(operation.request, operation.options) + + // TODO: the spec is wrong, this is needed to pass WPTs + if (requestResponses.length === 0) { + return [] + } + + // 4.2.5.2 + for (const requestResponse of requestResponses) { + const idx = cache.indexOf(requestResponse) + assert(idx !== -1) + + // 4.2.5.2.1 + cache.splice(idx, 1) + } + } else if (operation.type === 'put') { // 4.2.6 + // 4.2.6.1 + if (operation.response == null) { + throw webidl.errors.exception({ + header: 'Cache.#batchCacheOperations', + message: 'put operation should have an associated response' + }) + } + + // 4.2.6.2 + const r = operation.request + + // 4.2.6.3 + if (!urlIsHttpHttpsScheme(r.url)) { + throw webidl.errors.exception({ + header: 'Cache.#batchCacheOperations', + message: 'expected http or https scheme' + }) + } + + // 4.2.6.4 + if (r.method !== 'GET') { + throw webidl.errors.exception({ + header: 'Cache.#batchCacheOperations', + message: 'not get method' + }) + } + + // 4.2.6.5 + if (operation.options != null) { + throw webidl.errors.exception({ + header: 'Cache.#batchCacheOperations', + message: 'options must not be defined' + }) + } + + // 4.2.6.6 + requestResponses = this.#queryCache(operation.request) + + // 4.2.6.7 + for (const requestResponse of requestResponses) { + const idx = cache.indexOf(requestResponse) + assert(idx !== -1) + + // 4.2.6.7.1 + cache.splice(idx, 1) + } + + // 4.2.6.8 + cache.push([operation.request, operation.response]) + + // 4.2.6.10 + addedItems.push([operation.request, operation.response]) + } + + // 4.2.7 + resultList.push([operation.request, operation.response]) + } + + // 4.3 + return resultList + } catch (e) { // 5. + // 5.1 + this.#relevantRequestResponseList.length = 0 + + // 5.2 + this.#relevantRequestResponseList = backupCache + + // 5.3 + throw e + } + } + + /** + * @see https://w3c.github.io/ServiceWorker/#query-cache + * @param {any} requestQuery + * @param {import('../../types/cache').CacheQueryOptions} options + * @param {requestResponseList} targetStorage + * @returns {requestResponseList} + */ + #queryCache (requestQuery, options, targetStorage) { + /** @type {requestResponseList} */ + const resultList = [] + + const storage = targetStorage ?? this.#relevantRequestResponseList + + for (const requestResponse of storage) { + const [cachedRequest, cachedResponse] = requestResponse + if (this.#requestMatchesCachedItem(requestQuery, cachedRequest, cachedResponse, options)) { + resultList.push(requestResponse) + } + } + + return resultList + } + + /** + * @see https://w3c.github.io/ServiceWorker/#request-matches-cached-item-algorithm + * @param {any} requestQuery + * @param {any} request + * @param {any | null} response + * @param {import('../../types/cache').CacheQueryOptions | undefined} options + * @returns {boolean} + */ + #requestMatchesCachedItem (requestQuery, request, response = null, options) { + // if (options?.ignoreMethod === false && request.method === 'GET') { + // return false + // } + + /** @type {URL} */ + const queryURL = requestQuery.url + + /** @type {URL} */ + const cachedURL = request.url + + if (options?.ignoreSearch) { + cachedURL.search = '' + + queryURL.search = '' + } + + if (!urlEquals(queryURL, cachedURL, true)) { + return false + } + + if ( + response == null || + options?.ignoreVary || + !response.headersList.contains('vary') + ) { + return true + } + + const fieldValues = getFieldValues(response.headersList.get('vary')) + + for (const fieldValue of fieldValues) { + if (fieldValue === '*') { + return false + } + + const requestValue = request.headersList.get(fieldValue) + const queryValue = requestQuery.headersList.get(fieldValue) + + // If one has the header and the other doesn't, or one has + // a different value than the other, return false + if (requestValue !== queryValue) { + return false + } + } + + return true + } +} + +Object.defineProperties(Cache.prototype, { + [Symbol.toStringTag]: { + value: 'Cache', + configurable: true + }, + match: kEnumerableProperty, + matchAll: kEnumerableProperty, + add: kEnumerableProperty, + addAll: kEnumerableProperty, + put: kEnumerableProperty, + delete: kEnumerableProperty, + keys: kEnumerableProperty +}) + +webidl.converters.CacheQueryOptions = webidl.dictionaryConverter([ + { + key: 'ignoreSearch', + converter: webidl.converters.boolean, + defaultValue: false + }, + { + key: 'ignoreMethod', + converter: webidl.converters.boolean, + defaultValue: false + }, + { + key: 'ignoreVary', + converter: webidl.converters.boolean, + defaultValue: false + } +]) + +webidl.converters.Response = webidl.interfaceConverter(Response) + +webidl.converters['sequence'] = webidl.sequenceConverter( + webidl.converters.RequestInfo +) + +module.exports = { + Cache +} diff --git a/lib/cache/cachestorage.js b/lib/cache/cachestorage.js new file mode 100644 index 00000000000..4a6795932b7 --- /dev/null +++ b/lib/cache/cachestorage.js @@ -0,0 +1,133 @@ +'use strict' + +const { kConstruct } = require('./symbols') +const { Cache } = require('./cache') +const { webidl } = require('../fetch/webidl') +const { kEnumerableProperty } = require('../core/util') + +class CacheStorage { + /** + * @see https://w3c.github.io/ServiceWorker/#dfn-relevant-name-to-cache-map + * @type {Map} + */ + async has (cacheName) { + webidl.brandCheck(this, CacheStorage) + webidl.argumentLengthCheck(arguments, 1, { header: 'CacheStorage.has' }) + + cacheName = webidl.converters.DOMString(cacheName) + + // 2.1.1 + // 2.2 + return this.#caches.has(cacheName) + } + + /** + * @see https://w3c.github.io/ServiceWorker/#dom-cachestorage-open + * @param {string} cacheName + * @returns {Promise} + */ + async open (cacheName) { + webidl.brandCheck(this, CacheStorage) + webidl.argumentLengthCheck(arguments, 1, { header: 'CacheStorage.open' }) + + cacheName = webidl.converters.DOMString(cacheName) + + // 2.1 + if (this.#caches.has(cacheName)) { + // await caches.open('v1') !== await caches.open('v1') + + // 2.1.1 + const cache = this.#caches.get(cacheName) + + // 2.1.1.1 + return new Cache(kConstruct, cache) + } + + // 2.2 + const cache = [] + + // 2.3 + this.#caches.set(cacheName, cache) + + // 2.4 + return new Cache(kConstruct, cache) + } + + /** + * @see https://w3c.github.io/ServiceWorker/#cache-storage-delete + * @param {string} cacheName + * @returns {Promise} + */ + async delete (cacheName) { + webidl.brandCheck(this, CacheStorage) + webidl.argumentLengthCheck(arguments, 1, { header: 'CacheStorage.delete' }) + + cacheName = webidl.converters.DOMString(cacheName) + + // 1. + // 2. + const cacheExists = this.#caches.has(cacheName) + + // 2.1 + if (!cacheExists) { + return false + } + + // 2.3.1 + this.#caches.delete(cacheName) + + // 2.3.2 + return true + } + + /** + * @see https://w3c.github.io/ServiceWorker/#cache-storage-keys + * @returns {string[]} + */ + async keys () { + webidl.brandCheck(this, CacheStorage) + + // 2.1 + const keys = this.#caches.keys() + + // 2.2 + return [...keys] + } +} + +Object.defineProperties(CacheStorage.prototype, { + [Symbol.toStringTag]: { + value: 'CacheStorage', + configurable: true + }, + match: kEnumerableProperty, + has: kEnumerableProperty, + open: kEnumerableProperty, + delete: kEnumerableProperty, + keys: kEnumerableProperty +}) + +module.exports = { + CacheStorage +} diff --git a/lib/cache/symbols.js b/lib/cache/symbols.js new file mode 100644 index 00000000000..f9b19740af8 --- /dev/null +++ b/lib/cache/symbols.js @@ -0,0 +1,5 @@ +'use strict' + +module.exports = { + kConstruct: Symbol('constructable') +} diff --git a/lib/cache/util.js b/lib/cache/util.js new file mode 100644 index 00000000000..44d52b789ed --- /dev/null +++ b/lib/cache/util.js @@ -0,0 +1,49 @@ +'use strict' + +const assert = require('assert') +const { URLSerializer } = require('../fetch/dataURL') +const { isValidHeaderName } = require('../fetch/util') + +/** + * @see https://url.spec.whatwg.org/#concept-url-equals + * @param {URL} A + * @param {URL} B + * @param {boolean | undefined} excludeFragment + * @returns {boolean} + */ +function urlEquals (A, B, excludeFragment = false) { + const serializedA = URLSerializer(A, excludeFragment) + + const serializedB = URLSerializer(B, excludeFragment) + + return serializedA === serializedB +} + +/** + * @see https://github.com/chromium/chromium/blob/694d20d134cb553d8d89e5500b9148012b1ba299/content/browser/cache_storage/cache_storage_cache.cc#L260-L262 + * @param {string} header + */ +function fieldValues (header) { + assert(header !== null) + + const values = [] + + for (let value of header.split(',')) { + value = value.trim() + + if (!value.length) { + continue + } else if (!isValidHeaderName(value)) { + continue + } + + values.push(value) + } + + return values +} + +module.exports = { + urlEquals, + fieldValues +} diff --git a/lib/fetch/response.js b/lib/fetch/response.js index ff06bfb47d0..96cacbce157 100644 --- a/lib/fetch/response.js +++ b/lib/fetch/response.js @@ -569,5 +569,6 @@ module.exports = { makeResponse, makeAppropriateNetworkError, filterResponse, - Response + Response, + cloneResponse } diff --git a/lib/fetch/util.js b/lib/fetch/util.js index 23023262d14..400687ba2e7 100644 --- a/lib/fetch/util.js +++ b/lib/fetch/util.js @@ -1028,5 +1028,6 @@ module.exports = { isomorphicDecode, urlIsLocal, urlHasHttpsScheme, - urlIsHttpHttpsScheme + urlIsHttpHttpsScheme, + readAllBytes } diff --git a/lib/fetch/webidl.js b/lib/fetch/webidl.js index e55de139505..38a05e65759 100644 --- a/lib/fetch/webidl.js +++ b/lib/fetch/webidl.js @@ -51,6 +51,13 @@ webidl.argumentLengthCheck = function ({ length }, min, ctx) { } } +webidl.illegalConstructor = function () { + throw webidl.errors.exception({ + header: 'TypeError', + message: 'Illegal constructor' + }) +} + // https://tc39.es/ecma262/#sec-ecmascript-data-types-and-values webidl.util.Type = function (V) { switch (typeof V) { diff --git a/test/wpt/runner/worker.mjs b/test/wpt/runner/worker.mjs index 0b323b4c972..638f53d7a57 100644 --- a/test/wpt/runner/worker.mjs +++ b/test/wpt/runner/worker.mjs @@ -9,6 +9,9 @@ import { } from '../../../index.js' import { CloseEvent } from '../../../lib/websocket/events.js' import { WebSocket } from '../../../lib/websocket/websocket.js' +import { Cache } from '../../../lib/cache/cache.js' +import { CacheStorage } from '../../../lib/cache/cachestorage.js' +import { kConstruct } from '../../../lib/cache/symbols.js' const { initScripts, meta, test, url, path } = workerData @@ -74,6 +77,18 @@ Object.defineProperties(globalThis, { ...globalPropertyDescriptors, // See https://github.com/nodejs/node/pull/45659 value: buffer.Blob + }, + caches: { + ...globalPropertyDescriptors, + value: new CacheStorage(kConstruct) + }, + Cache: { + ...globalPropertyDescriptors, + value: Cache + }, + CacheStorage: { + ...globalPropertyDescriptors, + value: CacheStorage } }) diff --git a/test/wpt/server/server.mjs b/test/wpt/server/server.mjs index b06f86d13d5..75039acb916 100644 --- a/test/wpt/server/server.mjs +++ b/test/wpt/server/server.mjs @@ -32,6 +32,10 @@ const server = createServer(async (req, res) => { const fullUrl = new URL(req.url, `http://localhost:${server.address().port}`) switch (fullUrl.pathname) { + case '/service-workers/cache-storage/resources/blank.html': { + res.setHeader('content-type', 'text/html') + // fall through + } case '/fetch/content-encoding/resources/foo.octetstream.gz': case '/fetch/content-encoding/resources/foo.text.gz': case '/fetch/api/resources/cors-top.txt': @@ -357,9 +361,20 @@ const server = createServer(async (req, res) => { res.end('') return } + case '/resources/simple.txt': { + res.end(readFileSync(join(tests, 'service-workers/service-worker', fullUrl.pathname), 'utf-8')) + return + } + case '/resources/fetch-status.py': { + const status = Number(fullUrl.searchParams.get('status')) + + res.statusCode = status + res.end() + return + } default: { res.statusCode = 200 - res.end('body') + res.end(fullUrl.toString()) } } }).listen(0) diff --git a/test/wpt/start-cacheStorage.mjs b/test/wpt/start-cacheStorage.mjs new file mode 100644 index 00000000000..a630e052285 --- /dev/null +++ b/test/wpt/start-cacheStorage.mjs @@ -0,0 +1,26 @@ +import { WPTRunner } from './runner/runner.mjs' +import { join } from 'path' +import { fileURLToPath } from 'url' +import { fork } from 'child_process' +import { on } from 'events' + +const serverPath = fileURLToPath(join(import.meta.url, '../server/server.mjs')) + +const child = fork(serverPath, [], { + stdio: ['pipe', 'pipe', 'pipe', 'ipc'] +}) + +child.on('exit', (code) => process.exit(code)) + +for await (const [message] of on(child, 'message')) { + if (message.server) { + const runner = new WPTRunner('service-workers/cache-storage', message.server) + runner.run() + + runner.once('completion', () => { + if (child.connected) { + child.send('shutdown') + } + }) + } +} diff --git a/test/wpt/status/service-workers/cache-storage.status.json b/test/wpt/status/service-workers/cache-storage.status.json new file mode 100644 index 00000000000..ed84c140eca --- /dev/null +++ b/test/wpt/status/service-workers/cache-storage.status.json @@ -0,0 +1,54 @@ +{ + "cache-storage": { + "cache-abort.https.any.js": { + "skip": true + }, + "cache-storage-buckets.https.any.js": { + "skip": true, + "note": "navigator is not defined" + }, + "cache-storage-match.https.any.js": { + "skip": true, + "note": "CacheStorage.prototype.match isnt implemented yet" + }, + "cache-put.https.any.js": { + "note": "probably can be fixed", + "fail": [ + "Cache.put with a VARY:* opaque response should not reject", + "Cache.put with opaque-filtered HTTP 206 response", + "Cache.put with a relative URL" + ] + }, + "cache-match.https.any.js": { + "note": "requires https server", + "fail": [ + "cors-exposed header should be stored correctly.", + "Cache.match ignores vary headers on opaque response." + ] + }, + "cache-delete.https.any.js": { + "note": "spec bug? - https://github.com/w3c/ServiceWorker/issues/1677 (first fail)", + "fail": [ + "Cache.delete called with a string URL", + "Cache.delete with ignoreSearch option (when it is specified as false)" + ] + }, + "cache-keys.https.any.js": { + "note": "probably can be fixed", + "fail": [ + "Cache.keys with ignoreSearch option (request with search parameters)", + "Cache.keys without parameters", + "Cache.keys with explicitly undefined request" + ] + }, + "cache-matchAll.https.any.js": { + "note": "probably can be fixed", + "fail": [ + "Cache.matchAll with ignoreSearch option (request with search parameters)", + "Cache.matchAll without parameters", + "Cache.matchAll with explicitly undefined request", + "Cache.matchAll with explicitly undefined request and empty options" + ] + } + } +} diff --git a/test/wpt/tests/service-workers/META.yml b/test/wpt/tests/service-workers/META.yml new file mode 100644 index 00000000000..03a0dd0fe16 --- /dev/null +++ b/test/wpt/tests/service-workers/META.yml @@ -0,0 +1,6 @@ +spec: https://w3c.github.io/ServiceWorker/ +suggested_reviewers: + - asutherland + - mkruisselbrink + - mattto + - wanderview diff --git a/test/wpt/tests/service-workers/cache-storage/META.yml b/test/wpt/tests/service-workers/cache-storage/META.yml new file mode 100644 index 00000000000..bf34474f74a --- /dev/null +++ b/test/wpt/tests/service-workers/cache-storage/META.yml @@ -0,0 +1,3 @@ +suggested_reviewers: + - inexorabletash + - wanderview diff --git a/test/wpt/tests/service-workers/cache-storage/cache-abort.https.any.js b/test/wpt/tests/service-workers/cache-storage/cache-abort.https.any.js new file mode 100644 index 00000000000..960d1bb1bff --- /dev/null +++ b/test/wpt/tests/service-workers/cache-storage/cache-abort.https.any.js @@ -0,0 +1,81 @@ +// META: title=Cache Storage: Abort +// META: global=window,worker +// META: script=./resources/test-helpers.js +// META: script=/common/utils.js +// META: timeout=long + +// We perform the same tests on put, add, addAll. Parameterise the tests to +// reduce repetition. +const methodsToTest = { + put: async (cache, request) => { + const response = await fetch(request); + return cache.put(request, response); + }, + add: async (cache, request) => cache.add(request), + addAll: async (cache, request) => cache.addAll([request]), +}; + +for (const method in methodsToTest) { + const perform = methodsToTest[method]; + + cache_test(async (cache, test) => { + const controller = new AbortController(); + const signal = controller.signal; + controller.abort(); + const request = new Request('../resources/simple.txt', { signal }); + return promise_rejects_dom(test, 'AbortError', perform(cache, request), + `${method} should reject`); + }, `${method}() on an already-aborted request should reject with AbortError`); + + cache_test(async (cache, test) => { + const controller = new AbortController(); + const signal = controller.signal; + const request = new Request('../resources/simple.txt', { signal }); + const promise = perform(cache, request); + controller.abort(); + return promise_rejects_dom(test, 'AbortError', promise, + `${method} should reject`); + }, `${method}() synchronously followed by abort should reject with ` + + `AbortError`); + + cache_test(async (cache, test) => { + const controller = new AbortController(); + const signal = controller.signal; + const stateKey = token(); + const abortKey = token(); + const request = new Request( + `../../../fetch/api/resources/infinite-slow-response.py?stateKey=${stateKey}&abortKey=${abortKey}`, + { signal }); + + const promise = perform(cache, request); + + // Wait for the server to start sending the response body. + let opened = false; + do { + // Normally only one fetch to 'stash-take' is needed, but the fetches + // will be served in reverse order sometimes + // (i.e., 'stash-take' gets served before 'infinite-slow-response'). + + const response = + await fetch(`../../../fetch/api/resources/stash-take.py?key=${stateKey}`); + const body = await response.json(); + if (body === 'open') opened = true; + } while (!opened); + + // Sadly the above loop cannot guarantee that the browser has started + // processing the response body. This delay is needed to make the test + // failures non-flaky in Chrome version 66. My deepest apologies. + await new Promise(resolve => setTimeout(resolve, 250)); + + controller.abort(); + + await promise_rejects_dom(test, 'AbortError', promise, + `${method} should reject`); + + // infinite-slow-response.py doesn't know when to stop. + return fetch(`../../../fetch/api/resources/stash-put.py?key=${abortKey}`); + }, `${method}() followed by abort after headers received should reject ` + + `with AbortError`); +} + +done(); diff --git a/test/wpt/tests/service-workers/cache-storage/cache-add.https.any.js b/test/wpt/tests/service-workers/cache-storage/cache-add.https.any.js new file mode 100644 index 00000000000..eca516abd5f --- /dev/null +++ b/test/wpt/tests/service-workers/cache-storage/cache-add.https.any.js @@ -0,0 +1,368 @@ +// META: title=Cache.add and Cache.addAll +// META: global=window,worker +// META: script=/common/get-host-info.sub.js +// META: script=./resources/test-helpers.js +// META: timeout=long + +const { REMOTE_HOST } = get_host_info(); + +cache_test(function(cache, test) { + return promise_rejects_js( + test, + TypeError, + cache.add(), + 'Cache.add should throw a TypeError when no arguments are given.'); + }, 'Cache.add called with no arguments'); + +cache_test(function(cache) { + return cache.add('./resources/simple.txt') + .then(function(result) { + assert_equals(result, undefined, + 'Cache.add should resolve with undefined on success.'); + return cache.match('./resources/simple.txt'); + }) + .then(function(response) { + assert_class_string(response, 'Response', + 'Cache.add should put a resource in the cache.'); + return response.text(); + }) + .then(function(body) { + assert_equals(body, 'a simple text file\n', + 'Cache.add should retrieve the correct body.'); + }); + }, 'Cache.add called with relative URL specified as a string'); + +cache_test(function(cache, test) { + return promise_rejects_js( + test, + TypeError, + cache.add('javascript://this-is-not-http-mmkay'), + 'Cache.add should throw a TypeError for non-HTTP/HTTPS URLs.'); + }, 'Cache.add called with non-HTTP/HTTPS URL'); + +cache_test(function(cache) { + var request = new Request('./resources/simple.txt'); + return cache.add(request) + .then(function(result) { + assert_equals(result, undefined, + 'Cache.add should resolve with undefined on success.'); + }); + }, 'Cache.add called with Request object'); + +cache_test(function(cache, test) { + var request = new Request('./resources/simple.txt', + {method: 'POST', body: 'This is a body.'}); + return promise_rejects_js( + test, + TypeError, + cache.add(request), + 'Cache.add should throw a TypeError for non-GET requests.'); + }, 'Cache.add called with POST request'); + +cache_test(function(cache) { + var request = new Request('./resources/simple.txt'); + return cache.add(request) + .then(function(result) { + assert_equals(result, undefined, + 'Cache.add should resolve with undefined on success.'); + }) + .then(function() { + return cache.add(request); + }) + .then(function(result) { + assert_equals(result, undefined, + 'Cache.add should resolve with undefined on success.'); + }); + }, 'Cache.add called twice with the same Request object'); + +cache_test(function(cache) { + var request = new Request('./resources/simple.txt'); + return request.text() + .then(function() { + assert_false(request.bodyUsed); + }) + .then(function() { + return cache.add(request); + }); + }, 'Cache.add with request with null body (not consumed)'); + +cache_test(function(cache, test) { + return promise_rejects_js( + test, + TypeError, + cache.add('./resources/fetch-status.py?status=206'), + 'Cache.add should reject on partial response'); + }, 'Cache.add with 206 response'); + +cache_test(function(cache, test) { + var urls = ['./resources/fetch-status.py?status=206', + './resources/fetch-status.py?status=200']; + var requests = urls.map(function(url) { + return new Request(url); + }); + return promise_rejects_js( + test, + TypeError, + cache.addAll(requests), + 'Cache.addAll should reject with TypeError if any request fails'); + }, 'Cache.addAll with 206 response'); + +cache_test(function(cache, test) { + var urls = ['./resources/fetch-status.py?status=206', + './resources/fetch-status.py?status=200']; + var requests = urls.map(function(url) { + var cross_origin_url = new URL(url, location.href); + cross_origin_url.hostname = REMOTE_HOST; + return new Request(cross_origin_url.href, { mode: 'no-cors' }); + }); + return promise_rejects_js( + test, + TypeError, + cache.addAll(requests), + 'Cache.addAll should reject with TypeError if any request fails'); + }, 'Cache.addAll with opaque-filtered 206 response'); + +cache_test(function(cache, test) { + return promise_rejects_js( + test, + TypeError, + cache.add('this-does-not-exist-please-dont-create-it'), + 'Cache.add should reject if response is !ok'); + }, 'Cache.add with request that results in a status of 404'); + + +cache_test(function(cache, test) { + return promise_rejects_js( + test, + TypeError, + cache.add('./resources/fetch-status.py?status=500'), + 'Cache.add should reject if response is !ok'); + }, 'Cache.add with request that results in a status of 500'); + +cache_test(function(cache, test) { + return promise_rejects_js( + test, + TypeError, + cache.addAll(), + 'Cache.addAll with no arguments should throw TypeError.'); + }, 'Cache.addAll with no arguments'); + +cache_test(function(cache, test) { + // Assumes the existence of ../resources/simple.txt and ../resources/blank.html + var urls = ['./resources/simple.txt', undefined, './resources/blank.html']; + return promise_rejects_js( + test, + TypeError, + cache.addAll(urls), + 'Cache.addAll should throw TypeError for an undefined argument.'); + }, 'Cache.addAll with a mix of valid and undefined arguments'); + +cache_test(function(cache) { + return cache.addAll([]) + .then(function(result) { + assert_equals(result, undefined, + 'Cache.addAll should resolve with undefined on ' + + 'success.'); + return cache.keys(); + }) + .then(function(result) { + assert_equals(result.length, 0, + 'There should be no entry in the cache.'); + }); + }, 'Cache.addAll with an empty array'); + +cache_test(function(cache) { + // Assumes the existence of ../resources/simple.txt and + // ../resources/blank.html + var urls = ['./resources/simple.txt', + self.location.href, + './resources/blank.html']; + return cache.addAll(urls) + .then(function(result) { + assert_equals(result, undefined, + 'Cache.addAll should resolve with undefined on ' + + 'success.'); + return Promise.all( + urls.map(function(url) { return cache.match(url); })); + }) + .then(function(responses) { + assert_class_string( + responses[0], 'Response', + 'Cache.addAll should put a resource in the cache.'); + assert_class_string( + responses[1], 'Response', + 'Cache.addAll should put a resource in the cache.'); + assert_class_string( + responses[2], 'Response', + 'Cache.addAll should put a resource in the cache.'); + return Promise.all( + responses.map(function(response) { return response.text(); })); + }) + .then(function(bodies) { + assert_equals( + bodies[0], 'a simple text file\n', + 'Cache.add should retrieve the correct body.'); + assert_equals( + bodies[2], '\nEmpty doc\n', + 'Cache.add should retrieve the correct body.'); + }); + }, 'Cache.addAll with string URL arguments'); + +cache_test(function(cache) { + // Assumes the existence of ../resources/simple.txt and + // ../resources/blank.html + var urls = ['./resources/simple.txt', + self.location.href, + './resources/blank.html']; + var requests = urls.map(function(url) { + return new Request(url); + }); + return cache.addAll(requests) + .then(function(result) { + assert_equals(result, undefined, + 'Cache.addAll should resolve with undefined on ' + + 'success.'); + return Promise.all( + urls.map(function(url) { return cache.match(url); })); + }) + .then(function(responses) { + assert_class_string( + responses[0], 'Response', + 'Cache.addAll should put a resource in the cache.'); + assert_class_string( + responses[1], 'Response', + 'Cache.addAll should put a resource in the cache.'); + assert_class_string( + responses[2], 'Response', + 'Cache.addAll should put a resource in the cache.'); + return Promise.all( + responses.map(function(response) { return response.text(); })); + }) + .then(function(bodies) { + assert_equals( + bodies[0], 'a simple text file\n', + 'Cache.add should retrieve the correct body.'); + assert_equals( + bodies[2], '\nEmpty doc\n', + 'Cache.add should retrieve the correct body.'); + }); + }, 'Cache.addAll with Request arguments'); + +cache_test(function(cache, test) { + // Assumes that ../resources/simple.txt and ../resources/blank.html exist. + // The second resource does not. + var urls = ['./resources/simple.txt', + 'this-resource-should-not-exist', + './resources/blank.html']; + var requests = urls.map(function(url) { + return new Request(url); + }); + return promise_rejects_js( + test, + TypeError, + cache.addAll(requests), + 'Cache.addAll should reject with TypeError if any request fails') + .then(function() { + return Promise.all(urls.map(function(url) { + return cache.match(url); + })); + }) + .then(function(matches) { + assert_array_equals( + matches, + [undefined, undefined, undefined], + 'If any response fails, no response should be added to cache'); + }); + }, 'Cache.addAll with a mix of succeeding and failing requests'); + +cache_test(function(cache, test) { + var request = new Request('../resources/simple.txt'); + return promise_rejects_dom( + test, + 'InvalidStateError', + cache.addAll([request, request]), + 'Cache.addAll should throw InvalidStateError if the same request is added ' + + 'twice.'); + }, 'Cache.addAll called with the same Request object specified twice'); + +cache_test(async function(cache, test) { + const url = './resources/vary.py?vary=x-shape'; + let requests = [ + new Request(url, { headers: { 'x-shape': 'circle' }}), + new Request(url, { headers: { 'x-shape': 'square' }}), + ]; + let result = await cache.addAll(requests); + assert_equals(result, undefined, 'Cache.addAll() should succeed'); + }, 'Cache.addAll should succeed when entries differ by vary header'); + +cache_test(async function(cache, test) { + const url = './resources/vary.py?vary=x-shape'; + let requests = [ + new Request(url, { headers: { 'x-shape': 'circle' }}), + new Request(url, { headers: { 'x-shape': 'circle' }}), + ]; + await promise_rejects_dom( + test, + 'InvalidStateError', + cache.addAll(requests), + 'Cache.addAll() should reject when entries are duplicate by vary header'); + }, 'Cache.addAll should reject when entries are duplicate by vary header'); + +// VARY header matching is asymmetric. Determining if two entries are duplicate +// depends on which entry's response is used in the comparison. The target +// response's VARY header determines what request headers are examined. This +// test verifies that Cache.addAll() duplicate checking handles this asymmetric +// behavior correctly. +cache_test(async function(cache, test) { + const base_url = './resources/vary.py'; + + // Define a request URL that sets a VARY header in the + // query string to be echoed back by the server. + const url = base_url + '?vary=x-size'; + + // Set a cookie to override the VARY header of the response + // when the request is made with credentials. This will + // take precedence over the query string vary param. This + // is a bit confusing, but it's necessary to construct a test + // where the URL is the same, but the VARY headers differ. + // + // Note, the test could also pass this information in additional + // request headers. If the cookie approach becomes too unwieldy + // this test could be rewritten to use that technique. + await fetch(base_url + '?set-vary-value-override-cookie=x-shape'); + test.add_cleanup(_ => fetch(base_url + '?clear-vary-value-override-cookie')); + + let requests = [ + // This request will result in a Response with a "Vary: x-shape" + // header. This *will not* result in a duplicate match with the + // other entry. + new Request(url, { headers: { 'x-shape': 'circle', + 'x-size': 'big' }, + credentials: 'same-origin' }), + + // This request will result in a Response with a "Vary: x-size" + // header. This *will* result in a duplicate match with the other + // entry. + new Request(url, { headers: { 'x-shape': 'square', + 'x-size': 'big' }, + credentials: 'omit' }), + ]; + await promise_rejects_dom( + test, + 'InvalidStateError', + cache.addAll(requests), + 'Cache.addAll() should reject when one entry has a vary header ' + + 'matching an earlier entry.'); + + // Test the reverse order now. + await promise_rejects_dom( + test, + 'InvalidStateError', + cache.addAll(requests.reverse()), + 'Cache.addAll() should reject when one entry has a vary header ' + + 'matching a later entry.'); + + }, 'Cache.addAll should reject when one entry has a vary header ' + + 'matching another entry'); + +done(); diff --git a/test/wpt/tests/service-workers/cache-storage/cache-delete.https.any.js b/test/wpt/tests/service-workers/cache-storage/cache-delete.https.any.js new file mode 100644 index 00000000000..3eae2b6a08b --- /dev/null +++ b/test/wpt/tests/service-workers/cache-storage/cache-delete.https.any.js @@ -0,0 +1,164 @@ +// META: title=Cache.delete +// META: global=window,worker +// META: script=./resources/test-helpers.js +// META: timeout=long + +var test_url = 'https://example.com/foo'; + +// Construct a generic Request object. The URL is |test_url|. All other fields +// are defaults. +function new_test_request() { + return new Request(test_url); +} + +// Construct a generic Response object. +function new_test_response() { + return new Response('Hello world!', { status: 200 }); +} + +cache_test(function(cache, test) { + return promise_rejects_js( + test, + TypeError, + cache.delete(), + 'Cache.delete should reject with a TypeError when called with no ' + + 'arguments.'); + }, 'Cache.delete with no arguments'); + +cache_test(function(cache) { + return cache.put(new_test_request(), new_test_response()) + .then(function() { + return cache.delete(test_url); + }) + .then(function(result) { + assert_true(result, + 'Cache.delete should resolve with "true" if an entry ' + + 'was successfully deleted.'); + return cache.match(test_url); + }) + .then(function(result) { + assert_equals(result, undefined, + 'Cache.delete should remove matching entries from cache.'); + }); + }, 'Cache.delete called with a string URL'); + +cache_test(function(cache) { + var request = new Request(test_url); + return cache.put(request, new_test_response()) + .then(function() { + return cache.delete(request); + }) + .then(function(result) { + assert_true(result, + 'Cache.delete should resolve with "true" if an entry ' + + 'was successfully deleted.'); + }); + }, 'Cache.delete called with a Request object'); + +cache_test(function(cache) { + var request = new Request(test_url); + var response = new_test_response(); + return cache.put(request, response) + .then(function() { + return cache.delete(new Request(test_url, {method: 'HEAD'})); + }) + .then(function(result) { + assert_false(result, + 'Cache.delete should not match a non-GET request ' + + 'unless ignoreMethod option is set.'); + return cache.match(test_url); + }) + .then(function(result) { + assert_response_equals(result, response, + 'Cache.delete should leave non-matching response in the cache.'); + return cache.delete(new Request(test_url, {method: 'HEAD'}), + {ignoreMethod: true}); + }) + .then(function(result) { + assert_true(result, + 'Cache.delete should match a non-GET request ' + + ' if ignoreMethod is true.'); + }); + }, 'Cache.delete called with a HEAD request'); + +cache_test(function(cache) { + var vary_request = new Request('http://example.com/c', + {headers: {'Cookies': 'is-for-cookie'}}); + var vary_response = new Response('', {headers: {'Vary': 'Cookies'}}); + var mismatched_vary_request = new Request('http://example.com/c'); + + return cache.put(vary_request.clone(), vary_response.clone()) + .then(function() { + return cache.delete(mismatched_vary_request.clone()); + }) + .then(function(result) { + assert_false(result, + 'Cache.delete should not delete if vary does not ' + + 'match unless ignoreVary is true'); + return cache.delete(mismatched_vary_request.clone(), + {ignoreVary: true}); + }) + .then(function(result) { + assert_true(result, + 'Cache.delete should ignore vary if ignoreVary is true'); + }); + }, 'Cache.delete supports ignoreVary'); + +cache_test(function(cache) { + return cache.delete(test_url) + .then(function(result) { + assert_false(result, + 'Cache.delete should resolve with "false" if there ' + + 'are no matching entries.'); + }); + }, 'Cache.delete with a non-existent entry'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.matchAll(entries.a_with_query.request, + { ignoreSearch: true }) + .then(function(result) { + assert_response_array_equals( + result, + [ + entries.a.response, + entries.a_with_query.response + ]); + return cache.delete(entries.a_with_query.request, + { ignoreSearch: true }); + }) + .then(function(result) { + return cache.matchAll(entries.a_with_query.request, + { ignoreSearch: true }); + }) + .then(function(result) { + assert_response_array_equals(result, []); + }); + }, + 'Cache.delete with ignoreSearch option (request with search parameters)'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.matchAll(entries.a_with_query.request, + { ignoreSearch: true }) + .then(function(result) { + assert_response_array_equals( + result, + [ + entries.a.response, + entries.a_with_query.response + ]); + // cache.delete()'s behavior should be the same if ignoreSearch is + // not provided or if ignoreSearch is false. + return cache.delete(entries.a_with_query.request, + { ignoreSearch: false }); + }) + .then(function(result) { + return cache.matchAll(entries.a_with_query.request, + { ignoreSearch: true }); + }) + .then(function(result) { + assert_response_array_equals(result, [ entries.a.response ]); + }); + }, + 'Cache.delete with ignoreSearch option (when it is specified as false)'); + +done(); diff --git a/test/wpt/tests/service-workers/cache-storage/cache-keys-attributes-for-service-worker.https.html b/test/wpt/tests/service-workers/cache-storage/cache-keys-attributes-for-service-worker.https.html new file mode 100644 index 00000000000..3c96348e0e0 --- /dev/null +++ b/test/wpt/tests/service-workers/cache-storage/cache-keys-attributes-for-service-worker.https.html @@ -0,0 +1,75 @@ + +Cache.keys (checking request attributes that can be set only on service workers) + + + + + diff --git a/test/wpt/tests/service-workers/cache-storage/cache-keys.https.any.js b/test/wpt/tests/service-workers/cache-storage/cache-keys.https.any.js new file mode 100644 index 00000000000..232fb760d40 --- /dev/null +++ b/test/wpt/tests/service-workers/cache-storage/cache-keys.https.any.js @@ -0,0 +1,212 @@ +// META: title=Cache.keys +// META: global=window,worker +// META: script=./resources/test-helpers.js +// META: timeout=long + +cache_test(cache => { + return cache.keys() + .then(requests => { + assert_equals( + requests.length, 0, + 'Cache.keys should resolve to an empty array for an empty cache'); + }); + }, 'Cache.keys() called on an empty cache'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.keys('not-present-in-the-cache') + .then(function(result) { + assert_request_array_equals( + result, [], + 'Cache.keys should resolve with an empty array on failure.'); + }); + }, 'Cache.keys with no matching entries'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.keys(entries.a.request.url) + .then(function(result) { + assert_request_array_equals(result, [entries.a.request], + 'Cache.keys should match by URL.'); + }); + }, 'Cache.keys with URL'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.keys(entries.a.request) + .then(function(result) { + assert_request_array_equals( + result, [entries.a.request], + 'Cache.keys should match by Request.'); + }); + }, 'Cache.keys with Request'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.keys(new Request(entries.a.request.url)) + .then(function(result) { + assert_request_array_equals( + result, [entries.a.request], + 'Cache.keys should match by Request.'); + }); + }, 'Cache.keys with new Request'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.keys(entries.a.request, {ignoreSearch: true}) + .then(function(result) { + assert_request_array_equals( + result, + [ + entries.a.request, + entries.a_with_query.request + ], + 'Cache.keys with ignoreSearch should ignore the ' + + 'search parameters of cached request.'); + }); + }, + 'Cache.keys with ignoreSearch option (request with no search ' + + 'parameters)'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.keys(entries.a_with_query.request, {ignoreSearch: true}) + .then(function(result) { + assert_request_array_equals( + result, + [ + entries.a.request, + entries.a_with_query.request + ], + 'Cache.keys with ignoreSearch should ignore the ' + + 'search parameters of request.'); + }); + }, + 'Cache.keys with ignoreSearch option (request with search parameters)'); + +cache_test(function(cache) { + var request = new Request('http://example.com/'); + var head_request = new Request('http://example.com/', {method: 'HEAD'}); + var response = new Response('foo'); + return cache.put(request.clone(), response.clone()) + .then(function() { + return cache.keys(head_request.clone()); + }) + .then(function(result) { + assert_request_array_equals( + result, [], + 'Cache.keys should resolve with an empty array with a ' + + 'mismatched method.'); + return cache.keys(head_request.clone(), + {ignoreMethod: true}); + }) + .then(function(result) { + assert_request_array_equals( + result, + [ + request, + ], + 'Cache.keys with ignoreMethod should ignore the ' + + 'method of request.'); + }); + }, 'Cache.keys supports ignoreMethod'); + +cache_test(function(cache) { + var vary_request = new Request('http://example.com/c', + {headers: {'Cookies': 'is-for-cookie'}}); + var vary_response = new Response('', {headers: {'Vary': 'Cookies'}}); + var mismatched_vary_request = new Request('http://example.com/c'); + + return cache.put(vary_request.clone(), vary_response.clone()) + .then(function() { + return cache.keys(mismatched_vary_request.clone()); + }) + .then(function(result) { + assert_request_array_equals( + result, [], + 'Cache.keys should resolve with an empty array with a ' + + 'mismatched vary.'); + return cache.keys(mismatched_vary_request.clone(), + {ignoreVary: true}); + }) + .then(function(result) { + assert_request_array_equals( + result, + [ + vary_request, + ], + 'Cache.keys with ignoreVary should ignore the ' + + 'vary of request.'); + }); + }, 'Cache.keys supports ignoreVary'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.keys(entries.cat.request.url + '#mouse') + .then(function(result) { + assert_request_array_equals( + result, + [ + entries.cat.request, + ], + 'Cache.keys should ignore URL fragment.'); + }); + }, 'Cache.keys with URL containing fragment'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.keys('http') + .then(function(result) { + assert_request_array_equals( + result, [], + 'Cache.keys should treat query as a URL and not ' + + 'just a string fragment.'); + }); + }, 'Cache.keys with string fragment "http" as query'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.keys() + .then(function(result) { + assert_request_array_equals( + result, + simple_entries.map(entry => entry.request), + 'Cache.keys without parameters should match all entries.'); + }); + }, 'Cache.keys without parameters'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.keys(undefined) + .then(function(result) { + assert_request_array_equals( + result, + simple_entries.map(entry => entry.request), + 'Cache.keys with undefined request should match all entries.'); + }); + }, 'Cache.keys with explicitly undefined request'); + +cache_test(cache => { + return cache.keys(undefined, {}) + .then(requests => { + assert_equals( + requests.length, 0, + 'Cache.keys should resolve to an empty array for an empty cache'); + }); + }, 'Cache.keys with explicitly undefined request and empty options'); + +prepopulated_cache_test(vary_entries, function(cache, entries) { + return cache.keys() + .then(function(result) { + assert_request_array_equals( + result, + [ + entries.vary_cookie_is_cookie.request, + entries.vary_cookie_is_good.request, + entries.vary_cookie_absent.request, + ], + 'Cache.keys without parameters should match all entries.'); + }); + }, 'Cache.keys without parameters and VARY entries'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.keys(new Request(entries.cat.request.url, {method: 'HEAD'})) + .then(function(result) { + assert_request_array_equals( + result, [], + 'Cache.keys should not match HEAD request unless ignoreMethod ' + + 'option is set.'); + }); + }, 'Cache.keys with a HEAD Request'); + +done(); diff --git a/test/wpt/tests/service-workers/cache-storage/cache-match.https.any.js b/test/wpt/tests/service-workers/cache-storage/cache-match.https.any.js new file mode 100644 index 00000000000..9ca45903cbb --- /dev/null +++ b/test/wpt/tests/service-workers/cache-storage/cache-match.https.any.js @@ -0,0 +1,437 @@ +// META: title=Cache.match +// META: global=window,worker +// META: script=./resources/test-helpers.js +// META: script=/common/get-host-info.sub.js +// META: timeout=long + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.match('not-present-in-the-cache') + .then(function(result) { + assert_equals(result, undefined, + 'Cache.match failures should resolve with undefined.'); + }); + }, 'Cache.match with no matching entries'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.match(entries.a.request.url) + .then(function(result) { + assert_response_equals(result, entries.a.response, + 'Cache.match should match by URL.'); + }); + }, 'Cache.match with URL'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.match(entries.a.request) + .then(function(result) { + assert_response_equals(result, entries.a.response, + 'Cache.match should match by Request.'); + }); + }, 'Cache.match with Request'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + var alt_response = new Response('', {status: 201}); + + return self.caches.open('second_matching_cache') + .then(function(cache) { + return cache.put(entries.a.request, alt_response.clone()); + }) + .then(function() { + return cache.match(entries.a.request); + }) + .then(function(result) { + assert_response_equals( + result, entries.a.response, + 'Cache.match should match the first cache.'); + }); + }, 'Cache.match with multiple cache hits'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.match(new Request(entries.a.request.url)) + .then(function(result) { + assert_response_equals(result, entries.a.response, + 'Cache.match should match by Request.'); + }); + }, 'Cache.match with new Request'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.match(new Request(entries.a.request.url, {method: 'HEAD'})) + .then(function(result) { + assert_equals(result, undefined, + 'Cache.match should not match HEAD Request.'); + }); + }, 'Cache.match with HEAD'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.match(entries.a.request, + {ignoreSearch: true}) + .then(function(result) { + assert_response_in_array( + result, + [ + entries.a.response, + entries.a_with_query.response + ], + 'Cache.match with ignoreSearch should ignore the ' + + 'search parameters of cached request.'); + }); + }, + 'Cache.match with ignoreSearch option (request with no search ' + + 'parameters)'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.match(entries.a_with_query.request, + {ignoreSearch: true}) + .then(function(result) { + assert_response_in_array( + result, + [ + entries.a.response, + entries.a_with_query.response + ], + 'Cache.match with ignoreSearch should ignore the ' + + 'search parameters of request.'); + }); + }, + 'Cache.match with ignoreSearch option (request with search parameter)'); + +cache_test(function(cache) { + var request = new Request('http://example.com/'); + var head_request = new Request('http://example.com/', {method: 'HEAD'}); + var response = new Response('foo'); + return cache.put(request.clone(), response.clone()) + .then(function() { + return cache.match(head_request.clone()); + }) + .then(function(result) { + assert_equals( + result, undefined, + 'Cache.match should resolve as undefined with a ' + + 'mismatched method.'); + return cache.match(head_request.clone(), + {ignoreMethod: true}); + }) + .then(function(result) { + assert_response_equals( + result, response, + 'Cache.match with ignoreMethod should ignore the ' + + 'method of request.'); + }); + }, 'Cache.match supports ignoreMethod'); + +cache_test(function(cache) { + var vary_request = new Request('http://example.com/c', + {headers: {'Cookies': 'is-for-cookie'}}); + var vary_response = new Response('', {headers: {'Vary': 'Cookies'}}); + var mismatched_vary_request = new Request('http://example.com/c'); + + return cache.put(vary_request.clone(), vary_response.clone()) + .then(function() { + return cache.match(mismatched_vary_request.clone()); + }) + .then(function(result) { + assert_equals( + result, undefined, + 'Cache.match should resolve as undefined with a ' + + 'mismatched vary.'); + return cache.match(mismatched_vary_request.clone(), + {ignoreVary: true}); + }) + .then(function(result) { + assert_response_equals( + result, vary_response, + 'Cache.match with ignoreVary should ignore the ' + + 'vary of request.'); + }); + }, 'Cache.match supports ignoreVary'); + +cache_test(function(cache) { + let has_cache_name = false; + const opts = { + get cacheName() { + has_cache_name = true; + return undefined; + } + }; + return self.caches.open('foo') + .then(function() { + return cache.match('bar', opts); + }) + .then(function() { + assert_false(has_cache_name, + 'Cache.match does not support cacheName option ' + + 'which was removed in CacheQueryOptions.'); + }); + }, 'Cache.match does not support cacheName option'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.match(entries.cat.request.url + '#mouse') + .then(function(result) { + assert_response_equals(result, entries.cat.response, + 'Cache.match should ignore URL fragment.'); + }); + }, 'Cache.match with URL containing fragment'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.match('http') + .then(function(result) { + assert_equals( + result, undefined, + 'Cache.match should treat query as a URL and not ' + + 'just a string fragment.'); + }); + }, 'Cache.match with string fragment "http" as query'); + +prepopulated_cache_test(vary_entries, function(cache, entries) { + return cache.match('http://example.com/c') + .then(function(result) { + assert_response_in_array( + result, + [ + entries.vary_cookie_absent.response + ], + 'Cache.match should honor "Vary" header.'); + }); + }, 'Cache.match with responses containing "Vary" header'); + +cache_test(function(cache) { + var request = new Request('http://example.com'); + var response; + var request_url = new URL('./resources/simple.txt', location.href).href; + return fetch(request_url) + .then(function(fetch_result) { + response = fetch_result; + assert_equals( + response.url, request_url, + '[https://fetch.spec.whatwg.org/#dom-response-url] ' + + 'Reponse.url should return the URL of the response.'); + return cache.put(request, response.clone()); + }) + .then(function() { + return cache.match(request.url); + }) + .then(function(result) { + assert_response_equals( + result, response, + 'Cache.match should return a Response object that has the same ' + + 'properties as the stored response.'); + return cache.match(response.url); + }) + .then(function(result) { + assert_equals( + result, undefined, + 'Cache.match should not match cache entry based on response URL.'); + }); + }, 'Cache.match with Request and Response objects with different URLs'); + +cache_test(function(cache) { + var request_url = new URL('./resources/simple.txt', location.href).href; + return fetch(request_url) + .then(function(fetch_result) { + return cache.put(new Request(request_url), fetch_result); + }) + .then(function() { + return cache.match(request_url); + }) + .then(function(result) { + return result.text(); + }) + .then(function(body_text) { + assert_equals(body_text, 'a simple text file\n', + 'Cache.match should return a Response object with a ' + + 'valid body.'); + }) + .then(function() { + return cache.match(request_url); + }) + .then(function(result) { + return result.text(); + }) + .then(function(body_text) { + assert_equals(body_text, 'a simple text file\n', + 'Cache.match should return a Response object with a ' + + 'valid body each time it is called.'); + }); + }, 'Cache.match invoked multiple times for the same Request/Response'); + +cache_test(function(cache) { + var request_url = new URL('./resources/simple.txt', location.href).href; + return fetch(request_url) + .then(function(fetch_result) { + return cache.put(new Request(request_url), fetch_result); + }) + .then(function() { + return cache.match(request_url); + }) + .then(function(result) { + return result.blob(); + }) + .then(function(blob) { + var sliced = blob.slice(2,8); + + return new Promise(function (resolve, reject) { + var reader = new FileReader(); + reader.onloadend = function(event) { + resolve(event.target.result); + }; + reader.readAsText(sliced); + }); + }) + .then(function(text) { + assert_equals(text, 'simple', + 'A Response blob returned by Cache.match should be ' + + 'sliceable.' ); + }); + }, 'Cache.match blob should be sliceable'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + var request = new Request(entries.a.request.clone(), {method: 'POST'}); + return cache.match(request) + .then(function(result) { + assert_equals(result, undefined, + 'Cache.match should not find a match'); + }); + }, 'Cache.match with POST Request'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + var response = entries.non_2xx_response.response; + return cache.match(entries.non_2xx_response.request.url) + .then(function(result) { + assert_response_equals( + result, entries.non_2xx_response.response, + 'Cache.match should return a Response object that has the ' + + 'same properties as a stored non-2xx response.'); + }); + }, 'Cache.match with a non-2xx Response'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + var response = entries.error_response.response; + return cache.match(entries.error_response.request.url) + .then(function(result) { + assert_response_equals( + result, entries.error_response.response, + 'Cache.match should return a Response object that has the ' + + 'same properties as a stored network error response.'); + }); + }, 'Cache.match with a network error Response'); + +cache_test(function(cache) { + // This test validates that we can get a Response from the Cache API, + // clone it, and read just one side of the clone. This was previously + // bugged in FF for Responses with large bodies. + var data = []; + data.length = 80 * 1024; + data.fill('F'); + var response; + return cache.put('/', new Response(data.toString())) + .then(function(result) { + return cache.match('/'); + }) + .then(function(r) { + // Make sure the original response is not GC'd. + response = r; + // Return only the clone. We purposefully test that the other + // half of the clone does not need to be read here. + return response.clone().text(); + }) + .then(function(text) { + assert_equals(text, data.toString(), 'cloned body text can be read correctly'); + }); + }, 'Cache produces large Responses that can be cloned and read correctly.'); + +cache_test(async (cache) => { + const url = get_host_info().HTTPS_REMOTE_ORIGIN + + '/service-workers/cache-storage/resources/simple.txt?pipe=' + + 'header(access-control-allow-origin,*)|' + + 'header(access-control-expose-headers,*)|' + + 'header(foo,bar)|' + + 'header(set-cookie,X)'; + + const response = await fetch(url); + await cache.put(new Request(url), response); + const cached_response = await cache.match(url); + + const headers = cached_response.headers; + assert_equals(headers.get('access-control-expose-headers'), '*'); + assert_equals(headers.get('foo'), 'bar'); + assert_equals(headers.get('set-cookie'), null); + }, 'cors-exposed header should be stored correctly.'); + +cache_test(async (cache) => { + // A URL that should load a resource with a known mime type. + const url = '/service-workers/cache-storage/resources/blank.html'; + const expected_mime_type = 'text/html'; + + // Verify we get the expected mime type from the network. Note, + // we cannot use an exact match here since some browsers append + // character encoding information to the blob.type value. + const net_response = await fetch(url); + const net_mime_type = (await net_response.blob()).type; + assert_true(net_mime_type.includes(expected_mime_type), + 'network response should include the expected mime type'); + + // Verify we get the exact same mime type when reading the same + // URL resource back out of the cache. + await cache.add(url); + const cache_response = await cache.match(url); + const cache_mime_type = (await cache_response.blob()).type; + assert_equals(cache_mime_type, net_mime_type, + 'network and cache response mime types should match'); + }, 'MIME type should be set from content-header correctly.'); + +cache_test(async (cache) => { + const url = '/dummy'; + const original_type = 'text/html'; + const override_type = 'text/plain'; + const init_with_headers = { + headers: { + 'content-type': original_type + } + } + + // Verify constructing a synthetic response with a content-type header + // gets the correct mime type. + const response = new Response('hello world', init_with_headers); + const original_response_type = (await response.blob()).type; + assert_true(original_response_type.includes(original_type), + 'original response should include the expected mime type'); + + // Verify overwriting the content-type header changes the mime type. + const overwritten_response = new Response('hello world', init_with_headers); + overwritten_response.headers.set('content-type', override_type); + const overwritten_response_type = (await overwritten_response.blob()).type; + assert_equals(overwritten_response_type, override_type, + 'mime type can be overridden'); + + // Verify the Response read from Cache uses the original mime type + // computed when it was first constructed. + const tmp = new Response('hello world', init_with_headers); + tmp.headers.set('content-type', override_type); + await cache.put(url, tmp); + const cache_response = await cache.match(url); + const cache_mime_type = (await cache_response.blob()).type; + assert_equals(cache_mime_type, override_type, + 'overwritten and cached response mime types should match'); + }, 'MIME type should reflect Content-Type headers of response.'); + +cache_test(async (cache) => { + const url = new URL('./resources/vary.py?vary=foo', + get_host_info().HTTPS_REMOTE_ORIGIN + self.location.pathname); + const original_request = new Request(url, { mode: 'no-cors', + headers: { 'foo': 'bar' } }); + const fetch_response = await fetch(original_request); + assert_equals(fetch_response.type, 'opaque'); + + await cache.put(original_request, fetch_response); + + const match_response_1 = await cache.match(original_request); + assert_not_equals(match_response_1, undefined); + + // Verify that cache.match() finds the entry even if queried with a varied + // header that does not match the cache key. Vary headers should be ignored + // for opaque responses. + const different_request = new Request(url, { headers: { 'foo': 'CHANGED' } }); + const match_response_2 = await cache.match(different_request); + assert_not_equals(match_response_2, undefined); +}, 'Cache.match ignores vary headers on opaque response.'); + +done(); diff --git a/test/wpt/tests/service-workers/cache-storage/cache-matchAll.https.any.js b/test/wpt/tests/service-workers/cache-storage/cache-matchAll.https.any.js new file mode 100644 index 00000000000..93c55178918 --- /dev/null +++ b/test/wpt/tests/service-workers/cache-storage/cache-matchAll.https.any.js @@ -0,0 +1,244 @@ +// META: title=Cache.matchAll +// META: global=window,worker +// META: script=./resources/test-helpers.js +// META: timeout=long + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.matchAll('not-present-in-the-cache') + .then(function(result) { + assert_response_array_equals( + result, [], + 'Cache.matchAll should resolve with an empty array on failure.'); + }); + }, 'Cache.matchAll with no matching entries'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.matchAll(entries.a.request.url) + .then(function(result) { + assert_response_array_equals(result, [entries.a.response], + 'Cache.matchAll should match by URL.'); + }); + }, 'Cache.matchAll with URL'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.matchAll(entries.a.request) + .then(function(result) { + assert_response_array_equals( + result, [entries.a.response], + 'Cache.matchAll should match by Request.'); + }); + }, 'Cache.matchAll with Request'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.matchAll(new Request(entries.a.request.url)) + .then(function(result) { + assert_response_array_equals( + result, [entries.a.response], + 'Cache.matchAll should match by Request.'); + }); + }, 'Cache.matchAll with new Request'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.matchAll(new Request(entries.a.request.url, {method: 'HEAD'}), + {ignoreSearch: true}) + .then(function(result) { + assert_response_array_equals( + result, [], + 'Cache.matchAll should not match HEAD Request.'); + }); + }, 'Cache.matchAll with HEAD'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.matchAll(entries.a.request, + {ignoreSearch: true}) + .then(function(result) { + assert_response_array_equals( + result, + [ + entries.a.response, + entries.a_with_query.response + ], + 'Cache.matchAll with ignoreSearch should ignore the ' + + 'search parameters of cached request.'); + }); + }, + 'Cache.matchAll with ignoreSearch option (request with no search ' + + 'parameters)'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.matchAll(entries.a_with_query.request, + {ignoreSearch: true}) + .then(function(result) { + assert_response_array_equals( + result, + [ + entries.a.response, + entries.a_with_query.response + ], + 'Cache.matchAll with ignoreSearch should ignore the ' + + 'search parameters of request.'); + }); + }, + 'Cache.matchAll with ignoreSearch option (request with search parameters)'); + +cache_test(function(cache) { + var request = new Request('http://example.com/'); + var head_request = new Request('http://example.com/', {method: 'HEAD'}); + var response = new Response('foo'); + return cache.put(request.clone(), response.clone()) + .then(function() { + return cache.matchAll(head_request.clone()); + }) + .then(function(result) { + assert_response_array_equals( + result, [], + 'Cache.matchAll should resolve with empty array for a ' + + 'mismatched method.'); + return cache.matchAll(head_request.clone(), + {ignoreMethod: true}); + }) + .then(function(result) { + assert_response_array_equals( + result, [response], + 'Cache.matchAll with ignoreMethod should ignore the ' + + 'method of request.'); + }); + }, 'Cache.matchAll supports ignoreMethod'); + +cache_test(function(cache) { + var vary_request = new Request('http://example.com/c', + {headers: {'Cookies': 'is-for-cookie'}}); + var vary_response = new Response('', {headers: {'Vary': 'Cookies'}}); + var mismatched_vary_request = new Request('http://example.com/c'); + + return cache.put(vary_request.clone(), vary_response.clone()) + .then(function() { + return cache.matchAll(mismatched_vary_request.clone()); + }) + .then(function(result) { + assert_response_array_equals( + result, [], + 'Cache.matchAll should resolve as undefined with a ' + + 'mismatched vary.'); + return cache.matchAll(mismatched_vary_request.clone(), + {ignoreVary: true}); + }) + .then(function(result) { + assert_response_array_equals( + result, [vary_response], + 'Cache.matchAll with ignoreVary should ignore the ' + + 'vary of request.'); + }); + }, 'Cache.matchAll supports ignoreVary'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.matchAll(entries.cat.request.url + '#mouse') + .then(function(result) { + assert_response_array_equals( + result, + [ + entries.cat.response, + ], + 'Cache.matchAll should ignore URL fragment.'); + }); + }, 'Cache.matchAll with URL containing fragment'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.matchAll('http') + .then(function(result) { + assert_response_array_equals( + result, [], + 'Cache.matchAll should treat query as a URL and not ' + + 'just a string fragment.'); + }); + }, 'Cache.matchAll with string fragment "http" as query'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.matchAll() + .then(function(result) { + assert_response_array_equals( + result, + simple_entries.map(entry => entry.response), + 'Cache.matchAll without parameters should match all entries.'); + }); + }, 'Cache.matchAll without parameters'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.matchAll(undefined) + .then(result => { + assert_response_array_equals( + result, + simple_entries.map(entry => entry.response), + 'Cache.matchAll with undefined request should match all entries.'); + }); + }, 'Cache.matchAll with explicitly undefined request'); + +prepopulated_cache_test(simple_entries, function(cache, entries) { + return cache.matchAll(undefined, {}) + .then(result => { + assert_response_array_equals( + result, + simple_entries.map(entry => entry.response), + 'Cache.matchAll with undefined request should match all entries.'); + }); + }, 'Cache.matchAll with explicitly undefined request and empty options'); + +prepopulated_cache_test(vary_entries, function(cache, entries) { + return cache.matchAll('http://example.com/c') + .then(function(result) { + assert_response_array_equals( + result, + [ + entries.vary_cookie_absent.response + ], + 'Cache.matchAll should exclude matches if a vary header is ' + + 'missing in the query request, but is present in the cached ' + + 'request.'); + }) + + .then(function() { + return cache.matchAll( + new Request('http://example.com/c', + {headers: {'Cookies': 'none-of-the-above'}})); + }) + .then(function(result) { + assert_response_array_equals( + result, + [ + ], + 'Cache.matchAll should exclude matches if a vary header is ' + + 'missing in the cached request, but is present in the query ' + + 'request.'); + }) + + .then(function() { + return cache.matchAll( + new Request('http://example.com/c', + {headers: {'Cookies': 'is-for-cookie'}})); + }) + .then(function(result) { + assert_response_array_equals( + result, + [entries.vary_cookie_is_cookie.response], + 'Cache.matchAll should match the entire header if a vary header ' + + 'is present in both the query and cached requests.'); + }); + }, 'Cache.matchAll with responses containing "Vary" header'); + +prepopulated_cache_test(vary_entries, function(cache, entries) { + return cache.matchAll('http://example.com/c', + {ignoreVary: true}) + .then(function(result) { + assert_response_array_equals( + result, + [ + entries.vary_cookie_is_cookie.response, + entries.vary_cookie_is_good.response, + entries.vary_cookie_absent.response + ], + 'Cache.matchAll should support multiple vary request/response ' + + 'pairs.'); + }); + }, 'Cache.matchAll with multiple vary pairs'); + +done(); diff --git a/test/wpt/tests/service-workers/cache-storage/cache-put.https.any.js b/test/wpt/tests/service-workers/cache-storage/cache-put.https.any.js new file mode 100644 index 00000000000..dbf2650a75a --- /dev/null +++ b/test/wpt/tests/service-workers/cache-storage/cache-put.https.any.js @@ -0,0 +1,411 @@ +// META: title=Cache.put +// META: global=window,worker +// META: script=/common/get-host-info.sub.js +// META: script=./resources/test-helpers.js +// META: timeout=long + +var test_url = 'https://example.com/foo'; +var test_body = 'Hello world!'; +const { REMOTE_HOST } = get_host_info(); + +cache_test(function(cache) { + var request = new Request(test_url); + var response = new Response(test_body); + return cache.put(request, response) + .then(function(result) { + assert_equals(result, undefined, + 'Cache.put should resolve with undefined on success.'); + }); + }, 'Cache.put called with simple Request and Response'); + +cache_test(function(cache) { + var test_url = new URL('./resources/simple.txt', location.href).href; + var request = new Request(test_url); + var response; + return fetch(test_url) + .then(function(fetch_result) { + response = fetch_result.clone(); + return cache.put(request, fetch_result); + }) + .then(function() { + return cache.match(test_url); + }) + .then(function(result) { + assert_response_equals(result, response, + 'Cache.put should update the cache with ' + + 'new request and response.'); + return result.text(); + }) + .then(function(body) { + assert_equals(body, 'a simple text file\n', + 'Cache.put should store response body.'); + }); + }, 'Cache.put called with Request and Response from fetch()'); + +cache_test(function(cache) { + var request = new Request(test_url); + var response = new Response(test_body); + assert_false(request.bodyUsed, + '[https://fetch.spec.whatwg.org/#dom-body-bodyused] ' + + 'Request.bodyUsed should be initially false.'); + return cache.put(request, response) + .then(function() { + assert_false(request.bodyUsed, + 'Cache.put should not mark empty request\'s body used'); + }); + }, 'Cache.put with Request without a body'); + +cache_test(function(cache) { + var request = new Request(test_url); + var response = new Response(); + assert_false(response.bodyUsed, + '[https://fetch.spec.whatwg.org/#dom-body-bodyused] ' + + 'Response.bodyUsed should be initially false.'); + return cache.put(request, response) + .then(function() { + assert_false(response.bodyUsed, + 'Cache.put should not mark empty response\'s body used'); + }); + }, 'Cache.put with Response without a body'); + +cache_test(function(cache) { + var request = new Request(test_url); + var response = new Response(test_body); + return cache.put(request, response.clone()) + .then(function() { + return cache.match(test_url); + }) + .then(function(result) { + assert_response_equals(result, response, + 'Cache.put should update the cache with ' + + 'new Request and Response.'); + }); + }, 'Cache.put with a Response containing an empty URL'); + +cache_test(function(cache) { + var request = new Request(test_url); + var response = new Response('', { + status: 200, + headers: [['Content-Type', 'text/plain']] + }); + return cache.put(request, response) + .then(function() { + return cache.match(test_url); + }) + .then(function(result) { + assert_equals(result.status, 200, 'Cache.put should store status.'); + assert_equals(result.headers.get('Content-Type'), 'text/plain', + 'Cache.put should store headers.'); + return result.text(); + }) + .then(function(body) { + assert_equals(body, '', + 'Cache.put should store response body.'); + }); + }, 'Cache.put with an empty response body'); + +cache_test(function(cache, test) { + var request = new Request(test_url); + var response = new Response('', { + status: 206, + headers: [['Content-Type', 'text/plain']] + }); + + return promise_rejects_js( + test, + TypeError, + cache.put(request, response), + 'Cache.put should reject 206 Responses with a TypeError.'); + }, 'Cache.put with synthetic 206 response'); + +cache_test(function(cache, test) { + var test_url = new URL('./resources/fetch-status.py?status=206', location.href).href; + var request = new Request(test_url); + var response; + return fetch(test_url) + .then(function(fetch_result) { + assert_equals(fetch_result.status, 206, + 'Test framework error: The status code should be 206.'); + response = fetch_result.clone(); + return promise_rejects_js(test, TypeError, cache.put(request, fetch_result)); + }); + }, 'Cache.put with HTTP 206 response'); + +cache_test(function(cache, test) { + // We need to jump through some hoops to allow the test to perform opaque + // response filtering, but bypass the ORB safelist check. This is + // done, by forcing the MIME type retrieval to fail and the + // validation of partial first response to succeed. + var pipe = "status(206)|header(Content-Type,)|header(Content-Range, bytes 0-1/41)|slice(null, 1)"; + var test_url = new URL(`./resources/blank.html?pipe=${pipe}`, location.href); + test_url.hostname = REMOTE_HOST; + var request = new Request(test_url.href, { mode: 'no-cors' }); + var response; + return fetch(request) + .then(function(fetch_result) { + assert_equals(fetch_result.type, 'opaque', + 'Test framework error: The response type should be opaque.'); + assert_equals(fetch_result.status, 0, + 'Test framework error: The status code should be 0 for an ' + + ' opaque-filtered response. This is actually HTTP 206.'); + response = fetch_result.clone(); + return cache.put(request, fetch_result); + }) + .then(function() { + return cache.match(test_url); + }) + .then(function(result) { + assert_not_equals(result, undefined, + 'Cache.put should store an entry for the opaque response'); + }); + }, 'Cache.put with opaque-filtered HTTP 206 response'); + +cache_test(function(cache) { + var test_url = new URL('./resources/fetch-status.py?status=500', location.href).href; + var request = new Request(test_url); + var response; + return fetch(test_url) + .then(function(fetch_result) { + assert_equals(fetch_result.status, 500, + 'Test framework error: The status code should be 500.'); + response = fetch_result.clone(); + return cache.put(request, fetch_result); + }) + .then(function() { + return cache.match(test_url); + }) + .then(function(result) { + assert_response_equals(result, response, + 'Cache.put should update the cache with ' + + 'new request and response.'); + return result.text(); + }) + .then(function(body) { + assert_equals(body, '', + 'Cache.put should store response body.'); + }); + }, 'Cache.put with HTTP 500 response'); + +cache_test(function(cache) { + var alternate_response_body = 'New body'; + var alternate_response = new Response(alternate_response_body, + { statusText: 'New status' }); + return cache.put(new Request(test_url), + new Response('Old body', { statusText: 'Old status' })) + .then(function() { + return cache.put(new Request(test_url), alternate_response.clone()); + }) + .then(function() { + return cache.match(test_url); + }) + .then(function(result) { + assert_response_equals(result, alternate_response, + 'Cache.put should replace existing ' + + 'response with new response.'); + return result.text(); + }) + .then(function(body) { + assert_equals(body, alternate_response_body, + 'Cache put should store new response body.'); + }); + }, 'Cache.put called twice with matching Requests and different Responses'); + +cache_test(function(cache) { + var first_url = test_url; + var second_url = first_url + '#(O_o)'; + var third_url = first_url + '#fragment'; + var alternate_response_body = 'New body'; + var alternate_response = new Response(alternate_response_body, + { statusText: 'New status' }); + return cache.put(new Request(first_url), + new Response('Old body', { statusText: 'Old status' })) + .then(function() { + return cache.put(new Request(second_url), alternate_response.clone()); + }) + .then(function() { + return cache.match(test_url); + }) + .then(function(result) { + assert_response_equals(result, alternate_response, + 'Cache.put should replace existing ' + + 'response with new response.'); + return result.text(); + }) + .then(function(body) { + assert_equals(body, alternate_response_body, + 'Cache put should store new response body.'); + }) + .then(function() { + return cache.put(new Request(third_url), alternate_response.clone()); + }) + .then(function() { + return cache.keys(); + }) + .then(function(results) { + // Should match urls (without fragments or with different ones) to the + // same cache key. However, result.url should be the latest url used. + assert_equals(results[0].url, third_url); + return; + }); +}, 'Cache.put called multiple times with request URLs that differ only by a fragment'); + +cache_test(function(cache) { + var url = 'http://example.com/foo'; + return cache.put(url, new Response('some body')) + .then(function() { return cache.match(url); }) + .then(function(response) { return response.text(); }) + .then(function(body) { + assert_equals(body, 'some body', + 'Cache.put should accept a string as request.'); + }); + }, 'Cache.put with a string request'); + +cache_test(function(cache, test) { + return promise_rejects_js( + test, + TypeError, + cache.put(new Request(test_url), 'Hello world!'), + 'Cache.put should only accept a Response object as the response.'); + }, 'Cache.put with an invalid response'); + +cache_test(function(cache, test) { + return promise_rejects_js( + test, + TypeError, + cache.put(new Request('file:///etc/passwd'), + new Response(test_body)), + 'Cache.put should reject non-HTTP/HTTPS requests with a TypeError.'); + }, 'Cache.put with a non-HTTP/HTTPS request'); + +cache_test(function(cache) { + var response = new Response(test_body); + return cache.put(new Request('relative-url'), response.clone()) + .then(function() { + return cache.match(new URL('relative-url', location.href).href); + }) + .then(function(result) { + assert_response_equals(result, response, + 'Cache.put should accept a relative URL ' + + 'as the request.'); + }); + }, 'Cache.put with a relative URL'); + +cache_test(function(cache, test) { + var request = new Request('http://example.com/foo', { method: 'HEAD' }); + return promise_rejects_js( + test, + TypeError, + cache.put(request, new Response(test_body)), + 'Cache.put should throw a TypeError for non-GET requests.'); + }, 'Cache.put with a non-GET request'); + +cache_test(function(cache, test) { + return promise_rejects_js( + test, + TypeError, + cache.put(new Request(test_url), null), + 'Cache.put should throw a TypeError for a null response.'); + }, 'Cache.put with a null response'); + +cache_test(function(cache, test) { + var request = new Request(test_url, {method: 'POST', body: test_body}); + return promise_rejects_js( + test, + TypeError, + cache.put(request, new Response(test_body)), + 'Cache.put should throw a TypeError for a POST request.'); + }, 'Cache.put with a POST request'); + +cache_test(function(cache) { + var response = new Response(test_body); + assert_false(response.bodyUsed, + '[https://fetch.spec.whatwg.org/#dom-body-bodyused] ' + + 'Response.bodyUsed should be initially false.'); + return response.text().then(function() { + assert_true( + response.bodyUsed, + '[https://fetch.spec.whatwg.org/#concept-body-consume-body] ' + + 'The text() method should make the body disturbed.'); + var request = new Request(test_url); + return cache.put(request, response).then(() => { + assert_unreached('cache.put should be rejected'); + }, () => {}); + }); + }, 'Cache.put with a used response body'); + +cache_test(function(cache) { + var response = new Response(test_body); + return cache.put(new Request(test_url), response) + .then(function() { + assert_throws_js(TypeError, () => response.body.getReader()); + }); + }, 'getReader() after Cache.put'); + +cache_test(function(cache, test) { + return promise_rejects_js( + test, + TypeError, + cache.put(new Request(test_url), + new Response(test_body, { headers: { VARY: '*' }})), + 'Cache.put should reject VARY:* Responses with a TypeError.'); + }, 'Cache.put with a VARY:* Response'); + +cache_test(function(cache, test) { + return promise_rejects_js( + test, + TypeError, + cache.put(new Request(test_url), + new Response(test_body, + { headers: { VARY: 'Accept-Language,*' }})), + 'Cache.put should reject Responses with an embedded VARY:* with a ' + + 'TypeError.'); + }, 'Cache.put with an embedded VARY:* Response'); + +cache_test(async function(cache, test) { + const url = new URL('./resources/vary.py?vary=*', + get_host_info().HTTPS_REMOTE_ORIGIN + self.location.pathname); + const request = new Request(url, { mode: 'no-cors' }); + const response = await fetch(request); + assert_equals(response.type, 'opaque'); + await cache.put(request, response); + }, 'Cache.put with a VARY:* opaque response should not reject'); + +cache_test(function(cache) { + var url = 'foo.html'; + var redirectURL = 'http://example.com/foo-bar.html'; + var redirectResponse = Response.redirect(redirectURL); + assert_equals(redirectResponse.headers.get('Location'), redirectURL, + 'Response.redirect() should set Location header.'); + return cache.put(url, redirectResponse.clone()) + .then(function() { + return cache.match(url); + }) + .then(function(response) { + assert_response_equals(response, redirectResponse, + 'Redirect response is reproduced by the Cache API'); + assert_equals(response.headers.get('Location'), redirectURL, + 'Location header is preserved by Cache API.'); + }); + }, 'Cache.put should store Response.redirect() correctly'); + +cache_test(async (cache) => { + var request = new Request(test_url); + var response = new Response(new Blob([test_body])); + await cache.put(request, response); + var cachedResponse = await cache.match(request); + assert_equals(await cachedResponse.text(), test_body); + }, 'Cache.put called with simple Request and blob Response'); + +cache_test(async (cache) => { + var formData = new FormData(); + formData.append("name", "value"); + + var request = new Request(test_url); + var response = new Response(formData); + await cache.put(request, response); + var cachedResponse = await cache.match(request); + var cachedResponseText = await cachedResponse.text(); + assert_true(cachedResponseText.indexOf("name=\"name\"\r\n\r\nvalue") !== -1); +}, 'Cache.put called with simple Request and form data Response'); + +done(); diff --git a/test/wpt/tests/service-workers/cache-storage/cache-storage-buckets.https.any.js b/test/wpt/tests/service-workers/cache-storage/cache-storage-buckets.https.any.js new file mode 100644 index 00000000000..fd59ba464db --- /dev/null +++ b/test/wpt/tests/service-workers/cache-storage/cache-storage-buckets.https.any.js @@ -0,0 +1,64 @@ +// META: title=Cache.put +// META: global=window,worker +// META: script=/common/get-host-info.sub.js +// META: script=resources/test-helpers.js +// META: script=/storage/buckets/resources/util.js +// META: timeout=long + +var test_url = 'https://example.com/foo'; +var test_body = 'Hello world!'; +const { REMOTE_HOST } = get_host_info(); + +promise_test(async function(test) { + await prepareForBucketTest(test); + var inboxBucket = await navigator.storageBuckets.open('inbox'); + var draftsBucket = await navigator.storageBuckets.open('drafts'); + + const cacheName = 'attachments'; + const cacheKey = 'receipt1.txt'; + + var inboxCache = await inboxBucket.caches.open(cacheName); + var draftsCache = await draftsBucket.caches.open(cacheName); + + await inboxCache.put(cacheKey, new Response('bread x 2')) + await draftsCache.put(cacheKey, new Response('eggs x 1')); + + return inboxCache.match(cacheKey) + .then(function(result) { + return result.text(); + }) + .then(function(body) { + assert_equals(body, 'bread x 2', 'Wrong cache contents'); + return draftsCache.match(cacheKey); + }) + .then(function(result) { + return result.text(); + }) + .then(function(body) { + assert_equals(body, 'eggs x 1', 'Wrong cache contents'); + }); +}, 'caches from different buckets have different contents'); + +promise_test(async function(test) { + await prepareForBucketTest(test); + var inboxBucket = await navigator.storageBuckets.open('inbox'); + var draftBucket = await navigator.storageBuckets.open('drafts'); + + var caches = inboxBucket.caches; + var attachments = await caches.open('attachments'); + await attachments.put('receipt1.txt', new Response('bread x 2')); + var result = await attachments.match('receipt1.txt'); + assert_equals(await result.text(), 'bread x 2'); + + await navigator.storageBuckets.delete('inbox'); + + await promise_rejects_dom( + test, 'UnknownError', caches.open('attachments')); + + // Also test when `caches` is first accessed after the deletion. + await navigator.storageBuckets.delete('drafts'); + return promise_rejects_dom( + test, 'UnknownError', draftBucket.caches.open('attachments')); +}, 'cache.open promise is rejected when bucket is gone'); + +done(); diff --git a/test/wpt/tests/service-workers/cache-storage/cache-storage-keys.https.any.js b/test/wpt/tests/service-workers/cache-storage/cache-storage-keys.https.any.js new file mode 100644 index 00000000000..f19522be1b4 --- /dev/null +++ b/test/wpt/tests/service-workers/cache-storage/cache-storage-keys.https.any.js @@ -0,0 +1,35 @@ +// META: title=CacheStorage.keys +// META: global=window,worker +// META: script=./resources/test-helpers.js +// META: timeout=long + +var test_cache_list = + ['', 'example', 'Another cache name', 'A', 'a', 'ex ample']; + +promise_test(function(test) { + return self.caches.keys() + .then(function(keys) { + assert_true(Array.isArray(keys), + 'CacheStorage.keys should return an Array.'); + return Promise.all(keys.map(function(key) { + return self.caches.delete(key); + })); + }) + .then(function() { + return Promise.all(test_cache_list.map(function(key) { + return self.caches.open(key); + })); + }) + + .then(function() { return self.caches.keys(); }) + .then(function(keys) { + assert_true(Array.isArray(keys), + 'CacheStorage.keys should return an Array.'); + assert_array_equals(keys, + test_cache_list, + 'CacheStorage.keys should only return ' + + 'existing caches.'); + }); + }, 'CacheStorage keys'); + +done(); diff --git a/test/wpt/tests/service-workers/cache-storage/cache-storage-match.https.any.js b/test/wpt/tests/service-workers/cache-storage/cache-storage-match.https.any.js new file mode 100644 index 00000000000..0c31b726294 --- /dev/null +++ b/test/wpt/tests/service-workers/cache-storage/cache-storage-match.https.any.js @@ -0,0 +1,245 @@ +// META: title=CacheStorage.match +// META: global=window,worker +// META: script=./resources/test-helpers.js +// META: timeout=long + +(function() { + var next_index = 1; + + // Returns a transaction (request, response, and url) for a unique URL. + function create_unique_transaction(test) { + var uniquifier = String(next_index++); + var url = 'http://example.com/' + uniquifier; + + return { + request: new Request(url), + response: new Response('hello'), + url: url + }; + } + + self.create_unique_transaction = create_unique_transaction; +})(); + +cache_test(function(cache) { + var transaction = create_unique_transaction(); + + return cache.put(transaction.request.clone(), transaction.response.clone()) + .then(function() { + return self.caches.match(transaction.request); + }) + .then(function(response) { + assert_response_equals(response, transaction.response, + 'The response should not have changed.'); + }); +}, 'CacheStorageMatch with no cache name provided'); + +cache_test(function(cache) { + var transaction = create_unique_transaction(); + + var test_cache_list = ['a', 'b', 'c']; + return cache.put(transaction.request.clone(), transaction.response.clone()) + .then(function() { + return Promise.all(test_cache_list.map(function(key) { + return self.caches.open(key); + })); + }) + .then(function() { + return self.caches.match(transaction.request); + }) + .then(function(response) { + assert_response_equals(response, transaction.response, + 'The response should not have changed.'); + }); +}, 'CacheStorageMatch from one of many caches'); + +promise_test(function(test) { + var transaction = create_unique_transaction(); + + var test_cache_list = ['x', 'y', 'z']; + return Promise.all(test_cache_list.map(function(key) { + return self.caches.open(key); + })) + .then(function() { return self.caches.open('x'); }) + .then(function(cache) { + return cache.put(transaction.request.clone(), + transaction.response.clone()); + }) + .then(function() { + return self.caches.match(transaction.request, {cacheName: 'x'}); + }) + .then(function(response) { + assert_response_equals(response, transaction.response, + 'The response should not have changed.'); + }) + .then(function() { + return self.caches.match(transaction.request, {cacheName: 'y'}); + }) + .then(function(response) { + assert_equals(response, undefined, + 'Cache y should not have a response for the request.'); + }); +}, 'CacheStorageMatch from one of many caches by name'); + +cache_test(function(cache) { + var transaction = create_unique_transaction(); + return cache.put(transaction.url, transaction.response.clone()) + .then(function() { + return self.caches.match(transaction.request); + }) + .then(function(response) { + assert_response_equals(response, transaction.response, + 'The response should not have changed.'); + }); +}, 'CacheStorageMatch a string request'); + +cache_test(function(cache) { + var transaction = create_unique_transaction(); + return cache.put(transaction.request.clone(), transaction.response.clone()) + .then(function() { + return self.caches.match(new Request(transaction.request.url, + {method: 'HEAD'})); + }) + .then(function(response) { + assert_equals(response, undefined, + 'A HEAD request should not be matched'); + }); +}, 'CacheStorageMatch a HEAD request'); + +promise_test(function(test) { + var transaction = create_unique_transaction(); + return self.caches.match(transaction.request) + .then(function(response) { + assert_equals(response, undefined, + 'The response should not be found.'); + }); +}, 'CacheStorageMatch with no cached entry'); + +promise_test(function(test) { + var transaction = create_unique_transaction(); + return self.caches.delete('foo') + .then(function() { + return self.caches.has('foo'); + }) + .then(function(has_foo) { + assert_false(has_foo, "The cache should not exist."); + return self.caches.match(transaction.request, {cacheName: 'foo'}); + }) + .then(function(response) { + assert_equals(response, undefined, + 'The match with bad cache name should resolve to ' + + 'undefined.'); + return self.caches.has('foo'); + }) + .then(function(has_foo) { + assert_false(has_foo, "The cache should still not exist."); + }); +}, 'CacheStorageMatch with no caches available but name provided'); + +cache_test(function(cache) { + var transaction = create_unique_transaction(); + + return self.caches.delete('') + .then(function() { + return self.caches.has(''); + }) + .then(function(has_cache) { + assert_false(has_cache, "The cache should not exist."); + return cache.put(transaction.request, transaction.response.clone()); + }) + .then(function() { + return self.caches.match(transaction.request, {cacheName: ''}); + }) + .then(function(response) { + assert_equals(response, undefined, + 'The response should not be found.'); + return self.caches.open(''); + }) + .then(function(cache) { + return cache.put(transaction.request, transaction.response); + }) + .then(function() { + return self.caches.match(transaction.request, {cacheName: ''}); + }) + .then(function(response) { + assert_response_equals(response, transaction.response, + 'The response should be matched.'); + return self.caches.delete(''); + }); +}, 'CacheStorageMatch with empty cache name provided'); + +cache_test(function(cache) { + var request = new Request('http://example.com/?foo'); + var no_query_request = new Request('http://example.com/'); + var response = new Response('foo'); + return cache.put(request.clone(), response.clone()) + .then(function() { + return self.caches.match(no_query_request.clone()); + }) + .then(function(result) { + assert_equals( + result, undefined, + 'CacheStorageMatch should resolve as undefined with a ' + + 'mismatched query.'); + return self.caches.match(no_query_request.clone(), + {ignoreSearch: true}); + }) + .then(function(result) { + assert_response_equals( + result, response, + 'CacheStorageMatch with ignoreSearch should ignore the ' + + 'query of the request.'); + }); + }, 'CacheStorageMatch supports ignoreSearch'); + +cache_test(function(cache) { + var request = new Request('http://example.com/'); + var head_request = new Request('http://example.com/', {method: 'HEAD'}); + var response = new Response('foo'); + return cache.put(request.clone(), response.clone()) + .then(function() { + return self.caches.match(head_request.clone()); + }) + .then(function(result) { + assert_equals( + result, undefined, + 'CacheStorageMatch should resolve as undefined with a ' + + 'mismatched method.'); + return self.caches.match(head_request.clone(), + {ignoreMethod: true}); + }) + .then(function(result) { + assert_response_equals( + result, response, + 'CacheStorageMatch with ignoreMethod should ignore the ' + + 'method of request.'); + }); + }, 'Cache.match supports ignoreMethod'); + +cache_test(function(cache) { + var vary_request = new Request('http://example.com/c', + {headers: {'Cookies': 'is-for-cookie'}}); + var vary_response = new Response('', {headers: {'Vary': 'Cookies'}}); + var mismatched_vary_request = new Request('http://example.com/c'); + + return cache.put(vary_request.clone(), vary_response.clone()) + .then(function() { + return self.caches.match(mismatched_vary_request.clone()); + }) + .then(function(result) { + assert_equals( + result, undefined, + 'CacheStorageMatch should resolve as undefined with a ' + + ' mismatched vary.'); + return self.caches.match(mismatched_vary_request.clone(), + {ignoreVary: true}); + }) + .then(function(result) { + assert_response_equals( + result, vary_response, + 'CacheStorageMatch with ignoreVary should ignore the ' + + 'vary of request.'); + }); + }, 'CacheStorageMatch supports ignoreVary'); + +done(); diff --git a/test/wpt/tests/service-workers/cache-storage/cache-storage.https.any.js b/test/wpt/tests/service-workers/cache-storage/cache-storage.https.any.js new file mode 100644 index 00000000000..b7d5af7b532 --- /dev/null +++ b/test/wpt/tests/service-workers/cache-storage/cache-storage.https.any.js @@ -0,0 +1,239 @@ +// META: title=CacheStorage +// META: global=window,worker +// META: script=./resources/test-helpers.js +// META: timeout=long + +promise_test(function(t) { + var cache_name = 'cache-storage/foo'; + return self.caches.delete(cache_name) + .then(function() { + return self.caches.open(cache_name); + }) + .then(function(cache) { + assert_true(cache instanceof Cache, + 'CacheStorage.open should return a Cache.'); + }); + }, 'CacheStorage.open'); + +promise_test(function(t) { + var cache_name = 'cache-storage/bar'; + var first_cache = null; + var second_cache = null; + return self.caches.open(cache_name) + .then(function(cache) { + first_cache = cache; + return self.caches.delete(cache_name); + }) + .then(function() { + return first_cache.add('./resources/simple.txt'); + }) + .then(function() { + return self.caches.keys(); + }) + .then(function(cache_names) { + assert_equals(cache_names.indexOf(cache_name), -1); + return self.caches.open(cache_name); + }) + .then(function(cache) { + second_cache = cache; + return second_cache.keys(); + }) + .then(function(keys) { + assert_equals(keys.length, 0); + return first_cache.keys(); + }) + .then(function(keys) { + assert_equals(keys.length, 1); + // Clean up + return self.caches.delete(cache_name); + }); + }, 'CacheStorage.delete dooms, but does not delete immediately'); + +promise_test(function(t) { + // Note that this test may collide with other tests running in the same + // origin that also uses an empty cache name. + var cache_name = ''; + return self.caches.delete(cache_name) + .then(function() { + return self.caches.open(cache_name); + }) + .then(function(cache) { + assert_true(cache instanceof Cache, + 'CacheStorage.open should accept an empty name.'); + }); + }, 'CacheStorage.open with an empty name'); + +promise_test(function(t) { + return promise_rejects_js( + t, + TypeError, + self.caches.open(), + 'CacheStorage.open should throw TypeError if called with no arguments.'); + }, 'CacheStorage.open with no arguments'); + +promise_test(function(t) { + var test_cases = [ + { + name: 'cache-storage/lowercase', + should_not_match: + [ + 'cache-storage/Lowercase', + ' cache-storage/lowercase', + 'cache-storage/lowercase ' + ] + }, + { + name: 'cache-storage/has a space', + should_not_match: + [ + 'cache-storage/has' + ] + }, + { + name: 'cache-storage/has\000_in_the_name', + should_not_match: + [ + 'cache-storage/has', + 'cache-storage/has_in_the_name' + ] + } + ]; + return Promise.all(test_cases.map(function(testcase) { + var cache_name = testcase.name; + return self.caches.delete(cache_name) + .then(function() { + return self.caches.open(cache_name); + }) + .then(function() { + return self.caches.has(cache_name); + }) + .then(function(result) { + assert_true(result, + 'CacheStorage.has should return true for existing ' + + 'cache.'); + }) + .then(function() { + return Promise.all( + testcase.should_not_match.map(function(cache_name) { + return self.caches.has(cache_name) + .then(function(result) { + assert_false(result, + 'CacheStorage.has should only perform ' + + 'exact matches on cache names.'); + }); + })); + }) + .then(function() { + return self.caches.delete(cache_name); + }); + })); + }, 'CacheStorage.has with existing cache'); + +promise_test(function(t) { + return self.caches.has('cheezburger') + .then(function(result) { + assert_false(result, + 'CacheStorage.has should return false for ' + + 'nonexistent cache.'); + }); + }, 'CacheStorage.has with nonexistent cache'); + +promise_test(function(t) { + var cache_name = 'cache-storage/open'; + var cache; + return self.caches.delete(cache_name) + .then(function() { + return self.caches.open(cache_name); + }) + .then(function(result) { + cache = result; + }) + .then(function() { + return cache.add('./resources/simple.txt'); + }) + .then(function() { + return self.caches.open(cache_name); + }) + .then(function(result) { + assert_true(result instanceof Cache, + 'CacheStorage.open should return a Cache object'); + assert_not_equals(result, cache, + 'CacheStorage.open should return a new Cache ' + + 'object each time its called.'); + return Promise.all([cache.keys(), result.keys()]); + }) + .then(function(results) { + var expected_urls = results[0].map(function(r) { return r.url }); + var actual_urls = results[1].map(function(r) { return r.url }); + assert_array_equals(actual_urls, expected_urls, + 'CacheStorage.open should return a new Cache ' + + 'object for the same backing store.'); + }); + }, 'CacheStorage.open with existing cache'); + +promise_test(function(t) { + var cache_name = 'cache-storage/delete'; + + return self.caches.delete(cache_name) + .then(function() { + return self.caches.open(cache_name); + }) + .then(function() { return self.caches.delete(cache_name); }) + .then(function(result) { + assert_true(result, + 'CacheStorage.delete should return true after ' + + 'deleting an existing cache.'); + }) + + .then(function() { return self.caches.has(cache_name); }) + .then(function(cache_exists) { + assert_false(cache_exists, + 'CacheStorage.has should return false after ' + + 'fulfillment of CacheStorage.delete promise.'); + }); + }, 'CacheStorage.delete with existing cache'); + +promise_test(function(t) { + return self.caches.delete('cheezburger') + .then(function(result) { + assert_false(result, + 'CacheStorage.delete should return false for a ' + + 'nonexistent cache.'); + }); + }, 'CacheStorage.delete with nonexistent cache'); + +promise_test(function(t) { + var unpaired_name = 'unpaired\uD800'; + var converted_name = 'unpaired\uFFFD'; + + // The test assumes that a cache with converted_name does not + // exist, but if the implementation fails the test then such + // a cache will be created. Start off in a fresh state by + // deleting all caches. + return delete_all_caches() + .then(function() { + return self.caches.has(converted_name); + }) + .then(function(cache_exists) { + assert_false(cache_exists, + 'Test setup failure: cache should not exist'); + }) + .then(function() { return self.caches.open(unpaired_name); }) + .then(function() { return self.caches.keys(); }) + .then(function(keys) { + assert_true(keys.indexOf(unpaired_name) !== -1, + 'keys should include cache with bad name'); + }) + .then(function() { return self.caches.has(unpaired_name); }) + .then(function(cache_exists) { + assert_true(cache_exists, + 'CacheStorage names should be not be converted.'); + }) + .then(function() { return self.caches.has(converted_name); }) + .then(function(cache_exists) { + assert_false(cache_exists, + 'CacheStorage names should be not be converted.'); + }); + }, 'CacheStorage names are DOMStrings not USVStrings'); + +done(); diff --git a/test/wpt/tests/service-workers/cache-storage/common.https.window.js b/test/wpt/tests/service-workers/cache-storage/common.https.window.js new file mode 100644 index 00000000000..eba312c273d --- /dev/null +++ b/test/wpt/tests/service-workers/cache-storage/common.https.window.js @@ -0,0 +1,44 @@ +// META: title=Cache Storage: Verify that Window and Workers see same storage +// META: timeout=long + +function wait_for_message(worker) { + return new Promise(function(resolve) { + worker.addEventListener('message', function listener(e) { + resolve(e.data); + worker.removeEventListener('message', listener); + }); + }); +} + +promise_test(function(t) { + var cache_name = 'common-test'; + return self.caches.delete(cache_name) + .then(function() { + var worker = new Worker('resources/common-worker.js'); + worker.postMessage({name: cache_name}); + return wait_for_message(worker); + }) + .then(function(message) { + return self.caches.open(cache_name); + }) + .then(function(cache) { + return Promise.all([ + cache.match('https://example.com/a'), + cache.match('https://example.com/b'), + cache.match('https://example.com/c') + ]); + }) + .then(function(responses) { + return Promise.all(responses.map( + function(response) { return response.text(); } + )); + }) + .then(function(bodies) { + assert_equals(bodies[0], 'a', + 'Body should match response put by worker'); + assert_equals(bodies[1], 'b', + 'Body should match response put by worker'); + assert_equals(bodies[2], 'c', + 'Body should match response put by worker'); + }); +}, 'Window sees cache puts by Worker'); diff --git a/test/wpt/tests/service-workers/cache-storage/crashtests/cache-response-clone.https.html b/test/wpt/tests/service-workers/cache-storage/crashtests/cache-response-clone.https.html new file mode 100644 index 00000000000..ec930a87d93 --- /dev/null +++ b/test/wpt/tests/service-workers/cache-storage/crashtests/cache-response-clone.https.html @@ -0,0 +1,17 @@ + + + + diff --git a/test/wpt/tests/service-workers/cache-storage/credentials.https.html b/test/wpt/tests/service-workers/cache-storage/credentials.https.html new file mode 100644 index 00000000000..0fe4a0a0ac0 --- /dev/null +++ b/test/wpt/tests/service-workers/cache-storage/credentials.https.html @@ -0,0 +1,46 @@ + + +Cache Storage: Verify credentials are respected by Cache operations + + + + + + diff --git a/test/wpt/tests/service-workers/cache-storage/cross-partition.https.tentative.html b/test/wpt/tests/service-workers/cache-storage/cross-partition.https.tentative.html new file mode 100644 index 00000000000..1cfc2569088 --- /dev/null +++ b/test/wpt/tests/service-workers/cache-storage/cross-partition.https.tentative.html @@ -0,0 +1,269 @@ + + + + + + + + + + + + + + + diff --git a/test/wpt/tests/service-workers/cache-storage/resources/blank.html b/test/wpt/tests/service-workers/cache-storage/resources/blank.html new file mode 100644 index 00000000000..a3c3a4689a6 --- /dev/null +++ b/test/wpt/tests/service-workers/cache-storage/resources/blank.html @@ -0,0 +1,2 @@ + +Empty doc diff --git a/test/wpt/tests/service-workers/cache-storage/resources/cache-keys-attributes-for-service-worker.js b/test/wpt/tests/service-workers/cache-storage/resources/cache-keys-attributes-for-service-worker.js new file mode 100644 index 00000000000..ee574d2cb77 --- /dev/null +++ b/test/wpt/tests/service-workers/cache-storage/resources/cache-keys-attributes-for-service-worker.js @@ -0,0 +1,22 @@ +self.addEventListener('fetch', (event) => { + const params = new URL(event.request.url).searchParams; + if (params.has('ignore')) { + return; + } + if (!params.has('name')) { + event.respondWith(Promise.reject(TypeError('No name is provided.'))); + return; + } + + event.respondWith(Promise.resolve().then(async () => { + const name = params.get('name'); + await caches.delete('foo'); + const cache = await caches.open('foo'); + await cache.put(event.request, new Response('hello')); + const keys = await cache.keys(); + + const original = event.request[name]; + const stored = keys[0][name]; + return new Response(`original: ${original}, stored: ${stored}`); + })); + }); diff --git a/test/wpt/tests/service-workers/cache-storage/resources/common-worker.js b/test/wpt/tests/service-workers/cache-storage/resources/common-worker.js new file mode 100644 index 00000000000..d0e8544b56c --- /dev/null +++ b/test/wpt/tests/service-workers/cache-storage/resources/common-worker.js @@ -0,0 +1,15 @@ +self.onmessage = function(e) { + var cache_name = e.data.name; + + self.caches.open(cache_name) + .then(function(cache) { + return Promise.all([ + cache.put('https://example.com/a', new Response('a')), + cache.put('https://example.com/b', new Response('b')), + cache.put('https://example.com/c', new Response('c')) + ]); + }) + .then(function() { + self.postMessage('ok'); + }); +}; diff --git a/test/wpt/tests/service-workers/cache-storage/resources/credentials-iframe.html b/test/wpt/tests/service-workers/cache-storage/resources/credentials-iframe.html new file mode 100644 index 00000000000..00702df9e5b --- /dev/null +++ b/test/wpt/tests/service-workers/cache-storage/resources/credentials-iframe.html @@ -0,0 +1,38 @@ + + +Controlled frame for Cache API test with credentials + + +Hello? Yes, this is iframe. + diff --git a/test/wpt/tests/service-workers/cache-storage/resources/credentials-worker.js b/test/wpt/tests/service-workers/cache-storage/resources/credentials-worker.js new file mode 100644 index 00000000000..43965b5fe4c --- /dev/null +++ b/test/wpt/tests/service-workers/cache-storage/resources/credentials-worker.js @@ -0,0 +1,59 @@ +var cache_name = 'credentials'; + +function assert_equals(actual, expected, message) { + if (!Object.is(actual, expected)) + throw Error(message + ': expected: ' + expected + ', actual: ' + actual); +} + +self.onfetch = function(e) { + if (!/\.txt$/.test(e.request.url)) return; + var content = e.request.url; + var cache; + e.respondWith( + self.caches.open(cache_name) + .then(function(result) { + cache = result; + return cache.put(e.request, new Response(content)); + }) + + .then(function() { return cache.match(e.request); }) + .then(function(result) { return result.text(); }) + .then(function(text) { + assert_equals(text, content, 'Cache.match() body should match'); + }) + + .then(function() { return cache.matchAll(e.request); }) + .then(function(results) { + assert_equals(results.length, 1, 'Should have one response'); + return results[0].text(); + }) + .then(function(text) { + assert_equals(text, content, 'Cache.matchAll() body should match'); + }) + + .then(function() { return self.caches.match(e.request); }) + .then(function(result) { return result.text(); }) + .then(function(text) { + assert_equals(text, content, 'CacheStorage.match() body should match'); + }) + + .then(function() { + return new Response('dummy'); + }) + ); +}; + +self.onmessage = function(e) { + if (e.data === 'keys') { + self.caches.open(cache_name) + .then(function(cache) { return cache.keys(); }) + .then(function(requests) { + var urls = requests.map(function(request) { return request.url; }); + self.clients.matchAll().then(function(clients) { + clients.forEach(function(client) { + client.postMessage(urls); + }); + }); + }); + } +}; diff --git a/test/wpt/tests/service-workers/cache-storage/resources/fetch-status.py b/test/wpt/tests/service-workers/cache-storage/resources/fetch-status.py new file mode 100644 index 00000000000..b7109f4787f --- /dev/null +++ b/test/wpt/tests/service-workers/cache-storage/resources/fetch-status.py @@ -0,0 +1,2 @@ +def main(request, response): + return int(request.GET[b"status"]), [], b"" diff --git a/test/wpt/tests/service-workers/cache-storage/resources/iframe.html b/test/wpt/tests/service-workers/cache-storage/resources/iframe.html new file mode 100644 index 00000000000..a2f1e502bb3 --- /dev/null +++ b/test/wpt/tests/service-workers/cache-storage/resources/iframe.html @@ -0,0 +1,18 @@ + +ok + diff --git a/test/wpt/tests/service-workers/cache-storage/resources/simple.txt b/test/wpt/tests/service-workers/cache-storage/resources/simple.txt new file mode 100644 index 00000000000..9e3cb91fb9b --- /dev/null +++ b/test/wpt/tests/service-workers/cache-storage/resources/simple.txt @@ -0,0 +1 @@ +a simple text file diff --git a/test/wpt/tests/service-workers/cache-storage/resources/test-helpers.js b/test/wpt/tests/service-workers/cache-storage/resources/test-helpers.js new file mode 100644 index 00000000000..050ac0b5424 --- /dev/null +++ b/test/wpt/tests/service-workers/cache-storage/resources/test-helpers.js @@ -0,0 +1,272 @@ +(function() { + var next_cache_index = 1; + + // Returns a promise that resolves to a newly created Cache object. The + // returned Cache will be destroyed when |test| completes. + function create_temporary_cache(test) { + var uniquifier = String(++next_cache_index); + var cache_name = self.location.pathname + '/' + uniquifier; + + test.add_cleanup(function() { + self.caches.delete(cache_name); + }); + + return self.caches.delete(cache_name) + .then(function() { + return self.caches.open(cache_name); + }); + } + + self.create_temporary_cache = create_temporary_cache; +})(); + +// Runs |test_function| with a temporary unique Cache passed in as the only +// argument. The function is run as a part of Promise chain owned by +// promise_test(). As such, it is expected to behave in a manner identical (with +// the exception of the argument) to a function passed into promise_test(). +// +// E.g.: +// cache_test(function(cache) { +// // Do something with |cache|, which is a Cache object. +// }, "Some Cache test"); +function cache_test(test_function, description) { + promise_test(function(test) { + return create_temporary_cache(test) + .then(function(cache) { return test_function(cache, test); }); + }, description); +} + +// A set of Request/Response pairs to be used with prepopulated_cache_test(). +var simple_entries = [ + { + name: 'a', + request: new Request('http://example.com/a'), + response: new Response('') + }, + + { + name: 'b', + request: new Request('http://example.com/b'), + response: new Response('') + }, + + { + name: 'a_with_query', + request: new Request('http://example.com/a?q=r'), + response: new Response('') + }, + + { + name: 'A', + request: new Request('http://example.com/A'), + response: new Response('') + }, + + { + name: 'a_https', + request: new Request('https://example.com/a'), + response: new Response('') + }, + + { + name: 'a_org', + request: new Request('http://example.org/a'), + response: new Response('') + }, + + { + name: 'cat', + request: new Request('http://example.com/cat'), + response: new Response('') + }, + + { + name: 'catmandu', + request: new Request('http://example.com/catmandu'), + response: new Response('') + }, + + { + name: 'cat_num_lives', + request: new Request('http://example.com/cat?lives=9'), + response: new Response('') + }, + + { + name: 'cat_in_the_hat', + request: new Request('http://example.com/cat/in/the/hat'), + response: new Response('') + }, + + { + name: 'non_2xx_response', + request: new Request('http://example.com/non2xx'), + response: new Response('', {status: 404, statusText: 'nope'}) + }, + + { + name: 'error_response', + request: new Request('http://example.com/error'), + response: Response.error() + }, +]; + +// A set of Request/Response pairs to be used with prepopulated_cache_test(). +// These contain a mix of test cases that use Vary headers. +var vary_entries = [ + { + name: 'vary_cookie_is_cookie', + request: new Request('http://example.com/c', + {headers: {'Cookies': 'is-for-cookie'}}), + response: new Response('', + {headers: {'Vary': 'Cookies'}}) + }, + + { + name: 'vary_cookie_is_good', + request: new Request('http://example.com/c', + {headers: {'Cookies': 'is-good-enough-for-me'}}), + response: new Response('', + {headers: {'Vary': 'Cookies'}}) + }, + + { + name: 'vary_cookie_absent', + request: new Request('http://example.com/c'), + response: new Response('', + {headers: {'Vary': 'Cookies'}}) + } +]; + +// Run |test_function| with a Cache object and a map of entries. Prior to the +// call, the Cache is populated by cache entries from |entries|. The latter is +// expected to be an Object mapping arbitrary keys to objects of the form +// {request: , response: }. Entries are +// serially added to the cache in the order specified. +// +// |test_function| should return a Promise that can be used with promise_test. +function prepopulated_cache_test(entries, test_function, description) { + cache_test(function(cache) { + var p = Promise.resolve(); + var hash = {}; + entries.forEach(function(entry) { + hash[entry.name] = entry; + p = p.then(function() { + return cache.put(entry.request.clone(), entry.response.clone()) + .catch(function(e) { + assert_unreached( + 'Test setup failed for entry ' + entry.name + ': ' + e + ); + }); + }); + }); + return p + .then(function() { + assert_equals(Object.keys(hash).length, entries.length); + }) + .then(function() { + return test_function(cache, hash); + }); + }, description); +} + +// Helper for testing with Headers objects. Compares Headers instances +// by serializing |expected| and |actual| to arrays and comparing. +function assert_header_equals(actual, expected, description) { + assert_class_string(actual, "Headers", description); + var header; + var actual_headers = []; + var expected_headers = []; + for (header of actual) + actual_headers.push(header[0] + ": " + header[1]); + for (header of expected) + expected_headers.push(header[0] + ": " + header[1]); + assert_array_equals(actual_headers, expected_headers, + description + " Headers differ."); +} + +// Helper for testing with Response objects. Compares simple +// attributes defined on the interfaces, as well as the headers. It +// does not compare the response bodies. +function assert_response_equals(actual, expected, description) { + assert_class_string(actual, "Response", description); + ["type", "url", "status", "ok", "statusText"].forEach(function(attribute) { + assert_equals(actual[attribute], expected[attribute], + description + " Attributes differ: " + attribute + "."); + }); + assert_header_equals(actual.headers, expected.headers, description); +} + +// Assert that the two arrays |actual| and |expected| contain the same +// set of Responses as determined by assert_response_equals. The order +// is not significant. +// +// |expected| is assumed to not contain any duplicates. +function assert_response_array_equivalent(actual, expected, description) { + assert_true(Array.isArray(actual), description); + assert_equals(actual.length, expected.length, description); + expected.forEach(function(expected_element) { + // assert_response_in_array treats the first argument as being + // 'actual', and the second as being 'expected array'. We are + // switching them around because we want to be resilient + // against the |actual| array containing duplicates. + assert_response_in_array(expected_element, actual, description); + }); +} + +// Asserts that two arrays |actual| and |expected| contain the same +// set of Responses as determined by assert_response_equals(). The +// corresponding elements must occupy corresponding indices in their +// respective arrays. +function assert_response_array_equals(actual, expected, description) { + assert_true(Array.isArray(actual), description); + assert_equals(actual.length, expected.length, description); + actual.forEach(function(value, index) { + assert_response_equals(value, expected[index], + description + " : object[" + index + "]"); + }); +} + +// Equivalent to assert_in_array, but uses assert_response_equals. +function assert_response_in_array(actual, expected_array, description) { + assert_true(expected_array.some(function(element) { + try { + assert_response_equals(actual, element); + return true; + } catch (e) { + return false; + } + }), description); +} + +// Helper for testing with Request objects. Compares simple +// attributes defined on the interfaces, as well as the headers. +function assert_request_equals(actual, expected, description) { + assert_class_string(actual, "Request", description); + ["url"].forEach(function(attribute) { + assert_equals(actual[attribute], expected[attribute], + description + " Attributes differ: " + attribute + "."); + }); + assert_header_equals(actual.headers, expected.headers, description); +} + +// Asserts that two arrays |actual| and |expected| contain the same +// set of Requests as determined by assert_request_equals(). The +// corresponding elements must occupy corresponding indices in their +// respective arrays. +function assert_request_array_equals(actual, expected, description) { + assert_true(Array.isArray(actual), description); + assert_equals(actual.length, expected.length, description); + actual.forEach(function(value, index) { + assert_request_equals(value, expected[index], + description + " : object[" + index + "]"); + }); +} + +// Deletes all caches, returning a promise indicating success. +function delete_all_caches() { + return self.caches.keys() + .then(function(keys) { + return Promise.all(keys.map(self.caches.delete.bind(self.caches))); + }); +} diff --git a/test/wpt/tests/service-workers/cache-storage/resources/vary.py b/test/wpt/tests/service-workers/cache-storage/resources/vary.py new file mode 100644 index 00000000000..7fde1b1094e --- /dev/null +++ b/test/wpt/tests/service-workers/cache-storage/resources/vary.py @@ -0,0 +1,25 @@ +def main(request, response): + if b"clear-vary-value-override-cookie" in request.GET: + response.unset_cookie(b"vary-value-override") + return b"vary cookie cleared" + + set_cookie_vary = request.GET.first(b"set-vary-value-override-cookie", + default=b"") + if set_cookie_vary: + response.set_cookie(b"vary-value-override", set_cookie_vary) + return b"vary cookie set" + + # If there is a vary-value-override cookie set, then use its value + # for the VARY header no matter what the query string is set to. This + # override is necessary to test the case when two URLs are identical + # (including query), but differ by VARY header. + cookie_vary = request.cookies.get(b"vary-value-override") + if cookie_vary: + response.headers.set(b"vary", str(cookie_vary)) + else: + # If there is no cookie, then use the query string value, if present. + query_vary = request.GET.first(b"vary", default=b"") + if query_vary: + response.headers.set(b"vary", query_vary) + + return b"vary response" diff --git a/test/wpt/tests/service-workers/cache-storage/sandboxed-iframes.https.html b/test/wpt/tests/service-workers/cache-storage/sandboxed-iframes.https.html new file mode 100644 index 00000000000..098fa89daf4 --- /dev/null +++ b/test/wpt/tests/service-workers/cache-storage/sandboxed-iframes.https.html @@ -0,0 +1,66 @@ + +Cache Storage: Verify access in sandboxed iframes + + + + + diff --git a/test/wpt/tests/service-workers/idlharness.https.any.js b/test/wpt/tests/service-workers/idlharness.https.any.js new file mode 100644 index 00000000000..8db5d4d10ff --- /dev/null +++ b/test/wpt/tests/service-workers/idlharness.https.any.js @@ -0,0 +1,53 @@ +// META: global=window,worker +// META: script=/resources/WebIDLParser.js +// META: script=/resources/idlharness.js +// META: script=cache-storage/resources/test-helpers.js +// META: script=service-worker/resources/test-helpers.sub.js +// META: timeout=long + +// https://w3c.github.io/ServiceWorker + +idl_test( + ['service-workers'], + ['dom', 'html'], + async (idl_array, t) => { + self.cacheInstance = await create_temporary_cache(t); + + idl_array.add_objects({ + CacheStorage: ['caches'], + Cache: ['self.cacheInstance'], + ServiceWorkerContainer: ['navigator.serviceWorker'] + }); + + // TODO: Add ServiceWorker and ServiceWorkerRegistration instances for the + // other worker scopes. + if (self.GLOBAL.isWindow()) { + idl_array.add_objects({ + ServiceWorkerRegistration: ['registrationInstance'], + ServiceWorker: ['registrationInstance.installing'] + }); + + const scope = 'service-worker/resources/scope/idlharness'; + const registration = await service_worker_unregister_and_register( + t, 'service-worker/resources/empty-worker.js', scope); + t.add_cleanup(() => registration.unregister()); + + self.registrationInstance = registration; + } else if (self.ServiceWorkerGlobalScope) { + // self.ServiceWorkerGlobalScope should only be defined for the + // ServiceWorker scope, which allows us to detect and test the interfaces + // exposed only for ServiceWorker. + idl_array.add_objects({ + Clients: ['clients'], + ExtendableEvent: ['new ExtendableEvent("type")'], + FetchEvent: ['new FetchEvent("type", { request: new Request("") })'], + ServiceWorkerGlobalScope: ['self'], + ServiceWorkerRegistration: ['registration'], + ServiceWorker: ['serviceWorker'], + // TODO: Test instances of Client and WindowClient, e.g. + // Client: ['self.clientInstance'], + // WindowClient: ['self.windowClientInstance'] + }); + } + } +); diff --git a/test/wpt/tests/service-workers/service-worker/Service-Worker-Allowed-header.https.html b/test/wpt/tests/service-workers/service-worker/Service-Worker-Allowed-header.https.html new file mode 100644 index 00000000000..6f44bb17e7d --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/Service-Worker-Allowed-header.https.html @@ -0,0 +1,88 @@ + +Service Worker: Service-Worker-Allowed header + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/close.https.html b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/close.https.html new file mode 100644 index 00000000000..3e3cc8b2b08 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/close.https.html @@ -0,0 +1,11 @@ + +ServiceWorkerGlobalScope: close operation + + + + diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/extendable-message-event-constructor.https.html b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/extendable-message-event-constructor.https.html new file mode 100644 index 00000000000..525245fe9ec --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/extendable-message-event-constructor.https.html @@ -0,0 +1,10 @@ + +ServiceWorkerGlobalScope: ExtendableMessageEvent Constructor + + + + diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/extendable-message-event.https.html b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/extendable-message-event.https.html new file mode 100644 index 00000000000..89efd7a4a61 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/extendable-message-event.https.html @@ -0,0 +1,226 @@ + +ServiceWorkerGlobalScope: ExtendableMessageEvent + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/fetch-on-the-right-interface.https.any.js b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/fetch-on-the-right-interface.https.any.js new file mode 100644 index 00000000000..5ca5f65680c --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/fetch-on-the-right-interface.https.any.js @@ -0,0 +1,14 @@ +// META: title=fetch method on the right interface +// META: global=serviceworker + +test(function() { + assert_false(self.hasOwnProperty('fetch'), 'ServiceWorkerGlobalScope ' + + 'instance should not have "fetch" method as its property.'); + assert_inherits(self, 'fetch', 'ServiceWorkerGlobalScope should ' + + 'inherit "fetch" method.'); + assert_own_property(Object.getPrototypeOf(Object.getPrototypeOf(self)), 'fetch', + 'WorkerGlobalScope should have "fetch" propery in its prototype.'); + assert_equals(self.fetch, Object.getPrototypeOf(Object.getPrototypeOf(self)).fetch, + 'ServiceWorkerGlobalScope.fetch should be the same as ' + + 'WorkerGlobalScope.fetch.'); +}, 'Fetch method on the right interface'); diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/isSecureContext.https.html b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/isSecureContext.https.html new file mode 100644 index 00000000000..399820dd2c3 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/isSecureContext.https.html @@ -0,0 +1,32 @@ + + + +Service Worker: isSecureContext + + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/isSecureContext.serviceworker.js b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/isSecureContext.serviceworker.js new file mode 100644 index 00000000000..5033594e34b --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/isSecureContext.serviceworker.js @@ -0,0 +1,5 @@ +importScripts("/resources/testharness.js"); + +test(() => { + assert_true(self.isSecureContext, true); +}, "isSecureContext"); diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/postmessage.https.html b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/postmessage.https.html new file mode 100644 index 00000000000..99dedebf2e5 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/postmessage.https.html @@ -0,0 +1,83 @@ + +ServiceWorkerGlobalScope: postMessage + + + + diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/registration-attribute.https.html b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/registration-attribute.https.html new file mode 100644 index 00000000000..aa3c74a13bb --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/registration-attribute.https.html @@ -0,0 +1,107 @@ + +ServiceWorkerGlobalScope: registration + + + + diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/close-worker.js b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/close-worker.js new file mode 100644 index 00000000000..41a8bc069af --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/close-worker.js @@ -0,0 +1,5 @@ +importScripts('../../resources/worker-testharness.js'); + +test(function() { + assert_false('close' in self); +}, 'ServiceWorkerGlobalScope close operation'); diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/error-worker.js b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/error-worker.js new file mode 100644 index 00000000000..f6838ffb394 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/error-worker.js @@ -0,0 +1,12 @@ +var source; + +self.addEventListener('message', function(e) { + source = e.source; + throw 'testError'; +}); + +self.addEventListener('error', function(e) { + source.postMessage({ + error: e.error, filename: e.filename, message: e.message, lineno: e.lineno, + colno: e.colno}); +}); diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-constructor-worker.js b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-constructor-worker.js new file mode 100644 index 00000000000..42da5825c56 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-constructor-worker.js @@ -0,0 +1,197 @@ +importScripts('/resources/testharness.js'); + +const TEST_OBJECT = { wanwan: 123 }; +const CHANNEL1 = new MessageChannel(); +const CHANNEL2 = new MessageChannel(); +const PORTS = [CHANNEL1.port1, CHANNEL1.port2, CHANNEL2.port1]; +function createEvent(initializer) { + if (initializer === undefined) + return new ExtendableMessageEvent('type'); + return new ExtendableMessageEvent('type', initializer); +} + +// These test cases are mostly copied from the following file in the Chromium +// project (as of commit 848ad70823991e0f12b437d789943a4ab24d65bb): +// third_party/WebKit/LayoutTests/fast/events/constructors/message-event-constructor.html + +test(function() { + assert_false(createEvent().bubbles); + assert_false(createEvent().cancelable); + assert_equals(createEvent().data, null); + assert_equals(createEvent().origin, ''); + assert_equals(createEvent().lastEventId, ''); + assert_equals(createEvent().source, null); + assert_array_equals(createEvent().ports, []); +}, 'no initializer specified'); + +test(function() { + assert_false(createEvent({ bubbles: false }).bubbles); + assert_true(createEvent({ bubbles: true }).bubbles); +}, '`bubbles` is specified'); + +test(function() { + assert_false(createEvent({ cancelable: false }).cancelable); + assert_true(createEvent({ cancelable: true }).cancelable); +}, '`cancelable` is specified'); + +test(function() { + assert_equals(createEvent({ data: TEST_OBJECT }).data, TEST_OBJECT); + assert_equals(createEvent({ data: undefined }).data, null); + assert_equals(createEvent({ data: null }).data, null); + assert_equals(createEvent({ data: false }).data, false); + assert_equals(createEvent({ data: true }).data, true); + assert_equals(createEvent({ data: '' }).data, ''); + assert_equals(createEvent({ data: 'chocolate' }).data, 'chocolate'); + assert_equals(createEvent({ data: 12345 }).data, 12345); + assert_equals(createEvent({ data: 18446744073709551615 }).data, + 18446744073709552000); + assert_equals(createEvent({ data: NaN }).data, NaN); + // Note that valueOf() is not called, when the left hand side is + // evaluated. + assert_false( + createEvent({ data: { + valueOf: function() { return TEST_OBJECT; } } }).data == + TEST_OBJECT); + assert_equals(createEvent({ get data(){ return 123; } }).data, 123); + let thrown = { name: 'Error' }; + assert_throws_exactly(thrown, function() { + createEvent({ get data() { throw thrown; } }); }); +}, '`data` is specified'); + +test(function() { + assert_equals(createEvent({ origin: 'melancholy' }).origin, 'melancholy'); + assert_equals(createEvent({ origin: '' }).origin, ''); + assert_equals(createEvent({ origin: null }).origin, 'null'); + assert_equals(createEvent({ origin: false }).origin, 'false'); + assert_equals(createEvent({ origin: true }).origin, 'true'); + assert_equals(createEvent({ origin: 12345 }).origin, '12345'); + assert_equals( + createEvent({ origin: 18446744073709551615 }).origin, + '18446744073709552000'); + assert_equals(createEvent({ origin: NaN }).origin, 'NaN'); + assert_equals(createEvent({ origin: [] }).origin, ''); + assert_equals(createEvent({ origin: [1, 2, 3] }).origin, '1,2,3'); + assert_equals( + createEvent({ origin: { melancholy: 12345 } }).origin, + '[object Object]'); + // Note that valueOf() is not called, when the left hand side is + // evaluated. + assert_equals( + createEvent({ origin: { + valueOf: function() { return 'melancholy'; } } }).origin, + '[object Object]'); + assert_equals( + createEvent({ get origin() { return 123; } }).origin, '123'); + let thrown = { name: 'Error' }; + assert_throws_exactly(thrown, function() { + createEvent({ get origin() { throw thrown; } }); }); +}, '`origin` is specified'); + +test(function() { + assert_equals( + createEvent({ lastEventId: 'melancholy' }).lastEventId, 'melancholy'); + assert_equals(createEvent({ lastEventId: '' }).lastEventId, ''); + assert_equals(createEvent({ lastEventId: null }).lastEventId, 'null'); + assert_equals(createEvent({ lastEventId: false }).lastEventId, 'false'); + assert_equals(createEvent({ lastEventId: true }).lastEventId, 'true'); + assert_equals(createEvent({ lastEventId: 12345 }).lastEventId, '12345'); + assert_equals( + createEvent({ lastEventId: 18446744073709551615 }).lastEventId, + '18446744073709552000'); + assert_equals(createEvent({ lastEventId: NaN }).lastEventId, 'NaN'); + assert_equals(createEvent({ lastEventId: [] }).lastEventId, ''); + assert_equals( + createEvent({ lastEventId: [1, 2, 3] }).lastEventId, '1,2,3'); + assert_equals( + createEvent({ lastEventId: { melancholy: 12345 } }).lastEventId, + '[object Object]'); + // Note that valueOf() is not called, when the left hand side is + // evaluated. + assert_equals( + createEvent({ lastEventId: { + valueOf: function() { return 'melancholy'; } } }).lastEventId, + '[object Object]'); + assert_equals( + createEvent({ get lastEventId() { return 123; } }).lastEventId, + '123'); + let thrown = { name: 'Error' }; + assert_throws_exactly(thrown, function() { + createEvent({ get lastEventId() { throw thrown; } }); }); +}, '`lastEventId` is specified'); + +test(function() { + assert_equals(createEvent({ source: CHANNEL1.port1 }).source, CHANNEL1.port1); + assert_equals( + createEvent({ source: self.registration.active }).source, + self.registration.active); + assert_equals( + createEvent({ source: CHANNEL1.port1 }).source, CHANNEL1.port1); + assert_throws_js( + TypeError, function() { createEvent({ source: this }); }, + 'source should be Client or ServiceWorker or MessagePort'); +}, '`source` is specified'); + +test(function() { + // Valid message ports. + var passed_ports = createEvent({ ports: PORTS}).ports; + assert_equals(passed_ports[0], CHANNEL1.port1); + assert_equals(passed_ports[1], CHANNEL1.port2); + assert_equals(passed_ports[2], CHANNEL2.port1); + assert_array_equals(createEvent({ ports: [] }).ports, []); + assert_array_equals(createEvent({ ports: undefined }).ports, []); + + // Invalid message ports. + assert_throws_js(TypeError, + function() { createEvent({ ports: [1, 2, 3] }); }); + assert_throws_js(TypeError, + function() { createEvent({ ports: TEST_OBJECT }); }); + assert_throws_js(TypeError, + function() { createEvent({ ports: null }); }); + assert_throws_js(TypeError, + function() { createEvent({ ports: this }); }); + assert_throws_js(TypeError, + function() { createEvent({ ports: false }); }); + assert_throws_js(TypeError, + function() { createEvent({ ports: true }); }); + assert_throws_js(TypeError, + function() { createEvent({ ports: '' }); }); + assert_throws_js(TypeError, + function() { createEvent({ ports: 'chocolate' }); }); + assert_throws_js(TypeError, + function() { createEvent({ ports: 12345 }); }); + assert_throws_js(TypeError, + function() { createEvent({ ports: 18446744073709551615 }); }); + assert_throws_js(TypeError, + function() { createEvent({ ports: NaN }); }); + assert_throws_js(TypeError, + function() { createEvent({ get ports() { return 123; } }); }); + let thrown = { name: 'Error' }; + assert_throws_exactly(thrown, function() { + createEvent({ get ports() { throw thrown; } }); }); + // Note that valueOf() is not called, when the left hand side is + // evaluated. + var valueOf = function() { return PORTS; }; + assert_throws_js(TypeError, function() { + createEvent({ ports: { valueOf: valueOf } }); }); +}, '`ports` is specified'); + +test(function() { + var initializers = { + bubbles: true, + cancelable: true, + data: TEST_OBJECT, + origin: 'wonderful', + lastEventId: 'excellent', + source: CHANNEL1.port1, + ports: PORTS + }; + assert_equals(createEvent(initializers).bubbles, true); + assert_equals(createEvent(initializers).cancelable, true); + assert_equals(createEvent(initializers).data, TEST_OBJECT); + assert_equals(createEvent(initializers).origin, 'wonderful'); + assert_equals(createEvent(initializers).lastEventId, 'excellent'); + assert_equals(createEvent(initializers).source, CHANNEL1.port1); + assert_equals(createEvent(initializers).ports[0], PORTS[0]); + assert_equals(createEvent(initializers).ports[1], PORTS[1]); + assert_equals(createEvent(initializers).ports[2], PORTS[2]); +}, 'all initial values are specified'); diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-loopback-worker.js b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-loopback-worker.js new file mode 100644 index 00000000000..13cae88d800 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-loopback-worker.js @@ -0,0 +1,36 @@ +importScripts('./extendable-message-event-utils.js'); + +self.addEventListener('message', function(event) { + switch (event.data.type) { + case 'start': + self.registration.active.postMessage( + {type: '1st', client_id: event.source.id}); + break; + case '1st': + // 1st loopback message via ServiceWorkerRegistration.active. + var results = { + trial: 1, + event: ExtendableMessageEventUtils.serialize(event) + }; + var client_id = event.data.client_id; + event.source.postMessage({type: '2nd', client_id: client_id}); + event.waitUntil(clients.get(client_id) + .then(function(client) { + client.postMessage({type: 'record', results: results}); + })); + break; + case '2nd': + // 2nd loopback message via ExtendableMessageEvent.source. + var results = { + trial: 2, + event: ExtendableMessageEventUtils.serialize(event) + }; + var client_id = event.data.client_id; + event.waitUntil(clients.get(client_id) + .then(function(client) { + client.postMessage({type: 'record', results: results}); + client.postMessage({type: 'finish'}); + })); + break; + } + }); diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-ping-worker.js b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-ping-worker.js new file mode 100644 index 00000000000..d07b22959cc --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-ping-worker.js @@ -0,0 +1,23 @@ +importScripts('./extendable-message-event-utils.js'); + +self.addEventListener('message', function(event) { + switch (event.data.type) { + case 'start': + // Send a ping message to another service worker. + self.registration.waiting.postMessage( + {type: 'ping', client_id: event.source.id}); + break; + case 'pong': + var results = { + pingOrPong: 'pong', + event: ExtendableMessageEventUtils.serialize(event) + }; + var client_id = event.data.client_id; + event.waitUntil(clients.get(client_id) + .then(function(client) { + client.postMessage({type: 'record', results: results}); + client.postMessage({type: 'finish'}); + })); + break; + } + }); diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-pong-worker.js b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-pong-worker.js new file mode 100644 index 00000000000..5e9669e83c4 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-pong-worker.js @@ -0,0 +1,18 @@ +importScripts('./extendable-message-event-utils.js'); + +self.addEventListener('message', function(event) { + switch (event.data.type) { + case 'ping': + var results = { + pingOrPong: 'ping', + event: ExtendableMessageEventUtils.serialize(event) + }; + var client_id = event.data.client_id; + event.waitUntil(clients.get(client_id) + .then(function(client) { + client.postMessage({type: 'record', results: results}); + event.source.postMessage({type: 'pong', client_id: client_id}); + })); + break; + } + }); diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-utils.js b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-utils.js new file mode 100644 index 00000000000..d6a3b483f53 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-utils.js @@ -0,0 +1,78 @@ +var ExtendableMessageEventUtils = {}; + +// Create a representation of a given ExtendableMessageEvent that is suitable +// for transmission via the `postMessage` API. +ExtendableMessageEventUtils.serialize = function(event) { + var ports = event.ports.map(function(port) { + return { constructor: { name: port.constructor.name } }; + }); + return { + constructor: { + name: event.constructor.name + }, + origin: event.origin, + lastEventId: event.lastEventId, + source: { + constructor: { + name: event.source.constructor.name + }, + url: event.source.url, + frameType: event.source.frameType, + visibilityState: event.source.visibilityState, + focused: event.source.focused + }, + ports: ports + }; +}; + +// Compare the actual and expected values of an ExtendableMessageEvent that has +// been transformed using the `serialize` function defined in this file. +ExtendableMessageEventUtils.assert_equals = function(actual, expected) { + assert_equals( + actual.constructor.name, expected.constructor.name, 'event constructor' + ); + assert_equals(actual.origin, expected.origin, 'event `origin` property'); + assert_equals( + actual.lastEventId, + expected.lastEventId, + 'event `lastEventId` property' + ); + + assert_equals( + actual.source.constructor.name, + expected.source.constructor.name, + 'event `source` property constructor' + ); + assert_equals( + actual.source.url, expected.source.url, 'event `source` property `url`' + ); + assert_equals( + actual.source.frameType, + expected.source.frameType, + 'event `source` property `frameType`' + ); + assert_equals( + actual.source.visibilityState, + expected.source.visibilityState, + 'event `source` property `visibilityState`' + ); + assert_equals( + actual.source.focused, + expected.source.focused, + 'event `source` property `focused`' + ); + + assert_equals( + actual.ports.length, + expected.ports.length, + 'event `ports` property length' + ); + + for (var idx = 0; idx < expected.ports.length; ++idx) { + assert_equals( + actual.ports[idx].constructor.name, + expected.ports[idx].constructor.name, + 'MessagePort #' + idx + ' constructor' + ); + } +}; diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-worker.js b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-worker.js new file mode 100644 index 00000000000..f5e7647e3e7 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/extendable-message-event-worker.js @@ -0,0 +1,5 @@ +importScripts('./extendable-message-event-utils.js'); + +self.addEventListener('message', function(event) { + event.source.postMessage(ExtendableMessageEventUtils.serialize(event)); + }); diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-loopback-worker.js b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-loopback-worker.js new file mode 100644 index 00000000000..083e9aa2a85 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-loopback-worker.js @@ -0,0 +1,15 @@ +self.addEventListener('message', function(event) { + if ('port' in event.data) { + var port = event.data.port; + + var channel = new MessageChannel(); + channel.port1.onmessage = function(event) { + if ('pong' in event.data) + port.postMessage(event.data.pong); + }; + self.registration.active.postMessage({ping: channel.port2}, + [channel.port2]); + } else if ('ping' in event.data) { + event.data.ping.postMessage({pong: 'OK'}); + } + }); diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-ping-worker.js b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-ping-worker.js new file mode 100644 index 00000000000..ebb1eccce2d --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-ping-worker.js @@ -0,0 +1,15 @@ +self.addEventListener('message', function(event) { + if ('port' in event.data) { + var port = event.data.port; + + var channel = new MessageChannel(); + channel.port1.onmessage = function(event) { + if ('pong' in event.data) + port.postMessage(event.data.pong); + }; + + // Send a ping message to another service worker. + self.registration.waiting.postMessage({ping: channel.port2}, + [channel.port2]); + } + }); diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-pong-worker.js b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-pong-worker.js new file mode 100644 index 00000000000..4a0d90b6182 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/postmessage-pong-worker.js @@ -0,0 +1,4 @@ +self.addEventListener('message', function(event) { + if ('ping' in event.data) + event.data.ping.postMessage({pong: 'OK'}); + }); diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/registration-attribute-newer-worker.js b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/registration-attribute-newer-worker.js new file mode 100644 index 00000000000..44f3e2e8e9b --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/registration-attribute-newer-worker.js @@ -0,0 +1,33 @@ +// TODO(nhiroki): stop using global states because service workers can be killed +// at any point. Instead, we could post a message to the page on each event via +// Client object (http://crbug.com/558244). +var results = []; + +function stringify(worker) { + return worker ? worker.scriptURL : 'empty'; +} + +function record(event_name) { + results.push(event_name); + results.push(' installing: ' + stringify(self.registration.installing)); + results.push(' waiting: ' + stringify(self.registration.waiting)); + results.push(' active: ' + stringify(self.registration.active)); +} + +record('evaluate'); + +self.registration.addEventListener('updatefound', function() { + record('updatefound'); + var worker = self.registration.installing; + self.registration.installing.addEventListener('statechange', function() { + record('statechange(' + worker.state + ')'); + }); + }); + +self.addEventListener('install', function(e) { record('install'); }); + +self.addEventListener('activate', function(e) { record('activate'); }); + +self.addEventListener('message', function(e) { + e.data.port.postMessage(results); + }); diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/registration-attribute-worker.js b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/registration-attribute-worker.js new file mode 100644 index 00000000000..315f4375932 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/registration-attribute-worker.js @@ -0,0 +1,139 @@ +importScripts('../../resources/test-helpers.sub.js'); +importScripts('../../resources/worker-testharness.js'); + +// TODO(nhiroki): stop using global states because service workers can be killed +// at any point. Instead, we could post a message to the page on each event via +// Client object (http://crbug.com/558244). +var events_seen = []; + +// TODO(nhiroki): Move these assertions to registration-attribute.html because +// an assertion failure on the worker is not shown on the result page and +// handled as timeout. See registration-attribute-newer-worker.js for example. + +assert_equals( + self.registration.scope, + normalizeURL('scope/registration-attribute'), + 'On worker script evaluation, registration attribute should be set'); +assert_equals( + self.registration.installing, + null, + 'On worker script evaluation, installing worker should be null'); +assert_equals( + self.registration.waiting, + null, + 'On worker script evaluation, waiting worker should be null'); +assert_equals( + self.registration.active, + null, + 'On worker script evaluation, active worker should be null'); + +self.registration.addEventListener('updatefound', function() { + events_seen.push('updatefound'); + + assert_equals( + self.registration.scope, + normalizeURL('scope/registration-attribute'), + 'On updatefound event, registration attribute should be set'); + assert_equals( + self.registration.installing.scriptURL, + normalizeURL('registration-attribute-worker.js'), + 'On updatefound event, installing worker should be set'); + assert_equals( + self.registration.waiting, + null, + 'On updatefound event, waiting worker should be null'); + assert_equals( + self.registration.active, + null, + 'On updatefound event, active worker should be null'); + + assert_equals( + self.registration.installing.state, + 'installing', + 'On updatefound event, worker should be in the installing state'); + + var worker = self.registration.installing; + self.registration.installing.addEventListener('statechange', function() { + events_seen.push('statechange(' + worker.state + ')'); + }); + }); + +self.addEventListener('install', function(e) { + events_seen.push('install'); + + assert_equals( + self.registration.scope, + normalizeURL('scope/registration-attribute'), + 'On install event, registration attribute should be set'); + assert_equals( + self.registration.installing.scriptURL, + normalizeURL('registration-attribute-worker.js'), + 'On install event, installing worker should be set'); + assert_equals( + self.registration.waiting, + null, + 'On install event, waiting worker should be null'); + assert_equals( + self.registration.active, + null, + 'On install event, active worker should be null'); + + assert_equals( + self.registration.installing.state, + 'installing', + 'On install event, worker should be in the installing state'); + }); + +self.addEventListener('activate', function(e) { + events_seen.push('activate'); + + assert_equals( + self.registration.scope, + normalizeURL('scope/registration-attribute'), + 'On activate event, registration attribute should be set'); + assert_equals( + self.registration.installing, + null, + 'On activate event, installing worker should be null'); + assert_equals( + self.registration.waiting, + null, + 'On activate event, waiting worker should be null'); + assert_equals( + self.registration.active.scriptURL, + normalizeURL('registration-attribute-worker.js'), + 'On activate event, active worker should be set'); + + assert_equals( + self.registration.active.state, + 'activating', + 'On activate event, worker should be in the activating state'); + }); + +self.addEventListener('fetch', function(e) { + events_seen.push('fetch'); + + assert_equals( + self.registration.scope, + normalizeURL('scope/registration-attribute'), + 'On fetch event, registration attribute should be set'); + assert_equals( + self.registration.installing, + null, + 'On fetch event, installing worker should be null'); + assert_equals( + self.registration.waiting, + null, + 'On fetch event, waiting worker should be null'); + assert_equals( + self.registration.active.scriptURL, + normalizeURL('registration-attribute-worker.js'), + 'On fetch event, active worker should be set'); + + assert_equals( + self.registration.active.state, + 'activated', + 'On fetch event, worker should be in the activated state'); + + e.respondWith(new Response(events_seen)); + }); diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/unregister-controlling-worker.html b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/unregister-controlling-worker.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/unregister-worker.js b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/unregister-worker.js new file mode 100644 index 00000000000..6da397dd152 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/unregister-worker.js @@ -0,0 +1,25 @@ +function matchQuery(query) { + return self.location.href.indexOf(query) != -1; +} + +if (matchQuery('?evaluation')) + self.registration.unregister(); + +self.addEventListener('install', function(e) { + if (matchQuery('?install')) { + // Don't do waitUntil(unregister()) as that would deadlock as specified. + self.registration.unregister(); + } + }); + +self.addEventListener('activate', function(e) { + if (matchQuery('?activate')) + e.waitUntil(self.registration.unregister()); + }); + +self.addEventListener('message', function(e) { + e.waitUntil(self.registration.unregister() + .then(function(result) { + e.data.port.postMessage({result: result}); + })); + }); diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/update-worker.js b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/update-worker.js new file mode 100644 index 00000000000..8be8a1ffebe --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/update-worker.js @@ -0,0 +1,22 @@ +var events_seen = []; + +self.registration.addEventListener('updatefound', function() { + events_seen.push('updatefound'); + }); + +self.addEventListener('activate', function(e) { + events_seen.push('activate'); + }); + +self.addEventListener('fetch', function(e) { + events_seen.push('fetch'); + e.respondWith(new Response(events_seen)); + }); + +self.addEventListener('message', function(e) { + events_seen.push('message'); + self.registration.update(); + }); + +// update() during the script evaluation should be ignored. +self.registration.update(); diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/update-worker.py b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/update-worker.py new file mode 100644 index 00000000000..8a87e1baa4c --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/resources/update-worker.py @@ -0,0 +1,16 @@ +import os +import time + +from wptserve.utils import isomorphic_decode + +def main(request, response): + # update() does not bypass cache so set the max-age to 0 such that update() + # can find a new version in the network. + headers = [(b'Cache-Control', b'max-age: 0'), + (b'Content-Type', b'application/javascript')] + with open(os.path.join(os.path.dirname(isomorphic_decode(__file__)), + u'update-worker.js'), u'r') as file: + script = file.read() + # Return a different script for each access. + return headers, u'// %s\n%s' % (time.time(), script) + diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/service-worker-error-event.https.html b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/service-worker-error-event.https.html new file mode 100644 index 00000000000..988f5466b9e --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/service-worker-error-event.https.html @@ -0,0 +1,31 @@ + +ServiceWorkerGlobalScope: Error event error message + + + + diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/unregister.https.html b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/unregister.https.html new file mode 100644 index 00000000000..1a124d72768 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/unregister.https.html @@ -0,0 +1,139 @@ + +ServiceWorkerGlobalScope: unregister + + + + diff --git a/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/update.https.html b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/update.https.html new file mode 100644 index 00000000000..a7dde223397 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/ServiceWorkerGlobalScope/update.https.html @@ -0,0 +1,48 @@ + +ServiceWorkerGlobalScope: update + + + + diff --git a/test/wpt/tests/service-workers/service-worker/about-blank-replacement.https.html b/test/wpt/tests/service-workers/service-worker/about-blank-replacement.https.html new file mode 100644 index 00000000000..b6efe3ec562 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/about-blank-replacement.https.html @@ -0,0 +1,181 @@ + +Service Worker: about:blank replacement handling + + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/activate-event-after-install-state-change.https.html b/test/wpt/tests/service-workers/service-worker/activate-event-after-install-state-change.https.html new file mode 100644 index 00000000000..016a52c13c8 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/activate-event-after-install-state-change.https.html @@ -0,0 +1,33 @@ + +Service Worker: registration events + + + + diff --git a/test/wpt/tests/service-workers/service-worker/activation-after-registration.https.html b/test/wpt/tests/service-workers/service-worker/activation-after-registration.https.html new file mode 100644 index 00000000000..29f97e3e3f4 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/activation-after-registration.https.html @@ -0,0 +1,28 @@ + +Service Worker: Activation occurs after registration + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/activation.https.html b/test/wpt/tests/service-workers/service-worker/activation.https.html new file mode 100644 index 00000000000..278454d3383 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/activation.https.html @@ -0,0 +1,168 @@ + + + +service worker: activation + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/active.https.html b/test/wpt/tests/service-workers/service-worker/active.https.html new file mode 100644 index 00000000000..350a34b802f --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/active.https.html @@ -0,0 +1,50 @@ + +ServiceWorker: navigator.serviceWorker.active + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/claim-affect-other-registration.https.html b/test/wpt/tests/service-workers/service-worker/claim-affect-other-registration.https.html new file mode 100644 index 00000000000..52555ac271b --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/claim-affect-other-registration.https.html @@ -0,0 +1,136 @@ + + +Service Worker: claim() should affect other registration + + + + diff --git a/test/wpt/tests/service-workers/service-worker/claim-fetch.https.html b/test/wpt/tests/service-workers/service-worker/claim-fetch.https.html new file mode 100644 index 00000000000..ae0082df06b --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/claim-fetch.https.html @@ -0,0 +1,90 @@ + + + + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/claim-not-using-registration.https.html b/test/wpt/tests/service-workers/service-worker/claim-not-using-registration.https.html new file mode 100644 index 00000000000..fd61d05ba4e --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/claim-not-using-registration.https.html @@ -0,0 +1,131 @@ + +Service Worker: claim client not using registration + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/claim-shared-worker-fetch.https.html b/test/wpt/tests/service-workers/service-worker/claim-shared-worker-fetch.https.html new file mode 100644 index 00000000000..f5f44886baa --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/claim-shared-worker-fetch.https.html @@ -0,0 +1,71 @@ + + + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/claim-using-registration.https.html b/test/wpt/tests/service-workers/service-worker/claim-using-registration.https.html new file mode 100644 index 00000000000..8a2a6ff25c8 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/claim-using-registration.https.html @@ -0,0 +1,103 @@ + +Service Worker: claim client using registration + + + + diff --git a/test/wpt/tests/service-workers/service-worker/claim-with-redirect.https.html b/test/wpt/tests/service-workers/service-worker/claim-with-redirect.https.html new file mode 100644 index 00000000000..fd89cb9b00a --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/claim-with-redirect.https.html @@ -0,0 +1,59 @@ + +Service Worker: Claim() when update happens after redirect + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/claim-worker-fetch.https.html b/test/wpt/tests/service-workers/service-worker/claim-worker-fetch.https.html new file mode 100644 index 00000000000..7cb26c742b9 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/claim-worker-fetch.https.html @@ -0,0 +1,83 @@ + + + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/client-id.https.html b/test/wpt/tests/service-workers/service-worker/client-id.https.html new file mode 100644 index 00000000000..b93b3418999 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/client-id.https.html @@ -0,0 +1,60 @@ + +Service Worker: Client.id + + + + diff --git a/test/wpt/tests/service-workers/service-worker/client-navigate.https.html b/test/wpt/tests/service-workers/service-worker/client-navigate.https.html new file mode 100644 index 00000000000..f40a08635cf --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/client-navigate.https.html @@ -0,0 +1,107 @@ + + + +Service Worker: WindowClient.navigate + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/client-url-of-blob-url-worker.https.html b/test/wpt/tests/service-workers/service-worker/client-url-of-blob-url-worker.https.html new file mode 100644 index 00000000000..97a2fcf98f2 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/client-url-of-blob-url-worker.https.html @@ -0,0 +1,29 @@ + +Service Worker: client.url of a worker created from a blob URL + + + + diff --git a/test/wpt/tests/service-workers/service-worker/clients-get-client-types.https.html b/test/wpt/tests/service-workers/service-worker/clients-get-client-types.https.html new file mode 100644 index 00000000000..63e3e51b320 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/clients-get-client-types.https.html @@ -0,0 +1,108 @@ + +Service Worker: Clients.get with window and worker clients + + + + diff --git a/test/wpt/tests/service-workers/service-worker/clients-get-cross-origin.https.html b/test/wpt/tests/service-workers/service-worker/clients-get-cross-origin.https.html new file mode 100644 index 00000000000..1e4acfb286c --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/clients-get-cross-origin.https.html @@ -0,0 +1,69 @@ + +Service Worker: Clients.get across origins + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/clients-get-resultingClientId.https.html b/test/wpt/tests/service-workers/service-worker/clients-get-resultingClientId.https.html new file mode 100644 index 00000000000..3419cf14b52 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/clients-get-resultingClientId.https.html @@ -0,0 +1,177 @@ + + +Test clients.get(resultingClientId) + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/clients-get.https.html b/test/wpt/tests/service-workers/service-worker/clients-get.https.html new file mode 100644 index 00000000000..4cfbf595cad --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/clients-get.https.html @@ -0,0 +1,154 @@ + +Service Worker: Clients.get + + + + diff --git a/test/wpt/tests/service-workers/service-worker/clients-matchall-blob-url-worker.https.html b/test/wpt/tests/service-workers/service-worker/clients-matchall-blob-url-worker.https.html new file mode 100644 index 00000000000..c29bac8b894 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/clients-matchall-blob-url-worker.https.html @@ -0,0 +1,85 @@ + +Service Worker: Clients.matchAll with a blob URL worker client + + + + diff --git a/test/wpt/tests/service-workers/service-worker/clients-matchall-client-types.https.html b/test/wpt/tests/service-workers/service-worker/clients-matchall-client-types.https.html new file mode 100644 index 00000000000..54f182b6202 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/clients-matchall-client-types.https.html @@ -0,0 +1,92 @@ + +Service Worker: Clients.matchAll with various clientTypes + + + + diff --git a/test/wpt/tests/service-workers/service-worker/clients-matchall-exact-controller.https.html b/test/wpt/tests/service-workers/service-worker/clients-matchall-exact-controller.https.html new file mode 100644 index 00000000000..a61c8af7019 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/clients-matchall-exact-controller.https.html @@ -0,0 +1,67 @@ + +Service Worker: Clients.matchAll with exact controller + + + + diff --git a/test/wpt/tests/service-workers/service-worker/clients-matchall-frozen.https.html b/test/wpt/tests/service-workers/service-worker/clients-matchall-frozen.https.html new file mode 100644 index 00000000000..479c28a60f2 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/clients-matchall-frozen.https.html @@ -0,0 +1,64 @@ + +Service Worker: Clients.matchAll + + + + diff --git a/test/wpt/tests/service-workers/service-worker/clients-matchall-include-uncontrolled.https.html b/test/wpt/tests/service-workers/service-worker/clients-matchall-include-uncontrolled.https.html new file mode 100644 index 00000000000..9f34e5709eb --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/clients-matchall-include-uncontrolled.https.html @@ -0,0 +1,117 @@ + +Service Worker: Clients.matchAll with includeUncontrolled + + + + diff --git a/test/wpt/tests/service-workers/service-worker/clients-matchall-on-evaluation.https.html b/test/wpt/tests/service-workers/service-worker/clients-matchall-on-evaluation.https.html new file mode 100644 index 00000000000..8705f85b568 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/clients-matchall-on-evaluation.https.html @@ -0,0 +1,24 @@ + +Service Worker: Clients.matchAll on script evaluation + + + + diff --git a/test/wpt/tests/service-workers/service-worker/clients-matchall-order.https.html b/test/wpt/tests/service-workers/service-worker/clients-matchall-order.https.html new file mode 100644 index 00000000000..ec650f2264d --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/clients-matchall-order.https.html @@ -0,0 +1,427 @@ + +Service Worker: Clients.matchAll ordering + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/clients-matchall.https.html b/test/wpt/tests/service-workers/service-worker/clients-matchall.https.html new file mode 100644 index 00000000000..ce44f1924d5 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/clients-matchall.https.html @@ -0,0 +1,50 @@ + +Service Worker: Clients.matchAll + + + + diff --git a/test/wpt/tests/service-workers/service-worker/controller-on-disconnect.https.html b/test/wpt/tests/service-workers/service-worker/controller-on-disconnect.https.html new file mode 100644 index 00000000000..f23dfe71bac --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/controller-on-disconnect.https.html @@ -0,0 +1,40 @@ + +Service Worker: Controller on load + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/controller-on-load.https.html b/test/wpt/tests/service-workers/service-worker/controller-on-load.https.html new file mode 100644 index 00000000000..e4c5e5f81f8 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/controller-on-load.https.html @@ -0,0 +1,46 @@ + +Service Worker: Controller on load + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/controller-on-reload.https.html b/test/wpt/tests/service-workers/service-worker/controller-on-reload.https.html new file mode 100644 index 00000000000..2e966d42578 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/controller-on-reload.https.html @@ -0,0 +1,58 @@ + +Service Worker: Controller on reload + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/controller-with-no-fetch-event-handler.https.html b/test/wpt/tests/service-workers/service-worker/controller-with-no-fetch-event-handler.https.html new file mode 100644 index 00000000000..d947139c9e2 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/controller-with-no-fetch-event-handler.https.html @@ -0,0 +1,56 @@ + + +Service Worker: controller without a fetch event handler + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/credentials.https.html b/test/wpt/tests/service-workers/service-worker/credentials.https.html new file mode 100644 index 00000000000..0a90dc2897c --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/credentials.https.html @@ -0,0 +1,100 @@ + + +Credentials for service worker scripts + + + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/data-iframe.html b/test/wpt/tests/service-workers/service-worker/data-iframe.html new file mode 100644 index 00000000000..d767d574345 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/data-iframe.html @@ -0,0 +1,25 @@ + +Service Workers in data iframes + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/data-transfer-files.https.html b/test/wpt/tests/service-workers/service-worker/data-transfer-files.https.html new file mode 100644 index 00000000000..c503a28f965 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/data-transfer-files.https.html @@ -0,0 +1,41 @@ + + +Post a file in a navigation controlled by a service worker + + + + + +
+ +
+ + diff --git a/test/wpt/tests/service-workers/service-worker/dedicated-worker-service-worker-interception.https.html b/test/wpt/tests/service-workers/service-worker/dedicated-worker-service-worker-interception.https.html new file mode 100644 index 00000000000..2144f482712 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/dedicated-worker-service-worker-interception.https.html @@ -0,0 +1,40 @@ + +DedicatedWorker: ServiceWorker interception + + + + diff --git a/test/wpt/tests/service-workers/service-worker/detached-context.https.html b/test/wpt/tests/service-workers/service-worker/detached-context.https.html new file mode 100644 index 00000000000..747a953f620 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/detached-context.https.html @@ -0,0 +1,141 @@ + + +Service WorkerRegistration from a removed iframe + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/embed-and-object-are-not-intercepted.https.html b/test/wpt/tests/service-workers/service-worker/embed-and-object-are-not-intercepted.https.html new file mode 100644 index 00000000000..581dbeca977 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/embed-and-object-are-not-intercepted.https.html @@ -0,0 +1,104 @@ + + +embed and object are not intercepted + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/extendable-event-async-waituntil.https.html b/test/wpt/tests/service-workers/service-worker/extendable-event-async-waituntil.https.html new file mode 100644 index 00000000000..04e98266b4f --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/extendable-event-async-waituntil.https.html @@ -0,0 +1,120 @@ + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/extendable-event-waituntil.https.html b/test/wpt/tests/service-workers/service-worker/extendable-event-waituntil.https.html new file mode 100644 index 00000000000..33b4eac5c18 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/extendable-event-waituntil.https.html @@ -0,0 +1,140 @@ + +ExtendableEvent: waitUntil + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-audio-tainting.https.html b/test/wpt/tests/service-workers/service-worker/fetch-audio-tainting.https.html new file mode 100644 index 00000000000..9821759bc7b --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-audio-tainting.https.html @@ -0,0 +1,47 @@ + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-double-write.https.html b/test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-double-write.https.html new file mode 100644 index 00000000000..dab2153baa6 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-double-write.https.html @@ -0,0 +1,57 @@ + + + + + + +canvas tainting when written twice + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-image-cache.https.html b/test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-image-cache.https.html new file mode 100644 index 00000000000..21323811225 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-image-cache.https.html @@ -0,0 +1,16 @@ + + +Service Worker: canvas tainting of the fetched image using cached responses + + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-image.https.html b/test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-image.https.html new file mode 100644 index 00000000000..57dc7d98caa --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-image.https.html @@ -0,0 +1,16 @@ + + +Service Worker: canvas tainting of the fetched image + + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-video-cache.https.html b/test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-video-cache.https.html new file mode 100644 index 00000000000..c37e8e56244 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-video-cache.https.html @@ -0,0 +1,17 @@ + + +Service Worker: canvas tainting of the fetched video using cache responses + + + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-video-with-range-request.https.html b/test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-video-with-range-request.https.html new file mode 100644 index 00000000000..28c30718047 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-video-with-range-request.https.html @@ -0,0 +1,92 @@ + + +Canvas tainting due to video whose responses are fetched via a service worker including range requests + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-video.https.html b/test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-video.https.html new file mode 100644 index 00000000000..e8c23a2edd6 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-canvas-tainting-video.https.html @@ -0,0 +1,17 @@ + + +Service Worker: canvas tainting of the fetched video + + + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-cors-exposed-header-names.https.html b/test/wpt/tests/service-workers/service-worker/fetch-cors-exposed-header-names.https.html new file mode 100644 index 00000000000..317b02175f2 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-cors-exposed-header-names.https.html @@ -0,0 +1,30 @@ + +Service Worker: CORS-exposed header names should be transferred correctly + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-cors-xhr.https.html b/test/wpt/tests/service-workers/service-worker/fetch-cors-xhr.https.html new file mode 100644 index 00000000000..f8ff445673b --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-cors-xhr.https.html @@ -0,0 +1,49 @@ + +Service Worker: CORS XHR of fetch() + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-csp.https.html b/test/wpt/tests/service-workers/service-worker/fetch-csp.https.html new file mode 100644 index 00000000000..9e7b242b69a --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-csp.https.html @@ -0,0 +1,138 @@ + +Service Worker: CSP control of fetch() + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-error.https.html b/test/wpt/tests/service-workers/service-worker/fetch-error.https.html new file mode 100644 index 00000000000..ca2f884a9b3 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-error.https.html @@ -0,0 +1,33 @@ + + + + + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event-add-async.https.html b/test/wpt/tests/service-workers/service-worker/fetch-event-add-async.https.html new file mode 100644 index 00000000000..ac13e4f4167 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-event-add-async.https.html @@ -0,0 +1,11 @@ + +Service Worker: Fetch event added asynchronously doesn't throw + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event-after-navigation-within-page.https.html b/test/wpt/tests/service-workers/service-worker/fetch-event-after-navigation-within-page.https.html new file mode 100644 index 00000000000..4812d8a9155 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-event-after-navigation-within-page.https.html @@ -0,0 +1,71 @@ + +ServiceWorker: navigator.serviceWorker.waiting + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event-async-respond-with.https.html b/test/wpt/tests/service-workers/service-worker/fetch-event-async-respond-with.https.html new file mode 100644 index 00000000000..d9147f85494 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-event-async-respond-with.https.html @@ -0,0 +1,73 @@ + + +respondWith cannot be called asynchronously + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event-handled.https.html b/test/wpt/tests/service-workers/service-worker/fetch-event-handled.https.html new file mode 100644 index 00000000000..08b88ce3773 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-event-handled.https.html @@ -0,0 +1,86 @@ + + +Service Worker: FetchEvent.handled + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event-is-history-backward-navigation-manual.https.html b/test/wpt/tests/service-workers/service-worker/fetch-event-is-history-backward-navigation-manual.https.html new file mode 100644 index 00000000000..3cf5922f396 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-event-is-history-backward-navigation-manual.https.html @@ -0,0 +1,8 @@ + + +

Click this link. + Once you see "method = GET,..." in the page, go to another page, and then go back to the page using the Backward button. + You should see "method = GET, isHistoryNavigation = true". +

+ + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event-is-history-forward-navigation-manual.https.html b/test/wpt/tests/service-workers/service-worker/fetch-event-is-history-forward-navigation-manual.https.html new file mode 100644 index 00000000000..401939b3cb2 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-event-is-history-forward-navigation-manual.https.html @@ -0,0 +1,8 @@ + + +

Click this link. + Once you see "method = GET,..." in the page, go back to this page using the Backward button, and then go to the second page using the Forward button. + You should see "method = GET, isHistoryNavigation = true". +

+ + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event-is-reload-iframe-navigation-manual.https.html b/test/wpt/tests/service-workers/service-worker/fetch-event-is-reload-iframe-navigation-manual.https.html new file mode 100644 index 00000000000..cf1feccf6e4 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-event-is-reload-iframe-navigation-manual.https.html @@ -0,0 +1,31 @@ + + + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event-is-reload-navigation-manual.https.html b/test/wpt/tests/service-workers/service-worker/fetch-event-is-reload-navigation-manual.https.html new file mode 100644 index 00000000000..a349f07c36c --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-event-is-reload-navigation-manual.https.html @@ -0,0 +1,8 @@ + + +

Click this link. + Once you see "method = GET,..." in the page, reload the page. + You will see "method = GET, isReloadNavigation = true". +

+ + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event-network-error.https.html b/test/wpt/tests/service-workers/service-worker/fetch-event-network-error.https.html new file mode 100644 index 00000000000..fea2ad1e3c2 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-event-network-error.https.html @@ -0,0 +1,44 @@ + +Service Worker: Fetch event network error + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event-redirect.https.html b/test/wpt/tests/service-workers/service-worker/fetch-event-redirect.https.html new file mode 100644 index 00000000000..52292847570 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-event-redirect.https.html @@ -0,0 +1,1038 @@ + +Service Worker: Fetch Event Redirect Handling + + + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event-referrer-policy.https.html b/test/wpt/tests/service-workers/service-worker/fetch-event-referrer-policy.https.html new file mode 100644 index 00000000000..af4b20a9a4a --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-event-referrer-policy.https.html @@ -0,0 +1,274 @@ + + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-argument.https.html b/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-argument.https.html new file mode 100644 index 00000000000..05e22105243 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-argument.https.html @@ -0,0 +1,44 @@ + +Service Worker: FetchEvent.respondWith() argument type test. + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-body-loaded-in-chunk.https.html b/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-body-loaded-in-chunk.https.html new file mode 100644 index 00000000000..932f9030c51 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-body-loaded-in-chunk.https.html @@ -0,0 +1,24 @@ + + +respondWith with a response whose body is being loaded from the network by chunks + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-custom-response.https.html b/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-custom-response.https.html new file mode 100644 index 00000000000..645a29c9b4f --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-custom-response.https.html @@ -0,0 +1,82 @@ + + +respondWith with a new Response + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-partial-stream.https.html b/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-partial-stream.https.html new file mode 100644 index 00000000000..505cef29726 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-partial-stream.https.html @@ -0,0 +1,62 @@ + + +respondWith streams data to an intercepted fetch() + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-readable-stream-chunk.https.html b/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-readable-stream-chunk.https.html new file mode 100644 index 00000000000..4544a9e08f5 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-readable-stream-chunk.https.html @@ -0,0 +1,23 @@ + + +respondWith with a response built from a ReadableStream + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-readable-stream.https.html b/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-readable-stream.https.html new file mode 100644 index 00000000000..4651258e6ab --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-readable-stream.https.html @@ -0,0 +1,88 @@ + + +respondWith with a response built from a ReadableStream + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-response-body-with-invalid-chunk.https.html b/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-response-body-with-invalid-chunk.https.html new file mode 100644 index 00000000000..2a44811461a --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-response-body-with-invalid-chunk.https.html @@ -0,0 +1,46 @@ + + +respondWith with response body having invalid chunks + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-stops-propagation.https.html b/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-stops-propagation.https.html new file mode 100644 index 00000000000..31fd616b6d6 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-event-respond-with-stops-propagation.https.html @@ -0,0 +1,37 @@ + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event-throws-after-respond-with.https.html b/test/wpt/tests/service-workers/service-worker/fetch-event-throws-after-respond-with.https.html new file mode 100644 index 00000000000..d98fb22ff42 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-event-throws-after-respond-with.https.html @@ -0,0 +1,37 @@ + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event-within-sw-manual.https.html b/test/wpt/tests/service-workers/service-worker/fetch-event-within-sw-manual.https.html new file mode 100644 index 00000000000..15a2e95bd39 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-event-within-sw-manual.https.html @@ -0,0 +1,122 @@ + + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event-within-sw.https.html b/test/wpt/tests/service-workers/service-worker/fetch-event-within-sw.https.html new file mode 100644 index 00000000000..0b52b18305f --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-event-within-sw.https.html @@ -0,0 +1,53 @@ + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event.https.h2.html b/test/wpt/tests/service-workers/service-worker/fetch-event.https.h2.html new file mode 100644 index 00000000000..5cd381ec98d --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-event.https.h2.html @@ -0,0 +1,112 @@ + + + + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-event.https.html b/test/wpt/tests/service-workers/service-worker/fetch-event.https.html new file mode 100644 index 00000000000..ce53f3c9bff --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-event.https.html @@ -0,0 +1,1000 @@ + + + + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-frame-resource.https.html b/test/wpt/tests/service-workers/service-worker/fetch-frame-resource.https.html new file mode 100644 index 00000000000..a33309f34f9 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-frame-resource.https.html @@ -0,0 +1,236 @@ + +Service Worker: Fetch for the frame loading. + + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-header-visibility.https.html b/test/wpt/tests/service-workers/service-worker/fetch-header-visibility.https.html new file mode 100644 index 00000000000..1f4813c4f8e --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-header-visibility.https.html @@ -0,0 +1,54 @@ + +Service Worker: Visibility of headers during fetch. + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-mixed-content-to-inscope.https.html b/test/wpt/tests/service-workers/service-worker/fetch-mixed-content-to-inscope.https.html new file mode 100644 index 00000000000..0e8fa93b32c --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-mixed-content-to-inscope.https.html @@ -0,0 +1,21 @@ + +Service Worker: Mixed content of fetch() + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-mixed-content-to-outscope.https.html b/test/wpt/tests/service-workers/service-worker/fetch-mixed-content-to-outscope.https.html new file mode 100644 index 00000000000..391dc5d2c19 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-mixed-content-to-outscope.https.html @@ -0,0 +1,21 @@ + +Service Worker: Mixed content of fetch() + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-request-css-base-url.https.html b/test/wpt/tests/service-workers/service-worker/fetch-request-css-base-url.https.html new file mode 100644 index 00000000000..467a66cee46 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-request-css-base-url.https.html @@ -0,0 +1,87 @@ + +Service Worker: CSS's base URL must be the response URL + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-request-css-cross-origin.https.html b/test/wpt/tests/service-workers/service-worker/fetch-request-css-cross-origin.https.html new file mode 100644 index 00000000000..d9c1c7f5df2 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-request-css-cross-origin.https.html @@ -0,0 +1,81 @@ + +Service Worker: Cross-origin CSS files fetched via SW. + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-request-css-images.https.html b/test/wpt/tests/service-workers/service-worker/fetch-request-css-images.https.html new file mode 100644 index 00000000000..586dea26135 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-request-css-images.https.html @@ -0,0 +1,214 @@ + +Service Worker: FetchEvent for css image + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-request-fallback.https.html b/test/wpt/tests/service-workers/service-worker/fetch-request-fallback.https.html new file mode 100644 index 00000000000..a29f31d127b --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-request-fallback.https.html @@ -0,0 +1,282 @@ + +Service Worker: the fallback behavior of FetchEvent + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-request-no-freshness-headers.https.html b/test/wpt/tests/service-workers/service-worker/fetch-request-no-freshness-headers.https.html new file mode 100644 index 00000000000..03b7d357610 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-request-no-freshness-headers.https.html @@ -0,0 +1,55 @@ + +Service Worker: the headers of FetchEvent shouldn't contain freshness headers + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-request-redirect.https.html b/test/wpt/tests/service-workers/service-worker/fetch-request-redirect.https.html new file mode 100644 index 00000000000..5ce015b421f --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-request-redirect.https.html @@ -0,0 +1,385 @@ + +Service Worker: FetchEvent for resources + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-request-resources.https.html b/test/wpt/tests/service-workers/service-worker/fetch-request-resources.https.html new file mode 100644 index 00000000000..b4680c3ccd5 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-request-resources.https.html @@ -0,0 +1,302 @@ + +Service Worker: FetchEvent for resources + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-request-xhr-sync-error.https.window.js b/test/wpt/tests/service-workers/service-worker/fetch-request-xhr-sync-error.https.window.js new file mode 100644 index 00000000000..e6c02139286 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-request-xhr-sync-error.https.window.js @@ -0,0 +1,19 @@ +// META: script=resources/test-helpers.sub.js + +"use strict"; + +promise_test(async t => { + const url = "resources/fetch-request-xhr-sync-error-worker.js"; + const scope = "resources/fetch-request-xhr-sync-iframe.html"; + + const registration = await service_worker_unregister_and_register(t, url, scope); + t.add_cleanup(() => registration.unregister()); + + await wait_for_state(t, registration.installing, 'activated'); + const frame = await with_iframe(scope); + t.add_cleanup(() => frame.remove()); + + assert_throws_dom("NetworkError", frame.contentWindow.DOMException, () => frame.contentWindow.performSyncXHR("non-existent-stream-1.txt")); + assert_throws_dom("NetworkError", frame.contentWindow.DOMException, () => frame.contentWindow.performSyncXHR("non-existent-stream-2.txt")); + assert_throws_dom("NetworkError", frame.contentWindow.DOMException, () => frame.contentWindow.performSyncXHR("non-existent-stream-3.txt")); +}, "Verify synchronous XMLHttpRequest always throws a NetworkError for ReadableStream errors"); diff --git a/test/wpt/tests/service-workers/service-worker/fetch-request-xhr-sync-on-worker.https.html b/test/wpt/tests/service-workers/service-worker/fetch-request-xhr-sync-on-worker.https.html new file mode 100644 index 00000000000..9f18096aa29 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-request-xhr-sync-on-worker.https.html @@ -0,0 +1,41 @@ + +Service Worker: Synchronous XHR on Worker is intercepted + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-request-xhr-sync.https.html b/test/wpt/tests/service-workers/service-worker/fetch-request-xhr-sync.https.html new file mode 100644 index 00000000000..ec27fb89834 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-request-xhr-sync.https.html @@ -0,0 +1,53 @@ + +Service Worker: Synchronous XHR is intercepted + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-request-xhr.https.html b/test/wpt/tests/service-workers/service-worker/fetch-request-xhr.https.html new file mode 100644 index 00000000000..37a457393b0 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-request-xhr.https.html @@ -0,0 +1,75 @@ + +Service Worker: the body of FetchEvent using XMLHttpRequest + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-response-taint.https.html b/test/wpt/tests/service-workers/service-worker/fetch-response-taint.https.html new file mode 100644 index 00000000000..8e190f48506 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-response-taint.https.html @@ -0,0 +1,223 @@ + +Service Worker: Tainting of responses fetched via SW. + + + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-response-xhr.https.html b/test/wpt/tests/service-workers/service-worker/fetch-response-xhr.https.html new file mode 100644 index 00000000000..891eb029422 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-response-xhr.https.html @@ -0,0 +1,50 @@ + +Service Worker: the response of FetchEvent using XMLHttpRequest + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/fetch-waits-for-activate.https.html b/test/wpt/tests/service-workers/service-worker/fetch-waits-for-activate.https.html new file mode 100644 index 00000000000..7c888450f0d --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/fetch-waits-for-activate.https.html @@ -0,0 +1,128 @@ + +Service Worker: Fetch Event Waits for Activate Event + + + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/getregistration.https.html b/test/wpt/tests/service-workers/service-worker/getregistration.https.html new file mode 100644 index 00000000000..634c2efa124 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/getregistration.https.html @@ -0,0 +1,108 @@ + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/getregistrations.https.html b/test/wpt/tests/service-workers/service-worker/getregistrations.https.html new file mode 100644 index 00000000000..3a9b9a23317 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/getregistrations.https.html @@ -0,0 +1,134 @@ + +Service Worker: getRegistrations() + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/global-serviceworker.https.any.js b/test/wpt/tests/service-workers/service-worker/global-serviceworker.https.any.js new file mode 100644 index 00000000000..19d77847c42 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/global-serviceworker.https.any.js @@ -0,0 +1,53 @@ +// META: title=serviceWorker on service worker global +// META: global=serviceworker + +test(() => { + assert_equals(registration.installing, null, 'registration.installing'); + assert_equals(registration.waiting, null, 'registration.waiting'); + assert_equals(registration.active, null, 'registration.active'); + assert_true('serviceWorker' in self, 'self.serviceWorker exists'); + assert_equals(serviceWorker.state, 'parsed', 'serviceWorker.state'); + assert_readonly(self, 'serviceWorker', `self.serviceWorker is read only`); +}, 'First run'); + +// Cache this for later tests. +const initialServiceWorker = self.serviceWorker; + +async_test((t) => { + assert_true('serviceWorker' in self, 'self.serviceWorker exists'); + serviceWorker.postMessage({ messageTest: true }); + + // The rest of the test runs once this receives the above message. + addEventListener('message', t.step_func((event) => { + // Ignore unrelated messages. + if (!event.data.messageTest) return; + assert_equals(event.source, serviceWorker, 'event.source'); + t.done(); + })); +}, 'Can post message to self during startup'); + +// The test is registered now so there isn't a race condition when collecting tests, but the asserts +// don't happen until the 'install' event fires. +async_test((t) => { + addEventListener('install', t.step_func_done(() => { + assert_true('serviceWorker' in self, 'self.serviceWorker exists'); + assert_equals(serviceWorker, initialServiceWorker, `self.serviceWorker hasn't changed`); + assert_equals(registration.installing, serviceWorker, 'registration.installing'); + assert_equals(registration.waiting, null, 'registration.waiting'); + assert_equals(registration.active, null, 'registration.active'); + assert_equals(serviceWorker.state, 'installing', 'serviceWorker.state'); + })); +}, 'During install'); + +// The test is registered now so there isn't a race condition when collecting tests, but the asserts +// don't happen until the 'activate' event fires. +async_test((t) => { + addEventListener('activate', t.step_func_done(() => { + assert_true('serviceWorker' in self, 'self.serviceWorker exists'); + assert_equals(serviceWorker, initialServiceWorker, `self.serviceWorker hasn't changed`); + assert_equals(registration.installing, null, 'registration.installing'); + assert_equals(registration.waiting, null, 'registration.waiting'); + assert_equals(registration.active, serviceWorker, 'registration.active'); + assert_equals(serviceWorker.state, 'activating', 'serviceWorker.state'); + })); +}, 'During activate'); diff --git a/test/wpt/tests/service-workers/service-worker/historical.https.any.js b/test/wpt/tests/service-workers/service-worker/historical.https.any.js new file mode 100644 index 00000000000..20b3ddfbf7b --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/historical.https.any.js @@ -0,0 +1,5 @@ +// META: global=serviceworker + +test((t) => { + assert_false('targetClientId' in FetchEvent.prototype) +}, 'targetClientId should not be on FetchEvent'); diff --git a/test/wpt/tests/service-workers/service-worker/http-to-https-redirect-and-register.https.html b/test/wpt/tests/service-workers/service-worker/http-to-https-redirect-and-register.https.html new file mode 100644 index 00000000000..5626237dccc --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/http-to-https-redirect-and-register.https.html @@ -0,0 +1,49 @@ + +register on a secure page after redirect from an non-secure url + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/immutable-prototype-serviceworker.https.html b/test/wpt/tests/service-workers/service-worker/immutable-prototype-serviceworker.https.html new file mode 100644 index 00000000000..e63f6b348a8 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/immutable-prototype-serviceworker.https.html @@ -0,0 +1,23 @@ + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/import-scripts-cross-origin.https.html b/test/wpt/tests/service-workers/service-worker/import-scripts-cross-origin.https.html new file mode 100644 index 00000000000..773708a9fbc --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/import-scripts-cross-origin.https.html @@ -0,0 +1,18 @@ + + +Tests for importScripts: cross-origin + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/import-scripts-data-url.https.html b/test/wpt/tests/service-workers/service-worker/import-scripts-data-url.https.html new file mode 100644 index 00000000000..f0922193dd0 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/import-scripts-data-url.https.html @@ -0,0 +1,18 @@ + + +Tests for importScripts: data: URL + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/import-scripts-mime-types.https.html b/test/wpt/tests/service-workers/service-worker/import-scripts-mime-types.https.html new file mode 100644 index 00000000000..1679831d0f2 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/import-scripts-mime-types.https.html @@ -0,0 +1,30 @@ + + +Tests for importScripts: MIME types + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/import-scripts-redirect.https.html b/test/wpt/tests/service-workers/service-worker/import-scripts-redirect.https.html new file mode 100644 index 00000000000..07ea49439eb --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/import-scripts-redirect.https.html @@ -0,0 +1,55 @@ + + +Tests for importScripts: redirect + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/import-scripts-resource-map.https.html b/test/wpt/tests/service-workers/service-worker/import-scripts-resource-map.https.html new file mode 100644 index 00000000000..4742bd01268 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/import-scripts-resource-map.https.html @@ -0,0 +1,34 @@ + + +Tests for importScripts: script resource map + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/import-scripts-updated-flag.https.html b/test/wpt/tests/service-workers/service-worker/import-scripts-updated-flag.https.html new file mode 100644 index 00000000000..09b4496aa0e --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/import-scripts-updated-flag.https.html @@ -0,0 +1,83 @@ + + +Tests for importScripts: import scripts updated flag + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/indexeddb.https.html b/test/wpt/tests/service-workers/service-worker/indexeddb.https.html new file mode 100644 index 00000000000..be9be4968f7 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/indexeddb.https.html @@ -0,0 +1,78 @@ + +Service Worker: Indexed DB + + + + diff --git a/test/wpt/tests/service-workers/service-worker/install-event-type.https.html b/test/wpt/tests/service-workers/service-worker/install-event-type.https.html new file mode 100644 index 00000000000..7e74af85c3a --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/install-event-type.https.html @@ -0,0 +1,30 @@ + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/installing.https.html b/test/wpt/tests/service-workers/service-worker/installing.https.html new file mode 100644 index 00000000000..0f257b6aba4 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/installing.https.html @@ -0,0 +1,48 @@ + +ServiceWorker: navigator.serviceWorker.installing + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/interface-requirements-sw.https.html b/test/wpt/tests/service-workers/service-worker/interface-requirements-sw.https.html new file mode 100644 index 00000000000..eef868c889f --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/interface-requirements-sw.https.html @@ -0,0 +1,16 @@ + +Service Worker Global Scope Interfaces + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/invalid-blobtype.https.html b/test/wpt/tests/service-workers/service-worker/invalid-blobtype.https.html new file mode 100644 index 00000000000..1c5920fb039 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/invalid-blobtype.https.html @@ -0,0 +1,40 @@ + +Service Worker: respondWith with header value containing a null byte + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/invalid-header.https.html b/test/wpt/tests/service-workers/service-worker/invalid-header.https.html new file mode 100644 index 00000000000..1bc97697904 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/invalid-header.https.html @@ -0,0 +1,39 @@ + +Service Worker: respondWith with header value containing a null byte + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/iso-latin1-header.https.html b/test/wpt/tests/service-workers/service-worker/iso-latin1-header.https.html new file mode 100644 index 00000000000..c27a5f48a5b --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/iso-latin1-header.https.html @@ -0,0 +1,40 @@ + +Service Worker: respondWith with header value containing an ISO Latin 1 (ISO-8859-1 Character Set) string + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/local-url-inherit-controller.https.html b/test/wpt/tests/service-workers/service-worker/local-url-inherit-controller.https.html new file mode 100644 index 00000000000..6702abcadbb --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/local-url-inherit-controller.https.html @@ -0,0 +1,115 @@ + +Service Worker: local URL windows and workers inherit controller + + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/mime-sniffing.https.html b/test/wpt/tests/service-workers/service-worker/mime-sniffing.https.html new file mode 100644 index 00000000000..8175bcdf877 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/mime-sniffing.https.html @@ -0,0 +1,24 @@ + +Service Worker: MIME sniffing + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/multi-globals/current/current.https.html b/test/wpt/tests/service-workers/service-worker/multi-globals/current/current.https.html new file mode 100644 index 00000000000..82a48d40990 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/multi-globals/current/current.https.html @@ -0,0 +1,2 @@ + +Current page used as a test helper diff --git a/test/wpt/tests/service-workers/service-worker/multi-globals/current/test-sw.js b/test/wpt/tests/service-workers/service-worker/multi-globals/current/test-sw.js new file mode 100644 index 00000000000..e673292f2c0 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/multi-globals/current/test-sw.js @@ -0,0 +1 @@ +// Service worker for current/ \ No newline at end of file diff --git a/test/wpt/tests/service-workers/service-worker/multi-globals/incumbent/incumbent.https.html b/test/wpt/tests/service-workers/service-worker/multi-globals/incumbent/incumbent.https.html new file mode 100644 index 00000000000..4585f15b0f4 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/multi-globals/incumbent/incumbent.https.html @@ -0,0 +1,20 @@ + +Incumbent page used as a test helper + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/multi-globals/incumbent/test-sw.js b/test/wpt/tests/service-workers/service-worker/multi-globals/incumbent/test-sw.js new file mode 100644 index 00000000000..e2a0e93b583 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/multi-globals/incumbent/test-sw.js @@ -0,0 +1 @@ +// Service worker for incumbent/ \ No newline at end of file diff --git a/test/wpt/tests/service-workers/service-worker/multi-globals/relevant/relevant.https.html b/test/wpt/tests/service-workers/service-worker/multi-globals/relevant/relevant.https.html new file mode 100644 index 00000000000..44f42eda493 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/multi-globals/relevant/relevant.https.html @@ -0,0 +1,2 @@ + +Relevant page used as a test helper diff --git a/test/wpt/tests/service-workers/service-worker/multi-globals/relevant/test-sw.js b/test/wpt/tests/service-workers/service-worker/multi-globals/relevant/test-sw.js new file mode 100644 index 00000000000..ff44cdf0867 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/multi-globals/relevant/test-sw.js @@ -0,0 +1 @@ +// Service worker for relevant/ \ No newline at end of file diff --git a/test/wpt/tests/service-workers/service-worker/multi-globals/test-sw.js b/test/wpt/tests/service-workers/service-worker/multi-globals/test-sw.js new file mode 100644 index 00000000000..ce3c940eced --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/multi-globals/test-sw.js @@ -0,0 +1 @@ +// Service worker for / \ No newline at end of file diff --git a/test/wpt/tests/service-workers/service-worker/multi-globals/url-parsing.https.html b/test/wpt/tests/service-workers/service-worker/multi-globals/url-parsing.https.html new file mode 100644 index 00000000000..b9dfe363435 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/multi-globals/url-parsing.https.html @@ -0,0 +1,73 @@ + +register()/getRegistration() URL parsing, with multiple globals in play + + + + + + + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/multipart-image.https.html b/test/wpt/tests/service-workers/service-worker/multipart-image.https.html new file mode 100644 index 00000000000..00c20d25f90 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/multipart-image.https.html @@ -0,0 +1,68 @@ + +Tests for cross-origin multipart image returned by service worker + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/multiple-register.https.html b/test/wpt/tests/service-workers/service-worker/multiple-register.https.html new file mode 100644 index 00000000000..752e132fc13 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/multiple-register.https.html @@ -0,0 +1,117 @@ + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/multiple-update.https.html b/test/wpt/tests/service-workers/service-worker/multiple-update.https.html new file mode 100644 index 00000000000..6a83f73a054 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/multiple-update.https.html @@ -0,0 +1,94 @@ + + +Service Worker: Trigger multiple updates + + + + diff --git a/test/wpt/tests/service-workers/service-worker/navigate-window.https.html b/test/wpt/tests/service-workers/service-worker/navigate-window.https.html new file mode 100644 index 00000000000..46d32a48a0a --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/navigate-window.https.html @@ -0,0 +1,151 @@ + +Service Worker: Navigate a Window + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/navigation-headers.https.html b/test/wpt/tests/service-workers/service-worker/navigation-headers.https.html new file mode 100644 index 00000000000..a4b52035e22 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/navigation-headers.https.html @@ -0,0 +1,819 @@ + + + +Service Worker: Navigation Post Request Origin Header + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/broken-chunked-encoding.https.html b/test/wpt/tests/service-workers/service-worker/navigation-preload/broken-chunked-encoding.https.html new file mode 100644 index 00000000000..ec74282ac33 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/broken-chunked-encoding.https.html @@ -0,0 +1,42 @@ + + +Navigation Preload with chunked encoding + + + + diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/chunked-encoding.https.html b/test/wpt/tests/service-workers/service-worker/navigation-preload/chunked-encoding.https.html new file mode 100644 index 00000000000..830ce32cea5 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/chunked-encoding.https.html @@ -0,0 +1,25 @@ + + +Navigation Preload with chunked encoding + + + + diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/empty-preload-response-body.https.html b/test/wpt/tests/service-workers/service-worker/navigation-preload/empty-preload-response-body.https.html new file mode 100644 index 00000000000..7e8aacdd36a --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/empty-preload-response-body.https.html @@ -0,0 +1,25 @@ + + +Navigation Preload empty response body + + + + diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/get-state.https.html b/test/wpt/tests/service-workers/service-worker/navigation-preload/get-state.https.html new file mode 100644 index 00000000000..08e2f4976c5 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/get-state.https.html @@ -0,0 +1,217 @@ + + +NavigationPreloadManager.getState + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/navigationPreload.https.html b/test/wpt/tests/service-workers/service-worker/navigation-preload/navigationPreload.https.html new file mode 100644 index 00000000000..392e5c14dc8 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/navigationPreload.https.html @@ -0,0 +1,20 @@ + + +ServiceWorker: navigator.serviceWorker.navigationPreload + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/redirect.https.html b/test/wpt/tests/service-workers/service-worker/navigation-preload/redirect.https.html new file mode 100644 index 00000000000..5970f053e36 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/redirect.https.html @@ -0,0 +1,93 @@ + + +Navigation Preload redirect response + + + + diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/request-headers.https.html b/test/wpt/tests/service-workers/service-worker/navigation-preload/request-headers.https.html new file mode 100644 index 00000000000..09642010218 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/request-headers.https.html @@ -0,0 +1,41 @@ + + +Navigation Preload request headers + + + + diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/resource-timing.https.html b/test/wpt/tests/service-workers/service-worker/navigation-preload/resource-timing.https.html new file mode 100644 index 00000000000..468a1f51e85 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/resource-timing.https.html @@ -0,0 +1,92 @@ + + +Navigation Preload Resource Timing + + + + diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/broken-chunked-encoding-scope.asis b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/broken-chunked-encoding-scope.asis new file mode 100644 index 00000000000..2a719536fb8 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/broken-chunked-encoding-scope.asis @@ -0,0 +1,6 @@ +HTTP/1.1 200 OK +Content-type: text/html; charset=UTF-8 +Transfer-encoding: chunked + +hello +world diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/broken-chunked-encoding-worker.js b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/broken-chunked-encoding-worker.js new file mode 100644 index 00000000000..7a453e4055f --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/broken-chunked-encoding-worker.js @@ -0,0 +1,11 @@ +self.addEventListener('activate', event => { + event.waitUntil( + self.registration.navigationPreload.enable()); + }); + +self.addEventListener('fetch', event => { + event.respondWith(event.preloadResponse + .then( + _ => new Response('PASS: preloadResponse resolved'), + _ => new Response('FAIL: preloadResponse rejected'))); + }); diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/chunked-encoding-scope.py b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/chunked-encoding-scope.py new file mode 100644 index 00000000000..659c4d8cdf2 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/chunked-encoding-scope.py @@ -0,0 +1,19 @@ +import time + +def main(request, response): + use_broken_body = b'use_broken_body' in request.GET + + response.add_required_headers = False + response.writer.write_status(200) + response.writer.write_header(b"Content-type", b"text/html; charset=UTF-8") + response.writer.write_header(b"Transfer-encoding", b"chunked") + response.writer.end_headers() + + for idx in range(10): + if use_broken_body: + response.writer.write(u"%s\n%s\n" % (len(str(idx)), idx)) + else: + response.writer.write(u"%s\r\n%s\r\n" % (len(str(idx)), idx)) + time.sleep(0.001) + + response.writer.write(u"0\r\n\r\n") diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/chunked-encoding-worker.js b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/chunked-encoding-worker.js new file mode 100644 index 00000000000..f30e5ed274d --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/chunked-encoding-worker.js @@ -0,0 +1,8 @@ +self.addEventListener('activate', event => { + event.waitUntil( + self.registration.navigationPreload.enable()); + }); + +self.addEventListener('fetch', event => { + event.respondWith(event.preloadResponse); + }); diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/cookie.py b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/cookie.py new file mode 100644 index 00000000000..30a1dd498a5 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/cookie.py @@ -0,0 +1,20 @@ +def main(request, response): + """ + Returns a response with a Set-Cookie header based on the query params. + The body will be "1" if the cookie is present in the request and `drop` parameter is "0", + otherwise the body will be "0". + """ + same_site = request.GET.first(b"same-site") + cookie_name = request.GET.first(b"cookie-name") + drop = request.GET.first(b"drop") + cookie_in_request = b"0" + cookie = b"%s=1; Secure; SameSite=%s" % (cookie_name, same_site) + + if drop == b"1": + cookie += b"; Max-Age=0" + + if request.cookies.get(cookie_name): + cookie_in_request = request.cookies[cookie_name].value + + headers = [(b'Content-Type', b'text/html'), (b'Set-Cookie', cookie)] + return (200, headers, cookie_in_request) diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/empty-preload-response-body-scope.html b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/empty-preload-response-body-scope.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/empty-preload-response-body-worker.js b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/empty-preload-response-body-worker.js new file mode 100644 index 00000000000..48c14b7916f --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/empty-preload-response-body-worker.js @@ -0,0 +1,15 @@ +self.addEventListener('activate', event => { + event.waitUntil( + self.registration.navigationPreload.enable()); + }); + +self.addEventListener('fetch', event => { + event.respondWith( + event.preloadResponse + .then(res => res.text()) + .then(text => { + return new Response( + '[' + text + ']', + {headers: [['content-type', 'text/html']]}); + })); + }); diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/get-state-worker.js b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/get-state-worker.js new file mode 100644 index 00000000000..a14ffb4faaa --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/get-state-worker.js @@ -0,0 +1,21 @@ +// This worker listens for commands from the page and messages back +// the result. + +function handle(message) { + const np = self.registration.navigationPreload; + switch (message) { + case 'getState': + return np.getState(); + case 'enable': + return np.enable(); + case 'disable': + return np.disable(); + case 'setHeaderValue': + return np.setHeaderValue('insightful'); + } + return Promise.reject('bad message'); +} + +self.addEventListener('message', e => { + e.waitUntil(handle(e.data).then(result => e.source.postMessage(result))); + }); diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/helpers.js b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/helpers.js new file mode 100644 index 00000000000..86f0c0916ec --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/helpers.js @@ -0,0 +1,5 @@ +function expect_navigation_preload_state(state, enabled, header, desc) { + assert_equals(Object.keys(state).length, 2, desc + ': # of keys'); + assert_equals(state.enabled, enabled, desc + ': enabled'); + assert_equals(state.headerValue, header, desc + ': header'); +} diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/navigation-preload-worker.js b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/navigation-preload-worker.js new file mode 100644 index 00000000000..6e1ab232907 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/navigation-preload-worker.js @@ -0,0 +1,3 @@ +self.addEventListener('fetch', event => { + event.respondWith(event.preloadResponse); +}); diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/redirect-redirected.html b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/redirect-redirected.html new file mode 100644 index 00000000000..f9bfce5e895 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/redirect-redirected.html @@ -0,0 +1,3 @@ + + +redirected diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/redirect-scope.py b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/redirect-scope.py new file mode 100644 index 00000000000..84a97e594b2 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/redirect-scope.py @@ -0,0 +1,38 @@ +def main(request, response): + if b"base" in request.GET: + return [(b"Content-Type", b"text/html")], b"OK" + type = request.GET.first(b"type") + + if type == b"normal": + response.status = 302 + response.headers.append(b"Location", b"redirect-redirected.html") + response.headers.append(b"Custom-Header", b"hello") + return b"" + + if type == b"no-location": + response.status = 302 + response.headers.append(b"Content-Type", b"text/html") + response.headers.append(b"Custom-Header", b"hello") + return b"" + + if type == b"no-location-with-body": + response.status = 302 + response.headers.append(b"Content-Type", b"text/html") + response.headers.append(b"Custom-Header", b"hello") + return b"BODY" + + if type == b"redirect-to-scope": + response.status = 302 + response.headers.append(b"Location", + b"redirect-scope.py?type=redirect-to-scope2") + return b"" + if type == b"redirect-to-scope2": + response.status = 302 + response.headers.append(b"Location", + b"redirect-scope.py?type=redirect-to-scope3") + return b"" + if type == b"redirect-to-scope3": + response.status = 302 + response.headers.append(b"Location", b"redirect-redirected.html") + response.headers.append(b"Custom-Header", b"hello") + return b"" diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/redirect-worker.js b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/redirect-worker.js new file mode 100644 index 00000000000..1b55f2ef0d9 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/redirect-worker.js @@ -0,0 +1,35 @@ +self.addEventListener('activate', event => { + event.waitUntil( + self.registration.navigationPreload.enable()); + }); + +function get_response_info(r) { + var info = { + type: r.type, + url: r.url, + status: r.status, + ok: r.ok, + statusText: r.statusText, + headers: [] + }; + r.headers.forEach((value, name) => { info.headers.push([value, name]); }); + return info; +} + +function post_to_page(data) { + return self.clients.matchAll() + .then(clients => clients.forEach(client => client.postMessage(data))); +} + +self.addEventListener('fetch', event => { + event.respondWith( + event.preloadResponse + .then( + res => { + if (res.url.includes("base")) { + return res; + } + return post_to_page(get_response_info(res)).then(_ => res); + }, + err => new Response(err.toString()))); + }); diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/request-headers-scope.py b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/request-headers-scope.py new file mode 100644 index 00000000000..5bab5b01f36 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/request-headers-scope.py @@ -0,0 +1,14 @@ +import json + +from wptserve.utils import isomorphic_decode + +def main(request, response): + normalized = dict() + + for key, values in dict(request.headers).items(): + values = [isomorphic_decode(value) for value in values] + normalized[isomorphic_decode(key.upper())] = values + + response.headers.append(b"Content-Type", b"text/html") + + return json.dumps(normalized) diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/request-headers-worker.js b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/request-headers-worker.js new file mode 100644 index 00000000000..1006cf27913 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/request-headers-worker.js @@ -0,0 +1,10 @@ +self.addEventListener('activate', event => { + event.waitUntil( + Promise.all[ + self.registration.navigationPreload.enable(), + self.registration.navigationPreload.setHeaderValue('hello')]); + }); + +self.addEventListener('fetch', event => { + event.respondWith(event.preloadResponse); + }); diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/resource-timing-scope.py b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/resource-timing-scope.py new file mode 100644 index 00000000000..856f9dbc2a4 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/resource-timing-scope.py @@ -0,0 +1,19 @@ +import zlib + +def main(request, response): + type = request.GET.first(b"type") + + if type == "normal": + content = b"This is Navigation Preload Resource Timing test." + output = zlib.compress(content, 9) + headers = [(b"Content-type", b"text/plain"), + (b"Content-Encoding", b"deflate"), + (b"X-Decoded-Body-Size", len(content)), + (b"X-Encoded-Body-Size", len(output)), + (b"Content-Length", len(output))] + return headers, output + + if type == b"redirect": + response.status = 302 + response.headers.append(b"Location", b"redirect-redirected.html") + return b"" diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/resource-timing-worker.js b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/resource-timing-worker.js new file mode 100644 index 00000000000..fac0d8de2a7 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/resource-timing-worker.js @@ -0,0 +1,37 @@ +async function wait_for_performance_entries(url) { + let entries = performance.getEntriesByName(url); + if (entries.length > 0) { + return entries; + } + return new Promise((resolve) => { + new PerformanceObserver((list) => { + const entries = list.getEntriesByName(url); + if (entries.length > 0) { + resolve(entries); + } + }).observe({ entryTypes: ['resource'] }); + }); +} + +self.addEventListener('activate', event => { + event.waitUntil(self.registration.navigationPreload.enable()); + }); + +self.addEventListener('fetch', event => { + let headers; + event.respondWith( + event.preloadResponse + .then(response => { + headers = response.headers; + return response.text() + }) + .then(_ => wait_for_performance_entries(event.request.url)) + .then(entries => + new Response( + JSON.stringify({ + decodedBodySize: headers.get('X-Decoded-Body-Size'), + encodedBodySize: headers.get('X-Encoded-Body-Size'), + timingEntries: entries + }), + {headers: {'Content-Type': 'text/html'}}))); + }); diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/samesite-iframe.html b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/samesite-iframe.html new file mode 100644 index 00000000000..a28b61261e8 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/samesite-iframe.html @@ -0,0 +1,10 @@ + + +samesite + diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/samesite-sw-helper.html b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/samesite-sw-helper.html new file mode 100644 index 00000000000..51fdc9ec74b --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/samesite-sw-helper.html @@ -0,0 +1,34 @@ + + +Navigation Preload Same Site SW registrator + + + + diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/samesite-worker.js b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/samesite-worker.js new file mode 100644 index 00000000000..f30e5ed274d --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/samesite-worker.js @@ -0,0 +1,8 @@ +self.addEventListener('activate', event => { + event.waitUntil( + self.registration.navigationPreload.enable()); + }); + +self.addEventListener('fetch', event => { + event.respondWith(event.preloadResponse); + }); diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/wait-for-activate-worker.js b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/wait-for-activate-worker.js new file mode 100644 index 00000000000..87791d2e487 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/resources/wait-for-activate-worker.js @@ -0,0 +1,40 @@ +// This worker remains in the installing phase so that the +// navigation preload API can be tested when there is no +// active worker. +importScripts('/resources/testharness.js'); +importScripts('helpers.js'); + +function expect_rejection(promise) { + return promise.then( + () => { return Promise.reject('unexpected fulfillment'); }, + err => { assert_equals('InvalidStateError', err.name); }); +} + +function test_before_activation() { + const np = self.registration.navigationPreload; + return expect_rejection(np.enable()) + .then(() => expect_rejection(np.disable())) + .then(() => expect_rejection(np.setHeaderValue('hi'))) + .then(() => np.getState()) + .then(state => expect_navigation_preload_state( + state, false, 'true', 'state should be the default')) + .then(() => 'PASS') + .catch(err => 'FAIL: ' + err); +} + +var resolve_done_promise; +var done_promise = new Promise(resolve => { resolve_done_promise = resolve; }); + +// Run the test once the page messages this worker. +self.addEventListener('message', e => { + e.waitUntil(test_before_activation() + .then(result => { + e.source.postMessage(result); + resolve_done_promise(); + })); + }); + +// Don't become the active worker until the test is done. +self.addEventListener('install', e => { + e.waitUntil(done_promise); + }); diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/samesite-cookies.https.html b/test/wpt/tests/service-workers/service-worker/navigation-preload/samesite-cookies.https.html new file mode 100644 index 00000000000..a860d954566 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/samesite-cookies.https.html @@ -0,0 +1,61 @@ + + + +Navigation Preload: SameSite cookies + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/navigation-preload/samesite-iframe.https.html b/test/wpt/tests/service-workers/service-worker/navigation-preload/samesite-iframe.https.html new file mode 100644 index 00000000000..633da9926a8 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/navigation-preload/samesite-iframe.https.html @@ -0,0 +1,67 @@ + + +Navigation Preload for same site iframe + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/navigation-redirect-body.https.html b/test/wpt/tests/service-workers/service-worker/navigation-redirect-body.https.html new file mode 100644 index 00000000000..0441c610b17 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/navigation-redirect-body.https.html @@ -0,0 +1,53 @@ + +Service Worker: Navigation redirection must clear body + + + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/navigation-redirect-resolution.https.html b/test/wpt/tests/service-workers/service-worker/navigation-redirect-resolution.https.html new file mode 100644 index 00000000000..59e1cafec34 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/navigation-redirect-resolution.https.html @@ -0,0 +1,58 @@ + +Service Worker: Navigation Redirect Resolution + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/navigation-redirect-to-http.https.html b/test/wpt/tests/service-workers/service-worker/navigation-redirect-to-http.https.html new file mode 100644 index 00000000000..d4d2788c589 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/navigation-redirect-to-http.https.html @@ -0,0 +1,25 @@ + +Service Worker: Service Worker can receive HTTP opaqueredirect response. + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/navigation-redirect.https.html b/test/wpt/tests/service-workers/service-worker/navigation-redirect.https.html new file mode 100644 index 00000000000..d7d3d5259a4 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/navigation-redirect.https.html @@ -0,0 +1,846 @@ + +Service Worker: Navigation redirection + + + + + + + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/navigation-sets-cookie.https.html b/test/wpt/tests/service-workers/service-worker/navigation-sets-cookie.https.html new file mode 100644 index 00000000000..7f6c756f557 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/navigation-sets-cookie.https.html @@ -0,0 +1,133 @@ + + + +Service Worker: Navigation setting cookies + + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/navigation-timing-extended.https.html b/test/wpt/tests/service-workers/service-worker/navigation-timing-extended.https.html new file mode 100644 index 00000000000..acb02c6fe1f --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/navigation-timing-extended.https.html @@ -0,0 +1,55 @@ + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/navigation-timing.https.html b/test/wpt/tests/service-workers/service-worker/navigation-timing.https.html new file mode 100644 index 00000000000..6b51a5c2da2 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/navigation-timing.https.html @@ -0,0 +1,76 @@ + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/nested-blob-url-workers.https.html b/test/wpt/tests/service-workers/service-worker/nested-blob-url-workers.https.html new file mode 100644 index 00000000000..7269cbb701f --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/nested-blob-url-workers.https.html @@ -0,0 +1,42 @@ + + +Service Worker: nested blob URL worker clients + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/next-hop-protocol.https.html b/test/wpt/tests/service-workers/service-worker/next-hop-protocol.https.html new file mode 100644 index 00000000000..7a907438d5d --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/next-hop-protocol.https.html @@ -0,0 +1,49 @@ + + +Service Worker: Verify nextHopProtocol is set correctly + + + + diff --git a/test/wpt/tests/service-workers/service-worker/no-dynamic-import-in-module.any.js b/test/wpt/tests/service-workers/service-worker/no-dynamic-import-in-module.any.js new file mode 100644 index 00000000000..f7c2ef37b8b --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/no-dynamic-import-in-module.any.js @@ -0,0 +1,7 @@ +// META: global=serviceworker-module + +// This is imported to ensure import('./basic-module-2.js') fails even if +// it has been previously statically imported. +import './resources/basic-module-2.js'; + +import './resources/no-dynamic-import.js'; diff --git a/test/wpt/tests/service-workers/service-worker/no-dynamic-import.any.js b/test/wpt/tests/service-workers/service-worker/no-dynamic-import.any.js new file mode 100644 index 00000000000..25b370b7094 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/no-dynamic-import.any.js @@ -0,0 +1,3 @@ +// META: global=serviceworker + +importScripts('resources/no-dynamic-import.js'); diff --git a/test/wpt/tests/service-workers/service-worker/onactivate-script-error.https.html b/test/wpt/tests/service-workers/service-worker/onactivate-script-error.https.html new file mode 100644 index 00000000000..f5e80bb9a45 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/onactivate-script-error.https.html @@ -0,0 +1,74 @@ + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/oninstall-script-error.https.html b/test/wpt/tests/service-workers/service-worker/oninstall-script-error.https.html new file mode 100644 index 00000000000..fe7f6e90120 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/oninstall-script-error.https.html @@ -0,0 +1,72 @@ + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/opaque-response-preloaded.https.html b/test/wpt/tests/service-workers/service-worker/opaque-response-preloaded.https.html new file mode 100644 index 00000000000..417aa4ebec8 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/opaque-response-preloaded.https.html @@ -0,0 +1,50 @@ + + +Opaque responses should not be reused for XHRs + + + + diff --git a/test/wpt/tests/service-workers/service-worker/opaque-script.https.html b/test/wpt/tests/service-workers/service-worker/opaque-script.https.html new file mode 100644 index 00000000000..7d2121855df --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/opaque-script.https.html @@ -0,0 +1,71 @@ + +Cache Storage: verify scripts loaded from cache_storage are marked opaque + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/partitioned-claim.tentative.https.html b/test/wpt/tests/service-workers/service-worker/partitioned-claim.tentative.https.html new file mode 100644 index 00000000000..1f42c528e0b --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/partitioned-claim.tentative.https.html @@ -0,0 +1,74 @@ + + +Service Worker: Partitioned Service Workers + + + + + + + +This test creates a iframe in a first-party context and then registers a +service worker (such that the iframe client is unclaimed). +A third-party iframe is then created which has its SW call clients.claim() +and then the test checks that the 1p iframe was not claimed int he process. +Finally the test has its SW call clients.claim() and confirms the 1p iframe is +claimed. + + + + \ No newline at end of file diff --git a/test/wpt/tests/service-workers/service-worker/partitioned-getRegistrations.tentative.https.html b/test/wpt/tests/service-workers/service-worker/partitioned-getRegistrations.tentative.https.html new file mode 100644 index 00000000000..7c4d4f1e028 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/partitioned-getRegistrations.tentative.https.html @@ -0,0 +1,99 @@ + + +Service Worker: Partitioned Service Workers + + + + + + + +This test loads a SW in a first-party context and gets the SW's (randomly) +generated ID. It does the same thing for the SW but in a third-party context +and then confirms that the IDs are different. + + + + \ No newline at end of file diff --git a/test/wpt/tests/service-workers/service-worker/partitioned-matchAll.tentative.https.html b/test/wpt/tests/service-workers/service-worker/partitioned-matchAll.tentative.https.html new file mode 100644 index 00000000000..46beec819c8 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/partitioned-matchAll.tentative.https.html @@ -0,0 +1,65 @@ + + +Service Worker: Partitioned Service Workers + + + + + + + +This test loads a SW in a first-party context and gets has the SW send +its list of clients from client.matchAll(). It does the same thing for the +SW in a third-party context as well and confirms that each SW see's the correct +clients and that they don't see eachother's clients. + + + + \ No newline at end of file diff --git a/test/wpt/tests/service-workers/service-worker/partitioned.tentative.https.html b/test/wpt/tests/service-workers/service-worker/partitioned.tentative.https.html new file mode 100644 index 00000000000..17a375f9c73 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/partitioned.tentative.https.html @@ -0,0 +1,188 @@ + + +Service Worker: Partitioned Service Workers + + + + + + + + + The 3p iframe's postMessage: +

No message received

+ + The nested iframe's postMessage: +

No message received

+ + + \ No newline at end of file diff --git a/test/wpt/tests/service-workers/service-worker/performance-timeline.https.html b/test/wpt/tests/service-workers/service-worker/performance-timeline.https.html new file mode 100644 index 00000000000..e56e6fe416f --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/performance-timeline.https.html @@ -0,0 +1,49 @@ + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/postmessage-blob-url.https.html b/test/wpt/tests/service-workers/service-worker/postmessage-blob-url.https.html new file mode 100644 index 00000000000..16fddd57b83 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/postmessage-blob-url.https.html @@ -0,0 +1,33 @@ + +Service Worker: postMessage Blob URL + + + + diff --git a/test/wpt/tests/service-workers/service-worker/postmessage-from-waiting-serviceworker.https.html b/test/wpt/tests/service-workers/service-worker/postmessage-from-waiting-serviceworker.https.html new file mode 100644 index 00000000000..117def9eb2a --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/postmessage-from-waiting-serviceworker.https.html @@ -0,0 +1,50 @@ + +Service Worker: postMessage from waiting serviceworker + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/postmessage-msgport-to-client.https.html b/test/wpt/tests/service-workers/service-worker/postmessage-msgport-to-client.https.html new file mode 100644 index 00000000000..29c056080c7 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/postmessage-msgport-to-client.https.html @@ -0,0 +1,43 @@ + +Service Worker: postMessage via MessagePort to Client + + + + diff --git a/test/wpt/tests/service-workers/service-worker/postmessage-to-client-message-queue.https.html b/test/wpt/tests/service-workers/service-worker/postmessage-to-client-message-queue.https.html new file mode 100644 index 00000000000..83e5f4540d1 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/postmessage-to-client-message-queue.https.html @@ -0,0 +1,212 @@ + +Service Worker: postMessage to Client (message queue) + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/postmessage-to-client.https.html b/test/wpt/tests/service-workers/service-worker/postmessage-to-client.https.html new file mode 100644 index 00000000000..f834a4bffe2 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/postmessage-to-client.https.html @@ -0,0 +1,42 @@ + +Service Worker: postMessage to Client + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/postmessage.https.html b/test/wpt/tests/service-workers/service-worker/postmessage.https.html new file mode 100644 index 00000000000..7abb3022f91 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/postmessage.https.html @@ -0,0 +1,202 @@ + +Service Worker: postMessage + + + + diff --git a/test/wpt/tests/service-workers/service-worker/ready.https.window.js b/test/wpt/tests/service-workers/service-worker/ready.https.window.js new file mode 100644 index 00000000000..6c4e270682c --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/ready.https.window.js @@ -0,0 +1,223 @@ +// META: title=Service Worker: navigator.serviceWorker.ready +// META: script=resources/test-helpers.sub.js + +test(() => { + assert_equals( + navigator.serviceWorker.ready, + navigator.serviceWorker.ready, + 'repeated access to ready without intervening registrations should return the same Promise object' + ); +}, 'ready returns the same Promise object'); + +promise_test(async t => { + const frame = await with_iframe('resources/blank.html?uncontrolled'); + t.add_cleanup(() => frame.remove()); + + const promise = frame.contentWindow.navigator.serviceWorker.ready; + + assert_equals( + Object.getPrototypeOf(promise), + frame.contentWindow.Promise.prototype, + 'the Promise should be in the context of the related document' + ); +}, 'ready returns a Promise object in the context of the related document'); + +promise_test(async t => { + const url = 'resources/empty-worker.js'; + const scope = 'resources/blank.html?ready-controlled'; + const expectedURL = normalizeURL(url); + const registration = await service_worker_unregister_and_register(t, url, scope); + t.add_cleanup(() => registration.unregister()); + + await wait_for_state(t, registration.installing, 'activated'); + + const frame = await with_iframe(scope); + t.add_cleanup(() => frame.remove()); + + const readyReg = await frame.contentWindow.navigator.serviceWorker.ready; + + assert_equals(readyReg.installing, null, 'installing should be null'); + assert_equals(readyReg.waiting, null, 'waiting should be null'); + assert_equals(readyReg.active.scriptURL, expectedURL, 'active after ready should not be null'); + assert_equals( + frame.contentWindow.navigator.serviceWorker.controller, + readyReg.active, + 'the controller should be the active worker' + ); + assert_in_array( + readyReg.active.state, + ['activating', 'activated'], + '.ready should be resolved when the registration has an active worker' + ); +}, 'ready on a controlled document'); + +promise_test(async t => { + const url = 'resources/empty-worker.js'; + const scope = 'resources/blank.html?ready-potential-controlled'; + const expected_url = normalizeURL(url); + const frame = await with_iframe(scope); + t.add_cleanup(() => frame.remove()); + + const registration = await navigator.serviceWorker.register(url, { scope }); + t.add_cleanup(() => registration.unregister()); + + const readyReg = await frame.contentWindow.navigator.serviceWorker.ready; + + assert_equals(readyReg.installing, null, 'installing should be null'); + assert_equals(readyReg.waiting, null, 'waiting should be null.') + assert_equals(readyReg.active.scriptURL, expected_url, 'active after ready should not be null'); + assert_in_array( + readyReg.active.state, + ['activating', 'activated'], + '.ready should be resolved when the registration has an active worker' + ); + assert_equals( + frame.contentWindow.navigator.serviceWorker.controller, + null, + 'uncontrolled document should not have a controller' + ); +}, 'ready on a potential controlled document'); + +promise_test(async t => { + const url = 'resources/empty-worker.js'; + const scope = 'resources/blank.html?ready-installing'; + + await service_worker_unregister(t, scope); + + const frame = await with_iframe(scope); + const promise = frame.contentWindow.navigator.serviceWorker.ready; + navigator.serviceWorker.register(url, { scope }); + const registration = await promise; + + t.add_cleanup(async () => { + await registration.unregister(); + frame.remove(); + }); + + assert_equals(registration.installing, null, 'installing should be null'); + assert_equals(registration.waiting, null, 'waiting should be null'); + assert_not_equals(registration.active, null, 'active after ready should not be null'); + assert_in_array( + registration.active.state, + ['activating', 'activated'], + '.ready should be resolved when the registration has an active worker' + ); +}, 'ready on an iframe whose parent registers a new service worker'); + +promise_test(async t => { + const scope = 'resources/register-iframe.html'; + const frame = await with_iframe(scope); + + const registration = await frame.contentWindow.navigator.serviceWorker.ready; + + t.add_cleanup(async () => { + await registration.unregister(); + frame.remove(); + }); + + assert_equals(registration.installing, null, 'installing should be null'); + assert_equals(registration.waiting, null, 'waiting should be null'); + assert_not_equals(registration.active, null, 'active after ready should not be null'); + assert_in_array( + registration.active.state, + ['activating', 'activated'], + '.ready should be resolved with "active worker"' + ); + }, 'ready on an iframe that installs a new service worker'); + +promise_test(async t => { + const url = 'resources/empty-worker.js'; + const matchedScope = 'resources/blank.html?ready-after-match'; + const longerMatchedScope = 'resources/blank.html?ready-after-match-longer'; + + await service_worker_unregister(t, matchedScope); + await service_worker_unregister(t, longerMatchedScope); + + const frame = await with_iframe(longerMatchedScope); + const registration = await navigator.serviceWorker.register(url, { scope: matchedScope }); + + t.add_cleanup(async () => { + await registration.unregister(); + frame.remove(); + }); + + await wait_for_state(t, registration.installing, 'activated'); + + const longerRegistration = await navigator.serviceWorker.register(url, { scope: longerMatchedScope }); + + t.add_cleanup(() => longerRegistration.unregister()); + + const readyReg = await frame.contentWindow.navigator.serviceWorker.ready; + + assert_equals( + readyReg.scope, + normalizeURL(longerMatchedScope), + 'longer matched registration should be returned' + ); + assert_equals( + frame.contentWindow.navigator.serviceWorker.controller, + null, + 'controller should be null' + ); +}, 'ready after a longer matched registration registered'); + +promise_test(async t => { + const url = 'resources/empty-worker.js'; + const matchedScope = 'resources/blank.html?ready-after-resolve'; + const longerMatchedScope = 'resources/blank.html?ready-after-resolve-longer'; + const registration = await service_worker_unregister_and_register(t, url, matchedScope); + t.add_cleanup(() => registration.unregister()); + + await wait_for_state(t, registration.installing, 'activated'); + + const frame = await with_iframe(longerMatchedScope); + t.add_cleanup(() => frame.remove()); + + const readyReg1 = await frame.contentWindow.navigator.serviceWorker.ready; + + assert_equals( + readyReg1.scope, + normalizeURL(matchedScope), + 'matched registration should be returned' + ); + + const longerReg = await navigator.serviceWorker.register(url, { scope: longerMatchedScope }); + t.add_cleanup(() => longerReg.unregister()); + + const readyReg2 = await frame.contentWindow.navigator.serviceWorker.ready; + + assert_equals( + readyReg2.scope, + normalizeURL(matchedScope), + 'ready should only be resolved once' + ); +}, 'access ready after it has been resolved'); + +promise_test(async t => { + const url1 = 'resources/empty-worker.js'; + const url2 = url1 + '?2'; + const matchedScope = 'resources/blank.html?ready-after-unregister'; + const reg1 = await service_worker_unregister_and_register(t, url1, matchedScope); + t.add_cleanup(() => reg1.unregister()); + + await wait_for_state(t, reg1.installing, 'activating'); + + const frame = await with_iframe(matchedScope); + t.add_cleanup(() => frame.remove()); + + await reg1.unregister(); + + // Ready promise should be pending, waiting for a new registration to arrive + const readyPromise = frame.contentWindow.navigator.serviceWorker.ready; + + const reg2 = await navigator.serviceWorker.register(url2, { scope: matchedScope }); + t.add_cleanup(() => reg2.unregister()); + + const readyReg = await readyPromise; + + // Wait for registration update, since it comes from another global, the states are racy. + await wait_for_state(t, reg2.installing || reg2.waiting || reg2.active, 'activated'); + + assert_equals(readyReg.active.scriptURL, reg2.active.scriptURL, 'Resolves with the second registration'); + assert_not_equals(reg1, reg2, 'Registrations should be different'); +}, 'resolve ready after unregistering'); diff --git a/test/wpt/tests/service-workers/service-worker/redirected-response.https.html b/test/wpt/tests/service-workers/service-worker/redirected-response.https.html new file mode 100644 index 00000000000..71b35d0e120 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/redirected-response.https.html @@ -0,0 +1,471 @@ + +Service Worker: Redirected response + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/referer.https.html b/test/wpt/tests/service-workers/service-worker/referer.https.html new file mode 100644 index 00000000000..0957e4c5330 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/referer.https.html @@ -0,0 +1,40 @@ + +Service Worker: check referer of fetch() + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/referrer-policy-header.https.html b/test/wpt/tests/service-workers/service-worker/referrer-policy-header.https.html new file mode 100644 index 00000000000..784343e6d8f --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/referrer-policy-header.https.html @@ -0,0 +1,67 @@ + +Service Worker: check referer of fetch() with Referrer Policy + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/referrer-toplevel-script-fetch.https.html b/test/wpt/tests/service-workers/service-worker/referrer-toplevel-script-fetch.https.html new file mode 100644 index 00000000000..65c60a11db2 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/referrer-toplevel-script-fetch.https.html @@ -0,0 +1,64 @@ + +Service Worker: check referrer of top-level script fetch + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/register-closed-window.https.html b/test/wpt/tests/service-workers/service-worker/register-closed-window.https.html new file mode 100644 index 00000000000..9c1b639bb75 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/register-closed-window.https.html @@ -0,0 +1,35 @@ + +Service Worker: Register() on Closed Window + + + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/register-default-scope.https.html b/test/wpt/tests/service-workers/service-worker/register-default-scope.https.html new file mode 100644 index 00000000000..1d86548eb5e --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/register-default-scope.https.html @@ -0,0 +1,69 @@ + +register() and scope + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/register-same-scope-different-script-url.https.html b/test/wpt/tests/service-workers/service-worker/register-same-scope-different-script-url.https.html new file mode 100644 index 00000000000..6eb00f3071a --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/register-same-scope-different-script-url.https.html @@ -0,0 +1,233 @@ + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/register-wait-forever-in-install-worker.https.html b/test/wpt/tests/service-workers/service-worker/register-wait-forever-in-install-worker.https.html new file mode 100644 index 00000000000..0920b5cb223 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/register-wait-forever-in-install-worker.https.html @@ -0,0 +1,57 @@ + +Service Worker: Register wait-forever-in-install-worker + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/registration-basic.https.html b/test/wpt/tests/service-workers/service-worker/registration-basic.https.html new file mode 100644 index 00000000000..759b4244a26 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/registration-basic.https.html @@ -0,0 +1,39 @@ + +Service Worker: Registration (basic) + + + + diff --git a/test/wpt/tests/service-workers/service-worker/registration-end-to-end.https.html b/test/wpt/tests/service-workers/service-worker/registration-end-to-end.https.html new file mode 100644 index 00000000000..1af4582d387 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/registration-end-to-end.https.html @@ -0,0 +1,88 @@ + +Service Worker: registration end-to-end + + + + diff --git a/test/wpt/tests/service-workers/service-worker/registration-events.https.html b/test/wpt/tests/service-workers/service-worker/registration-events.https.html new file mode 100644 index 00000000000..5bcfd66846b --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/registration-events.https.html @@ -0,0 +1,42 @@ + +Service Worker: registration events + + + + diff --git a/test/wpt/tests/service-workers/service-worker/registration-iframe.https.html b/test/wpt/tests/service-workers/service-worker/registration-iframe.https.html new file mode 100644 index 00000000000..ae39ddfea3e --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/registration-iframe.https.html @@ -0,0 +1,116 @@ + + +Service Worker: Registration for iframe + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/registration-mime-types.https.html b/test/wpt/tests/service-workers/service-worker/registration-mime-types.https.html new file mode 100644 index 00000000000..3a21aac5c75 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/registration-mime-types.https.html @@ -0,0 +1,10 @@ + +Service Worker: Registration (MIME types) + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/registration-schedule-job.https.html b/test/wpt/tests/service-workers/service-worker/registration-schedule-job.https.html new file mode 100644 index 00000000000..25d758ee8f0 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/registration-schedule-job.https.html @@ -0,0 +1,107 @@ + + + +Service Worker: Schedule Job algorithm + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/registration-scope-module-static-import.https.html b/test/wpt/tests/service-workers/service-worker/registration-scope-module-static-import.https.html new file mode 100644 index 00000000000..5c75295aed1 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/registration-scope-module-static-import.https.html @@ -0,0 +1,41 @@ + +Service Worker: Static imports from module top-level scripts shouldn't be affected by the service worker script path restriction + + + + diff --git a/test/wpt/tests/service-workers/service-worker/registration-scope.https.html b/test/wpt/tests/service-workers/service-worker/registration-scope.https.html new file mode 100644 index 00000000000..141875f5847 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/registration-scope.https.html @@ -0,0 +1,9 @@ + +Service Worker: Registration (scope) + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/registration-script-module.https.html b/test/wpt/tests/service-workers/service-worker/registration-script-module.https.html new file mode 100644 index 00000000000..9e39a1f75b2 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/registration-script-module.https.html @@ -0,0 +1,13 @@ + +Service Worker: Registration (module script) + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/registration-script-url.https.html b/test/wpt/tests/service-workers/service-worker/registration-script-url.https.html new file mode 100644 index 00000000000..bda61adb002 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/registration-script-url.https.html @@ -0,0 +1,9 @@ + +Service Worker: Registration (scriptURL) + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/registration-script.https.html b/test/wpt/tests/service-workers/service-worker/registration-script.https.html new file mode 100644 index 00000000000..f1e51fd265e --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/registration-script.https.html @@ -0,0 +1,12 @@ + +Service Worker: Registration (script) + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/registration-security-error.https.html b/test/wpt/tests/service-workers/service-worker/registration-security-error.https.html new file mode 100644 index 00000000000..860c2d22ea9 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/registration-security-error.https.html @@ -0,0 +1,9 @@ + +Service Worker: Registration (SecurityError) + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/registration-service-worker-attributes.https.html b/test/wpt/tests/service-workers/service-worker/registration-service-worker-attributes.https.html new file mode 100644 index 00000000000..f7b52d5ddce --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/registration-service-worker-attributes.https.html @@ -0,0 +1,72 @@ + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/registration-updateviacache.https.html b/test/wpt/tests/service-workers/service-worker/registration-updateviacache.https.html new file mode 100644 index 00000000000..b2f6bbc6f84 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/registration-updateviacache.https.html @@ -0,0 +1,204 @@ + +Service Worker: Registration-updateViaCache + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/rejections.https.html b/test/wpt/tests/service-workers/service-worker/rejections.https.html new file mode 100644 index 00000000000..8002ad9a81b --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/rejections.https.html @@ -0,0 +1,21 @@ + +Service Worker: Rejection Types + + + diff --git a/test/wpt/tests/service-workers/service-worker/request-end-to-end.https.html b/test/wpt/tests/service-workers/service-worker/request-end-to-end.https.html new file mode 100644 index 00000000000..a39ceadd9f3 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/request-end-to-end.https.html @@ -0,0 +1,40 @@ + +Service Worker: FetchEvent.request passed to onfetch + + + + diff --git a/test/wpt/tests/service-workers/service-worker/resource-timing-bodySize.https.html b/test/wpt/tests/service-workers/service-worker/resource-timing-bodySize.https.html new file mode 100644 index 00000000000..5c2b1eba8c8 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resource-timing-bodySize.https.html @@ -0,0 +1,55 @@ + + + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/resource-timing-cross-origin.https.html b/test/wpt/tests/service-workers/service-worker/resource-timing-cross-origin.https.html new file mode 100644 index 00000000000..2155d7ff6e4 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resource-timing-cross-origin.https.html @@ -0,0 +1,46 @@ + + + + +This test validates Resource Timing for cross origin content fetched by Service Worker from an originally same-origin URL. + + + + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/resource-timing-fetch-variants.https.html b/test/wpt/tests/service-workers/service-worker/resource-timing-fetch-variants.https.html new file mode 100644 index 00000000000..8d4f0be01a8 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resource-timing-fetch-variants.https.html @@ -0,0 +1,121 @@ + + + +Test various interactions between fetch, service-workers and resource timing + + + + + + + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/resource-timing.sub.https.html b/test/wpt/tests/service-workers/service-worker/resource-timing.sub.https.html new file mode 100644 index 00000000000..9808ae5ae1b --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resource-timing.sub.https.html @@ -0,0 +1,150 @@ + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/404.py b/test/wpt/tests/service-workers/service-worker/resources/404.py new file mode 100644 index 00000000000..1ee4af169e7 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/404.py @@ -0,0 +1,5 @@ +# iframe does not fire onload event if the response's content-type is not +# text/plain or text/html so this script exists if you want to test a 404 load +# in an iframe. +def main(req, res): + return 404, [(b'Content-Type', b'text/plain')], b"Page not found" diff --git a/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-blank-dynamic-nested-frame.html b/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-blank-dynamic-nested-frame.html new file mode 100644 index 00000000000..1e0c6209bfc --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-blank-dynamic-nested-frame.html @@ -0,0 +1,21 @@ + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-blank-nested-frame.html b/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-blank-nested-frame.html new file mode 100644 index 00000000000..16f7e7c60ff --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-blank-nested-frame.html @@ -0,0 +1,21 @@ + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-frame.py b/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-frame.py new file mode 100644 index 00000000000..a29ff9d4134 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-frame.py @@ -0,0 +1,31 @@ +def main(request, response): + if b'nested' in request.GET: + return ( + [(b'Content-Type', b'text/html')], + b'failed: nested frame was not intercepted by the service worker' + ) + + return ([(b'Content-Type', b'text/html')], b""" + + + + + + + + +""") diff --git a/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-ping-frame.py b/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-ping-frame.py new file mode 100644 index 00000000000..30fbbbb5357 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-ping-frame.py @@ -0,0 +1,49 @@ +def main(request, response): + if b'nested' in request.GET: + return ( + [(b'Content-Type', b'text/html')], + b'failed: nested frame was not intercepted by the service worker' + ) + + return ([(b'Content-Type', b'text/html')], b""" + + + + + + + + +""") diff --git a/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-popup-frame.py b/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-popup-frame.py new file mode 100644 index 00000000000..04c12a6037d --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-popup-frame.py @@ -0,0 +1,32 @@ +def main(request, response): + if b'nested' in request.GET: + return ( + [(b'Content-Type', b'text/html')], + b'failed: nested frame was not intercepted by the service worker' + ) + + return ([(b'Content-Type', b'text/html')], b""" + + + + + + +""") diff --git a/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-srcdoc-nested-frame.html b/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-srcdoc-nested-frame.html new file mode 100644 index 00000000000..0122a00aa43 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-srcdoc-nested-frame.html @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-uncontrolled-nested-frame.html b/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-uncontrolled-nested-frame.html new file mode 100644 index 00000000000..89509159a41 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-uncontrolled-nested-frame.html @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-worker.js b/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-worker.js new file mode 100644 index 00000000000..f43598e41c1 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/about-blank-replacement-worker.js @@ -0,0 +1,95 @@ +// Helper routine to find a client that matches a particular URL. Note, we +// require that Client to be controlled to avoid false matches with other +// about:blank windows the browser might have. The initial about:blank should +// inherit the controller from its parent. +async function getClientByURL(url) { + let list = await clients.matchAll(); + return list.find(client => client.url === url); +} + +// Helper routine to perform a ping-pong with the given target client. We +// expect the Client to respond with its location URL. +async function pingPong(target) { + function waitForPong() { + return new Promise(resolve => { + self.addEventListener('message', function onMessage(evt) { + if (evt.data.type === 'PONG') { + resolve(evt.data.location); + } + }); + }); + } + + target.postMessage({ type: 'PING' }) + return await waitForPong(target); +} + +addEventListener('fetch', async evt => { + let url = new URL(evt.request.url); + if (!url.searchParams.get('nested')) { + return; + } + + evt.respondWith(async function() { + // Find the initial about:blank document. + const client = await getClientByURL('about:blank'); + if (!client) { + return new Response('failure: could not find about:blank client'); + } + + // If the nested frame is configured to support a ping-pong, then + // ping it now to verify its message listener exists. We also + // verify the Client's idea of its own location URL while we are doing + // this. + if (url.searchParams.get('ping')) { + const loc = await pingPong(client); + if (loc !== 'about:blank') { + return new Response(`failure: got location {$loc}, expected about:blank`); + } + } + + // Finally, allow the nested frame to complete loading. We place the + // Client ID we found for the initial about:blank in the body. + return new Response(client.id); + }()); +}); + +addEventListener('message', evt => { + if (evt.data.type !== 'GET_CLIENT_ID') { + return; + } + + evt.waitUntil(async function() { + let url = new URL(evt.data.url); + + // Find the given Client by its URL. + let client = await getClientByURL(evt.data.url); + if (!client) { + evt.source.postMessage({ + type: 'GET_CLIENT_ID', + result: `failure: could not find ${evt.data.url} client` + }); + return; + } + + // If the Client supports a ping-pong, then do it now to verify + // the message listener exists and its location matches the + // Client object. + if (url.searchParams.get('ping')) { + let loc = await pingPong(client); + if (loc !== evt.data.url) { + evt.source.postMessage({ + type: 'GET_CLIENT_ID', + result: `failure: got location ${loc}, expected ${evt.data.url}` + }); + return; + } + } + + // Finally, send the client ID back. + evt.source.postMessage({ + type: 'GET_CLIENT_ID', + result: client.id + }); + }()); +}); diff --git a/test/wpt/tests/service-workers/service-worker/resources/basic-module-2.js b/test/wpt/tests/service-workers/service-worker/resources/basic-module-2.js new file mode 100644 index 00000000000..189b1c87fee --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/basic-module-2.js @@ -0,0 +1 @@ +export default 'hello again!'; diff --git a/test/wpt/tests/service-workers/service-worker/resources/basic-module.js b/test/wpt/tests/service-workers/service-worker/resources/basic-module.js new file mode 100644 index 00000000000..789a89bc630 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/basic-module.js @@ -0,0 +1 @@ +export default 'hello!'; diff --git a/test/wpt/tests/service-workers/service-worker/resources/blank.html b/test/wpt/tests/service-workers/service-worker/resources/blank.html new file mode 100644 index 00000000000..a3c3a4689a6 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/blank.html @@ -0,0 +1,2 @@ + +Empty doc diff --git a/test/wpt/tests/service-workers/service-worker/resources/bytecheck-worker-imported-script.py b/test/wpt/tests/service-workers/service-worker/resources/bytecheck-worker-imported-script.py new file mode 100644 index 00000000000..1931c77b678 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/bytecheck-worker-imported-script.py @@ -0,0 +1,20 @@ +import time + +def main(request, response): + headers = [(b'Content-Type', b'application/javascript'), + (b'Cache-Control', b'max-age=0'), + (b'Access-Control-Allow-Origin', b'*')] + + imported_content_type = b'' + if b'imported' in request.GET: + imported_content_type = request.GET[b'imported'] + + imported_content = b'default' + if imported_content_type == b'time': + imported_content = b'%f' % time.time() + + body = b''' + // %s + ''' % (imported_content) + + return headers, body diff --git a/test/wpt/tests/service-workers/service-worker/resources/bytecheck-worker.py b/test/wpt/tests/service-workers/service-worker/resources/bytecheck-worker.py new file mode 100644 index 00000000000..10f3bceb4fd --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/bytecheck-worker.py @@ -0,0 +1,38 @@ +import time + +def main(request, response): + headers = [(b'Content-Type', b'application/javascript'), + (b'Cache-Control', b'max-age=0')] + + main_content_type = b'' + if b'main' in request.GET: + main_content_type = request.GET[b'main'] + + main_content = b'default' + if main_content_type == b'time': + main_content = b'%f' % time.time() + + imported_request_path = b'' + if b'path' in request.GET: + imported_request_path = request.GET[b'path'] + + imported_request_type = b'' + if b'imported' in request.GET: + imported_request_type = request.GET[b'imported'] + + imported_request = b'' + if imported_request_type == b'time': + imported_request = b'?imported=time' + + if b'type' in request.GET and request.GET[b'type'] == b'module': + body = b''' + // %s + import '%sbytecheck-worker-imported-script.py%s'; + ''' % (main_content, imported_request_path, imported_request) + else: + body = b''' + // %s + importScripts('%sbytecheck-worker-imported-script.py%s'); + ''' % (main_content, imported_request_path, imported_request) + + return headers, body diff --git a/test/wpt/tests/service-workers/service-worker/resources/claim-blob-url-worker-fetch-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/claim-blob-url-worker-fetch-iframe.html new file mode 100644 index 00000000000..12ae1a87258 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/claim-blob-url-worker-fetch-iframe.html @@ -0,0 +1,21 @@ + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/claim-nested-worker-fetch-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/claim-nested-worker-fetch-iframe.html new file mode 100644 index 00000000000..2fa15db61d9 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/claim-nested-worker-fetch-iframe.html @@ -0,0 +1,16 @@ + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/claim-nested-worker-fetch-parent-worker.js b/test/wpt/tests/service-workers/service-worker/resources/claim-nested-worker-fetch-parent-worker.js new file mode 100644 index 00000000000..f5ff7c234b4 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/claim-nested-worker-fetch-parent-worker.js @@ -0,0 +1,12 @@ +try { + var worker = new Worker('./claim-worker-fetch-worker.js'); + + self.onmessage = (event) => { + worker.postMessage(event.data); + } + worker.onmessage = (event) => { + self.postMessage(event.data); + }; +} catch (e) { + self.postMessage("Fail: " + e.data); +} diff --git a/test/wpt/tests/service-workers/service-worker/resources/claim-shared-worker-fetch-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/claim-shared-worker-fetch-iframe.html new file mode 100644 index 00000000000..ad865b848f8 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/claim-shared-worker-fetch-iframe.html @@ -0,0 +1,13 @@ + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/claim-shared-worker-fetch-worker.js b/test/wpt/tests/service-workers/service-worker/resources/claim-shared-worker-fetch-worker.js new file mode 100644 index 00000000000..ddc8bea7af4 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/claim-shared-worker-fetch-worker.js @@ -0,0 +1,8 @@ +self.onconnect = (event) => { + var port = event.ports[0]; + event.ports[0].onmessage = (evt) => { + fetch(evt.data) + .then(response => response.text()) + .then(text => port.postMessage(text)); + }; +}; diff --git a/test/wpt/tests/service-workers/service-worker/resources/claim-with-redirect-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/claim-with-redirect-iframe.html new file mode 100644 index 00000000000..4150d7e6857 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/claim-with-redirect-iframe.html @@ -0,0 +1,48 @@ + + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/claim-worker-fetch-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/claim-worker-fetch-iframe.html new file mode 100644 index 00000000000..92c5d15def0 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/claim-worker-fetch-iframe.html @@ -0,0 +1,13 @@ + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/claim-worker-fetch-worker.js b/test/wpt/tests/service-workers/service-worker/resources/claim-worker-fetch-worker.js new file mode 100644 index 00000000000..7080181c85c --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/claim-worker-fetch-worker.js @@ -0,0 +1,5 @@ +self.onmessage = (event) => { + fetch(event.data) + .then(response => response.text()) + .then(text => self.postMessage(text)); +}; diff --git a/test/wpt/tests/service-workers/service-worker/resources/claim-worker.js b/test/wpt/tests/service-workers/service-worker/resources/claim-worker.js new file mode 100644 index 00000000000..18004079475 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/claim-worker.js @@ -0,0 +1,19 @@ +self.addEventListener('message', function(event) { + self.clients.claim() + .then(function(result) { + if (result !== undefined) { + event.data.port.postMessage( + 'FAIL: claim() should be resolved with undefined'); + return; + } + event.data.port.postMessage('PASS'); + }) + .catch(function(error) { + event.data.port.postMessage('FAIL: exception: ' + error.name); + }); + }); + +self.addEventListener('fetch', function(event) { + if (!/404/.test(event.request.url)) + event.respondWith(new Response('Intercepted!')); + }); diff --git a/test/wpt/tests/service-workers/service-worker/resources/classic-worker.js b/test/wpt/tests/service-workers/service-worker/resources/classic-worker.js new file mode 100644 index 00000000000..36a32b1a1f8 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/classic-worker.js @@ -0,0 +1 @@ +importScripts('./imported-classic-script.js'); diff --git a/test/wpt/tests/service-workers/service-worker/resources/client-id-worker.js b/test/wpt/tests/service-workers/service-worker/resources/client-id-worker.js new file mode 100644 index 00000000000..ec71b3458b7 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/client-id-worker.js @@ -0,0 +1,27 @@ +self.onmessage = function(e) { + var port = e.data.port; + var message = []; + + var promise = Promise.resolve() + .then(function() { + // 1st matchAll() + return self.clients.matchAll().then(function(clients) { + clients.forEach(function(client) { + message.push(client.id); + }); + }); + }) + .then(function() { + // 2nd matchAll() + return self.clients.matchAll().then(function(clients) { + clients.forEach(function(client) { + message.push(client.id); + }); + }); + }) + .then(function() { + // Send an array containing ids of clients from 1st and 2nd matchAll() + port.postMessage(message); + }); + e.waitUntil(promise); +}; diff --git a/test/wpt/tests/service-workers/service-worker/resources/client-navigate-frame.html b/test/wpt/tests/service-workers/service-worker/resources/client-navigate-frame.html new file mode 100644 index 00000000000..7e186f8ee79 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/client-navigate-frame.html @@ -0,0 +1,12 @@ + + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/client-navigate-worker.js b/test/wpt/tests/service-workers/service-worker/resources/client-navigate-worker.js new file mode 100644 index 00000000000..6101d5d8f92 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/client-navigate-worker.js @@ -0,0 +1,92 @@ +importScripts("worker-testharness.js"); +importScripts("test-helpers.sub.js"); +importScripts("/common/get-host-info.sub.js") +importScripts("testharness-helpers.js") + +setup({ explicit_done: true }); + +self.onfetch = function(e) { + if (e.request.url.indexOf("client-navigate-frame.html") >= 0) { + return; + } + e.respondWith(new Response(e.clientId)); +}; + +function pass(test, url) { + return { result: test, + url: url }; +} + +function fail(test, reason) { + return { result: "FAILED " + test + " " + reason } +} + +self.onmessage = function(e) { + var port = e.data.port; + var test = e.data.test; + var clientId = e.data.clientId; + var clientUrl = ""; + if (test === "test_client_navigate_success") { + promise_test(function(t) { + this.add_cleanup(() => port.postMessage(pass(test, clientUrl))); + return self.clients.get(clientId) + .then(client => client.navigate("client-navigated-frame.html")) + .then(client => { + clientUrl = client.url; + assert_true(client instanceof WindowClient); + }) + .catch(unreached_rejection(t)); + }, "Return value should be instance of WindowClient"); + done(); + } else if (test === "test_client_navigate_cross_origin") { + promise_test(function(t) { + this.add_cleanup(() => port.postMessage(pass(test, clientUrl))); + var path = new URL('client-navigated-frame.html', self.location.href).pathname; + var url = get_host_info()['HTTPS_REMOTE_ORIGIN'] + path; + return self.clients.get(clientId) + .then(client => client.navigate(url)) + .then(client => { + clientUrl = (client && client.url) || ""; + assert_equals(client, null, + 'cross-origin navigate resolves with null'); + }) + .catch(unreached_rejection(t)); + }, "Navigating to different origin should resolve with null"); + done(); + } else if (test === "test_client_navigate_about_blank") { + promise_test(function(t) { + this.add_cleanup(function() { port.postMessage(pass(test, "")); }); + return self.clients.get(clientId) + .then(client => promise_rejects_js(t, TypeError, client.navigate("about:blank"))) + .catch(unreached_rejection(t)); + }, "Navigating to about:blank should reject with TypeError"); + done(); + } else if (test === "test_client_navigate_mixed_content") { + promise_test(function(t) { + this.add_cleanup(function() { port.postMessage(pass(test, "")); }); + var path = new URL('client-navigated-frame.html', self.location.href).pathname; + // Insecure URL should fail since the frame is owned by a secure parent + // and navigating to http:// would create a mixed-content violation. + var url = get_host_info()['HTTP_REMOTE_ORIGIN'] + path; + return self.clients.get(clientId) + .then(client => promise_rejects_js(t, TypeError, client.navigate(url))) + .catch(unreached_rejection(t)); + }, "Navigating to mixed-content iframe should reject with TypeError"); + done(); + } else if (test === "test_client_navigate_redirect") { + var host_info = get_host_info(); + var url = new URL(host_info['HTTPS_REMOTE_ORIGIN']).toString() + + new URL("client-navigated-frame.html", location).pathname.substring(1); + promise_test(function(t) { + this.add_cleanup(() => port.postMessage(pass(test, clientUrl))); + return self.clients.get(clientId) + .then(client => client.navigate("redirect.py?Redirect=" + url)) + .then(client => { + clientUrl = (client && client.url) || "" + assert_equals(client, null); + }) + .catch(unreached_rejection(t)); + }, "Redirecting to another origin should resolve with null"); + done(); + } +}; diff --git a/test/wpt/tests/service-workers/service-worker/resources/client-navigated-frame.html b/test/wpt/tests/service-workers/service-worker/resources/client-navigated-frame.html new file mode 100644 index 00000000000..307f7f9ac6e --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/client-navigated-frame.html @@ -0,0 +1,3 @@ + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/client-url-of-blob-url-worker.html b/test/wpt/tests/service-workers/service-worker/resources/client-url-of-blob-url-worker.html new file mode 100644 index 00000000000..00f6acede8e --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/client-url-of-blob-url-worker.html @@ -0,0 +1,26 @@ + + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/client-url-of-blob-url-worker.js b/test/wpt/tests/service-workers/service-worker/resources/client-url-of-blob-url-worker.js new file mode 100644 index 00000000000..fd754f8250d --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/client-url-of-blob-url-worker.js @@ -0,0 +1,10 @@ +addEventListener('fetch', e => { + if (e.request.url.includes('get-worker-client-url')) { + e.respondWith((async () => { + const clients = await self.clients.matchAll({type: 'worker'}); + if (clients.length != 1) + return new Response('one worker client should exist'); + return new Response(clients[0].url); + })()); + } +}); diff --git a/test/wpt/tests/service-workers/service-worker/resources/clients-frame-freeze.html b/test/wpt/tests/service-workers/service-worker/resources/clients-frame-freeze.html new file mode 100644 index 00000000000..7468a660e90 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/clients-frame-freeze.html @@ -0,0 +1,15 @@ + + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/clients-get-client-types-frame-worker.js b/test/wpt/tests/service-workers/service-worker/resources/clients-get-client-types-frame-worker.js new file mode 100644 index 00000000000..0a1461b40e0 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/clients-get-client-types-frame-worker.js @@ -0,0 +1,11 @@ +onmessage = function(e) { + if (e.data.cmd == 'GetClientId') { + fetch('clientId') + .then(function(response) { + return response.text(); + }) + .then(function(text) { + e.data.port.postMessage({clientId: text}); + }); + } +}; diff --git a/test/wpt/tests/service-workers/service-worker/resources/clients-get-client-types-frame.html b/test/wpt/tests/service-workers/service-worker/resources/clients-get-client-types-frame.html new file mode 100644 index 00000000000..4324e6d4054 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/clients-get-client-types-frame.html @@ -0,0 +1,17 @@ + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/clients-get-client-types-shared-worker.js b/test/wpt/tests/service-workers/service-worker/resources/clients-get-client-types-shared-worker.js new file mode 100644 index 00000000000..fadef970374 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/clients-get-client-types-shared-worker.js @@ -0,0 +1,10 @@ +onconnect = function(e) { + var port = e.ports[0]; + fetch('clientId') + .then(function(response) { + return response.text(); + }) + .then(function(text) { + port.postMessage({clientId: text}); + }); +}; diff --git a/test/wpt/tests/service-workers/service-worker/resources/clients-get-client-types-worker.js b/test/wpt/tests/service-workers/service-worker/resources/clients-get-client-types-worker.js new file mode 100644 index 00000000000..0a1461b40e0 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/clients-get-client-types-worker.js @@ -0,0 +1,11 @@ +onmessage = function(e) { + if (e.data.cmd == 'GetClientId') { + fetch('clientId') + .then(function(response) { + return response.text(); + }) + .then(function(text) { + e.data.port.postMessage({clientId: text}); + }); + } +}; diff --git a/test/wpt/tests/service-workers/service-worker/resources/clients-get-cross-origin-frame.html b/test/wpt/tests/service-workers/service-worker/resources/clients-get-cross-origin-frame.html new file mode 100644 index 00000000000..e16bb1116dc --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/clients-get-cross-origin-frame.html @@ -0,0 +1,50 @@ + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/clients-get-frame.html b/test/wpt/tests/service-workers/service-worker/resources/clients-get-frame.html new file mode 100644 index 00000000000..27143d4b99b --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/clients-get-frame.html @@ -0,0 +1,12 @@ + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/clients-get-other-origin.html b/test/wpt/tests/service-workers/service-worker/resources/clients-get-other-origin.html new file mode 100644 index 00000000000..6342fe04f40 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/clients-get-other-origin.html @@ -0,0 +1,64 @@ + + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/clients-get-resultingClientId-worker.js b/test/wpt/tests/service-workers/service-worker/resources/clients-get-resultingClientId-worker.js new file mode 100644 index 00000000000..5a46ff9cf44 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/clients-get-resultingClientId-worker.js @@ -0,0 +1,60 @@ +let savedPort = null; +let savedResultingClientId = null; + +async function getTestingPage() { + const clientList = await self.clients.matchAll({ type: 'window', includeUncontrolled: true }); + for (let c of clientList) { + if (c.url.endsWith('clients-get.https.html')) { + c.focus(); + return c; + } + } + return null; +} + +async function destroyResultingClient(testingPage) { + const destroyedPromise = new Promise(resolve => { + self.addEventListener('message', e => { + if (e.data.msg == 'resultingClientDestroyed') { + resolve(); + } + }, {once: true}); + }); + testingPage.postMessage({ msg: 'destroyResultingClient' }); + return destroyedPromise; +} + +self.addEventListener('fetch', async (e) => { + let { resultingClientId } = e; + savedResultingClientId = resultingClientId; + + if (e.request.url.endsWith('simple.html?fail')) { + e.waitUntil((async () => { + const testingPage = await getTestingPage(); + await destroyResultingClient(testingPage); + testingPage.postMessage({ msg: 'resultingClientDestroyedAck', + resultingDestroyedClientId: savedResultingClientId }); + })()); + return; + } + + e.respondWith(fetch(e.request)); +}); + +self.addEventListener('message', (e) => { + let { msg, resultingClientId } = e.data; + e.waitUntil((async () => { + if (msg == 'getIsResultingClientUndefined') { + const client = await self.clients.get(resultingClientId); + let isUndefined = typeof client == 'undefined'; + e.source.postMessage({ msg: 'getIsResultingClientUndefined', + isResultingClientUndefined: isUndefined }); + return; + } + if (msg == 'getResultingClientId') { + e.source.postMessage({ msg: 'getResultingClientId', + resultingClientId: savedResultingClientId }); + return; + } + })()); +}); diff --git a/test/wpt/tests/service-workers/service-worker/resources/clients-get-worker.js b/test/wpt/tests/service-workers/service-worker/resources/clients-get-worker.js new file mode 100644 index 00000000000..8effa56c98c --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/clients-get-worker.js @@ -0,0 +1,41 @@ +// This worker is designed to expose information about clients that is only available from Service Worker contexts. +// +// In the case of the `onfetch` handler, it provides the `clientId` property of +// the `event` object. In the case of the `onmessage` handler, it provides the +// Client instance attributes of the requested clients. +self.onfetch = function(e) { + if (/\/clientId$/.test(e.request.url)) { + e.respondWith(new Response(e.clientId)); + return; + } +}; + +self.onmessage = function(e) { + var client_ids = e.data.clientIds; + var message = []; + + e.waitUntil(Promise.all( + client_ids.map(function(client_id) { + return self.clients.get(client_id); + })) + .then(function(clients) { + // No matching client for a given id or a matched client is off-origin + // from the service worker. + if (clients.length == 1 && clients[0] == undefined) { + e.source.postMessage(clients[0]); + } else { + clients.forEach(function(client) { + if (client instanceof Client) { + message.push([client.visibilityState, + client.focused, + client.url, + client.type, + client.frameType]); + } else { + message.push(client); + } + }); + e.source.postMessage(message); + } + })); +}; diff --git a/test/wpt/tests/service-workers/service-worker/resources/clients-matchall-blob-url-worker.html b/test/wpt/tests/service-workers/service-worker/resources/clients-matchall-blob-url-worker.html new file mode 100644 index 00000000000..ee89a0d8b3e --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/clients-matchall-blob-url-worker.html @@ -0,0 +1,20 @@ + + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/clients-matchall-client-types-dedicated-worker.js b/test/wpt/tests/service-workers/service-worker/resources/clients-matchall-client-types-dedicated-worker.js new file mode 100644 index 00000000000..5a3f04d33aa --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/clients-matchall-client-types-dedicated-worker.js @@ -0,0 +1,3 @@ +onmessage = function(e) { + postMessage(e.data); +}; diff --git a/test/wpt/tests/service-workers/service-worker/resources/clients-matchall-client-types-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/clients-matchall-client-types-iframe.html new file mode 100644 index 00000000000..7607b035de3 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/clients-matchall-client-types-iframe.html @@ -0,0 +1,8 @@ + +Empty doc + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/clients-matchall-client-types-shared-worker.js b/test/wpt/tests/service-workers/service-worker/resources/clients-matchall-client-types-shared-worker.js new file mode 100644 index 00000000000..1ae72fb8944 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/clients-matchall-client-types-shared-worker.js @@ -0,0 +1,4 @@ +onconnect = function(e) { + var port = e.ports[0]; + port.postMessage('started'); +}; diff --git a/test/wpt/tests/service-workers/service-worker/resources/clients-matchall-on-evaluation-worker.js b/test/wpt/tests/service-workers/service-worker/resources/clients-matchall-on-evaluation-worker.js new file mode 100644 index 00000000000..f1559aca39b --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/clients-matchall-on-evaluation-worker.js @@ -0,0 +1,11 @@ +importScripts('test-helpers.sub.js'); + +var page_url = normalizeURL('../clients-matchall-on-evaluation.https.html'); + +self.clients.matchAll({includeUncontrolled: true}) + .then(function(clients) { + clients.forEach(function(client) { + if (client.url == page_url) + client.postMessage('matched'); + }); + }); diff --git a/test/wpt/tests/service-workers/service-worker/resources/clients-matchall-worker.js b/test/wpt/tests/service-workers/service-worker/resources/clients-matchall-worker.js new file mode 100644 index 00000000000..13e111a2f91 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/clients-matchall-worker.js @@ -0,0 +1,40 @@ +self.onmessage = function(e) { + var port = e.data.port; + var options = e.data.options; + + e.waitUntil(self.clients.matchAll(options) + .then(function(clients) { + var message = []; + clients.forEach(function(client) { + var frame_type = client.frameType; + if (client.url.indexOf('clients-matchall-include-uncontrolled.https.html') > -1 && + client.frameType == 'auxiliary') { + // The test tab might be opened using window.open() by the test framework. + // In that case, just pretend it's top-level! + frame_type = 'top-level'; + } + if (e.data.includeLifecycleState) { + message.push({visibilityState: client.visibilityState, + focused: client.focused, + url: client.url, + lifecycleState: client.lifecycleState, + type: client.type, + frameType: frame_type}); + } else { + message.push([client.visibilityState, + client.focused, + client.url, + client.type, + frame_type]); + } + }); + // Sort by url + if (!e.data.disableSort) { + message.sort(function(a, b) { return a[2] > b[2] ? 1 : -1; }); + } + port.postMessage(message); + }) + .catch(e => { + port.postMessage('clients.matchAll() rejected: ' + e); + })); +}; diff --git a/test/wpt/tests/service-workers/service-worker/resources/cors-approved.txt b/test/wpt/tests/service-workers/service-worker/resources/cors-approved.txt new file mode 100644 index 00000000000..1cd89bb14d6 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/cors-approved.txt @@ -0,0 +1 @@ +plaintext diff --git a/test/wpt/tests/service-workers/service-worker/resources/cors-approved.txt.headers b/test/wpt/tests/service-workers/service-worker/resources/cors-approved.txt.headers new file mode 100644 index 00000000000..f7985fd9bd5 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/cors-approved.txt.headers @@ -0,0 +1,3 @@ +Content-Type: text/plain +Access-Control-Allow-Origin: * + diff --git a/test/wpt/tests/service-workers/service-worker/resources/cors-denied.txt b/test/wpt/tests/service-workers/service-worker/resources/cors-denied.txt new file mode 100644 index 00000000000..ff333bd97da --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/cors-denied.txt @@ -0,0 +1,2 @@ +this file is served without Access-Control-Allow-Origin headers so it should not +be readable from cross-origin. diff --git a/test/wpt/tests/service-workers/service-worker/resources/create-blob-url-worker.js b/test/wpt/tests/service-workers/service-worker/resources/create-blob-url-worker.js new file mode 100644 index 00000000000..57e4882c24f --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/create-blob-url-worker.js @@ -0,0 +1,22 @@ +const childWorkerScript = ` + self.onmessage = async (e) => { + const response = await fetch(e.data); + const text = await response.text(); + self.postMessage(text); + }; +`; +const blob = new Blob([childWorkerScript], { type: 'text/javascript' }); +const blobUrl = URL.createObjectURL(blob); +const childWorker = new Worker(blobUrl); + +// When a message comes from the parent frame, sends a resource url to the child +// worker. +self.onmessage = (e) => { + childWorker.postMessage(e.data); +}; + +// When a message comes from the child worker, sends a content of fetch() to the +// parent frame. +childWorker.onmessage = (e) => { + self.postMessage(e.data); +}; diff --git a/test/wpt/tests/service-workers/service-worker/resources/create-out-of-scope-worker.html b/test/wpt/tests/service-workers/service-worker/resources/create-out-of-scope-worker.html new file mode 100644 index 00000000000..b51c4517509 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/create-out-of-scope-worker.html @@ -0,0 +1,19 @@ + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/echo-content.py b/test/wpt/tests/service-workers/service-worker/resources/echo-content.py new file mode 100644 index 00000000000..70ae4b60254 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/echo-content.py @@ -0,0 +1,16 @@ +# This is a copy of fetch/api/resources/echo-content.py since it's more +# convenient in this directory due to service worker's path restriction. +from wptserve.utils import isomorphic_encode + +def main(request, response): + + headers = [(b"X-Request-Method", isomorphic_encode(request.method)), + (b"X-Request-Content-Length", request.headers.get(b"Content-Length", b"NO")), + (b"X-Request-Content-Type", request.headers.get(b"Content-Type", b"NO")), + + # Avoid any kind of content sniffing on the response. + (b"Content-Type", b"text/plain")] + + content = request.body + + return headers, content diff --git a/test/wpt/tests/service-workers/service-worker/resources/echo-cookie-worker.py b/test/wpt/tests/service-workers/service-worker/resources/echo-cookie-worker.py new file mode 100644 index 00000000000..561f64a35ad --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/echo-cookie-worker.py @@ -0,0 +1,24 @@ +def main(request, response): + headers = [(b"Content-Type", b"text/javascript")] + + values = [] + for key in request.cookies: + for cookie in request.cookies.get_list(key): + values.append(b'"%s": "%s"' % (key, cookie.value)) + + # Update the counter to change the script body for every request to trigger + # update of the service worker. + key = request.GET[b'key'] + counter = request.server.stash.take(key) + if counter is None: + counter = 0 + counter += 1 + request.server.stash.put(key, counter) + + body = b""" +// %d +self.addEventListener('message', e => { + e.source.postMessage({%s}) +});""" % (counter, b','.join(values)) + + return headers, body diff --git a/test/wpt/tests/service-workers/service-worker/resources/echo-message-to-source-worker.js b/test/wpt/tests/service-workers/service-worker/resources/echo-message-to-source-worker.js new file mode 100644 index 00000000000..bbbd35fb4f6 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/echo-message-to-source-worker.js @@ -0,0 +1,3 @@ +addEventListener('message', evt => { + evt.source.postMessage(evt.data); +}); diff --git a/test/wpt/tests/service-workers/service-worker/resources/embed-and-object-are-not-intercepted-worker.js b/test/wpt/tests/service-workers/service-worker/resources/embed-and-object-are-not-intercepted-worker.js new file mode 100644 index 00000000000..ffcdb751288 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/embed-and-object-are-not-intercepted-worker.js @@ -0,0 +1,14 @@ +// This worker intercepts a request for EMBED/OBJECT and responds with a +// response that indicates that interception occurred. The tests expect +// that interception does not occur. +self.addEventListener('fetch', e => { + if (e.request.url.indexOf('embedded-content-from-server.html') != -1) { + e.respondWith(fetch('embedded-content-from-service-worker.html')); + return; + } + + if (e.request.url.indexOf('green.png') != -1) { + e.respondWith(Promise.reject('network error to show interception occurred')); + return; + } + }); diff --git a/test/wpt/tests/service-workers/service-worker/resources/embed-image-is-not-intercepted-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/embed-image-is-not-intercepted-iframe.html new file mode 100644 index 00000000000..7b8b257203a --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/embed-image-is-not-intercepted-iframe.html @@ -0,0 +1,21 @@ + + +iframe for embed-and-object-are-not-intercepted test + + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/embed-is-not-intercepted-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/embed-is-not-intercepted-iframe.html new file mode 100644 index 00000000000..39149915cc5 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/embed-is-not-intercepted-iframe.html @@ -0,0 +1,17 @@ + + +iframe for embed-and-object-are-not-intercepted test + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/embed-navigation-is-not-intercepted-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/embed-navigation-is-not-intercepted-iframe.html new file mode 100644 index 00000000000..5e86f67735f --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/embed-navigation-is-not-intercepted-iframe.html @@ -0,0 +1,23 @@ + + +iframe for embed-and-object-are-not-intercepted test + + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/embedded-content-from-server.html b/test/wpt/tests/service-workers/service-worker/resources/embedded-content-from-server.html new file mode 100644 index 00000000000..ff50a9c7526 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/embedded-content-from-server.html @@ -0,0 +1,6 @@ + + +embed for embed-and-object-are-not-intercepted test + diff --git a/test/wpt/tests/service-workers/service-worker/resources/embedded-content-from-service-worker.html b/test/wpt/tests/service-workers/service-worker/resources/embedded-content-from-service-worker.html new file mode 100644 index 00000000000..2e2b9236082 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/embedded-content-from-service-worker.html @@ -0,0 +1,7 @@ + + +embed for embed-and-object-are-not-intercepted test + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/empty-but-slow-worker.js b/test/wpt/tests/service-workers/service-worker/resources/empty-but-slow-worker.js new file mode 100644 index 00000000000..92abac7a384 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/empty-but-slow-worker.js @@ -0,0 +1,8 @@ +addEventListener('fetch', evt => { + if (evt.request.url.endsWith('slow')) { + // Performance.now() might be a bit better here, but Date.now() has + // better compat in workers right now. + let start = Date.now(); + while(Date.now() - start < 2000); + } +}); diff --git a/test/wpt/tests/service-workers/service-worker/resources/empty-worker.js b/test/wpt/tests/service-workers/service-worker/resources/empty-worker.js new file mode 100644 index 00000000000..49ceb2648a9 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/empty-worker.js @@ -0,0 +1 @@ +// Do nothing. diff --git a/test/wpt/tests/service-workers/service-worker/resources/empty.h2.js b/test/wpt/tests/service-workers/service-worker/resources/empty.h2.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/wpt/tests/service-workers/service-worker/resources/empty.html b/test/wpt/tests/service-workers/service-worker/resources/empty.html new file mode 100644 index 00000000000..6feb11946b8 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/empty.html @@ -0,0 +1,6 @@ + + + +hello world + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/empty.js b/test/wpt/tests/service-workers/service-worker/resources/empty.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/wpt/tests/service-workers/service-worker/resources/enable-client-message-queue.html b/test/wpt/tests/service-workers/service-worker/resources/enable-client-message-queue.html new file mode 100644 index 00000000000..512bd14bc67 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/enable-client-message-queue.html @@ -0,0 +1,39 @@ + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/end-to-end-worker.js b/test/wpt/tests/service-workers/service-worker/resources/end-to-end-worker.js new file mode 100644 index 00000000000..d45a50556a9 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/end-to-end-worker.js @@ -0,0 +1,7 @@ +onmessage = function(e) { + var message = e.data; + if (typeof message === 'object' && 'port' in message) { + var response = 'Ack for: ' + message.from; + message.port.postMessage(response); + } +}; diff --git a/test/wpt/tests/service-workers/service-worker/resources/events-worker.js b/test/wpt/tests/service-workers/service-worker/resources/events-worker.js new file mode 100644 index 00000000000..80a2188677b --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/events-worker.js @@ -0,0 +1,12 @@ +var eventsSeen = []; + +function handler(event) { eventsSeen.push(event.type); } + +['activate', 'install'].forEach(function(type) { + self.addEventListener(type, handler); + }); + +onmessage = function(e) { + var message = e.data; + message.port.postMessage({events: eventsSeen}); +}; diff --git a/test/wpt/tests/service-workers/service-worker/resources/extendable-event-async-waituntil.js b/test/wpt/tests/service-workers/service-worker/resources/extendable-event-async-waituntil.js new file mode 100644 index 00000000000..8a975b0d2e9 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/extendable-event-async-waituntil.js @@ -0,0 +1,210 @@ +// This worker calls waitUntil() and respondWith() asynchronously and +// reports back to the test whether they threw. +// +// These test cases are confusing. Bear in mind that the event is active +// (calling waitUntil() is allowed) if: +// * The pending promise count is not 0, or +// * The event dispatch flag is set. + +// Controlled by 'init'/'done' messages. +var resolveLockPromise; +var port; + +self.addEventListener('message', function(event) { + var waitPromise; + var resolveTestPromise; + + switch (event.data.step) { + case 'init': + event.waitUntil(new Promise((res) => { resolveLockPromise = res; })); + port = event.data.port; + break; + case 'done': + resolveLockPromise(); + break; + + // Throws because waitUntil() is called in a task after event dispatch + // finishes. + case 'no-current-extension-different-task': + async_task_waituntil(event).then(reportResultExpecting('InvalidStateError')); + break; + + // OK because waitUntil() is called in a microtask that runs after the + // event handler runs, while the event dispatch flag is still set. + case 'no-current-extension-different-microtask': + async_microtask_waituntil(event).then(reportResultExpecting('OK')); + break; + + // OK because the second waitUntil() is called while the first waitUntil() + // promise is still pending. + case 'current-extension-different-task': + event.waitUntil(new Promise((res) => { resolveTestPromise = res; })); + async_task_waituntil(event).then(reportResultExpecting('OK')).then(resolveTestPromise); + break; + + // OK because all promises involved resolve "immediately", so the second + // waitUntil() is called during the microtask checkpoint at the end of + // event dispatching, when the event dispatch flag is still set. + case 'during-event-dispatch-current-extension-expired-same-microtask-turn': + waitPromise = Promise.resolve(); + event.waitUntil(waitPromise); + waitPromise.then(() => { return sync_waituntil(event); }) + .then(reportResultExpecting('OK')) + break; + + // OK for the same reason as above. + case 'during-event-dispatch-current-extension-expired-same-microtask-turn-extra': + waitPromise = Promise.resolve(); + event.waitUntil(waitPromise); + waitPromise.then(() => { return async_microtask_waituntil(event); }) + .then(reportResultExpecting('OK')) + break; + + + // OK because the pending promise count is decremented in a microtask + // queued upon fulfillment of the first waitUntil() promise, so the second + // waitUntil() is called while the pending promise count is still + // positive. + case 'after-event-dispatch-current-extension-expired-same-microtask-turn': + waitPromise = makeNewTaskPromise(); + event.waitUntil(waitPromise); + waitPromise.then(() => { return sync_waituntil(event); }) + .then(reportResultExpecting('OK')) + break; + + // Throws because the second waitUntil() is called after the pending + // promise count was decremented to 0. + case 'after-event-dispatch-current-extension-expired-same-microtask-turn-extra': + waitPromise = makeNewTaskPromise(); + event.waitUntil(waitPromise); + waitPromise.then(() => { return async_microtask_waituntil(event); }) + .then(reportResultExpecting('InvalidStateError')) + break; + + // Throws because the second waitUntil() is called in a new task, after + // first waitUntil() promise settled and the event dispatch flag is unset. + case 'current-extension-expired-different-task': + event.waitUntil(Promise.resolve()); + async_task_waituntil(event).then(reportResultExpecting('InvalidStateError')); + break; + + case 'script-extendable-event': + self.dispatchEvent(new ExtendableEvent('nontrustedevent')); + break; + } + + event.source.postMessage('ACK'); + }); + +self.addEventListener('fetch', function(event) { + const path = new URL(event.request.url).pathname; + const step = path.substring(path.lastIndexOf('/') + 1); + let response; + switch (step) { + // OK because waitUntil() is called while the respondWith() promise is still + // unsettled, so the pending promise count is positive. + case 'pending-respondwith-async-waituntil': + var resolveFetch; + response = new Promise((res) => { resolveFetch = res; }); + event.respondWith(response); + async_task_waituntil(event) + .then(reportResultExpecting('OK')) + .then(() => { resolveFetch(new Response('OK')); }); + break; + + // OK because all promises involved resolve "immediately", so waitUntil() is + // called during the microtask checkpoint at the end of event dispatching, + // when the event dispatch flag is still set. + case 'during-event-dispatch-respondwith-microtask-sync-waituntil': + response = Promise.resolve(new Response('RESP')); + event.respondWith(response); + response.then(() => { return sync_waituntil(event); }) + .then(reportResultExpecting('OK')); + break; + + // OK because all promises involved resolve "immediately", so waitUntil() is + // called during the microtask checkpoint at the end of event dispatching, + // when the event dispatch flag is still set. + case 'during-event-dispatch-respondwith-microtask-async-waituntil': + response = Promise.resolve(new Response('RESP')); + event.respondWith(response); + response.then(() => { return async_microtask_waituntil(event); }) + .then(reportResultExpecting('OK')); + break; + + // OK because the pending promise count is decremented in a microtask queued + // upon fulfillment of the respondWith() promise, so waitUntil() is called + // while the pending promise count is still positive. + case 'after-event-dispatch-respondwith-microtask-sync-waituntil': + response = makeNewTaskPromise().then(() => {return new Response('RESP');}); + event.respondWith(response); + response.then(() => { return sync_waituntil(event); }) + .then(reportResultExpecting('OK')); + break; + + + // Throws because waitUntil() is called after the pending promise count was + // decremented to 0. + case 'after-event-dispatch-respondwith-microtask-async-waituntil': + response = makeNewTaskPromise().then(() => {return new Response('RESP');}); + event.respondWith(response); + response.then(() => { return async_microtask_waituntil(event); }) + .then(reportResultExpecting('InvalidStateError')) + break; + } +}); + +self.addEventListener('nontrustedevent', function(event) { + sync_waituntil(event).then(reportResultExpecting('InvalidStateError')); + }); + +function reportResultExpecting(expectedResult) { + return function (result) { + port.postMessage({result : result, expected: expectedResult}); + return result; + }; +} + +function sync_waituntil(event) { + return new Promise((res, rej) => { + try { + event.waitUntil(Promise.resolve()); + res('OK'); + } catch (error) { + res(error.name); + } + }); +} + +function async_microtask_waituntil(event) { + return new Promise((res, rej) => { + Promise.resolve().then(() => { + try { + event.waitUntil(Promise.resolve()); + res('OK'); + } catch (error) { + res(error.name); + } + }); + }); +} + +function async_task_waituntil(event) { + return new Promise((res, rej) => { + setTimeout(() => { + try { + event.waitUntil(Promise.resolve()); + res('OK'); + } catch (error) { + res(error.name); + } + }, 0); + }); +} + +// Returns a promise that settles in a separate task. +function makeNewTaskPromise() { + return new Promise(resolve => { + setTimeout(resolve, 0); + }); +} diff --git a/test/wpt/tests/service-workers/service-worker/resources/extendable-event-waituntil.js b/test/wpt/tests/service-workers/service-worker/resources/extendable-event-waituntil.js new file mode 100644 index 00000000000..20a9eb023f6 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/extendable-event-waituntil.js @@ -0,0 +1,87 @@ +var pendingPorts = []; +var portResolves = []; + +onmessage = function(e) { + var message = e.data; + if ('port' in message) { + var resolve = self.portResolves.shift(); + if (resolve) + resolve(message.port); + else + self.pendingPorts.push(message.port); + } +}; + +function fulfillPromise() { + return new Promise(function(resolve) { + // Make sure the oninstall/onactivate callback returns first. + Promise.resolve().then(function() { + var port = self.pendingPorts.shift(); + if (port) + resolve(port); + else + self.portResolves.push(resolve); + }); + }).then(function(port) { + port.postMessage('SYNC'); + return new Promise(function(resolve) { + port.onmessage = function(e) { + if (e.data == 'ACK') + resolve(); + }; + }); + }); +} + +function rejectPromise() { + return new Promise(function(resolve, reject) { + // Make sure the oninstall/onactivate callback returns first. + Promise.resolve().then(reject); + }); +} + +function stripScopeName(url) { + return url.split('/').slice(-1)[0]; +} + +oninstall = function(e) { + switch (stripScopeName(self.location.href)) { + case 'install-fulfilled': + e.waitUntil(fulfillPromise()); + break; + case 'install-rejected': + e.waitUntil(rejectPromise()); + break; + case 'install-multiple-fulfilled': + e.waitUntil(fulfillPromise()); + e.waitUntil(fulfillPromise()); + break; + case 'install-reject-precedence': + // Three "extend lifetime promises" are needed to verify that the user + // agent waits for all promises to settle even in the event of rejection. + // The first promise is fulfilled on demand by the client, the second is + // immediately scheduled for rejection, and the third is fulfilled on + // demand by the client (but only after the first promise has been + // fulfilled). + // + // User agents which simply expose `Promise.all` semantics in this case + // (by entering the "redundant state" following the rejection of the + // second promise but prior to the fulfillment of the third) can be + // identified from the client context. + e.waitUntil(fulfillPromise()); + e.waitUntil(rejectPromise()); + e.waitUntil(fulfillPromise()); + break; + } +}; + +onactivate = function(e) { + switch (stripScopeName(self.location.href)) { + case 'activate-fulfilled': + e.waitUntil(fulfillPromise()); + break; + case 'activate-rejected': + e.waitUntil(rejectPromise()); + break; + } +}; diff --git a/test/wpt/tests/service-workers/service-worker/resources/fail-on-fetch-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fail-on-fetch-worker.js new file mode 100644 index 00000000000..517f289fbc8 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fail-on-fetch-worker.js @@ -0,0 +1,5 @@ +importScripts('worker-testharness.js'); + +this.addEventListener('fetch', function(event) { + event.respondWith(new Response('ERROR')); + }); diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-access-control-login.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-access-control-login.html new file mode 100644 index 00000000000..ee296807ed1 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-access-control-login.html @@ -0,0 +1,16 @@ + diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-access-control.py b/test/wpt/tests/service-workers/service-worker/resources/fetch-access-control.py new file mode 100644 index 00000000000..446af87b249 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-access-control.py @@ -0,0 +1,109 @@ +import json +import os +from base64 import decodebytes + +from wptserve.utils import isomorphic_decode, isomorphic_encode + +def main(request, response): + headers = [] + headers.append((b'X-ServiceWorker-ServerHeader', b'SetInTheServer')) + + if b"ACAOrigin" in request.GET: + for item in request.GET[b"ACAOrigin"].split(b","): + headers.append((b"Access-Control-Allow-Origin", item)) + + for suffix in [b"Headers", b"Methods", b"Credentials"]: + query = b"ACA%s" % suffix + header = b"Access-Control-Allow-%s" % suffix + if query in request.GET: + headers.append((header, request.GET[query])) + + if b"ACEHeaders" in request.GET: + headers.append((b"Access-Control-Expose-Headers", request.GET[b"ACEHeaders"])) + + if (b"Auth" in request.GET and not request.auth.username) or b"AuthFail" in request.GET: + status = 401 + headers.append((b'WWW-Authenticate', b'Basic realm="Restricted"')) + body = b'Authentication canceled' + return status, headers, body + + if b"PNGIMAGE" in request.GET: + headers.append((b"Content-Type", b"image/png")) + body = decodebytes(b"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1B" + b"AACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAAhSURBVDhPY3wro/KfgQLABKXJBqMG" + b"jBoAAqMGDLwBDAwAEsoCTFWunmQAAAAASUVORK5CYII=") + return headers, body + + if b"VIDEO" in request.GET: + headers.append((b"Content-Type", b"video/ogg")) + body = open(os.path.join(request.doc_root, u"media", u"movie_5.ogv"), "rb").read() + length = len(body) + # If "PartialContent" is specified, the requestor wants to test range + # requests. For the initial request, respond with "206 Partial Content" + # and don't send the entire content. Then expect subsequent requests to + # have a "Range" header with a byte range. Respond with that range. + if b"PartialContent" in request.GET: + if length < 1: + return 500, headers, b"file is too small for range requests" + start = 0 + end = length - 1 + if b"Range" in request.headers: + range_header = request.headers[b"Range"] + prefix = b"bytes=" + split_header = range_header[len(prefix):].split(b"-") + # The first request might be "bytes=0-". We want to force a range + # request, so just return the first byte. + if split_header[0] == b"0" and split_header[1] == b"": + end = start + # Otherwise, it is a range request. Respect the values sent. + if split_header[0] != b"": + start = int(split_header[0]) + if split_header[1] != b"": + end = int(split_header[1]) + else: + # The request doesn't have a range. Force a range request by + # returning the first byte. + end = start + + headers.append((b"Accept-Ranges", b"bytes")) + headers.append((b"Content-Length", isomorphic_encode(str(end -start + 1)))) + headers.append((b"Content-Range", b"bytes %d-%d/%d" % (start, end, length))) + chunk = body[start:(end + 1)] + return 206, headers, chunk + return headers, body + + username = request.auth.username if request.auth.username else b"undefined" + password = request.auth.password if request.auth.username else b"undefined" + cookie = request.cookies[b'cookie'].value if b'cookie' in request.cookies else b"undefined" + + files = [] + for key, values in request.POST.items(): + assert len(values) == 1 + value = values[0] + if not hasattr(value, u"file"): + continue + data = value.file.read() + files.append({u"key": isomorphic_decode(key), + u"name": value.file.name, + u"type": value.type, + u"error": 0, #TODO, + u"size": len(data), + u"content": data}) + + get_data = {isomorphic_decode(key):isomorphic_decode(request.GET[key]) for key, value in request.GET.items()} + post_data = {isomorphic_decode(key):isomorphic_decode(request.POST[key]) for key, value in request.POST.items() + if not hasattr(request.POST[key], u"file")} + headers_data = {isomorphic_decode(key):isomorphic_decode(request.headers[key]) for key, value in request.headers.items()} + + data = {u"jsonpResult": u"success", + u"method": request.method, + u"headers": headers_data, + u"body": isomorphic_decode(request.body), + u"files": files, + u"GET": get_data, + u"POST": post_data, + u"username": isomorphic_decode(username), + u"password": isomorphic_decode(password), + u"cookie": isomorphic_decode(cookie)} + + return headers, u"report( %s )" % json.dumps(data) diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-canvas-tainting-double-write-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-canvas-tainting-double-write-worker.js new file mode 100644 index 00000000000..17723dcdda2 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-canvas-tainting-double-write-worker.js @@ -0,0 +1,7 @@ +self.addEventListener('fetch', (event) => { + url = new URL(event.request.url); + if (url.search == '?PNGIMAGE') { + localUrl = new URL(url.pathname + url.search, self.location); + event.respondWith(fetch(localUrl)); + } +}); diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-canvas-tainting-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-canvas-tainting-iframe.html new file mode 100644 index 00000000000..75d766c193a --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-canvas-tainting-iframe.html @@ -0,0 +1,70 @@ + +iframe for fetch canvas tainting test + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-canvas-tainting-tests.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-canvas-tainting-tests.js new file mode 100644 index 00000000000..2aada3669ef --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-canvas-tainting-tests.js @@ -0,0 +1,241 @@ +// This is the main driver of the canvas tainting tests. +const NOT_TAINTED = 'NOT_TAINTED'; +const TAINTED = 'TAINTED'; +const LOAD_ERROR = 'LOAD_ERROR'; + +let frame; + +// Creates a single promise_test. +function canvas_taint_test(url, cross_origin, expected_result) { + promise_test(t => { + return frame.contentWindow.create_test_case_promise(url, cross_origin) + .then(result => { + assert_equals(result, expected_result); + }); + }, 'url "' + url + '" with crossOrigin "' + cross_origin + '" should be ' + + expected_result); +} + + +// Runs all the tests. The given |params| has these properties: +// * |resource_path|: the relative path to the (image/video) resource to test. +// * |cache|: when true, the service worker bounces responses into +// Cache Storage and back out before responding with them. +function do_canvas_tainting_tests(params) { + const host_info = get_host_info(); + let resource_path = params.resource_path; + if (params.cache) + resource_path += "&cache=true"; + const resource_url = host_info['HTTPS_ORIGIN'] + resource_path; + const remote_resource_url = host_info['HTTPS_REMOTE_ORIGIN'] + resource_path; + + // Set up the service worker and the frame. + promise_test(function(t) { + const SCOPE = 'resources/fetch-canvas-tainting-iframe.html'; + const SCRIPT = 'resources/fetch-rewrite-worker.js'; + const host_info = get_host_info(); + + // login_https() is needed because some test cases use credentials. + return login_https(t) + .then(function() { + return service_worker_unregister_and_register(t, SCRIPT, SCOPE); + }) + .then(function(registration) { + promise_test(() => { + if (frame) + frame.remove(); + return registration.unregister(); + }, 'restore global state'); + + return wait_for_state(t, registration.installing, 'activated'); + }) + .then(function() { return with_iframe(SCOPE); }) + .then(f => { + frame = f; + }); + }, 'initialize global state'); + + // Reject tests. Add '&reject' so the service worker responds with a rejected promise. + // A load error is expected. + canvas_taint_test(resource_url + '&reject', '', LOAD_ERROR); + canvas_taint_test(resource_url + '&reject', 'anonymous', LOAD_ERROR); + canvas_taint_test(resource_url + '&reject', 'use-credentials', LOAD_ERROR); + + // Fallback tests. Add '&ignore' so the service worker does not respond to the fetch + // request, and we fall back to network. + canvas_taint_test(resource_url + '&ignore', '', NOT_TAINTED); + canvas_taint_test(remote_resource_url + '&ignore', '', TAINTED); + canvas_taint_test(remote_resource_url + '&ignore', 'anonymous', LOAD_ERROR); + canvas_taint_test( + remote_resource_url + '&ACAOrigin=' + host_info['HTTPS_ORIGIN'] + + '&ignore', + 'anonymous', + NOT_TAINTED); + canvas_taint_test(remote_resource_url + '&ignore', 'use-credentials', LOAD_ERROR); + canvas_taint_test( + remote_resource_url + '&ACAOrigin=' + host_info['HTTPS_ORIGIN'] + + '&ignore', + 'use-credentials', + LOAD_ERROR); + canvas_taint_test( + remote_resource_url + '&ACAOrigin=' + host_info['HTTPS_ORIGIN'] + + '&ACACredentials=true&ignore', + 'use-credentials', + NOT_TAINTED); + + // Credential tests (with fallback). Add '&Auth' so the server requires authentication. + // Furthermore, add '&ignore' so the service worker falls back to network. + canvas_taint_test(resource_url + '&Auth&ignore', '', NOT_TAINTED); + canvas_taint_test(remote_resource_url + '&Auth&ignore', '', TAINTED); + canvas_taint_test( + remote_resource_url + '&Auth&ignore', 'anonymous', LOAD_ERROR); + canvas_taint_test( + remote_resource_url + '&Auth&ignore', + 'use-credentials', + LOAD_ERROR); + canvas_taint_test( + remote_resource_url + '&Auth&ACAOrigin=' + host_info['HTTPS_ORIGIN'] + + '&ignore', + 'use-credentials', + LOAD_ERROR); + canvas_taint_test( + remote_resource_url + '&Auth&ACAOrigin=' + host_info['HTTPS_ORIGIN'] + + '&ACACredentials=true&ignore', + 'use-credentials', + NOT_TAINTED); + + // In the following tests, the service worker provides a response. + // Add '&url' so the service worker responds with fetch(url). + // Add '&mode' to configure the fetch request options. + + // Basic response tests. Set &url to the original url. + canvas_taint_test( + resource_url + '&mode=same-origin&url=' + encodeURIComponent(resource_url), + '', + NOT_TAINTED); + canvas_taint_test( + resource_url + '&mode=same-origin&url=' + encodeURIComponent(resource_url), + 'anonymous', + NOT_TAINTED); + canvas_taint_test( + resource_url + '&mode=same-origin&url=' + encodeURIComponent(resource_url), + 'use-credentials', + NOT_TAINTED); + canvas_taint_test( + remote_resource_url + '&mode=same-origin&url=' + + encodeURIComponent(resource_url), + '', + NOT_TAINTED); + canvas_taint_test( + remote_resource_url + '&mode=same-origin&url=' + + encodeURIComponent(resource_url), + 'anonymous', + NOT_TAINTED); + canvas_taint_test( + remote_resource_url + '&mode=same-origin&url=' + + encodeURIComponent(resource_url), + 'use-credentials', + NOT_TAINTED); + + // Opaque response tests. Set &url to the cross-origin URL, and &mode to + // 'no-cors' so we expect an opaque response. + canvas_taint_test( + resource_url + + '&mode=no-cors&url=' + encodeURIComponent(remote_resource_url), + '', + TAINTED); + canvas_taint_test( + resource_url + + '&mode=no-cors&url=' + encodeURIComponent(remote_resource_url), + 'anonymous', + LOAD_ERROR); + canvas_taint_test( + resource_url + + '&mode=no-cors&url=' + encodeURIComponent(remote_resource_url), + 'use-credentials', + LOAD_ERROR); + canvas_taint_test( + remote_resource_url + + '&mode=no-cors&url=' + encodeURIComponent(remote_resource_url), + '', + TAINTED); + canvas_taint_test( + remote_resource_url + + '&mode=no-cors&url=' + encodeURIComponent(remote_resource_url), + 'anonymous', + LOAD_ERROR); + canvas_taint_test( + remote_resource_url + + '&mode=no-cors&url=' + encodeURIComponent(remote_resource_url), + 'use-credentials', + LOAD_ERROR); + + // CORS response tests. Set &url to the cross-origin URL, and &mode + // to 'cors' to attempt a CORS request. + canvas_taint_test( + resource_url + '&mode=cors&url=' + + encodeURIComponent(remote_resource_url + + '&ACAOrigin=' + host_info['HTTPS_ORIGIN']), + '', + LOAD_ERROR); // We expect LOAD_ERROR since the server doesn't respond + // with an Access-Control-Allow-Credentials header. + canvas_taint_test( + resource_url + '&mode=cors&credentials=same-origin&url=' + + encodeURIComponent(remote_resource_url + + '&ACAOrigin=' + host_info['HTTPS_ORIGIN']), + '', + NOT_TAINTED); + canvas_taint_test( + resource_url + '&mode=cors&url=' + + encodeURIComponent(remote_resource_url + + '&ACAOrigin=' + host_info['HTTPS_ORIGIN']), + 'anonymous', + NOT_TAINTED); + canvas_taint_test( + resource_url + '&mode=cors&url=' + + encodeURIComponent(remote_resource_url + + '&ACAOrigin=' + host_info['HTTPS_ORIGIN']), + 'use-credentials', + LOAD_ERROR); // We expect LOAD_ERROR since the server doesn't respond + // with an Access-Control-Allow-Credentials header. + canvas_taint_test( + resource_url + '&mode=cors&url=' + + encodeURIComponent( + remote_resource_url + + '&ACACredentials=true&ACAOrigin=' + host_info['HTTPS_ORIGIN']), + 'use-credentials', + NOT_TAINTED); + canvas_taint_test( + remote_resource_url + '&mode=cors&url=' + + encodeURIComponent(remote_resource_url + + '&ACAOrigin=' + host_info['HTTPS_ORIGIN']), + '', + LOAD_ERROR); // We expect LOAD_ERROR since the server doesn't respond + // with an Access-Control-Allow-Credentials header. + canvas_taint_test( + remote_resource_url + '&mode=cors&credentials=same-origin&url=' + + encodeURIComponent(remote_resource_url + + '&ACAOrigin=' + host_info['HTTPS_ORIGIN']), + '', + NOT_TAINTED); + canvas_taint_test( + remote_resource_url + '&mode=cors&url=' + + encodeURIComponent(remote_resource_url + + '&ACAOrigin=' + host_info['HTTPS_ORIGIN']), + 'anonymous', + NOT_TAINTED); + canvas_taint_test( + remote_resource_url + '&mode=cors&url=' + + encodeURIComponent(remote_resource_url + + '&ACAOrigin=' + host_info['HTTPS_ORIGIN']), + 'use-credentials', + LOAD_ERROR); // We expect LOAD_ERROR since the server doesn't respond + // with an Access-Control-Allow-Credentials header. + canvas_taint_test( + remote_resource_url + '&mode=cors&url=' + + encodeURIComponent( + remote_resource_url + + '&ACACredentials=true&ACAOrigin=' + host_info['HTTPS_ORIGIN']), + 'use-credentials', + NOT_TAINTED); +} diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-cors-exposed-header-names-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-cors-exposed-header-names-worker.js new file mode 100644 index 00000000000..145952a22cf --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-cors-exposed-header-names-worker.js @@ -0,0 +1,3 @@ +self.addEventListener('fetch', (e) => { + e.respondWith(fetch(e.request)); + }); diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-cors-xhr-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-cors-xhr-iframe.html new file mode 100644 index 00000000000..d88c5103d35 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-cors-xhr-iframe.html @@ -0,0 +1,170 @@ + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-csp-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-csp-iframe.html new file mode 100644 index 00000000000..33bf0416d58 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-csp-iframe.html @@ -0,0 +1,16 @@ + diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-csp-iframe.html.sub.headers b/test/wpt/tests/service-workers/service-worker/resources/fetch-csp-iframe.html.sub.headers new file mode 100644 index 00000000000..5a1c7b941ac --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-csp-iframe.html.sub.headers @@ -0,0 +1 @@ +Content-Security-Policy: img-src https://{{host}}:{{ports[https][0]}}; connect-src 'unsafe-inline' 'self' diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-error-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-error-worker.js new file mode 100644 index 00000000000..788252cf3b3 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-error-worker.js @@ -0,0 +1,22 @@ +importScripts("/resources/testharness.js"); + +function doTest(event) +{ + if (!event.request.url.includes("fetch-error-test")) + return; + + let counter = 0; + const stream = new ReadableStream({ pull: controller => { + switch (++counter) { + case 1: + controller.enqueue(new Uint8Array([1])); + return; + default: + // We asynchronously error the stream so that there is ample time to resolve the fetch promise and call text() on the response. + step_timeout(() => controller.error("Sorry"), 50); + } + }}); + event.respondWith(new Response(stream)); +} + +self.addEventListener("fetch", doTest); diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-event-add-async-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-add-async-worker.js new file mode 100644 index 00000000000..a5a44a57c99 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-add-async-worker.js @@ -0,0 +1,6 @@ +importScripts('/resources/testharness.js'); + +promise_test(async () => { + await new Promise(handler => { step_timeout(handler, 0); }); + self.addEventListener('fetch', () => {}); +}, 'fetch event added asynchronously does not throw'); diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-event-after-navigation-within-page-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-after-navigation-within-page-iframe.html new file mode 100644 index 00000000000..bf8a6d5ce51 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-after-navigation-within-page-iframe.html @@ -0,0 +1,22 @@ + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-event-async-respond-with-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-async-respond-with-worker.js new file mode 100644 index 00000000000..dc3f1a1e985 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-async-respond-with-worker.js @@ -0,0 +1,66 @@ +// This worker attempts to call respondWith() asynchronously after the +// fetch event handler finished. It reports back to the test whether +// an exception was thrown. + +// These get reset at the start of a test case. +let reportResult; + +// The test page sends a message to tell us that a new test case is starting. +// We expect a fetch event after this. +self.addEventListener('message', (event) => { + // Ensure tests run mutually exclusive. + if (reportResult) { + event.source.postMessage('testAlreadyRunning'); + return; + } + + const resultPromise = new Promise((resolve) => { + reportResult = resolve; + // Tell the client that everything is initialized and that it's safe to + // proceed with the test without relying on the order of events (which some + // browsers like Chrome may not guarantee). + event.source.postMessage('messageHandlerInitialized'); + }); + + // Keep the worker alive until the test case finishes, and report + // back the result to the test page. + event.waitUntil(resultPromise.then(result => { + reportResult = null; + event.source.postMessage(result); + })); +}); + +// Calls respondWith() and reports back whether an exception occurred. +function tryRespondWith(event) { + try { + event.respondWith(new Response()); + reportResult({didThrow: false}); + } catch (error) { + reportResult({didThrow: true, error: error.name}); + } +} + +function respondWithInTask(event) { + setTimeout(() => { + tryRespondWith(event); + }, 0); +} + +function respondWithInMicrotask(event) { + Promise.resolve().then(() => { + tryRespondWith(event); + }); +} + +self.addEventListener('fetch', function(event) { + const path = new URL(event.request.url).pathname; + const test = path.substring(path.lastIndexOf('/') + 1); + + // If this is a test case, try respondWith() and report back to the test page + // the result. + if (test == 'respondWith-in-task') { + respondWithInTask(event); + } else if (test == 'respondWith-in-microtask') { + respondWithInMicrotask(event); + } +}); diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-event-handled-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-handled-worker.js new file mode 100644 index 00000000000..53ee1493743 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-handled-worker.js @@ -0,0 +1,37 @@ +// This worker reports back the final state of FetchEvent.handled (RESOLVED or +// REJECTED) to the test. + +self.addEventListener('message', function(event) { + self.port = event.data.port; +}); + +self.addEventListener('fetch', function(event) { + try { + event.handled.then(() => { + self.port.postMessage('RESOLVED'); + }, () => { + self.port.postMessage('REJECTED'); + }); + } catch (e) { + self.port.postMessage('FAILED'); + return; + } + + const search = new URL(event.request.url).search; + switch (search) { + case '?respondWith-not-called': + break; + case '?respondWith-not-called-and-event-canceled': + event.preventDefault(); + break; + case '?respondWith-called-and-promise-resolved': + event.respondWith(Promise.resolve(new Response('body'))); + break; + case '?respondWith-called-and-promise-resolved-to-invalid-response': + event.respondWith(Promise.resolve('invalid response')); + break; + case '?respondWith-called-and-promise-rejected': + event.respondWith(Promise.reject(new Error('respondWith rejected'))); + break; + } +}); diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-event-network-error-controllee-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-network-error-controllee-iframe.html new file mode 100644 index 00000000000..f6c1919bbcf --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-network-error-controllee-iframe.html @@ -0,0 +1,60 @@ + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-event-network-error-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-network-error-worker.js new file mode 100644 index 00000000000..5bfe3a0bbd9 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-network-error-worker.js @@ -0,0 +1,49 @@ +// Test that multiple fetch handlers do not confuse the implementation. +self.addEventListener('fetch', function(event) {}); + +self.addEventListener('fetch', function(event) { + var testcase = new URL(event.request.url).search; + switch (testcase) { + case '?reject': + event.respondWith(Promise.reject()); + break; + case '?prevent-default': + event.preventDefault(); + break; + case '?prevent-default-and-respond-with': + event.preventDefault(); + break; + case '?unused-body': + event.respondWith(new Response('body')); + break; + case '?used-body': + var res = new Response('body'); + res.text(); + event.respondWith(res); + break; + case '?unused-fetched-body': + event.respondWith(fetch('other.html').then(function(res){ + return res; + })); + break; + case '?used-fetched-body': + event.respondWith(fetch('other.html').then(function(res){ + res.text(); + return res; + })); + break; + case '?throw-exception': + throw('boom'); + break; + } + }); + +self.addEventListener('fetch', function(event) {}); + +self.addEventListener('fetch', function(event) { + var testcase = new URL(event.request.url).search; + if (testcase == '?prevent-default-and-respond-with') + event.respondWith(new Response('responding!')); + }); + +self.addEventListener('fetch', function(event) {}); diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-event-network-fallback-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-network-fallback-worker.js new file mode 100644 index 00000000000..376bdbed05e --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-network-fallback-worker.js @@ -0,0 +1,3 @@ +self.addEventListener('fetch', () => { + // Do nothing. +}); diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-argument-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-argument-iframe.html new file mode 100644 index 00000000000..0ebd1ca8153 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-argument-iframe.html @@ -0,0 +1,55 @@ + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-argument-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-argument-worker.js new file mode 100644 index 00000000000..712c4b73c9b --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-argument-worker.js @@ -0,0 +1,14 @@ +self.addEventListener('fetch', function(event) { + var testcase = new URL(event.request.url).search; + switch (testcase) { + case '?response-object': + event.respondWith(new Response('body')); + break; + case '?response-promise-object': + event.respondWith(Promise.resolve(new Response('body'))); + break; + case '?other-value': + event.respondWith(new Object()); + break; + } + }); diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-body-loaded-in-chunk-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-body-loaded-in-chunk-worker.js new file mode 100644 index 00000000000..d3ba8a8df2e --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-body-loaded-in-chunk-worker.js @@ -0,0 +1,7 @@ +'use strict'; + +self.addEventListener('fetch', event => { + if (!event.request.url.match(/body-in-chunk$/)) + return; + event.respondWith(fetch("../../../fetch/api/resources/trickle.py?count=4&delay=50")); +}); diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-custom-response-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-custom-response-worker.js new file mode 100644 index 00000000000..ff24aed1282 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-custom-response-worker.js @@ -0,0 +1,45 @@ +'use strict'; + +addEventListener('fetch', event => { + const url = new URL(event.request.url); + const type = url.searchParams.get('type'); + + if (!type) return; + + if (type === 'string') { + event.respondWith(new Response('PASS')); + } + else if (type === 'blob') { + event.respondWith( + new Response(new Blob(['PASS'])) + ); + } + else if (type === 'buffer-view') { + const encoder = new TextEncoder(); + event.respondWith( + new Response(encoder.encode('PASS')) + ); + } + else if (type === 'buffer') { + const encoder = new TextEncoder(); + event.respondWith( + new Response(encoder.encode('PASS').buffer) + ); + } + else if (type === 'form-data') { + const body = new FormData(); + body.set('result', 'PASS'); + event.respondWith( + new Response(body) + ); + } + else if (type === 'search-params') { + const body = new URLSearchParams(); + body.set('result', 'PASS'); + event.respondWith( + new Response(body, { + headers: { 'Content-Type': 'text/plain' } + }) + ); + } +}); diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-partial-stream-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-partial-stream-worker.js new file mode 100644 index 00000000000..b7307f29f57 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-partial-stream-worker.js @@ -0,0 +1,28 @@ +let waitUntilResolve; + +let bodyController; + +self.addEventListener('message', evt => { + if (evt.data === 'done') { + bodyController.close(); + waitUntilResolve(); + } +}); + +self.addEventListener('fetch', evt => { + if (!evt.request.url.includes('partial-stream.txt')) { + return; + } + + evt.waitUntil(new Promise(resolve => waitUntilResolve = resolve)); + + let body = new ReadableStream({ + start: controller => { + let encoder = new TextEncoder(); + controller.enqueue(encoder.encode('partial-stream-content')); + bodyController = controller; + }, + }); + + evt.respondWith(new Response(body)); +}); diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-readable-stream-chunk-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-readable-stream-chunk-worker.js new file mode 100644 index 00000000000..f954e3a18a5 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-readable-stream-chunk-worker.js @@ -0,0 +1,40 @@ +'use strict'; + +self.addEventListener('fetch', event => { + if (!event.request.url.match(/body-stream$/)) + return; + + var counter = 0; + const encoder = new TextEncoder(); + const stream = new ReadableStream({ pull: controller => { + switch (++counter) { + case 1: + controller.enqueue(encoder.encode('')); + return; + case 2: + controller.enqueue(encoder.encode('chunk #1')); + return; + case 3: + controller.enqueue(encoder.encode(' ')); + return; + case 4: + controller.enqueue(encoder.encode('chunk #2')); + return; + case 5: + controller.enqueue(encoder.encode(' ')); + return; + case 6: + controller.enqueue(encoder.encode('chunk #3')); + return; + case 7: + controller.enqueue(encoder.encode(' ')); + return; + case 8: + controller.enqueue(encoder.encode('chunk #4')); + return; + default: + controller.close(); + } + }}); + event.respondWith(new Response(stream)); +}); diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-readable-stream-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-readable-stream-worker.js new file mode 100644 index 00000000000..056351322c6 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-readable-stream-worker.js @@ -0,0 +1,81 @@ +'use strict'; +importScripts("/resources/testharness.js"); + +const map = new Map(); + +self.addEventListener('fetch', event => { + const url = new URL(event.request.url); + if (!url.searchParams.has('stream')) return; + + if (url.searchParams.has('observe-cancel')) { + const id = url.searchParams.get('id'); + if (id === undefined) { + event.respondWith(new Error('error')); + return; + } + event.waitUntil(new Promise(resolve => { + map.set(id, {label: 'pending', resolve}); + })); + + const stream = new ReadableStream({ + pull(c) { + if (url.searchParams.get('enqueue') === 'true') { + url.searchParams.delete('enqueue'); + c.enqueue(new Uint8Array([65])); + } + }, + cancel() { + map.get(id).label = 'cancelled'; + } + }); + event.respondWith(new Response(stream)); + return; + } + + if (url.searchParams.has('query-cancel')) { + const id = url.searchParams.get('id'); + if (id === undefined) { + event.respondWith(new Error('error')); + return; + } + const entry = map.get(id); + if (entry === undefined) { + event.respondWith(new Error('not found')); + return; + } + map.delete(id); + entry.resolve(); + event.respondWith(new Response(entry.label)); + return; + } + + if (url.searchParams.has('use-fetch-stream')) { + event.respondWith(async function() { + const response = await fetch('pass.txt'); + return new Response(response.body); + }()); + return; + } + + const delayEnqueue = url.searchParams.has('delay'); + + const stream = new ReadableStream({ + start(controller) { + const encoder = new TextEncoder(); + + const populate = () => { + controller.enqueue(encoder.encode('PASS')); + controller.close(); + } + + if (delayEnqueue) { + step_timeout(populate, 16); + } + else { + populate(); + } + } + }); + + event.respondWith(new Response(stream)); +}); diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-response-body-with-invalid-chunk-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-response-body-with-invalid-chunk-iframe.html new file mode 100644 index 00000000000..d15454daa5b --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-response-body-with-invalid-chunk-iframe.html @@ -0,0 +1,15 @@ + + +respond-with-response-body-with-invalid-chunk + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-response-body-with-invalid-chunk-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-response-body-with-invalid-chunk-worker.js new file mode 100644 index 00000000000..0254e24f94a --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-response-body-with-invalid-chunk-worker.js @@ -0,0 +1,12 @@ +'use strict'; + +self.addEventListener('fetch', event => { + if (!event.request.url.match(/body-stream-with-invalid-chunk$/)) + return; + const stream = new ReadableStream({start: controller => { + // The argument is intentionally a string, not a Uint8Array. + controller.enqueue('hello'); + }}); + const headers = { 'x-content-type-options': 'nosniff' }; + event.respondWith(new Response(stream, { headers })); + }); diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-stops-propagation-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-stops-propagation-worker.js new file mode 100644 index 00000000000..18da049d69f --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-respond-with-stops-propagation-worker.js @@ -0,0 +1,15 @@ +var result = null; + +self.addEventListener('message', function(event) { + event.data.port.postMessage(result); + }); + +self.addEventListener('fetch', function(event) { + if (!result) + result = 'PASS'; + event.respondWith(new Response()); + }); + +self.addEventListener('fetch', function(event) { + result = 'FAIL: fetch event propagated'; + }); diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-event-test-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-test-worker.js new file mode 100644 index 00000000000..813f79d1b07 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-test-worker.js @@ -0,0 +1,224 @@ +function handleHeaders(event) { + const headers = Array.from(event.request.headers); + event.respondWith(new Response(JSON.stringify(headers))); +} + +function handleString(event) { + event.respondWith(new Response('Test string')); +} + +function handleBlob(event) { + event.respondWith(new Response(new Blob(['Test blob']))); +} + +function handleReferrer(event) { + event.respondWith(new Response(new Blob( + ['Referrer: ' + event.request.referrer]))); +} + +function handleReferrerPolicy(event) { + event.respondWith(new Response(new Blob( + ['ReferrerPolicy: ' + event.request.referrerPolicy]))); +} + +function handleReferrerFull(event) { + event.respondWith(new Response(new Blob( + ['Referrer: ' + event.request.referrer + '\n' + + 'ReferrerPolicy: ' + event.request.referrerPolicy]))); +} + +function handleClientId(event) { + var body; + if (event.clientId !== "") { + body = 'Client ID Found: ' + event.clientId; + } else { + body = 'Client ID Not Found'; + } + event.respondWith(new Response(body)); +} + +function handleResultingClientId(event) { + var body; + if (event.resultingClientId !== "") { + body = 'Resulting Client ID Found: ' + event.resultingClientId; + } else { + body = 'Resulting Client ID Not Found'; + } + event.respondWith(new Response(body)); +} + +function handleNullBody(event) { + event.respondWith(new Response()); +} + +function handleFetch(event) { + event.respondWith(fetch('other.html')); +} + +function handleFormPost(event) { + event.respondWith(new Promise(function(resolve) { + event.request.text() + .then(function(result) { + resolve(new Response(event.request.method + ':' + + event.request.headers.get('Content-Type') + ':' + + result)); + }); + })); +} + +function handleMultipleRespondWith(event) { + var logForMultipleRespondWith = ''; + for (var i = 0; i < 3; ++i) { + logForMultipleRespondWith += '(' + i + ')'; + try { + event.respondWith(new Promise(function(resolve) { + setTimeout(function() { + resolve(new Response(logForMultipleRespondWith)); + }, 0); + })); + } catch (e) { + logForMultipleRespondWith += '[' + e.name + ']'; + } + } +} + +var lastResponseForUsedCheck = undefined; + +function handleUsedCheck(event) { + if (!lastResponseForUsedCheck) { + event.respondWith(fetch('other.html').then(function(response) { + lastResponseForUsedCheck = response; + return response; + })); + } else { + event.respondWith(new Response( + 'bodyUsed: ' + lastResponseForUsedCheck.bodyUsed)); + } +} +function handleFragmentCheck(event) { + var body; + if (event.request.url.indexOf('#') === -1) { + body = 'Fragment Not Found'; + } else { + body = 'Fragment Found :' + + event.request.url.substring(event.request.url.indexOf('#')); + } + event.respondWith(new Response(body)); +} +function handleCache(event) { + event.respondWith(new Response(event.request.cache)); +} +function handleEventSource(event) { + if (event.request.mode === 'navigate') { + return; + } + var data = { + mode: event.request.mode, + cache: event.request.cache, + credentials: event.request.credentials + }; + var body = 'data:' + JSON.stringify(data) + '\n\n'; + event.respondWith(new Response(body, { + headers: { 'Content-Type': 'text/event-stream' } + } + )); +} + +function handleIntegrity(event) { + event.respondWith(new Response(event.request.integrity)); +} + +function handleRequestBody(event) { + event.respondWith(event.request.text().then(text => { + return new Response(text); + })); +} + +function handleKeepalive(event) { + event.respondWith(new Response(event.request.keepalive)); +} + +function handleIsReloadNavigation(event) { + const request = event.request; + const body = + `method = ${request.method}, ` + + `isReloadNavigation = ${request.isReloadNavigation}`; + event.respondWith(new Response(body)); +} + +function handleIsHistoryNavigation(event) { + const request = event.request; + const body = + `method = ${request.method}, ` + + `isHistoryNavigation = ${request.isHistoryNavigation}`; + event.respondWith(new Response(body)); +} + +function handleUseAndIgnore(event) { + const request = event.request; + request.text(); + return; +} + +function handleCloneAndIgnore(event) { + const request = event.request; + request.clone().text(); + return; +} + +var handle_status_count = 0; +function handleStatus(event) { + handle_status_count++; + event.respondWith(async function() { + const res = await fetch(event.request); + const text = await res.text(); + return new Response(`${text}. Request was sent ${handle_status_count} times.`, + {"status": new URL(event.request.url).searchParams.get("status")}); + }()); +} + +self.addEventListener('fetch', function(event) { + var url = event.request.url; + var handlers = [ + { pattern: '?headers', fn: handleHeaders }, + { pattern: '?string', fn: handleString }, + { pattern: '?blob', fn: handleBlob }, + { pattern: '?referrerFull', fn: handleReferrerFull }, + { pattern: '?referrerPolicy', fn: handleReferrerPolicy }, + { pattern: '?referrer', fn: handleReferrer }, + { pattern: '?clientId', fn: handleClientId }, + { pattern: '?resultingClientId', fn: handleResultingClientId }, + { pattern: '?ignore', fn: function() {} }, + { pattern: '?null', fn: handleNullBody }, + { pattern: '?fetch', fn: handleFetch }, + { pattern: '?form-post', fn: handleFormPost }, + { pattern: '?multiple-respond-with', fn: handleMultipleRespondWith }, + { pattern: '?used-check', fn: handleUsedCheck }, + { pattern: '?fragment-check', fn: handleFragmentCheck }, + { pattern: '?cache', fn: handleCache }, + { pattern: '?eventsource', fn: handleEventSource }, + { pattern: '?integrity', fn: handleIntegrity }, + { pattern: '?request-body', fn: handleRequestBody }, + { pattern: '?keepalive', fn: handleKeepalive }, + { pattern: '?isReloadNavigation', fn: handleIsReloadNavigation }, + { pattern: '?isHistoryNavigation', fn: handleIsHistoryNavigation }, + { pattern: '?use-and-ignore', fn: handleUseAndIgnore }, + { pattern: '?clone-and-ignore', fn: handleCloneAndIgnore }, + { pattern: '?status', fn: handleStatus }, + ]; + + var handler = null; + for (var i = 0; i < handlers.length; ++i) { + if (url.indexOf(handlers[i].pattern) != -1) { + handler = handlers[i]; + break; + } + } + + if (handler) { + handler.fn(event); + } else { + event.respondWith(new Response(new Blob( + ['Service Worker got an unexpected request: ' + url]))); + } + }); diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-event-within-sw-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-within-sw-worker.js new file mode 100644 index 00000000000..5903bab968d --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-event-within-sw-worker.js @@ -0,0 +1,48 @@ +skipWaiting(); + +addEventListener('fetch', event => { + const url = new URL(event.request.url); + + if (url.origin != location.origin) return; + + if (url.pathname.endsWith('/sample.txt')) { + event.respondWith(new Response('intercepted')); + return; + } + + if (url.pathname.endsWith('/sample.txt-inner-fetch')) { + event.respondWith(fetch('sample.txt')); + return; + } + + if (url.pathname.endsWith('/sample.txt-inner-cache')) { + event.respondWith( + caches.open('test-inner-cache').then(cache => + cache.add('sample.txt').then(() => cache.match('sample.txt')) + ) + ); + return; + } + + if (url.pathname.endsWith('/show-notification')) { + // Copy the currect search string onto the icon url + const iconURL = new URL('notification_icon.py', location); + iconURL.search = url.search; + + event.respondWith( + registration.showNotification('test', { + icon: iconURL + }).then(() => registration.getNotifications()).then(notifications => { + for (const n of notifications) n.close(); + return new Response('done'); + }) + ); + return; + } + + if (url.pathname.endsWith('/notification_icon.py')) { + new BroadcastChannel('icon-request').postMessage('yay'); + event.respondWith(new Response('done')); + return; + } +}); diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-header-visibility-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-header-visibility-iframe.html new file mode 100644 index 00000000000..0d9ab6ff904 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-header-visibility-iframe.html @@ -0,0 +1,66 @@ + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-mixed-content-iframe-inscope-to-inscope.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-mixed-content-iframe-inscope-to-inscope.html new file mode 100644 index 00000000000..64a634e9db7 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-mixed-content-iframe-inscope-to-inscope.html @@ -0,0 +1,71 @@ + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-mixed-content-iframe-inscope-to-outscope.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-mixed-content-iframe-inscope-to-outscope.html new file mode 100644 index 00000000000..be0b5c8f561 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-mixed-content-iframe-inscope-to-outscope.html @@ -0,0 +1,80 @@ + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-mixed-content-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-mixed-content-iframe.html new file mode 100644 index 00000000000..2831c381b5e --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-mixed-content-iframe.html @@ -0,0 +1,71 @@ + + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-base-url-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-base-url-iframe.html new file mode 100644 index 00000000000..504e1043564 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-base-url-iframe.html @@ -0,0 +1,20 @@ + + +iframe for css base url test + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-base-url-style.css b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-base-url-style.css new file mode 100644 index 00000000000..f14fcaae727 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-base-url-style.css @@ -0,0 +1 @@ +body { background-image: url("./sample.png");} diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-base-url-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-base-url-worker.js new file mode 100644 index 00000000000..f3d6a73bdde --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-base-url-worker.js @@ -0,0 +1,45 @@ +let source; +let resolveDone; +let done = new Promise(resolve => resolveDone = resolve); + +// The page messages this worker to ask for the result. Keep the worker alive +// via waitUntil() until the result is sent. +self.addEventListener('message', event => { + source = event.data.port; + source.postMessage('pong'); + event.waitUntil(done); +}); + +self.addEventListener('fetch', event => { + const url = new URL(event.request.url); + + // For the CSS file, respond in a way that may change the response URL, + // depending on |url.search|. + const cssPath = 'request-url-path/fetch-request-css-base-url-style.css'; + if (url.pathname.indexOf(cssPath) != -1) { + // Respond with a different URL, deleting "request-url-path/". + if (url.search == '?fetch') { + event.respondWith(fetch('fetch-request-css-base-url-style.css?fetch')); + } + // Respond with new Response(). + else if (url.search == '?newResponse') { + const styleString = 'body { background-image: url("./sample.png");}'; + const headers = {'content-type': 'text/css'}; + event.respondWith(new Response(styleString, headers)); + } + } + + // The image request indicates what the base URL of the CSS was. Message the + // result back to the test page. + else if (url.pathname.indexOf('sample.png') != -1) { + // For some reason |source| is undefined here when running the test manually + // in Firefox. The test author experimented with both using Client + // (event.source) and MessagePort to try to get the test to pass, but + // failed. + source.postMessage({ + url: event.request.url, + referrer: event.request.referrer + }); + resolveDone(); + } +}); diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-cross.css b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-cross.css new file mode 100644 index 00000000000..9a7545d0702 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-cross.css @@ -0,0 +1 @@ +#crossOriginCss { color: blue; } diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-cross.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-cross.html new file mode 100644 index 00000000000..3211f78084d --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-cross.html @@ -0,0 +1 @@ +#crossOriginHtml { color: red; } diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-iframe.html new file mode 100644 index 00000000000..9a4adedb84d --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-iframe.html @@ -0,0 +1,17 @@ + + + + + + +

I should be blue

+

I should be blue

+

I should be blue

+

I should be blue

+

I should be blue

diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-same.css b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-same.css new file mode 100644 index 00000000000..55455bd5da5 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-same.css @@ -0,0 +1 @@ +#sameOriginCss { color: blue; } diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-same.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-same.html new file mode 100644 index 00000000000..6fad4b9ff04 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-mime-check-same.html @@ -0,0 +1 @@ +#sameOriginHtml { color: blue; } diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-read-contents.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-read-contents.html new file mode 100644 index 00000000000..c902366b023 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-read-contents.html @@ -0,0 +1,15 @@ + + +iframe: cross-origin CSS via service worker + + + + + + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-worker.js new file mode 100644 index 00000000000..a71e91216c1 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-css-cross-origin-worker.js @@ -0,0 +1,65 @@ +importScripts('/common/get-host-info.sub.js'); +importScripts('test-helpers.sub.js'); + +const HOST_INFO = get_host_info(); +const REMOTE_ORIGIN = HOST_INFO.HTTPS_REMOTE_ORIGIN; +const BASE_PATH = base_path(); +const CSS_FILE = 'fetch-request-css-cross-origin-mime-check-cross.css'; +const HTML_FILE = 'fetch-request-css-cross-origin-mime-check-cross.html'; + +function add_pipe_header(url_str, header) { + if (url_str.indexOf('?pipe=') == -1) { + url_str += '?pipe='; + } else { + url_str += '|'; + } + url_str += `header${header}`; + return url_str; +} + +self.addEventListener('fetch', function(event) { + const url = new URL(event.request.url); + + const use_mime = + (url.searchParams.get('mime') != 'no'); + const mime_header = '(Content-Type, text/css)'; + + const use_cors = + (url.searchParams.has('cors')); + const cors_header = '(Access-Control-Allow-Origin, *)'; + + const file = url.pathname.substring(url.pathname.lastIndexOf('/') + 1); + + // Respond with a cross-origin CSS resource, using CORS if desired. + if (file == 'cross-origin-css.css') { + let fetch_url = REMOTE_ORIGIN + BASE_PATH + CSS_FILE; + if (use_mime) + fetch_url = add_pipe_header(fetch_url, mime_header); + if (use_cors) + fetch_url = add_pipe_header(fetch_url, cors_header); + const mode = use_cors ? 'cors' : 'no-cors'; + event.respondWith(fetch(fetch_url, {'mode': mode})); + return; + } + + // Respond with a cross-origin CSS resource with an HTML name. This is only + // used in the MIME sniffing test, so MIME is never added. + if (file == 'cross-origin-html.css') { + const fetch_url = REMOTE_ORIGIN + BASE_PATH + HTML_FILE; + event.respondWith(fetch(fetch_url, {mode: 'no-cors'})); + return; + } + + // Respond with synthetic CSS. + if (file == 'synthetic.css') { + let headers = {}; + if (use_mime) { + headers['Content-Type'] = 'text/css'; + } + + event.respondWith(new Response("#synthetic { color: blue; }", {headers})); + return; + } + + // Otherwise, fallback to network. + }); diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-fallback-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-fallback-iframe.html new file mode 100644 index 00000000000..d117d0f55ef --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-fallback-iframe.html @@ -0,0 +1,32 @@ + diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-fallback-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-fallback-worker.js new file mode 100644 index 00000000000..3b028b24bde --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-fallback-worker.js @@ -0,0 +1,13 @@ +var requests = []; + +self.addEventListener('message', function(event) { + event.data.port.postMessage({requests: requests}); + requests = []; + }); + +self.addEventListener('fetch', function(event) { + requests.push({ + url: event.request.url, + mode: event.request.mode + }); + }); diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-html-imports-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-html-imports-iframe.html new file mode 100644 index 00000000000..07a084257a8 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-html-imports-iframe.html @@ -0,0 +1,13 @@ + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-html-imports-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-html-imports-worker.js new file mode 100644 index 00000000000..110727bd52c --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-html-imports-worker.js @@ -0,0 +1,30 @@ +importScripts('/common/get-host-info.sub.js'); +var host_info = get_host_info(); + +self.addEventListener('fetch', function(event) { + var url = event.request.url; + if (url.indexOf('sample-dir') == -1) { + return; + } + var result = 'mode=' + event.request.mode + + ' credentials=' + event.request.credentials; + if (url == host_info.HTTPS_ORIGIN + '/sample-dir/same.html') { + event.respondWith(new Response( + result + + '' + + '')); + } else if (url == host_info.HTTPS_REMOTE_ORIGIN + '/sample-dir/other.html') { + event.respondWith(new Response( + result + + '' + + '')); + } else { + event.respondWith(new Response(result)); + } + }); diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-iframe.html new file mode 100644 index 00000000000..e6e9380ba62 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-iframe.html @@ -0,0 +1 @@ + diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-script.py b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-script.py new file mode 100644 index 00000000000..bf8df154a88 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-script.py @@ -0,0 +1,6 @@ +def main(request, response): + headers = [] + # Sets an ETag header to check the cache revalidation behavior. + headers.append((b"ETag", b"abc123")) + headers.append((b"Content-Type", b"text/javascript")) + return headers, b"/* empty script */" diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-worker.js new file mode 100644 index 00000000000..2bd59d73922 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-no-freshness-headers-worker.js @@ -0,0 +1,18 @@ +var requests = []; + +self.addEventListener('message', function(event) { + event.data.port.postMessage({requests: requests}); + }); + +self.addEventListener('fetch', function(event) { + var url = event.request.url; + var headers = []; + for (var header of event.request.headers) { + headers.push(header); + } + requests.push({ + url: url, + headers: headers + }); + event.respondWith(fetch(event.request)); + }); diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-redirect-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-redirect-iframe.html new file mode 100644 index 00000000000..ffd76bfc499 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-redirect-iframe.html @@ -0,0 +1,35 @@ + diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-resources-iframe.https.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-resources-iframe.https.html new file mode 100644 index 00000000000..86e9f4bb359 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-resources-iframe.https.html @@ -0,0 +1,87 @@ + + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-resources-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-resources-worker.js new file mode 100644 index 00000000000..983cccb8db7 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-resources-worker.js @@ -0,0 +1,26 @@ +const requests = []; +let port = undefined; + +self.onmessage = e => { + const message = e.data; + if ('port' in message) { + port = message.port; + port.postMessage({ready: true}); + } +}; + +self.addEventListener('fetch', e => { + const url = e.request.url; + if (!url.includes('sample?test')) { + return; + } + port.postMessage({ + url: url, + mode: e.request.mode, + redirect: e.request.redirect, + credentials: e.request.credentials, + integrity: e.request.integrity, + destination: e.request.destination + }); + e.respondWith(Promise.reject()); +}); diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-iframe.https.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-iframe.https.html new file mode 100644 index 00000000000..b3ddec1a701 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-iframe.https.html @@ -0,0 +1,208 @@ + + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-error-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-error-worker.js new file mode 100644 index 00000000000..b8d3db99bcc --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-error-worker.js @@ -0,0 +1,19 @@ +"use strict"; + +self.onfetch = event => { + if (event.request.url.endsWith("non-existent-stream-1.txt")) { + const rs1 = new ReadableStream(); + event.respondWith(new Response(rs1)); + rs1.cancel(1); + } else if (event.request.url.endsWith("non-existent-stream-2.txt")) { + const rs2 = new ReadableStream({ + start(controller) { controller.error(1) } + }); + event.respondWith(new Response(rs2)); + } else if (event.request.url.endsWith("non-existent-stream-3.txt")) { + const rs3 = new ReadableStream({ + pull(controller) { controller.error(1) } + }); + event.respondWith(new Response(rs3)); + } +}; diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-iframe.html new file mode 100644 index 00000000000..900762ffc6c --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-iframe.html @@ -0,0 +1,13 @@ + +Service Worker: Synchronous XHR is intercepted iframe + diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-on-worker-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-on-worker-worker.js new file mode 100644 index 00000000000..0d24ffc1f33 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-on-worker-worker.js @@ -0,0 +1,41 @@ +'use strict'; + +self.onfetch = function(event) { + if (event.request.url.indexOf('non-existent-file.txt') !== -1) { + event.respondWith(new Response('Response from service worker')); + } else if (event.request.url.indexOf('/iframe_page') !== -1) { + event.respondWith(new Response( + '\n' + + '', + { + headers: [['content-type', 'text/html']] + })); + } else if (event.request.url.indexOf('/worker_script') !== -1) { + event.respondWith(new Response( + 'self.onmessage = (msg) => {' + + ' const syncXhr = new XMLHttpRequest();' + + ' syncXhr.open(\'GET\', msg.data.url, false);' + + ' syncXhr.send();' + + ' self.postMessage({' + + ' status: syncXhr.status,' + + ' responseText: syncXhr.responseText' + + ' });' + + '}', + { + headers: [['content-type', 'application/javascript']] + })); + } +}; diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-worker.js new file mode 100644 index 00000000000..070e572f400 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-sync-worker.js @@ -0,0 +1,7 @@ +'use strict'; + +self.onfetch = function(event) { + if (event.request.url.indexOf('non-existent-file.txt') !== -1) { + event.respondWith(new Response('Response from service worker')); + } +}; diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-worker.js new file mode 100644 index 00000000000..4e428374bc2 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-request-xhr-worker.js @@ -0,0 +1,22 @@ +self.addEventListener('fetch', function(event) { + var url = event.request.url; + if (url.indexOf('sample?test') == -1) { + return; + } + event.respondWith(new Promise(function(resolve) { + var headers = []; + for (var header of event.request.headers) { + headers.push(header); + } + event.request.text() + .then(function(result) { + resolve(new Response(JSON.stringify({ + method: event.request.method, + mode: event.request.mode, + credentials: event.request.credentials, + headers: headers, + body: result + }))); + }); + })); + }); diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-response-taint-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-response-taint-iframe.html new file mode 100644 index 00000000000..5f09efe28df --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-response-taint-iframe.html @@ -0,0 +1,2 @@ + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-response-xhr-iframe.https.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-response-xhr-iframe.https.html new file mode 100644 index 00000000000..c26eebee49d --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-response-xhr-iframe.https.html @@ -0,0 +1,53 @@ + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-response-xhr-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-response-xhr-worker.js new file mode 100644 index 00000000000..0301b12c18a --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-response-xhr-worker.js @@ -0,0 +1,12 @@ +self.addEventListener('fetch', function(event) { + var url = event.request.url; + if (url.indexOf('sample?test') == -1) { + return; + } + event.respondWith(new Promise(function(resolve) { + var headers = new Headers; + headers.append('foo', 'foo'); + headers.append('foo', 'bar'); + resolve(new Response('hello world', {'headers': headers})); + })); + }); diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-response.html b/test/wpt/tests/service-workers/service-worker/resources/fetch-response.html new file mode 100644 index 00000000000..6d27cf19e56 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-response.html @@ -0,0 +1,29 @@ + + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-response.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-response.js new file mode 100644 index 00000000000..775efc0bbd2 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-response.js @@ -0,0 +1,35 @@ +self.addEventListener('fetch', event => { + const path = event.request.url.match(/\/(?[^\/]+)$/); + switch (path?.groups?.name) { + case 'constructed': + event.respondWith(new Response(new Uint8Array([1, 2, 3]))); + break; + case 'forward': + event.respondWith(fetch('/common/text-plain.txt')); + break; + case 'stream': + event.respondWith((async() => { + const res = await fetch('/common/text-plain.txt'); + const body = await res.body; + const reader = await body.getReader(); + const stream = new ReadableStream({ + async start(controller) { + while (true) { + const {done, value} = await reader.read(); + if (done) + break; + + controller.enqueue(value); + } + controller.close(); + reader.releaseLock(); + } + }); + return new Response(stream); + })()); + break; + default: + event.respondWith(fetch(event.request)); + break; + } +}); diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-rewrite-worker-referrer-policy.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-rewrite-worker-referrer-policy.js new file mode 100644 index 00000000000..64c99c95d86 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-rewrite-worker-referrer-policy.js @@ -0,0 +1,4 @@ +// This script is intended to be served with the `Referrer-Policy` header as +// defined in the corresponding `.headers` file. + +importScripts('fetch-rewrite-worker.js'); diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-rewrite-worker-referrer-policy.js.headers b/test/wpt/tests/service-workers/service-worker/resources/fetch-rewrite-worker-referrer-policy.js.headers new file mode 100644 index 00000000000..5ae4265418e --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-rewrite-worker-referrer-policy.js.headers @@ -0,0 +1,2 @@ +Content-Type: application/javascript +Referrer-Policy: origin diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-rewrite-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-rewrite-worker.js new file mode 100644 index 00000000000..20a80665270 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-rewrite-worker.js @@ -0,0 +1,166 @@ +// By default, this worker responds to fetch events with +// respondWith(fetch(request)). Additionally, if the request has a &url +// parameter, it fetches the provided URL instead. Because it forwards fetch +// events to this other URL, it is called the "fetch rewrite" worker. +// +// The worker also looks for other params on the request to do more custom +// behavior, like falling back to network or throwing an error. + +function get_query_params(url) { + var search = (new URL(url)).search; + if (!search) { + return {}; + } + var ret = {}; + var params = search.substring(1).split('&'); + params.forEach(function(param) { + var element = param.split('='); + ret[decodeURIComponent(element[0])] = decodeURIComponent(element[1]); + }); + return ret; +} + +function get_request_init(base, params) { + var init = {}; + init['method'] = params['method'] || base['method']; + init['mode'] = params['mode'] || base['mode']; + if (init['mode'] == 'navigate') { + init['mode'] = 'same-origin'; + } + init['credentials'] = params['credentials'] || base['credentials']; + init['redirect'] = params['redirect-mode'] || base['redirect']; + return init; +} + +self.addEventListener('fetch', function(event) { + var params = get_query_params(event.request.url); + var init = get_request_init(event.request, params); + var url = params['url']; + if (params['ignore']) { + return; + } + if (params['throw']) { + throw new Error('boom'); + } + if (params['reject']) { + event.respondWith(new Promise(function(resolve, reject) { + reject(); + })); + return; + } + if (params['resolve-null']) { + event.respondWith(new Promise(function(resolve) { + resolve(null); + })); + return; + } + if (params['generate-png']) { + var binary = atob( + 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAA' + + 'RnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAAhSURBVDhPY3wro/Kf' + + 'gQLABKXJBqMGjBoAAqMGDLwBDAwAEsoCTFWunmQAAAAASUVORK5CYII='); + var array = new Uint8Array(binary.length); + for(var i = 0; i < binary.length; i++) { + array[i] = binary.charCodeAt(i); + }; + event.respondWith(new Response(new Blob([array], {type: 'image/png'}))); + return; + } + if (params['check-ua-header']) { + var ua = event.request.headers.get('User-Agent'); + if (ua) { + // We have a user agent! + event.respondWith(new Response(new Blob([ua]))); + } else { + // We don't have a user-agent! + event.respondWith(new Response(new Blob(["NO_UA"]))); + } + return; + } + if (params['check-accept-header']) { + var accept = event.request.headers.get('Accept'); + if (accept) { + event.respondWith(new Response(accept)); + } else { + event.respondWith(new Response('NO_ACCEPT')); + } + return; + } + event.respondWith(new Promise(function(resolve, reject) { + var request = event.request; + if (url) { + request = new Request(url, init); + } else if (params['change-request']) { + request = new Request(request, init); + } + const response_promise = params['navpreload'] ? event.preloadResponse + : fetch(request); + response_promise.then(function(response) { + var expectedType = params['expected_type']; + if (expectedType && response.type !== expectedType) { + // Resolve a JSON object with a failure instead of rejecting + // in order to distinguish this from a NetworkError, which + // may be expected even if the type is correct. + resolve(new Response(JSON.stringify({ + result: 'failure', + detail: 'got ' + response.type + ' Response.type instead of ' + + expectedType + }))); + } + + var expectedRedirected = params['expected_redirected']; + if (typeof expectedRedirected !== 'undefined') { + var expected_redirected = (expectedRedirected === 'true'); + if(response.redirected !== expected_redirected) { + // This is simply determining how to pass an error to the outer + // test case(fetch-request-redirect.https.html). + var execptedResolves = params['expected_resolves']; + if (execptedResolves === 'true') { + // Reject a JSON object with a failure since promise is expected + // to be resolved. + reject(new Response(JSON.stringify({ + result: 'failure', + detail: 'got '+ response.redirected + + ' Response.redirected instead of ' + + expectedRedirected + }))); + } else { + // Resolve a JSON object with a failure since promise is + // expected to be rejected. + resolve(new Response(JSON.stringify({ + result: 'failure', + detail: 'got '+ response.redirected + + ' Response.redirected instead of ' + + expectedRedirected + }))); + } + } + } + + if (params['clone']) { + response = response.clone(); + } + + // |cache| means to bounce responses through Cache Storage and back. + if (params['cache']) { + var cacheName = "cached-fetches-" + performance.now() + "-" + + event.request.url; + var cache; + var cachedResponse; + return self.caches.open(cacheName).then(function(opened) { + cache = opened; + return cache.put(request, response); + }).then(function() { + return cache.match(request); + }).then(function(cached) { + cachedResponse = cached; + return self.caches.delete(cacheName); + }).then(function() { + resolve(cachedResponse); + }); + } else { + resolve(response); + } + }, reject) + })); + }); diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-rewrite-worker.js.headers b/test/wpt/tests/service-workers/service-worker/resources/fetch-rewrite-worker.js.headers new file mode 100644 index 00000000000..123053b38c6 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-rewrite-worker.js.headers @@ -0,0 +1,2 @@ +Content-Type: text/javascript +Service-Worker-Allowed: / diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-variants-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-variants-worker.js new file mode 100644 index 00000000000..b950b9a18a6 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-variants-worker.js @@ -0,0 +1,35 @@ +importScripts('/common/get-host-info.sub.js'); +importScripts('test-helpers.sub.js'); +importScripts('/resources/testharness.js'); + +const storedResponse = new Response(new Blob(['a simple text file'])) +const absolultePath = `${base_path()}/simple.txt` + +self.addEventListener('fetch', event => { + const search = new URLSearchParams(new URL(event.request.url).search.substr(1)) + const variant = search.get('variant') + const delay = search.get('delay') + if (!variant) + return + + switch (variant) { + case 'forward': + event.respondWith(fetch(event.request.url)) + break + case 'redirect': + event.respondWith(fetch(`/xhr/resources/redirect.py?location=${base_path()}/simple.txt`)) + break + case 'delay-before-fetch': + event.respondWith( + new Promise(resolve => { + step_timeout(() => fetch(event.request.url).then(resolve), delay) + })) + break + case 'delay-after-fetch': + event.respondWith(new Promise(resolve => { + fetch(event.request.url) + .then(response => step_timeout(() => resolve(response), delay)) + })) + break + } +}); diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-waits-for-activate-worker.js b/test/wpt/tests/service-workers/service-worker/resources/fetch-waits-for-activate-worker.js new file mode 100644 index 00000000000..92a96ff88fb --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-waits-for-activate-worker.js @@ -0,0 +1,31 @@ +var activatePromiseResolve; + +addEventListener('activate', function(evt) { + evt.waitUntil(new Promise(function(resolve) { + activatePromiseResolve = resolve; + })); +}); + +addEventListener('message', async function(evt) { + switch (evt.data) { + case 'CLAIM': + evt.waitUntil(new Promise(async resolve => { + await clients.claim(); + evt.source.postMessage('CLAIMED'); + resolve(); + })); + break; + case 'ACTIVATE': + if (typeof activatePromiseResolve !== 'function') { + throw new Error('Not activating!'); + } + activatePromiseResolve(); + break; + default: + throw new Error('Unknown message!'); + } +}); + +addEventListener('fetch', function(evt) { + evt.respondWith(new Response('Hello world')); +}); diff --git a/test/wpt/tests/service-workers/service-worker/resources/form-poster.html b/test/wpt/tests/service-workers/service-worker/resources/form-poster.html new file mode 100644 index 00000000000..cd11a30a5e8 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/form-poster.html @@ -0,0 +1,13 @@ + + + +
+ diff --git a/test/wpt/tests/service-workers/service-worker/resources/frame-for-getregistrations.html b/test/wpt/tests/service-workers/service-worker/resources/frame-for-getregistrations.html new file mode 100644 index 00000000000..7fc35f18914 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/frame-for-getregistrations.html @@ -0,0 +1,19 @@ + +Service Worker: frame for getRegistrations() + diff --git a/test/wpt/tests/service-workers/service-worker/resources/get-resultingClientId-worker.js b/test/wpt/tests/service-workers/service-worker/resources/get-resultingClientId-worker.js new file mode 100644 index 00000000000..f0e6c7becab --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/get-resultingClientId-worker.js @@ -0,0 +1,107 @@ +// This worker expects a fetch event for a navigation and messages back the +// result of clients.get(event.resultingClientId). + +// Resolves when the test finishes. +let testFinishPromise; +let resolveTestFinishPromise; +let rejectTestFinishPromise; + +// Resolves to clients.get(event.resultingClientId) from the fetch event. +let getPromise; +let resolveGetPromise; +let rejectGetPromise; + +let resultingClientId; + +function startTest() { + testFinishPromise = new Promise((resolve, reject) => { + resolveTestFinishPromise = resolve; + rejectTestFinishPromise = reject; + }); + + getPromise = new Promise((resolve, reject) => { + resolveGetPromise = resolve; + rejectGetPromise = reject; + }); +} + +async function describeGetPromiseResult(promise) { + const result = {}; + + await promise.then( + (client) => { + result.promiseState = 'fulfilled'; + if (client === undefined) { + result.promiseValue = 'undefinedValue'; + } else if (client instanceof Client) { + result.promiseValue = 'client'; + result.client = { + id: client.id, + url: client.url + }; + } else { + result.promiseValue = 'unknown'; + } + }, + (error) => { + result.promiseState = 'rejected'; + }); + + return result; +} + +async function handleGetResultingClient(event) { + // Note that this message can arrive before |resultingClientId| is populated. + const result = await describeGetPromiseResult(getPromise); + // |resultingClientId| must be populated by now. + result.queriedId = resultingClientId; + event.source.postMessage(result); +}; + +async function handleGetClient(event) { + const id = event.data.id; + const result = await describeGetPromiseResult(self.clients.get(id)); + result.queriedId = id; + event.source.postMessage(result); +}; + +self.addEventListener('message', (event) => { + if (event.data.command == 'startTest') { + startTest(); + event.waitUntil(testFinishPromise); + event.source.postMessage('ok'); + return; + } + + if (event.data.command == 'finishTest') { + resolveTestFinishPromise(); + event.source.postMessage('ok'); + return; + } + + if (event.data.command == 'getResultingClient') { + event.waitUntil(handleGetResultingClient(event)); + return; + } + + if (event.data.command == 'getClient') { + event.waitUntil(handleGetClient(event)); + return; + } +}); + +async function handleFetch(event) { + try { + resultingClientId = event.resultingClientId; + const client = await self.clients.get(resultingClientId); + resolveGetPromise(client); + } catch (error) { + rejectGetPromise(error); + } +} + +self.addEventListener('fetch', (event) => { + if (event.request.mode != 'navigate') + return; + event.waitUntil(handleFetch(event)); +}); diff --git a/test/wpt/tests/service-workers/service-worker/resources/http-to-https-redirect-and-register-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/http-to-https-redirect-and-register-iframe.html new file mode 100644 index 00000000000..bcab35364df --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/http-to-https-redirect-and-register-iframe.html @@ -0,0 +1,25 @@ + +register, unregister, and report result to opener + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/iframe-with-fetch-variants.html b/test/wpt/tests/service-workers/service-worker/resources/iframe-with-fetch-variants.html new file mode 100644 index 00000000000..3a61d7bb890 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/iframe-with-fetch-variants.html @@ -0,0 +1,14 @@ + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/iframe-with-image.html b/test/wpt/tests/service-workers/service-worker/resources/iframe-with-image.html new file mode 100644 index 00000000000..ce78840cb28 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/iframe-with-image.html @@ -0,0 +1,2 @@ + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/immutable-prototype-serviceworker.js b/test/wpt/tests/service-workers/service-worker/resources/immutable-prototype-serviceworker.js new file mode 100644 index 00000000000..d8a94ad46be --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/immutable-prototype-serviceworker.js @@ -0,0 +1,19 @@ +function prototypeChain(global) { + let result = []; + while (global !== null) { + let thrown = false; + let next = Object.getPrototypeOf(global); + try { + Object.setPrototypeOf(global, {}); + result.push('mutable'); + } catch (e) { + result.push('immutable'); + } + global = next; + } + return result; +} + +self.onmessage = function(e) { + e.data.postMessage(prototypeChain(self)); +}; diff --git a/test/wpt/tests/service-workers/service-worker/resources/import-echo-cookie-worker-module.py b/test/wpt/tests/service-workers/service-worker/resources/import-echo-cookie-worker-module.py new file mode 100644 index 00000000000..8f0b68e5a3a --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/import-echo-cookie-worker-module.py @@ -0,0 +1,6 @@ +def main(request, response): + # This script generates a worker script for static imports from module + # service workers. + headers = [(b'Content-Type', b'text/javascript')] + body = b"import './echo-cookie-worker.py?key=%s'" % request.GET[b'key'] + return headers, body diff --git a/test/wpt/tests/service-workers/service-worker/resources/import-echo-cookie-worker.js b/test/wpt/tests/service-workers/service-worker/resources/import-echo-cookie-worker.js new file mode 100644 index 00000000000..f5eac9508c9 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/import-echo-cookie-worker.js @@ -0,0 +1 @@ +importScripts(`echo-cookie-worker.py${location.search}`); diff --git a/test/wpt/tests/service-workers/service-worker/resources/import-mime-type-worker.py b/test/wpt/tests/service-workers/service-worker/resources/import-mime-type-worker.py new file mode 100644 index 00000000000..b6e82f31d37 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/import-mime-type-worker.py @@ -0,0 +1,10 @@ +def main(request, response): + if b'mime' in request.GET: + return ( + [(b'Content-Type', b'application/javascript')], + b"importScripts('./mime-type-worker.py?mime=%s');" % request.GET[b'mime'] + ) + return ( + [(b'Content-Type', b'application/javascript')], + b"importScripts('./mime-type-worker.py');" + ) diff --git a/test/wpt/tests/service-workers/service-worker/resources/import-relative.xsl b/test/wpt/tests/service-workers/service-worker/resources/import-relative.xsl new file mode 100644 index 00000000000..063a62d0314 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/import-relative.xsl @@ -0,0 +1,5 @@ + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/import-scripts-404-after-update-plus-update-worker.js b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-404-after-update-plus-update-worker.js new file mode 100644 index 00000000000..e9899d8e727 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-404-after-update-plus-update-worker.js @@ -0,0 +1,8 @@ +// This worker imports a script that returns 200 on the first request and 404 +// on the second request, and a script that is updated every time when +// requesting it. +const params = new URLSearchParams(location.search); +const key = params.get('Key'); +const additional_key = params.get('AdditionalKey'); +importScripts(`update-worker.py?Key=${key}&Mode=not_found`, + `update-worker.py?Key=${additional_key}&Mode=normal`); diff --git a/test/wpt/tests/service-workers/service-worker/resources/import-scripts-404-after-update.js b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-404-after-update.js new file mode 100644 index 00000000000..b569346035a --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-404-after-update.js @@ -0,0 +1,6 @@ +// This worker imports a script that returns 200 on the first request and 404 +// on the second request. The resulting body also changes each time it is +// requested. +const params = new URLSearchParams(location.search); +const key = params.get('Key'); +importScripts(`update-worker.py?Key=${key}&Mode=not_found`); diff --git a/test/wpt/tests/service-workers/service-worker/resources/import-scripts-404.js b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-404.js new file mode 100644 index 00000000000..19c7a4b8e56 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-404.js @@ -0,0 +1 @@ +importScripts('404.py'); diff --git a/test/wpt/tests/service-workers/service-worker/resources/import-scripts-cross-origin-worker.sub.js b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-cross-origin-worker.sub.js new file mode 100644 index 00000000000..b432854db8b --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-cross-origin-worker.sub.js @@ -0,0 +1 @@ +importScripts('https://{{domains[www1]}}:{{ports[https][0]}}/service-workers/service-worker/resources/import-scripts-version.py'); diff --git a/test/wpt/tests/service-workers/service-worker/resources/import-scripts-data-url-worker.js b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-data-url-worker.js new file mode 100644 index 00000000000..fdabdafc630 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-data-url-worker.js @@ -0,0 +1 @@ +importScripts('data:text/javascript,'); diff --git a/test/wpt/tests/service-workers/service-worker/resources/import-scripts-diff-resource-map-worker.js b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-diff-resource-map-worker.js new file mode 100644 index 00000000000..0fdcb0fcf80 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-diff-resource-map-worker.js @@ -0,0 +1,10 @@ +importScripts('/resources/testharness.js'); + +let echo1 = null; +let echo2 = null; +let arg1 = 'import-scripts-get.py?output=echo1&msg=test1'; +let arg2 = 'import-scripts-get.py?output=echo2&msg=test2'; + +importScripts(arg1, arg2); +assert_equals(echo1, 'test1'); +assert_equals(echo2, 'test2'); diff --git a/test/wpt/tests/service-workers/service-worker/resources/import-scripts-echo.py b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-echo.py new file mode 100644 index 00000000000..d38d660e659 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-echo.py @@ -0,0 +1,6 @@ +def main(req, res): + return ([ + (b'Cache-Control', b'no-cache, must-revalidate'), + (b'Pragma', b'no-cache'), + (b'Content-Type', b'application/javascript')], + b'echo_output = "%s";\n' % req.GET[b'msg']) diff --git a/test/wpt/tests/service-workers/service-worker/resources/import-scripts-get.py b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-get.py new file mode 100644 index 00000000000..ab7b84e3e34 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-get.py @@ -0,0 +1,6 @@ +def main(req, res): + return ([ + (b'Cache-Control', b'no-cache, must-revalidate'), + (b'Pragma', b'no-cache'), + (b'Content-Type', b'application/javascript')], + b'%s = "%s";\n' % (req.GET[b'output'], req.GET[b'msg'])) diff --git a/test/wpt/tests/service-workers/service-worker/resources/import-scripts-mime-types-worker.js b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-mime-types-worker.js new file mode 100644 index 00000000000..d4f1f3e26d8 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-mime-types-worker.js @@ -0,0 +1,49 @@ +const badMimeTypes = [ + null, // no MIME type + 'text/plain', +]; + +const validMimeTypes = [ + 'application/ecmascript', + 'application/javascript', + 'application/x-ecmascript', + 'application/x-javascript', + 'text/ecmascript', + 'text/javascript', + 'text/javascript1.0', + 'text/javascript1.1', + 'text/javascript1.2', + 'text/javascript1.3', + 'text/javascript1.4', + 'text/javascript1.5', + 'text/jscript', + 'text/livescript', + 'text/x-ecmascript', + 'text/x-javascript', +]; + +function importScriptsWithMimeType(mimeType) { + importScripts(`./mime-type-worker.py${mimeType ? '?mime=' + mimeType : ''}`); +} + +importScripts('/resources/testharness.js'); + +for (const mimeType of badMimeTypes) { + test(() => { + assert_throws_dom( + 'NetworkError', + () => { importScriptsWithMimeType(mimeType); }, + `importScripts with ${mimeType ? 'bad' : 'no'} MIME type ${mimeType || ''} throws NetworkError`, + ); + }, `Importing script with ${mimeType ? 'bad' : 'no'} MIME type ${mimeType || ''}`); +} + +for (const mimeType of validMimeTypes) { + test(() => { + try { + importScriptsWithMimeType(mimeType); + } catch { + assert_unreached(`importScripts with MIME type ${mimeType} should not throw`); + } + }, `Importing script with valid JavaScript MIME type ${mimeType}`); +} diff --git a/test/wpt/tests/service-workers/service-worker/resources/import-scripts-redirect-import.js b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-redirect-import.js new file mode 100644 index 00000000000..56c04f09460 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-redirect-import.js @@ -0,0 +1 @@ +// empty script diff --git a/test/wpt/tests/service-workers/service-worker/resources/import-scripts-redirect-on-second-time-worker.js b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-redirect-on-second-time-worker.js new file mode 100644 index 00000000000..f612ab8e6aa --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-redirect-on-second-time-worker.js @@ -0,0 +1,7 @@ +// This worker imports a script that returns 200 on the first request and a +// redirect on the second request. The resulting body also changes each time it +// is requested. +const params = new URLSearchParams(location.search); +const key = params.get('Key'); +importScripts(`update-worker.py?Key=${key}&Mode=redirect&` + + `Redirect=update-worker.py?Key=${key}%26Mode=normal`); diff --git a/test/wpt/tests/service-workers/service-worker/resources/import-scripts-redirect-worker.js b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-redirect-worker.js new file mode 100644 index 00000000000..d02a45349c2 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-redirect-worker.js @@ -0,0 +1 @@ +importScripts('redirect.py?Redirect=import-scripts-version.py'); diff --git a/test/wpt/tests/service-workers/service-worker/resources/import-scripts-resource-map-worker.js b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-resource-map-worker.js new file mode 100644 index 00000000000..b3b9bc46a02 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-resource-map-worker.js @@ -0,0 +1,15 @@ +importScripts('/resources/testharness.js'); + +let version = null; +importScripts('import-scripts-version.py'); +// Once imported, the stored script should be loaded for subsequent importScripts. +const expected_version = version; + +version = null; +importScripts('import-scripts-version.py'); +assert_equals(expected_version, version, 'second import'); + +version = null; +importScripts('import-scripts-version.py', 'import-scripts-version.py', + 'import-scripts-version.py'); +assert_equals(expected_version, version, 'multiple imports'); diff --git a/test/wpt/tests/service-workers/service-worker/resources/import-scripts-updated-flag-worker.js b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-updated-flag-worker.js new file mode 100644 index 00000000000..e01664662ef --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-updated-flag-worker.js @@ -0,0 +1,31 @@ +importScripts('/resources/testharness.js'); + +let echo_output = null; + +// Tests importing a script that sets |echo_output| to the query string. +function test_import(str) { + echo_output = null; + importScripts('import-scripts-echo.py?msg=' + str); + assert_equals(echo_output, str); +} + +test_import('root'); +test_import('root-and-message'); + +self.addEventListener('install', () => { + test_import('install'); + test_import('install-and-message'); + }); + +self.addEventListener('message', e => { + var error = null; + echo_output = null; + + try { + importScripts('import-scripts-echo.py?msg=' + e.data); + } catch (e) { + error = e && e.name; + } + + e.source.postMessage({ error: error, value: echo_output }); + }); diff --git a/test/wpt/tests/service-workers/service-worker/resources/import-scripts-version.py b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-version.py new file mode 100644 index 00000000000..cde28544e60 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/import-scripts-version.py @@ -0,0 +1,17 @@ +import datetime +import time + +epoch = datetime.datetime(1970, 1, 1) + +def main(req, res): + # Artificially delay response time in order to ensure uniqueness of + # computed value + time.sleep(0.1) + + now = (datetime.datetime.now() - epoch).total_seconds() + + return ([ + (b'Cache-Control', b'no-cache, must-revalidate'), + (b'Pragma', b'no-cache'), + (b'Content-Type', b'application/javascript')], + u'version = "%s";\n' % now) diff --git a/test/wpt/tests/service-workers/service-worker/resources/imported-classic-script.js b/test/wpt/tests/service-workers/service-worker/resources/imported-classic-script.js new file mode 100644 index 00000000000..5fc52040513 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/imported-classic-script.js @@ -0,0 +1 @@ +const imported = 'A classic script.'; diff --git a/test/wpt/tests/service-workers/service-worker/resources/imported-module-script.js b/test/wpt/tests/service-workers/service-worker/resources/imported-module-script.js new file mode 100644 index 00000000000..56d196df040 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/imported-module-script.js @@ -0,0 +1 @@ +export const imported = 'A module script.'; diff --git a/test/wpt/tests/service-workers/service-worker/resources/indexeddb-worker.js b/test/wpt/tests/service-workers/service-worker/resources/indexeddb-worker.js new file mode 100644 index 00000000000..9add4768388 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/indexeddb-worker.js @@ -0,0 +1,57 @@ +self.addEventListener('message', function(e) { + var message = e.data; + if (message.action === 'create') { + e.waitUntil(deleteDB() + .then(doIndexedDBTest) + .then(function() { + message.port.postMessage({ type: 'created' }); + }) + .catch(function(reason) { + message.port.postMessage({ type: 'error', value: reason }); + })); + } else if (message.action === 'cleanup') { + e.waitUntil(deleteDB() + .then(function() { + message.port.postMessage({ type: 'done' }); + }) + .catch(function(reason) { + message.port.postMessage({ type: 'error', value: reason }); + })); + } + }); + +function deleteDB() { + return new Promise(function(resolve, reject) { + var delete_request = indexedDB.deleteDatabase('db'); + + delete_request.onsuccess = resolve; + delete_request.onerror = reject; + }); +} + +function doIndexedDBTest(port) { + return new Promise(function(resolve, reject) { + var open_request = indexedDB.open('db'); + + open_request.onerror = reject; + open_request.onupgradeneeded = function() { + var db = open_request.result; + db.createObjectStore('store'); + }; + open_request.onsuccess = function() { + var db = open_request.result; + var tx = db.transaction('store', 'readwrite'); + var store = tx.objectStore('store'); + store.put('value', 'key'); + + tx.onerror = function() { + db.close(); + reject(tx.error); + }; + tx.oncomplete = function() { + db.close(); + resolve(); + }; + }; + }); +} diff --git a/test/wpt/tests/service-workers/service-worker/resources/install-event-type-worker.js b/test/wpt/tests/service-workers/service-worker/resources/install-event-type-worker.js new file mode 100644 index 00000000000..1c94ae21ea9 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/install-event-type-worker.js @@ -0,0 +1,9 @@ +importScripts('worker-testharness.js'); + +self.oninstall = function(event) { + assert_true(event instanceof ExtendableEvent, 'instance of ExtendableEvent'); + assert_true(event instanceof InstallEvent, 'instance of InstallEvent'); + assert_equals(event.type, 'install', '`type` property value'); + assert_false(event.cancelable, '`cancelable` property value'); + assert_false(event.bubbles, '`bubbles` property value'); +}; diff --git a/test/wpt/tests/service-workers/service-worker/resources/install-worker.html b/test/wpt/tests/service-workers/service-worker/resources/install-worker.html new file mode 100644 index 00000000000..ed20cd4dca6 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/install-worker.html @@ -0,0 +1,22 @@ + + + +

Loading...

+ + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/interface-requirements-worker.sub.js b/test/wpt/tests/service-workers/service-worker/resources/interface-requirements-worker.sub.js new file mode 100644 index 00000000000..a3f239b6548 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/interface-requirements-worker.sub.js @@ -0,0 +1,59 @@ +'use strict'; + +// This file checks additional interface requirements, on top of the basic IDL +// that is validated in service-workers/idlharness.any.js + +importScripts('/resources/testharness.js'); + +test(function() { + var req = new Request('http://{{host}}/', + {method: 'POST', + headers: [['Content-Type', 'Text/Html']]}); + assert_equals( + new ExtendableEvent('ExtendableEvent').type, + 'ExtendableEvent', 'Type of ExtendableEvent should be ExtendableEvent'); + assert_throws_js(TypeError, function() { + new FetchEvent('FetchEvent'); + }, 'FetchEvent constructor with one argument throws'); + assert_throws_js(TypeError, function() { + new FetchEvent('FetchEvent', {}); + }, 'FetchEvent constructor with empty init dict throws'); + assert_throws_js(TypeError, function() { + new FetchEvent('FetchEvent', {request: null}); + }, 'FetchEvent constructor with null request member throws'); + assert_equals( + new FetchEvent('FetchEvent', {request: req}).type, + 'FetchEvent', 'Type of FetchEvent should be FetchEvent'); + assert_equals( + new FetchEvent('FetchEvent', {request: req}).cancelable, + false, 'Default FetchEvent.cancelable should be false'); + assert_equals( + new FetchEvent('FetchEvent', {request: req}).bubbles, + false, 'Default FetchEvent.bubbles should be false'); + assert_equals( + new FetchEvent('FetchEvent', {request: req}).clientId, + '', 'Default FetchEvent.clientId should be the empty string'); + assert_equals( + new FetchEvent('FetchEvent', {request: req, cancelable: false}).cancelable, + false, 'FetchEvent.cancelable should be false'); + assert_equals( + new FetchEvent('FetchEvent', {request: req, clientId : 'test-client-id'}).clientId, 'test-client-id', + 'FetchEvent.clientId with option {clientId : "test-client-id"} should be "test-client-id"'); + assert_equals( + new FetchEvent('FetchEvent', {request : req}).request.url, + 'http://{{host}}/', + 'FetchEvent.request.url should return the value it was initialized to'); + assert_equals( + new FetchEvent('FetchEvent', {request : req}).isReload, + undefined, + 'FetchEvent.isReload should not exist'); + + }, 'Event constructors'); + +test(() => { + assert_false('XMLHttpRequest' in self); + }, 'xhr is not exposed'); + +test(() => { + assert_false('createObjectURL' in self.URL); + }, 'URL.createObjectURL is not exposed') diff --git a/test/wpt/tests/service-workers/service-worker/resources/invalid-blobtype-iframe.https.html b/test/wpt/tests/service-workers/service-worker/resources/invalid-blobtype-iframe.https.html new file mode 100644 index 00000000000..04a9cb515e0 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/invalid-blobtype-iframe.https.html @@ -0,0 +1,28 @@ + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/invalid-blobtype-worker.js b/test/wpt/tests/service-workers/service-worker/resources/invalid-blobtype-worker.js new file mode 100644 index 00000000000..865dc30d428 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/invalid-blobtype-worker.js @@ -0,0 +1,10 @@ +self.addEventListener('fetch', function(event) { + var url = event.request.url; + if (url.indexOf('sample?test') == -1) { + return; + } + event.respondWith(new Promise(function(resolve) { + // null byte in blob type + resolve(new Response(new Blob([],{type: 'a\0b'}))); + })); + }); diff --git a/test/wpt/tests/service-workers/service-worker/resources/invalid-chunked-encoding-with-flush.py b/test/wpt/tests/service-workers/service-worker/resources/invalid-chunked-encoding-with-flush.py new file mode 100644 index 00000000000..05977c6ab0b --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/invalid-chunked-encoding-with-flush.py @@ -0,0 +1,9 @@ +import time +def main(request, response): + response.headers.set(b"Content-Type", b"application/javascript") + response.headers.set(b"Transfer-encoding", b"chunked") + response.write_status_headers() + + time.sleep(1) + + response.writer.write(b"XX\r\n\r\n") diff --git a/test/wpt/tests/service-workers/service-worker/resources/invalid-chunked-encoding.py b/test/wpt/tests/service-workers/service-worker/resources/invalid-chunked-encoding.py new file mode 100644 index 00000000000..a8edd06b8dc --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/invalid-chunked-encoding.py @@ -0,0 +1,2 @@ +def main(request, response): + return [(b"Content-Type", b"application/javascript"), (b"Transfer-encoding", b"chunked")], b"XX\r\n\r\n" diff --git a/test/wpt/tests/service-workers/service-worker/resources/invalid-header-iframe.https.html b/test/wpt/tests/service-workers/service-worker/resources/invalid-header-iframe.https.html new file mode 100644 index 00000000000..8f0e6baca17 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/invalid-header-iframe.https.html @@ -0,0 +1,25 @@ + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/invalid-header-worker.js b/test/wpt/tests/service-workers/service-worker/resources/invalid-header-worker.js new file mode 100644 index 00000000000..850874b8116 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/invalid-header-worker.js @@ -0,0 +1,12 @@ +self.addEventListener('fetch', function(event) { + var url = event.request.url; + if (url.indexOf('sample?test') == -1) { + return; + } + event.respondWith(new Promise(function(resolve) { + var headers = new Headers; + headers.append('foo', 'foo'); + headers.append('foo', 'b\0r'); // header value with a null byte + resolve(new Response('hello world', {'headers': headers})); + })); + }); diff --git a/test/wpt/tests/service-workers/service-worker/resources/iso-latin1-header-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/iso-latin1-header-iframe.html new file mode 100644 index 00000000000..cf2fa8d14f7 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/iso-latin1-header-iframe.html @@ -0,0 +1,23 @@ + diff --git a/test/wpt/tests/service-workers/service-worker/resources/iso-latin1-header-worker.js b/test/wpt/tests/service-workers/service-worker/resources/iso-latin1-header-worker.js new file mode 100644 index 00000000000..d9ecca277b3 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/iso-latin1-header-worker.js @@ -0,0 +1,12 @@ +self.addEventListener('fetch', function(event) { + var url = event.request.url; + if (url.indexOf('sample?test') == -1) { + return; + } + + event.respondWith(new Promise(function(resolve) { + var headers = new Headers; + headers.append('TEST', 'ßÀ¿'); // header value holds the Latin1 (ISO8859-1) string. + resolve(new Response('hello world', {'headers': headers})); + })); + }); diff --git a/test/wpt/tests/service-workers/service-worker/resources/load_worker.js b/test/wpt/tests/service-workers/service-worker/resources/load_worker.js new file mode 100644 index 00000000000..18c673bebca --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/load_worker.js @@ -0,0 +1,29 @@ +function run_test(data, sender) { + if (data === 'xhr') { + const xhr = new XMLHttpRequest(); + xhr.open('GET', 'synthesized-response.txt', true); + xhr.responseType = 'text'; + xhr.send(); + xhr.onload = evt => sender.postMessage(xhr.responseText); + xhr.onerror = () => sender.postMessage('XHR failed!'); + } else if (data === 'fetch') { + fetch('synthesized-response.txt') + .then(response => response.text()) + .then(data => sender.postMessage(data)) + .catch(error => sender.postMessage('Fetch failed!')); + } else if (data === 'importScripts') { + importScripts('synthesized-response.js'); + // |message| is provided by 'synthesized-response.js'; + sender.postMessage(message); + } else { + sender.postMessage('Unexpected message! ' + data); + } +} + +// Entry point for dedicated workers. +self.onmessage = evt => run_test(evt.data, self); + +// Entry point for shared workers. +self.onconnect = evt => { + evt.ports[0].onmessage = e => run_test(e.data, evt.ports[0]); +}; diff --git a/test/wpt/tests/service-workers/service-worker/resources/loaded.html b/test/wpt/tests/service-workers/service-worker/resources/loaded.html new file mode 100644 index 00000000000..0cabce69f8e --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/loaded.html @@ -0,0 +1,9 @@ + diff --git a/test/wpt/tests/service-workers/service-worker/resources/local-url-inherit-controller-frame.html b/test/wpt/tests/service-workers/service-worker/resources/local-url-inherit-controller-frame.html new file mode 100644 index 00000000000..b1e554d2204 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/local-url-inherit-controller-frame.html @@ -0,0 +1,130 @@ + + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/local-url-inherit-controller-worker.js b/test/wpt/tests/service-workers/service-worker/resources/local-url-inherit-controller-worker.js new file mode 100644 index 00000000000..4b7aad0f58a --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/local-url-inherit-controller-worker.js @@ -0,0 +1,5 @@ +addEventListener('fetch', evt => { + if (evt.request.url.includes('sample')) { + evt.respondWith(new Response('intercepted')); + } +}); diff --git a/test/wpt/tests/service-workers/service-worker/resources/location-setter.html b/test/wpt/tests/service-workers/service-worker/resources/location-setter.html new file mode 100644 index 00000000000..f0ced06ec26 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/location-setter.html @@ -0,0 +1,10 @@ + + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/malformed-http-response.asis b/test/wpt/tests/service-workers/service-worker/resources/malformed-http-response.asis new file mode 100644 index 00000000000..bc3c68d46d5 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/malformed-http-response.asis @@ -0,0 +1 @@ +HAHAHA THIS IS NOT HTTP AND THE BROWSER SHOULD CONSIDER IT A NETWORK ERROR diff --git a/test/wpt/tests/service-workers/service-worker/resources/malformed-worker.py b/test/wpt/tests/service-workers/service-worker/resources/malformed-worker.py new file mode 100644 index 00000000000..319b6e277ba --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/malformed-worker.py @@ -0,0 +1,14 @@ +def main(request, response): + headers = [(b"Content-Type", b"application/javascript")] + + body = {u'parse-error': u'var foo = function() {;', + u'undefined-error': u'foo.bar = 42;', + u'uncaught-exception': u'throw new DOMException("AbortError");', + u'caught-exception': u'try { throw new Error; } catch(e) {}', + u'import-malformed-script': u'importScripts("malformed-worker.py?parse-error");', + u'import-no-such-script': u'importScripts("no-such-script.js");', + u'top-level-await': u'await Promise.resolve(1);', + u'instantiation-error': u'import nonexistent from "./imported-module-script.js";', + u'instantiation-error-and-top-level-await': u'import nonexistent from "./imported-module-script.js"; await Promise.resolve(1);'}[request.url_parts.query] + + return headers, body diff --git a/test/wpt/tests/service-workers/service-worker/resources/message-vs-microtask.html b/test/wpt/tests/service-workers/service-worker/resources/message-vs-microtask.html new file mode 100644 index 00000000000..2c45c59a475 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/message-vs-microtask.html @@ -0,0 +1,18 @@ + + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/mime-sniffing-worker.js b/test/wpt/tests/service-workers/service-worker/resources/mime-sniffing-worker.js new file mode 100644 index 00000000000..5c34a7a49e8 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/mime-sniffing-worker.js @@ -0,0 +1,9 @@ +self.addEventListener('fetch', function(event) { + // Use an empty content-type value to force mime-sniffing. Note, this + // must be passed to the constructor since the mime-type of the Response + // is fixed and cannot be later changed. + var res = new Response('\n

test

', { + headers: { 'content-type': '' } + }); + event.respondWith(res); + }); diff --git a/test/wpt/tests/service-workers/service-worker/resources/mime-type-worker.py b/test/wpt/tests/service-workers/service-worker/resources/mime-type-worker.py new file mode 100644 index 00000000000..92a602e634c --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/mime-type-worker.py @@ -0,0 +1,4 @@ +def main(request, response): + if b'mime' in request.GET: + return [(b'Content-Type', request.GET[b'mime'])], b"" + return [], b"" diff --git a/test/wpt/tests/service-workers/service-worker/resources/mint-new-worker.py b/test/wpt/tests/service-workers/service-worker/resources/mint-new-worker.py new file mode 100644 index 00000000000..ebee4ff8e8d --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/mint-new-worker.py @@ -0,0 +1,27 @@ +import random + +import time + +body = u''' +onactivate = (e) => e.waitUntil(clients.claim()); +var resolve_wait_until; +var wait_until = new Promise(resolve => { + resolve_wait_until = resolve; + }); +onmessage = (e) => { + if (e.data == 'wait') + e.waitUntil(wait_until); + if (e.data == 'go') + resolve_wait_until(); + };''' + +def main(request, response): + headers = [(b'Cache-Control', b'no-cache, must-revalidate'), + (b'Pragma', b'no-cache'), + (b'Content-Type', b'application/javascript')] + + skipWaiting = u'' + if b'skip-waiting' in request.GET: + skipWaiting = u'skipWaiting();' + + return headers, u'/* %s %s */ %s %s' % (time.time(), random.random(), skipWaiting, body) diff --git a/test/wpt/tests/service-workers/service-worker/resources/module-worker.js b/test/wpt/tests/service-workers/service-worker/resources/module-worker.js new file mode 100644 index 00000000000..385fe710150 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/module-worker.js @@ -0,0 +1 @@ +import * as module from './imported-module-script.js'; diff --git a/test/wpt/tests/service-workers/service-worker/resources/multipart-image-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/multipart-image-iframe.html new file mode 100644 index 00000000000..c59b95594ff --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/multipart-image-iframe.html @@ -0,0 +1,19 @@ + diff --git a/test/wpt/tests/service-workers/service-worker/resources/multipart-image-worker.js b/test/wpt/tests/service-workers/service-worker/resources/multipart-image-worker.js new file mode 100644 index 00000000000..a38fe54d34f --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/multipart-image-worker.js @@ -0,0 +1,21 @@ +importScripts('/common/get-host-info.sub.js'); +importScripts('test-helpers.sub.js'); + +const host_info = get_host_info(); + +const multipart_image_path = base_path() + 'multipart-image.py'; +const sameorigin_url = host_info['HTTPS_ORIGIN'] + multipart_image_path; +const cross_origin_url = host_info['HTTPS_REMOTE_ORIGIN'] + multipart_image_path; + +self.addEventListener('fetch', event => { + const url = event.request.url; + if (url.indexOf('cross-origin-multipart-image-with-no-cors') >= 0) { + event.respondWith(fetch(cross_origin_url, {mode: 'no-cors'})); + } else if (url.indexOf('cross-origin-multipart-image-with-cors-rejected') >= 0) { + event.respondWith(fetch(cross_origin_url, {mode: 'cors'})); + } else if (url.indexOf('cross-origin-multipart-image-with-cors-approved') >= 0) { + event.respondWith(fetch(cross_origin_url + '?approvecors', {mode: 'cors'})); + } else if (url.indexOf('same-origin-multipart-image') >= 0) { + event.respondWith(fetch(sameorigin_url)); + } +}); diff --git a/test/wpt/tests/service-workers/service-worker/resources/multipart-image.py b/test/wpt/tests/service-workers/service-worker/resources/multipart-image.py new file mode 100644 index 00000000000..9a3c035f492 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/multipart-image.py @@ -0,0 +1,23 @@ +# A request handler that serves a multipart image. + +import os + + +BOUNDARY = b'cutHere' + + +def create_part(path): + with open(path, u'rb') as f: + return b'Content-Type: image/png\r\n\r\n' + f.read() + b'--%s' % BOUNDARY + + +def main(request, response): + content_type = b'multipart/x-mixed-replace; boundary=%s' % BOUNDARY + headers = [(b'Content-Type', content_type)] + if b'approvecors' in request.GET: + headers.append((b'Access-Control-Allow-Origin', b'*')) + + image_path = os.path.join(request.doc_root, u'images') + body = create_part(os.path.join(image_path, u'red.png')) + body = body + create_part(os.path.join(image_path, u'red-16x16.png')) + return headers, body diff --git a/test/wpt/tests/service-workers/service-worker/resources/navigate-window-worker.js b/test/wpt/tests/service-workers/service-worker/resources/navigate-window-worker.js new file mode 100644 index 00000000000..f9617439fc6 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/navigate-window-worker.js @@ -0,0 +1,21 @@ +addEventListener('message', function(evt) { + if (evt.data.type === 'GET_CLIENTS') { + clients.matchAll(evt.data.opts).then(function(clientList) { + var resultList = clientList.map(function(c) { + return { url: c.url, frameType: c.frameType, id: c.id }; + }); + evt.source.postMessage({ type: 'success', detail: resultList }); + }).catch(function(err) { + evt.source.postMessage({ + type: 'failure', + detail: 'matchAll() rejected with "' + err + '"' + }); + }); + return; + } + + evt.source.postMessage({ + type: 'failure', + detail: 'Unexpected message type "' + evt.data.type + '"' + }); +}); diff --git a/test/wpt/tests/service-workers/service-worker/resources/navigation-headers-server.py b/test/wpt/tests/service-workers/service-worker/resources/navigation-headers-server.py new file mode 100644 index 00000000000..5b2e044f8b5 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/navigation-headers-server.py @@ -0,0 +1,19 @@ +def main(request, response): + response.status = (200, b"OK") + response.headers.set(b"Content-Type", b"text/html") + return b""" + """ % (request.headers.get( + b"origin", b"not set"), request.headers.get(b"referer", b"not set"), + request.headers.get(b"sec-fetch-site", b"not set"), + request.headers.get(b"sec-fetch-mode", b"not set"), + request.headers.get(b"sec-fetch-dest", b"not set")) diff --git a/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-body-worker.js b/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-body-worker.js new file mode 100644 index 00000000000..39f11baf8cb --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-body-worker.js @@ -0,0 +1,11 @@ +self.addEventListener('fetch', function(event) { + event.respondWith( + fetch(event.request) + .then( + function(response) { + return response; + }, + function(error) { + return new Response('Error:' + error); + })); + }); diff --git a/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-body.py b/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-body.py new file mode 100644 index 00000000000..d10329e7836 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-body.py @@ -0,0 +1,11 @@ +import os + +from wptserve.utils import isomorphic_encode + +filename = os.path.basename(isomorphic_encode(__file__)) + +def main(request, response): + if request.method == u'POST': + return 302, [(b'Location', b'./%s?redirect' % filename)], b'' + + return [(b'Content-Type', b'text/plain')], request.request_path diff --git a/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-other-origin.html b/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-other-origin.html new file mode 100644 index 00000000000..d82571d1a3c --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-other-origin.html @@ -0,0 +1,89 @@ + + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-out-scope.py b/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-out-scope.py new file mode 100644 index 00000000000..9b90b146955 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-out-scope.py @@ -0,0 +1,22 @@ +def main(request, response): + if b"url" in request.GET: + headers = [(b"Location", request.GET[b"url"])] + return 302, headers, b'' + + status = 200 + + if b"noLocationRedirect" in request.GET: + status = 302 + + return status, [(b"content-type", b"text/html")], b''' + + +''' diff --git a/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-scope1.py b/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-scope1.py new file mode 100644 index 00000000000..9b90b146955 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-scope1.py @@ -0,0 +1,22 @@ +def main(request, response): + if b"url" in request.GET: + headers = [(b"Location", request.GET[b"url"])] + return 302, headers, b'' + + status = 200 + + if b"noLocationRedirect" in request.GET: + status = 302 + + return status, [(b"content-type", b"text/html")], b''' + + +''' diff --git a/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-scope2.py b/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-scope2.py new file mode 100644 index 00000000000..9b90b146955 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-scope2.py @@ -0,0 +1,22 @@ +def main(request, response): + if b"url" in request.GET: + headers = [(b"Location", request.GET[b"url"])] + return 302, headers, b'' + + status = 200 + + if b"noLocationRedirect" in request.GET: + status = 302 + + return status, [(b"content-type", b"text/html")], b''' + + +''' diff --git a/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-to-http-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-to-http-iframe.html new file mode 100644 index 00000000000..40e27c630d2 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-to-http-iframe.html @@ -0,0 +1,42 @@ + + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-to-http-worker.js b/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-to-http-worker.js new file mode 100644 index 00000000000..6f2a8ae1d74 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/navigation-redirect-to-http-worker.js @@ -0,0 +1,22 @@ +importScripts('/resources/testharness.js'); + +self.addEventListener('fetch', function(event) { + event.respondWith(new Promise(function(resolve) { + Promise.resolve() + .then(function() { + assert_equals( + event.request.redirect, 'manual', + 'The redirect mode of navigation request must be manual.'); + return fetch(event.request); + }) + .then(function(response) { + assert_equals( + response.type, 'opaqueredirect', + 'The response type of 302 response must be opaqueredirect.'); + resolve(new Response('OK')); + }) + .catch(function(error) { + resolve(new Response('Failed in SW: ' + error)); + }); + })); + }); diff --git a/test/wpt/tests/service-workers/service-worker/resources/navigation-timing-worker-extended.js b/test/wpt/tests/service-workers/service-worker/resources/navigation-timing-worker-extended.js new file mode 100644 index 00000000000..79c54088ff6 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/navigation-timing-worker-extended.js @@ -0,0 +1,22 @@ +importScripts("/resources/testharness.js"); +const timings = {} + +const DELAY_ACTIVATION = 500 + +self.addEventListener('activate', event => { + event.waitUntil(new Promise(resolve => { + timings.activateWorkerStart = performance.now() + performance.timeOrigin; + + // This gives us enough time to ensure activation would delay fetch handling + step_timeout(resolve, DELAY_ACTIVATION); + }).then(() => timings.activateWorkerEnd = performance.now() + performance.timeOrigin)); +}) + +self.addEventListener('fetch', event => { + timings.handleFetchEvent = performance.now() + performance.timeOrigin; + event.respondWith(Promise.resolve(new Response(new Blob([` + + `])))); +}); diff --git a/test/wpt/tests/service-workers/service-worker/resources/navigation-timing-worker.js b/test/wpt/tests/service-workers/service-worker/resources/navigation-timing-worker.js new file mode 100644 index 00000000000..8539b40066d --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/navigation-timing-worker.js @@ -0,0 +1,15 @@ +self.addEventListener('fetch', (event) => { + const url = event.request.url; + + // Network fallback. + if (url.indexOf('network-fallback') >= 0) { + return; + } + + // Don't intercept redirect. + if (url.indexOf('redirect.py') >= 0) { + return; + } + + event.respondWith(fetch(url)); +}); diff --git a/test/wpt/tests/service-workers/service-worker/resources/nested-blob-url-worker-created-from-worker.html b/test/wpt/tests/service-workers/service-worker/resources/nested-blob-url-worker-created-from-worker.html new file mode 100644 index 00000000000..fc048e288e9 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/nested-blob-url-worker-created-from-worker.html @@ -0,0 +1,16 @@ + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/nested-blob-url-workers.html b/test/wpt/tests/service-workers/service-worker/resources/nested-blob-url-workers.html new file mode 100644 index 00000000000..f0eafcd3e01 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/nested-blob-url-workers.html @@ -0,0 +1,38 @@ + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/nested-iframe-parent.html b/test/wpt/tests/service-workers/service-worker/resources/nested-iframe-parent.html new file mode 100644 index 00000000000..115ab26e122 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/nested-iframe-parent.html @@ -0,0 +1,5 @@ + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/nested-parent.html b/test/wpt/tests/service-workers/service-worker/resources/nested-parent.html new file mode 100644 index 00000000000..b4832d461d5 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/nested-parent.html @@ -0,0 +1,18 @@ + + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/nested-worker-created-from-blob-url-worker.html b/test/wpt/tests/service-workers/service-worker/resources/nested-worker-created-from-blob-url-worker.html new file mode 100644 index 00000000000..3fad2c9228c --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/nested-worker-created-from-blob-url-worker.html @@ -0,0 +1,33 @@ + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/nested_load_worker.js b/test/wpt/tests/service-workers/service-worker/resources/nested_load_worker.js new file mode 100644 index 00000000000..ef0ed8fc704 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/nested_load_worker.js @@ -0,0 +1,23 @@ +// Entry point for dedicated workers. +self.onmessage = evt => { + try { + const worker = new Worker('load_worker.js'); + worker.onmessage = evt => self.postMessage(evt.data); + worker.postMessage(evt.data); + } catch (err) { + self.postMessage('Unexpected error! ' + err.message); + } +}; + +// Entry point for shared workers. +self.onconnect = evt => { + evt.ports[0].onmessage = e => { + try { + const worker = new Worker('load_worker.js'); + worker.onmessage = e => evt.ports[0].postMessage(e.data); + worker.postMessage(evt.data); + } catch (err) { + evt.ports[0].postMessage('Unexpected error! ' + err.message); + } + }; +}; diff --git a/test/wpt/tests/service-workers/service-worker/resources/no-dynamic-import.js b/test/wpt/tests/service-workers/service-worker/resources/no-dynamic-import.js new file mode 100644 index 00000000000..ecedd6c5d75 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/no-dynamic-import.js @@ -0,0 +1,18 @@ +/** @type {[name: string, url: string][]} */ +const importUrlTests = [ + ["Module URL", "./basic-module.js"], + // In no-dynamic-import-in-module.any.js, this module is also statically imported + ["Another module URL", "./basic-module-2.js"], + [ + "Module data: URL", + "data:text/javascript;charset=utf-8," + + encodeURIComponent(`export default 'hello!';`), + ], +]; + +for (const [name, url] of importUrlTests) { + promise_test( + (t) => promise_rejects_js(t, TypeError, import(url), "Import must reject"), + name + ); +} diff --git a/test/wpt/tests/service-workers/service-worker/resources/notification_icon.py b/test/wpt/tests/service-workers/service-worker/resources/notification_icon.py new file mode 100644 index 00000000000..71f5a9d488d --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/notification_icon.py @@ -0,0 +1,11 @@ +from urllib.parse import parse_qs + +from wptserve.utils import isomorphic_encode + +def main(req, res): + qs_cookie_val = parse_qs(req.url_parts.query).get(u'set-cookie-notification') + + if qs_cookie_val: + res.set_cookie(b'notification', isomorphic_encode(qs_cookie_val[0])) + + return b'not really an icon' diff --git a/test/wpt/tests/service-workers/service-worker/resources/object-image-is-not-intercepted-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/object-image-is-not-intercepted-iframe.html new file mode 100644 index 00000000000..5a20a58ab16 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/object-image-is-not-intercepted-iframe.html @@ -0,0 +1,21 @@ + + +iframe for embed-and-object-are-not-intercepted test + + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/object-is-not-intercepted-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/object-is-not-intercepted-iframe.html new file mode 100644 index 00000000000..0aeb81951ed --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/object-is-not-intercepted-iframe.html @@ -0,0 +1,18 @@ + + +iframe for embed-and-object-are-not-intercepted test + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/object-navigation-is-not-intercepted-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/object-navigation-is-not-intercepted-iframe.html new file mode 100644 index 00000000000..5c8ab79a500 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/object-navigation-is-not-intercepted-iframe.html @@ -0,0 +1,24 @@ + + +iframe for embed-and-object-are-not-intercepted test + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/onactivate-throw-error-from-nested-event-worker.js b/test/wpt/tests/service-workers/service-worker/resources/onactivate-throw-error-from-nested-event-worker.js new file mode 100644 index 00000000000..7c97014fd04 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/onactivate-throw-error-from-nested-event-worker.js @@ -0,0 +1,13 @@ +var max_nesting_level = 8; + +self.addEventListener('message', function(event) { + var level = event.data; + if (level < max_nesting_level) + dispatchEvent(new MessageEvent('message', { data: level + 1 })); + throw Error('error at level ' + level); + }); + +self.addEventListener('activate', function(event) { + dispatchEvent(new MessageEvent('message', { data: 1 })); + }); + diff --git a/test/wpt/tests/service-workers/service-worker/resources/onactivate-throw-error-then-cancel-worker.js b/test/wpt/tests/service-workers/service-worker/resources/onactivate-throw-error-then-cancel-worker.js new file mode 100644 index 00000000000..0bd9d318b24 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/onactivate-throw-error-then-cancel-worker.js @@ -0,0 +1,3 @@ +self.onerror = function(event) { return true; }; + +self.addEventListener('activate', function(event) { throw new Error(); }); diff --git a/test/wpt/tests/service-workers/service-worker/resources/onactivate-throw-error-then-prevent-default-worker.js b/test/wpt/tests/service-workers/service-worker/resources/onactivate-throw-error-then-prevent-default-worker.js new file mode 100644 index 00000000000..d56c9511391 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/onactivate-throw-error-then-prevent-default-worker.js @@ -0,0 +1,7 @@ +// Ensure we can handle multiple error handlers. One error handler +// calling preventDefault should cause the event to be treated as +// handled. +self.addEventListener('error', function(event) {}); +self.addEventListener('error', function(event) { event.preventDefault(); }); +self.addEventListener('error', function(event) {}); +self.addEventListener('activate', function(event) { throw new Error(); }); diff --git a/test/wpt/tests/service-workers/service-worker/resources/onactivate-throw-error-with-empty-onerror-worker.js b/test/wpt/tests/service-workers/service-worker/resources/onactivate-throw-error-with-empty-onerror-worker.js new file mode 100644 index 00000000000..eb12ae862c5 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/onactivate-throw-error-with-empty-onerror-worker.js @@ -0,0 +1,2 @@ +self.addEventListener('error', function(event) {}); +self.addEventListener('activate', function(event) { throw new Error(); }); diff --git a/test/wpt/tests/service-workers/service-worker/resources/onactivate-throw-error-worker.js b/test/wpt/tests/service-workers/service-worker/resources/onactivate-throw-error-worker.js new file mode 100644 index 00000000000..1e88ac5c4e7 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/onactivate-throw-error-worker.js @@ -0,0 +1,7 @@ +// Ensure we can handle multiple activate handlers. One handler throwing an +// error should cause the event dispatch to be treated as having unhandled +// errors. +self.addEventListener('activate', function(event) {}); +self.addEventListener('activate', function(event) {}); +self.addEventListener('activate', function(event) { throw new Error(); }); +self.addEventListener('activate', function(event) {}); diff --git a/test/wpt/tests/service-workers/service-worker/resources/onactivate-waituntil-forever.js b/test/wpt/tests/service-workers/service-worker/resources/onactivate-waituntil-forever.js new file mode 100644 index 00000000000..65b02b12b36 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/onactivate-waituntil-forever.js @@ -0,0 +1,8 @@ +'use strict'; + +self.addEventListener('activate', event => { + event.waitUntil(new Promise(() => { + // Use a promise that never resolves to prevent this service worker from + // advancing past the 'activating' state. + })); +}); diff --git a/test/wpt/tests/service-workers/service-worker/resources/onfetch-waituntil-forever.js b/test/wpt/tests/service-workers/service-worker/resources/onfetch-waituntil-forever.js new file mode 100644 index 00000000000..b905d555986 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/onfetch-waituntil-forever.js @@ -0,0 +1,10 @@ +'use strict'; + +self.addEventListener('fetch', event => { + if (event.request.url.endsWith('waituntil-forever')) { + event.respondWith(new Promise(() => { + // Use a promise that never resolves to prevent this fetch from + // completing. + })); + } +}); diff --git a/test/wpt/tests/service-workers/service-worker/resources/oninstall-throw-error-from-nested-event-worker.js b/test/wpt/tests/service-workers/service-worker/resources/oninstall-throw-error-from-nested-event-worker.js new file mode 100644 index 00000000000..6729ab61a37 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/oninstall-throw-error-from-nested-event-worker.js @@ -0,0 +1,12 @@ +var max_nesting_level = 8; + +self.addEventListener('message', function(event) { + var level = event.data; + if (level < max_nesting_level) + dispatchEvent(new MessageEvent('message', { data: level + 1 })); + throw Error('error at level ' + level); + }); + +self.addEventListener('install', function(event) { + dispatchEvent(new MessageEvent('message', { data: 1 })); + }); diff --git a/test/wpt/tests/service-workers/service-worker/resources/oninstall-throw-error-then-cancel-worker.js b/test/wpt/tests/service-workers/service-worker/resources/oninstall-throw-error-then-cancel-worker.js new file mode 100644 index 00000000000..c2c499ab1a3 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/oninstall-throw-error-then-cancel-worker.js @@ -0,0 +1,3 @@ +self.onerror = function(event) { return true; }; + +self.addEventListener('install', function(event) { throw new Error(); }); diff --git a/test/wpt/tests/service-workers/service-worker/resources/oninstall-throw-error-then-prevent-default-worker.js b/test/wpt/tests/service-workers/service-worker/resources/oninstall-throw-error-then-prevent-default-worker.js new file mode 100644 index 00000000000..7667c2781d0 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/oninstall-throw-error-then-prevent-default-worker.js @@ -0,0 +1,7 @@ +// Ensure we can handle multiple error handlers. One error handler +// calling preventDefault should cause the event to be treated as +// handled. +self.addEventListener('error', function(event) {}); +self.addEventListener('error', function(event) { event.preventDefault(); }); +self.addEventListener('error', function(event) {}); +self.addEventListener('install', function(event) { throw new Error(); }); diff --git a/test/wpt/tests/service-workers/service-worker/resources/oninstall-throw-error-with-empty-onerror-worker.js b/test/wpt/tests/service-workers/service-worker/resources/oninstall-throw-error-with-empty-onerror-worker.js new file mode 100644 index 00000000000..8f56d1bf149 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/oninstall-throw-error-with-empty-onerror-worker.js @@ -0,0 +1,2 @@ +self.addEventListener('error', function(event) {}); +self.addEventListener('install', function(event) { throw new Error(); }); diff --git a/test/wpt/tests/service-workers/service-worker/resources/oninstall-throw-error-worker.js b/test/wpt/tests/service-workers/service-worker/resources/oninstall-throw-error-worker.js new file mode 100644 index 00000000000..cc2f6d7e5e7 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/oninstall-throw-error-worker.js @@ -0,0 +1,7 @@ +// Ensure we can handle multiple install handlers. One handler throwing an +// error should cause the event dispatch to be treated as having unhandled +// errors. +self.addEventListener('install', function(event) {}); +self.addEventListener('install', function(event) {}); +self.addEventListener('install', function(event) { throw new Error(); }); +self.addEventListener('install', function(event) {}); diff --git a/test/wpt/tests/service-workers/service-worker/resources/oninstall-waituntil-forever.js b/test/wpt/tests/service-workers/service-worker/resources/oninstall-waituntil-forever.js new file mode 100644 index 00000000000..964483f2f42 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/oninstall-waituntil-forever.js @@ -0,0 +1,8 @@ +'use strict'; + +self.addEventListener('install', event => { + event.waitUntil(new Promise(() => { + // Use a promise that never resolves to prevent this service worker from + // advancing past the 'installing' state. + })); +}); diff --git a/test/wpt/tests/service-workers/service-worker/resources/oninstall-waituntil-throw-error-worker.js b/test/wpt/tests/service-workers/service-worker/resources/oninstall-waituntil-throw-error-worker.js new file mode 100644 index 00000000000..6cb8f6ede63 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/oninstall-waituntil-throw-error-worker.js @@ -0,0 +1,5 @@ +self.addEventListener('install', function(event) { + event.waitUntil(new Promise(function(aRequest, aResponse) { + throw new Error(); + })); +}); diff --git a/test/wpt/tests/service-workers/service-worker/resources/onparse-infiniteloop-worker.js b/test/wpt/tests/service-workers/service-worker/resources/onparse-infiniteloop-worker.js new file mode 100644 index 00000000000..6f439aee94d --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/onparse-infiniteloop-worker.js @@ -0,0 +1,8 @@ +'use strict'; + +// Use an infinite loop to prevent this service worker from advancing past the +// 'parsed' state. +let i = 0; +while (true) { + ++i; +} diff --git a/test/wpt/tests/service-workers/service-worker/resources/opaque-response-being-preloaded-xhr.html b/test/wpt/tests/service-workers/service-worker/resources/opaque-response-being-preloaded-xhr.html new file mode 100644 index 00000000000..9c6d8bd504a --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/opaque-response-being-preloaded-xhr.html @@ -0,0 +1,33 @@ + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/opaque-response-preloaded-worker.js b/test/wpt/tests/service-workers/service-worker/resources/opaque-response-preloaded-worker.js new file mode 100644 index 00000000000..4fbe35df277 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/opaque-response-preloaded-worker.js @@ -0,0 +1,12 @@ +importScripts('/common/get-host-info.sub.js'); + +var remoteUrl = get_host_info()['HTTPS_REMOTE_ORIGIN'] + + '/service-workers/service-worker/resources/sample.js' + +self.addEventListener('fetch', event => { + if (!event.request.url.match(/opaque-response\?from=/)) { + return; + } + + event.respondWith(fetch(remoteUrl, {mode: 'no-cors'})); + }); diff --git a/test/wpt/tests/service-workers/service-worker/resources/opaque-response-preloaded-xhr.html b/test/wpt/tests/service-workers/service-worker/resources/opaque-response-preloaded-xhr.html new file mode 100644 index 00000000000..f31ac9b5c4c --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/opaque-response-preloaded-xhr.html @@ -0,0 +1,35 @@ + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/opaque-script-frame.html b/test/wpt/tests/service-workers/service-worker/resources/opaque-script-frame.html new file mode 100644 index 00000000000..a57aacec7c6 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/opaque-script-frame.html @@ -0,0 +1,21 @@ + + + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/opaque-script-large.js b/test/wpt/tests/service-workers/service-worker/resources/opaque-script-large.js new file mode 100644 index 00000000000..7e1c598efc5 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/opaque-script-large.js @@ -0,0 +1,41 @@ +function runScript() { + throw new Error("Intentional error."); +} + +function unused() { + // The following string is intended to be relatively large since some + // browsers trigger different code paths based on script size. + return "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a " + + "tortor ut orci bibendum blandit non quis diam. Aenean sit amet " + + "urna sit amet neque malesuada ultricies at vel nisi. Nunc et lacus " + + "est. Nam posuere erat enim, ac fringilla purus pellentesque " + + "cursus. Proin sodales eleifend lorem, eu semper massa scelerisque " + + "ac. Maecenas pharetra leo malesuada vulputate vulputate. Sed at " + + "efficitur odio. In rhoncus neque varius nibh efficitur gravida. " + + "Curabitur vitae dolor enim. Mauris semper lobortis libero sed " + + "congue. Donec felis ante, fringilla eget urna ut, finibus " + + "hendrerit lacus. Donec at interdum diam. Proin a neque vitae diam " + + "egestas euismod. Mauris posuere elementum lorem, eget convallis " + + "nisl elementum et. In ut leo ac neque dapibus pharetra quis ac " + + "velit. Integer pretium lectus non urna vulputate, in interdum mi " + + "lobortis. Sed laoreet ex et metus pharetra blandit. Curabitur " + + "sollicitudin non neque eu varius. Phasellus posuere congue arcu, " + + "in aliquam nunc fringilla a. Morbi id facilisis libero. Phasellus " + + "metus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. " + + "tortor ut orci bibendum blandit non quis diam. Aenean sit amet " + + "urna sit amet neque malesuada ultricies at vel nisi. Nunc et lacus " + + "est. Nam posuere erat enim, ac fringilla purus pellentesque " + + "cursus. Proin sodales eleifend lorem, eu semper massa scelerisque " + + "ac. Maecenas pharetra leo malesuada vulputate vulputate. Sed at " + + "efficitur odio. In rhoncus neque varius nibh efficitur gravida. " + + "Curabitur vitae dolor enim. Mauris semper lobortis libero sed " + + "congue. Donec felis ante, fringilla eget urna ut, finibus " + + "hendrerit lacus. Donec at interdum diam. Proin a neque vitae diam " + + "egestas euismod. Mauris posuere elementum lorem, eget convallis " + + "nisl elementum et. In ut leo ac neque dapibus pharetra quis ac " + + "velit. Integer pretium lectus non urna vulputate, in interdum mi " + + "lobortis. Sed laoreet ex et metus pharetra blandit. Curabitur " + + "sollicitudin non neque eu varius. Phasellus posuere congue arcu, " + + "in aliquam nunc fringilla a. Morbi id facilisis libero. Phasellus " + + "metus."; +} diff --git a/test/wpt/tests/service-workers/service-worker/resources/opaque-script-small.js b/test/wpt/tests/service-workers/service-worker/resources/opaque-script-small.js new file mode 100644 index 00000000000..8b890985752 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/opaque-script-small.js @@ -0,0 +1,3 @@ +function runScript() { + throw new Error("Intentional error."); +} diff --git a/test/wpt/tests/service-workers/service-worker/resources/opaque-script-sw.js b/test/wpt/tests/service-workers/service-worker/resources/opaque-script-sw.js new file mode 100644 index 00000000000..4d882c617d8 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/opaque-script-sw.js @@ -0,0 +1,37 @@ +importScripts('test-helpers.sub.js'); +importScripts('/common/get-host-info.sub.js'); + +const NAME = 'foo'; +const SAME_ORIGIN_BASE = new URL('./', self.location.href).href; +const CROSS_ORIGIN_BASE = new URL('./', + get_host_info().HTTPS_REMOTE_ORIGIN + base_path()).href; + +const urls = [ + `${SAME_ORIGIN_BASE}opaque-script-small.js`, + `${SAME_ORIGIN_BASE}opaque-script-large.js`, + `${CROSS_ORIGIN_BASE}opaque-script-small.js`, + `${CROSS_ORIGIN_BASE}opaque-script-large.js`, +]; + +self.addEventListener('install', evt => { + evt.waitUntil(async function() { + const c = await caches.open(NAME); + const promises = urls.map(async function(u) { + const r = await fetch(u, { mode: 'no-cors' }); + await c.put(u, r); + }); + await Promise.all(promises); + }()); +}); + +self.addEventListener('fetch', evt => { + const url = new URL(evt.request.url); + if (!url.pathname.includes('opaque-script-small.js') && + !url.pathname.includes('opaque-script-large.js')) { + return; + } + evt.respondWith(async function() { + const c = await caches.open(NAME); + return c.match(evt.request); + }()); +}); diff --git a/test/wpt/tests/service-workers/service-worker/resources/other.html b/test/wpt/tests/service-workers/service-worker/resources/other.html new file mode 100644 index 00000000000..b9f35043877 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/other.html @@ -0,0 +1,3 @@ + +Other +Here's an other html file. diff --git a/test/wpt/tests/service-workers/service-worker/resources/override_assert_object_equals.js b/test/wpt/tests/service-workers/service-worker/resources/override_assert_object_equals.js new file mode 100644 index 00000000000..835046d472b --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/override_assert_object_equals.js @@ -0,0 +1,58 @@ +// .body attribute of Request and Response object are experimental feture. It is +// enabled when --enable-experimental-web-platform-features flag is set. +// Touching this attribute can change the behavior of the objects. To avoid +// touching it while comparing the objects in LayoutTest, we overwrite +// assert_object_equals method. + +(function() { + var original_assert_object_equals = self.assert_object_equals; + function _brand(object) { + return Object.prototype.toString.call(object).match(/^\[object (.*)\]$/)[1]; + } + var assert_request_equals = function(actual, expected, prefix) { + if (typeof actual !== 'object') { + assert_equals(actual, expected, prefix); + return; + } + assert_true(actual instanceof Request, prefix); + assert_true(expected instanceof Request, prefix); + assert_equals(actual.bodyUsed, expected.bodyUsed, prefix + '.bodyUsed'); + assert_equals(actual.method, expected.method, prefix + '.method'); + assert_equals(actual.url, expected.url, prefix + '.url'); + original_assert_object_equals(actual.headers, expected.headers, + prefix + '.headers'); + assert_equals(actual.context, expected.context, prefix + '.context'); + assert_equals(actual.referrer, expected.referrer, prefix + '.referrer'); + assert_equals(actual.mode, expected.mode, prefix + '.mode'); + assert_equals(actual.credentials, expected.credentials, + prefix + '.credentials'); + assert_equals(actual.cache, expected.cache, prefix + '.cache'); + }; + var assert_response_equals = function(actual, expected, prefix) { + if (typeof actual !== 'object') { + assert_equals(actual, expected, prefix); + return; + } + assert_true(actual instanceof Response, prefix); + assert_true(expected instanceof Response, prefix); + assert_equals(actual.bodyUsed, expected.bodyUsed, prefix + '.bodyUsed'); + assert_equals(actual.type, expected.type, prefix + '.type'); + assert_equals(actual.url, expected.url, prefix + '.url'); + assert_equals(actual.status, expected.status, prefix + '.status'); + assert_equals(actual.statusText, expected.statusText, + prefix + '.statusText'); + original_assert_object_equals(actual.headers, expected.headers, + prefix + '.headers'); + }; + var assert_object_equals = function(actual, expected, description) { + var prefix = (description ? description + ': ' : '') + _brand(expected); + if (expected instanceof Request) { + assert_request_equals(actual, expected, prefix); + } else if (expected instanceof Response) { + assert_response_equals(actual, expected, prefix); + } else { + original_assert_object_equals(actual, expected, description); + } + }; + self.assert_object_equals = assert_object_equals; +})(); diff --git a/test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-iframe-claim.html b/test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-iframe-claim.html new file mode 100644 index 00000000000..12b048ee04c --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-iframe-claim.html @@ -0,0 +1,59 @@ + +Service Worker: 3P iframe for partitioned service workers + + + + + + + \ No newline at end of file diff --git a/test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-nested-iframe-child.html b/test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-nested-iframe-child.html new file mode 100644 index 00000000000..d05fef48bf0 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-nested-iframe-child.html @@ -0,0 +1,44 @@ + +Service Worker: Innermost nested iframe for partitioned service workers + + + + + +Innermost 1p iframe (A2) with 3p ancestor (A1-B-A2-A3): this iframe will +register a service worker when it loads and then add its own iframe (A3) that +will attempt to navigate to a url. ServiceWorker will intercept this navigation +and resolve the ServiceWorker's internal Promise. When +ThirdPartyStoragePartitioning is enabled, this iframe should be partitioned +from the main frame and should not share a ServiceWorker. + + \ No newline at end of file diff --git a/test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-nested-iframe-parent.html b/test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-nested-iframe-parent.html new file mode 100644 index 00000000000..f748e2f78d9 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-nested-iframe-parent.html @@ -0,0 +1,30 @@ + +Service Worker: Middle nested iframe for partitioned service workers + + + + + +Middle of the nested iframes (3p ancestor or B in A1-B-A2). + + \ No newline at end of file diff --git a/test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe-getRegistrations.html b/test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe-getRegistrations.html new file mode 100644 index 00000000000..747c0589460 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe-getRegistrations.html @@ -0,0 +1,40 @@ + +Service Worker: 3P iframe for partitioned service workers + + + + + + This iframe will register a service worker when it loads and then will use + getRegistrations to get a handle to the SW. It will then postMessage to the + SW to retrieve the SW's ID. This iframe will then forward that message up, + eventually, to the test. + + \ No newline at end of file diff --git a/test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe-matchAll.html b/test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe-matchAll.html new file mode 100644 index 00000000000..7a2c36693e1 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe-matchAll.html @@ -0,0 +1,27 @@ + +Service Worker: 3P iframe for partitioned service workers + + + + + + This iframe will register a service worker when it loads and then will use + getRegistrations to get a handle to the SW. It will then postMessage to the + SW to get the SW's clients via matchAll(). This iframe will then forward the + SW's response up, eventually, to the test. + + \ No newline at end of file diff --git a/test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe.html new file mode 100644 index 00000000000..1b7f671b371 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-iframe.html @@ -0,0 +1,36 @@ + +Service Worker: 3P iframe for partitioned service workers + + + + + + +This iframe will register a service worker when it loads and then add its own +iframe that will attempt to navigate to a url that service worker will intercept +and use to resolve the service worker's internal Promise. + + \ No newline at end of file diff --git a/test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-window.html b/test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-window.html new file mode 100644 index 00000000000..86384ce2808 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/partitioned-service-worker-third-party-window.html @@ -0,0 +1,41 @@ + +Service Worker: 3P window for partitioned service workers + + + + + +This page should be opened as a third-party window. It then loads an iframe +specified by the query parameter. Finally it forwards the postMessage from the +iframe up to the opener (the test). + + + \ No newline at end of file diff --git a/test/wpt/tests/service-workers/service-worker/resources/partitioned-storage-sw.js b/test/wpt/tests/service-workers/service-worker/resources/partitioned-storage-sw.js new file mode 100644 index 00000000000..00f79798103 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/partitioned-storage-sw.js @@ -0,0 +1,81 @@ +// Holds the promise that the "resolve.fakehtml" call attempts to resolve. +// This is "the SW's promise" that other parts of the test refer to. +var promise; +// Stores the resolve funcution for the current promise. +var pending_resolve_func = null; +// Unique ID to determine which service worker is being used. +const ID = Math.random(); + +function callAndResetResolve() { + var local_resolve = pending_resolve_func; + pending_resolve_func = null; + local_resolve(); +} + +self.addEventListener('fetch', function(event) { + fetchEventHandler(event); +}) + +self.addEventListener('message', (event) => { + event.waitUntil(async function() { + if(!event.data) + return; + + if (event.data.type === "get-id") { + event.source.postMessage({ID: ID}); + } + else if(event.data.type === "get-match-all") { + clients.matchAll({includeUncontrolled: true}).then(clients_list => { + const url_list = clients_list.map(item => item.url); + event.source.postMessage({urls_list: url_list}); + }); + } + else if(event.data.type === "claim") { + await clients.claim(); + } + }()); +}); + +async function fetchEventHandler(event){ + var request_url = new URL(event.request.url); + var url_search = request_url.search.substr(1); + request_url.search = ""; + if ( request_url.href.endsWith('waitUntilResolved.fakehtml') ) { + + if (pending_resolve_func != null) { + // Respond with an error if there is already a pending promise + event.respondWith(Response.error()); + return; + } + + // Create the new promise. + promise = new Promise(function(resolve) { + pending_resolve_func = resolve; + }); + event.waitUntil(promise); + + event.respondWith(new Response(` + + Promise created by ${url_search} + + + `, {headers: {'Content-Type': 'text/html'}} + )); + + } + else if ( request_url.href.endsWith('resolve.fakehtml') ) { + var has_pending = !!pending_resolve_func; + event.respondWith(new Response(` + + Promise settled for ${url_search} + + + `, {headers: {'Content-Type': 'text/html'}})); + + if (has_pending) { + callAndResetResolve(); + } + } +} \ No newline at end of file diff --git a/test/wpt/tests/service-workers/service-worker/resources/partitioned-utils.js b/test/wpt/tests/service-workers/service-worker/resources/partitioned-utils.js new file mode 100644 index 00000000000..22e90beaec7 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/partitioned-utils.js @@ -0,0 +1,110 @@ +// The resolve function for the current pending event listener's promise. +// It is nulled once the promise is resolved. +var message_event_promise_resolve = null; + +function messageEventHandler(evt) { + if (message_event_promise_resolve) { + local_resolve = message_event_promise_resolve; + message_event_promise_resolve = null; + local_resolve(evt.data); + } +} + +function makeMessagePromise() { + if (message_event_promise_resolve != null) { + // Do not create a new promise until the previous is settled. + return; + } + + return new Promise(resolve => { + message_event_promise_resolve = resolve; + }); +} + +// Loads a url for the frame type and then returns a promise for +// the data that was postMessage'd from the loaded frame. +// If the frame type is 'window' then `url` is encoded into the search param +// as the url the 3p window is meant to iframe. +function loadAndReturnSwData(t, url, frame_type) { + if (frame_type !== 'iframe' && frame_type !== 'window') { + return; + } + + const message_promise = makeMessagePromise(); + + // Create the iframe or window and then return the promise for data. + if ( frame_type === 'iframe' ) { + const frame = with_iframe(url, false); + t.add_cleanup(async () => { + const f = await frame; + f.remove(); + }); + } + else { + // 'window' case. + const search_param = new URLSearchParams(); + search_param.append('target', url); + + const third_party_window_url = new URL( + './resources/partitioned-service-worker-third-party-window.html' + + '?' + search_param, + get_host_info().HTTPS_NOTSAMESITE_ORIGIN + self.location.pathname); + + const w = window.open(third_party_window_url); + t.add_cleanup(() => w.close()); + } + + return message_promise; +} + +// Checks for an existing service worker registration. If not present, +// registers and maintains a service worker. Used in windows or iframes +// that will be partitioned from the main frame. +async function setupServiceWorker() { + + const script = './partitioned-storage-sw.js'; + const scope = './partitioned-'; + + var reg = await navigator.serviceWorker.register(script, { scope: scope }); + + // We should keep track if we installed a worker or not. If we did then we + // need to uninstall it. Otherwise we let the top level test uninstall it + // (If partitioning is not working). + var installed_a_worker = true; + await new Promise(resolve => { + // Check if a worker is already activated. + var worker = reg.active; + // If so, just resolve. + if ( worker ) { + installed_a_worker = false; + resolve(); + return; + } + + //Otherwise check if one is waiting. + worker = reg.waiting; + // If not waiting, grab the installing worker. + if ( !worker ) { + worker = reg.installing; + } + + // Resolve once it's activated. + worker.addEventListener('statechange', evt => { + if (worker.state === 'activated') { + resolve(); + } + }); + }); + + self.addEventListener('unload', async () => { + // If we didn't install a worker then that means the top level test did, and + // that test is therefore responsible for cleaning it up. + if ( !installed_a_worker ) { + return; + } + + await reg.unregister(); + }); + + return reg; +} \ No newline at end of file diff --git a/test/wpt/tests/service-workers/service-worker/resources/pass-through-worker.js b/test/wpt/tests/service-workers/service-worker/resources/pass-through-worker.js new file mode 100644 index 00000000000..5eaf48d5887 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/pass-through-worker.js @@ -0,0 +1,3 @@ +addEventListener('fetch', evt => { + evt.respondWith(fetch(evt.request)); +}); diff --git a/test/wpt/tests/service-workers/service-worker/resources/pass.txt b/test/wpt/tests/service-workers/service-worker/resources/pass.txt new file mode 100644 index 00000000000..7ef22e9a431 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/pass.txt @@ -0,0 +1 @@ +PASS diff --git a/test/wpt/tests/service-workers/service-worker/resources/performance-timeline-worker.js b/test/wpt/tests/service-workers/service-worker/resources/performance-timeline-worker.js new file mode 100644 index 00000000000..6c6dfcbd281 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/performance-timeline-worker.js @@ -0,0 +1,62 @@ +importScripts('/resources/testharness.js'); + +promise_test(function(test) { + var durationMsec = 100; + // There are limits to our accuracy here. Timers may fire up to a + // millisecond early due to platform-dependent rounding. In addition + // the performance API introduces some rounding as well to prevent + // timing attacks. + var accuracy = 1.5; + return new Promise(function(resolve) { + performance.mark('startMark'); + setTimeout(resolve, durationMsec); + }).then(function() { + performance.mark('endMark'); + performance.measure('measure', 'startMark', 'endMark'); + var startMark = performance.getEntriesByName('startMark')[0]; + var endMark = performance.getEntriesByName('endMark')[0]; + var measure = performance.getEntriesByType('measure')[0]; + assert_equals(measure.startTime, startMark.startTime); + assert_approx_equals(endMark.startTime - startMark.startTime, + measure.duration, 0.001); + assert_greater_than(measure.duration, durationMsec - accuracy); + assert_equals(performance.getEntriesByType('mark').length, 2); + assert_equals(performance.getEntriesByType('measure').length, 1); + performance.clearMarks('startMark'); + performance.clearMeasures('measure'); + assert_equals(performance.getEntriesByType('mark').length, 1); + assert_equals(performance.getEntriesByType('measure').length, 0); + }); + }, 'User Timing'); + +promise_test(function(test) { + return fetch('sample.txt') + .then(function(resp) { + return resp.text(); + }) + .then(function(text) { + var expectedResources = ['testharness.js', 'sample.txt']; + assert_equals(performance.getEntriesByType('resource').length, expectedResources.length); + for (var i = 0; i < expectedResources.length; i++) { + var entry = performance.getEntriesByType('resource')[i]; + assert_true(entry.name.endsWith(expectedResources[i])); + assert_equals(entry.workerStart, 0); + assert_greater_than(entry.startTime, 0); + assert_greater_than(entry.responseEnd, entry.startTime); + } + return new Promise(function(resolve) { + performance.onresourcetimingbufferfull = _ => { + resolve('bufferfull'); + } + performance.setResourceTimingBufferSize(expectedResources.length); + fetch('sample.txt'); + }); + }) + .then(function(result) { + assert_equals(result, 'bufferfull'); + performance.clearResourceTimings(); + assert_equals(performance.getEntriesByType('resource').length, 0); + }) + }, 'Resource Timing'); + +done(); diff --git a/test/wpt/tests/service-workers/service-worker/resources/postmessage-blob-url.js b/test/wpt/tests/service-workers/service-worker/resources/postmessage-blob-url.js new file mode 100644 index 00000000000..9095194a4c0 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/postmessage-blob-url.js @@ -0,0 +1,5 @@ +self.onmessage = e => { + fetch(e.data) + .then(response => response.text()) + .then(text => e.source.postMessage('Worker reply:' + text)); +}; diff --git a/test/wpt/tests/service-workers/service-worker/resources/postmessage-dictionary-transferables-worker.js b/test/wpt/tests/service-workers/service-worker/resources/postmessage-dictionary-transferables-worker.js new file mode 100644 index 00000000000..87a4500d754 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/postmessage-dictionary-transferables-worker.js @@ -0,0 +1,24 @@ +var messageHandler = function(port, e) { + var text_decoder = new TextDecoder; + port.postMessage({ + content: text_decoder.decode(e.data), + byteLength: e.data.byteLength + }); + + // Send back the array buffer via Client.postMessage. + port.postMessage(e.data, {transfer: [e.data.buffer]}); + + port.postMessage({ + content: text_decoder.decode(e.data), + byteLength: e.data.byteLength + }); +}; + +self.addEventListener('message', e => { + if (e.ports[0]) { + // Wait for messages sent via MessagePort. + e.ports[0].onmessage = messageHandler.bind(null, e.ports[0]); + return; + } + messageHandler(e.source, e); + }); diff --git a/test/wpt/tests/service-workers/service-worker/resources/postmessage-echo-worker.js b/test/wpt/tests/service-workers/service-worker/resources/postmessage-echo-worker.js new file mode 100644 index 00000000000..f088ad12780 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/postmessage-echo-worker.js @@ -0,0 +1,3 @@ +self.addEventListener('message', event => { + event.source.postMessage(event.data); +}); diff --git a/test/wpt/tests/service-workers/service-worker/resources/postmessage-fetched-text.js b/test/wpt/tests/service-workers/service-worker/resources/postmessage-fetched-text.js new file mode 100644 index 00000000000..9fc67171d05 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/postmessage-fetched-text.js @@ -0,0 +1,5 @@ +self.onmessage = async (e) => { + const response = await fetch(e.data); + const text = await response.text(); + self.postMessage(text); +}; diff --git a/test/wpt/tests/service-workers/service-worker/resources/postmessage-msgport-to-client-worker.js b/test/wpt/tests/service-workers/service-worker/resources/postmessage-msgport-to-client-worker.js new file mode 100644 index 00000000000..7af935f4f8f --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/postmessage-msgport-to-client-worker.js @@ -0,0 +1,19 @@ +self.onmessage = function(e) { + e.waitUntil(self.clients.matchAll().then(function(clients) { + clients.forEach(function(client) { + var messageChannel = new MessageChannel(); + messageChannel.port1.onmessage = + onMessageViaMessagePort.bind(null, messageChannel.port1); + client.postMessage(undefined, [messageChannel.port2]); + }); + })); +}; + +function onMessageViaMessagePort(port, e) { + var message = e.data; + if ('value' in message) { + port.postMessage({ack: 'Acking value: ' + message.value}); + } else if ('done' in message) { + port.postMessage({done: true}); + } +} diff --git a/test/wpt/tests/service-workers/service-worker/resources/postmessage-on-load-worker.js b/test/wpt/tests/service-workers/service-worker/resources/postmessage-on-load-worker.js new file mode 100644 index 00000000000..c2b0bcb8bfb --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/postmessage-on-load-worker.js @@ -0,0 +1,9 @@ +if ('DedicatedWorkerGlobalScope' in self && + self instanceof DedicatedWorkerGlobalScope) { + postMessage('dedicated worker script loaded'); +} else if ('SharedWorkerGlobalScope' in self && + self instanceof SharedWorkerGlobalScope) { + self.onconnect = evt => { + evt.ports[0].postMessage('shared worker script loaded'); + }; +} diff --git a/test/wpt/tests/service-workers/service-worker/resources/postmessage-to-client-worker.js b/test/wpt/tests/service-workers/service-worker/resources/postmessage-to-client-worker.js new file mode 100644 index 00000000000..17913063583 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/postmessage-to-client-worker.js @@ -0,0 +1,10 @@ +self.onmessage = function(e) { + e.waitUntil(self.clients.matchAll().then(function(clients) { + clients.forEach(function(client) { + client.postMessage('Sending message via clients'); + if (!Array.isArray(clients)) + client.postMessage('clients is not an array'); + client.postMessage('quit'); + }); + })); +}; diff --git a/test/wpt/tests/service-workers/service-worker/resources/postmessage-transferables-worker.js b/test/wpt/tests/service-workers/service-worker/resources/postmessage-transferables-worker.js new file mode 100644 index 00000000000..d35c1c952b8 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/postmessage-transferables-worker.js @@ -0,0 +1,24 @@ +var messageHandler = function(port, e) { + var text_decoder = new TextDecoder; + port.postMessage({ + content: text_decoder.decode(e.data), + byteLength: e.data.byteLength + }); + + // Send back the array buffer via Client.postMessage. + port.postMessage(e.data, [e.data.buffer]); + + port.postMessage({ + content: text_decoder.decode(e.data), + byteLength: e.data.byteLength + }); +}; + +self.addEventListener('message', e => { + if (e.ports[0]) { + // Wait for messages sent via MessagePort. + e.ports[0].onmessage = messageHandler.bind(null, e.ports[0]); + return; + } + messageHandler(e.source, e); + }); diff --git a/test/wpt/tests/service-workers/service-worker/resources/postmessage-worker.js b/test/wpt/tests/service-workers/service-worker/resources/postmessage-worker.js new file mode 100644 index 00000000000..858cf04267c --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/postmessage-worker.js @@ -0,0 +1,19 @@ +var port; + +// Exercise the 'onmessage' handler: +self.onmessage = function(e) { + var message = e.data; + if ('port' in message) { + port = message.port; + } +}; + +// And an event listener: +self.addEventListener('message', function(e) { + var message = e.data; + if ('value' in message) { + port.postMessage('Acking value: ' + message.value); + } else if ('done' in message) { + port.postMessage('quit'); + } + }); diff --git a/test/wpt/tests/service-workers/service-worker/resources/range-request-to-different-origins-worker.js b/test/wpt/tests/service-workers/service-worker/resources/range-request-to-different-origins-worker.js new file mode 100644 index 00000000000..cab60583390 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/range-request-to-different-origins-worker.js @@ -0,0 +1,40 @@ +// This worker is meant to test range requests where the responses come from +// multiple origins. It forwards the first request to a cross-origin URL +// (generating an opaque response). The server is expected to return a 206 +// Partial Content response. Then the worker lets subsequent range requests +// fall back to network (generating same-origin responses). The intent is to try +// to trick the browser into treating the resource as same-origin. +// +// It would also be interesting to do the reverse test where the first request +// goes to the same-origin URL, and subsequent range requests go cross-origin in +// 'no-cors' mode to receive opaque responses. But the service worker cannot do +// this, because in 'no-cors' mode the 'range' HTTP header is disallowed. + +importScripts('/common/get-host-info.sub.js') + +let initial = true; +function is_initial_request() { + const old = initial; + initial = false; + return old; +} + +self.addEventListener('fetch', e => { + const url = new URL(e.request.url); + if (url.search.indexOf('VIDEO') == -1) { + // Fall back for non-video. + return; + } + + // Make the first request go cross-origin. + if (is_initial_request()) { + const cross_origin_url = get_host_info().HTTPS_REMOTE_ORIGIN + + url.pathname + url.search; + const cross_origin_request = new Request(cross_origin_url, + {mode: 'no-cors', headers: e.request.headers}); + e.respondWith(fetch(cross_origin_request)); + return; + } + + // Fall back to same origin for subsequent range requests. + }); diff --git a/test/wpt/tests/service-workers/service-worker/resources/range-request-with-different-cors-modes-worker.js b/test/wpt/tests/service-workers/service-worker/resources/range-request-with-different-cors-modes-worker.js new file mode 100644 index 00000000000..7580b0b68a9 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/range-request-with-different-cors-modes-worker.js @@ -0,0 +1,60 @@ +// This worker is meant to test range requests where the responses are a mix of +// opaque ones and non-opaque ones. It forwards the first request to a +// cross-origin URL (generating an opaque response). The server is expected to +// return a 206 Partial Content response. Then the worker forwards subsequent +// range requests to that URL, with CORS sharing generating a non-opaque +// responses. The intent is to try to trick the browser into treating the +// resource as non-opaque. +// +// It would also be interesting to do the reverse test where the first request +// uses 'cors', and subsequent range requests use 'no-cors' mode. But the +// service worker cannot do this, because in 'no-cors' mode the 'range' HTTP +// header is disallowed. + +importScripts('/common/get-host-info.sub.js') + +let initial = true; +function is_initial_request() { + const old = initial; + initial = false; + return old; +} + +self.addEventListener('fetch', e => { + const url = new URL(e.request.url); + if (url.search.indexOf('VIDEO') == -1) { + // Fall back for non-video. + return; + } + + let cross_origin_url = get_host_info().HTTPS_REMOTE_ORIGIN + + url.pathname + url.search; + + // The first request is no-cors. + if (is_initial_request()) { + const init = { mode: 'no-cors', headers: e.request.headers }; + const cross_origin_request = new Request(cross_origin_url, init); + e.respondWith(fetch(cross_origin_request)); + return; + } + + // Subsequent range requests are cors. + + // Copy headers needed for range requests. + let my_headers = new Headers; + if (e.request.headers.get('accept')) + my_headers.append('accept', e.request.headers.get('accept')); + if (e.request.headers.get('range')) + my_headers.append('range', e.request.headers.get('range')); + + // Add &ACAOrigin to allow CORS. + cross_origin_url += '&ACAOrigin=' + get_host_info().HTTPS_ORIGIN; + // Add &ACAHeaders to allow range requests. + cross_origin_url += '&ACAHeaders=accept,range'; + + // Make the CORS request. + const init = { mode: 'cors', headers: my_headers }; + const cross_origin_request = new Request(cross_origin_url, init); + e.respondWith(fetch(cross_origin_request)); + }); + diff --git a/test/wpt/tests/service-workers/service-worker/resources/redirect-worker.js b/test/wpt/tests/service-workers/service-worker/resources/redirect-worker.js new file mode 100644 index 00000000000..82e21fc26fd --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/redirect-worker.js @@ -0,0 +1,145 @@ +// We store an empty response for each fetch event request we see +// in this Cache object so we can get the list of urls in the +// message event. +var cacheName = 'urls-' + self.registration.scope; + +var waitUntilPromiseList = []; + +// Sends the requests seen by this worker. The output is: +// { +// requestInfos: [ +// {url: url1, resultingClientId: id1}, +// {url: url2, resultingClientId: id2}, +// ] +// } +async function getRequestInfos(event) { + // Wait for fetch events to finish. + await Promise.all(waitUntilPromiseList); + waitUntilPromiseList = []; + + // Generate the message. + const cache = await caches.open(cacheName); + const requestList = await cache.keys(); + const requestInfos = []; + for (let i = 0; i < requestList.length; i++) { + const response = await cache.match(requestList[i]); + const body = await response.json(); + requestInfos[i] = { + url: requestList[i].url, + resultingClientId: body.resultingClientId + }; + } + await caches.delete(cacheName); + + event.data.port.postMessage({requestInfos}); +} + +// Sends the results of clients.get(id) from this worker. The +// input is: +// { +// actual_ids: {a: id1, b: id2, x: id3} +// } +// +// The output is: +// { +// clients: { +// a: {found: false}, +// b: {found: false}, +// x: { +// id: id3, +// url: url1, +// found: true +// } +// } +// } +async function getClients(event) { + // |actual_ids| is like: + // {a: id1, b: id2, x: id3} + const actual_ids = event.data.actual_ids; + const result = {} + for (let key of Object.keys(actual_ids)) { + const id = actual_ids[key]; + const client = await self.clients.get(id); + if (client === undefined) + result[key] = {found: false}; + else + result[key] = {found: true, url: client.url, id: client.id}; + } + event.data.port.postMessage({clients: result}); +} + +self.addEventListener('message', async function(event) { + if (event.data.command == 'getRequestInfos') { + event.waitUntil(getRequestInfos(event)); + return; + } + + if (event.data.command == 'getClients') { + event.waitUntil(getClients(event)); + return; + } +}); + +function get_query_params(url) { + var search = (new URL(url)).search; + if (!search) { + return {}; + } + var ret = {}; + var params = search.substring(1).split('&'); + params.forEach(function(param) { + var element = param.split('='); + ret[decodeURIComponent(element[0])] = decodeURIComponent(element[1]); + }); + return ret; +} + +self.addEventListener('fetch', function(event) { + var waitUntilPromise = caches.open(cacheName).then(function(cache) { + const responseBody = {}; + responseBody['resultingClientId'] = event.resultingClientId; + const headers = new Headers({'Content-Type': 'application/json'}); + const response = new Response(JSON.stringify(responseBody), {headers}); + return cache.put(event.request, response); + }); + event.waitUntil(waitUntilPromise); + + var params = get_query_params(event.request.url); + if (!params['sw']) { + // To avoid races, add the waitUntil() promise to our global list. + // If we get a message event before we finish here, it will wait + // these promises to complete before proceeding to read from the + // cache. + waitUntilPromiseList.push(waitUntilPromise); + return; + } + + event.respondWith(waitUntilPromise.then(async () => { + if (params['sw'] == 'gen') { + return Response.redirect(params['url']); + } else if (params['sw'] == 'gen-manual') { + // Note this differs from Response.redirect() in that relative URLs are + // preserved. + return new Response("", { + status: 301, + headers: {location: params['url']}, + }); + } else if (params['sw'] == 'fetch') { + return fetch(event.request); + } else if (params['sw'] == 'fetch-url') { + return fetch(params['url']); + } else if (params['sw'] == 'follow') { + return fetch(new Request(event.request.url, {redirect: 'follow'})); + } else if (params['sw'] == 'manual') { + return fetch(new Request(event.request.url, {redirect: 'manual'})); + } else if (params['sw'] == 'manualThroughCache') { + const url = event.request.url; + await caches.delete(url) + const cache = await self.caches.open(url); + const response = await fetch(new Request(url, {redirect: 'manual'})); + await cache.put(event.request, response); + return cache.match(url); + } + // unexpected... trigger an interception failure + })); + }); diff --git a/test/wpt/tests/service-workers/service-worker/resources/redirect.py b/test/wpt/tests/service-workers/service-worker/resources/redirect.py new file mode 100644 index 00000000000..bd559d5d1e2 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/redirect.py @@ -0,0 +1,27 @@ +from wptserve.utils import isomorphic_decode + +def main(request, response): + if b'Status' in request.GET: + status = int(request.GET[b"Status"]) + else: + status = 302 + + headers = [] + + url = isomorphic_decode(request.GET[b'Redirect']) + headers.append((b"Location", url)) + + if b"ACAOrigin" in request.GET: + for item in request.GET[b"ACAOrigin"].split(b","): + headers.append((b"Access-Control-Allow-Origin", item)) + + for suffix in [b"Headers", b"Methods", b"Credentials"]: + query = b"ACA%s" % suffix + header = b"Access-Control-Allow-%s" % suffix + if query in request.GET: + headers.append((header, request.GET[query])) + + if b"ACEHeaders" in request.GET: + headers.append((b"Access-Control-Expose-Headers", request.GET[b"ACEHeaders"])) + + return status, headers, b"" diff --git a/test/wpt/tests/service-workers/service-worker/resources/referer-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/referer-iframe.html new file mode 100644 index 00000000000..295ff456715 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/referer-iframe.html @@ -0,0 +1,39 @@ + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/referrer-policy-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/referrer-policy-iframe.html new file mode 100644 index 00000000000..9ef3cd19a98 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/referrer-policy-iframe.html @@ -0,0 +1,32 @@ + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/register-closed-window-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/register-closed-window-iframe.html new file mode 100644 index 00000000000..117f25477b0 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/register-closed-window-iframe.html @@ -0,0 +1,19 @@ + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/register-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/register-iframe.html new file mode 100644 index 00000000000..f5a040e41d9 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/register-iframe.html @@ -0,0 +1,4 @@ + diff --git a/test/wpt/tests/service-workers/service-worker/resources/register-rewrite-worker.html b/test/wpt/tests/service-workers/service-worker/resources/register-rewrite-worker.html new file mode 100644 index 00000000000..bf06317ad9e --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/register-rewrite-worker.html @@ -0,0 +1,32 @@ + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/registration-tests-mime-types.js b/test/wpt/tests/service-workers/service-worker/resources/registration-tests-mime-types.js new file mode 100644 index 00000000000..037e6c0fde2 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/registration-tests-mime-types.js @@ -0,0 +1,96 @@ +// Registration tests that mostly verify the MIME type. +// +// This file tests every MIME type so it necessarily starts many service +// workers, so it may be slow. +function registration_tests_mime_types(register_method) { + promise_test(function(t) { + var script = 'resources/mime-type-worker.py'; + var scope = 'resources/scope/no-mime-type-worker/'; + return promise_rejects_dom(t, + 'SecurityError', + register_method(script, {scope: scope}), + 'Registration of no MIME type script should fail.'); + }, 'Registering script with no MIME type'); + + promise_test(function(t) { + var script = 'resources/mime-type-worker.py?mime=text/plain'; + var scope = 'resources/scope/bad-mime-type-worker/'; + return promise_rejects_dom(t, + 'SecurityError', + register_method(script, {scope: scope}), + 'Registration of plain text script should fail.'); + }, 'Registering script with bad MIME type'); + + /** + * ServiceWorkerContainer.register() should throw a TypeError, according to + * step 17.1 of https://w3c.github.io/ServiceWorker/#importscripts + * + * "[17] If an uncaught runtime script error occurs during the above step, then: + * [17.1] Invoke Reject Job Promise with job and TypeError" + * + * (Where the "uncaught runtime script error" is thrown by an unsuccessful + * importScripts()) + */ + promise_test(function(t) { + var script = 'resources/import-mime-type-worker.py'; + var scope = 'resources/scope/no-mime-type-worker/'; + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'Registration of no MIME type imported script should fail.'); + }, 'Registering script that imports script with no MIME type'); + + promise_test(function(t) { + var script = 'resources/import-mime-type-worker.py?mime=text/plain'; + var scope = 'resources/scope/bad-mime-type-worker/'; + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'Registration of plain text imported script should fail.'); + }, 'Registering script that imports script with bad MIME type'); + + const validMimeTypes = [ + 'application/ecmascript', + 'application/javascript', + 'application/x-ecmascript', + 'application/x-javascript', + 'text/ecmascript', + 'text/javascript', + 'text/javascript1.0', + 'text/javascript1.1', + 'text/javascript1.2', + 'text/javascript1.3', + 'text/javascript1.4', + 'text/javascript1.5', + 'text/jscript', + 'text/livescript', + 'text/x-ecmascript', + 'text/x-javascript' + ]; + + for (const validMimeType of validMimeTypes) { + promise_test(() => { + var script = `resources/mime-type-worker.py?mime=${validMimeType}`; + var scope = 'resources/scope/good-mime-type-worker/'; + + return register_method(script, {scope}).then(registration => { + assert_true( + registration instanceof ServiceWorkerRegistration, + 'Successfully registered.'); + return registration.unregister(); + }); + }, `Registering script with good MIME type ${validMimeType}`); + + promise_test(() => { + var script = `resources/import-mime-type-worker.py?mime=${validMimeType}`; + var scope = 'resources/scope/good-mime-type-worker/'; + + return register_method(script, { scope }).then(registration => { + assert_true( + registration instanceof ServiceWorkerRegistration, + 'Successfully registered.'); + return registration.unregister(); + }); + }, `Registering script that imports script with good MIME type ${validMimeType}`); + } +} diff --git a/test/wpt/tests/service-workers/service-worker/resources/registration-tests-scope.js b/test/wpt/tests/service-workers/service-worker/resources/registration-tests-scope.js new file mode 100644 index 00000000000..30c424b2b42 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/registration-tests-scope.js @@ -0,0 +1,120 @@ +// Registration tests that mostly exercise the scope option. +function registration_tests_scope(register_method) { + promise_test(function(t) { + var script = 'resources/empty-worker.js'; + var scope = 'resources/scope%2fencoded-slash-in-scope'; + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'URL-encoded slash in the scope should be rejected.'); + }, 'Scope including URL-encoded slash'); + + promise_test(function(t) { + var script = 'resources/empty-worker.js'; + var scope = 'resources/scope%5cencoded-slash-in-scope'; + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'URL-encoded backslash in the scope should be rejected.'); + }, 'Scope including URL-encoded backslash'); + + promise_test(function(t) { + var script = 'resources/empty-worker.js'; + var scope = 'data:text/html,'; + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'scope URL scheme is not "http" or "https"'); + }, 'Scope URL scheme is a data: URL'); + + promise_test(function(t) { + var script = 'resources/empty-worker.js'; + var scope = new URL('resources', location).href.replace('https:', 'ftp:'); + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'scope URL scheme is not "http" or "https"'); + }, 'Scope URL scheme is an ftp: URL'); + + promise_test(function(t) { + // URL-encoded full-width 'scope'. + var name = '%ef%bd%93%ef%bd%83%ef%bd%8f%ef%bd%90%ef%bd%85'; + var script = 'resources/empty-worker.js'; + var scope = 'resources/' + name + '/escaped-multibyte-character-scope'; + return register_method(script, {scope: scope}) + .then(function(registration) { + assert_equals( + registration.scope, + normalizeURL(scope), + 'URL-encoded multibyte characters should be available.'); + return registration.unregister(); + }); + }, 'Scope including URL-encoded multibyte characters'); + + promise_test(function(t) { + // Non-URL-encoded full-width "scope". + var name = String.fromCodePoint(0xff53, 0xff43, 0xff4f, 0xff50, 0xff45); + var script = 'resources/empty-worker.js'; + var scope = 'resources/' + name + '/non-escaped-multibyte-character-scope'; + return register_method(script, {scope: scope}) + .then(function(registration) { + assert_equals( + registration.scope, + normalizeURL(scope), + 'Non-URL-encoded multibyte characters should be available.'); + return registration.unregister(); + }); + }, 'Scope including non-escaped multibyte characters'); + + promise_test(function(t) { + var script = 'resources/empty-worker.js'; + var scope = 'resources/././scope/self-reference-in-scope'; + return register_method(script, {scope: scope}) + .then(function(registration) { + assert_equals( + registration.scope, + normalizeURL('resources/scope/self-reference-in-scope'), + 'Scope including self-reference should be normalized.'); + return registration.unregister(); + }); + }, 'Scope including self-reference'); + + promise_test(function(t) { + var script = 'resources/empty-worker.js'; + var scope = 'resources/../resources/scope/parent-reference-in-scope'; + return register_method(script, {scope: scope}) + .then(function(registration) { + assert_equals( + registration.scope, + normalizeURL('resources/scope/parent-reference-in-scope'), + 'Scope including parent-reference should be normalized.'); + return registration.unregister(); + }); + }, 'Scope including parent-reference'); + + promise_test(function(t) { + var script = 'resources/empty-worker.js'; + var scope = 'resources/scope////consecutive-slashes-in-scope'; + return register_method(script, {scope: scope}) + .then(function(registration) { + // Although consecutive slashes in the scope are not unified, the + // scope is under the script directory and registration should + // succeed. + assert_equals( + registration.scope, + normalizeURL(scope), + 'Should successfully be registered.'); + return registration.unregister(); + }) + }, 'Scope including consecutive slashes'); + + promise_test(function(t) { + var script = 'resources/empty-worker.js'; + var scope = 'filesystem:' + normalizeURL('resources/scope/filesystem-scope-url'); + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'Registering with the scope that has same-origin filesystem: URL ' + + 'should fail with TypeError.'); + }, 'Scope URL is same-origin filesystem: URL'); +} diff --git a/test/wpt/tests/service-workers/service-worker/resources/registration-tests-script-url.js b/test/wpt/tests/service-workers/service-worker/resources/registration-tests-script-url.js new file mode 100644 index 00000000000..55cbe6fa959 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/registration-tests-script-url.js @@ -0,0 +1,82 @@ +// Registration tests that mostly exercise the scriptURL parameter. +function registration_tests_script_url(register_method) { + promise_test(function(t) { + var script = 'resources%2fempty-worker.js'; + var scope = 'resources/scope/encoded-slash-in-script-url'; + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'URL-encoded slash in the script URL should be rejected.'); + }, 'Script URL including URL-encoded slash'); + + promise_test(function(t) { + var script = 'resources%2Fempty-worker.js'; + var scope = 'resources/scope/encoded-slash-in-script-url'; + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'URL-encoded slash in the script URL should be rejected.'); + }, 'Script URL including uppercase URL-encoded slash'); + + promise_test(function(t) { + var script = 'resources%5cempty-worker.js'; + var scope = 'resources/scope/encoded-slash-in-script-url'; + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'URL-encoded backslash in the script URL should be rejected.'); + }, 'Script URL including URL-encoded backslash'); + + promise_test(function(t) { + var script = 'resources%5Cempty-worker.js'; + var scope = 'resources/scope/encoded-slash-in-script-url'; + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'URL-encoded backslash in the script URL should be rejected.'); + }, 'Script URL including uppercase URL-encoded backslash'); + + promise_test(function(t) { + var script = 'data:application/javascript,'; + var scope = 'resources/scope/data-url-in-script-url'; + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'Data URLs should not be registered as service workers.'); + }, 'Script URL is a data URL'); + + promise_test(function(t) { + var script = 'data:application/javascript,'; + var scope = new URL('resources/scope/data-url-in-script-url', location); + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'Data URLs should not be registered as service workers.'); + }, 'Script URL is a data URL and scope URL is not relative'); + + promise_test(function(t) { + var script = 'resources/././empty-worker.js'; + var scope = 'resources/scope/parent-reference-in-script-url'; + return register_method(script, {scope: scope}) + .then(function(registration) { + assert_equals( + get_newest_worker(registration).scriptURL, + normalizeURL('resources/empty-worker.js'), + 'Script URL including self-reference should be normalized.'); + return registration.unregister(); + }); + }, 'Script URL including self-reference'); + + promise_test(function(t) { + var script = 'resources/../resources/empty-worker.js'; + var scope = 'resources/scope/parent-reference-in-script-url'; + return register_method(script, {scope: scope}) + .then(function(registration) { + assert_equals( + get_newest_worker(registration).scriptURL, + normalizeURL('resources/empty-worker.js'), + 'Script URL including parent-reference should be normalized.'); + return registration.unregister(); + }); + }, 'Script URL including parent-reference'); +} diff --git a/test/wpt/tests/service-workers/service-worker/resources/registration-tests-script.js b/test/wpt/tests/service-workers/service-worker/resources/registration-tests-script.js new file mode 100644 index 00000000000..e5bdaf4291a --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/registration-tests-script.js @@ -0,0 +1,121 @@ +// Registration tests that mostly exercise the service worker script contents or +// response. +function registration_tests_script(register_method, type) { + promise_test(function(t) { + var script = 'resources/invalid-chunked-encoding.py'; + var scope = 'resources/scope/invalid-chunked-encoding/'; + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'Registration of invalid chunked encoding script should fail.'); + }, 'Registering invalid chunked encoding script'); + + promise_test(function(t) { + var script = 'resources/invalid-chunked-encoding-with-flush.py'; + var scope = 'resources/scope/invalid-chunked-encoding-with-flush/'; + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'Registration of invalid chunked encoding script should fail.'); + }, 'Registering invalid chunked encoding script with flush'); + + promise_test(function(t) { + var script = 'resources/malformed-worker.py?parse-error'; + var scope = 'resources/scope/parse-error'; + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'Registration of script including parse error should fail.'); + }, 'Registering script including parse error'); + + promise_test(function(t) { + var script = 'resources/malformed-worker.py?undefined-error'; + var scope = 'resources/scope/undefined-error'; + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'Registration of script including undefined error should fail.'); + }, 'Registering script including undefined error'); + + promise_test(function(t) { + var script = 'resources/malformed-worker.py?uncaught-exception'; + var scope = 'resources/scope/uncaught-exception'; + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'Registration of script including uncaught exception should fail.'); + }, 'Registering script including uncaught exception'); + + if (type === 'classic') { + promise_test(function(t) { + var script = 'resources/malformed-worker.py?import-malformed-script'; + var scope = 'resources/scope/import-malformed-script'; + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'Registration of script importing malformed script should fail.'); + }, 'Registering script importing malformed script'); + } + + if (type === 'module') { + promise_test(function(t) { + var script = 'resources/malformed-worker.py?top-level-await'; + var scope = 'resources/scope/top-level-await'; + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'Registration of script with top-level await should fail.'); + }, 'Registering script with top-level await'); + + promise_test(function(t) { + var script = 'resources/malformed-worker.py?instantiation-error'; + var scope = 'resources/scope/instantiation-error'; + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'Registration of script with module instantiation error should fail.'); + }, 'Registering script with module instantiation error'); + + promise_test(function(t) { + var script = 'resources/malformed-worker.py?instantiation-error-and-top-level-await'; + var scope = 'resources/scope/instantiation-error-and-top-level-await'; + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'Registration of script with module instantiation error and top-level await should fail.'); + }, 'Registering script with module instantiation error and top-level await'); + } + + promise_test(function(t) { + var script = 'resources/no-such-worker.js'; + var scope = 'resources/scope/no-such-worker'; + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'Registration of non-existent script should fail.'); + }, 'Registering non-existent script'); + + if (type === 'classic') { + promise_test(function(t) { + var script = 'resources/malformed-worker.py?import-no-such-script'; + var scope = 'resources/scope/import-no-such-script'; + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'Registration of script importing non-existent script should fail.'); + }, 'Registering script importing non-existent script'); + } + + promise_test(function(t) { + var script = 'resources/malformed-worker.py?caught-exception'; + var scope = 'resources/scope/caught-exception'; + return register_method(script, {scope: scope}) + .then(function(registration) { + assert_true( + registration instanceof ServiceWorkerRegistration, + 'Successfully registered.'); + return registration.unregister(); + }); + }, 'Registering script including caught exception'); + +} diff --git a/test/wpt/tests/service-workers/service-worker/resources/registration-tests-security-error.js b/test/wpt/tests/service-workers/service-worker/resources/registration-tests-security-error.js new file mode 100644 index 00000000000..c45fbd45789 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/registration-tests-security-error.js @@ -0,0 +1,78 @@ +// Registration tests that mostly exercise SecurityError cases. +function registration_tests_security_error(register_method) { + promise_test(function(t) { + var script = 'resources/registration-worker.js'; + var scope = 'resources'; + return promise_rejects_dom(t, + 'SecurityError', + register_method(script, {scope: scope}), + 'Registering same scope as the script directory without the last ' + + 'slash should fail with SecurityError.'); + }, 'Registering same scope as the script directory without the last slash'); + + promise_test(function(t) { + var script = 'resources/registration-worker.js'; + var scope = 'different-directory/'; + return promise_rejects_dom(t, + 'SecurityError', + register_method(script, {scope: scope}), + 'Registration scope outside the script directory should fail ' + + 'with SecurityError.'); + }, 'Registration scope outside the script directory'); + + promise_test(function(t) { + var script = 'resources/registration-worker.js'; + var scope = 'http://example.com/'; + return promise_rejects_dom(t, + 'SecurityError', + register_method(script, {scope: scope}), + 'Registration scope outside domain should fail with SecurityError.'); + }, 'Registering scope outside domain'); + + promise_test(function(t) { + var script = 'http://example.com/worker.js'; + var scope = 'http://example.com/scope/'; + return promise_rejects_dom(t, + 'SecurityError', + register_method(script, {scope: scope}), + 'Registration script outside domain should fail with SecurityError.'); + }, 'Registering script outside domain'); + + promise_test(function(t) { + var script = 'resources/redirect.py?Redirect=' + + encodeURIComponent('/resources/registration-worker.js'); + var scope = 'resources/scope/redirect/'; + return promise_rejects_dom(t, + 'SecurityError', + register_method(script, {scope: scope}), + 'Registration of redirected script should fail.'); + }, 'Registering redirected script'); + + promise_test(function(t) { + var script = 'resources/empty-worker.js'; + var scope = 'resources/../scope/parent-reference-in-scope'; + return promise_rejects_dom(t, + 'SecurityError', + register_method(script, {scope: scope}), + 'Scope not under the script directory should be rejected.'); + }, 'Scope including parent-reference and not under the script directory'); + + promise_test(function(t) { + var script = 'resources////empty-worker.js'; + var scope = 'resources/scope/consecutive-slashes-in-script-url'; + return promise_rejects_dom(t, + 'SecurityError', + register_method(script, {scope: scope}), + 'Consecutive slashes in the script url should not be unified.'); + }, 'Script URL including consecutive slashes'); + + promise_test(function(t) { + var script = 'filesystem:' + normalizeURL('resources/empty-worker.js'); + var scope = 'resources/scope/filesystem-script-url'; + return promise_rejects_js(t, + TypeError, + register_method(script, {scope: scope}), + 'Registering a script which has same-origin filesystem: URL should ' + + 'fail with TypeError.'); + }, 'Script URL is same-origin filesystem: URL'); +} diff --git a/test/wpt/tests/service-workers/service-worker/resources/registration-worker.js b/test/wpt/tests/service-workers/service-worker/resources/registration-worker.js new file mode 100644 index 00000000000..44d1d2774a2 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/registration-worker.js @@ -0,0 +1 @@ +// empty for now diff --git a/test/wpt/tests/service-workers/service-worker/resources/reject-install-worker.js b/test/wpt/tests/service-workers/service-worker/resources/reject-install-worker.js new file mode 100644 index 00000000000..41f07fd5db8 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/reject-install-worker.js @@ -0,0 +1,3 @@ +self.oninstall = function(event) { + event.waitUntil(Promise.reject()); +}; diff --git a/test/wpt/tests/service-workers/service-worker/resources/reply-to-message.html b/test/wpt/tests/service-workers/service-worker/resources/reply-to-message.html new file mode 100644 index 00000000000..8a70e2ad933 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/reply-to-message.html @@ -0,0 +1,7 @@ + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/request-end-to-end-worker.js b/test/wpt/tests/service-workers/service-worker/resources/request-end-to-end-worker.js new file mode 100644 index 00000000000..6bd2b72137e --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/request-end-to-end-worker.js @@ -0,0 +1,34 @@ +'use strict'; + +onfetch = function(e) { + var headers = {}; + for (var header of e.request.headers) { + var key = header[0], value = header[1]; + headers[key] = value; + } + var append_header_error = ''; + try { + e.request.headers.append('Test-Header', 'TestValue'); + } catch (error) { + append_header_error = error.name; + } + + var request_construct_error = ''; + try { + new Request(e.request, {method: 'GET'}); + } catch (error) { + request_construct_error = error.name; + } + + e.respondWith(new Response(JSON.stringify({ + url: e.request.url, + method: e.request.method, + referrer: e.request.referrer, + headers: headers, + mode: e.request.mode, + credentials: e.request.credentials, + redirect: e.request.redirect, + append_header_error: append_header_error, + request_construct_error: request_construct_error + }))); +}; diff --git a/test/wpt/tests/service-workers/service-worker/resources/request-headers.py b/test/wpt/tests/service-workers/service-worker/resources/request-headers.py new file mode 100644 index 00000000000..6ab148e22e7 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/request-headers.py @@ -0,0 +1,8 @@ +import json + +from wptserve.utils import isomorphic_decode + +def main(request, response): + data = {isomorphic_decode(key):isomorphic_decode(request.headers[key]) for key, value in request.headers.items()} + + return [(b"Content-Type", b"application/json")], json.dumps(data) diff --git a/test/wpt/tests/service-workers/service-worker/resources/resource-timing-iframe.sub.html b/test/wpt/tests/service-workers/service-worker/resources/resource-timing-iframe.sub.html new file mode 100644 index 00000000000..384c29b536b --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/resource-timing-iframe.sub.html @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/resource-timing-worker.js b/test/wpt/tests/service-workers/service-worker/resources/resource-timing-worker.js new file mode 100644 index 00000000000..b74e8cd6a23 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/resource-timing-worker.js @@ -0,0 +1,12 @@ +self.addEventListener('fetch', function(event) { + if (event.request.url.indexOf('sample.js') != -1) { + event.respondWith(new Promise(resolve => { + // Slightly delay the response so we ensure we get a non-zero + // duration. + setTimeout(_ => resolve(new Response('// Empty javascript')), 50); + })); + } + else if (event.request.url.indexOf('missing.jpg?SWRespondsWithFetch') != -1) { + event.respondWith(fetch('sample.txt?SWFetched')); + } +}); diff --git a/test/wpt/tests/service-workers/service-worker/resources/respond-then-throw-worker.js b/test/wpt/tests/service-workers/service-worker/resources/respond-then-throw-worker.js new file mode 100644 index 00000000000..adb48de69e7 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/respond-then-throw-worker.js @@ -0,0 +1,40 @@ +var syncport = null; + +self.addEventListener('message', function(e) { + if ('port' in e.data) { + if (syncport) { + syncport(e.data.port); + } else { + syncport = e.data.port; + } + } +}); + +function sync() { + return new Promise(function(resolve) { + if (syncport) { + resolve(syncport); + } else { + syncport = resolve; + } + }).then(function(port) { + port.postMessage('SYNC'); + return new Promise(function(resolve) { + port.onmessage = function(e) { + if (e.data === 'ACK') { + resolve(); + } + } + }); + }); +} + + +self.addEventListener('fetch', function(event) { + // In Firefox the result would depend on a race between fetch handling + // and exception handling code. On the assumption that this might be a common + // design error, we explicitly allow the exception to be handled first. + event.respondWith(sync().then(() => new Response('intercepted'))); + + throw("error"); + }); diff --git a/test/wpt/tests/service-workers/service-worker/resources/respond-with-body-accessed-response-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/respond-with-body-accessed-response-iframe.html new file mode 100644 index 00000000000..7be31487949 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/respond-with-body-accessed-response-iframe.html @@ -0,0 +1,20 @@ + diff --git a/test/wpt/tests/service-workers/service-worker/resources/respond-with-body-accessed-response-worker.js b/test/wpt/tests/service-workers/service-worker/resources/respond-with-body-accessed-response-worker.js new file mode 100644 index 00000000000..c602109bc68 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/respond-with-body-accessed-response-worker.js @@ -0,0 +1,93 @@ +importScripts('/common/get-host-info.sub.js'); +importScripts('test-helpers.sub.js'); + +function getQueryParams(url) { + var search = (new URL(url)).search; + if (!search) { + return {}; + } + var ret = {}; + var params = search.substring(1).split('&'); + params.forEach(function(param) { + var element = param.split('='); + ret[decodeURIComponent(element[0])] = decodeURIComponent(element[1]); + }); + return ret; +} + +function createResponse(params) { + if (params['type'] == 'basic') { + return fetch('respond-with-body-accessed-response.jsonp'); + } + if (params['type'] == 'opaque') { + return fetch(get_host_info()['HTTPS_REMOTE_ORIGIN'] + base_path() + + 'respond-with-body-accessed-response.jsonp', + {mode: 'no-cors'}); + } + if (params['type'] == 'default') { + return Promise.resolve(new Response('callback(\'OK\');')); + } + + return Promise.reject(new Error('unexpected type :' + params['type'])); +} + +function cloneResponseIfNeeded(params, response) { + if (params['clone'] == '1') { + return response.clone(); + } else if (params['clone'] == '2') { + response.clone(); + return response; + } + return response; +} + +function passThroughCacheIfNeeded(params, request, response) { + return new Promise(function(resolve) { + if (params['passThroughCache'] == 'true') { + var cache_name = request.url; + var cache; + self.caches.delete(cache_name) + .then(function() { + return self.caches.open(cache_name); + }) + .then(function(c) { + cache = c; + return cache.put(request, response); + }) + .then(function() { + return cache.match(request.url); + }) + .then(function(res) { + // Touch .body here to test the behavior after touching it. + res.body; + resolve(res); + }); + } else { + resolve(response); + } + }) +} + +self.addEventListener('fetch', function(event) { + if (event.request.url.indexOf('TestRequest') == -1) { + return; + } + var params = getQueryParams(event.request.url); + event.respondWith( + createResponse(params) + .then(function(response) { + // Touch .body here to test the behavior after touching it. + response.body; + return cloneResponseIfNeeded(params, response); + }) + .then(function(response) { + // Touch .body here to test the behavior after touching it. + response.body; + return passThroughCacheIfNeeded(params, event.request, response); + }) + .then(function(response) { + // Touch .body here to test the behavior after touching it. + response.body; + return response; + })); + }); diff --git a/test/wpt/tests/service-workers/service-worker/resources/respond-with-body-accessed-response.jsonp b/test/wpt/tests/service-workers/service-worker/resources/respond-with-body-accessed-response.jsonp new file mode 100644 index 00000000000..b9c28f51f90 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/respond-with-body-accessed-response.jsonp @@ -0,0 +1 @@ +callback('OK'); diff --git a/test/wpt/tests/service-workers/service-worker/resources/sample-worker-interceptor.js b/test/wpt/tests/service-workers/service-worker/resources/sample-worker-interceptor.js new file mode 100644 index 00000000000..c06f8dd77b7 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/sample-worker-interceptor.js @@ -0,0 +1,62 @@ +importScripts('/common/get-host-info.sub.js'); + +const text = 'worker loading intercepted by service worker'; +const dedicated_worker_script = `postMessage('${text}');`; +const shared_worker_script = + `onconnect = evt => evt.ports[0].postMessage('${text}');`; + +let source; +let resolveDone; +let done = new Promise(resolve => resolveDone = resolve); + +// The page messages this worker to ask for the result. Keep the worker alive +// via waitUntil() until the result is sent. +self.addEventListener('message', event => { + source = event.data.port; + source.postMessage({id: event.source.id}); + source.onmessage = resolveDone; + event.waitUntil(done); +}); + +self.onfetch = event => { + const url = event.request.url; + const destination = event.request.destination; + + if (source) + source.postMessage({clientId:event.clientId, resultingClientId: event.resultingClientId}); + + // Request handler for a synthesized response. + if (url.indexOf('synthesized') != -1) { + let script_headers = new Headers({ "Content-Type": "text/javascript" }); + if (destination === 'worker') + event.respondWith(new Response(dedicated_worker_script, { 'headers': script_headers })); + else if (destination === 'sharedworker') + event.respondWith(new Response(shared_worker_script, { 'headers': script_headers })); + else + event.respondWith(new Response('Unexpected request! ' + destination)); + return; + } + + // Request handler for a same-origin response. + if (url.indexOf('same-origin') != -1) { + event.respondWith(fetch('postmessage-on-load-worker.js')); + return; + } + + // Request handler for a cross-origin response. + if (url.indexOf('cors') != -1) { + const filename = 'postmessage-on-load-worker.js'; + const path = (new URL(filename, self.location)).pathname; + let new_url = get_host_info()['HTTPS_REMOTE_ORIGIN'] + path; + let mode; + if (url.indexOf('no-cors') != -1) { + // Test no-cors mode. + mode = 'no-cors'; + } else { + // Test cors mode. + new_url += '?pipe=header(Access-Control-Allow-Origin,*)'; + mode = 'cors'; + } + event.respondWith(fetch(new_url, { mode: mode })); + } +}; diff --git a/test/wpt/tests/service-workers/service-worker/resources/sample.html b/test/wpt/tests/service-workers/service-worker/resources/sample.html new file mode 100644 index 00000000000..12a179980df --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/sample.html @@ -0,0 +1,2 @@ + +Hello world diff --git a/test/wpt/tests/service-workers/service-worker/resources/sample.js b/test/wpt/tests/service-workers/service-worker/resources/sample.js new file mode 100644 index 00000000000..b8889db05d0 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/sample.js @@ -0,0 +1 @@ +var hello = "world"; diff --git a/test/wpt/tests/service-workers/service-worker/resources/sample.txt b/test/wpt/tests/service-workers/service-worker/resources/sample.txt new file mode 100644 index 00000000000..802992c4220 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/sample.txt @@ -0,0 +1 @@ +Hello world diff --git a/test/wpt/tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-iframe.html new file mode 100644 index 00000000000..239fa733037 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-iframe.html @@ -0,0 +1,63 @@ + diff --git a/test/wpt/tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-iframe.py b/test/wpt/tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-iframe.py new file mode 100644 index 00000000000..0281b6c2755 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-iframe.py @@ -0,0 +1,18 @@ +import os.path + +from wptserve.utils import isomorphic_decode + +def main(request, response): + header = [(b'Content-Type', b'text/html')] + if b'test' in request.GET: + with open(os.path.join(os.path.dirname(isomorphic_decode(__file__)), u'sample.js'), u'r') as f: + body = f.read() + return (header, body) + + if b'sandbox' in request.GET: + header.append((b'Content-Security-Policy', + b'sandbox %s' % request.GET[b'sandbox'])) + with open(os.path.join(os.path.dirname(isomorphic_decode(__file__)), + u'sandboxed-iframe-fetch-event-iframe.html'), u'r') as f: + body = f.read() + return (header, body) diff --git a/test/wpt/tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-worker.js b/test/wpt/tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-worker.js new file mode 100644 index 00000000000..4035a8b19b9 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/sandboxed-iframe-fetch-event-worker.js @@ -0,0 +1,20 @@ +var requests = []; + +self.addEventListener('message', function(event) { + event.waitUntil(self.clients.matchAll() + .then(function(clients) { + var client_urls = []; + for(var client of clients){ + client_urls.push(client.url); + } + client_urls = client_urls.sort(); + event.data.port.postMessage( + {clients: client_urls, requests: requests}); + requests = []; + })); + }); + +self.addEventListener('fetch', function(event) { + requests.push(event.request.url); + event.respondWith(fetch(event.request)); + }); diff --git a/test/wpt/tests/service-workers/service-worker/resources/sandboxed-iframe-navigator-serviceworker-iframe.html b/test/wpt/tests/service-workers/service-worker/resources/sandboxed-iframe-navigator-serviceworker-iframe.html new file mode 100644 index 00000000000..1d682e47ef5 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/sandboxed-iframe-navigator-serviceworker-iframe.html @@ -0,0 +1,25 @@ + diff --git a/test/wpt/tests/service-workers/service-worker/resources/scope1/module-worker-importing-redirect-to-scope2.js b/test/wpt/tests/service-workers/service-worker/resources/scope1/module-worker-importing-redirect-to-scope2.js new file mode 100644 index 00000000000..ae681ba30e0 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/scope1/module-worker-importing-redirect-to-scope2.js @@ -0,0 +1 @@ +import * as module from './redirect.py?Redirect=/service-workers/service-worker/resources/scope2/imported-module-script.js'; diff --git a/test/wpt/tests/service-workers/service-worker/resources/scope1/module-worker-importing-scope2.js b/test/wpt/tests/service-workers/service-worker/resources/scope1/module-worker-importing-scope2.js new file mode 100644 index 00000000000..e28505249c2 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/scope1/module-worker-importing-scope2.js @@ -0,0 +1 @@ +import * as module from '../scope2/imported-module-script.js'; diff --git a/test/wpt/tests/service-workers/service-worker/resources/scope1/redirect.py b/test/wpt/tests/service-workers/service-worker/resources/scope1/redirect.py new file mode 100644 index 00000000000..bb4c874aace --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/scope1/redirect.py @@ -0,0 +1,6 @@ +import os +import imp +# Use the file from the parent directory. +mod = imp.load_source("_parent", os.path.join(os.path.dirname(os.path.dirname(__file__)), + os.path.basename(__file__))) +main = mod.main diff --git a/test/wpt/tests/service-workers/service-worker/resources/scope2/import-scripts-echo.py b/test/wpt/tests/service-workers/service-worker/resources/scope2/import-scripts-echo.py new file mode 100644 index 00000000000..5f785b5cc27 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/scope2/import-scripts-echo.py @@ -0,0 +1,6 @@ +def main(req, res): + return ([ + (b'Cache-Control', b'no-cache, must-revalidate'), + (b'Pragma', b'no-cache'), + (b'Content-Type', b'application/javascript')], + b'echo_output = "%s (scope2/)";\n' % req.GET[b'msg']) diff --git a/test/wpt/tests/service-workers/service-worker/resources/scope2/imported-module-script.js b/test/wpt/tests/service-workers/service-worker/resources/scope2/imported-module-script.js new file mode 100644 index 00000000000..a18e704a3c5 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/scope2/imported-module-script.js @@ -0,0 +1,4 @@ +export const imported = 'A module script.'; +onmessage = msg => { + msg.source.postMessage('pong'); +}; diff --git a/test/wpt/tests/service-workers/service-worker/resources/scope2/simple.txt b/test/wpt/tests/service-workers/service-worker/resources/scope2/simple.txt new file mode 100644 index 00000000000..cd876676e85 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/scope2/simple.txt @@ -0,0 +1 @@ +a simple text file (scope2/) diff --git a/test/wpt/tests/service-workers/service-worker/resources/scope2/worker_interception_redirect_webworker.py b/test/wpt/tests/service-workers/service-worker/resources/scope2/worker_interception_redirect_webworker.py new file mode 100644 index 00000000000..bb4c874aace --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/scope2/worker_interception_redirect_webworker.py @@ -0,0 +1,6 @@ +import os +import imp +# Use the file from the parent directory. +mod = imp.load_source("_parent", os.path.join(os.path.dirname(os.path.dirname(__file__)), + os.path.basename(__file__))) +main = mod.main diff --git a/test/wpt/tests/service-workers/service-worker/resources/secure-context-service-worker.js b/test/wpt/tests/service-workers/service-worker/resources/secure-context-service-worker.js new file mode 100644 index 00000000000..5ba99f07536 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/secure-context-service-worker.js @@ -0,0 +1,21 @@ +self.addEventListener('fetch', event => { + let url = new URL(event.request.url); + if (url.pathname.indexOf('sender.html') != -1) { + event.respondWith(new Response( + "", + { headers: { 'Content-Type': 'text/html'} } + )); + } else if (url.pathname.indexOf('report') != -1) { + self.clients.matchAll().then(clients => { + for (client of clients) { + client.postMessage(url.searchParams.get('result')); + } + }); + event.respondWith( + new Response( + '', + { headers: { 'Content-Type': 'text/html'} } + ) + ); + } +}); diff --git a/test/wpt/tests/service-workers/service-worker/resources/secure-context/sender.html b/test/wpt/tests/service-workers/service-worker/resources/secure-context/sender.html new file mode 100644 index 00000000000..05e58822a86 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/secure-context/sender.html @@ -0,0 +1 @@ + diff --git a/test/wpt/tests/service-workers/service-worker/resources/secure-context/window.html b/test/wpt/tests/service-workers/service-worker/resources/secure-context/window.html new file mode 100644 index 00000000000..071a507cb33 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/secure-context/window.html @@ -0,0 +1,15 @@ + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/service-worker-csp-worker.py b/test/wpt/tests/service-workers/service-worker/resources/service-worker-csp-worker.py new file mode 100644 index 00000000000..35a46964a78 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/service-worker-csp-worker.py @@ -0,0 +1,183 @@ +bodyDefault = b''' +importScripts('worker-testharness.js'); +importScripts('test-helpers.sub.js'); +importScripts('/common/get-host-info.sub.js'); + +var host_info = get_host_info(); + +test(function() { + var import_script_failed = false; + try { + importScripts(host_info.HTTPS_REMOTE_ORIGIN + + base_path() + 'empty.js'); + } catch(e) { + import_script_failed = true; + } + assert_true(import_script_failed, + 'Importing the other origins script should fail.'); + }, 'importScripts test for default-src'); + +test(function() { + assert_throws_js(EvalError, + function() { eval('1 + 1'); }, + 'eval() should throw EvalError.') + assert_throws_js(EvalError, + function() { new Function('1 + 1'); }, + 'new Function() should throw EvalError.') + }, 'eval test for default-src'); + +async_test(function(t) { + fetch(host_info.HTTPS_REMOTE_ORIGIN + + base_path() + 'fetch-access-control.py?ACAOrigin=*', + {mode: 'cors'}) + .then(function(response){ + assert_unreached('fetch should fail.'); + }, function(){ + t.done(); + }) + .catch(unreached_rejection(t)); + }, 'Fetch test for default-src'); + +async_test(function(t) { + var REDIRECT_URL = host_info.HTTPS_ORIGIN + + base_path() + 'redirect.py?Redirect='; + var OTHER_BASE_URL = host_info.HTTPS_REMOTE_ORIGIN + + base_path() + 'fetch-access-control.py?' + fetch(REDIRECT_URL + encodeURIComponent(OTHER_BASE_URL + 'ACAOrigin=*'), + {mode: 'cors'}) + .then(function(response){ + assert_unreached('Redirected fetch should fail.'); + }, function(){ + t.done(); + }) + .catch(unreached_rejection(t)); + }, 'Redirected fetch test for default-src');''' + +bodyScript = b''' +importScripts('worker-testharness.js'); +importScripts('test-helpers.sub.js'); +importScripts('/common/get-host-info.sub.js'); + +var host_info = get_host_info(); + +test(function() { + var import_script_failed = false; + try { + importScripts(host_info.HTTPS_REMOTE_ORIGIN + + base_path() + 'empty.js'); + } catch(e) { + import_script_failed = true; + } + assert_true(import_script_failed, + 'Importing the other origins script should fail.'); + }, 'importScripts test for script-src'); + +test(function() { + assert_throws_js(EvalError, + function() { eval('1 + 1'); }, + 'eval() should throw EvalError.') + assert_throws_js(EvalError, + function() { new Function('1 + 1'); }, + 'new Function() should throw EvalError.') + }, 'eval test for script-src'); + +async_test(function(t) { + fetch(host_info.HTTPS_REMOTE_ORIGIN + + base_path() + 'fetch-access-control.py?ACAOrigin=*', + {mode: 'cors'}) + .then(function(response){ + t.done(); + }, function(){ + assert_unreached('fetch should not fail.'); + }) + .catch(unreached_rejection(t)); + }, 'Fetch test for script-src'); + +async_test(function(t) { + var REDIRECT_URL = host_info.HTTPS_ORIGIN + + base_path() + 'redirect.py?Redirect='; + var OTHER_BASE_URL = host_info.HTTPS_REMOTE_ORIGIN + + base_path() + 'fetch-access-control.py?' + fetch(REDIRECT_URL + encodeURIComponent(OTHER_BASE_URL + 'ACAOrigin=*'), + {mode: 'cors'}) + .then(function(response){ + t.done(); + }, function(){ + assert_unreached('Redirected fetch should not fail.'); + }) + .catch(unreached_rejection(t)); + }, 'Redirected fetch test for script-src');''' + +bodyConnect = b''' +importScripts('worker-testharness.js'); +importScripts('test-helpers.sub.js'); +importScripts('/common/get-host-info.sub.js'); + +var host_info = get_host_info(); + +test(function() { + var import_script_failed = false; + try { + importScripts(host_info.HTTPS_REMOTE_ORIGIN + + base_path() + 'empty.js'); + } catch(e) { + import_script_failed = true; + } + assert_false(import_script_failed, + 'Importing the other origins script should not fail.'); + }, 'importScripts test for connect-src'); + +test(function() { + var eval_failed = false; + try { + eval('1 + 1'); + new Function('1 + 1'); + } catch(e) { + eval_failed = true; + } + assert_false(eval_failed, + 'connect-src without unsafe-eval should not block eval().'); + }, 'eval test for connect-src'); + +async_test(function(t) { + fetch(host_info.HTTPS_REMOTE_ORIGIN + + base_path() + 'fetch-access-control.py?ACAOrigin=*', + {mode: 'cors'}) + .then(function(response){ + assert_unreached('fetch should fail.'); + }, function(){ + t.done(); + }) + .catch(unreached_rejection(t)); + }, 'Fetch test for connect-src'); + +async_test(function(t) { + var REDIRECT_URL = host_info.HTTPS_ORIGIN + + base_path() + 'redirect.py?Redirect='; + var OTHER_BASE_URL = host_info.HTTPS_REMOTE_ORIGIN + + base_path() + 'fetch-access-control.py?' + fetch(REDIRECT_URL + encodeURIComponent(OTHER_BASE_URL + 'ACAOrigin=*'), + {mode: 'cors'}) + .then(function(response){ + assert_unreached('Redirected fetch should fail.'); + }, function(){ + t.done(); + }) + .catch(unreached_rejection(t)); + }, 'Redirected fetch test for connect-src');''' + +def main(request, response): + headers = [] + headers.append((b'Content-Type', b'application/javascript')) + directive = request.GET[b'directive'] + body = b'ERROR: Unknown directive' + if directive == b'default': + headers.append((b'Content-Security-Policy', b"default-src 'self'")) + body = bodyDefault + elif directive == b'script': + headers.append((b'Content-Security-Policy', b"script-src 'self'")) + body = bodyScript + elif directive == b'connect': + headers.append((b'Content-Security-Policy', b"connect-src 'self'")) + body = bodyConnect + return headers, body diff --git a/test/wpt/tests/service-workers/service-worker/resources/service-worker-header.py b/test/wpt/tests/service-workers/service-worker/resources/service-worker-header.py new file mode 100644 index 00000000000..d64a9d24945 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/service-worker-header.py @@ -0,0 +1,20 @@ +def main(request, response): + service_worker_header = request.headers.get(b'service-worker') + + if b'header' in request.GET and service_worker_header != b'script': + return 400, [(b'Content-Type', b'text/plain')], b'Bad Request' + + if b'no-header' in request.GET and service_worker_header == b'script': + return 400, [(b'Content-Type', b'text/plain')], b'Bad Request' + + # no-cache itself to ensure the user agent finds a new version for each + # update. + headers = [(b'Cache-Control', b'no-cache, must-revalidate'), + (b'Pragma', b'no-cache'), + (b'Content-Type', b'application/javascript')] + body = b'/* This is a service worker script */\n' + + if b'import' in request.GET: + body += b"importScripts('%s');" % request.GET[b'import'] + + return 200, headers, body diff --git a/test/wpt/tests/service-workers/service-worker/resources/service-worker-interception-dynamic-import-worker.js b/test/wpt/tests/service-workers/service-worker/resources/service-worker-interception-dynamic-import-worker.js new file mode 100644 index 00000000000..680e07ff588 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/service-worker-interception-dynamic-import-worker.js @@ -0,0 +1 @@ +import('./service-worker-interception-network-worker.js'); diff --git a/test/wpt/tests/service-workers/service-worker/resources/service-worker-interception-network-worker.js b/test/wpt/tests/service-workers/service-worker/resources/service-worker-interception-network-worker.js new file mode 100644 index 00000000000..5ff39001013 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/service-worker-interception-network-worker.js @@ -0,0 +1 @@ +postMessage('LOADED_FROM_NETWORK'); diff --git a/test/wpt/tests/service-workers/service-worker/resources/service-worker-interception-service-worker.js b/test/wpt/tests/service-workers/service-worker/resources/service-worker-interception-service-worker.js new file mode 100644 index 00000000000..6b43a376963 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/service-worker-interception-service-worker.js @@ -0,0 +1,9 @@ +const kURL = '/service-worker-interception-network-worker.js'; +const kScript = 'postMessage("LOADED_FROM_SERVICE_WORKER")'; +const kHeaders = [['content-type', 'text/javascript']]; + +self.addEventListener('fetch', e => { + // Serve a generated response for kURL. + if (e.request.url.indexOf(kURL) != -1) + e.respondWith(new Response(kScript, { headers: kHeaders })); +}); diff --git a/test/wpt/tests/service-workers/service-worker/resources/service-worker-interception-static-import-worker.js b/test/wpt/tests/service-workers/service-worker/resources/service-worker-interception-static-import-worker.js new file mode 100644 index 00000000000..e570958701d --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/service-worker-interception-static-import-worker.js @@ -0,0 +1 @@ +import './service-worker-interception-network-worker.js'; diff --git a/test/wpt/tests/service-workers/service-worker/resources/silence.oga b/test/wpt/tests/service-workers/service-worker/resources/silence.oga new file mode 100644 index 0000000000000000000000000000000000000000..af591880436af43abb4cee59fde320081bcac7ed GIT binary patch literal 12983 zcmaiabzD?!*Y1Fl0wR(MNOw01(%nOMHzP3vNaF({-8sO}AVWzDNJw``w{(L@mz<5y z`+e^@f1Ufc_nte}UbEu5*IsMSsM*?Tf>1&KY2$DI?WE+oG^0?WcsRRR*h3$tP^3%$ zT`}AJ_wy@?+QXgy+aB&j0Y>fzZtrlF?*H5R9{tOO0$9|ucd_G8bF-y#vbWIr%bQAp ziie9!h?|>7@Zs(kz$v3Dr6w%{7PYZ~xLDir!0fD@-OSm!IC%I_(EerhN>WM(phf|a zT52)Nrh$JbhLJ%NxnYDM-2`sMFsJIK##MTqF0%sX@%o06^Q?m0It9Xf!bUM z72*i{D(RGduqlm8YiRpT3TSJeyBkkG98`Q(L=RlW<%`k^UQo5;Sdn7l>Z5_E0OQFr zwc|hfAM=1Ln1n+aYBqhL(LpxCb}U_+Fc+H1O@((Xo0}QXVPVE}Xt&T-1~lBwhT9IH z%YYID*)adxLv(IRI3geBx`m5^TxCfCI(W^dgoD5oFh^WdDD1$|1@ve(HWeTolR$4+ z7$Et3L6Q_fT~r@9=bmpGyptpWdT+^!e4vsKbN{9L*F1o}5~Knlu0>SxS$Q8wWkT(H z^D!3lI_T_h+^~z~^77)qyHtr<-3rW^|7wm20#OGOg5{|{{^t@(9YSi+%UqQ44>n72 zer`BVKU?)6&%ncF?@RUY>wT_Hd?}o=8UiJi+DSdjvWBe;N4y^f_GPtGip&Lomzn?C zFA#7KCYe>NgzN+7Q0l@Aw|?f&*{FZb2`oPB5|KWrv>fceM-Nm)E>T(mIBJv!CY05P zv?r5{4X3OZ&mra{9*Y<5%NOoz(;v(?6V z*Fk2dAg0NXu{&|tD-yzv@IW=7;J)nsxJWnWkO+KDZ zAvs?Gv7w)9<4|bpjr@%Imqh%5WGtqpPu9m<)~tM(ep|9=O^ zUx3GgK%;TweR1Rfz!iWF%3tdOdjS>Xg3}WVfF3~ne{@tk1q^CDhW39u00iQq>j1OK zLHQICwUlbLRBE-9h_uzT|9_2NWt>k1Ff2W=>p*Q5QQtSHsOjl9mhjv06A;`C)U_tm zkK^K(xY3D%5Ing!ZcCDD_(7C_(Vl^%0nNLqdhnsW!g9q41j1XNVTmuc$P1#uw#;3r z7DG%)GvLxFXf#-^EHBZ5jol*=997&6paqp2IN5>$@gIXUK%ii5dFFgVi%8+345>)w ztyE|T_a-?sT6ilP24>mHfT=2Wk^;k`3@G?DV-i$Vkue9r2M7Z=pX7|j5}}A^fC44r zMbv)tX(aNg5z1)7j5YX-r+VBFzv-;$oz$|A+d*sHe^xDcwSI@`rLT-+)sbX-SW4H_L=gyyXZK~deR+Y z=x%I$p{$v#GM!{S?hctxvNra#p1t57hZ&D+ThGFbXFaTS>y7o#{l?utE+<)=>RRiQ zdKh1PG@MS_fEh39TI&kx0IP=6A6KT_&C^8Gv;oe>BYeiX?#8CN#?w>$N2Wd=H%G1&no*A2{nKyD!23Vr%_NZQXNs{pqBYDaeYT@$`kZ{`p5>^?`GP zyD{lo?K3}3kB=)b>*eW>%lhw(g_6hI{mhdfD|*)Y!u~6I$%_|B)&MI#$SkRjK5>ZY zW#-1Y-`ab3W3qRPTZ4^9I6X)CH~r(25X&^bMSUAn&*&A>*R8+SijfuR<*YbGY)@Jb z_PVQYTB;(;_im8DkY&ETa_J>oGrZ&*`K_Xh6Fh8h9MmPa(0+Q+QT0>v!@rb z43tA|TK4*@-XW1r0O!32A)N+Vj{1>@e-->IMiu*f)zLsz>CImSAH+a%3{*j0Ry*A^ z+QI)L70Y}T`%NPt)lq-Ba|yC^?_qd&UGB2i$n~wt=>}PP-N-uM=)A`>f9@v@Kv0t( zG7sz%0D|Y%z|PpA_U!H#Gk5TOtdw`Q`L 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 literal 0 HcmV?d00001 diff --git a/test/wpt/tests/service-workers/service-worker/resources/simple-intercept-worker.js b/test/wpt/tests/service-workers/service-worker/resources/simple-intercept-worker.js new file mode 100644 index 00000000000..f8b5f8c5cb7 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/simple-intercept-worker.js @@ -0,0 +1,5 @@ +self.onfetch = function(event) { + if (event.request.url.indexOf('simple') != -1) + event.respondWith( + new Response(new Blob(['intercepted by service worker']))); +}; diff --git a/test/wpt/tests/service-workers/service-worker/resources/simple-intercept-worker.js.headers b/test/wpt/tests/service-workers/service-worker/resources/simple-intercept-worker.js.headers new file mode 100644 index 00000000000..a17a9a3a12c --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/simple-intercept-worker.js.headers @@ -0,0 +1 @@ +Content-Type: application/javascript diff --git a/test/wpt/tests/service-workers/service-worker/resources/simple.html b/test/wpt/tests/service-workers/service-worker/resources/simple.html new file mode 100644 index 00000000000..0c3e3e78707 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/simple.html @@ -0,0 +1,3 @@ + +Simple +Here's a simple html file. diff --git a/test/wpt/tests/service-workers/service-worker/resources/simple.txt b/test/wpt/tests/service-workers/service-worker/resources/simple.txt new file mode 100644 index 00000000000..9e3cb91fb9b --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/simple.txt @@ -0,0 +1 @@ +a simple text file diff --git a/test/wpt/tests/service-workers/service-worker/resources/skip-waiting-installed-worker.js b/test/wpt/tests/service-workers/service-worker/resources/skip-waiting-installed-worker.js new file mode 100644 index 00000000000..6f7008bddcd --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/skip-waiting-installed-worker.js @@ -0,0 +1,33 @@ +var saw_activate_event = false + +self.addEventListener('activate', function() { + saw_activate_event = true; + }); + +self.addEventListener('message', function(event) { + var port = event.data.port; + event.waitUntil(self.skipWaiting() + .then(function(result) { + if (result !== undefined) { + port.postMessage('FAIL: Promise should be resolved with undefined'); + return; + } + + if (!saw_activate_event) { + port.postMessage( + 'FAIL: Promise should be resolved after activate event is dispatched'); + return; + } + + if (self.registration.active.state !== 'activating') { + port.postMessage( + 'FAIL: Promise should be resolved before ServiceWorker#state is set to activated'); + return; + } + + port.postMessage('PASS'); + }) + .catch(function(e) { + port.postMessage('FAIL: unexpected exception: ' + e); + })); + }); diff --git a/test/wpt/tests/service-workers/service-worker/resources/skip-waiting-worker.js b/test/wpt/tests/service-workers/service-worker/resources/skip-waiting-worker.js new file mode 100644 index 00000000000..3fc1d1e237a --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/skip-waiting-worker.js @@ -0,0 +1,21 @@ +importScripts('worker-testharness.js'); + +promise_test(function() { + return skipWaiting() + .then(function(result) { + assert_equals(result, undefined, + 'Promise should be resolved with undefined'); + }) + .then(function() { + var promises = []; + for (var i = 0; i < 8; ++i) + promises.push(self.skipWaiting()); + return Promise.all(promises); + }) + .then(function(results) { + results.forEach(function(r) { + assert_equals(r, undefined, + 'Promises should be resolved with undefined'); + }); + }); + }, 'skipWaiting'); diff --git a/test/wpt/tests/service-workers/service-worker/resources/square.png b/test/wpt/tests/service-workers/service-worker/resources/square.png new file mode 100644 index 0000000000000000000000000000000000000000..01c9666a8de9d5535615aff830810e5df4b2156f GIT binary patch literal 18299 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 bDgs((CoOJ&@$4~l+kmsZyIqm(x-I_(KvUo& literal 0 HcmV?d00001 diff --git a/test/wpt/tests/service-workers/service-worker/resources/square.png.sub.headers b/test/wpt/tests/service-workers/service-worker/resources/square.png.sub.headers new file mode 100644 index 00000000000..7341132745b --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/square.png.sub.headers @@ -0,0 +1,2 @@ +Content-Type: image/png +Access-Control-Allow-Origin: * diff --git a/test/wpt/tests/service-workers/service-worker/resources/stalling-service-worker.js b/test/wpt/tests/service-workers/service-worker/resources/stalling-service-worker.js new file mode 100644 index 00000000000..fdf1e6cac04 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/stalling-service-worker.js @@ -0,0 +1,54 @@ +async function post_message_to_client(role, message, ports) { + (await clients.matchAll()).forEach(client => { + if (new URL(client.url).searchParams.get('role') === role) { + client.postMessage(message, ports); + } + }); +} + +async function post_message_to_child(message, ports) { + await post_message_to_client('child', message, ports); +} + +function ping_message(data) { + return { type: 'ping', data }; +} + +self.onmessage = event => { + const message = ping_message(event.data); + post_message_to_child(message); + post_message_to_parent(message); +} + +async function post_message_to_parent(message, ports) { + await post_message_to_client('parent', message, ports); +} + +function fetch_message(key) { + return { type: 'fetch', key }; +} + +// Send a message to the parent along with a MessagePort to respond +// with. +function report_fetch_request(key) { + const channel = new MessageChannel(); + const reply = new Promise(resolve => { + channel.port1.onmessage = resolve; + }).then(event => event.data); + return post_message_to_parent(fetch_message(key), [channel.port2]).then(() => reply); +} + +function respond_with_script(script) { + return new Response(new Blob(script, { type: 'text/javascript' })); +} + +// Whenever a controlled document requests a URL with a 'key' search +// parameter we report the request to the parent frame and wait for +// a response. The content of the response is then used to respond to +// the fetch request. +addEventListener('fetch', event => { + let key = new URL(event.request.url).searchParams.get('key'); + if (key) { + event.respondWith(report_fetch_request(key).then(respond_with_script)); + } +}); diff --git a/test/wpt/tests/service-workers/service-worker/resources/subdir/blank.html b/test/wpt/tests/service-workers/service-worker/resources/subdir/blank.html new file mode 100644 index 00000000000..a3c3a4689a6 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/subdir/blank.html @@ -0,0 +1,2 @@ + +Empty doc diff --git a/test/wpt/tests/service-workers/service-worker/resources/subdir/import-scripts-echo.py b/test/wpt/tests/service-workers/service-worker/resources/subdir/import-scripts-echo.py new file mode 100644 index 00000000000..f745d7ae46f --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/subdir/import-scripts-echo.py @@ -0,0 +1,6 @@ +def main(req, res): + return ([ + (b'Cache-Control', b'no-cache, must-revalidate'), + (b'Pragma', b'no-cache'), + (b'Content-Type', b'application/javascript')], + b'echo_output = "%s (subdir/)";\n' % req.GET[b'msg']) diff --git a/test/wpt/tests/service-workers/service-worker/resources/subdir/simple.txt b/test/wpt/tests/service-workers/service-worker/resources/subdir/simple.txt new file mode 100644 index 00000000000..86bcdd7dc56 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/subdir/simple.txt @@ -0,0 +1 @@ +a simple text file (subdir/) diff --git a/test/wpt/tests/service-workers/service-worker/resources/subdir/worker_interception_redirect_webworker.py b/test/wpt/tests/service-workers/service-worker/resources/subdir/worker_interception_redirect_webworker.py new file mode 100644 index 00000000000..bb4c874aace --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/subdir/worker_interception_redirect_webworker.py @@ -0,0 +1,6 @@ +import os +import imp +# Use the file from the parent directory. +mod = imp.load_source("_parent", os.path.join(os.path.dirname(os.path.dirname(__file__)), + os.path.basename(__file__))) +main = mod.main diff --git a/test/wpt/tests/service-workers/service-worker/resources/success.py b/test/wpt/tests/service-workers/service-worker/resources/success.py new file mode 100644 index 00000000000..a0269918ee8 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/success.py @@ -0,0 +1,8 @@ +def main(request, response): + headers = [] + + if b"ACAOrigin" in request.GET: + for item in request.GET[b"ACAOrigin"].split(b","): + headers.append((b"Access-Control-Allow-Origin", item)) + + return headers, b"{ \"result\": \"success\" }" diff --git a/test/wpt/tests/service-workers/service-worker/resources/svg-target-reftest-001-frame.html b/test/wpt/tests/service-workers/service-worker/resources/svg-target-reftest-001-frame.html new file mode 100644 index 00000000000..59fb524049d --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/svg-target-reftest-001-frame.html @@ -0,0 +1,3 @@ + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/svg-target-reftest-001.html b/test/wpt/tests/service-workers/service-worker/resources/svg-target-reftest-001.html new file mode 100644 index 00000000000..9a93d3b3704 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/svg-target-reftest-001.html @@ -0,0 +1,5 @@ + + +Green svg box reference file +

Pass if you see a green box below.

+ + diff --git a/test/wpt/tests/storage/resources/partitioned-estimate-usage-details-caches-helper-frame.html b/test/wpt/tests/storage/resources/partitioned-estimate-usage-details-caches-helper-frame.html new file mode 100644 index 00000000000..0679c1decf5 --- /dev/null +++ b/test/wpt/tests/storage/resources/partitioned-estimate-usage-details-caches-helper-frame.html @@ -0,0 +1,30 @@ + + + +Helper frame + + diff --git a/test/wpt/tests/storage/resources/partitioned-estimate-usage-details-indexeddb-helper-frame.html b/test/wpt/tests/storage/resources/partitioned-estimate-usage-details-indexeddb-helper-frame.html new file mode 100644 index 00000000000..fd2cfb669bd --- /dev/null +++ b/test/wpt/tests/storage/resources/partitioned-estimate-usage-details-indexeddb-helper-frame.html @@ -0,0 +1,28 @@ + + +Helper frame + + diff --git a/test/wpt/tests/storage/resources/partitioned-estimate-usage-details-service-workers-helper-frame.html b/test/wpt/tests/storage/resources/partitioned-estimate-usage-details-service-workers-helper-frame.html new file mode 100644 index 00000000000..25d7554868f --- /dev/null +++ b/test/wpt/tests/storage/resources/partitioned-estimate-usage-details-service-workers-helper-frame.html @@ -0,0 +1,30 @@ + + +Helper frame + + diff --git a/test/wpt/tests/storage/resources/worker.js b/test/wpt/tests/storage/resources/worker.js new file mode 100644 index 00000000000..9271c769b25 --- /dev/null +++ b/test/wpt/tests/storage/resources/worker.js @@ -0,0 +1,3 @@ +// Dummy service worker to observe some weight when querying the storage usage +// details from of the service worker from estimate(). +globalThis.oninstall = e => {}; diff --git a/test/wpt/tests/storage/storagemanager-estimate.https.any.js b/test/wpt/tests/storage/storagemanager-estimate.https.any.js new file mode 100644 index 00000000000..c2f5c569dc5 --- /dev/null +++ b/test/wpt/tests/storage/storagemanager-estimate.https.any.js @@ -0,0 +1,60 @@ +// META: title=StorageManager: estimate() + +test(function(t) { + assert_true(navigator.storage.estimate() instanceof Promise); +}, 'estimate() method returns a Promise'); + +promise_test(function(t) { + return navigator.storage.estimate().then(function(result) { + assert_equals(typeof result, 'object'); + assert_true('usage' in result); + assert_equals(typeof result.usage, 'number'); + assert_true('quota' in result); + assert_equals(typeof result.quota, 'number'); + }); +}, 'estimate() resolves to dictionary with members'); + +promise_test(function(t) { + const large_value = new Uint8Array(1e6); + const dbname = `db-${location}-${t.name}`; + let db, before, after; + + indexedDB.deleteDatabase(dbname); + return new Promise((resolve, reject) => { + const open = indexedDB.open(dbname); + open.onerror = () => { reject(open.error); }; + open.onupgradeneeded = () => { + const connection = open.result; + connection.createObjectStore('store'); + }; + open.onsuccess = () => { + const connection = open.result; + t.add_cleanup(() => { + connection.close(); + indexedDB.deleteDatabase(dbname); + }); + resolve(connection); + }; + }) + .then(connection => { + db = connection; + return navigator.storage.estimate(); + }) + .then(estimate => { + before = estimate.usage; + return new Promise((resolve, reject) => { + const tx = db.transaction('store', 'readwrite'); + tx.objectStore('store').put(large_value, 'key'); + tx.onabort = () => { reject(tx.error); }; + tx.oncomplete = () => { resolve(); }; + }); + }) + .then(() => { + return navigator.storage.estimate(); + }) + .then(estimate => { + after = estimate.usage; + assert_greater_than(after, before, + 'estimated usage should increase'); + }); +}, 'estimate() shows usage increase after 1MB IndexedDB record is stored'); diff --git a/test/wpt/tests/storage/storagemanager-persist.https.window.js b/test/wpt/tests/storage/storagemanager-persist.https.window.js new file mode 100644 index 00000000000..13e17a16e14 --- /dev/null +++ b/test/wpt/tests/storage/storagemanager-persist.https.window.js @@ -0,0 +1,10 @@ +// META: title=StorageManager: persist() + +promise_test(function() { + var promise = navigator.storage.persist(); + assert_true(promise instanceof Promise, + 'navigator.storage.persist() returned a Promise.'); + return promise.then(function(result) { + assert_equals(typeof result, 'boolean', result + ' should be boolean'); + }); +}, 'navigator.storage.persist() returns a promise that resolves.'); diff --git a/test/wpt/tests/storage/storagemanager-persist.https.worker.js b/test/wpt/tests/storage/storagemanager-persist.https.worker.js new file mode 100644 index 00000000000..fcf8175f706 --- /dev/null +++ b/test/wpt/tests/storage/storagemanager-persist.https.worker.js @@ -0,0 +1,8 @@ +// META: title=StorageManager: persist() (worker) +importScripts("/resources/testharness.js"); + +test(function() { + assert_false('persist' in navigator.storage); +}, 'navigator.storage.persist should not exist in workers'); + +done(); diff --git a/test/wpt/tests/storage/storagemanager-persisted.https.any.js b/test/wpt/tests/storage/storagemanager-persisted.https.any.js new file mode 100644 index 00000000000..70999406690 --- /dev/null +++ b/test/wpt/tests/storage/storagemanager-persisted.https.any.js @@ -0,0 +1,10 @@ +// META: title=StorageManager: persisted() + +promise_test(function() { + var promise = navigator.storage.persisted(); + assert_true(promise instanceof Promise, + 'navigator.storage.persisted() returned a Promise.'); + return promise.then(function (result) { + assert_equals(typeof result, 'boolean', result + ' should be boolean'); + }); +}, 'navigator.storage.persisted() returns a promise that resolves.'); diff --git a/types/cache.d.ts b/types/cache.d.ts new file mode 100644 index 00000000000..4767091be4b --- /dev/null +++ b/types/cache.d.ts @@ -0,0 +1,29 @@ +import type { RequestInfo, Response, Request } from './fetch' + +export interface CacheStorage { + match (request: RequestInfo, options?: MultiCacheQueryOptions): Promise, + has (cacheName: string): Promise, + open (cacheName: string): Promise, + delete (cacheName: string): Promise, + keys (): Promise +} + +export interface Cache { + match (request: RequestInfo, options?: CacheQueryOptions): Promise, + matchAll (request?: RequestInfo, options?: CacheQueryOptions): Promise, + add (request: RequestInfo): Promise, + addAll (requests: RequestInfo[]): Promise, + put (request: RequestInfo, response: Response): Promise, + delete (request: RequestInfo, options?: CacheQueryOptions): Promise, + keys (request?: RequestInfo, options?: CacheQueryOptions): Promise +} + +export interface CacheQueryOptions { + ignoreSearch?: boolean, + ignoreMethod?: boolean, + ignoreVary?: boolean +} + +export interface MultiCacheQueryOptions extends CacheQueryOptions { + cacheName?: string +} diff --git a/types/webidl.d.ts b/types/webidl.d.ts index 182d18e0d4c..40cfe064f8f 100644 --- a/types/webidl.d.ts +++ b/types/webidl.d.ts @@ -170,6 +170,8 @@ export interface Webidl { */ sequenceConverter (C: Converter): SequenceConverter + illegalConstructor (): never + /** * @see https://webidl.spec.whatwg.org/#es-to-record * @description Convert a value, V, to a WebIDL record type. From 27569e97c099276a21fc7ecaef961714a5ed8e80 Mon Sep 17 00:00:00 2001 From: Khafra Date: Thu, 20 Apr 2023 14:46:10 -0400 Subject: [PATCH 044/259] test: skip content-disposition test in node 18 (#2081) --- test/issue-1903.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/issue-1903.js b/test/issue-1903.js index 99eb6f60517..76ac81ef217 100644 --- a/test/issue-1903.js +++ b/test/issue-1903.js @@ -13,7 +13,7 @@ function createPromise () { return result } -test('should parse content-disposition consistently', { skip: nodeMajor >= 19 }, async (t) => { +test('should parse content-disposition consistently', { skip: nodeMajor >= 18 }, async (t) => { t.plan(5) // create promise to allow server spinup in parallel From 674e2b6901ca91681c5657b686f778898d3387e5 Mon Sep 17 00:00:00 2001 From: Khafra Date: Fri, 21 Apr 2023 04:14:42 -0400 Subject: [PATCH 045/259] Cache storage cleanup (#2082) * cache: add docs, expose, types, type tests * cache: implement CacheStorage.prototype.match * fix(cache): use correct webidl converter in CacheStorage.prototype.match --- docs/api/CacheStorage.md | 30 ++++++++++++++ docs/api/Fetch.md | 2 + docsify/sidebar.md | 1 + index.d.ts | 2 + index.js | 7 ++++ lib/cache/cache.js | 12 +++++- lib/cache/cachestorage.js | 26 ++++++++++++- test/types/cache-storage.test-d.ts | 39 +++++++++++++++++++ .../service-workers/cache-storage.status.json | 4 -- types/cache.d.ts | 7 ++++ 10 files changed, 124 insertions(+), 6 deletions(-) create mode 100644 docs/api/CacheStorage.md create mode 100644 test/types/cache-storage.test-d.ts diff --git a/docs/api/CacheStorage.md b/docs/api/CacheStorage.md new file mode 100644 index 00000000000..08ee99fab14 --- /dev/null +++ b/docs/api/CacheStorage.md @@ -0,0 +1,30 @@ +# CacheStorage + +Undici exposes a W3C spec-compliant implementation of [CacheStorage](https://developer.mozilla.org/en-US/docs/Web/API/CacheStorage) and [Cache](https://developer.mozilla.org/en-US/docs/Web/API/Cache). + +## Opening a Cache + +Undici exports a top-level CacheStorage instance. You can open a new Cache, or duplicate a Cache with an existing name, by using `CacheStorage.prototype.open`. If you open a Cache with the same name as an already-existing Cache, its list of cached Responses will be shared between both instances. + +```mjs +import { caches } from 'undici' + +const cache_1 = await caches.open('v1') +const cache_2 = await caches.open('v1') + +// Although .open() creates a new instance, +assert(cache_1 !== cache_2) +// The same Response is matched in both. +assert.deepStrictEqual(await cache_1.match('/req'), await cache_2.match('/req')) +``` + +## Deleting a Cache + +If a Cache is deleted, the cached Responses/Requests can still be used. + +```mjs +const response = await cache_1.match('/req') +await caches.delete('v1') + +await response.text() // the Response's body +``` diff --git a/docs/api/Fetch.md b/docs/api/Fetch.md index 0a5c3d09699..b5a62422a24 100644 --- a/docs/api/Fetch.md +++ b/docs/api/Fetch.md @@ -8,6 +8,8 @@ Documentation and examples can be found on [MDN](https://developer.mozilla.org/e This API is implemented as per the standard, you can find documentation on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/File) +In Node versions v18.13.0 and above and v19.2.0 and above, undici will default to using Node's [File](https://nodejs.org/api/buffer.html#class-file) class. In versions where it's not available, it will default to the undici one. + ## FormData This API is implemented as per the standard, you can find documentation on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/FormData) diff --git a/docsify/sidebar.md b/docsify/sidebar.md index 015e6f48842..b7c7d6a3bdc 100644 --- a/docsify/sidebar.md +++ b/docsify/sidebar.md @@ -20,6 +20,7 @@ * [Diagnostics Channel Support](/docs/api/DiagnosticsChannel.md "Diagnostics Channel Support") * [WebSocket](/docs/api/WebSocket.md "Undici API - WebSocket") * [MIME Type Parsing](/docs/api/ContentType.md "Undici API - MIME Type Parsing") + * [CacheStorage](/docs/api/CacheStorage.md "Undici API - CacheStorage") * Best Practices * [Proxy](/docs/best-practices/proxy.md "Connecting through a proxy") * [Client Certificate](/docs/best-practices/client-certificate.md "Connect using a client certificate") diff --git a/index.d.ts b/index.d.ts index d67de97241d..0730677b29e 100644 --- a/index.d.ts +++ b/index.d.ts @@ -24,6 +24,7 @@ export * from './types/formdata' export * from './types/diagnostics-channel' export * from './types/websocket' export * from './types/content-type' +export * from './types/cache' export { Interceptable } from './types/mock-interceptor' export { Dispatcher, BalancedPool, Pool, Client, buildConnector, errors, Agent, request, stream, pipeline, connect, upgrade, setGlobalDispatcher, getGlobalDispatcher, setGlobalOrigin, getGlobalOrigin, MockClient, MockPool, MockAgent, mockErrors, ProxyAgent, RedirectHandler, DecoratorHandler } @@ -52,4 +53,5 @@ declare namespace Undici { var MockAgent: typeof import('./types/mock-agent').default; var mockErrors: typeof import('./types/mock-errors').default; var fetch: typeof import('./types/fetch').fetch; + var caches: typeof import('./types/cache').caches; } diff --git a/index.js b/index.js index 02ac246fa45..7e8831ceeea 100644 --- a/index.js +++ b/index.js @@ -121,6 +121,13 @@ if (util.nodeMajor > 16 || (util.nodeMajor === 16 && util.nodeMinor >= 8)) { module.exports.setGlobalOrigin = setGlobalOrigin module.exports.getGlobalOrigin = getGlobalOrigin + + const { CacheStorage } = require('./lib/cache/cachestorage') + const { kConstruct } = require('./lib/cache/symbols') + + // Cache & CacheStorage are tightly coupled with fetch. Even if it may run + // in an older version of Node, it doesn't have any use without fetch. + module.exports.caches = new CacheStorage(kConstruct) } if (util.nodeMajor >= 16) { diff --git a/lib/cache/cache.js b/lib/cache/cache.js index 8838e31c5f3..087185b6704 100644 --- a/lib/cache/cache.js +++ b/lib/cache/cache.js @@ -805,7 +805,7 @@ Object.defineProperties(Cache.prototype, { keys: kEnumerableProperty }) -webidl.converters.CacheQueryOptions = webidl.dictionaryConverter([ +const cacheQueryOptionConverters = [ { key: 'ignoreSearch', converter: webidl.converters.boolean, @@ -821,6 +821,16 @@ webidl.converters.CacheQueryOptions = webidl.dictionaryConverter([ converter: webidl.converters.boolean, defaultValue: false } +] + +webidl.converters.CacheQueryOptions = webidl.dictionaryConverter(cacheQueryOptionConverters) + +webidl.converters.MultiCacheQueryOptions = webidl.dictionaryConverter([ + ...cacheQueryOptionConverters, + { + key: 'cacheName', + converter: webidl.converters.DOMString + } ]) webidl.converters.Response = webidl.interfaceConverter(Response) diff --git a/lib/cache/cachestorage.js b/lib/cache/cachestorage.js index 4a6795932b7..f28f42abf73 100644 --- a/lib/cache/cachestorage.js +++ b/lib/cache/cachestorage.js @@ -23,7 +23,31 @@ class CacheStorage { webidl.argumentLengthCheck(arguments, 1, { header: 'CacheStorage.match' }) request = webidl.converters.RequestInfo(request) - options = webidl.converters.CacheQueryOptions(options) + options = webidl.converters.MultiCacheQueryOptions(options) + + // 1. + if (options.cacheName != null) { + // 1.1.1.1 + if (this.#caches.has(options.cacheName)) { + // 1.1.1.1.1 + const cacheList = this.#caches.get(options.cacheName) + const cache = new Cache(kConstruct, cacheList) + + return await cache.match(request, options) + } + } else { // 2. + // 2.2 + for (const cacheList of this.#caches.values()) { + const cache = new Cache(kConstruct, cacheList) + + // 2.2.1.2 + const response = await cache.match(request, options) + + if (response !== undefined) { + return response + } + } + } } /** diff --git a/test/types/cache-storage.test-d.ts b/test/types/cache-storage.test-d.ts new file mode 100644 index 00000000000..c21efbdf56c --- /dev/null +++ b/test/types/cache-storage.test-d.ts @@ -0,0 +1,39 @@ +import { expectAssignable } from 'tsd' +import { + caches, + CacheStorage, + Cache, + CacheQueryOptions, + MultiCacheQueryOptions, + RequestInfo, + Request, + Response +} from '../..' + +declare const response: Response +declare const request: Request +declare const options: RequestInfo +declare const cache: Cache + +expectAssignable(caches) +expectAssignable({}) +expectAssignable({ cacheName: 'v1' }) +expectAssignable({ ignoreMethod: false, ignoreSearch: true }) + +expectAssignable({}) +expectAssignable({ ignoreVary: false, ignoreMethod: true, ignoreSearch: true }) + +expectAssignable>(caches.open('v1')) +expectAssignable>(caches.match(options)) +expectAssignable>(caches.match(request)) +expectAssignable>(caches.has('v1')) +expectAssignable>(caches.delete('v1')) +expectAssignable>(caches.keys()) + +expectAssignable>(cache.match(options)) +expectAssignable>(cache.matchAll('v1')) +expectAssignable>(cache.delete('v1')) +expectAssignable>(cache.keys()) +expectAssignable>(cache.add(options)) +expectAssignable>(cache.addAll([options])) +expectAssignable>(cache.put(options, response)) diff --git a/test/wpt/status/service-workers/cache-storage.status.json b/test/wpt/status/service-workers/cache-storage.status.json index ed84c140eca..06469aec3cd 100644 --- a/test/wpt/status/service-workers/cache-storage.status.json +++ b/test/wpt/status/service-workers/cache-storage.status.json @@ -7,10 +7,6 @@ "skip": true, "note": "navigator is not defined" }, - "cache-storage-match.https.any.js": { - "skip": true, - "note": "CacheStorage.prototype.match isnt implemented yet" - }, "cache-put.https.any.js": { "note": "probably can be fixed", "fail": [ diff --git a/types/cache.d.ts b/types/cache.d.ts index 4767091be4b..4c333357666 100644 --- a/types/cache.d.ts +++ b/types/cache.d.ts @@ -8,6 +8,11 @@ export interface CacheStorage { keys (): Promise } +declare const CacheStorage: { + prototype: CacheStorage + new(): CacheStorage +} + export interface Cache { match (request: RequestInfo, options?: CacheQueryOptions): Promise, matchAll (request?: RequestInfo, options?: CacheQueryOptions): Promise, @@ -27,3 +32,5 @@ export interface CacheQueryOptions { export interface MultiCacheQueryOptions extends CacheQueryOptions { cacheName?: string } + +export declare const caches: CacheStorage From eeaa0735e1c51dd09706dff97ba06e63ae9fc72b Mon Sep 17 00:00:00 2001 From: Khafra Date: Sat, 22 Apr 2023 03:43:17 -0400 Subject: [PATCH 046/259] Cache storage fixes (#2083) --- lib/cache/cache.js | 6 +++--- lib/cache/cachestorage.js | 15 +-------------- test/wpt/runner/worker.mjs | 4 ++-- test/wpt/server/server.mjs | 7 ++----- test/wpt/status/fetch.status.json | 3 ++- .../service-workers/cache-storage.status.json | 1 - 6 files changed, 10 insertions(+), 26 deletions(-) diff --git a/lib/cache/cache.js b/lib/cache/cache.js index 087185b6704..04a29151d17 100644 --- a/lib/cache/cache.js +++ b/lib/cache/cache.js @@ -32,7 +32,7 @@ class Cache { * @see https://w3c.github.io/ServiceWorker/#dfn-relevant-request-response-list * @type {requestResponseList} */ - #relevantRequestResponseList = [] + #relevantRequestResponseList constructor () { if (arguments[0] !== kConstruct) { @@ -314,7 +314,7 @@ class Cache { let innerRequest = null // 2. - if (typeof request !== 'string') { + if (request instanceof Request) { innerRequest = request[kState] } else { // 3. innerRequest = new Request(request)[kState] @@ -457,7 +457,7 @@ class Cache { } else { assert(typeof request === 'string') - r = new Request(r)[kState] + r = new Request(request)[kState] } /** @type {CacheBatchOperation[]} */ diff --git a/lib/cache/cachestorage.js b/lib/cache/cachestorage.js index f28f42abf73..7e7f0cff2b5 100644 --- a/lib/cache/cachestorage.js +++ b/lib/cache/cachestorage.js @@ -109,20 +109,7 @@ class CacheStorage { cacheName = webidl.converters.DOMString(cacheName) - // 1. - // 2. - const cacheExists = this.#caches.has(cacheName) - - // 2.1 - if (!cacheExists) { - return false - } - - // 2.3.1 - this.#caches.delete(cacheName) - - // 2.3.2 - return true + return this.#caches.delete(cacheName) } /** diff --git a/test/wpt/runner/worker.mjs b/test/wpt/runner/worker.mjs index 638f53d7a57..90bfcf6dbde 100644 --- a/test/wpt/runner/worker.mjs +++ b/test/wpt/runner/worker.mjs @@ -108,7 +108,7 @@ runInThisContext(` } } globalThis.window = globalThis - globalThis.location = new URL('${url}') + globalThis.location = new URL('${urlPath.replace(/\\/g, '/')}', '${url}') globalThis.Window = Object.getPrototypeOf(globalThis).constructor `) @@ -142,7 +142,7 @@ add_completion_callback((_, status) => { }) }) -setGlobalOrigin(new URL(urlPath, url)) +setGlobalOrigin(globalThis.location) // Inject any script the user provided before // running the tests. diff --git a/test/wpt/server/server.mjs b/test/wpt/server/server.mjs index 75039acb916..7074e154dd1 100644 --- a/test/wpt/server/server.mjs +++ b/test/wpt/server/server.mjs @@ -36,6 +36,7 @@ const server = createServer(async (req, res) => { res.setHeader('content-type', 'text/html') // fall through } + case '/service-workers/cache-storage/resources/simple.txt': case '/fetch/content-encoding/resources/foo.octetstream.gz': case '/fetch/content-encoding/resources/foo.text.gz': case '/fetch/api/resources/cors-top.txt': @@ -361,11 +362,7 @@ const server = createServer(async (req, res) => { res.end('') return } - case '/resources/simple.txt': { - res.end(readFileSync(join(tests, 'service-workers/service-worker', fullUrl.pathname), 'utf-8')) - return - } - case '/resources/fetch-status.py': { + case '/service-workers/cache-storage/resources/fetch-status.py': { const status = Number(fullUrl.searchParams.get('status')) res.statusCode = status diff --git a/test/wpt/status/fetch.status.json b/test/wpt/status/fetch.status.json index cf814114752..72930388cb3 100644 --- a/test/wpt/status/fetch.status.json +++ b/test/wpt/status/fetch.status.json @@ -53,7 +53,8 @@ "fail": [ "origin-when-cross-origin policy on a cross-origin URL", "origin-when-cross-origin policy on a cross-origin URL after same-origin redirection", - "origin-when-cross-origin policy on a same-origin URL after cross-origin redirection" + "origin-when-cross-origin policy on a same-origin URL after cross-origin redirection", + "origin-when-cross-origin policy on a same-origin URL" ] }, "request-forbidden-headers.any.js": { diff --git a/test/wpt/status/service-workers/cache-storage.status.json b/test/wpt/status/service-workers/cache-storage.status.json index 06469aec3cd..6f7cd5287c0 100644 --- a/test/wpt/status/service-workers/cache-storage.status.json +++ b/test/wpt/status/service-workers/cache-storage.status.json @@ -25,7 +25,6 @@ "cache-delete.https.any.js": { "note": "spec bug? - https://github.com/w3c/ServiceWorker/issues/1677 (first fail)", "fail": [ - "Cache.delete called with a string URL", "Cache.delete with ignoreSearch option (when it is specified as false)" ] }, From 1b3fed97209f5da29db853b7742c14b94aaa8b53 Mon Sep 17 00:00:00 2001 From: Khafra Date: Sat, 22 Apr 2023 22:14:38 -0400 Subject: [PATCH 047/259] test: improve test coverage for ErrorEvent and MessageEvent (#2085) --- test/websocket/events.js | 103 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/test/websocket/events.js b/test/websocket/events.js index 89e4e0ba353..e5b565c3e66 100644 --- a/test/websocket/events.js +++ b/test/websocket/events.js @@ -99,3 +99,106 @@ test('Event handlers', (t) => { t.equal(ws.onmessage, listen) }) }) + +test('CloseEvent WPTs ported', (t) => { + t.test('initCloseEvent', (t) => { + // Taken from websockets/interfaces/CloseEvent/historical.html + t.notOk('initCloseEvent' in CloseEvent.prototype) + t.notOk('initCloseEvent' in new CloseEvent('close')) + + t.end() + }) + + t.test('CloseEvent constructor', (t) => { + // Taken from websockets/interfaces/CloseEvent/constructor.html + + { + const event = new CloseEvent('foo') + + t.ok(event instanceof CloseEvent, 'should be a CloseEvent') + t.equal(event.type, 'foo') + t.notOk(event.bubbles, 'bubbles') + t.notOk(event.cancelable, 'cancelable') + t.notOk(event.wasClean, 'wasClean') + t.equal(event.code, 0) + t.equal(event.reason, '') + } + + { + const event = new CloseEvent('foo', { + bubbles: true, + cancelable: true, + wasClean: true, + code: 7, + reason: 'x' + }) + t.ok(event instanceof CloseEvent, 'should be a CloseEvent') + t.equal(event.type, 'foo') + t.ok(event.bubbles, 'bubbles') + t.ok(event.cancelable, 'cancelable') + t.ok(event.wasClean, 'wasClean') + t.equal(event.code, 7) + t.equal(event.reason, 'x') + } + + t.end() + }) + + t.end() +}) + +test('ErrorEvent WPTs ported', (t) => { + t.test('Synthetic ErrorEvent', (t) => { + // Taken from html/webappapis/scripting/events/event-handler-processing-algorithm-error/document-synthetic-errorevent.html + + { + const e = new ErrorEvent('error') + t.equal(e.message, '') + t.equal(e.filename, '') + t.equal(e.lineno, 0) + t.equal(e.colno, 0) + t.equal(e.error, undefined) + } + + { + const e = new ErrorEvent('error', { error: null }) + t.equal(e.error, null) + } + + { + const e = new ErrorEvent('error', { error: undefined }) + t.equal(e.error, undefined) + } + + { + const e = new ErrorEvent('error', { error: 'foo' }) + t.equal(e.error, 'foo') + } + + t.end() + }) + + t.test('webidl', (t) => { + // Taken from webidl/ecmascript-binding/no-regexp-special-casing.any.js + + const regExp = new RegExp() + regExp.message = 'some message' + + const errorEvent = new ErrorEvent('type', regExp) + + t.equal(errorEvent.message, 'some message') + + t.end() + }) + + t.test('initErrorEvent', (t) => { + // Taken from workers/Worker_dispatchEvent_ErrorEvent.htm + + const e = new ErrorEvent('error') + t.notOk('initErrorEvent' in e, 'should not be supported') + + t.end() + }) + + t.end() +}) From 286d5ece34fbe57757a35cbc70e38c3cad14a492 Mon Sep 17 00:00:00 2001 From: Khafra Date: Sun, 23 Apr 2023 10:50:01 -0400 Subject: [PATCH 048/259] test: remove --experimental-wasm-simd (#2087) --- .taprc | 1 - package.json | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.taprc b/.taprc index 49ff54c9f59..61f70513a70 100644 --- a/.taprc +++ b/.taprc @@ -5,4 +5,3 @@ coverage: false expose-gc: true timeout: 60 check-coverage: false -node-arg: --experimental-wasm-simd diff --git a/package.json b/package.json index 481a6c88108..fb43ff7e83c 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "bench": "PORT=3042 concurrently -k -s first npm:bench:server npm:bench:run", "bench:server": "node benchmarks/server.js", "prebench:run": "node benchmarks/wait.js", - "bench:run": "CONNECTIONS=1 node --experimental-wasm-simd benchmarks/benchmark.js; CONNECTIONS=50 node --experimental-wasm-simd benchmarks/benchmark.js", + "bench:run": "CONNECTIONS=1 node benchmarks/benchmark.js; CONNECTIONS=50 node benchmarks/benchmark.js", "serve:website": "docsify serve .", "prepare": "husky install", "fuzz": "jsfuzz test/fuzzing/fuzz.js corpus" From 7fe5a0d85f802bfbeaa967d05a5a8c7146e7085d Mon Sep 17 00:00:00 2001 From: Khafra Date: Sun, 23 Apr 2023 18:30:57 -0400 Subject: [PATCH 049/259] websocket: add websocketinit (#2088) * websocket: add websocketinit Refs: https://github.com/nodejs/undici/issues/1811#issuecomment-1518940816 * update types * remove 3rd param it's not as backwards compatible as I thought... * update docs --- docs/api/WebSocket.md | 29 ++++++++++++++++++--- lib/websocket/connection.js | 5 ++-- lib/websocket/websocket.js | 34 +++++++++++++++++++++++-- test/websocket/websocketinit.js | 45 +++++++++++++++++++++++++++++++++ types/websocket.d.ts | 8 +++++- 5 files changed, 113 insertions(+), 8 deletions(-) create mode 100644 test/websocket/websocketinit.js diff --git a/docs/api/WebSocket.md b/docs/api/WebSocket.md index 639a5333a1c..9d374f4046c 100644 --- a/docs/api/WebSocket.md +++ b/docs/api/WebSocket.md @@ -1,17 +1,40 @@ # Class: WebSocket -> ⚠️ Warning: the WebSocket API is experimental and has known bugs. +> ⚠️ Warning: the WebSocket API is experimental. Extends: [`EventTarget`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget) -The WebSocket object provides a way to manage a WebSocket connection to a server, allowing bidirectional communication. The API follows the [WebSocket spec](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket). +The WebSocket object provides a way to manage a WebSocket connection to a server, allowing bidirectional communication. The API follows the [WebSocket spec](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) and [RFC 6455](https://datatracker.ietf.org/doc/html/rfc6455). ## `new WebSocket(url[, protocol])` Arguments: * **url** `URL | string` - The url's protocol *must* be `ws` or `wss`. -* **protocol** `string | string[]` (optional) - Subprotocol(s) to request the server use. +* **protocol** `string | string[] | WebSocketInit` (optional) - Subprotocol(s) to request the server use, or a [`Dispatcher`](./Dispatcher.md). + +### Example: + +This example will not work in browsers or other platforms that don't allow passing an object. + +```mjs +import { WebSocket, ProxyAgent } from 'undici' + +const proxyAgent = new ProxyAgent('my.proxy.server') + +const ws = new WebSocket('wss://echo.websocket.events', { + dispatcher: proxyAgent, + protocols: ['echo', 'chat'] +}) +``` + +If you do not need a custom Dispatcher, it's recommended to use the following pattern: + +```mjs +import { WebSocket } from 'undici' + +const ws = new WebSocket('wss://echo.websocket.events', ['echo', 'chat']) +``` ## Read More diff --git a/lib/websocket/connection.js b/lib/websocket/connection.js index 09770247e3f..581f268e059 100644 --- a/lib/websocket/connection.js +++ b/lib/websocket/connection.js @@ -26,8 +26,9 @@ channels.socketError = diagnosticsChannel.channel('undici:websocket:socket_error * @param {string|string[]} protocols * @param {import('./websocket').WebSocket} ws * @param {(response: any) => void} onEstablish + * @param {Partial} options */ -function establishWebSocketConnection (url, protocols, ws, onEstablish) { +function establishWebSocketConnection (url, protocols, ws, onEstablish, options) { // 1. Let requestURL be a copy of url, with its scheme set to "http", if url’s // scheme is "ws", and to "https" otherwise. const requestURL = url @@ -88,7 +89,7 @@ function establishWebSocketConnection (url, protocols, ws, onEstablish) { const controller = fetching({ request, useParallelQueue: true, - dispatcher: getGlobalDispatcher(), + dispatcher: options.dispatcher ?? getGlobalDispatcher(), processResponse (response) { // 1. If response is a network error or its status is not 101, // fail the WebSocket connection. diff --git a/lib/websocket/websocket.js b/lib/websocket/websocket.js index 164d24c6f8a..762f25684ab 100644 --- a/lib/websocket/websocket.js +++ b/lib/websocket/websocket.js @@ -18,6 +18,7 @@ const { establishWebSocketConnection } = require('./connection') const { WebsocketFrameSend } = require('./frame') const { ByteParser } = require('./receiver') const { kEnumerableProperty, isBlobLike } = require('../core/util') +const { getGlobalDispatcher } = require('../global') const { types } = require('util') let experimentalWarned = false @@ -51,8 +52,10 @@ class WebSocket extends EventTarget { }) } + const options = webidl.converters['DOMString or sequence or WebSocketInit'](protocols) + url = webidl.converters.USVString(url) - protocols = webidl.converters['DOMString or sequence'](protocols) + protocols = options.protocols // 1. Let urlRecord be the result of applying the URL parser to url. let urlRecord @@ -110,7 +113,8 @@ class WebSocket extends EventTarget { urlRecord, protocols, this, - (response) => this.#onConnectionEstablished(response) + (response) => this.#onConnectionEstablished(response), + options ) // Each WebSocket object has an associated ready state, which is a @@ -577,6 +581,32 @@ webidl.converters['DOMString or sequence'] = function (V) { return webidl.converters.DOMString(V) } +// This implements the propsal made in https://github.com/whatwg/websockets/issues/42 +webidl.converters.WebSocketInit = webidl.dictionaryConverter([ + { + key: 'protocols', + converter: webidl.converters['DOMString or sequence'], + get defaultValue () { + return [] + } + }, + { + key: 'dispatcher', + converter: (V) => V, + get defaultValue () { + return getGlobalDispatcher() + } + } +]) + +webidl.converters['DOMString or sequence or WebSocketInit'] = function (V) { + if (webidl.util.Type(V) === 'Object' && !(Symbol.iterator in V)) { + return webidl.converters.WebSocketInit(V) + } + + return { protocols: webidl.converters['DOMString or sequence'](V) } +} + webidl.converters.WebSocketSendData = function (V) { if (webidl.util.Type(V) === 'Object') { if (isBlobLike(V)) { diff --git a/test/websocket/websocketinit.js b/test/websocket/websocketinit.js new file mode 100644 index 00000000000..4dda3b48188 --- /dev/null +++ b/test/websocket/websocketinit.js @@ -0,0 +1,45 @@ +'use strict' + +const { test } = require('tap') +const { WebSocketServer } = require('ws') +const { WebSocket, Dispatcher, Agent } = require('../..') + +test('WebSocketInit', (t) => { + t.plan(2) + + class WsDispatcher extends Dispatcher { + constructor () { + super() + this.agent = new Agent() + } + + dispatch () { + t.pass() + return this.agent.dispatch(...arguments) + } + } + + t.test('WebSocketInit as 2nd param', (t) => { + t.plan(1) + + const server = new WebSocketServer({ port: 0 }) + + server.on('connection', (ws) => { + ws.send(Buffer.from('hello, world')) + }) + + t.teardown(server.close.bind(server)) + + const ws = new WebSocket(`ws://localhost:${server.address().port}`, { + dispatcher: new WsDispatcher() + }) + + ws.onerror = t.fail + + ws.addEventListener('message', async (event) => { + t.equal(await event.data.text(), 'hello, world') + server.close() + ws.close() + }) + }) +}) diff --git a/types/websocket.d.ts b/types/websocket.d.ts index 7524cbda6c4..578c7602e64 100644 --- a/types/websocket.d.ts +++ b/types/websocket.d.ts @@ -10,6 +10,7 @@ import { AddEventListenerOptions, EventListenerOrEventListenerObject } from './patch' +import Dispatcher from './dispatcher' export type BinaryType = 'blob' | 'arraybuffer' @@ -67,7 +68,7 @@ interface WebSocket extends EventTarget { export declare const WebSocket: { prototype: WebSocket - new (url: string | URL, protocols?: string | string[]): WebSocket + new (url: string | URL, protocols?: string | string[] | WebSocketInit): WebSocket readonly CLOSED: number readonly CLOSING: number readonly CONNECTING: number @@ -121,3 +122,8 @@ export declare const MessageEvent: { prototype: MessageEvent new(type: string, eventInitDict?: MessageEventInit): MessageEvent } + +interface WebSocketInit { + protocols?: string | string[], + dispatcher?: Dispatcher +} From a3efc9814447001a43a976f1c64adc41995df7e3 Mon Sep 17 00:00:00 2001 From: Khafra Date: Mon, 24 Apr 2023 10:59:18 -0400 Subject: [PATCH 050/259] feat(websocket): allow setting custom headers (#2089) --- lib/websocket/connection.js | 9 +++++++++ lib/websocket/websocket.js | 4 ++++ test/websocket/custom-headers.js | 30 ++++++++++++++++++++++++++++++ types/websocket.d.ts | 4 +++- 4 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 test/websocket/custom-headers.js diff --git a/lib/websocket/connection.js b/lib/websocket/connection.js index 581f268e059..8c821899f65 100644 --- a/lib/websocket/connection.js +++ b/lib/websocket/connection.js @@ -13,7 +13,9 @@ const { fireEvent, failWebsocketConnection } = require('./util') const { CloseEvent } = require('./events') const { makeRequest } = require('../fetch/request') const { fetching } = require('../fetch/index') +const { Headers } = require('../fetch/headers') const { getGlobalDispatcher } = require('../global') +const { kHeadersList } = require('../core/symbols') const channels = {} channels.open = diagnosticsChannel.channel('undici:websocket:open') @@ -49,6 +51,13 @@ function establishWebSocketConnection (url, protocols, ws, onEstablish, options) redirect: 'error' }) + // Note: undici extension, allow setting custom headers. + if (options.headers) { + const headersList = new Headers(options.headers)[kHeadersList] + + request.headersList = headersList + } + // 3. Append (`Upgrade`, `websocket`) to request’s header list. // 4. Append (`Connection`, `Upgrade`) to request’s header list. // Note: both of these are handled by undici currently. diff --git a/lib/websocket/websocket.js b/lib/websocket/websocket.js index 762f25684ab..22ad2fb11a1 100644 --- a/lib/websocket/websocket.js +++ b/lib/websocket/websocket.js @@ -596,6 +596,10 @@ webidl.converters.WebSocketInit = webidl.dictionaryConverter([ get defaultValue () { return getGlobalDispatcher() } + }, + { + key: 'headers', + converter: webidl.nullableConverter(webidl.converters.HeadersInit) } ]) diff --git a/test/websocket/custom-headers.js b/test/websocket/custom-headers.js new file mode 100644 index 00000000000..01f1830d3e9 --- /dev/null +++ b/test/websocket/custom-headers.js @@ -0,0 +1,30 @@ +'use strict' + +const { test } = require('tap') +const assert = require('assert') +const { Agent, WebSocket } = require('../..') + +test('Setting custom headers', (t) => { + t.plan(1) + + const headers = { + 'x-khafra-hello': 'hi', + Authorization: 'Bearer base64orsomethingitreallydoesntmatter' + } + + class TestAgent extends Agent { + dispatch (options) { + t.match(options.headers, headers) + + return false + } + } + + const ws = new WebSocket('wss://echo.websocket.events', { + headers, + dispatcher: new TestAgent() + }) + + // We don't want to make a request, just ensure the headers are set. + ws.onclose = ws.onerror = ws.onmessage = assert.fail +}) diff --git a/types/websocket.d.ts b/types/websocket.d.ts index 578c7602e64..15a357d36d5 100644 --- a/types/websocket.d.ts +++ b/types/websocket.d.ts @@ -11,6 +11,7 @@ import { EventListenerOrEventListenerObject } from './patch' import Dispatcher from './dispatcher' +import { HeadersInit } from './fetch' export type BinaryType = 'blob' | 'arraybuffer' @@ -125,5 +126,6 @@ export declare const MessageEvent: { interface WebSocketInit { protocols?: string | string[], - dispatcher?: Dispatcher + dispatcher?: Dispatcher, + headers?: HeadersInit } From 699d3e8a1699c02fe395c77336c61f852c71770e Mon Sep 17 00:00:00 2001 From: Khafra Date: Wed, 26 Apr 2023 16:21:56 -0400 Subject: [PATCH 051/259] test: fix tests failing only on node v20 (#2096) --- lib/fetch/index.js | 2 +- test/autoselectfamily.js | 9 +++++++-- test/balanced-pool.js | 11 ++++++++++- test/fetch/resource-timing.js | 8 ++++---- 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/lib/fetch/index.js b/lib/fetch/index.js index f3016c60dde..51998732427 100644 --- a/lib/fetch/index.js +++ b/lib/fetch/index.js @@ -318,7 +318,7 @@ function finalizeAndReportTiming (response, initiatorType = 'other') { // https://w3c.github.io/resource-timing/#dfn-mark-resource-timing function markResourceTiming (timingInfo, originalURL, initiatorType, globalThis, cacheState) { - if (nodeMajor >= 18 && nodeMinor >= 2) { + if (nodeMajor > 18 || (nodeMajor === 18 && nodeMinor >= 2)) { performance.markResourceTiming(timingInfo, originalURL, initiatorType, globalThis, cacheState) } } diff --git a/test/autoselectfamily.js b/test/autoselectfamily.js index 03f15545cf3..6588123d2c7 100644 --- a/test/autoselectfamily.js +++ b/test/autoselectfamily.js @@ -1,12 +1,17 @@ 'use strict' -const { test } = require('tap') +const { test, skip } = require('tap') const dgram = require('dgram') const { Resolver } = require('dns') const dnsPacket = require('dns-packet') const { createServer } = require('http') const { Client, Agent, request } = require('..') -const { nodeHasAutoSelectFamily } = require('../lib/core/util') +const { nodeHasAutoSelectFamily, nodeMajor } = require('../lib/core/util') + +if (nodeMajor >= 20) { + skip('some tests are failing') + process.exit() +} function _lookup (resolver, hostname, options, cb) { resolver.resolve(hostname, 'ANY', (err, replies) => { diff --git a/test/balanced-pool.js b/test/balanced-pool.js index 669e04a571c..d20f926e931 100644 --- a/test/balanced-pool.js +++ b/test/balanced-pool.js @@ -515,7 +515,16 @@ for (const [index, { config, expected, expectedRatios, iterations = 9, expectedC try { await client.request({ path: '/', method: 'GET' }) } catch (e) { - const serverWithError = servers.find(server => server.port === e.port) || servers.find(server => server.port === e.socket.remotePort) + const serverWithError = + servers.find(server => server.port === e.port) || + servers.find(server => { + if (typeof AggregateError === 'function' && e instanceof AggregateError) { + return e.errors.some(e => server.port === (e.socket?.remotePort ?? e.port)) + } + + return server.port === e.socket.remotePort + }) + serverWithError.requestsCount++ if (e.code === 'ECONNREFUSED') { diff --git a/test/fetch/resource-timing.js b/test/fetch/resource-timing.js index 885e3b3c171..353f07f685b 100644 --- a/test/fetch/resource-timing.js +++ b/test/fetch/resource-timing.js @@ -14,6 +14,7 @@ const skip = nodeMajor < 18 || (nodeMajor === 18 && nodeMinor < 2) test('should create a PerformanceResourceTiming after each fetch request', { skip }, (t) => { t.plan(6) + const obs = new PerformanceObserver(list => { const entries = list.getEntries() t.equal(entries.length, 1) @@ -36,11 +37,10 @@ test('should create a PerformanceResourceTiming after each fetch request', { ski const server = createServer((req, res) => { res.end('ok') - }) - t.teardown(server.close.bind(server)) - - server.listen(0, async () => { + }).listen(0, async () => { const body = await fetch(`http://localhost:${server.address().port}`) t.strictSame('ok', await body.text()) }) + + t.teardown(server.close.bind(server)) }) From 8ffad9d782146d5dd405b81eeb2cbfe97c6a03b1 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Thu, 27 Apr 2023 12:55:25 +0800 Subject: [PATCH 052/259] fix: skip set content-length when FormData value is stream (#2091) https://github.com/nodejs/undici/pull/2066/files#r1174932605 --- lib/fetch/body.js | 10 +++++++++- test/issue-2065.js | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/lib/fetch/body.js b/lib/fetch/body.js index c291afa9368..db450ee6bd4 100644 --- a/lib/fetch/body.js +++ b/lib/fetch/body.js @@ -123,6 +123,7 @@ function extractBody (object, keepalive = false) { const blobParts = [] const rn = new Uint8Array([13, 10]) // '\r\n' length = 0 + let hasUnknownSizeValue = false for (const [name, value] of object) { if (typeof value === 'string') { @@ -138,13 +139,20 @@ function extractBody (object, keepalive = false) { value.type || 'application/octet-stream' }\r\n\r\n`) blobParts.push(chunk, value, rn) - length += chunk.byteLength + value.size + rn.byteLength + if (typeof value.size === 'number') { + length += chunk.byteLength + value.size + rn.byteLength + } else { + hasUnknownSizeValue = true + } } } const chunk = enc.encode(`--${boundary}--`) blobParts.push(chunk) length += chunk.byteLength + if (hasUnknownSizeValue) { + length = null + } // Set source to object. source = object diff --git a/test/issue-2065.js b/test/issue-2065.js index 84183f0fb7c..cc288c48a74 100644 --- a/test/issue-2065.js +++ b/test/issue-2065.js @@ -4,6 +4,7 @@ const { test, skip } = require('tap') const { nodeMajor, nodeMinor } = require('../lib/core/util') const { createServer } = require('http') const { once } = require('events') +const { createReadStream } = require('fs') const { File, FormData, request } = require('..') if (nodeMajor < 16 || (nodeMajor === 16 && nodeMinor < 8)) { @@ -28,3 +29,43 @@ test('undici.request with a FormData body should set content-length header', asy body }) }) + +test('undici.request with a FormData stream value should set transfer-encoding header', async (t) => { + const server = createServer((req, res) => { + t.equal(req.headers['transfer-encoding'], 'chunked') + res.end() + }).listen(0) + + t.teardown(server.close.bind(server)) + await once(server, 'listening') + + class BlobFromStream { + #stream + #type + constructor (stream, type) { + this.#stream = stream + this.#type = type + } + + stream () { + return this.#stream + } + + get type () { + return this.#type + } + + get [Symbol.toStringTag] () { + return 'Blob' + } + } + + const body = new FormData() + const fileReadable = createReadStream(__filename) + body.set('file', new BlobFromStream(fileReadable, '.js'), 'streamfile') + + await request(`http://localhost:${server.address().port}`, { + method: 'POST', + body + }) +}) From 73c59ddf3f47ea427ebdb6fe49603dc481fc7723 Mon Sep 17 00:00:00 2001 From: Jason Zhang Date: Fri, 28 Apr 2023 23:51:31 +0930 Subject: [PATCH 053/259] doc: update outdated command in contributing.md (#2099) --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 17286753f08..c6e8b57f357 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -51,7 +51,7 @@ npm i > This requires [docker](https://www.docker.com/) installed on your machine. ```bash -npm run build:wasm +npm run build-wasm ``` #### Copy the sources to `undici` From ba188230aaa1ce12205eed4f0cc752019106d98d Mon Sep 17 00:00:00 2001 From: Khafra Date: Fri, 28 Apr 2023 15:17:51 -0400 Subject: [PATCH 054/259] cache: fix most failing WPTs (#2100) --- lib/cache/cache.js | 8 ++--- .../service-workers/cache-storage.status.json | 29 ++----------------- 2 files changed, 5 insertions(+), 32 deletions(-) diff --git a/lib/cache/cache.js b/lib/cache/cache.js index 04a29151d17..18f06a348a0 100644 --- a/lib/cache/cache.js +++ b/lib/cache/cache.js @@ -575,7 +575,7 @@ class Cache { } /** - * @see https://w3c.github.io/ServiceWorker/#batch-cache-operations + * @see https://w3c.github.io/ServiceWorker/#batch-cache-operations-algorithm * @param {CacheBatchOperation[]} operations * @returns {requestResponseList} */ @@ -746,11 +746,9 @@ class Cache { // return false // } - /** @type {URL} */ - const queryURL = requestQuery.url + const queryURL = new URL(requestQuery.url) - /** @type {URL} */ - const cachedURL = request.url + const cachedURL = new URL(request.url) if (options?.ignoreSearch) { cachedURL.search = '' diff --git a/test/wpt/status/service-workers/cache-storage.status.json b/test/wpt/status/service-workers/cache-storage.status.json index 6f7cd5287c0..09a291e4dbb 100644 --- a/test/wpt/status/service-workers/cache-storage.status.json +++ b/test/wpt/status/service-workers/cache-storage.status.json @@ -11,38 +11,13 @@ "note": "probably can be fixed", "fail": [ "Cache.put with a VARY:* opaque response should not reject", - "Cache.put with opaque-filtered HTTP 206 response", - "Cache.put with a relative URL" + "Cache.put with opaque-filtered HTTP 206 response" ] }, "cache-match.https.any.js": { "note": "requires https server", "fail": [ - "cors-exposed header should be stored correctly.", - "Cache.match ignores vary headers on opaque response." - ] - }, - "cache-delete.https.any.js": { - "note": "spec bug? - https://github.com/w3c/ServiceWorker/issues/1677 (first fail)", - "fail": [ - "Cache.delete with ignoreSearch option (when it is specified as false)" - ] - }, - "cache-keys.https.any.js": { - "note": "probably can be fixed", - "fail": [ - "Cache.keys with ignoreSearch option (request with search parameters)", - "Cache.keys without parameters", - "Cache.keys with explicitly undefined request" - ] - }, - "cache-matchAll.https.any.js": { - "note": "probably can be fixed", - "fail": [ - "Cache.matchAll with ignoreSearch option (request with search parameters)", - "Cache.matchAll without parameters", - "Cache.matchAll with explicitly undefined request", - "Cache.matchAll with explicitly undefined request and empty options" + "cors-exposed header should be stored correctly." ] } } From 3c514d8d98b3887db165b0ace3014a9c6ad0dfb9 Mon Sep 17 00:00:00 2001 From: Jason Zhang Date: Sun, 30 Apr 2023 01:11:42 +0930 Subject: [PATCH 055/259] feat: allow build:wasm to auto detect platform (#2102) --- build/Dockerfile | 11 ++--------- build/wasm.js | 34 ++++++++++++++++++++++++---------- package.json | 2 +- 3 files changed, 27 insertions(+), 20 deletions(-) diff --git a/build/Dockerfile b/build/Dockerfile index 2f5b6a1647f..ff3d287e5a2 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -1,16 +1,9 @@ -FROM node:16.13.0-buster +FROM node:18-alpine ARG UID=1000 ARG GID=1000 -ARG WASI_SDK_VERSION_MAJOR=14 -ARG WASI_SDK_VERSION_MINOR=0 - -ENV WASI_ROOT=/home/node/wasi-sdk-${WASI_SDK_VERSION_MAJOR}.${WASI_SDK_VERSION_MINOR} - -RUN wget https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${WASI_SDK_VERSION_MAJOR}/wasi-sdk-${WASI_SDK_VERSION_MAJOR}.${WASI_SDK_VERSION_MINOR}-linux.tar.gz -P /tmp - -RUN tar xvf /tmp/wasi-sdk-${WASI_SDK_VERSION_MAJOR}.${WASI_SDK_VERSION_MINOR}-linux.tar.gz --directory /home/node +RUN apk add -U clang lld wasi-sdk RUN mkdir /home/node/undici WORKDIR /home/node/undici diff --git a/build/wasm.js b/build/wasm.js index 48ec53cb0b4..4f0724bd7d1 100644 --- a/build/wasm.js +++ b/build/wasm.js @@ -3,29 +3,41 @@ const { execSync } = require('child_process') const { writeFileSync, readFileSync } = require('fs') const { join, resolve } = require('path') -const { WASI_ROOT } = process.env const ROOT = resolve(__dirname, '../') const WASM_SRC = resolve(__dirname, '../deps/llhttp') const WASM_OUT = resolve(__dirname, '../lib/llhttp') +const DOCKERFILE = resolve(__dirname, './Dockerfile') + +let platform = process.env.WASM_PLATFORM +if (!platform && process.argv[2]) { + platform = execSync('docker info -f "{{.OSType}}/{{.Architecture}}"').toString().trim() +} + +if (process.argv[2] === '--prebuild') { + const cmd = `docker build --platform=${platform.toString().trim()} -t llhttp_wasm_builder -f ${DOCKERFILE} ${ROOT}` + + console.log(`> ${cmd}\n\n`) + execSync(cmd, { stdio: 'inherit' }) + + process.exit(0) +} if (process.argv[2] === '--docker') { - let cmd = 'docker run --rm -it' + let cmd = `docker run --rm -it --platform=${platform.toString().trim()}` if (process.platform === 'linux') { cmd += ` --user ${process.getuid()}:${process.getegid()}` } + cmd += ` --mount type=bind,source=${ROOT}/lib/llhttp,target=/home/node/undici/lib/llhttp llhttp_wasm_builder node build/wasm.js` + console.log(`> ${cmd}\n\n`) execSync(cmd, { stdio: 'inherit' }) process.exit(0) } -if (!WASI_ROOT) { - throw new Error('Please setup the WASI_ROOT env variable.') -} - // Build wasm binary -execSync(`${WASI_ROOT}/bin/clang \ - --sysroot=${WASI_ROOT}/share/wasi-sysroot \ +execSync(`clang \ + --sysroot=/usr/share/wasi-sysroot \ -target wasm32-unknown-wasi \ -Ofast \ -fno-exceptions \ @@ -40,6 +52,7 @@ execSync(`${WASI_ROOT}/bin/clang \ -Wl,--export-table \ -Wl,--export=malloc \ -Wl,--export=free \ + -Wl,--no-entry \ ${join(WASM_SRC, 'src')}/*.c \ -I${join(WASM_SRC, 'include')} \ -o ${join(WASM_OUT, 'llhttp.wasm')}`, { stdio: 'inherit' }) @@ -51,8 +64,8 @@ writeFileSync( ) // Build wasm simd binary -execSync(`${WASI_ROOT}/bin/clang \ - --sysroot=${WASI_ROOT}/share/wasi-sysroot \ +execSync(`clang \ + --sysroot=/usr/share/wasi-sysroot \ -target wasm32-unknown-wasi \ -msimd128 \ -Ofast \ @@ -68,6 +81,7 @@ execSync(`${WASI_ROOT}/bin/clang \ -Wl,--export-table \ -Wl,--export=malloc \ -Wl,--export=free \ + -Wl,--no-entry \ ${join(WASM_SRC, 'src')}/*.c \ -I${join(WASM_SRC, 'include')} \ -o ${join(WASM_OUT, 'llhttp_simd.wasm')}`, { stdio: 'inherit' }) diff --git a/package.json b/package.json index fb43ff7e83c..da9bd36d21e 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ ], "scripts": { "build:node": "npx esbuild@0.14.38 index-fetch.js --bundle --platform=node --outfile=undici-fetch.js", - "prebuild:wasm": "docker build -t llhttp_wasm_builder -f build/Dockerfile .", + "prebuild:wasm": "node build/wasm.js --prebuild", "build:wasm": "node build/wasm.js --docker", "lint": "standard | snazzy", "lint:fix": "standard --fix | snazzy", From 48f20780b32b63d235f9f69e463cf4415c229522 Mon Sep 17 00:00:00 2001 From: titanism <101466223+titanism@users.noreply.github.com> Date: Sat, 29 Apr 2023 20:38:54 -0500 Subject: [PATCH 056/259] docs: updated Error documentation (fixes #2090) (#2092) --- docs/api/Errors.md | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/docs/api/Errors.md b/docs/api/Errors.md index fba0e8c7cef..917e45df9fc 100644 --- a/docs/api/Errors.md +++ b/docs/api/Errors.md @@ -7,19 +7,25 @@ You can find all the error objects inside the `errors` key. import { errors } from 'undici' ``` -| Error | Error Codes | Description | -| ------------------------------------ | ------------------------------------- | -------------------------------------------------- | -| `InvalidArgumentError` | `UND_ERR_INVALID_ARG` | passed an invalid argument. | -| `InvalidReturnValueError` | `UND_ERR_INVALID_RETURN_VALUE` | returned an invalid value. | -| `RequestAbortedError` | `UND_ERR_ABORTED` | the request has been aborted by the user | -| `ClientDestroyedError` | `UND_ERR_DESTROYED` | trying to use a destroyed client. | -| `ClientClosedError` | `UND_ERR_CLOSED` | trying to use a closed client. | -| `SocketError` | `UND_ERR_SOCKET` | there is an error with the socket. | -| `NotSupportedError` | `UND_ERR_NOT_SUPPORTED` | encountered unsupported functionality. | -| `RequestContentLengthMismatchError` | `UND_ERR_REQ_CONTENT_LENGTH_MISMATCH` | request body does not match content-length header | -| `ResponseContentLengthMismatchError` | `UND_ERR_RES_CONTENT_LENGTH_MISMATCH` | response body does not match content-length header | -| `InformationalError` | `UND_ERR_INFO` | expected error with reason | -| `ResponseExceededMaxSizeError` | `UND_ERR_RES_EXCEEDED_MAX_SIZE` | response body exceed the max size allowed | +| Error | Error Codes | Description | +| ------------------------------------ | ------------------------------------- | ------------------------------------------------------------------------- | +| `UndiciError` | `UND_ERR` | all errors below are extended from `UndiciError`. | +| `ConnectTimeoutError` | `UND_ERR_CONNECT_TIMEOUT` | socket is destroyed due to connect timeout. | +| `HeadersTimeoutError` | `UND_ERR_HEADERS_TIMEOUT` | socket is destroyed due to headers timeout. | +| `HeadersOverflowError` | `UND_ERR_HEADERS_OVERFLOW` | socket is destroyed due to headers' max size being exceeded. | +| `BodyTimeoutError` | `UND_ERR_BODY_TIMEOUT` | socket is destroyed due to body timeout. | +| `ResponseStatusCodeError` | `UND_ERR_RESPONSE_STATUS_CODE` | an error is thrown when `throwOnError` is `true` for status codes >= 400. | +| `InvalidArgumentError` | `UND_ERR_INVALID_ARG` | passed an invalid argument. | +| `InvalidReturnValueError` | `UND_ERR_INVALID_RETURN_VALUE` | returned an invalid value. | +| `RequestAbortedError` | `UND_ERR_ABORTED` | the request has been aborted by the user | +| `ClientDestroyedError` | `UND_ERR_DESTROYED` | trying to use a destroyed client. | +| `ClientClosedError` | `UND_ERR_CLOSED` | trying to use a closed client. | +| `SocketError` | `UND_ERR_SOCKET` | there is an error with the socket. | +| `NotSupportedError` | `UND_ERR_NOT_SUPPORTED` | encountered unsupported functionality. | +| `RequestContentLengthMismatchError` | `UND_ERR_REQ_CONTENT_LENGTH_MISMATCH` | request body does not match content-length header | +| `ResponseContentLengthMismatchError` | `UND_ERR_RES_CONTENT_LENGTH_MISMATCH` | response body does not match content-length header | +| `InformationalError` | `UND_ERR_INFO` | expected error with reason | +| `ResponseExceededMaxSizeError` | `UND_ERR_RES_EXCEEDED_MAX_SIZE` | response body exceed the max size allowed | ### `SocketError` From 56e7624d281eaa5b87ce9b6f91cc7ec367f75c88 Mon Sep 17 00:00:00 2001 From: Khafra Date: Sun, 30 Apr 2023 10:02:38 -0400 Subject: [PATCH 057/259] mimesniff: fix many broken tests (#2103) * mimesniff: fix many broken tests * mimesniff: dont export this * mimesniff: undo small change * mimesniff: fix some bugs --- lib/fetch/dataURL.js | 95 ++++++++++++++++++++++++++++++-------- lib/fetch/response.js | 2 +- test/wpt/runner/runner.mjs | 3 +- 3 files changed, 79 insertions(+), 21 deletions(-) diff --git a/lib/fetch/dataURL.js b/lib/fetch/dataURL.js index beefad15482..6df4fcc8cc6 100644 --- a/lib/fetch/dataURL.js +++ b/lib/fetch/dataURL.js @@ -1,14 +1,18 @@ const assert = require('assert') const { atob } = require('buffer') -const { isValidHTTPToken, isomorphicDecode } = require('./util') +const { isomorphicDecode } = require('./util') const encoder = new TextEncoder() -// Regex -const HTTP_TOKEN_CODEPOINTS = /^[!#$%&'*+-.^_|~A-z0-9]+$/ +/** + * @see https://mimesniff.spec.whatwg.org/#http-token-code-point + */ +const HTTP_TOKEN_CODEPOINTS = /^[!#$%&'*+-.^_|~A-Za-z0-9]+$/ const HTTP_WHITESPACE_REGEX = /(\u000A|\u000D|\u0009|\u0020)/ // eslint-disable-line -// https://mimesniff.spec.whatwg.org/#http-quoted-string-token-code-point -const HTTP_QUOTED_STRING_TOKENS = /^(\u0009|\x{0020}-\x{007E}|\x{0080}-\x{00FF})+$/ // eslint-disable-line +/** + * @see https://mimesniff.spec.whatwg.org/#http-quoted-string-token-code-point + */ +const HTTP_QUOTED_STRING_TOKENS = /[\u0009|\u0020-\u007E|\u0080-\u00FF]/ // eslint-disable-line // https://fetch.spec.whatwg.org/#data-url-processor /** @param {URL} dataURL */ @@ -38,14 +42,12 @@ function dataURLProcessor (dataURL) { // 6. Strip leading and trailing ASCII whitespace // from mimeType. - // Note: This will only remove U+0020 SPACE code - // points, if any. // Undici implementation note: we need to store the // length because if the mimetype has spaces removed, // the wrong amount will be sliced from the input in // step #9 const mimeTypeLength = mimeType.length - mimeType = mimeType.replace(/^(\u0020)+|(\u0020)+$/g, '') + mimeType = removeASCIIWhitespace(mimeType, true, true) // 7. If position is past the end of input, then // return failure @@ -233,7 +235,7 @@ function percentDecode (input) { function parseMIMEType (input) { // 1. Remove any leading and trailing HTTP whitespace // from input. - input = input.trim() + input = removeHTTPWhitespace(input, true, true) // 2. Let position be a position variable for input, // initially pointing at the start of input. @@ -274,7 +276,7 @@ function parseMIMEType (input) { ) // 8. Remove any trailing HTTP whitespace from subtype. - subtype = subtype.trimEnd() + subtype = removeHTTPWhitespace(subtype, false, true) // 9. If subtype is the empty string or does not solely // contain HTTP token code points, then return failure. @@ -282,17 +284,20 @@ function parseMIMEType (input) { return 'failure' } + const typeLowercase = type.toLowerCase() + const subtypeLowercase = subtype.toLowerCase() + // 10. Let mimeType be a new MIME type record whose type // is type, in ASCII lowercase, and subtype is subtype, // in ASCII lowercase. // https://mimesniff.spec.whatwg.org/#mime-type const mimeType = { - type: type.toLowerCase(), - subtype: subtype.toLowerCase(), + type: typeLowercase, + subtype: subtypeLowercase, /** @type {Map} */ parameters: new Map(), // https://mimesniff.spec.whatwg.org/#mime-type-essence - essence: `${type}/${subtype}` + essence: `${typeLowercase}/${subtypeLowercase}` } // 11. While position is not past the end of input: @@ -370,8 +375,7 @@ function parseMIMEType (input) { ) // 2. Remove any trailing HTTP whitespace from parameterValue. - // Note: it says "trailing" whitespace; leading is fine. - parameterValue = parameterValue.trimEnd() + parameterValue = removeHTTPWhitespace(parameterValue, false, true) // 3. If parameterValue is the empty string, then continue. if (parameterValue.length === 0) { @@ -388,7 +392,7 @@ function parseMIMEType (input) { if ( parameterName.length !== 0 && HTTP_TOKEN_CODEPOINTS.test(parameterName) && - !HTTP_QUOTED_STRING_TOKENS.test(parameterValue) && + (parameterValue.length === 0 || HTTP_QUOTED_STRING_TOKENS.test(parameterValue)) && !mimeType.parameters.has(parameterName) ) { mimeType.parameters.set(parameterName, parameterValue) @@ -522,11 +526,11 @@ function collectAnHTTPQuotedString (input, position, extractValue) { */ function serializeAMimeType (mimeType) { assert(mimeType !== 'failure') - const { type, subtype, parameters } = mimeType + const { parameters, essence } = mimeType // 1. Let serialization be the concatenation of mimeType’s // type, U+002F (/), and mimeType’s subtype. - let serialization = `${type}/${subtype}` + let serialization = essence // 2. For each name → value of mimeType’s parameters: for (let [name, value] of parameters.entries()) { @@ -541,7 +545,7 @@ function serializeAMimeType (mimeType) { // 4. If value does not solely contain HTTP token code // points or value is the empty string, then: - if (!isValidHTTPToken(value)) { + if (!HTTP_TOKEN_CODEPOINTS.test(value)) { // 1. Precede each occurence of U+0022 (") or // U+005C (\) in value with U+005C (\). value = value.replace(/(\\|")/g, '\\$1') @@ -561,6 +565,59 @@ function serializeAMimeType (mimeType) { return serialization } +/** + * @see https://fetch.spec.whatwg.org/#http-whitespace + * @param {string} char + */ +function isHTTPWhiteSpace (char) { + return char === '\r' || char === '\n' || char === '\t' || char === ' ' +} + +/** + * @see https://fetch.spec.whatwg.org/#http-whitespace + * @param {string} str + */ +function removeHTTPWhitespace (str, leading = true, trailing = true) { + let lead = 0 + let trail = str.length - 1 + + if (leading) { + for (; lead < str.length && isHTTPWhiteSpace(str[lead]); lead++); + } + + if (trailing) { + for (; trail > 0 && isHTTPWhiteSpace(str[trail]); trail--); + } + + return str.slice(lead, trail + 1) +} + +/** + * @see https://infra.spec.whatwg.org/#ascii-whitespace + * @param {string} char + */ +function isASCIIWhitespace (char) { + return char === '\r' || char === '\n' || char === '\t' || char === '\f' || char === ' ' +} + +/** + * @see https://infra.spec.whatwg.org/#strip-leading-and-trailing-ascii-whitespace + */ +function removeASCIIWhitespace (str, leading = true, trailing = true) { + let lead = 0 + let trail = str.length - 1 + + if (leading) { + for (; lead < str.length && isASCIIWhitespace(str[lead]); lead++); + } + + if (trailing) { + for (; trail > 0 && isASCIIWhitespace(str[trail]); trail--); + } + + return str.slice(lead, trail + 1) +} + module.exports = { dataURLProcessor, URLSerializer, diff --git a/lib/fetch/response.js b/lib/fetch/response.js index 96cacbce157..1029dbef533 100644 --- a/lib/fetch/response.js +++ b/lib/fetch/response.js @@ -467,7 +467,7 @@ function initializeResponse (response, init, body) { // 5. If init["headers"] exists, then fill response’s headers with init["headers"]. if ('headers' in init && init.headers != null) { - fill(response[kState].headersList, init.headers) + fill(response[kHeaders], init.headers) } // 6. If body was given, then: diff --git a/test/wpt/runner/runner.mjs b/test/wpt/runner/runner.mjs index 5979730d70a..ec236454e74 100644 --- a/test/wpt/runner/runner.mjs +++ b/test/wpt/runner/runner.mjs @@ -150,8 +150,9 @@ export class WPTRunner extends EventEmitter { return [test, code, meta] }) + console.log('='.repeat(96)) + for (const [test, code, meta] of files) { - console.log('='.repeat(96)) console.log(`Started ${test}`) const status = resolveStatusPath(test, this.#status) From 72ca86a5dbbe078cb8a552a7b7501407c6518fd5 Mon Sep 17 00:00:00 2001 From: Khafra Date: Mon, 1 May 2023 00:19:08 -0400 Subject: [PATCH 058/259] test: fix failing tests (#2097) * test: increase timeout for some redirect- tests * test: fixes * test: fixes * test: fixes * test: fixes * test: fixes * test: fixes * test: fixes * test: fixes --- package.json | 2 +- test/agent.js | 4 +- test/connect-timeout.js | 6 +- test/fetch/client-fetch.js | 4 +- .../jsdom-abortcontroller-1910-1464495619.js | 12 +- test/fetch/request.js | 11 +- test/proxy-agent.js | 4 +- test/redirect-request.js | 164 ++++-------------- test/redirect-stream.js | 2 + test/tls-session-reuse.js | 4 +- 10 files changed, 61 insertions(+), 152 deletions(-) diff --git a/package.json b/package.json index da9bd36d21e..6c325d75cdf 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "lint:fix": "standard --fix | snazzy", "test": "npm run test:tap && npm run test:node-fetch && npm run test:fetch && npm run test:cookies && npm run test:wpt && npm run test:websocket && npm run test:jest && npm run test:typescript", "test:cookies": "node scripts/verifyVersion 16 || tap test/cookie/*.js", - "test:node-fetch": "node scripts/verifyVersion.js 16 || mocha test/node-fetch", + "test:node-fetch": "node scripts/verifyVersion.js 16 || mocha --exit test/node-fetch", "test:fetch": "node scripts/verifyVersion.js 16 || (npm run build:node && tap --expose-gc test/fetch/*.js && tap test/webidl/*.js)", "test:jest": "node scripts/verifyVersion.js 14 || jest", "test:tap": "tap test/*.js test/diagnostics-channel/*.js", diff --git a/test/agent.js b/test/agent.js index 0d3c84d9d53..ecdbdd2f91d 100644 --- a/test/agent.js +++ b/test/agent.js @@ -1,6 +1,6 @@ 'use strict' -const { test } = require('tap') +const { test, teardown } = require('tap') const http = require('http') const { PassThrough } = require('stream') const { kRunning } = require('../lib/core/symbols') @@ -705,3 +705,5 @@ test('the dispatcher is truly global', t => { t.equal(agent, undiciFresh.getGlobalDispatcher()) t.end() }) + +teardown(() => process.exit()) diff --git a/test/connect-timeout.js b/test/connect-timeout.js index 98ed1922979..c904a7335a2 100644 --- a/test/connect-timeout.js +++ b/test/connect-timeout.js @@ -8,7 +8,7 @@ const sleep = require('atomic-sleep') test('priotorise socket errors over timeouts', (t) => { t.plan(1) const connectTimeout = 1000 - const client = new Pool('http://foobar.bar:1234', { connectTimeout }) + const client = new Pool('http://foobar.bar:1234', { connectTimeout: 1 }) client.request({ method: 'GET', path: '/foobar' }) .then(() => t.fail()) @@ -16,8 +16,8 @@ test('priotorise socket errors over timeouts', (t) => { t.equal(err.code, 'ENOTFOUND') }) - // block for 2s which is enough for the dns lookup to complete and TO to fire - sleep(connectTimeout * 2) + // block for 1s which is enough for the dns lookup to complete and TO to fire + sleep(connectTimeout) }) // never connect diff --git a/test/fetch/client-fetch.js b/test/fetch/client-fetch.js index 3ce058ee9ae..048b76a9f78 100644 --- a/test/fetch/client-fetch.js +++ b/test/fetch/client-fetch.js @@ -2,7 +2,7 @@ 'use strict' -const { test } = require('tap') +const { test, teardown } = require('tap') const { createServer } = require('http') const { ReadableStream } = require('stream/web') const { Blob } = require('buffer') @@ -672,3 +672,5 @@ test('Receiving non-Latin1 headers', async (t) => { t.same(lengths, [30, 34, 94, 104, 90]) t.end() }) + +teardown(() => process.exit()) diff --git a/test/fetch/jsdom-abortcontroller-1910-1464495619.js b/test/fetch/jsdom-abortcontroller-1910-1464495619.js index fad1e77c12b..e5a86abeaf3 100644 --- a/test/fetch/jsdom-abortcontroller-1910-1464495619.js +++ b/test/fetch/jsdom-abortcontroller-1910-1464495619.js @@ -10,11 +10,15 @@ const { JSDOM } = require('jsdom') test('third party AbortControllers', async (t) => { const server = createServer((_, res) => res.end()).listen(0) - t.teardown(server.close.bind(server)) - await once(server, 'listening') - const { AbortController } = new JSDOM().window - const controller = new AbortController() + let controller = new AbortController() + + t.teardown(() => { + controller.abort() + controller = null + return server.close() + }) + await once(server, 'listening') await t.resolves(fetch(`http://localhost:${server.address().port}`, { signal: controller.signal diff --git a/test/fetch/request.js b/test/fetch/request.js index cd32adc7d6f..e6ab49b7808 100644 --- a/test/fetch/request.js +++ b/test/fetch/request.js @@ -2,13 +2,12 @@ 'use strict' -const { test } = require('tap') +const { test, teardown } = require('tap') const { Request, Headers, fetch } = require('../../') -const { kState } = require('../../lib/fetch/symbols.js') const { Blob: ThirdPartyBlob, FormData: ThirdPartyFormData @@ -199,7 +198,7 @@ test('undefined window', t => { test('undefined body', t => { const req = new Request('http://asd', { body: undefined }) - t.equal(req[kState].body, null) + t.equal(req.body, null) t.end() }) @@ -298,7 +297,7 @@ test('post aborted signal', t => { } else { t.pass() } - }) + }, { once: true }) ac.abort('gwak') }) @@ -346,7 +345,7 @@ test('post aborted signal cloned', t => { } else { t.pass() } - }) + }, { once: true }) ac.abort('gwak') }) @@ -476,3 +475,5 @@ test('set-cookie headers get cleared when passing a Request as first param', (t) t.same(req2.headers.getSetCookie(), []) t.end() }) + +teardown(() => process.exit()) diff --git a/test/proxy-agent.js b/test/proxy-agent.js index 3d8f9903bde..a35101234c6 100644 --- a/test/proxy-agent.js +++ b/test/proxy-agent.js @@ -1,6 +1,6 @@ 'use strict' -const { test } = require('tap') +const { test, teardown } = require('tap') const { request, fetch, setGlobalDispatcher, getGlobalDispatcher } = require('..') const { InvalidArgumentError } = require('../lib/core/errors') const { nodeMajor } = require('../lib/core/util') @@ -692,3 +692,5 @@ function buildSSLProxy () { server.listen(0, () => resolve(server)) }) } + +teardown(() => process.exit()) diff --git a/test/redirect-request.js b/test/redirect-request.js index f996bfa8231..46e60cd5471 100644 --- a/test/redirect-request.js +++ b/test/redirect-request.js @@ -22,21 +22,17 @@ for (const factory of [ const request = (server, opts, ...args) => { const dispatcher = factory(server, opts) return undici.request(args[0], { ...args[1], dispatcher }, args[2]) + .finally(() => dispatcher.destroy()) } t.test('should always have a history with the final URL even if no redirections were followed', async t => { - t.plan(4) - - let body = '' const server = await startRedirectingServer(t) const { statusCode, headers, body: bodyStream, context: { history } } = await request(server, undefined, `http://${server}/200?key=value`, { maxRedirections: 10 }) - for await (const b of bodyStream) { - body += b - } + const body = await bodyStream.text() t.equal(statusCode, 200) t.notOk(headers.location) @@ -45,15 +41,10 @@ for (const factory of [ }) t.test('should not follow redirection by default if not using RedirectAgent', async t => { - t.plan(3) - - let body = '' const server = await startRedirectingServer(t) const { statusCode, headers, body: bodyStream } = await request(server, undefined, `http://${server}`) - for await (const b of bodyStream) { - body += b - } + const body = await bodyStream.text() t.equal(statusCode, 302) t.equal(headers.location, `http://${server}/302/1`) @@ -61,18 +52,13 @@ for (const factory of [ }) t.test('should follow redirection after a HTTP 300', async t => { - t.plan(4) - - let body = '' const server = await startRedirectingServer(t) const { statusCode, headers, body: bodyStream, context: { history } } = await request(server, undefined, `http://${server}/300?key=value`, { maxRedirections: 10 }) - for await (const b of bodyStream) { - body += b - } + const body = await bodyStream.text() t.equal(statusCode, 200) t.notOk(headers.location) @@ -88,16 +74,10 @@ for (const factory of [ }) t.test('should follow redirection after a HTTP 300 default', async t => { - t.plan(4) - - let body = '' const server = await startRedirectingServer(t) const { statusCode, headers, body: bodyStream, context: { history } } = await request(server, { maxRedirections: 10 }, `http://${server}/300?key=value`) - - for await (const b of bodyStream) { - body += b - } + const body = await bodyStream.text() t.equal(statusCode, 200) t.notOk(headers.location) @@ -113,9 +93,6 @@ for (const factory of [ }) t.test('should follow redirection after a HTTP 301', async t => { - t.plan(3) - - let body = '' const server = await startRedirectingServer(t) const { statusCode, headers, body: bodyStream } = await request(server, undefined, `http://${server}/301`, { @@ -124,9 +101,7 @@ for (const factory of [ maxRedirections: 10 }) - for await (const b of bodyStream) { - body += b - } + const body = await bodyStream.text() t.equal(statusCode, 200) t.notOk(headers.location) @@ -134,9 +109,6 @@ for (const factory of [ }) t.test('should follow redirection after a HTTP 302', async t => { - t.plan(3) - - let body = '' const server = await startRedirectingServer(t) const { statusCode, headers, body: bodyStream } = await request(server, undefined, `http://${server}/302`, { @@ -145,9 +117,7 @@ for (const factory of [ maxRedirections: 10 }) - for await (const b of bodyStream) { - body += b - } + const body = await bodyStream.text() t.equal(statusCode, 200) t.notOk(headers.location) @@ -155,9 +125,6 @@ for (const factory of [ }) t.test('should follow redirection after a HTTP 303 changing method to GET', async t => { - t.plan(3) - - let body = '' const server = await startRedirectingServer(t) const { statusCode, headers, body: bodyStream } = await request(server, undefined, `http://${server}/303`, { @@ -166,9 +133,7 @@ for (const factory of [ maxRedirections: 10 }) - for await (const b of bodyStream) { - body += b - } + const body = await bodyStream.text() t.equal(statusCode, 200) t.notOk(headers.location) @@ -176,9 +141,6 @@ for (const factory of [ }) t.test('should remove Host and request body related headers when following HTTP 303 (array)', async t => { - t.plan(3) - - let body = '' const server = await startRedirectingServer(t) const { statusCode, headers, body: bodyStream } = await request(server, undefined, `http://${server}/303`, { @@ -202,9 +164,7 @@ for (const factory of [ maxRedirections: 10 }) - for await (const b of bodyStream) { - body += b - } + const body = await bodyStream.text() t.equal(statusCode, 200) t.notOk(headers.location) @@ -212,9 +172,6 @@ for (const factory of [ }) t.test('should remove Host and request body related headers when following HTTP 303 (object)', async t => { - t.plan(3) - - let body = '' const server = await startRedirectingServer(t) const { statusCode, headers, body: bodyStream } = await request(server, undefined, `http://${server}/303`, { @@ -231,9 +188,7 @@ for (const factory of [ maxRedirections: 10 }) - for await (const b of bodyStream) { - body += b - } + const body = await bodyStream.text() t.equal(statusCode, 200) t.notOk(headers.location) @@ -241,9 +196,6 @@ for (const factory of [ }) t.test('should follow redirection after a HTTP 307', async t => { - t.plan(3) - - let body = '' const server = await startRedirectingServer(t) const { statusCode, headers, body: bodyStream } = await request(server, undefined, `http://${server}/307`, { @@ -251,9 +203,7 @@ for (const factory of [ maxRedirections: 10 }) - for await (const b of bodyStream) { - body += b - } + const body = await bodyStream.text() t.equal(statusCode, 200) t.notOk(headers.location) @@ -261,9 +211,6 @@ for (const factory of [ }) t.test('should follow redirection after a HTTP 308', async t => { - t.plan(3) - - let body = '' const server = await startRedirectingServer(t) const { statusCode, headers, body: bodyStream } = await request(server, undefined, `http://${server}/308`, { @@ -271,9 +218,7 @@ for (const factory of [ maxRedirections: 10 }) - for await (const b of bodyStream) { - body += b - } + const body = await bodyStream.text() t.equal(statusCode, 200) t.notOk(headers.location) @@ -281,18 +226,13 @@ for (const factory of [ }) t.test('should ignore HTTP 3xx response bodies', async t => { - t.plan(4) - - let body = '' const server = await startRedirectingWithBodyServer(t) const { statusCode, headers, body: bodyStream, context: { history } } = await request(server, undefined, `http://${server}/`, { maxRedirections: 10 }) - for await (const b of bodyStream) { - body += b - } + const body = await bodyStream.text() t.equal(statusCode, 200) t.notOk(headers.location) @@ -301,8 +241,6 @@ for (const factory of [ }) t.test('should ignore query after redirection', async t => { - t.plan(3) - const server = await startRedirectingWithQueryParams(t) const { statusCode, headers, context: { history } } = await request(server, undefined, `http://${server}/`, { @@ -316,18 +254,13 @@ for (const factory of [ }) t.test('should follow a redirect chain up to the allowed number of times', async t => { - t.plan(4) - - let body = '' const server = await startRedirectingServer(t) const { statusCode, headers, body: bodyStream, context: { history } } = await request(server, undefined, `http://${server}/300`, { maxRedirections: 2 }) - for await (const b of bodyStream) { - body += b - } + const body = await bodyStream.text() t.equal(statusCode, 300) t.equal(headers.location, `http://${server}/300/3`) @@ -340,44 +273,32 @@ for (const factory of [ const server = await startRedirectingWithoutLocationServer(t) for (const code of redirectCodes) { - t.test(`should return the original response after a HTTP ${code}`, async t => { - t.plan(3) - - let body = '' - - const { statusCode, headers, body: bodyStream } = await request(server, undefined, `http://${server}/${code}`, { - maxRedirections: 10 - }) + const { statusCode, headers, body: bodyStream } = await request(server, undefined, `http://${server}/${code}`, { + maxRedirections: 10 + }) - for await (const b of bodyStream) { - body += b - } + const body = await bodyStream.text() - t.equal(statusCode, code) - t.notOk(headers.location) - t.equal(body.length, 0) - }) + t.equal(statusCode, code) + t.notOk(headers.location) + t.equal(body.length, 0) } }) t.test('should not allow invalid maxRedirections arguments', async t => { - t.plan(1) - try { await request('localhost', undefined, 'http://localhost', { method: 'GET', maxRedirections: 'INVALID' }) - throw new Error('Did not throw') + t.fail('Did not throw') } catch (err) { t.equal(err.message, 'maxRedirections must be a positive number') } }) t.test('should not allow invalid maxRedirections arguments default', async t => { - t.plan(1) - try { await request('localhost', { maxRedirections: 'INVALID' @@ -385,16 +306,13 @@ for (const factory of [ method: 'GET' }) - throw new Error('Did not throw') + t.fail('Did not throw') } catch (err) { t.equal(err.message, 'maxRedirections must be a positive number') } }) t.test('should not follow redirects when using ReadableStream request bodies', { skip: nodeMajor < 16 }, async t => { - t.plan(3) - - let body = '' const server = await startRedirectingServer(t) const { statusCode, headers, body: bodyStream } = await request(server, undefined, `http://${server}/301`, { @@ -403,9 +321,7 @@ for (const factory of [ maxRedirections: 10 }) - for await (const b of bodyStream) { - body += b - } + const body = await bodyStream.text() t.equal(statusCode, 301) t.equal(headers.location, `http://${server}/301/2`) @@ -413,9 +329,6 @@ for (const factory of [ }) t.test('should not follow redirects when using Readable request bodies', async t => { - t.plan(3) - - let body = '' const server = await startRedirectingServer(t) const { statusCode, headers, body: bodyStream } = await request(server, undefined, `http://${server}/301`, { @@ -424,9 +337,7 @@ for (const factory of [ maxRedirections: 10 }) - for await (const b of bodyStream) { - body += b - } + const body = await bodyStream.text() t.equal(statusCode, 301) t.equal(headers.location, `http://${server}/301/1`) @@ -435,19 +346,14 @@ for (const factory of [ } t.test('should follow redirections when going cross origin', async t => { - t.plan(4) - const [server1, server2, server3] = await startRedirectingChainServers(t) - let body = '' const { statusCode, headers, body: bodyStream, context: { history } } = await undici.request(`http://${server1}`, { method: 'POST', maxRedirections: 10 }) - for await (const b of bodyStream) { - body += b - } + const body = await bodyStream.text() t.equal(statusCode, 200) t.notOk(headers.location) @@ -477,19 +383,15 @@ t.test('should handle errors (callback)', t => { }) t.test('should handle errors (promise)', async t => { - t.plan(1) - try { await undici.request('http://localhost:0', { maxRedirections: 10 }) - throw new Error('Did not throw') + t.fail('Did not throw') } catch (error) { t.match(error.code, /EADDRNOTAVAIL|ECONNREFUSED/) } }) t.test('removes authorization header on third party origin', async t => { - t.plan(1) - const [server1] = await startRedirectingWithAuthorization(t, 'secret') const { body: bodyStream } = await undici.request(`http://${server1}`, { maxRedirections: 10, @@ -498,17 +400,12 @@ t.test('removes authorization header on third party origin', async t => { } }) - let body = '' - for await (const b of bodyStream) { - body += b - } + const body = await bodyStream.text() t.equal(body, '') }) t.test('removes cookie header on third party origin', async t => { - t.plan(1) - const [server1] = await startRedirectingWithCookie(t, 'a=b') const { body: bodyStream } = await undici.request(`http://${server1}`, { maxRedirections: 10, @@ -517,10 +414,7 @@ t.test('removes cookie header on third party origin', async t => { } }) - let body = '' - for await (const b of bodyStream) { - body += b - } + const body = await bodyStream.text() t.equal(body, '') }) diff --git a/test/redirect-stream.js b/test/redirect-stream.js index abb778c2c8f..55dd97beb49 100644 --- a/test/redirect-stream.js +++ b/test/redirect-stream.js @@ -419,3 +419,5 @@ t.test('removes cookie header on third party origin', async t => { t.equal(body.length, 0) }) + +t.teardown(() => process.exit()) diff --git a/test/tls-session-reuse.js b/test/tls-session-reuse.js index 147e92fd76b..ab012f16756 100644 --- a/test/tls-session-reuse.js +++ b/test/tls-session-reuse.js @@ -4,7 +4,7 @@ const { readFileSync } = require('fs') const { join } = require('path') const https = require('https') const crypto = require('crypto') -const { test } = require('tap') +const { test, teardown } = require('tap') const { Client, Pool } = require('..') const { kSocket } = require('../lib/core/symbols') const { nodeMajor } = require('../lib/core/util') @@ -181,3 +181,5 @@ test('A pool should be able to reuse TLS sessions between clients', { t.end() }) + +teardown(() => process.exit()) From c1c8d83d6cce6a2211f1b984272c6af49c01a2e7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 May 2023 23:13:58 +0000 Subject: [PATCH 059/259] build(deps): bump github/codeql-action from 2.2.9 to 2.3.2 (#2105) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2.2.9 to 2.3.2. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/04df1262e6247151b5ac09cd2c303ac36ad3f62b...f3feb00acb00f31a6f60280e6ace9ca31d91c76a) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/scorecard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index a6affe5f2b0..1f22fd9c34c 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -51,6 +51,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@04df1262e6247151b5ac09cd2c303ac36ad3f62b # v2.2.9 + uses: github/codeql-action/upload-sarif@f3feb00acb00f31a6f60280e6ace9ca31d91c76a # v2.3.2 with: sarif_file: results.sarif From ab2e0ce9cbdef85e215dd5b2f268b644b308012b Mon Sep 17 00:00:00 2001 From: Songkeys Date: Wed, 3 May 2023 16:51:15 +0800 Subject: [PATCH 060/259] fix: more informative error message to tell that the server doesn't match http/1.1 protocol (#2055) * fix: http2 error message * test: fix * test: fix --- lib/client.js | 5 ++++- test/http2.js | 32 ++++++++++++++++++++++++++++++++ test/parser-issues.js | 7 +++---- 3 files changed, 39 insertions(+), 5 deletions(-) create mode 100644 test/http2.js diff --git a/lib/client.js b/lib/client.js index 688df9e6156..f237fe02ab8 100644 --- a/lib/client.js +++ b/lib/client.js @@ -569,7 +569,10 @@ class Parser { /* istanbul ignore else: difficult to make a test case for */ if (ptr) { const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0) - message = Buffer.from(llhttp.memory.buffer, ptr, len).toString() + message = + 'Response does not match the HTTP/1.1 protocol (' + + Buffer.from(llhttp.memory.buffer, ptr, len).toString() + + ')' } throw new HTTPParserError(message, constants.ERROR[ret], data.slice(offset)) } diff --git a/test/http2.js b/test/http2.js new file mode 100644 index 00000000000..ab8752a7816 --- /dev/null +++ b/test/http2.js @@ -0,0 +1,32 @@ +'use strict' + +const { test } = require('tap') +const { Client, errors } = require('..') +const { createSecureServer } = require('http2') +const pem = require('https-pem') + +test('throw http2 not supported error', (t) => { + t.plan(1) + + const server = createSecureServer({ key: pem.key, cert: pem.cert }, (req, res) => { + res.stream.respond({ 'content-type': 'text/plain' }) + res.stream.end('hello') + }).on('unknownProtocol', (socket) => { + // continue sending data in http2 to our http1.1 client to trigger error + socket.write('PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n') + }) + t.teardown(server.close.bind(server)) + + server.listen(0, () => { + const client = new Client(`https://localhost:${server.address().port}`, { + tls: { + rejectUnauthorized: false + } + }) + t.teardown(client.close.bind(client)) + + client.request({ path: '/', method: 'GET' }, (err, data) => { + t.type(err, errors.HTTPParserError) + }) + }) +}) diff --git a/test/parser-issues.js b/test/parser-issues.js index 7f5bfbb5f99..b98edf159ef 100644 --- a/test/parser-issues.js +++ b/test/parser-issues.js @@ -1,6 +1,6 @@ const net = require('net') const { test } = require('tap') -const { Client } = require('..') +const { Client, errors } = require('..') test('https://github.com/mcollina/undici/issues/268', (t) => { t.plan(2) @@ -40,7 +40,7 @@ test('https://github.com/mcollina/undici/issues/268', (t) => { }) test('parser fail', (t) => { - t.plan(3) + t.plan(2) const server = net.createServer(socket => { socket.write('HTT/1.1 200 OK\r\n') @@ -56,8 +56,7 @@ test('parser fail', (t) => { path: '/' }, (err, data) => { t.ok(err) - t.equal(err.code, 'HPE_INVALID_CONSTANT') - t.equal(err.message, 'Expected HTTP/') + t.type(err, errors.HTTPParserError) }) }) }) From 4688da22005f18f44ef5c11b670dc72e714387a3 Mon Sep 17 00:00:00 2001 From: George MacKerron Date: Wed, 3 May 2023 10:43:23 +0100 Subject: [PATCH 061/259] Fix bug in 16-bit frame length when buffer is a subarray (#2106) --- lib/websocket/frame.js | 2 +- test/websocket/frame.js | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 test/websocket/frame.js diff --git a/lib/websocket/frame.js b/lib/websocket/frame.js index 1df5e16934b..61bfd3915ce 100644 --- a/lib/websocket/frame.js +++ b/lib/websocket/frame.js @@ -43,7 +43,7 @@ class WebsocketFrameSend { buffer[1] = payloadLength if (payloadLength === 126) { - new DataView(buffer.buffer).setUint16(2, bodyLength) + buffer.writeUInt16BE(bodyLength, 2) } else if (payloadLength === 127) { // Clear extended payload length buffer[2] = buffer[3] = 0 diff --git a/test/websocket/frame.js b/test/websocket/frame.js new file mode 100644 index 00000000000..b4b73b7cf08 --- /dev/null +++ b/test/websocket/frame.js @@ -0,0 +1,24 @@ +'use strict' + +const { test } = require('tap') +const { WebsocketFrameSend } = require('../../lib/websocket/frame') +const { opcodes } = require('../../lib/websocket/constants') + +test('Writing 16-bit frame length value at correct offset when buffer has a non-zero byteOffset', (t) => { + /* + When writing 16-bit frame lengths, a `DataView` was being used without setting a `byteOffset` into the buffer: + i.e. `new DataView(buffer.buffer)` instead of `new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength)`. + Small `Buffers` returned by `allocUnsafe` are usually returned from the buffer pool, and thus have a non-zero `byteOffset`. + Invalid frames were therefore being returned in that case. + */ + t.plan(3) + + const payloadLength = 126 // 126 bytes is the smallest payload to trigger a 16-bit length field + const smallBuffer = Buffer.allocUnsafe(1) // make it very likely that the next buffer returned by allocUnsafe DOESN'T have a zero byteOffset + const payload = Buffer.allocUnsafe(payloadLength).fill(0) + const frame = new WebsocketFrameSend(payload).createFrame(opcodes.BINARY) + + t.equal(frame[2], payloadLength >>> 8) + t.equal(frame[3], payloadLength & 0xff) + t.equal(smallBuffer.length, 1) // ensure smallBuffer can't be garbage-collected too soon +}) From 790b35da57ca136cfa93d14049f77701f02e806d Mon Sep 17 00:00:00 2001 From: Khafra Date: Thu, 4 May 2023 01:09:28 -0400 Subject: [PATCH 062/259] update wpts (#2108) --- CONTRIBUTING.md | 9 +- test/wpt/status/fetch.status.json | 8 + test/wpt/tests/.azure-pipelines.yml | 37 +++ test/wpt/tests/.gitignore | 1 - test/wpt/tests/.taskcluster.yml | 2 +- test/wpt/tests/CODEOWNERS | 3 - .../tests/fetch/api/basic/keepalive.any.js | 10 - .../wpt/tests/fetch/api/body/mime-type.any.js | 87 ++++++++ .../api/redirect/redirect-keepalive.any.js | 118 ++++++---- .../fetch/api/resources/keepalive-helper.js | 8 +- .../resources/keepalive-redirect-window.html | 18 +- .../fetch/api/response/response-clone.any.js | 16 +- .../tests/fetch/data-urls/navigate.window.js | 75 +++++++ ...fetch-from-treat-as-public.https.window.js | 20 +- .../iframe.tentative.https.window.js | 19 +- .../redirect.https.window.js | 187 +++++++++++++++- .../resources/support.sub.js | 2 + .../service-worker-fetch.https.window.js | 21 +- .../service-worker-update.https.window.js | 23 +- .../service-worker.https.window.js | 29 +-- .../shared-worker-blob-fetch.https.window.js | 25 ++- .../shared-worker-fetch.https.window.js | 21 +- .../shared-worker.https.window.js | 24 -- .../worker.https.window.js | 34 +-- .../xhr-from-treat-as-public.https.window.js | 15 +- test/wpt/tests/lint.ignore | 210 +----------------- .../chromium/mock-pressure-service.js | 18 +- .../partitioned-cookies.tentative.https.html | 85 +++++++ ...ioned-cookies-3p-credentialless-frame.html | 69 ++++++ .../partitioned-cookies-3p-frame.html | 67 ++++++ .../resources/partitioned-cookies-3p-sw.js | 30 +++ .../partitioned-cookies-3p-window.html | 35 +++ .../resources/partitioned-cookies-sw.js | 30 +++ ...kets_storage_policy.tentative.https.any.js | 25 +++ .../buckets/resources/cached-resource.txt | 1 + .../tests/storage/buckets/resources/util.js | 42 ++++ .../storage/estimate-indexeddb.https.any.js | 58 +---- 37 files changed, 1024 insertions(+), 458 deletions(-) create mode 100644 test/wpt/tests/fetch/data-urls/navigate.window.js create mode 100644 test/wpt/tests/service-workers/service-worker/partitioned-cookies.tentative.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-credentialless-frame.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-frame.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-sw.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-window.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-sw.js create mode 100644 test/wpt/tests/storage/buckets/resources/cached-resource.txt diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c6e8b57f357..2d67ba7f9e4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -91,14 +91,17 @@ Here are the steps to update them. ```bash git clone --depth 1 --single-branch --branch epochs/daily --filter=blob:none --sparse https://github.com/web-platform-tests/wpt.git test/wpt/tests cd test/wpt/tests + +git sparse-checkout add /resources +git sparse-checkout add /interfaces +git sparse-checkout add /common git sparse-checkout add /fetch git sparse-checkout add /FileAPI git sparse-checkout add /xhr git sparse-checkout add /websockets -git sparse-checkout add /resources -git sparse-checkout add /common git sparse-checkout add /mimesniff -git sparse-checkout add /interfaces +git sparse-checkout add /storage +git sparse-checkout add /service-workers ``` diff --git a/test/wpt/status/fetch.status.json b/test/wpt/status/fetch.status.json index 72930388cb3..cb5949579cf 100644 --- a/test/wpt/status/fetch.status.json +++ b/test/wpt/status/fetch.status.json @@ -99,6 +99,14 @@ ] } }, + "body": { + "mime-type.any.js": { + "note": "fails on all platforms, https://wpt.fyi/results/fetch/api/body/mime-type.any.html?label=master&label=experimental&product=chrome&product=firefox&product=safari&product=node.js&product=deno&aligned", + "fail": [ + "Response: Extract a MIME type with clone" + ] + } + }, "cors": { "note": "undici doesn't implement CORs", "skip": true diff --git a/test/wpt/tests/.azure-pipelines.yml b/test/wpt/tests/.azure-pipelines.yml index aacd78815ad..20d5ec0f431 100644 --- a/test/wpt/tests/.azure-pipelines.yml +++ b/test/wpt/tests/.azure-pipelines.yml @@ -541,3 +541,40 @@ jobs: parameters: dependsOn: results_safari_preview artifactName: safari-preview-results + +- job: results_wktr_preview + displayName: 'all tests: WebKitTestRunner' + condition: | + or(eq(variables['Build.SourceBranch'], 'refs/heads/triggers/wktr_preview'), + and(eq(variables['Build.Reason'], 'Manual'), variables['run_all_wktr_preview'])) + strategy: + parallel: 8 # chosen to make runtime ~2h + timeoutInMinutes: 180 + pool: + vmImage: 'macOS-12' + steps: + - task: UsePythonVersion@0 + inputs: + versionSpec: '3.10' + - template: tools/ci/azure/checkout.yml + - template: tools/ci/azure/pip_install.yml + parameters: + packages: virtualenv + - template: tools/ci/azure/install_certs.yml + - template: tools/ci/azure/update_hosts.yml + - template: tools/ci/azure/update_manifest.yml + - script: | + set -eux -o pipefail + export SYSTEM_VERSION_COMPAT=0 + ./wpt run --no-manifest-update --no-restart-on-unexpected --no-fail-on-unexpected --this-chunk=$(System.JobPositionInPhase) --total-chunks=$(System.TotalJobsInPhase) --chunk-type hash --log-wptreport $(Build.ArtifactStagingDirectory)/wpt_report_$(System.JobPositionInPhase).json --log-wptscreenshot $(Build.ArtifactStagingDirectory)/wpt_screenshot_$(System.JobPositionInPhase).txt --log-mach - --log-mach-level info --channel main --install-browser wktr + displayName: 'Run tests' + - task: PublishBuildArtifacts@1 + displayName: 'Publish results' + inputs: + artifactName: 'wktr-preview-results' + - template: tools/ci/azure/publish_logs.yml + - template: tools/ci/azure/sysdiagnose.yml +- template: tools/ci/azure/fyi_hook.yml + parameters: + dependsOn: results_wktr_preview + artifactName: wktr-preview-results diff --git a/test/wpt/tests/.gitignore b/test/wpt/tests/.gitignore index 23f0370c4d0..061700a9604 100644 --- a/test/wpt/tests/.gitignore +++ b/test/wpt/tests/.gitignore @@ -45,7 +45,6 @@ scratch /css/build-temp /css/dist /css/dist_last -/css/tools/cache /url/tools/IdnaTestV2.txt /webaudio/idl/* diff --git a/test/wpt/tests/.taskcluster.yml b/test/wpt/tests/.taskcluster.yml index f830bff668f..c80e92af204 100644 --- a/test/wpt/tests/.taskcluster.yml +++ b/test/wpt/tests/.taskcluster.yml @@ -57,7 +57,7 @@ tasks: owner: ${owner} source: ${event.repository.clone_url} payload: - image: webplatformtests/wpt:0.52 + image: webplatformtests/wpt:0.53 maxRunTime: 7200 artifacts: public/results: diff --git a/test/wpt/tests/CODEOWNERS b/test/wpt/tests/CODEOWNERS index 2372633782e..140e0c6545b 100644 --- a/test/wpt/tests/CODEOWNERS +++ b/test/wpt/tests/CODEOWNERS @@ -1,6 +1,3 @@ -# Prevent accidentially touching CSS subtree -/css/tools/apiclient/ @plinss @web-platform-tests/wpt-core-team - # Require review for changes that often need an RFC /resources/testdriver* @web-platform-tests/wpt-core-team /resources/testharness* @web-platform-tests/wpt-core-team diff --git a/test/wpt/tests/fetch/api/basic/keepalive.any.js b/test/wpt/tests/fetch/api/basic/keepalive.any.js index 047f1ed2348..4f33284d0c7 100644 --- a/test/wpt/tests/fetch/api/basic/keepalive.any.js +++ b/test/wpt/tests/fetch/api/basic/keepalive.any.js @@ -27,13 +27,3 @@ for (const method of ['GET', 'POST']) { assertStashedTokenAsync(`simple ${method} request: no payload`, token1); }, `simple ${method} request: no payload; setting up`); } - -promise_test(async (test) => { - const w = window.open(`${ - HTTP_NOTSAMESITE_ORIGIN}/fetch/api/resources/keepalive-redirect-window.html`); - const token = await getTokenFromMessage(); - w.close(); - - assertStashedTokenAsync( - 'keepalive in onunload in nested frame in another window', token); -}, 'keepalive in onunload in nested frame in another window; setting up'); diff --git a/test/wpt/tests/fetch/api/body/mime-type.any.js b/test/wpt/tests/fetch/api/body/mime-type.any.js index a0f90a0abdf..67c9af7da2d 100644 --- a/test/wpt/tests/fetch/api/body/mime-type.any.js +++ b/test/wpt/tests/fetch/api/body/mime-type.any.js @@ -38,3 +38,90 @@ assert_equals(blob.type, newMIMEType); }, `${bodyContainer.constructor.name}: setting missing Content-Type`); }); + +[ + () => new Request("about:blank", { method: "POST" }), + () => new Response(), +].forEach(bodyContainerCreator => { + const bodyContainer = bodyContainerCreator(); + promise_test(async t => { + const blob = await bodyContainer.blob(); + assert_equals(blob.type, ""); + }, `${bodyContainer.constructor.name}: MIME type for Blob from empty body`); +}); + +[ + () => new Request("about:blank", { method: "POST", headers: [["Content-Type", "Mytext/Plain"]] }), + () => new Response("", { headers: [["Content-Type", "Mytext/Plain"]] }) +].forEach(bodyContainerCreator => { + const bodyContainer = bodyContainerCreator(); + promise_test(async t => { + const blob = await bodyContainer.blob(); + assert_equals(blob.type, 'mytext/plain'); + }, `${bodyContainer.constructor.name}: MIME type for Blob from empty body with Content-Type`); +}); + +[ + () => new Request("about:blank", { body: new Blob([""]), method: "POST" }), + () => new Response(new Blob([""])) +].forEach(bodyContainerCreator => { + const bodyContainer = bodyContainerCreator(); + promise_test(async t => { + const blob = await bodyContainer.blob(); + assert_equals(blob.type, ""); + assert_equals(bodyContainer.headers.get("Content-Type"), null); + }, `${bodyContainer.constructor.name}: MIME type for Blob`); +}); + +[ + () => new Request("about:blank", { body: new Blob([""], { type: "Text/Plain" }), method: "POST" }), + () => new Response(new Blob([""], { type: "Text/Plain" })) +].forEach(bodyContainerCreator => { + const bodyContainer = bodyContainerCreator(); + promise_test(async t => { + const blob = await bodyContainer.blob(); + assert_equals(blob.type, "text/plain"); + assert_equals(bodyContainer.headers.get("Content-Type"), "text/plain"); + }, `${bodyContainer.constructor.name}: MIME type for Blob with non-empty type`); +}); + +[ + () => new Request("about:blank", { method: "POST", body: new Blob([""], { type: "Text/Plain" }), headers: [["Content-Type", "Text/Html"]] }), + () => new Response(new Blob([""], { type: "Text/Plain" }, { headers: [["Content-Type", "Text/Html"]] })) +].forEach(bodyContainerCreator => { + const bodyContainer = bodyContainerCreator(); + const cloned = bodyContainer.clone(); + promise_test(async t => { + const blobs = [await bodyContainer.blob(), await cloned.blob()]; + assert_equals(blobs[0].type, "text/html"); + assert_equals(blobs[1].type, "text/html"); + assert_equals(bodyContainer.headers.get("Content-Type"), "Text/Html"); + assert_equals(cloned.headers.get("Content-Type"), "Text/Html"); + }, `${bodyContainer.constructor.name}: Extract a MIME type with clone`); +}); + +[ + () => new Request("about:blank", { body: new Blob([], { type: "text/plain" }), method: "POST", headers: [["Content-Type", "text/html"]] }), + () => new Response(new Blob([], { type: "text/plain" }), { headers: [["Content-Type", "text/html"]] }), +].forEach(bodyContainerCreator => { + const bodyContainer = bodyContainerCreator(); + promise_test(async t => { + assert_equals(bodyContainer.headers.get("Content-Type"), "text/html"); + const blob = await bodyContainer.blob(); + assert_equals(blob.type, "text/html"); + }, `${bodyContainer.constructor.name}: Content-Type in headers wins Blob"s type`); +}); + +[ + () => new Request("about:blank", { body: new Blob([], { type: "text/plain" }), method: "POST" }), + () => new Response(new Blob([], { type: "text/plain" })), +].forEach(bodyContainerCreator => { + const bodyContainer = bodyContainerCreator(); + promise_test(async t => { + assert_equals(bodyContainer.headers.get("Content-Type"), "text/plain"); + const newMIMEType = "text/html"; + bodyContainer.headers.set("Content-Type", newMIMEType); + const blob = await bodyContainer.blob(); + assert_equals(blob.type, newMIMEType); + }, `${bodyContainer.constructor.name}: setting missing Content-Type in headers and it wins Blob"s type`); +}); diff --git a/test/wpt/tests/fetch/api/redirect/redirect-keepalive.any.js b/test/wpt/tests/fetch/api/redirect/redirect-keepalive.any.js index 9f7cca7dbf7..bcfc444f5a6 100644 --- a/test/wpt/tests/fetch/api/redirect/redirect-keepalive.any.js +++ b/test/wpt/tests/fetch/api/redirect/redirect-keepalive.any.js @@ -14,43 +14,81 @@ const { HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT } = get_host_info(); -promise_test(async (test) => { - const token1 = token(); - const iframe = document.createElement('iframe'); - iframe.src = getKeepAliveAndRedirectIframeUrl( - token1, '', '', /*withPreflight=*/ false); - document.body.appendChild(iframe); - await iframeLoaded(iframe); - assert_equals(await getTokenFromMessage(), token1); - iframe.remove(); - - assertStashedTokenAsync('same-origin redirect', token1); -}, 'same-origin redirect; setting up'); - -promise_test(async (test) => { - const token1 = token(); - const iframe = document.createElement('iframe'); - iframe.src = getKeepAliveAndRedirectIframeUrl( - token1, HTTP_REMOTE_ORIGIN, HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT, - /*withPreflight=*/ false); - document.body.appendChild(iframe); - await iframeLoaded(iframe); - assert_equals(await getTokenFromMessage(), token1); - iframe.remove(); - - assertStashedTokenAsync('cross-origin redirect', token1); -}, 'cross-origin redirect; setting up'); - -promise_test(async (test) => { - const token1 = token(); - const iframe = document.createElement('iframe'); - iframe.src = getKeepAliveAndRedirectIframeUrl( - token1, HTTP_REMOTE_ORIGIN, HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT, - /*withPreflight=*/ true); - document.body.appendChild(iframe); - await iframeLoaded(iframe); - assert_equals(await getTokenFromMessage(), token1); - iframe.remove(); - - assertStashedTokenAsync('cross-origin redirect with preflight', token1); -}, 'cross-origin redirect with preflight; setting up'); +/** + * In an iframe, test to fetch a keepalive URL that involves in redirect to + * another URL. + */ +function keepaliveRedirectTest( + desc, {origin1 = '', origin2 = '', withPreflight = false} = {}) { + desc = `[keepalive] ${desc}`; + promise_test(async (test) => { + const tokenToStash = token(); + const iframe = document.createElement('iframe'); + iframe.src = getKeepAliveAndRedirectIframeUrl( + tokenToStash, origin1, origin2, withPreflight); + document.body.appendChild(iframe); + await iframeLoaded(iframe); + assert_equals(await getTokenFromMessage(), tokenToStash); + iframe.remove(); + + assertStashedTokenAsync(desc, tokenToStash); + }, `${desc}; setting up`); +} + +/** + * Opens a different site window, and in `unload` event handler, test to fetch + * a keepalive URL that involves in redirect to another URL. + */ +function keepaliveRedirectInUnloadTest(desc, { + origin1 = '', + origin2 = '', + url2 = '', + withPreflight = false, + shouldPass = true +} = {}) { + desc = `[keepalive][new window][unload] ${desc}`; + + promise_test(async (test) => { + const targetUrl = + `${HTTP_NOTSAMESITE_ORIGIN}/fetch/api/resources/keepalive-redirect-window.html?` + + `origin1=${origin1}&` + + `origin2=${origin2}&` + + `url2=${url2}&` + (withPreflight ? `with-headers` : ``); + const w = window.open(targetUrl); + const token = await getTokenFromMessage(); + w.close(); + + assertStashedTokenAsync(desc, token, {shouldPass}); + }, `${desc}; setting up`); +} + +keepaliveRedirectTest(`same-origin redirect`); +keepaliveRedirectTest( + `same-origin redirect + preflight`, {withPreflight: true}); +keepaliveRedirectTest(`cross-origin redirect`, { + origin1: HTTP_REMOTE_ORIGIN, + origin2: HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT +}); +keepaliveRedirectTest(`cross-origin redirect + preflight`, { + origin1: HTTP_REMOTE_ORIGIN, + origin2: HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT, + withPreflight: true +}); + +keepaliveRedirectInUnloadTest('same-origin redirect'); +keepaliveRedirectInUnloadTest( + 'same-origin redirect + preflight', {withPreflight: true}); +keepaliveRedirectInUnloadTest('cross-origin redirect', { + origin1: HTTP_REMOTE_ORIGIN, + origin2: HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT +}); +keepaliveRedirectInUnloadTest('cross-origin redirect + preflight', { + origin1: HTTP_REMOTE_ORIGIN, + origin2: HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT, + withPreflight: true +}); +keepaliveRedirectInUnloadTest( + 'redirect to file URL', {url2: 'file://tmp/bar.txt', shouldPass: false}); +keepaliveRedirectInUnloadTest( + 'redirect to data URL', + {url2: 'data:text/plain;base64,cmVzcG9uc2UncyBib2R5', shouldPass: false}); diff --git a/test/wpt/tests/fetch/api/resources/keepalive-helper.js b/test/wpt/tests/fetch/api/resources/keepalive-helper.js index 42f20ac30af..c7048d1ff33 100644 --- a/test/wpt/tests/fetch/api/resources/keepalive-helper.js +++ b/test/wpt/tests/fetch/api/resources/keepalive-helper.js @@ -60,7 +60,7 @@ async function queryToken(token) { // for the rest of the work. Note that we want the serialized behavior // for the steps so far, so we don't want to make the entire test case // an async_test. -function assertStashedTokenAsync(testName, token) { +function assertStashedTokenAsync(testName, token, {shouldPass = true} = {}) { async_test((test) => { new Promise((resolve) => test.step_timeout(resolve, 3000)) .then(() => { @@ -73,7 +73,11 @@ function assertStashedTokenAsync(testName, token) { test.done(); }) .catch(test.step_func((e) => { - assert_unreached(e); + if (shouldPass) { + assert_unreached(e); + } else { + test.done(); + } })); }, testName); } diff --git a/test/wpt/tests/fetch/api/resources/keepalive-redirect-window.html b/test/wpt/tests/fetch/api/resources/keepalive-redirect-window.html index 6ccf484644c..c18650796cc 100644 --- a/test/wpt/tests/fetch/api/resources/keepalive-redirect-window.html +++ b/test/wpt/tests/fetch/api/resources/keepalive-redirect-window.html @@ -10,20 +10,28 @@ HTTP_REMOTE_ORIGIN, HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT } = get_host_info(); -const REDIRECT_DESTINATION = - `${HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT}/fetch/api/resources/stash-put.py` + + +const SEARCH_PARAMS = new URL(location.href).searchParams; +const WITH_HEADERS = !!SEARCH_PARAMS.has('with-headers'); +const ORIGIN1 = SEARCH_PARAMS.get('origin1') || ''; +const ORIGIN2 = SEARCH_PARAMS.get('origin2') || ''; +const URL2 = SEARCH_PARAMS.get('url2') || ''; + +const REDIRECT_DESTINATION = URL2 ? URL2 : + `${ORIGIN2}/fetch/api/resources/stash-put.py` + `?key=${TOKEN}&value=on`; -const URL = - `${HTTP_REMOTE_ORIGIN}/fetch/api/resources/redirect.py?` + +const FROM_URL = + `${ORIGIN1}/fetch/api/resources/redirect.py?` + `delay=500&` + `allow_headers=foo&` + `location=${encodeURIComponent(REDIRECT_DESTINATION)}`; addEventListener('load', () => { + const headers = WITH_HEADERS ? {'foo': 'bar'} : undefined; const iframe = document.createElement('iframe'); document.body.appendChild(iframe); iframe.contentWindow.addEventListener('unload', () => { - iframe.contentWindow.fetch(URL, {keepalive: true, headers: {foo: 'bar'}}); + iframe.contentWindow.fetch(FROM_URL, {keepalive: true, headers}); }); window.opener.postMessage(TOKEN, '*'); diff --git a/test/wpt/tests/fetch/api/response/response-clone.any.js b/test/wpt/tests/fetch/api/response/response-clone.any.js index 9f4f36ed2d1..f5cda75149e 100644 --- a/test/wpt/tests/fetch/api/response/response-clone.any.js +++ b/test/wpt/tests/fetch/api/response/response-clone.any.js @@ -103,7 +103,21 @@ function testReadableStreamClone(initialBuffer, bufferType) return stream2.getReader().read(); }).then(function(data) { assert_false(data.done); - assert_array_equals(data.value, initialBuffer, "Cloned buffer chunks have the same content"); + if (initialBuffer instanceof ArrayBuffer) { + assert_true(data.value instanceof ArrayBuffer, "Cloned buffer is ArrayBufer"); + assert_equals(initialBuffer.byteLength, data.value.byteLength, "Length equal"); + assert_array_equals(new Uint8Array(data.value), new Uint8Array(initialBuffer), "Cloned buffer chunks have the same content"); + } else if (initialBuffer instanceof DataView) { + assert_true(data.value instanceof DataView, "Cloned buffer is DataView"); + assert_equals(initialBuffer.byteLength, data.value.byteLength, "Lengths equal"); + assert_equals(initialBuffer.byteOffset, data.value.byteOffset, "Offsets equal"); + for (let i = 0; i < initialBuffer.byteLength; ++i) { + assert_equals( + data.value.getUint8(i), initialBuffer.getUint8(i), "Mismatch at byte ${i}"); + } + } else { + assert_array_equals(data.value, initialBuffer, "Cloned buffer chunks have the same content"); + } assert_equals(Object.getPrototypeOf(data.value), Object.getPrototypeOf(initialBuffer), "Cloned buffers have the same type"); assert_not_equals(data.value, initialBuffer, "Buffer of cloned response stream is a clone of the original buffer"); }); diff --git a/test/wpt/tests/fetch/data-urls/navigate.window.js b/test/wpt/tests/fetch/data-urls/navigate.window.js new file mode 100644 index 00000000000..b532a006830 --- /dev/null +++ b/test/wpt/tests/fetch/data-urls/navigate.window.js @@ -0,0 +1,75 @@ +// META: timeout=long +// +// Test some edge cases around navigation to data: URLs to ensure they use the same code path + +[ + { + input: "data:text/html,", + result: 1, + name: "Nothing fancy", + }, + { + input: "data:text/html;base64,PHNjcmlwdD5wYXJlbnQucG9zdE1lc3NhZ2UoMiwgJyonKTwvc2NyaXB0Pg==", + result: 2, + name: "base64", + }, + { + input: "data:text/html;base64,PHNjcmlwdD5wYXJlbnQucG9zdE1lc3NhZ2UoNCwgJyonKTwvc2NyaXB0Pr+/", + result: 4, + name: "base64 with code points that differ from base64url" + }, + { + input: "data:text/html;base64,PHNjcml%09%20%20%0A%0C%0DwdD5wYXJlbnQucG9zdE1lc3NhZ2UoNiwgJyonKTwvc2NyaXB0Pg==", + result: 6, + name: "ASCII whitespace in the input is removed" + } +].forEach(({ input, result, name }) => { + // Use promise_test so they go sequentially + promise_test(async t => { + const event = await new Promise((resolve, reject) => { + self.addEventListener("message", t.step_func(resolve), { once: true }); + const frame = document.body.appendChild(document.createElement("iframe")); + t.add_cleanup(() => frame.remove()); + + // The assumption is that postMessage() is quicker + t.step_timeout(reject, 500); + frame.src = input; + }); + assert_equals(event.data, result); + }, name); +}); + +// Failure cases +[ + { + input: "data:text/html;base64,PHNjcmlwdD5wYXJlbnQucG9zdE1lc3NhZ2UoMywgJyonKTwvc2NyaXB0Pg=", + name: "base64 with incorrect padding", + }, + { + input: "data:text/html;base64,PHNjcmlwdD5wYXJlbnQucG9zdE1lc3NhZ2UoNSwgJyonKTwvc2NyaXB0Pr-_", + name: "base64url is not supported" + }, + { + input: "data:text/html;base64,%0BPHNjcmlwdD5wYXJlbnQucG9zdE1lc3NhZ2UoNywgJyonKTwvc2NyaXB0Pg==", + name: "Vertical tab in the input leads to an error" + } +].forEach(({ input, name }) => { + // Continue to use promise_test so they go sequentially + promise_test(async t => { + const event = await new Promise((resolve, reject) => { + self.addEventListener("message", t.step_func(reject), { once: true }); + const frame = document.body.appendChild(document.createElement("iframe")); + t.add_cleanup(() => frame.remove()); + + // The assumption is that postMessage() is quicker + t.step_timeout(resolve, 500); + frame.src = input; + }); + }, name); +}); + +// I found some of the interesting code point cases above through brute force: +// +// for (i = 0; i < 256; i++) { +// w(btoa(" + + + + + + + + + + + \ No newline at end of file diff --git a/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-credentialless-frame.html b/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-credentialless-frame.html new file mode 100644 index 00000000000..ff24bf3670c --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-credentialless-frame.html @@ -0,0 +1,69 @@ + + + + +Service Worker: Partitioned Cookies 3P Credentialless Iframe + + + + + + + + + \ No newline at end of file diff --git a/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-frame.html b/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-frame.html new file mode 100644 index 00000000000..d3962d2e600 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-frame.html @@ -0,0 +1,67 @@ + + + + +Service Worker: Partitioned Cookies 3P Iframe + + + + + + + + \ No newline at end of file diff --git a/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-sw.js b/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-sw.js new file mode 100644 index 00000000000..2f54a984b19 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-sw.js @@ -0,0 +1,30 @@ +self.addEventListener('message', ev => ev.waitUntil(onMessage(ev))); + +async function onMessage(event) { + if (!event.data) + return; + switch (event.data.type) { + case 'test_message': + return onTestMessage(event); + case 'echo_cookies': + return onEchoCookies(event); + default: + return; + } +} + +// test_message just verifies that the message passing is working. +async function onTestMessage(event) { + event.source.postMessage({ok: true}); +} + +// echo_cookies returns the names of all of the cookies available to the worker. +async function onEchoCookies(event) { + try { + const cookie_objects = await self.cookieStore.getAll(); + const cookies = cookie_objects.map(c => c.name); + event.source.postMessage({ok: true, cookies}); + } catch (err) { + event.source.postMessage({ok: false}); + } +} diff --git a/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-window.html b/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-window.html new file mode 100644 index 00000000000..8e90609da22 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-window.html @@ -0,0 +1,35 @@ + + + + +Service Worker: Partitioned Cookies 3P Window + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-sw.js b/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-sw.js new file mode 100644 index 00000000000..2f54a984b19 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-sw.js @@ -0,0 +1,30 @@ +self.addEventListener('message', ev => ev.waitUntil(onMessage(ev))); + +async function onMessage(event) { + if (!event.data) + return; + switch (event.data.type) { + case 'test_message': + return onTestMessage(event); + case 'echo_cookies': + return onEchoCookies(event); + default: + return; + } +} + +// test_message just verifies that the message passing is working. +async function onTestMessage(event) { + event.source.postMessage({ok: true}); +} + +// echo_cookies returns the names of all of the cookies available to the worker. +async function onEchoCookies(event) { + try { + const cookie_objects = await self.cookieStore.getAll(); + const cookies = cookie_objects.map(c => c.name); + event.source.postMessage({ok: true, cookies}); + } catch (err) { + event.source.postMessage({ok: false}); + } +} diff --git a/test/wpt/tests/storage/buckets/buckets_storage_policy.tentative.https.any.js b/test/wpt/tests/storage/buckets/buckets_storage_policy.tentative.https.any.js index d6dce3675d0..a66fd81cd43 100644 --- a/test/wpt/tests/storage/buckets/buckets_storage_policy.tentative.https.any.js +++ b/test/wpt/tests/storage/buckets/buckets_storage_policy.tentative.https.any.js @@ -19,3 +19,28 @@ promise_test(async testCase => { navigator.storageBuckets.open( 'above_max', {quota: Number.MAX_SAFE_INTEGER + 1})); }, 'The open promise should reject with a TypeError when quota is requested outside the range of 1 to Number.MAX_SAFE_INTEGER.'); + + +promise_test(async testCase => { + await prepareForBucketTest(testCase); + + // IndexedDB + { + const quota = 1; + const bucket = await navigator.storageBuckets.open('idb', {quota}); + + const objectStoreName = 'store'; + const db = await indexedDbOpenRequest( + testCase, bucket.indexedDB, 'db', (db_to_upgrade) => { + db_to_upgrade.createObjectStore(objectStoreName); + }); + + const overflowBuffer = new Uint8Array(quota + 1); + + const txn = db.transaction(objectStoreName, 'readwrite'); + txn.objectStore(objectStoreName).add('', overflowBuffer); + + await promise_rejects_dom( + testCase, 'QuotaExceededError', transactionPromise(txn)); + } +}, 'A QuotaExceededError is thrown when a storage API exceeds the quota of the bucket its in.'); diff --git a/test/wpt/tests/storage/buckets/resources/cached-resource.txt b/test/wpt/tests/storage/buckets/resources/cached-resource.txt new file mode 100644 index 00000000000..c57eff55ebc --- /dev/null +++ b/test/wpt/tests/storage/buckets/resources/cached-resource.txt @@ -0,0 +1 @@ +Hello World! \ No newline at end of file diff --git a/test/wpt/tests/storage/buckets/resources/util.js b/test/wpt/tests/storage/buckets/resources/util.js index 50abce14cdc..425303ce2c9 100644 --- a/test/wpt/tests/storage/buckets/resources/util.js +++ b/test/wpt/tests/storage/buckets/resources/util.js @@ -13,3 +13,45 @@ async function prepareForBucketTest(test) { } }); } + +function indexedDbOpenRequest(t, idb, dbname, upgrade_func) { + return new Promise((resolve, reject) => { + const openRequest = idb.open(dbname); + t.add_cleanup(() => { + indexedDbDeleteRequest(idb, dbname); + }); + + openRequest.onerror = () => { + reject(openRequest.error); + }; + openRequest.onsuccess = () => { + resolve(openRequest.result); + }; + openRequest.onupgradeneeded = event => { + upgrade_func(openRequest.result); + }; + }); +} + +function indexedDbDeleteRequest(idb, name) { + return new Promise((resolve, reject) => { + const deleteRequest = idb.deleteDatabase(name); + deleteRequest.onerror = () => { + reject(deleteRequest.error); + }; + deleteRequest.onsuccess = () => { + resolve(); + }; + }); +} + +function transactionPromise(txn) { + return new Promise((resolve, reject) => { + txn.onabort = () => { + reject(txn.error); + }; + txn.oncomplete = () => { + resolve(); + }; + }); +} diff --git a/test/wpt/tests/storage/estimate-indexeddb.https.any.js b/test/wpt/tests/storage/estimate-indexeddb.https.any.js index b0c6b944dd6..f0b82b9fa09 100644 --- a/test/wpt/tests/storage/estimate-indexeddb.https.any.js +++ b/test/wpt/tests/storage/estimate-indexeddb.https.any.js @@ -1,46 +1,5 @@ // META: title=StorageManager: estimate() for indexeddb - -function indexedDbOpenRequest(t, dbname, upgrade_func) { - return new Promise((resolve, reject) => { - const openRequest = indexedDB.open(dbname); - t.add_cleanup(() => { - indexedDbDeleteRequest(dbname); - }); - - openRequest.onerror = () => { - reject(openRequest.error); - }; - openRequest.onsuccess = () => { - resolve(openRequest.result); - }; - openRequest.onupgradeneeded = event => { - upgrade_func(openRequest.result); - }; - }); -} - -function indexedDbDeleteRequest(name) { - return new Promise((resolve, reject) => { - const deleteRequest = indexedDB.deleteDatabase(name); - deleteRequest.onerror = () => { - reject(deleteRequest.error); - }; - deleteRequest.onsuccess = () => { - resolve(); - }; - }); -} - -function transactionPromise(txn) { - return new Promise((resolve, reject) => { - txn.onabort = () => { - reject(txn.error); - }; - txn.oncomplete = () => { - resolve(); - }; - }); -} +// META: script=/storage/buckets/resources/util.js test(t => { assert_true('estimate' in navigator.storage); @@ -60,16 +19,17 @@ promise_test(async t => { promise_test(async t => { const arraySize = 1e6; const objectStoreName = "storageManager"; - const dbname = this.window ? window.location.pathname : - "estimate-worker.https.html"; + const dbname = + this.window ? window.location.pathname : 'estimate-worker.https.html'; - await indexedDbDeleteRequest(dbname); + await indexedDbDeleteRequest(indexedDB, dbname); let estimate = await navigator.storage.estimate(); const usageBeforeCreate = estimate.usage; - const db = await indexedDbOpenRequest(t, dbname, (db_to_upgrade) => { - db_to_upgrade.createObjectStore(objectStoreName); - }); + const db = + await indexedDbOpenRequest(t, indexedDB, dbname, (db_to_upgrade) => { + db_to_upgrade.createObjectStore(objectStoreName); + }); estimate = await navigator.storage.estimate(); const usageAfterCreate = estimate.usage; @@ -86,7 +46,7 @@ promise_test(async t => { view[i] = Math.floor(Math.random() * 255); } - const testBlob = new Blob([buffer], {type: "binary/random"}); + const testBlob = new Blob([buffer], {type: 'binary/random'}); txn.objectStore(objectStoreName).add(testBlob, 1); await transactionPromise(txn); From 4cda7d5a010707ae459ec7e22f3616c29182c426 Mon Sep 17 00:00:00 2001 From: Filatov Dmitry Date: Mon, 8 May 2023 10:45:15 +0300 Subject: [PATCH 063/259] fix: update error definitions (#2112) --- test/types/errors.test-d.ts | 7 +++++++ types/errors.d.ts | 11 ++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/test/types/errors.test-d.ts b/test/types/errors.test-d.ts index 8708519504b..837dbf89443 100644 --- a/test/types/errors.test-d.ts +++ b/test/types/errors.test-d.ts @@ -1,8 +1,11 @@ import { expectAssignable } from 'tsd' import { errors } from '../..' import Client from '../../types/client' +import { IncomingHttpHeaders } from "../../types/header"; expectAssignable(new errors.UndiciError()) +expectAssignable(new errors.UndiciError().name) +expectAssignable(new errors.UndiciError().code) expectAssignable(new errors.ConnectTimeoutError()) expectAssignable(new errors.ConnectTimeoutError()) @@ -28,6 +31,10 @@ expectAssignable(new errors.ResponseStatusCodeError()) expectAssignable(new errors.ResponseStatusCodeError()) expectAssignable<'ResponseStatusCodeError'>(new errors.ResponseStatusCodeError().name) expectAssignable<'UND_ERR_RESPONSE_STATUS_CODE'>(new errors.ResponseStatusCodeError().code) +expectAssignable(new errors.ResponseStatusCodeError().status) +expectAssignable(new errors.ResponseStatusCodeError().statusCode) +expectAssignable(new errors.ResponseStatusCodeError().headers) +expectAssignable | string>(new errors.ResponseStatusCodeError().body) expectAssignable(new errors.InvalidArgumentError()) expectAssignable(new errors.InvalidArgumentError()) diff --git a/types/errors.d.ts b/types/errors.d.ts index fd2ce7c3a99..7923ddd9796 100644 --- a/types/errors.d.ts +++ b/types/errors.d.ts @@ -4,7 +4,10 @@ import Client from './client' export default Errors declare namespace Errors { - export class UndiciError extends Error { } + export class UndiciError extends Error { + name: string; + code: string; + } /** Connect timeout error. */ export class ConnectTimeoutError extends UndiciError { @@ -31,6 +34,12 @@ declare namespace Errors { } export class ResponseStatusCodeError extends UndiciError { + constructor ( + message?: string, + statusCode?: number, + headers?: IncomingHttpHeaders | string[] | null, + body?: null | Record | string + ); name: 'ResponseStatusCodeError'; code: 'UND_ERR_RESPONSE_STATUS_CODE'; body: null | Record | string From 241dfafd289dfc461107ee04df9b0621db8a07d5 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Mon, 8 May 2023 20:08:31 +0300 Subject: [PATCH 064/259] fix: make assertion a noop (#2111) Not sure how this can occur. I'm guessing if 'finished' and 'drain' are both queued then onDrain will be invoked even though event listeners have been removed. Fixes: https://github.com/nodejs/undici/issues/2109 --- lib/client.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/client.js b/lib/client.js index f237fe02ab8..7d9ec8d7c27 100644 --- a/lib/client.js +++ b/lib/client.js @@ -1497,9 +1497,11 @@ function writeStream ({ body, client, request, socket, contentLength, header, ex const writer = new AsyncWriter({ socket, request, contentLength, client, expectsPayload, header }) const onData = function (chunk) { - try { - assert(!finished) + if (finished) { + return + } + try { if (!writer.write(chunk) && this.pause) { this.pause() } @@ -1508,7 +1510,9 @@ function writeStream ({ body, client, request, socket, contentLength, header, ex } } const onDrain = function () { - assert(!finished) + if (finished) { + return + } if (body.resume) { body.resume() From 9d30456aa6f195b83ea4ba36ed0b51a951e6bd87 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Thu, 11 May 2023 09:47:31 +0200 Subject: [PATCH 065/259] 5.22.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6c325d75cdf..49b657fded2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "5.22.0", + "version": "5.22.1", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { From 135ad3f87465d90bc91811cba4c7540137bce51c Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Fri, 12 May 2023 17:43:02 +0200 Subject: [PATCH 066/259] bump engines to node >= 16 (#2119) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 49b657fded2..671f78520be 100644 --- a/package.json +++ b/package.json @@ -104,7 +104,7 @@ "ws": "^8.11.0" }, "engines": { - "node": ">=14.0" + "node": ">=16.0" }, "standard": { "env": [ From 09e9068b395352a045e9646edd0190c4ea1b8ae1 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Fri, 12 May 2023 23:51:47 +0200 Subject: [PATCH 067/259] Revert "bump engines to node >= 16 (#2119)" (#2121) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 671f78520be..49b657fded2 100644 --- a/package.json +++ b/package.json @@ -104,7 +104,7 @@ "ws": "^8.11.0" }, "engines": { - "node": ">=16.0" + "node": ">=14.0" }, "standard": { "env": [ From f5f7c18698b2b373d04867a07b1e59af9e284714 Mon Sep 17 00:00:00 2001 From: Khafra Date: Mon, 15 May 2023 00:55:12 -0400 Subject: [PATCH 068/259] fetch: set referrer properly (#2125) * fetch: set referrer properly Fixes https://github.com/nodejs/undici/issues/2124 * fix: origin is not a url --- lib/fetch/request.js | 18 +++++++++++------- test/fetch/request.js | 11 +++++++++++ 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/lib/fetch/request.js b/lib/fetch/request.js index 5c836e4e558..17fafca6507 100644 --- a/lib/fetch/request.js +++ b/lib/fetch/request.js @@ -232,14 +232,18 @@ class Request { } // 3. If one of the following is true - // parsedReferrer’s cannot-be-a-base-URL is true, scheme is "about", - // and path contains a single string "client" - // parsedReferrer’s origin is not same origin with origin + // - parsedReferrer’s scheme is "about" and path is the string "client" + // - parsedReferrer’s origin is not same origin with origin // then set request’s referrer to "client". - // TODO - - // 4. Otherwise, set request’s referrer to parsedReferrer. - request.referrer = parsedReferrer + if ( + (parsedReferrer.protocol === 'about:' && parsedReferrer.hostname === 'client') || + (origin && !sameOrigin(parsedReferrer, this[kRealm].settingsObject.baseUrl)) + ) { + request.referrer = 'client' + } else { + // 4. Otherwise, set request’s referrer to parsedReferrer. + request.referrer = parsedReferrer + } } } diff --git a/test/fetch/request.js b/test/fetch/request.js index e6ab49b7808..acd3e6ab439 100644 --- a/test/fetch/request.js +++ b/test/fetch/request.js @@ -476,4 +476,15 @@ test('set-cookie headers get cleared when passing a Request as first param', (t) t.end() }) +// https://github.com/nodejs/undici/issues/2124 +test('request.referrer', (t) => { + for (const referrer of ['about://client', 'about://client:1234']) { + const request = new Request('http://a', { referrer }) + + t.equal(request.referrer, 'about:client') + } + + t.end() +}) + teardown(() => process.exit()) From 51fa0fe3c62a770e8b1bbead0665f0bf9c5b81a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20W=C3=A4rting?= Date: Mon, 15 May 2023 23:01:17 +0200 Subject: [PATCH 069/259] fix: support truncated gzip (#2126) --- lib/fetch/index.js | 9 ++++++++- test/node-fetch/main.js | 14 ++++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/lib/fetch/index.js b/lib/fetch/index.js index 51998732427..90683a8febb 100644 --- a/lib/fetch/index.js +++ b/lib/fetch/index.js @@ -2007,7 +2007,14 @@ async function httpNetworkFetch ( for (const coding of codings) { // https://www.rfc-editor.org/rfc/rfc9112.html#section-7.2 if (coding === 'x-gzip' || coding === 'gzip') { - decoders.push(zlib.createGunzip()) + decoders.push(zlib.createGunzip({ + // Be less strict when decoding compressed responses, since sometimes + // servers send slightly invalid responses that are still accepted + // by common browsers. + // Always using Z_SYNC_FLUSH is what cURL does. + flush: zlib.constants.Z_SYNC_FLUSH, + finishFlush: zlib.constants.Z_SYNC_FLUSH + })) } else if (coding === 'deflate') { decoders.push(zlib.createInflate()) } else if (coding === 'br') { diff --git a/test/node-fetch/main.js b/test/node-fetch/main.js index 8dc20f2ce64..358a969b574 100644 --- a/test/node-fetch/main.js +++ b/test/node-fetch/main.js @@ -640,15 +640,13 @@ describe('node-fetch', () => { }) }) - xit('should decompress slightly invalid gzip response', () => { + it('should decompress slightly invalid gzip response', async () => { const url = `${base}gzip-truncated` - return fetch(url).then(res => { - expect(res.headers.get('content-type')).to.equal('text/plain') - return res.text().then(result => { - expect(result).to.be.a('string') - expect(result).to.equal('hello world') - }) - }) + const res = await fetch(url) + expect(res.headers.get('content-type')).to.equal('text/plain') + const result = await res.text() + expect(result).to.be.a('string') + expect(result).to.equal('hello world') }) it('should decompress deflate response', () => { From 5281fa8c7ab45cb6d0887247558feafef56ad198 Mon Sep 17 00:00:00 2001 From: StepSecurity Bot Date: Sun, 21 May 2023 18:16:43 -0700 Subject: [PATCH 070/259] workflow: apply security best practices (#2130) Reference: https://github.com/nodejs/security-wg/issues/859 Signed-off-by: StepSecurity Bot --- .github/dependabot.yml | 10 ++++ .github/workflows/bench.yml | 11 ++-- .github/workflows/codeql.yml | 78 +++++++++++++++++++++++++ .github/workflows/dependency-review.yml | 27 +++++++++ .github/workflows/fuzz.yml | 9 ++- .github/workflows/lint.yml | 7 ++- .github/workflows/nodejs.yml | 2 +- build/Dockerfile | 2 +- 8 files changed, 135 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/dependency-review.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml index dfa7fa6cba8..18b9fbf0503 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -11,3 +11,13 @@ updates: schedule: interval: "weekly" open-pull-requests-limit: 10 + + - package-ecosystem: docker + directory: /build + schedule: + interval: daily + + - package-ecosystem: pip + directory: /test/wpt/tests/resources/test + schedule: + interval: daily diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index cb939e084ca..ccc9aee11e7 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -3,18 +3,21 @@ on: - push - pull_request +permissions: + contents: read + jobs: benchmark_current: name: benchmark current runs-on: ubuntu-latest steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 with: persist-credentials: false ref: ${{ github.base_ref }} - name: Setup Node - uses: actions/setup-node@v3 + uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0 with: node-version: lts/* - name: Install Modules @@ -27,11 +30,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 with: persist-credentials: false - name: Setup Node - uses: actions/setup-node@v3 + uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0 with: node-version: lts/* - name: Install Modules diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000000..2537953d4a6 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,78 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: ["main"] + pull_request: + # The branches below must be a subset of the branches above + branches: ["main"] + schedule: + - cron: "0 0 * * 1" + +permissions: + contents: read + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: ["javascript", "python", "typescript"] + # CodeQL supports [ $supported-codeql-languages ] + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Harden Runner + uses: step-security/harden-runner@128a63446a954579617e875aaab7d2978154e969 # v2.4.0 + with: + egress-policy: audit + + - name: Checkout repository + uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@29b1f65c5e92e24fe6b6647da1eaabe529cec70f # v2.3.3 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@29b1f65c5e92e24fe6b6647da1eaabe529cec70f # v2.3.3 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@29b1f65c5e92e24fe6b6647da1eaabe529cec70f # v2.3.3 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 00000000000..ebe6e77da8b --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,27 @@ +# Dependency Review Action +# +# This Action will scan dependency manifest files that change as part of a Pull Request, +# surfacing known-vulnerable versions of the packages declared or updated in the PR. +# Once installed, if the workflow run is marked as required, +# PRs introducing known-vulnerable packages will be blocked from merging. +# +# Source repository: https://github.com/actions/dependency-review-action +name: 'Dependency Review' +on: [pull_request] + +permissions: + contents: read + +jobs: + dependency-review: + runs-on: ubuntu-latest + steps: + - name: Harden Runner + uses: step-security/harden-runner@128a63446a954579617e875aaab7d2978154e969 # v2.4.0 + with: + egress-policy: audit + + - name: 'Checkout Repository' + uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + - name: 'Dependency Review' + uses: actions/dependency-review-action@0efb1d1d84fc9633afcdaad14c485cbbc90ef46c # v2.5.1 diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index db963acd72b..9119601908f 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -2,18 +2,21 @@ name: Fuzzing on: [push, pull_request] +permissions: + contents: read + jobs: fuzzing: name: Fuzz runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 with: persist-credentials: false - name: Setup Node - uses: actions/setup-node@v3 + uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0 with: node-version: lts/* @@ -26,7 +29,7 @@ jobs: run: | npm run fuzz - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 if: ${{ failure() }} with: name: undici-fuzz-results-${{ github.sha }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 5ee9c3f5d1e..25e4ccad312 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,13 +1,16 @@ name: Lint on: [push, pull_request] +permissions: + contents: read + jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 with: persist-credentials: false - - uses: actions/setup-node@v3 + - uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0 with: node-version: lts/* - run: npm install diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 984d7be3b47..5de6c82b737 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -32,6 +32,6 @@ jobs: pull-requests: write contents: write steps: - - uses: fastify/github-action-merge-dependabot@v3 + - uses: fastify/github-action-merge-dependabot@415548af92cebc9895bff7b30e3e89d8970a2576 # v3.7.0 with: github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/build/Dockerfile b/build/Dockerfile index ff3d287e5a2..759c7f1ad59 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -1,4 +1,4 @@ -FROM node:18-alpine +FROM node:18-alpine@sha256:1ccc70acda680aa4ba47f53e7c40b2d4d6892de74817128e0662d32647dd7f4d ARG UID=1000 ARG GID=1000 From fe174cb5b12392d7e1d5578b61896e5646afaf91 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 May 2023 01:57:39 +0000 Subject: [PATCH 071/259] build(deps): bump actions/upload-artifact from 3.1.0 to 3.1.2 (#2135) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3.1.0 to 3.1.2. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v3.1.0...0b7f8abb1508181956e8e162db84b466c27e18ce) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/scorecard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 1f22fd9c34c..1d097df5009 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -43,7 +43,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" - uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # v3.1.0 + uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 with: name: SARIF file path: results.sarif From 4592bb7f627b9d50685e3b7f42dd09bb25e57018 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 May 2023 01:58:10 +0000 Subject: [PATCH 072/259] build(deps): bump actions/dependency-review-action from 2.5.1 to 3.0.4 (#2133) Bumps [actions/dependency-review-action](https://github.com/actions/dependency-review-action) from 2.5.1 to 3.0.4. - [Release notes](https://github.com/actions/dependency-review-action/releases) - [Commits](https://github.com/actions/dependency-review-action/compare/0efb1d1d84fc9633afcdaad14c485cbbc90ef46c...f46c48ed6d4f1227fb2d9ea62bf6bcbed315589e) --- updated-dependencies: - dependency-name: actions/dependency-review-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/dependency-review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index ebe6e77da8b..f3ab3392afd 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -24,4 +24,4 @@ jobs: - name: 'Checkout Repository' uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 - name: 'Dependency Review' - uses: actions/dependency-review-action@0efb1d1d84fc9633afcdaad14c485cbbc90ef46c # v2.5.1 + uses: actions/dependency-review-action@f46c48ed6d4f1227fb2d9ea62bf6bcbed315589e # v3.0.4 From 49fd30d96995e10a7e37062a41b2b0c236845d4b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 May 2023 01:58:23 +0000 Subject: [PATCH 073/259] build(deps): bump node from 18-alpine to 20-alpine in /build (#2131) Bumps node from 18-alpine to 20-alpine. --- updated-dependencies: - dependency-name: node dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/Dockerfile b/build/Dockerfile index 759c7f1ad59..5438b73690c 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -1,4 +1,4 @@ -FROM node:18-alpine@sha256:1ccc70acda680aa4ba47f53e7c40b2d4d6892de74817128e0662d32647dd7f4d +FROM node:20-alpine@sha256:4559bc033338938e54d0a3c2f0d7c3ad7d1d13c28c4c405b85c6b3a26f4ce5f7 ARG UID=1000 ARG GID=1000 From bdaf3e42d60bee16c4539de4497ab5fc5d61a027 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 May 2023 02:01:01 +0000 Subject: [PATCH 074/259] build(deps): bump pkgjs/action from 0.1.6 to 0.1.7 (#2136) Bumps [pkgjs/action](https://github.com/pkgjs/action) from 0.1.6 to 0.1.7. - [Release notes](https://github.com/pkgjs/action/releases) - [Commits](https://github.com/pkgjs/action/compare/v0.1.6...v0.1.7) --- updated-dependencies: - dependency-name: pkgjs/action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/nodejs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 5de6c82b737..9b2623bf2fc 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -9,7 +9,7 @@ on: [push, pull_request] jobs: build: name: Test - uses: pkgjs/action/.github/workflows/node-test.yaml@v0.1.6 + uses: pkgjs/action/.github/workflows/node-test.yaml@v0.1.7 with: runs-on: ubuntu-latest, windows-latest test-command: npm run coverage:ci From c5f3bdda4214b172e578b6adb933b1b4a7858195 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 May 2023 02:14:34 +0000 Subject: [PATCH 075/259] build(deps): bump actions/checkout from 3.1.0 to 3.5.2 (#2132) Bumps [actions/checkout](https://github.com/actions/checkout) from 3.1.0 to 3.5.2. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3.1.0...8e5e7e5ab8b370d6c329ec480221332ada57f0ab) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/scorecard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 1d097df5009..adfd0076908 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -29,7 +29,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v3.1.0 + uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 with: persist-credentials: false From 4d73c90cf385e1c3ef148db1b692dfde6b93bada Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 May 2023 23:09:58 +0000 Subject: [PATCH 076/259] build(deps-dev): bump jsdom from 21.1.2 to 22.1.0 (#2142) Bumps [jsdom](https://github.com/jsdom/jsdom) from 21.1.2 to 22.1.0. - [Release notes](https://github.com/jsdom/jsdom/releases) - [Changelog](https://github.com/jsdom/jsdom/blob/master/Changelog.md) - [Commits](https://github.com/jsdom/jsdom/compare/21.1.2...22.1.0) --- updated-dependencies: - dependency-name: jsdom dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 49b657fded2..d5e312a6828 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "husky": "^8.0.1", "import-fresh": "^3.3.0", "jest": "^29.0.2", - "jsdom": "^21.1.0", + "jsdom": "^22.1.0", "jsfuzz": "^1.0.15", "mocha": "^10.0.0", "p-timeout": "^3.2.0", From 80471053134529c61df8e4b1b1f0b544c4df1656 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Jun 2023 23:08:08 +0000 Subject: [PATCH 077/259] build(deps): bump fastify/github-action-merge-dependabot (#2148) Bumps [fastify/github-action-merge-dependabot](https://github.com/fastify/github-action-merge-dependabot) from 3.7.0 to 3.8.0. - [Release notes](https://github.com/fastify/github-action-merge-dependabot/releases) - [Commits](https://github.com/fastify/github-action-merge-dependabot/compare/415548af92cebc9895bff7b30e3e89d8970a2576...f4fba1d411acf25f03affabc4ac209291cb9d6da) --- updated-dependencies: - dependency-name: fastify/github-action-merge-dependabot dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/nodejs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 9b2623bf2fc..0afd197845d 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -32,6 +32,6 @@ jobs: pull-requests: write contents: write steps: - - uses: fastify/github-action-merge-dependabot@415548af92cebc9895bff7b30e3e89d8970a2576 # v3.7.0 + - uses: fastify/github-action-merge-dependabot@f4fba1d411acf25f03affabc4ac209291cb9d6da # v3.8.0 with: github-token: ${{ secrets.GITHUB_TOKEN }} From a3e0fdc44e44dfcee16f1c032e8aba148c75c199 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy Date: Fri, 2 Jun 2023 19:52:47 +0200 Subject: [PATCH 078/259] fix(pr): use correct pr template file (#2141) --- .../template.md => PULL_REQUEST_TEMPLATE.md} | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) rename .github/{PULL_REQUEST_TEMPLATE/template.md => PULL_REQUEST_TEMPLATE.md} (97%) diff --git a/.github/PULL_REQUEST_TEMPLATE/template.md b/.github/PULL_REQUEST_TEMPLATE.md similarity index 97% rename from .github/PULL_REQUEST_TEMPLATE/template.md rename to .github/PULL_REQUEST_TEMPLATE.md index cb295de6da6..2620ffbc504 100644 --- a/.github/PULL_REQUEST_TEMPLATE/template.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -39,7 +39,8 @@ deprecations (removed features) here --> ## Status -KEY: S = Skipped, x = complete + + - [ ] I have read and agreed to the [Developer's Certificate of Origin][cert] - [ ] Tested From 2032e8997695f569c3bfdf975cdd8c7289478f83 Mon Sep 17 00:00:00 2001 From: George MacKerron Date: Wed, 7 Jun 2023 04:00:49 +0100 Subject: [PATCH 079/259] Additional WebSocket send tests to cover all payload size categories (#2149) * Fixed bug in 16-bit frame length when buffer is a subarray I was just bitten by this issue. Because buffer could be a subarray, the `byteOffset` and `byteLength` arguments to `new DataView()` are required, otherwise the length value may be written at the wrong offset. Alternatively, it looks like it would be simpler (and it also works) to replace the patched line with `buffer.writeUInt16BE(bodyLength, 2)`. * Switched 16-bit length-writing fix to use Buffer.writeUInt16 instead of DataView.setUint16, and added a test for the fix * Added WebSocket send tests for the two previously-untested payload size categories --- test/websocket/send.js | 64 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/test/websocket/send.js b/test/websocket/send.js index 4ba2a5b7dfb..ac295fded73 100644 --- a/test/websocket/send.js +++ b/test/websocket/send.js @@ -5,7 +5,11 @@ const { WebSocketServer } = require('ws') const { Blob } = require('buffer') const { WebSocket } = require('../..') -test('Sending > 2^16 bytes', (t) => { +// the following three tests exercise different code paths because of the three +// different ways a payload length may be specified in a WebSocket frame +// (https://datatracker.ietf.org/doc/html/rfc6455#section-5.2) + +test('Sending >= 2^16 bytes', (t) => { t.plan(3) const server = new WebSocketServer({ port: 0 }) @@ -34,6 +38,64 @@ test('Sending > 2^16 bytes', (t) => { }) }) +test('Sending >= 126, < 2^16 bytes', (t) => { + t.plan(3) + + const server = new WebSocketServer({ port: 0 }) + + server.on('connection', (ws) => { + ws.on('message', (m, isBinary) => { + ws.send(m, { binary: isBinary }) + }) + }) + + const payload = Buffer.allocUnsafe(126).fill('Hello') + + const ws = new WebSocket(`ws://localhost:${server.address().port}`) + + ws.addEventListener('open', () => { + ws.send(payload) + }) + + ws.addEventListener('message', async ({ data }) => { + t.type(data, Blob) + t.equal(data.size, payload.length) + t.same(Buffer.from(await data.arrayBuffer()), payload) + + ws.close() + server.close() + }) +}) + +test('Sending < 126 bytes', (t) => { + t.plan(3) + + const server = new WebSocketServer({ port: 0 }) + + server.on('connection', (ws) => { + ws.on('message', (m, isBinary) => { + ws.send(m, { binary: isBinary }) + }) + }) + + const payload = Buffer.allocUnsafe(125).fill('Hello') + + const ws = new WebSocket(`ws://localhost:${server.address().port}`) + + ws.addEventListener('open', () => { + ws.send(payload) + }) + + ws.addEventListener('message', async ({ data }) => { + t.type(data, Blob) + t.equal(data.size, payload.length) + t.same(Buffer.from(await data.arrayBuffer()), payload) + + ws.close() + server.close() + }) +}) + test('Sending data after close', (t) => { t.plan(2) From c415fbbb59e2b898c5db6a681265cf3da865d02c Mon Sep 17 00:00:00 2001 From: Georgii Rychko Date: Wed, 14 Jun 2023 05:33:47 +0300 Subject: [PATCH 080/259] fix: reverse decompression order of "Content-Encoding" encodings (fixes #2158) (#2159) Co-authored-by: rychkog-ma --- lib/fetch/index.js | 2 +- test/fetch/encoding.js | 33 +++++++++++++++++++++++++++++---- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/lib/fetch/index.js b/lib/fetch/index.js index 90683a8febb..7388da51ffc 100644 --- a/lib/fetch/index.js +++ b/lib/fetch/index.js @@ -1986,7 +1986,7 @@ async function httpNetworkFetch ( if (key.toLowerCase() === 'content-encoding') { // https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1 // "All content-coding values are case-insensitive..." - codings = val.toLowerCase().split(',').map((x) => x.trim()) + codings = val.toLowerCase().split(',').map((x) => x.trim()).reverse() } else if (key.toLowerCase() === 'location') { location = val } diff --git a/test/fetch/encoding.js b/test/fetch/encoding.js index 63d22f8b5e4..5817924521a 100644 --- a/test/fetch/encoding.js +++ b/test/fetch/encoding.js @@ -4,7 +4,7 @@ const { test } = require('tap') const { createServer } = require('http') const { once } = require('events') const { fetch } = require('../..') -const { createBrotliCompress, createGzip } = require('zlib') +const { createBrotliCompress, createGzip, createDeflate } = require('zlib') test('content-encoding header is case-iNsENsITIve', async (t) => { const contentCodings = 'GZiP, bR' @@ -17,10 +17,10 @@ test('content-encoding header is case-iNsENsITIve', async (t) => { res.setHeader('Content-Encoding', contentCodings) res.setHeader('Content-Type', 'text/plain') - brotli.pipe(gzip).pipe(res) + gzip.pipe(brotli).pipe(res) - brotli.write(text) - brotli.end() + gzip.write(text) + gzip.end() }).listen(0) t.teardown(server.close.bind(server)) @@ -31,3 +31,28 @@ test('content-encoding header is case-iNsENsITIve', async (t) => { t.equal(await response.text(), text) t.equal(response.headers.get('content-encoding'), contentCodings) }) + +test('response decompression according to content-encoding should be handled in a correct order', async (t) => { + const contentCodings = 'deflate, gzip' + const text = 'Hello, World!' + + const server = createServer((req, res) => { + const gzip = createGzip() + const deflate = createDeflate() + + res.setHeader('Content-Encoding', contentCodings) + res.setHeader('Content-Type', 'text/plain') + + deflate.pipe(gzip).pipe(res) + + deflate.write(text) + deflate.end() + }).listen(0) + + t.teardown(server.close.bind(server)) + await once(server, 'listening') + + const response = await fetch(`http://localhost:${server.address().port}`) + + t.equal(await response.text(), text) +}) From 593c56c2553ccd4fa651bbb288abd48a15f51a36 Mon Sep 17 00:00:00 2001 From: Khafra Date: Mon, 19 Jun 2023 22:01:32 -0400 Subject: [PATCH 081/259] fix: keep running WPTs if a test times out (#2165) --- test/wpt/runner/runner.mjs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/test/wpt/runner/runner.mjs b/test/wpt/runner/runner.mjs index ec236454e74..0b66f58466b 100644 --- a/test/wpt/runner/runner.mjs +++ b/test/wpt/runner/runner.mjs @@ -228,19 +228,15 @@ export class WPTRunner extends EventEmitter { if (variant) console.log('Variant:', variant) console.log(`Test took ${(performance.now() - start).toFixed(2)}ms`) console.log('='.repeat(96)) - + } catch (e) { + console.log(`${test} timed out after ${timeout}ms`) + } finally { if (result?.subtests.length > 0) { writeFileSync(this.#reportPath, JSON.stringify(report)) } finishedFiles++ activeWorkers.delete(worker) - } catch (e) { - console.log(`${test} timed out after ${timeout}ms`) - queueMicrotask(() => { - throw e - }) - return } } } From 23e62c4c0ac992be4fcd5a95151f9edeb76d03cd Mon Sep 17 00:00:00 2001 From: Michael Dawson Date: Fri, 23 Jun 2023 06:42:26 -0400 Subject: [PATCH 082/259] feat: add build environment info (#2168) --- CONTRIBUTING.md | 4 ++++ build/wasm.js | 12 ++++++++++-- lib/llhttp/llhttp-wasm.js | 2 +- lib/llhttp/llhttp.wasm | Bin 56001 -> 55466 bytes lib/llhttp/llhttp_simd-wasm.js | 2 +- lib/llhttp/llhttp_simd.wasm | Bin 55985 -> 55450 bytes lib/llhttp/wasm_build_env.txt | 32 ++++++++++++++++++++++++++++++++ 7 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 lib/llhttp/wasm_build_env.txt diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2d67ba7f9e4..3a7f3ffd14f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -78,6 +78,10 @@ cd npm run build:wasm ``` +#### Commit the contents of lib/llhttp + +Create a commit which includes all of the updated files in lib/llhttp. + ### Update `WPTs` diff --git a/build/wasm.js b/build/wasm.js index 4f0724bd7d1..fd90ac26fc9 100644 --- a/build/wasm.js +++ b/build/wasm.js @@ -35,6 +35,14 @@ if (process.argv[2] === '--docker') { process.exit(0) } +// Gather information about the tools used for the build +const buildInfo = execSync('apk info -v').toString() +if (!buildInfo.includes('wasi-sdk')) { + console.log('Failed to generate build environment information') + process.exit(-1) +} +writeFileSync(join(WASM_OUT, 'wasm_build_env.txt'), buildInfo) + // Build wasm binary execSync(`clang \ --sysroot=/usr/share/wasi-sysroot \ @@ -60,7 +68,7 @@ execSync(`clang \ const base64Wasm = readFileSync(join(WASM_OUT, 'llhttp.wasm')).toString('base64') writeFileSync( join(WASM_OUT, 'llhttp-wasm.js'), - `module.exports = "${base64Wasm}";\n` + `module.exports = '${base64Wasm}'\n` ) // Build wasm simd binary @@ -89,5 +97,5 @@ execSync(`clang \ const base64WasmSimd = readFileSync(join(WASM_OUT, 'llhttp_simd.wasm')).toString('base64') writeFileSync( join(WASM_OUT, 'llhttp_simd-wasm.js'), - `module.exports = "${base64WasmSimd}";\n` + `module.exports = '${base64WasmSimd}'\n` ) diff --git a/lib/llhttp/llhttp-wasm.js b/lib/llhttp/llhttp-wasm.js index e176ce2cf51..ad4682c364d 100644 --- a/lib/llhttp/llhttp-wasm.js +++ b/lib/llhttp/llhttp-wasm.js @@ -1 +1 @@ -module.exports = 'AGFzbQEAAAABMAhgAX8Bf2ADf39/AX9gBH9/f38Bf2AAAGADf39/AGABfwBgAn9/AGAGf39/f39/AALLAQgDZW52GHdhc21fb25faGVhZGVyc19jb21wbGV0ZQACA2VudhV3YXNtX29uX21lc3NhZ2VfYmVnaW4AAANlbnYLd2FzbV9vbl91cmwAAQNlbnYOd2FzbV9vbl9zdGF0dXMAAQNlbnYUd2FzbV9vbl9oZWFkZXJfZmllbGQAAQNlbnYUd2FzbV9vbl9oZWFkZXJfdmFsdWUAAQNlbnYMd2FzbV9vbl9ib2R5AAEDZW52GHdhc21fb25fbWVzc2FnZV9jb21wbGV0ZQAAA0ZFAwMEAAAFAAAAAAAABQEFAAUFBQAABgAAAAAGBgYGAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAAABAQcAAAUFAAMBBAUBcAESEgUDAQACBggBfwFBgNQECwfRBSIGbWVtb3J5AgALX2luaXRpYWxpemUACRlfX2luZGlyZWN0X2Z1bmN0aW9uX3RhYmxlAQALbGxodHRwX2luaXQAChhsbGh0dHBfc2hvdWxkX2tlZXBfYWxpdmUAQQxsbGh0dHBfYWxsb2MADAZtYWxsb2MARgtsbGh0dHBfZnJlZQANBGZyZWUASA9sbGh0dHBfZ2V0X3R5cGUADhVsbGh0dHBfZ2V0X2h0dHBfbWFqb3IADxVsbGh0dHBfZ2V0X2h0dHBfbWlub3IAEBFsbGh0dHBfZ2V0X21ldGhvZAARFmxsaHR0cF9nZXRfc3RhdHVzX2NvZGUAEhJsbGh0dHBfZ2V0X3VwZ3JhZGUAEwxsbGh0dHBfcmVzZXQAFA5sbGh0dHBfZXhlY3V0ZQAVFGxsaHR0cF9zZXR0aW5nc19pbml0ABYNbGxodHRwX2ZpbmlzaAAXDGxsaHR0cF9wYXVzZQAYDWxsaHR0cF9yZXN1bWUAGRtsbGh0dHBfcmVzdW1lX2FmdGVyX3VwZ3JhZGUAGhBsbGh0dHBfZ2V0X2Vycm5vABsXbGxodHRwX2dldF9lcnJvcl9yZWFzb24AHBdsbGh0dHBfc2V0X2Vycm9yX3JlYXNvbgAdFGxsaHR0cF9nZXRfZXJyb3JfcG9zAB4RbGxodHRwX2Vycm5vX25hbWUAHxJsbGh0dHBfbWV0aG9kX25hbWUAIBJs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EGpCADcDACAAQQhqQgA3AwAgAEHdATYCHEEAC3sBAX8CQCAAKAIMIgMNAAJAIAAoAgRFDQAgACABNgIECwJAIAAgASACEMSAgIAAIgMNACAAKAIMDwsgACADNgIcQQAhAyAAKAIEIgFFDQAgACABIAIgACgCCBGBgICAAAAiAUUNACAAIAI2AhQgACABNgIMIAEhAwsgAwvc9wEDKH8DfgV/I4CAgIAAQRBrIgMkgICAgAAgASEEIAEhBSABIQYgASEHIAEhCCABIQkgASEKIAEhCyABIQwgASENIAEhDiABIQ8gASEQIAEhESABIRIgASETIAEhFCABIRUgASEWIAEhFyABIRggASEZIAEhGiABIRsgASEcIAEhHSABIR4gASEfIAEhICABISEgASEiIAEhIyABISQgASElIAEhJiABIScgASEoIAEhKQJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQCAAKAIcIipBf2oO3QHaAQHZAQIDBAUGBwgJCgsMDQ7YAQ8Q1wEREtYBExQVFhcYGRob4AHfARwdHtUBHyAhIiMkJdQBJicoKSorLNMB0gEtLtEB0AEvMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUbbAUdISUrPAc4B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AQtBwQEhKgsDQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAgKg7HAQABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHh8gISMlKD9AQURFRkdISUpLTE1PUFFSU+MDV1lbXF1gYmVmZ2hpamtsbW9wcXJzdHV2d3h5ent8fX6AAYIBhQGGAYcBiQGLAYwBjQGOAY8BkAGRAZQBlQGWAZcBmAGZAZoBmwGcAZ0BngGfAaABoQGiAaMBpAGlAaYBpwGoAakBqgGrAawBrQGuAa8BsAGxAbIBswG0AbUBtgG3AbgBuQG6AbsBvAG9Ab4BvwHAAcEBwgHDAcQBxQHGAccByAHJAcoBywHMAc0BzgHPAdAB0QHSAdMB1AHVAdYB1wHYAdkB2gHbAdwB3QHeAeAB4QHiAeMB5AHlAeYB5wHoAekB6gHrAewB7QHuAe8B8AHxAfIB8wGZAqQCsgKEA4QDCyABIgQgAkcN8wFB3QEhKgyGBAsgASIqIAJHDd0BQcMBISoMhQQLIAEiASACRw2QAUH3ACEqDIQECyABIgEgAkcNhgFB7wAhKgyDBAsgASIBIAJHDX9B6gAhKgyCBAsgASIBIAJHDXtB6AAhKgyBBAsgASIBIAJHDXhB5gAhKgyABAsgASIBIAJHDRpBGCEqDP8DCyABIgEgAkcNFEESISoM/gMLIAEiASACRw1ZQcUAISoM/QMLIAEiASACRw1KQT8hKgz8AwsgASIBIAJHDUhBPCEqDPsDCyABIgEgAkcNQUExISoM+gMLIAAtAC5BAUYN8gMMhwILIAAgASIBIAIQwICAgABBAUcN5gEgAEIANwMgDOcBCyAAIAEiASACELSAgIAAIioN5wEgASEBDPsCCwJAIAEiASACRw0AQQYhKgz3AwsgACABQQFqIgEgAhC7gICAACIqDegBIAEhAQwxCyAAQgA3AyBBEiEqDNwDCyABIiogAkcNK0EdISoM9AMLAkAgASIBIAJGDQAgAUEBaiEBQRAhKgzbAwtBByEqDPMDCyAAQgAgACkDICIrIAIgASIqa60iLH0iLSAtICtWGzcDICArICxWIi5FDeUBQQghKgzyAwsCQCABIgEgAkYNACAAQYmAgIAANgIIIAAgATYCBCABIQFBFCEqDNkDC0EJISoM8QMLIAEhASAAKQMgUA3kASABIQEM+AILAkAgASIBIAJHDQBBCyEqDPADCyAAIAFBAWoiASACELaAgIAAIioN5QEgASEBDPgCCyAAIAEiASACELiAgIAAIioN5QEgASEBDPgCCyAAIAEiASACELiAgIAAIioN5gEgASEBDA0LIAAgASIBIAIQuoCAgAAiKg3nASABIQEM9gILAkAgASIBIAJHDQBBDyEqDOwDCyABLQAAIipBO0YNCCAqQQ1HDegBIAFBAWohAQz1AgsgACABIgEgAhC6gICAACIqDegBIAEhAQz4AgsDQAJAIAEtAABB8LWAgABqLQAAIipBAUYNACAqQQJHDesBIAAoAgQhKiAAQQA2AgQgACAqIAFBAWoiARC5gICAACIqDeoBIAEhAQz6AgsgAUEBaiIBIAJHDQALQRIhKgzpAwsgACABIgEgAhC6gICAACIqDekBIAEhAQwKCyABIgEgAkcNBkEbISoM5wMLAkAgASIBIAJHDQBBFiEqDOcDCyAAQYqAgIAANgIIIAAgATYCBCAAIAEgAhC4gICAACIqDeoBIAEhAUEgISoMzQMLAkAgASIBIAJGDQADQAJAIAEtAABB8LeAgABqLQAAIipBAkYNAAJAICpBf2oOBOUB7AEA6wHsAQsgAUEBaiEBQQghKgzPAwsgAUEBaiIBIAJHDQALQRUhKgzmAwtBFSEqDOUDCwNAAkAgAS0AAEHwuYCAAGotAAAiKkECRg0AICpBf2oOBN4B7AHgAesB7AELIAFBAWoiASACRw0AC0EYISoM5AMLAkAgASIBIAJGDQAgAEGLgICAADYCCCAAIAE2AgQgASEBQQchKgzLAwtBGSEqDOMDCyABQQFqIQEMAgsCQCABIi4gAkcNAEEaISoM4gMLIC4hAQJAIC4tAABBc2oOFOMC9AL0AvQC9AL0AvQC9AL0AvQC9AL0AvQC9AL0AvQC9AL0AvQCAPQCC0EAISogAEEANgIcIABBr4uAgAA2AhAgAEECNgIMIAAgLkEBajYCFAzhAwsCQCABLQAAIipBO0YNACAqQQ1HDegBIAFBAWohAQzrAgsgAUEBaiEBC0EiISoMxgMLAkAgASIqIAJHDQBBHCEqDN8DC0IAISsgKiEBICotAABBUGoON+cB5gEBAgMEBQYHCAAAAAAAAAAJCgsMDQ4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8QERITFAALQR4hKgzEAwtCAiErDOUBC0IDISsM5AELQgQhKwzjAQtCBSErDOIBC0IGISsM4QELQgchKwzgAQtCCCErDN8BC0IJISsM3gELQgohKwzdAQtCCyErDNwBC0IMISsM2wELQg0hKwzaAQtCDiErDNkBC0IPISsM2AELQgohKwzXAQtCCyErDNYBC0IMISsM1QELQg0hKwzUAQtCDiErDNMBC0IPISsM0gELQgAhKwJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAgKi0AAEFQag435QHkAQABAgMEBQYH5gHmAeYB5gHmAeYB5gEICQoLDA3mAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYBDg8QERIT5gELQgIhKwzkAQtCAyErDOMBC0IEISsM4gELQgUhKwzhAQtCBiErDOABC0IHISsM3wELQgghKwzeAQtCCSErDN0BC0IKISsM3AELQgshKwzbAQtCDCErDNoBC0INISsM2QELQg4hKwzYAQtCDyErDNcBC0IKISsM1gELQgshKwzVAQtCDCErDNQBC0INISsM0wELQg4hKwzSAQtCDyErDNEBCyAAQgAgACkDICIrIAIgASIqa60iLH0iLSAtICtWGzcDICArICxWIi5FDdIBQR8hKgzHAwsCQCABIgEgAkYNACAAQYmAgIAANgIIIAAgATYCBCABIQFBJCEqDK4DC0EgISoMxgMLIAAgASIqIAIQvoCAgABBf2oOBbYBAMsCAdEB0gELQREhKgyrAwsgAEEBOgAvICohAQzCAwsgASIBIAJHDdIBQSQhKgzCAwsgASInIAJHDR5BxgAhKgzBAwsgACABIgEgAhCygICAACIqDdQBIAEhAQy1AQsgASIqIAJHDSZB0AAhKgy/AwsCQCABIgEgAkcNAEEoISoMvwMLIABBADYCBCAAQYyAgIAANgIIIAAgASABELGAgIAAIioN0wEgASEBDNgBCwJAIAEiKiACRw0AQSkhKgy+AwsgKi0AACIBQSBGDRQgAUEJRw3TASAqQQFqIQEMFQsCQCABIgEgAkYNACABQQFq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AUEBaiEBIARBAWoiBCACRw0ACyAAICo2AgBBgwEhKgzoAgtBACEqIABBADYCACAuIDJrQQNqIQEMygELAkAgASIqIAJHDQBBhAEhKgznAgsgAiAqayAAKAIAIi5qITIgKiEEIC4hAQJAA0AgBC0AACABQYPPgIAAai0AAEcNzAEgAUEERg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAyNgIAQYQBISoM5wILIABBADYCACAqIC5rQQVqIQFBIyEqDMkBCwJAIAEiBCACRw0AQYUBISoM5gILAkACQCAELQAAQbR/ag4IAMwBzAHMAcwBzAHMAQHMAQsgBEEBaiEBQe8AISoMzQILIARBAWohAUHwACEqDMwCCwJAIAEiBCACRw0AQYYBISoM5QILIAQtAABBxQBHDckBIARBAWohAQyKAgsCQCABIiogAkcNAEGHASEqDOQCCyACICprIAAoAgAiLmohMiAqIQQgLiEBAkADQCAELQAAIAFBiM+AgABqLQAARw3JASABQQNGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIDI2AgBBhwEhKgzkAgsgAEEANgIAICogLmtBBGohAUEtISoMxgELAkAgASIqIAJHDQBBiAEhKgzjAgsgAiAqayAAKAIAIi5qITIgKiEEIC4hAQJAA0AgBC0AACABQdDPgIAAai0AAEcNyAEgAUEIRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAyNgIAQYgBISoM4wILIABBADYCACAqIC5rQQlqIQFBKSEqDMUBCwJAIAEiASACRw0AQYkBISoM4gILQQEhKiABLQAAQd8ARw3EASABQQFqIQEMiAILAkAgASIqIAJHDQBBigEhKgzhAgsgAiAqayAAKAIAIi5qITIgKiEEIC4hAQNAIAQtAAAgAUGMz4CAAGotAABHDcUBIAFBAUYNtwIgAUEBaiEBIARBAWoiBCACRw0ACyAAIDI2AgBBigEhKgzgAgsCQCABIiogAkcNAEGLASEqDOACCyACICprIAAoAgAiLmohMiAqIQQgLiEBAkADQCAELQAAIAFBjs+AgABqLQAARw3FASABQQJGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIDI2AgBBiwEhKgzgAgsgAEEANgIAICogLmtBA2ohAUECISoMwgELAkAgASIqIAJHDQBB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AABBt39qDgcBrgGuAa4BrgGuAQCuAQsgBEEBaiEEQY4BISoMrwILIARBAWohAUEiISoMqgELAkAgDiACRw0AQaQBISoMxwILIAIgDmsgACgCACIqaiEuIA4hBCAqIQECQANAIAQtAAAgAUHAz4CAAGotAABHDawBIAFBAUYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgLjYCAEGkASEqDMcCCyAAQQA2AgAgDiAqa0ECaiEBQR0hKgypAQsCQCAEIAJHDQBBpQEhKgzGAgsCQAJAIAQtAABBrn9qDgMArAEBrAELIARBAWohDkGQASEqDK0CCyAEQQFqIQFBBCEqDKgBCwJAIAQgAkcNAEGmASEqDMUCCwJAAkACQAJAAkAgBC0AAEG/f2oOFQCuAa4BrgGuAa4BrgGuAa4BrgGuAQGuAa4BAq4BrgEDrgGuAQSuAQsgBEEBaiEEQYgBISoMrwILIARBAWohCkGJASEqDK4CCyAEQQFqIQtBigEhKgytAgsgBEEBaiEEQY8BISoMrAILIARBAWohBEGRASEqDKsCCwJAIA8gAkcNAEGnASEqDMQCCyACIA9rIAAoAgAiKmohLiAPIQQgKiEBAkADQCAELQAAIAFB7c+AgABqLQAARw2pASABQQJGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIC42AgBBpwEhKgzEAgsgAEEANgIAIA8gKmtBA2ohAUERISoMpgELAkAgECACRw0AQagBISoMwwILIAIgEGsgACgCACIqaiEuIBAhBCAqIQECQANAIAQtAAAgAUHCz4CAAGotAABHDagBIAFBAkYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgLjYCAEGoASEqDMMCCyAAQQA2AgAgECAqa0EDaiEBQSwhKgylAQsCQCARIAJHDQBBqQEhKgzCAgsgAiARayAAKAIAIipqIS4gESEEICohAQJAA0AgBC0AACABQcXPgIAAai0AAEcNpwEgAUEERg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAuNgIAQakBISoMwgILIABBADYCACARICprQQVqIQFBKyEqDKQBCwJAIBIgAkcNAEGqASEqDMECCyACIBJrIAAoAgAiKmohLiASIQQgKiEBAkADQCAELQAAIAFBys+AgABqLQAARw2mASABQQJGDQEgAUEBaiEB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8s+AgABqLQAARw2WASABQQFGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIC42AgBBugEhKgyxAgsgAEEANgIAIBsgKmtBAmohAUEPISoMkwELAkAgBCACRw0AQbsBISoMsAILAkACQCAELQAAQbd/ag4HAJYBlgGWAZYBlgEBlgELIARBAWohGkGlASEqDJcCCyAEQQFqIRtBpgEhKgyWAgsCQCAcIAJHDQBBvAEhKgyvAgsgAiAcayAAKAIAIipqIS4gHCEEICohAQJAA0AgBC0AACABQfTPgIAAai0AAEcNlAEgAUEHRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAuNgIAQbwBISoMrwILIABBADYCACAcICprQQhqIQFBGyEqDJEBCwJAIAQgAkcNAEG9ASEqDK4CCwJAAkACQCAELQAAQb5/ag4SAJUBlQGVAZUBlQGVAZUBlQGVAQGVAZUBlQGVAZUBlQEClQELIARBAWohGUGkASEqDJYCCyAEQQFqIQRBpwEhKgyVAgsgBEEBaiEcQagBISoMlAILAkAgBCACRw0AQb4BISoMrQILIAQtAABBzgBHDZEBIARBAWohBAzWAQsCQCAEIAJHDQBBvwEhKgysAgsCQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQCAELQAAQb9/ag4VAAECA6ABBAUGoAGgAaABBwgJCgugAQwNDg+gAQsgBEEBaiEBQegAISoMoQILIARBAWohAUHpACEqDKACCyAEQQFqIQFB7gAhKgyfAgsgBEEBaiEBQfIAISoMngILIARBAWohAUHzACEqDJ0CCyAEQQFqIQFB9gAhKgycAgsgBEEBaiEBQfcAISoMmwILIARBAWohAUH6ACEqDJoCCyAEQQFqIQRBgwEhKgyZAgsgBEEBaiEGQYQBISoMmAILIARBAWohB0GFASEqDJcCCyAEQQFqIQRBkgEhKgyWAgsgBEEBaiEEQZgBISoMlQILIARBAWohBEGgASEqDJQCCyAEQQFqIQRBowEhKgyTAgsgBEEBaiEEQaoBISoMkgILAkAgBCACRg0AIABBkICAgAA2AgggACAENgIEQasBISoMkgILQcABISoMqgILIAAgHSACEKqAgIAAIgENjwEgHSEBDF4LAkAgHiACRg0AIB5BAWohHQyRAQtBwgEhKgyoAgsD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NgIUIABB2o2AgAA2AhAgAEEUNgIMQQAhKgyNAgsgACgCBCEyIABBADYCBCAqICunaiIvIQEgACAyICogLyAuGyIqELWAgIAAIi5FDZMBIABBBzYCHCAAICo2AhQgACAuNgIMQQAhKgyMAgsgACAALwEwQYABcjsBMCABIQELQSohKgzxAQsgKkEVRg3ZASAAQQA2AhwgACABNgIUIABBg4yAgAA2AhAgAEETNgIMQQAhKgyJAgsgKkEVRg3XASAAQQA2AhwgACABNgIUIABBmo+AgAA2AhAgAEEiNgIMQQAhKgyIAgsgACgCBCEqIABBADYCBAJAIAAgKiABELeAgIAAIioNACABQQFqIQEMkwELIABBDDYCHCAAICo2AgwgACABQQFqNgIUQQAhKgyHAgsgKkEVRg3UASAAQQA2AhwgACABNgIUIABBmo+AgAA2AhAgAEEiNgIMQQAhKgyGAgsgACgCBCEqIABBADYCBAJAIAAgKiABELeAgIAAIioNACABQQFqIQEMkgELIABBDTYCHCAAICo2AgwgACABQQFqNgIUQQAhKgyFAgsgKkEVRg3RASAAQQA2AhwgACABNgIUIABBxoyAgAA2AhAgAEEjNgIMQQAhKgyEAgsgACgCBCEqIABBADYCBAJAIAAgKiABELmAgIAAIioNACABQQFqIQEMkQELIABBDjYCHCAAICo2AgwgACABQQFqNgIUQQAhKgyDAgsgAEEANgIcIAAgATYCFCAAQcCVgIAANgIQIABBAjYCDEEAISoMggILICpBFUYNzQEgAEEANgIcIAAgATYCFCAAQcaMgIAANgIQIABBIzYCDEEAISoMgQILIABBEDYCHCAAIAE2AhQgACAqNgIMQQAhKgyAAgsgACgCBCEEIABBADYCBAJAIAAgBCABELmAgIAAIgQNACABQQFqIQEM+AELIABBETYCHCAAIAQ2AgwgACABQQFqNgIUQQAhKgz/AQsgKkEVRg3JASAAQQA2AhwgACABNgIUIABBxoyAgAA2AhAgAEEjNgIMQQAhKgz+AQsgACgCBCEqIABBADYCBAJAIAAgKiABELmAgIAAIioNACABQQFqIQEMjgELIABBEzYCHCAAICo2AgwgACABQQFqNgIUQQAhKgz9AQsgACgCBCEEIABBADYCBAJAIAAgBCABELmAgIAAIgQNACAB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AkAgAC0AKUEjTw0AIAEhAQxcCyAAQQA2AhwgACABNgIUIABB04mAgAA2AhAgAEEINgIMQQAhKgy8AQsgAEEANgIAC0EAISogAEEANgIcIAAgATYCFCAAQZCzgIAANgIQIABBCDYCDAy6AQsgAEEANgIAIDIgL2tBA2ohAQJAIAAtAClBIUcNACABIQEMWQsgAEEANgIcIAAgATYCFCAAQZuKgIAANgIQIABBCDYCDEEAISoMuQELIABBADYCACAyIC9rQQRqIQECQCAALQApIipBXWpBC08NACABIQEMWAsCQCAqQQZLDQBBASAqdEHKAHFFDQAgASEBDFgLQQAhKiAAQQA2AhwgACABNgIUIABB94mAgAA2AhAgAEEINgIMDLgBCyAqQRVGDXUgAEEANgIcIAAgATYCFCAAQbmNgIAANgIQIABBGjYCDEEAISoMtwELIAAoAgQhKiAAQQA2AgQCQCAAICogARCngICAACIqDQAgASEBDFcLIABB5QA2AhwgACABNgIUIAAgKjYCDEEAISoMtgELIAAoAgQhKiAAQQA2AgQCQCAAICogARCngICAACIqDQAgASEBDE8LIABB0gA2AhwgACABNgIUIAAgKjYCDEEAISoMtQELIAAoAgQhKiAAQQA2AgQCQCAAICogARCngICAACIqDQAgASEBDE8LIABB0wA2AhwgACABNgIUIAAgKjYCDEEAISoMtAELIAAoAgQhKiAAQQA2AgQCQCAAICogARCngICAACIqDQAgASEBDFQLIABB5QA2AhwgACABNgIUIAAgKjYCDEEAISoMswELIABBADYCHCAAIAE2AhQgAEHGioCAADYCECAAQQc2AgxBACEqDLIBCyAAKAIEISogAEEANgIEAkAgACAqIAEQp4CAgAAiKg0AIAEhAQxLCyAAQdIANgIcIAAgATYCFCAAICo2AgxBACEqDLEBCyAAKAIEISogAEEANgIEAkAgACAqIAEQp4CAgAAiKg0AIAEhAQxLCyAAQdMANgIcIAAgATYCFCAAICo2AgxBACEqDLABCyAAKAIEISogAEEANgIEAkAgACAqIAEQp4CAgAAiKg0AIAEhAQxQCyAAQeUANgIcIAAgATYCFCAAICo2AgxBACEqDK8BCyAAQQA2AhwgACABNgIUIABB3IiAgAA2AhAgAEEHNgIMQQAhKgyuAQsgKkE/Rw0BIAFBAWohAQtBBSEqDJMBC0EAISogAEEANgIcIAAgATYCFCAAQf2SgIAANgIQIABBBzYCDAyrAQsgACgCBCEqIABBADYCBAJAIAAgKiABEKeAgIAAIioNACABIQEMRAsgAEHSADYCHCAAIAE2AhQgACAqNgIMQQAhKgyqAQsgACgCBCEqIABBADYCBAJAIAAgKiABEKeAgIAAIioNACABIQEMRAsgAEHTADYCHCAAIAE2AhQgACAqNgIMQQAhKgypAQsgACgCBCEqIABBADYCBAJAIAAgKiABEKeAgIAAIioNACABIQEMSQsgAEHlADYCHCAAIAE2AhQgACAqNgIMQQAhKgyoAQsgACgCBCEBIABBADYCBAJAIAAgASAuEKeAgIAAIgENACAuIQEMQQsgAEHSADYCHCAAIC42AhQgACABNgIMQQAhKgynAQsgACgCBCEBIABBADYCBAJAIAAgASAuEKeAgIAAIgENACAuIQEMQQsgAEHTADYCHCAAIC42AhQgACABNgIMQQAhKgymAQsgACgCBCEBIABBADYCBAJAIAAgASAuEKeAgIAAIgENACAuIQEMRgsgAEHlADYCHCAAIC42AhQgACABNgIMQQAhKgylAQsgAEEANgIcIAAgLjYCFCAAQcOPgIAANgIQIABBBzYCDEEAISoMpAELIABBADYCHCAAIAE2AhQgAEHDj4CAADYCECAAQQc2AgxBACEqDKMBC0EAISogAEEANgIcIAAgLjYCFCAAQYycgIAANgIQIABBBzYCDAyiAQsgAEEANgIcIAAgLjYCFCAAQYycgIAANgIQIABBBzYCDEEAISoMoQELIABBADYCHCAAIC42AhQgAEH+kYCAADYCECAAQQc2AgxBACEqDKABCyAAQQA2AhwgACABNgIUIABBjpuAgAA2AhAgAEEGNgIMQQAhKgyfAQsgKkEVRg1bIABBADYCHCAAIAE2AhQgAEHMjoCAADYCECAAQSA2AgxBACEqDJ4BCyAAQQA2AgAgKiAua0EGaiEBQSQhKgsgACAqOgApIAAoAgQhKiAAQQA2AgQgACAqIAEQq4CAgAAiKg1YIAEhAQxBCyAAQQA2AgALQQAhKiAAQQA2AhwgACAENgIUIABB8ZuAgAA2AhAgAEEGNgIMDJoBCyABQRVGDVQgAEEANgIcIAAgHTYCFCAAQfCMgIAANgIQIABBGzYCDEEAISoMmQELIAAoAgQhHSAAQQA2AgQgACAdICoQqYCAgAAiHQ0BICpBAWohHQtBrQEhKgx+CyAAQcEBNgIcIAAgHTYCDCAAICpBAWo2AhRBACEqDJYBCyAAKAIEIR4gAEEANgIEIAAgHiAqEKmAgIAAIh4NASAqQQFqIR4LQa4BISoMewsgAEHCATYCHCAAIB42AgwgACAqQQFqNgIUQQAhKgyTAQsgAEEANgIcIAAgHzYCFCAAQZeLgIAANgIQIABBDTYCDEEAISoMkgELIABBADYCHCAAICA2AhQgAEHjkICAADYCECAAQQk2AgxBACEqDJEBCyAAQQA2AhwgACAgNgIUIABBlI2AgAA2AhAgAEEhNgIMQQAhKgyQAQtBASEvQQAhMkEAIS5BASEqCyAAICo6ACsgIUEBaiEgAkACQCAALQAtQRBxDQACQAJAAkAgAC0AKg4DAQACBAsgL0UNAwwCCyAuDQEMAgsgMkUNAQsgACgCBCEqIABBADYCBCAAICogIBCtgICAACIqRQ1AIABByQE2AhwgACAgNgIUIAAgKjYCDEEAISoMjwELIAAoAgQhASAAQQA2AgQgACABICAQrYCAgAAiAUUNeSAAQcoBNgIcIAAgIDYCFCAAIAE2AgxBACEqDI4BCyAAKAIEIQEgAEEANgIEIAAgASAhEK2AgIAAIgFFDXcgAEHLATYCHCAAICE2AhQgACABNgIMQQAhKgyNAQsgACgCBCEBIABBADYCBCAAIAEgIhCtgICAACIBRQ11IABBzQE2AhwgACAiNgIUIAAgATYCDEEAISoMjAELQQEhKgsgACAqOgAqICNBAWohIgw9CyAAKAIEIQEgAEEANgIEIAAgASAjEK2AgIAAIgFFDXEgAEHPATYCHCAAICM2AhQgACABNgIMQQAhKgyJAQsgAEEANgIcIAAgIzYCFCAAQZCzgIAANgIQIABBCDYCDCAAQQA2AgBBACEqDIgBCyABQRVGDUEgAEEANgIcIAAgJDYCFCAAQcyOgIAANgIQIABBIDYCDEEAISoMhwELIABBADYCACAAQYEEOwEoIAAoAgQhKiAAQQA2AgQgACAqICUgJGtBAmoiJBCrgICAACIqRQ06IABB0wE2AhwgACAkNgIUIAAgKjYCDEEAISoMhgELIABBADYCAAtBACEqIABBADYCHCAAIAQ2AhQgAEHYm4CAADYCECAAQQg2AgwMhAELIABBADYCACAAKAIEISogAEEANgIEIAAgKiAmICNrQQNqIiMQq4CAgAAiKg0BQcYBISoMagsgAEECOgAoDFcLIABB1QE2AhwgACAjNgIUIAAgKjYCDEEAISoMgQELICpBFUYNOSAAQQA2AhwgACAENgIUIABBpIyAgAA2AhAgAEEQNgIMQQAhKgyAAQsgAC0ANEEBRw02IAAgBCACELyAgIAAIipFDTYgKkEVRw03IABB3AE2AhwgACAENgIUIABB1ZaAgAA2AhAgAEEVNgIMQQAhKgx/C0EAISogAEEANgIcIABBr4uAgAA2AhAgAEECNgIMIAAgLkEBajYCFAx+C0EAISoMZAtBAiEqDGMLQQ0hKgxiC0EPISoMYQtBJSEqDGALQRMhKgxfC0EVISoMXgtBFiEqDF0LQRchKgxcC0EYISoMWwtBGSEqDFoLQRohKgxZC0EbISoMWAtBHCEqDFcLQR0hKgxWC0EfISoMVQtBISEqDFQLQSMhKgxTC0HGACEqDFILQS4hKgxRC0EvISoMUAtBOyEqDE8LQT0hKgxOC0HIACEqDE0LQckAISoMTAtBywAhKgxLC0HMACEqDEoLQc4AISoMSQtBzwAhKgxIC0HRACEqDEcLQdUAISoMRgtB2AAhKgxFC0HZACEqDEQLQdsAISoMQwtB5AAhKgxCC0HlACEqDEELQfEAISoMQAtB9AAhKgw/C0GNASEqDD4LQZcBISoMPQtBqQEhKgw8C0GsASEqDDsLQcABISoMOgtBuQEhKgw5C0GvASEqDDgLQbEBISoMNwtBsgEhKgw2C0G0ASEqDDULQbUBISoMNAtBtgEhKgwzC0G6ASEqDDILQb0BISoMMQtBvwEhKgwwC0HBASEqDC8LIABBADYCHCAAIAQ2AhQgAEHpi4CAADYCECAAQR82AgxBACEqDEcLIABB2wE2AhwgACAENgIUIABB+paAgAA2AhAgAEEVNgIMQQAhKgxGCyAAQfgANgIcIAAgJDYCFCAAQcqYgIAANgIQIABBFTYCDEEAISoMRQsgAEHRADYCHCAAIB02AhQgAEGwl4CAADYCECAAQRU2AgxBACEqDEQLIABB+QA2AhwgACABNgIUIAAgKjYCDEEAISoMQwsgAEH4ADYCHCAAIAE2AhQgAEHKmICAADYCECAAQRU2AgxBACEqDEILIABB5AA2AhwgACABNgIUIABB45eAgAA2AhAgAEEVNgIMQQAhKgxBCyAAQdcANgIcIAAgATYCFCAAQcmXgIAANgIQIABBFTYCDEEAISoMQAsgAEEANgIcIAAgATYCFCAAQbmNgIAANgIQIABBGjYCDEEAISoMPwsgAEHCADYCHCAAIAE2AhQgAEHjmICAADYCECAAQRU2AgxBACEqDD4LIABBADYCBCAAICkgKRCxgICAACIBRQ0BIABBOjYCHCAAIAE2AgwgACApQQFqNgIUQQAhKgw9CyAAKAIEIQQgAEEANgIEAkAgACAEIAEQsYCAgAAiBEUNACAAQTs2AhwgACAENgIMIAAgAUEBajYCFEEAISoMPQsgAUEBaiEBDCwLIClBAWohAQwsCyAAQQA2AhwgACApNgIUIABB5JKAgAA2AhAgAEEENgIMQQAhKgw6CyAAQTY2AhwgACABNgIUIAAgBDYCDEEAISoMOQsgAEEuNgIcIAAgKDYCFCAAIAE2AgxBACEqDDgLIABB0AA2AhwgACABNgIUIABBkZiAgAA2AhAgAEEVNgIMQQAhKgw3CyAnQQFqIQEMKwsgAEEVNgIcIAAgATYCFCAAQYKZgIAANgIQIABBFTYCDEEAISoMNQsgAEEbNgIcIAAgATYCFCAAQZGXgIAANgIQIABBFTYCDEEAISoMNAsgAEEPNgIcIAAgATYCFCAAQZGXgIAANgIQIABBFTYCDEEAISoMMwsgAEELNgIcIAAgATYCFCAAQZGXgIAANgIQIABBFTYCDEEAISoMMgsgAEEaNgIcIAAgATYCFCAAQYKZgIAANgIQIABBFTYCDEEAISoMMQsgAEELNgIcIAAgATYCFCAAQYKZgIAANgIQIABBFTYCDEEAISoMMAsgAEEKNgIcIAAgATYCFCAAQeSWgIAANgIQIABBFTYCDEEAISoMLwsgAEEeNgIcIAAgATYCFCAAQfmXgIAANgIQIABBFTYCDEEAISoMLgsgAEEANgIcIAAgKjYCFCAAQdqNgIAANgIQIABBFDYCDEEAISoMLQsgAEEENgIcIAAgATYCFCAAQbCYgIAANgIQIABBFTYCDEEAISoMLAsgAEEANgIAIAQgLmtBBWohIwtBuAEhKgwRCyAAQQA2AgAgKiAua0ECaiEBQfUAISoMEAsgASEBAkAgAC0AKUEFRw0AQeMAISoMEAtB4gAhKgwPC0EAISogAEEANgIcIABB5JGAgAA2AhAgAEEHNgIMIAAgLkEBajYCFAwnCyAAQQA2AgAgMiAva0ECaiEBQcAAISoMDQsgASEBC0E4ISoMCwsCQCABIikgAkYNAANAAkAgKS0AAEGAvoCAAGotAAAiAUEBRg0AIAFBAkcNAyApQQFqIQEMBAsgKUEBaiIpIAJHDQALQT4hKgwkC0E+ISoMIwsgAEEAOgAsICkhAQwBC0ELISoMCAtBOiEqDAcLIAFBAWohAUEtISoMBgtBKCEqDAULIABBADYCACAvIDBrQQRqIQFBBiEqCyAAICo6ACwgASEBQQwhKgwDCyAAQQA2AgAgMiAva0EHaiEBQQohKgwCCyAAQQA2AgALIABBADoALCAnIQFBCSEqDAALC0EAISogAEEANgIcIAAgIzYCFCAAQc2QgIAANgIQIABBCTYCDAwXC0EAISogAEEANgIcIAAgIjYCFCAAQemKgIAANgIQIABBCTYCDAwWC0EAISogAEEANgIcIAAgITYCFCAAQbeQgIAANgIQIABBCTYCDAwVC0EAISogAEEANgIcIAAgIDYCFCAAQZyRgIAANgIQIABBCTYCDAwUC0EAISogAEEANgIcIAAgATYCFCAAQc2QgIAANgIQIABBCTYCDAwTC0EAISogAEEANgIcIAAgATYCFCAAQemKgIAANgIQIABBCTYCDAwSC0EAISogAEEANgIcIAAgATYCFCAAQbeQgIAANgIQIABBCTYCDAwRC0EAISogAEEANgIcIAAgATYCFCAAQZyRgIAANgIQIABBCTYCDAwQC0EAISogAEEANgIcIAAgATYCFCAAQZeVgIAANgIQIABBDzYCDAwPC0EAISogAEEANgIcIAAgATYCFCAAQZeVgIAANgIQIABBDzYCDAwOC0EAISogAEEANgIcIAAgATYCFCAAQcCSgIAANgIQIABBCzYCDAwNC0EAISogAEEANgIcIAAgATYCFCAAQZWJgIAANgIQIABBCzYCDAwMC0EAISogAEEANgIcIAAgATYCFCAAQeGPgIAANgIQIABBCjYCDAwLC0EAISogAEEANgIcIAAgATYCFCAAQfuPgIAANgIQIABBCjYCDAwKC0EA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AAAABQAAAAAAAAAAAAAABgAAAAcAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABJbnZhbGlkIGNoYXIgaW4gdXJsIHF1ZXJ5AFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fYm9keQBDb250ZW50LUxlbmd0aCBvdmVyZmxvdwBDaHVuayBzaXplIG92ZXJmbG93AFJlc3BvbnNlIG92ZXJmbG93AEludmFsaWQgbWV0aG9kIGZvciBIVFRQL3gueCByZXF1ZXN0AEludmFsaWQgbWV0aG9kIGZvciBSVFNQL3gueCByZXF1ZXN0AEV4cGVjdGVkIFNPVVJDRSBtZXRob2QgZm9yIElDRS94LnggcmVxdWVzdABJbnZhbGlkIGNoYXIgaW4gdXJsIGZyYWdtZW50IHN0YXJ0AEV4cGVjdGVkIGRvdABTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX3N0YXR1cwBJbnZhbGlkIHJlc3BvbnNlIHN0YXR1cwBJbnZhbGlkIGNoYXJhY3RlciBpbiBjaHVuayBleHRlbnNpb25zAFVzZXIgY2FsbGJhY2sgZXJyb3IAYG9uX3Jlc2V0YCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfaGVhZGVyYCBjYWxsYmFjayBlcnJvcgBgb25fbWVzc2FnZV9iZWdpbmAgY2FsbGJhY2sgZXJyb3IAYG9uX2NodW5rX2V4dGVuc2lvbl92YWx1ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX3N0YXR1c19jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX3ZlcnNpb25fY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl91cmxfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl9jaHVua19jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX2hlYWRlcl92YWx1ZV9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX21lc3NhZ2VfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl9tZXRob2RfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl9oZWFkZXJfZmllbGRfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl9jaHVua19leHRlbnNpb25fbmFtZWAgY2FsbGJhY2sgZXJyb3IAVW5leHBlY3RlZCBjaGFyIGluIHVybCBzZXJ2ZXIASW52YWxpZCBoZWFkZXIgdmFsdWUgY2hhcgBJbnZhbGlkIGhlYWRlciBmaWVsZCBjaGFyAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fdmVyc2lvbgBJbnZhbGlkIG1pbm9yIHZlcnNpb24ASW52YWxpZCBtYWpvciB2ZXJzaW9uAEV4cGVjdGVkIHNwYWNlIGFmdGVyIHZlcnNpb24ARXhwZWN0ZWQgQ1JMRiBhZnRlciB2ZXJzaW9uAEludmFsaWQgSFRUUCB2ZXJzaW9uAEludmFsaWQgaGVhZGVyIHRva2VuAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fdXJsAEludmFsaWQgY2hhcmFjdGVycyBpbiB1cmwAVW5leHBlY3RlZCBzdGFydCBjaGFyIGluIHVybABEb3VibGUgQCBpbiB1cmwARW1wdHkgQ29udGVudC1MZW5ndGgASW52YWxpZCBjaGFyYWN0ZXIgaW4gQ29udGVudC1MZW5ndGgARHVwbGljYXRlIENvbnRlbnQtTGVuZ3RoAEludmFsaWQgY2hhciBpbiB1cmwgcGF0aABDb250ZW50LUxlbmd0aCBjYW4ndCBiZSBwcmVzZW50IHdpdGggVHJhbnNmZXItRW5jb2RpbmcASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgc2l6ZQBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX2hlYWRlcl92YWx1ZQBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX2NodW5rX2V4dGVuc2lvbl92YWx1ZQBJbnZhbGlkIGNoYXJhY3RlciBpbiBjaHVuayBleHRlbnNpb25zIHZhbHVlAE1pc3NpbmcgZXhwZWN0ZWQgTEYgYWZ0ZXIgaGVhZGVyIHZhbHVlAEludmFsaWQgYFRyYW5zZmVyLUVuY29kaW5nYCBoZWFkZXIgdmFsdWUASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucyBxdW90ZSB2YWx1ZQBJbnZhbGlkIGNoYXJhY3RlciBpbiBjaHVuayBleHRlbnNpb25zIHF1b3RlZCB2YWx1ZQBQYXVzZWQgYnkgb25faGVhZGVyc19jb21wbGV0ZQBJbnZhbGlkIEVPRiBzdGF0ZQBvbl9yZXNldCBwYXVzZQBvbl9jaHVua19oZWFkZXIgcGF1c2UAb25fbWVzc2FnZV9iZWdpbiBwYXVzZQBvbl9jaHVua19leHRlbnNpb25fdmFsdWUgcGF1c2UAb25fc3RhdHVzX2NvbXBsZXRlIHBhdXNlAG9uX3ZlcnNpb25fY29tcGxldGUgcGF1c2UAb25fdXJsX2NvbXBsZXRlIHBhdXNlAG9uX2NodW5rX2NvbXBsZXRlIHBhdXNlAG9uX2hlYWRlcl92YWx1ZV9jb21wbGV0ZSBwYXVzZQBvbl9tZXNzYWdlX2NvbXBsZXRlIHBhdXNlAG9uX21ldGhvZF9jb21wbGV0ZSBwYXVzZQBvbl9oZWFkZXJfZmllbGRfY29tcGxldGUgcGF1c2UAb25fY2h1bmtfZXh0ZW5zaW9uX25hbWUgcGF1c2UAVW5leHBlY3RlZCBzcGFjZSBhZnRlciBzdGFydCBsaW5lAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fY2h1bmtfZXh0ZW5zaW9uX25hbWUASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucyBuYW1lAFBhdXNlIG9uIENPTk5FQ1QvVXBncmFkZQBQYXVzZSBvbiBQUkkvVXBncmFkZQBFeHBlY3RlZCBIVFRQLzIgQ29ubmVjdGlvbiBQcmVmYWNlAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fbWV0aG9kAEV4cGVjdGVkIHNwYWNlIGFmdGVyIG1ldGhvZABTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX2hlYWRlcl9maWVsZABQYXVzZWQASW52YWxpZCB3b3JkIGVuY291bnRlcmVkAEludmFsaWQgbWV0aG9kIGVuY291bnRlcmVkAFVuZXhwZWN0ZWQgY2hhciBpbiB1cmwgc2NoZW1hAFJlcXVlc3QgaGFzIGludmFsaWQgYFRyYW5zZmVyLUVuY29kaW5nYABTV0lUQ0hfUFJPWFkAVVNFX1BST1hZAE1LQUNUSVZJVFkAVU5QUk9DRVNTQUJMRV9FTlRJVFkAQ09QWQBNT1ZFRF9QRVJNQU5FTlRMWQBUT09fRUFSTFkATk9USUZZAEZBSUxFRF9ERVBFTkRFTkNZAEJBRF9HQVRFV0FZAFBMQVkAUFVUAENIRUNLT1VUAEdBVEVXQVlfVElNRU9VVABSRVFVRVNUX1RJTUVPVVQATkVUV09SS19DT05ORUNUX1RJTUVPVVQAQ09OTkVDVElPTl9USU1FT1VUAExPR0lOX1RJTUVPVVQATkVUV09SS19SRUFEX1RJTUVPVVQAUE9TVABNSVNESVJFQ1RFRF9SRVFVRVNUAENMSUVOVF9DTE9TRURfUkVRVUVTVABDTElFTlRfQ0xPU0VEX0xPQURfQkFMQU5DRURfUkVRVUVTVABCQURfUkVRVUVTVABIVFRQX1JFUVVFU1RfU0VOVF9UT19IVFRQU19QT1JUAFJFUE9SVABJTV9BX1RFQVBPVABSRVNFVF9DT05URU5UAE5PX0NPTlRFTlQAUEFSVElBTF9DT05URU5UAEhQRV9JTlZBTElEX0NPTlNUQU5UAEhQRV9DQl9SRVNFVABHRVQASFBFX1NUUklDVABDT05GTElDVABURU1QT1JBUllfUkVESVJFQ1QAUEVSTUFORU5UX1JFRElSRUNUAENPTk5FQ1QATVVMVElfU1RBVFVTAEhQRV9JTlZBTElEX1NUQVRVUwBUT09fTUFOWV9SRVFVRVNUUwBFQVJMWV9ISU5UUwBVTkFWQUlMQUJMRV9GT1JfTEVHQUxfUkVBU09OUwBPUFRJT05TAFNXSVRDSElOR19QUk9UT0NPTFMAVkFSSUFOVF9BTFNPX05FR09USUFURVMATVVMVElQTEVfQ0hPSUNFUwBJTlRFUk5BTF9TRVJWRVJfRVJST1IAV0VCX1NFUlZFUl9VTktOT1dOX0VSUk9SAFJBSUxHVU5fRVJST1IASURFTlRJVFlfUFJPVklERVJfQVVUSEVOVElDQVRJT05fRVJST1IAU1NMX0NFUlRJRklDQVRFX0VSUk9SAElOVkFMSURfWF9GT1JXQVJERURfRk9SAFNFVF9QQVJBTUVURVIAR0VUX1BBUkFNRVRFUgBIUEVfVVNFUgBTRUVfT1RIRVIASFBFX0NCX0NIVU5LX0hFQURFUgBNS0NBTEVOREFSAFNFVFVQAFdFQl9TRVJWRVJfSVNfRE9XTgBURUFSRE9XTgBIUEVfQ0xPU0VEX0NPTk5FQ1RJT04ASEVVUklTVElDX0VYUElSQVRJT04ARElTQ09OTkVDVEVEX09QRVJBVElPTgBOT05fQVVUSE9SSVRBVElWRV9JTkZPUk1BVElPTgBIUEVfSU5WQUxJRF9WRVJTSU9OAEhQRV9DQl9NRVNTQUdFX0JFR0lOAFNJVEVfSVNfRlJPWkVOAEhQRV9JTlZBTElEX0hFQURFUl9UT0tFTgBJTlZBTElEX1RPS0VOAEZPUkJJRERFTgBFTkhBTkNFX1lPVVJfQ0FMTQBIUEVfSU5WQUxJRF9VUkwAQkxPQ0tFRF9CWV9QQVJFTlRBTF9DT05UUk9MAE1LQ09MAEFDTABIUEVfSU5URVJOQUwAUkVRVUVTVF9IRUFERVJfRklFTERTX1RPT19MQVJHRV9VTk9GRklDSUFMAEhQRV9PSwBVTkxJTksAVU5MT0NLAFBSSQBSRVRSWV9XSVRIAEhQRV9JTlZBTElEX0NPTlRFTlRfTEVOR1RIAEhQRV9VTkVYUEVDVEVEX0NPTlRFTlRfTEVOR1RIAEZMVVNIAFBST1BQQVRDSABNLVNFQVJDSABVUklfVE9PX0xPTkcAUFJPQ0VTU0lORwBNSVNDRUxMQU5FT1VTX1BFUlNJU1RFTlRfV0FSTklORwBNSVNDRUxMQU5FT1VTX1dBUk5JTkcASFBFX0lOVkFMSURfVFJBTlNGRVJfRU5DT0RJTkcARXhwZWN0ZWQgQ1JMRgBIUEVfSU5WQUxJRF9DSFVOS19TSVpFAE1PVkUAQ09OVElOVUUASFBFX0NCX1NUQVRVU19DT01QTEVURQBIUEVfQ0JfSEVBREVSU19DT01QTEVURQBIUEVfQ0JfVkVSU0lPTl9DT01QTEVURQBIUEVfQ0JfVVJMX0NPTVBMRVRFAEhQRV9DQl9DSFVOS19DT01QTEVURQBIUEVfQ0JfSEVBREVSX1ZBTFVFX0NPTVBMRVRFAEhQRV9DQl9DSFVOS19FWFRFTlNJT05fVkFMVUVfQ09NUExFVEUASFBFX0NCX0NIVU5LX0VYVEVOU0lPTl9OQU1FX0NPTVBMRVRFAEhQRV9DQl9NRVNTQUdFX0NPTVBMRVRFAEhQRV9DQl9NRVRIT0RfQ09NUExFVEUASFBFX0NCX0hFQURFUl9GSUVMRF9DT01QTEVURQBERUxFVEUASFBFX0lOVkFMSURfRU9GX1NUQVRFAElOVkFMSURfU1NMX0NFUlRJRklDQVRFAFBBVVNFAE5PX1JFU1BPTlNFAFVOU1VQUE9SVEVEX01FRElBX1RZUEUAR09ORQBOT1RfQUNDRVBUQUJMRQBTRVJWSUNFX1VOQVZBSUxBQkxFAFJBTkdFX05PVF9TQVRJU0ZJQUJMRQBPUklHSU5fSVNfVU5SRUFDSEFCTEUAUkVTUE9OU0VfSVNfU1RBTEUAUFVSR0UATUVSR0UAUkVRVUVTVF9IRUFERVJfRklFTERTX1RPT19MQVJHRQBSRVFVRVNUX0hFQURFUl9UT09fTEFSR0UAUEFZTE9BRF9UT09fTEFSR0UASU5TVUZGSUNJRU5UX1NUT1JBR0UASFBFX1BBVVNFRF9VUEdSQURFAEhQRV9QQVVTRURfSDJfVVBHUkFERQBTT1VSQ0UAQU5OT1VOQ0UAVFJBQ0UASFBFX1VORVhQRUNURURfU1BBQ0UAREVTQ1JJQkUAVU5TVUJTQ1JJQkUAUkVDT1JEAEhQRV9JTlZBTElEX01FVEhPRABOT1RfRk9VTkQAUFJPUEZJTkQAVU5CSU5EAFJFQklORABVTkFVVEhPUklaRUQATUVUSE9EX05PVF9BTExPV0VEAEhUVFBfVkVSU0lPTl9OT1RfU1VQUE9SVEVEAEFMUkVBRFlfUkVQT1JURUQAQUNDRVBURUQATk9UX0lNUExFTUVOVEVEAExPT1BfREVURUNURUQASFBFX0NSX0VYUEVDVEVEAEhQRV9MRl9FWFBFQ1RFRABDUkVBVEVEAElNX1VTRUQASFBFX1BBVVNFRABUSU1FT1VUX09DQ1VSRUQAUEFZTUVOVF9SRVFVSVJFRABQUkVDT05ESVRJT05fUkVRVUlSRUQAUFJPWFlfQVVUSEVOVElDQVRJT05fUkVRVUlSRUQATkVUV09SS19BVVRIRU5USUNBVElPTl9SRVFVSVJFRABMRU5HVEhfUkVRVUlSRUQAU1NMX0NFUlRJRklDQVRFX1JFUVVJUkVEAFVQR1JBREVfUkVRVUlSRUQAUEFHRV9FWFBJUkVEAFBSRUNPTkRJVElPTl9GQUlMRUQARVhQRUNUQVRJT05fRkFJTEVEAFJFVkFMSURBVElPTl9GQUlMRUQAU1NMX0hBTkRTSEFLRV9GQUlMRUQATE9DS0VEAFRSQU5TRk9STUFUSU9OX0FQUExJRUQATk9UX01PRElGSUVEAE5PVF9FWFRFTkRFRABCQU5EV0lEVEhfTElNSVRfRVhDRUVERUQAU0lURV9JU19PVkVSTE9BREVEAEhFQUQARXhwZWN0ZWQgSFRUUC8AAF4TAAAmEwAAMBAAAPAXAACdEwAAFRIAADkXAADwEgAAChAAAHUSAACtEgAAghMAAE8UAAB/EAAAoBUAACMUAACJEgAAixQAAE0VAADUEQAAzxQAABAYAADJFgAA3BYAAMERAADgFwAAuxQAAHQUAAB8FQAA5RQAAAgXAAAfEAAAZRUAAKMUAAAoFQAAAhUAAJkVAAAsEAAAixkAAE8PAADUDgAAahAAAM4QAAACFwAAiQ4AAG4TAAAcEwAAZhQAAFYXAADBEwAAzRMAAGwTAABoFwAAZhcAAF8XAAAiEwAAzg8AAGkOAADYDgAAYxYAAMsTAACqDgAAKBcAACYXAADFEwAAXRYAAOgRAABnEwAAZRMAAPIWAABzEwAAHRcAAPkWAADzEQAAzw4AAM4VAAAMEgAAsxEAAKURAABhEAAAMhcAALsTAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQECAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAwICAgICAAACAgACAgACAgICAgICAgICAAQAAAAAAAICAgICAgICAgICAgICAgICAgICAgICAgICAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAAgACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgACAgICAgAAAgIAAgIAAgICAgICAgICAgADAAQAAAACAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAICAgICAgICAgICAgICAgICAgICAgICAgICAgICAAIAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGxvc2VlZXAtYWxpdmUAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQECAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAWNodW5rZWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAAEBAQEBAAABAQABAQABAQEBAQEBAQEBAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZWN0aW9uZW50LWxlbmd0aG9ucm94eS1jb25uZWN0aW9uAAAAAAAAAAAAAAAAAAAAcmFuc2Zlci1lbmNvZGluZ3BncmFkZQ0KDQoNClNNDQoNClRUUC9DRS9UU1AvAAAAAAAAAAAAAAAAAQIAAQMAAAAAAAAAAAAAAAAAAAAAAAAEAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAECAAEDAAAAAAAAAAAAAAAAAAAAAAAABAEBBQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAABAAABAAAAAAAAAAAAAAAAAAAAAAAAAAABAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAEAAAIAAAAAAAAAAAAAAAAAAAAAAAADBAAABAQEBAQEBAQEBAQFBAQEBAQEBAQEBAQEAAQABgcEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAAEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAABAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAAAAAAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAACAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAAAAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATk9VTkNFRUNLT1VUTkVDVEVURUNSSUJFTFVTSEVURUFEU0VBUkNIUkdFQ1RJVklUWUxFTkRBUlZFT1RJRllQVElPTlNDSFNFQVlTVEFUQ0hHRU9SRElSRUNUT1JUUkNIUEFSQU1FVEVSVVJDRUJTQ1JJQkVBUkRPV05BQ0VJTkROS0NLVUJTQ1JJQkVIVFRQL0FEVFAv' +module.exports = 'AGFzbQEAAAABMAhgAX8Bf2ADf39/AX9gBH9/f38Bf2AAAGADf39/AGABfwBgAn9/AGAGf39/f39/AALLAQgDZW52GHdhc21fb25faGVhZGVyc19jb21wbGV0ZQACA2VudhV3YXNtX29uX21lc3NhZ2VfYmVnaW4AAANlbnYLd2FzbV9vbl91cmwAAQNlbnYOd2FzbV9vbl9zdGF0dXMAAQNlbnYUd2FzbV9vbl9oZWFkZXJfZmllbGQAAQNlbnYUd2FzbV9vbl9oZWFkZXJfdmFsdWUAAQNlbnYMd2FzbV9vbl9ib2R5AAEDZW52GHdhc21fb25fbWVzc2FnZV9jb21wbGV0ZQAAA0ZFAwMEAAAFAAAAAAAABQEFAAUFBQAABgAAAAAGBgYGAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAAABAQcAAAUFAwABBAUBcAESEgUDAQACBggBfwFBgNQECwfRBSIGbWVtb3J5AgALX2luaXRpYWxpemUACRlfX2luZGlyZWN0X2Z1bmN0aW9uX3RhYmxlAQALbGxodHRwX2luaXQAChhsbGh0dHBfc2hvdWxkX2tlZXBfYWxpdmUAQQxsbGh0dHBfYWxsb2MADAZtYWxsb2MARgtsbGh0dHBfZnJlZQANBGZyZWUASA9sbGh0dHBfZ2V0X3R5cGUADhVsbGh0dHBfZ2V0X2h0dHBfbWFqb3IADxVsbGh0dHBfZ2V0X2h0dHBfbWlub3IAEBFsbGh0dHBfZ2V0X21ldGhvZAARFmxsaHR0cF9nZXRfc3RhdHVzX2NvZGUAEhJsbGh0dHBfZ2V0X3VwZ3JhZGUAEwxsbGh0dHBfcmVzZXQAFA5sbGh0dHBfZXhlY3V0ZQAVFGxsaHR0cF9zZXR0aW5nc19pbml0ABYNbGxodHRwX2ZpbmlzaAAXDGxsaHR0cF9wYXVzZQAYDWxsaHR0cF9yZXN1bWUAGRtsbGh0dHBfcmVzdW1lX2FmdGVyX3VwZ3JhZGUAGhBsbGh0dHBfZ2V0X2Vycm5vABsXbGxodHRwX2dldF9lcnJvcl9yZWFzb24AHBdsbGh0dHBfc2V0X2Vycm9yX3JlYXNvbgAdFGxsaHR0cF9nZXRfZXJyb3JfcG9zAB4RbGxodHRwX2Vycm5vX25hbWUAHxJsbGh0dHBfbWV0aG9kX25hbWUAIBJs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ADcDACAAQShqQgA3AwAgAEEgakIANwMAIABBEGpCADcDACAAQQhqQgA3AwAgAEHdATYCHEEAC3sBAX8CQCAAKAIMIgMNAAJAIAAoAgRFDQAgACABNgIECwJAIAAgASACEMSAgIAAIgMNACAAKAIMDwsgACADNgIcQQAhAyAAKAIEIgFFDQAgACABIAIgACgCCBGBgICAAAAiAUUNACAAIAI2AhQgACABNgIMIAEhAwsgAwvk8wEDDn8DfgR/I4CAgIAAQRBrIgMkgICAgAAgASEEIAEhBSABIQYgASEHIAEhCCABIQkgASEKIAEhCyABIQwgASENIAEhDiABIQ8CQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAgACgCHCIQQX9qDt0B2gEB2QECAwQFBgcICQoLDA0O2AEPENcBERLWARMUFRYXGBkaG+AB3wEcHR7VAR8gISIjJCXUASYnKCkqKyzTAdIBLS7RAdABLzAxMjM0NTY3ODk6Ozw9Pj9AQUJDREVG2wFHSElKzwHOAUvNAUzMAU1OT1BRUlNUVVZXWFlaW1xdXl9gYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXp7fH1+f4ABgQGCAYMBhAGFAYYBhwGIAYkB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QAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAC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FCAAIBA2AgxBACEQDLIBCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQp4CAgAAiEA0AIAEhAQxNCyAAQdMANgIcIAAgATYCFCAAIBA2AgxBACEQDLEBCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQp4CAgAAiEA0AIAEhAQxRCyAAQeUANgIcIAAgATYCFCAAIBA2AgxBACEQDLABCyAAQQA2AhwgACABNgIUIABBxoqAgAA2AhAgAEEHNgIMQQAhEAyvAQsgACgCBCEQIABBADYCBAJAIAAgECABEKeAgIAAIhANACABIQEMSQsgAEHSADYCHCAAIAE2AhQgACAQNgIMQQAhEAyuAQsgACgCBCEQIABBADYCBAJAIAAgECABEKeAgIAAIhANACABIQEMSQsgAEHTADYCHCAAIAE2AhQgACAQNgIMQQAhEAytAQsgACgCBCEQIABBADYCBAJAIAAgECABEKeAgIAAIhANACABIQEMTQsgAEHlADYCHCAAIAE2AhQgACAQNgIMQQAhEAysAQsgAEEANgIcIAAgATYCFCAAQdyIgIAANgIQIABBBzYCDEEAIRAMqwELIBBBP0cNASABQQFqIQELQQUhEAyQAQtBACEQIABBADYCHCAAIAE2AhQgAEH9koCAADYCECAAQQc2AgwMqAELIAAoAgQhECAAQQA2AgQCQCAAIBAgARCngICAACIQDQAgASEBDEILIABB0gA2AhwgACABNgIUIAAgEDYCDEEAIRAMpwELIAAoAgQhECAAQQA2AgQCQCAAIBAgARCngICAACIQDQAgASEBDEILIABB0wA2AhwgACABNgIUIAAgEDYCDEEAIRAMpgELIAAoAgQhECAAQQA2AgQCQCAAIBAgARCngICAACIQDQAgASEBDEYLIABB5QA2AhwgACABNgIUIAAgEDYCDEEAIRAMpQELIAAoAgQhASAAQQA2AgQCQCAAIAEgFBCngICAACIBDQAgFCEBDD8LIABB0gA2AhwgACAUNgIUIAAgATYCDEEAIRAMpAELIAAoAgQhASAAQQA2AgQCQCAAIAEgFBCngICAACIBDQAgFCEBDD8LIABB0wA2AhwgACAUNgIUIAAgATYCDEEAIRAMowELIAAoAgQhASAAQQA2AgQCQCAAIAEgFBCngICAACIBDQAgFCEBDEMLIABB5QA2AhwgACAU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YWdlX2JlZ2luYCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfZXh0ZW5zaW9uX3ZhbHVlYCBjYWxsYmFjayBlcnJvcgBgb25fc3RhdHVzX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fdmVyc2lvbl9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX3VybF9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX2NodW5rX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25faGVhZGVyX3ZhbHVlX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fbWVzc2FnZV9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX21ldGhvZF9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX2hlYWRlcl9maWVsZF9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX2NodW5rX2V4dGVuc2lvbl9uYW1lYCBjYWxsYmFjayBlcnJvcgBVbmV4cGVjdGVkIGNoYXIgaW4gdXJsIHNlcnZlcgBJbnZhbGlkIGhlYWRlciB2YWx1ZSBjaGFyAEludmFsaWQgaGVhZGVyIGZpZWxkIGNoYXIAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl92ZXJzaW9uAEludmFsaWQgbWlub3IgdmVyc2lvbgBJbnZhbGlkIG1ham9yIHZlcnNpb24ARXhwZWN0ZWQgc3BhY2UgYWZ0ZXIgdmVyc2lvbgBFeHBlY3RlZCBDUkxGIGFmdGVyIHZlcnNpb24ASW52YWxpZCBIVFRQIHZlcnNpb24ASW52YWxpZCBoZWFkZXIgdG9rZW4AU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl91cmwASW52YWxpZCBjaGFyYWN0ZXJzIGluIHVybABVbmV4cGVjdGVkIHN0YXJ0IGNoYXIgaW4gdXJsAERvdWJsZSBAIGluIHVybABFbXB0eSBDb250ZW50LUxlbmd0aABJbnZhbGlkIGNoYXJhY3RlciBpbiBDb250ZW50LUxlbmd0aABEdXBsaWNhdGUgQ29udGVudC1MZW5ndGgASW52YWxpZCBjaGFyIGluIHVybCBwYXRoAENvbnRlbnQtTGVuZ3RoIGNhbid0IGJlIHByZXNlbnQgd2l0aCBUcmFuc2Zlci1FbmNvZGluZwBJbnZhbGlkIGNoYXJhY3RlciBpbiBjaHVuayBzaXplAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25faGVhZGVyX3ZhbHVlAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fY2h1bmtfZXh0ZW5zaW9uX3ZhbHVlAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMgdmFsdWUATWlzc2luZyBleHBlY3RlZCBMRiBhZnRlciBoZWFkZXIgdmFsdWUASW52YWxpZCBgVHJhbnNmZXItRW5jb2RpbmdgIGhlYWRlciB2YWx1ZQBJbnZhbGlkIGNoYXJhY3RlciBpbiBjaHVuayBleHRlbnNpb25zIHF1b3RlIHZhbHVlAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMgcXVvdGVkIHZhbHVlAFBhdXNlZCBieSBvbl9oZWFkZXJzX2NvbXBsZXRlAEludmFsaWQgRU9GIHN0YXRlAG9uX3Jlc2V0IHBhdXNlAG9uX2NodW5rX2hlYWRlciBwYXVzZQBvbl9tZXNzYWdlX2JlZ2luIHBhdXNlAG9uX2NodW5rX2V4dGVuc2lvbl92YWx1ZSBwYXVzZQBvbl9zdGF0dXNfY29tcGxldGUgcGF1c2UAb25fdmVyc2lvbl9jb21wbGV0ZSBwYXVzZQBvbl91cmxfY29tcGxldGUgcGF1c2UAb25fY2h1bmtfY29tcGxldGUgcGF1c2UAb25faGVhZGVyX3ZhbHVlX2NvbXBsZXRlIHBhdXNlAG9uX21lc3NhZ2VfY29tcGxldGUgcGF1c2UAb25fbWV0aG9kX2NvbXBsZXRlIHBhdXNlAG9uX2hlYWRlcl9maWVsZF9jb21wbGV0ZSBwYXVzZQBvbl9jaHVua19leHRlbnNpb25fbmFtZSBwYXVzZQBVbmV4cGVjdGVkIHNwYWNlIGFmdGVyIHN0YXJ0IGxpbmUAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9jaHVua19leHRlbnNpb25fbmFtZQBJbnZhbGlkIGNoYXJhY3RlciBpbiBjaHVuayBleHRlbnNpb25zIG5hbWUAUGF1c2Ugb24gQ09OTkVDVC9VcGdyYWRlAFBhdXNlIG9uIFBSSS9VcGdyYWRlAEV4cGVjdGVkIEhUVFAvMiBDb25uZWN0aW9uIFByZWZhY2UAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9tZXRob2QARXhwZWN0ZWQgc3BhY2UgYWZ0ZXIgbWV0aG9kAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25faGVhZGVyX2ZpZWxkAFBhdXNlZABJbnZhbGlkIHdvcmQgZW5jb3VudGVyZWQASW52YWxpZCBtZXRob2QgZW5jb3VudGVyZWQAVW5leHBlY3RlZCBjaGFyIGluIHVybCBzY2hlbWEAUmVxdWVzdCBoYXMgaW52YWxpZCBgVHJhbnNmZXItRW5jb2RpbmdgAFNXSVRDSF9QUk9YWQBVU0VfUFJPWFkATUtBQ1RJVklUWQBVTlBST0NFU1NBQkxFX0VOVElUWQBDT1BZAE1PVkVEX1BFUk1BTkVOVExZAFRPT19FQVJMWQBOT1RJRlkARkFJTEVEX0RFUEVOREVOQ1kAQkFEX0dBVEVXQVkAUExBWQBQVVQAQ0hFQ0tPVVQAR0FURVdBWV9USU1FT1VUAFJFUVVFU1RfVElNRU9VVABORVRXT1JLX0NPTk5FQ1RfVElNRU9VVABDT05ORUNUSU9OX1RJTUVPVVQATE9HSU5fVElNRU9VVABORVRXT1JLX1JFQURfVElNRU9VVABQT1NUAE1JU0RJUkVDVEVEX1JFUVVFU1QAQ0xJRU5UX0NMT1NFRF9SRVFVRVNUAENMSUVOVF9DTE9TRURfTE9BRF9CQUxBTkNFRF9SRVFVRVNUAEJBRF9SRVFVRVNUAEhUVFBfUkVRVUVTVF9TRU5UX1RPX0hUVFBTX1BPUlQAUkVQT1JUAElNX0FfVEVBUE9UAFJFU0VUX0NPTlRFTlQATk9fQ09OVEVOVABQQVJUSUFMX0NPTlRFTlQASFBFX0lOVkFMSURfQ09OU1RBTlQASFBFX0NCX1JFU0VUAEdFVABIUEVfU1RSSUNUAENPTkZMSUNUAFRFTVBPUkFSWV9SRURJUkVDVABQRVJNQU5FTlRfUkVESVJFQ1QAQ09OTkVDVABNVUxUSV9TVEFUVVMASFBFX0lOVkFMSURfU1RBVFVTAFRPT19NQU5ZX1JFUVVFU1RTAEVBUkxZX0hJTlRTAFVOQVZBSUxBQkxFX0ZPUl9MRUdBTF9SRUFTT05TAE9QVElPTlMAU1dJVENISU5HX1BST1RPQ09MUwBWQVJJQU5UX0FMU09fTkVHT1RJQVRFUwBNVUxUSVBMRV9DSE9JQ0VTAElOVEVSTkFMX1NFUlZFUl9FUlJPUgBXRUJfU0VSVkVSX1VOS05PV05fRVJST1IAUkFJTEdVTl9FUlJPUgBJREVOVElUWV9QUk9WSURFUl9BVVRIRU5USUNBVElPTl9FUlJPUgBTU0xfQ0VSVElGSUNBVEVfRVJST1IASU5WQUxJRF9YX0ZPUldBUkRFRF9GT1IAU0VUX1BBUkFNRVRFUgBHRVRfUEFSQU1FVEVSAEhQRV9VU0VSAFNFRV9PVEhFUgBIUEVfQ0JfQ0hVTktfSEVBREVSAE1LQ0FMRU5EQVIAU0VUVVAAV0VCX1NFUlZFUl9JU19ET1dOAFRFQVJET1dOAEhQRV9DTE9TRURfQ09OTkVDVElPTgBIRVVSSVNUSUNfRVhQSVJBVElPTgBESVNDT05ORUNURURfT1BFUkFUSU9OAE5PTl9BVVRIT1JJVEFUSVZFX0lORk9STUFUSU9OAEhQRV9JTlZBTElEX1ZFUlNJT04ASFBFX0NCX01FU1NBR0VfQkVHSU4AU0lURV9JU19GUk9aRU4ASFBFX0lOVkFMSURfSEVBREVSX1RPS0VOAElOVkFMSURfVE9LRU4ARk9SQklEREVOAEVOSEFOQ0VfWU9VUl9DQUxNAEhQRV9JTlZBTElEX1VSTABCTE9DS0VEX0JZX1BBUkVOVEFMX0NPTlRST0wATUtDT0wAQUNMAEhQRV9JTlRFUk5BTABSRVFVRVNUX0hFQURFUl9GSUVMRFNfVE9PX0xBUkdFX1VOT0ZGSUNJQUwASFBFX09LAFVOTElOSwBVTkxPQ0sAUFJJAFJFVFJZX1dJVEgASFBFX0lOVkFMSURfQ09OVEVOVF9MRU5HVEgASFBFX1VORVhQRUNURURfQ09OVEVOVF9MRU5HVEgARkxVU0gAUFJPUFBBVENIAE0tU0VBUkNIAFVSSV9UT09fTE9ORwBQUk9DRVNTSU5HAE1JU0NFTExBTkVPVVNfUEVSU0lTVEVOVF9XQVJOSU5HAE1JU0NFTExBTkVPVVNfV0FSTklORwBIUEVfSU5WQUxJRF9UUkFOU0ZFUl9FTkNPRElORwBFeHBlY3RlZCBDUkxGAEhQRV9JTlZBTElEX0NIVU5LX1NJWkUATU9WRQBDT05USU5VRQBIUEVfQ0JfU1RBVFVTX0NPTVBMRVRFAEhQRV9DQl9IRUFERVJTX0NPTVBMRVRFAEhQRV9DQl9WRVJTSU9OX0NPTVBMRVRFAEhQRV9DQl9VUkxfQ09NUExFVEUASFBFX0NCX0NIVU5LX0NPTVBMRVRFAEhQRV9DQl9IRUFERVJfVkFMVUVfQ09NUExFVEUASFBFX0NCX0NIVU5LX0VYVEVOU0lPTl9WQUxVRV9DT01QTEVURQBIUEVfQ0JfQ0hVTktfRVhURU5TSU9OX05BTUVfQ09NUExFVEUASFBFX0NCX01FU1NBR0VfQ09NUExFVEUASFBFX0NCX01FVEhPRF9DT01QTEVURQBIUEVfQ0JfSEVBREVSX0ZJRUxEX0NPTVBMRVRFAERFTEVURQBIUEVfSU5WQUxJRF9FT0ZfU1RBVEUASU5WQUxJRF9TU0xfQ0VSVElGSUNBVEUAUEFVU0UATk9fUkVTUE9OU0UAVU5TVVBQT1JURURfTUVESUFfVFlQRQBHT05FAE5PVF9BQ0NFUFRBQkxFAFNFUlZJQ0VfVU5BVkFJTEFCTEUAUkFOR0VfTk9UX1NBVElTRklBQkxFAE9SSUdJTl9JU19VTlJFQUNIQUJMRQBSRVNQT05TRV9JU19TVEFMRQBQVVJHRQBNRVJHRQBSRVFVRVNUX0hFQURFUl9GSUVMRFNfVE9PX0xBUkdFAFJFUVVFU1RfSEVBREVSX1RPT19MQVJHRQBQQVlMT0FEX1RPT19MQVJHRQBJTlNVRkZJQ0lFTlRfU1RPUkFHRQBIUEVfUEFVU0VEX1VQR1JBREUASFBFX1BBVVNFRF9IMl9VUEdSQURFAFNPVVJDRQBBTk5PVU5DRQBUUkFDRQBIUEVfVU5FWFBFQ1RFRF9TUEFDRQBERVNDUklCRQBVTlNVQlNDUklCRQBSRUNPUkQASFBFX0lOVkFMSURfTUVUSE9EAE5PVF9GT1VORABQUk9QRklORABVTkJJTkQAUkVCSU5EAFVOQVVUSE9SSVpFRABNRVRIT0RfTk9UX0FMTE9XRUQASFRUUF9WRVJTSU9OX05PVF9TVVBQT1JURUQAQUxSRUFEWV9SRVBPUlRFRABBQ0NFUFRFRABOT1RfSU1QTEVNRU5URUQATE9PUF9ERVRFQ1RFRABIUEVfQ1JfRVhQRUNURUQASFBFX0xGX0VYUEVDVEVEAENSRUFURUQASU1fVVNFRABIUEVfUEFVU0VEAFRJTUVPVVRfT0NDVVJFRABQQVlNRU5UX1JFUVVJUkVEAFBSRUNPTkRJVElPTl9SRVFVSVJFRABQUk9YWV9BVVRIRU5USUNBVElPTl9SRVFVSVJFRABORVRXT1JLX0FVVEhFTlRJQ0FUSU9OX1JFUVVJUkVEAExFTkdUSF9SRVFVSVJFRABTU0xfQ0VSVElGSUNBVEVfUkVRVUlSRUQAVVBHUkFERV9SRVFVSVJFRABQQUdFX0VYUElSRUQAUFJFQ09ORElUSU9OX0ZBSUxFRABFWFBFQ1RBVElPTl9GQUlMRUQAUkVWQUxJREFUSU9OX0ZBSUxFRABTU0xfSEFORFNIQUtFX0ZBSUxFRABMT0NLRUQAVFJBTlNGT1JNQVRJT05fQVBQTElFRABOT1RfTU9ESUZJRUQATk9UX0VYVEVOREVEAEJBTkRXSURUSF9MSU1JVF9FWENFRURFRABTSVRFX0lTX09WRVJMT0FERUQASEVBRABFeHBlY3RlZCBIVFRQLwAAXhMAACYTAAAwEAAA8BcAAJ0TAAAVEgAAORcAAPASAAAKEAAAdRIAAK0SAACCEwAATxQAAH8QAACgFQAAIxQAAIkSAACLFAAATRUAANQRAADPFAAAEBgAAMkWAADcFgAAwREAAOAXAAC7FAAAdBQAAHwVAADlFAAACBcAAB8QAABlFQAAoxQAACgVAAACFQAAmRUAACwQAACLGQAATw8AANQOAABqEAAAzhAAAAIXAACJDgAAbhMAABwTAABmFAAAVhcAAMETAADNEwAAbBMAAGgXAABmFwAAXxcAACITAADODwAAaQ4AANgOAABjFgAAyxMAAKoOAAAoFwAAJhcAAMUTAABdFgAA6BEAAGcTAABlEwAA8hYAAHMTAAAdFwAA+RYAAPMRAADPDgAAzhUAAAwSAACzEQAApREAAGEQAAAyFwAAuxMAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQIBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIDAgICAgIAAAICAAICAAICAgICAgICAgIABAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgIAAAACAgICAgICAgICAgICAgICAgICAgICAgICAgICAgACAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAICAgICAAACAgACAgACAgICAgICAgICAAMABAAAAAICAgICAgICAgICAgICAgICAgICAgICAgICAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAAgACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbG9zZWVlcC1hbGl2ZQAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQIBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBY2h1bmtlZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEAAQEBAQEAAAEBAAEBAAEBAQEBAQEBAQEAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABlY3Rpb25lbnQtbGVuZ3Rob25yb3h5LWNvbm5lY3Rpb24AAAAAAAAAAAAAAAAAAAByYW5zZmVyLWVuY29kaW5ncGdyYWRlDQoNCg0KU00NCg0KVFRQL0NFL1RTUC8AAAAAAAAAAAAAAAABAgABAwAAAAAAAAAAAAAAAAAAAAAAAAQBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAQIAAQMAAAAAAAAAAAAAAAAAAAAAAAAEAQEFAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAEAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAQAAAgAAAAAAAAAAAAAAAAAAAAAAAAMEAAAEBAQEBAQEBAQEBAUEBAQEBAQEBAQEBAQABAAGBwQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEAAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwAAAAAAAAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAABAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAIAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAAAAAAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABOT1VOQ0VFQ0tPVVRORUNURVRFQ1JJQkVMVVNIRVRFQURTRUFSQ0hSR0VDVElWSVRZTEVOREFSVkVPVElGWVBUSU9OU0NIU0VBWVNUQVRDSEdFT1JESVJFQ1RPUlRSQ0hQQVJBTUVURVJVUkNFQlNDUklCRUFSRE9XTkFDRUlORE5LQ0tVQlNDUklCRUhUVFAvQURUUC8=' diff --git a/lib/llhttp/llhttp.wasm b/lib/llhttp/llhttp.wasm index 633d00a08975ea47eedfa1f7a6e2cdb08c35b330..fe63282c9a5e6f49066a0c6fc7a2dd7293204c33 100755 GIT binary patch literal 55466 zcmeHw34C2uwfEWQ-lS86B zw7^qo83mb<8}2dSDxpoU90w0Yt`C)Gi%opWB~|0Pf|QQ^!D}F;=f8h!G-^@s>&35 z)=WMCC^}d4bS^G-EiCr-buL)3eC4uYf6?>#cuV(qd9kmrYyV>Be#QNl^mrbh1@77E z-esO*%2=1u*WcB@y3Zumx`bv{`@c?Co?kY@M@DDTiWjZ_f&E z%A|DCir&uNVprda9&a1>1Ozd2{Ny8!REq{rR<7vtKI&v7m^*vAK+m>L6h>wyF?5Mm zXp*yTYfA{LTUP8@Qtat>?aRy8m3g>e@#>zX#f8kgfB#}{yN{H~ldzcWw=eU&zqhNW zZxKXP?1AbacJE`(=3Im6DHa#@brx4F^1e8FlJc|)#w$OQtq3YdeeUzSe`=R$pZMes zWA1+0FTl*JyxdXi)~)k`t-R37-ObCbLof7q^Y;KydwNrYZSW-Yc2S=Wy=mUg$aNQU z7KkI#vxf8xD+MEadeg|rZvNz8JZc3wD)hT^ z_Xr(1q-amS;N>>*e(PX=vg6e$e>yQ&*LePP^UL~n84HJz@dK$euL?E5Hr3zBT;X~= zaIEtng_=yDy>UVAA-P9>ea~qkjhr4^rvk|0qd;tHz|$wrGEZ^2L2`n<*2IBV}k0MvEu}BE`ff0 z;`;BpoUldhmJ=sU-fHV9e;1me3vct0k8T^*<+t1ZV;}#-)E##G z{oHO{yYKP&JzeR&aPbA+{L8<7(WOoA>eBAIJe*NKv(PYWcH_UzX`0*I()#ah?Ji%( zy!m^7X`e6uN7q-r>XORCua!?ur*`h!73fzkf;o>1^abY{`0(WhzJHB@8!j;LG7FER z`6K_&eqi9bs|~#6Dg&Rj`F{I-bN`Bkf3fiG^(OsdoBzsd&HYgp&#x^#hn{EBk3Yx2 z$FDT-q4N#A&zAe?+2;O93-7h~pSAEjTkl)8{AISDO&6MS8*TbuEd1ML7N5=cJ6r#4 z7T>$}`H5CO&suoi6{g%KOaIF@-}kLN-?aIjw)8w??_aWXZ2F!l|BmJ3BFo=3w*K`N z-eSw$Zs|MO>g_tKk2h?6ms@c z<>X+?-Dvr}+m`#2P5+*y`*E9otHpDOfv#K7&yYUTNlj!w!tPeTuU3Uhd$W4cn@Xf$ zlHaq*TJ@Ja`Kx>4mp*ydJ;|6SJ1d_(ZpQS7d*rk1RrIt@zXvM^^C{9HxUK4WC^ zob51;tm4pRAGG3AP8nInO%7AK;=vTz@!_(6seJYV;%^^uH%DW;>e^Mh7OqVz4ude7 z(yF0G$v{oN<~7u1sxi_)`VP7RH@FNjB-&Yf!TU;O*0PjEl+Ws_RUBpPkO^maZOCit zz+ex)7#S-Zni?f{Sq%7A6~fFat!C9I40Vu0(vaJi>kmH2tb*qyT2zwJ>$0nWiuDPd z;*i;|j6nunfoLi_4wWKfXi7MCKoo|7TF^J~p;1~yC{nETB?%ouU7>F! z=+!A)J9S~OZV8nk@;<<(CwI^AcF*9-0=$xqBHyFpp#&CyhvOP+uq$4gC>x;J(TQ9Wpl{vnaL1 zgu#1=_Qv?-)!(bk*tMC1vun4Lutw*W<}=&NC9-7kSAoAu{Efn275+x!Zw&r0ZBR!v zHTWBgzi~!w|NH;{yAq(yY?IGv%s;%So>%I}GEEoF5t)3pq!f1)^~eoQ^8<~*XBHYz)`c0=l~ zeD!M>5AU~NVR6y^i(n9YP<5C(Tpgi~ zRNqoZsiW1m)iLT=b(}h0ouE!sC#jRwcho8BRCSsU-+@YP~vF zou|%M7pM!>57b5KVs(kSR9&VnS68Sj)m7?hb&a}KU8k;BH>excP3mFwkh)ntsBTdY zs9V)->UMR9`k}g0ZBTcqyVX7FUUi?kUwJP`AC#-UA8>44uKGT}8o+x2#{u30m;<~U za0|e@0BZp^0B#9*C*VZD9|BGSyaRAD;O&500p13|CjedvI3Dl{!0iDq2mBb|Wq=5$~y!aL_?&ZaixVVoON8sXqUL1~#9RUvm{1Fol#l-`>I0P3D@?sq>9^%E? zKtD`)aG)O{{AQpx5`H7lj}m@8(2o)RXP_S^JSfml5FQxlCkYP-^izau0=<{$Qgnfao1n3R)v%Fmu=n8<9fqss+D-a6-^aT3Hyj>pX=Lweu`X_`-158!}b_Y5G zuq4ng@^*2cUn1N;&_5+ygn2E(VxV6pT!{H9!Uci;IpKZ*0<~OqSAaMNaNhtCBVcEs ze?j=QK>w2PtAT!<@GF6SgYZ8B{VT#R2l`FIeFFV!!Y>8-EyBG6{Tsshf&MMwyg+Xz z>p=x z{sG}f1AP(UM*@8@;WmN3gm6lrFD2YM(3cTz73j+eCkOfp!byR?l5is9*If0Mfxe2c zHqciSZV~8f2qy&kI>PaRzLqc-=<5kF4!D7EY@lx>tO@i@gw=t*nJ@_SEreqNeJkPU zK;K4Kg;q{DD$utRRtEYG!iqrukT4tQI|;EWvw;u+_uYhO@pl0Rnd$%Uh7kWt`Tt7b ze2bO7@yggXT)o=vV^a|t$16>;(DYJf?|uH`D_`ArzbFbW4T9Gp z0yfd_(It`ZKPu|I8S(Dm(e95B3qOds_+hnCJ%V8VF@*e2AozbuZBkFGXVkOmIgBHo zS3glNs29~s>Sgtc`kDHLu zU*nXoo7mznams-v`uKC4vbDtFe~J@oB~X4hPT0ppdVh>l{!IeLKg0>MCE|Q1PS6tL z{vHXKxY(J~W^ZPyzr}TZunC@W8#wIeRXGg7jOl)6 zq$B*h+TSqw$FNl?Hla!xR=yl-h<3ygDUwhx){jz}f;?fQPMW569U}YvF$P#pg0-7s z);M1ZmSxf*`x@MUM_*tlhXv479-u&zjBt?rQ0>+oc$CM!A zsQSeekzntb6830_wDNmVBZuEFSaM(&R{6CQnkQrOpWtDw?ANJ<>U3ZC*xDEU-;Chb z`whZC3=%*9^tuheRUm{5gt)}^r)p@r7DCv~udROGkQ~_<`Apf^YgYA`{nZ@d-)VW2 zR_)1T!E>2lhF3Q}%%Mr;NNQ{M*xi2#m+bl}&-|DTBKD>XFdZ4Ji!%s!0I6R$zWOy4 z%*X(~3YZ1_Iba3g&j2d{UjZBi_%dJ>;78AU~rQB)KeMMaTOR1_ISMG;U`6ahs=5l~bV0Yya- zP*fBFMaBFS74uV6%ui7ChbFInxttS*Bm*n3Xd_H>6uEXVXlQ#W&h?#o??PD3Ec( zPP(c=a6>XDr5WKA-1QU|oB0@)5O<1<57JNM$5LQVE18nJHsMaufq`sKyz`pwu~|%P zzMwo9R>(dP2ATHCoT$473dKYnkisW6)LflGFi^XcT^5AUUAJWjAuQFI?z)No^vv{- zJ~BT^hrQvXaFV_g>s%kGReI~jDTKhj8Ig+kbFAx8xH?18R-*RP$d8CoydtZ^BS<2| zjzy1j7`?%1A~za=#=M~;>#Fi(RgILacS^~sB3Tuv=+vRfx;jl3Oor2)j;t%olT|TN zvL3&?L|PRjYZS;jWoWXlN|OZ_L1D9LLH%*Jy$(&&^{y@=X{Z?^)&Mt@wgyh;UI}X; zydt*8BtmOnXb!Y44icW1p+X5J&eH;e%-3E%&^%Ebt;#ZJ+yQzcPU>DZFtZQOZ|lSQ z2I_;!GS1~K1>G~Y{73NcgI$6FzeGr@*TI>Ud$%oBcT97Do!jFRaQ;6$^JU=;Q&#QH5O zA@H%Wynd;UUPP0*(uRl!;%=Nfpb$lj=b(Zuyo~u#7G^SjRFw>D;EgP=GKYU+e9Ni) z6L<@v9xpW@&g)+YwG+bgm$%51LI(mo=)a`4$Pzf6kMc6De?BThHa^Q?pPIPu zXOxsfG2@nv?Lb_vMmD0mIeojHX>oHnpnL3OPe!PRXE;tB?$?u{Y%-2@Se+?AY11^< zvF{L?%;L!r6p-QODP|0b%$&6dh++!!YETwHiqS7GWBFJ(@gFlrXJNMr#@6t;@JywT=oAj%95raZ9iB z0p!gEAjk0OIji z4ooBrWp?Pn#UEL-la)V7^Nlke+VqU0e$Oxet4y+=c@H#LgOu(iaWAgwZ}IbcAd~99 z#`m(&`ImSygznhW$I*+F%;|_PLQb=4c{17nf_ZXbMq3O8)W!bAzIQROJD1F5=0C;Q zsk|phfsJ$-*fLZVPmZ-(C&|d6{zx1umv5oSN%SlaD6>3@{zukFkuD)CI_6Z!YCCn? z%owl&`7^OSZDdnN@5365_u?o$5s!-Za-Fv_owqE!A}tEZ4HnF+RsSgNoXMpu)gDLX zQ~l&qBJoEs*NT6RQI@jRn?>W_Lv|oEkNR@hNG~1T!y*H*=4#9|F+|uw=NoE{$W;Fz zEm~RH{Wb*S)q$D1N%)(H>l}$l{~D4obueZVW_&#}cTj~P30$cxd~sD6>t0+##Hsvsw(cv#aU{IQl=#wVQdL#?$gz53y_SY(y?XeYp(XVt_41TUd5&P22P zoaoGrT^?V?E@knYlQ1vSu#NrL zb*6!I(H`nJ_=8!fbf<>CT^7P!Er*w{#ZT4pi?|lSj=uQOA!|7twfsE6%VFhfN%QhX zTnh|Gcp18u!>orgESd#;q|pp5=Q$iz5le({5ULs5ej_$L=q8G;?AX8;dxIOe8J%a~ zVRa$>>pvD7{*%<(Thu-UuFS57U&r^d`{7M{?=+!h!@;4DkX#&+jI1_05r7yw-Z{8? zY{so{n2I7qvQq(lxDRt+qPQcWgeEWh7tp0g4C>9;%$FQZpcpTSLO0p~C zm!)LOJ_rPqt~UA#*%M*YV3431zGCZJF#vcPTb20#aK8^p4$sBVoIYe*{dtP+`z_sZ zy!pLS>SR|2dIv{cS{83oQHj2Sw9CE?q*>*`FSxRND!V!U96dGuU}o^>*cn_1XV=~>u6vi^iV zT|JOHHv4))flOx8pPMcm;DM^vaRT3sr=P@$ZmiM99-*a8b@V z1*=)vX~qSha?(H_pCaRLF&U4rel)Kk$%v4r$zWXfP+A)6xKjTr6*mp`_+!}P!;w95 zQSZTY_CuH*{fu}`DRM3BBjasU*jEr~UwRIBB4-WKYhV&PsfI&|>1Rnyj2)`Hhp|P$ z{o*3RA$=7m!mWi#U-Lj3=V8S8N~uh9p|loQSZ&h6*2Na~AcZ-Eh2_{>9P+r|ah{nw zh?A_z=0J8D5^WU6u@V3G9+&BPcp=4FQ^7 z=cf?f28X@+khJi=O*yllEN4RSYze=AY)tcAg07+FD$|<{;wlCAkJI%X!}^d`w!Tu| ze0)ibL$n;}Xd>ikDRJnDz4J+vm2I(zFUpcZwv!Ebb4-Sj<1osKkf+Jumis%>WXP^u z1Q#PB9&D5Q$By$KC?ly@i2GMHb0sQF5G zatDaE4pO#0-=0>pY%hL#5XMUTzLR3cIML0y-ys=`kf#~r=8ap^jLGh11Yl+5TWW2m z$7I~j`iHZ&2zihxnldwwv{Hw1Q7qdVMYqGKI6|J5EqAwFmlT$4%74T#6EoD_J$5_r z^I?7ynSE0(laPjt6(VUUbH)MEaMF?3?fUp$A}}l$#afk90~-gCR%zgJNi41FDXrlG zmk4=UTHLCIlT9%Za%AD*L5L`A4p+uRV5MyM<`5xI6T#UN9IJ|nkn<9ZUkl?B_St8B+h2qGz=0h zM@#fQoVklEOQOQ7K&Z6mtT_d9Wzw?boXL+y$jZ$zD_4z#l?Zv7m5+m!%ag3gAr}U? zBa!E=F%egegop@vng~v;UzQ|7PS_x-E=xpd-<>-!?)X(D(=52pZQrLL!= zymt^HN@MfeV@X_2B8D4$M#$4d@MtBDAjU+TOCs(uZqZoQt+1@XyBr5Q)4#ZlY)gX4 zOF_D|HHqpjN!r?Z#Ctb$&{`iyM|O$r$gmbzaLQganaG7pM zixrN|M!ZOj#1$CZ0dw64r#@peKOmYP8Z_*(_=M7AFT5qBGVoWDe5PmkUp65*86`!M z%bKy!MXvU&IUE;JlQoCnE{vZ`D0M+n#d6Gyv2MARJVYb9kTM>w5k<(;qUI6X^O7=> zGj0g5M?}QkvA!=D2@w(UG!fh*cy5viIbe6&AVidQ+4sdnoKGT#>#`%{X(G7Gaeb1A zOKB~)4njn!Iv$FNIBz79h>)j=;7M$pKaNfIG7@o1Y_i5+e+YvehXv0IaC&^;Zw5y1 z+PB2N=Ya|9SR}+fH@=sUbbWj;(ckxzR(UxoxS2WFR5&^E{mAiP8)i(ZoO)#ZI}-DK zB$gM@4R5{?^0d5o!X4+bV-H?|w-CCT@KggcGlX zecz46EfLu1@x25?rzORI1371$TF#0~hZe8KoS#n4hZ|`}$kUv2dpcfc5OXePlo1@4 zC8E?>{wgNoG!ijf-xVQG6TvMU-$@c72c)ks;y3P!!XN9Mh8J*@`$b|TcZu6hiRCQu z)_0O}mSfkKkC4SgjC;yRSd5UTS>&5APD-*UXTC2Rgox5`EHRh(9TG8II2IvK6A^+4 zyqh9cwj4NTv|7&MN{L9!B%VAHA|m8zB6zs%gd`Di+Fe3q*V4>9;>mdHiunbI)GHC> ziLrVm6g(lRUO6q#c&;2%qe|w@-ia;vL^3sMm_Ro|o@Rbq_j*B`OWlq0Fos2TONzlm{7L<9DM&uQh zS@Dm&qB2YD@QNJcJ#_+?ObYBpZ&xJ_ly|76`hR7bZwVel7&v7o`vfjYzgqK8iEh-lMS0Q6#LA zgqh2VYP_<>RLA1?vp6>LNDSnPPr^%-PEhh6FNXRBPZlv=dm#bU;lfRoq3!{qKL-M= zB4wdiHyYwzc#C_LSy7h`9!q^8HXO<%*rdEjLK$XSAtAOGGzS)gAPztvXo*rT3 z`_x%owFwkQ8|iE$Ql#Js_T&t?;c_(P#|@XK;iXm>f8gEVl~oAC^xv`Jl5aUG-Ee7q z;uu5Uqoj|%U<_-=cFW^cIEHE)&1L@(4x35(ab|-{@TW7$G^xZ^%YaQv0)+Qb;Si{eA;}QjsgULw)zO7 zN_L;mu?BJ5#Qp#zgBD>%zI*|iA*H;xl`imIn8uHZBNZ7uZ&M7-4|okV_pqJudk|cF z!zDhfunUw6K67}ZwrrF-PMP=Wyu%E9B_vq&Z^C*J)Ga<)1R(pVkH`DsJT!xp?U=Vs z!2JWgyq{9Glk?S~aBWcmXh7&@Bs}q&yzN|5V`<(0=j^U&=q%atG^qeio6L>#Z}sT<*G#20fPaF&kQ8-dFZ`!vUca^d)ky7Lq3P#%Xib0PM}Ps!(Ucv%5tj@LV&Md8e0lHf4H zb_B7>J3RF|)yo%uLTu)|>#rbtAq>0LN@aH#4!Bvuws3?U>KMT$uG0Kxo)G2FyZn%- zVZ=K6tdAZ9bf?kyQNw%`f@RhQX^o zmRU7=9a;|5SXwpwrjZgh7L`_ge=Mc#9M|s+TD5yuM8x$mQuWwDtNxojPaE}yQzJq1 z4Fw{dzd91J-DV^t{zdSD*SN6==You$Mnso2xQy1a(3+v2?Zs;CcY+*$b)GPqP0IdRKs)emY_m|A*wQ5`~lCUncOjui*jZ!u5V&$#&#`%RdUJu)1BINvfuKYVx@^jE||U4;I($$ZvnmLuag%=e+3U>~@s zh$IQ89s59|VavO%^iqCf!t61}0baxVuUToLYfv@l>gg>x-};1fq{{UVXna;_gY^$| zuTg8XwU+B2s5xm+*rH-ywv5BeJdx`0UdS;o4^p`J-qAO3cQN)=AjrLhrPg5wyu^nAdcF}W(jMVMKGH;$#RrYY9E)yCo?dz zFtWy5Q{sF!^m-)@2gbO3ub6zSk?|I`=?&zJ4Fhrh7|e4DI8dn1}IYp_n4Z@ z@Q%A%Utj{q-Tw4Y2w+_7dtL_n`iacEB?L1S%q$p(qYZNHPY{OCf{qYMF}$L}k@DA< zK*RDS{PEolsVD)@vyq&#HR~ZX5Q8=cyWa_~60t50Myq0@O{qHzu4x!Jv*-hek+PPQ zk(v;e+4#sf6R~X;H7_XUHcHqn?My;lzTB9`W^;!w2(EVF>3|Tyyk$l@S5(Avo}S)4 zQZ*$SIY%6%!NaPkGQN5PgCl=BS(_Rk*P;ugKdORX=cF|7@|@l@Z&!Zr;1p%1l5Ucc zc-sU9Yt}Bt(J)(aicwzEGKO<2c-Mryp#d8)?d#uWoN?VRHu7()`H-|1j-N5+;QV;= z1~_%aj1Sad=)l(=VIPJQzQIT1Xkq5f7DjO5glSm!eE}4b$pM3st4aqY3zBHjK@u%H zYS+e74${(hjs3z92#OrvgF;_S9s`E;XGG!0M;#yBzAyZuA$WS-guT>`sBse2q%d4{ zCppD>>^rP4H=>$Va~MVSM7oZjmk9>g%avyz-|TW@GU)efB;m;vzPJN|@(rQvtr0kr zp}RmHvQw(&%pMg7ZqOdEiGM_OewA3C8l=vboMZJm=joCRDG&p`7mG$#Ucpd!re>3!08EJbf++HMXJlkr`=-;rN}#D^lniG`XB zeMzl+$0MA?Z+8sB?-eN-e}()$HweGjOmC13@Jp&kAuX2(mKO%Wa$DRA;m4_AF`b)t)GfVEtpC+{id1jLO%4x<&5w|gui%7<7XM}#x{6O z^NlO6qx)gS^9NRZ7T)ogK*EZ+PrEE4i0`Z2I&f^vqkqfkN=(aZ3|ac|LIQEctO+k* zFGupbX*T)60^@X74Uot!al6^tNkp7>SvQ*YBORpwwUmToKZ1?bgEW*hlDR}Om@~Te z!yp;VyIeCuB>z4XJJ@tQKz8@XN~K#ArCv3{${L4a`^pc4WH7eHd5l1dqJY7)$UdmF z5xsC!WXloTrd$7F6br)h@(d$lgR%ztUOdzw3V!HfM8+~>D5;6R7y-kAWH6~=qv(hz zX&@zoNevr44H6sUWZBDvI7^;0wy)qQaL5|pBS_f3>1*S7{(=vK#IaL~-J=Dt@Z-}`as z5Ycx|GM}aI__HEou5@kqY~+(k!M)?^ZT5|Eh#I==*nem8p|3+chX5Y$&O{P>z8%uW z#6(QC;uD?{j&(?P@vp9UiBvESVes|4G=Xl(&GD4=6h|HR14&R= zVX`yuuLyIwON53B3@L$_QPhLx)c^ z$hO4;iSBNTjsh-0CxZuPGEfw`I6a>8;d-&Cm`i4nY1viapJR7{?Nb>YO1|UE*VT!ITX%Dg)s@W28AC=7mfz1 z$CNER+SZK1{V4pfEeu&f%JC@o+hq%ni3|6l@FTV`#yzRRM@!rnO!H(MJ237;0N!0U z#(eGrQ=4LlDGr0Uo|UNQF>|SbW)L!ie>7c=C@Q|-|jkZv;2e3S4=`=`VH6G#UEDa_t&CkV+S%S&K zobVA|#x+uE`M}kPuQJMJk|k^cSeBiNun$*Hr$xmvtnj`LD8+m2j!ZT7EKm0HRqnp97*7Y;6|P(`Dvw@j<0zFgnEhw(-d&YH5Ml zWU-(5npco;#)TGr6L~0#D|LbW(Itxh^IsGnIfK5#aD`WCJi4c;-*!6 zqljD2_BNmH!WW8o;sWt8PYvVyM0Ei8JP}Xd%8jC?Guz$id_ONE3kd>|jUDs2aGaftgVSd4 zxTooe(dyW9a=H#ekjBHPq3qz_PH@U>o!%=|J~_dK^1MY9IxP^y<|^V03uknfuf~x& ziIA%IB4@irn5mR#xq44N+u1y`Gt^PJ&-2;iX^EUvMk0H-w&;bpT?K8}VF>@kYIoduw5L>xy{ALJe6%*KA zEz`iHy<$=^k*(aa;4Z{w1&#JXMV8x*!ZkY$^(3&B+YAE(5o}&4-8uI6$=2=(QL+tt zVcvYOuuh2Lk^ttou}&dhM?|#Ck0WA5&NL}m)8VQ>ipGhC3!d@h@<5Vs z&2#&hW;BUW6*XX*ubc2nwp4~DJkpP(BVfMk9wLfl8=7cMzY)(<$TTWG-#$c`(+% z5R*i9}w?G8^IPs{TyEnsmJ8*W9S)Xrux;G)dr}-L_AhagWN-WAZsVq zZ`=@9gpD!m9l0se^^UGBkuo?qI>3kG0<>E)i1zDGH}ud?@t`n0i-x8+vaB+NvkV^b zZD*b9wjXlN1y?D2S36Yr3`1R|j1=)*a{7^Mh;v!^FU)j@{!)D7*$*rA+P<)&JC6qy z;(>gOmSo>03TVGB3vU1_DskhXXhoe*_kdE^U2ZpeLO>W0pbAilP2ot-)Cq@ZYw$54 z2G$;Eg(mQwb@(80iNd%`4#war_apKZyuF(W5?`lQhADr#Mm({eTVhE`?qTND>oy&} z&Rd%Iu?T_HNf4MZ`CfhsPxDx((B6!-yZgW9EiTusI~>I}c)RgJCSF65!h0gSYTq{O?g5F4&-y+GPspjR_ga^|-IIK8e)o!L9 zwkW(Gm#ql~jE2WO!8nMD@}XeFf8aRFTkbLkCNyNG;Jcs@4Cp;k=p8jzzFCrqDC7Gy zvB|q1^g=dEdso?uRcJc-3{+gbM(2urdHSh5HfB>tWIlVqsyv#ch?(qHgis=#2@D22K^m52%8)Yc22uXf zys@Xy@6IEuk%5n+CyeAr=>u0;$=8WY&ZbQ2vZ5m#kwJ#cn*MwhRxwFLSOoz}{h?o1 zMH+n2SXYJPA~NvM^hyP|1W_V?ex((%!zxQRj(LcqX4kz|wq@M87EKgLwxA1H zxymTo2%JU$@5{=4co+KOZBd1fqd6apVkxK&Rt>Q3{V#UwoR~XJ3l_HSp+`{`#p)mwJ2LCf#e+jQ440ukXm-vYu@3aSv=AT%*G z09v@Gw?aO(W-0#PwqZ(t9qF&iz(IwOoKq2cIIeS6LO&ICRf$fp3ilaygqcXcm9T3x zCH)b$T+lB{uV|QpER|DGr>7Na2|&#EYJ(DG0=P~th2yV z$RzY)XHh|CA3gX3!&M4Eq6}Fmz+H zFv}?r41{4A3#(uWP$~>mnk5uN0m7&zi*?|l8twz{LzKi4TShI$meIf&45I!L8x`ZE zc#O3gTSmN*FR^)J%b-HqAmbIF^zf->e|kh4>`^QgxsvV*sql3=$L&sVKnZULjW~CM zOXq};3DJ}|M`SmylP-;Yy6Za7kwT+U*TL>Ha$&p_S-GAEEzVFWy(y&S&!wT*=!aCZ zBcjcNOzDaYVWpiftReE`;yDGeK`l`ZR7JF}vl~a8htcA&(NcAH94!qQE1?l%=Law{ zB%>VhF`4fCD8!i{HoG(*tb$APxoN=DyYA-yi8MrvZ{Qu9fsF`mmV0yqj6$-I1`I!g z{#MYZ5onC-C0IivtjQXe5X%nJRyd?2{AQ%BLnj__(nkR&G=$D>4nHr2Ygi{6tBW>a zV{02~D#9wp)Wj)4Bzz(IbLc~MJ)k`-1-D)|xJ2tkjLhZ&1y;ZW-#6Z{!jz7gBreBd zrDc)6Br~Um#Z9aWy`v9T_{umCq7n!&!ib38WTzImqzMGCog(|4>n{{$eB71)lq#m+!iDgE>ITmMA22ycs zlJeS*$Z~uHc$PzUzOLwCKFPQ!>y;3AFg}Fw%kWPcyWy{_RwxYD4^%*S6;^l^R(Np9 zA#z{y#?d10muDNi2f$rUcP%u)?Rr;%;;+H*83wbxzlYqfrAmnbK{`(CWYmrkWd+awd)#F8!5CYk!*@!J?F1ENtOhfQ0i*QNv=KA#M@)lk z94=pC14KK3rxIBo^uzvVTaG|kXL1xGV~%(f;>H~DD5Ro4 z4>=@9A)bsvU@F*afKdo00Q%AVa9%eag@AV%v~CmnSSY6iV})@{NdC-N3F83H3W#gN z%OxOD`V$!vO9tS9Nlpf^OaWAb8}PwF3o6H;C2IyPAPGiS=fTOq^q>O{L^0C`QZNpw zgRVH}KrZlO1|@k!;9eecXdG*^@>F#c9&0s|{pCG8luQqgA*Y)TfZ z$25wA?$(+m&}h&!FqKoKw;RkIJNKfooa5XvgMIDo;6KiuaCgv*Rn6ePQG%xNJyhc+Wo;7Cl zh0*K>qFtlx*qCwP2gd9)M7W7wG5zB#tib4#$08G)G+2MH>9Qz62=T;!{~(*=UHRSn2&N(9%*C7%REAi<15)u z3;i@^^tRy97_-nn8R#riY@rMkkq^d<9stIiF~$rP(3q>B8F+VV%$W7ySQy3(gQGDc z7x=Np47cOutEe|P9^;#&?MQVL9&2UW0XjtUwC!q?r-`Faq>D4=@0l2k9gX9JIQ4!}ZW+@{!IJS3y5CV1xRRcN%v^jPNuzr~|qGtlZ zIfE4)0>|SNItOP^BKib8wv|qnXc$r&$4*+N@6fR9AQAtEBLl~}+{%QcR6hvaCnW0YG z%%G5wH8Xh5WHUp9k(nzrGv75cP6?!&nN*Kept8*j+LP8L$7SKdA!yhS7Hy)|g+@mk zh9+py0wjoGq+7mq`TlgYFF$Hu^6E}$U+6Yn`|?xm%Qx+d)uW2ILQ0MGQn5y0p^Tlnk8F92*zy?KvPWAsnI*WX z;`%ae8Mv)2!v>8lBO7d)<{Q~E4cpnWJeKIo+A_?8mg;O7%nY<;^rSL&w?Qp#tu4bZ zMz%cEY`L-NXs$e@Qw7t;n+`HtF2-vD9{L%IADx$ROH`GcCWm#~IVKL)7&}%Wn_zCiq#68nu>EGY$Me9su+7H$+fAT43HoaXj}i|B6-)CNc4$Li|#c@N`g= z8xCjjx+OSRl<-hO9tc4Qs0D+G=25T;@j5WAkwqLA%~wh^kEhjGTQp-5qIS`IVhsoE zU?CIHd}`f*K@3Vywu1*jR2iE66s!`N5zS-zfNclmAv~C}5Q=CXD-yDTD1wUa;FT+p ziA|^udc_n>GAjdrOqH8xnyse7M)Od+5_=LcZG#9h>Q4nV{kBkm;H5O0$8wEx-r#eT z82l(2yrRK_YLtY%q8{sO1AJ$_XmiE>AG-{q2i>g17_gE!tj8h zC8h^~*o@#Kb(9o9f$iO|6S12w&pGf+WETZPQt{iP^Y+L6vrL5@oZZM)T}ZY!%N^04@muT z%VL-XNCp>`5?sK6^&-^f{s#<5(UgM)XYXF6L@5r|0qg z$WUPirX&PG*7Lle*`q3Iyf0!7reAyr^};y^C}?ey_$@qIGabWa&RmmMw^E_k!n8~| zxAo_3-_NX72Lq>gUeLIwNAC)!!xaL1t23O+l!ti4MEFkqP5U^57ihSQcvYdz$nnoN zqV&;{P9w+Xh%sHyK*Su-KjhHEtCr=dJz6~{Nxvb_eJRc4td`OuW5hxfHT&HIft-(h z-EXh5dMj2VIVKxWYXCS0+zeM|(O? z;lP!}1^vZ^VSCHGw)#Stz}Hw`D2p!1>7w4Q{g;D_u&=+Xx8IAfEnLw*1pmm~zq&7? zuh(%`lFo{{K%9tefrzDeAY|6J1hVkv_2EHUyxzVLF0d8*_bvBCP;@RXb}cOSmd&ud z*w@#!f3b7F;{Ho|%H}XAA}Tte`_;v=ISntJ3sx*&xvbbxx7V>)Vo}hN?Dl2Ytr!ytzyI`ao(JnQWx>q=7~# z_Z`6GzG2ynk!~q}!*ZhTRjXG(`X7c`DY4KNY3*9whkjtcgF<*DQ+;35a3fj^Ee-Vb z{YB5Y+HfTc^418>QXZu`k0MUBG;>70@lEmL689NDux%=$EPj%7lyRyd%DJ;R-H=)0 z6642?>VXAIoL^kTVoZCy_;IOO(aqVX$rhS$AEvb~Thdef5Y|exk+z057|AMXhswkH zmgeR{ea9~IR_@=69@2{*w6--yiIFFhVB^zl>ph4c(20euy~RaveM69HBo_w>Nmpr~ z4epea(lxX#jMzM2Meo9}2nVqmAyscNeqgYs9wlU*^hkXR78jRyd2J?i3Kw_LeWEQP zvMbX(z4pBuJL+e5wzjo=X&-N1d%<4KoulhJ8s|55Ah8(_>kIAedPY;Bv(VhZ#QK)j zeZ09X^9wUOTMKP-bu-eN_VGGeS~?554Oh)A9gPk9cn!L-33+A~S_{oH3(fWWcr*0O z&RM#nu(#gFYi;6x^E$lx*@gN!Ex52xIy)NY7I@!Q`1g5*_KxVjxzMq9OWT}ItK%r$ z-Z!>1M@daBvl`<&SFo*s`W^Pxmi7*BZe#n*#x@uX=(N>&^-YansI$JQrG3!UrWRnD zp__DbeIgIpb699v?xwSy2s>Ijnb_Xh+R{em1T@a=)SVp#-P*#W_5$i{Y3@LF2r;^B z)omS(x+%J!-CF2uY@V;18fP-Cy+b2E6YFQ7N+9uO;g1RJ9c_*E5OGU$LlZAL3Ug7d zZrca>4C7v;!Z^ta)SEl6siP4%bjQ4QLFX#9Nz^#hPh_I0c2D%*IlHmBqaC)R=feWT zavECNI-3f!zyoy94wAi=)($AF-P&Md^DG))M@xN6Q@b}`w>5&LPTkbr(%D>?1#^PY zv>SR_VTkp!TM*Z`d#I<-)(oueg|_*Hw$4IZTT7d_cVUK0nAbd~xn=KW`=AZv%$n!! z8ex;PXI4BPH*KAIUdL>v)oapb3$?dH@C7KKf$3-jQiqeompZ}P-nwlj%m7y&H3)U- zxdl*0H6-q+5VSI8Ds;A>sy3&z`q?0~b2iKqX>;e)>n5}qE!gI@Ci!V>@0lm?#(XDYin!=Bb|jWwKld1JKoI3cAEvXwm|JBxfz_2@s_qmD7tYzO$(gN zHBX7(@d^pI3wC2^bLkdl6*^}W(AK>6M#u)_HMF&SxnSsWziyTV-}GI zu@p2k7MfmW)7qgxTtH2RoR;QUd`?%3wm`pBUuc4R zfg^;&py1o3I+$AXzzi-msjZGS-Q3>5M$uf~GLvPZZjS+cni;Xb_Qo$4#IMs|bTl^4 zD@Z0d71Q3~Yv!W$bwtTVtnCBRt)UD^hZYB<8PhM1xf3pK9$I@zwWdhnOArX{afG>= z(Iy9=ayDFA1)A^d7CP5*40nnh#Sdo|BB40zMt~_oD@68Q;zqny4Oc)X4~N|fM_7RA zwa;s1rvMwD3-646VV~B5H>;(Y`QQ!f>kF+Nbf<72ZS&y(VZV`!^`LH8G&8oN>9#jC zN=~$D_JnAd^P1s$>t{=GgP^S-0FpI6@ymso5Q|uYD50sT zWp5-&_wMXmgzj|b=_bY*>=cbM;g+a46k@(cTGU*K1NTiWEv*QTI>cuPne}L{)`!S_ zQ$u`T540?WkO8J+*fL`DY)sSHQeQt09Xl1lDx_aVctb8BisqS((yvj!HWh(iX|ESO zbx~7EuNXZyo-eMW#KPkTR>^TfE4vNpnMz4BA){wxAU#m0O=>Ghypu{}jp#LIw$Ijc z3NEARC1@*ff36?v)UB-u>x>HK!X2Usn;UT>=)Dn}HqYFjUAw`zEI!; z*RvtuZKHW3i7g{`r!tE0ysvHHc^}8$t~t-!JjwIU!S$9CJn!?k-V8Vf>8k;+z~7;G z)>7+vYmv5oOV8U5*GD7$7{Ix>-!$Iy9>aBRvgbWG(eqxw-(5(18MtmkdO!ZYfqbt4 zRv~R$;3?w%eB4h3^znBF{&qt8Fz)KzgRBC zayc3J8J|DUrf>eqEmOEGz~sSxhI~i_{QV#GQX9U*);#z_=hDw62S=g|F3(7LCN@U=U=AYFfBCFAEgY*v<&ml z@n$i&+kp@n1|}v_JRlq?8UM6=X2y<-kIQ87aYaw>iUSXtwji3!E2-Jd!E=g@bMR({ zux1Q@+voCsExd;(8pz*&3`BRWWEfuagy*m&_ageZ@B0Gtvq`e zt#i`BtW9uDiQ{xmF>o#q*LtRz!^4b;T`e(_w(|?J`eCNG>t`d=KAb+QpFOL9$p~3& nz#I=I|DtJPZdtKY7G_o#6B?L`Xr5C)XP!-$A*P;*{^|b!Rej;> literal 56001 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 diff --git a/lib/llhttp/llhttp_simd-wasm.js b/lib/llhttp/llhttp_simd-wasm.js index 0fb8a67a0e5..017e247de6d 100644 --- a/lib/llhttp/llhttp_simd-wasm.js +++ b/lib/llhttp/llhttp_simd-wasm.js @@ -1 +1 @@ -module.exports = 'AGFzbQEAAAABMAhgAX8Bf2ADf39/AX9gBH9/f38Bf2AAAGADf39/AGABfwBgAn9/AGAGf39/f39/AALLAQgDZW52GHdhc21fb25faGVhZGVyc19jb21wbGV0ZQACA2VudhV3YXNtX29uX21lc3NhZ2VfYmVnaW4AAANlbnYLd2FzbV9vbl91cmwAAQNlbnYOd2FzbV9vbl9zdGF0dXMAAQNlbnYUd2FzbV9vbl9oZWFkZXJfZmllbGQAAQNlbnYUd2FzbV9vbl9oZWFkZXJfdmFsdWUAAQNlbnYMd2FzbV9vbl9ib2R5AAEDZW52GHdhc21fb25fbWVzc2FnZV9jb21wbGV0ZQAAA0ZFAwMEAAAFAAAAAAAABQEFAAUFBQAABgAAAAAGBgYGAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAAABAQcAAAUFAAMBBAUBcAESEgUDAQACBggBfwFBgNQECwfRBSIGbWVtb3J5AgALX2luaXRpYWxpemUACRlfX2luZGlyZWN0X2Z1bmN0aW9uX3RhYmxlAQALbGxodHRwX2luaXQAChhsbGh0dHBfc2hvdWxkX2tlZXBfYWxpdmUAQQxsbGh0dHBfYWxsb2MADAZtYWxsb2MARgtsbGh0dHBfZnJlZQANBGZyZWUASA9sbGh0dHBfZ2V0X3R5cGUADhVsbGh0dHBfZ2V0X2h0dHBfbWFqb3IADxVsbGh0dHBfZ2V0X2h0dHBfbWlub3IAEBFsbGh0dHBfZ2V0X21ldGhvZAARFmxsaHR0cF9nZXRfc3RhdHVzX2NvZGUAEhJsbGh0dHBfZ2V0X3VwZ3JhZGUAEwxsbGh0dHBfcmVzZXQAFA5sbGh0dHBfZXhlY3V0ZQAVFGxsaHR0cF9zZXR0aW5nc19pbml0ABYNbGxodHRwX2ZpbmlzaAAXDGxsaHR0cF9wYXVzZQAYDWxsaHR0cF9yZXN1bWUAGRtsbGh0dHBfcmVzdW1lX2FmdGVyX3VwZ3JhZGUAGhBsbGh0dHBfZ2V0X2Vycm5vABsXbGxodHRwX2dldF9lcnJvcl9yZWFzb24AHBdsbGh0dHBfc2V0X2Vycm9yX3JlYXNvbgAdFGxsaHR0cF9nZXRfZXJyb3JfcG9zAB4RbGxodHRwX2Vycm5vX25hbWUAHxJsbGh0dHBfbWV0aG9kX25hbWUAIBJs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ACAAQd0BNgIcQQALewEBfwJAIAAoAgwiAw0AAkAgACgCBEUNACAAIAE2AgQLAkAgACABIAIQxICAgAAiAw0AIAAoAgwPCyAAIAM2AhxBACEDIAAoAgQiAUUNACAAIAEgAiAAKAIIEYGAgIAAACIBRQ0AIAAgAjYCFCAAIAE2AgwgASEDCyADC9z3AQMofwN+BX8jgICAgABBEGsiAySAgICAACABIQQgASEFIAEhBiABIQcgASEIIAEhCSABIQogASELIAEhDCABIQ0gASEOIAEhDyABIRAgASERIAEhEiABIRMgASEUIAEhFSABIRYgASEXIAEhGCABIRkgASEaIAEhGyABIRwgASEdIAEhHiABIR8gASEgIAEhISABISIgASEjIAEhJCABISUgASEmIAEhJyABISggASEpAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAIAAoAhwiKkF/ag7dAdoBAdkBAgMEBQYHCAkKCwwNDtgBDxDXARES1gETFBUWFxgZGhvgAd8BHB0e1QEfICEiIyQl1AEmJygpKiss0wHSAS0u0QHQAS8wMTIzNDU2Nzg5Ojs8PT4/QEFCQ0RFRtsBR0hJSs8BzgFLzQFMzAFNTk9QUVJTVFVW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AkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJA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AAAAAAAAAAkKCwwNDgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADxAREhMUAAtBHiEqDMQDC0ICISsM5QELQgMhKwzkAQtCBCErDOMBC0IFISsM4gELQgYhKwzhAQtCByErDOABC0IIISsM3wELQgkhKwzeAQtCCiErDN0BC0ILISsM3AELQgwhKwzbAQtCDSErDNoBC0IOISsM2QELQg8hKwzYAQtCCiErDNcBC0ILISsM1gELQgwhKwzVAQtCDSErDNQBC0IOISsM0wELQg8hKwzSAQtCACErAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQCAqLQAAQVBqDjflAeQBAAECAwQFBgfmAeYB5gHmAeYB5gHmAQgJCgsMDeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gHmAeYB5gEODxAREhPmAQtCAiErDOQBC0IDISsM4wELQgQhKwziAQtCBSErDOEBC0IGISsM4AELQgchKwzfAQtCCCErDN4BC0IJISsM3QELQgohKwzcAQtCCyErDNsBC0IMISsM2gELQg0hKwzZAQtCDiErDNgBC0IPISsM1wELQgohKwzWAQtCCyErDNUBC0IMISsM1AELQg0hKwzTAQtCDiErDNIBC0IPISsM0QELIABCACAAKQMgIisgAiABIiprrSIsfSItIC0gK1YbNwMgICsgLFYiLkUN0gFBHyEqDMcDCwJAIAEiASACRg0AIABBiYCAgAA2AgggACABNgIEIAEhAUEkISoMrgMLQSAhKgzGAwsgACABIiogAhC+gICAAEF/ag4FtgEAywIB0QHSAQtBESEqDKsDCyAAQQE6AC8gKiEBDMIDCyABIgEgAkcN0gFBJCEqDMIDCyABIicgAkcNHkHGACEqDMEDCyAAIAEiASACELKAgIAAIioN1AEgASEBDLUBCyABIiogAkcNJkHQACEqDL8DCwJAIAEiASACRw0AQSghKgy/AwsgAEEANgIEIABBjICAgAA2AgggACABIAEQsYCAgAAiKg3TASABIQEM2AELAkAgASIqIAJHDQBBKSEqDL4DCyAqLQAAIgFBIEYNFCABQQlHDdMBICpBAWohAQwVCwJAIAEiASACRg0AIAFBAWohAQwXC0EqISoMvAMLAkAg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DQALIAAgKjYCAEGDASEqDOgCC0EAISogAEEANgIAIC4gMmtBA2ohAQzKAQsCQCABIiogAkcNAEGEASEqDOcCCyACICprIAAoAgAiLmohMiAqIQQgLiEBAkADQCAELQAAIAFBg8+AgABqLQAARw3MASABQQRGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIDI2AgBBhAEhKgznAgsgAEEANgIAICogLmtBBWohAUEjISoMyQELAkAgASIEIAJHDQBBhQEhKgzmAgsCQAJAIAQtAABBtH9qDggAzAHMAcwBzAHMAcwBAcwBCyAEQQFqIQFB7wAhKgzNAgsgBEEBaiEBQfAAISoMzAILAkAgASIEIAJHDQBBhgEhKgzlAgsgBC0AAEHFAEcNyQEgBEEBaiEBDIoCCwJAIAEiKiACRw0AQYcBISoM5AILIAIgKmsgACgCACIuaiEyICohBCAuIQECQANAIAQtAAAgAUGIz4CAAGotAABHDckBIAFBA0YNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgMjYCAEGHASEqDOQCCyAAQQA2AgAgKiAua0EEaiEBQS0hKgzGAQsCQCABIiogAkcNAEGIASEqDOMCCyACICprIAAoAgAiLmohMiAqIQQgLiEBAkADQCAELQAAIAFB0M+AgABqLQAARw3IASABQQhGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIDI2AgBBiAEhKgzjAgsgAEEANgIAICogLmtBCWohAUEpISoMxQELAkAgASIBIAJHDQBBiQEhKgziAgtBASEqIAEtAABB3wBHDcQBIAFBAWohAQyIAgsCQCABIiogAkcNAEGKASEqDOECCyACICprIAAoAgAiLmohMiAqIQQgLiEBA0AgBC0AACABQYzPgIAAai0AAEcNxQEgAUEBRg23AiABQQFqIQEgBEEBaiIEIAJHDQALIAAgMjYCAEGKASEqDOACCwJAIAEiKiACRw0AQYsBISoM4AILIAIgKmsgACgCACIuaiEyICohBCAuIQECQANAIAQtAAAgAUGOz4CAAGotAABHDcUBIAFBAkYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgMjYCAEGLASEqDOACCyAAQQA2AgAgKiAua0EDaiEBQQIhKgzCAQsCQCABIiogAkcNAEGMASEqDN8CCyACICprIAAoAgAiLmohMiAqIQQgLiEBAkADQCAELQAAIAFB8M+AgABqLQAARw3EASABQQFGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIDI2AgBBjAEhKgzfAgsgAEEANgIAICogLmtBAmohAUEfISoMwQELAkAgASIqIAJHDQBBjQEhKgzeAgsgAiAqayAAKAIAIi5qITIgKiEEIC4hAQJAA0AgBC0AACABQfLPgIAAai0AAEcNwwEgAUEBRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAyNgIAQY0BISoM3gILIABBADYCACAqIC5rQQJqIQFBCSEqDMABCwJAIAEiBCACRw0AQY4BISoM3QILAkACQCAELQAAQbd/ag4HAMMBwwHDAcMBwwEBwwELIARBAWohAUH4ACEqDMQCCyAEQQFqIQFB+QAhKgzDAgsCQCABIiogAkcNAEGPASEqDNwCCyACICprIAAoAgAiLmohMiAqIQQgLiEBAkADQCAELQAAIAFBkc+AgABqLQAARw3BASABQQVGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIDI2AgBBjwEhKgzcAgsgAEEANgIAICogLmtBBmohAUEYISoMvgELAkAgASIqIAJHDQBBkAEhKgzbAgsgAiAqayAAKAIAIi5qITIgKiEEIC4hAQJAA0AgBC0AACABQZfPgIAAai0AAEcNwAEgAUECRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAyNgIAQZABISoM2wILIABBADYCACAqIC5rQQNqIQFBFyEqDL0BCwJAIAEiKiACRw0AQZEBISoM2gILIAIgKmsgACgCACIuaiEyICohBCAuIQECQANAIAQtAAAgAUGaz4CAAGotAABHDb8BIAFBBkYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgMjYCAEGRASEqDNoCCyAAQQA2AgAgKiAua0EHaiEBQRUhKgy8AQsCQCABIiogAkcNAEGSASEqDNkCCyACICprIAAoAgAiLmohMiAqIQQgLiEBAkADQCAELQAAIAFBoc+AgABqLQAARw2+ASABQQVGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIDI2AgBBkgEhKgzZAgsgAEEANgIAICogLmtBBmohAUEeISoMuwELAkAgASIEIAJHDQBBkwEhKgzYAgsgBC0AAEHMAEcNvAEgBEEBaiEBQQohKgy6AQsCQCAEIAJHDQBBlAEhKgzXAgsCQAJAIAQtAABBv39qDg8AvQG9Ab0BvQG9Ab0BvQG9Ab0BvQG9Ab0BvQEBvQELIARBAWohAUH+ACEqDL4CCyAEQQFqIQFB/wAhKgy9AgsCQCAEIAJHDQBBlQEhKgzWAgsCQAJAIAQtAABBv39qDgMAvAEBvAELIARBAWohAUH9ACEqDL0CCyAEQQFqIQRBgAEhKgy8AgsCQCAFIAJHDQBBlgEhKgzVAgsgAiAFayAAKAIAIipqIS4gBSEEICohAQJAA0AgBC0AACABQafPgIAAai0AAEcNugEgAUEBRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAuNgIAQZYBISoM1QILIABBADYCACAFICprQQJqIQFBCyEqDLcBCwJAIAQgAkcNAEGXASEqDNQCCwJAAkACQAJAIAQtAABBU2oOIwC8AbwBvAG8AbwBvAG8AbwBvAG8AbwBvAG8AbwBvAG8AbwBvAG8AbwBvAG8AbwBAbwBvAG8AbwBvAECvAG8AbwBA7wBCyAEQQFqIQFB+wAhKgy9AgsgBEEBaiEBQfwAISoMvAILIARBAWohBEGBASEqDLsCCyAEQQFqIQVBggEhKgy6AgsCQCAGIAJHDQBBmAEhKgzTAgsgAiAGayAAKAIAIipqIS4gBiEEICohAQJAA0AgBC0AACABQanPgIAAai0AAEcNuAEgAUEERg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAuNgIAQZgBISoM0wILIABBADYCACAGICprQQVqIQFBGSEqDLUBCwJAIAcgAkcNAEGZASEqDNICCyACIAdrIAAoAgAiLmohKiAHIQQgLiEBAkADQCAELQAAIAFBrs+AgABqLQAARw23ASABQQVGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAICo2AgBBmQEhKgzSAgsgAEEANgIAQQYhKiAHIC5rQQZqIQEMtAELAkAgCCACRw0AQZoBISoM0QILIAIgCGsgACgCACIqaiEuIAghBCAqIQECQANAIAQtAAAgAUG0z4CAAGotAABHDbYBIAFBAUYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgLjYCAEGaASEqDNECCyAAQQA2AgAgCCAqa0ECaiEBQRwhKgyzAQsCQCAJIAJHDQBBmwEhKgzQAgsgAiAJayAAKAIAIipqIS4gCSEEICohAQJAA0AgBC0AACABQbbPgIAAai0AAEcNtQEgAUEBRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAuNgIAQZsBISoM0AILIABBADYCACAJICprQQJqIQFBJyEqDLIBCwJAIAQgAkcNAEGcASEqDM8CCwJAAkAgBC0AAEGsf2oOAgABtQELIARBAWohCEGGASEqDLYCCyAEQQFqIQlBhwEhKgy1AgsCQCAKIAJHDQBBnQEhKgzOAgsgAiAKayAAKAIAIipqIS4gCiEEICohAQJAA0AgBC0AACABQbjPgIAAai0AAEcNswEgAUEBRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAuNgIAQZ0BISoMzgILIABBADYCACAKICprQQJqIQFBJiEqDLABCwJAIAsgAkcNAEGeASEqDM0CCyACIAtrIAAoAgAiKmohLiALIQQgKiEBAkADQCAELQAAIAFBus+AgABqLQAARw2yASABQQFGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIC42AgBBngEhKgzNAgsgAEEANgIAIAsgKmtBAmohAUEDISoMrwELAkAgDCACRw0AQZ8BISoMzAILIAIgDGsgACgCACIqaiEuIAwhBCAqIQECQANAIAQtAAAgAUHtz4CAAGotAABHDbEBIAFBAkYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgLjYCAEGfASEqDMwCCyAAQQA2AgAgDCAqa0EDaiEBQQwhKgyuAQsCQCANIAJHDQBBoAEhKgzLAgsgAiANayAAKAIAIipqIS4gDSEEICohAQJAA0AgBC0AACABQbzPgIAAai0AAEcNsAEgAUEDRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAuNgIAQaABISoMywILIABBADYCACANICprQQRqIQFBDSEqDK0BCwJAIAQgAkcNAEGhASEqDMoCCwJAAkAgBC0AAEG6f2oOCwCwAbABsAGwAbABsAGwAbABsAEBsAELIARBAWohDEGLASEqDLECCyAEQQFqIQ1BjAEhKgywAgsCQCAEIAJHDQBBogEhKgzJAgsgBC0AAEHQAEcNrQEgBEEBaiEEDPABCwJAIAQgAkcNAEGjASEqDMgCCwJAAkAgBC0AAEG3f2oOBwGuAa4BrgGuAa4BAK4BCyAEQQFqIQRBjgEhKgyvAgsgBEEBaiEBQSIhKgyqAQsCQCAOIAJHDQBBpAEhKgzHAgsgAiAOayAAKAIAIipqIS4gDiEEICohAQJAA0AgBC0AACABQcDPgIAAai0AAEcNrAEgAUEBRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAuNgIAQaQBISoMxwILIABBADYCACAOICprQQJqIQFBHSEqDKkBCwJAIAQgAkcNAEGlASEqDMYCCwJAAkAgBC0AAEGuf2oOAwCsAQGsAQsgBEEBaiEOQZABISoMrQILIARBAWohAUEEISoMqAELAkAgBCACRw0AQaYBISoMxQILAkACQAJAAkACQCAELQAAQb9/ag4VAK4BrgGuAa4BrgGuAa4BrgGuAa4BAa4BrgECrgGuAQOuAa4BBK4BCyAEQQFqIQRBiAEhKgyvAgsgBEEBaiEKQYkBISoMrgILIARBAWohC0GKASEqDK0CCyAEQQFqIQRBjwEhKgysAgsgBEEBaiEEQZEBISoMqwILAkAgDyACRw0AQacBISoMxAILIAIgD2sgACgCACIqaiEuIA8hBCAqIQECQANAIAQtAAAgAUHtz4CAAGotAABHDakBIAFBAkYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgLjYCAEGnASEqDMQCCyAAQQA2AgAgDyAqa0EDaiEBQREhKgymAQsCQCAQIAJHDQBBqAEhKgzDAgsgAiAQayAAKAIAIipqIS4gECEEICohAQJAA0AgBC0AACABQcLPgIAAai0AAEcNqAEgAUECRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAuNgIAQagBISoMwwILIABBADYCACAQICprQQNqIQFBLCEqDKUBCwJAIBEgAkcNAEGpASEqDMICCyACIBFrIAAoAgAiKmohLiARIQQgKiEBAkADQCAELQAAIAFBxc+AgABqLQAARw2nASABQQRGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIC42AgBBqQEhKgzCAgsgAEEANgIAIBEgKmtBBWohAUErISoMpAELAkAgEiACRw0AQaoBISoMwQILIAIgEmsgACgCACIqaiEuIBIhBCAqIQECQANAIAQtAAAgAUHKz4CAAGotAABHDaYBIAFBAkYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgLjYCAEGqASEqDMECCyAAQQA2AgAgEiAqa0EDaiEBQRQhKgyjAQsCQCAEIAJHDQBBqwEhKgzAAgsCQAJAAkACQCAELQAAQb5/ag4PAAECqAGoAagBqAGoAagBqAGoAagBqAGoAQOoAQsgBEEBaiEPQZMBISoMqQILIARBAWohEEGUASEqDKgCCyAEQQFqIRFBlQEhKgynAgsgBEEBaiESQZYBISoMpgILAkAgBCACRw0AQawBISoMvwILIAQtAABBxQBHDaMBIARBAWohBAznAQsCQCATIAJHDQBBrQEhKgy+AgsgAiATayAAKAIAIipqIS4gEyEEICohAQJAA0AgBC0AACABQc3PgIAAai0AAEcNowEgAUECRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAuNgIAQa0BISoMvgILIABBADYCACATICprQQNqIQFBDiEqDKABCwJAIAQgAkcNAEGuASEqDL0CCyAELQAAQdAARw2hASAEQQFqIQFBJSEqDJ8BCwJAIBQgAkcNAEGvASEqDLwCCyACIBRrIAAoAgAiKmohLiAUIQQgKiEBAkADQCAELQAAIAFB0M+AgABqLQAARw2hASABQQhGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIC42AgBBrwEhKgy8AgsgAEEANgIAIBQgKmtBCWohAUEqISoMngELAkAgBCACRw0AQbABISoMuwILAkACQCAELQAAQat/ag4LAKEBoQGhAaEBoQGhAaEBoQGhAQGhAQsgBEEBaiEEQZoBISoMogILIARBAWohFEGbASEqDKECCwJAIAQgAkcNAEGxASEqDLoCCwJAAkAgBC0AAEG/f2oOFACgAaABoAGgAaABoAGgAaABoAGgAaABoAGgAaABoAGgAaABoAEBoAELIARBAWohE0GZASEqDKECCyAEQQFqIQRBnAEhKgygAgsCQCAVIAJHDQBBsgEhKgy5AgsgAiAVayAAKAIAIipqIS4gFSEEICohAQJAA0AgBC0AACABQdnPgIAAai0AAEcNngEgAUEDRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAuNgIAQbIBISoMuQILIABBADYCACAVICprQQRqIQFBISEqDJsBCwJAIBYgAkcNAEGzASEqDLgCCyACIBZrIAAoAgAiKmohLiAWIQQgKiEBAkADQCAELQAAIAFB3c+AgABqLQAARw2dASABQQZGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIC42AgBBswEhKgy4AgsgAEEANgIAIBYgKmtBB2ohAUEaISoMmgELAkAgBCACRw0AQbQBISoMtwILAkACQAJAIAQtAABBu39qDhEAngGeAZ4BngGeAZ4BngGeAZ4BAZ4BngGeAZ4BngECngELIARBAWohBEGdASEqDJ8CCyAEQQFqIRVBngEhKgyeAgsgBEEBaiEWQZ8BISoMnQILAkAgFyACRw0AQbUBISoMtgILIAIgF2sgACgCACIqaiEuIBchBCAqIQECQANAIAQtAAAgAUHkz4CAAGotAABHDZsBIAFBBUYNASABQQFqIQEgBEEBaiIEIAJHDQALIAAgLjYCAEG1ASEqDLYCCyAAQQA2AgAgFyAqa0EGaiEBQSghKgyYAQsCQCAYIAJHDQBBtgEhKgy1AgsgAiAYayAAKAIAIipqIS4gGCEEICohAQJAA0AgBC0AACABQerPgIAAai0AAEcNmgEgAUECRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAuNgIAQbYBISoMtQILIABBADYCACAYICprQQNqIQFBByEqDJcBCwJAIAQgAkcNAEG3ASEqDLQCCwJAAkAgBC0AAEG7f2oODgCaAZoBmgGaAZoBmgGaAZoBmgGaAZoBmgEBmgELIARBAWohF0GhASEqDJsCCyAEQQFqIRhBogEhKgyaAgsCQCAZIAJHDQBBuAEhKgyzAgsgAiAZayAAKAIAIipqIS4gGSEEICohAQJAA0AgBC0AACABQe3PgIAAai0AAEcNmAEgAUECRg0BIAFBAWohASAEQQFqIgQgAkcNAAsgACAuNgIAQbgBISoMswILIABBADYCACAZICprQQNqIQFBEiEqDJUBCwJAIBogAkcNAEG5ASEqDLICCyACIBprIAAoAgAiKmohLiAaIQQgKiEBAkADQCAELQAAIAFB8M+AgABqLQAARw2XASABQQFGDQEgAUEBaiEBIARBAWoiBCACRw0ACyAAIC42AgBBuQEhKgyyAgsgAEEANgIAIBogKmtBAmohAUEgISoMlAELAkAgGyACRw0AQboBISoMsQILIAIgG2sgACgCACIqaiEuIBshBCAqIQECQANAIAQtAAAgAUHyz4CAAGotAABHDZYBIAFB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DM4BCyAAQQA2AhwgACABNgIUIABBoJmAgAA2AhAgAEEeNgIMQQAhKgzNAQsgAUEBaiEBCyAAQYASOwEqIAAgASACEKiAgIAAIioNASABIQELQccAISoMsQELICpBFUcNiQEgAEHRADYCHCAAIAE2AhQgAEHjl4CAADYCECAAQRU2AgxBACEqDMkBCyAAKAIEISogAEEANgIEAkAgACAqIAEQp4CAgAAiKg0AIAEhAQxiCyAAQdIANgIcIAAgATYCFCAAICo2AgxBACEqDMgBCyAAQQA2AhwgACAuNgIUIABBwaiAgAA2AhAgAEEHNgIMIABBADYCAEEAISoMxwELIAAoAgQhKiAAQQA2AgQCQCAAICogARCngICAACIqDQAgASEBDGELIABB0wA2AhwgACABNgIUIAAgKjYCDEEAISoMxgELQQAhKiAAQQA2AhwgACABNgIUIABBgJGAgAA2AhAgAEEJNgIMDMUBCyAqQRVGDYMBIABBADYCHCAAIAE2AhQgAEGUjYCAADYCECAAQSE2AgxBACEqDMQBC0EBIS9BACEyQQAhLkEBISoLIAAgKjoAKyABQQFqIQECQAJAIAAtAC1BEHENAAJAAkACQCAALQAqDgMBAAIECyAvRQ0DDAILIC4NAQwCCyAyRQ0BCyAAKAIEISogAEEANgIEAkAgACAqIAEQrYCAgAAiKg0AIAEhAQxgCyAAQdgANgIcIAAgATYCFCAAICo2AgxBACEqDMMBCyAAKAIEIQQgAEEANgIEAkAgACAEIAEQrYCAgAAiBA0AIAEhAQyyAQsgAEHZADYCHCAAIAE2AhQgACAENgIMQQAhKgzCAQsgACgCBCEEIABBADYCBAJAIAAgBCABEK2AgIAAIgQNACABIQEMsAELIABB2gA2AhwgACABNgIUIAAgBDYCDEEAISoMwQELIAAoAgQhBCAAQQA2AgQCQCAAIAQgARCtgICAACIEDQAgASEBDK4BCyAAQdwANgIcIAAgATYCFCAAIAQ2AgxBACEqDMABC0EBISoLIAAgKjoAKiABQQFqIQEMXAsgACgCBCEEIABBADYCBAJAIAAgBCABEK2AgIAAIgQNACABIQEMqgELIABB3gA2AhwgACABNgIUIAAgBDYCDEEAISoMvQELIABBADYCACAyIC9rQQRqIQECQCAALQApQSNPDQAgASEBDFwLIABBADYCHCAAIAE2AhQgAEHTiYCAADYCECAAQQg2AgxBACEqDLwBCyAAQQA2AgALQQAhKiAAQQA2AhwgACABNgIUIABBkLOAgAA2AhAgAEEINgIMDLoBCyAAQQA2AgAgMiAva0EDaiEBAkAgAC0AKUEhRw0AIAEhAQxZCyAAQQA2AhwgACABNgIUIABBm4qAgAA2AhAgAEEINgIMQQAhKgy5AQsgAEEANgIAIDIgL2tBBGohAQJAIAAtACkiKkFdakELTw0AIAEhAQxYCwJAICpBBksNAEEBICp0QcoAcUUNACABIQEMWAtBACEqIABBADYCHCAAIAE2AhQgAEH3iYCAADYCECAAQQg2AgwMuAELICpBFUYNdSAAQQA2AhwgACABNgIUIABBuY2AgAA2AhAgAEEaNgIMQQAhKgy3AQsgACgCBCEqIABBADYCBAJAIAAgKiABEKeAgIAAIioNACABIQEMVwsgAEHlADYCHCAAIAE2AhQgACAqNgIMQQAhKgy2AQsgACgCBCEqIABBADYCBAJAIAAgKiABEKeAgIAAIioNACABIQEMTwsgAEHSADYCHCAAIAE2AhQgACAqNgIMQQAhKgy1AQsgACgCBCEqIABBADYCBAJAIAAgKiABEKeAgIAAIioNACABIQEMTwsgAEHTADYCHCAAIAE2AhQgACAqNgIMQQAhKgy0AQsgACgCBCEqIABBADYCBAJAIAAgKiABEKeAgIAAIioNACABIQEMVAsgAEHlADYCHCAAIAE2AhQgACAqNgIMQQAhKgyzAQsgAEEANgIcIAAgATYCFCAAQcaKgIAANgIQIABBBzYCDEEAISoMsgELIAAoAgQhKiAAQQA2AgQCQCAAICogARCngICAACIqDQAgASEBDEsLIABB0gA2AhwgACABNgIUIAAgKjYCDEEAISoMsQELIAAoAgQhKiAAQQA2AgQCQCAAICogARCngICAACIqDQAgASEBDEsLIABB0wA2AhwgACABNgIUIAAgKjYCDEEAISoMsAELIAAoAgQhKiAAQQA2AgQCQCAAICogARCngICAACIqDQAgASEBDFALIABB5QA2AhwgACABNgIUIAAgKjYCDEEAISoMrwELIABBADYCHCAAIAE2AhQgAEHciICAADYCECAAQQc2AgxBACEqDK4BCyAqQT9HDQEgAUEBaiEB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AAAABwAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEludmFsaWQgY2hhciBpbiB1cmwgcXVlcnkAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9ib2R5AENvbnRlbnQtTGVuZ3RoIG92ZXJmbG93AENodW5rIHNpemUgb3ZlcmZsb3cAUmVzcG9uc2Ugb3ZlcmZsb3cASW52YWxpZCBtZXRob2QgZm9yIEhUVFAveC54IHJlcXVlc3QASW52YWxpZCBtZXRob2QgZm9yIFJUU1AveC54IHJlcXVlc3QARXhwZWN0ZWQgU09VUkNFIG1ldGhvZCBmb3IgSUNFL3gueCByZXF1ZXN0AEludmFsaWQgY2hhciBpbiB1cmwgZnJhZ21lbnQgc3RhcnQARXhwZWN0ZWQgZG90AFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fc3RhdHVzAEludmFsaWQgcmVzcG9uc2Ugc3RhdHVzAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMAVXNlciBjYWxsYmFjayBlcnJvcgBgb25fcmVzZXRgIGNhbGxiYWNrIGVycm9yAGBvbl9jaHVua19oZWFkZXJgIGNhbGxiYWNrIGVycm9yAGBvbl9tZXNzYWdlX2JlZ2luYCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfZXh0ZW5zaW9uX3ZhbHVlYCBjYWxsYmFjayBlcnJvcgBgb25fc3RhdHVzX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fdmVyc2lvbl9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX3VybF9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX2NodW5rX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25faGVhZGVyX3ZhbHVlX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fbWVzc2FnZV9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX21ldGhvZF9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX2hlYWRlcl9maWVsZF9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX2NodW5rX2V4dGVuc2lvbl9uYW1lYCBjYWxsYmFjayBlcnJvcgBVbmV4cGVjdGVkIGNoYXIgaW4gdXJsIHNlcnZlcgBJbnZhbGlkIGhlYWRlciB2YWx1ZSBjaGFyAEludmFsaWQgaGVhZGVyIGZpZWxkIGNoYXIAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl92ZXJzaW9uAEludmFsaWQgbWlub3IgdmVyc2lvbgBJbnZhbGlkIG1ham9yIHZlcnNpb24ARXhwZWN0ZWQgc3BhY2UgYWZ0ZXIgdmVyc2lvbgBFeHBlY3RlZCBDUkxGIGFmdGVyIHZlcnNpb24ASW52YWxpZCBIVFRQIHZlcnNpb24ASW52YWxpZCBoZWFkZXIgdG9rZW4AU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl91cmwASW52YWxpZCBjaGFyYWN0ZXJzIGluIHVybABVbmV4cGVjdGVkIHN0YXJ0IGNoYXIgaW4gdXJsAERvdWJsZSBAIGluIHVybABFbXB0eSBDb250ZW50LUxlbmd0aABJbnZhbGlkIGNoYXJhY3RlciBpbiBDb250ZW50LUxlbmd0aABEdXBsaWNhdGUgQ29udGVudC1MZW5ndGgASW52YWxpZCBjaGFyIGluIHVybCBwYXRoAENvbnRlbnQtTGVuZ3RoIGNhbid0IGJlIHByZXNlbnQgd2l0aCBUcmFuc2Zlci1FbmNvZGluZwBJbnZhbGlkIGNoYXJhY3RlciBpbiBjaHVuayBzaXplAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25faGVhZGVyX3ZhbHVlAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fY2h1bmtfZXh0ZW5zaW9uX3ZhbHVlAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMgdmFsdWUATWlzc2luZyBleHBlY3RlZCBMRiBhZnRlciBoZWFkZXIgdmFsdWUASW52YWxpZCBgVHJhbnNmZXItRW5jb2RpbmdgIGhlYWRlciB2YWx1ZQBJbnZhbGlkIGNoYXJhY3RlciBpbiBjaHVuayBleHRlbnNpb25zIHF1b3RlIHZhbHVlAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMgcXVvdGVkIHZhbHVlAFBhdXNlZCBieSBvbl9oZWFkZXJzX2NvbXBsZXRlAEludmFsaWQgRU9GIHN0YXRlAG9uX3Jlc2V0IHBhdXNlAG9uX2NodW5rX2hlYWRlciBwYXVzZQBvbl9tZXNzYWdlX2JlZ2luIHBhdXNlAG9uX2NodW5rX2V4dGVuc2lvbl92YWx1ZSBwYXVzZQBvbl9zdGF0dXNfY29tcGxldGUgcGF1c2UAb25fdmVyc2lvbl9jb21wbGV0ZSBwYXVzZQBvbl91cmxfY29tcGxldGUgcGF1c2UAb25fY2h1bmtfY29tcGxldGUgcGF1c2UAb25faGVhZGVyX3ZhbHVlX2NvbXBsZXRlIHBhdXNlAG9uX21lc3NhZ2VfY29tcGxldGUgcGF1c2UAb25fbWV0aG9kX2NvbXBsZXRlIHBhdXNlAG9uX2hlYWRlcl9maWVsZF9jb21wbGV0ZSBwYXVzZQBvbl9jaHVua19leHRlbnNpb25fbmFtZSBwYXVzZQBVbmV4cGVjdGVkIHNwYWNlIGFmdGVyIHN0YXJ0IGxpbmUAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9jaHVua19leHRlbnNpb25fbmFtZQBJbnZhbGlkIGNoYXJhY3RlciBpbiBjaHVuayBleHRlbnNpb25zIG5hbWUAUGF1c2Ugb24gQ09OTkVDVC9VcGdyYWRlAFBhdXNlIG9uIFBSSS9VcGdyYWRlAEV4cGVjdGVkIEhUVFAvMiBDb25uZWN0aW9uIFByZWZhY2UAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9tZXRob2QARXhwZWN0ZWQgc3BhY2UgYWZ0ZXIgbWV0aG9kAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25faGVhZGVyX2ZpZWxkAFBhdXNlZABJbnZhbGlkIHdvcmQgZW5jb3VudGVyZWQASW52YWxpZCBtZXRob2QgZW5jb3VudGVyZWQAVW5leHBlY3RlZCBjaGFyIGluIHVybCBzY2hlbWEAUmVxdWVzdCBoYXMgaW52YWxpZCBgVHJhbnNmZXItRW5jb2RpbmdgAFNXSVRDSF9QUk9YWQBVU0VfUFJPWFkATUtBQ1RJVklUWQBVTlBST0NFU1NBQkxFX0VOVElUWQBDT1BZAE1PVkVEX1BFUk1BTkVOVExZAFRPT19FQVJMWQBOT1RJRlkARkFJTEVEX0RFUEVOREVOQ1kAQkFEX0dBVEVXQVkAUExBWQBQVVQAQ0hFQ0tPVVQAR0FURVdBWV9USU1FT1VUAFJFUVVFU1RfVElNRU9VVABORVRXT1JLX0NPTk5FQ1RfVElNRU9VVABDT05ORUNUSU9OX1RJTUVPVVQATE9HSU5fVElNRU9VVABORVRXT1JLX1JFQURfVElNRU9VVABQT1NUAE1JU0RJUkVDVEVEX1JFUVVFU1QAQ0xJRU5UX0NMT1NFRF9SRVFVRVNUAENMSUVOVF9DTE9TRURfTE9BRF9CQUxBTkNFRF9SRVFVRVNUAEJBRF9SRVFVRVNUAEhUVFBfUkVRVUVTVF9TRU5UX1RPX0hUVFBTX1BPUlQAUkVQT1JUAElNX0FfVEVBUE9UAFJFU0VUX0NPTlRFTlQATk9fQ09OVEVOVABQQVJUSUFMX0NPTlRFTlQASFBFX0lOVkFMSURfQ09OU1RBTlQASFBFX0NCX1JFU0VUAEdFVABIUEVfU1RSSUNUAENPTkZMSUNUAFRFTVBPUkFSWV9SRURJUkVDVABQRVJNQU5FTlRfUkVESVJFQ1QAQ09OTkVDVABNVUxUSV9TVEFUVVMASFBFX0lOVkFMSURfU1RBVFVTAFRPT19NQU5ZX1JFUVVFU1RTAEVBUkxZX0hJTlRTAFVOQVZBSUxBQkxFX0ZPUl9MRUdBTF9SRUFTT05TAE9QVElPTlMAU1dJVENISU5HX1BST1RPQ09MUwBWQVJJQU5UX0FMU09fTkVHT1RJQVRFUwBNVUxUSVBMRV9DSE9JQ0VTAElOVEVSTkFMX1NFUlZFUl9FUlJPUgBXRUJfU0VSVkVSX1VOS05PV05fRVJST1IAUkFJTEdVTl9FUlJPUgBJREVOVElUWV9QUk9WSURFUl9BVVRIRU5USUNBVElPTl9FUlJPUgBTU0xfQ0VSVElGSUNBVEVfRVJST1IASU5WQUxJRF9YX0ZPUldBUkRFRF9GT1IAU0VUX1BBUkFNRVRFUgBHRVRfUEFSQU1FVEVSAEhQRV9VU0VSAFNFRV9PVEhFUgBIUEVfQ0JfQ0hVTktfSEVBREVSAE1LQ0FMRU5EQVIAU0VUVVAAV0VCX1NFUlZFUl9JU19ET1dOAFRFQVJET1dOAEhQRV9DTE9TRURfQ09OTkVDVElPTgBIRVVSSVNUSUNfRVhQSVJBVElPTgBESVNDT05ORUNURURfT1BFUkFUSU9OAE5PTl9BVVRIT1JJVEFUSVZFX0lORk9STUFUSU9OAEhQRV9JTlZBTElEX1ZFUlNJT04ASFBFX0NCX01FU1NBR0VfQkVHSU4AU0lURV9JU19GUk9aRU4ASFBFX0lOVkFMSURfSEVBREVSX1RPS0VOAElOVkFMSURfVE9LRU4ARk9SQklEREVOAEVOSEFOQ0VfWU9VUl9DQUxNAEhQRV9JTlZBTElEX1VSTABCTE9DS0VEX0JZX1BBUkVOVEFMX0NPTlRST0wATUtDT0wAQUNMAEhQRV9JTlRFUk5BTABSRVFVRVNUX0hFQURFUl9GSUVMRFNfVE9PX0xBUkdFX1VOT0ZGSUNJQUwASFBFX09LAFVOTElOSwBVTkxPQ0sAUFJJAFJFVFJZX1dJVEgASFBFX0lOVkFMSURfQ09OVEVOVF9MRU5HVEgASFBFX1VORVhQRUNURURfQ09OVEVOVF9MRU5HVEgARkxVU0gAUFJPUFBBVENIAE0tU0VBUkNIAFVSSV9UT09fTE9ORwBQUk9DRVNTSU5HAE1JU0NFTExBTkVPVVNfUEVSU0lTVEVOVF9XQVJOSU5HAE1JU0NFTExBTkVPVVNfV0FSTklORwBIUEVfSU5WQUxJRF9UUkFOU0ZFUl9FTkNPRElORwBFeHBlY3RlZCBDUkxGAEhQRV9JTlZBTElEX0NIVU5LX1NJWkUATU9WRQBDT05USU5VRQBIUEVfQ0JfU1RBVFVTX0NPTVBMRVRFAEhQRV9DQl9IRUFERVJTX0NPTVBMRVRFAEhQRV9DQl9WRVJTSU9OX0NPTVBMRVRFAEhQRV9DQl9VUkxfQ09NUExFVEUASFBFX0NCX0NIVU5LX0NPTVBMRVRFAEhQRV9DQl9IRUFERVJfVkFMVUVfQ09NUExFVEUASFBFX0NCX0NIVU5LX0VYVEVOU0lPTl9WQUxVRV9DT01QTEVURQBIUEVfQ0JfQ0hVTktfRVhURU5TSU9OX05BTUVfQ09NUExFVEUASFBFX0NCX01FU1NBR0VfQ09NUExFVEUASFBFX0NCX01FVEhPRF9DT01QTEVURQBIUEVfQ0JfSEVBREVSX0ZJRUxEX0NPTVBMRVRFAERFTEVURQBIUEVfSU5WQUxJRF9FT0ZfU1RBVEUASU5WQUxJRF9TU0xfQ0VSVElGSUNBVEUAUEFVU0UATk9fUkVTUE9OU0UAVU5TVVBQT1JURURfTUVESUFfVFlQRQBHT05FAE5PVF9BQ0NFUFRBQkxFAFNFUlZJQ0VfVU5BVkFJTEFCTEUAUkFOR0VfTk9UX1NBVElTRklBQkxFAE9SSUdJTl9JU19VTlJFQUNIQUJMRQBSRVNQT05TRV9JU19TVEFMRQBQVVJHRQBNRVJHRQBSRVFVRVNUX0hFQURFUl9GSUVMRFNfVE9PX0xBUkdFAFJFUVVFU1RfSEVBREVSX1RPT19MQVJHRQBQQVlMT0FEX1RPT19MQVJHRQBJTlNVRkZJQ0lFTlRfU1RPUkFHRQBIUEVfUEFVU0VEX1VQR1JBREUASFBFX1BBVVNFRF9IMl9VUEdSQURFAFNPVVJDRQBBTk5PVU5DRQBUUkFDRQBIUEVfVU5FWFBFQ1RFRF9TUEFDRQBERVNDUklCRQBVTlNVQlNDUklCRQBSRUNPUkQASFBFX0lOVkFMSURfTUVUSE9EAE5PVF9GT1VORABQUk9QRklORABVTkJJTkQAUkVCSU5EAFVOQVVUSE9SSVpFRABNRVRIT0RfTk9UX0FMTE9XRUQASFRUUF9WRVJTSU9OX05PVF9TVVBQT1JURUQAQUxSRUFEWV9SRVBPUlRFRABBQ0NFUFRFRABOT1RfSU1QTEVNRU5URUQATE9PUF9ERVRFQ1RFRABIUEVfQ1JfRVhQRUNURUQASFBFX0xGX0VYUEVDVEVEAENSRUFURUQASU1fVVNFRABIUEVfUEFVU0VEAFRJTUVPVVRfT0NDVVJFRABQQVlNRU5UX1JFUVVJUkVEAFBSRUNPTkRJVElPTl9SRVFVSVJFRABQUk9YWV9BVVRIRU5USUNBVElPTl9SRVFVSVJFRABORVRXT1JLX0FVVEhFTlRJQ0FUSU9OX1JFUVVJUkVEAExFTkdUSF9SRVFVSVJFRABTU0xfQ0VSVElGSUNBVEVfUkVRVUlSRUQAVVBHUkFERV9SRVFVSVJFRABQQUdFX0VYUElSRUQAUFJFQ09ORElUSU9OX0ZBSUxFRABFWFBFQ1RBVElPTl9GQUlMRUQAUkVWQUxJREFUSU9OX0ZBSUxFRABTU0xfSEFORFNIQUtFX0ZBSUxFRABMT0NLRUQAVFJBTlNGT1JNQVRJT05fQVBQTElFRABOT1RfTU9ESUZJRUQATk9UX0VYVEVOREVEAEJBTkRXSURUSF9MSU1JVF9FWENFRURFRABTSVRFX0lTX09WRVJMT0FERUQASEVBRABFeHBlY3RlZCBIVFRQLwAAXhMAACYTAAAwEAAA8BcAAJ0TAAAVEgAAORcAAPASAAAKEAAAdRIAAK0SAACCEwAATxQAAH8QAACgFQAAIxQAAIkSAACLFAAATRUAANQRAADPFAAAEBgAAMkWAADcFgAAwREAAOAXAAC7FAAAdBQAAHwVAADlFAAACBcAAB8QAABlFQAAoxQAACgVAAACFQAAmRUAACwQAACLGQAATw8AANQOAABqEAAAzhAAAAIXAACJDgAAbhMAABwTAABmFAAAVhcAAMETAADNEwAAbBMAAGgXAABmFwAAXxcAACITAADODwAAaQ4AANgOAABjFgAAyxMAAKoOAAAoFwAAJhcAAMUTAABdFgAA6BEAAGcTAABlEwAA8hYAAHMTAAAdFwAA+RYAAPMRAADPDgAAzhUAAAwSAACzEQAApREAAGEQAAAyFwAAuxMAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQIBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIDAgICAgIAAAICAAICAAICAgICAgICAgIABAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgIAAAACAgICAgICAgICAgICAgICAgICAgICAgICAgICAgACAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAICAgICAAACAgACAgACAgICAgICAgICAAMABAAAAAICAgICAgICAgICAgICAgICAgICAgICAgICAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAAgACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbG9zZWVlcC1hbGl2ZQAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQIBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBY2h1bmtlZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEAAQEBAQEAAAEBAAEBAAEBAQEBAQEBAQEAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABlY3Rpb25lbnQtbGVuZ3Rob25yb3h5LWNvbm5lY3Rpb24AAAAAAAAAAAAAAAAAAAByYW5zZmVyLWVuY29kaW5ncGdyYWRlDQoNCg0KU00NCg0KVFRQL0NFL1RTUC8AAAAAAAAAAAAAAAABAgABAwAAAAAAAAAAAAAAAAAAAAAAAAQBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAQIAAQMAAAAAAAAAAAAAAAAAAAAAAAAEAQEFAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAEAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAQAAAgAAAAAAAAAAAAAAAAAAAAAAAAMEAAAEBAQEBAQEBAQEBAUEBAQEBAQEBAQEBAQABAAGBwQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEAAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwAAAAAAAAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAABAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAIAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAAAAAAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABOT1VOQ0VFQ0tPVVRORUNURVRFQ1JJQkVMVVNIRVRFQURTRUFSQ0hSR0VDVElWSVRZTEVOREFSVkVPVElGWVBUSU9OU0NIU0VBWVNUQVRDSEdFT1JESVJFQ1RPUlRSQ0hQQVJBTUVURVJVUkNFQlNDUklCRUFSRE9XTkFDRUlORE5LQ0tVQlNDUklCRUhUVFAvQURUUC8=' +module.exports = 'AGFzbQEAAAABMAhgAX8Bf2ADf39/AX9gBH9/f38Bf2AAAGADf39/AGABfwBgAn9/AGAGf39/f39/AALLAQgDZW52GHdhc21fb25faGVhZGVyc19jb21wbGV0ZQACA2VudhV3YXNtX29uX21lc3NhZ2VfYmVnaW4AAANlbnYLd2FzbV9vbl91cmwAAQNlbnYOd2FzbV9vbl9zdGF0dXMAAQNlbnYUd2FzbV9vbl9oZWFkZXJfZmllbGQAAQNlbnYUd2FzbV9vbl9oZWFkZXJfdmFsdWUAAQNlbnYMd2FzbV9vbl9ib2R5AAEDZW52GHdhc21fb25fbWVzc2FnZV9jb21wbGV0ZQAAA0ZFAwMEAAAFAAAAAAAABQEFAAUFBQAABgAAAAAGBgYGAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAAABAQcAAAUFAwABBAUBcAESEgUDAQACBggBfwFBgNQECwfRBSIGbWVtb3J5AgALX2luaXRpYWxpemUACRlfX2luZGlyZWN0X2Z1bmN0aW9uX3RhYmxlAQALbGxodHRwX2luaXQAChhsbGh0dHBfc2hvdWxkX2tlZXBfYWxpdmUAQQxsbGh0dHBfYWxsb2MADAZtYWxsb2MARgtsbGh0dHBfZnJlZQANBGZyZWUASA9sbGh0dHBfZ2V0X3R5cGUADhVsbGh0dHBfZ2V0X2h0dHBfbWFqb3IADxVsbGh0dHBfZ2V0X2h0dHBfbWlub3IAEBFsbGh0dHBfZ2V0X21ldGhvZAARFmxsaHR0cF9nZXRfc3RhdHVzX2NvZGUAEhJsbGh0dHBfZ2V0X3VwZ3JhZGUAEwxsbGh0dHBfcmVzZXQAFA5sbGh0dHBfZXhlY3V0ZQAVFGxsaHR0cF9zZXR0aW5nc19pbml0ABYNbGxodHRwX2ZpbmlzaAAXDGxsaHR0cF9wYXVzZQAYDWxsaHR0cF9yZXN1bWUAGRtsbGh0dHBfcmVzdW1lX2FmdGVyX3VwZ3JhZGUAGhBsbGh0dHBfZ2V0X2Vycm5vABsXbGxodHRwX2dldF9lcnJvcl9yZWFzb24AHBdsbGh0dHBfc2V0X2Vycm9yX3JlYXNvbgAdFGxsaHR0cF9nZXRfZXJyb3JfcG9zAB4RbGxodHRwX2Vycm5vX25hbWUAHxJsbGh0dHBfbWV0aG9kX25hbWUAIBJs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IAH9CwMAIABBMGogAf0LAwAgAEEgaiAB/QsDACAAQd0BNgIcQQALewEBfwJAIAAoAgwiAw0AAkAgACgCBEUNACAAIAE2AgQLAkAgACABIAIQxICAgAAiAw0AIAAoAgwPCyAAIAM2AhxBACEDIAAoAgQiAUUNACAAIAEgAiAAKAIIEYGAgIAAACIBRQ0AIAAgAjYCFCAAIAE2AgwgASEDCyADC+TzAQMOfwN+BH8jgICAgABBEGsiAySAgICAACABIQQgASEFIAEhBiABIQcgASEIIAEhCSABIQogASELIAEhDCABIQ0gASEOIAEhDwJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQCAAKAIcIhBBf2oO3QHaAQHZAQIDBAUGBwgJCgsMDQ7YAQ8Q1wEREtYBExQVFhcYGRob4AHfARwdHtUBHyAhIiMkJdQBJicoKSorLNMB0gEtLtEB0AEvMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUbbAUdISUrPAc4BS80BTMwBTU5PUFFSU1RVVldYWVpbXF1eX2BhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ent8fX5/gAGBAYIBgwGEAYUBhgGHAYgBiQGKAYsBjAGNAY4BjwGQAZEB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QAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAIBAOxwEAAQID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YWNrIGVycm9yAGBvbl9jaHVua19leHRlbnNpb25fdmFsdWVgIGNhbGxiYWNrIGVycm9yAGBvbl9zdGF0dXNfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl92ZXJzaW9uX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fdXJsX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl9oZWFkZXJfdmFsdWVfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl9tZXNzYWdlX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fbWV0aG9kX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25faGVhZGVyX2ZpZWxkX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfZXh0ZW5zaW9uX25hbWVgIGNhbGxiYWNrIGVycm9yAFVuZXhwZWN0ZWQgY2hhciBpbiB1cmwgc2VydmVyAEludmFsaWQgaGVhZGVyIHZhbHVlIGNoYXIASW52YWxpZCBoZWFkZXIgZmllbGQgY2hhcgBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX3ZlcnNpb24ASW52YWxpZCBtaW5vciB2ZXJzaW9uAEludmFsaWQgbWFqb3IgdmVyc2lvbgBFeHBlY3RlZCBzcGFjZSBhZnRlciB2ZXJzaW9uAEV4cGVjdGVkIENSTEYgYWZ0ZXIgdmVyc2lvbgBJbnZhbGlkIEhUVFAgdmVyc2lvbgBJbnZhbGlkIGhlYWRlciB0b2tlbgBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX3VybABJbnZhbGlkIGNoYXJhY3RlcnMgaW4gdXJsAFVuZXhwZWN0ZWQgc3RhcnQgY2hhciBpbiB1cmwARG91YmxlIEAgaW4gdXJsAEVtcHR5IENvbnRlbnQtTGVuZ3RoAEludmFsaWQgY2hhcmFjdGVyIGluIENvbnRlbnQtTGVuZ3RoAER1cGxpY2F0ZSBDb250ZW50LUxlbmd0aABJbnZhbGlkIGNoYXIgaW4gdXJsIHBhdGgAQ29udGVudC1MZW5ndGggY2FuJ3QgYmUgcHJlc2VudCB3aXRoIFRyYW5zZmVyLUVuY29kaW5nAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIHNpemUAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9oZWFkZXJfdmFsdWUAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9jaHVua19leHRlbnNpb25fdmFsdWUASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucyB2YWx1ZQBNaXNzaW5nIGV4cGVjdGVkIExGIGFmdGVyIGhlYWRlciB2YWx1ZQBJbnZhbGlkIGBUcmFuc2Zlci1FbmNvZGluZ2AgaGVhZGVyIHZhbHVlAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMgcXVvdGUgdmFsdWUASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucyBxdW90ZWQgdmFsdWUAUGF1c2VkIGJ5IG9uX2hlYWRlcnNfY29tcGxldGUASW52YWxpZCBFT0Ygc3RhdGUAb25fcmVzZXQgcGF1c2UAb25fY2h1bmtfaGVhZGVyIHBhdXNlAG9uX21lc3NhZ2VfYmVnaW4gcGF1c2UAb25fY2h1bmtfZXh0ZW5zaW9uX3ZhbHVlIHBhdXNlAG9uX3N0YXR1c19jb21wbGV0ZSBwYXVzZQBvbl92ZXJzaW9uX2NvbXBsZXRlIHBhdXNlAG9uX3VybF9jb21wbGV0ZSBwYXVzZQBvbl9jaHVua19jb21wbGV0ZSBwYXVzZQBvbl9oZWFkZXJfdmFsdWVfY29tcGxldGUgcGF1c2UAb25fbWVzc2FnZV9jb21wbGV0ZSBwYXVzZQBvbl9tZXRob2RfY29tcGxldGUgcGF1c2UAb25faGVhZGVyX2ZpZWxkX2NvbXBsZXRlIHBhdXNlAG9uX2NodW5rX2V4dGVuc2lvbl9uYW1lIHBhdXNlAFVuZXhwZWN0ZWQgc3BhY2UgYWZ0ZXIgc3RhcnQgbGluZQBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX2NodW5rX2V4dGVuc2lvbl9uYW1lAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMgbmFtZQBQYXVzZSBvbiBDT05ORUNUL1VwZ3JhZGUAUGF1c2Ugb24gUFJJL1VwZ3JhZGUARXhwZWN0ZWQgSFRUUC8yIENvbm5lY3Rpb24gUHJlZmFjZQBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX21ldGhvZABFeHBlY3RlZCBzcGFjZSBhZnRlciBtZXRob2QAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9oZWFkZXJfZmllbGQAUGF1c2VkAEludmFsaWQgd29yZCBlbmNvdW50ZXJlZABJbnZhbGlkIG1ldGhvZCBlbmNvdW50ZXJlZABVbmV4cGVjdGVkIGNoYXIgaW4gdXJsIHNjaGVtYQBSZXF1ZXN0IGhhcyBpbnZhbGlkIGBUcmFuc2Zlci1FbmNvZGluZ2AAU1dJVENIX1BST1hZAFVTRV9QUk9YWQBNS0FDVElWSVRZAFVOUFJPQ0VTU0FCTEVfRU5USVRZAENPUFkATU9WRURfUEVSTUFORU5UTFkAVE9PX0VBUkxZAE5PVElGWQBGQUlMRURfREVQRU5ERU5DWQBCQURfR0FURVdBWQBQTEFZAFBVVABDSEVDS09VVABHQVRFV0FZX1RJTUVPVVQAUkVRVUVTVF9USU1FT1VUAE5FVFdPUktfQ09OTkVDVF9USU1FT1VUAENPTk5FQ1RJT05fVElNRU9VVABMT0dJTl9USU1FT1VUAE5FVFdPUktfUkVBRF9USU1FT1VUAFBPU1QATUlTRElSRUNURURfUkVRVUVTVABDTElFTlRfQ0xPU0VEX1JFUVVFU1QAQ0xJRU5UX0NMT1NFRF9MT0FEX0JBTEFOQ0VEX1JFUVVFU1QAQkFEX1JFUVVFU1QASFRUUF9SRVFVRVNUX1NFTlRfVE9fSFRUUFNfUE9SVABSRVBPUlQASU1fQV9URUFQT1QAUkVTRVRfQ09OVEVOVABOT19DT05URU5UAFBBUlRJQUxfQ09OVEVOVABIUEVfSU5WQUxJRF9DT05TVEFOVABIUEVfQ0JfUkVTRVQAR0VUAEhQRV9TVFJJQ1QAQ09ORkxJQ1QAVEVNUE9SQVJZX1JFRElSRUNUAFBFUk1BTkVOVF9SRURJUkVDVABDT05ORUNUAE1VTFRJX1NUQVRVUwBIUEVfSU5WQUxJRF9TVEFUVVMAVE9PX01BTllfUkVRVUVTVFMARUFSTFlfSElOVFMAVU5BVkFJTEFCTEVfRk9SX0xFR0FMX1JFQVNPTlMAT1BUSU9OUwBTV0lUQ0hJTkdfUFJPVE9DT0xTAFZBUklBTlRfQUxTT19ORUdPVElBVEVTAE1VTFRJUExFX0NIT0lDRVMASU5URVJOQUxfU0VSVkVSX0VSUk9SAFdFQl9TRVJWRVJfVU5LTk9XTl9FUlJPUgBSQUlMR1VOX0VSUk9SAElERU5USVRZX1BST1ZJREVSX0FVVEhFTlRJQ0FUSU9OX0VSUk9SAFNTTF9DRVJUSUZJQ0FURV9FUlJPUgBJTlZBTElEX1hfRk9SV0FSREVEX0ZPUgBTRVRfUEFSQU1FVEVSAEdFVF9QQVJBTUVURVIASFBFX1VTRVIAU0VFX09USEVSAEhQRV9DQl9DSFVOS19IRUFERVIATUtDQUxFTkRBUgBTRVRVUABXRUJfU0VSVkVSX0lTX0RPV04AVEVBUkRPV04ASFBFX0NMT1NFRF9DT05ORUNUSU9OAEhFVVJJU1RJQ19FWFBJUkFUSU9OAERJU0NPTk5FQ1RFRF9PUEVSQVRJT04ATk9OX0FVVEhPUklUQVRJVkVfSU5GT1JNQVRJT04ASFBFX0lOVkFMSURfVkVSU0lPTgBIUEVfQ0JfTUVTU0FHRV9CRUdJTgBTSVRFX0lTX0ZST1pFTgBIUEVfSU5WQUxJRF9IRUFERVJfVE9LRU4ASU5WQUxJRF9UT0tFTgBGT1JCSURERU4ARU5IQU5DRV9ZT1VSX0NBTE0ASFBFX0lOVkFMSURfVVJMAEJMT0NLRURfQllfUEFSRU5UQUxfQ09OVFJPTABNS0NPTABBQ0wASFBFX0lOVEVSTkFMAFJFUVVFU1RfSEVBREVSX0ZJRUxEU19UT09fTEFSR0VfVU5PRkZJQ0lBTABIUEVfT0sAVU5MSU5LAFVOTE9DSwBQUkkAUkVUUllfV0lUSABIUEVfSU5WQUxJRF9DT05URU5UX0xFTkdUSABIUEVfVU5FWFBFQ1RFRF9DT05URU5UX0xFTkdUSABGTFVTSABQUk9QUEFUQ0gATS1TRUFSQ0gAVVJJX1RPT19MT05HAFBST0NFU1NJTkcATUlTQ0VMTEFORU9VU19QRVJTSVNURU5UX1dBUk5JTkcATUlTQ0VMTEFORU9VU19XQVJOSU5HAEhQRV9JTlZBTElEX1RSQU5TRkVSX0VOQ09ESU5HAEV4cGVjdGVkIENSTEYASFBFX0lOVkFMSURfQ0hVTktfU0laRQBNT1ZFAENPTlRJTlVFAEhQRV9DQl9TVEFUVVNfQ09NUExFVEUASFBFX0NCX0hFQURFUlNfQ09NUExFVEUASFBFX0NCX1ZFUlNJT05fQ09NUExFVEUASFBFX0NCX1VSTF9DT01QTEVURQBIUEVfQ0JfQ0hVTktfQ09NUExFVEUASFBFX0NCX0hFQURFUl9WQUxVRV9DT01QTEVURQBIUEVfQ0JfQ0hVTktfRVhURU5TSU9OX1ZBTFVFX0NPTVBMRVRFAEhQRV9DQl9DSFVOS19FWFRFTlNJT05fTkFNRV9DT01QTEVURQBIUEVfQ0JfTUVTU0FHRV9DT01QTEVURQBIUEVfQ0JfTUVUSE9EX0NPTVBMRVRFAEhQRV9DQl9IRUFERVJfRklFTERfQ09NUExFVEUAREVMRVRFAEhQRV9JTlZBTElEX0VPRl9TVEFURQBJTlZBTElEX1NTTF9DRVJUSUZJQ0FURQBQQVVTRQBOT19SRVNQT05TRQBVTlNVUFBPUlRFRF9NRURJQV9UWVBFAEdPTkUATk9UX0FDQ0VQVEFCTEUAU0VSVklDRV9VTkFWQUlMQUJMRQBSQU5HRV9OT1RfU0FUSVNGSUFCTEUAT1JJR0lOX0lTX1VOUkVBQ0hBQkxFAFJFU1BPTlNFX0lTX1NUQUxFAFBVUkdFAE1FUkdFAFJFUVVFU1RfSEVBREVSX0ZJRUxEU19UT09fTEFSR0UAUkVRVUVTVF9IRUFERVJfVE9PX0xBUkdFAFBBWUxPQURfVE9PX0xBUkdFAElOU1VGRklDSUVOVF9TVE9SQUdFAEhQRV9QQVVTRURfVVBHUkFERQBIUEVfUEFVU0VEX0gyX1VQR1JBREUAU09VUkNFAEFOTk9VTkNFAFRSQUNFAEhQRV9VTkVYUEVDVEVEX1NQQUNFAERFU0NSSUJFAFVOU1VCU0NSSUJFAFJFQ09SRABIUEVfSU5WQUxJRF9NRVRIT0QATk9UX0ZPVU5EAFBST1BGSU5EAFVOQklORABSRUJJTkQAVU5BVVRIT1JJWkVEAE1FVEhPRF9OT1RfQUxMT1dFRABIVFRQX1ZFUlNJT05fTk9UX1NVUFBPUlRFRABBTFJFQURZX1JFUE9SVEVEAEFDQ0VQVEVEAE5PVF9JTVBMRU1FTlRFRABMT09QX0RFVEVDVEVEAEhQRV9DUl9FWFBFQ1RFRABIUEVfTEZfRVhQRUNURUQAQ1JFQVRFRABJTV9VU0VEAEhQRV9QQVVTRUQAVElNRU9VVF9PQ0NVUkVEAFBBWU1FTlRfUkVRVUlSRUQAUFJFQ09ORElUSU9OX1JFUVVJUkVEAFBST1hZX0FVVEhFTlRJQ0FUSU9OX1JFUVVJUkVEAE5FVFdPUktfQVVUSEVOVElDQVRJT05fUkVRVUlSRUQATEVOR1RIX1JFUVVJUkVEAFNTTF9DRVJUSUZJQ0FURV9SRVFVSVJFRABVUEdSQURFX1JFUVVJUkVEAFBBR0VfRVhQSVJFRABQUkVDT05ESVRJT05fRkFJTEVEAEVYUEVDVEFUSU9OX0ZBSUxFRABSRVZBTElEQVRJT05fRkFJTEVEAFNTTF9IQU5EU0hBS0VfRkFJTEVEAExPQ0tFRABUUkFOU0ZPUk1BVElPTl9BUFBMSUVEAE5PVF9NT0RJRklFRABOT1RfRVhURU5ERUQAQkFORFdJRFRIX0xJTUlUX0VYQ0VFREVEAFNJVEVfSVNfT1ZFUkxPQURFRABIRUFEAEV4cGVjdGVkIEhUVFAvAABeEwAAJhMAADAQAADwFwAAnRMAABUSAAA5FwAA8BIAAAoQAAB1EgAArRIAAIITAABPFAAAfxAAAKAVAAAjFAAAiRIAAIsUAABNFQAA1BEAAM8UAAAQGAAAyRYAANwWAADBEQAA4BcAALsUAAB0FAAAfBUAAOUUAAAIFwAAHxAAAGUVAACjFAAAKBUAAAIVAACZFQAALBAAAIsZAABPDwAA1A4AAGoQAADOEAAAAhcAAIkOAABuEwAAHBMAAGYUAABWFwAAwRMAAM0TAABsEwAAaBcAAGYXAABfFwAAIhMAAM4PAABpDgAA2A4AAGMWAADLEwAAqg4AACgXAAAmFwAAxRMAAF0WAADoEQAAZxMAAGUTAADyFgAAcxMAAB0XAAD5FgAA8xEAAM8OAADOFQAADBIAALMRAAClEQAAYRAAADIXAAC7EwAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAgEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgMCAgICAgAAAgIAAgIAAgICAgICAgICAgAEAAAAAAACAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAICAgICAgICAgICAgICAgICAgICAgICAgICAgICAAIAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAgICAgIAAAICAAICAAICAgICAgICAgIAAwAEAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgIAAAACAgICAgICAgICAgICAgICAgICAgICAgICAgICAgACAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABsb3NlZWVwLWFsaXZlAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAgEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQFjaHVua2VkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQABAQEBAQAAAQEAAQEAAQEBAQEBAQEBAQAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGVjdGlvbmVudC1sZW5ndGhvbnJveHktY29ubmVjdGlvbgAAAAAAAAAAAAAAAAAAAHJhbnNmZXItZW5jb2RpbmdwZ3JhZGUNCg0KDQpTTQ0KDQpUVFAvQ0UvVFNQLwAAAAAAAAAAAAAAAAECAAEDAAAAAAAAAAAAAAAAAAAAAAAABAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAABAgABAwAAAAAAAAAAAAAAAAAAAAAAAAQBAQUBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAQAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAABAAACAAAAAAAAAAAAAAAAAAAAAAAAAwQAAAQEBAQEBAQEBAQEBQQEBAQEBAQEBAQEBAAEAAYHBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQABAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAAAAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAgAAAAACAAAAAAAAAAAAAAAAAAAAAAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwAAAAAAAAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE5PVU5DRUVDS09VVE5FQ1RFVEVDUklCRUxVU0hFVEVBRFNFQVJDSFJHRUNUSVZJVFlMRU5EQVJWRU9USUZZUFRJT05TQ0hTRUFZU1RBVENIR0VPUkRJUkVDVE9SVFJDSFBBUkFNRVRFUlVSQ0VCU0NSSUJFQVJET1dOQUNFSU5ETktDS1VCU0NSSUJFSFRUUC9BRFRQLw==' diff --git a/lib/llhttp/llhttp_simd.wasm b/lib/llhttp/llhttp_simd.wasm index 93cdf56803ad435cf520c0763186546e4a7ccfd6..7aa0b55fe59ef501ccd82e4caa6c4056290df721 100755 GIT binary patch literal 55450 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

N7M)r78>o3Qifz&hWQup zW-$b}Lm@H@O^l^DKsZ`5{%QHlj2#yrSIFYys@}d;2OczIQ8<}bRC6!~&nY&}!J8Sv z%vk<*%;*1Hdc<-eR~O@DmFuYZqN{zDjtS(Sa5=mN{s|QpqYOn*p8tyCGXKE)|3Unt zHH5;5T?7rRG}C`C{eN%y(o}tj)QD4XUm_exAN)72{ttw);CBE3 literal 55985 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 ug$r_+jF80!%<*9IFPbLimK8f?VP Date: Mon, 26 Jun 2023 21:31:02 -0400 Subject: [PATCH 083/259] fix: forward error reason to fetch controller (#2172) * fix: forward error reason to fetch controller Fixes https://github.com/nodejs/undici/issues/2171 * fix: update spec text * fixup --- lib/fetch/index.js | 13 +++++++------ test/fetch/issue-2171.js | 25 +++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 6 deletions(-) create mode 100644 test/fetch/issue-2171.js diff --git a/lib/fetch/index.js b/lib/fetch/index.js index 7388da51ffc..f69371a941d 100644 --- a/lib/fetch/index.js +++ b/lib/fetch/index.js @@ -180,14 +180,15 @@ async function fetch (input, init = {}) { // 1. Set locallyAborted to true. locallyAborted = true - // 2. Abort the fetch() call with p, request, responseObject, + // 2. Assert: controller is non-null. + assert(controller != null) + + // 3. Abort controller with requestObject’s signal’s abort reason. + controller.abort(requestObject.signal.reason) + + // 4. Abort the fetch() call with p, request, responseObject, // and requestObject’s signal’s abort reason. abortFetch(p, request, responseObject, requestObject.signal.reason) - - // 3. If controller is not null, then abort controller. - if (controller != null) { - controller.abort() - } }, { once: true } ) diff --git a/test/fetch/issue-2171.js b/test/fetch/issue-2171.js new file mode 100644 index 00000000000..b04ae0e6c38 --- /dev/null +++ b/test/fetch/issue-2171.js @@ -0,0 +1,25 @@ +'use strict' + +const { fetch } = require('../..') +const { DOMException } = require('../../lib/fetch/constants') +const { once } = require('events') +const { createServer } = require('http') +const { test } = require('tap') + +test('error reason is forwarded - issue #2171', { skip: !AbortSignal.timeout }, async (t) => { + const server = createServer(() => {}).listen(0) + + t.teardown(server.close.bind(server)) + await once(server, 'listening') + + const timeout = AbortSignal.timeout(100) + await t.rejects( + fetch(`http://localhost:${server.address().port}`, { + signal: timeout + }), + { + name: 'TimeoutError', + code: DOMException.TIMEOUT_ERR + } + ) +}) From 06f51a62151bb9c4950f38eed9536591421b4d96 Mon Sep 17 00:00:00 2001 From: Khafra Date: Fri, 7 Jul 2023 03:14:43 -0400 Subject: [PATCH 084/259] stricter types for bodymixin.json (#2181) --- test/types/dispatcher.test-d.ts | 2 +- test/types/readable.test-d.ts | 2 +- types/dispatcher.d.ts | 2 +- types/readable.d.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/types/dispatcher.test-d.ts b/test/types/dispatcher.test-d.ts index 2f31b91c987..cd4ebfd68e3 100644 --- a/test/types/dispatcher.test-d.ts +++ b/test/types/dispatcher.test-d.ts @@ -119,5 +119,5 @@ declare const { body }: Dispatcher.ResponseData; expectType>(body.blob()) expectType>(body.formData()) expectType>(body.text()) - expectType>(body.json()) + expectType>(body.json()) } diff --git a/test/types/readable.test-d.ts b/test/types/readable.test-d.ts index 671f6f3c6ef..d004b706569 100644 --- a/test/types/readable.test-d.ts +++ b/test/types/readable.test-d.ts @@ -15,7 +15,7 @@ expectAssignable(new BodyReadable()) expectAssignable>(readable.text()) // json - expectAssignable>(readable.json()) + expectAssignable>(readable.json()) // blob expectAssignable>(readable.blob()) diff --git a/types/dispatcher.d.ts b/types/dispatcher.d.ts index 412520386f3..7f621371f86 100644 --- a/types/dispatcher.d.ts +++ b/types/dispatcher.d.ts @@ -229,7 +229,7 @@ declare namespace Dispatcher { arrayBuffer(): Promise; blob(): Promise; formData(): Promise; - json(): Promise; + json(): Promise; text(): Promise; } diff --git a/types/readable.d.ts b/types/readable.d.ts index 032b53b01f9..4549a8c87e8 100644 --- a/types/readable.d.ts +++ b/types/readable.d.ts @@ -18,7 +18,7 @@ declare class BodyReadable extends Readable { /** Consumes and returns the body as a JavaScript Object * https://fetch.spec.whatwg.org/#dom-body-json */ - json(): Promise + json(): Promise /** Consumes and returns the body as a Blob * https://fetch.spec.whatwg.org/#dom-body-blob From 22bdbd8c7820035276b4e876daccef513c29f5c4 Mon Sep 17 00:00:00 2001 From: Paolo Insogna Date: Fri, 7 Jul 2023 21:29:39 +0200 Subject: [PATCH 085/259] chore: Renable autoSelectFamily tests. (#2180) * chore: Renable autoSelectFamily tests. * chore: Disable CI for Node 14 on Windows. --- .github/workflows/nodejs.yml | 2 + test/autoselectfamily.js | 128 ++++++++++++++++++----------------- 2 files changed, 69 insertions(+), 61 deletions(-) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 0afd197845d..463a04b1dc7 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -21,6 +21,8 @@ jobs: - runs-on: ubuntu-latest node-version: 16.8 exclude: | + - runs-on: windows-latest + node-version: 14 - runs-on: windows-latest node-version: 16 automerge: diff --git a/test/autoselectfamily.js b/test/autoselectfamily.js index 6588123d2c7..aa9a751365a 100644 --- a/test/autoselectfamily.js +++ b/test/autoselectfamily.js @@ -6,10 +6,18 @@ const { Resolver } = require('dns') const dnsPacket = require('dns-packet') const { createServer } = require('http') const { Client, Agent, request } = require('..') -const { nodeHasAutoSelectFamily, nodeMajor } = require('../lib/core/util') - -if (nodeMajor >= 20) { - skip('some tests are failing') +const { nodeHasAutoSelectFamily } = require('../lib/core/util') + +/* + * IMPORTANT + * + * As only some version of Node have autoSelectFamily enabled by default (>= 20), make sure the option is always + * explicitly passed in tests in this file to avoid compatibility problems across release lines. + * + */ + +if (!nodeHasAutoSelectFamily) { + skip('autoSelectFamily is not supportee') process.exit() } @@ -59,84 +67,82 @@ function createDnsServer (ipv6Addr, ipv4Addr, cb) { }) } -if (nodeHasAutoSelectFamily) { - test('with autoSelectFamily enable the request succeeds when using request', (t) => { - t.plan(3) +test('with autoSelectFamily enable the request succeeds when using request', (t) => { + t.plan(3) - createDnsServer('::1', '127.0.0.1', function (_, { dnsServer, lookup }) { - const server = createServer((req, res) => { - res.end('hello') - }) + createDnsServer('::1', '127.0.0.1', function (_, { dnsServer, lookup }) { + const server = createServer((req, res) => { + res.end('hello') + }) - t.teardown(() => { - server.close() - dnsServer.close() - }) + t.teardown(() => { + server.close() + dnsServer.close() + }) - server.listen(0, '127.0.0.1', () => { - const agent = new Agent({ connect: { lookup }, autoSelectFamily: true }) + server.listen(0, '127.0.0.1', () => { + const agent = new Agent({ connect: { lookup }, autoSelectFamily: true }) - request( - `http://example.org:${server.address().port}/`, { - method: 'GET', - dispatcher: agent - }, (err, { statusCode, body }) => { - t.error(err) + request( + `http://example.org:${server.address().port}/`, { + method: 'GET', + dispatcher: agent + }, (err, { statusCode, body }) => { + t.error(err) - let response = Buffer.alloc(0) + let response = Buffer.alloc(0) - body.on('data', chunk => { - response = Buffer.concat([response, chunk]) - }) + body.on('data', chunk => { + response = Buffer.concat([response, chunk]) + }) - body.on('end', () => { - t.strictSame(statusCode, 200) - t.strictSame(response.toString('utf-8'), 'hello') - }) + body.on('end', () => { + t.strictSame(statusCode, 200) + t.strictSame(response.toString('utf-8'), 'hello') }) - }) + }) }) }) +}) - test('with autoSelectFamily enable the request succeeds when using a client', (t) => { - t.plan(3) +test('with autoSelectFamily enable the request succeeds when using a client', (t) => { + t.plan(3) - createDnsServer('::1', '127.0.0.1', function (_, { dnsServer, lookup }) { - const server = createServer((req, res) => { - res.end('hello') - }) + createDnsServer('::1', '127.0.0.1', function (_, { dnsServer, lookup }) { + const server = createServer((req, res) => { + res.end('hello') + }) - t.teardown(() => { - server.close() - dnsServer.close() - }) + t.teardown(() => { + server.close() + dnsServer.close() + }) - server.listen(0, '127.0.0.1', () => { - const client = new Client(`http://example.org:${server.address().port}`, { connect: { lookup }, autoSelectFamily: true }) + server.listen(0, '127.0.0.1', () => { + const client = new Client(`http://example.org:${server.address().port}`, { connect: { lookup }, autoSelectFamily: true }) - t.teardown(client.destroy.bind(client)) + t.teardown(client.destroy.bind(client)) - client.request({ - path: '/', - method: 'GET' - }, (err, { statusCode, body }) => { - t.error(err) + client.request({ + path: '/', + method: 'GET' + }, (err, { statusCode, body }) => { + t.error(err) - let response = Buffer.alloc(0) + let response = Buffer.alloc(0) - body.on('data', chunk => { - response = Buffer.concat([response, chunk]) - }) + body.on('data', chunk => { + response = Buffer.concat([response, chunk]) + }) - body.on('end', () => { - t.strictSame(statusCode, 200) - t.strictSame(response.toString('utf-8'), 'hello') - }) + body.on('end', () => { + t.strictSame(statusCode, 200) + t.strictSame(response.toString('utf-8'), 'hello') }) }) }) }) -} +}) test('with autoSelectFamily disabled the request fails when using request', (t) => { t.plan(1) @@ -152,7 +158,7 @@ test('with autoSelectFamily disabled the request fails when using request', (t) }) server.listen(0, '127.0.0.1', () => { - const agent = new Agent({ connect: { lookup } }) + const agent = new Agent({ connect: { lookup, autoSelectFamily: false } }) request(`http://example.org:${server.address().port}`, { method: 'GET', @@ -178,7 +184,7 @@ test('with autoSelectFamily disabled the request fails when using a client', (t) }) server.listen(0, '127.0.0.1', () => { - const client = new Client(`http://example.org:${server.address().port}`, { connect: { lookup } }) + const client = new Client(`http://example.org:${server.address().port}`, { connect: { lookup, autoSelectFamily: false } }) t.teardown(client.destroy.bind(client)) client.request({ From 151e9a96b7c26327969a7e262a9e96c1cbe0fe1e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 Jul 2023 08:16:06 +0000 Subject: [PATCH 086/259] build(deps): bump actions/dependency-review-action from 3.0.4 to 3.0.6 (#2147) Bumps [actions/dependency-review-action](https://github.com/actions/dependency-review-action) from 3.0.4 to 3.0.6. - [Release notes](https://github.com/actions/dependency-review-action/releases) - [Commits](https://github.com/actions/dependency-review-action/compare/f46c48ed6d4f1227fb2d9ea62bf6bcbed315589e...1360a344ccb0ab6e9475edef90ad2f46bf8003b1) --- updated-dependencies: - dependency-name: actions/dependency-review-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/dependency-review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index f3ab3392afd..ae7c80248c6 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -24,4 +24,4 @@ jobs: - name: 'Checkout Repository' uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 - name: 'Dependency Review' - uses: actions/dependency-review-action@f46c48ed6d4f1227fb2d9ea62bf6bcbed315589e # v3.0.4 + uses: actions/dependency-review-action@1360a344ccb0ab6e9475edef90ad2f46bf8003b1 # v3.0.6 From e22448d8de7e8d051e9314d59774821cffd0f907 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 Jul 2023 08:26:35 +0000 Subject: [PATCH 087/259] build(deps): bump github/codeql-action from 2.3.2 to 2.20.3 (#2185) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2.3.2 to 2.20.3. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v2.3.2...46ed16ded91731b2df79a2893d3aea8e9f03b5c4) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 6 +++--- .github/workflows/scorecard.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 2537953d4a6..b1f19acf553 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -50,7 +50,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@29b1f65c5e92e24fe6b6647da1eaabe529cec70f # v2.3.3 + uses: github/codeql-action/init@46ed16ded91731b2df79a2893d3aea8e9f03b5c4 # v2.3.3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -60,7 +60,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@29b1f65c5e92e24fe6b6647da1eaabe529cec70f # v2.3.3 + uses: github/codeql-action/autobuild@46ed16ded91731b2df79a2893d3aea8e9f03b5c4 # v2.3.3 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -73,6 +73,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@29b1f65c5e92e24fe6b6647da1eaabe529cec70f # v2.3.3 + uses: github/codeql-action/analyze@46ed16ded91731b2df79a2893d3aea8e9f03b5c4 # v2.3.3 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index adfd0076908..1d6a8bf5328 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -51,6 +51,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@f3feb00acb00f31a6f60280e6ace9ca31d91c76a # v2.3.2 + uses: github/codeql-action/upload-sarif@46ed16ded91731b2df79a2893d3aea8e9f03b5c4 # v2.20.3 with: sarif_file: results.sarif From eb24cc8e2d2276e6c1f394c1de07d22e9141c544 Mon Sep 17 00:00:00 2001 From: Gary Wilber <41303831+GaryWilber@users.noreply.github.com> Date: Fri, 14 Jul 2023 12:45:32 -0700 Subject: [PATCH 088/259] fix: fetch resource timing performance entry names should be strings (#2188) * fix: fetch resource timing performance entry names should be strings * change .toString() to .href --- lib/fetch/index.js | 2 +- test/fetch/resource-timing.js | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/fetch/index.js b/lib/fetch/index.js index f69371a941d..2b7fe3632ea 100644 --- a/lib/fetch/index.js +++ b/lib/fetch/index.js @@ -320,7 +320,7 @@ function finalizeAndReportTiming (response, initiatorType = 'other') { // https://w3c.github.io/resource-timing/#dfn-mark-resource-timing function markResourceTiming (timingInfo, originalURL, initiatorType, globalThis, cacheState) { if (nodeMajor > 18 || (nodeMajor === 18 && nodeMinor >= 2)) { - performance.markResourceTiming(timingInfo, originalURL, initiatorType, globalThis, cacheState) + performance.markResourceTiming(timingInfo, originalURL.href, initiatorType, globalThis, cacheState) } } diff --git a/test/fetch/resource-timing.js b/test/fetch/resource-timing.js index 353f07f685b..25b3bcaafbb 100644 --- a/test/fetch/resource-timing.js +++ b/test/fetch/resource-timing.js @@ -13,22 +13,24 @@ const { const skip = nodeMajor < 18 || (nodeMajor === 18 && nodeMinor < 2) test('should create a PerformanceResourceTiming after each fetch request', { skip }, (t) => { - t.plan(6) + t.plan(8) const obs = new PerformanceObserver(list => { + const expectedResourceEntryName = `http://localhost:${server.address().port}/` + const entries = list.getEntries() t.equal(entries.length, 1) const [entry] = entries - t.same(entry.name, { - href: `http://localhost:${server.address().port}/`, - origin: `http://localhost:${server.address().port}`, - protocol: 'http' - }) + t.same(entry.name, expectedResourceEntryName) t.strictSame(entry.entryType, 'resource') t.ok(entry.duration >= 0) t.ok(entry.startTime >= 0) + const entriesByName = list.getEntriesByName(expectedResourceEntryName) + t.equal(entriesByName.length, 1) + t.strictSame(entriesByName[0], entry) + obs.disconnect() performance.clearResourceTimings() }) From 4140ab5ed508bb7473bd3f1bf2f04f2f5845f288 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jul 2023 09:03:40 +0000 Subject: [PATCH 089/259] build(deps): bump actions/checkout from 3.5.2 to 3.5.3 (#2176) Bumps [actions/checkout](https://github.com/actions/checkout) from 3.5.2 to 3.5.3. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/8e5e7e5ab8b370d6c329ec480221332ada57f0ab...c85c95e3d7251135ab7dc9ce3241c5835cc595a9) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/bench.yml | 4 ++-- .github/workflows/codeql.yml | 2 +- .github/workflows/dependency-review.yml | 2 +- .github/workflows/fuzz.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/scorecard.yml | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index ccc9aee11e7..bff3c5ef7d2 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Code - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 with: persist-credentials: false ref: ${{ github.base_ref }} @@ -30,7 +30,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Code - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 with: persist-credentials: false - name: Setup Node diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index b1f19acf553..a58d05c3a84 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -46,7 +46,7 @@ jobs: egress-policy: audit - name: Checkout repository - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index ae7c80248c6..1ccabd65476 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -22,6 +22,6 @@ jobs: egress-policy: audit - name: 'Checkout Repository' - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - name: 'Dependency Review' uses: actions/dependency-review-action@1360a344ccb0ab6e9475edef90ad2f46bf8003b1 # v3.0.6 diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 9119601908f..85247144889 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 with: persist-credentials: false diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 25e4ccad312..e00f3fe7bf0 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -7,7 +7,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 with: persist-credentials: false - uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 1d6a8bf5328..085a50a0025 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -29,7 +29,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 with: persist-credentials: false From 0b3c47c4af621b76652f2a30ccca620e37947ade Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Jul 2023 21:36:47 +0000 Subject: [PATCH 090/259] build(deps): bump fastify/github-action-merge-dependabot (#2177) Bumps [fastify/github-action-merge-dependabot](https://github.com/fastify/github-action-merge-dependabot) from 3.8.0 to 3.9.0. - [Release notes](https://github.com/fastify/github-action-merge-dependabot/releases) - [Commits](https://github.com/fastify/github-action-merge-dependabot/compare/f4fba1d411acf25f03affabc4ac209291cb9d6da...d37100b180dfd816bb1d7e4fbb544b3c734957a1) --- updated-dependencies: - dependency-name: fastify/github-action-merge-dependabot dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/nodejs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 463a04b1dc7..1c41627e289 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -34,6 +34,6 @@ jobs: pull-requests: write contents: write steps: - - uses: fastify/github-action-merge-dependabot@f4fba1d411acf25f03affabc4ac209291cb9d6da # v3.8.0 + - uses: fastify/github-action-merge-dependabot@d37100b180dfd816bb1d7e4fbb544b3c734957a1 # v3.9.0 with: github-token: ${{ secrets.GITHUB_TOKEN }} From b287cae825b2d464f9e73cb8110b51b67271afb5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Jul 2023 21:37:51 +0000 Subject: [PATCH 091/259] build(deps): bump ossf/scorecard-action from 2.1.3 to 2.2.0 (#2178) Bumps [ossf/scorecard-action](https://github.com/ossf/scorecard-action) from 2.1.3 to 2.2.0. - [Release notes](https://github.com/ossf/scorecard-action/releases) - [Changelog](https://github.com/ossf/scorecard-action/blob/main/RELEASE.md) - [Commits](https://github.com/ossf/scorecard-action/compare/80e868c13c90f172d68d1f4501dee99e2479f7af...08b4669551908b1024bb425080c797723083c031) --- updated-dependencies: - dependency-name: ossf/scorecard-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/scorecard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 085a50a0025..d7eef45dccc 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -34,7 +34,7 @@ jobs: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@80e868c13c90f172d68d1f4501dee99e2479f7af # v2.1.3 + uses: ossf/scorecard-action@08b4669551908b1024bb425080c797723083c031 # v2.2.0 with: results_file: results.sarif results_format: sarif From 512cdadc403874571cd5035a6c41debab1165310 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Jul 2023 21:38:07 +0000 Subject: [PATCH 092/259] build(deps): bump step-security/harden-runner from 2.4.0 to 2.4.1 (#2175) Bumps [step-security/harden-runner](https://github.com/step-security/harden-runner) from 2.4.0 to 2.4.1. - [Release notes](https://github.com/step-security/harden-runner/releases) - [Commits](https://github.com/step-security/harden-runner/compare/128a63446a954579617e875aaab7d2978154e969...55d479fb1c5bcad5a4f9099a5d9f37c8857b2845) --- updated-dependencies: - dependency-name: step-security/harden-runner dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 2 +- .github/workflows/dependency-review.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index a58d05c3a84..9f2b8727e8c 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -41,7 +41,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@128a63446a954579617e875aaab7d2978154e969 # v2.4.0 + uses: step-security/harden-runner@55d479fb1c5bcad5a4f9099a5d9f37c8857b2845 # v2.4.1 with: egress-policy: audit diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 1ccabd65476..aa20c458b2a 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@128a63446a954579617e875aaab7d2978154e969 # v2.4.0 + uses: step-security/harden-runner@55d479fb1c5bcad5a4f9099a5d9f37c8857b2845 # v2.4.1 with: egress-policy: audit From 103757865a520984cf9ead3684baa5766baf3d2a Mon Sep 17 00:00:00 2001 From: Livia Medeiros Date: Fri, 28 Jul 2023 15:35:18 +0900 Subject: [PATCH 093/259] test: fix `autoselectfamily` on platforms without IPv6 support (#2197) --- test/autoselectfamily.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/autoselectfamily.js b/test/autoselectfamily.js index aa9a751365a..0b44a3eb887 100644 --- a/test/autoselectfamily.js +++ b/test/autoselectfamily.js @@ -164,7 +164,7 @@ test('with autoSelectFamily disabled the request fails when using request', (t) method: 'GET', dispatcher: agent }, (err, { statusCode, body }) => { - t.strictSame(err.code, 'ECONNREFUSED') + t.ok(['ECONNREFUSED', 'EAFNOSUPPORT'].includes(err.code)) }) }) }) @@ -191,7 +191,7 @@ test('with autoSelectFamily disabled the request fails when using a client', (t) path: '/', method: 'GET' }, (err, { statusCode, body }) => { - t.strictSame(err.code, 'ECONNREFUSED') + t.ok(['ECONNREFUSED', 'EAFNOSUPPORT'].includes(err.code)) }) }) }) From bae93dceb0708854206557ccdce3ad6f7673e459 Mon Sep 17 00:00:00 2001 From: Livia Medeiros Date: Fri, 28 Jul 2023 18:09:42 +0900 Subject: [PATCH 094/259] fix: make multipart/form-data boundary string more consistent (#2196) --- lib/fetch/body.js | 2 +- test/client-request.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/fetch/body.js b/lib/fetch/body.js index db450ee6bd4..0c7b8b65363 100644 --- a/lib/fetch/body.js +++ b/lib/fetch/body.js @@ -105,7 +105,7 @@ function extractBody (object, keepalive = false) { // Set source to a copy of the bytes held by object. source = new Uint8Array(object.buffer.slice(object.byteOffset, object.byteOffset + object.byteLength)) } else if (util.isFormDataLike(object)) { - const boundary = `----formdata-undici-${Math.random()}`.replace('.', '').slice(0, 32) + const boundary = `----formdata-undici-0${`${Math.floor(Math.random() * 1e11)}`.padStart(11, '0')}` const prefix = `--${boundary}\r\nContent-Disposition: form-data` /*! formdata-polyfill. MIT License. Jimmy Wärting */ diff --git a/test/client-request.js b/test/client-request.js index 626db41187a..703cedf6803 100644 --- a/test/client-request.js +++ b/test/client-request.js @@ -726,7 +726,7 @@ test('request with FormData body', { skip: nodeMajor < 16 }, (t) => { const server = createServer(async (req, res) => { const contentType = req.headers['content-type'] // ensure we received a multipart/form-data header - t.ok(/^multipart\/form-data; boundary=-+formdata-undici-0.\d+$/.test(contentType)) + t.ok(/^multipart\/form-data; boundary=-+formdata-undici-0\d+$/.test(contentType)) const chunks = [] From d92d617bdb9facea8c564eb82f4e882c3f639445 Mon Sep 17 00:00:00 2001 From: Dan Castillo Date: Fri, 28 Jul 2023 05:46:37 -0400 Subject: [PATCH 095/259] docs: add proxy agent options docs (#2193) * add proxy agent options docs * Apply suggestions from code review * updated docs to suggested --- docs/api/ProxyAgent.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/api/ProxyAgent.md b/docs/api/ProxyAgent.md index 6a8b07fe6bf..cebfe689f39 100644 --- a/docs/api/ProxyAgent.md +++ b/docs/api/ProxyAgent.md @@ -19,7 +19,9 @@ Extends: [`AgentOptions`](Agent.md#parameter-agentoptions) * **uri** `string` (required) - It can be passed either by a string or a object containing `uri` as string. * **token** `string` (optional) - It can be passed by a string of token for authentication. * **auth** `string` (**deprecated**) - Use token. -* **clientFactory** `(origin: URL, opts: Object) => Dispatcher` - Default: `(origin, opts) => new Pool(origin, opts)` +* **clientFactory** `(origin: URL, opts: Object) => Dispatcher` (optional) - Default: `(origin, opts) => new Pool(origin, opts)` +* **requestTls** `BuildOptions` (optional) - Options object passed when creating the underlying socket via the connector builder for the request. See [TLS](https://nodejs.org/api/tls.html#tlsconnectoptions-callback). +* **proxyTls** `BuildOptions` (optional) - Options object passed when creating the underlying socket via the connector builder for the proxy server. See [TLS](https://nodejs.org/api/tls.html#tlsconnectoptions-callback). Examples: From 4477dc2e5d71998f274a80c274e5969ab3b80fb1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Aug 2023 22:11:44 +0000 Subject: [PATCH 096/259] build(deps): bump github/codeql-action from 2.20.3 to 2.21.2 (#2205) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2.20.3 to 2.21.2. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/46ed16ded91731b2df79a2893d3aea8e9f03b5c4...0ba4244466797eb048eb91a6cd43d5c03ca8bd05) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 6 +++--- .github/workflows/scorecard.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 9f2b8727e8c..1af31d7eecc 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -50,7 +50,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@46ed16ded91731b2df79a2893d3aea8e9f03b5c4 # v2.3.3 + uses: github/codeql-action/init@0ba4244466797eb048eb91a6cd43d5c03ca8bd05 # v2.3.3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -60,7 +60,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@46ed16ded91731b2df79a2893d3aea8e9f03b5c4 # v2.3.3 + uses: github/codeql-action/autobuild@0ba4244466797eb048eb91a6cd43d5c03ca8bd05 # v2.3.3 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -73,6 +73,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@46ed16ded91731b2df79a2893d3aea8e9f03b5c4 # v2.3.3 + uses: github/codeql-action/analyze@0ba4244466797eb048eb91a6cd43d5c03ca8bd05 # v2.3.3 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index d7eef45dccc..040de504a44 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -51,6 +51,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@46ed16ded91731b2df79a2893d3aea8e9f03b5c4 # v2.20.3 + uses: github/codeql-action/upload-sarif@0ba4244466797eb048eb91a6cd43d5c03ca8bd05 # v2.21.2 with: sarif_file: results.sarif From caca9367e28129a0400dc3c644926b85adbd2fed Mon Sep 17 00:00:00 2001 From: Chemi Atlow Date: Wed, 2 Aug 2023 12:55:58 +0300 Subject: [PATCH 097/259] feat: make use of `addAbortListener` where applicable (#2195) * feat: make use of `addAbortListener` where applicable * lint * fix UT --- lib/api/abort-signal.js | 7 ++----- lib/api/readable.js | 9 ++++++--- lib/core/util.js | 19 +++++++++++++++++++ lib/fetch/index.js | 9 ++++----- lib/fetch/request.js | 11 ++++++----- 5 files changed, 37 insertions(+), 18 deletions(-) diff --git a/lib/api/abort-signal.js b/lib/api/abort-signal.js index 895629aa466..2985c1efa96 100644 --- a/lib/api/abort-signal.js +++ b/lib/api/abort-signal.js @@ -1,3 +1,4 @@ +const { addAbortListener } = require('../core/util') const { RequestAbortedError } = require('../core/errors') const kListener = Symbol('kListener') @@ -29,11 +30,7 @@ function addSignal (self, signal) { abort(self) } - if ('addEventListener' in self[kSignal]) { - self[kSignal].addEventListener('abort', self[kListener]) - } else { - self[kSignal].addListener('abort', self[kListener]) - } + addAbortListener(self[kSignal], self[kListener]) } function removeSignal (self) { diff --git a/lib/api/readable.js b/lib/api/readable.js index 398a75ba8bb..508fbdef928 100644 --- a/lib/api/readable.js +++ b/lib/api/readable.js @@ -155,12 +155,13 @@ module.exports = class BodyReadable extends Readable { const abortFn = () => { this.destroy() } + let signalListenerCleanup if (signal) { if (typeof signal !== 'object' || !('aborted' in signal)) { throw new InvalidArgumentError('signal must be an AbortSignal') } util.throwIfAborted(signal) - signal.addEventListener('abort', abortFn, { once: true }) + signalListenerCleanup = util.addAbortListener(signal, abortFn) } try { for await (const chunk of this) { @@ -173,8 +174,10 @@ module.exports = class BodyReadable extends Readable { } catch { util.throwIfAborted(signal) } finally { - if (signal) { - signal.removeEventListener('abort', abortFn) + if (typeof signalListenerCleanup === 'function') { + signalListenerCleanup() + } else if (signalListenerCleanup) { + signalListenerCleanup[Symbol.dispose]() } } } diff --git a/lib/core/util.js b/lib/core/util.js index 6f247d22a52..4f8c1f8f1a1 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -422,6 +422,24 @@ function throwIfAborted (signal) { } } +let events +function addAbortListener (signal, listener) { + if (typeof Symbol.dispose === 'symbol') { + if (!events) { + events = require('events') + } + if (typeof events.addAbortListener === 'function' && 'aborted' in signal) { + return events.addAbortListener(signal, listener) + } + } + if ('addEventListener' in signal) { + signal.addEventListener('abort', listener, { once: true }) + return () => signal.removeEventListener('abort', listener) + } + signal.addListener('abort', listener) + return () => signal.removeListener('abort', listener) +} + const hasToWellFormed = !!String.prototype.toWellFormed /** @@ -469,6 +487,7 @@ module.exports = { isFormDataLike, buildURL, throwIfAborted, + addAbortListener, nodeMajor, nodeMinor, nodeHasAutoSelectFamily: nodeMajor > 18 || (nodeMajor === 18 && nodeMinor >= 13) diff --git a/lib/fetch/index.js b/lib/fetch/index.js index 2b7fe3632ea..d615f07ea27 100644 --- a/lib/fetch/index.js +++ b/lib/fetch/index.js @@ -56,7 +56,7 @@ const { const { kHeadersList } = require('../core/symbols') const EE = require('events') const { Readable, pipeline } = require('stream') -const { isErrored, isReadable, nodeMajor, nodeMinor } = require('../core/util') +const { addAbortListener, isErrored, isReadable, nodeMajor, nodeMinor } = require('../core/util') const { dataURLProcessor, serializeAMimeType } = require('./dataURL') const { TransformStream } = require('stream/web') const { getGlobalDispatcher } = require('../global') @@ -174,8 +174,8 @@ async function fetch (input, init = {}) { let controller = null // 11. Add the following abort steps to requestObject’s signal: - requestObject.signal.addEventListener( - 'abort', + addAbortListener( + requestObject.signal, () => { // 1. Set locallyAborted to true. locallyAborted = true @@ -189,8 +189,7 @@ async function fetch (input, init = {}) { // 4. Abort the fetch() call with p, request, responseObject, // and requestObject’s signal’s abort reason. abortFetch(p, request, responseObject, requestObject.signal.reason) - }, - { once: true } + } ) // 12. Let handleFetchDone given response response be to finalize and diff --git a/lib/fetch/request.js b/lib/fetch/request.js index 17fafca6507..912bd5b8c98 100644 --- a/lib/fetch/request.js +++ b/lib/fetch/request.js @@ -340,6 +340,8 @@ class Request { // 28. Set this’s signal to a new AbortSignal object with this’s relevant // Realm. + // TODO: could this be simplified with AbortSignal.any + // (https://dom.spec.whatwg.org/#dom-abortsignal-any) const ac = new AbortController() this[kSignal] = ac.signal this[kSignal][kRealm] = this[kRealm] @@ -385,7 +387,7 @@ class Request { } } catch {} - signal.addEventListener('abort', abort, { once: true }) + util.addAbortListener(signal, abort) requestFinalizer.register(ac, { signal, abort }) } } @@ -733,12 +735,11 @@ class Request { if (this.signal.aborted) { ac.abort(this.signal.reason) } else { - this.signal.addEventListener( - 'abort', + util.addAbortListener( + this.signal, () => { ac.abort(this.signal.reason) - }, - { once: true } + } ) } clonedRequestObject[kSignal] = ac.signal From 59abe3f50d9c41a7e32a068654905f0919c9be71 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Thu, 3 Aug 2023 10:32:22 +0200 Subject: [PATCH 098/259] 5.23.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d5e312a6828..598a78654a9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "5.22.1", + "version": "5.23.0", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { From c83b084879fa0bb8e0469d31ec61428ac68160d5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 3 Aug 2023 21:07:49 +0000 Subject: [PATCH 099/259] build(deps): bump step-security/harden-runner from 2.4.1 to 2.5.0 (#2203) Bumps [step-security/harden-runner](https://github.com/step-security/harden-runner) from 2.4.1 to 2.5.0. - [Release notes](https://github.com/step-security/harden-runner/releases) - [Commits](https://github.com/step-security/harden-runner/compare/55d479fb1c5bcad5a4f9099a5d9f37c8857b2845...cba0d00b1fc9a034e1e642ea0f1103c282990604) --- updated-dependencies: - dependency-name: step-security/harden-runner dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 2 +- .github/workflows/dependency-review.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 1af31d7eecc..b921c4eebe4 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -41,7 +41,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@55d479fb1c5bcad5a4f9099a5d9f37c8857b2845 # v2.4.1 + uses: step-security/harden-runner@cba0d00b1fc9a034e1e642ea0f1103c282990604 # v2.5.0 with: egress-policy: audit diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index aa20c458b2a..b95c4b9752b 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@55d479fb1c5bcad5a4f9099a5d9f37c8857b2845 # v2.4.1 + uses: step-security/harden-runner@cba0d00b1fc9a034e1e642ea0f1103c282990604 # v2.5.0 with: egress-policy: audit From f08379c55857819f258b0cdc5ad6f944074e2c42 Mon Sep 17 00:00:00 2001 From: Khafra Date: Thu, 17 Aug 2023 10:24:55 -0400 Subject: [PATCH 100/259] better stack trace for body.json (#2215) * better stack trace for body.json Fixes #2212 * not needed * nit --- lib/cache/cache.js | 6 +----- lib/fetch/body.js | 2 +- lib/fetch/util.js | 34 +++++++++++++--------------------- 3 files changed, 15 insertions(+), 27 deletions(-) diff --git a/lib/cache/cache.js b/lib/cache/cache.js index 18f06a348a0..9b3110860cd 100644 --- a/lib/cache/cache.js +++ b/lib/cache/cache.js @@ -379,11 +379,7 @@ class Cache { const reader = stream.getReader() // 11.3 - readAllBytes( - reader, - (bytes) => bodyReadPromise.resolve(bytes), - (error) => bodyReadPromise.reject(error) - ) + readAllBytes(reader).then(bodyReadPromise.resolve, bodyReadPromise.reject) } else { bodyReadPromise.resolve(undefined) } diff --git a/lib/fetch/body.js b/lib/fetch/body.js index 0c7b8b65363..11b945d18bf 100644 --- a/lib/fetch/body.js +++ b/lib/fetch/body.js @@ -532,7 +532,7 @@ async function specConsumeBody (object, convertBytesToJSValue, instance) { // 6. Otherwise, fully read object’s body given successSteps, // errorSteps, and object’s relevant global object. - fullyReadBody(object[kState].body, successSteps, errorSteps) + await fullyReadBody(object[kState].body, successSteps, errorSteps) // 7. Return promise. return promise.promise diff --git a/lib/fetch/util.js b/lib/fetch/util.js index 400687ba2e7..98a049dc79d 100644 --- a/lib/fetch/util.js +++ b/lib/fetch/util.js @@ -812,17 +812,17 @@ function iteratorResult (pair, kind) { /** * @see https://fetch.spec.whatwg.org/#body-fully-read */ -function fullyReadBody (body, processBody, processBodyError) { +async function fullyReadBody (body, processBody, processBodyError) { // 1. If taskDestination is null, then set taskDestination to // the result of starting a new parallel queue. // 2. Let successSteps given a byte sequence bytes be to queue a // fetch task to run processBody given bytes, with taskDestination. - const successSteps = (bytes) => queueMicrotask(() => processBody(bytes)) + const successSteps = processBody // 3. Let errorSteps be to queue a fetch task to run processBodyError, // with taskDestination. - const errorSteps = (error) => queueMicrotask(() => processBodyError(error)) + const errorSteps = processBodyError // 4. Let reader be the result of getting a reader for body’s stream. // If that threw an exception, then run errorSteps with that @@ -837,7 +837,12 @@ function fullyReadBody (body, processBody, processBodyError) { } // 5. Read all bytes from reader, given successSteps and errorSteps. - readAllBytes(reader, successSteps, errorSteps) + try { + const result = await readAllBytes(reader) + successSteps(result) + } catch (e) { + errorSteps(e) + } } /** @type {ReadableStream} */ @@ -906,36 +911,23 @@ function isomorphicEncode (input) { * @see https://streams.spec.whatwg.org/#readablestreamdefaultreader-read-all-bytes * @see https://streams.spec.whatwg.org/#read-loop * @param {ReadableStreamDefaultReader} reader - * @param {(bytes: Uint8Array) => void} successSteps - * @param {(error: Error) => void} failureSteps */ -async function readAllBytes (reader, successSteps, failureSteps) { +async function readAllBytes (reader) { const bytes = [] let byteLength = 0 while (true) { - let done - let chunk - - try { - ({ done, value: chunk } = await reader.read()) - } catch (e) { - // 1. Call failureSteps with e. - failureSteps(e) - return - } + const { done, value: chunk } = await reader.read() if (done) { // 1. Call successSteps with bytes. - successSteps(Buffer.concat(bytes, byteLength)) - return + return Buffer.concat(bytes, byteLength) } // 1. If chunk is not a Uint8Array object, call failureSteps // with a TypeError and abort these steps. if (!isUint8Array(chunk)) { - failureSteps(new TypeError('Received non-Uint8Array chunk')) - return + throw new TypeError('Received non-Uint8Array chunk') } // 2. Append the bytes represented by chunk to bytes. From 37b7a4997d2bc84dcf1efe69d9f7b6603b818e48 Mon Sep 17 00:00:00 2001 From: Khafra Date: Mon, 21 Aug 2023 13:38:27 -0400 Subject: [PATCH 101/259] allow http & https websocket urls (#2218) * allow http & https websocket urls * this test no longer throws --- lib/websocket/websocket.js | 37 +++++++++----- test/websocket/constructor.js | 8 --- test/wpt/server/server.mjs | 4 +- test/wpt/server/websocket.mjs | 9 +++- test/wpt/status/websockets.status.json | 26 ++++++++++ .../tests/websockets/Close-1000-reason.any.js | 2 +- .../websockets/Close-1000-verify-code.any.js | 2 +- test/wpt/tests/websockets/Close-1000.any.js | 2 +- .../websockets/Close-1005-verify-code.any.js | 2 +- test/wpt/tests/websockets/Close-1005.any.js | 2 +- .../tests/websockets/Close-2999-reason.any.js | 2 +- .../tests/websockets/Close-3000-reason.any.js | 2 +- .../websockets/Close-3000-verify-code.any.js | 2 +- .../tests/websockets/Close-4999-reason.any.js | 2 +- .../websockets/Close-Reason-124Bytes.any.js | 2 +- .../wpt/tests/websockets/Close-delayed.any.js | 2 +- .../tests/websockets/Close-onlyReason.any.js | 2 +- .../websockets/Close-readyState-Closed.any.js | 2 +- .../Close-readyState-Closing.any.js | 2 +- .../Close-reason-unpaired-surrogates.any.js | 2 +- .../Close-server-initiated-close.any.js | 2 +- .../tests/websockets/Close-undefined.any.js | 2 +- .../Create-asciiSep-protocol-string.any.js | 2 +- .../websockets/Create-blocked-port.any.js | 2 +- .../websockets/Create-extensions-empty.any.js | 2 +- .../tests/websockets/Create-http-urls.any.js | 19 +++++++ .../websockets/Create-invalid-urls.any.js | 48 ++++++------------ .../websockets/Create-non-absolute-url.any.js | 25 +++++----- .../Create-nonAscii-protocol-string.any.js | 2 +- .../Create-protocol-with-space.any.js | 2 +- ...protocols-repeated-case-insensitive.any.js | 2 +- .../Create-protocols-repeated.any.js | 2 +- .../websockets/Create-url-with-space.any.js | 2 +- ...Create-url-with-windows-1252-encoding.html | 20 ++++++++ .../Create-valid-url-array-protocols.any.js | 2 +- .../Create-valid-url-binaryType-blob.any.js | 2 +- .../Create-valid-url-protocol-empty.any.js | 2 +- ...ate-valid-url-protocol-setCorrectly.any.js | 2 +- .../Create-valid-url-protocol-string.any.js | 2 +- .../Create-valid-url-protocol.any.js | 2 +- .../tests/websockets/Create-valid-url.any.js | 2 +- .../websockets/Create-wrong-scheme.any.js | 11 ----- .../tests/websockets/Send-0byte-data.any.js | 2 +- .../wpt/tests/websockets/Send-65K-data.any.js | 2 +- .../tests/websockets/Send-before-open.any.js | 2 +- .../Send-binary-65K-arraybuffer.any.js | 2 +- .../websockets/Send-binary-arraybuffer.any.js | 2 +- ...Send-binary-arraybufferview-float32.any.js | 2 +- ...Send-binary-arraybufferview-float64.any.js | 2 +- ...binary-arraybufferview-int16-offset.any.js | 2 +- .../Send-binary-arraybufferview-int32.any.js | 2 +- .../Send-binary-arraybufferview-int8.any.js | 2 +- ...rraybufferview-uint16-offset-length.any.js | 2 +- ...inary-arraybufferview-uint32-offset.any.js | 2 +- ...arraybufferview-uint8-offset-length.any.js | 2 +- ...binary-arraybufferview-uint8-offset.any.js | 2 +- .../tests/websockets/Send-binary-blob.any.js | 2 +- test/wpt/tests/websockets/Send-data.any.js | 2 +- test/wpt/tests/websockets/Send-data.worker.js | 2 +- test/wpt/tests/websockets/Send-null.any.js | 2 +- .../websockets/Send-paired-surrogates.any.js | 2 +- .../tests/websockets/Send-unicode-data.any.js | 2 +- .../Send-unpaired-surrogates.any.js | 2 +- ...socket-connection-ccns.tentative.window.js | 34 +++++++++++++ ...with-closed-websocket-connection.window.js | 20 ++++++++ ...socket-connection-ccns.tentative.window.js | 35 +++++++++++++ ...e-with-open-websocket-connection.window.js | 21 ++++++++ test/wpt/tests/websockets/binary/001.html | 2 +- test/wpt/tests/websockets/binary/002.html | 2 +- test/wpt/tests/websockets/binary/004.html | 2 +- test/wpt/tests/websockets/binary/005.html | 2 +- .../websockets/binaryType-wrong-value.any.js | 2 +- ...ufferedAmount-unchanged-by-sync-xhr.any.js | 2 +- .../wpt/tests/websockets/close-invalid.any.js | 2 +- .../websockets/closing-handshake/002.html | 2 +- .../websockets/closing-handshake/003.html | 2 +- .../websockets/closing-handshake/004.html | 2 +- test/wpt/tests/websockets/constants.sub.js | 30 +++++------- test/wpt/tests/websockets/constructor.any.js | 2 +- .../wpt/tests/websockets/constructor/001.html | 2 +- .../wpt/tests/websockets/constructor/002.html | 21 -------- .../wpt/tests/websockets/constructor/004.html | 2 +- .../wpt/tests/websockets/constructor/005.html | 2 +- .../wpt/tests/websockets/constructor/006.html | 2 +- .../wpt/tests/websockets/constructor/007.html | 2 +- .../wpt/tests/websockets/constructor/008.html | 2 +- .../wpt/tests/websockets/constructor/009.html | 2 +- .../wpt/tests/websockets/constructor/010.html | 2 +- .../wpt/tests/websockets/constructor/011.html | 2 +- .../wpt/tests/websockets/constructor/012.html | 2 +- .../wpt/tests/websockets/constructor/013.html | 2 +- .../wpt/tests/websockets/constructor/014.html | 2 +- .../wpt/tests/websockets/constructor/016.html | 2 +- .../wpt/tests/websockets/constructor/017.html | 2 +- .../wpt/tests/websockets/constructor/018.html | 2 +- .../wpt/tests/websockets/constructor/019.html | 2 +- .../wpt/tests/websockets/constructor/020.html | 2 +- .../wpt/tests/websockets/constructor/021.html | 2 +- .../wpt/tests/websockets/constructor/022.html | 2 +- test/wpt/tests/websockets/cookies/001.html | 2 +- test/wpt/tests/websockets/cookies/002.html | 2 +- test/wpt/tests/websockets/cookies/003.html | 2 +- test/wpt/tests/websockets/cookies/004.html | 2 +- test/wpt/tests/websockets/cookies/005.html | 2 +- test/wpt/tests/websockets/cookies/006.html | 2 +- test/wpt/tests/websockets/cookies/007.html | 2 +- .../wpt/tests/websockets/eventhandlers.any.js | 2 +- .../websockets/extended-payload-length.html | 2 +- .../receive-many-with-backpressure_wsh.py | 23 +++++++++ .../interfaces/CloseEvent/clean-close.html | 2 +- .../bufferedAmount-arraybuffer.html | 2 +- .../bufferedAmount/bufferedAmount-blob.html | 2 +- .../bufferedAmount-defineProperty-getter.html | 2 +- .../bufferedAmount-defineProperty-setter.html | 2 +- .../bufferedAmount-deleting.html | 2 +- .../bufferedAmount-getting.html | 2 +- .../bufferedAmount-initial.html | 2 +- .../bufferedAmount/bufferedAmount-large.html | 2 +- .../bufferedAmount-readonly.html | 2 +- .../bufferedAmount-unicode.html | 2 +- .../WebSocket/close/close-basic.html | 2 +- .../WebSocket/close/close-connecting.html | 2 +- .../WebSocket/close/close-multiple.html | 2 +- .../WebSocket/close/close-nested.html | 2 +- .../WebSocket/close/close-replace.html | 2 +- .../WebSocket/close/close-return.html | 2 +- .../interfaces/WebSocket/constants/001.html | 2 +- .../interfaces/WebSocket/constants/002.html | 2 +- .../interfaces/WebSocket/constants/003.html | 2 +- .../interfaces/WebSocket/constants/004.html | 2 +- .../interfaces/WebSocket/constants/005.html | 2 +- .../interfaces/WebSocket/constants/006.html | 2 +- .../interfaces/WebSocket/events/001.html | 2 +- .../interfaces/WebSocket/events/002.html | 2 +- .../interfaces/WebSocket/events/003.html | 2 +- .../interfaces/WebSocket/events/004.html | 2 +- .../interfaces/WebSocket/events/006.html | 2 +- .../interfaces/WebSocket/events/007.html | 2 +- .../interfaces/WebSocket/events/008.html | 2 +- .../interfaces/WebSocket/events/009.html | 2 +- .../interfaces/WebSocket/events/010.html | 2 +- .../interfaces/WebSocket/events/011.html | 2 +- .../interfaces/WebSocket/events/012.html | 2 +- .../interfaces/WebSocket/events/013.html | 2 +- .../interfaces/WebSocket/events/014.html | 2 +- .../interfaces/WebSocket/events/015.html | 2 +- .../interfaces/WebSocket/events/016.html | 2 +- .../interfaces/WebSocket/events/017.html | 2 +- .../interfaces/WebSocket/events/018.html | 2 +- .../interfaces/WebSocket/events/019.html | 2 +- .../interfaces/WebSocket/events/020.html | 2 +- .../interfaces/WebSocket/extensions/001.html | 2 +- .../WebSocket/protocol/protocol-initial.html | 2 +- .../interfaces/WebSocket/readyState/001.html | 2 +- .../interfaces/WebSocket/readyState/002.html | 2 +- .../interfaces/WebSocket/readyState/003.html | 2 +- .../interfaces/WebSocket/readyState/004.html | 2 +- .../interfaces/WebSocket/readyState/005.html | 2 +- .../interfaces/WebSocket/readyState/006.html | 2 +- .../interfaces/WebSocket/readyState/007.html | 2 +- .../interfaces/WebSocket/readyState/008.html | 2 +- .../interfaces/WebSocket/send/001.html | 2 +- .../interfaces/WebSocket/send/002.html | 2 +- .../interfaces/WebSocket/send/003.html | 2 +- .../interfaces/WebSocket/send/004.html | 2 +- .../interfaces/WebSocket/send/005.html | 2 +- .../interfaces/WebSocket/send/006.html | 2 +- .../interfaces/WebSocket/send/007.html | 2 +- .../interfaces/WebSocket/send/008.html | 2 +- .../interfaces/WebSocket/send/009.html | 2 +- .../interfaces/WebSocket/send/010.html | 2 +- .../interfaces/WebSocket/send/011.html | 2 +- .../interfaces/WebSocket/send/012.html | 2 +- .../interfaces/WebSocket/url/001.html | 2 +- .../interfaces/WebSocket/url/002.html | 2 +- .../interfaces/WebSocket/url/003.html | 2 +- .../interfaces/WebSocket/url/004.html | 2 +- .../interfaces/WebSocket/url/005.html | 2 +- .../interfaces/WebSocket/url/006.html | 2 +- .../interfaces/WebSocket/url/resolve.html | 2 +- .../keeping-connection-open/001.html | 2 +- .../websockets/mixed-content.https.any.js | 7 +++ .../url-parsing/current/current.html | 2 + .../url-parsing/incumbent/incumbent.html | 13 +++++ .../url-parsing/url-parsing.html | 22 +++++++++ .../websockets/opening-handshake/001.html | 2 +- .../websockets/opening-handshake/002.html | 2 +- .../websockets/opening-handshake/003.html | 2 +- .../websockets/opening-handshake/005.html | 2 +- ...remove-own-iframe-during-onerror.window.js | 2 +- .../resources/websockets-test-helpers.sub.js | 25 ++++++++++ test/wpt/tests/websockets/security/001.html | 2 +- test/wpt/tests/websockets/security/002.html | 2 +- ...many-64K-messages-with-backpressure.any.js | 49 +++++++++++++++++++ .../websockets/stream/tentative/abort.any.js | 16 +++--- .../tentative/backpressure-receive.any.js | 2 +- .../stream/tentative/backpressure-send.any.js | 2 +- .../websockets/stream/tentative/close.any.js | 43 ++++++++-------- .../stream/tentative/constructor.any.js | 21 ++++---- .../websockets/unload-a-document/001.html | 2 +- .../websockets/unload-a-document/002.html | 2 +- .../websockets/unload-a-document/005.html | 2 +- 202 files changed, 608 insertions(+), 333 deletions(-) create mode 100644 test/wpt/tests/websockets/Create-http-urls.any.js create mode 100644 test/wpt/tests/websockets/Create-url-with-windows-1252-encoding.html delete mode 100644 test/wpt/tests/websockets/Create-wrong-scheme.any.js create mode 100644 test/wpt/tests/websockets/back-forward-cache-with-closed-websocket-connection-ccns.tentative.window.js create mode 100644 test/wpt/tests/websockets/back-forward-cache-with-closed-websocket-connection.window.js create mode 100644 test/wpt/tests/websockets/back-forward-cache-with-open-websocket-connection-ccns.tentative.window.js create mode 100644 test/wpt/tests/websockets/back-forward-cache-with-open-websocket-connection.window.js delete mode 100644 test/wpt/tests/websockets/constructor/002.html create mode 100644 test/wpt/tests/websockets/handlers/receive-many-with-backpressure_wsh.py create mode 100644 test/wpt/tests/websockets/mixed-content.https.any.js create mode 100644 test/wpt/tests/websockets/multi-globals/url-parsing/current/current.html create mode 100644 test/wpt/tests/websockets/multi-globals/url-parsing/incumbent/incumbent.html create mode 100644 test/wpt/tests/websockets/multi-globals/url-parsing/url-parsing.html create mode 100644 test/wpt/tests/websockets/resources/websockets-test-helpers.sub.js create mode 100644 test/wpt/tests/websockets/send-many-64K-messages-with-backpressure.any.js diff --git a/lib/websocket/websocket.js b/lib/websocket/websocket.js index 22ad2fb11a1..e4aa58f52fc 100644 --- a/lib/websocket/websocket.js +++ b/lib/websocket/websocket.js @@ -3,6 +3,7 @@ const { webidl } = require('../fetch/webidl') const { DOMException } = require('../fetch/constants') const { URLSerializer } = require('../fetch/dataURL') +const { getGlobalOrigin } = require('../fetch/global') const { staticPropertyDescriptors, states, opcodes, emptyBuffer } = require('./constants') const { kWebSocketURL, @@ -57,18 +58,28 @@ class WebSocket extends EventTarget { url = webidl.converters.USVString(url) protocols = options.protocols - // 1. Let urlRecord be the result of applying the URL parser to url. + // 1. Let baseURL be this's relevant settings object's API base URL. + const baseURL = getGlobalOrigin() + + // 1. Let urlRecord be the result of applying the URL parser to url with baseURL. let urlRecord try { - urlRecord = new URL(url) + urlRecord = new URL(url, baseURL) } catch (e) { - // 2. If urlRecord is failure, then throw a "SyntaxError" DOMException. + // 3. If urlRecord is failure, then throw a "SyntaxError" DOMException. throw new DOMException(e, 'SyntaxError') } - // 3. If urlRecord’s scheme is not "ws" or "wss", then throw a - // "SyntaxError" DOMException. + // 4. If urlRecord’s scheme is "http", then set urlRecord’s scheme to "ws". + if (urlRecord.protocol === 'http:') { + urlRecord.protocol = 'ws:' + } else if (urlRecord.protocol === 'https:') { + // 5. Otherwise, if urlRecord’s scheme is "https", set urlRecord’s scheme to "wss". + urlRecord.protocol = 'wss:' + } + + // 6. If urlRecord’s scheme is not "ws" or "wss", then throw a "SyntaxError" DOMException. if (urlRecord.protocol !== 'ws:' && urlRecord.protocol !== 'wss:') { throw new DOMException( `Expected a ws: or wss: protocol, got ${urlRecord.protocol}`, @@ -76,19 +87,19 @@ class WebSocket extends EventTarget { ) } - // 4. If urlRecord’s fragment is non-null, then throw a "SyntaxError" + // 7. If urlRecord’s fragment is non-null, then throw a "SyntaxError" // DOMException. - if (urlRecord.hash) { + if (urlRecord.hash || urlRecord.href.endsWith('#')) { throw new DOMException('Got fragment', 'SyntaxError') } - // 5. If protocols is a string, set protocols to a sequence consisting + // 8. If protocols is a string, set protocols to a sequence consisting // of just that string. if (typeof protocols === 'string') { protocols = [protocols] } - // 6. If any of the values in protocols occur more than once or otherwise + // 9. If any of the values in protocols occur more than once or otherwise // fail to match the requirements for elements that comprise the value // of `Sec-WebSocket-Protocol` fields as defined by The WebSocket // protocol, then throw a "SyntaxError" DOMException. @@ -100,12 +111,12 @@ class WebSocket extends EventTarget { throw new DOMException('Invalid Sec-WebSocket-Protocol value', 'SyntaxError') } - // 7. Set this's url to urlRecord. - this[kWebSocketURL] = urlRecord + // 10. Set this's url to urlRecord. + this[kWebSocketURL] = new URL(urlRecord.href) - // 8. Let client be this's relevant settings object. + // 11. Let client be this's relevant settings object. - // 9. Run this step in parallel: + // 12. Run this step in parallel: // 1. Establish a WebSocket connection given urlRecord, protocols, // and client. diff --git a/test/websocket/constructor.js b/test/websocket/constructor.js index a09515868c8..dd87dead652 100644 --- a/test/websocket/constructor.js +++ b/test/websocket/constructor.js @@ -12,14 +12,6 @@ test('Constructor', (t) => { } ) - t.throws( - () => new WebSocket('https://www.google.com'), - { - name: 'SyntaxError', - constructor: DOMException - } - ) - t.throws( () => new WebSocket('wss://echo.websocket.events/#a'), { diff --git a/test/wpt/server/server.mjs b/test/wpt/server/server.mjs index 7074e154dd1..82b9080f407 100644 --- a/test/wpt/server/server.mjs +++ b/test/wpt/server/server.mjs @@ -384,7 +384,9 @@ const send = (message) => { } } -send({ server: `http://localhost:${server.address().port}` }) +const url = `http://localhost:${server.address().port}` +console.log('server opened ' + url) +send({ server: url }) process.on('message', (message) => { if (message === 'shutdown') { diff --git a/test/wpt/server/websocket.mjs b/test/wpt/server/websocket.mjs index 8b6471a6206..cc8ce78151b 100644 --- a/test/wpt/server/websocket.mjs +++ b/test/wpt/server/websocket.mjs @@ -11,10 +11,17 @@ const wss = new WebSocketServer({ handleProtocols: (protocols) => [...protocols].join(', ') }) -wss.on('connection', (ws) => { +wss.on('connection', (ws, request) => { ws.on('message', (data, isBinary) => { const str = data.toString('utf-8') + if (request.url === '/receive-many-with-backpressure') { + setTimeout(() => { + ws.send(str.length.toString(), { binary: false }) + }, 100) + return + } + if (str === 'Goodbye') { // Close-server-initiated-close.any.js sends a "Goodbye" message // when it wants the server to close the connection. diff --git a/test/wpt/status/websockets.status.json b/test/wpt/status/websockets.status.json index 4a5bdf502c3..68bc6e2ebe7 100644 --- a/test/wpt/status/websockets.status.json +++ b/test/wpt/status/websockets.status.json @@ -85,5 +85,31 @@ "flaky": [ "Send 0 byte data on a WebSocket - Connection should be closed" ] + }, + "send-many-64K-messages-with-backpressure.any.js": { + "note": "probably flaky based on other flaky tests.", + "flaky": [ + "sending 50 messages of size 65536 with backpressure applied should not hang" + ] + }, + "back-forward-cache-with-closed-websocket-connection-ccns.tentative.window.js": { + "skip": true, + "note": "browser-only test" + }, + "back-forward-cache-with-closed-websocket-connection.window.js": { + "skip": true, + "note": "browser-only test" + }, + "back-forward-cache-with-open-websocket-connection-ccns.tentative.window.js": { + "skip": true, + "note": "browser-only test" + }, + "back-forward-cache-with-open-websocket-connection.window.js": { + "skip": true, + "note": "browser-only test" + }, + "mixed-content.https.any.js": { + "note": "node has no concept of origin, thus there is no 'secure' or 'insecure' contexts", + "skip": true } } diff --git a/test/wpt/tests/websockets/Close-1000-reason.any.js b/test/wpt/tests/websockets/Close-1000-reason.any.js index 6fc3c1fade8..79ad6a0418b 100644 --- a/test/wpt/tests/websockets/Close-1000-reason.any.js +++ b/test/wpt/tests/websockets/Close-1000-reason.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wss // META: variant=?wpt_flags=h2 diff --git a/test/wpt/tests/websockets/Close-1000-verify-code.any.js b/test/wpt/tests/websockets/Close-1000-verify-code.any.js index de501306602..c3a9274caa5 100644 --- a/test/wpt/tests/websockets/Close-1000-verify-code.any.js +++ b/test/wpt/tests/websockets/Close-1000-verify-code.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wss // META: variant=?wpt_flags=h2 diff --git a/test/wpt/tests/websockets/Close-1000.any.js b/test/wpt/tests/websockets/Close-1000.any.js index f3100c6caaa..2f535ba21af 100644 --- a/test/wpt/tests/websockets/Close-1000.any.js +++ b/test/wpt/tests/websockets/Close-1000.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wss // META: variant=?wpt_flags=h2 diff --git a/test/wpt/tests/websockets/Close-1005-verify-code.any.js b/test/wpt/tests/websockets/Close-1005-verify-code.any.js index afa7d7b0d98..28f84c8003f 100644 --- a/test/wpt/tests/websockets/Close-1005-verify-code.any.js +++ b/test/wpt/tests/websockets/Close-1005-verify-code.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wss // META: variant=?wpt_flags=h2 diff --git a/test/wpt/tests/websockets/Close-1005.any.js b/test/wpt/tests/websockets/Close-1005.any.js index 514d03ac632..5055e283f30 100644 --- a/test/wpt/tests/websockets/Close-1005.any.js +++ b/test/wpt/tests/websockets/Close-1005.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wss // META: variant=?wpt_flags=h2 diff --git a/test/wpt/tests/websockets/Close-2999-reason.any.js b/test/wpt/tests/websockets/Close-2999-reason.any.js index 95e481e53cf..6336c7db4f3 100644 --- a/test/wpt/tests/websockets/Close-2999-reason.any.js +++ b/test/wpt/tests/websockets/Close-2999-reason.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wss // META: variant=?wpt_flags=h2 diff --git a/test/wpt/tests/websockets/Close-3000-reason.any.js b/test/wpt/tests/websockets/Close-3000-reason.any.js index 2db122934c7..8e34ce7c053 100644 --- a/test/wpt/tests/websockets/Close-3000-reason.any.js +++ b/test/wpt/tests/websockets/Close-3000-reason.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wss // META: variant=?wpt_flags=h2 diff --git a/test/wpt/tests/websockets/Close-3000-verify-code.any.js b/test/wpt/tests/websockets/Close-3000-verify-code.any.js index bfa441f1ee4..a6703dec168 100644 --- a/test/wpt/tests/websockets/Close-3000-verify-code.any.js +++ b/test/wpt/tests/websockets/Close-3000-verify-code.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wss // META: variant=?wpt_flags=h2 diff --git a/test/wpt/tests/websockets/Close-4999-reason.any.js b/test/wpt/tests/websockets/Close-4999-reason.any.js index 3516dc2f462..8c2a1c9ad91 100644 --- a/test/wpt/tests/websockets/Close-4999-reason.any.js +++ b/test/wpt/tests/websockets/Close-4999-reason.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wss // META: variant=?wpt_flags=h2 diff --git a/test/wpt/tests/websockets/Close-Reason-124Bytes.any.js b/test/wpt/tests/websockets/Close-Reason-124Bytes.any.js index aa7fc8ffe83..063b12be16f 100644 --- a/test/wpt/tests/websockets/Close-Reason-124Bytes.any.js +++ b/test/wpt/tests/websockets/Close-Reason-124Bytes.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wpt_flags=h2 // META: variant=?wss diff --git a/test/wpt/tests/websockets/Close-delayed.any.js b/test/wpt/tests/websockets/Close-delayed.any.js index 212925bb931..9e0a60cc6ae 100644 --- a/test/wpt/tests/websockets/Close-delayed.any.js +++ b/test/wpt/tests/websockets/Close-delayed.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wss // META: variant=?wpt_flags=h2 diff --git a/test/wpt/tests/websockets/Close-onlyReason.any.js b/test/wpt/tests/websockets/Close-onlyReason.any.js index 7c5d10d2a83..243eb05e73e 100644 --- a/test/wpt/tests/websockets/Close-onlyReason.any.js +++ b/test/wpt/tests/websockets/Close-onlyReason.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wss // META: variant=?wpt_flags=h2 diff --git a/test/wpt/tests/websockets/Close-readyState-Closed.any.js b/test/wpt/tests/websockets/Close-readyState-Closed.any.js index bfd61c48c14..6c7b5f11322 100644 --- a/test/wpt/tests/websockets/Close-readyState-Closed.any.js +++ b/test/wpt/tests/websockets/Close-readyState-Closed.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wss // META: variant=?wpt_flags=h2 diff --git a/test/wpt/tests/websockets/Close-readyState-Closing.any.js b/test/wpt/tests/websockets/Close-readyState-Closing.any.js index 554744d6297..221130b8f71 100644 --- a/test/wpt/tests/websockets/Close-readyState-Closing.any.js +++ b/test/wpt/tests/websockets/Close-readyState-Closing.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wss // META: variant=?wpt_flags=h2 diff --git a/test/wpt/tests/websockets/Close-reason-unpaired-surrogates.any.js b/test/wpt/tests/websockets/Close-reason-unpaired-surrogates.any.js index 647a1216b99..e5a71d2735e 100644 --- a/test/wpt/tests/websockets/Close-reason-unpaired-surrogates.any.js +++ b/test/wpt/tests/websockets/Close-reason-unpaired-surrogates.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wss // META: variant=?wpt_flags=h2 diff --git a/test/wpt/tests/websockets/Close-server-initiated-close.any.js b/test/wpt/tests/websockets/Close-server-initiated-close.any.js index c86793b23a1..82fd457e1e7 100644 --- a/test/wpt/tests/websockets/Close-server-initiated-close.any.js +++ b/test/wpt/tests/websockets/Close-server-initiated-close.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wpt_flags=h2 // META: variant=?wss diff --git a/test/wpt/tests/websockets/Close-undefined.any.js b/test/wpt/tests/websockets/Close-undefined.any.js index a8106c6f155..e24ef0c3db8 100644 --- a/test/wpt/tests/websockets/Close-undefined.any.js +++ b/test/wpt/tests/websockets/Close-undefined.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wpt_flags=h2 // META: variant=?wss diff --git a/test/wpt/tests/websockets/Create-asciiSep-protocol-string.any.js b/test/wpt/tests/websockets/Create-asciiSep-protocol-string.any.js index 1221c561145..d0102ce533c 100644 --- a/test/wpt/tests/websockets/Create-asciiSep-protocol-string.any.js +++ b/test/wpt/tests/websockets/Create-asciiSep-protocol-string.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wss // META: variant=?wpt_flags=h2 diff --git a/test/wpt/tests/websockets/Create-blocked-port.any.js b/test/wpt/tests/websockets/Create-blocked-port.any.js index c670009b25d..2962312ff5f 100644 --- a/test/wpt/tests/websockets/Create-blocked-port.any.js +++ b/test/wpt/tests/websockets/Create-blocked-port.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wss // META: variant=?wpt_flags=h2 diff --git a/test/wpt/tests/websockets/Create-extensions-empty.any.js b/test/wpt/tests/websockets/Create-extensions-empty.any.js index 1fba4bd2cc5..98a7d65ab5a 100644 --- a/test/wpt/tests/websockets/Create-extensions-empty.any.js +++ b/test/wpt/tests/websockets/Create-extensions-empty.any.js @@ -1,6 +1,6 @@ // META: timeout=long // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wpt_flags=h2 // META: variant=?wss diff --git a/test/wpt/tests/websockets/Create-http-urls.any.js b/test/wpt/tests/websockets/Create-http-urls.any.js new file mode 100644 index 00000000000..17590fc43ef --- /dev/null +++ b/test/wpt/tests/websockets/Create-http-urls.any.js @@ -0,0 +1,19 @@ +test(() => { + const url = new URL ("/", location); + url.protocol = "http"; + const httpURL = url.href; + url.protocol = "https"; + const httpsURL = url.href; + url.protocol = "ws"; + const wsURL = url.href; + url.protocol = "wss"; + const wssURL = url.href; + + let ws = new WebSocket(httpURL); + assert_equals(ws.url, wsURL); + ws.close(); + + ws = new WebSocket(httpsURL); + assert_equals(ws.url, wssURL); + ws.close(); +}, "WebSocket: ensure both HTTP schemes are supported"); diff --git a/test/wpt/tests/websockets/Create-invalid-urls.any.js b/test/wpt/tests/websockets/Create-invalid-urls.any.js index 89783a9ea74..73c9fadab9d 100644 --- a/test/wpt/tests/websockets/Create-invalid-urls.any.js +++ b/test/wpt/tests/websockets/Create-invalid-urls.any.js @@ -1,34 +1,14 @@ -// META: variant= -// META: variant=?wss -// META: variant=?wpt_flags=h2 - -var wsocket; -test(function() { - assert_throws_dom("SYNTAX_ERR", function() { - wsocket = new WebSocket("/echo") - }); -}, "Url is /echo - should throw SYNTAX_ERR"); - -test(function() { - assert_throws_dom("SYNTAX_ERR", function() { - wsocket = new WebSocket("mailto:microsoft@microsoft.com") - }); -}, "Url is a mail address - should throw SYNTAX_ERR"); - -test(function() { - assert_throws_dom("SYNTAX_ERR", function() { - wsocket = new WebSocket("about:blank") - }); -}, "Url is about:blank - should throw SYNTAX_ERR"); - -test(function() { - assert_throws_dom("SYNTAX_ERR", function() { - wsocket = new WebSocket("?test") - }); -}, "Url is ?test - should throw SYNTAX_ERR"); - -test(function() { - assert_throws_dom("SYNTAX_ERR", function() { - wsocket = new WebSocket("#test") - }); -}, "Url is #test - should throw SYNTAX_ERR"); +[ + "ws://foo bar.com/", + "wss://foo bar.com/", + "ftp://"+location.host+"/", + "mailto:example@example.org", + "about:blank", + location.origin + "/#", + location.origin + "/#test", + "#test" +].forEach(input => { + test(() => { + assert_throws_dom("SyntaxError", () => new WebSocket(input)); + }, `new WebSocket("${input}") should throw a "SyntaxError" DOMException`); +}); diff --git a/test/wpt/tests/websockets/Create-non-absolute-url.any.js b/test/wpt/tests/websockets/Create-non-absolute-url.any.js index 8d533fd2e04..5a7b1794d04 100644 --- a/test/wpt/tests/websockets/Create-non-absolute-url.any.js +++ b/test/wpt/tests/websockets/Create-non-absolute-url.any.js @@ -1,11 +1,14 @@ -// META: script=constants.sub.js -// META: variant= -// META: variant=?wss -// META: variant=?wpt_flags=h2 - -test(function() { - var wsocket; - assert_throws_dom("SYNTAX_ERR", function() { - wsocket = CreateWebSocketNonAbsolute() - }); -}, "Create WebSocket - Pass a non absolute URL - SYNTAX_ERR is thrown") +[ + "test", + "?", + null, + 123, +].forEach(input => { + test(() => { + const url = new URL(input, location); + url.protocol = "ws"; + const ws = new WebSocket(input); + assert_equals(ws.url, url.href); + ws.close(); + }, `Create WebSocket - Pass a non absolute URL: ${input}`); +}); diff --git a/test/wpt/tests/websockets/Create-nonAscii-protocol-string.any.js b/test/wpt/tests/websockets/Create-nonAscii-protocol-string.any.js index 1b56cc914b7..fda926a9d54 100644 --- a/test/wpt/tests/websockets/Create-nonAscii-protocol-string.any.js +++ b/test/wpt/tests/websockets/Create-nonAscii-protocol-string.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wss // META: variant=?wpt_flags=h2 diff --git a/test/wpt/tests/websockets/Create-protocol-with-space.any.js b/test/wpt/tests/websockets/Create-protocol-with-space.any.js index f49d1fec0c3..a85d4e5df9d 100644 --- a/test/wpt/tests/websockets/Create-protocol-with-space.any.js +++ b/test/wpt/tests/websockets/Create-protocol-with-space.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wpt_flags=h2 // META: variant=?wss diff --git a/test/wpt/tests/websockets/Create-protocols-repeated-case-insensitive.any.js b/test/wpt/tests/websockets/Create-protocols-repeated-case-insensitive.any.js index 41f78396fc3..1a508e87d38 100644 --- a/test/wpt/tests/websockets/Create-protocols-repeated-case-insensitive.any.js +++ b/test/wpt/tests/websockets/Create-protocols-repeated-case-insensitive.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wss // META: variant=?wpt_flags=h2 diff --git a/test/wpt/tests/websockets/Create-protocols-repeated.any.js b/test/wpt/tests/websockets/Create-protocols-repeated.any.js index fc7d1b6ad2f..2f12a47f964 100644 --- a/test/wpt/tests/websockets/Create-protocols-repeated.any.js +++ b/test/wpt/tests/websockets/Create-protocols-repeated.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wss // META: variant=?wpt_flags=h2 diff --git a/test/wpt/tests/websockets/Create-url-with-space.any.js b/test/wpt/tests/websockets/Create-url-with-space.any.js index d1e1ea1cba9..f2bea5b9d9f 100644 --- a/test/wpt/tests/websockets/Create-url-with-space.any.js +++ b/test/wpt/tests/websockets/Create-url-with-space.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wpt_flags=h2 // META: variant=?wss diff --git a/test/wpt/tests/websockets/Create-url-with-windows-1252-encoding.html b/test/wpt/tests/websockets/Create-url-with-windows-1252-encoding.html new file mode 100644 index 00000000000..6596b5e1a0b --- /dev/null +++ b/test/wpt/tests/websockets/Create-url-with-windows-1252-encoding.html @@ -0,0 +1,20 @@ + + + + + diff --git a/test/wpt/tests/websockets/Create-valid-url-array-protocols.any.js b/test/wpt/tests/websockets/Create-valid-url-array-protocols.any.js index 00ab1ca9873..fe71fd7ca23 100644 --- a/test/wpt/tests/websockets/Create-valid-url-array-protocols.any.js +++ b/test/wpt/tests/websockets/Create-valid-url-array-protocols.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wpt_flags=h2 // META: variant=?wss diff --git a/test/wpt/tests/websockets/Create-valid-url-binaryType-blob.any.js b/test/wpt/tests/websockets/Create-valid-url-binaryType-blob.any.js index 59eec8e29d3..7840ff314ff 100644 --- a/test/wpt/tests/websockets/Create-valid-url-binaryType-blob.any.js +++ b/test/wpt/tests/websockets/Create-valid-url-binaryType-blob.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wpt_flags=h2 // META: variant=?wss diff --git a/test/wpt/tests/websockets/Create-valid-url-protocol-empty.any.js b/test/wpt/tests/websockets/Create-valid-url-protocol-empty.any.js index 9e1de6dab46..f18a9d89084 100644 --- a/test/wpt/tests/websockets/Create-valid-url-protocol-empty.any.js +++ b/test/wpt/tests/websockets/Create-valid-url-protocol-empty.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wpt_flags=h2 // META: variant=?wss diff --git a/test/wpt/tests/websockets/Create-valid-url-protocol-setCorrectly.any.js b/test/wpt/tests/websockets/Create-valid-url-protocol-setCorrectly.any.js index bb1f32fbce1..c5d06ac84c0 100644 --- a/test/wpt/tests/websockets/Create-valid-url-protocol-setCorrectly.any.js +++ b/test/wpt/tests/websockets/Create-valid-url-protocol-setCorrectly.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wpt_flags=h2 // META: variant=?wss diff --git a/test/wpt/tests/websockets/Create-valid-url-protocol-string.any.js b/test/wpt/tests/websockets/Create-valid-url-protocol-string.any.js index 4f730db94f7..10e928d333f 100644 --- a/test/wpt/tests/websockets/Create-valid-url-protocol-string.any.js +++ b/test/wpt/tests/websockets/Create-valid-url-protocol-string.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wss // META: variant=?wpt_flags=h2 diff --git a/test/wpt/tests/websockets/Create-valid-url-protocol.any.js b/test/wpt/tests/websockets/Create-valid-url-protocol.any.js index 599a9eb8f1b..37b5a0e886c 100644 --- a/test/wpt/tests/websockets/Create-valid-url-protocol.any.js +++ b/test/wpt/tests/websockets/Create-valid-url-protocol.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wpt_flags=h2 // META: variant=?wss diff --git a/test/wpt/tests/websockets/Create-valid-url.any.js b/test/wpt/tests/websockets/Create-valid-url.any.js index edb27f61d3c..1df995fb8b0 100644 --- a/test/wpt/tests/websockets/Create-valid-url.any.js +++ b/test/wpt/tests/websockets/Create-valid-url.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wpt_flags=h2 // META: variant=?wss diff --git a/test/wpt/tests/websockets/Create-wrong-scheme.any.js b/test/wpt/tests/websockets/Create-wrong-scheme.any.js deleted file mode 100644 index 00cfffece60..00000000000 --- a/test/wpt/tests/websockets/Create-wrong-scheme.any.js +++ /dev/null @@ -1,11 +0,0 @@ -// META: script=constants.sub.js -// META: variant= -// META: variant=?wss -// META: variant=?wpt_flags=h2 - -test(function() { - var wsocket; - assert_throws_dom("SYNTAX_ERR", function() { - wsocket = CreateWebSocketNonWsScheme() - }); -}, "Create WebSocket - Pass a URL with a non ws/wss scheme - SYNTAX_ERR is thrown") diff --git a/test/wpt/tests/websockets/Send-0byte-data.any.js b/test/wpt/tests/websockets/Send-0byte-data.any.js index b984b641084..4176de411c6 100644 --- a/test/wpt/tests/websockets/Send-0byte-data.any.js +++ b/test/wpt/tests/websockets/Send-0byte-data.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wss // META: variant=?wpt_flags=h2 diff --git a/test/wpt/tests/websockets/Send-65K-data.any.js b/test/wpt/tests/websockets/Send-65K-data.any.js index 5c3437999b9..20e5ba7c949 100644 --- a/test/wpt/tests/websockets/Send-65K-data.any.js +++ b/test/wpt/tests/websockets/Send-65K-data.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wpt_flags=h2 // META: variant=?wss diff --git a/test/wpt/tests/websockets/Send-before-open.any.js b/test/wpt/tests/websockets/Send-before-open.any.js index 5982535583f..4fdbf71c6f5 100644 --- a/test/wpt/tests/websockets/Send-before-open.any.js +++ b/test/wpt/tests/websockets/Send-before-open.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wss // META: variant=?wpt_flags=h2 diff --git a/test/wpt/tests/websockets/Send-binary-65K-arraybuffer.any.js b/test/wpt/tests/websockets/Send-binary-65K-arraybuffer.any.js index 1e02ac2d37f..6bee660bb38 100644 --- a/test/wpt/tests/websockets/Send-binary-65K-arraybuffer.any.js +++ b/test/wpt/tests/websockets/Send-binary-65K-arraybuffer.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wpt_flags=h2 // META: variant=?wss diff --git a/test/wpt/tests/websockets/Send-binary-arraybuffer.any.js b/test/wpt/tests/websockets/Send-binary-arraybuffer.any.js index 5c985edd616..0b34e0cfc93 100644 --- a/test/wpt/tests/websockets/Send-binary-arraybuffer.any.js +++ b/test/wpt/tests/websockets/Send-binary-arraybuffer.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wpt_flags=h2 // META: variant=?wss diff --git a/test/wpt/tests/websockets/Send-binary-arraybufferview-float32.any.js b/test/wpt/tests/websockets/Send-binary-arraybufferview-float32.any.js index 9a8e3426f49..47ee5b1170b 100644 --- a/test/wpt/tests/websockets/Send-binary-arraybufferview-float32.any.js +++ b/test/wpt/tests/websockets/Send-binary-arraybufferview-float32.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wpt_flags=h2 // META: variant=?wss diff --git a/test/wpt/tests/websockets/Send-binary-arraybufferview-float64.any.js b/test/wpt/tests/websockets/Send-binary-arraybufferview-float64.any.js index d71d2d8c58f..78bcb13d43f 100644 --- a/test/wpt/tests/websockets/Send-binary-arraybufferview-float64.any.js +++ b/test/wpt/tests/websockets/Send-binary-arraybufferview-float64.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wpt_flags=h2 // META: variant=?wss diff --git a/test/wpt/tests/websockets/Send-binary-arraybufferview-int16-offset.any.js b/test/wpt/tests/websockets/Send-binary-arraybufferview-int16-offset.any.js index bb77d300ad1..3dd64552b01 100644 --- a/test/wpt/tests/websockets/Send-binary-arraybufferview-int16-offset.any.js +++ b/test/wpt/tests/websockets/Send-binary-arraybufferview-int16-offset.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wpt_flags=h2 // META: variant=?wss diff --git a/test/wpt/tests/websockets/Send-binary-arraybufferview-int32.any.js b/test/wpt/tests/websockets/Send-binary-arraybufferview-int32.any.js index f4312e410ab..853ba39b4b5 100644 --- a/test/wpt/tests/websockets/Send-binary-arraybufferview-int32.any.js +++ b/test/wpt/tests/websockets/Send-binary-arraybufferview-int32.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wpt_flags=h2 // META: variant=?wss diff --git a/test/wpt/tests/websockets/Send-binary-arraybufferview-int8.any.js b/test/wpt/tests/websockets/Send-binary-arraybufferview-int8.any.js index f2374fb4139..aa90020bba8 100644 --- a/test/wpt/tests/websockets/Send-binary-arraybufferview-int8.any.js +++ b/test/wpt/tests/websockets/Send-binary-arraybufferview-int8.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wss // META: variant=?wpt_flags=h2 diff --git a/test/wpt/tests/websockets/Send-binary-arraybufferview-uint16-offset-length.any.js b/test/wpt/tests/websockets/Send-binary-arraybufferview-uint16-offset-length.any.js index f917a3af007..a3c1f326a59 100644 --- a/test/wpt/tests/websockets/Send-binary-arraybufferview-uint16-offset-length.any.js +++ b/test/wpt/tests/websockets/Send-binary-arraybufferview-uint16-offset-length.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wpt_flags=h2 // META: variant=?wss diff --git a/test/wpt/tests/websockets/Send-binary-arraybufferview-uint32-offset.any.js b/test/wpt/tests/websockets/Send-binary-arraybufferview-uint32-offset.any.js index 33758dc6544..fede995450c 100644 --- a/test/wpt/tests/websockets/Send-binary-arraybufferview-uint32-offset.any.js +++ b/test/wpt/tests/websockets/Send-binary-arraybufferview-uint32-offset.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wpt_flags=h2 // META: variant=?wss diff --git a/test/wpt/tests/websockets/Send-binary-arraybufferview-uint8-offset-length.any.js b/test/wpt/tests/websockets/Send-binary-arraybufferview-uint8-offset-length.any.js index 1d256dbdca1..de3ae00f270 100644 --- a/test/wpt/tests/websockets/Send-binary-arraybufferview-uint8-offset-length.any.js +++ b/test/wpt/tests/websockets/Send-binary-arraybufferview-uint8-offset-length.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wpt_flags=h2 // META: variant=?wss diff --git a/test/wpt/tests/websockets/Send-binary-arraybufferview-uint8-offset.any.js b/test/wpt/tests/websockets/Send-binary-arraybufferview-uint8-offset.any.js index 43e9fe68499..089174b384d 100644 --- a/test/wpt/tests/websockets/Send-binary-arraybufferview-uint8-offset.any.js +++ b/test/wpt/tests/websockets/Send-binary-arraybufferview-uint8-offset.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wpt_flags=h2 // META: variant=?wss diff --git a/test/wpt/tests/websockets/Send-binary-blob.any.js b/test/wpt/tests/websockets/Send-binary-blob.any.js index 56c89a1b53c..5131b716b4f 100644 --- a/test/wpt/tests/websockets/Send-binary-blob.any.js +++ b/test/wpt/tests/websockets/Send-binary-blob.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wpt_flags=h2 // META: variant=?wss diff --git a/test/wpt/tests/websockets/Send-data.any.js b/test/wpt/tests/websockets/Send-data.any.js index 203ab54dffc..a606ada310d 100644 --- a/test/wpt/tests/websockets/Send-data.any.js +++ b/test/wpt/tests/websockets/Send-data.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wss // META: variant=?wpt_flags=h2 diff --git a/test/wpt/tests/websockets/Send-data.worker.js b/test/wpt/tests/websockets/Send-data.worker.js index b141fb38207..5a8bdd5fa3a 100644 --- a/test/wpt/tests/websockets/Send-data.worker.js +++ b/test/wpt/tests/websockets/Send-data.worker.js @@ -1,4 +1,4 @@ -// META: variant= +// META: variant=?default // META: variant=?wss // META: variant=?wpt_flags=h2 diff --git a/test/wpt/tests/websockets/Send-null.any.js b/test/wpt/tests/websockets/Send-null.any.js index a12eaf9c591..e621bb892bb 100644 --- a/test/wpt/tests/websockets/Send-null.any.js +++ b/test/wpt/tests/websockets/Send-null.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wpt_flags=h2 // META: variant=?wss diff --git a/test/wpt/tests/websockets/Send-paired-surrogates.any.js b/test/wpt/tests/websockets/Send-paired-surrogates.any.js index e2dc004bfcf..51e4fb965e2 100644 --- a/test/wpt/tests/websockets/Send-paired-surrogates.any.js +++ b/test/wpt/tests/websockets/Send-paired-surrogates.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wpt_flags=h2 // META: variant=?wss diff --git a/test/wpt/tests/websockets/Send-unicode-data.any.js b/test/wpt/tests/websockets/Send-unicode-data.any.js index f22094a243c..a3556b26ab2 100644 --- a/test/wpt/tests/websockets/Send-unicode-data.any.js +++ b/test/wpt/tests/websockets/Send-unicode-data.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wpt_flags=h2 // META: variant=?wss diff --git a/test/wpt/tests/websockets/Send-unpaired-surrogates.any.js b/test/wpt/tests/websockets/Send-unpaired-surrogates.any.js index 1cb5d0ac9cd..cbbcc6ebf64 100644 --- a/test/wpt/tests/websockets/Send-unpaired-surrogates.any.js +++ b/test/wpt/tests/websockets/Send-unpaired-surrogates.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wss // META: variant=?wpt_flags=h2 diff --git a/test/wpt/tests/websockets/back-forward-cache-with-closed-websocket-connection-ccns.tentative.window.js b/test/wpt/tests/websockets/back-forward-cache-with-closed-websocket-connection-ccns.tentative.window.js new file mode 100644 index 00000000000..ccc45f2877d --- /dev/null +++ b/test/wpt/tests/websockets/back-forward-cache-with-closed-websocket-connection-ccns.tentative.window.js @@ -0,0 +1,34 @@ +// META: title=Testing BFCache support for page with closed WebSocket connection and "Cache-Control: no-store" header. +// META: script=/common/dispatcher/dispatcher.js +// META: script=/common/utils.js +// META: script=/html/browsers/browsing-the-web/back-forward-cache/resources/rc-helper.js +// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js +// META: script=/websockets/constants.sub.js +// META: script=resources/websockets-test-helpers.sub.js + +'use strict'; + +promise_test(async t => { + const rcHelper = new RemoteContextHelper(); + + // Open a window with noopener so that BFCache will work. + const rc1 = await rcHelper.addWindow( + /*config=*/ { headers: [['Cache-Control', 'no-store']] }, + /*options=*/ { features: 'noopener' } + ); + // Make sure that we only run the remaining of the test when page with + // "Cache-Control: no-store" header is eligible for BFCache. + await assertBFCacheEligibility(rc1, /*shouldRestoreFromBFCache=*/ true); + + await openThenCloseWebSocket(rc1); + // The page should not be eligible for BFCache because of the usage + // of WebSocket. + await assertBFCacheEligibility(rc1, /*shouldRestoreFromBFCache=*/ false); + // The `BrowsingInstanceNotSwapped` reason will be added because of the + // sticky feature, and it will be reported as "Internal error". + await assertNotRestoredFromBFCache(rc1, [ + 'WebSocketSticky', + 'MainResourceHasCacheControlNoStore', + 'Internal error' + ]); +}); diff --git a/test/wpt/tests/websockets/back-forward-cache-with-closed-websocket-connection.window.js b/test/wpt/tests/websockets/back-forward-cache-with-closed-websocket-connection.window.js new file mode 100644 index 00000000000..30b8e63a2cd --- /dev/null +++ b/test/wpt/tests/websockets/back-forward-cache-with-closed-websocket-connection.window.js @@ -0,0 +1,20 @@ +// META: title=Testing BFCache support for page with closed WebSocket connection. +// META: script=/common/dispatcher/dispatcher.js +// META: script=/common/utils.js +// META: script=/html/browsers/browsing-the-web/back-forward-cache/resources/rc-helper.js +// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js +// META: script=/websockets/constants.sub.js +// META: script=resources/websockets-test-helpers.sub.js + +'use strict'; + +promise_test(async t => { + const rcHelper = new RemoteContextHelper(); + + // Open a window with noopener so that BFCache will work. + const rc1 = await rcHelper.addWindow( + /*config=*/ null, /*options=*/ { features: 'noopener' }); + await openThenCloseWebSocket(rc1); + // The page should be eligible for BFCache because the WebSocket connection has been closed. + await assertBFCacheEligibility(rc1, /*shouldRestoreFromBFCache=*/ true); +}); diff --git a/test/wpt/tests/websockets/back-forward-cache-with-open-websocket-connection-ccns.tentative.window.js b/test/wpt/tests/websockets/back-forward-cache-with-open-websocket-connection-ccns.tentative.window.js new file mode 100644 index 00000000000..563fd4792ef --- /dev/null +++ b/test/wpt/tests/websockets/back-forward-cache-with-open-websocket-connection-ccns.tentative.window.js @@ -0,0 +1,35 @@ +// META: title=Testing BFCache support for page with open WebSocket connection and "Cache-Control: no-store" header. +// META: script=/common/dispatcher/dispatcher.js +// META: script=/common/utils.js +// META: script=/html/browsers/browsing-the-web/back-forward-cache/resources/rc-helper.js +// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js +// META: script=/websockets/constants.sub.js +// META: script=resources/websockets-test-helpers.sub.js + +'use strict'; + +promise_test(async t => { + const rcHelper = new RemoteContextHelper(); + + // Open a window with noopener so that BFCache will work. + const rc1 = await rcHelper.addWindow( + /*config=*/ { headers: [['Cache-Control', 'no-store']] }, + /*options=*/ { features: 'noopener' } + ); + // Make sure that we only run the remaining of the test when page with + // "Cache-Control: no-store" header is eligible for BFCache. + await assertBFCacheEligibility(rc1, /*shouldRestoreFromBFCache=*/ true); + + await openWebSocket(rc1); + // The page should not be eligible for BFCache because of the usage + // of WebSocket. + await assertBFCacheEligibility(rc1, /*shouldRestoreFromBFCache=*/ false); + // The `BrowsingInstanceNotSwapped` reason will be added because of the + // sticky feature, and it will be reported as "Internal error". + await assertNotRestoredFromBFCache(rc1, [ + 'WebSocket', + 'WebSocketSticky', + 'MainResourceHasCacheControlNoStore', + 'Internal error' + ]); +}); diff --git a/test/wpt/tests/websockets/back-forward-cache-with-open-websocket-connection.window.js b/test/wpt/tests/websockets/back-forward-cache-with-open-websocket-connection.window.js new file mode 100644 index 00000000000..2baf38f303c --- /dev/null +++ b/test/wpt/tests/websockets/back-forward-cache-with-open-websocket-connection.window.js @@ -0,0 +1,21 @@ +// META: title=Testing BFCache support for page with open WebSocket connection. +// META: script=/common/dispatcher/dispatcher.js +// META: script=/common/utils.js +// META: script=/html/browsers/browsing-the-web/back-forward-cache/resources/rc-helper.js +// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js +// META: script=/websockets/constants.sub.js +// META: script=resources/websockets-test-helpers.sub.js + +'use strict'; + +promise_test(async t => { + const rcHelper = new RemoteContextHelper(); + + // Open a window with noopener so that BFCache will work. + const rc1 = await rcHelper.addWindow( + /*config=*/ null, /*options=*/ { features: 'noopener' }); + await openWebSocket(rc1); + // The page should not be eligible for BFCache because of open WebSocket connection. + await assertBFCacheEligibility(rc1, /*shouldRestoreFromBFCache=*/ false); + await assertNotRestoredFromBFCache(rc1, ['WebSocket']); +}); diff --git a/test/wpt/tests/websockets/binary/001.html b/test/wpt/tests/websockets/binary/001.html index 21ffff40eb0..077bf79def3 100644 --- a/test/wpt/tests/websockets/binary/001.html +++ b/test/wpt/tests/websockets/binary/001.html @@ -3,7 +3,7 @@ - +
diff --git a/test/wpt/tests/websockets/binary/002.html b/test/wpt/tests/websockets/binary/002.html index ffd1ee7a7a2..558777694bb 100644 --- a/test/wpt/tests/websockets/binary/002.html +++ b/test/wpt/tests/websockets/binary/002.html @@ -4,7 +4,7 @@ - +
diff --git a/test/wpt/tests/websockets/binary/004.html b/test/wpt/tests/websockets/binary/004.html index 76bb902cf95..8ca4e923ea0 100644 --- a/test/wpt/tests/websockets/binary/004.html +++ b/test/wpt/tests/websockets/binary/004.html @@ -4,7 +4,7 @@ - +
diff --git a/test/wpt/tests/websockets/binary/005.html b/test/wpt/tests/websockets/binary/005.html index 9b8b2c4edfd..e89f4c0e837 100644 --- a/test/wpt/tests/websockets/binary/005.html +++ b/test/wpt/tests/websockets/binary/005.html @@ -3,7 +3,7 @@ - +
diff --git a/test/wpt/tests/websockets/binaryType-wrong-value.any.js b/test/wpt/tests/websockets/binaryType-wrong-value.any.js index 007510d030b..683fb47bede 100644 --- a/test/wpt/tests/websockets/binaryType-wrong-value.any.js +++ b/test/wpt/tests/websockets/binaryType-wrong-value.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wss // META: variant=?wpt_flags=h2 diff --git a/test/wpt/tests/websockets/bufferedAmount-unchanged-by-sync-xhr.any.js b/test/wpt/tests/websockets/bufferedAmount-unchanged-by-sync-xhr.any.js index b247ee56f62..c15536d7674 100644 --- a/test/wpt/tests/websockets/bufferedAmount-unchanged-by-sync-xhr.any.js +++ b/test/wpt/tests/websockets/bufferedAmount-unchanged-by-sync-xhr.any.js @@ -1,6 +1,6 @@ // META: script=constants.sub.js // META: global=window,dedicatedworker,sharedworker -// META: variant= +// META: variant=?default // META: variant=?wss // META: variant=?wpt_flags=h2 diff --git a/test/wpt/tests/websockets/close-invalid.any.js b/test/wpt/tests/websockets/close-invalid.any.js index 3223063765a..c964c8391d5 100644 --- a/test/wpt/tests/websockets/close-invalid.any.js +++ b/test/wpt/tests/websockets/close-invalid.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wss // META: variant=?wpt_flags=h2 diff --git a/test/wpt/tests/websockets/closing-handshake/002.html b/test/wpt/tests/websockets/closing-handshake/002.html index b0e39a955ce..8d1e43b6de9 100644 --- a/test/wpt/tests/websockets/closing-handshake/002.html +++ b/test/wpt/tests/websockets/closing-handshake/002.html @@ -3,7 +3,7 @@ - +
diff --git a/test/wpt/tests/websockets/closing-handshake/003.html b/test/wpt/tests/websockets/closing-handshake/003.html index 72eb09d8337..43e1603d34f 100644 --- a/test/wpt/tests/websockets/closing-handshake/003.html +++ b/test/wpt/tests/websockets/closing-handshake/003.html @@ -3,7 +3,7 @@ - +
diff --git a/test/wpt/tests/websockets/closing-handshake/004.html b/test/wpt/tests/websockets/closing-handshake/004.html index eb89ad33857..96411ea6390 100644 --- a/test/wpt/tests/websockets/closing-handshake/004.html +++ b/test/wpt/tests/websockets/closing-handshake/004.html @@ -3,7 +3,7 @@ - +
diff --git a/test/wpt/tests/websockets/constants.sub.js b/test/wpt/tests/websockets/constants.sub.js index 65ea4f66f29..fd3c3b84b96 100644 --- a/test/wpt/tests/websockets/constants.sub.js +++ b/test/wpt/tests/websockets/constants.sub.js @@ -1,14 +1,14 @@ const __SERVER__NAME = "{{host}}"; const __PATH = "echo"; -var __SCHEME; -var __PORT; -if (url_has_variant('wss')) { - __SCHEME = 'wss'; - __PORT = "{{ports[wss][0]}}"; -} else if (url_has_flag('h2')) { +let __SCHEME; +let __PORT; +if (url_has_flag('h2')) { __SCHEME = 'wss'; __PORT = "{{ports[h2][0]}}"; +} else if (url_has_variant('wss') || location.protocol === 'https:') { + __SCHEME = 'wss'; + __PORT = "{{ports[wss][0]}}"; } else { __SCHEME = 'ws'; __PORT = "{{ports[ws][0]}}"; @@ -32,18 +32,6 @@ function IsWebSocket() { } } -function CreateWebSocketNonAbsolute() { - IsWebSocket(); - const url = __SERVER__NAME; - return new WebSocket(url); -} - -function CreateWebSocketNonWsScheme() { - IsWebSocket(); - const url = "http://" + __SERVER__NAME + ":" + __PORT + "/" + __PATH; - return new WebSocket(url); -} - function CreateWebSocketNonAsciiProtocol(nonAsciiProtocol) { IsWebSocket(); const url = SCHEME_DOMAIN_PORT + "/" + __PATH; @@ -86,6 +74,12 @@ function CreateWebSocketWithRepeatedProtocolsCaseInsensitive() { wsocket = new WebSocket(url, ["echo", "eCho"]); } +function CreateInsecureWebSocket() { + IsWebSocket(); + const url = `ws://${__SERVER__NAME}:{{ports[ws][0]}}/${__PATH}`; + return new WebSocket(url); +} + function CreateWebSocket(isProtocol, isProtocols) { IsWebSocket(); const url = SCHEME_DOMAIN_PORT + "/" + __PATH; diff --git a/test/wpt/tests/websockets/constructor.any.js b/test/wpt/tests/websockets/constructor.any.js index c92dda4567c..0cef2065f6d 100644 --- a/test/wpt/tests/websockets/constructor.any.js +++ b/test/wpt/tests/websockets/constructor.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wss // META: variant=?wpt_flags=h2 diff --git a/test/wpt/tests/websockets/constructor/001.html b/test/wpt/tests/websockets/constructor/001.html index 0c39a0ee71b..13493e3430f 100644 --- a/test/wpt/tests/websockets/constructor/001.html +++ b/test/wpt/tests/websockets/constructor/001.html @@ -3,7 +3,7 @@ - +
diff --git a/test/wpt/tests/websockets/constructor/002.html b/test/wpt/tests/websockets/constructor/002.html deleted file mode 100644 index 8c80c23735f..00000000000 --- a/test/wpt/tests/websockets/constructor/002.html +++ /dev/null @@ -1,21 +0,0 @@ - -WebSockets: new WebSocket(invalid url) - - - - - - -
- diff --git a/test/wpt/tests/websockets/constructor/004.html b/test/wpt/tests/websockets/constructor/004.html index e599bf224ae..814321089be 100644 --- a/test/wpt/tests/websockets/constructor/004.html +++ b/test/wpt/tests/websockets/constructor/004.html @@ -3,7 +3,7 @@ - +
diff --git a/test/wpt/tests/websockets/constructor/005.html b/test/wpt/tests/websockets/constructor/005.html index 377d9c4217a..9d467def3f9 100644 --- a/test/wpt/tests/websockets/constructor/005.html +++ b/test/wpt/tests/websockets/constructor/005.html @@ -3,7 +3,7 @@ - +
diff --git a/test/wpt/tests/websockets/constructor/006.html b/test/wpt/tests/websockets/constructor/006.html index 65178e00165..59875830da2 100644 --- a/test/wpt/tests/websockets/constructor/006.html +++ b/test/wpt/tests/websockets/constructor/006.html @@ -3,7 +3,7 @@ - +
diff --git a/test/wpt/tests/websockets/constructor/007.html b/test/wpt/tests/websockets/constructor/007.html index 647f4202a17..e126d1aa0f8 100644 --- a/test/wpt/tests/websockets/constructor/007.html +++ b/test/wpt/tests/websockets/constructor/007.html @@ -3,7 +3,7 @@ - +
diff --git a/test/wpt/tests/websockets/constructor/008.html b/test/wpt/tests/websockets/constructor/008.html index de7fb457bae..e10c652134e 100644 --- a/test/wpt/tests/websockets/constructor/008.html +++ b/test/wpt/tests/websockets/constructor/008.html @@ -3,7 +3,7 @@ - +
- +
diff --git a/test/wpt/tests/websockets/constructor/010.html b/test/wpt/tests/websockets/constructor/010.html index 0adf2b13f9b..e5bc6ecc36e 100644 --- a/test/wpt/tests/websockets/constructor/010.html +++ b/test/wpt/tests/websockets/constructor/010.html @@ -3,7 +3,7 @@ - +
diff --git a/test/wpt/tests/websockets/constructor/011.html b/test/wpt/tests/websockets/constructor/011.html index 9b7f114dc03..33b09dbaf8f 100644 --- a/test/wpt/tests/websockets/constructor/011.html +++ b/test/wpt/tests/websockets/constructor/011.html @@ -3,7 +3,7 @@ - +
diff --git a/test/wpt/tests/websockets/constructor/012.html b/test/wpt/tests/websockets/constructor/012.html index 34723616728..ba2b6b2df06 100644 --- a/test/wpt/tests/websockets/constructor/012.html +++ b/test/wpt/tests/websockets/constructor/012.html @@ -3,7 +3,7 @@ - +
diff --git a/test/wpt/tests/websockets/constructor/013.html b/test/wpt/tests/websockets/constructor/013.html index 53b0400a2d6..d599fde528a 100644 --- a/test/wpt/tests/websockets/constructor/013.html +++ b/test/wpt/tests/websockets/constructor/013.html @@ -4,7 +4,7 @@ - +
diff --git a/test/wpt/tests/websockets/constructor/014.html b/test/wpt/tests/websockets/constructor/014.html index f3f38ad2788..afa0dac4c1d 100644 --- a/test/wpt/tests/websockets/constructor/014.html +++ b/test/wpt/tests/websockets/constructor/014.html @@ -4,7 +4,7 @@ - +
- +
diff --git a/test/wpt/tests/websockets/constructor/017.html b/test/wpt/tests/websockets/constructor/017.html index 5087290f3ca..e1795b175e3 100644 --- a/test/wpt/tests/websockets/constructor/017.html +++ b/test/wpt/tests/websockets/constructor/017.html @@ -3,7 +3,7 @@ - +
diff --git a/test/wpt/tests/websockets/constructor/018.html b/test/wpt/tests/websockets/constructor/018.html index f4d6ab30d95..71f7376496a 100644 --- a/test/wpt/tests/websockets/constructor/018.html +++ b/test/wpt/tests/websockets/constructor/018.html @@ -3,7 +3,7 @@ - +
diff --git a/test/wpt/tests/websockets/constructor/019.html b/test/wpt/tests/websockets/constructor/019.html index a0ec6c3ad78..8fbb1cbfff6 100644 --- a/test/wpt/tests/websockets/constructor/019.html +++ b/test/wpt/tests/websockets/constructor/019.html @@ -3,7 +3,7 @@ - +
diff --git a/test/wpt/tests/websockets/constructor/020.html b/test/wpt/tests/websockets/constructor/020.html index f050a1b8fa9..e4d61f366ea 100644 --- a/test/wpt/tests/websockets/constructor/020.html +++ b/test/wpt/tests/websockets/constructor/020.html @@ -3,7 +3,7 @@ - +
diff --git a/test/wpt/tests/websockets/constructor/021.html b/test/wpt/tests/websockets/constructor/021.html index 039d74b0430..d3854feeb47 100644 --- a/test/wpt/tests/websockets/constructor/021.html +++ b/test/wpt/tests/websockets/constructor/021.html @@ -3,7 +3,7 @@ - +
diff --git a/test/wpt/tests/websockets/constructor/022.html b/test/wpt/tests/websockets/constructor/022.html index a55d8f349d7..fd53c0f29ab 100644 --- a/test/wpt/tests/websockets/constructor/022.html +++ b/test/wpt/tests/websockets/constructor/022.html @@ -3,7 +3,7 @@ - +
diff --git a/test/wpt/tests/websockets/cookies/001.html b/test/wpt/tests/websockets/cookies/001.html index c43947fa877..abec94e4b10 100644 --- a/test/wpt/tests/websockets/cookies/001.html +++ b/test/wpt/tests/websockets/cookies/001.html @@ -3,7 +3,7 @@ - +
diff --git a/test/wpt/tests/websockets/cookies/002.html b/test/wpt/tests/websockets/cookies/002.html index 1a5e03e3351..758ce473a91 100644 --- a/test/wpt/tests/websockets/cookies/002.html +++ b/test/wpt/tests/websockets/cookies/002.html @@ -3,7 +3,7 @@ - +
diff --git a/test/wpt/tests/websockets/cookies/003.html b/test/wpt/tests/websockets/cookies/003.html index 2af47354028..9f770aef220 100644 --- a/test/wpt/tests/websockets/cookies/003.html +++ b/test/wpt/tests/websockets/cookies/003.html @@ -3,7 +3,7 @@ - +
diff --git a/test/wpt/tests/websockets/cookies/004.html b/test/wpt/tests/websockets/cookies/004.html index efc3a9f84d4..523dabaf635 100644 --- a/test/wpt/tests/websockets/cookies/004.html +++ b/test/wpt/tests/websockets/cookies/004.html @@ -3,7 +3,7 @@ - +
- +
- +
diff --git a/test/wpt/tests/websockets/cookies/007.html b/test/wpt/tests/websockets/cookies/007.html index 2c214a1dbb0..3e69bfc09d5 100644 --- a/test/wpt/tests/websockets/cookies/007.html +++ b/test/wpt/tests/websockets/cookies/007.html @@ -4,7 +4,7 @@ - +
diff --git a/test/wpt/tests/websockets/eventhandlers.any.js b/test/wpt/tests/websockets/eventhandlers.any.js index 7bccd47139b..f596328b923 100644 --- a/test/wpt/tests/websockets/eventhandlers.any.js +++ b/test/wpt/tests/websockets/eventhandlers.any.js @@ -1,5 +1,5 @@ // META: script=constants.sub.js -// META: variant= +// META: variant=?default // META: variant=?wss // META: variant=?wpt_flags=h2 diff --git a/test/wpt/tests/websockets/extended-payload-length.html b/test/wpt/tests/websockets/extended-payload-length.html index 23caedc461c..92e3802c352 100644 --- a/test/wpt/tests/websockets/extended-payload-length.html +++ b/test/wpt/tests/websockets/extended-payload-length.html @@ -4,7 +4,7 @@ - +
diff --git a/test/wpt/tests/websockets/handlers/receive-many-with-backpressure_wsh.py b/test/wpt/tests/websockets/handlers/receive-many-with-backpressure_wsh.py new file mode 100644 index 00000000000..8e35beebdf5 --- /dev/null +++ b/test/wpt/tests/websockets/handlers/receive-many-with-backpressure_wsh.py @@ -0,0 +1,23 @@ +# Sleep to build backpressure, receive messages, and send back their length. +# Used by send-many-64K-messages-with-backpressure.any.js. + + +import time + + +def web_socket_do_extra_handshake(request): + # Compression will interfere with backpressure, so disable the + # permessage-delate extension. + request.ws_extension_processors = [] + + +def web_socket_transfer_data(request): + while True: + # Don't read the message immediately, so backpressure can build. + time.sleep(0.1) + line = request.ws_stream.receive_message() + if line is None: + return + # Send back the size of the message as acknowledgement that it was + # received. + request.ws_stream.send_message(str(len(line)), binary=False) diff --git a/test/wpt/tests/websockets/interfaces/CloseEvent/clean-close.html b/test/wpt/tests/websockets/interfaces/CloseEvent/clean-close.html index db253761f63..8614028fd05 100644 --- a/test/wpt/tests/websockets/interfaces/CloseEvent/clean-close.html +++ b/test/wpt/tests/websockets/interfaces/CloseEvent/clean-close.html @@ -3,7 +3,7 @@ - +
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-arraybuffer.html b/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-arraybuffer.html index 258eaa78279..5d2bfd076cb 100644 --- a/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-arraybuffer.html +++ b/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-arraybuffer.html @@ -4,7 +4,7 @@ - +
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-blob.html b/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-blob.html index ac5140de6cc..d0028dae0f4 100644 --- a/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-blob.html +++ b/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-blob.html @@ -4,7 +4,7 @@ - +
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-defineProperty-getter.html b/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-defineProperty-getter.html index df1c3a5bb22..ea6e70cfcf7 100644 --- a/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-defineProperty-getter.html +++ b/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-defineProperty-getter.html @@ -4,7 +4,7 @@ - +
- +
- +
- +
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-initial.html b/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-initial.html index 7c66d202826..be37b6dea0e 100644 --- a/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-initial.html +++ b/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-initial.html @@ -4,7 +4,7 @@ - +
- +
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-readonly.html b/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-readonly.html index 951aaea57b7..152da696e1a 100644 --- a/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-readonly.html +++ b/test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-readonly.html @@ -4,7 +4,7 @@ - +
- +
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/close/close-basic.html b/test/wpt/tests/websockets/interfaces/WebSocket/close/close-basic.html index b9059414954..b646ca41679 100644 --- a/test/wpt/tests/websockets/interfaces/WebSocket/close/close-basic.html +++ b/test/wpt/tests/websockets/interfaces/WebSocket/close/close-basic.html @@ -3,7 +3,7 @@ - +
- +
- +
- +
- +
- +
- +
- +
- +
- +
- +
- +
- +
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/events/002.html b/test/wpt/tests/websockets/interfaces/WebSocket/events/002.html index df8e9f73d54..481730800c2 100644 --- a/test/wpt/tests/websockets/interfaces/WebSocket/events/002.html +++ b/test/wpt/tests/websockets/interfaces/WebSocket/events/002.html @@ -3,7 +3,7 @@ - +
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/events/003.html b/test/wpt/tests/websockets/interfaces/WebSocket/events/003.html index f03ab49be67..a5373ecded2 100644 --- a/test/wpt/tests/websockets/interfaces/WebSocket/events/003.html +++ b/test/wpt/tests/websockets/interfaces/WebSocket/events/003.html @@ -3,7 +3,7 @@ - +
- +
- +
- +
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/events/008.html b/test/wpt/tests/websockets/interfaces/WebSocket/events/008.html index eb46efbb1a3..066eb0922e3 100644 --- a/test/wpt/tests/websockets/interfaces/WebSocket/events/008.html +++ b/test/wpt/tests/websockets/interfaces/WebSocket/events/008.html @@ -3,7 +3,7 @@ - +
- +
- +
- +
- +
- +
- +
- +
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/events/016.html b/test/wpt/tests/websockets/interfaces/WebSocket/events/016.html index fcd14c854ce..8b5aaf9f42c 100644 --- a/test/wpt/tests/websockets/interfaces/WebSocket/events/016.html +++ b/test/wpt/tests/websockets/interfaces/WebSocket/events/016.html @@ -4,7 +4,7 @@ - +
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/events/017.html b/test/wpt/tests/websockets/interfaces/WebSocket/events/017.html index 9208261c121..a9f06eaf7df 100644 --- a/test/wpt/tests/websockets/interfaces/WebSocket/events/017.html +++ b/test/wpt/tests/websockets/interfaces/WebSocket/events/017.html @@ -3,7 +3,7 @@ - +
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/events/018.html b/test/wpt/tests/websockets/interfaces/WebSocket/events/018.html index 3dc36d0c5f1..a340c69f451 100644 --- a/test/wpt/tests/websockets/interfaces/WebSocket/events/018.html +++ b/test/wpt/tests/websockets/interfaces/WebSocket/events/018.html @@ -3,7 +3,7 @@ - +
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/events/019.html b/test/wpt/tests/websockets/interfaces/WebSocket/events/019.html index cbc271d9e63..deb079f81aa 100644 --- a/test/wpt/tests/websockets/interfaces/WebSocket/events/019.html +++ b/test/wpt/tests/websockets/interfaces/WebSocket/events/019.html @@ -3,7 +3,7 @@ - +
- +
- +
- +
- +
- +
- +
- +
- +
- +
- +
- +
- +
- +
- +
- +
- +
- +
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/send/007.html b/test/wpt/tests/websockets/interfaces/WebSocket/send/007.html index e2a8999e2fa..6a5614257a3 100644 --- a/test/wpt/tests/websockets/interfaces/WebSocket/send/007.html +++ b/test/wpt/tests/websockets/interfaces/WebSocket/send/007.html @@ -4,7 +4,7 @@ - +
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/send/008.html b/test/wpt/tests/websockets/interfaces/WebSocket/send/008.html index 7b6125b8c8e..709c066aded 100644 --- a/test/wpt/tests/websockets/interfaces/WebSocket/send/008.html +++ b/test/wpt/tests/websockets/interfaces/WebSocket/send/008.html @@ -4,7 +4,7 @@ - +
- +
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/send/010.html b/test/wpt/tests/websockets/interfaces/WebSocket/send/010.html index 4e4f5c57b82..4a008b610c5 100644 --- a/test/wpt/tests/websockets/interfaces/WebSocket/send/010.html +++ b/test/wpt/tests/websockets/interfaces/WebSocket/send/010.html @@ -5,7 +5,7 @@ - +
- +
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/send/012.html b/test/wpt/tests/websockets/interfaces/WebSocket/send/012.html index 058ab9f6214..9876c7bdd32 100644 --- a/test/wpt/tests/websockets/interfaces/WebSocket/send/012.html +++ b/test/wpt/tests/websockets/interfaces/WebSocket/send/012.html @@ -4,7 +4,7 @@ - +
diff --git a/test/wpt/tests/websockets/interfaces/WebSocket/url/001.html b/test/wpt/tests/websockets/interfaces/WebSocket/url/001.html index 76f68b90933..6c7306d0388 100644 --- a/test/wpt/tests/websockets/interfaces/WebSocket/url/001.html +++ b/test/wpt/tests/websockets/interfaces/WebSocket/url/001.html @@ -3,7 +3,7 @@ - +
- +
- +
- +
- +
- +
- +
- +
diff --git a/test/wpt/tests/websockets/mixed-content.https.any.js b/test/wpt/tests/websockets/mixed-content.https.any.js new file mode 100644 index 00000000000..b7a6d8381da --- /dev/null +++ b/test/wpt/tests/websockets/mixed-content.https.any.js @@ -0,0 +1,7 @@ +// META: global=window,worker +// META: script=constants.sub.js + +test(() => { + assert_throws_dom('SecurityError', () => CreateInsecureWebSocket(), + 'constructor should throw'); +}, 'constructing an insecure WebSocket in a secure context should throw'); diff --git a/test/wpt/tests/websockets/multi-globals/url-parsing/current/current.html b/test/wpt/tests/websockets/multi-globals/url-parsing/current/current.html new file mode 100644 index 00000000000..82a48d40990 --- /dev/null +++ b/test/wpt/tests/websockets/multi-globals/url-parsing/current/current.html @@ -0,0 +1,2 @@ + +Current page used as a test helper diff --git a/test/wpt/tests/websockets/multi-globals/url-parsing/incumbent/incumbent.html b/test/wpt/tests/websockets/multi-globals/url-parsing/incumbent/incumbent.html new file mode 100644 index 00000000000..2c5572b7749 --- /dev/null +++ b/test/wpt/tests/websockets/multi-globals/url-parsing/incumbent/incumbent.html @@ -0,0 +1,13 @@ + +Incumbent page used as a test helper + + + + diff --git a/test/wpt/tests/websockets/multi-globals/url-parsing/url-parsing.html b/test/wpt/tests/websockets/multi-globals/url-parsing/url-parsing.html new file mode 100644 index 00000000000..21ef6cd3a95 --- /dev/null +++ b/test/wpt/tests/websockets/multi-globals/url-parsing/url-parsing.html @@ -0,0 +1,22 @@ + +Multiple globals for base URL in WebSocket constructor + + + + + + + + diff --git a/test/wpt/tests/websockets/opening-handshake/001.html b/test/wpt/tests/websockets/opening-handshake/001.html index cbc0355e2bd..d8585d833ba 100644 --- a/test/wpt/tests/websockets/opening-handshake/001.html +++ b/test/wpt/tests/websockets/opening-handshake/001.html @@ -3,7 +3,7 @@ - +
diff --git a/test/wpt/tests/websockets/opening-handshake/002.html b/test/wpt/tests/websockets/opening-handshake/002.html index 27b85602d44..00d8dccaeee 100644 --- a/test/wpt/tests/websockets/opening-handshake/002.html +++ b/test/wpt/tests/websockets/opening-handshake/002.html @@ -4,7 +4,7 @@ - +
diff --git a/test/wpt/tests/websockets/opening-handshake/003.html b/test/wpt/tests/websockets/opening-handshake/003.html index f21219f5b5a..1fc7535d503 100644 --- a/test/wpt/tests/websockets/opening-handshake/003.html +++ b/test/wpt/tests/websockets/opening-handshake/003.html @@ -5,7 +5,7 @@ - +
- +
- +
- +
- +

Test requires popup blocker disabled

diff --git a/test/wpt/tests/websockets/unload-a-document/002.html b/test/wpt/tests/websockets/unload-a-document/002.html index 013f5266bf9..f79b3b72976 100644 --- a/test/wpt/tests/websockets/unload-a-document/002.html +++ b/test/wpt/tests/websockets/unload-a-document/002.html @@ -5,7 +5,7 @@ - +

Test requires popup blocker disabled

diff --git a/test/wpt/tests/websockets/unload-a-document/005.html b/test/wpt/tests/websockets/unload-a-document/005.html index fc4b02cbb29..1abb1b552f3 100644 --- a/test/wpt/tests/websockets/unload-a-document/005.html +++ b/test/wpt/tests/websockets/unload-a-document/005.html @@ -4,7 +4,7 @@ - +
From 403c249f103b8305ed7ee72a7395fcc959e699a5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Aug 2023 22:31:24 +0000 Subject: [PATCH 102/259] build(deps-dev): bump @sinonjs/fake-timers from 10.3.0 to 11.1.0 (#2221) Bumps [@sinonjs/fake-timers](https://github.com/sinonjs/fake-timers) from 10.3.0 to 11.1.0. - [Release notes](https://github.com/sinonjs/fake-timers/releases) - [Changelog](https://github.com/sinonjs/fake-timers/blob/main/CHANGELOG.md) - [Commits](https://github.com/sinonjs/fake-timers/compare/v10.3.0...v11.1.0) --- updated-dependencies: - dependency-name: "@sinonjs/fake-timers" dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 598a78654a9..48f15ef1ee1 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "fuzz": "jsfuzz test/fuzzing/fuzz.js corpus" }, "devDependencies": { - "@sinonjs/fake-timers": "^10.0.2", + "@sinonjs/fake-timers": "^11.1.0", "@types/node": "^18.0.3", "abort-controller": "^3.0.0", "atomic-sleep": "^1.0.0", From dd09c00075cd318b65d5a0fe7aa239583e1e1236 Mon Sep 17 00:00:00 2001 From: Batychko Nikolaj Date: Wed, 23 Aug 2023 21:11:45 +0300 Subject: [PATCH 103/259] fix: pass ProxyAgent proxy status code error (#2162) --- lib/fetch/index.js | 2 +- lib/fetch/response.js | 6 +++--- test/proxy-agent.js | 24 ++++++++++++++++++++++++ 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/lib/fetch/index.js b/lib/fetch/index.js index d615f07ea27..8faae32a70f 100644 --- a/lib/fetch/index.js +++ b/lib/fetch/index.js @@ -1760,7 +1760,7 @@ async function httpNetworkFetch ( fetchParams.controller.connection.destroy() // 2. Return the appropriate network error for fetchParams. - return makeAppropriateNetworkError(fetchParams) + return makeAppropriateNetworkError(fetchParams, err) } return makeNetworkError(err) diff --git a/lib/fetch/response.js b/lib/fetch/response.js index 1029dbef533..66c0e50e32b 100644 --- a/lib/fetch/response.js +++ b/lib/fetch/response.js @@ -426,15 +426,15 @@ function filterResponse (response, type) { } // https://fetch.spec.whatwg.org/#appropriate-network-error -function makeAppropriateNetworkError (fetchParams) { +function makeAppropriateNetworkError (fetchParams, err = null) { // 1. Assert: fetchParams is canceled. assert(isCancelled(fetchParams)) // 2. Return an aborted network error if fetchParams is aborted; // otherwise return a network error. return isAborted(fetchParams) - ? makeNetworkError(new DOMException('The operation was aborted.', 'AbortError')) - : makeNetworkError('Request was cancelled.') + ? makeNetworkError(Object.assign(new DOMException('The operation was aborted.', 'AbortError'), { cause: err })) + : makeNetworkError(Object.assign(new DOMException('Request was cancelled.'), { cause: err })) } // https://whatpr.org/fetch/1392.html#initialize-a-response diff --git a/test/proxy-agent.js b/test/proxy-agent.js index a35101234c6..6764cb106e7 100644 --- a/test/proxy-agent.js +++ b/test/proxy-agent.js @@ -443,6 +443,30 @@ test('should throw when proxy does not return 200', async (t) => { t.end() }) +test('pass ProxyAgent proxy status code error when using fetch - #2161', async (t) => { + const server = await buildServer() + const proxy = await buildProxy() + + const serverUrl = `http://localhost:${server.address().port}` + const proxyUrl = `http://localhost:${proxy.address().port}` + + proxy.authenticate = function (req, fn) { + fn(null, false) + } + + const proxyAgent = new ProxyAgent(proxyUrl) + try { + await fetch(serverUrl, { dispatcher: proxyAgent }) + } catch (e) { + t.hasProp(e, 'cause') + } + + server.close() + proxy.close() + proxyAgent.close() + t.end() +}) + test('Proxy via HTTP to HTTPS endpoint', async (t) => { t.plan(4) From 37e7fd8a3db8a9c13157d24c1e48681099f5e52a Mon Sep 17 00:00:00 2001 From: Khafra Date: Thu, 24 Aug 2023 02:39:54 -0400 Subject: [PATCH 104/259] fix failing test (#2223) --- test/proxy-agent.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/proxy-agent.js b/test/proxy-agent.js index 6764cb106e7..0a9212639f8 100644 --- a/test/proxy-agent.js +++ b/test/proxy-agent.js @@ -443,7 +443,7 @@ test('should throw when proxy does not return 200', async (t) => { t.end() }) -test('pass ProxyAgent proxy status code error when using fetch - #2161', async (t) => { +test('pass ProxyAgent proxy status code error when using fetch - #2161', { skip: nodeMajor < 16 }, async (t) => { const server = await buildServer() const proxy = await buildProxy() From 5c3c69400ef46fac25a798fdf3dc6d422ef8b79e Mon Sep 17 00:00:00 2001 From: Jiri Spac Date: Thu, 24 Aug 2023 23:10:51 +0200 Subject: [PATCH 105/259] Update MockPool.md intercept method description (#2220) --- docs/api/MockPool.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/api/MockPool.md b/docs/api/MockPool.md index 923c157aa64..de53914002e 100644 --- a/docs/api/MockPool.md +++ b/docs/api/MockPool.md @@ -35,7 +35,8 @@ const mockPool = mockAgent.get('http://localhost:3000') ### `MockPool.intercept(options)` -This method defines the interception rules for matching against requests for a MockPool or MockPool. We can intercept multiple times on a single instance. +This method defines the interception rules for matching against requests for a MockPool or MockPool. We can intercept multiple times on a single instance, but each intercept is only used once. +For example if you expect to make 2 requests inside a test, you need to call `intercept()` twice. Assuming you use `disableNetConnect()` you will get `MockNotMatchedError` on the second request when you only call `intercept()` once. When defining interception rules, all the rules must pass for a request to be intercepted. If a request is not intercepted, a real request will be attempted. From 111fd2388094270259814ca7ea5da7d07b2126f6 Mon Sep 17 00:00:00 2001 From: Khafra Date: Sat, 26 Aug 2023 07:32:35 -0400 Subject: [PATCH 106/259] Update wpts (#2226) --- lib/fetch/response.js | 2 +- lib/fetch/util.js | 25 +- test/wpt/runner/runner.mjs | 2 + test/wpt/status/fetch.status.json | 8 +- test/wpt/tests/.azure-pipelines.yml | 125 ++++--- test/wpt/tests/.taskcluster.yml | 2 +- .../filereader_readAsDataURL.any.js | 14 +- test/wpt/tests/common/media.js | 20 +- test/wpt/tests/common/rendering-utils.js | 8 +- test/wpt/tests/fetch/api/abort/general.any.js | 2 +- .../fetch/api/basic/integrity.sub.any.js | 12 +- .../tests/fetch/api/basic/keepalive.any.js | 36 +- .../fetch/api/basic/scheme-blob.sub.any.js | 4 + .../tests/fetch/api/cors/cors-basic.any.js | 44 ++- .../fetch/api/cors/cors-keepalive.any.js | 118 +++++++ .../fetch/api/cors/cors-preflight-star.any.js | 4 + .../api/crashtests/body-window-destroy.html | 11 + .../destination/resources/dummy_video.webm | Bin 0 -> 96902 bytes .../construct-in-detached-frame.window.js | 11 + .../fetch/api/resources/keepalive-helper.js | 30 +- .../fetch/api/resources/keepalive-iframe.html | 12 +- .../tests/fetch/api/resources/stash-put.py | 26 +- .../response-body-read-task-handling.html | 38 +- .../response/response-cancel-stream.any.js | 7 + .../api/response/response-static-json.any.js | 15 + .../resources/content-lengths.json | 16 + .../corb/resources/response_block_probe.js | 2 +- .../corb/response_block.tentative.https.html | 50 +++ .../response_block.tentative.sub.https.html | 44 --- test/wpt/tests/fetch/fetch-later/META.yml | 3 + test/wpt/tests/fetch/fetch-later/README.md | 3 + .../basic.tentative.https.window.js | 13 + .../fetch/fetch-later/non-secure.window.js | 8 + .../sendondiscard.tentative.https.window.js | 28 ++ .../resources-with-0x00-in-header.window.js | 2 +- .../resources/document-with-0x00-in-header.py | 2 +- .../fetch/local-network-access/README.md | 10 - .../orb/resources/script-asm-js-invalid.js | 4 + .../orb/resources/script-asm-js-valid.js | 4 + .../orb/tentative/known-mime-type.sub.any.js | 10 + .../META.yml | 0 .../fetch/private-network-access/README.md | 10 + ...eflight-required.tentative.https.window.js | 91 +++++ ...ubresource-fetch.tentative.https.window.js | 330 ++++++++++++++++++ .../fenced-frame.tentative.https.window.js | 150 ++++++++ ...treat-as-public.tentative.https.window.js} | 0 .../fetch.tentative.https.window.js} | 0 .../fetch.tentative.window.js} | 0 .../iframe.tentative.https.window.js | 142 ++++---- .../iframe.tentative.window.js | 26 +- ...ed-content-fetch.tentative.https.window.js | 92 +++-- .../nested-worker.tentative.https.window.js} | 0 .../nested-worker.tentative.window.js} | 0 ...preflight-cache.https.tentative.window.js} | 0 .../redirect.tentative.https.window.js} | 0 .../resources/executor.html | 0 .../resources/fenced-frame-fetcher.https.html | 25 ++ .../fenced-frame-fetcher.https.html.headers | 1 + ...ame-local-network-access-target.https.html | 8 + ...nced-frame-local-network-access.https.html | 14 + ...me-local-network-access.https.html.headers | 1 + .../resources/fetcher.html | 0 .../resources/fetcher.js | 0 .../resources/iframed.html | 0 .../resources/iframer.html | 0 .../resources/preflight.py | 6 + .../resources/service-worker-bridge.html | 0 .../resources/service-worker.js | 0 .../resources/shared-fetcher.js | 0 .../resources/shared-worker-blob-fetcher.html | 0 .../resources/shared-worker-fetcher.html | 0 .../resources/socket-opener.html | 0 .../resources/support.sub.js | 95 ++++- .../resources/worker-blob-fetcher.html | 0 .../resources/worker-fetcher.html | 0 .../resources/worker-fetcher.js | 0 .../resources/xhr-sender.html | 0 ...ackground-fetch.tentative.https.window.js} | 0 ...ce-worker-fetch.tentative.https.window.js} | 33 +- ...e-worker-update.tentative.https.window.js} | 0 .../service-worker.tentative.https.window.js} | 0 ...rker-blob-fetch.tentative.https.window.js} | 0 ...red-worker-blob-fetch.tentative.window.js} | 0 ...ed-worker-fetch.tentative.https.window.js} | 0 .../shared-worker-fetch.tentative.window.js} | 0 .../shared-worker.tentative.https.window.js} | 0 .../shared-worker.tentative.window.js} | 0 .../websocket.tentative.https.window.js} | 0 .../websocket.tentative.window.js} | 0 .../worker-blob-fetch.tentative.window.js} | 0 .../worker-fetch.tentative.https.window.js} | 0 .../worker-fetch.tentative.window.js} | 0 .../worker.tentative.https.window.js} | 0 .../worker.tentative.window.js} | 0 ...treat-as-public.tentative.https.window.js} | 0 .../xhr.https.tentative.window.js} | 0 .../xhr.tentative.window.js} | 0 test/wpt/tests/fetch/range/blob.any.js | 11 +- .../interfaces/attribution-reporting-api.idl | 16 +- .../captured-mouse-events.tentative.idl | 25 ++ .../tests/interfaces/css-anchor-position.idl | 11 + test/wpt/tests/interfaces/css-cascade-6.idl | 4 +- test/wpt/tests/interfaces/css-cascade.idl | 4 - test/wpt/tests/interfaces/cssom.idl | 16 +- .../document-picture-in-picture.idl | 34 ++ test/wpt/tests/interfaces/dom.idl | 1 + test/wpt/tests/interfaces/fenced-frame.idl | 15 +- test/wpt/tests/interfaces/fs.idl | 4 + test/wpt/tests/interfaces/html.idl | 8 + .../interfaces/mediastream-recording.idl | 2 + test/wpt/tests/interfaces/notifications.idl | 5 +- .../tests/interfaces/permissions-policy.idl | 1 + .../tests/interfaces/real-world-meshing.idl | 21 ++ test/wpt/tests/interfaces/resource-timing.idl | 2 + test/wpt/tests/interfaces/scheduling-apis.idl | 6 + test/wpt/tests/interfaces/screen-capture.idl | 2 +- .../tests/interfaces/scroll-animations.idl | 6 +- .../secure-payment-confirmation.idl | 4 + test/wpt/tests/interfaces/shared-storage.idl | 80 +++++ test/wpt/tests/interfaces/storage-buckets.idl | 53 +++ test/wpt/tests/interfaces/trust-token-api.idl | 7 +- test/wpt/tests/interfaces/turtledove.idl | 29 +- test/wpt/tests/interfaces/url.idl | 4 +- test/wpt/tests/interfaces/webauthn.idl | 75 +++- .../webcodecs-av1-codec-registration.idl | 8 + .../webcodecs-avc-codec-registration.idl | 8 + test/wpt/tests/interfaces/webcodecs.idl | 1 + test/wpt/tests/interfaces/webgpu.idl | 244 +++++++------ test/wpt/tests/interfaces/webnn.idl | 11 +- .../interfaces/webrtc-encoded-transform.idl | 5 +- test/wpt/tests/interfaces/webrtc-stats.idl | 4 +- test/wpt/tests/interfaces/webrtc.idl | 8 +- test/wpt/tests/interfaces/webtransport.idl | 5 +- test/wpt/tests/interfaces/webxrlayers.idl | 9 + test/wpt/tests/lint.ignore | 28 +- .../tests/resources/chromium/webusb-test.js | 9 +- .../tests/resources/chromium/webxr-test.js | 3 - test/wpt/tests/resources/idlharness.js | 7 +- .../tests/functional/assert-throws-dom.html | 55 +++ .../test_partial_interface_of.html | 1 - .../test_partial_interface_of.html | 1 - test/wpt/tests/resources/test/tox.ini | 2 +- test/wpt/tests/resources/testdriver.js | 171 ++++++++- test/wpt/tests/resources/testharness.js | 10 +- ...led-dedicatedworker-postMessage.https.html | 44 +++ .../controlled-iframe-postMessage.https.html | 67 ++++ .../detached-context.https.html | 19 +- .../service-worker/fetch-error.https.html | 8 +- .../navigation-redirect.https.html | 4 +- .../navigation-timing.https.html | 1 + .../partitioned-cookies.tentative.https.html | 48 ++- .../postMessage-client-worker.js | 23 ++ .../resource-timing.sub.https.html | 4 +- .../controlled-frame-postMessage.html | 39 +++ .../controlled-worker-late-postMessage.js | 6 + .../controlled-worker-postMessage.js | 4 + .../resources/fetch-access-control.py | 9 +- .../service-worker/resources/missing.asis | 4 + ...ioned-cookies-3p-credentialless-frame.html | 53 ++- .../partitioned-cookies-3p-frame.html | 49 ++- .../resources/partitioned-cookies-3p-sw.js | 29 +- .../partitioned-cookies-3p-window.html | 4 +- .../resources/partitioned-cookies-sw.js | 29 +- .../resources/resource-timing-iframe.sub.html | 2 +- .../tentative/static-router/README.md | 4 + .../static-router/resources/direct.txt | 1 + ...mple-test-for-condition-main-resource.html | 3 + .../static-router/resources/simple.html | 3 + .../resources/static-router-sw.js | 35 ++ .../resources/test-helpers.sub.js | 303 ++++++++++++++++ .../static-router-main-resource.https.html | 58 +++ .../static-router-subresource.https.html | 48 +++ ...ket-quota-indexeddb.tentative.https.any.js | 35 ++ ...cket-storage-policy.tentative.https.any.js | 21 ++ ...kets_storage_policy.tentative.https.any.js | 46 --- ...nager-persist-persisted-match.https.any.js | 9 + ...socket-connection-ccns.tentative.window.js | 5 +- ...socket-connection-ccns.tentative.window.js | 5 +- ...e-with-open-websocket-connection.window.js | 2 +- test/wpt/tests/xhr/blob-range.any.js | 246 +++++++++++++ .../tests/xhr/responsexml-invalid-type.html | 21 ++ ...-authentication-basic-cors-not-enabled.htm | 5 +- ...entication-cors-basic-setrequestheader.htm | 5 +- ...tication-cors-setrequestheader-no-cred.htm | 5 +- .../send-network-error-sync-events.sub.htm | 2 +- 185 files changed, 3480 insertions(+), 663 deletions(-) create mode 100644 test/wpt/tests/fetch/api/cors/cors-keepalive.any.js create mode 100644 test/wpt/tests/fetch/api/crashtests/body-window-destroy.html create mode 100644 test/wpt/tests/fetch/api/request/destination/resources/dummy_video.webm create mode 100644 test/wpt/tests/fetch/api/request/multi-globals/construct-in-detached-frame.window.js create mode 100644 test/wpt/tests/fetch/corb/response_block.tentative.https.html delete mode 100644 test/wpt/tests/fetch/corb/response_block.tentative.sub.https.html create mode 100644 test/wpt/tests/fetch/fetch-later/META.yml create mode 100644 test/wpt/tests/fetch/fetch-later/README.md create mode 100644 test/wpt/tests/fetch/fetch-later/basic.tentative.https.window.js create mode 100644 test/wpt/tests/fetch/fetch-later/non-secure.window.js create mode 100644 test/wpt/tests/fetch/fetch-later/sendondiscard.tentative.https.window.js delete mode 100644 test/wpt/tests/fetch/local-network-access/README.md create mode 100644 test/wpt/tests/fetch/orb/resources/script-asm-js-invalid.js create mode 100644 test/wpt/tests/fetch/orb/resources/script-asm-js-valid.js rename test/wpt/tests/fetch/{local-network-access => private-network-access}/META.yml (100%) create mode 100644 test/wpt/tests/fetch/private-network-access/README.md create mode 100644 test/wpt/tests/fetch/private-network-access/fenced-frame-no-preflight-required.tentative.https.window.js create mode 100644 test/wpt/tests/fetch/private-network-access/fenced-frame-subresource-fetch.tentative.https.window.js create mode 100644 test/wpt/tests/fetch/private-network-access/fenced-frame.tentative.https.window.js rename test/wpt/tests/fetch/{local-network-access/fetch-from-treat-as-public.https.window.js => private-network-access/fetch-from-treat-as-public.tentative.https.window.js} (100%) rename test/wpt/tests/fetch/{local-network-access/fetch.https.window.js => private-network-access/fetch.tentative.https.window.js} (100%) rename test/wpt/tests/fetch/{local-network-access/fetch.window.js => private-network-access/fetch.tentative.window.js} (100%) rename test/wpt/tests/fetch/{local-network-access => private-network-access}/iframe.tentative.https.window.js (63%) rename test/wpt/tests/fetch/{local-network-access => private-network-access}/iframe.tentative.window.js (86%) rename test/wpt/tests/fetch/{local-network-access => private-network-access}/mixed-content-fetch.tentative.https.window.js (77%) rename test/wpt/tests/fetch/{local-network-access/nested-worker.https.window.js => private-network-access/nested-worker.tentative.https.window.js} (100%) rename test/wpt/tests/fetch/{local-network-access/nested-worker.window.js => private-network-access/nested-worker.tentative.window.js} (100%) rename test/wpt/tests/fetch/{local-network-access/preflight-cache.https.window.js => private-network-access/preflight-cache.https.tentative.window.js} (100%) rename test/wpt/tests/fetch/{local-network-access/redirect.https.window.js => private-network-access/redirect.tentative.https.window.js} (100%) rename test/wpt/tests/fetch/{local-network-access => private-network-access}/resources/executor.html (100%) create mode 100644 test/wpt/tests/fetch/private-network-access/resources/fenced-frame-fetcher.https.html create mode 100644 test/wpt/tests/fetch/private-network-access/resources/fenced-frame-fetcher.https.html.headers create mode 100644 test/wpt/tests/fetch/private-network-access/resources/fenced-frame-local-network-access-target.https.html create mode 100644 test/wpt/tests/fetch/private-network-access/resources/fenced-frame-local-network-access.https.html create mode 100644 test/wpt/tests/fetch/private-network-access/resources/fenced-frame-local-network-access.https.html.headers rename test/wpt/tests/fetch/{local-network-access => private-network-access}/resources/fetcher.html (100%) rename test/wpt/tests/fetch/{local-network-access => private-network-access}/resources/fetcher.js (100%) rename test/wpt/tests/fetch/{local-network-access => private-network-access}/resources/iframed.html (100%) rename test/wpt/tests/fetch/{local-network-access => private-network-access}/resources/iframer.html (100%) rename test/wpt/tests/fetch/{local-network-access => private-network-access}/resources/preflight.py (97%) rename test/wpt/tests/fetch/{local-network-access => private-network-access}/resources/service-worker-bridge.html (100%) rename test/wpt/tests/fetch/{local-network-access => private-network-access}/resources/service-worker.js (100%) rename test/wpt/tests/fetch/{local-network-access => private-network-access}/resources/shared-fetcher.js (100%) rename test/wpt/tests/fetch/{local-network-access => private-network-access}/resources/shared-worker-blob-fetcher.html (100%) rename test/wpt/tests/fetch/{local-network-access => private-network-access}/resources/shared-worker-fetcher.html (100%) rename test/wpt/tests/fetch/{local-network-access => private-network-access}/resources/socket-opener.html (100%) rename test/wpt/tests/fetch/{local-network-access => private-network-access}/resources/support.sub.js (86%) rename test/wpt/tests/fetch/{local-network-access => private-network-access}/resources/worker-blob-fetcher.html (100%) rename test/wpt/tests/fetch/{local-network-access => private-network-access}/resources/worker-fetcher.html (100%) rename test/wpt/tests/fetch/{local-network-access => private-network-access}/resources/worker-fetcher.js (100%) rename test/wpt/tests/fetch/{local-network-access => private-network-access}/resources/xhr-sender.html (100%) rename test/wpt/tests/fetch/{local-network-access/service-worker-background-fetch.https.window.js => private-network-access/service-worker-background-fetch.tentative.https.window.js} (100%) rename test/wpt/tests/fetch/{local-network-access/service-worker-fetch.https.window.js => private-network-access/service-worker-fetch.tentative.https.window.js} (88%) rename test/wpt/tests/fetch/{local-network-access/service-worker-update.https.window.js => private-network-access/service-worker-update.tentative.https.window.js} (100%) rename test/wpt/tests/fetch/{local-network-access/service-worker.https.window.js => private-network-access/service-worker.tentative.https.window.js} (100%) rename test/wpt/tests/fetch/{local-network-access/shared-worker-blob-fetch.https.window.js => private-network-access/shared-worker-blob-fetch.tentative.https.window.js} (100%) rename test/wpt/tests/fetch/{local-network-access/shared-worker-blob-fetch.window.js => private-network-access/shared-worker-blob-fetch.tentative.window.js} (100%) rename test/wpt/tests/fetch/{local-network-access/shared-worker-fetch.https.window.js => private-network-access/shared-worker-fetch.tentative.https.window.js} (100%) rename test/wpt/tests/fetch/{local-network-access/shared-worker-fetch.window.js => private-network-access/shared-worker-fetch.tentative.window.js} (100%) rename test/wpt/tests/fetch/{local-network-access/shared-worker.https.window.js => private-network-access/shared-worker.tentative.https.window.js} (100%) rename test/wpt/tests/fetch/{local-network-access/shared-worker.window.js => private-network-access/shared-worker.tentative.window.js} (100%) rename test/wpt/tests/fetch/{local-network-access/websocket.https.window.js => private-network-access/websocket.tentative.https.window.js} (100%) rename test/wpt/tests/fetch/{local-network-access/websocket.window.js => private-network-access/websocket.tentative.window.js} (100%) rename test/wpt/tests/fetch/{local-network-access/worker-blob-fetch.window.js => private-network-access/worker-blob-fetch.tentative.window.js} (100%) rename test/wpt/tests/fetch/{local-network-access/worker-fetch.https.window.js => private-network-access/worker-fetch.tentative.https.window.js} (100%) rename test/wpt/tests/fetch/{local-network-access/worker-fetch.window.js => private-network-access/worker-fetch.tentative.window.js} (100%) rename test/wpt/tests/fetch/{local-network-access/worker.https.window.js => private-network-access/worker.tentative.https.window.js} (100%) rename test/wpt/tests/fetch/{local-network-access/worker.window.js => private-network-access/worker.tentative.window.js} (100%) rename test/wpt/tests/fetch/{local-network-access/xhr-from-treat-as-public.https.window.js => private-network-access/xhr-from-treat-as-public.tentative.https.window.js} (100%) rename test/wpt/tests/fetch/{local-network-access/xhr.https.window.js => private-network-access/xhr.https.tentative.window.js} (100%) rename test/wpt/tests/fetch/{local-network-access/xhr.window.js => private-network-access/xhr.tentative.window.js} (100%) create mode 100644 test/wpt/tests/interfaces/captured-mouse-events.tentative.idl create mode 100644 test/wpt/tests/interfaces/css-anchor-position.idl create mode 100644 test/wpt/tests/interfaces/document-picture-in-picture.idl create mode 100644 test/wpt/tests/interfaces/real-world-meshing.idl create mode 100644 test/wpt/tests/interfaces/shared-storage.idl create mode 100644 test/wpt/tests/interfaces/storage-buckets.idl create mode 100644 test/wpt/tests/resources/test/tests/functional/assert-throws-dom.html create mode 100644 test/wpt/tests/service-workers/service-worker/controlled-dedicatedworker-postMessage.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/controlled-iframe-postMessage.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/postMessage-client-worker.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/controlled-frame-postMessage.html create mode 100644 test/wpt/tests/service-workers/service-worker/resources/controlled-worker-late-postMessage.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/controlled-worker-postMessage.js create mode 100644 test/wpt/tests/service-workers/service-worker/resources/missing.asis create mode 100644 test/wpt/tests/service-workers/service-worker/tentative/static-router/README.md create mode 100644 test/wpt/tests/service-workers/service-worker/tentative/static-router/resources/direct.txt create mode 100644 test/wpt/tests/service-workers/service-worker/tentative/static-router/resources/simple-test-for-condition-main-resource.html create mode 100644 test/wpt/tests/service-workers/service-worker/tentative/static-router/resources/simple.html create mode 100644 test/wpt/tests/service-workers/service-worker/tentative/static-router/resources/static-router-sw.js create mode 100644 test/wpt/tests/service-workers/service-worker/tentative/static-router/resources/test-helpers.sub.js create mode 100644 test/wpt/tests/service-workers/service-worker/tentative/static-router/static-router-main-resource.https.html create mode 100644 test/wpt/tests/service-workers/service-worker/tentative/static-router/static-router-subresource.https.html create mode 100644 test/wpt/tests/storage/buckets/bucket-quota-indexeddb.tentative.https.any.js create mode 100644 test/wpt/tests/storage/buckets/bucket-storage-policy.tentative.https.any.js delete mode 100644 test/wpt/tests/storage/buckets/buckets_storage_policy.tentative.https.any.js create mode 100644 test/wpt/tests/storage/storagemanager-persist-persisted-match.https.any.js create mode 100644 test/wpt/tests/xhr/blob-range.any.js create mode 100644 test/wpt/tests/xhr/responsexml-invalid-type.html diff --git a/lib/fetch/response.js b/lib/fetch/response.js index 66c0e50e32b..88deb71a062 100644 --- a/lib/fetch/response.js +++ b/lib/fetch/response.js @@ -49,7 +49,7 @@ class Response { } // https://fetch.spec.whatwg.org/#dom-response-json - static json (data = undefined, init = {}) { + static json (data, init = {}) { webidl.argumentLengthCheck(arguments, 1, { header: 'Response.json' }) if (init !== null) { diff --git a/lib/fetch/util.js b/lib/fetch/util.js index 98a049dc79d..fcbba84bc9a 100644 --- a/lib/fetch/util.js +++ b/lib/fetch/util.js @@ -556,16 +556,37 @@ function bytesMatch (bytes, metadataList) { const algorithm = item.algo // 2. Let expectedValue be the val component of item. - const expectedValue = item.hash + let expectedValue = item.hash + + // See https://github.com/web-platform-tests/wpt/commit/e4c5cc7a5e48093220528dfdd1c4012dc3837a0e + // "be liberal with padding". This is annoying, and it's not even in the spec. + + if (expectedValue.endsWith('==')) { + expectedValue = expectedValue.slice(0, -2) + } // 3. Let actualValue be the result of applying algorithm to bytes. - const actualValue = crypto.createHash(algorithm).update(bytes).digest('base64') + let actualValue = crypto.createHash(algorithm).update(bytes).digest('base64') + + if (actualValue.endsWith('==')) { + actualValue = actualValue.slice(0, -2) + } // 4. If actualValue is a case-sensitive match for expectedValue, // return true. if (actualValue === expectedValue) { return true } + + let actualBase64URL = crypto.createHash(algorithm).update(bytes).digest('base64url') + + if (actualBase64URL.endsWith('==')) { + actualBase64URL = actualBase64URL.slice(0, -2) + } + + if (actualBase64URL === expectedValue) { + return true + } } // 6. Return false. diff --git a/test/wpt/runner/runner.mjs b/test/wpt/runner/runner.mjs index 0b66f58466b..5bec326d3e1 100644 --- a/test/wpt/runner/runner.mjs +++ b/test/wpt/runner/runner.mjs @@ -312,6 +312,8 @@ export class WPTRunner extends EventEmitter { `unexpected failures: ${failed - expectedFailures}, ` + `skipped: ${skipped}` ) + + process.exit(0) } addInitScript (code) { diff --git a/test/wpt/status/fetch.status.json b/test/wpt/status/fetch.status.json index cb5949579cf..5910bf37f6f 100644 --- a/test/wpt/status/fetch.status.json +++ b/test/wpt/status/fetch.status.json @@ -2,11 +2,13 @@ "api": { "abort": { "general.any.js": { + "note": "TODO(@KhafraDev): Clone aborts with original controller can probably be fixed", "fail": [ "Already aborted signal rejects immediately", "Underlying connection is closed when aborting after receiving response - no-cors", "Stream errors once aborted. Underlying connection closed.", - "Readable stream synchronously cancels with AbortError if aborted before reading" + "Readable stream synchronously cancels with AbortError if aborted before reading", + "Clone aborts with original controller" ] }, "cache.https.any.js": { @@ -128,6 +130,10 @@ ] } }, + "fetch-later": { + "note": "this is not part of the spec, only a proposal", + "skip": true + }, "headers": { "header-setcookie.any.js": { "note": "undici doesn't filter headers", diff --git a/test/wpt/tests/.azure-pipelines.yml b/test/wpt/tests/.azure-pipelines.yml index 20d5ec0f431..75a87df90f0 100644 --- a/test/wpt/tests/.azure-pipelines.yml +++ b/test/wpt/tests/.azure-pipelines.yml @@ -21,6 +21,7 @@ trigger: - triggers/edge_canary - triggers/safari_stable - triggers/safari_preview +- triggers/wktr_preview # Set safaridriver_diagnose to true to enable safaridriver diagnostics. The # logs won't appear in `./wpt run` output but will be uploaded as an artifact. @@ -34,11 +35,11 @@ jobs: displayName: 'affected tests: Safari Technology Preview' condition: eq(variables['Build.Reason'], 'PullRequest') pool: - vmImage: 'macOS-12' + vmImage: 'macOS-13' steps: - task: UsePythonVersion@0 inputs: - versionSpec: '3.10' + versionSpec: '3.11' - template: tools/ci/azure/affected_tests.yml parameters: artifactName: 'safari-preview-affected-tests' @@ -51,11 +52,11 @@ jobs: displayName: 'affected tests without changes: Safari Technology Preview' condition: eq(variables['Build.Reason'], 'PullRequest') pool: - vmImage: 'macOS-12' + vmImage: 'macOS-13' steps: - task: UsePythonVersion@0 inputs: - versionSpec: '3.10' + versionSpec: '3.11' - template: tools/ci/azure/affected_tests.yml parameters: checkoutCommit: 'HEAD^1' @@ -76,7 +77,7 @@ jobs: steps: - task: UsePythonVersion@0 inputs: - versionSpec: '3.10' + versionSpec: '3.11' - template: tools/ci/azure/checkout.yml - script: | set -eux -o pipefail @@ -93,17 +94,18 @@ jobs: dependsOn: decision condition: dependencies.decision.outputs['test_jobs.wptrunner_infrastructure'] pool: - vmImage: 'macOS-12' + vmImage: 'macOS-13' steps: - task: UsePythonVersion@0 inputs: - versionSpec: '3.10' + versionSpec: '3.11' - template: tools/ci/azure/checkout.yml - template: tools/ci/azure/pip_install.yml parameters: packages: virtualenv - template: tools/ci/azure/install_fonts.yml - template: tools/ci/azure/install_certs.yml + - template: tools/ci/azure/color_profile.yml - template: tools/ci/azure/install_chrome.yml - template: tools/ci/azure/install_firefox.yml - template: tools/ci/azure/install_safari.yml @@ -111,20 +113,25 @@ jobs: - template: tools/ci/azure/update_manifest.yml - script: | set -eux -o pipefail - ./wpt run --yes --no-manifest-update --manifest MANIFEST.json --metadata infrastructure/metadata/ --log-mach - --log-mach-level info --channel dev chrome infrastructure/ + ./wpt run --yes --no-manifest-update --manifest MANIFEST.json --metadata infrastructure/metadata/ --log-mach - --log-mach-level info --log-wptreport $(Build.ArtifactStagingDirectory)/wpt_report_macos_chrome.json --channel dev chrome infrastructure/ condition: succeededOrFailed() displayName: 'Run tests (Chrome Dev)' - script: | set -eux -o pipefail - ./wpt run --yes --no-manifest-update --manifest MANIFEST.json --metadata infrastructure/metadata/ --log-mach - --log-mach-level info --channel nightly firefox infrastructure/ + ./wpt run --yes --no-manifest-update --manifest MANIFEST.json --metadata infrastructure/metadata/ --log-mach - --log-mach-level info --log-wptreport $(Build.ArtifactStagingDirectory)/wpt_report_macos_firefox.json --channel nightly firefox infrastructure/ condition: succeededOrFailed() displayName: 'Run tests (Firefox Nightly)' - script: | set -eux -o pipefail export SYSTEM_VERSION_COMPAT=0 - ./wpt run --yes --no-manifest-update --manifest MANIFEST.json --metadata infrastructure/metadata/ --log-mach - --log-mach-level info --channel preview safari infrastructure/ + ./wpt run --yes --no-manifest-update --manifest MANIFEST.json --metadata infrastructure/metadata/ --log-mach - --log-mach-level info --log-wptreport $(Build.ArtifactStagingDirectory)/wpt_report_macos_safari.json --channel preview safari infrastructure/ condition: succeededOrFailed() displayName: 'Run tests (Safari Technology Preview)' + - task: PublishBuildArtifacts@1 + condition: succeededOrFailed() + displayName: 'Publish results' + inputs: + artifactName: 'infrastructure-results' - template: tools/ci/azure/publish_logs.yml - template: tools/ci/azure/sysdiagnose.yml @@ -133,76 +140,79 @@ jobs: dependsOn: decision condition: dependencies.decision.outputs['test_jobs.tools_unittest'] pool: - vmImage: 'macOS-12' + vmImage: 'macOS-13' steps: - task: UsePythonVersion@0 inputs: - versionSpec: '3.7' + # TODO(#40525): Revert back to 3.7 once the Mac agent's Python v3.7 contains bz2 again. + versionSpec: '3.7.16' - template: tools/ci/azure/checkout.yml - template: tools/ci/azure/tox_pytest.yml parameters: directory: tools/ toxenv: py37 -- job: tools_unittest_mac_py310 - displayName: 'tools/ unittests: macOS + Python 3.10' +- job: tools_unittest_mac_py311 + displayName: 'tools/ unittests: macOS + Python 3.11' dependsOn: decision condition: dependencies.decision.outputs['test_jobs.tools_unittest'] pool: - vmImage: 'macOS-12' + vmImage: 'macOS-13' steps: - task: UsePythonVersion@0 inputs: - versionSpec: '3.10' + versionSpec: '3.11' - template: tools/ci/azure/checkout.yml - template: tools/ci/azure/tox_pytest.yml parameters: directory: tools/ - toxenv: py310 + toxenv: py311 - job: wptrunner_unittest_mac_py37 displayName: 'tools/wptrunner/ unittests: macOS + Python 3.7' dependsOn: decision condition: dependencies.decision.outputs['test_jobs.wptrunner_unittest'] pool: - vmImage: 'macOS-12' + vmImage: 'macOS-13' steps: - task: UsePythonVersion@0 inputs: - versionSpec: '3.7' + # TODO(#40525): Revert back to 3.7 once the Mac agent's Python v3.7 contains bz2 again. + versionSpec: '3.7.16' - template: tools/ci/azure/checkout.yml - template: tools/ci/azure/tox_pytest.yml parameters: directory: tools/wptrunner/ toxenv: py37 -- job: wptrunner_unittest_mac_py310 - displayName: 'tools/wptrunner/ unittests: macOS + Python 3.10' +- job: wptrunner_unittest_mac_py311 + displayName: 'tools/wptrunner/ unittests: macOS + Python 3.11' dependsOn: decision condition: dependencies.decision.outputs['test_jobs.wptrunner_unittest'] pool: - vmImage: 'macOS-12' + vmImage: 'macOS-13' steps: - task: UsePythonVersion@0 inputs: - versionSpec: '3.10' + versionSpec: '3.11' - template: tools/ci/azure/checkout.yml - template: tools/ci/azure/tox_pytest.yml parameters: directory: tools/wptrunner/ - toxenv: py310 + toxenv: py311 - job: wpt_integration_mac_py37 displayName: 'tools/wpt/ tests: macOS + Python 3.7' dependsOn: decision condition: dependencies.decision.outputs['test_jobs.wpt_integration'] pool: - vmImage: 'macOS-12' + vmImage: 'macOS-13' steps: # full checkout required - task: UsePythonVersion@0 inputs: - versionSpec: '3.7' + # TODO(#40525): Revert back to 3.7 once the Mac agent's Python v3.7 contains bz2 again. + versionSpec: '3.7.16' - template: tools/ci/azure/install_chrome.yml - template: tools/ci/azure/install_firefox.yml - template: tools/ci/azure/update_hosts.yml @@ -212,17 +222,17 @@ jobs: directory: tools/wpt/ toxenv: py37 -- job: wpt_integration_mac_py310 - displayName: 'tools/wpt/ tests: macOS + Python 3.10' +- job: wpt_integration_mac_py311 + displayName: 'tools/wpt/ tests: macOS + Python 3.11' dependsOn: decision condition: dependencies.decision.outputs['test_jobs.wpt_integration'] pool: - vmImage: 'macOS-12' + vmImage: 'macOS-13' steps: # full checkout required - task: UsePythonVersion@0 inputs: - versionSpec: '3.10' + versionSpec: '3.11' - template: tools/ci/azure/install_chrome.yml - template: tools/ci/azure/install_firefox.yml - template: tools/ci/azure/update_hosts.yml @@ -230,7 +240,7 @@ jobs: - template: tools/ci/azure/tox_pytest.yml parameters: directory: tools/wpt/ - toxenv: py310 + toxenv: py311 - job: tools_unittest_win_py37 displayName: 'tools/ unittests: Windows + Python 3.7' @@ -251,8 +261,8 @@ jobs: directory: tools/ toxenv: py37 -- job: tools_unittest_win_py310 - displayName: 'tools/ unittests: Windows + Python 3.10' +- job: tools_unittest_win_py311 + displayName: 'tools/ unittests: Windows + Python 3.11' dependsOn: decision condition: dependencies.decision.outputs['test_jobs.tools_unittest'] pool: @@ -260,13 +270,13 @@ jobs: steps: - task: UsePythonVersion@0 inputs: - versionSpec: '3.10' + versionSpec: '3.11' addToPath: false - template: tools/ci/azure/checkout.yml - template: tools/ci/azure/tox_pytest.yml parameters: directory: tools/ - toxenv: py310 + toxenv: py311 - job: wptrunner_unittest_win_py37 displayName: 'tools/wptrunner/ unittests: Windows + Python 3.7' @@ -285,8 +295,8 @@ jobs: directory: tools/wptrunner/ toxenv: py37 -- job: wptrunner_unittest_win_py310 - displayName: 'tools/wptrunner/ unittests: Windows + Python 3.10' +- job: wptrunner_unittest_win_py311 + displayName: 'tools/wptrunner/ unittests: Windows + Python 3.11' dependsOn: decision condition: dependencies.decision.outputs['test_jobs.wptrunner_unittest'] pool: @@ -294,13 +304,13 @@ jobs: steps: - task: UsePythonVersion@0 inputs: - versionSpec: '3.10' + versionSpec: '3.11' addToPath: false - template: tools/ci/azure/checkout.yml - template: tools/ci/azure/tox_pytest.yml parameters: directory: tools/wptrunner/ - toxenv: py310 + toxenv: py311 - job: wpt_integration_win_py37 displayName: 'tools/wpt/ tests: Windows + Python 3.7' @@ -324,8 +334,8 @@ jobs: directory: tools/wpt/ toxenv: py37 -- job: wpt_integration_win_py310 - displayName: 'tools/wpt/ tests: Windows + Python 3.10' +- job: wpt_integration_win_py311 + displayName: 'tools/wpt/ tests: Windows + Python 3.11' dependsOn: decision condition: dependencies.decision.outputs['test_jobs.wpt_integration'] pool: @@ -334,7 +344,7 @@ jobs: # full checkout required - task: UsePythonVersion@0 inputs: - versionSpec: '3.10' + versionSpec: '3.11' # currently just using the outdated Chrome/Firefox on the VM rather than # figuring out how to install Chrome Dev channel on Windows # - template: tools/ci/azure/install_chrome.yml @@ -344,7 +354,7 @@ jobs: - template: tools/ci/azure/tox_pytest.yml parameters: directory: tools/wpt/ - toxenv: py310 + toxenv: py311 - job: results_edge_stable displayName: 'all tests: Edge Stable' @@ -360,7 +370,7 @@ jobs: steps: - task: UsePythonVersion@0 inputs: - versionSpec: '3.10' + versionSpec: '3.11' - template: tools/ci/azure/system_info.yml - template: tools/ci/azure/checkout.yml - template: tools/ci/azure/pip_install.yml @@ -399,7 +409,7 @@ jobs: steps: - task: UsePythonVersion@0 inputs: - versionSpec: '3.10' + versionSpec: '3.11' - template: tools/ci/azure/system_info.yml - template: tools/ci/azure/checkout.yml - template: tools/ci/azure/pip_install.yml @@ -438,7 +448,7 @@ jobs: steps: - task: UsePythonVersion@0 inputs: - versionSpec: '3.10' + versionSpec: '3.11' - template: tools/ci/azure/checkout.yml - template: tools/ci/azure/pip_install.yml parameters: @@ -472,16 +482,17 @@ jobs: parallel: 8 # chosen to make runtime ~2h timeoutInMinutes: 180 pool: - vmImage: 'macOS-12' + vmImage: 'macOS-13' steps: - task: UsePythonVersion@0 inputs: - versionSpec: '3.10' + versionSpec: '3.11' - template: tools/ci/azure/checkout.yml - template: tools/ci/azure/pip_install.yml parameters: packages: virtualenv - template: tools/ci/azure/install_certs.yml + - template: tools/ci/azure/color_profile.yml - template: tools/ci/azure/install_safari.yml parameters: channel: stable @@ -490,8 +501,9 @@ jobs: - script: | set -eux -o pipefail export SYSTEM_VERSION_COMPAT=0 - ./wpt run --no-manifest-update --no-restart-on-unexpected --no-fail-on-unexpected --this-chunk=$(System.JobPositionInPhase) --total-chunks=$(System.TotalJobsInPhase) --chunk-type hash --log-wptreport $(Build.ArtifactStagingDirectory)/wpt_report_$(System.JobPositionInPhase).json --log-wptscreenshot $(Build.ArtifactStagingDirectory)/wpt_screenshot_$(System.JobPositionInPhase).txt --log-mach - --log-mach-level info --channel stable --kill-safari safari + ./wpt run --no-manifest-update --no-restart-on-unexpected --no-fail-on-unexpected --this-chunk=$(System.JobPositionInPhase) --total-chunks=$(System.TotalJobsInPhase) --chunk-type hash --log-wptreport $(Build.ArtifactStagingDirectory)/wpt_report_$(System.JobPositionInPhase).json --log-wptscreenshot $(Build.ArtifactStagingDirectory)/wpt_screenshot_$(System.JobPositionInPhase).txt --log-mach - --log-mach-level info --channel stable --kill-safari --max-restarts 100 safari displayName: 'Run tests' + retryCountOnTaskFailure: 2 - task: PublishBuildArtifacts@1 displayName: 'Publish results' inputs: @@ -513,24 +525,26 @@ jobs: parallel: 8 # chosen to make runtime ~2h timeoutInMinutes: 180 pool: - vmImage: 'macOS-12' + vmImage: 'macOS-13' steps: - task: UsePythonVersion@0 inputs: - versionSpec: '3.10' + versionSpec: '3.11' - template: tools/ci/azure/checkout.yml - template: tools/ci/azure/pip_install.yml parameters: packages: virtualenv - template: tools/ci/azure/install_certs.yml + - template: tools/ci/azure/color_profile.yml - template: tools/ci/azure/install_safari.yml - template: tools/ci/azure/update_hosts.yml - template: tools/ci/azure/update_manifest.yml - script: | set -eux -o pipefail export SYSTEM_VERSION_COMPAT=0 - ./wpt run --no-manifest-update --no-restart-on-unexpected --no-fail-on-unexpected --this-chunk=$(System.JobPositionInPhase) --total-chunks=$(System.TotalJobsInPhase) --chunk-type hash --log-wptreport $(Build.ArtifactStagingDirectory)/wpt_report_$(System.JobPositionInPhase).json --log-wptscreenshot $(Build.ArtifactStagingDirectory)/wpt_screenshot_$(System.JobPositionInPhase).txt --log-mach - --log-mach-level info --channel preview --kill-safari safari + ./wpt run --no-manifest-update --no-restart-on-unexpected --no-fail-on-unexpected --this-chunk=$(System.JobPositionInPhase) --total-chunks=$(System.TotalJobsInPhase) --chunk-type hash --log-wptreport $(Build.ArtifactStagingDirectory)/wpt_report_$(System.JobPositionInPhase).json --log-wptscreenshot $(Build.ArtifactStagingDirectory)/wpt_screenshot_$(System.JobPositionInPhase).txt --log-mach - --log-mach-level info --channel preview --kill-safari --max-restarts 100 safari displayName: 'Run tests' + retryCountOnTaskFailure: 2 - task: PublishBuildArtifacts@1 displayName: 'Publish results' inputs: @@ -551,22 +565,23 @@ jobs: parallel: 8 # chosen to make runtime ~2h timeoutInMinutes: 180 pool: - vmImage: 'macOS-12' + vmImage: 'macOS-13' steps: - task: UsePythonVersion@0 inputs: - versionSpec: '3.10' + versionSpec: '3.11' - template: tools/ci/azure/checkout.yml - template: tools/ci/azure/pip_install.yml parameters: packages: virtualenv - template: tools/ci/azure/install_certs.yml + - template: tools/ci/azure/color_profile.yml - template: tools/ci/azure/update_hosts.yml - template: tools/ci/azure/update_manifest.yml - script: | set -eux -o pipefail export SYSTEM_VERSION_COMPAT=0 - ./wpt run --no-manifest-update --no-restart-on-unexpected --no-fail-on-unexpected --this-chunk=$(System.JobPositionInPhase) --total-chunks=$(System.TotalJobsInPhase) --chunk-type hash --log-wptreport $(Build.ArtifactStagingDirectory)/wpt_report_$(System.JobPositionInPhase).json --log-wptscreenshot $(Build.ArtifactStagingDirectory)/wpt_screenshot_$(System.JobPositionInPhase).txt --log-mach - --log-mach-level info --channel main --install-browser wktr + ./wpt run --no-manifest-update --no-restart-on-unexpected --no-fail-on-unexpected --this-chunk=$(System.JobPositionInPhase) --total-chunks=$(System.TotalJobsInPhase) --chunk-type hash --log-wptreport $(Build.ArtifactStagingDirectory)/wpt_report_$(System.JobPositionInPhase).json --log-wptscreenshot $(Build.ArtifactStagingDirectory)/wpt_screenshot_$(System.JobPositionInPhase).txt --log-mach - --log-mach-level info --channel experimental --install-browser --yes wktr displayName: 'Run tests' - task: PublishBuildArtifacts@1 displayName: 'Publish results' diff --git a/test/wpt/tests/.taskcluster.yml b/test/wpt/tests/.taskcluster.yml index c80e92af204..c817999b8e6 100644 --- a/test/wpt/tests/.taskcluster.yml +++ b/test/wpt/tests/.taskcluster.yml @@ -57,7 +57,7 @@ tasks: owner: ${owner} source: ${event.repository.clone_url} payload: - image: webplatformtests/wpt:0.53 + image: webplatformtests/wpt:0.54 maxRunTime: 7200 artifacts: public/results: diff --git a/test/wpt/tests/FileAPI/reading-data-section/filereader_readAsDataURL.any.js b/test/wpt/tests/FileAPI/reading-data-section/filereader_readAsDataURL.any.js index d6812121295..4f9dbf7a754 100644 --- a/test/wpt/tests/FileAPI/reading-data-section/filereader_readAsDataURL.any.js +++ b/test/wpt/tests/FileAPI/reading-data-section/filereader_readAsDataURL.any.js @@ -39,4 +39,16 @@ async_test(function(testCase) { testCase.done(); }); reader.readAsDataURL(blob); -}, 'readAsDataURL result for Blob with unspecified MIME type'); \ No newline at end of file +}, 'readAsDataURL result for Blob with unspecified MIME type'); + +async_test(function(testCase) { + var blob = new Blob([]); + var reader = new FileReader(); + + reader.onload = this.step_func(function() { + assert_equals(reader.result, + "data:application/octet-stream;base64,"); + testCase.done(); + }); + reader.readAsDataURL(blob); +}, 'readAsDataURL result for empty Blob'); \ No newline at end of file diff --git a/test/wpt/tests/common/media.js b/test/wpt/tests/common/media.js index f2dc8612660..800593f5343 100644 --- a/test/wpt/tests/common/media.js +++ b/test/wpt/tests/common/media.js @@ -9,10 +9,15 @@ function getVideoURI(base) var videotag = document.createElement("video"); - if ( videotag.canPlayType && - videotag.canPlayType('video/ogg; codecs="theora, vorbis"') ) + if ( videotag.canPlayType ) { - extension = '.ogv'; + if (videotag.canPlayType('video/webm; codecs="vp9, opus"') ) + { + extension = '.webm'; + } else if ( videotag.canPlayType('video/ogg; codecs="theora, vorbis"') ) + { + extension = '.ogv'; + } } return base + extension; @@ -46,10 +51,11 @@ function getAudioURI(base) function getMediaContentType(url) { var extension = new URL(url, location).pathname.split(".").pop(); var map = { - "mp4": "video/mp4", - "ogv": "application/ogg", - "mp3": "audio/mp3", - "oga": "application/ogg", + "mp4" : "video/mp4", + "ogv" : "application/ogg", + "webm": "video/webm", + "mp3" : "audio/mp3", + "oga" : "application/ogg", }; return map[extension]; } diff --git a/test/wpt/tests/common/rendering-utils.js b/test/wpt/tests/common/rendering-utils.js index 8027cd5f848..46283bd5d07 100644 --- a/test/wpt/tests/common/rendering-utils.js +++ b/test/wpt/tests/common/rendering-utils.js @@ -7,14 +7,12 @@ */ function waitForAtLeastOneFrame() { return new Promise(resolve => { - // Different web engines work slightly different on this area but 1) waiting - // for two requestAnimationFrames() to happen one after another and 2) - // adding a step_timeout(0) to guarantee events have finished should be + // Different web engines work slightly different on this area but waiting + // for two requestAnimationFrames() to happen, one after another, should be // sufficient to ensure at least one frame has been generated anywhere. - // See https://bugzilla.mozilla.org/show_bug.cgi?id=1785615 window.requestAnimationFrame(() => { window.requestAnimationFrame(() => { - setTimeout(resolve, 0); + resolve(); }); }); }); diff --git a/test/wpt/tests/fetch/api/abort/general.any.js b/test/wpt/tests/fetch/api/abort/general.any.js index 7bf98ba9b24..3727bb42afe 100644 --- a/test/wpt/tests/fetch/api/abort/general.any.js +++ b/test/wpt/tests/fetch/api/abort/general.any.js @@ -566,7 +566,7 @@ test(() => { controller.abort(); - assert_array_equals(log, ['clone-aborted', 'original-aborted'], "Abort events fired in correct order"); + assert_array_equals(log, ['original-aborted', 'clone-aborted'], "Abort events fired in correct order"); assert_true(request.signal.aborted, 'Signal aborted'); assert_true(clonedRequest.signal.aborted, 'Signal aborted'); }, "Clone aborts with original controller"); diff --git a/test/wpt/tests/fetch/api/basic/integrity.sub.any.js b/test/wpt/tests/fetch/api/basic/integrity.sub.any.js index 56dbd4909f6..e3cfd1b2f6e 100644 --- a/test/wpt/tests/fetch/api/basic/integrity.sub.any.js +++ b/test/wpt/tests/fetch/api/basic/integrity.sub.any.js @@ -28,6 +28,9 @@ function integrity(desc, url, integrity, initRequestMode, shouldPass) { const topSha256 = "sha256-KHIDZcXnR2oBHk9DrAA+5fFiR6JjudYjqoXtMR1zvzk="; const topSha384 = "sha384-MgZYnnAzPM/MjhqfOIMfQK5qcFvGZsGLzx4Phd7/A8fHTqqLqXqKo8cNzY3xEPTL"; const topSha512 = "sha512-D6yns0qxG0E7+TwkevZ4Jt5t7Iy3ugmAajG/dlf6Pado1JqTyneKXICDiqFIkLMRExgtvg8PlxbKTkYfRejSOg=="; +const topSha512wrongpadding = "sha512-D6yns0qxG0E7+TwkevZ4Jt5t7Iy3ugmAajG/dlf6Pado1JqTyneKXICDiqFIkLMRExgtvg8PlxbKTkYfRejSOg"; +const topSha512base64url = "sha512-D6yns0qxG0E7-TwkevZ4Jt5t7Iy3ugmAajG_dlf6Pado1JqTyneKXICDiqFIkLMRExgtvg8PlxbKTkYfRejSOg=="; +const topSha512base64url_nopadding = "sha512-D6yns0qxG0E7-TwkevZ4Jt5t7Iy3ugmAajG_dlf6Pado1JqTyneKXICDiqFIkLMRExgtvg8PlxbKTkYfRejSOg"; const invalidSha256 = "sha256-dKUcPOn/AlUjWIwcHeHNqYXPlvyGiq+2dWOdFcE+24I="; const invalidSha512 = "sha512-oUceBRNxPxnY60g/VtPCj2syT4wo4EZh2CgYdWy9veW8+OsReTXoh7dizMGZafvx9+QhMS39L/gIkxnPIn41Zg=="; @@ -38,13 +41,20 @@ const corsUrl = const corsUrl2 = `https://{{host}}:{{ports[https][0]}}${path}` integrity("Empty string integrity", url, "", /* initRequestMode */ undefined, - /* shouldPass */ true); + /* 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("SHA-512 integrity with missing padding", url, topSha512wrongpadding, + /* initRequestMode */ undefined, /* shouldPass */ true); +integrity("SHA-512 integrity base64url encoded", url, topSha512base64url, + /* initRequestMode */ undefined, /* shouldPass */ true); +integrity("SHA-512 integrity base64url encoded with missing padding", url, + topSha512base64url_nopadding, /* initRequestMode */ undefined, + /* shouldPass */ true); integrity("Invalid integrity", url, invalidSha256, /* initRequestMode */ undefined, /* shouldPass */ false); integrity("Multiple integrities: valid stronger than invalid", url, diff --git a/test/wpt/tests/fetch/api/basic/keepalive.any.js b/test/wpt/tests/fetch/api/basic/keepalive.any.js index 4f33284d0c7..899d41d676a 100644 --- a/test/wpt/tests/fetch/api/basic/keepalive.any.js +++ b/test/wpt/tests/fetch/api/basic/keepalive.any.js @@ -14,16 +14,30 @@ const { HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT } = get_host_info(); -for (const method of ['GET', 'POST']) { - promise_test(async (test) => { - const token1 = token(); - const iframe = document.createElement('iframe'); - iframe.src = getKeepAliveIframeUrl(token1, method); - document.body.appendChild(iframe); - await iframeLoaded(iframe); - assert_equals(await getTokenFromMessage(), token1); - iframe.remove(); +/** + * In a different-site iframe, test to fetch a keepalive URL on the specified + * document event. + */ +function keepaliveSimpleRequestTest(method) { + for (const evt of ['load', 'pagehide', 'unload']) { + const desc = + `[keepalive] simple ${method} request on '${evt}' [no payload]`; + promise_test(async (test) => { + const token1 = token(); + const iframe = document.createElement('iframe'); + iframe.src = getKeepAliveIframeUrl(token1, method, {sendOn: evt}); + document.body.appendChild(iframe); + await iframeLoaded(iframe); + if (evt != 'load') { + iframe.remove(); + } + assert_equals(await getTokenFromMessage(), token1); + + assertStashedTokenAsync(desc, token1); + }, `${desc}; setting up`); + } +} - assertStashedTokenAsync(`simple ${method} request: no payload`, token1); - }, `simple ${method} request: no payload; setting up`); +for (const method of ['GET', 'POST']) { + keepaliveSimpleRequestTest(method); } diff --git a/test/wpt/tests/fetch/api/basic/scheme-blob.sub.any.js b/test/wpt/tests/fetch/api/basic/scheme-blob.sub.any.js index a6059ea93d9..8afdc033c9d 100644 --- a/test/wpt/tests/fetch/api/basic/scheme-blob.sub.any.js +++ b/test/wpt/tests/fetch/api/basic/scheme-blob.sub.any.js @@ -57,6 +57,10 @@ let empty_data_blob = new Blob([], {type: "text/plain"}); checkFetchResponse(URL.createObjectURL(empty_data_blob), "", "text/plain", 0, "Fetching URL.createObjectURL(empty_data_blob) is OK"); +let invalid_type_blob = new Blob([], {type: "invalid"}); +checkFetchResponse(URL.createObjectURL(invalid_type_blob), "", "", 0, + "Fetching URL.createObjectURL(invalid_type_blob) is OK"); + promise_test(function(test) { return fetch("/images/blue.png").then(function(resp) { return resp.arrayBuffer(); diff --git a/test/wpt/tests/fetch/api/cors/cors-basic.any.js b/test/wpt/tests/fetch/api/cors/cors-basic.any.js index 23f5f91c87d..95de0af2d8f 100644 --- a/test/wpt/tests/fetch/api/cors/cors-basic.any.js +++ b/test/wpt/tests/fetch/api/cors/cors-basic.any.js @@ -1,37 +1,43 @@ // META: script=../resources/utils.js // META: script=/common/get-host-info.sub.js +const { + HTTPS_ORIGIN, + HTTP_ORIGIN_WITH_DIFFERENT_PORT, + HTTP_REMOTE_ORIGIN, + HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT, + HTTPS_REMOTE_ORIGIN, +} = get_host_info(); + function cors(desc, origin) { - var url = origin + dirname(location.pathname); - var urlParameters = "?pipe=header(Access-Control-Allow-Origin,*)"; + const url = `${origin}${dirname(location.pathname)}${RESOURCES_DIR}top.txt`; + const urlAllowCors = `${url}?pipe=header(Access-Control-Allow-Origin,*)`; - promise_test(function(test) { - return fetch(url + RESOURCES_DIR + "top.txt" + urlParameters, {"mode": "no-cors"} ).then(function(resp) { + promise_test((test) => { + return fetch(urlAllowCors, {'mode': 'no-cors'}).then((resp) => { assert_equals(resp.status, 0, "Opaque filter: status is 0"); assert_equals(resp.statusText, "", "Opaque filter: statusText is \"\""); assert_equals(resp.type , "opaque", "Opaque filter: response's type is opaque"); - return resp.text().then(function(value) { + return resp.text().then((value) => { assert_equals(value, "", "Opaque response should have an empty body"); }); }); - }, desc + " [no-cors mode]"); + }, `${desc} [no-cors mode]`); - promise_test(function(test) { - return promise_rejects_js(test, TypeError, fetch(url + RESOURCES_DIR + "top.txt", {"mode": "cors"})); - }, desc + " [server forbid CORS]"); + promise_test((test) => { + return promise_rejects_js(test, TypeError, fetch(url, {'mode': 'cors'})); + }, `${desc} [server forbid CORS]`); - promise_test(function(test) { - return fetch(url + RESOURCES_DIR + "top.txt" + urlParameters, {"mode": "cors"} ).then(function(resp) { + promise_test((test) => { + return fetch(urlAllowCors, {'mode': 'cors'}).then((resp) => { assert_equals(resp.status, 200, "Fetch's response's status is 200"); assert_equals(resp.type , "cors", "CORS response's type is cors"); }); - }, desc + " [cors mode]"); + }, `${desc} [cors mode]`); } -var host_info = get_host_info(); - -cors("Same domain different port", host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT); -cors("Same domain different protocol different port", host_info.HTTPS_ORIGIN); -cors("Cross domain basic usage", host_info.HTTP_REMOTE_ORIGIN); -cors("Cross domain different port", host_info.HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT); -cors("Cross domain different protocol", host_info.HTTPS_REMOTE_ORIGIN); +cors('Same domain different port', HTTP_ORIGIN_WITH_DIFFERENT_PORT); +cors('Same domain different protocol different port', HTTPS_ORIGIN); +cors('Cross domain basic usage', HTTP_REMOTE_ORIGIN); +cors('Cross domain different port', HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT); +cors('Cross domain different protocol', HTTPS_REMOTE_ORIGIN); diff --git a/test/wpt/tests/fetch/api/cors/cors-keepalive.any.js b/test/wpt/tests/fetch/api/cors/cors-keepalive.any.js new file mode 100644 index 00000000000..f68d90ef9aa --- /dev/null +++ b/test/wpt/tests/fetch/api/cors/cors-keepalive.any.js @@ -0,0 +1,118 @@ +// META: global=window +// META: timeout=long +// META: title=Fetch API: keepalive handling +// META: script=/resources/testharness.js +// META: script=/resources/testharnessreport.js +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=../resources/keepalive-helper.js +// META: script=../resources/utils.js + +'use strict'; + +const { + HTTP_NOTSAMESITE_ORIGIN, + HTTPS_ORIGIN, + HTTP_ORIGIN_WITH_DIFFERENT_PORT, + HTTP_REMOTE_ORIGIN, + HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT, + HTTPS_REMOTE_ORIGIN, +} = get_host_info(); + +/** + * Tests to cover the basic behaviors of keepalive + cors/no-cors mode requests + * to different `origin` when the initiator document is still alive. They should + * behave the same as without setting keepalive. + */ +function keepaliveCorsBasicTest(desc, origin) { + const url = `${origin}${dirname(location.pathname)}${RESOURCES_DIR}top.txt`; + const urlAllowCors = `${url}?pipe=header(Access-Control-Allow-Origin,*)`; + + promise_test((test) => { + return fetch(urlAllowCors, {keepalive: true, 'mode': 'no-cors'}) + .then((resp) => { + assert_equals(resp.status, 0, 'Opaque filter: status is 0'); + assert_equals(resp.statusText, '', 'Opaque filter: statusText is ""'); + assert_equals( + resp.type, 'opaque', 'Opaque filter: response\'s type is opaque'); + return resp.text().then((value) => { + assert_equals( + value, '', 'Opaque response should have an empty body'); + }); + }); + }, `${desc} [no-cors mode]`); + + promise_test((test) => { + return promise_rejects_js( + test, TypeError, fetch(url, {keepalive: true, 'mode': 'cors'})); + }, `${desc} [cors mode, server forbid CORS]`); + + promise_test((test) => { + return fetch(urlAllowCors, {keepalive: true, 'mode': 'cors'}) + .then((resp) => { + assert_equals(resp.status, 200, 'Fetch\'s response\'s status is 200'); + assert_equals(resp.type, 'cors', 'CORS response\'s type is cors'); + }); + }, `${desc} [cors mode]`); +} + +keepaliveCorsBasicTest( + `[keepalive] Same domain different port`, HTTP_ORIGIN_WITH_DIFFERENT_PORT); +keepaliveCorsBasicTest( + `[keepalive] Same domain different protocol different port`, HTTPS_ORIGIN); +keepaliveCorsBasicTest( + `[keepalive] Cross domain basic usage`, HTTP_REMOTE_ORIGIN); +keepaliveCorsBasicTest( + `[keepalive] Cross domain different port`, + HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT); +keepaliveCorsBasicTest( + `[keepalive] Cross domain different protocol`, HTTPS_REMOTE_ORIGIN); + +/** + * In a same-site iframe, and in `unload` event handler, test to fetch + * a keepalive URL that involves in different cors modes. + */ +function keepaliveCorsInUnloadTest(description, origin, method) { + const evt = 'unload'; + for (const mode of ['no-cors', 'cors']) { + for (const disallowOrigin of [false, true]) { + const desc = `${description} ${method} request in ${evt} [${mode} mode` + + (disallowOrigin ? `, server forbid CORS]` : `]`); + const shouldPass = !disallowOrigin || mode === 'no-cors'; + promise_test(async (test) => { + const token1 = token(); + const iframe = document.createElement('iframe'); + iframe.src = getKeepAliveIframeUrl(token1, method, { + frameOrigin: '', + requestOrigin: origin, + sendOn: evt, + mode: mode, + disallowOrigin + }); + document.body.appendChild(iframe); + await iframeLoaded(iframe); + iframe.remove(); + assert_equals(await getTokenFromMessage(), token1); + + assertStashedTokenAsync(desc, token1, {shouldPass}); + }, `${desc}; setting up`); + } + } +} + +for (const method of ['GET', 'POST']) { + keepaliveCorsInUnloadTest( + '[keepalive] Same domain different port', HTTP_ORIGIN_WITH_DIFFERENT_PORT, + method); + keepaliveCorsInUnloadTest( + `[keepalive] Same domain different protocol different port`, HTTPS_ORIGIN, + method); + keepaliveCorsInUnloadTest( + `[keepalive] Cross domain basic usage`, HTTP_REMOTE_ORIGIN, method); + keepaliveCorsInUnloadTest( + `[keepalive] Cross domain different port`, + HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT, method); + keepaliveCorsInUnloadTest( + `[keepalive] Cross domain different protocol`, HTTPS_REMOTE_ORIGIN, + method); +} diff --git a/test/wpt/tests/fetch/api/cors/cors-preflight-star.any.js b/test/wpt/tests/fetch/api/cors/cors-preflight-star.any.js index e8cbc80b808..f9fb20469cf 100644 --- a/test/wpt/tests/fetch/api/cors/cors-preflight-star.any.js +++ b/test/wpt/tests/fetch/api/cors/cors-preflight-star.any.js @@ -80,3 +80,7 @@ preflightTest(true, true, "PATCH", "*", "PATCH", []) preflightTest(false, true, "PATCH", "*", "patch", []) preflightTest(false, true, "patch", "*", "PATCH", []) preflightTest(true, true, "patch", "*", "patch", []) + +// "Authorization" header can't be wildcarded. +preflightTest(false, false, "*", "*", "POST", ["Authorization", "123"]) +preflightTest(true, false, "*", "*, Authorization", "POST", ["Authorization", "123"]) diff --git a/test/wpt/tests/fetch/api/crashtests/body-window-destroy.html b/test/wpt/tests/fetch/api/crashtests/body-window-destroy.html new file mode 100644 index 00000000000..646d3c5f8ce --- /dev/null +++ b/test/wpt/tests/fetch/api/crashtests/body-window-destroy.html @@ -0,0 +1,11 @@ + + + diff --git a/test/wpt/tests/fetch/api/request/destination/resources/dummy_video.webm b/test/wpt/tests/fetch/api/request/destination/resources/dummy_video.webm new file mode 100644 index 0000000000000000000000000000000000000000..c3d433a3e02e86eee1026b6620dc0c50498e61a4 GIT binary patch literal 96902 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%0AxWB;E~=xj7<{Wt4OeKYACt+d&&88fiV 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 O{=nA(AP!glu>TjvI>Ctm literal 0 HcmV?d00001 diff --git a/test/wpt/tests/fetch/api/request/multi-globals/construct-in-detached-frame.window.js b/test/wpt/tests/fetch/api/request/multi-globals/construct-in-detached-frame.window.js new file mode 100644 index 00000000000..b0d6ba5b80d --- /dev/null +++ b/test/wpt/tests/fetch/api/request/multi-globals/construct-in-detached-frame.window.js @@ -0,0 +1,11 @@ +// This is a regression test for Chromium issue https://crbug.com/1427266. +test(() => { + const iframe = document.createElement('iframe'); + document.body.append(iframe); + const otherRequest = iframe.contentWindow.Request; + iframe.remove(); + const r1 = new otherRequest('resource', { method: 'POST', body: 'string' }); + const r2 = new otherRequest(r1); + assert_true(r1.bodyUsed); + assert_false(r2.bodyUsed); +}, 'creating a request from another request in a detached realm should work'); diff --git a/test/wpt/tests/fetch/api/resources/keepalive-helper.js b/test/wpt/tests/fetch/api/resources/keepalive-helper.js index c7048d1ff33..ad1d4b2c7c3 100644 --- a/test/wpt/tests/fetch/api/resources/keepalive-helper.js +++ b/test/wpt/tests/fetch/api/resources/keepalive-helper.js @@ -1,19 +1,35 @@ // Utility functions to help testing keepalive requests. -// Returns a different-site URL to an iframe that loads a keepalive URL. +// Returns a URL to an iframe that loads a keepalive URL on iframe loaded. // // The keepalive URL points to a target that stores `token`. The token will then -// be posted back to parent document. +// be posted back on iframe loaded to the parent document. // `method` defaults to GET. -// `sendOnPagehide` to tell if request should be sent on pagehide instead. -function getKeepAliveIframeUrl(token, method, sendOnPagehide = false) { +// `frameOrigin` to specify the origin of the iframe to load. If not set, +// default to a different site origin. +// `requestOrigin` to specify the origin of the fetch request target. +// `sendOn` to specify the name of the event when the keepalive request should +// be sent instead of the default 'load'. +// `mode` to specify the fetch request's CORS mode. +// `disallowOrigin` to ask the iframe to set up a server that forbids CORS +// requests. +function getKeepAliveIframeUrl(token, method, { + frameOrigin = 'DEFAULT', + requestOrigin = '', + sendOn = 'load', + mode = 'cors', + disallowOrigin = false +} = {}) { const https = location.protocol.startsWith('https'); - const frameOrigin = - get_host_info()[https ? 'HTTPS_NOTSAMESITE_ORIGIN' : 'HTTP_NOTSAMESITE_ORIGIN']; + frameOrigin = frameOrigin === 'DEFAULT' ? + get_host_info()[https ? 'HTTPS_NOTSAMESITE_ORIGIN' : 'HTTP_NOTSAMESITE_ORIGIN'] : + frameOrigin; return `${frameOrigin}/fetch/api/resources/keepalive-iframe.html?` + `token=${token}&` + `method=${method}&` + - `sendOnPagehide=${sendOnPagehide}`; + `sendOn=${sendOn}&` + + `mode=${mode}&` + (disallowOrigin ? `disallowOrigin=1&` : ``) + + `origin=${requestOrigin}`; } // Returns a different-site URL to an iframe that loads a keepalive URL. diff --git a/test/wpt/tests/fetch/api/resources/keepalive-iframe.html b/test/wpt/tests/fetch/api/resources/keepalive-iframe.html index ac00f3a331a..335a1f8e318 100644 --- a/test/wpt/tests/fetch/api/resources/keepalive-iframe.html +++ b/test/wpt/tests/fetch/api/resources/keepalive-iframe.html @@ -3,12 +3,18 @@ diff --git a/test/wpt/tests/fetch/api/resources/stash-put.py b/test/wpt/tests/fetch/api/resources/stash-put.py index dbc7ceebb88..0530e1ba5b4 100644 --- a/test/wpt/tests/fetch/api/resources/stash-put.py +++ b/test/wpt/tests/fetch/api/resources/stash-put.py @@ -1,17 +1,19 @@ from wptserve.utils import isomorphic_decode def main(request, response): - if request.method == u'OPTIONS': - # CORS preflight - response.headers.set(b'Access-Control-Allow-Origin', b'*') - response.headers.set(b'Access-Control-Allow-Methods', b'*') - response.headers.set(b'Access-Control-Allow-Headers', b'*') - return 'done' + if request.method == u'OPTIONS': + # CORS preflight + response.headers.set(b'Access-Control-Allow-Origin', b'*') + response.headers.set(b'Access-Control-Allow-Methods', b'*') + response.headers.set(b'Access-Control-Allow-Headers', b'*') + return 'done' + + url_dir = u'/'.join(request.url_parts.path.split(u'/')[:-1]) + u'/' + key = request.GET.first(b'key') + value = request.GET.first(b'value') + # value here must be a text string. It will be json.dump()'ed in stash-take.py. + request.server.stash.put(key, isomorphic_decode(value), url_dir) - url_dir = u'/'.join(request.url_parts.path.split(u'/')[:-1]) + u'/' - key = request.GET.first(b"key") - value = request.GET.first(b"value") - # value here must be a text string. It will be json.dump()'ed in stash-take.py. - request.server.stash.put(key, isomorphic_decode(value), url_dir) + if b'disallow_origin' not in request.GET: response.headers.set(b'Access-Control-Allow-Origin', b'*') - return "done" + return 'done' diff --git a/test/wpt/tests/fetch/api/response/response-body-read-task-handling.html b/test/wpt/tests/fetch/api/response/response-body-read-task-handling.html index c2c90eaa8bd..64b07556661 100644 --- a/test/wpt/tests/fetch/api/response/response-body-read-task-handling.html +++ b/test/wpt/tests/fetch/api/response/response-body-read-task-handling.html @@ -35,9 +35,7 @@ // The fulfill handler above shouldn't have run yet. If it has run, // throw to reject this promise and fail the test. - if (executed) { - throw "shouldn't have run microtasks yet"; - } + assert_false(executed, "shouldn't have run microtasks yet"); // Otherwise act as if there's no "then" property so the promise // fulfills and the test passes. @@ -49,6 +47,40 @@ return response.body.getReader().read(); }); }, "reading from a body stream should occur in a microtask scope"); + +promise_test(function() { + return fetch("../resources/data.json").then(function(response) { + // Add a getter for "then" that will incidentally be invoked + // during promise resolution. + Object.prototype.__defineGetter__('then', () => { + // Clean up behind ourselves. + delete Object.prototype.then; + + // This promise should (like all promises) be resolved + // asynchronously. + var executed = false; + Promise.resolve().then(_ => { executed = true; }); + + // This shouldn't run microtasks! They should only run + // after the fetch is resolved. + performMicrotaskCheckpoint(); + + // The fulfill handler above shouldn't have run yet. If it has run, + // throw to reject this promise and fail the test. + assert_false(executed, "shouldn't have run microtasks yet"); + + // Otherwise act as if there's no "then" property so the promise + // fulfills and the test passes. + return undefined; + }); + + // Create a read request, incidentally resolving a promise with an + // object value, thereby invoking the getter installed above. + return response.body.pipeTo(new WritableStream({ + write(chunk) {} + })) + }); +}, "piping from a body stream to a JS-written WritableStream should occur in a microtask scope"); diff --git a/test/wpt/tests/fetch/api/response/response-cancel-stream.any.js b/test/wpt/tests/fetch/api/response/response-cancel-stream.any.js index baa46de4039..91140d1afd1 100644 --- a/test/wpt/tests/fetch/api/response/response-cancel-stream.any.js +++ b/test/wpt/tests/fetch/api/response/response-cancel-stream.any.js @@ -55,3 +55,10 @@ promise_test(function() { return readAll(reader).then(() => reader.cancel()); }); }, "Cancelling a closed Response stream"); + +promise_test(async () => { + const response = await fetch(RESOURCES_DIR + "top.txt"); + const { body } = response; + await body.cancel(); + assert_equals(body, response.body, ".body should not change after cancellation"); +}, "Accessing .body after canceling it"); diff --git a/test/wpt/tests/fetch/api/response/response-static-json.any.js b/test/wpt/tests/fetch/api/response/response-static-json.any.js index 3c8a2b637f7..5ec79e69aa3 100644 --- a/test/wpt/tests/fetch/api/response/response-static-json.any.js +++ b/test/wpt/tests/fetch/api/response/response-static-json.any.js @@ -79,3 +79,18 @@ promise_test(async function () { } ) }, "Check static json() propagates JSON serializer errors"); + +const encodingChecks = [ + ["𝌆", [34, 240, 157, 140, 134, 34]], + ["\uDF06\uD834", [34, 92, 117, 100, 102, 48, 54, 92, 117, 100, 56, 51, 52, 34]], + ["\uDEAD", [34, 92, 117, 100, 101, 97, 100, 34]], +]; + +for (const [input, expected] of encodingChecks) { + promise_test(async function () { + const response = Response.json(input); + const buffer = await response.arrayBuffer(); + const data = new Uint8Array(buffer); + assert_array_equals(data, expected); + }, `Check response returned by static json() with input ${input}`); +} diff --git a/test/wpt/tests/fetch/content-length/resources/content-lengths.json b/test/wpt/tests/fetch/content-length/resources/content-lengths.json index dac9c82dc09..ac6f1a24680 100644 --- a/test/wpt/tests/fetch/content-length/resources/content-lengths.json +++ b/test/wpt/tests/fetch/content-length/resources/content-lengths.json @@ -31,6 +31,22 @@ "input": "Content-Length: 30\r\nContent-Length: 30,30", "output": 30 }, + { + "input": "Content-Length: 30,30\r\nContent-Length: 30,30", + "output": 30 + }, + { + "input": "Content-Length: 30,30, 30 \r\nContent-Length: 30 ", + "output": 30 + }, + { + "input": "Content-Length: 30,42\r\nContent-Length: 30", + "output": null + }, + { + "input": "Content-Length: 30,42\r\nContent-Length: 30,42", + "output": null + }, { "input": "Content-Length: 42,30", "output": null diff --git a/test/wpt/tests/fetch/corb/resources/response_block_probe.js b/test/wpt/tests/fetch/corb/resources/response_block_probe.js index d23ad488af2..9c3b87bcbd3 100644 --- a/test/wpt/tests/fetch/corb/resources/response_block_probe.js +++ b/test/wpt/tests/fetch/corb/resources/response_block_probe.js @@ -1 +1 @@ -window.script_callback(); +alert(1); // Arbitrary JavaScript. Details don't matter for the test. diff --git a/test/wpt/tests/fetch/corb/response_block.tentative.https.html b/test/wpt/tests/fetch/corb/response_block.tentative.https.html new file mode 100644 index 00000000000..6b116000d45 --- /dev/null +++ b/test/wpt/tests/fetch/corb/response_block.tentative.https.html @@ -0,0 +1,50 @@ + + + + + + diff --git a/test/wpt/tests/fetch/corb/response_block.tentative.sub.https.html b/test/wpt/tests/fetch/corb/response_block.tentative.sub.https.html deleted file mode 100644 index 860e0d3b93c..00000000000 --- a/test/wpt/tests/fetch/corb/response_block.tentative.sub.https.html +++ /dev/null @@ -1,44 +0,0 @@ - - - - - diff --git a/test/wpt/tests/fetch/fetch-later/META.yml b/test/wpt/tests/fetch/fetch-later/META.yml new file mode 100644 index 00000000000..f8fd46bec3e --- /dev/null +++ b/test/wpt/tests/fetch/fetch-later/META.yml @@ -0,0 +1,3 @@ +spec: https://whatpr.org/fetch/1647/094ea69...152d725.html#fetch-later-method +suggested_reviewers: + - mingyc diff --git a/test/wpt/tests/fetch/fetch-later/README.md b/test/wpt/tests/fetch/fetch-later/README.md new file mode 100644 index 00000000000..661e2b91843 --- /dev/null +++ b/test/wpt/tests/fetch/fetch-later/README.md @@ -0,0 +1,3 @@ +# FetchLater Tests + +These tests cover [FetchLater method](https://whatpr.org/fetch/1647/094ea69...152d725.html#fetch-later-method) related behaviors. diff --git a/test/wpt/tests/fetch/fetch-later/basic.tentative.https.window.js b/test/wpt/tests/fetch/fetch-later/basic.tentative.https.window.js new file mode 100644 index 00000000000..a8ca011a7c9 --- /dev/null +++ b/test/wpt/tests/fetch/fetch-later/basic.tentative.https.window.js @@ -0,0 +1,13 @@ +// META: script=/resources/testharness.js +// META: script=/resources/testharnessreport.js + +'use strict'; + +test(() => { + assert_throws_js(TypeError, () => fetchLater()); +}, `fetchLater() cannot be called without request.`); + +test(() => { + const result = fetchLater('/'); + assert_false(result.sent); +}, `fetchLater()'s return tells the deferred request is not yet sent.`); diff --git a/test/wpt/tests/fetch/fetch-later/non-secure.window.js b/test/wpt/tests/fetch/fetch-later/non-secure.window.js new file mode 100644 index 00000000000..2f2c3ea8d34 --- /dev/null +++ b/test/wpt/tests/fetch/fetch-later/non-secure.window.js @@ -0,0 +1,8 @@ +// META: script=/resources/testharness.js +// META: script=/resources/testharnessreport.js + +'use strict'; + +test(() => { + assert_false(window.hasOwnProperty('fetchLater')); +}, `fetchLater() is not supported in non-secure context.`); diff --git a/test/wpt/tests/fetch/fetch-later/sendondiscard.tentative.https.window.js b/test/wpt/tests/fetch/fetch-later/sendondiscard.tentative.https.window.js new file mode 100644 index 00000000000..0613d18dffb --- /dev/null +++ b/test/wpt/tests/fetch/fetch-later/sendondiscard.tentative.https.window.js @@ -0,0 +1,28 @@ +// META: script=/resources/testharness.js +// META: script=/resources/testharnessreport.js +// META: script=/common/utils.js +// META: script=/pending-beacon/resources/pending_beacon-helper.js + +'use strict'; + +parallelPromiseTest(async t => { + const uuid = token(); + const url = generateSetBeaconURL(uuid); + const numPerMethod = 20; + const total = numPerMethod * 2; + + // Loads an iframe that creates `numPerMethod` GET & POST fetchLater requests. + const iframe = await loadScriptAsIframe(` + const url = "${url}"; + for (let i = 0; i < ${numPerMethod}; i++) { + let get = fetchLater(url); + let post = fetchLater(url, {method: 'POST'}); + } + `); + + // Delete the iframe to trigger deferred request sending. + document.body.removeChild(iframe); + + // The iframe should have sent all requests. + await expectBeacon(uuid, {count: total}); +}, 'A discarded document sends all its fetchLater requests with default config.'); diff --git a/test/wpt/tests/fetch/h1-parsing/resources-with-0x00-in-header.window.js b/test/wpt/tests/fetch/h1-parsing/resources-with-0x00-in-header.window.js index f1afeeb740b..37a61c12b56 100644 --- a/test/wpt/tests/fetch/h1-parsing/resources-with-0x00-in-header.window.js +++ b/test/wpt/tests/fetch/h1-parsing/resources-with-0x00-in-header.window.js @@ -15,7 +15,7 @@ async_test(t => { // https://github.com/whatwg/html/issues/125 and https://github.com/whatwg/html/issues/1230 this // should be changed to use the load event instead. t.step_timeout(() => { - assert_equals(frame.contentDocument, null); + assert_equals(window.frameLoaded, undefined); t.done(); }, 1000); document.body.append(frame); diff --git a/test/wpt/tests/fetch/h1-parsing/resources/document-with-0x00-in-header.py b/test/wpt/tests/fetch/h1-parsing/resources/document-with-0x00-in-header.py index 223b3c40278..d91998b998d 100644 --- a/test/wpt/tests/fetch/h1-parsing/resources/document-with-0x00-in-header.py +++ b/test/wpt/tests/fetch/h1-parsing/resources/document-with-0x00-in-header.py @@ -1,4 +1,4 @@ def main(request, response): response.headers.set(b"Content-Type", b"text/html") response.headers.set(b"Custom", b"\0") - return b"This is a document." + return b"This is a document." diff --git a/test/wpt/tests/fetch/local-network-access/README.md b/test/wpt/tests/fetch/local-network-access/README.md deleted file mode 100644 index 8995e3d7ef6..00000000000 --- a/test/wpt/tests/fetch/local-network-access/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# Local Network Access tests - -This directory contains tests for Local Network Access' integration with -the Fetch specification. - -See also: - -* [The specification](https://wicg.github.io/local-network-access/) -* [The repository](https://github.com/WICG/local-network-access/) -* [Open issues](https://github.com/WICG/local-network-access/issues/) diff --git a/test/wpt/tests/fetch/orb/resources/script-asm-js-invalid.js b/test/wpt/tests/fetch/orb/resources/script-asm-js-invalid.js new file mode 100644 index 00000000000..8d1bbd6abc8 --- /dev/null +++ b/test/wpt/tests/fetch/orb/resources/script-asm-js-invalid.js @@ -0,0 +1,4 @@ +function f() { + "use asm"; + return; +} diff --git a/test/wpt/tests/fetch/orb/resources/script-asm-js-valid.js b/test/wpt/tests/fetch/orb/resources/script-asm-js-valid.js new file mode 100644 index 00000000000..79b375fe054 --- /dev/null +++ b/test/wpt/tests/fetch/orb/resources/script-asm-js-valid.js @@ -0,0 +1,4 @@ +function f() { + "use asm"; + return {}; +} diff --git a/test/wpt/tests/fetch/orb/tentative/known-mime-type.sub.any.js b/test/wpt/tests/fetch/orb/tentative/known-mime-type.sub.any.js index 66a63c8b28a..b0521e8b363 100644 --- a/test/wpt/tests/fetch/orb/tentative/known-mime-type.sub.any.js +++ b/test/wpt/tests/fetch/orb/tentative/known-mime-type.sub.any.js @@ -74,3 +74,13 @@ promise_test(async () => { promise_test(async () => { await fetchORB(`${path}/script-iso-8559-1.js`, null, contentType("application/json")); }, "ORB shouldn't block opaque text/javascript (iso-8559-1 encoded)"); + +// Test javascript validation can correctly parse asm.js. +promise_test(async () => { + await fetchORB(`${path}/script-asm-js-valid.js`, null, contentType("application/json")); +}, "ORB shouldn't block text/javascript with valid asm.js"); + +// Test javascript validation can correctly parse invalid asm.js with valid JS syntax. +promise_test(async () => { + await fetchORB(`${path}/script-asm-js-invalid.js`, null, contentType("application/json")); +}, "ORB shouldn't block text/javascript with invalid asm.js"); diff --git a/test/wpt/tests/fetch/local-network-access/META.yml b/test/wpt/tests/fetch/private-network-access/META.yml similarity index 100% rename from test/wpt/tests/fetch/local-network-access/META.yml rename to test/wpt/tests/fetch/private-network-access/META.yml diff --git a/test/wpt/tests/fetch/private-network-access/README.md b/test/wpt/tests/fetch/private-network-access/README.md new file mode 100644 index 00000000000..a69aab48723 --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/README.md @@ -0,0 +1,10 @@ +# Private Network Access tests + +This directory contains tests for Private Network Access' integration with +the Fetch specification. + +See also: + +* [The specification](https://wicg.github.io/private-network-access/) +* [The repository](https://github.com/WICG/private-network-access/) +* [Open issues](https://github.com/WICG/private-network-access/issues/) diff --git a/test/wpt/tests/fetch/private-network-access/fenced-frame-no-preflight-required.tentative.https.window.js b/test/wpt/tests/fetch/private-network-access/fenced-frame-no-preflight-required.tentative.https.window.js new file mode 100644 index 00000000000..21233f61ea6 --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/fenced-frame-no-preflight-required.tentative.https.window.js @@ -0,0 +1,91 @@ +// META: script=/common/dispatcher/dispatcher.js +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// META: script=/fenced-frame/resources/utils.js +// META: timeout=long +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests verify that contexts can navigate fenced frames to more-public or +// same address spaces without private network access preflight request header. + +setup(() => { + assert_true(window.isSecureContext); +}); + +// Source: secure local context. +// +// All fetches unaffected by Private Network Access. + +promise_test_parallel( + t => fencedFrameTest(t, { + source: {server: Server.HTTPS_LOCAL}, + target: {server: Server.HTTPS_LOCAL}, + expected: FrameTestResult.SUCCESS, + }), + 'local to local: no preflight required.'); + +promise_test_parallel( + t => fencedFrameTest(t, { + source: {server: Server.HTTPS_LOCAL}, + target: {server: Server.HTTPS_PRIVATE}, + expected: FrameTestResult.SUCCESS, + }), + 'local to private: no preflight required.'); + +promise_test_parallel( + t => fencedFrameTest(t, { + source: {server: Server.HTTPS_LOCAL}, + target: {server: Server.HTTPS_PUBLIC}, + expected: FrameTestResult.SUCCESS, + }), + 'local to public: no preflight required.'); + +promise_test_parallel( + t => fencedFrameTest(t, { + source: {server: Server.HTTPS_PRIVATE}, + target: {server: Server.HTTPS_PRIVATE}, + expected: FrameTestResult.SUCCESS, + }), + 'private to private: no preflight required.'); + +promise_test_parallel( + t => fencedFrameTest(t, { + source: {server: Server.HTTPS_PRIVATE}, + target: {server: Server.HTTPS_PUBLIC}, + expected: FrameTestResult.SUCCESS, + }), + 'private to public: no preflight required.'); + +promise_test_parallel( + t => fencedFrameTest(t, { + source: {server: Server.HTTPS_PUBLIC}, + target: {server: Server.HTTPS_PUBLIC}, + expected: FrameTestResult.SUCCESS, + }), + 'public to public: no preflight required.'); + +promise_test_parallel( + t => fencedFrameTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: {server: Server.HTTPS_PUBLIC}, + expected: FrameTestResult.SUCCESS, + }), + 'treat-as-public-address to public: no preflight required.'); + +promise_test_parallel( + t => fencedFrameTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PUBLIC, + behavior: {preflight: PreflightBehavior.optionalSuccess(token())} + }, + expected: FrameTestResult.SUCCESS, + }), + 'treat-as-public-address to local: optional preflight'); diff --git a/test/wpt/tests/fetch/private-network-access/fenced-frame-subresource-fetch.tentative.https.window.js b/test/wpt/tests/fetch/private-network-access/fenced-frame-subresource-fetch.tentative.https.window.js new file mode 100644 index 00000000000..2dff325e3e1 --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/fenced-frame-subresource-fetch.tentative.https.window.js @@ -0,0 +1,330 @@ +// META: script=/common/subset-tests-by-key.js +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// META: script=/fenced-frame/resources/utils.js +// META: variant=?include=baseline +// META: variant=?include=from-local +// META: variant=?include=from-private +// META: variant=?include=from-public +// META: timeout=long +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests verify that secure contexts can fetch subresources in fenced +// frames from all address spaces, provided that the target server, if more +// private than the initiator, respond affirmatively to preflight requests. +// + +setup(() => { + // Making sure we are in a secure context, as expected. + assert_true(window.isSecureContext); +}); + +// Source: secure local context. +// +// All fetches unaffected by Private Network Access. + +subsetTestByKey( + 'from-local', promise_test, t => fencedFrameFetchTest(t, { + source: {server: Server.HTTPS_LOCAL}, + target: {server: Server.HTTPS_LOCAL}, + fetchOptions: {method: 'GET', mode: 'cors'}, + expected: FetchTestResult.SUCCESS, + }), + 'local to local: no preflight required.'); + +subsetTestByKey( + 'from-local', promise_test, + t => fencedFrameFetchTest(t, { + source: {server: Server.HTTPS_LOCAL}, + target: { + server: Server.HTTPS_PRIVATE, + behavior: {response: ResponseBehavior.allowCrossOrigin()}, + }, + fetchOptions: {method: 'GET', mode: 'cors'}, + expected: FetchTestResult.SUCCESS, + }), + 'local to private: no preflight required.'); + + +subsetTestByKey( + 'from-local', promise_test, + t => fencedFrameFetchTest(t, { + source: {server: Server.HTTPS_LOCAL}, + target: { + server: Server.HTTPS_PUBLIC, + behavior: {response: ResponseBehavior.allowCrossOrigin()}, + }, + fetchOptions: {method: 'GET', mode: 'cors'}, + expected: FetchTestResult.SUCCESS, + }), + 'local to public: no preflight required.'); + +// Strictly speaking, the following two tests do not exercise PNA-specific +// logic, but they serve as a baseline for comparison, ensuring that non-PNA +// preflight requests are sent and handled as expected. + +subsetTestByKey( + 'baseline', promise_test, + t => fencedFrameFetchTest(t, { + source: {server: Server.HTTPS_LOCAL}, + target: { + server: Server.HTTPS_PUBLIC, + behavior: { + preflight: PreflightBehavior.failure(), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + fetchOptions: {method: 'PUT', mode: 'cors'}, + expected: FetchTestResult.FAILURE, + }), + 'local to public: PUT preflight failure.'); + +subsetTestByKey( + 'baseline', promise_test, + t => fencedFrameFetchTest(t, { + source: {server: Server.HTTPS_LOCAL}, + target: { + server: Server.HTTPS_PUBLIC, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + } + }, + fetchOptions: {method: 'PUT', mode: 'cors'}, + expected: FetchTestResult.SUCCESS, + }), + 'local to public: PUT preflight success.'); + +// Generates tests of preflight behavior for a single (source, target) pair. +// +// Scenarios: +// +// - cors mode: +// - preflight response has non-2xx HTTP code +// - preflight response is missing CORS headers +// - preflight response is missing the PNA-specific `Access-Control` header +// - final response is missing CORS headers +// - success +// - success with PUT method (non-"simple" request) +// - no-cors mode: +// - preflight response has non-2xx HTTP code +// - preflight response is missing CORS headers +// - preflight response is missing the PNA-specific `Access-Control` header +// - success +// +function makePreflightTests({ + subsetKey, + source, + sourceDescription, + targetServer, + targetDescription, +}) { + const prefix = `${sourceDescription} to ${targetDescription}: `; + + subsetTestByKey( + subsetKey, promise_test, + t => fencedFrameFetchTest(t, { + source, + target: { + server: targetServer, + behavior: { + preflight: PreflightBehavior.failure(), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + fetchOptions: {method: 'GET', mode: 'cors'}, + expected: FetchTestResult.FAILURE, + }), + prefix + 'failed preflight.'); + + subsetTestByKey( + subsetKey, promise_test, + t => fencedFrameFetchTest(t, { + source, + target: { + server: targetServer, + behavior: { + preflight: PreflightBehavior.noCorsHeader(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + fetchOptions: {method: 'GET', mode: 'cors'}, + expected: FetchTestResult.FAILURE, + }), + prefix + 'missing CORS headers on preflight response.'); + + subsetTestByKey( + subsetKey, promise_test, + t => fencedFrameFetchTest(t, { + source, + target: { + server: targetServer, + behavior: { + preflight: PreflightBehavior.noPnaHeader(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + fetchOptions: {method: 'GET', mode: 'cors'}, + expected: FetchTestResult.FAILURE, + }), + prefix + 'missing PNA header on preflight response.'); + + subsetTestByKey( + subsetKey, promise_test, + t => fencedFrameFetchTest(t, { + source, + target: { + server: targetServer, + behavior: {preflight: PreflightBehavior.success(token())}, + }, + fetchOptions: {method: 'GET', mode: 'cors'}, + expected: FetchTestResult.FAILURE, + }), + prefix + 'missing CORS headers on final response.'); + + subsetTestByKey( + subsetKey, promise_test, + t => fencedFrameFetchTest(t, { + source, + target: { + server: targetServer, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + fetchOptions: {method: 'GET', mode: 'cors'}, + expected: FetchTestResult.SUCCESS, + }), + prefix + 'success.'); + + subsetTestByKey( + subsetKey, promise_test, + t => fencedFrameFetchTest(t, { + source, + target: { + server: targetServer, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + fetchOptions: {method: 'PUT', mode: 'cors'}, + expected: FetchTestResult.SUCCESS, + }), + prefix + 'PUT success.'); + + subsetTestByKey( + subsetKey, promise_test, t => fencedFrameFetchTest(t, { + source, + target: {server: targetServer}, + fetchOptions: {method: 'GET', mode: 'no-cors'}, + expected: FetchTestResult.FAILURE, + }), + prefix + 'no-CORS mode failed preflight.'); + + subsetTestByKey( + subsetKey, promise_test, + t => fencedFrameFetchTest(t, { + source, + target: { + server: targetServer, + behavior: {preflight: PreflightBehavior.noCorsHeader(token())}, + }, + fetchOptions: {method: 'GET', mode: 'no-cors'}, + expected: FetchTestResult.FAILURE, + }), + prefix + 'no-CORS mode missing CORS headers on preflight response.'); + + subsetTestByKey( + subsetKey, promise_test, + t => fencedFrameFetchTest(t, { + source, + target: { + server: targetServer, + behavior: {preflight: PreflightBehavior.noPnaHeader(token())}, + }, + fetchOptions: {method: 'GET', mode: 'no-cors'}, + expected: FetchTestResult.FAILURE, + }), + prefix + 'no-CORS mode missing PNA header on preflight response.'); + + subsetTestByKey( + subsetKey, promise_test, + t => fencedFrameFetchTest(t, { + source, + target: { + server: targetServer, + behavior: {preflight: PreflightBehavior.success(token())}, + }, + fetchOptions: {method: 'GET', mode: 'no-cors'}, + expected: FetchTestResult.OPAQUE, + }), + prefix + 'no-CORS mode success.'); +} + +// Source: private secure context. +// +// Fetches to the local address space require a successful preflight response +// carrying a PNA-specific header. + +makePreflightTests({ + subsetKey: 'from-private', + source: {server: Server.HTTPS_PRIVATE}, + sourceDescription: 'private', + targetServer: Server.HTTPS_LOCAL, + targetDescription: 'local', +}); + +subsetTestByKey( + 'from-private', promise_test, t => fencedFrameFetchTest(t, { + source: {server: Server.HTTPS_PRIVATE}, + target: {server: Server.HTTPS_PRIVATE}, + fetchOptions: {method: 'GET', mode: 'cors'}, + expected: FetchTestResult.SUCCESS, + }), + 'private to private: no preflight required.'); + +subsetTestByKey( + 'from-private', promise_test, + t => fencedFrameFetchTest(t, { + source: {server: Server.HTTPS_PRIVATE}, + target: { + server: Server.HTTPS_PRIVATE, + behavior: {response: ResponseBehavior.allowCrossOrigin()}, + }, + fetchOptions: {method: 'GET', mode: 'cors'}, + expected: FetchTestResult.SUCCESS, + }), + 'private to public: no preflight required.'); + +// Source: public secure context. +// +// Fetches to the local and private address spaces require a successful +// preflight response carrying a PNA-specific header. + +makePreflightTests({ + subsetKey: 'from-public', + source: {server: Server.HTTPS_PUBLIC}, + sourceDescription: 'public', + targetServer: Server.HTTPS_LOCAL, + targetDescription: 'local', +}); + +makePreflightTests({ + subsetKey: 'from-public', + source: {server: Server.HTTPS_PUBLIC}, + sourceDescription: 'public', + targetServer: Server.HTTPS_PRIVATE, + targetDescription: 'private', +}); + +subsetTestByKey( + 'from-public', promise_test, t => fencedFrameFetchTest(t, { + source: {server: Server.HTTPS_PUBLIC}, + target: {server: Server.HTTPS_PUBLIC}, + fetchOptions: {method: 'GET', mode: 'cors'}, + expected: FetchTestResult.SUCCESS, + }), + 'public to public: no preflight required.'); diff --git a/test/wpt/tests/fetch/private-network-access/fenced-frame.tentative.https.window.js b/test/wpt/tests/fetch/private-network-access/fenced-frame.tentative.https.window.js new file mode 100644 index 00000000000..370cc9fbe9d --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/fenced-frame.tentative.https.window.js @@ -0,0 +1,150 @@ +// META: script=/common/dispatcher/dispatcher.js +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// META: script=/fenced-frame/resources/utils.js +// META: timeout=long +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests verify that contexts can navigate fenced frames to less-public +// address spaces iff the target server responds affirmatively to preflight +// requests. + +setup(() => { + assert_true(window.isSecureContext); +}); + +// Generates tests of preflight behavior for a single (source, target) pair. +// +// Scenarios: +// +// - parent navigates child: +// - preflight response has non-2xx HTTP code +// - preflight response is missing CORS headers +// - preflight response is missing the PNA-specific `Access-Control` header +// - preflight response has the required PNA related headers, but still fails +// because of the limitation of fenced frame that subjects to PNA checks. +// +function makePreflightTests({ + sourceName, + sourceServer, + sourceTreatAsPublic, + targetName, + targetServer, +}) { + const prefix = `${sourceName} to ${targetName}: `; + + const source = { + server: sourceServer, + treatAsPublic: sourceTreatAsPublic, + }; + + promise_test_parallel( + t => fencedFrameTest(t, { + source, + target: { + server: targetServer, + behavior: {preflight: PreflightBehavior.failure()}, + }, + expected: FrameTestResult.FAILURE, + }), + prefix + 'failed preflight.'); + + promise_test_parallel( + t => fencedFrameTest(t, { + source, + target: { + server: targetServer, + behavior: {preflight: PreflightBehavior.noCorsHeader(token())}, + }, + expected: FrameTestResult.FAILURE, + }), + prefix + 'missing CORS headers.'); + + promise_test_parallel( + t => fencedFrameTest(t, { + source, + target: { + server: targetServer, + behavior: {preflight: PreflightBehavior.noPnaHeader(token())}, + }, + expected: FrameTestResult.FAILURE, + }), + prefix + 'missing PNA header.'); + + promise_test_parallel( + t => fencedFrameTest(t, { + source, + target: { + server: targetServer, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin() + }, + }, + expected: FrameTestResult.FAILURE, + }), + prefix + 'failed because fenced frames are incompatible with PNA.'); +} + +// Source: private secure context. +// +// Fetches to the local address space require a successful preflight response +// carrying a PNA-specific header. + +makePreflightTests({ + sourceServer: Server.HTTPS_PRIVATE, + sourceName: 'private', + targetServer: Server.HTTPS_LOCAL, + targetName: 'local', +}); + +// Source: public secure context. +// +// Fetches to the local and private address spaces require a successful +// preflight response carrying a PNA-specific header. + +makePreflightTests({ + sourceServer: Server.HTTPS_PUBLIC, + sourceName: 'public', + targetServer: Server.HTTPS_LOCAL, + targetName: 'local', +}); + +makePreflightTests({ + sourceServer: Server.HTTPS_PUBLIC, + sourceName: 'public', + targetServer: Server.HTTPS_PRIVATE, + targetName: 'private', +}); + +// The following tests verify that `CSP: treat-as-public-address` makes +// documents behave as if they had been served from a public IP address. + +makePreflightTests({ + sourceServer: Server.HTTPS_LOCAL, + sourceTreatAsPublic: true, + sourceName: 'treat-as-public-address', + targetServer: Server.OTHER_HTTPS_LOCAL, + targetName: 'local', +}); + +promise_test_parallel( + t => fencedFrameTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: {server: Server.HTTPS_LOCAL}, + expected: FrameTestResult.FAILURE, + }), + 'treat-as-public-address to local (same-origin): fenced frame embedder ' + + 'initiated navigation has opaque origin.'); + +makePreflightTests({ + sourceServer: Server.HTTPS_LOCAL, + sourceTreatAsPublic: true, + sourceName: 'treat-as-public-address', + targetServer: Server.HTTPS_PRIVATE, + targetName: 'private', +}); diff --git a/test/wpt/tests/fetch/local-network-access/fetch-from-treat-as-public.https.window.js b/test/wpt/tests/fetch/private-network-access/fetch-from-treat-as-public.tentative.https.window.js similarity index 100% rename from test/wpt/tests/fetch/local-network-access/fetch-from-treat-as-public.https.window.js rename to test/wpt/tests/fetch/private-network-access/fetch-from-treat-as-public.tentative.https.window.js diff --git a/test/wpt/tests/fetch/local-network-access/fetch.https.window.js b/test/wpt/tests/fetch/private-network-access/fetch.tentative.https.window.js similarity index 100% rename from test/wpt/tests/fetch/local-network-access/fetch.https.window.js rename to test/wpt/tests/fetch/private-network-access/fetch.tentative.https.window.js diff --git a/test/wpt/tests/fetch/local-network-access/fetch.window.js b/test/wpt/tests/fetch/private-network-access/fetch.tentative.window.js similarity index 100% rename from test/wpt/tests/fetch/local-network-access/fetch.window.js rename to test/wpt/tests/fetch/private-network-access/fetch.tentative.window.js diff --git a/test/wpt/tests/fetch/local-network-access/iframe.tentative.https.window.js b/test/wpt/tests/fetch/private-network-access/iframe.tentative.https.window.js similarity index 63% rename from test/wpt/tests/fetch/local-network-access/iframe.tentative.https.window.js rename to test/wpt/tests/fetch/private-network-access/iframe.tentative.https.window.js index 6a83b88d3ff..0c12970557d 100644 --- a/test/wpt/tests/fetch/local-network-access/iframe.tentative.https.window.js +++ b/test/wpt/tests/fetch/private-network-access/iframe.tentative.https.window.js @@ -1,6 +1,13 @@ +// META: script=/common/subset-tests-by-key.js // META: script=/common/dispatcher/dispatcher.js // META: script=/common/utils.js // META: script=resources/support.sub.js +// META: timeout=long +// META: variant=?include=from-local +// META: variant=?include=from-private +// META: variant=?include=from-public +// META: variant=?include=from-treat-as-public +// META: variant=?include=grandparent // // Spec: https://wicg.github.io/private-network-access/#integration-fetch // @@ -18,22 +25,22 @@ setup(() => { // // All fetches unaffected by Private Network Access. -promise_test_parallel(t => iframeTest(t, { +subsetTestByKey("from-local", promise_test_parallel, t => iframeTest(t, { source: { server: Server.HTTPS_LOCAL }, target: { server: Server.HTTPS_LOCAL }, - expected: IframeTestResult.SUCCESS, + expected: FrameTestResult.SUCCESS, }), "local to local: no preflight required."); -promise_test_parallel(t => iframeTest(t, { +subsetTestByKey("from-local", promise_test_parallel, t => iframeTest(t, { source: { server: Server.HTTPS_LOCAL }, target: { server: Server.HTTPS_PRIVATE }, - expected: IframeTestResult.SUCCESS, + expected: FrameTestResult.SUCCESS, }), "local to private: no preflight required."); -promise_test_parallel(t => iframeTest(t, { +subsetTestByKey("from-local", promise_test_parallel, t => iframeTest(t, { source: { server: Server.HTTPS_LOCAL }, target: { server: Server.HTTPS_PUBLIC }, - expected: IframeTestResult.SUCCESS, + expected: FrameTestResult.SUCCESS, }), "local to public: no preflight required."); // Generates tests of preflight behavior for a single (source, target) pair. @@ -47,6 +54,7 @@ promise_test_parallel(t => iframeTest(t, { // - success // function makePreflightTests({ + key, sourceName, sourceServer, sourceTreatAsPublic, @@ -67,7 +75,7 @@ function makePreflightTests({ server: targetServer, behavior: { preflight: PreflightBehavior.failure() }, }, - expected: IframeTestResult.FAILURE, + expected: FrameTestResult.FAILURE, }), prefix + "failed preflight."); promise_test_parallel(t => iframeTest(t, { @@ -76,7 +84,7 @@ function makePreflightTests({ server: targetServer, behavior: { preflight: PreflightBehavior.noCorsHeader(token()) }, }, - expected: IframeTestResult.FAILURE, + expected: FrameTestResult.FAILURE, }), prefix + "missing CORS headers."); promise_test_parallel(t => iframeTest(t, { @@ -85,7 +93,7 @@ function makePreflightTests({ server: targetServer, behavior: { preflight: PreflightBehavior.noPnaHeader(token()) }, }, - expected: IframeTestResult.FAILURE, + expected: FrameTestResult.FAILURE, }), prefix + "missing PNA header."); promise_test_parallel(t => iframeTest(t, { @@ -94,7 +102,7 @@ function makePreflightTests({ server: targetServer, behavior: { preflight: PreflightBehavior.success(token()) }, }, - expected: IframeTestResult.SUCCESS, + expected: FrameTestResult.SUCCESS, }), prefix + "success."); } @@ -103,23 +111,23 @@ function makePreflightTests({ // Fetches to the local address space require a successful preflight response // carrying a PNA-specific header. -makePreflightTests({ +subsetTestByKey('from-private', makePreflightTests, { sourceServer: Server.HTTPS_PRIVATE, - sourceName: "private", + sourceName: 'private', targetServer: Server.HTTPS_LOCAL, - targetName: "local", + targetName: 'local', }); -promise_test_parallel(t => iframeTest(t, { +subsetTestByKey("from-private", promise_test_parallel, t => iframeTest(t, { source: { server: Server.HTTPS_PRIVATE }, target: { server: Server.HTTPS_PRIVATE }, - expected: IframeTestResult.SUCCESS, + expected: FrameTestResult.SUCCESS, }), "private to private: no preflight required."); -promise_test_parallel(t => iframeTest(t, { +subsetTestByKey("from-private", promise_test_parallel, t => iframeTest(t, { source: { server: Server.HTTPS_PRIVATE }, target: { server: Server.HTTPS_PUBLIC }, - expected: IframeTestResult.SUCCESS, + expected: FrameTestResult.SUCCESS, }), "private to public: no preflight required."); // Source: public secure context. @@ -127,30 +135,30 @@ promise_test_parallel(t => iframeTest(t, { // Fetches to the local and private address spaces require a successful // preflight response carrying a PNA-specific header. -makePreflightTests({ +subsetTestByKey('from-public', makePreflightTests, { sourceServer: Server.HTTPS_PUBLIC, sourceName: "public", targetServer: Server.HTTPS_LOCAL, targetName: "local", }); -makePreflightTests({ +subsetTestByKey('from-public', makePreflightTests, { sourceServer: Server.HTTPS_PUBLIC, sourceName: "public", targetServer: Server.HTTPS_PRIVATE, targetName: "private", }); -promise_test_parallel(t => iframeTest(t, { +subsetTestByKey("from-public", promise_test_parallel, t => iframeTest(t, { source: { server: Server.HTTPS_PUBLIC }, target: { server: Server.HTTPS_PUBLIC }, - expected: IframeTestResult.SUCCESS, + expected: FrameTestResult.SUCCESS, }), "public to public: no preflight required."); // The following tests verify that `CSP: treat-as-public-address` makes // documents behave as if they had been served from a public IP address. -makePreflightTests({ +subsetTestByKey('from-treat-as-public', makePreflightTests, { sourceServer: Server.HTTPS_LOCAL, sourceTreatAsPublic: true, sourceName: "treat-as-public-address", @@ -158,16 +166,20 @@ makePreflightTests({ targetName: "local", }); -promise_test_parallel(t => iframeTest(t, { - source: { - server: Server.HTTPS_LOCAL, - treatAsPublic: true, - }, - target: { server: Server.HTTPS_LOCAL }, - expected: IframeTestResult.SUCCESS, -}), "treat-as-public-address to local (same-origin): no preflight required."); +subsetTestByKey( + 'from-treat-as-public', promise_test_parallel, + t => iframeTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: {server: Server.HTTPS_LOCAL}, + expected: FrameTestResult.SUCCESS, + }), + 'treat-as-public-address to local (same-origin): no preflight required.' +); -makePreflightTests({ +subsetTestByKey('from-treat-as-public', makePreflightTests, { sourceServer: Server.HTTPS_LOCAL, sourceTreatAsPublic: true, sourceName: "treat-as-public-address", @@ -175,49 +187,57 @@ makePreflightTests({ targetName: "private", }); -promise_test_parallel(t => iframeTest(t, { - source: { - server: Server.HTTPS_LOCAL, - treatAsPublic: true, - }, - target: { server: Server.HTTPS_PUBLIC }, - expected: IframeTestResult.SUCCESS, -}), "treat-as-public-address to public: no preflight required."); +subsetTestByKey( + 'from-treat-as-public', promise_test_parallel, + t => iframeTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: {server: Server.HTTPS_PUBLIC}, + expected: FrameTestResult.SUCCESS, + }), + 'treat-as-public-address to public: no preflight required.' +); -promise_test_parallel(t => iframeTest(t, { - source: { - server: Server.HTTPS_LOCAL, - treatAsPublic: true, - }, - target: { - server: Server.HTTPS_PUBLIC, - behavior: { preflight: PreflightBehavior.optionalSuccess(token()) } - }, - expected: IframeTestResult.SUCCESS, -}), "treat-as-public-address to local: optional preflight"); +subsetTestByKey( + 'from-treat-as-public', promise_test_parallel, + t => iframeTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PUBLIC, + behavior: {preflight: PreflightBehavior.optionalSuccess(token())} + }, + expected: FrameTestResult.SUCCESS, + }), + 'treat-as-public-address to local: optional preflight' +); // The following tests verify that when a grandparent frame navigates its // grandchild, the IP address space of the grandparent is compared against the // IP address space of the response. Indeed, the navigation initiator in this // case is the grandparent, not the parent. -iframeGrandparentTest({ - name: "local to local, grandparent navigates: no preflight required.", +subsetTestByKey('grandparent', iframeGrandparentTest, { + name: 'local to local, grandparent navigates: no preflight required.', grandparentServer: Server.HTTPS_LOCAL, - child: { server: Server.HTTPS_PUBLIC }, - grandchild: { server: Server.OTHER_HTTPS_LOCAL }, - expected: IframeTestResult.SUCCESS, + child: {server: Server.HTTPS_PUBLIC}, + grandchild: {server: Server.OTHER_HTTPS_LOCAL}, + expected: FrameTestResult.SUCCESS, }); -iframeGrandparentTest({ +subsetTestByKey('grandparent', iframeGrandparentTest, { name: "local to local (same-origin), grandparent navigates: no preflight required.", grandparentServer: Server.HTTPS_LOCAL, child: { server: Server.HTTPS_PUBLIC }, grandchild: { server: Server.HTTPS_LOCAL }, - expected: IframeTestResult.SUCCESS, + expected: FrameTestResult.SUCCESS, }); -iframeGrandparentTest({ +subsetTestByKey('grandparent', iframeGrandparentTest, { name: "public to local, grandparent navigates: failure.", grandparentServer: Server.HTTPS_PUBLIC, child: { @@ -228,10 +248,10 @@ iframeGrandparentTest({ server: Server.HTTPS_LOCAL, behavior: { preflight: PreflightBehavior.failure() }, }, - expected: IframeTestResult.FAILURE, + expected: FrameTestResult.FAILURE, }); -iframeGrandparentTest({ +subsetTestByKey('grandparent', iframeGrandparentTest, { name: "public to local, grandparent navigates: success.", grandparentServer: Server.HTTPS_PUBLIC, child: { @@ -242,5 +262,5 @@ iframeGrandparentTest({ server: Server.HTTPS_LOCAL, behavior: { preflight: PreflightBehavior.success(token()) }, }, - expected: IframeTestResult.SUCCESS, + expected: FrameTestResult.SUCCESS, }); diff --git a/test/wpt/tests/fetch/local-network-access/iframe.tentative.window.js b/test/wpt/tests/fetch/private-network-access/iframe.tentative.window.js similarity index 86% rename from test/wpt/tests/fetch/local-network-access/iframe.tentative.window.js rename to test/wpt/tests/fetch/private-network-access/iframe.tentative.window.js index e00cb202bec..c0770df8385 100644 --- a/test/wpt/tests/fetch/local-network-access/iframe.tentative.window.js +++ b/test/wpt/tests/fetch/private-network-access/iframe.tentative.window.js @@ -18,55 +18,55 @@ setup(() => { promise_test_parallel(t => iframeTest(t, { source: { server: Server.HTTP_LOCAL }, target: { server: Server.HTTP_LOCAL }, - expected: IframeTestResult.SUCCESS, + expected: FrameTestResult.SUCCESS, }), "local to local: no preflight required."); promise_test_parallel(t => iframeTest(t, { source: { server: Server.HTTP_LOCAL }, target: { server: Server.HTTP_PRIVATE }, - expected: IframeTestResult.SUCCESS, + expected: FrameTestResult.SUCCESS, }), "local to private: no preflight required."); promise_test_parallel(t => iframeTest(t, { source: { server: Server.HTTP_LOCAL }, target: { server: Server.HTTP_PUBLIC }, - expected: IframeTestResult.SUCCESS, + expected: FrameTestResult.SUCCESS, }), "local to public: no preflight required."); promise_test_parallel(t => iframeTest(t, { source: { server: Server.HTTP_PRIVATE }, target: { server: Server.HTTP_LOCAL }, - expected: IframeTestResult.FAILURE, + expected: FrameTestResult.FAILURE, }), "private to local: failure."); promise_test_parallel(t => iframeTest(t, { source: { server: Server.HTTP_PRIVATE }, target: { server: Server.HTTP_PRIVATE }, - expected: IframeTestResult.SUCCESS, + expected: FrameTestResult.SUCCESS, }), "private to private: no preflight required."); promise_test_parallel(t => iframeTest(t, { source: { server: Server.HTTP_PRIVATE }, target: { server: Server.HTTP_PUBLIC }, - expected: IframeTestResult.SUCCESS, + expected: FrameTestResult.SUCCESS, }), "private to public: no preflight required."); promise_test_parallel(t => iframeTest(t, { source: { server: Server.HTTP_PUBLIC }, target: { server: Server.HTTP_LOCAL }, - expected: IframeTestResult.FAILURE, + expected: FrameTestResult.FAILURE, }), "public to local: failure."); promise_test_parallel(t => iframeTest(t, { source: { server: Server.HTTP_PUBLIC }, target: { server: Server.HTTP_PRIVATE }, - expected: IframeTestResult.FAILURE, + expected: FrameTestResult.FAILURE, }), "public to private: failure."); promise_test_parallel(t => iframeTest(t, { source: { server: Server.HTTP_PUBLIC }, target: { server: Server.HTTP_PUBLIC }, - expected: IframeTestResult.SUCCESS, + expected: FrameTestResult.SUCCESS, }), "public to public: no preflight required."); promise_test_parallel(t => iframeTest(t, { @@ -75,7 +75,7 @@ promise_test_parallel(t => iframeTest(t, { treatAsPublic: true, }, target: { server: Server.HTTP_LOCAL }, - expected: IframeTestResult.FAILURE, + expected: FrameTestResult.FAILURE, }), "treat-as-public-address to local: failure."); promise_test_parallel(t => iframeTest(t, { @@ -84,7 +84,7 @@ promise_test_parallel(t => iframeTest(t, { treatAsPublic: true, }, target: { server: Server.HTTP_PRIVATE }, - expected: IframeTestResult.FAILURE, + expected: FrameTestResult.FAILURE, }), "treat-as-public-address to private: failure."); promise_test_parallel(t => iframeTest(t, { @@ -93,7 +93,7 @@ promise_test_parallel(t => iframeTest(t, { treatAsPublic: true, }, target: { server: Server.HTTP_PUBLIC }, - expected: IframeTestResult.SUCCESS, + expected: FrameTestResult.SUCCESS, }), "treat-as-public-address to public: no preflight required."); // The following test verifies that when a grandparent frame navigates its @@ -106,5 +106,5 @@ iframeGrandparentTest({ grandparentServer: Server.HTTP_LOCAL, child: { server: Server.HTTP_PUBLIC }, grandchild: { server: Server.HTTP_LOCAL }, - expected: IframeTestResult.SUCCESS, + expected: FrameTestResult.SUCCESS, }); diff --git a/test/wpt/tests/fetch/local-network-access/mixed-content-fetch.tentative.https.window.js b/test/wpt/tests/fetch/private-network-access/mixed-content-fetch.tentative.https.window.js similarity index 77% rename from test/wpt/tests/fetch/local-network-access/mixed-content-fetch.tentative.https.window.js rename to test/wpt/tests/fetch/private-network-access/mixed-content-fetch.tentative.https.window.js index 6f7d765617c..54485dc7047 100644 --- a/test/wpt/tests/fetch/local-network-access/mixed-content-fetch.tentative.https.window.js +++ b/test/wpt/tests/fetch/private-network-access/mixed-content-fetch.tentative.https.window.js @@ -1,10 +1,10 @@ // META: script=/common/utils.js // META: script=resources/support.sub.js // -// Spec: https://wicg.github.io/local-network-access +// Spec: https://wicg.github.io/private-network-access // // These tests verify that secure contexts can fetch non-secure subresources -// from more local address spaces, avoiding mixed context checks, as long as +// from more private address spaces, avoiding mixed context checks, as long as // they specify a valid `targetAddressSpace` fetch option that matches the // target server's address space. @@ -16,9 +16,9 @@ setup(() => { // Given `addressSpace`, returns the other three possible IP address spaces. function otherAddressSpaces(addressSpace) { switch (addressSpace) { - case "loopback": return ["unknown", "local", "public"]; - case "local": return ["unknown", "loopback", "public"]; - case "public": return ["unknown", "loopback", "local"]; + case "local": return ["unknown", "private", "public"]; + case "private": return ["unknown", "local", "public"]; + case "public": return ["unknown", "local", "private"]; } } @@ -169,66 +169,64 @@ function makeNoBypassTests({ source, target }) { }, fetchOptions: { targetAddressSpace: correctAddressSpace }, expected: FetchTestResult.FAILURE, - }), prefix + 'not a local network request.'); + }), prefix + 'not a private network request.'); } -// Source: loopback secure context. +// Source: local secure context. // -// Fetches to the loopback and local address spaces cannot use +// Fetches to the local and private address spaces cannot use // `targetAddressSpace` to bypass mixed content, as they are not otherwise -// blocked by Local Network Access. +// blocked by Private Network Access. -makeNoBypassTests({ source: "loopback", target: "loopback" }); -makeNoBypassTests({ source: "loopback", target: "local" }); -makeNoBypassTests({ source: "loopback", target: "public" }); +makeNoBypassTests({ source: "local", target: "local" }); +makeNoBypassTests({ source: "local", target: "private" }); +makeNoBypassTests({ source: "local", target: "public" }); -// Source: local secure context. +// Source: private secure context. // -// Fetches to the loopback address space requires the right `targetAddressSpace` +// Fetches to the local address space requires the right `targetAddressSpace` // option, as well as a successful preflight response carrying a PNA-specific // header. // -// Fetches to the local address space cannot use `targetAddressSpace` to -// bypass mixed content, as they are not otherwise blocked by Local Network +// Fetches to the private address space cannot use `targetAddressSpace` to +// bypass mixed content, as they are not otherwise blocked by Private Network // Access. -makeTests({ source: "local", target: "loopback" }); +makeTests({ source: "private", target: "local" }); -makeNoBypassTests({ source: "local", target: "local" }); -makeNoBypassTests({ source: "local", target: "public" }); +makeNoBypassTests({ source: "private", target: "private" }); +makeNoBypassTests({ source: "private", target: "public" }); // Source: public secure context. // -// Fetches to the loopback and local address spaces require the right +// Fetches to the local and private address spaces require the right // `targetAddressSpace` option, as well as a successful preflight response // carrying a PNA-specific header. -makeTests({ source: "public", target: "loopback" }); makeTests({ source: "public", target: "local" }); +makeTests({ source: "public", target: "private" }); makeNoBypassTests({ source: "public", target: "public" }); -// These tests verify that documents fetched from the `loopback` address space -// yet carrying the `treat-as-public-address` CSP directive are treated as if -// they had been fetched from the `public` address space. +// These tests verify that documents fetched from the `local` address space yet +// carrying the `treat-as-public-address` CSP directive are treated as if they +// had been fetched from the `public` address space. -promise_test_parallel( - t => fetchTest(t, { - source: { - server: Server.HTTPS_LOCAL, - treatAsPublic: true, - }, - target: { - server: Server.HTTP_LOCAL, - behavior: { - preflight: PreflightBehavior.optionalSuccess(token()), - response: ResponseBehavior.allowCrossOrigin(), - }, - }, - fetchOptions: {targetAddressSpace: 'local'}, - expected: FetchTestResult.FAILURE, - }), - 'https-treat-as-public to http-loopback: wrong targetAddressSpace "local".'); +promise_test_parallel(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTP_LOCAL, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + fetchOptions: { targetAddressSpace: "private" }, + expected: FetchTestResult.FAILURE, +}), 'https-treat-as-public to http-local: wrong targetAddressSpace "private".'); promise_test_parallel(t => fetchTest(t, { source: { @@ -242,9 +240,9 @@ promise_test_parallel(t => fetchTest(t, { response: ResponseBehavior.allowCrossOrigin(), }, }, - fetchOptions: { targetAddressSpace: "loopback" }, + fetchOptions: { targetAddressSpace: "local" }, expected: FetchTestResult.SUCCESS, -}), "https-treat-as-public to http-loopback: success."); +}), "https-treat-as-public to http-local: success."); promise_test_parallel(t => fetchTest(t, { source: { @@ -258,9 +256,9 @@ promise_test_parallel(t => fetchTest(t, { response: ResponseBehavior.allowCrossOrigin(), }, }, - fetchOptions: { targetAddressSpace: "loopback" }, + fetchOptions: { targetAddressSpace: "local" }, expected: FetchTestResult.FAILURE, -}), 'https-treat-as-public to http-local: wrong targetAddressSpace "loopback".'); +}), 'https-treat-as-public to http-private: wrong targetAddressSpace "local".'); promise_test_parallel(t => fetchTest(t, { source: { @@ -274,6 +272,6 @@ promise_test_parallel(t => fetchTest(t, { response: ResponseBehavior.allowCrossOrigin(), }, }, - fetchOptions: { targetAddressSpace: "local" }, + fetchOptions: { targetAddressSpace: "private" }, expected: FetchTestResult.SUCCESS, -}), "https-treat-as-public to http-local: success."); +}), "https-treat-as-public to http-private: success."); diff --git a/test/wpt/tests/fetch/local-network-access/nested-worker.https.window.js b/test/wpt/tests/fetch/private-network-access/nested-worker.tentative.https.window.js similarity index 100% rename from test/wpt/tests/fetch/local-network-access/nested-worker.https.window.js rename to test/wpt/tests/fetch/private-network-access/nested-worker.tentative.https.window.js diff --git a/test/wpt/tests/fetch/local-network-access/nested-worker.window.js b/test/wpt/tests/fetch/private-network-access/nested-worker.tentative.window.js similarity index 100% rename from test/wpt/tests/fetch/local-network-access/nested-worker.window.js rename to test/wpt/tests/fetch/private-network-access/nested-worker.tentative.window.js diff --git a/test/wpt/tests/fetch/local-network-access/preflight-cache.https.window.js b/test/wpt/tests/fetch/private-network-access/preflight-cache.https.tentative.window.js similarity index 100% rename from test/wpt/tests/fetch/local-network-access/preflight-cache.https.window.js rename to test/wpt/tests/fetch/private-network-access/preflight-cache.https.tentative.window.js diff --git a/test/wpt/tests/fetch/local-network-access/redirect.https.window.js b/test/wpt/tests/fetch/private-network-access/redirect.tentative.https.window.js similarity index 100% rename from test/wpt/tests/fetch/local-network-access/redirect.https.window.js rename to test/wpt/tests/fetch/private-network-access/redirect.tentative.https.window.js diff --git a/test/wpt/tests/fetch/local-network-access/resources/executor.html b/test/wpt/tests/fetch/private-network-access/resources/executor.html similarity index 100% rename from test/wpt/tests/fetch/local-network-access/resources/executor.html rename to test/wpt/tests/fetch/private-network-access/resources/executor.html diff --git a/test/wpt/tests/fetch/private-network-access/resources/fenced-frame-fetcher.https.html b/test/wpt/tests/fetch/private-network-access/resources/fenced-frame-fetcher.https.html new file mode 100644 index 00000000000..b14601dba51 --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/resources/fenced-frame-fetcher.https.html @@ -0,0 +1,25 @@ + + + +Fetcher + \ No newline at end of file diff --git a/test/wpt/tests/fetch/private-network-access/resources/fenced-frame-fetcher.https.html.headers b/test/wpt/tests/fetch/private-network-access/resources/fenced-frame-fetcher.https.html.headers new file mode 100644 index 00000000000..6247f6d6321 --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/resources/fenced-frame-fetcher.https.html.headers @@ -0,0 +1 @@ +Supports-Loading-Mode: fenced-frame \ No newline at end of file diff --git a/test/wpt/tests/fetch/private-network-access/resources/fenced-frame-local-network-access-target.https.html b/test/wpt/tests/fetch/private-network-access/resources/fenced-frame-local-network-access-target.https.html new file mode 100644 index 00000000000..2b55e056f39 --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/resources/fenced-frame-local-network-access-target.https.html @@ -0,0 +1,8 @@ + + + +Fenced frame target + diff --git a/test/wpt/tests/fetch/private-network-access/resources/fenced-frame-local-network-access.https.html b/test/wpt/tests/fetch/private-network-access/resources/fenced-frame-local-network-access.https.html new file mode 100644 index 00000000000..98f118432e0 --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/resources/fenced-frame-local-network-access.https.html @@ -0,0 +1,14 @@ + + + + + +Fenced frame + + diff --git a/test/wpt/tests/fetch/private-network-access/resources/fenced-frame-local-network-access.https.html.headers b/test/wpt/tests/fetch/private-network-access/resources/fenced-frame-local-network-access.https.html.headers new file mode 100644 index 00000000000..6247f6d6321 --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/resources/fenced-frame-local-network-access.https.html.headers @@ -0,0 +1 @@ +Supports-Loading-Mode: fenced-frame \ No newline at end of file diff --git a/test/wpt/tests/fetch/local-network-access/resources/fetcher.html b/test/wpt/tests/fetch/private-network-access/resources/fetcher.html similarity index 100% rename from test/wpt/tests/fetch/local-network-access/resources/fetcher.html rename to test/wpt/tests/fetch/private-network-access/resources/fetcher.html diff --git a/test/wpt/tests/fetch/local-network-access/resources/fetcher.js b/test/wpt/tests/fetch/private-network-access/resources/fetcher.js similarity index 100% rename from test/wpt/tests/fetch/local-network-access/resources/fetcher.js rename to test/wpt/tests/fetch/private-network-access/resources/fetcher.js diff --git a/test/wpt/tests/fetch/local-network-access/resources/iframed.html b/test/wpt/tests/fetch/private-network-access/resources/iframed.html similarity index 100% rename from test/wpt/tests/fetch/local-network-access/resources/iframed.html rename to test/wpt/tests/fetch/private-network-access/resources/iframed.html diff --git a/test/wpt/tests/fetch/local-network-access/resources/iframer.html b/test/wpt/tests/fetch/private-network-access/resources/iframer.html similarity index 100% rename from test/wpt/tests/fetch/local-network-access/resources/iframer.html rename to test/wpt/tests/fetch/private-network-access/resources/iframer.html diff --git a/test/wpt/tests/fetch/local-network-access/resources/preflight.py b/test/wpt/tests/fetch/private-network-access/resources/preflight.py similarity index 97% rename from test/wpt/tests/fetch/local-network-access/resources/preflight.py rename to test/wpt/tests/fetch/private-network-access/resources/preflight.py index 4b0bfefd4d6..be3abdbb2a1 100644 --- a/test/wpt/tests/fetch/local-network-access/resources/preflight.py +++ b/test/wpt/tests/fetch/private-network-access/resources/preflight.py @@ -85,6 +85,9 @@ def _is_preflight_optional(request): def _get_preflight_uuid(request): return request.GET.get(b"preflight-uuid") +def _is_loaded_in_fenced_frame(request): + return request.GET.get(b"is-loaded-in-fenced-frame") + def _should_treat_as_public_once(request): uuid = request.GET.get(b"treat-as-public-once") if uuid is None: @@ -155,6 +158,9 @@ def _handle_final_request(request, response): if mime_type is not None: headers.append(("Content-Type", mime_type),) + if _is_loaded_in_fenced_frame(request): + headers.append(("Supports-Loading-Mode", "fenced-frame")) + body = _final_response_body(request) return (headers, body) diff --git a/test/wpt/tests/fetch/local-network-access/resources/service-worker-bridge.html b/test/wpt/tests/fetch/private-network-access/resources/service-worker-bridge.html similarity index 100% rename from test/wpt/tests/fetch/local-network-access/resources/service-worker-bridge.html rename to test/wpt/tests/fetch/private-network-access/resources/service-worker-bridge.html diff --git a/test/wpt/tests/fetch/local-network-access/resources/service-worker.js b/test/wpt/tests/fetch/private-network-access/resources/service-worker.js similarity index 100% rename from test/wpt/tests/fetch/local-network-access/resources/service-worker.js rename to test/wpt/tests/fetch/private-network-access/resources/service-worker.js diff --git a/test/wpt/tests/fetch/local-network-access/resources/shared-fetcher.js b/test/wpt/tests/fetch/private-network-access/resources/shared-fetcher.js similarity index 100% rename from test/wpt/tests/fetch/local-network-access/resources/shared-fetcher.js rename to test/wpt/tests/fetch/private-network-access/resources/shared-fetcher.js diff --git a/test/wpt/tests/fetch/local-network-access/resources/shared-worker-blob-fetcher.html b/test/wpt/tests/fetch/private-network-access/resources/shared-worker-blob-fetcher.html similarity index 100% rename from test/wpt/tests/fetch/local-network-access/resources/shared-worker-blob-fetcher.html rename to test/wpt/tests/fetch/private-network-access/resources/shared-worker-blob-fetcher.html diff --git a/test/wpt/tests/fetch/local-network-access/resources/shared-worker-fetcher.html b/test/wpt/tests/fetch/private-network-access/resources/shared-worker-fetcher.html similarity index 100% rename from test/wpt/tests/fetch/local-network-access/resources/shared-worker-fetcher.html rename to test/wpt/tests/fetch/private-network-access/resources/shared-worker-fetcher.html diff --git a/test/wpt/tests/fetch/local-network-access/resources/socket-opener.html b/test/wpt/tests/fetch/private-network-access/resources/socket-opener.html similarity index 100% rename from test/wpt/tests/fetch/local-network-access/resources/socket-opener.html rename to test/wpt/tests/fetch/private-network-access/resources/socket-opener.html diff --git a/test/wpt/tests/fetch/local-network-access/resources/support.sub.js b/test/wpt/tests/fetch/private-network-access/resources/support.sub.js similarity index 86% rename from test/wpt/tests/fetch/local-network-access/resources/support.sub.js rename to test/wpt/tests/fetch/private-network-access/resources/support.sub.js index a09c46031f5..27d733d8b7f 100644 --- a/test/wpt/tests/fetch/local-network-access/resources/support.sub.js +++ b/test/wpt/tests/fetch/private-network-access/resources/support.sub.js @@ -75,21 +75,21 @@ async function postMessageAndAwaitReply(target, message) { // Maps protocol (without the trailing colon) and address space to port. const SERVER_PORTS = { "http": { - "loopback": {{ports[http][0]}}, - "local": {{ports[http-private][0]}}, + "local": {{ports[http][0]}}, + "private": {{ports[http-private][0]}}, "public": {{ports[http-public][0]}}, }, "https": { - "loopback": {{ports[https][0]}}, - "other-loopback": {{ports[https][1]}}, - "local": {{ports[https-private][0]}}, + "local": {{ports[https][0]}}, + "other-local": {{ports[https][1]}}, + "private": {{ports[https-private][0]}}, "public": {{ports[https-public][0]}}, }, "ws": { - "loopback": {{ports[ws][0]}}, + "local": {{ports[ws][0]}}, }, "wss": { - "loopback": {{ports[wss][0]}}, + "local": {{ports[wss][0]}}, }, }; @@ -127,15 +127,15 @@ class Server { }; } - static HTTP_LOCAL = Server.get("http", "loopback"); - static HTTP_PRIVATE = Server.get("http", "local"); + static HTTP_LOCAL = Server.get("http", "local"); + static HTTP_PRIVATE = Server.get("http", "private"); static HTTP_PUBLIC = Server.get("http", "public"); - static HTTPS_LOCAL = Server.get("https", "loopback"); - static OTHER_HTTPS_LOCAL = Server.get("https", "other-loopback"); - static HTTPS_PRIVATE = Server.get("https", "local"); + static HTTPS_LOCAL = Server.get("https", "local"); + static OTHER_HTTPS_LOCAL = Server.get("https", "other-local"); + static HTTPS_PRIVATE = Server.get("https", "private"); static HTTPS_PUBLIC = Server.get("https", "public"); - static WS_LOCAL = Server.get("ws", "loopback"); - static WSS_LOCAL = Server.get("wss", "loopback"); + static WS_LOCAL = Server.get("ws", "local"); + static WSS_LOCAL = Server.get("wss", "local"); }; // Resolves a URL relative to the current location, returning an absolute URL. @@ -341,6 +341,40 @@ async function fetchTest(t, { source, target, fetchOptions, expected }) { } } +// Similar to `fetchTest`, but replaced iframes with fenced frames. +async function fencedFrameFetchTest(t, { source, target, fetchOptions, expected }) { + const fetcher_url = + resolveUrl("resources/fenced-frame-fetcher.https.html", sourceResolveOptions(source)); + + const target_url = preflightUrl(target); + target_url.searchParams.set("is-loaded-in-fenced-frame", true); + + fetcher_url.searchParams.set("mode", fetchOptions.mode); + fetcher_url.searchParams.set("method", fetchOptions.method); + fetcher_url.searchParams.set("url", target_url); + + const error_token = token(); + const ok_token = token(); + const body_token = token(); + const type_token = token(); + const source_url = generateURL(fetcher_url, [error_token, ok_token, body_token, type_token]); + + const urn = await generateURNFromFledge(source_url, []); + attachFencedFrame(urn); + + const error = await nextValueFromServer(error_token); + const ok = await nextValueFromServer(ok_token); + const body = await nextValueFromServer(body_token); + const type = await nextValueFromServer(type_token); + + assert_equals(error, expected.error || "" , "error"); + assert_equals(body, expected.body || "", "response body"); + assert_equals(ok, expected.ok !== undefined ? expected.ok.toString() : "", "response ok"); + if (expected.type !== undefined) { + assert_equals(type, expected.type, "response type"); + } +} + const XhrTestResult = { SUCCESS: { loaded: true, @@ -393,7 +427,7 @@ async function xhrTest(t, { source, target, method, expected }) { assert_equals(body, expected.body, "response body"); } -const IframeTestResult = { +const FrameTestResult = { SUCCESS: "loaded", FAILURE: "timeout", }; @@ -429,6 +463,37 @@ async function iframeTest(t, { source, target, expected }) { assert_equals(result, expected); } +// Similar to `iframeTest`, but replaced iframes with fenced frames. +async function fencedFrameTest(t, { source, target, expected }) { + // Allows running tests in parallel. + const target_url = preflightUrl(target); + target_url.searchParams.set("file", "fenced-frame-local-network-access-target.https.html"); + target_url.searchParams.set("is-loaded-in-fenced-frame", true); + + const frame_loaded_key = token(); + const child_frame_target = generateURL(target_url, [frame_loaded_key]); + + const source_url = + resolveUrl("resources/fenced-frame-local-network-access.https.html", sourceResolveOptions(source)); + source_url.searchParams.set("fenced_frame_url", child_frame_target); + + const urn = await generateURNFromFledge(source_url, []); + attachFencedFrame(urn); + + // The grandchild fenced frame writes a value to the server iff it loads + // successfully. + const result = (expected == FrameTestResult.SUCCESS) ? + await nextValueFromServer(frame_loaded_key) : + await Promise.race([ + nextValueFromServer(frame_loaded_key), + new Promise((resolve) => { + t.step_timeout(() => resolve("timeout"), 10000 /* ms */); + }), + ]); + + assert_equals(result, expected); +} + const iframeGrandparentTest = ({ name, grandparentServer, diff --git a/test/wpt/tests/fetch/local-network-access/resources/worker-blob-fetcher.html b/test/wpt/tests/fetch/private-network-access/resources/worker-blob-fetcher.html similarity index 100% rename from test/wpt/tests/fetch/local-network-access/resources/worker-blob-fetcher.html rename to test/wpt/tests/fetch/private-network-access/resources/worker-blob-fetcher.html diff --git a/test/wpt/tests/fetch/local-network-access/resources/worker-fetcher.html b/test/wpt/tests/fetch/private-network-access/resources/worker-fetcher.html similarity index 100% rename from test/wpt/tests/fetch/local-network-access/resources/worker-fetcher.html rename to test/wpt/tests/fetch/private-network-access/resources/worker-fetcher.html diff --git a/test/wpt/tests/fetch/local-network-access/resources/worker-fetcher.js b/test/wpt/tests/fetch/private-network-access/resources/worker-fetcher.js similarity index 100% rename from test/wpt/tests/fetch/local-network-access/resources/worker-fetcher.js rename to test/wpt/tests/fetch/private-network-access/resources/worker-fetcher.js diff --git a/test/wpt/tests/fetch/local-network-access/resources/xhr-sender.html b/test/wpt/tests/fetch/private-network-access/resources/xhr-sender.html similarity index 100% rename from test/wpt/tests/fetch/local-network-access/resources/xhr-sender.html rename to test/wpt/tests/fetch/private-network-access/resources/xhr-sender.html diff --git a/test/wpt/tests/fetch/local-network-access/service-worker-background-fetch.https.window.js b/test/wpt/tests/fetch/private-network-access/service-worker-background-fetch.tentative.https.window.js similarity index 100% rename from test/wpt/tests/fetch/local-network-access/service-worker-background-fetch.https.window.js rename to test/wpt/tests/fetch/private-network-access/service-worker-background-fetch.tentative.https.window.js diff --git a/test/wpt/tests/fetch/local-network-access/service-worker-fetch.https.window.js b/test/wpt/tests/fetch/private-network-access/service-worker-fetch.tentative.https.window.js similarity index 88% rename from test/wpt/tests/fetch/local-network-access/service-worker-fetch.https.window.js rename to test/wpt/tests/fetch/private-network-access/service-worker-fetch.tentative.https.window.js index 3d0f6d8097a..cb6d1f79b01 100644 --- a/test/wpt/tests/fetch/local-network-access/service-worker-fetch.https.window.js +++ b/test/wpt/tests/fetch/private-network-access/service-worker-fetch.tentative.https.window.js @@ -1,5 +1,8 @@ // META: script=/common/utils.js // META: script=resources/support.sub.js +// META: script=/common/subset-tests.js +// META: variant=?1-8 +// META: variant=?9-last // // Spec: https://wicg.github.io/private-network-access/#integration-fetch // @@ -75,13 +78,13 @@ async function makeTest(t, { source, target, expected }) { } } -promise_test(t => makeTest(t, { +subsetTest(promise_test, t => makeTest(t, { source: { server: Server.HTTPS_LOCAL }, target: { server: Server.HTTPS_LOCAL }, expected: TestResult.SUCCESS, }), "local to local: success."); -promise_test(t => makeTest(t, { +subsetTest(promise_test, t => makeTest(t, { source: { server: Server.HTTPS_PRIVATE }, target: { server: Server.HTTPS_LOCAL, @@ -90,7 +93,7 @@ promise_test(t => makeTest(t, { expected: TestResult.FAILURE, }), "private to local: failed preflight."); -promise_test(t => makeTest(t, { +subsetTest(promise_test, t => makeTest(t, { source: { server: Server.HTTPS_PRIVATE }, target: { server: Server.HTTPS_LOCAL, @@ -102,13 +105,13 @@ promise_test(t => makeTest(t, { expected: TestResult.SUCCESS, }), "private to local: success."); -promise_test(t => makeTest(t, { +subsetTest(promise_test, t => makeTest(t, { source: { server: Server.HTTPS_PRIVATE }, target: { server: Server.HTTPS_PRIVATE }, expected: TestResult.SUCCESS, }), "private to private: success."); -promise_test(t => makeTest(t, { +subsetTest(promise_test, t => makeTest(t, { source: { server: Server.HTTPS_PUBLIC }, target: { server: Server.HTTPS_LOCAL, @@ -117,7 +120,7 @@ promise_test(t => makeTest(t, { expected: TestResult.FAILURE, }), "public to local: failed preflight."); -promise_test(t => makeTest(t, { +subsetTest(promise_test, t => makeTest(t, { source: { server: Server.HTTPS_PUBLIC }, target: { server: Server.HTTPS_LOCAL, @@ -129,7 +132,7 @@ promise_test(t => makeTest(t, { expected: TestResult.SUCCESS, }), "public to local: success."); -promise_test(t => makeTest(t, { +subsetTest(promise_test, t => makeTest(t, { source: { server: Server.HTTPS_PUBLIC }, target: { server: Server.HTTPS_PRIVATE, @@ -138,7 +141,7 @@ promise_test(t => makeTest(t, { expected: TestResult.FAILURE, }), "public to private: failed preflight."); -promise_test(t => makeTest(t, { +subsetTest(promise_test, t => makeTest(t, { source: { server: Server.HTTPS_PUBLIC }, target: { server: Server.HTTPS_PRIVATE, @@ -150,13 +153,13 @@ promise_test(t => makeTest(t, { expected: TestResult.SUCCESS, }), "public to private: success."); -promise_test(t => makeTest(t, { +subsetTest(promise_test, t => makeTest(t, { source: { server: Server.HTTPS_PUBLIC }, target: { server: Server.HTTPS_PUBLIC }, expected: TestResult.SUCCESS, }), "public to public: success."); -promise_test(t => makeTest(t, { +subsetTest(promise_test, t => makeTest(t, { source: { server: Server.HTTPS_LOCAL, treatAsPublic: true, @@ -168,7 +171,7 @@ promise_test(t => makeTest(t, { expected: TestResult.FAILURE, }), "treat-as-public to local: failed preflight."); -promise_test(t => makeTest(t, { +subsetTest(promise_test, t => makeTest(t, { source: { server: Server.HTTPS_LOCAL, treatAsPublic: true, @@ -183,7 +186,7 @@ promise_test(t => makeTest(t, { expected: TestResult.SUCCESS, }), "treat-as-public to local: success."); -promise_test(t => makeTest(t, { +subsetTest(promise_test, t => makeTest(t, { source: { server: Server.HTTPS_LOCAL, treatAsPublic: true, @@ -192,7 +195,7 @@ promise_test(t => makeTest(t, { expected: TestResult.SUCCESS, }), "treat-as-public to local (same-origin): no preflight required."); -promise_test(t => makeTest(t, { +subsetTest(promise_test, t => makeTest(t, { source: { server: Server.HTTPS_LOCAL, treatAsPublic: true, @@ -204,7 +207,7 @@ promise_test(t => makeTest(t, { expected: TestResult.FAILURE, }), "treat-as-public to private: failed preflight."); -promise_test(t => makeTest(t, { +subsetTest(promise_test, t => makeTest(t, { source: { server: Server.HTTPS_LOCAL, treatAsPublic: true, @@ -219,7 +222,7 @@ promise_test(t => makeTest(t, { expected: TestResult.SUCCESS, }), "treat-as-public to private: success."); -promise_test(t => makeTest(t, { +subsetTest(promise_test, t => makeTest(t, { source: { server: Server.HTTPS_LOCAL, treatAsPublic: true, diff --git a/test/wpt/tests/fetch/local-network-access/service-worker-update.https.window.js b/test/wpt/tests/fetch/private-network-access/service-worker-update.tentative.https.window.js similarity index 100% rename from test/wpt/tests/fetch/local-network-access/service-worker-update.https.window.js rename to test/wpt/tests/fetch/private-network-access/service-worker-update.tentative.https.window.js diff --git a/test/wpt/tests/fetch/local-network-access/service-worker.https.window.js b/test/wpt/tests/fetch/private-network-access/service-worker.tentative.https.window.js similarity index 100% rename from test/wpt/tests/fetch/local-network-access/service-worker.https.window.js rename to test/wpt/tests/fetch/private-network-access/service-worker.tentative.https.window.js diff --git a/test/wpt/tests/fetch/local-network-access/shared-worker-blob-fetch.https.window.js b/test/wpt/tests/fetch/private-network-access/shared-worker-blob-fetch.tentative.https.window.js similarity index 100% rename from test/wpt/tests/fetch/local-network-access/shared-worker-blob-fetch.https.window.js rename to test/wpt/tests/fetch/private-network-access/shared-worker-blob-fetch.tentative.https.window.js diff --git a/test/wpt/tests/fetch/local-network-access/shared-worker-blob-fetch.window.js b/test/wpt/tests/fetch/private-network-access/shared-worker-blob-fetch.tentative.window.js similarity index 100% rename from test/wpt/tests/fetch/local-network-access/shared-worker-blob-fetch.window.js rename to test/wpt/tests/fetch/private-network-access/shared-worker-blob-fetch.tentative.window.js diff --git a/test/wpt/tests/fetch/local-network-access/shared-worker-fetch.https.window.js b/test/wpt/tests/fetch/private-network-access/shared-worker-fetch.tentative.https.window.js similarity index 100% rename from test/wpt/tests/fetch/local-network-access/shared-worker-fetch.https.window.js rename to test/wpt/tests/fetch/private-network-access/shared-worker-fetch.tentative.https.window.js diff --git a/test/wpt/tests/fetch/local-network-access/shared-worker-fetch.window.js b/test/wpt/tests/fetch/private-network-access/shared-worker-fetch.tentative.window.js similarity index 100% rename from test/wpt/tests/fetch/local-network-access/shared-worker-fetch.window.js rename to test/wpt/tests/fetch/private-network-access/shared-worker-fetch.tentative.window.js diff --git a/test/wpt/tests/fetch/local-network-access/shared-worker.https.window.js b/test/wpt/tests/fetch/private-network-access/shared-worker.tentative.https.window.js similarity index 100% rename from test/wpt/tests/fetch/local-network-access/shared-worker.https.window.js rename to test/wpt/tests/fetch/private-network-access/shared-worker.tentative.https.window.js diff --git a/test/wpt/tests/fetch/local-network-access/shared-worker.window.js b/test/wpt/tests/fetch/private-network-access/shared-worker.tentative.window.js similarity index 100% rename from test/wpt/tests/fetch/local-network-access/shared-worker.window.js rename to test/wpt/tests/fetch/private-network-access/shared-worker.tentative.window.js diff --git a/test/wpt/tests/fetch/local-network-access/websocket.https.window.js b/test/wpt/tests/fetch/private-network-access/websocket.tentative.https.window.js similarity index 100% rename from test/wpt/tests/fetch/local-network-access/websocket.https.window.js rename to test/wpt/tests/fetch/private-network-access/websocket.tentative.https.window.js diff --git a/test/wpt/tests/fetch/local-network-access/websocket.window.js b/test/wpt/tests/fetch/private-network-access/websocket.tentative.window.js similarity index 100% rename from test/wpt/tests/fetch/local-network-access/websocket.window.js rename to test/wpt/tests/fetch/private-network-access/websocket.tentative.window.js diff --git a/test/wpt/tests/fetch/local-network-access/worker-blob-fetch.window.js b/test/wpt/tests/fetch/private-network-access/worker-blob-fetch.tentative.window.js similarity index 100% rename from test/wpt/tests/fetch/local-network-access/worker-blob-fetch.window.js rename to test/wpt/tests/fetch/private-network-access/worker-blob-fetch.tentative.window.js diff --git a/test/wpt/tests/fetch/local-network-access/worker-fetch.https.window.js b/test/wpt/tests/fetch/private-network-access/worker-fetch.tentative.https.window.js similarity index 100% rename from test/wpt/tests/fetch/local-network-access/worker-fetch.https.window.js rename to test/wpt/tests/fetch/private-network-access/worker-fetch.tentative.https.window.js diff --git a/test/wpt/tests/fetch/local-network-access/worker-fetch.window.js b/test/wpt/tests/fetch/private-network-access/worker-fetch.tentative.window.js similarity index 100% rename from test/wpt/tests/fetch/local-network-access/worker-fetch.window.js rename to test/wpt/tests/fetch/private-network-access/worker-fetch.tentative.window.js diff --git a/test/wpt/tests/fetch/local-network-access/worker.https.window.js b/test/wpt/tests/fetch/private-network-access/worker.tentative.https.window.js similarity index 100% rename from test/wpt/tests/fetch/local-network-access/worker.https.window.js rename to test/wpt/tests/fetch/private-network-access/worker.tentative.https.window.js diff --git a/test/wpt/tests/fetch/local-network-access/worker.window.js b/test/wpt/tests/fetch/private-network-access/worker.tentative.window.js similarity index 100% rename from test/wpt/tests/fetch/local-network-access/worker.window.js rename to test/wpt/tests/fetch/private-network-access/worker.tentative.window.js diff --git a/test/wpt/tests/fetch/local-network-access/xhr-from-treat-as-public.https.window.js b/test/wpt/tests/fetch/private-network-access/xhr-from-treat-as-public.tentative.https.window.js similarity index 100% rename from test/wpt/tests/fetch/local-network-access/xhr-from-treat-as-public.https.window.js rename to test/wpt/tests/fetch/private-network-access/xhr-from-treat-as-public.tentative.https.window.js diff --git a/test/wpt/tests/fetch/local-network-access/xhr.https.window.js b/test/wpt/tests/fetch/private-network-access/xhr.https.tentative.window.js similarity index 100% rename from test/wpt/tests/fetch/local-network-access/xhr.https.window.js rename to test/wpt/tests/fetch/private-network-access/xhr.https.tentative.window.js diff --git a/test/wpt/tests/fetch/local-network-access/xhr.window.js b/test/wpt/tests/fetch/private-network-access/xhr.tentative.window.js similarity index 100% rename from test/wpt/tests/fetch/local-network-access/xhr.window.js rename to test/wpt/tests/fetch/private-network-access/xhr.tentative.window.js diff --git a/test/wpt/tests/fetch/range/blob.any.js b/test/wpt/tests/fetch/range/blob.any.js index 1db3b248f6c..7bcd4b9d11f 100644 --- a/test/wpt/tests/fetch/range/blob.any.js +++ b/test/wpt/tests/fetch/range/blob.any.js @@ -10,6 +10,15 @@ const supportedBlobRange = [ content_range: "bytes 9-21/30", result: "Hello, World!", }, + { + name: "A blob range request with no type.", + data: ["A simple Hello, World! example"], + type: undefined, + range: "bytes=9-21", + content_length: 13, + content_range: "bytes 9-21/30", + result: "Hello, World!", + }, { name: "A blob range request with no end.", data: ["Range with no end"], @@ -201,7 +210,7 @@ supportedBlobRange.forEach(({ name, data, type, range, content_length, content_r }); assert_equals(resp.status, 206, "HTTP status is 206"); assert_equals(resp.type, "basic", "response type is basic"); - assert_equals(resp.headers.get("Content-Type"), type, "Content-Type is " + resp.headers.get("Content-Type")); + assert_equals(resp.headers.get("Content-Type"), type || "", "Content-Type is " + resp.headers.get("Content-Type")); assert_equals(resp.headers.get("Content-Length"), content_length.toString(), "Content-Length is " + resp.headers.get("Content-Length")); assert_equals(resp.headers.get("Content-Range"), content_range, "Content-Range is " + resp.headers.get("Content-Range")); const text = await resp.text(); diff --git a/test/wpt/tests/interfaces/attribution-reporting-api.idl b/test/wpt/tests/interfaces/attribution-reporting-api.idl index 76640f54c8d..ed4497b56ff 100644 --- a/test/wpt/tests/interfaces/attribution-reporting-api.idl +++ b/test/wpt/tests/interfaces/attribution-reporting-api.idl @@ -4,9 +4,23 @@ // Source: Attribution Reporting (https://wicg.github.io/attribution-reporting-api/) interface mixin HTMLAttributionSrcElementUtils { - [CEReactions] attribute USVString attributionSrc; + [CEReactions, SecureContext] attribute USVString attributionSrc; }; HTMLAnchorElement includes HTMLAttributionSrcElementUtils; HTMLImageElement includes HTMLAttributionSrcElementUtils; HTMLScriptElement includes HTMLAttributionSrcElementUtils; + +dictionary AttributionReportingRequestOptions { + required boolean eventSourceEligible; + required boolean triggerEligible; +}; + +partial dictionary RequestInit { + AttributionReportingRequestOptions attributionReporting; +}; + +partial interface XMLHttpRequest { + [SecureContext] + undefined setAttributionReporting(AttributionReportingRequestOptions options); +}; diff --git a/test/wpt/tests/interfaces/captured-mouse-events.tentative.idl b/test/wpt/tests/interfaces/captured-mouse-events.tentative.idl new file mode 100644 index 00000000000..7b081cd9fd8 --- /dev/null +++ b/test/wpt/tests/interfaces/captured-mouse-events.tentative.idl @@ -0,0 +1,25 @@ +// https://screen-share.github.io/mouse-events/ + +enum CaptureStartFocusBehavior { + "focus-captured-surface", + "no-focus-change" +}; + +[Exposed=Window, SecureContext] +interface CaptureController : EventTarget { + constructor(); + undefined setFocusBehavior(CaptureStartFocusBehavior focusBehavior); + attribute EventHandler oncapturedmousechange; +}; + +[Exposed=Window] +interface CapturedMouseEvent : Event { + constructor(DOMString type, optional CapturedMouseEventInit eventInitDict = {}); + readonly attribute long surfaceX; + readonly attribute long surfaceY; +}; + +dictionary CapturedMouseEventInit : EventInit { + long surfaceX = -1; + long surfaceY = -1; +}; diff --git a/test/wpt/tests/interfaces/css-anchor-position.idl b/test/wpt/tests/interfaces/css-anchor-position.idl new file mode 100644 index 00000000000..c5da3f43f72 --- /dev/null +++ b/test/wpt/tests/interfaces/css-anchor-position.idl @@ -0,0 +1,11 @@ +// Source: CSS Anchor Positioning (https://drafts.csswg.org/css-anchor-position-1/) + +[Exposed=Window] +interface CSSPositionFallbackRule : CSSGroupingRule { + readonly attribute CSSOMString name; +}; + +[Exposed=Window] +interface CSSTryRule : CSSRule { + [SameObject, PutForwards=cssText] readonly attribute CSSStyleDeclaration style; +}; diff --git a/test/wpt/tests/interfaces/css-cascade-6.idl b/test/wpt/tests/interfaces/css-cascade-6.idl index 37cdfb82930..3bdf6ba3a6b 100644 --- a/test/wpt/tests/interfaces/css-cascade-6.idl +++ b/test/wpt/tests/interfaces/css-cascade-6.idl @@ -5,6 +5,6 @@ [Exposed=Window] interface CSSScopeRule : CSSGroupingRule { - readonly attribute CSSOMString start; - readonly attribute CSSOMString end; + readonly attribute CSSOMString? start; + readonly attribute CSSOMString? end; }; diff --git a/test/wpt/tests/interfaces/css-cascade.idl b/test/wpt/tests/interfaces/css-cascade.idl index 9011dc7fd9e..0dd9969f6eb 100644 --- a/test/wpt/tests/interfaces/css-cascade.idl +++ b/test/wpt/tests/interfaces/css-cascade.idl @@ -3,10 +3,6 @@ // (https://github.com/w3c/webref) // Source: CSS Cascading and Inheritance Level 5 (https://drafts.csswg.org/css-cascade-5/) -partial interface CSSImportRule { - readonly attribute CSSOMString? layerName; -}; - [Exposed=Window] interface CSSLayerBlockRule : CSSGroupingRule { readonly attribute CSSOMString name; diff --git a/test/wpt/tests/interfaces/cssom.idl b/test/wpt/tests/interfaces/cssom.idl index 222b3dc09ec..0574f1a771c 100644 --- a/test/wpt/tests/interfaces/cssom.idl +++ b/test/wpt/tests/interfaces/cssom.idl @@ -96,17 +96,13 @@ interface CSSRule { const unsigned short NAMESPACE_RULE = 10; }; -[Exposed=Window] -interface CSSStyleRule : CSSRule { - attribute CSSOMString selectorText; - [SameObject, PutForwards=cssText] readonly attribute CSSStyleDeclaration style; -}; - [Exposed=Window] interface CSSImportRule : CSSRule { readonly attribute USVString href; [SameObject, PutForwards=mediaText] readonly attribute MediaList media; - [SameObject] readonly attribute CSSStyleSheet styleSheet; + [SameObject] readonly attribute CSSStyleSheet? styleSheet; + readonly attribute CSSOMString? layerName; + readonly attribute CSSOMString? supportsText; }; [Exposed=Window] @@ -116,6 +112,12 @@ interface CSSGroupingRule : CSSRule { undefined deleteRule(unsigned long index); }; +[Exposed=Window] +interface CSSStyleRule : CSSGroupingRule { + attribute CSSOMString selectorText; + [SameObject, PutForwards=cssText] readonly attribute CSSStyleDeclaration style; +}; + [Exposed=Window] interface CSSPageRule : CSSGroupingRule { attribute CSSOMString selectorText; diff --git a/test/wpt/tests/interfaces/document-picture-in-picture.idl b/test/wpt/tests/interfaces/document-picture-in-picture.idl new file mode 100644 index 00000000000..742f65e6f06 --- /dev/null +++ b/test/wpt/tests/interfaces/document-picture-in-picture.idl @@ -0,0 +1,34 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: Document Picture-in-Picture Specification (https://wicg.github.io/document-picture-in-picture/) + +[Exposed=Window] +partial interface Window { + [SameObject, SecureContext] readonly attribute DocumentPictureInPicture + documentPictureInPicture; +}; + +[Exposed=Window, SecureContext] +interface DocumentPictureInPicture : EventTarget { + [NewObject] Promise requestWindow( + optional DocumentPictureInPictureOptions options = {}); + readonly attribute Window window; + attribute EventHandler onenter; +}; + +dictionary DocumentPictureInPictureOptions { + [EnforceRange] unsigned long long width = 0; + [EnforceRange] unsigned long long height = 0; + boolean copyStyleSheets = false; +}; + +[Exposed=Window] +interface DocumentPictureInPictureEvent : Event { + constructor(DOMString type, DocumentPictureInPictureEventInit eventInitDict); + [SameObject] readonly attribute Window window; +}; + +dictionary DocumentPictureInPictureEventInit : EventInit { + required Window window; +}; diff --git a/test/wpt/tests/interfaces/dom.idl b/test/wpt/tests/interfaces/dom.idl index c5b5c94dbcc..c2def872fa2 100644 --- a/test/wpt/tests/interfaces/dom.idl +++ b/test/wpt/tests/interfaces/dom.idl @@ -95,6 +95,7 @@ interface AbortController { interface AbortSignal : EventTarget { [NewObject] static AbortSignal abort(optional any reason); [Exposed=(Window,Worker), NewObject] static AbortSignal timeout([EnforceRange] unsigned long long milliseconds); + [NewObject] static AbortSignal _any(sequence signals); readonly attribute boolean aborted; readonly attribute any reason; diff --git a/test/wpt/tests/interfaces/fenced-frame.idl b/test/wpt/tests/interfaces/fenced-frame.idl index 2869b95e6bb..440ec2bbaa1 100644 --- a/test/wpt/tests/interfaces/fenced-frame.idl +++ b/test/wpt/tests/interfaces/fenced-frame.idl @@ -1,7 +1,7 @@ // GENERATED CONTENT - DO NOT EDIT // Content was automatically extracted by Reffy into webref // (https://github.com/w3c/webref) -// Source: Fenced frame (https://wicg.github.io/fenced-frame/) +// Source: Fenced Frame (https://wicg.github.io/fenced-frame/) [Exposed=Window] interface HTMLFencedFrameElement : HTMLElement { @@ -10,19 +10,22 @@ interface HTMLFencedFrameElement : HTMLElement { [CEReactions] attribute FencedFrameConfig? config; [CEReactions] attribute DOMString width; [CEReactions] attribute DOMString height; + [CEReactions] attribute DOMString allow; }; enum OpaqueProperty {"opaque"}; typedef (unsigned long or OpaqueProperty) FencedFrameConfigSize; -typedef (USVString or OpaqueProperty) FencedFrameConfigURL; +typedef USVString FencedFrameConfigURL; [Exposed=Window] interface FencedFrameConfig { - constructor(USVString url); - readonly attribute FencedFrameConfigURL? url; - readonly attribute FencedFrameConfigSize? width; - readonly attribute FencedFrameConfigSize? height; + readonly attribute FencedFrameConfigSize? containerWidth; + readonly attribute FencedFrameConfigSize? containerHeight; + readonly attribute FencedFrameConfigSize? contentWidth; + readonly attribute FencedFrameConfigSize? contentHeight; + + undefined setSharedStorageContext(DOMString contextString); }; enum FenceReportingDestination { diff --git a/test/wpt/tests/interfaces/fs.idl b/test/wpt/tests/interfaces/fs.idl index e2474132abf..e341ab387d9 100644 --- a/test/wpt/tests/interfaces/fs.idl +++ b/test/wpt/tests/interfaces/fs.idl @@ -15,6 +15,7 @@ interface FileSystemHandle { Promise isSameEntry(FileSystemHandle other); }; + dictionary FileSystemCreateWritableOptions { boolean keepExistingData = false; }; @@ -26,6 +27,7 @@ interface FileSystemFileHandle : FileSystemHandle { [Exposed=DedicatedWorker] Promise createSyncAccessHandle(); }; + dictionary FileSystemGetFileOptions { boolean create = false; }; @@ -49,6 +51,7 @@ interface FileSystemDirectoryHandle : FileSystemHandle { Promise?> resolve(FileSystemHandle possibleDescendant); }; + enum WriteCommandType { "write", "seek", @@ -70,6 +73,7 @@ interface FileSystemWritableFileStream : WritableStream { Promise seek(unsigned long long position); Promise truncate(unsigned long long size); }; + dictionary FileSystemReadWriteOptions { [EnforceRange] unsigned long long at; }; diff --git a/test/wpt/tests/interfaces/html.idl b/test/wpt/tests/interfaces/html.idl index 33d4de0db97..99b33705b39 100644 --- a/test/wpt/tests/interfaces/html.idl +++ b/test/wpt/tests/interfaces/html.idl @@ -1643,6 +1643,14 @@ dictionary ValidityStateFlags { boolean customError = false; }; +[Exposed=(Window)] +interface VisibilityStateEntry : PerformanceEntry { + readonly attribute DOMString name; // shadows inherited name + readonly attribute DOMString entryType; // shadows inherited entryType + readonly attribute DOMHighResTimeStamp startTime; // shadows inherited startTime + readonly attribute unsigned long duration; // shadows inherited duration +}; + [Exposed=Window] interface UserActivation { readonly attribute boolean hasBeenActive; diff --git a/test/wpt/tests/interfaces/mediastream-recording.idl b/test/wpt/tests/interfaces/mediastream-recording.idl index 99f30282333..496bfcf2e27 100644 --- a/test/wpt/tests/interfaces/mediastream-recording.idl +++ b/test/wpt/tests/interfaces/mediastream-recording.idl @@ -34,6 +34,8 @@ dictionary MediaRecorderOptions { unsigned long videoBitsPerSecond; unsigned long bitsPerSecond; BitrateMode audioBitrateMode = "variable"; + DOMHighResTimeStamp videoKeyFrameIntervalDuration; + unsigned long videoKeyFrameIntervalCount; }; enum BitrateMode { diff --git a/test/wpt/tests/interfaces/notifications.idl b/test/wpt/tests/interfaces/notifications.idl index bfcfa2e66af..4300b171071 100644 --- a/test/wpt/tests/interfaces/notifications.idl +++ b/test/wpt/tests/interfaces/notifications.idl @@ -28,7 +28,7 @@ interface Notification : EventTarget { [SameObject] readonly attribute FrozenArray vibrate; readonly attribute EpochTimeStamp timestamp; readonly attribute boolean renotify; - readonly attribute boolean silent; + readonly attribute boolean? silent; readonly attribute boolean requireInteraction; [SameObject] readonly attribute any data; [SameObject] readonly attribute FrozenArray actions; @@ -47,7 +47,7 @@ dictionary NotificationOptions { VibratePattern vibrate; EpochTimeStamp timestamp; boolean renotify = false; - boolean silent = false; + boolean? silent = null; boolean requireInteraction = false; any data = null; sequence actions = []; @@ -72,6 +72,7 @@ dictionary NotificationAction { }; callback NotificationPermissionCallback = undefined (NotificationPermission permission); + dictionary GetNotificationOptions { DOMString tag = ""; }; diff --git a/test/wpt/tests/interfaces/permissions-policy.idl b/test/wpt/tests/interfaces/permissions-policy.idl index a789d41738c..16945e3a9b7 100644 --- a/test/wpt/tests/interfaces/permissions-policy.idl +++ b/test/wpt/tests/interfaces/permissions-policy.idl @@ -18,6 +18,7 @@ partial interface Document { partial interface HTMLIFrameElement { [SameObject] readonly attribute PermissionsPolicy permissionsPolicy; }; + [Exposed=Window] interface PermissionsPolicyViolationReportBody : ReportBody { readonly attribute DOMString featureId; diff --git a/test/wpt/tests/interfaces/real-world-meshing.idl b/test/wpt/tests/interfaces/real-world-meshing.idl new file mode 100644 index 00000000000..38fe71f6c66 --- /dev/null +++ b/test/wpt/tests/interfaces/real-world-meshing.idl @@ -0,0 +1,21 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: WebXR Mesh Detection Module (https://immersive-web.github.io/real-world-meshing/) + +[Exposed=Window] interface XRMesh { + [SameObject] readonly attribute XRSpace meshSpace; + + readonly attribute FrozenArray vertices; + readonly attribute Uint32Array indices; + readonly attribute DOMHighResTimeStamp lastChangedTime; + readonly attribute DOMString? semanticLabel; +}; + +[Exposed=Window] interface XRMeshSet { + readonly setlike; +}; + +partial interface XRFrame { + readonly attribute XRMeshSet detectedMeshs; +}; diff --git a/test/wpt/tests/interfaces/resource-timing.idl b/test/wpt/tests/interfaces/resource-timing.idl index 151e5d46d84..33fed05b756 100644 --- a/test/wpt/tests/interfaces/resource-timing.idl +++ b/test/wpt/tests/interfaces/resource-timing.idl @@ -18,6 +18,7 @@ interface PerformanceResourceTiming : PerformanceEntry { readonly attribute DOMHighResTimeStamp connectEnd; readonly attribute DOMHighResTimeStamp secureConnectionStart; readonly attribute DOMHighResTimeStamp requestStart; + readonly attribute DOMHighResTimeStamp firstInterimResponseStart; readonly attribute DOMHighResTimeStamp responseStart; readonly attribute DOMHighResTimeStamp responseEnd; readonly attribute unsigned long long transferSize; @@ -25,6 +26,7 @@ interface PerformanceResourceTiming : PerformanceEntry { readonly attribute unsigned long long decodedBodySize; readonly attribute unsigned short responseStatus; readonly attribute RenderBlockingStatusType renderBlockingStatus; + readonly attribute DOMString contentType; [Default] object toJSON(); }; diff --git a/test/wpt/tests/interfaces/scheduling-apis.idl b/test/wpt/tests/interfaces/scheduling-apis.idl index 9ed49cfb689..1e84e79cd15 100644 --- a/test/wpt/tests/interfaces/scheduling-apis.idl +++ b/test/wpt/tests/interfaces/scheduling-apis.idl @@ -45,8 +45,14 @@ interface TaskController : AbortController { undefined setPriority(TaskPriority priority); }; +dictionary TaskSignalAnyInit { + (TaskPriority or TaskSignal) priority = "user-visible"; +}; + [Exposed=(Window, Worker)] interface TaskSignal : AbortSignal { + [NewObject] static TaskSignal _any(sequence signals, optional TaskSignalAnyInit init = {}); + readonly attribute TaskPriority priority; attribute EventHandler onprioritychange; diff --git a/test/wpt/tests/interfaces/screen-capture.idl b/test/wpt/tests/interfaces/screen-capture.idl index 9abd4d2c1ad..830b96d16fa 100644 --- a/test/wpt/tests/interfaces/screen-capture.idl +++ b/test/wpt/tests/interfaces/screen-capture.idl @@ -13,7 +13,7 @@ enum CaptureStartFocusBehavior { }; [Exposed=Window, SecureContext] -interface CaptureController { +interface CaptureController : EventTarget { constructor(); undefined setFocusBehavior(CaptureStartFocusBehavior focusBehavior); }; diff --git a/test/wpt/tests/interfaces/scroll-animations.idl b/test/wpt/tests/interfaces/scroll-animations.idl index 14215509c9f..31b3746e9d4 100644 --- a/test/wpt/tests/interfaces/scroll-animations.idl +++ b/test/wpt/tests/interfaces/scroll-animations.idl @@ -36,7 +36,11 @@ interface ViewTimeline : ScrollTimeline { readonly attribute CSSNumericValue endOffset; }; +dictionary AnimationTimeOptions { + DOMString? range; +}; + [Exposed=Window] partial interface AnimationTimeline { - CSSNumericValue? getCurrentTime(optional CSSOMString rangeName); + CSSNumericValue? getCurrentTime(optional AnimationTimeOptions options = {}); }; diff --git a/test/wpt/tests/interfaces/secure-payment-confirmation.idl b/test/wpt/tests/interfaces/secure-payment-confirmation.idl index 9061b243477..08ec8065c53 100644 --- a/test/wpt/tests/interfaces/secure-payment-confirmation.idl +++ b/test/wpt/tests/interfaces/secure-payment-confirmation.idl @@ -15,6 +15,7 @@ dictionary SecurePaymentConfirmationRequest { sequence locale; boolean showOptOut; }; + partial dictionary AuthenticationExtensionsClientInputs { AuthenticationExtensionsPaymentInputs payment; }; @@ -30,9 +31,11 @@ dictionary AuthenticationExtensionsPaymentInputs { PaymentCurrencyAmount total; PaymentCredentialInstrument instrument; }; + dictionary CollectedClientPaymentData : CollectedClientData { required CollectedClientAdditionalPaymentData payment; }; + dictionary CollectedClientAdditionalPaymentData { required USVString rpId; required USVString topOrigin; @@ -41,6 +44,7 @@ dictionary CollectedClientAdditionalPaymentData { required PaymentCurrencyAmount total; required PaymentCredentialInstrument instrument; }; + dictionary PaymentCredentialInstrument { required USVString displayName; required USVString icon; diff --git a/test/wpt/tests/interfaces/shared-storage.idl b/test/wpt/tests/interfaces/shared-storage.idl new file mode 100644 index 00000000000..eb5806f9a6d --- /dev/null +++ b/test/wpt/tests/interfaces/shared-storage.idl @@ -0,0 +1,80 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: Shared Storage API (https://wicg.github.io/shared-storage/) + +[Exposed=(Window)] +interface SharedStorageWorklet : Worklet { +}; + +[Exposed=SharedStorageWorklet, Global=SharedStorageWorklet] +interface SharedStorageWorkletGlobalScope : WorkletGlobalScope { + undefined register(DOMString name, + SharedStorageOperationConstructor operationCtor); +}; + +callback SharedStorageOperationConstructor = + SharedStorageOperation(optional SharedStorageRunOperationMethodOptions options); + +[Exposed=SharedStorageWorklet] +interface SharedStorageOperation { +}; + +dictionary SharedStorageRunOperationMethodOptions { + object data; + boolean resolveToConfig = false; + boolean keepAlive = false; +}; + +[Exposed=SharedStorageWorklet] +interface SharedStorageRunOperation : SharedStorageOperation { + Promise run(object data); +}; + +[Exposed=SharedStorageWorklet] +interface SharedStorageSelectURLOperation : SharedStorageOperation { + Promise run(object data, + FrozenArray urls); +}; + +[Exposed=(Window,SharedStorageWorklet)] +interface SharedStorage { + Promise set(DOMString key, + DOMString value, + optional SharedStorageSetMethodOptions options = {}); + Promise append(DOMString key, + DOMString value); + Promise delete(DOMString key); + Promise clear(); +}; + +dictionary SharedStorageSetMethodOptions { + boolean ignoreIfPresent = false; +}; + +typedef (USVString or FencedFrameConfig) SharedStorageResponse; + +[Exposed=(Window)] +interface WindowSharedStorage : SharedStorage { + Promise run(DOMString name, + optional SharedStorageRunOperationMethodOptions options = {}); + Promise selectURL(DOMString name, + FrozenArray urls, + optional SharedStorageRunOperationMethodOptions options = {}); + + readonly attribute SharedStorageWorklet worklet; +}; + +dictionary SharedStorageUrlWithMetadata { + required USVString url; + object reportingMetadata; +}; + +[Exposed=(SharedStorageWorklet)] +interface WorkletSharedStorage : SharedStorage { + Promise get(DOMString key); + Promise length(); + Promise remainingBudget(); + + async iterable; +}; diff --git a/test/wpt/tests/interfaces/storage-buckets.idl b/test/wpt/tests/interfaces/storage-buckets.idl new file mode 100644 index 00000000000..f3d500a5711 --- /dev/null +++ b/test/wpt/tests/interfaces/storage-buckets.idl @@ -0,0 +1,53 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: Storage Buckets API (https://wicg.github.io/storage-buckets/) + +[SecureContext] +interface mixin NavigatorStorageBuckets { + [SameObject] readonly attribute StorageBucketManager storageBuckets; +}; +Navigator includes NavigatorStorageBuckets; +WorkerNavigator includes NavigatorStorageBuckets; + +[Exposed=(Window,Worker), + SecureContext] +interface StorageBucketManager { + Promise open(DOMString name, optional StorageBucketOptions options = {}); + Promise> keys(); + Promise delete(DOMString name); +}; + +enum StorageBucketDurability { + "strict", + "relaxed" +}; + +dictionary StorageBucketOptions { + boolean? persisted = null; + StorageBucketDurability? durability = null; + unsigned long long? quota = null; + DOMHighResTimeStamp? expires = null; +}; + +[Exposed=(Window,Worker), + SecureContext] +interface StorageBucket { + readonly attribute DOMString name; + + [Exposed=Window] Promise persist(); + Promise persisted(); + + Promise estimate(); + + Promise durability(); + + Promise setExpires(DOMHighResTimeStamp expires); + Promise expires(); + + [SameObject] readonly attribute IDBFactory indexedDB; + + [SameObject] readonly attribute CacheStorage caches; + + Promise getDirectory(); +}; diff --git a/test/wpt/tests/interfaces/trust-token-api.idl b/test/wpt/tests/interfaces/trust-token-api.idl index ee339590827..f521acea1f5 100644 --- a/test/wpt/tests/interfaces/trust-token-api.idl +++ b/test/wpt/tests/interfaces/trust-token-api.idl @@ -5,14 +5,11 @@ enum RefreshPolicy { "none", "refresh" }; -enum TokenType { "private-state-token" }; - enum TokenVersion { "1" }; enum OperationType { "token-request", "send-redemption-record", "token-redemption" }; dictionary PrivateToken { - required TokenType type; required TokenVersion version; required OperationType operation; RefreshPolicy refreshPolicy = "none"; @@ -24,6 +21,6 @@ partial dictionary RequestInit { }; partial interface Document { - Promise hasPrivateTokens(USVString issuer, USVString type); - Promise hasRedemptionRecord(USVString issuer, USVString type); + Promise hasPrivateTokens(USVString issuer); + Promise hasRedemptionRecord(USVString issuer); }; diff --git a/test/wpt/tests/interfaces/turtledove.idl b/test/wpt/tests/interfaces/turtledove.idl index cd81a3d87ef..f5867e995a8 100644 --- a/test/wpt/tests/interfaces/turtledove.idl +++ b/test/wpt/tests/interfaces/turtledove.idl @@ -1,7 +1,7 @@ // GENERATED CONTENT - DO NOT EDIT // Content was automatically extracted by Reffy into webref // (https://github.com/w3c/webref) -// Source: FLEDGE (https://wicg.github.io/turtledove/) +// Source: Protected Audience (formerly FLEDGE) (https://wicg.github.io/turtledove/) [SecureContext] partial interface Navigator { @@ -25,7 +25,7 @@ dictionary AuctionAdInterestGroup { DOMString executionMode = "compatibility"; USVString biddingLogicURL; USVString biddingWasmHelperURL; - USVString dailyUpdateURL; + USVString updateURL; USVString trustedBiddingSignalsURL; sequence trustedBiddingSignalsKeys; any userBiddingSignals; @@ -67,9 +67,15 @@ dictionary AuctionAdConfig { AbortSignal? signal; }; +[Exposed=InterestGroupScriptRunnerGlobalScope] +interface InterestGroupScriptRunnerGlobalScope { +}; + [Exposed=InterestGroupBiddingScriptRunnerGlobalScope, -Global=InterestGroupBiddingScriptRunnerGlobalScope] -interface InterestGroupBiddingScriptRunnerGlobalScope { + Global=(InterestGroupScriptRunnerGlobalScope, + InterestGroupBiddingScriptRunnerGlobalScope)] +interface InterestGroupBiddingScriptRunnerGlobalScope + : InterestGroupScriptRunnerGlobalScope { boolean setBid(); boolean setBid(GenerateBidOutput generateBidOutput); undefined setPriority(double priority); @@ -77,14 +83,19 @@ interface InterestGroupBiddingScriptRunnerGlobalScope { }; [Exposed=InterestGroupScoringScriptRunnerGlobalScope, -Global=InterestGroupScoringScriptRunnerGlobalScope] -interface InterestGroupScoringScriptRunnerGlobalScope { + Global=(InterestGroupScriptRunnerGlobalScope, + InterestGroupScoringScriptRunnerGlobalScope)] +interface InterestGroupScoringScriptRunnerGlobalScope + : InterestGroupScriptRunnerGlobalScope { }; [Exposed=InterestGroupReportingScriptRunnerGlobalScope, -Global=InterestGroupReportingScriptRunnerGlobalScope] -interface InterestGroupReportingScriptRunnerGlobalScope { + Global=(InterestGroupScriptRunnerGlobalScope, + InterestGroupReportingScriptRunnerGlobalScope)] +interface InterestGroupReportingScriptRunnerGlobalScope + : InterestGroupScriptRunnerGlobalScope { undefined sendReportTo(DOMString url); + undefined registerAdBeacon(record map); }; dictionary AdRender { @@ -96,7 +107,7 @@ dictionary AdRender { dictionary GenerateBidOutput { required double bid; required (DOMString or AdRender) adRender; - DOMString ad; + any ad; sequence<(DOMString or AdRender)> adComponents; double adCost; double modelingSignals; diff --git a/test/wpt/tests/interfaces/url.idl b/test/wpt/tests/interfaces/url.idl index 6549e45f419..a5e4d1eb492 100644 --- a/test/wpt/tests/interfaces/url.idl +++ b/test/wpt/tests/interfaces/url.idl @@ -33,10 +33,10 @@ interface URLSearchParams { readonly attribute unsigned long size; undefined append(USVString name, USVString value); - undefined delete(USVString name); + undefined delete(USVString name, optional USVString value); USVString? get(USVString name); sequence getAll(USVString name); - boolean has(USVString name); + boolean has(USVString name, optional USVString value); undefined set(USVString name, USVString value); undefined sort(); diff --git a/test/wpt/tests/interfaces/webauthn.idl b/test/wpt/tests/interfaces/webauthn.idl index 58a9e285232..9a37207ba2c 100644 --- a/test/wpt/tests/interfaces/webauthn.idl +++ b/test/wpt/tests/interfaces/webauthn.idl @@ -12,51 +12,56 @@ interface PublicKeyCredential : Credential { static Promise isConditionalMediationAvailable(); PublicKeyCredentialJSON toJSON(); }; + typedef DOMString Base64URLString; typedef (RegistrationResponseJSON or AuthenticationResponseJSON) PublicKeyCredentialJSON; dictionary RegistrationResponseJSON { - Base64URLString id; - Base64URLString rawId; - AuthenticatorAttestationResponseJSON response; - DOMString? authenticatorAttachment; - AuthenticationExtensionsClientOutputsJSON clientExtensionResults; - DOMString type; + required Base64URLString id; + required Base64URLString rawId; + required AuthenticatorAttestationResponseJSON response; + DOMString authenticatorAttachment; + required AuthenticationExtensionsClientOutputsJSON clientExtensionResults; + required DOMString type; }; dictionary AuthenticatorAttestationResponseJSON { - Base64URLString clientDataJSON; - Base64URLString attestationObject; - sequence transports; + required Base64URLString clientDataJSON; + required Base64URLString attestationObject; + required sequence transports; }; dictionary AuthenticationResponseJSON { - Base64URLString id; - Base64URLString rawId; - AuthenticatorAssertionResponseJSON response; - DOMString? authenticatorAttachment; - AuthenticationExtensionsClientOutputsJSON clientExtensionResults; - DOMString type; + required Base64URLString id; + required Base64URLString rawId; + required AuthenticatorAssertionResponseJSON response; + DOMString authenticatorAttachment; + required AuthenticationExtensionsClientOutputsJSON clientExtensionResults; + required DOMString type; }; dictionary AuthenticatorAssertionResponseJSON { - Base64URLString clientDataJSON; - Base64URLString authenticatorData; - Base64URLString signature; - Base64URLString? userHandle; + required Base64URLString clientDataJSON; + required Base64URLString authenticatorData; + required Base64URLString signature; + Base64URLString userHandle; }; dictionary AuthenticationExtensionsClientOutputsJSON { }; + partial dictionary CredentialCreationOptions { PublicKeyCredentialCreationOptions publicKey; }; + partial dictionary CredentialRequestOptions { PublicKeyCredentialRequestOptions publicKey; }; + partial interface PublicKeyCredential { static Promise isUserVerifyingPlatformAuthenticatorAvailable(); }; + partial interface PublicKeyCredential { static PublicKeyCredentialCreationOptions parseCreationOptionsFromJSON(PublicKeyCredentialCreationOptionsJSON options); }; @@ -87,6 +92,7 @@ dictionary PublicKeyCredentialDescriptorJSON { dictionary AuthenticationExtensionsClientInputsJSON { }; + partial interface PublicKeyCredential { static PublicKeyCredentialRequestOptions parseRequestOptionsFromJSON(PublicKeyCredentialRequestOptionsJSON options); }; @@ -99,10 +105,12 @@ dictionary PublicKeyCredentialRequestOptionsJSON { DOMString userVerification = "preferred"; AuthenticationExtensionsClientInputsJSON extensions; }; + [SecureContext, Exposed=Window] interface AuthenticatorResponse { [SameObject] readonly attribute ArrayBuffer clientDataJSON; }; + [SecureContext, Exposed=Window] interface AuthenticatorAttestationResponse : AuthenticatorResponse { [SameObject] readonly attribute ArrayBuffer attestationObject; @@ -111,6 +119,7 @@ interface AuthenticatorAttestationResponse : AuthenticatorResponse { ArrayBuffer? getPublicKey(); COSEAlgorithmIdentifier getPublicKeyAlgorithm(); }; + [SecureContext, Exposed=Window] interface AuthenticatorAssertionResponse : AuthenticatorResponse { [SameObject] readonly attribute ArrayBuffer authenticatorData; @@ -118,10 +127,12 @@ interface AuthenticatorAssertionResponse : AuthenticatorResponse { [SameObject] readonly attribute ArrayBuffer? userHandle; [SameObject] readonly attribute ArrayBuffer? attestationObject; }; + dictionary PublicKeyCredentialParameters { required DOMString type; required COSEAlgorithmIdentifier alg; }; + dictionary PublicKeyCredentialCreationOptions { required PublicKeyCredentialRpEntity rp; required PublicKeyCredentialUserEntity user; @@ -136,37 +147,45 @@ dictionary PublicKeyCredentialCreationOptions { sequence attestationFormats = []; AuthenticationExtensionsClientInputs extensions; }; + dictionary PublicKeyCredentialEntity { required DOMString name; }; + dictionary PublicKeyCredentialRpEntity : PublicKeyCredentialEntity { DOMString id; }; + dictionary PublicKeyCredentialUserEntity : PublicKeyCredentialEntity { required BufferSource id; required DOMString displayName; }; + dictionary AuthenticatorSelectionCriteria { DOMString authenticatorAttachment; DOMString residentKey; boolean requireResidentKey = false; DOMString userVerification = "preferred"; }; + enum AuthenticatorAttachment { "platform", "cross-platform" }; + enum ResidentKeyRequirement { "discouraged", "preferred", "required" }; + enum AttestationConveyancePreference { "none", "indirect", "direct", "enterprise" }; + dictionary PublicKeyCredentialRequestOptions { required BufferSource challenge; unsigned long timeout; @@ -177,10 +196,13 @@ dictionary PublicKeyCredentialRequestOptions { sequence attestationFormats = []; AuthenticationExtensionsClientInputs extensions; }; + dictionary AuthenticationExtensionsClientInputs { }; + dictionary AuthenticationExtensionsClientOutputs { }; + dictionary CollectedClientData { required DOMString type; required DOMString challenge; @@ -195,42 +217,54 @@ dictionary TokenBinding { }; enum TokenBindingStatus { "present", "supported" }; + enum PublicKeyCredentialType { "public-key" }; + dictionary PublicKeyCredentialDescriptor { required DOMString type; required BufferSource id; sequence transports; }; + enum AuthenticatorTransport { "usb", "nfc", "ble", + "smart-card", "hybrid", "internal" }; + typedef long COSEAlgorithmIdentifier; + enum UserVerificationRequirement { "required", "preferred", "discouraged" }; + partial dictionary AuthenticationExtensionsClientInputs { USVString appid; }; + partial dictionary AuthenticationExtensionsClientOutputs { boolean appid; }; + partial dictionary AuthenticationExtensionsClientInputs { USVString appidExclude; }; + partial dictionary AuthenticationExtensionsClientOutputs { boolean appidExclude; }; + partial dictionary AuthenticationExtensionsClientInputs { boolean credProps; }; + dictionary CredentialPropertiesOutput { boolean rk; }; @@ -238,6 +272,7 @@ dictionary CredentialPropertiesOutput { partial dictionary AuthenticationExtensionsClientOutputs { CredentialPropertiesOutput credProps; }; + dictionary AuthenticationExtensionsPRFValues { required BufferSource first; BufferSource second; @@ -289,12 +324,14 @@ dictionary AuthenticationExtensionsLargeBlobOutputs { partial dictionary AuthenticationExtensionsClientInputs { boolean uvm; }; + typedef sequence UvmEntry; typedef sequence UvmEntries; partial dictionary AuthenticationExtensionsClientOutputs { UvmEntries uvm; }; + dictionary AuthenticationExtensionsDevicePublicKeyInputs { DOMString attestation = "none"; sequence attestationFormats = []; diff --git a/test/wpt/tests/interfaces/webcodecs-av1-codec-registration.idl b/test/wpt/tests/interfaces/webcodecs-av1-codec-registration.idl index 00e4493d3c0..ab20879728d 100644 --- a/test/wpt/tests/interfaces/webcodecs-av1-codec-registration.idl +++ b/test/wpt/tests/interfaces/webcodecs-av1-codec-registration.idl @@ -3,6 +3,14 @@ // (https://github.com/w3c/webref) // Source: AV1 WebCodecs Registration (https://w3c.github.io/webcodecs/av1_codec_registration.html) +partial dictionary VideoEncoderConfig { + AV1EncoderConfig av1; +}; + +dictionary AV1EncoderConfig { + boolean forceScreenContentTools = false; +}; + partial dictionary VideoEncoderEncodeOptions { VideoEncoderEncodeOptionsForAv1 av1; }; diff --git a/test/wpt/tests/interfaces/webcodecs-avc-codec-registration.idl b/test/wpt/tests/interfaces/webcodecs-avc-codec-registration.idl index d4074f647da..2b952c22194 100644 --- a/test/wpt/tests/interfaces/webcodecs-avc-codec-registration.idl +++ b/test/wpt/tests/interfaces/webcodecs-avc-codec-registration.idl @@ -15,3 +15,11 @@ enum AvcBitstreamFormat { "annexb", "avc", }; + +partial dictionary VideoEncoderEncodeOptions { + VideoEncoderEncodeOptionsForAvc avc; +}; + +dictionary VideoEncoderEncodeOptionsForAvc { + unsigned short? quantizer; +}; diff --git a/test/wpt/tests/interfaces/webcodecs.idl b/test/wpt/tests/interfaces/webcodecs.idl index 77649029db6..0b95dc8b757 100644 --- a/test/wpt/tests/interfaces/webcodecs.idl +++ b/test/wpt/tests/interfaces/webcodecs.idl @@ -161,6 +161,7 @@ dictionary AudioEncoderConfig { [EnforceRange] unsigned long sampleRate; [EnforceRange] unsigned long numberOfChannels; [EnforceRange] unsigned long long bitrate; + BitrateMode bitrateMode; }; dictionary VideoEncoderConfig { diff --git a/test/wpt/tests/interfaces/webgpu.idl b/test/wpt/tests/interfaces/webgpu.idl index 284327a5789..25943d99c35 100644 --- a/test/wpt/tests/interfaces/webgpu.idl +++ b/test/wpt/tests/interfaces/webgpu.idl @@ -18,6 +18,7 @@ interface GPUSupportedLimits { readonly attribute unsigned long maxTextureDimension3D; readonly attribute unsigned long maxTextureArrayLayers; readonly attribute unsigned long maxBindGroups; + readonly attribute unsigned long maxBindGroupsPlusVertexBuffers; readonly attribute unsigned long maxBindingsPerBindGroup; readonly attribute unsigned long maxDynamicUniformBuffersPerPipelineLayout; readonly attribute unsigned long maxDynamicStorageBuffersPerPipelineLayout; @@ -26,7 +27,6 @@ interface GPUSupportedLimits { readonly attribute unsigned long maxStorageBuffersPerShaderStage; readonly attribute unsigned long maxStorageTexturesPerShaderStage; readonly attribute unsigned long maxUniformBuffersPerShaderStage; - readonly attribute unsigned long maxFragmentCombinedOutputResources; readonly attribute unsigned long long maxUniformBufferBindingSize; readonly attribute unsigned long long maxStorageBufferBindingSize; readonly attribute unsigned long minUniformBufferOffsetAlignment; @@ -85,7 +85,7 @@ dictionary GPURequestAdapterOptions { enum GPUPowerPreference { "low-power", - "high-performance" + "high-performance", }; [Exposed=(Window, DedicatedWorker), SecureContext] @@ -98,7 +98,8 @@ interface GPUAdapter { Promise requestAdapterInfo(optional sequence unmaskHints = []); }; -dictionary GPUDeviceDescriptor : GPUObjectDescriptorBase { +dictionary GPUDeviceDescriptor + : GPUObjectDescriptorBase { sequence requiredFeatures = []; record requiredLimits = {}; GPUQueueDescriptor defaultQueue = {}; @@ -115,7 +116,7 @@ enum GPUFeatureName { "shader-f16", "rg11b10ufloat-renderable", "bgra8unorm-storage", - "float32-filterable" + "float32-filterable", }; [Exposed=(Window, DedicatedWorker), SecureContext] @@ -151,8 +152,8 @@ GPUDevice includes GPUObjectBase; [Exposed=(Window, DedicatedWorker), SecureContext] interface GPUBuffer { - readonly attribute GPUSize64 size; - readonly attribute GPUBufferUsageFlags usage; + readonly attribute GPUSize64Out size; + readonly attribute GPUFlagsConstant usage; readonly attribute GPUBufferMapState mapState; @@ -167,10 +168,11 @@ GPUBuffer includes GPUObjectBase; enum GPUBufferMapState { "unmapped", "pending", - "mapped" + "mapped", }; -dictionary GPUBufferDescriptor : GPUObjectDescriptorBase { +dictionary GPUBufferDescriptor + : GPUObjectDescriptorBase { required GPUSize64 size; required GPUBufferUsageFlags usage; boolean mappedAtCreation = false; @@ -204,18 +206,19 @@ interface GPUTexture { undefined destroy(); - readonly attribute GPUIntegerCoordinate width; - readonly attribute GPUIntegerCoordinate height; - readonly attribute GPUIntegerCoordinate depthOrArrayLayers; - readonly attribute GPUIntegerCoordinate mipLevelCount; - readonly attribute GPUSize32 sampleCount; + readonly attribute GPUIntegerCoordinateOut width; + readonly attribute GPUIntegerCoordinateOut height; + readonly attribute GPUIntegerCoordinateOut depthOrArrayLayers; + readonly attribute GPUIntegerCoordinateOut mipLevelCount; + readonly attribute GPUSize32Out sampleCount; readonly attribute GPUTextureDimension dimension; readonly attribute GPUTextureFormat format; - readonly attribute GPUTextureUsageFlags usage; + readonly attribute GPUFlagsConstant usage; }; GPUTexture includes GPUObjectBase; -dictionary GPUTextureDescriptor : GPUObjectDescriptorBase { +dictionary GPUTextureDescriptor + : GPUObjectDescriptorBase { required GPUExtent3D size; GPUIntegerCoordinate mipLevelCount = 1; GPUSize32 sampleCount = 1; @@ -228,7 +231,7 @@ dictionary GPUTextureDescriptor : GPUObjectDescriptorBase { enum GPUTextureDimension { "1d", "2d", - "3d" + "3d", }; typedef [EnforceRange] unsigned long GPUTextureUsageFlags; @@ -246,7 +249,8 @@ interface GPUTextureView { }; GPUTextureView includes GPUObjectBase; -dictionary GPUTextureViewDescriptor : GPUObjectDescriptorBase { +dictionary GPUTextureViewDescriptor + : GPUObjectDescriptorBase { GPUTextureFormat format; GPUTextureViewDimension dimension; GPUTextureAspect aspect = "all"; @@ -262,13 +266,13 @@ enum GPUTextureViewDimension { "2d-array", "cube", "cube-array", - "3d" + "3d", }; enum GPUTextureAspect { "all", "stencil-only", - "depth-only" + "depth-only", }; enum GPUTextureFormat { @@ -388,7 +392,7 @@ enum GPUTextureFormat { "astc-12x10-unorm", "astc-12x10-unorm-srgb", "astc-12x12-unorm", - "astc-12x12-unorm-srgb" + "astc-12x12-unorm-srgb", }; [Exposed=(Window, DedicatedWorker), SecureContext] @@ -396,8 +400,9 @@ interface GPUExternalTexture { }; GPUExternalTexture includes GPUObjectBase; -dictionary GPUExternalTextureDescriptor : GPUObjectDescriptorBase { - required HTMLVideoElement source; +dictionary GPUExternalTextureDescriptor + : GPUObjectDescriptorBase { + required (HTMLVideoElement or VideoFrame) source; PredefinedColorSpace colorSpace = "srgb"; }; @@ -406,7 +411,8 @@ interface GPUSampler { }; GPUSampler includes GPUObjectBase; -dictionary GPUSamplerDescriptor : GPUObjectDescriptorBase { +dictionary GPUSamplerDescriptor + : GPUObjectDescriptorBase { GPUAddressMode addressModeU = "clamp-to-edge"; GPUAddressMode addressModeV = "clamp-to-edge"; GPUAddressMode addressModeW = "clamp-to-edge"; @@ -422,17 +428,17 @@ dictionary GPUSamplerDescriptor : GPUObjectDescriptorBase { enum GPUAddressMode { "clamp-to-edge", "repeat", - "mirror-repeat" + "mirror-repeat", }; enum GPUFilterMode { "nearest", - "linear" + "linear", }; enum GPUMipmapFilterMode { "nearest", - "linear" + "linear", }; enum GPUCompareFunction { @@ -443,7 +449,7 @@ enum GPUCompareFunction { "greater", "not-equal", "greater-equal", - "always" + "always", }; [Exposed=(Window, DedicatedWorker), SecureContext] @@ -451,7 +457,8 @@ interface GPUBindGroupLayout { }; GPUBindGroupLayout includes GPUObjectBase; -dictionary GPUBindGroupLayoutDescriptor : GPUObjectDescriptorBase { +dictionary GPUBindGroupLayoutDescriptor + : GPUObjectDescriptorBase { required sequence entries; }; @@ -477,7 +484,7 @@ namespace GPUShaderStage { enum GPUBufferBindingType { "uniform", "storage", - "read-only-storage" + "read-only-storage", }; dictionary GPUBufferBindingLayout { @@ -489,7 +496,7 @@ dictionary GPUBufferBindingLayout { enum GPUSamplerBindingType { "filtering", "non-filtering", - "comparison" + "comparison", }; dictionary GPUSamplerBindingLayout { @@ -501,7 +508,7 @@ enum GPUTextureSampleType { "unfilterable-float", "depth", "sint", - "uint" + "uint", }; dictionary GPUTextureBindingLayout { @@ -511,7 +518,7 @@ dictionary GPUTextureBindingLayout { }; enum GPUStorageTextureAccess { - "write-only" + "write-only", }; dictionary GPUStorageTextureBindingLayout { @@ -528,7 +535,8 @@ interface GPUBindGroup { }; GPUBindGroup includes GPUObjectBase; -dictionary GPUBindGroupDescriptor : GPUObjectDescriptorBase { +dictionary GPUBindGroupDescriptor + : GPUObjectDescriptorBase { required GPUBindGroupLayout layout; required sequence entries; }; @@ -551,7 +559,8 @@ interface GPUPipelineLayout { }; GPUPipelineLayout includes GPUObjectBase; -dictionary GPUPipelineLayoutDescriptor : GPUObjectDescriptorBase { +dictionary GPUPipelineLayoutDescriptor + : GPUObjectDescriptorBase { required sequence bindGroupLayouts; }; @@ -561,7 +570,8 @@ interface GPUShaderModule { }; GPUShaderModule includes GPUObjectBase; -dictionary GPUShaderModuleDescriptor : GPUObjectDescriptorBase { +dictionary GPUShaderModuleDescriptor + : GPUObjectDescriptorBase { required USVString code; object sourceMap; record hints; @@ -574,7 +584,7 @@ dictionary GPUShaderModuleCompilationHint { enum GPUCompilationMessageType { "error", "warning", - "info" + "info", }; [Exposed=(Window, DedicatedWorker), Serializable, SecureContext] @@ -604,14 +614,15 @@ dictionary GPUPipelineErrorInit { enum GPUPipelineErrorReason { "validation", - "internal" + "internal", }; enum GPUAutoLayoutMode { - "auto" + "auto", }; -dictionary GPUPipelineDescriptorBase : GPUObjectDescriptorBase { +dictionary GPUPipelineDescriptorBase + : GPUObjectDescriptorBase { required (GPUPipelineLayout or GPUAutoLayoutMode) layout; }; @@ -633,7 +644,8 @@ interface GPUComputePipeline { GPUComputePipeline includes GPUObjectBase; GPUComputePipeline includes GPUPipelineBase; -dictionary GPUComputePipelineDescriptor : GPUPipelineDescriptorBase { +dictionary GPUComputePipelineDescriptor + : GPUPipelineDescriptorBase { required GPUProgrammableStage compute; }; @@ -643,7 +655,8 @@ interface GPURenderPipeline { GPURenderPipeline includes GPUObjectBase; GPURenderPipeline includes GPUPipelineBase; -dictionary GPURenderPipelineDescriptor : GPUPipelineDescriptorBase { +dictionary GPURenderPipelineDescriptor + : GPUPipelineDescriptorBase { required GPUVertexState vertex; GPUPrimitiveState primitive = {}; GPUDepthStencilState depthStencil; @@ -666,18 +679,18 @@ enum GPUPrimitiveTopology { "line-list", "line-strip", "triangle-list", - "triangle-strip" + "triangle-strip", }; enum GPUFrontFace { "ccw", - "cw" + "cw", }; enum GPUCullMode { "none", "front", - "back" + "back", }; dictionary GPUMultisampleState { @@ -686,7 +699,8 @@ dictionary GPUMultisampleState { boolean alphaToCoverageEnabled = false; }; -dictionary GPUFragmentState : GPUProgrammableStage { +dictionary GPUFragmentState + : GPUProgrammableStage { required sequence targets; }; @@ -731,7 +745,7 @@ enum GPUBlendFactor { "one-minus-dst-alpha", "src-alpha-saturated", "constant", - "one-minus-constant" + "one-minus-constant", }; enum GPUBlendOperation { @@ -739,7 +753,7 @@ enum GPUBlendOperation { "subtract", "reverse-subtract", "min", - "max" + "max", }; dictionary GPUDepthStencilState { @@ -774,12 +788,12 @@ enum GPUStencilOperation { "increment-clamp", "decrement-clamp", "increment-wrap", - "decrement-wrap" + "decrement-wrap", }; enum GPUIndexFormat { "uint16", - "uint32" + "uint32", }; enum GPUVertexFormat { @@ -812,15 +826,16 @@ enum GPUVertexFormat { "sint32", "sint32x2", "sint32x3", - "sint32x4" + "sint32x4", }; enum GPUVertexStepMode { "vertex", - "instance" + "instance", }; -dictionary GPUVertexState : GPUProgrammableStage { +dictionary GPUVertexState + : GPUProgrammableStage { sequence buffers = []; }; @@ -837,17 +852,43 @@ dictionary GPUVertexAttribute { required GPUIndex32 shaderLocation; }; -dictionary GPUImageDataLayout { GPUSize64 offset = 0; GPUSize32 bytesPerRow; GPUSize32 rowsPerImage;}; -dictionary GPUImageCopyBuffer : GPUImageDataLayout { required GPUBuffer buffer;}; -dictionary GPUImageCopyTexture { required GPUTexture texture; GPUIntegerCoordinate mipLevel = 0; GPUOrigin3D origin = {}; GPUTextureAspect aspect = "all";}; -dictionary GPUImageCopyTextureTagged : GPUImageCopyTexture { PredefinedColorSpace colorSpace = "srgb"; boolean premultipliedAlpha = false;}; -dictionary GPUImageCopyExternalImage { required (ImageBitmap or HTMLVideoElement or HTMLCanvasElement or OffscreenCanvas) source; GPUOrigin2D origin = {}; boolean flipY = false;}; +dictionary GPUImageDataLayout { + GPUSize64 offset = 0; + GPUSize32 bytesPerRow; + GPUSize32 rowsPerImage; +}; + +dictionary GPUImageCopyBuffer + : GPUImageDataLayout { + required GPUBuffer buffer; +}; + +dictionary GPUImageCopyTexture { + required GPUTexture texture; + GPUIntegerCoordinate mipLevel = 0; + GPUOrigin3D origin = {}; + GPUTextureAspect aspect = "all"; +}; + +dictionary GPUImageCopyTextureTagged + : GPUImageCopyTexture { + PredefinedColorSpace colorSpace = "srgb"; + boolean premultipliedAlpha = false; +}; + +dictionary GPUImageCopyExternalImage { + required (ImageBitmap or HTMLVideoElement or HTMLCanvasElement or OffscreenCanvas) source; + GPUOrigin2D origin = {}; + boolean flipY = false; +}; + [Exposed=(Window, DedicatedWorker), SecureContext] interface GPUCommandBuffer { }; GPUCommandBuffer includes GPUObjectBase; -dictionary GPUCommandBufferDescriptor : GPUObjectDescriptorBase { +dictionary GPUCommandBufferDescriptor + : GPUObjectDescriptorBase { }; interface mixin GPUCommandsMixin { @@ -900,14 +941,15 @@ GPUCommandEncoder includes GPUObjectBase; GPUCommandEncoder includes GPUCommandsMixin; GPUCommandEncoder includes GPUDebugCommandsMixin; -dictionary GPUCommandEncoderDescriptor : GPUObjectDescriptorBase { +dictionary GPUCommandEncoderDescriptor + : GPUObjectDescriptorBase { }; interface mixin GPUBindingCommandsMixin { - undefined setBindGroup(GPUIndex32 index, GPUBindGroup bindGroup, + undefined setBindGroup(GPUIndex32 index, GPUBindGroup? bindGroup, optional sequence dynamicOffsets = []); - undefined setBindGroup(GPUIndex32 index, GPUBindGroup bindGroup, + undefined setBindGroup(GPUIndex32 index, GPUBindGroup? bindGroup, Uint32Array dynamicOffsetsData, GPUSize64 dynamicOffsetsDataStart, GPUSize32 dynamicOffsetsDataLength); @@ -932,21 +974,15 @@ GPUComputePassEncoder includes GPUCommandsMixin; GPUComputePassEncoder includes GPUDebugCommandsMixin; GPUComputePassEncoder includes GPUBindingCommandsMixin; -enum GPUComputePassTimestampLocation { - "beginning", - "end" -}; - -dictionary GPUComputePassTimestampWrite { +dictionary GPUComputePassTimestampWrites { required GPUQuerySet querySet; - required GPUSize32 queryIndex; - required GPUComputePassTimestampLocation location; + GPUSize32 beginningOfPassWriteIndex; + GPUSize32 endOfPassWriteIndex; }; -typedef sequence GPUComputePassTimestampWrites; - -dictionary GPUComputePassDescriptor : GPUObjectDescriptorBase { - GPUComputePassTimestampWrites timestampWrites = []; +dictionary GPUComputePassDescriptor + : GPUObjectDescriptorBase { + GPUComputePassTimestampWrites timestampWrites; }; [Exposed=(Window, DedicatedWorker), SecureContext] @@ -973,24 +1009,18 @@ GPURenderPassEncoder includes GPUDebugCommandsMixin; GPURenderPassEncoder includes GPUBindingCommandsMixin; GPURenderPassEncoder includes GPURenderCommandsMixin; -enum GPURenderPassTimestampLocation { - "beginning", - "end" -}; - -dictionary GPURenderPassTimestampWrite { +dictionary GPURenderPassTimestampWrites { required GPUQuerySet querySet; - required GPUSize32 queryIndex; - required GPURenderPassTimestampLocation location; + GPUSize32 beginningOfPassWriteIndex; + GPUSize32 endOfPassWriteIndex; }; -typedef sequence GPURenderPassTimestampWrites; - -dictionary GPURenderPassDescriptor : GPUObjectDescriptorBase { +dictionary GPURenderPassDescriptor + : GPUObjectDescriptorBase { required sequence colorAttachments; GPURenderPassDepthStencilAttachment depthStencilAttachment; GPUQuerySet occlusionQuerySet; - GPURenderPassTimestampWrites timestampWrites = []; + GPURenderPassTimestampWrites timestampWrites; GPUSize64 maxDrawCount = 50000000; }; @@ -1019,15 +1049,16 @@ dictionary GPURenderPassDepthStencilAttachment { enum GPULoadOp { "load", - "clear" + "clear", }; enum GPUStoreOp { "store", - "discard" + "discard", }; -dictionary GPURenderPassLayout : GPUObjectDescriptorBase { +dictionary GPURenderPassLayout + : GPUObjectDescriptorBase { required sequence colorFormats; GPUTextureFormat depthStencilFormat; GPUSize32 sampleCount = 1; @@ -1037,7 +1068,7 @@ interface mixin GPURenderCommandsMixin { undefined setPipeline(GPURenderPipeline pipeline); undefined setIndexBuffer(GPUBuffer buffer, GPUIndexFormat indexFormat, optional GPUSize64 offset = 0, optional GPUSize64 size); - undefined setVertexBuffer(GPUIndex32 slot, GPUBuffer buffer, optional GPUSize64 offset = 0, optional GPUSize64 size); + undefined setVertexBuffer(GPUIndex32 slot, GPUBuffer? buffer, optional GPUSize64 offset = 0, optional GPUSize64 size); undefined draw(GPUSize32 vertexCount, optional GPUSize32 instanceCount = 1, optional GPUSize32 firstVertex = 0, optional GPUSize32 firstInstance = 0); @@ -1055,7 +1086,8 @@ interface GPURenderBundle { }; GPURenderBundle includes GPUObjectBase; -dictionary GPURenderBundleDescriptor : GPUObjectDescriptorBase { +dictionary GPURenderBundleDescriptor + : GPUObjectDescriptorBase { }; [Exposed=(Window, DedicatedWorker), SecureContext] @@ -1068,12 +1100,14 @@ GPURenderBundleEncoder includes GPUDebugCommandsMixin; GPURenderBundleEncoder includes GPUBindingCommandsMixin; GPURenderBundleEncoder includes GPURenderCommandsMixin; -dictionary GPURenderBundleEncoderDescriptor : GPURenderPassLayout { +dictionary GPURenderBundleEncoderDescriptor + : GPURenderPassLayout { boolean depthReadOnly = false; boolean stencilReadOnly = false; }; -dictionary GPUQueueDescriptor : GPUObjectDescriptorBase { +dictionary GPUQueueDescriptor + : GPUObjectDescriptorBase { }; [Exposed=(Window, DedicatedWorker), SecureContext] @@ -1107,18 +1141,19 @@ interface GPUQuerySet { undefined destroy(); readonly attribute GPUQueryType type; - readonly attribute GPUSize32 count; + readonly attribute GPUSize32Out count; }; GPUQuerySet includes GPUObjectBase; -dictionary GPUQuerySetDescriptor : GPUObjectDescriptorBase { +dictionary GPUQuerySetDescriptor + : GPUObjectDescriptorBase { required GPUQueryType type; required GPUSize32 count; }; enum GPUQueryType { "occlusion", - "timestamp" + "timestamp", }; [Exposed=(Window, DedicatedWorker), SecureContext] @@ -1133,7 +1168,7 @@ interface GPUCanvasContext { enum GPUCanvasAlphaMode { "opaque", - "premultiplied" + "premultiplied", }; dictionary GPUCanvasConfiguration { @@ -1147,7 +1182,7 @@ dictionary GPUCanvasConfiguration { enum GPUDeviceLostReason { "unknown", - "destroyed" + "destroyed", }; [Exposed=(Window, DedicatedWorker), SecureContext] @@ -1166,24 +1201,27 @@ interface GPUError { }; [Exposed=(Window, DedicatedWorker), SecureContext] -interface GPUValidationError : GPUError { +interface GPUValidationError + : GPUError { constructor(DOMString message); }; [Exposed=(Window, DedicatedWorker), SecureContext] -interface GPUOutOfMemoryError : GPUError { +interface GPUOutOfMemoryError + : GPUError { constructor(DOMString message); }; [Exposed=(Window, DedicatedWorker), SecureContext] -interface GPUInternalError : GPUError { +interface GPUInternalError + : GPUError { constructor(DOMString message); }; enum GPUErrorFilter { "validation", "out-of-memory", - "internal" + "internal", }; partial interface GPUDevice { @@ -1220,6 +1258,10 @@ typedef [EnforceRange] unsigned long GPUIndex32; typedef [EnforceRange] unsigned long GPUSize32; typedef [EnforceRange] long GPUSignedOffset32; +typedef unsigned long long GPUSize64Out; +typedef unsigned long GPUIntegerCoordinateOut; +typedef unsigned long GPUSize32Out; + typedef unsigned long GPUFlagsConstant; dictionary GPUColorDict { diff --git a/test/wpt/tests/interfaces/webnn.idl b/test/wpt/tests/interfaces/webnn.idl index 2c2ab35e909..17e30803b88 100644 --- a/test/wpt/tests/interfaces/webnn.idl +++ b/test/wpt/tests/interfaces/webnn.idl @@ -127,10 +127,10 @@ interface MLGraphBuilder { constructor(MLContext context); // Create an operand for a graph input. - MLOperand input(DOMString name, MLOperandDescriptor desc); + MLOperand input(DOMString name, MLOperandDescriptor descriptor); // Create an operand for a graph constant. - MLOperand constant(MLOperandDescriptor desc, MLBufferView bufferView); + MLOperand constant(MLOperandDescriptor descriptor, MLBufferView bufferView); // Create a single-value operand from the specified number of the specified type. MLOperand constant(double value, optional MLOperandType type = "float32"); @@ -489,13 +489,8 @@ partial interface MLGraphBuilder { MLActivation sigmoid(); }; -dictionary MLSliceOptions { - sequence axes; -}; - partial interface MLGraphBuilder { - MLOperand slice(MLOperand input, sequence starts, sequence sizes, - optional MLSliceOptions options = {}); + MLOperand slice(MLOperand input, sequence starts, sequence sizes); }; partial interface MLGraphBuilder { diff --git a/test/wpt/tests/interfaces/webrtc-encoded-transform.idl b/test/wpt/tests/interfaces/webrtc-encoded-transform.idl index e48f1080c41..6dd2ba3fffa 100644 --- a/test/wpt/tests/interfaces/webrtc-encoded-transform.idl +++ b/test/wpt/tests/interfaces/webrtc-encoded-transform.idl @@ -27,7 +27,7 @@ typedef [EnforceRange] unsigned long long SmallCryptoKeyID; typedef (SmallCryptoKeyID or bigint) CryptoKeyID; [Exposed=(Window,DedicatedWorker)] -interface SFrameTransform { +interface SFrameTransform : EventTarget { constructor(optional SFrameTransformOptions options = {}); Promise setEncryptionKey(CryptoKey key, optional CryptoKeyID keyID); attribute EventHandler onerror; @@ -72,7 +72,8 @@ dictionary RTCEncodedVideoFrameMetadata { unsigned long temporalIndex; unsigned long synchronizationSource; octet payloadType; - sequence contributingSources; + sequence contributingSources; + long long timestamp; // microseconds }; // New interfaces to define encoded video and audio frames. Will eventually diff --git a/test/wpt/tests/interfaces/webrtc-stats.idl b/test/wpt/tests/interfaces/webrtc-stats.idl index 7e820a26df4..b398c73e3f4 100644 --- a/test/wpt/tests/interfaces/webrtc-stats.idl +++ b/test/wpt/tests/interfaces/webrtc-stats.idl @@ -13,8 +13,6 @@ enum RTCStatsType { "media-playout", "peer-connection", "data-channel", -"stream", -"track", "transport", "candidate-pair", "local-candidate", @@ -94,6 +92,8 @@ dictionary RTCInboundRtpStreamStats : RTCReceivedRtpStreamStats { boolean powerEfficientDecoder; unsigned long framesAssembledFromMultiplePackets; double totalAssemblyTime; + unsigned long long retransmittedPacketsReceived; + unsigned long long retransmittedBytesReceived; }; dictionary RTCRemoteInboundRtpStreamStats : RTCReceivedRtpStreamStats { diff --git a/test/wpt/tests/interfaces/webrtc.idl b/test/wpt/tests/interfaces/webrtc.idl index 578cbe92974..4c31d3be67a 100644 --- a/test/wpt/tests/interfaces/webrtc.idl +++ b/test/wpt/tests/interfaces/webrtc.idl @@ -441,13 +441,13 @@ enum RTCIceGathererState { }; enum RTCIceTransportState { + "closed", + "failed", + "disconnected", "new", "checking", - "connected", "completed", - "disconnected", - "failed", - "closed" + "connected" }; enum RTCIceRole { diff --git a/test/wpt/tests/interfaces/webtransport.idl b/test/wpt/tests/interfaces/webtransport.idl index 2bea483e1b9..a9f514e2366 100644 --- a/test/wpt/tests/interfaces/webtransport.idl +++ b/test/wpt/tests/interfaces/webtransport.idl @@ -98,6 +98,7 @@ dictionary WebTransportDatagramStats { [Exposed=(Window,Worker), SecureContext, Transferable] interface WebTransportSendStream : WritableStream { + attribute long long? sendOrder; Promise getStats(); }; @@ -130,12 +131,12 @@ interface WebTransportError : DOMException { constructor(optional DOMString message = "", optional WebTransportErrorOptions options = {}); readonly attribute WebTransportErrorSource source; - readonly attribute octet? streamErrorCode; + readonly attribute unsigned long? streamErrorCode; }; dictionary WebTransportErrorOptions { WebTransportErrorSource source = "stream"; - [Clamp] octet? streamErrorCode = null; + [Clamp] unsigned long? streamErrorCode = null; }; enum WebTransportErrorSource { diff --git a/test/wpt/tests/interfaces/webxrlayers.idl b/test/wpt/tests/interfaces/webxrlayers.idl index e182f47b9c5..c8b3a71c699 100644 --- a/test/wpt/tests/interfaces/webxrlayers.idl +++ b/test/wpt/tests/interfaces/webxrlayers.idl @@ -11,6 +11,12 @@ enum XRLayerLayout { "stereo-top-bottom" }; +enum XRLayerQuality { + "default", + "text-optimized", + "graphics-optimized" +}; + [Exposed=Window] interface XRCompositionLayer : XRLayer { readonly attribute XRLayerLayout layout; @@ -18,6 +24,7 @@ enum XRLayerLayout { attribute boolean forceMonoPresentation; attribute float opacity; readonly attribute unsigned long mipLevels; + attribute XRLayerQuality quality; readonly attribute boolean needsRedraw; @@ -106,6 +113,7 @@ dictionary XRProjectionLayerInit { GLenum colorFormat = 0x1908; // RGBA GLenum depthFormat = 0x1902; // DEPTH_COMPONENT double scaleFactor = 1.0; + boolean clearOnAccess = true; }; dictionary XRLayerInit { @@ -117,6 +125,7 @@ dictionary XRLayerInit { required unsigned long viewPixelHeight; XRLayerLayout layout = "mono"; boolean isStatic = false; + boolean clearOnAccess = true; }; dictionary XRQuadLayerInit : XRLayerInit { diff --git a/test/wpt/tests/lint.ignore b/test/wpt/tests/lint.ignore index 6f8ec10470b..489c717cd6a 100644 --- a/test/wpt/tests/lint.ignore +++ b/test/wpt/tests/lint.ignore @@ -48,6 +48,7 @@ TRAILING WHITESPACE, INDENT TABS, CR AT EOL: *.bmp TRAILING WHITESPACE, INDENT TABS, CR AT EOL: *.sxg TRAILING WHITESPACE, INDENT TABS, CR AT EOL: *.wbn TRAILING WHITESPACE, INDENT TABS, CR AT EOL: *.avif +TRAILING WHITESPACE, INDENT TABS, CR AT EOL: *.annexb ## .gitignore W3C-TEST.ORG: .gitignore @@ -225,7 +226,11 @@ SET TIMEOUT: service-workers/service-worker/activation.https.html SET TIMEOUT: service-workers/service-worker/fetch-frame-resource.https.html SET TIMEOUT: service-workers/service-worker/fetch-request-redirect.https.html SET TIMEOUT: service-workers/service-worker/fetch-waits-for-activate.https.html +SET TIMEOUT: service-workers/service-worker/postMessage-client-worker.js SET TIMEOUT: service-workers/service-worker/update-recovery.https.html +SET TIMEOUT: service-workers/service-worker/resources/controlled-frame-postMessage.html +SET TIMEOUT: service-workers/service-worker/resources/controlled-worker-late-postMessage.js +SET TIMEOUT: service-workers/service-worker/resources/controlled-worker-postMessage.js SET TIMEOUT: service-workers/service-worker/resources/extendable-event-async-waituntil.js SET TIMEOUT: service-workers/service-worker/resources/fetch-event-async-respond-with-worker.js SET TIMEOUT: service-workers/service-worker/resources/fetch-event-test-worker.js @@ -247,11 +252,16 @@ SET TIMEOUT: webaudio/the-audio-api/the-mediaelementaudiosourcenode-interface/me SET TIMEOUT: webauthn/*timeout.https.html SET TIMEOUT: webdriver/* SET TIMEOUT: webmessaging/* +SET TIMEOUT: webrtc-encoded-transform/script-metadata-transform-worker.js +SET TIMEOUT: webrtc-encoded-transform/script-transform-generateKeyFrame.js +SET TIMEOUT: webrtc-encoded-transform/script-transform-sendKeyFrameRequest.js SET TIMEOUT: webstorage/eventTestHarness.js SET TIMEOUT: webvtt/* SET TIMEOUT: workers/* SET TIMEOUT: xhr/resources/init.htm SET TIMEOUT: xhr/resources/xmlhttprequest-timeout.js +SET TIMEOUT: fenced-frame/resolve-to-config-promise.https.html +SET TIMEOUT: credential-management/support/fedcm-iframe.html # generate_tests implementation and sample usage GENERATE_TESTS: resources/test/tests/functional/generate-callback.html @@ -361,7 +371,6 @@ SET TIMEOUT: speculation-rules/prerender/resources/media-autoplay-attribute.html SET TIMEOUT: speculation-rules/prerender/resources/media-play.html SET TIMEOUT: html/browsers/browsing-the-web/back-forward-cache/timers.html SET TIMEOUT: dom/abort/crashtests/timeout-close.html -SET TIMEOUT: common/rendering-utils.js # setTimeout use in reftests SET TIMEOUT: acid/acid3/test.html @@ -427,8 +436,12 @@ TRAILING WHITESPACE: css/css-fonts/support/fonts/gsubtest-lookup3.ufo/features.f SET TIMEOUT: css/compositing/mix-blend-mode/mix-blend-mode-parent-with-3D-transform-and-transition.html SET TIMEOUT: css/compositing/mix-blend-mode/mix-blend-mode-sibling-with-3D-transform-and-transition.html +SET TIMEOUT: css/css-backgrounds/color-mix-currentcolor-background-repaint-parent.html +SET TIMEOUT: css/css-backgrounds/color-mix-currentcolor-background-repaint.html SET TIMEOUT: css/css-backgrounds/color-mix-currentcolor-border-repaint-parent.html SET TIMEOUT: css/css-backgrounds/color-mix-currentcolor-border-repaint.html +SET TIMEOUT: css/css-backgrounds/color-mix-currentcolor-outline-repaint-parent.html +SET TIMEOUT: css/css-backgrounds/color-mix-currentcolor-outline-repaint.html SET TIMEOUT: css/css-backgrounds/currentcolor-border-repaint-parent.html SET TIMEOUT: css/css-transitions/events-007.html SET TIMEOUT: css/css-transitions/support/generalParallelTest.js @@ -593,7 +606,7 @@ AHEM SYSTEM FONT: acid/acid3/test.html AHEM SYSTEM FONT: resource-timing/font-timestamps.html AHEM SYSTEM FONT: resource-timing/initiator-type/style.html AHEM SYSTEM FONT: resource-timing/resources/iframe-reload-TAO.sub.html -AHEM SYSTEM FONT: html/canvas/element/drawing-text-to-the-canvas/2d.text.measure.fontBoundingBox.ahem.html +AHEM SYSTEM FONT: html/canvas/element/text/2d.text.measure.fontBoundingBox.ahem.html AHEM SYSTEM FONT: css/css-font-loading/fontface-override-descriptors.html AHEM SYSTEM FONT: css/css-font-loading/fontface-size-adjust-descriptor.html AHEM SYSTEM FONT: css/css-font-loading/fontface-size-adjust-descriptor-ref.html @@ -602,7 +615,12 @@ AHEM SYSTEM FONT: css/css-fonts/font-size-adjust-012.html AHEM SYSTEM FONT: css/css-fonts/font-size-adjust-012-ref.html AHEM SYSTEM FONT: css/css-fonts/font-size-adjust-013.html AHEM SYSTEM FONT: css/css-fonts/font-size-adjust-013-ref.html +AHEM SYSTEM FONT: css/css-fonts/font-size-adjust-014.html +AHEM SYSTEM FONT: css/css-fonts/font-size-adjust-014-ref.html +AHEM SYSTEM FONT: css/css-fonts/font-size-adjust-metrics-override.html +AHEM SYSTEM FONT: css/css-fonts/font-size-adjust-metrics-override-ref.html AHEM SYSTEM FONT: css/css-fonts/line-gap-override.html +AHEM SYSTEM FONT: css/css-fonts/parsing/font-size-adjust-computed.html AHEM SYSTEM FONT: html/dom/render-blocking/remove-attr-unblocks-rendering.optional.html AHEM SYSTEM FONT: html/dom/render-blocking/remove-element-unblocks-rendering.optional.html @@ -655,8 +673,8 @@ TESTHARNESS-IN-OTHER-TYPE: svg/svg-in-svg/svg-in-svg-circular-filter-reference-c # Adding the testharnessreport.js script causes the test to never complete. MISSING-TESTHARNESSREPORT: accessibility/crashtests/computed-node-checked.html -PRINT STATEMENT: webdriver/tests/print/* PRINT STATEMENT: webdriver/tests/bidi/browsing_context/print/* +PRINT STATEMENT: webdriver/tests/classic/print/* PRINT STATEMENT: webdriver/tests/support/fixtures_bidi.py DUPLICATE-BASENAME-PATH: acid/acid3/empty.html @@ -669,6 +687,10 @@ DUPLICATE-BASENAME-PATH: svg/struct/reftests/reference/green-100x100.svg SET TIMEOUT: mediacapture-insertable-streams/MediaStreamTrackProcessor-video.https.html +# This is a subresource which cannot use step_timeout without becoming a test +# itself. See https://github.com/web-platform-tests/wpt/issues/16933 +SET TIMEOUT: scroll-to-text-fragment/iframe-target.html + # Ported crashtests from Mozilla SET TIMEOUT: editing/crashtests/backcolor-in-nested-editing-host-td-from-DOMAttrModified.html SET TIMEOUT: editing/crashtests/contenteditable-will-be-blurred-by-focus-event-listener.html diff --git a/test/wpt/tests/resources/chromium/webusb-test.js b/test/wpt/tests/resources/chromium/webusb-test.js index 94ff1bcadd9..7cca63d9196 100644 --- a/test/wpt/tests/resources/chromium/webusb-test.js +++ b/test/wpt/tests/resources/chromium/webusb-test.js @@ -440,11 +440,11 @@ class FakeWebUsbService { } } - getPermission(deviceFilters) { + getPermission(options) { return new Promise(resolve => { if (navigator.usb.test.onrequestdevice) { navigator.usb.test.onrequestdevice( - new USBDeviceRequestEvent(deviceFilters, resolve)); + new USBDeviceRequestEvent(options, resolve)); } else { resolve({ result: null }); } @@ -457,8 +457,9 @@ class FakeWebUsbService { } class USBDeviceRequestEvent { - constructor(deviceFilters, resolve) { - this.filters = convertMojoDeviceFilters(deviceFilters); + constructor(options, resolve) { + this.filters = convertMojoDeviceFilters(options.filters); + this.exclusionFilters = convertMojoDeviceFilters(options.exclusionFilters); this.resolveFunc_ = resolve; } diff --git a/test/wpt/tests/resources/chromium/webxr-test.js b/test/wpt/tests/resources/chromium/webxr-test.js index ab2c6faa0ee..c5eb1bd14b8 100644 --- a/test/wpt/tests/resources/chromium/webxr-test.js +++ b/test/wpt/tests/resources/chromium/webxr-test.js @@ -985,8 +985,6 @@ class MockRuntime { environmentProviderRequest.handle); } - setInputSourceButtonListener(listener) { listener.$.close(); } - // XREnvironmentIntegrationProvider implementation: subscribeToHitTest(nativeOriginInformation, entityTypes, ray) { if (!this.supportedModes_.includes(xrSessionMojom.XRSessionMode.kImmersiveAr)) { @@ -1212,7 +1210,6 @@ class MockRuntime { clientReceiver: clientReceiver, enabledFeatures: enabled_features, deviceConfig: { - usesInputEventing: false, defaultFramebufferScale: this.defaultFramebufferScale_, supportsViewportScaling: true, depthConfiguration: diff --git a/test/wpt/tests/resources/idlharness.js b/test/wpt/tests/resources/idlharness.js index 46aa11e5ca1..8f741b09b26 100644 --- a/test/wpt/tests/resources/idlharness.js +++ b/test/wpt/tests/resources/idlharness.js @@ -2357,12 +2357,13 @@ IdlInterface.prototype.do_member_operation_asserts = function(memberHolderObject assert_equals(typeof memberHolderObject[member.name], "function", "property must be a function"); - const ctors = this.members.filter(function(m) { - return m.type == "operation" && m.name == member.name; + const operationOverloads = this.members.filter(function(m) { + return m.type == "operation" && m.name == member.name && + (m.special === "static") === (member.special === "static"); }); assert_equals( memberHolderObject[member.name].length, - minOverloadLength(ctors), + minOverloadLength(operationOverloads), "property has wrong .length"); assert_equals( memberHolderObject[member.name].name, diff --git a/test/wpt/tests/resources/test/tests/functional/assert-throws-dom.html b/test/wpt/tests/resources/test/tests/functional/assert-throws-dom.html new file mode 100644 index 00000000000..4dd66b2372a --- /dev/null +++ b/test/wpt/tests/resources/test/tests/functional/assert-throws-dom.html @@ -0,0 +1,55 @@ + +assert_throws_dom + + +
+ + + diff --git a/test/wpt/tests/resources/test/tests/functional/idlharness/IdlDictionary/test_partial_interface_of.html b/test/wpt/tests/resources/test/tests/functional/idlharness/IdlDictionary/test_partial_interface_of.html index 05e6e0b1e06..f635768c69f 100644 --- a/test/wpt/tests/resources/test/tests/functional/idlharness/IdlDictionary/test_partial_interface_of.html +++ b/test/wpt/tests/resources/test/tests/functional/idlharness/IdlDictionary/test_partial_interface_of.html @@ -3,7 +3,6 @@ - idlharness: Partial dictionary diff --git a/test/wpt/tests/resources/test/tests/functional/idlharness/IdlInterface/test_partial_interface_of.html b/test/wpt/tests/resources/test/tests/functional/idlharness/IdlInterface/test_partial_interface_of.html index 671196cc5df..7dd9e676af4 100644 --- a/test/wpt/tests/resources/test/tests/functional/idlharness/IdlInterface/test_partial_interface_of.html +++ b/test/wpt/tests/resources/test/tests/functional/idlharness/IdlInterface/test_partial_interface_of.html @@ -3,7 +3,6 @@ - idlharness: Partial interface diff --git a/test/wpt/tests/resources/test/tox.ini b/test/wpt/tests/resources/test/tox.ini index 4fbeb67fb52..12013a1a705 100644 --- a/test/wpt/tests/resources/test/tox.ini +++ b/test/wpt/tests/resources/test/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py37,py38,py39,py310 +envlist = py37,py38,py39,py310,py311 skipsdist=True [testenv] diff --git a/test/wpt/tests/resources/testdriver.js b/test/wpt/tests/resources/testdriver.js index 446b033b0a4..a23d6eaf4cf 100644 --- a/test/wpt/tests/resources/testdriver.js +++ b/test/wpt/tests/resources/testdriver.js @@ -296,12 +296,6 @@ inline: "nearest"}); } - var pointerInteractablePaintTree = getPointerInteractablePaintTree(element); - if (pointerInteractablePaintTree.length === 0 || - !element.contains(pointerInteractablePaintTree[0])) { - return Promise.reject(new Error("element send_keys intercepted error")); - } - return window.test_driver_internal.send_keys(element, keys); }, @@ -334,9 +328,9 @@ * to run the call, or null for the current * browsing context. * - * @returns {Promise} fulfilled with the previous {@link - * https://www.w3.org/TR/webdriver/#dfn-windowrect-object|WindowRect} - * value, after the window is minimized. + * @returns {Promise} fulfilled with the previous `WindowRect + * `_ + * value, after the window is minimized. */ minimize_window: function(context=null) { return window.test_driver_internal.minimize_window(context); @@ -349,8 +343,8 @@ * `_ * WebDriver command * - * @param {Object} rect - A {@link - * https://www.w3.org/TR/webdriver/#dfn-windowrect-object|WindowRect} + * @param {Object} rect - A `WindowRect + * `_ * @param {WindowProxy} context - Browsing context in which * to run the call, or null for the current * browsing context. @@ -680,6 +674,134 @@ set_spc_transaction_mode: function(mode, context=null) { return window.test_driver_internal.set_spc_transaction_mode(mode, context); }, + + /** + * Cancels the Federated Credential Management dialog + * + * Matches the `Cancel dialog + * `_ + * WebDriver command. + * + * @param {WindowProxy} context - Browsing context in which + * to run the call, or null for the current + * browsing context. + * + * @returns {Promise} Fulfilled after the dialog is canceled, or rejected + * in case the WebDriver command errors + */ + cancel_fedcm_dialog: function(context=null) { + return window.test_driver_internal.cancel_fedcm_dialog(context); + }, + + /** + * Selects an account from the Federated Credential Management dialog + * + * Matches the `Select account + * `_ + * WebDriver command. + * + * @param {number} account_index - Index of the account to select. + * @param {WindowProxy} context - Browsing context in which + * to run the call, or null for the current + * browsing context. + * + * @returns {Promise} Fulfilled after the account is selected, + * or rejected in case the WebDriver command errors + */ + select_fedcm_account: function(account_index, context=null) { + return window.test_driver_internal.select_fedcm_account(account_index, context); + }, + + /** + * Gets the account list from the Federated Credential Management dialog + * + * Matches the `Account list + * `_ + * WebDriver command. + * + * @param {WindowProxy} context - Browsing context in which + * to run the call, or null for the current + * browsing context. + * + * @returns {Promise} fulfilled after the account list is returned, or + * rejected in case the WebDriver command errors + */ + get_fedcm_account_list: function(context=null) { + return window.test_driver_internal.get_fedcm_account_list(context); + }, + + /** + * Gets the title of the Federated Credential Management dialog + * + * Matches the `Get title + * `_ + * WebDriver command. + * + * @param {WindowProxy} context - Browsing context in which + * to run the call, or null for the current + * browsing context. + * + * @returns {Promise} Fulfilled after the title is returned, or rejected + * in case the WebDriver command errors + */ + get_fedcm_dialog_title: function(context=null) { + return window.test_driver_internal.get_fedcm_dialog_title(context); + }, + + /** + * Gets the type of the Federated Credential Management dialog + * + * Matches the `Get dialog type + * `_ + * WebDriver command. + * + * @param {WindowProxy} context - Browsing context in which + * to run the call, or null for the current + * browsing context. + * + * @returns {Promise} Fulfilled after the dialog type is returned, or + * rejected in case the WebDriver command errors + */ + get_fedcm_dialog_type: function(context=null) { + return window.test_driver_internal.get_fedcm_dialog_type(context); + }, + + /** + * Sets whether promise rejection delay is enabled for the Federated Credential Management dialog + * + * Matches the `Set delay enabled + * `_ + * WebDriver command. + * + * @param {boolean} enabled - Whether to delay FedCM promise rejection. + * @param {WindowProxy} context - Browsing context in which + * to run the call, or null for the current + * browsing context. + * + * @returns {Promise} Fulfilled after the delay has been enabled or disabled, + * or rejected in case the WebDriver command errors + */ + set_fedcm_delay_enabled: function(enabled, context=null) { + return window.test_driver_internal.set_fedcm_delay_enabled(enabled, context); + }, + + /** + * Resets the Federated Credential Management dialog's cooldown + * + * Matches the `Reset cooldown + * `_ + * WebDriver command. + * + * @param {WindowProxy} context - Browsing context in which + * to run the call, or null for the current + * browsing context. + * + * @returns {Promise} Fulfilled after the cooldown has been reset, + * or rejected in case the WebDriver command errors + */ + reset_fedcm_cooldown: function(context=null) { + return window.test_driver_internal.reset_fedcm_cooldown(context); + } }; window.test_driver_internal = { @@ -805,5 +927,32 @@ throw new Error("set_spc_transaction_mode() is not implemented by testdriver-vendor.js"); }, + async cancel_fedcm_dialog(context=null) { + throw new Error("cancel_fedcm_dialog() is not implemented by testdriver-vendor.js"); + }, + + async select_fedcm_account(account_index, context=null) { + throw new Error("select_fedcm_account() is not implemented by testdriver-vendor.js"); + }, + + async get_fedcm_account_list(context=null) { + throw new Error("get_fedcm_account_list() is not implemented by testdriver-vendor.js"); + }, + + async get_fedcm_dialog_title(context=null) { + throw new Error("get_fedcm_dialog_title() is not implemented by testdriver-vendor.js"); + }, + + async get_fedcm_dialog_type(context=null) { + throw new Error("get_fedcm_dialog_type() is not implemented by testdriver-vendor.js"); + }, + + async set_fedcm_delay_enabled(enabled, context=null) { + throw new Error("set_fedcm_delay_enabled() is not implemented by testdriver-vendor.js"); + }, + + async reset_fedcm_cooldown(context=null) { + throw new Error("reset_fedcm_cooldown() is not implemented by testdriver-vendor.js"); + } }; })(); diff --git a/test/wpt/tests/resources/testharness.js b/test/wpt/tests/resources/testharness.js index 112790bb1ee..413993089be 100644 --- a/test/wpt/tests/resources/testharness.js +++ b/test/wpt/tests/resources/testharness.js @@ -1426,12 +1426,16 @@ function assert_wrapper(...args) { let status = Test.statuses.TIMEOUT; let stack = null; + let new_assert_index = null; try { if (settings.debug) { console.debug("ASSERT", name, tests.current_test && tests.current_test.name, args); } if (tests.output) { tests.set_assert(name, args); + // Remember the newly pushed assert's index, because `apply` + // below might push new asserts. + new_assert_index = tests.asserts_run.length - 1; } const rv = f.apply(undefined, args); status = Test.statuses.PASS; @@ -1445,7 +1449,7 @@ stack = get_stack(); } if (tests.output) { - tests.set_assert_status(status, stack); + tests.set_assert_status(new_assert_index, status, stack); } } } @@ -3673,8 +3677,8 @@ this.asserts_run.push(new AssertRecord(this.current_test, assert_name, args)) } - Tests.prototype.set_assert_status = function(status, stack) { - let assert_record = this.asserts_run[this.asserts_run.length - 1]; + Tests.prototype.set_assert_status = function(index, status, stack) { + let assert_record = this.asserts_run[index]; assert_record.status = status; assert_record.stack = stack; } diff --git a/test/wpt/tests/service-workers/service-worker/controlled-dedicatedworker-postMessage.https.html b/test/wpt/tests/service-workers/service-worker/controlled-dedicatedworker-postMessage.https.html new file mode 100644 index 00000000000..7e2a604621d --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/controlled-dedicatedworker-postMessage.https.html @@ -0,0 +1,44 @@ + + + + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/controlled-iframe-postMessage.https.html b/test/wpt/tests/service-workers/service-worker/controlled-iframe-postMessage.https.html new file mode 100644 index 00000000000..8f39b7fdbf8 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/controlled-iframe-postMessage.https.html @@ -0,0 +1,67 @@ + + + + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/detached-context.https.html b/test/wpt/tests/service-workers/service-worker/detached-context.https.html index 747a953f620..ce8e4cc8400 100644 --- a/test/wpt/tests/service-workers/service-worker/detached-context.https.html +++ b/test/wpt/tests/service-workers/service-worker/detached-context.https.html @@ -119,23 +119,6 @@ } assert_not_equals(get_navigator().serviceWorker, null); iframe.remove(); - assert_throws_js(TypeError, () => get_navigator()); - }, 'accessing navigator on a removed frame'); - -// It seems weird that about:blank and blank.html (the test above) have -// different behavior. These expectations are based on Chromium behavior, which -// might not be right. -test(t => { - const iframe = document.createElement('iframe'); - iframe.src = 'about:blank'; - document.body.appendChild(iframe); - const f = iframe.contentWindow.Function; - function get_navigator() { - return f('return navigator')(); - } assert_not_equals(get_navigator().serviceWorker, null); - iframe.remove(); - assert_equals(get_navigator().serviceWorker, null); - }, 'accessing navigator.serviceWorker on a removed about:blank frame'); - + }, 'accessing navigator on a removed frame'); diff --git a/test/wpt/tests/service-workers/service-worker/fetch-error.https.html b/test/wpt/tests/service-workers/service-worker/fetch-error.https.html index ca2f884a9b3..e9fdf574312 100644 --- a/test/wpt/tests/service-workers/service-worker/fetch-error.https.html +++ b/test/wpt/tests/service-workers/service-worker/fetch-error.https.html @@ -21,12 +21,8 @@ const iframe = await with_iframe(scope); test.add_cleanup(() => iframe.remove()); const response = await iframe.contentWindow.fetch("fetch-error-test"); - try { - await response.text(); - assert_unreached(); - } catch (error) { - assert_true(error.message.includes("Sorry")); - } + return promise_rejects_js(test, iframe.contentWindow.TypeError, + response.text(), 'text() should reject'); }, "Make sure a load that makes progress does not time out"); diff --git a/test/wpt/tests/service-workers/service-worker/navigation-redirect.https.html b/test/wpt/tests/service-workers/service-worker/navigation-redirect.https.html index d7d3d5259a4..a87de569316 100644 --- a/test/wpt/tests/service-workers/service-worker/navigation-redirect.https.html +++ b/test/wpt/tests/service-workers/service-worker/navigation-redirect.https.html @@ -1,8 +1,8 @@ Service Worker: Navigation redirection - - + + diff --git a/test/wpt/tests/service-workers/service-worker/navigation-timing.https.html b/test/wpt/tests/service-workers/service-worker/navigation-timing.https.html index 6b51a5c2da2..75cab40458c 100644 --- a/test/wpt/tests/service-workers/service-worker/navigation-timing.https.html +++ b/test/wpt/tests/service-workers/service-worker/navigation-timing.https.html @@ -54,6 +54,7 @@ t.add_cleanup(() => frame.remove()); const timing = await navigate_in_frame(frame, scope); + assert_greater_than(timing.workerStart, 0); verify(timing); }, 'Service worker controlled navigation timing network fallback'); diff --git a/test/wpt/tests/service-workers/service-worker/partitioned-cookies.tentative.https.html b/test/wpt/tests/service-workers/service-worker/partitioned-cookies.tentative.https.html index 6744edc0eac..5f6371cb428 100644 --- a/test/wpt/tests/service-workers/service-worker/partitioned-cookies.tentative.https.html +++ b/test/wpt/tests/service-workers/service-worker/partitioned-cookies.tentative.https.html @@ -23,6 +23,14 @@ const scope = './resources/partitioned-cookies-' const absolute_scope = new URL(scope, window.location).href; + // Set a Partitioned cookie. + document.cookie = '__Host-partitioned=123; Secure; Path=/; SameSite=None; Partitioned;'; + assert_true(document.cookie.includes('__Host-partitioned=123')); + + // Set an unpartitioned cookie. + document.cookie = 'unpartitioned=456; Secure; Path=/; SameSite=None;'; + assert_true(document.cookie.includes('unpartitioned=456')); + const reg = await service_worker_unregister_and_register(t, script, scope); await wait_for_state(t, reg.installing, 'activated'); t.add_cleanup(() => reg.unregister()); @@ -55,11 +63,36 @@ await wait_promise; assert_true(got.ok, 'Message passing'); - // Set a Partitioned cookie. - document.cookie = '__Host-partitioned=123; Secure; Path=/; SameSite=None; Partitioned;'; - assert_true(document.cookie.includes('__Host-partitioned=123')); + // Test that the partitioned cookie is available to this worker via HTTP. + wait_promise = new Promise(resolve => { + resolve_wait_promise = resolve; + }); + on_message = ev => { + got = ev.data; + resolve_wait_promise(); + }; + filtered_registrations[0].active.postMessage({type: 'echo_cookies_http'}); + await wait_promise; + assert_true(got.ok, 'Get cookies'); + assert_true(got.cookies.includes('__Host-partitioned'), 'Can access partitioned cookie via HTTP'); + assert_true(got.cookies.includes('unpartitioned'), 'Can access unpartitioned cookie via HTTP'); + + // Test that the partitioned cookie is available to this worker via CookieStore API. + wait_promise = new Promise(resolve => { + resolve_wait_promise = resolve; + }); + on_message = ev => { + got = ev.data; + resolve_wait_promise(); + }; + filtered_registrations[0].active.postMessage({type: 'echo_cookies_js'}); + await wait_promise; + assert_true(got.ok, 'Get cookies'); + assert_true(got.cookies.includes('__Host-partitioned'), 'Can access partitioned cookie via JS'); + assert_true(got.cookies.includes('unpartitioned'), 'Can access unpartitioned cookie via JS'); - // Test that the partitioned cookie is available to this worker. + // Test that the partitioned cookie is not available to this worker in HTTP + // requests from importScripts. wait_promise = new Promise(resolve => { resolve_wait_promise = resolve; }); @@ -67,10 +100,11 @@ got = ev.data; resolve_wait_promise(); }; - filtered_registrations[0].active.postMessage({type: 'echo_cookies'}); + filtered_registrations[0].active.postMessage({type: 'echo_cookies_import'}); await wait_promise; assert_true(got.ok, 'Get cookies'); - assert_true(got.cookies.includes('__Host-partitioned'), 'Can access partitioned cookie'); + assert_true(got.cookies.includes('__Host-partitioned'), 'Can access partitioned cookie via importScripts'); + assert_true(got.cookies.includes('unpartitioned'), 'Can access unpartitioned cookie via importScripts'); const popup = window.open( new URL( @@ -82,4 +116,4 @@ - \ No newline at end of file + diff --git a/test/wpt/tests/service-workers/service-worker/postMessage-client-worker.js b/test/wpt/tests/service-workers/service-worker/postMessage-client-worker.js new file mode 100644 index 00000000000..64d944d2b54 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/postMessage-client-worker.js @@ -0,0 +1,23 @@ +async function doTest(e) +{ + if (e.resultingClientId) { + const promise = new Promise(async resolve => { + let counter = 0; + const client = await self.clients.get(e.resultingClientId); + if (client) + client.postMessage(counter++); + if (e.request.url.includes("repeatMessage")) { + setInterval(() => { + if (client) + client.postMessage(counter++); + }, 100); + } + setTimeout(() => { + resolve(fetch(e.request)); + }, 1000); + }); + e.respondWith(promise); + } +} + +self.addEventListener("fetch", e => e.waitUntil(doTest(e))); diff --git a/test/wpt/tests/service-workers/service-worker/resource-timing.sub.https.html b/test/wpt/tests/service-workers/service-worker/resource-timing.sub.https.html index 9808ae5ae1b..e8328f3597b 100644 --- a/test/wpt/tests/service-workers/service-worker/resource-timing.sub.https.html +++ b/test/wpt/tests/service-workers/service-worker/resource-timing.sub.https.html @@ -114,9 +114,9 @@ }); verify({ performance: performance, - resource: 'resources/missing.jpg', + resource: 'resources/missing.asis', // ORB-compatible 404 response. mode: 'cross-origin', - description: 'Network fallback cross-origin load failure', + description: 'Network fallback cross-origin load failure (404 response)', }); // Tests for respondWith(fetch()). verify({ diff --git a/test/wpt/tests/service-workers/service-worker/resources/controlled-frame-postMessage.html b/test/wpt/tests/service-workers/service-worker/resources/controlled-frame-postMessage.html new file mode 100644 index 00000000000..c4428e88a3d --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/controlled-frame-postMessage.html @@ -0,0 +1,39 @@ + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/resources/controlled-worker-late-postMessage.js b/test/wpt/tests/service-workers/service-worker/resources/controlled-worker-late-postMessage.js new file mode 100644 index 00000000000..41d2db43b1f --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/controlled-worker-late-postMessage.js @@ -0,0 +1,6 @@ +setTimeout(() => { + navigator.serviceWorker.onmessage = e => self.postMessage(e.data); +}, 500); +setTimeout(() => { + self.postMessage("No message received"); +}, 5000); diff --git a/test/wpt/tests/service-workers/service-worker/resources/controlled-worker-postMessage.js b/test/wpt/tests/service-workers/service-worker/resources/controlled-worker-postMessage.js new file mode 100644 index 00000000000..628dc65db11 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/controlled-worker-postMessage.js @@ -0,0 +1,4 @@ +navigator.serviceWorker.onmessage = e => self.postMessage(e.data); +setTimeout(() => { + self.postMessage("No message received"); +}, 5000); diff --git a/test/wpt/tests/service-workers/service-worker/resources/fetch-access-control.py b/test/wpt/tests/service-workers/service-worker/resources/fetch-access-control.py index 446af87b249..380a7d62225 100644 --- a/test/wpt/tests/service-workers/service-worker/resources/fetch-access-control.py +++ b/test/wpt/tests/service-workers/service-worker/resources/fetch-access-control.py @@ -35,8 +35,13 @@ def main(request, response): return headers, body if b"VIDEO" in request.GET: - headers.append((b"Content-Type", b"video/ogg")) - body = open(os.path.join(request.doc_root, u"media", u"movie_5.ogv"), "rb").read() + if b"mp4" in request.GET: + headers.append((b"Content-Type", b"video/mp4")) + body = open(os.path.join(request.doc_root, u"media", u"movie_5.mp4"), "rb").read() + else: + headers.append((b"Content-Type", b"video/ogg")) + body = open(os.path.join(request.doc_root, u"media", u"movie_5.ogv"), "rb").read() + length = len(body) # If "PartialContent" is specified, the requestor wants to test range # requests. For the initial request, respond with "206 Partial Content" diff --git a/test/wpt/tests/service-workers/service-worker/resources/missing.asis b/test/wpt/tests/service-workers/service-worker/resources/missing.asis new file mode 100644 index 00000000000..4846fe01d61 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/resources/missing.asis @@ -0,0 +1,4 @@ +HTTP/1.1 404 Not Found +Content-Type: text/javascript + +alert("hello"); diff --git a/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-credentialless-frame.html b/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-credentialless-frame.html index ff24bf3670c..25ddf601457 100644 --- a/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-credentialless-frame.html +++ b/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-credentialless-frame.html @@ -21,6 +21,9 @@ document.cookie = '__Host-partitioned=123; Secure; Path=/; SameSite=None; Partitioned;'; assert_true(document.cookie.includes('__Host-partitioned=123')); + // Make sure DOM cannot access the unpartitioned cookie. + assert_false(document.cookie.includes('unpartitioned=456')); + const reg = await service_worker_unregister_and_register(t, script, scope); await wait_for_state(t, reg.installing, 'activated'); @@ -50,7 +53,44 @@ await wait_promise; assert_true(got.ok, 'Message passing'); - // Test that the partitioned cookie is available to this worker. + // Test that the partitioned cookie is available to this worker via CookieStore API. + wait_promise = new Promise(resolve => { + resolve_wait_promise = resolve; + }); + on_message = ev => { + got = ev.data; + resolve_wait_promise(); + }; + filtered_registrations[0].active.postMessage({type: 'echo_cookies_js'}); + await wait_promise; + assert_true(got.ok, 'Get cookies'); + assert_true( + got.cookies.includes('__Host-partitioned'), + 'Credentialless frame worker can access partitioned cookie via JS'); + assert_false( + got.cookies.includes('unpartitioned'), + 'Credentialless frame worker cannot access unpartitioned cookie via JS'); + + // Test that the partitioned cookie is available to this worker via HTTP. + wait_promise = new Promise(resolve => { + resolve_wait_promise = resolve; + }); + on_message = ev => { + got = ev.data; + resolve_wait_promise(); + }; + filtered_registrations[0].active.postMessage({ type: 'echo_cookies_http' }); + await wait_promise; + assert_true(got.ok, 'Get cookies'); + assert_true( + got.cookies.includes('__Host-partitioned'), + 'Credentialless frame worker can access partitioned cookie via HTTP'); + assert_false( + got.cookies.includes('unpartitioned'), + 'Credentialless frame worker cannot access unpartitioned cookie via HTTP'); + + // Test that the partitioned cookie is not available to this worker in HTTP + // requests from importScripts. wait_promise = new Promise(resolve => { resolve_wait_promise = resolve; }); @@ -58,12 +98,17 @@ got = ev.data; resolve_wait_promise(); }; - filtered_registrations[0].active.postMessage({type: 'echo_cookies'}); + filtered_registrations[0].active.postMessage({ type: 'echo_cookies_import' }); await wait_promise; assert_true(got.ok, 'Get cookies'); - assert_true(got.cookies.includes('__Host-partitioned'), 'Can access partitioned cookie');; + assert_true( + got.cookies.includes('__Host-partitioned'), + 'Credentialless frame worker can access partitioned cookie via importScripts'); + assert_false( + got.cookies.includes('unpartitioned'), + 'Credentialless frame worker cannot access unpartitioned cookie via importScripts'); }); - \ No newline at end of file + diff --git a/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-frame.html b/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-frame.html index d3962d2e600..00b3412c41f 100644 --- a/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-frame.html +++ b/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-frame.html @@ -16,6 +16,7 @@ const absolute_scope = new URL(scope, window.location).href; assert_false(document.cookie.includes('__Host-partitioned=123'), 'DOM cannot access partitioned cookie'); + assert_true(document.cookie.includes('unpartitioned=456'), 'DOM can access unpartitioned cookie'); const reg = await service_worker_unregister_and_register(t, script, scope); await wait_for_state(t, reg.installing, 'activated'); @@ -46,7 +47,7 @@ await wait_promise; assert_true(got.ok, 'Message passing'); - // Test that the partitioned cookie is not available to this worker. + // Test that the partitioned cookie is not available to this worker via HTTP. wait_promise = new Promise(resolve => { resolve_wait_promise = resolve; }); @@ -54,14 +55,54 @@ got = ev.data; resolve_wait_promise(); }; - filtered_registrations[0].active.postMessage({type: 'echo_cookies'}); + filtered_registrations[0].active.postMessage({type: 'echo_cookies_http'}); await wait_promise; assert_true(got.ok, 'Get cookies'); assert_false( got.cookies.includes('__Host-partitioned'), - 'Worker cannot access partitioned cookie'); + 'Worker cannot access partitioned cookie via HTTP'); + assert_true( + got.cookies.includes('unpartitioned'), + 'Worker can access unpartitioned cookie via HTTP'); + + // Test that the partitioned cookie is not available to this worker via CookieStore API. + wait_promise = new Promise(resolve => { + resolve_wait_promise = resolve; + }); + on_message = ev => { + got = ev.data; + resolve_wait_promise(); + }; + filtered_registrations[0].active.postMessage({type: 'echo_cookies_js'}); + await wait_promise; + assert_true(got.ok, 'Get cookies'); + assert_false( + got.cookies.includes('__Host-partitioned'), + 'Worker cannot access partitioned cookie via JS'); + assert_true( + got.cookies.includes('unpartitioned'), + 'Worker can access unpartitioned cookie via JS'); + + // Test that the partitioned cookie is not available to this worker in HTTP + // requests from importScripts. + wait_promise = new Promise(resolve => { + resolve_wait_promise = resolve; + }); + on_message = ev => { + got = ev.data; + resolve_wait_promise(); + }; + filtered_registrations[0].active.postMessage({type: 'echo_cookies_import'}); + await wait_promise; + assert_true(got.ok, 'Get cookies'); + assert_false( + got.cookies.includes('__Host-partitioned'), + 'Worker cannot access partitioned cookie via importScripts'); + assert_true( + got.cookies.includes('unpartitioned'), + 'Worker can access unpartitioned cookie via importScripts'); }); - \ No newline at end of file + diff --git a/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-sw.js b/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-sw.js index 2f54a984b19..767dbf44327 100644 --- a/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-sw.js +++ b/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-sw.js @@ -6,8 +6,12 @@ async function onMessage(event) { switch (event.data.type) { case 'test_message': return onTestMessage(event); - case 'echo_cookies': - return onEchoCookies(event); + case 'echo_cookies_http': + return onEchoCookiesHttp(event); + case 'echo_cookies_js': + return onEchoCookiesJs(event); + case 'echo_cookies_import': + return onEchoCookiesImport(event); default: return; } @@ -18,8 +22,19 @@ async function onTestMessage(event) { event.source.postMessage({ok: true}); } +async function onEchoCookiesHttp(event) { + try { + const resp = await fetch( + `${self.origin}/cookies/resources/list.py`, {credentials: 'include'}); + const cookies = await resp.json(); + event.source.postMessage({ok: true, cookies: Object.keys(cookies)}); + } catch (err) { + event.source.postMessage({ok: false}); + } +} + // echo_cookies returns the names of all of the cookies available to the worker. -async function onEchoCookies(event) { +async function onEchoCookiesJs(event) { try { const cookie_objects = await self.cookieStore.getAll(); const cookies = cookie_objects.map(c => c.name); @@ -28,3 +43,11 @@ async function onEchoCookies(event) { event.source.postMessage({ok: false}); } } + +// Sets `self._cookies` variable, array of the names of cookies available to +// the request. +importScripts(`${self.origin}/cookies/resources/list-cookies-for-script.py`); + +function onEchoCookiesImport(event) { + event.source.postMessage({ok: true, cookies: self._cookies}); +} diff --git a/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-window.html b/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-window.html index 8e90609da22..40d38b3f79f 100644 --- a/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-window.html +++ b/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-3p-window.html @@ -19,7 +19,7 @@ './partitioned-cookies-3p-frame.html', first_party_origin + location.pathname).href; document.body.appendChild(iframe); - fetch_tests_from_window(iframe.contentWindow); + await fetch_tests_from_window(iframe.contentWindow); const credentialless_frame = document.createElement('iframe'); credentialless_frame.credentialless = true; @@ -27,7 +27,7 @@ './partitioned-cookies-3p-credentialless-frame.html', first_party_origin + location.pathname).href; document.body.appendChild(credentialless_frame); - fetch_tests_from_window(credentialless_frame.contentWindow); + await fetch_tests_from_window(credentialless_frame.contentWindow); }); diff --git a/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-sw.js b/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-sw.js index 2f54a984b19..767dbf44327 100644 --- a/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-sw.js +++ b/test/wpt/tests/service-workers/service-worker/resources/partitioned-cookies-sw.js @@ -6,8 +6,12 @@ async function onMessage(event) { switch (event.data.type) { case 'test_message': return onTestMessage(event); - case 'echo_cookies': - return onEchoCookies(event); + case 'echo_cookies_http': + return onEchoCookiesHttp(event); + case 'echo_cookies_js': + return onEchoCookiesJs(event); + case 'echo_cookies_import': + return onEchoCookiesImport(event); default: return; } @@ -18,8 +22,19 @@ async function onTestMessage(event) { event.source.postMessage({ok: true}); } +async function onEchoCookiesHttp(event) { + try { + const resp = await fetch( + `${self.origin}/cookies/resources/list.py`, {credentials: 'include'}); + const cookies = await resp.json(); + event.source.postMessage({ok: true, cookies: Object.keys(cookies)}); + } catch (err) { + event.source.postMessage({ok: false}); + } +} + // echo_cookies returns the names of all of the cookies available to the worker. -async function onEchoCookies(event) { +async function onEchoCookiesJs(event) { try { const cookie_objects = await self.cookieStore.getAll(); const cookies = cookie_objects.map(c => c.name); @@ -28,3 +43,11 @@ async function onEchoCookies(event) { event.source.postMessage({ok: false}); } } + +// Sets `self._cookies` variable, array of the names of cookies available to +// the request. +importScripts(`${self.origin}/cookies/resources/list-cookies-for-script.py`); + +function onEchoCookiesImport(event) { + event.source.postMessage({ok: true, cookies: self._cookies}); +} diff --git a/test/wpt/tests/service-workers/service-worker/resources/resource-timing-iframe.sub.html b/test/wpt/tests/service-workers/service-worker/resources/resource-timing-iframe.sub.html index 384c29b536b..ec4c726331d 100644 --- a/test/wpt/tests/service-workers/service-worker/resources/resource-timing-iframe.sub.html +++ b/test/wpt/tests/service-workers/service-worker/resources/resource-timing-iframe.sub.html @@ -5,6 +5,6 @@ - + diff --git a/test/wpt/tests/service-workers/service-worker/tentative/static-router/README.md b/test/wpt/tests/service-workers/service-worker/tentative/static-router/README.md new file mode 100644 index 00000000000..8826b3c7827 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/tentative/static-router/README.md @@ -0,0 +1,4 @@ +A test stuite for the ServiceWorker Static Routing API. + +WICG proposal: https://github.com/WICG/proposals/issues/102 +Specification PR: https://github.com/w3c/ServiceWorker/pull/1686 diff --git a/test/wpt/tests/service-workers/service-worker/tentative/static-router/resources/direct.txt b/test/wpt/tests/service-workers/service-worker/tentative/static-router/resources/direct.txt new file mode 100644 index 00000000000..f3d9861c137 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/tentative/static-router/resources/direct.txt @@ -0,0 +1 @@ +Network diff --git a/test/wpt/tests/service-workers/service-worker/tentative/static-router/resources/simple-test-for-condition-main-resource.html b/test/wpt/tests/service-workers/service-worker/tentative/static-router/resources/simple-test-for-condition-main-resource.html new file mode 100644 index 00000000000..0c3e3e78707 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/tentative/static-router/resources/simple-test-for-condition-main-resource.html @@ -0,0 +1,3 @@ + +Simple +Here's a simple html file. diff --git a/test/wpt/tests/service-workers/service-worker/tentative/static-router/resources/simple.html b/test/wpt/tests/service-workers/service-worker/tentative/static-router/resources/simple.html new file mode 100644 index 00000000000..0c3e3e78707 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/tentative/static-router/resources/simple.html @@ -0,0 +1,3 @@ + +Simple +Here's a simple html file. diff --git a/test/wpt/tests/service-workers/service-worker/tentative/static-router/resources/static-router-sw.js b/test/wpt/tests/service-workers/service-worker/tentative/static-router/resources/static-router-sw.js new file mode 100644 index 00000000000..4655ab5321c --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/tentative/static-router/resources/static-router-sw.js @@ -0,0 +1,35 @@ +'use strict'; + +var requests = []; + +self.addEventListener('install', e => { + e.registerRouter([ + { + condition: {urlPattern: '/**/*.txt??*'}, + // Note: "??*" is for allowing arbitrary query strings. + // Upon my experiment, the URLPattern needs two '?'s for specifying + // a coming string as a query. + source: 'network' + }, { + condition: { + urlPattern: '/**/simple-test-for-condition-main-resource.html'}, + source: 'network' + }]); + self.skipWaiting(); +}); + +self.addEventListener('activate', e => { + e.waitUntil(clients.claim()); +}); + +self.addEventListener('fetch', function(event) { + requests.push({url: event.request.url, mode: event.request.mode}); + const url = new URL(event.request.url); + const nonce = url.searchParams.get('nonce'); + event.respondWith(new Response(nonce)); +}); + +self.addEventListener('message', function(event) { + event.data.port.postMessage({requests: requests}); + requests = []; +}); diff --git a/test/wpt/tests/service-workers/service-worker/tentative/static-router/resources/test-helpers.sub.js b/test/wpt/tests/service-workers/service-worker/tentative/static-router/resources/test-helpers.sub.js new file mode 100644 index 00000000000..64a7f7d24fd --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/tentative/static-router/resources/test-helpers.sub.js @@ -0,0 +1,303 @@ +// Copied from +// service-workers/service-worker/resources/testharness-helpers.js to be used under tentative. + +// Adapter for testharness.js-style tests with Service Workers + +/** + * @param options an object that represents RegistrationOptions except for scope. + * @param options.type a WorkerType. + * @param options.updateViaCache a ServiceWorkerUpdateViaCache. + * @see https://w3c.github.io/ServiceWorker/#dictdef-registrationoptions + */ +function service_worker_unregister_and_register(test, url, scope, options) { + if (!scope || scope.length == 0) + return Promise.reject(new Error('tests must define a scope')); + + if (options && options.scope) + return Promise.reject(new Error('scope must not be passed in options')); + + options = Object.assign({ scope: scope }, options); + return service_worker_unregister(test, scope) + .then(function() { + return navigator.serviceWorker.register(url, options); + }) + .catch(unreached_rejection(test, + 'unregister and register should not fail')); +} + +// This unregisters the registration that precisely matches scope. Use this +// when unregistering by scope. If no registration is found, it just resolves. +function service_worker_unregister(test, scope) { + var absoluteScope = (new URL(scope, window.location).href); + return navigator.serviceWorker.getRegistration(scope) + .then(function(registration) { + if (registration && registration.scope === absoluteScope) + return registration.unregister(); + }) + .catch(unreached_rejection(test, 'unregister should not fail')); +} + +function service_worker_unregister_and_done(test, scope) { + return service_worker_unregister(test, scope) + .then(test.done.bind(test)); +} + +function unreached_fulfillment(test, prefix) { + return test.step_func(function(result) { + var error_prefix = prefix || 'unexpected fulfillment'; + assert_unreached(error_prefix + ': ' + result); + }); +} + +// Rejection-specific helper that provides more details +function unreached_rejection(test, prefix) { + return test.step_func(function(error) { + var reason = error.message || error.name || error; + var error_prefix = prefix || 'unexpected rejection'; + assert_unreached(error_prefix + ': ' + reason); + }); +} + +/** + * Adds an iframe to the document and returns a promise that resolves to the + * iframe when it finishes loading. The caller is responsible for removing the + * iframe later if needed. + * + * @param {string} url + * @returns {HTMLIFrameElement} + */ +function with_iframe(url) { + return new Promise(function(resolve) { + var frame = document.createElement('iframe'); + frame.className = 'test-iframe'; + frame.src = url; + frame.onload = function() { resolve(frame); }; + document.body.appendChild(frame); + }); +} + +function normalizeURL(url) { + return new URL(url, self.location).toString().replace(/#.*$/, ''); +} + +function wait_for_update(test, registration) { + if (!registration || registration.unregister == undefined) { + return Promise.reject(new Error( + 'wait_for_update must be passed a ServiceWorkerRegistration')); + } + + return new Promise(test.step_func(function(resolve) { + var handler = test.step_func(function() { + registration.removeEventListener('updatefound', handler); + resolve(registration.installing); + }); + registration.addEventListener('updatefound', handler); + })); +} + +// Return true if |state_a| is more advanced than |state_b|. +function is_state_advanced(state_a, state_b) { + if (state_b === 'installing') { + switch (state_a) { + case 'installed': + case 'activating': + case 'activated': + case 'redundant': + return true; + } + } + + if (state_b === 'installed') { + switch (state_a) { + case 'activating': + case 'activated': + case 'redundant': + return true; + } + } + + if (state_b === 'activating') { + switch (state_a) { + case 'activated': + case 'redundant': + return true; + } + } + + if (state_b === 'activated') { + switch (state_a) { + case 'redundant': + return true; + } + } + return false; +} + +function wait_for_state(test, worker, state) { + if (!worker || worker.state == undefined) { + return Promise.reject(new Error( + 'wait_for_state needs a ServiceWorker object to be passed.')); + } + if (worker.state === state) + return Promise.resolve(state); + + if (is_state_advanced(worker.state, state)) { + return Promise.reject(new Error( + `Waiting for ${state} but the worker is already ${worker.state}.`)); + } + return new Promise(test.step_func(function(resolve, reject) { + worker.addEventListener('statechange', test.step_func(function() { + if (worker.state === state) + resolve(state); + + if (is_state_advanced(worker.state, state)) { + reject(new Error( + `The state of the worker becomes ${worker.state} while waiting` + + `for ${state}.`)); + } + })); + })); +} + +// Declare a test that runs entirely in the ServiceWorkerGlobalScope. The |url| +// is the service worker script URL. This function: +// - Instantiates a new test with the description specified in |description|. +// The test will succeed if the specified service worker can be successfully +// registered and installed. +// - Creates a new ServiceWorker registration with a scope unique to the current +// document URL. Note that this doesn't allow more than one +// service_worker_test() to be run from the same document. +// - Waits for the new worker to begin installing. +// - Imports tests results from tests running inside the ServiceWorker. +function service_worker_test(url, description) { + // If the document URL is https://example.com/document and the script URL is + // https://example.com/script/worker.js, then the scope would be + // https://example.com/script/scope/document. + var scope = new URL('scope' + window.location.pathname, + new URL(url, window.location)).toString(); + promise_test(function(test) { + return service_worker_unregister_and_register(test, url, scope) + .then(function(registration) { + add_completion_callback(function() { + registration.unregister(); + }); + return wait_for_update(test, registration) + .then(function(worker) { + return fetch_tests_from_worker(worker); + }); + }); + }, description); +} + +function base_path() { + return location.pathname.replace(/\/[^\/]*$/, '/'); +} + +function test_login(test, origin, username, password, cookie) { + return new Promise(function(resolve, reject) { + with_iframe( + origin + base_path() + + 'resources/fetch-access-control-login.html') + .then(test.step_func(function(frame) { + var channel = new MessageChannel(); + channel.port1.onmessage = test.step_func(function() { + frame.remove(); + resolve(); + }); + frame.contentWindow.postMessage( + {username: username, password: password, cookie: cookie}, + origin, [channel.port2]); + })); + }); +} + +function test_websocket(test, frame, url) { + return new Promise(function(resolve, reject) { + var ws = new frame.contentWindow.WebSocket(url, ['echo', 'chat']); + var openCalled = false; + ws.addEventListener('open', test.step_func(function(e) { + assert_equals(ws.readyState, 1, "The WebSocket should be open"); + openCalled = true; + ws.close(); + }), true); + + ws.addEventListener('close', test.step_func(function(e) { + assert_true(openCalled, "The WebSocket should be closed after being opened"); + resolve(); + }), true); + + ws.addEventListener('error', reject); + }); +} + +function login_https(test) { + var host_info = get_host_info(); + return test_login(test, host_info.HTTPS_REMOTE_ORIGIN, + 'username1s', 'password1s', 'cookie1') + .then(function() { + return test_login(test, host_info.HTTPS_ORIGIN, + 'username2s', 'password2s', 'cookie2'); + }); +} + +function websocket(test, frame) { + return test_websocket(test, frame, get_websocket_url()); +} + +function get_websocket_url() { + return 'wss://{{host}}:{{ports[wss][0]}}/echo'; +} + +// The navigator.serviceWorker.register() method guarantees that the newly +// installing worker is available as registration.installing when its promise +// resolves. However some tests test installation using a element where +// it is possible for the installing worker to have already become the waiting +// or active worker. So this method is used to get the newest worker when these +// tests need access to the ServiceWorker itself. +function get_newest_worker(registration) { + if (registration.installing) + return registration.installing; + if (registration.waiting) + return registration.waiting; + if (registration.active) + return registration.active; +} + +function register_using_link(script, options) { + var scope = options.scope; + var link = document.createElement('link'); + link.setAttribute('rel', 'serviceworker'); + link.setAttribute('href', script); + link.setAttribute('scope', scope); + document.getElementsByTagName('head')[0].appendChild(link); + return new Promise(function(resolve, reject) { + link.onload = resolve; + link.onerror = reject; + }) + .then(() => navigator.serviceWorker.getRegistration(scope)); +} + +function with_sandboxed_iframe(url, sandbox) { + return new Promise(function(resolve) { + var frame = document.createElement('iframe'); + frame.sandbox = sandbox; + frame.src = url; + frame.onload = function() { resolve(frame); }; + document.body.appendChild(frame); + }); +} + +// Registers, waits for activation, then unregisters on a sample scope. +// +// This can be used to wait for a period of time needed to register, +// activate, and then unregister a service worker. When checking that +// certain behavior does *NOT* happen, this is preferable to using an +// arbitrary delay. +async function wait_for_activation_on_sample_scope(t, window_or_workerglobalscope) { + const script = '/service-workers/service-worker/resources/empty-worker.js'; + const scope = 'resources/there/is/no/there/there?' + Date.now(); + let registration = await window_or_workerglobalscope.navigator.serviceWorker.register(script, { scope }); + await wait_for_state(t, registration.installing, 'activated'); + await registration.unregister(); +} + diff --git a/test/wpt/tests/service-workers/service-worker/tentative/static-router/static-router-main-resource.https.html b/test/wpt/tests/service-workers/service-worker/tentative/static-router/static-router-main-resource.https.html new file mode 100644 index 00000000000..5a55783af57 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/tentative/static-router/static-router-main-resource.https.html @@ -0,0 +1,58 @@ + + +Static Router: simply skip fetch handler for main resource if pattern matches + + + + + + + diff --git a/test/wpt/tests/service-workers/service-worker/tentative/static-router/static-router-subresource.https.html b/test/wpt/tests/service-workers/service-worker/tentative/static-router/static-router-subresource.https.html new file mode 100644 index 00000000000..721c2797603 --- /dev/null +++ b/test/wpt/tests/service-workers/service-worker/tentative/static-router/static-router-subresource.https.html @@ -0,0 +1,48 @@ + + +Static Router: simply skip fetch handler if pattern matches + + + + + + diff --git a/test/wpt/tests/storage/buckets/bucket-quota-indexeddb.tentative.https.any.js b/test/wpt/tests/storage/buckets/bucket-quota-indexeddb.tentative.https.any.js new file mode 100644 index 00000000000..ba82edb72ec --- /dev/null +++ b/test/wpt/tests/storage/buckets/bucket-quota-indexeddb.tentative.https.any.js @@ -0,0 +1,35 @@ +// META: title=Bucket quota enforcement for indexeddb +// META: script=/storage/buckets/resources/util.js + +promise_test(async t => { + const arraySize = 1e6; + const objectStoreName = "storageManager"; + const dbname = + this.window ? window.location.pathname : 'estimate-worker.https.html'; + + let quota = arraySize / 2; + const bucket = await navigator.storageBuckets.open('idb', {quota}); + + await indexedDbDeleteRequest(bucket.indexedDB, dbname); + + const db = + await indexedDbOpenRequest(t, bucket.indexedDB, dbname, (db_to_upgrade) => { + db_to_upgrade.createObjectStore(objectStoreName); + }); + + const txn = db.transaction(objectStoreName, 'readwrite'); + const buffer = new ArrayBuffer(arraySize); + const view = new Uint8Array(buffer); + + for (let i = 0; i < arraySize; i++) { + view[i] = Math.floor(Math.random() * 255); + } + + const testBlob = new Blob([buffer], {type: 'binary/random'}); + txn.objectStore(objectStoreName).add(testBlob, 1); + + await promise_rejects_dom( + t, 'QuotaExceededError', transactionPromise(txn)); + + db.close(); +}, 'IDB respects bucket quota'); diff --git a/test/wpt/tests/storage/buckets/bucket-storage-policy.tentative.https.any.js b/test/wpt/tests/storage/buckets/bucket-storage-policy.tentative.https.any.js new file mode 100644 index 00000000000..d6dce3675d0 --- /dev/null +++ b/test/wpt/tests/storage/buckets/bucket-storage-policy.tentative.https.any.js @@ -0,0 +1,21 @@ +// META: title=Buckets API: Tests for bucket storage policies. +// META: script=/storage/buckets/resources/util.js +// META: global=window,worker + +'use strict'; + +promise_test(async testCase => { + await prepareForBucketTest(testCase); + + await promise_rejects_js( + testCase, TypeError, + navigator.storageBuckets.open('negative', {quota: -1})); + + await promise_rejects_js( + testCase, TypeError, navigator.storageBuckets.open('zero', {quota: 0})); + + await promise_rejects_js( + testCase, TypeError, + navigator.storageBuckets.open( + 'above_max', {quota: Number.MAX_SAFE_INTEGER + 1})); +}, 'The open promise should reject with a TypeError when quota is requested outside the range of 1 to Number.MAX_SAFE_INTEGER.'); diff --git a/test/wpt/tests/storage/buckets/buckets_storage_policy.tentative.https.any.js b/test/wpt/tests/storage/buckets/buckets_storage_policy.tentative.https.any.js deleted file mode 100644 index a66fd81cd43..00000000000 --- a/test/wpt/tests/storage/buckets/buckets_storage_policy.tentative.https.any.js +++ /dev/null @@ -1,46 +0,0 @@ -// META: title=Buckets API: Tests for bucket storage policies. -// META: script=/storage/buckets/resources/util.js -// META: global=window,worker - -'use strict'; - -promise_test(async testCase => { - await prepareForBucketTest(testCase); - - await promise_rejects_js( - testCase, TypeError, - navigator.storageBuckets.open('negative', {quota: -1})); - - await promise_rejects_js( - testCase, TypeError, navigator.storageBuckets.open('zero', {quota: 0})); - - await promise_rejects_js( - testCase, TypeError, - navigator.storageBuckets.open( - 'above_max', {quota: Number.MAX_SAFE_INTEGER + 1})); -}, 'The open promise should reject with a TypeError when quota is requested outside the range of 1 to Number.MAX_SAFE_INTEGER.'); - - -promise_test(async testCase => { - await prepareForBucketTest(testCase); - - // IndexedDB - { - const quota = 1; - const bucket = await navigator.storageBuckets.open('idb', {quota}); - - const objectStoreName = 'store'; - const db = await indexedDbOpenRequest( - testCase, bucket.indexedDB, 'db', (db_to_upgrade) => { - db_to_upgrade.createObjectStore(objectStoreName); - }); - - const overflowBuffer = new Uint8Array(quota + 1); - - const txn = db.transaction(objectStoreName, 'readwrite'); - txn.objectStore(objectStoreName).add('', overflowBuffer); - - await promise_rejects_dom( - testCase, 'QuotaExceededError', transactionPromise(txn)); - } -}, 'A QuotaExceededError is thrown when a storage API exceeds the quota of the bucket its in.'); diff --git a/test/wpt/tests/storage/storagemanager-persist-persisted-match.https.any.js b/test/wpt/tests/storage/storagemanager-persist-persisted-match.https.any.js new file mode 100644 index 00000000000..edbe67fae2c --- /dev/null +++ b/test/wpt/tests/storage/storagemanager-persist-persisted-match.https.any.js @@ -0,0 +1,9 @@ +// META: title=StorageManager: result of persist() matches result of persisted() + +promise_test(async t => { + var persistResult = await navigator.storage.persist(); + assert_equals(typeof persistResult, 'boolean', persistResult + ' should be boolean'); + var persistedResult = await navigator.storage.persisted(); + assert_equals(typeof persistedResult, 'boolean', persistedResult + ' should be boolean'); + assert_equals(persistResult, persistedResult); +}, 'navigator.storage.persist() resolves to a value that matches navigator.storage.persisted()'); diff --git a/test/wpt/tests/websockets/back-forward-cache-with-closed-websocket-connection-ccns.tentative.window.js b/test/wpt/tests/websockets/back-forward-cache-with-closed-websocket-connection-ccns.tentative.window.js index ccc45f2877d..f6ee5edeaa9 100644 --- a/test/wpt/tests/websockets/back-forward-cache-with-closed-websocket-connection-ccns.tentative.window.js +++ b/test/wpt/tests/websockets/back-forward-cache-with-closed-websocket-connection-ccns.tentative.window.js @@ -24,11 +24,8 @@ promise_test(async t => { // The page should not be eligible for BFCache because of the usage // of WebSocket. await assertBFCacheEligibility(rc1, /*shouldRestoreFromBFCache=*/ false); - // The `BrowsingInstanceNotSwapped` reason will be added because of the - // sticky feature, and it will be reported as "Internal error". await assertNotRestoredFromBFCache(rc1, [ 'WebSocketSticky', - 'MainResourceHasCacheControlNoStore', - 'Internal error' + 'MainResourceHasCacheControlNoStore' ]); }); diff --git a/test/wpt/tests/websockets/back-forward-cache-with-open-websocket-connection-ccns.tentative.window.js b/test/wpt/tests/websockets/back-forward-cache-with-open-websocket-connection-ccns.tentative.window.js index 563fd4792ef..f37a04af91a 100644 --- a/test/wpt/tests/websockets/back-forward-cache-with-open-websocket-connection-ccns.tentative.window.js +++ b/test/wpt/tests/websockets/back-forward-cache-with-open-websocket-connection-ccns.tentative.window.js @@ -24,12 +24,9 @@ promise_test(async t => { // The page should not be eligible for BFCache because of the usage // of WebSocket. await assertBFCacheEligibility(rc1, /*shouldRestoreFromBFCache=*/ false); - // The `BrowsingInstanceNotSwapped` reason will be added because of the - // sticky feature, and it will be reported as "Internal error". await assertNotRestoredFromBFCache(rc1, [ 'WebSocket', 'WebSocketSticky', - 'MainResourceHasCacheControlNoStore', - 'Internal error' + 'MainResourceHasCacheControlNoStore' ]); }); diff --git a/test/wpt/tests/websockets/back-forward-cache-with-open-websocket-connection.window.js b/test/wpt/tests/websockets/back-forward-cache-with-open-websocket-connection.window.js index 2baf38f303c..6c48a570101 100644 --- a/test/wpt/tests/websockets/back-forward-cache-with-open-websocket-connection.window.js +++ b/test/wpt/tests/websockets/back-forward-cache-with-open-websocket-connection.window.js @@ -17,5 +17,5 @@ promise_test(async t => { await openWebSocket(rc1); // The page should not be eligible for BFCache because of open WebSocket connection. await assertBFCacheEligibility(rc1, /*shouldRestoreFromBFCache=*/ false); - await assertNotRestoredFromBFCache(rc1, ['WebSocket']); + await assertNotRestoredFromBFCache(rc1, ['websocket']); }); diff --git a/test/wpt/tests/xhr/blob-range.any.js b/test/wpt/tests/xhr/blob-range.any.js new file mode 100644 index 00000000000..2a5c54fc34f --- /dev/null +++ b/test/wpt/tests/xhr/blob-range.any.js @@ -0,0 +1,246 @@ +// See also /fetch/range/blob.any.js + +const supportedBlobRange = [ + { + name: "A simple blob range request.", + data: ["A simple Hello, World! example"], + type: "text/plain", + range: "bytes=9-21", + content_length: 13, + content_range: "bytes 9-21/30", + result: "Hello, World!", + }, + { + name: "A blob range request with no type.", + data: ["A simple Hello, World! example"], + type: undefined, + range: "bytes=9-21", + content_length: 13, + content_range: "bytes 9-21/30", + result: "Hello, World!", + }, + { + name: "A blob range request with no end.", + data: ["Range with no end"], + type: "text/plain", + range: "bytes=11-", + content_length: 6, + content_range: "bytes 11-16/17", + result: "no end", + }, + { + name: "A blob range request with no start.", + data: ["Range with no start"], + type: "text/plain", + range: "bytes=-8", + content_length: 8, + content_range: "bytes 11-18/19", + result: "no start", + }, + { + name: "A simple blob range request with whitespace.", + data: ["A simple Hello, World! example"], + type: "text/plain", + range: "bytes= \t9-21", + content_length: 13, + content_range: "bytes 9-21/30", + result: "Hello, World!", + }, + { + name: "Blob content with short content and a large range end", + data: ["Not much here"], + type: "text/plain", + range: "bytes=4-100000000000", + content_length: 9, + content_range: "bytes 4-12/13", + result: "much here", + }, + { + name: "Blob content with short content and a range end matching content length", + data: ["Not much here"], + type: "text/plain", + range: "bytes=4-13", + content_length: 9, + content_range: "bytes 4-12/13", + result: "much here", + }, + { + name: "Blob range with whitespace before and after hyphen", + data: ["Valid whitespace #1"], + type: "text/plain", + range: "bytes=5 - 10", + content_length: 6, + content_range: "bytes 5-10/19", + result: " white", + }, + { + name: "Blob range with whitespace after hyphen", + data: ["Valid whitespace #2"], + type: "text/plain", + range: "bytes=-\t 5", + content_length: 5, + content_range: "bytes 14-18/19", + result: "ce #2", + }, + { + name: "Blob range with whitespace around equals sign", + data: ["Valid whitespace #3"], + type: "text/plain", + range: "bytes \t =\t 6-", + content_length: 13, + content_range: "bytes 6-18/19", + result: "whitespace #3", + }, +]; + +const unsupportedBlobRange = [ + { + name: "Blob range with no value", + data: ["Blob range should have a value"], + type: "text/plain", + range: "", + }, + { + name: "Blob range with incorrect range header", + data: ["A"], + type: "text/plain", + range: "byte=0-" + }, + { + name: "Blob range with incorrect range header #2", + data: ["A"], + type: "text/plain", + range: "bytes" + }, + { + name: "Blob range with incorrect range header #3", + data: ["A"], + type: "text/plain", + range: "bytes\t \t" + }, + { + name: "Blob range request with multiple range values", + data: ["Multiple ranges are not currently supported"], + type: "text/plain", + range: "bytes=0-5,15-", + }, + { + name: "Blob range request with multiple range values and whitespace", + data: ["Multiple ranges are not currently supported"], + type: "text/plain", + range: "bytes=0-5, 15-", + }, + { + name: "Blob range request with trailing comma", + data: ["Range with invalid trailing comma"], + type: "text/plain", + range: "bytes=0-5,", + }, + { + name: "Blob range with no start or end", + data: ["Range with no start or end"], + type: "text/plain", + range: "bytes=-", + }, + { + name: "Blob range request with short range end", + data: ["Range end should be greater than range start"], + type: "text/plain", + range: "bytes=10-5", + }, + { + name: "Blob range start should be an ASCII digit", + data: ["Range start must be an ASCII digit"], + type: "text/plain", + range: "bytes=x-5", + }, + { + name: "Blob range should have a dash", + data: ["Blob range should have a dash"], + type: "text/plain", + range: "bytes=5", + }, + { + name: "Blob range end should be an ASCII digit", + data: ["Range end must be an ASCII digit"], + type: "text/plain", + range: "bytes=5-x", + }, + { + name: "Blob range should include '-'", + data: ["Range end must include '-'"], + type: "text/plain", + range: "bytes=x", + }, + { + name: "Blob range should include '='", + data: ["Range end must include '='"], + type: "text/plain", + range: "bytes 5-", + }, + { + name: "Blob range should include 'bytes='", + data: ["Range end must include 'bytes='"], + type: "text/plain", + range: "5-", + }, + { + name: "Blob content with short content and a large range start", + data: ["Not much here"], + type: "text/plain", + range: "bytes=100000-", + }, + { + name: "Blob content with short content and a range start matching the content length", + data: ["Not much here"], + type: "text/plain", + range: "bytes=13-", + }, +]; + +supportedBlobRange.forEach(({ name, data, type, range, content_length, content_range, result }) => { + promise_test(async t => { + const blob = new Blob(data, { "type" : type }); + const blobURL = URL.createObjectURL(blob); + t.add_cleanup(() => URL.revokeObjectURL(blobURL)); + const xhr = new XMLHttpRequest(); + xhr.open("GET", blobURL); + xhr.responseType = "text"; + xhr.setRequestHeader("Range", range); + await new Promise(resolve => { + xhr.onloadend = resolve; + xhr.send(); + }); + assert_equals(xhr.status, 206, "HTTP status is 206"); + assert_equals(xhr.getResponseHeader("Content-Type"), type || "", "Content-Type is " + xhr.getResponseHeader("Content-Type")); + assert_equals(xhr.getResponseHeader("Content-Length"), content_length.toString(), "Content-Length is " + xhr.getResponseHeader("Content-Length")); + assert_equals(xhr.getResponseHeader("Content-Range"), content_range, "Content-Range is " + xhr.getResponseHeader("Content-Range")); + assert_equals(xhr.responseText, result, "Response's body is correct"); + const all = xhr.getAllResponseHeaders().toLowerCase(); + assert_true(all.includes(`content-type: ${type || ""}`), "Expected Content-Type in getAllResponseHeaders()"); + assert_true(all.includes(`content-length: ${content_length}`), "Expected Content-Length in getAllResponseHeaders()"); + assert_true(all.includes(`content-range: ${content_range}`), "Expected Content-Range in getAllResponseHeaders()") + }, name); +}); + +unsupportedBlobRange.forEach(({ name, data, type, range }) => { + promise_test(t => { + const blob = new Blob(data, { "type" : type }); + const blobURL = URL.createObjectURL(blob); + t.add_cleanup(() => URL.revokeObjectURL(blobURL)); + + const xhr = new XMLHttpRequest(); + xhr.open("GET", blobURL, false); + xhr.setRequestHeader("Range", range); + assert_throws_dom("NetworkError", () => xhr.send()); + + xhr.open("GET", blobURL); + xhr.setRequestHeader("Range", range); + xhr.responseType = "text"; + return new Promise((resolve, reject) => { + xhr.onload = reject; + xhr.onerror = resolve; + xhr.send(); + }); + }, name); +}); diff --git a/test/wpt/tests/xhr/responsexml-invalid-type.html b/test/wpt/tests/xhr/responsexml-invalid-type.html new file mode 100644 index 00000000000..57ba462551f --- /dev/null +++ b/test/wpt/tests/xhr/responsexml-invalid-type.html @@ -0,0 +1,21 @@ + + + +XMLHttpRequest: response with an invalid responseXML document + + + + + + + diff --git a/test/wpt/tests/xhr/send-authentication-basic-cors-not-enabled.htm b/test/wpt/tests/xhr/send-authentication-basic-cors-not-enabled.htm index 2e7cbaf0340..41201960cc3 100644 --- a/test/wpt/tests/xhr/send-authentication-basic-cors-not-enabled.htm +++ b/test/wpt/tests/xhr/send-authentication-basic-cors-not-enabled.htm @@ -5,6 +5,7 @@ + @@ -13,10 +14,10 @@ +
+ @@ -16,9 +17,9 @@ var test = async_test(desc) test.step(function() { var client = new XMLHttpRequest(), - urlstart = location.host + location.pathname.replace(/\/[^\/]*$/, '/'), + urlstart = get_host_info().REMOTE_ORIGIN + location.pathname.replace(/\/[^\/]*$/, '/'), user = token() - client.open("GET", location.protocol + "//www1." + urlstart + "resources/" + pathsuffix, false) + client.open("GET", urlstart + "resources/" + pathsuffix, false) client.setRequestHeader("x-user", user) client.setRequestHeader("x-pass", 'pass') client.setRequestHeader("Authorization", "Basic " + btoa(user + ":pass")) diff --git a/test/wpt/tests/xhr/send-network-error-sync-events.sub.htm b/test/wpt/tests/xhr/send-network-error-sync-events.sub.htm index 2266eb36e1e..8011c58bdd1 100644 --- a/test/wpt/tests/xhr/send-network-error-sync-events.sub.htm +++ b/test/wpt/tests/xhr/send-network-error-sync-events.sub.htm @@ -17,7 +17,7 @@ { var xhr = new XMLHttpRequest(); - xhr.open("POST", "http://nonexistent.{{host}}:{{ports[http][0]}}", false); + xhr.open("POST", "http://{{host}}:1", false); // Bad port. assert_throws_dom("NetworkError", function() { From 43910c3c59e21014ae7866d80270d7701349dcd9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Sep 2023 23:03:44 +0000 Subject: [PATCH 107/259] build(deps): bump github/codeql-action from 2.21.2 to 2.21.5 (#2240) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2.21.2 to 2.21.5. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/0ba4244466797eb048eb91a6cd43d5c03ca8bd05...00e563ead9f72a8461b24876bee2d0c2e8bd2ee8) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 6 +++--- .github/workflows/scorecard.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index b921c4eebe4..998e5dce85d 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -50,7 +50,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@0ba4244466797eb048eb91a6cd43d5c03ca8bd05 # v2.3.3 + uses: github/codeql-action/init@00e563ead9f72a8461b24876bee2d0c2e8bd2ee8 # v2.3.3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -60,7 +60,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@0ba4244466797eb048eb91a6cd43d5c03ca8bd05 # v2.3.3 + uses: github/codeql-action/autobuild@00e563ead9f72a8461b24876bee2d0c2e8bd2ee8 # v2.3.3 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -73,6 +73,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@0ba4244466797eb048eb91a6cd43d5c03ca8bd05 # v2.3.3 + uses: github/codeql-action/analyze@00e563ead9f72a8461b24876bee2d0c2e8bd2ee8 # v2.3.3 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 040de504a44..ff3df9e365b 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -51,6 +51,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@0ba4244466797eb048eb91a6cd43d5c03ca8bd05 # v2.21.2 + uses: github/codeql-action/upload-sarif@00e563ead9f72a8461b24876bee2d0c2e8bd2ee8 # v2.21.5 with: sarif_file: results.sarif From 08c29140f276121b3f2e730cc7eb6e5a2e5d71cf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Sep 2023 23:05:16 +0000 Subject: [PATCH 108/259] build(deps): bump actions/setup-node from 3.6.0 to 3.8.1 (#2237) Bumps [actions/setup-node](https://github.com/actions/setup-node) from 3.6.0 to 3.8.1. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c...5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d) --- updated-dependencies: - dependency-name: actions/setup-node dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/bench.yml | 4 ++-- .github/workflows/fuzz.yml | 2 +- .github/workflows/lint.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index bff3c5ef7d2..f1b3e5047da 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -17,7 +17,7 @@ jobs: persist-credentials: false ref: ${{ github.base_ref }} - name: Setup Node - uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0 + uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 with: node-version: lts/* - name: Install Modules @@ -34,7 +34,7 @@ jobs: with: persist-credentials: false - name: Setup Node - uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0 + uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 with: node-version: lts/* - name: Install Modules diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 85247144889..7a868ea9400 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -16,7 +16,7 @@ jobs: persist-credentials: false - name: Setup Node - uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0 + uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 with: node-version: lts/* diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e00f3fe7bf0..ff6ac45a24a 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -10,7 +10,7 @@ jobs: - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 with: persist-credentials: false - - uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0 + - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 with: node-version: lts/* - run: npm install From faf836c445c18580c3da9fd50a9fb2ae41841b53 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Sep 2023 23:07:29 +0000 Subject: [PATCH 109/259] build(deps): bump fastify/github-action-merge-dependabot (#2236) Bumps [fastify/github-action-merge-dependabot](https://github.com/fastify/github-action-merge-dependabot) from 3.9.0 to 3.9.1. - [Release notes](https://github.com/fastify/github-action-merge-dependabot/releases) - [Commits](https://github.com/fastify/github-action-merge-dependabot/compare/d37100b180dfd816bb1d7e4fbb544b3c734957a1...59fc8817458fac20df8884576cfe69dbb77c9a07) --- updated-dependencies: - dependency-name: fastify/github-action-merge-dependabot dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/nodejs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 1c41627e289..27533ab77bf 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -34,6 +34,6 @@ jobs: pull-requests: write contents: write steps: - - uses: fastify/github-action-merge-dependabot@d37100b180dfd816bb1d7e4fbb544b3c734957a1 # v3.9.0 + - uses: fastify/github-action-merge-dependabot@59fc8817458fac20df8884576cfe69dbb77c9a07 # v3.9.1 with: github-token: ${{ secrets.GITHUB_TOKEN }} From 58cfdeac9a7a764e2964c36aeee3f6761c8acc2b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Sep 2023 23:08:47 +0000 Subject: [PATCH 110/259] build(deps): bump actions/checkout from 3.5.3 to 3.6.0 (#2241) Bumps [actions/checkout](https://github.com/actions/checkout) from 3.5.3 to 3.6.0. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/c85c95e3d7251135ab7dc9ce3241c5835cc595a9...f43a0e5ff2bd294095638e18286ca9a3d1956744) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/bench.yml | 4 ++-- .github/workflows/codeql.yml | 2 +- .github/workflows/dependency-review.yml | 2 +- .github/workflows/fuzz.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/scorecard.yml | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index f1b3e5047da..295e44b2c55 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Code - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 with: persist-credentials: false ref: ${{ github.base_ref }} @@ -30,7 +30,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Code - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 with: persist-credentials: false - name: Setup Node diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 998e5dce85d..520750ff88a 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -46,7 +46,7 @@ jobs: egress-policy: audit - name: Checkout repository - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index b95c4b9752b..8413f8ce4f4 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -22,6 +22,6 @@ jobs: egress-policy: audit - name: 'Checkout Repository' - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - name: 'Dependency Review' uses: actions/dependency-review-action@1360a344ccb0ab6e9475edef90ad2f46bf8003b1 # v3.0.6 diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 7a868ea9400..c2c918d3544 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 with: persist-credentials: false diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index ff6ac45a24a..62645af1bc0 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -7,7 +7,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 with: persist-credentials: false - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index ff3df9e365b..f641f9512b8 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -29,7 +29,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 with: persist-credentials: false From 68e22750f90af164fa83b502a4d315f838b2d59e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Sep 2023 23:18:26 +0000 Subject: [PATCH 111/259] build(deps): bump actions/dependency-review-action from 3.0.6 to 3.0.8 (#2238) Bumps [actions/dependency-review-action](https://github.com/actions/dependency-review-action) from 3.0.6 to 3.0.8. - [Release notes](https://github.com/actions/dependency-review-action/releases) - [Commits](https://github.com/actions/dependency-review-action/compare/1360a344ccb0ab6e9475edef90ad2f46bf8003b1...f6fff72a3217f580d5afd49a46826795305b63c7) --- updated-dependencies: - dependency-name: actions/dependency-review-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/dependency-review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 8413f8ce4f4..eb24b0d0d6f 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -24,4 +24,4 @@ jobs: - name: 'Checkout Repository' uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 - name: 'Dependency Review' - uses: actions/dependency-review-action@1360a344ccb0ab6e9475edef90ad2f46bf8003b1 # v3.0.6 + uses: actions/dependency-review-action@f6fff72a3217f580d5afd49a46826795305b63c7 # v3.0.8 From 6653fca44dc84bf2e9648a09a94ee45acb8593da Mon Sep 17 00:00:00 2001 From: Khafra Date: Sun, 3 Sep 2023 04:44:39 -0400 Subject: [PATCH 112/259] fix: aborting request with non-object error (#2243) --- index.js | 5 ++++- test/fetch/issue-2242.js | 8 ++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 test/fetch/issue-2242.js diff --git a/index.js b/index.js index 7e8831ceeea..7c0c8adcd6c 100644 --- a/index.js +++ b/index.js @@ -106,7 +106,10 @@ if (util.nodeMajor > 16 || (util.nodeMajor === 16 && util.nodeMinor >= 8)) { try { return await fetchImpl(...arguments) } catch (err) { - Error.captureStackTrace(err, this) + if (typeof err === 'object') { + Error.captureStackTrace(err, this) + } + throw err } } diff --git a/test/fetch/issue-2242.js b/test/fetch/issue-2242.js new file mode 100644 index 00000000000..fe704123e9a --- /dev/null +++ b/test/fetch/issue-2242.js @@ -0,0 +1,8 @@ +'use strict' + +const { test } = require('tap') +const { fetch } = require('../..') + +test('fetch with signal already aborted', async (t) => { + await t.rejects(fetch('http://localhost', { signal: AbortSignal.abort('Already aborted') }), 'Already aborted') +}) From 92ddf0ca7716796e1fc13b5342eb4320f69ba721 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20W=C3=A4rting?= Date: Mon, 4 Sep 2023 16:02:23 +0200 Subject: [PATCH 113/259] fix: preserve file path when parsing formdata (#2245) --- lib/fetch/body.js | 1 + test/fetch/client-fetch.js | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/lib/fetch/body.js b/lib/fetch/body.js index 11b945d18bf..105eb553157 100644 --- a/lib/fetch/body.js +++ b/lib/fetch/body.js @@ -387,6 +387,7 @@ function bodyMixinMethods (instance) { try { busboy = Busboy({ headers, + preservePath: true, defParamCharset: 'utf8' }) } catch (err) { diff --git a/test/fetch/client-fetch.js b/test/fetch/client-fetch.js index 048b76a9f78..9009d547ec6 100644 --- a/test/fetch/client-fetch.js +++ b/test/fetch/client-fetch.js @@ -272,6 +272,18 @@ test('busboy emit error', async (t) => { await t.rejects(res.formData(), 'Unexpected end of multipart data') }) +// https://github.com/nodejs/undici/issues/2244 +test('parsing formData preserve full path on files', async (t) => { + t.plan(1) + const formData = new FormData() + formData.append('field1', new File(['foo'], 'a/b/c/foo.txt')) + + const tempRes = new Response(formData) + const form = await tempRes.formData() + + t.equal(form.get('field1').name, 'a/b/c/foo.txt') +}) + test('urlencoded formData', (t) => { t.plan(2) From 83cb522ae0157a19d149d72c7d03d46e34510d0a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Sep 2023 22:26:28 +0000 Subject: [PATCH 114/259] build(deps-dev): bump tsd from 0.28.1 to 0.29.0 (#2246) Bumps [tsd](https://github.com/SamVerschueren/tsd) from 0.28.1 to 0.29.0. - [Release notes](https://github.com/SamVerschueren/tsd/releases) - [Commits](https://github.com/SamVerschueren/tsd/compare/v0.28.1...v0.29.0) --- updated-dependencies: - dependency-name: tsd dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 48f15ef1ee1..969aaf6680f 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,7 @@ "standard": "^17.0.0", "table": "^6.8.0", "tap": "^16.1.0", - "tsd": "^0.28.1", + "tsd": "^0.29.0", "typescript": "^5.0.2", "wait-on": "^7.0.1", "ws": "^8.11.0" From 932e5f681fd59af7da9fd3090cbef10ef8a0da97 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Thu, 7 Sep 2023 14:01:43 +0200 Subject: [PATCH 115/259] Updated benchmarks (#2250) * Updated benchmarks Signed-off-by: Matteo Collina * fixup Signed-off-by: Matteo Collina --------- Signed-off-by: Matteo Collina --- README.md | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 05a5d21ed11..e6c3552ade4 100644 --- a/README.md +++ b/README.md @@ -18,30 +18,34 @@ npm i undici ## Benchmarks The benchmark is a simple `hello world` [example](benchmarks/benchmark.js) using a -number of unix sockets (connections) with a pipelining depth of 10 running on Node 16. -The benchmarks below have the [simd](https://github.com/WebAssembly/simd) feature enabled. +number of unix sockets (connections) with a pipelining depth of 10 running on Node 20.6.0. ### Connections 1 -| Tests | Samples | Result | Tolerance | Difference with slowest | -|---------------------|---------|---------------|-----------|-------------------------| -| http - no keepalive | 15 | 4.63 req/sec | ± 2.77 % | - | -| http - keepalive | 10 | 4.81 req/sec | ± 2.16 % | + 3.94 % | -| undici - stream | 25 | 62.22 req/sec | ± 2.67 % | + 1244.58 % | -| undici - dispatch | 15 | 64.33 req/sec | ± 2.47 % | + 1290.24 % | -| undici - request | 15 | 66.08 req/sec | ± 2.48 % | + 1327.88 % | -| undici - pipeline | 10 | 66.13 req/sec | ± 1.39 % | + 1329.08 % | + +│ Tests │ Samples │ Result │ Tolerance │ Difference with slowest │ +|─────────────────────|─────────|───────────────|───────────|─────────────────────────| +│ http - no keepalive │ 15 │ 5.32 req/sec │ ± 2.61 % │ - │ +│ http - keepalive │ 10 │ 5.35 req/sec │ ± 2.47 % │ + 0.44 % │ +│ undici - fetch │ 15 │ 41.85 req/sec │ ± 2.49 % │ + 686.04 % │ +│ undici - pipeline │ 40 │ 50.36 req/sec │ ± 2.77 % │ + 845.92 % │ +│ undici - stream │ 15 │ 60.58 req/sec │ ± 2.75 % │ + 1037.72 % │ +│ undici - request │ 10 │ 61.19 req/sec │ ± 2.60 % │ + 1049.24 % │ +│ undici - dispatch │ 20 │ 64.84 req/sec │ ± 2.81 % │ + 1117.81 % │ + ### Connections 50 -| Tests | Samples | Result | Tolerance | Difference with slowest | -|---------------------|---------|------------------|-----------|-------------------------| -| http - no keepalive | 50 | 3546.49 req/sec | ± 2.90 % | - | -| http - keepalive | 15 | 5692.67 req/sec | ± 2.48 % | + 60.52 % | -| undici - pipeline | 25 | 8478.71 req/sec | ± 2.62 % | + 139.07 % | -| undici - request | 20 | 9766.66 req/sec | ± 2.79 % | + 175.39 % | -| undici - stream | 15 | 10109.74 req/sec | ± 2.94 % | + 185.06 % | -| undici - dispatch | 25 | 10949.73 req/sec | ± 2.54 % | + 208.75 % | +│ Tests │ Samples │ Result │ Tolerance │ Difference with slowest │ +|─────────────────────|─────────|──────────────────|───────────|─────────────────────────| +│ undici - fetch │ 30 │ 2107.19 req/sec │ ± 2.69 % │ - │ +│ http - no keepalive │ 10 │ 2698.90 req/sec │ ± 2.68 % │ + 28.08 % │ +│ http - keepalive │ 10 │ 4639.49 req/sec │ ± 2.55 % │ + 120.17 % │ +│ undici - pipeline │ 40 │ 6123.33 req/sec │ ± 2.97 % │ + 190.59 % │ +│ undici - stream │ 50 │ 9426.51 req/sec │ ± 2.92 % │ + 347.35 % │ +│ undici - request │ 10 │ 10162.88 req/sec │ ± 2.13 % │ + 382.29 % │ +│ undici - dispatch │ 50 │ 11191.11 req/sec │ ± 2.98 % │ + 431.09 % │ + ## Quick Start From b95f3e11316bb66f5eed1afc1b177f77b80d7edb Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Fri, 8 Sep 2023 13:38:28 +0200 Subject: [PATCH 116/259] Fix fetch in node v20.6.0 (#2251) --- lib/core/util.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/core/util.js b/lib/core/util.js index 4f8c1f8f1a1..88e34a90123 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -355,6 +355,12 @@ function getSocketInfo (socket) { } } +async function * convertIterableToBuffer (iterable) { + for await (const chunk of iterable) { + yield Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk) + } +} + let ReadableStream function ReadableStreamFrom (iterable) { if (!ReadableStream) { @@ -362,8 +368,7 @@ function ReadableStreamFrom (iterable) { } if (ReadableStream.from) { - // https://github.com/whatwg/streams/pull/1083 - return ReadableStream.from(iterable) + return ReadableStream.from(convertIterableToBuffer(iterable)) } let iterator From ea4f25752a0d42de546f2e2b119faeadbcb191e5 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Fri, 8 Sep 2023 13:39:01 +0200 Subject: [PATCH 117/259] Maybe fix v20 (#2252) --- test/utils/redirecting-servers.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/test/utils/redirecting-servers.js b/test/utils/redirecting-servers.js index db7a4dd6ded..53d674c4862 100644 --- a/test/utils/redirecting-servers.js +++ b/test/utils/redirecting-servers.js @@ -2,6 +2,17 @@ const { createServer } = require('http') +function close (server) { + return function () { + return new Promise(resolve => { + if (typeof server.closeAllConnections === 'function') { + server.closeAllConnections() + } + server.close(resolve) + }) + } +} + function startServer (t, handler) { return new Promise(resolve => { const server = createServer(handler) @@ -10,7 +21,7 @@ function startServer (t, handler) { resolve(`localhost:${server.address().port}`) }) - t.teardown(server.close.bind(server)) + t.teardown(close(server)) }) } From a8a5d0a3b8638c9b0a65b5adb4defe0e9bc4c63e Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Fri, 8 Sep 2023 14:42:10 +0200 Subject: [PATCH 118/259] feat: Add H2 support (#2061) * feat: port H2 work with latest main * fix: linting errors * refactor: adjust support for headers and set testing * test: add testing for h2 * refactor: make http2 session handle shorter * feat: add support for sending body over http2 * feat: ensure support for streams over H2 * refactor: remove noisy logs * feat: support 100 continue * feat: support for iterators * feat: add support for Blobs * refactor: adapt contracts to h2 support * refactor: cleanup * feat: support for content-length * refactor: body write * test: refactor check continue test * fix: bad check for headers * fix: bad change * chore: add http2 alpn test (#34) * chore: add http2 alpn test using fastify * chore: update to test https 1 with http2 * chore: update alpn test to return server request alpn protocol and http version * chore: add alpn with body * fix: remove fastify from package json * refactor: remove leftover * test: ensure dispatch feature * feat(h2): support connect * fix: pass signal down the road * test: ensure stream works as expected * test: ensure pipeline works as expected * test: ensure upgrade fails * test: ensure destroy works as expected * feat: allow to disable H2 calls upon request * fix: linting * feat: support GOAWAY frame (server-side) * refactor; use h2 constants * feat: initial shape of concurrent stream handling * refactor: header processing * chore: http/2 benchmark (#35) Co-authored-by: Carlos Fuentes * refactor: adjust accordingly to review * fix: add missing error handler for socket * refactor: headers handling * feat: initial concurrent stream support * fix: lint * refactor: adjust several pieces * fix: support h2 headers for fetch * feat: enhance h2 for fetch * refactor: apply review suggestions Co-authored-by: Robert Nagy * refactor: set allowh2 to false * fix: linting * refactor: implement kHTTPConnVersion symbol * test: adjust testing * feat: buil factory * fix: rebase * feat: enhance TS types for maxConcurrent streams * test: move fetch tests to fetch folder * feat: add experimental warning * test: refactor suite * refactor: apply several changes * test: split tests between v20 and lower --------- Co-authored-by: Michael Kaufman <2073135+mkaufmaner@users.noreply.github.com> Co-authored-by: Robert Nagy Co-authored-by: Matteo Collina --- benchmarks/benchmark-http2.js | 306 +++++++++++ benchmarks/benchmark-https.js | 319 ++++++++++++ benchmarks/benchmark.js | 12 +- benchmarks/server-http2.js | 49 ++ benchmarks/server-https.js | 41 ++ docs/api/Client.md | 4 + docs/api/Dispatcher.md | 1 + lib/api/api-connect.js | 10 +- lib/api/api-request.js | 1 - lib/client.js | 557 ++++++++++++++++++-- lib/core/connect.js | 6 +- lib/core/request.js | 76 ++- lib/core/symbols.js | 8 +- lib/core/util.js | 4 + lib/fetch/index.js | 38 +- package.json | 2 +- test/fetch/encoding.js | 12 +- test/fetch/http2.js | 256 +++++++++ test/http2-alpn.js | 277 ++++++++++ test/http2.js | 945 +++++++++++++++++++++++++++++++++- types/client.d.ts | 10 + types/dispatcher.d.ts | 2 + 22 files changed, 2846 insertions(+), 90 deletions(-) create mode 100644 benchmarks/benchmark-http2.js create mode 100644 benchmarks/benchmark-https.js create mode 100644 benchmarks/server-http2.js create mode 100644 benchmarks/server-https.js create mode 100644 test/fetch/http2.js create mode 100644 test/http2-alpn.js diff --git a/benchmarks/benchmark-http2.js b/benchmarks/benchmark-http2.js new file mode 100644 index 00000000000..d8555de22b5 --- /dev/null +++ b/benchmarks/benchmark-http2.js @@ -0,0 +1,306 @@ +'use strict' + +const { connect } = require('http2') +const { createSecureContext } = require('tls') +const os = require('os') +const path = require('path') +const { readFileSync } = require('fs') +const { table } = require('table') +const { Writable } = require('stream') +const { WritableStream } = require('stream/web') +const { isMainThread } = require('worker_threads') + +const { Pool, Client, fetch, Agent, setGlobalDispatcher } = require('..') + +const ca = readFileSync(path.join(__dirname, '..', 'test', 'fixtures', 'ca.pem'), 'utf8') +const servername = 'agent1' + +const iterations = (parseInt(process.env.SAMPLES, 10) || 10) + 1 +const errorThreshold = parseInt(process.env.ERROR_THRESHOLD, 10) || 3 +const connections = parseInt(process.env.CONNECTIONS, 10) || 50 +const pipelining = parseInt(process.env.PIPELINING, 10) || 10 +const parallelRequests = parseInt(process.env.PARALLEL, 10) || 100 +const headersTimeout = parseInt(process.env.HEADERS_TIMEOUT, 10) || 0 +const bodyTimeout = parseInt(process.env.BODY_TIMEOUT, 10) || 0 +const dest = {} + +if (process.env.PORT) { + dest.port = process.env.PORT + dest.url = `https://localhost:${process.env.PORT}` +} else { + dest.url = 'https://localhost' + dest.socketPath = path.join(os.tmpdir(), 'undici.sock') +} + +const httpsBaseOptions = { + ca, + servername, + protocol: 'https:', + hostname: 'localhost', + method: 'GET', + path: '/', + query: { + frappucino: 'muffin', + goat: 'scone', + pond: 'moose', + foo: ['bar', 'baz', 'bal'], + bool: true, + numberKey: 256 + }, + ...dest +} + +const http2ClientOptions = { + secureContext: createSecureContext({ ca }), + servername +} + +const undiciOptions = { + path: '/', + method: 'GET', + headersTimeout, + bodyTimeout +} + +const Class = connections > 1 ? Pool : Client +const dispatcher = new Class(httpsBaseOptions.url, { + allowH2: true, + pipelining, + connections, + connect: { + rejectUnauthorized: false, + ca, + servername + }, + ...dest +}) + +setGlobalDispatcher(new Agent({ + allowH2: true, + pipelining, + connections, + connect: { + rejectUnauthorized: false, + ca, + servername + } +})) + +class SimpleRequest { + constructor (resolve) { + this.dst = new Writable({ + write (chunk, encoding, callback) { + callback() + } + }).on('finish', resolve) + } + + onConnect (abort) { } + + onHeaders (statusCode, headers, resume) { + this.dst.on('drain', resume) + } + + onData (chunk) { + return this.dst.write(chunk) + } + + onComplete () { + this.dst.end() + } + + onError (err) { + throw err + } +} + +function makeParallelRequests (cb) { + return Promise.all(Array.from(Array(parallelRequests)).map(() => new Promise(cb))) +} + +function printResults (results) { + // Sort results by least performant first, then compare relative performances and also printing padding + let last + + const rows = Object.entries(results) + // If any failed, put on the top of the list, otherwise order by mean, ascending + .sort((a, b) => (!a[1].success ? -1 : b[1].mean - a[1].mean)) + .map(([name, result]) => { + if (!result.success) { + return [name, result.size, 'Errored', 'N/A', 'N/A'] + } + + // Calculate throughput and relative performance + const { size, mean, standardError } = result + const relative = last !== 0 ? (last / mean - 1) * 100 : 0 + + // Save the slowest for relative comparison + if (typeof last === 'undefined') { + last = mean + } + + return [ + name, + size, + `${((connections * 1e9) / mean).toFixed(2)} req/sec`, + `± ${((standardError / mean) * 100).toFixed(2)} %`, + relative > 0 ? `+ ${relative.toFixed(2)} %` : '-' + ] + }) + + console.log(results) + + // Add the header row + rows.unshift(['Tests', 'Samples', 'Result', 'Tolerance', 'Difference with slowest']) + + return table(rows, { + columns: { + 0: { + alignment: 'left' + }, + 1: { + alignment: 'right' + }, + 2: { + alignment: 'right' + }, + 3: { + alignment: 'right' + }, + 4: { + alignment: 'right' + } + }, + drawHorizontalLine: (index, size) => index > 0 && index < size, + border: { + bodyLeft: '│', + bodyRight: '│', + bodyJoin: '│', + joinLeft: '|', + joinRight: '|', + joinJoin: '|' + } + }) +} + +const experiments = { + 'http2 - request' () { + return makeParallelRequests(resolve => { + connect(dest.url, http2ClientOptions, (session) => { + const headers = { + ':path': '/', + ':method': 'GET', + ':scheme': 'https', + ':authority': `localhost:${dest.port}` + } + + const request = session.request(headers) + + request.pipe( + new Writable({ + write (chunk, encoding, callback) { + callback() + } + }) + ).on('finish', resolve) + }) + }) + }, + 'undici - pipeline' () { + return makeParallelRequests(resolve => { + dispatcher + .pipeline(undiciOptions, data => { + return data.body + }) + .end() + .pipe( + new Writable({ + write (chunk, encoding, callback) { + callback() + } + }) + ) + .on('finish', resolve) + }) + }, + 'undici - request' () { + return makeParallelRequests(resolve => { + try { + dispatcher.request(undiciOptions).then(({ body }) => { + body + .pipe( + new Writable({ + write (chunk, encoding, callback) { + callback() + } + }) + ) + .on('error', (err) => { + console.log('undici - request - dispatcher.request - body - error', err) + }) + .on('finish', () => { + resolve() + }) + }) + } catch (err) { + console.error('undici - request - dispatcher.request - requestCount', err) + } + }) + }, + 'undici - stream' () { + return makeParallelRequests(resolve => { + return dispatcher + .stream(undiciOptions, () => { + return new Writable({ + write (chunk, encoding, callback) { + callback() + } + }) + }) + .then(resolve) + }) + }, + 'undici - dispatch' () { + return makeParallelRequests(resolve => { + dispatcher.dispatch(undiciOptions, new SimpleRequest(resolve)) + }) + } +} + +if (process.env.PORT) { + // fetch does not support the socket + experiments['undici - fetch'] = () => { + return makeParallelRequests(resolve => { + fetch(dest.url, {}).then(res => { + res.body.pipeTo(new WritableStream({ write () { }, close () { resolve() } })) + }).catch(console.log) + }) + } +} + +async function main () { + const { cronometro } = await import('cronometro') + + cronometro( + experiments, + { + iterations, + errorThreshold, + print: false + }, + (err, results) => { + if (err) { + throw err + } + + console.log(printResults(results)) + dispatcher.destroy() + } + ) +} + +if (isMainThread) { + main() +} else { + module.exports = main +} diff --git a/benchmarks/benchmark-https.js b/benchmarks/benchmark-https.js new file mode 100644 index 00000000000..a364f0a0c43 --- /dev/null +++ b/benchmarks/benchmark-https.js @@ -0,0 +1,319 @@ +'use strict' + +const https = require('https') +const os = require('os') +const path = require('path') +const { readFileSync } = require('fs') +const { table } = require('table') +const { Writable } = require('stream') +const { WritableStream } = require('stream/web') +const { isMainThread } = require('worker_threads') + +const { Pool, Client, fetch, Agent, setGlobalDispatcher } = require('..') + +const ca = readFileSync(path.join(__dirname, '..', 'test', 'fixtures', 'ca.pem'), 'utf8') +const servername = 'agent1' + +const iterations = (parseInt(process.env.SAMPLES, 10) || 10) + 1 +const errorThreshold = parseInt(process.env.ERROR_TRESHOLD, 10) || 3 +const connections = parseInt(process.env.CONNECTIONS, 10) || 50 +const pipelining = parseInt(process.env.PIPELINING, 10) || 10 +const parallelRequests = parseInt(process.env.PARALLEL, 10) || 100 +const headersTimeout = parseInt(process.env.HEADERS_TIMEOUT, 10) || 0 +const bodyTimeout = parseInt(process.env.BODY_TIMEOUT, 10) || 0 +const dest = {} + +if (process.env.PORT) { + dest.port = process.env.PORT + dest.url = `https://localhost:${process.env.PORT}` +} else { + dest.url = 'https://localhost' + dest.socketPath = path.join(os.tmpdir(), 'undici.sock') +} + +const httpsBaseOptions = { + ca, + servername, + protocol: 'https:', + hostname: 'localhost', + method: 'GET', + path: '/', + query: { + frappucino: 'muffin', + goat: 'scone', + pond: 'moose', + foo: ['bar', 'baz', 'bal'], + bool: true, + numberKey: 256 + }, + ...dest +} + +const httpsNoKeepAliveOptions = { + ...httpsBaseOptions, + agent: new https.Agent({ + keepAlive: false, + maxSockets: connections, + // rejectUnauthorized: false, + ca, + servername + }) +} + +const httpsKeepAliveOptions = { + ...httpsBaseOptions, + agent: new https.Agent({ + keepAlive: true, + maxSockets: connections, + // rejectUnauthorized: false, + ca, + servername + }) +} + +const undiciOptions = { + path: '/', + method: 'GET', + headersTimeout, + bodyTimeout +} + +const Class = connections > 1 ? Pool : Client +const dispatcher = new Class(httpsBaseOptions.url, { + pipelining, + connections, + connect: { + // rejectUnauthorized: false, + ca, + servername + }, + ...dest +}) + +setGlobalDispatcher(new Agent({ + pipelining, + connections, + connect: { + // rejectUnauthorized: false, + ca, + servername + } +})) + +class SimpleRequest { + constructor (resolve) { + this.dst = new Writable({ + write (chunk, encoding, callback) { + callback() + } + }).on('finish', resolve) + } + + onConnect (abort) { } + + onHeaders (statusCode, headers, resume) { + this.dst.on('drain', resume) + } + + onData (chunk) { + return this.dst.write(chunk) + } + + onComplete () { + this.dst.end() + } + + onError (err) { + throw err + } +} + +function makeParallelRequests (cb) { + return Promise.all(Array.from(Array(parallelRequests)).map(() => new Promise(cb))) +} + +function printResults (results) { + // Sort results by least performant first, then compare relative performances and also printing padding + let last + + const rows = Object.entries(results) + // If any failed, put on the top of the list, otherwise order by mean, ascending + .sort((a, b) => (!a[1].success ? -1 : b[1].mean - a[1].mean)) + .map(([name, result]) => { + if (!result.success) { + return [name, result.size, 'Errored', 'N/A', 'N/A'] + } + + // Calculate throughput and relative performance + const { size, mean, standardError } = result + const relative = last !== 0 ? (last / mean - 1) * 100 : 0 + + // Save the slowest for relative comparison + if (typeof last === 'undefined') { + last = mean + } + + return [ + name, + size, + `${((connections * 1e9) / mean).toFixed(2)} req/sec`, + `± ${((standardError / mean) * 100).toFixed(2)} %`, + relative > 0 ? `+ ${relative.toFixed(2)} %` : '-' + ] + }) + + console.log(results) + + // Add the header row + rows.unshift(['Tests', 'Samples', 'Result', 'Tolerance', 'Difference with slowest']) + + return table(rows, { + columns: { + 0: { + alignment: 'left' + }, + 1: { + alignment: 'right' + }, + 2: { + alignment: 'right' + }, + 3: { + alignment: 'right' + }, + 4: { + alignment: 'right' + } + }, + drawHorizontalLine: (index, size) => index > 0 && index < size, + border: { + bodyLeft: '│', + bodyRight: '│', + bodyJoin: '│', + joinLeft: '|', + joinRight: '|', + joinJoin: '|' + } + }) +} + +const experiments = { + 'https - no keepalive' () { + return makeParallelRequests(resolve => { + https.get(httpsNoKeepAliveOptions, res => { + res + .pipe( + new Writable({ + write (chunk, encoding, callback) { + callback() + } + }) + ) + .on('finish', resolve) + }) + }) + }, + 'https - keepalive' () { + return makeParallelRequests(resolve => { + https.get(httpsKeepAliveOptions, res => { + res + .pipe( + new Writable({ + write (chunk, encoding, callback) { + callback() + } + }) + ) + .on('finish', resolve) + }) + }) + }, + 'undici - pipeline' () { + return makeParallelRequests(resolve => { + dispatcher + .pipeline(undiciOptions, data => { + return data.body + }) + .end() + .pipe( + new Writable({ + write (chunk, encoding, callback) { + callback() + } + }) + ) + .on('finish', resolve) + }) + }, + 'undici - request' () { + return makeParallelRequests(resolve => { + dispatcher.request(undiciOptions).then(({ body }) => { + body + .pipe( + new Writable({ + write (chunk, encoding, callback) { + callback() + } + }) + ) + .on('finish', resolve) + }) + }) + }, + 'undici - stream' () { + return makeParallelRequests(resolve => { + return dispatcher + .stream(undiciOptions, () => { + return new Writable({ + write (chunk, encoding, callback) { + callback() + } + }) + }) + .then(resolve) + }) + }, + 'undici - dispatch' () { + return makeParallelRequests(resolve => { + dispatcher.dispatch(undiciOptions, new SimpleRequest(resolve)) + }) + } +} + +if (process.env.PORT) { + // fetch does not support the socket + experiments['undici - fetch'] = () => { + return makeParallelRequests(resolve => { + fetch(dest.url, {}).then(res => { + res.body.pipeTo(new WritableStream({ write () { }, close () { resolve() } })) + }).catch(console.log) + }) + } +} + +async function main () { + const { cronometro } = await import('cronometro') + + cronometro( + experiments, + { + iterations, + errorThreshold, + print: false + }, + (err, results) => { + if (err) { + throw err + } + + console.log(printResults(results)) + dispatcher.destroy() + } + ) +} + +if (isMainThread) { + main() +} else { + module.exports = main +} diff --git a/benchmarks/benchmark.js b/benchmarks/benchmark.js index 4cf129f7b98..5bf3d2ede4f 100644 --- a/benchmarks/benchmark.js +++ b/benchmarks/benchmark.js @@ -73,7 +73,13 @@ const dispatcher = new Class(httpBaseOptions.url, { ...dest }) -setGlobalDispatcher(new Agent({ pipelining, connections })) +setGlobalDispatcher(new Agent({ + pipelining, + connections, + connect: { + rejectUnauthorized: false + } +})) class SimpleRequest { constructor (resolve) { @@ -84,7 +90,7 @@ class SimpleRequest { }).on('finish', resolve) } - onConnect (abort) {} + onConnect (abort) { } onHeaders (statusCode, headers, resume) { this.dst.on('drain', resume) @@ -260,7 +266,7 @@ if (process.env.PORT) { experiments['undici - fetch'] = () => { return makeParallelRequests(resolve => { fetch(dest.url).then(res => { - res.body.pipeTo(new WritableStream({ write () {}, close () { resolve() } })) + res.body.pipeTo(new WritableStream({ write () { }, close () { resolve() } })) }).catch(console.log) }) } diff --git a/benchmarks/server-http2.js b/benchmarks/server-http2.js new file mode 100644 index 00000000000..0be99cd2fd7 --- /dev/null +++ b/benchmarks/server-http2.js @@ -0,0 +1,49 @@ +'use strict' + +const { unlinkSync, readFileSync } = require('fs') +const { createSecureServer } = require('http2') +const os = require('os') +const path = require('path') +const cluster = require('cluster') + +const key = readFileSync(path.join(__dirname, '..', 'test', 'fixtures', 'key.pem'), 'utf8') +const cert = readFileSync(path.join(__dirname, '..', 'test', 'fixtures', 'cert.pem'), 'utf8') + +const socketPath = path.join(os.tmpdir(), 'undici.sock') + +const port = process.env.PORT || socketPath +const timeout = parseInt(process.env.TIMEOUT, 10) || 1 +const workers = parseInt(process.env.WORKERS) || os.cpus().length + +const sessionTimeout = 600e3 // 10 minutes + +if (cluster.isPrimary) { + try { + unlinkSync(socketPath) + } catch (_) { + // Do nothing if the socket does not exist + } + + for (let i = 0; i < workers; i++) { + cluster.fork() + } +} else { + const buf = Buffer.alloc(64 * 1024, '_') + const server = createSecureServer( + { + key, + cert, + allowHTTP1: true, + sessionTimeout + }, + (req, res) => { + setTimeout(() => { + res.end(buf) + }, timeout) + } + ) + + server.keepAliveTimeout = 600e3 + + server.listen(port) +} diff --git a/benchmarks/server-https.js b/benchmarks/server-https.js new file mode 100644 index 00000000000..f0275d9cbcc --- /dev/null +++ b/benchmarks/server-https.js @@ -0,0 +1,41 @@ +'use strict' + +const { unlinkSync, readFileSync } = require('fs') +const { createServer } = require('https') +const os = require('os') +const path = require('path') +const cluster = require('cluster') + +const key = readFileSync(path.join(__dirname, '..', 'test', 'fixtures', 'key.pem'), 'utf8') +const cert = readFileSync(path.join(__dirname, '..', 'test', 'fixtures', 'cert.pem'), 'utf8') + +const socketPath = path.join(os.tmpdir(), 'undici.sock') + +const port = process.env.PORT || socketPath +const timeout = parseInt(process.env.TIMEOUT, 10) || 1 +const workers = parseInt(process.env.WORKERS) || os.cpus().length + +if (cluster.isPrimary) { + try { + unlinkSync(socketPath) + } catch (_) { + // Do nothing if the socket does not exist + } + + for (let i = 0; i < workers; i++) { + cluster.fork() + } +} else { + const buf = Buffer.alloc(64 * 1024, '_') + const server = createServer({ + key, + cert, + keepAliveTimeout: 600e3 + }, (req, res) => { + setTimeout(() => { + res.end(buf) + }, timeout) + }) + + server.listen(port) +} diff --git a/docs/api/Client.md b/docs/api/Client.md index fc7c5d26e8f..591fb97c44b 100644 --- a/docs/api/Client.md +++ b/docs/api/Client.md @@ -17,6 +17,8 @@ Returns: `Client` ### Parameter: `ClientOptions` +> ⚠️ Warning: The `H2` support is experimental. + * **bodyTimeout** `number | null` (optional) - Default: `300e3` - 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 300 seconds. * **headersTimeout** `number | null` (optional) - Default: `300e3` - The amount of time the parser will wait to receive the complete HTTP headers while not sending the request. Defaults to 300 seconds. * **keepAliveMaxTimeout** `number | null` (optional) - Default: `600e3` - The maximum allowed `keepAliveTimeout` when overridden by *keep-alive* hints from the server. Defaults to 10 minutes. @@ -30,6 +32,8 @@ Returns: `Client` * **interceptors** `{ Client: DispatchInterceptor[] }` - Default: `[RedirectInterceptor]` - A list of interceptors that are applied to the dispatch method. Additional logic can be applied (such as, but not limited to: 302 status code handling, authentication, cookies, compression and caching). Note that the behavior of interceptors is Experimental and might change at any given time. * **autoSelectFamily**: `boolean` (optional) - Default: depends on local Node version, on Node 18.13.0 and above is `false`. Enables a family autodetection algorithm that loosely implements section 5 of [RFC 8305](https://tools.ietf.org/html/rfc8305#section-5). See [here](https://nodejs.org/api/net.html#socketconnectoptions-connectlistener) for more details. This option is ignored if not supported by the current Node version. * **autoSelectFamilyAttemptTimeout**: `number` - Default: depends on local Node version, on Node 18.13.0 and above is `250`. The amount of time in milliseconds to wait for a connection attempt to finish before trying the next address when using the `autoSelectFamily` option. See [here](https://nodejs.org/api/net.html#socketconnectoptions-connectlistener) for more details. +* **allowH2**: `boolean` - Default: `false`. Enables support for H2 if the server has assigned bigger priority to it through ALPN negotiation. +* **maxConcurrentStreams**: `number` - Default: `100`. Dictates the maximum number of concurrent streams for a single H2 session. It can be overriden by a SETTINGS remote frame. #### Parameter: `ConnectOptions` diff --git a/docs/api/Dispatcher.md b/docs/api/Dispatcher.md index a50642948aa..a04be8cff1e 100644 --- a/docs/api/Dispatcher.md +++ b/docs/api/Dispatcher.md @@ -202,6 +202,7 @@ Returns: `Boolean` - `false` if dispatcher is busy and further dispatch calls wo * **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 300 seconds. * **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 300 seconds. * **throwOnError** `boolean` (optional) - Default: `false` - Whether Undici should throw an error upon receiving a 4xx or 5xx response from the server. +* **expectContinue** `boolean` (optional) - Default: `false` - For H2, it appends the expect: 100-continue header, and halts the request body until a 100-continue is received from the remote server #### Parameter: `DispatchHandler` diff --git a/lib/api/api-connect.js b/lib/api/api-connect.js index 0503b1a2f0e..fd2b6ad97a5 100644 --- a/lib/api/api-connect.js +++ b/lib/api/api-connect.js @@ -1,7 +1,7 @@ 'use strict' -const { InvalidArgumentError, RequestAbortedError, SocketError } = require('../core/errors') const { AsyncResource } = require('async_hooks') +const { InvalidArgumentError, RequestAbortedError, SocketError } = require('../core/errors') const util = require('../core/util') const { addSignal, removeSignal } = require('./abort-signal') @@ -50,7 +50,13 @@ class ConnectHandler extends AsyncResource { removeSignal(this) this.callback = null - const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) + + let headers = rawHeaders + // Indicates is an HTTP2Session + if (headers != null) { + headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) + } + this.runInAsyncScope(callback, null, null, { statusCode, headers, diff --git a/lib/api/api-request.js b/lib/api/api-request.js index 71d7e926b4c..f130ecc9867 100644 --- a/lib/api/api-request.js +++ b/lib/api/api-request.js @@ -95,7 +95,6 @@ class RequestHandler extends AsyncResource { this.callback = null this.res = body - if (callback !== null) { if (this.throwOnError && statusCode >= 400) { this.runInAsyncScope(getResolveErrorBodyCallback, null, diff --git a/lib/client.js b/lib/client.js index 7d9ec8d7c27..ebc9a6afc11 100644 --- a/lib/client.js +++ b/lib/client.js @@ -6,6 +6,8 @@ const assert = require('assert') const net = require('net') +const http2 = require('http2') +const { pipeline } = require('stream') const util = require('./core/util') const timers = require('./timers') const Request = require('./core/request') @@ -67,8 +69,30 @@ const { kDispatch, kInterceptors, kLocalAddress, - kMaxResponseSize + kMaxResponseSize, + kHTTPConnVersion, + // HTTP2 + kHost, + kHTTP2Session, + kHTTP2SessionState, + kHTTP2BuildRequest, + kHTTP2CopyHeaders, + kHTTP1BuildRequest } = require('./core/symbols') +const { + constants: { + HTTP2_HEADER_AUTHORITY, + HTTP2_HEADER_METHOD, + HTTP2_HEADER_PATH, + HTTP2_HEADER_CONTENT_LENGTH, + HTTP2_HEADER_EXPECT, + HTTP2_HEADER_STATUS + } +} = http2 + +// Experimental +let h2ExperimentalWarned = false + const FastBuffer = Buffer[Symbol.species] const kClosedResolve = Symbol('kClosedResolve') @@ -122,7 +146,10 @@ class Client extends DispatcherBase { localAddress, maxResponseSize, autoSelectFamily, - autoSelectFamilyAttemptTimeout + autoSelectFamilyAttemptTimeout, + // h2 + allowH2, + maxConcurrentStreams } = {}) { super() @@ -205,10 +232,20 @@ class Client extends DispatcherBase { throw new InvalidArgumentError('autoSelectFamilyAttemptTimeout must be a positive number') } + // h2 + if (allowH2 != null && typeof allowH2 !== 'boolean') { + throw new InvalidArgumentError('allowH2 must be a valid boolean value') + } + + if (maxConcurrentStreams != null && (typeof maxConcurrentStreams !== 'number' || maxConcurrentStreams < 1)) { + throw new InvalidArgumentError('maxConcurrentStreams must be a possitive integer, greater than 0') + } + if (typeof connect !== 'function') { connect = buildConnector({ ...tls, maxCachedSessions, + allowH2, socketPath, timeout: connectTimeout, ...(util.nodeHasAutoSelectFamily && autoSelectFamily ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined), @@ -240,6 +277,18 @@ class Client extends DispatcherBase { this[kMaxRequests] = maxRequestsPerClient this[kClosedResolve] = null this[kMaxResponseSize] = maxResponseSize > -1 ? maxResponseSize : -1 + this[kHTTPConnVersion] = 'h1' + + // HTTP/2 + this[kHTTP2Session] = null + this[kHTTP2SessionState] = !allowH2 + ? null + : { + // streams: null, // Fixed queue of streams - For future support of `push` + openStreams: 0, // Keep track of them to decide wether or not unref the session + maxConcurrentStreams: maxConcurrentStreams != null ? maxConcurrentStreams : 100 // Max peerConcurrentStreams for a Node h2 server + } + this[kHost] = `${this[kUrl].hostname}${this[kUrl].port ? `:${this[kUrl].port}` : ''}` // kQueue is built up of 3 sections separated by // the kRunningIdx and kPendingIdx indices. @@ -298,7 +347,9 @@ class Client extends DispatcherBase { [kDispatch] (opts, handler) { const origin = opts.origin || this[kUrl].origin - const request = new Request(origin, opts, handler) + const request = this[kHTTPConnVersion] === 'h2' + ? Request[kHTTP2BuildRequest](origin, opts, handler) + : Request[kHTTP1BuildRequest](origin, opts, handler) this[kQueue].push(request) if (this[kResuming]) { @@ -319,6 +370,8 @@ class Client extends DispatcherBase { } async [kClose] () { + // TODO: for H2 we need to gracefully flush the remaining enqueued + // request and close each stream. return new Promise((resolve) => { if (!this[kSize]) { resolve(null) @@ -345,6 +398,12 @@ class Client extends DispatcherBase { resolve() } + if (this[kHTTP2Session] != null) { + util.destroy(this[kHTTP2Session], err) + this[kHTTP2Session] = null + this[kHTTP2SessionState] = null + } + if (!this[kSocket]) { queueMicrotask(callback) } else { @@ -356,6 +415,64 @@ class Client extends DispatcherBase { } } +function onHttp2SessionError (err) { + assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID') + + this[kSocket][kError] = err + + onError(this[kClient], err) +} + +function onHttp2FrameError (type, code, id) { + const err = new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`) + + if (id === 0) { + this[kSocket][kError] = err + onError(this[kClient], err) + } +} + +function onHttp2SessionEnd () { + util.destroy(this, new SocketError('other side closed')) + util.destroy(this[kSocket], new SocketError('other side closed')) +} + +function onHTTP2GoAway (code) { + const client = this[kClient] + const err = new InformationalError(`HTTP/2: "GOAWAY" frame received with code ${code}`) + client[kSocket] = null + client[kHTTP2Session] = null + + if (client.destroyed) { + assert(this[kPending] === 0) + + // Fail entire queue. + const requests = client[kQueue].splice(client[kRunningIdx]) + for (let i = 0; i < requests.length; i++) { + const request = requests[i] + errorRequest(this, request, err) + } + } else if (client[kRunning] > 0) { + // Fail head of pipeline. + const request = client[kQueue][client[kRunningIdx]] + client[kQueue][client[kRunningIdx]++] = null + + errorRequest(client, request, err) + } + + client[kPendingIdx] = client[kRunningIdx] + + assert(client[kRunning] === 0) + + client.emit('disconnect', + client[kUrl], + [client], + err + ) + + resume(client) +} + const constants = require('./llhttp/constants') const createRedirectInterceptor = require('./interceptor/redirectInterceptor') const EMPTY_BUF = Buffer.alloc(0) @@ -946,16 +1063,18 @@ function onSocketReadable () { } function onSocketError (err) { - const { [kParser]: parser } = this + const { [kClient]: client, [kParser]: parser } = this assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID') - // On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded - // to the user. - if (err.code === 'ECONNRESET' && parser.statusCode && !parser.shouldKeepAlive) { - // We treat all incoming data so for as a valid response. - parser.onMessageComplete() - return + if (client[kHTTPConnVersion] !== 'h2') { + // On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded + // to the user. + if (err.code === 'ECONNRESET' && parser.statusCode && !parser.shouldKeepAlive) { + // We treat all incoming data so for as a valid response. + parser.onMessageComplete() + return + } } this[kError] = err @@ -984,27 +1103,31 @@ function onError (client, err) { } function onSocketEnd () { - const { [kParser]: parser } = this + const { [kParser]: parser, [kClient]: client } = this - if (parser.statusCode && !parser.shouldKeepAlive) { - // We treat all incoming data so far as a valid response. - parser.onMessageComplete() - return + if (client[kHTTPConnVersion] !== 'h2') { + if (parser.statusCode && !parser.shouldKeepAlive) { + // We treat all incoming data so far as a valid response. + parser.onMessageComplete() + return + } } util.destroy(this, new SocketError('other side closed', util.getSocketInfo(this))) } function onSocketClose () { - const { [kClient]: client } = this + const { [kClient]: client, [kParser]: parser } = this - if (!this[kError] && this[kParser].statusCode && !this[kParser].shouldKeepAlive) { - // We treat all incoming data so far as a valid response. - this[kParser].onMessageComplete() - } + if (client[kHTTPConnVersion] === 'h1' && parser) { + if (!this[kError] && parser.statusCode && !parser.shouldKeepAlive) { + // We treat all incoming data so far as a valid response. + parser.onMessageComplete() + } - this[kParser].destroy() - this[kParser] = null + this[kParser].destroy() + this[kParser] = null + } const err = this[kError] || new SocketError('closed', util.getSocketInfo(this)) @@ -1092,24 +1215,54 @@ async function connect (client) { return } - if (!llhttpInstance) { - llhttpInstance = await llhttpPromise - llhttpPromise = null - } - client[kConnecting] = false assert(socket) - socket[kNoRef] = false - socket[kWriting] = false - socket[kReset] = false - socket[kBlocking] = false - socket[kError] = null - socket[kParser] = new Parser(client, socket, llhttpInstance) - socket[kClient] = client + const isH2 = socket.alpnProtocol === 'h2' + if (isH2) { + if (!h2ExperimentalWarned) { + h2ExperimentalWarned = true + process.emitWarning('H2 support is experimental, expect them to change at any time.', { + code: 'UNDICI-H2' + }) + } + + const session = http2.connect(client[kUrl], { + createConnection: () => socket, + peerMaxConcurrentStreams: client[kHTTP2SessionState].maxConcurrentStreams + }) + + client[kHTTPConnVersion] = 'h2' + session[kClient] = client + session[kSocket] = socket + session.on('error', onHttp2SessionError) + session.on('frameError', onHttp2FrameError) + session.on('end', onHttp2SessionEnd) + session.on('goaway', onHTTP2GoAway) + session.on('close', onSocketClose) + session.unref() + + client[kHTTP2Session] = session + socket[kHTTP2Session] = session + } else { + if (!llhttpInstance) { + llhttpInstance = await llhttpPromise + llhttpPromise = null + } + + socket[kNoRef] = false + socket[kWriting] = false + socket[kReset] = false + socket[kBlocking] = false + socket[kParser] = new Parser(client, socket, llhttpInstance) + } + socket[kCounter] = 0 socket[kMaxRequests] = client[kMaxRequests] + socket[kClient] = client + socket[kError] = null + socket .on('error', onSocketError) .on('readable', onSocketReadable) @@ -1208,7 +1361,7 @@ function _resume (client, sync) { const socket = client[kSocket] - if (socket && !socket.destroyed) { + if (socket && !socket.destroyed && socket.alpnProtocol !== 'h2') { if (client[kSize] === 0) { if (!socket[kNoRef] && socket.unref) { socket.unref() @@ -1273,7 +1426,7 @@ function _resume (client, sync) { return } - if (!socket) { + if (!socket && !client[kHTTP2Session]) { connect(client) return } @@ -1334,6 +1487,11 @@ function _resume (client, sync) { } function write (client, request) { + if (client[kHTTPConnVersion] === 'h2') { + writeH2(client, client[kHTTP2Session], request) + return + } + const { body, method, path, host, upgrade, headers, blocking, reset } = request // https://tools.ietf.org/html/rfc7231#section-4.3.1 @@ -1489,9 +1647,286 @@ function write (client, request) { return true } -function writeStream ({ body, client, request, socket, contentLength, header, expectsPayload }) { +function writeH2 (client, session, request) { + const { body, method, path, host, upgrade, expectContinue, signal, headers: reqHeaders } = request + + let headers + if (typeof reqHeaders === 'string') headers = Request[kHTTP2CopyHeaders](reqHeaders.trim()) + else headers = reqHeaders + + if (upgrade) { + errorRequest(client, request, new Error('Upgrade not supported for H2')) + return false + } + + try { + // TODO(HTTP/2): Should we call onConnect immediately or on stream ready event? + request.onConnect((err) => { + if (request.aborted || request.completed) { + return + } + + errorRequest(client, request, err || new RequestAbortedError()) + }) + } catch (err) { + errorRequest(client, request, err) + } + + if (request.aborted) { + return false + } + + let stream + const h2State = client[kHTTP2SessionState] + + headers[HTTP2_HEADER_AUTHORITY] = host || client[kHost] + headers[HTTP2_HEADER_PATH] = path + + if (method === 'CONNECT') { + session.ref() + // we are already connected, streams are pending, first request + // will create a new stream. We trigger a request to create the stream and wait until + // `ready` event is triggered + stream = session.request(headers, { endStream: false, signal }) + + if (stream.id && !stream.pending) { + request.onUpgrade(null, null, stream) + ++h2State.openStreams + } else { + stream.once('ready', () => { + request.onUpgrade(null, null, stream) + ++h2State.openStreams + }) + } + + stream.once('close', () => { + h2State.openStreams -= 1 + // TODO(HTTP/2): unref only if current streams count is 0 + if (h2State.openStreams === 0) session.unref() + }) + + return true + } else { + headers[HTTP2_HEADER_METHOD] = method + } + + // https://tools.ietf.org/html/rfc7231#section-4.3.1 + // https://tools.ietf.org/html/rfc7231#section-4.3.2 + // https://tools.ietf.org/html/rfc7231#section-4.3.5 + + // Sending a payload body on a request that does not + // expect it can cause undefined behavior on some + // servers and corrupt connection state. Do not + // re-use the connection for further requests. + + const expectsPayload = ( + method === 'PUT' || + method === 'POST' || + method === 'PATCH' + ) + + if (body && typeof body.read === 'function') { + // Try to read EOF in order to get length. + body.read(0) + } + + let contentLength = util.bodyLength(body) + + if (contentLength == null) { + contentLength = request.contentLength + } + + if (contentLength === 0 || !expectsPayload) { + // https://tools.ietf.org/html/rfc7230#section-3.3.2 + // A user agent SHOULD NOT send a Content-Length header field when + // the request message does not contain a payload body and the method + // semantics do not anticipate such a body. + + contentLength = null + } + + if (request.contentLength != null && request.contentLength !== contentLength) { + if (client[kStrictContentLength]) { + errorRequest(client, request, new RequestContentLengthMismatchError()) + return false + } + + process.emitWarning(new RequestContentLengthMismatchError()) + } + + if (contentLength != null) { + assert(body, 'no body must not have content length') + headers[HTTP2_HEADER_CONTENT_LENGTH] = `${contentLength}` + } + + session.ref() + + if (expectContinue) { + headers[HTTP2_HEADER_EXPECT] = '100-continue' + /** + * @type {import('node:http2').ClientHttp2Stream} + */ + stream = session.request(headers, { endStream: false, signal }) + + stream.once('continue', writeBodyH2) + } else { + /** @type {import('node:http2').ClientHttp2Stream} */ + stream = session.request(headers, { endStream: false, signal }) + writeBodyH2() + } + + // Increment counter as we have new several streams open + ++h2State.openStreams + + stream.once('response', headers => { + if (request.onHeaders(Number(headers[HTTP2_HEADER_STATUS]), headers, stream.resume.bind(stream), '') === false) { + stream.pause() + } + }) + + stream.once('end', () => { + request.onComplete([]) + }) + + stream.on('data', (chunk) => { + if (request.onData(chunk) === false) stream.pause() + }) + + stream.once('close', () => { + h2State.openStreams -= 1 + // TODO(HTTP/2): unref only if current streams count is 0 + if (h2State.openStreams === 0) session.unref() + }) + + stream.once('error', function (err) { + if (client[kHTTP2Session] && !client[kHTTP2Session].destroyed && !this.closed && !this.destroyed) { + h2State.streams -= 1 + util.destroy(stream, err) + } + }) + + stream.once('frameError', (type, code) => { + const err = new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`) + errorRequest(client, request, err) + + if (client[kHTTP2Session] && !client[kHTTP2Session].destroyed && !this.closed && !this.destroyed) { + h2State.streams -= 1 + util.destroy(stream, err) + } + }) + + // stream.on('aborted', () => { + // // TODO(HTTP/2): Support aborted + // }) + + // stream.on('timeout', () => { + // // TODO(HTTP/2): Support timeout + // }) + + // stream.on('push', headers => { + // // TODO(HTTP/2): Suppor push + // }) + + // stream.on('trailers', headers => { + // // TODO(HTTP/2): Support trailers + // }) + + return true + + function writeBodyH2 () { + /* istanbul ignore else: assertion */ + if (!body) { + request.onRequestSent() + } else if (util.isBuffer(body)) { + assert(contentLength === body.byteLength, 'buffer body must have content length') + stream.cork() + stream.write(body) + stream.uncork() + request.onBodySent(body) + request.onRequestSent() + } else if (util.isBlobLike(body)) { + if (typeof body.stream === 'function') { + writeIterable({ + client, + request, + contentLength, + h2stream: stream, + expectsPayload, + body: body.stream(), + socket: client[kSocket], + header: '' + }) + } else { + writeBlob({ + body, + client, + request, + contentLength, + expectsPayload, + h2stream: stream, + header: '', + socket: client[kSocket] + }) + } + } else if (util.isStream(body)) { + writeStream({ + body, + client, + request, + contentLength, + expectsPayload, + socket: client[kSocket], + h2stream: stream, + header: '' + }) + } else if (util.isIterable(body)) { + writeIterable({ + body, + client, + request, + contentLength, + expectsPayload, + header: '', + h2stream: stream, + socket: client[kSocket] + }) + } else { + assert(false) + } + } +} + +function writeStream ({ h2stream, body, client, request, socket, contentLength, header, expectsPayload }) { assert(contentLength !== 0 || client[kRunning] === 0, 'stream body cannot be pipelined') + if (client[kHTTPConnVersion] === 'h2') { + // For HTTP/2, is enough to pipe the stream + const pipe = pipeline( + body, + h2stream, + (err) => { + if (err) { + util.destroy(body, err) + util.destroy(h2stream, err) + } else { + request.onRequestSent() + } + } + ) + + pipe.on('data', onPipeData) + pipe.once('end', () => { + pipe.removeListener('data', onPipeData) + util.destroy(pipe) + }) + + function onPipeData (chunk) { + request.onBodySent(chunk) + } + + return + } + let finished = false const writer = new AsyncWriter({ socket, request, contentLength, client, expectsPayload, header }) @@ -1572,9 +2007,10 @@ function writeStream ({ body, client, request, socket, contentLength, header, ex .on('error', onFinished) } -async function writeBlob ({ body, client, request, socket, contentLength, header, expectsPayload }) { +async function writeBlob ({ h2stream, body, client, request, socket, contentLength, header, expectsPayload }) { assert(contentLength === body.size, 'blob body must have content length') + const isH2 = client[kHTTPConnVersion] === 'h2' try { if (contentLength != null && contentLength !== body.size) { throw new RequestContentLengthMismatchError() @@ -1582,10 +2018,16 @@ async function writeBlob ({ body, client, request, socket, contentLength, header const buffer = Buffer.from(await body.arrayBuffer()) - socket.cork() - socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1') - socket.write(buffer) - socket.uncork() + if (isH2) { + h2stream.cork() + h2stream.write(buffer) + h2stream.uncork() + } else { + socket.cork() + socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1') + socket.write(buffer) + socket.uncork() + } request.onBodySent(buffer) request.onRequestSent() @@ -1596,11 +2038,11 @@ async function writeBlob ({ body, client, request, socket, contentLength, header resume(client) } catch (err) { - util.destroy(socket, err) + util.destroy(isH2 ? h2stream : socket, err) } } -async function writeIterable ({ body, client, request, socket, contentLength, header, expectsPayload }) { +async function writeIterable ({ h2stream, body, client, request, socket, contentLength, header, expectsPayload }) { assert(contentLength !== 0 || client[kRunning] === 0, 'iterator body cannot be pipelined') let callback = null @@ -1622,6 +2064,33 @@ async function writeIterable ({ body, client, request, socket, contentLength, he } }) + if (client[kHTTPConnVersion] === 'h2') { + h2stream + .on('close', onDrain) + .on('drain', onDrain) + + try { + // It's up to the user to somehow abort the async iterable. + for await (const chunk of body) { + if (socket[kError]) { + throw socket[kError] + } + + if (!h2stream.write(chunk)) { + await waitForDrain() + } + } + } catch (err) { + h2stream.destroy(err) + } finally { + h2stream + .off('close', onDrain) + .off('drain', onDrain) + } + + return + } + socket .on('close', onDrain) .on('drain', onDrain) diff --git a/lib/core/connect.js b/lib/core/connect.js index f3b5cc33edd..bb71085a156 100644 --- a/lib/core/connect.js +++ b/lib/core/connect.js @@ -71,7 +71,7 @@ if (global.FinalizationRegistry) { } } -function buildConnector ({ maxCachedSessions, socketPath, timeout, ...opts }) { +function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, ...opts }) { if (maxCachedSessions != null && (!Number.isInteger(maxCachedSessions) || maxCachedSessions < 0)) { throw new InvalidArgumentError('maxCachedSessions must be a positive integer or zero') } @@ -79,7 +79,7 @@ function buildConnector ({ maxCachedSessions, socketPath, timeout, ...opts }) { const options = { path: socketPath, ...opts } const sessionCache = new SessionCache(maxCachedSessions == null ? 100 : maxCachedSessions) timeout = timeout == null ? 10e3 : timeout - + allowH2 = allowH2 != null ? allowH2 : false return function connect ({ hostname, host, protocol, port, servername, localAddress, httpSocket }, callback) { let socket if (protocol === 'https:') { @@ -99,6 +99,8 @@ function buildConnector ({ maxCachedSessions, socketPath, timeout, ...opts }) { servername, session, localAddress, + // TODO(HTTP/2): Add support for h2c + ALPNProtocols: allowH2 ? ['http/1.1', 'h2'] : ['http/1.1'], socket: httpSocket, // upgrade socket connection port: port || 443, host: hostname diff --git a/lib/core/request.js b/lib/core/request.js index 6c9a24d5d59..3ddc7fdbe32 100644 --- a/lib/core/request.js +++ b/lib/core/request.js @@ -5,6 +5,7 @@ const { NotSupportedError } = require('./errors') const assert = require('assert') +const { kHTTP2BuildRequest, kHTTP2CopyHeaders, kHTTP1BuildRequest } = require('./symbols') const util = require('./util') // tokenRegExp and headerCharRegex have been lifted from @@ -62,7 +63,8 @@ class Request { headersTimeout, bodyTimeout, reset, - throwOnError + throwOnError, + expectContinue }, handler) { if (typeof path !== 'string') { throw new InvalidArgumentError('path must be a string') @@ -98,6 +100,10 @@ class Request { throw new InvalidArgumentError('invalid reset') } + if (expectContinue != null && typeof expectContinue !== 'boolean') { + throw new InvalidArgumentError('invalid expectContinue') + } + this.headersTimeout = headersTimeout this.bodyTimeout = bodyTimeout @@ -150,6 +156,9 @@ class Request { this.headers = '' + // Only for H2 + this.expectContinue = expectContinue != null ? expectContinue : false + if (Array.isArray(headers)) { if (headers.length % 2 !== 0) { throw new InvalidArgumentError('headers array must be even') @@ -269,13 +278,62 @@ class Request { return this[kHandler].onError(error) } + // TODO: adjust to support H2 addHeader (key, value) { processHeader(this, key, value) return this } + + static [kHTTP1BuildRequest] (origin, opts, handler) { + // TODO: Migrate header parsing here, to make Requests + // HTTP agnostic + return new Request(origin, opts, handler) + } + + static [kHTTP2BuildRequest] (origin, opts, handler) { + const headers = opts.headers + opts = { ...opts, headers: null } + + const request = new Request(origin, opts, handler) + + request.headers = {} + + if (Array.isArray(headers)) { + if (headers.length % 2 !== 0) { + throw new InvalidArgumentError('headers array must be even') + } + for (let i = 0; i < headers.length; i += 2) { + processHeader(request, headers[i], headers[i + 1], true) + } + } else if (headers && typeof headers === 'object') { + const keys = Object.keys(headers) + for (let i = 0; i < keys.length; i++) { + const key = keys[i] + processHeader(request, key, headers[key], true) + } + } else if (headers != null) { + throw new InvalidArgumentError('headers must be an object or an array') + } + + return request + } + + static [kHTTP2CopyHeaders] (raw) { + const rawHeaders = raw.split('\r\n') + + const headers = {} + for (const header of rawHeaders) { + const [key, value] = header.split(': ') + + if (headers[key]) headers[key] += `,${value}` + else headers[key] = value + } + + return headers + } } -function processHeaderValue (key, val) { +function processHeaderValue (key, val, skipAppend) { if (val && typeof val === 'object') { throw new InvalidArgumentError(`invalid ${key} header`) } @@ -286,10 +344,10 @@ function processHeaderValue (key, val) { throw new InvalidArgumentError(`invalid ${key} header`) } - return `${key}: ${val}\r\n` + return skipAppend ? val : `${key}: ${val}\r\n` } -function processHeader (request, key, val) { +function processHeader (request, key, val, skipAppend = false) { if (val && (typeof val === 'object' && !Array.isArray(val))) { throw new InvalidArgumentError(`invalid ${key} header`) } else if (val === undefined) { @@ -357,10 +415,16 @@ function processHeader (request, key, val) { } else { if (Array.isArray(val)) { for (let i = 0; i < val.length; i++) { - request.headers += processHeaderValue(key, val[i]) + if (skipAppend) { + if (request.headers[key]) request.headers[key] += `,${processHeaderValue(key, val[i], skipAppend)}` + else request.headers[key] = processHeaderValue(key, val[i], skipAppend) + } else { + request.headers += processHeaderValue(key, val[i]) + } } } else { - request.headers += processHeaderValue(key, val) + if (skipAppend) request.headers[key] = processHeaderValue(key, val, skipAppend) + else request.headers += processHeaderValue(key, val) } } } diff --git a/lib/core/symbols.js b/lib/core/symbols.js index c852107a72a..c2492f4355f 100644 --- a/lib/core/symbols.js +++ b/lib/core/symbols.js @@ -51,5 +51,11 @@ module.exports = { kProxy: Symbol('proxy agent options'), kCounter: Symbol('socket request counter'), kInterceptors: Symbol('dispatch interceptors'), - kMaxResponseSize: Symbol('max response size') + kMaxResponseSize: Symbol('max response size'), + kHTTP2Session: Symbol('http2Session'), + kHTTP2SessionState: Symbol('http2Session state'), + kHTTP2BuildRequest: Symbol('http2 build request'), + kHTTP1BuildRequest: Symbol('http1 build request'), + kHTTP2CopyHeaders: Symbol('http2 copy headers'), + kHTTPConnVersion: Symbol('http connection version') } diff --git a/lib/core/util.js b/lib/core/util.js index 88e34a90123..0e6197a4b29 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -199,6 +199,7 @@ function destroy (stream, err) { // See: https://github.com/nodejs/node/pull/38505/files stream.socket = null } + stream.destroy(err) } else if (err) { process.nextTick((stream, err) => { @@ -218,6 +219,9 @@ function parseKeepAliveTimeout (val) { } function parseHeaders (headers, obj = {}) { + // For H2 support + if (!Array.isArray(headers)) return headers + for (let i = 0; i < headers.length; i += 2) { const key = headers[i].toString().toLowerCase() let val = obj[key] diff --git a/lib/fetch/index.js b/lib/fetch/index.js index 8faae32a70f..50f1b9f3fcd 100644 --- a/lib/fetch/index.js +++ b/lib/fetch/index.js @@ -1979,19 +1979,37 @@ async function httpNetworkFetch ( let location = '' const headers = new Headers() - for (let n = 0; n < headersList.length; n += 2) { - const key = headersList[n + 0].toString('latin1') - const val = headersList[n + 1].toString('latin1') - if (key.toLowerCase() === 'content-encoding') { - // https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1 - // "All content-coding values are case-insensitive..." - codings = val.toLowerCase().split(',').map((x) => x.trim()).reverse() - } else if (key.toLowerCase() === 'location') { - location = val + // For H2, the headers are a plain JS object + // We distinguish between them and iterate accordingly + if (Array.isArray(headersList)) { + for (let n = 0; n < headersList.length; n += 2) { + const key = headersList[n + 0].toString('latin1') + const val = headersList[n + 1].toString('latin1') + if (key.toLowerCase() === 'content-encoding') { + // https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1 + // "All content-coding values are case-insensitive..." + codings = val.toLowerCase().split(',').map((x) => x.trim()) + } else if (key.toLowerCase() === 'location') { + location = val + } + + headers.append(key, val) } + } else { + const keys = Object.keys(headersList) + for (const key of keys) { + const val = headersList[key] + if (key.toLowerCase() === 'content-encoding') { + // https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1 + // "All content-coding values are case-insensitive..." + codings = val.toLowerCase().split(',').map((x) => x.trim()).reverse() + } else if (key.toLowerCase() === 'location') { + location = val + } - headers.append(key, val) + headers.append(key, val) + } } this.body = new Readable({ read: resume }) diff --git a/package.json b/package.json index 969aaf6680f..863e85604f4 100644 --- a/package.json +++ b/package.json @@ -133,4 +133,4 @@ "dependencies": { "busboy": "^1.6.0" } -} +} \ No newline at end of file diff --git a/test/fetch/encoding.js b/test/fetch/encoding.js index 5817924521a..75d8fc37d5b 100644 --- a/test/fetch/encoding.js +++ b/test/fetch/encoding.js @@ -17,10 +17,10 @@ test('content-encoding header is case-iNsENsITIve', async (t) => { res.setHeader('Content-Encoding', contentCodings) res.setHeader('Content-Type', 'text/plain') - gzip.pipe(brotli).pipe(res) + brotli.pipe(gzip).pipe(res) - gzip.write(text) - gzip.end() + brotli.write(text) + brotli.end() }).listen(0) t.teardown(server.close.bind(server)) @@ -43,10 +43,10 @@ test('response decompression according to content-encoding should be handled in res.setHeader('Content-Encoding', contentCodings) res.setHeader('Content-Type', 'text/plain') - deflate.pipe(gzip).pipe(res) + gzip.pipe(deflate).pipe(res) - deflate.write(text) - deflate.end() + gzip.write(text) + gzip.end() }).listen(0) t.teardown(server.close.bind(server)) diff --git a/test/fetch/http2.js b/test/fetch/http2.js new file mode 100644 index 00000000000..246c14e1eef --- /dev/null +++ b/test/fetch/http2.js @@ -0,0 +1,256 @@ +'use strict' + +const { createSecureServer } = require('node:http2') +const { createReadStream, readFileSync } = require('node:fs') +const { once } = require('node:events') +const { Blob } = require('node:buffer') + +const { test, plan } = require('tap') +const pem = require('https-pem') + +const { Client, fetch } = require('../..') + +plan(4) + +test('[Fetch] Should handle h2 request with body (string or buffer)', async t => { + const server = createSecureServer(pem) + const expectedBody = 'hello from client!' + const expectedRequestBody = 'hello h2!' + const requestBody = [] + + server.on('stream', async (stream, headers) => { + stream.on('data', chunk => requestBody.push(chunk)) + + stream.respond({ + 'content-type': 'text/plain; charset=utf-8', + 'x-custom-h2': headers['x-my-header'], + ':status': 200 + }) + + stream.end(expectedRequestBody) + }) + + t.plan(2) + + server.listen() + await once(server, 'listening') + + const client = new Client(`https://localhost:${server.address().port}`, { + connect: { + rejectUnauthorized: false + }, + allowH2: true + }) + + const response = await fetch( + `https://localhost:${server.address().port}/`, + // Needs to be passed to disable the reject unauthorized + { + method: 'POST', + dispatcher: client, + headers: { + 'x-my-header': 'foo', + 'content-type': 'text-plain' + }, + body: expectedBody + } + ) + + const responseBody = await response.text() + + t.teardown(server.close.bind(server)) + t.teardown(client.close.bind(client)) + + t.equal(Buffer.concat(requestBody).toString('utf-8'), expectedBody) + t.equal(responseBody, expectedRequestBody) +}) + +test('[Fetch] Should handle h2 request with body (stream)', async t => { + const server = createSecureServer(pem) + const expectedBody = readFileSync(__filename, 'utf-8') + const stream = createReadStream(__filename) + const requestChunks = [] + + server.on('stream', async (stream, headers) => { + t.equal(headers[':method'], 'PUT') + t.equal(headers[':path'], '/') + t.equal(headers[':scheme'], 'https') + + stream.on('data', chunk => requestChunks.push(chunk)) + + stream.respond({ + 'content-type': 'text/plain; charset=utf-8', + 'x-custom-h2': headers['x-my-header'], + ':status': 200 + }) + + stream.end('hello h2!') + }) + + t.plan(8) + + server.listen(0) + await once(server, 'listening') + + const client = new Client(`https://localhost:${server.address().port}`, { + connect: { + rejectUnauthorized: false + }, + allowH2: true + }) + + t.teardown(server.close.bind(server)) + t.teardown(client.close.bind(client)) + + const response = await fetch( + `https://localhost:${server.address().port}/`, + // Needs to be passed to disable the reject unauthorized + { + method: 'PUT', + dispatcher: client, + headers: { + 'x-my-header': 'foo', + 'content-type': 'text-plain' + }, + body: stream, + duplex: 'half' + } + ) + + const responseBody = await response.text() + + t.equal(response.status, 200) + t.equal(response.headers.get('content-type'), 'text/plain; charset=utf-8') + t.equal(response.headers.get('x-custom-h2'), 'foo') + t.equal(responseBody, 'hello h2!') + t.equal(Buffer.concat(requestChunks).toString('utf-8'), expectedBody) +}) +test('Should handle h2 request with body (Blob)', { skip: !Blob }, async t => { + const server = createSecureServer(pem) + const expectedBody = 'asd' + const requestChunks = [] + const body = new Blob(['asd'], { + type: 'text/plain' + }) + + server.on('stream', async (stream, headers) => { + t.equal(headers[':method'], 'POST') + t.equal(headers[':path'], '/') + t.equal(headers[':scheme'], 'https') + + stream.on('data', chunk => requestChunks.push(chunk)) + + stream.respond({ + 'content-type': 'text/plain; charset=utf-8', + 'x-custom-h2': headers['x-my-header'], + ':status': 200 + }) + + stream.end('hello h2!') + }) + + t.plan(8) + + server.listen(0) + await once(server, 'listening') + + const client = new Client(`https://localhost:${server.address().port}`, { + connect: { + rejectUnauthorized: false + }, + allowH2: true + }) + + t.teardown(server.close.bind(server)) + t.teardown(client.close.bind(client)) + + const response = await fetch( + `https://localhost:${server.address().port}/`, + // Needs to be passed to disable the reject unauthorized + { + body, + method: 'POST', + dispatcher: client, + headers: { + 'x-my-header': 'foo', + 'content-type': 'text-plain' + } + } + ) + + const responseBody = await response.arrayBuffer() + + t.equal(response.status, 200) + t.equal(response.headers.get('content-type'), 'text/plain; charset=utf-8') + t.equal(response.headers.get('x-custom-h2'), 'foo') + t.same(new TextDecoder().decode(responseBody).toString(), 'hello h2!') + t.equal(Buffer.concat(requestChunks).toString('utf-8'), expectedBody) +}) + +test( + 'Should handle h2 request with body (Blob:ArrayBuffer)', + { skip: !Blob }, + async t => { + const server = createSecureServer(pem) + const expectedBody = 'hello' + const requestChunks = [] + const expectedResponseBody = { hello: 'h2' } + const buf = Buffer.from(expectedBody) + const body = new ArrayBuffer(buf.byteLength) + + buf.copy(new Uint8Array(body)) + + server.on('stream', async (stream, headers) => { + t.equal(headers[':method'], 'PUT') + t.equal(headers[':path'], '/') + t.equal(headers[':scheme'], 'https') + + stream.on('data', chunk => requestChunks.push(chunk)) + + stream.respond({ + 'content-type': 'application/json', + 'x-custom-h2': headers['x-my-header'], + ':status': 200 + }) + + stream.end(JSON.stringify(expectedResponseBody)) + }) + + t.plan(8) + + server.listen(0) + await once(server, 'listening') + + const client = new Client(`https://localhost:${server.address().port}`, { + connect: { + rejectUnauthorized: false + }, + allowH2: true + }) + + t.teardown(server.close.bind(server)) + t.teardown(client.close.bind(client)) + + const response = await fetch( + `https://localhost:${server.address().port}/`, + // Needs to be passed to disable the reject unauthorized + { + body, + method: 'PUT', + dispatcher: client, + headers: { + 'x-my-header': 'foo', + 'content-type': 'text-plain' + } + } + ) + + const responseBody = await response.json() + + t.equal(response.status, 200) + t.equal(response.headers.get('content-type'), 'application/json') + t.equal(response.headers.get('x-custom-h2'), 'foo') + t.same(responseBody, expectedResponseBody) + t.equal(Buffer.concat(requestChunks).toString('utf-8'), expectedBody) + } +) diff --git a/test/http2-alpn.js b/test/http2-alpn.js new file mode 100644 index 00000000000..04b8cb6abd8 --- /dev/null +++ b/test/http2-alpn.js @@ -0,0 +1,277 @@ +'use strict' + +const https = require('node:https') +const { once } = require('node:events') +const { createSecureServer } = require('node:http2') +const { readFileSync } = require('node:fs') +const { join } = require('node:path') +const { test } = require('tap') + +const { Client } = require('..') + +// get the crypto fixtures +const key = readFileSync(join(__dirname, 'fixtures', 'key.pem'), 'utf8') +const cert = readFileSync(join(__dirname, 'fixtures', 'cert.pem'), 'utf8') +const ca = readFileSync(join(__dirname, 'fixtures', 'ca.pem'), 'utf8') + +test('Should upgrade to HTTP/2 when HTTPS/1 is available for GET', async (t) => { + t.plan(10) + + const body = [] + const httpsBody = [] + + // create the server and server stream handler + const server = createSecureServer( + { + key, + cert, + allowHTTP1: true + }, + (req, res) => { + const { socket: { alpnProtocol } } = req.httpVersion === '2.0' ? req.stream.session : req + + // handle http/1 requests + res.writeHead(200, { + 'content-type': 'application/json; charset=utf-8', + 'x-custom-request-header': req.headers['x-custom-request-header'] || '', + 'x-custom-response-header': `using ${req.httpVersion}` + }) + res.end(JSON.stringify({ + alpnProtocol, + httpVersion: req.httpVersion + })) + } + ) + + server.listen(0) + await once(server, 'listening') + + // close the server on teardown + t.teardown(server.close.bind(server)) + + // set the port + const port = server.address().port + + // test undici against http/2 + const client = new Client(`https://localhost:${port}`, { + connect: { + ca, + servername: 'agent1' + }, + allowH2: true + }) + + // close the client on teardown + t.teardown(client.close.bind(client)) + + // make an undici request using where it wants http/2 + const response = await client.request({ + path: '/', + method: 'GET', + headers: { + 'x-custom-request-header': 'want 2.0' + } + }) + + response.body.on('data', chunk => { + body.push(chunk) + }) + + await once(response.body, 'end') + + t.equal(response.statusCode, 200) + t.equal(response.headers['content-type'], 'application/json; charset=utf-8') + t.equal(response.headers['x-custom-request-header'], 'want 2.0') + t.equal(response.headers['x-custom-response-header'], 'using 2.0') + t.equal(Buffer.concat(body).toString('utf8'), JSON.stringify({ + alpnProtocol: 'h2', + httpVersion: '2.0' + })) + + // make an https request for http/1 to confirm undici is using http/2 + const httpsOptions = { + ca, + servername: 'agent1', + headers: { + 'x-custom-request-header': 'want 1.1' + } + } + + const httpsResponse = await new Promise((resolve, reject) => { + const httpsRequest = https.get(`https://localhost:${port}/`, httpsOptions, (res) => { + res.on('data', (chunk) => { + httpsBody.push(chunk) + }) + + res.on('end', () => { + resolve(res) + }) + }).on('error', (err) => { + reject(err) + }) + + t.teardown(httpsRequest.destroy.bind(httpsRequest)) + }) + + t.equal(httpsResponse.statusCode, 200) + t.equal(httpsResponse.headers['content-type'], 'application/json; charset=utf-8') + t.equal(httpsResponse.headers['x-custom-request-header'], 'want 1.1') + t.equal(httpsResponse.headers['x-custom-response-header'], 'using 1.1') + t.equal(Buffer.concat(httpsBody).toString('utf8'), JSON.stringify({ + alpnProtocol: false, + httpVersion: '1.1' + })) +}) + +test('Should upgrade to HTTP/2 when HTTPS/1 is available for POST', async (t) => { + t.plan(15) + + const requestChunks = [] + const responseBody = [] + + const httpsRequestChunks = [] + const httpsResponseBody = [] + + const expectedBody = 'hello' + const buf = Buffer.from(expectedBody) + const body = new ArrayBuffer(buf.byteLength) + + buf.copy(new Uint8Array(body)) + + // create the server and server stream handler + const server = createSecureServer( + { + key, + cert, + allowHTTP1: true + }, + (req, res) => { + // use the stream handler for http2 + if (req.httpVersion === '2.0') { + return + } + + const { socket: { alpnProtocol } } = req + + req.on('data', (chunk) => { + httpsRequestChunks.push(chunk) + }) + + req.on('end', () => { + // handle http/1 requests + res.writeHead(201, { + 'content-type': 'text/plain; charset=utf-8', + 'x-custom-request-header': req.headers['x-custom-request-header'] || '', + 'x-custom-alpn-protocol': alpnProtocol + }) + res.end('hello http/1!') + }) + } + ) + + server.on('stream', (stream, headers) => { + t.equal(headers[':method'], 'POST') + t.equal(headers[':path'], '/') + t.equal(headers[':scheme'], 'https') + + const { socket: { alpnProtocol } } = stream.session + + stream.on('data', (chunk) => { + requestChunks.push(chunk) + }) + + stream.respond({ + ':status': 201, + 'content-type': 'text/plain; charset=utf-8', + 'x-custom-request-header': headers['x-custom-request-header'] || '', + 'x-custom-alpn-protocol': alpnProtocol + }) + + stream.end('hello h2!') + }) + + server.listen(0) + await once(server, 'listening') + + // close the server on teardown + t.teardown(server.close.bind(server)) + + // set the port + const port = server.address().port + + // test undici against http/2 + const client = new Client(`https://localhost:${port}`, { + connect: { + ca, + servername: 'agent1' + }, + allowH2: true + }) + + // close the client on teardown + t.teardown(client.close.bind(client)) + + // make an undici request using where it wants http/2 + const response = await client.request({ + path: '/', + method: 'POST', + headers: { + 'x-custom-request-header': 'want 2.0' + }, + body + }) + + response.body.on('data', (chunk) => { + responseBody.push(chunk) + }) + + await once(response.body, 'end') + + t.equal(response.statusCode, 201) + t.equal(response.headers['content-type'], 'text/plain; charset=utf-8') + t.equal(response.headers['x-custom-request-header'], 'want 2.0') + t.equal(response.headers['x-custom-alpn-protocol'], 'h2') + t.equal(Buffer.concat(responseBody).toString('utf-8'), 'hello h2!') + t.equal(Buffer.concat(requestChunks).toString('utf-8'), expectedBody) + + // make an https request for http/1 to confirm undici is using http/2 + const httpsOptions = { + ca, + servername: 'agent1', + method: 'POST', + headers: { + 'content-type': 'text/plain; charset=utf-8', + 'content-length': Buffer.byteLength(body), + 'x-custom-request-header': 'want 1.1' + } + } + + const httpsResponse = await new Promise((resolve, reject) => { + const httpsRequest = https.request(`https://localhost:${port}/`, httpsOptions, (res) => { + res.on('data', (chunk) => { + httpsResponseBody.push(chunk) + }) + + res.on('end', () => { + resolve(res) + }) + }).on('error', (err) => { + reject(err) + }) + + httpsRequest.on('error', (err) => { + reject(err) + }) + + httpsRequest.write(Buffer.from(body)) + + t.teardown(httpsRequest.destroy.bind(httpsRequest)) + }) + + t.equal(httpsResponse.statusCode, 201) + t.equal(httpsResponse.headers['content-type'], 'text/plain; charset=utf-8') + t.equal(httpsResponse.headers['x-custom-request-header'], 'want 1.1') + t.equal(httpsResponse.headers['x-custom-alpn-protocol'], 'false') + t.equal(Buffer.concat(httpsResponseBody).toString('utf-8'), 'hello http/1!') + t.equal(Buffer.concat(httpsRequestChunks).toString('utf-8'), expectedBody) +}) diff --git a/test/http2.js b/test/http2.js index ab8752a7816..8fd9c616bc3 100644 --- a/test/http2.js +++ b/test/http2.js @@ -1,32 +1,949 @@ 'use strict' -const { test } = require('tap') -const { Client, errors } = require('..') -const { createSecureServer } = require('http2') +const { createSecureServer } = require('node:http2') +const { createReadStream, readFileSync } = require('node:fs') +const { once } = require('node:events') +const { Blob } = require('node:buffer') +const { Writable, pipeline, PassThrough, Readable } = require('node:stream') + +const { test, plan } = require('tap') const pem = require('https-pem') -test('throw http2 not supported error', (t) => { - t.plan(1) +const { Client } = require('..') + +const isGreaterThanv20 = Number(process.version.slice(1).split('.')[0]) >= 20 + +plan(18) + +test('Should support H2 connection', async t => { + const body = [] + const server = createSecureServer(pem) + + server.on('stream', (stream, headers) => { + t.equal(headers['x-my-header'], 'foo') + t.equal(headers[':method'], 'GET') + stream.respond({ + 'content-type': 'text/plain; charset=utf-8', + 'x-custom-h2': 'hello', + ':status': 200 + }) + stream.end('hello h2!') + }) + + server.listen(0) + await once(server, 'listening') + + const client = new Client(`https://localhost:${server.address().port}`, { + connect: { + rejectUnauthorized: false + }, + allowH2: true + }) + + t.plan(6) + t.teardown(server.close.bind(server)) + t.teardown(client.close.bind(client)) + + const response = await client.request({ + path: '/', + method: 'GET', + headers: { + 'x-my-header': 'foo' + } + }) + + response.body.on('data', chunk => { + body.push(chunk) + }) + + await once(response.body, 'end') + t.equal(response.statusCode, 200) + t.equal(response.headers['content-type'], 'text/plain; charset=utf-8') + t.equal(response.headers['x-custom-h2'], 'hello') + t.equal(Buffer.concat(body).toString('utf8'), 'hello h2!') +}) + +test('Should support H2 connection (headers as array)', async t => { + const body = [] + const server = createSecureServer(pem) + + server.on('stream', (stream, headers) => { + t.equal(headers['x-my-header'], 'foo') + t.equal(headers['x-my-drink'], 'coffee,tea') + t.equal(headers[':method'], 'GET') + stream.respond({ + 'content-type': 'text/plain; charset=utf-8', + 'x-custom-h2': 'hello', + ':status': 200 + }) + stream.end('hello h2!') + }) + + server.listen(0) + await once(server, 'listening') + + const client = new Client(`https://localhost:${server.address().port}`, { + connect: { + rejectUnauthorized: false + }, + allowH2: true + }) + + t.plan(7) + t.teardown(server.close.bind(server)) + t.teardown(client.close.bind(client)) + + const response = await client.request({ + path: '/', + method: 'GET', + headers: ['x-my-header', 'foo', 'x-my-drink', ['coffee', 'tea']] + }) + + response.body.on('data', chunk => { + body.push(chunk) + }) + + await once(response.body, 'end') + t.equal(response.statusCode, 200) + t.equal(response.headers['content-type'], 'text/plain; charset=utf-8') + t.equal(response.headers['x-custom-h2'], 'hello') + t.equal(Buffer.concat(body).toString('utf8'), 'hello h2!') +}) + +test('Should support H2 GOAWAY (server-side)', async t => { + const body = [] + const server = createSecureServer(pem) + + server.on('stream', (stream, headers) => { + t.equal(headers['x-my-header'], 'foo') + t.equal(headers[':method'], 'GET') + stream.respond({ + 'content-type': 'text/plain; charset=utf-8', + 'x-custom-h2': 'hello', + ':status': 200 + }) + stream.end('hello h2!') + }) + + server.on('session', session => { + setTimeout(() => { + session.goaway(204) + }, 1000) + }) + + server.listen(0) + await once(server, 'listening') + + const client = new Client(`https://localhost:${server.address().port}`, { + connect: { + rejectUnauthorized: false + }, + allowH2: true + }) + + t.plan(9) + t.teardown(server.close.bind(server)) + t.teardown(client.close.bind(client)) + + const response = await client.request({ + path: '/', + method: 'GET', + headers: { + 'x-my-header': 'foo' + } + }) + + response.body.on('data', chunk => { + body.push(chunk) + }) + + await once(response.body, 'end') + t.equal(response.statusCode, 200) + t.equal(response.headers['content-type'], 'text/plain; charset=utf-8') + t.equal(response.headers['x-custom-h2'], 'hello') + t.equal(Buffer.concat(body).toString('utf8'), 'hello h2!') + + const [url, disconnectClient, err] = await once(client, 'disconnect') + + t.type(url, URL) + t.same(disconnectClient, [client]) + t.equal(err.message, 'HTTP/2: "GOAWAY" frame received with code 204') +}) + +test('Should throw if bad allowH2 has been pased', async t => { + try { + // eslint-disable-next-line + new Client('https://localhost:1000', { + allowH2: 'true' + }) + t.fail() + } catch (error) { + t.equal(error.message, 'allowH2 must be a valid boolean value') + } +}) + +test('Should throw if bad maxConcurrentStreams has been pased', async t => { + try { + // eslint-disable-next-line + new Client('https://localhost:1000', { + allowH2: true, + maxConcurrentStreams: {} + }) + t.fail() + } catch (error) { + t.equal( + error.message, + 'maxConcurrentStreams must be a possitive integer, greater than 0' + ) + } + + try { + // eslint-disable-next-line + new Client('https://localhost:1000', { + allowH2: true, + maxConcurrentStreams: -1 + }) + t.fail() + } catch (error) { + t.equal( + error.message, + 'maxConcurrentStreams must be a possitive integer, greater than 0' + ) + } +}) + +test( + 'Request should fail if allowH2 is false and server advertises h1 only', + { skip: isGreaterThanv20 }, + async t => { + const server = createSecureServer( + { + ...pem, + allowHTTP1: false, + ALPNProtocols: ['http/1.1'] + }, + (req, res) => { + t.fail('Should not create a valid h2 stream') + } + ) + + server.listen(0) + await once(server, 'listening') + + const client = new Client(`https://localhost:${server.address().port}`, { + allowH2: false, + connect: { + rejectUnauthorized: false + } + }) + + t.teardown(server.close.bind(server)) + t.teardown(client.close.bind(client)) - const server = createSecureServer({ key: pem.key, cert: pem.cert }, (req, res) => { - res.stream.respond({ 'content-type': 'text/plain' }) - res.stream.end('hello') - }).on('unknownProtocol', (socket) => { - // continue sending data in http2 to our http1.1 client to trigger error - socket.write('PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n') + const response = await client.request({ + path: '/', + method: 'GET', + headers: { + 'x-my-header': 'foo' + } + }) + + t.equal(response.statusCode, 403) + } +) + +test( + '[v20] Request should fail if allowH2 is false and server advertises h1 only', + { skip: !isGreaterThanv20 }, + async t => { + const server = createSecureServer( + { + ...pem, + allowHTTP1: false, + ALPNProtocols: ['http/1.1'] + }, + (req, res) => { + t.fail('Should not create a valid h2 stream') + } + ) + + server.listen(0) + await once(server, 'listening') + + const client = new Client(`https://localhost:${server.address().port}`, { + allowH2: false, + connect: { + rejectUnauthorized: false + } + }) + + t.teardown(server.close.bind(server)) + t.teardown(client.close.bind(client)) + t.plan(2) + + try { + await client.request({ + path: '/', + method: 'GET', + headers: { + 'x-my-header': 'foo' + } + }) + } catch (error) { + t.equal( + error.message, + 'Client network socket disconnected before secure TLS connection was established' + ) + t.equal(error.code, 'ECONNRESET') + } + } +) + +test('Should handle h2 continue', async t => { + const requestBody = [] + const server = createSecureServer(pem, () => {}) + const responseBody = [] + + server.on('checkContinue', (request, response) => { + t.equal(request.headers.expect, '100-continue') + t.equal(request.headers['x-my-header'], 'foo') + t.equal(request.headers[':method'], 'POST') + response.writeContinue() + + request.on('data', chunk => requestBody.push(chunk)) + + response.writeHead(200, { + 'content-type': 'text/plain; charset=utf-8', + 'x-custom-h2': 'foo' + }) + response.end('hello h2!') + }) + + t.plan(7) + + server.listen(0) + await once(server, 'listening') + + const client = new Client(`https://localhost:${server.address().port}`, { + connect: { + rejectUnauthorized: false + }, + expectContinue: true, + allowH2: true }) + t.teardown(server.close.bind(server)) + t.teardown(client.close.bind(client)) + + const response = await client.request({ + path: '/', + method: 'POST', + headers: { + 'x-my-header': 'foo' + }, + expectContinue: true + }) + + response.body.on('data', chunk => { + responseBody.push(chunk) + }) + + await once(response.body, 'end') + + t.equal(response.statusCode, 200) + t.equal(response.headers['content-type'], 'text/plain; charset=utf-8') + t.equal(response.headers['x-custom-h2'], 'foo') + t.equal(Buffer.concat(responseBody).toString('utf-8'), 'hello h2!') +}) + +test('Dispatcher#Stream', t => { + const server = createSecureServer(pem) + const expectedBody = 'hello from client!' + const bufs = [] + let requestBody = '' + + server.on('stream', async (stream, headers) => { + stream.setEncoding('utf-8') + stream.on('data', chunk => { + requestBody += chunk + }) + + stream.respond({ ':status': 200, 'x-custom': 'custom-header' }) + stream.end('hello h2!') + }) + + t.plan(4) + + server.listen(0, async () => { + const client = new Client(`https://localhost:${server.address().port}`, { + connect: { + rejectUnauthorized: false + }, + allowH2: true + }) + + t.teardown(server.close.bind(server)) + t.teardown(client.close.bind(client)) + + await client.stream( + { path: '/', opaque: { bufs }, method: 'POST', body: expectedBody }, + ({ statusCode, headers, opaque: { bufs } }) => { + t.equal(statusCode, 200) + t.equal(headers['x-custom'], 'custom-header') + + return new Writable({ + write (chunk, _encoding, cb) { + bufs.push(chunk) + cb() + } + }) + } + ) + + t.equal(Buffer.concat(bufs).toString('utf-8'), 'hello h2!') + t.equal(requestBody, expectedBody) + }) +}) + +test('Dispatcher#Pipeline', t => { + const server = createSecureServer(pem) + const expectedBody = 'hello from client!' + const bufs = [] + let requestBody = '' + + server.on('stream', async (stream, headers) => { + stream.setEncoding('utf-8') + stream.on('data', chunk => { + requestBody += chunk + }) + + stream.respond({ ':status': 200, 'x-custom': 'custom-header' }) + stream.end('hello h2!') + }) + + t.plan(5) + + server.listen(0, () => { + const client = new Client(`https://localhost:${server.address().port}`, { + connect: { + rejectUnauthorized: false + }, + allowH2: true + }) + + t.teardown(server.close.bind(server)) + t.teardown(client.close.bind(client)) + + pipeline( + new Readable({ + read () { + this.push(Buffer.from(expectedBody)) + this.push(null) + } + }), + client.pipeline( + { path: '/', method: 'POST', body: expectedBody }, + ({ statusCode, headers, body }) => { + t.equal(statusCode, 200) + t.equal(headers['x-custom'], 'custom-header') + + return pipeline(body, new PassThrough(), () => {}) + } + ), + new Writable({ + write (chunk, _, cb) { + bufs.push(chunk) + cb() + } + }), + err => { + t.error(err) + t.equal(Buffer.concat(bufs).toString('utf-8'), 'hello h2!') + t.equal(requestBody, expectedBody) + } + ) + }) +}) + +test('Dispatcher#Connect', t => { + const server = createSecureServer(pem) + const expectedBody = 'hello from client!' + let requestBody = '' + + server.on('stream', async (stream, headers) => { + stream.setEncoding('utf-8') + stream.on('data', chunk => { + requestBody += chunk + }) + + stream.respond({ ':status': 200, 'x-custom': 'custom-header' }) + stream.end('hello h2!') + }) + + t.plan(6) server.listen(0, () => { const client = new Client(`https://localhost:${server.address().port}`, { - tls: { + connect: { + rejectUnauthorized: false + }, + allowH2: true + }) + + t.teardown(server.close.bind(server)) + t.teardown(client.close.bind(client)) + + let result = '' + client.connect({ path: '/' }, (err, { socket }) => { + t.error(err) + socket.on('data', chunk => { + result += chunk + }) + socket.on('response', headers => { + t.equal(headers[':status'], 200) + t.equal(headers['x-custom'], 'custom-header') + t.notOk(socket.closed) + }) + + // We need to handle the error event although + // is not controlled by Undici, the fact that a session + // is destroyed and destroys subsequent streams, causes + // unhandled errors to surface if not handling this event. + socket.on('error', () => {}) + + socket.once('end', () => { + t.equal(requestBody, expectedBody) + t.equal(result, 'hello h2!') + }) + socket.end(expectedBody) + }) + }) +}) + +test('Dispatcher#Upgrade', t => { + const server = createSecureServer(pem) + + server.on('stream', async (stream, headers) => { + stream.end() + }) + + t.plan(1) + + server.listen(0, async () => { + const client = new Client(`https://localhost:${server.address().port}`, { + connect: { rejectUnauthorized: false + }, + allowH2: true + }) + + t.teardown(server.close.bind(server)) + t.teardown(client.close.bind(client)) + + try { + await client.upgrade({ path: '/' }) + } catch (error) { + t.equal(error.message, 'Upgrade not supported for H2') + } + }) +}) + +test('Dispatcher#destroy', async t => { + const promises = [] + const server = createSecureServer(pem) + + server.on('stream', (stream, headers) => { + setTimeout(stream.end.bind(stream), 1500) + }) + + server.listen(0) + await once(server, 'listening') + + const client = new Client(`https://localhost:${server.address().port}`, { + connect: { + rejectUnauthorized: false + }, + allowH2: true + }) + + t.plan(4) + t.teardown(server.close.bind(server)) + + promises.push( + client.request({ + path: '/', + method: 'GET', + headers: { + 'x-my-header': 'foo' } }) + ) + + promises.push( + client.request({ + path: '/', + method: 'GET', + headers: { + 'x-my-header': 'foo' + } + }) + ) + + promises.push( + client.request({ + path: '/', + method: 'GET', + headers: { + 'x-my-header': 'foo' + } + }) + ) + + promises.push( + client.request({ + path: '/', + method: 'GET', + headers: { + 'x-my-header': 'foo' + } + }) + ) + + await client.destroy() + + const results = await Promise.allSettled(promises) + + t.equal(results[0].status, 'rejected') + t.equal(results[1].status, 'rejected') + t.equal(results[2].status, 'rejected') + t.equal(results[3].status, 'rejected') +}) + +test('Should handle h2 request with body (string or buffer) - dispatch', t => { + const server = createSecureServer(pem) + const expectedBody = 'hello from client!' + const response = [] + const requestBody = [] + + server.on('stream', async (stream, headers) => { + stream.on('data', chunk => requestBody.push(chunk)) + + stream.respond({ + 'content-type': 'text/plain; charset=utf-8', + 'x-custom-h2': headers['x-my-header'], + ':status': 200 + }) + + stream.end('hello h2!') + }) + + t.plan(7) + + server.listen(0, () => { + const client = new Client(`https://localhost:${server.address().port}`, { + connect: { + rejectUnauthorized: false + }, + allowH2: true + }) + + t.teardown(server.close.bind(server)) t.teardown(client.close.bind(client)) - client.request({ path: '/', method: 'GET' }, (err, data) => { - t.type(err, errors.HTTPParserError) + client.dispatch( + { + path: '/', + method: 'POST', + headers: { + 'x-my-header': 'foo', + 'content-type': 'text/plain' + }, + body: expectedBody + }, + { + onConnect () { + t.ok(true) + }, + onError (err) { + t.error(err) + }, + onHeaders (statusCode, headers) { + t.equal(statusCode, 200) + t.equal(headers['content-type'], 'text/plain; charset=utf-8') + t.equal(headers['x-custom-h2'], 'foo') + }, + onData (chunk) { + response.push(chunk) + }, + onBodySent (body) { + t.equal(body.toString('utf-8'), expectedBody) + }, + onComplete () { + t.equal(Buffer.concat(response).toString('utf-8'), 'hello h2!') + t.equal( + Buffer.concat(requestBody).toString('utf-8'), + 'hello from client!' + ) + } + } + ) + }) +}) + +test('Should handle h2 request with body (stream)', async t => { + const server = createSecureServer(pem) + const expectedBody = readFileSync(__filename, 'utf-8') + const stream = createReadStream(__filename) + const requestChunks = [] + const responseBody = [] + + server.on('stream', async (stream, headers) => { + t.equal(headers[':method'], 'PUT') + t.equal(headers[':path'], '/') + t.equal(headers[':scheme'], 'https') + + stream.respond({ + 'content-type': 'text/plain; charset=utf-8', + 'x-custom-h2': headers['x-my-header'], + ':status': 200 + }) + + for await (const chunk of stream) { + requestChunks.push(chunk) + } + + stream.end('hello h2!') + }) + + t.plan(8) + + server.listen(0) + await once(server, 'listening') + + const client = new Client(`https://localhost:${server.address().port}`, { + connect: { + rejectUnauthorized: false + }, + allowH2: true + }) + + t.teardown(server.close.bind(server)) + t.teardown(client.close.bind(client)) + + const response = await client.request({ + path: '/', + method: 'PUT', + headers: { + 'x-my-header': 'foo' + }, + body: stream + }) + + for await (const chunk of response.body) { + responseBody.push(chunk) + } + + t.equal(response.statusCode, 200) + t.equal(response.headers['content-type'], 'text/plain; charset=utf-8') + t.equal(response.headers['x-custom-h2'], 'foo') + t.equal(Buffer.concat(responseBody).toString('utf-8'), 'hello h2!') + t.equal(Buffer.concat(requestChunks).toString('utf-8'), expectedBody) +}) + +test('Should handle h2 request with body (iterable)', async t => { + const server = createSecureServer(pem) + const expectedBody = 'hello' + const requestChunks = [] + const responseBody = [] + const iterableBody = { + [Symbol.iterator]: function * () { + const end = expectedBody.length - 1 + for (let i = 0; i < end + 1; i++) { + yield expectedBody[i] + } + + return expectedBody[end] + } + } + + server.on('stream', async (stream, headers) => { + t.equal(headers[':method'], 'POST') + t.equal(headers[':path'], '/') + t.equal(headers[':scheme'], 'https') + + stream.on('data', chunk => requestChunks.push(chunk)) + + stream.respond({ + 'content-type': 'text/plain; charset=utf-8', + 'x-custom-h2': headers['x-my-header'], + ':status': 200 }) + + stream.end('hello h2!') + }) + + t.plan(8) + + server.listen(0) + await once(server, 'listening') + + const client = new Client(`https://localhost:${server.address().port}`, { + connect: { + rejectUnauthorized: false + }, + allowH2: true + }) + + t.teardown(server.close.bind(server)) + t.teardown(client.close.bind(client)) + + const response = await client.request({ + path: '/', + method: 'POST', + headers: { + 'x-my-header': 'foo' + }, + body: iterableBody + }) + + response.body.on('data', chunk => { + responseBody.push(chunk) }) + + await once(response.body, 'end') + + t.equal(response.statusCode, 200) + t.equal(response.headers['content-type'], 'text/plain; charset=utf-8') + t.equal(response.headers['x-custom-h2'], 'foo') + t.equal(Buffer.concat(responseBody).toString('utf-8'), 'hello h2!') + t.equal(Buffer.concat(requestChunks).toString('utf-8'), expectedBody) }) + +test('Should handle h2 request with body (Blob)', { skip: !Blob }, async t => { + const server = createSecureServer(pem) + const expectedBody = 'asd' + const requestChunks = [] + const responseBody = [] + const body = new Blob(['asd'], { + type: 'application/json' + }) + + server.on('stream', async (stream, headers) => { + t.equal(headers[':method'], 'POST') + t.equal(headers[':path'], '/') + t.equal(headers[':scheme'], 'https') + + stream.on('data', chunk => requestChunks.push(chunk)) + + stream.respond({ + 'content-type': 'text/plain; charset=utf-8', + 'x-custom-h2': headers['x-my-header'], + ':status': 200 + }) + + stream.end('hello h2!') + }) + + t.plan(8) + + server.listen(0) + await once(server, 'listening') + + const client = new Client(`https://localhost:${server.address().port}`, { + connect: { + rejectUnauthorized: false + }, + allowH2: true + }) + + t.teardown(server.close.bind(server)) + t.teardown(client.close.bind(client)) + + const response = await client.request({ + path: '/', + method: 'POST', + headers: { + 'x-my-header': 'foo' + }, + body + }) + + response.body.on('data', chunk => { + responseBody.push(chunk) + }) + + await once(response.body, 'end') + + t.equal(response.statusCode, 200) + t.equal(response.headers['content-type'], 'text/plain; charset=utf-8') + t.equal(response.headers['x-custom-h2'], 'foo') + t.equal(Buffer.concat(responseBody).toString('utf-8'), 'hello h2!') + t.equal(Buffer.concat(requestChunks).toString('utf-8'), expectedBody) +}) + +test( + 'Should handle h2 request with body (Blob:ArrayBuffer)', + { skip: !Blob }, + async t => { + const server = createSecureServer(pem) + const expectedBody = 'hello' + const requestChunks = [] + const responseBody = [] + const buf = Buffer.from(expectedBody) + const body = new ArrayBuffer(buf.byteLength) + + buf.copy(new Uint8Array(body)) + + server.on('stream', async (stream, headers) => { + t.equal(headers[':method'], 'POST') + t.equal(headers[':path'], '/') + t.equal(headers[':scheme'], 'https') + + stream.on('data', chunk => requestChunks.push(chunk)) + + stream.respond({ + 'content-type': 'text/plain; charset=utf-8', + 'x-custom-h2': headers['x-my-header'], + ':status': 200 + }) + + stream.end('hello h2!') + }) + + t.plan(8) + + server.listen(0) + await once(server, 'listening') + + const client = new Client(`https://localhost:${server.address().port}`, { + connect: { + rejectUnauthorized: false + }, + allowH2: true + }) + + t.teardown(server.close.bind(server)) + t.teardown(client.close.bind(client)) + + const response = await client.request({ + path: '/', + method: 'POST', + headers: { + 'x-my-header': 'foo' + }, + body + }) + + response.body.on('data', chunk => { + responseBody.push(chunk) + }) + + await once(response.body, 'end') + + t.equal(response.statusCode, 200) + t.equal(response.headers['content-type'], 'text/plain; charset=utf-8') + t.equal(response.headers['x-custom-h2'], 'foo') + t.equal(Buffer.concat(responseBody).toString('utf-8'), 'hello h2!') + t.equal(Buffer.concat(requestChunks).toString('utf-8'), expectedBody) + } +) diff --git a/types/client.d.ts b/types/client.d.ts index 56074a15ae7..3d348f9427b 100644 --- a/types/client.d.ts +++ b/types/client.d.ts @@ -72,6 +72,16 @@ export declare namespace Client { autoSelectFamily?: boolean; /** The amount of time in milliseconds to wait for a connection attempt to finish before trying the next address when using the `autoSelectFamily` option. */ autoSelectFamilyAttemptTimeout?: number; + /** + * @description Enables support for H2 if the server has assigned bigger priority to it through ALPN negotiation. + * @default false + */ + allowH2?: boolean; + /** + * @description Dictates the maximum number of concurrent streams for a single H2 session. It can be overriden by a SETTINGS remote frame. + * @default 100 + */ + maxConcurrentStreams?: number } export interface SocketInfo { localAddress?: string diff --git a/types/dispatcher.d.ts b/types/dispatcher.d.ts index 7f621371f86..42a78ba0834 100644 --- a/types/dispatcher.d.ts +++ b/types/dispatcher.d.ts @@ -117,6 +117,8 @@ declare namespace Dispatcher { reset?: boolean; /** Whether Undici should throw an error upon receiving a 4xx or 5xx response from the server. Defaults to false */ throwOnError?: boolean; + /** For H2, it appends the expect: 100-continue header, and halts the request body until a 100-continue is received from the remote server*/ + expectContinue?: boolean; } export interface ConnectOptions { path: string; From fd09517453f3d5cfae8dff6a3190f83dcdba686d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20R=C3=A8gne?= Date: Fri, 8 Sep 2023 15:12:32 +0200 Subject: [PATCH 119/259] docs: fix tables in README (#2254) --- README.md | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index e6c3552ade4..051a9df2dea 100644 --- a/README.md +++ b/README.md @@ -23,28 +23,28 @@ number of unix sockets (connections) with a pipelining depth of 10 running on No ### Connections 1 -│ Tests │ Samples │ Result │ Tolerance │ Difference with slowest │ -|─────────────────────|─────────|───────────────|───────────|─────────────────────────| -│ http - no keepalive │ 15 │ 5.32 req/sec │ ± 2.61 % │ - │ -│ http - keepalive │ 10 │ 5.35 req/sec │ ± 2.47 % │ + 0.44 % │ -│ undici - fetch │ 15 │ 41.85 req/sec │ ± 2.49 % │ + 686.04 % │ -│ undici - pipeline │ 40 │ 50.36 req/sec │ ± 2.77 % │ + 845.92 % │ -│ undici - stream │ 15 │ 60.58 req/sec │ ± 2.75 % │ + 1037.72 % │ -│ undici - request │ 10 │ 61.19 req/sec │ ± 2.60 % │ + 1049.24 % │ -│ undici - dispatch │ 20 │ 64.84 req/sec │ ± 2.81 % │ + 1117.81 % │ +| Tests | Samples | Result | Tolerance | Difference with slowest | +|---------------------|---------|---------------|-----------|-------------------------| +| http - no keepalive | 15 | 5.32 req/sec | ± 2.61 % | - | +| http - keepalive | 10 | 5.35 req/sec | ± 2.47 % | + 0.44 % | +| undici - fetch | 15 | 41.85 req/sec | ± 2.49 % | + 686.04 % | +| undici - pipeline | 40 | 50.36 req/sec | ± 2.77 % | + 845.92 % | +| undici - stream | 15 | 60.58 req/sec | ± 2.75 % | + 1037.72 % | +| undici - request | 10 | 61.19 req/sec | ± 2.60 % | + 1049.24 % | +| undici - dispatch | 20 | 64.84 req/sec | ± 2.81 % | + 1117.81 % | ### Connections 50 -│ Tests │ Samples │ Result │ Tolerance │ Difference with slowest │ -|─────────────────────|─────────|──────────────────|───────────|─────────────────────────| -│ undici - fetch │ 30 │ 2107.19 req/sec │ ± 2.69 % │ - │ -│ http - no keepalive │ 10 │ 2698.90 req/sec │ ± 2.68 % │ + 28.08 % │ -│ http - keepalive │ 10 │ 4639.49 req/sec │ ± 2.55 % │ + 120.17 % │ -│ undici - pipeline │ 40 │ 6123.33 req/sec │ ± 2.97 % │ + 190.59 % │ -│ undici - stream │ 50 │ 9426.51 req/sec │ ± 2.92 % │ + 347.35 % │ -│ undici - request │ 10 │ 10162.88 req/sec │ ± 2.13 % │ + 382.29 % │ -│ undici - dispatch │ 50 │ 11191.11 req/sec │ ± 2.98 % │ + 431.09 % │ +| Tests | Samples | Result | Tolerance | Difference with slowest | +|---------------------|---------|------------------|-----------|-------------------------| +| undici - fetch | 30 | 2107.19 req/sec | ± 2.69 % | - | +| http - no keepalive | 10 | 2698.90 req/sec | ± 2.68 % | + 28.08 % | +| http - keepalive | 10 | 4639.49 req/sec | ± 2.55 % | + 120.17 % | +| undici - pipeline | 40 | 6123.33 req/sec | ± 2.97 % | + 190.59 % | +| undici - stream | 50 | 9426.51 req/sec | ± 2.92 % | + 347.35 % | +| undici - request | 10 | 10162.88 req/sec | ± 2.13 % | + 382.29 % | +| undici - dispatch | 50 | 11191.11 req/sec | ± 2.98 % | + 431.09 % | ## Quick Start From 1360d1f79679906a5eb38eae7c02e10dbc7cc9c1 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Fri, 8 Sep 2023 16:06:13 +0200 Subject: [PATCH 120/259] Fix http2 fetch test (#2253) * Fix http2 fetch test Signed-off-by: Matteo Collina * fixup Signed-off-by: Matteo Collina * fixup Signed-off-by: Matteo Collina * fixup Signed-off-by: Matteo Collina * fixup Signed-off-by: Matteo Collina * fixup Signed-off-by: Matteo Collina * fixup Signed-off-by: Matteo Collina --------- Signed-off-by: Matteo Collina --- test/fetch/http2.js | 6 +++-- test/redirect-request.js | 42 +++++++++++++++---------------- test/utils/redirecting-servers.js | 4 ++- 3 files changed, 28 insertions(+), 24 deletions(-) diff --git a/test/fetch/http2.js b/test/fetch/http2.js index 246c14e1eef..94de32dd36b 100644 --- a/test/fetch/http2.js +++ b/test/fetch/http2.js @@ -4,6 +4,7 @@ const { createSecureServer } = require('node:http2') const { createReadStream, readFileSync } = require('node:fs') const { once } = require('node:events') const { Blob } = require('node:buffer') +const { Readable } = require('node:stream') const { test, plan } = require('tap') const pem = require('https-pem') @@ -65,7 +66,8 @@ test('[Fetch] Should handle h2 request with body (string or buffer)', async t => t.equal(responseBody, expectedRequestBody) }) -test('[Fetch] Should handle h2 request with body (stream)', async t => { +// Skipping for now, there is something odd in the way the body is handled +test('[Fetch] Should handle h2 request with body (stream)', { skip: true }, async t => { const server = createSecureServer(pem) const expectedBody = readFileSync(__filename, 'utf-8') const stream = createReadStream(__filename) @@ -112,7 +114,7 @@ test('[Fetch] Should handle h2 request with body (stream)', async t => { 'x-my-header': 'foo', 'content-type': 'text-plain' }, - body: stream, + body: Readable.toWeb(stream), duplex: 'half' } ) diff --git a/test/redirect-request.js b/test/redirect-request.js index 46e60cd5471..5a1ae6de5b4 100644 --- a/test/redirect-request.js +++ b/test/redirect-request.js @@ -19,16 +19,16 @@ for (const factory of [ (server, opts) => new undici.Pool(`http://${server}`, opts), (server, opts) => new undici.Client(`http://${server}`, opts) ]) { - const request = (server, opts, ...args) => { + const request = (t, server, opts, ...args) => { const dispatcher = factory(server, opts) + t.teardown(() => dispatcher.close()) return undici.request(args[0], { ...args[1], dispatcher }, args[2]) - .finally(() => dispatcher.destroy()) } t.test('should always have a history with the final URL even if no redirections were followed', async t => { const server = await startRedirectingServer(t) - const { statusCode, headers, body: bodyStream, context: { history } } = await request(server, undefined, `http://${server}/200?key=value`, { + const { statusCode, headers, body: bodyStream, context: { history } } = await request(t, server, undefined, `http://${server}/200?key=value`, { maxRedirections: 10 }) @@ -43,7 +43,7 @@ for (const factory of [ t.test('should not follow redirection by default if not using RedirectAgent', async t => { const server = await startRedirectingServer(t) - const { statusCode, headers, body: bodyStream } = await request(server, undefined, `http://${server}`) + const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}`) const body = await bodyStream.text() t.equal(statusCode, 302) @@ -54,7 +54,7 @@ for (const factory of [ t.test('should follow redirection after a HTTP 300', async t => { const server = await startRedirectingServer(t) - const { statusCode, headers, body: bodyStream, context: { history } } = await request(server, undefined, `http://${server}/300?key=value`, { + const { statusCode, headers, body: bodyStream, context: { history } } = await request(t, server, undefined, `http://${server}/300?key=value`, { maxRedirections: 10 }) @@ -76,7 +76,7 @@ for (const factory of [ t.test('should follow redirection after a HTTP 300 default', async t => { const server = await startRedirectingServer(t) - const { statusCode, headers, body: bodyStream, context: { history } } = await request(server, { maxRedirections: 10 }, `http://${server}/300?key=value`) + const { statusCode, headers, body: bodyStream, context: { history } } = await request(t, server, { maxRedirections: 10 }, `http://${server}/300?key=value`) const body = await bodyStream.text() t.equal(statusCode, 200) @@ -95,7 +95,7 @@ for (const factory of [ t.test('should follow redirection after a HTTP 301', async t => { const server = await startRedirectingServer(t) - const { statusCode, headers, body: bodyStream } = await request(server, undefined, `http://${server}/301`, { + const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/301`, { method: 'POST', body: 'REQUEST', maxRedirections: 10 @@ -111,7 +111,7 @@ for (const factory of [ t.test('should follow redirection after a HTTP 302', async t => { const server = await startRedirectingServer(t) - const { statusCode, headers, body: bodyStream } = await request(server, undefined, `http://${server}/302`, { + const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/302`, { method: 'PUT', body: Buffer.from('REQUEST'), maxRedirections: 10 @@ -127,7 +127,7 @@ for (const factory of [ t.test('should follow redirection after a HTTP 303 changing method to GET', async t => { const server = await startRedirectingServer(t) - const { statusCode, headers, body: bodyStream } = await request(server, undefined, `http://${server}/303`, { + const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/303`, { method: 'PATCH', body: 'REQUEST', maxRedirections: 10 @@ -143,7 +143,7 @@ for (const factory of [ t.test('should remove Host and request body related headers when following HTTP 303 (array)', async t => { const server = await startRedirectingServer(t) - const { statusCode, headers, body: bodyStream } = await request(server, undefined, `http://${server}/303`, { + const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/303`, { method: 'PATCH', headers: [ 'Content-Encoding', @@ -174,7 +174,7 @@ for (const factory of [ t.test('should remove Host and request body related headers when following HTTP 303 (object)', async t => { const server = await startRedirectingServer(t) - const { statusCode, headers, body: bodyStream } = await request(server, undefined, `http://${server}/303`, { + const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/303`, { method: 'PATCH', headers: { 'Content-Encoding': 'gzip', @@ -198,7 +198,7 @@ for (const factory of [ t.test('should follow redirection after a HTTP 307', async t => { const server = await startRedirectingServer(t) - const { statusCode, headers, body: bodyStream } = await request(server, undefined, `http://${server}/307`, { + const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/307`, { method: 'DELETE', maxRedirections: 10 }) @@ -213,7 +213,7 @@ for (const factory of [ t.test('should follow redirection after a HTTP 308', async t => { const server = await startRedirectingServer(t) - const { statusCode, headers, body: bodyStream } = await request(server, undefined, `http://${server}/308`, { + const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/308`, { method: 'OPTIONS', maxRedirections: 10 }) @@ -228,7 +228,7 @@ for (const factory of [ t.test('should ignore HTTP 3xx response bodies', async t => { const server = await startRedirectingWithBodyServer(t) - const { statusCode, headers, body: bodyStream, context: { history } } = await request(server, undefined, `http://${server}/`, { + const { statusCode, headers, body: bodyStream, context: { history } } = await request(t, server, undefined, `http://${server}/`, { maxRedirections: 10 }) @@ -243,7 +243,7 @@ for (const factory of [ t.test('should ignore query after redirection', async t => { const server = await startRedirectingWithQueryParams(t) - const { statusCode, headers, context: { history } } = await request(server, undefined, `http://${server}/`, { + const { statusCode, headers, context: { history } } = await request(t, server, undefined, `http://${server}/`, { maxRedirections: 10, query: { param1: 'first' } }) @@ -256,7 +256,7 @@ for (const factory of [ t.test('should follow a redirect chain up to the allowed number of times', async t => { const server = await startRedirectingServer(t) - const { statusCode, headers, body: bodyStream, context: { history } } = await request(server, undefined, `http://${server}/300`, { + const { statusCode, headers, body: bodyStream, context: { history } } = await request(t, server, undefined, `http://${server}/300`, { maxRedirections: 2 }) @@ -273,7 +273,7 @@ for (const factory of [ const server = await startRedirectingWithoutLocationServer(t) for (const code of redirectCodes) { - const { statusCode, headers, body: bodyStream } = await request(server, undefined, `http://${server}/${code}`, { + const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/${code}`, { maxRedirections: 10 }) @@ -287,7 +287,7 @@ for (const factory of [ t.test('should not allow invalid maxRedirections arguments', async t => { try { - await request('localhost', undefined, 'http://localhost', { + await request(t, 'localhost', undefined, 'http://localhost', { method: 'GET', maxRedirections: 'INVALID' }) @@ -300,7 +300,7 @@ for (const factory of [ t.test('should not allow invalid maxRedirections arguments default', async t => { try { - await request('localhost', { + await request(t, 'localhost', { maxRedirections: 'INVALID' }, 'http://localhost', { method: 'GET' @@ -315,7 +315,7 @@ for (const factory of [ t.test('should not follow redirects when using ReadableStream request bodies', { skip: nodeMajor < 16 }, async t => { const server = await startRedirectingServer(t) - const { statusCode, headers, body: bodyStream } = await request(server, undefined, `http://${server}/301`, { + const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/301`, { method: 'POST', body: createReadableStream('REQUEST'), maxRedirections: 10 @@ -331,7 +331,7 @@ for (const factory of [ t.test('should not follow redirects when using Readable request bodies', async t => { const server = await startRedirectingServer(t) - const { statusCode, headers, body: bodyStream } = await request(server, undefined, `http://${server}/301`, { + const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/301`, { method: 'POST', body: createReadable('REQUEST'), maxRedirections: 10 diff --git a/test/utils/redirecting-servers.js b/test/utils/redirecting-servers.js index 53d674c4862..ad8aa583fdb 100644 --- a/test/utils/redirecting-servers.js +++ b/test/utils/redirecting-servers.js @@ -2,10 +2,12 @@ const { createServer } = require('http') +const isNode20 = process.version.startsWith('v20.') + function close (server) { return function () { return new Promise(resolve => { - if (typeof server.closeAllConnections === 'function') { + if (isNode20) { server.closeAllConnections() } server.close(resolve) From 9fa8224c274d52f67cd82d4bb820e72627df1e9f Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Fri, 8 Sep 2023 16:09:23 +0200 Subject: [PATCH 121/259] Bumped v5.24.0 Signed-off-by: Matteo Collina --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 863e85604f4..d4baa312a24 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "5.23.0", + "version": "5.24.0", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { @@ -133,4 +133,4 @@ "dependencies": { "busboy": "^1.6.0" } -} \ No newline at end of file +} From e39a6324c4474c6614cac98b8668e3d036aa6b18 Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Sun, 10 Sep 2023 02:24:38 +0200 Subject: [PATCH 122/259] fix: h2 without body (#2258) * fix: h2 without body * refactor: close writable on non-expected body requests --- lib/client.js | 9 +++++++-- lib/core/request.js | 4 +++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/client.js b/lib/client.js index ebc9a6afc11..50d5ca34ae3 100644 --- a/lib/client.js +++ b/lib/client.js @@ -1687,6 +1687,7 @@ function writeH2 (client, session, request) { // we are already connected, streams are pending, first request // will create a new stream. We trigger a request to create the stream and wait until // `ready` event is triggered + // We disabled endStream to allow the user to write to the stream stream = session.request(headers, { endStream: false, signal }) if (stream.id && !stream.pending) { @@ -1761,17 +1762,21 @@ function writeH2 (client, session, request) { session.ref() + const shouldEndStream = method === 'GET' || method === 'HEAD' if (expectContinue) { headers[HTTP2_HEADER_EXPECT] = '100-continue' /** * @type {import('node:http2').ClientHttp2Stream} */ - stream = session.request(headers, { endStream: false, signal }) + stream = session.request(headers, { endStream: shouldEndStream, signal }) stream.once('continue', writeBodyH2) } else { /** @type {import('node:http2').ClientHttp2Stream} */ - stream = session.request(headers, { endStream: false, signal }) + stream = session.request(headers, { + endStream: shouldEndStream, + signal + }) writeBodyH2() } diff --git a/lib/core/request.js b/lib/core/request.js index 3ddc7fdbe32..e3b0c7b9dbf 100644 --- a/lib/core/request.js +++ b/lib/core/request.js @@ -320,11 +320,13 @@ class Request { static [kHTTP2CopyHeaders] (raw) { const rawHeaders = raw.split('\r\n') - const headers = {} + for (const header of rawHeaders) { const [key, value] = header.split(': ') + if (value == null || value.length === 0) continue + if (headers[key]) headers[key] += `,${value}` else headers[key] = value } From 1e2e75278a6eab57ea99289684759d73a768b5f6 Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Fri, 15 Sep 2023 14:24:44 +0200 Subject: [PATCH 123/259] ci: remove duplicated runs (#2265) --- .github/workflows/nodejs.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 27533ab77bf..4c3a77ec1e3 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -4,7 +4,13 @@ name: Node CI -on: [push, pull_request] +on: + push: + branches: + - current + - next + - 'v*' + pull_request: jobs: build: From a41c6e68554d0938eb043952c6d36a21f04a83ec Mon Sep 17 00:00:00 2001 From: Fred Cox Date: Fri, 15 Sep 2023 15:29:33 +0100 Subject: [PATCH 124/259] improve documentation of timeouts by making the units clear in all places (#2266) --- docs/api/Client.md | 10 +++++----- docs/api/Connector.md | 4 ++-- docs/api/Dispatcher.md | 2 +- types/client.d.ts | 14 +++++++------- types/dispatcher.d.ts | 2 +- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/api/Client.md b/docs/api/Client.md index 591fb97c44b..c0987713a32 100644 --- a/docs/api/Client.md +++ b/docs/api/Client.md @@ -20,10 +20,10 @@ Returns: `Client` > ⚠️ Warning: The `H2` support is experimental. * **bodyTimeout** `number | null` (optional) - Default: `300e3` - 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 300 seconds. -* **headersTimeout** `number | null` (optional) - Default: `300e3` - The amount of time the parser will wait to receive the complete HTTP headers while not sending the request. Defaults to 300 seconds. -* **keepAliveMaxTimeout** `number | null` (optional) - Default: `600e3` - The maximum allowed `keepAliveTimeout` when overridden by *keep-alive* hints from the server. Defaults to 10 minutes. -* **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. -* **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. +* **headersTimeout** `number | null` (optional) - Default: `300e3` - The amount of time, in milliseconds, the parser will wait to receive the complete HTTP headers while not sending the request. Defaults to 300 seconds. +* **keepAliveMaxTimeout** `number | null` (optional) - Default: `600e3` - The maximum allowed `keepAliveTimeout`, in milliseconds, when overridden by *keep-alive* hints from the server. Defaults to 10 minutes. +* **keepAliveTimeout** `number | null` (optional) - Default: `4e3` - The timeout, in milliseconds, 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. +* **keepAliveTimeoutThreshold** `number | null` (optional) - Default: `1e3` - A number of milliseconds subtracted from server *keep-alive* hints when overriding `keepAliveTimeout` to account for timing inaccuracies caused by e.g. transport latency. Defaults to 1 second. * **maxHeaderSize** `number | null` (optional) - Default: `16384` - The maximum length of request headers in bytes. Defaults to 16KiB. * **maxResponseSize** `number | null` (optional) - Default: `-1` - The maximum length of response body in bytes. Set to `-1` to disable. * **pipelining** `number | null` (optional) - Default: `1` - The amount of concurrent requests to be sent over the single TCP/TLS connection according to [RFC7230](https://tools.ietf.org/html/rfc7230#section-6.3.2). Carefully consider your workload and environment before enabling concurrent requests as pipelining may reduce performance if used incorrectly. Pipelining is sensitive to network stack settings as well as head of line blocking caused by e.g. long running requests. Set to `0` to disable keep-alive connections. @@ -42,7 +42,7 @@ Furthermore, the following options can be passed: * **socketPath** `string | null` (optional) - Default: `null` - An IPC endpoint, either Unix domain socket or Windows named pipe. * **maxCachedSessions** `number | null` (optional) - Default: `100` - Maximum number of TLS cached sessions. Use 0 to disable TLS session caching. Default: 100. -* **timeout** `number | null` (optional) - Default `10e3` +* **timeout** `number | null` (optional) - In milliseconds, Default `10e3`. * **servername** `string | null` (optional) * **keepAlive** `boolean | null` (optional) - Default: `true` - TCP keep-alive enabled * **keepAliveInitialDelay** `number | null` (optional) - Default: `60000` - TCP keep-alive interval for the socket in milliseconds diff --git a/docs/api/Connector.md b/docs/api/Connector.md index 7c966507e5f..56821bd6430 100644 --- a/docs/api/Connector.md +++ b/docs/api/Connector.md @@ -13,8 +13,8 @@ Every Tls option, see [here](https://nodejs.org/api/tls.html#tls_tls_connect_opt Furthermore, the following options can be passed: * **socketPath** `string | null` (optional) - Default: `null` - An IPC endpoint, either Unix domain socket or Windows named pipe. -* **maxCachedSessions** `number | null` (optional) - Default: `100` - Maximum number of TLS cached sessions. Use 0 to disable TLS session caching. Default: 100. -* **timeout** `number | null` (optional) - Default `10e3` +* **maxCachedSessions** `number | null` (optional) - Default: `100` - Maximum number of TLS cached sessions. Use 0 to disable TLS session caching. Default: `100`. +* **timeout** `number | null` (optional) - In milliseconds. Default `10e3`. * **servername** `string | null` (optional) Once you call `buildConnector`, it will return a connector function, which takes the following parameters. diff --git a/docs/api/Dispatcher.md b/docs/api/Dispatcher.md index a04be8cff1e..fd463bfea16 100644 --- a/docs/api/Dispatcher.md +++ b/docs/api/Dispatcher.md @@ -200,7 +200,7 @@ Returns: `Boolean` - `false` if dispatcher is busy and further dispatch calls wo * **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. * **upgrade** `string | null` (optional) - Default: `null` - Upgrade the request. Should be used to specify the kind of upgrade i.e. `'Websocket'`. * **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 300 seconds. -* **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 300 seconds. +* **headersTimeout** `number | null` (optional) - The amount of time, in milliseconds, the parser will wait to receive the complete HTTP headers while not sending the request. Defaults to 300 seconds. * **throwOnError** `boolean` (optional) - Default: `false` - Whether Undici should throw an error upon receiving a 4xx or 5xx response from the server. * **expectContinue** `boolean` (optional) - Default: `false` - For H2, it appends the expect: 100-continue header, and halts the request body until a 100-continue is received from the remote server diff --git a/types/client.d.ts b/types/client.d.ts index 3d348f9427b..0ed42cd4f1f 100644 --- a/types/client.d.ts +++ b/types/client.d.ts @@ -26,7 +26,7 @@ export declare namespace Client { interceptors?: OptionsInterceptors; /** The maximum length of request headers in bytes. Default: `16384` (16KiB). */ maxHeaderSize?: number; - /** The amount of time the parser will wait to receive the complete HTTP headers (Node 14 and above only). Default: `300e3` milliseconds (300s). */ + /** The amount of time, in milliseconds, the parser will wait to receive the complete HTTP headers (Node 14 and above only). Default: `300e3` milliseconds (300s). */ headersTimeout?: number; /** @deprecated unsupported socketTimeout, use headersTimeout & bodyTimeout instead */ socketTimeout?: never; @@ -40,13 +40,13 @@ export declare namespace Client { idleTimeout?: never; /** @deprecated unsupported keepAlive, use pipelining=0 instead */ keepAlive?: never; - /** 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. Default: `4e3` milliseconds (4s). */ + /** the timeout, in milliseconds, 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. Default: `4e3` milliseconds (4s). */ keepAliveTimeout?: number; /** @deprecated unsupported maxKeepAliveTimeout, use keepAliveMaxTimeout instead */ maxKeepAliveTimeout?: never; - /** the maximum allowed `idleTimeout` when overridden by *keep-alive* hints from the server. Default: `600e3` milliseconds (10min). */ + /** the maximum allowed `idleTimeout`, in milliseconds, when overridden by *keep-alive* hints from the server. Default: `600e3` milliseconds (10min). */ keepAliveMaxTimeout?: number; - /** A number subtracted from server *keep-alive* hints when overriding `idleTimeout` to account for timing inaccuracies caused by e.g. transport latency. Default: `1e3` milliseconds (1s). */ + /** A number of milliseconds subtracted from server *keep-alive* hints when overriding `idleTimeout` to account for timing inaccuracies caused by e.g. transport latency. Default: `1e3` milliseconds (1s). */ keepAliveTimeoutThreshold?: number; /** TODO */ socketPath?: string; @@ -71,13 +71,13 @@ export declare namespace Client { /** Enables a family autodetection algorithm that loosely implements section 5 of RFC 8305. */ autoSelectFamily?: boolean; /** The amount of time in milliseconds to wait for a connection attempt to finish before trying the next address when using the `autoSelectFamily` option. */ - autoSelectFamilyAttemptTimeout?: number; - /** + autoSelectFamilyAttemptTimeout?: number; + /** * @description Enables support for H2 if the server has assigned bigger priority to it through ALPN negotiation. * @default false */ allowH2?: boolean; - /** + /** * @description Dictates the maximum number of concurrent streams for a single H2 session. It can be overriden by a SETTINGS remote frame. * @default 100 */ diff --git a/types/dispatcher.d.ts b/types/dispatcher.d.ts index 42a78ba0834..816db19d20d 100644 --- a/types/dispatcher.d.ts +++ b/types/dispatcher.d.ts @@ -109,7 +109,7 @@ declare namespace Dispatcher { blocking?: boolean; /** Upgrade the request. Should be used to specify the kind of upgrade i.e. `'Websocket'`. Default: `method === 'CONNECT' || null`. */ upgrade?: boolean | string | null; - /** The amount of time the parser will wait to receive the complete HTTP headers. Defaults to 300 seconds. */ + /** The amount of time, in milliseconds, the parser will wait to receive the complete HTTP headers. Defaults to 300 seconds. */ headersTimeout?: number | null; /** 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 300 seconds. */ bodyTimeout?: number | null; From a926023a247f6b5559f4384ab82dc5e30ac4f99c Mon Sep 17 00:00:00 2001 From: Khafra Date: Fri, 15 Sep 2023 18:08:24 -0400 Subject: [PATCH 125/259] expose websocket in node bundle (#2217) --- index-fetch.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/index-fetch.js b/index-fetch.js index 0d59d254f7d..23ac5306007 100644 --- a/index-fetch.js +++ b/index-fetch.js @@ -2,9 +2,9 @@ const fetchImpl = require('./lib/fetch').fetch -module.exports.fetch = async function fetch (resource) { +module.exports.fetch = async function fetch (resource, init = undefined) { try { - return await fetchImpl(...arguments) + return await fetchImpl(resource, init) } catch (err) { Error.captureStackTrace(err, this) throw err @@ -14,3 +14,4 @@ module.exports.FormData = require('./lib/fetch/formdata').FormData module.exports.Headers = require('./lib/fetch/headers').Headers module.exports.Response = require('./lib/fetch/response').Response module.exports.Request = require('./lib/fetch/request').Request +module.exports.WebSocket = require('./lib/websocket/websocket').WebSocket From b0d3ca7701766b6cb9ebb8fed7dd08d3a684b91b Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Sat, 16 Sep 2023 16:49:27 +0200 Subject: [PATCH 126/259] test: fix Fetch/HTTP2 tests (#2263) --- test/fetch/http2.js | 62 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 4 deletions(-) diff --git a/test/fetch/http2.js b/test/fetch/http2.js index 94de32dd36b..e1d81d197d8 100644 --- a/test/fetch/http2.js +++ b/test/fetch/http2.js @@ -11,7 +11,59 @@ const pem = require('https-pem') const { Client, fetch } = require('../..') -plan(4) +const nodeVersion = Number(process.version.split('v')[1].split('.')[0]) + +plan(5) + +test('[Fetch] Simple GET with h2', async t => { + const server = createSecureServer(pem) + const expectedRequestBody = 'hello h2!' + + server.on('stream', async (stream, headers) => { + stream.respond({ + 'content-type': 'text/plain; charset=utf-8', + 'x-custom-h2': headers['x-my-header'], + 'x-method': headers[':method'], + ':status': 200 + }) + + stream.end(expectedRequestBody) + }) + + t.plan(3) + + server.listen() + await once(server, 'listening') + + const client = new Client(`https://localhost:${server.address().port}`, { + connect: { + rejectUnauthorized: false + }, + allowH2: true + }) + + const response = await fetch( + `https://localhost:${server.address().port}/`, + // Needs to be passed to disable the reject unauthorized + { + method: 'GET', + dispatcher: client, + headers: { + 'x-my-header': 'foo', + 'content-type': 'text-plain' + } + } + ) + + const responseBody = await response.text() + + t.teardown(server.close.bind(server)) + t.teardown(client.close.bind(client)) + + t.equal(responseBody, expectedRequestBody) + t.equal(response.headers.get('x-method'), 'GET') + t.equal(response.headers.get('x-custom-h2'), 'foo') +}) test('[Fetch] Should handle h2 request with body (string or buffer)', async t => { const server = createSecureServer(pem) @@ -67,7 +119,7 @@ test('[Fetch] Should handle h2 request with body (string or buffer)', async t => }) // Skipping for now, there is something odd in the way the body is handled -test('[Fetch] Should handle h2 request with body (stream)', { skip: true }, async t => { +test('[Fetch] Should handle h2 request with body (stream)', { skip: nodeVersion === 16 }, async t => { const server = createSecureServer(pem) const expectedBody = readFileSync(__filename, 'utf-8') const stream = createReadStream(__filename) @@ -78,8 +130,6 @@ test('[Fetch] Should handle h2 request with body (stream)', { skip: true }, asyn t.equal(headers[':path'], '/') t.equal(headers[':scheme'], 'https') - stream.on('data', chunk => requestChunks.push(chunk)) - stream.respond({ 'content-type': 'text/plain; charset=utf-8', 'x-custom-h2': headers['x-my-header'], @@ -87,6 +137,10 @@ test('[Fetch] Should handle h2 request with body (stream)', { skip: true }, asyn }) stream.end('hello h2!') + + for await (const chunk of stream) { + requestChunks.push(chunk) + } }) t.plan(8) From 8c5577bcda00b0d3fdbe5297bfbc783a3efb08a7 Mon Sep 17 00:00:00 2001 From: Khafra Date: Tue, 19 Sep 2023 01:58:44 -0400 Subject: [PATCH 127/259] fix undici when node is built with --without-ssl (#2272) --- lib/client.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/client.js b/lib/client.js index 50d5ca34ae3..b5170d4f88d 100644 --- a/lib/client.js +++ b/lib/client.js @@ -6,7 +6,6 @@ const assert = require('assert') const net = require('net') -const http2 = require('http2') const { pipeline } = require('stream') const util = require('./core/util') const timers = require('./timers') @@ -79,6 +78,16 @@ const { kHTTP2CopyHeaders, kHTTP1BuildRequest } = require('./core/symbols') + +/** @type {import('http2')} */ +let http2 +try { + http2 = require('http2') +} catch { + // @ts-ignore + http2 = { constants: {} } +} + const { constants: { HTTP2_HEADER_AUTHORITY, From 4d7c319d67a73f7284216ac38e81be4f94f70cde Mon Sep 17 00:00:00 2001 From: Scott Kool Date: Tue, 19 Sep 2023 08:12:45 -0500 Subject: [PATCH 128/259] Fix type definition for Client Interceptors (#2269) Updating the client constructor to accept an array of interceptor functions to match the documentation. --- test/types/client.test-d.ts | 12 ++++++++++++ types/client.d.ts | 3 +-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/test/types/client.test-d.ts b/test/types/client.test-d.ts index d9c1e447813..c416d779bec 100644 --- a/test/types/client.test-d.ts +++ b/test/types/client.test-d.ts @@ -68,6 +68,18 @@ expectAssignable(new Client(new URL('http://localhost'), {})) expectAssignable(new Client('', { autoSelectFamilyAttemptTimeout: 300e3 })) + expectAssignable(new Client('', { + interceptors: { + Client: [(dispatcher) => { + expectAssignable(dispatcher); + return (opts, handlers) => { + expectAssignable(opts); + expectAssignable(handlers); + return dispatcher(opts, handlers) + } + }] + } + })) } { diff --git a/types/client.d.ts b/types/client.d.ts index 0ed42cd4f1f..ac1779721f6 100644 --- a/types/client.d.ts +++ b/types/client.d.ts @@ -1,7 +1,6 @@ import { URL } from 'url' import { TlsOptions } from 'tls' import Dispatcher from './dispatcher' -import DispatchInterceptor from './dispatcher' import buildConnector from "./connector"; /** @@ -19,7 +18,7 @@ export class Client extends Dispatcher { export declare namespace Client { export interface OptionsInterceptors { - Client: readonly DispatchInterceptor[]; + Client: readonly Dispatcher.DispatchInterceptor[]; } export interface Options { /** TODO */ From d1e867e1cddbfed6ee405c8d8872ed4f0c964689 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Wed, 20 Sep 2023 14:00:33 +0200 Subject: [PATCH 129/259] Fix http2 agent (#2275) --- lib/pool.js | 4 +++- test/http2.js | 53 +++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/lib/pool.js b/lib/pool.js index 93b3158f21a..08509958069 100644 --- a/lib/pool.js +++ b/lib/pool.js @@ -34,6 +34,7 @@ class Pool extends PoolBase { socketPath, autoSelectFamily, autoSelectFamilyAttemptTimeout, + allowH2, ...options } = {}) { super() @@ -54,6 +55,7 @@ class Pool extends PoolBase { connect = buildConnector({ ...tls, maxCachedSessions, + allowH2, socketPath, timeout: connectTimeout == null ? 10e3 : connectTimeout, ...(util.nodeHasAutoSelectFamily && autoSelectFamily ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined), @@ -66,7 +68,7 @@ class Pool extends PoolBase { : [] this[kConnections] = connections || null this[kUrl] = util.parseOrigin(origin) - this[kOptions] = { ...util.deepClone(options), connect } + this[kOptions] = { ...util.deepClone(options), connect, allowH2 } this[kOptions].interceptors = options.interceptors ? { ...options.interceptors } : undefined diff --git a/test/http2.js b/test/http2.js index 8fd9c616bc3..538255399eb 100644 --- a/test/http2.js +++ b/test/http2.js @@ -9,11 +9,11 @@ const { Writable, pipeline, PassThrough, Readable } = require('node:stream') const { test, plan } = require('tap') const pem = require('https-pem') -const { Client } = require('..') +const { Client, Agent } = require('..') const isGreaterThanv20 = Number(process.version.slice(1).split('.')[0]) >= 20 -plan(18) +plan(19) test('Should support H2 connection', async t => { const body = [] @@ -947,3 +947,52 @@ test( t.equal(Buffer.concat(requestChunks).toString('utf-8'), expectedBody) } ) + +test('Agent should support H2 connection', async t => { + const body = [] + const server = createSecureServer(pem) + + server.on('stream', (stream, headers) => { + t.equal(headers['x-my-header'], 'foo') + t.equal(headers[':method'], 'GET') + stream.respond({ + 'content-type': 'text/plain; charset=utf-8', + 'x-custom-h2': 'hello', + ':status': 200 + }) + stream.end('hello h2!') + }) + + server.listen(0) + await once(server, 'listening') + + const client = new Agent({ + connect: { + rejectUnauthorized: false + }, + allowH2: true + }) + + t.plan(6) + t.teardown(server.close.bind(server)) + t.teardown(client.close.bind(client)) + + const response = await client.request({ + origin: `https://localhost:${server.address().port}`, + path: '/', + method: 'GET', + headers: { + 'x-my-header': 'foo' + } + }) + + response.body.on('data', chunk => { + body.push(chunk) + }) + + await once(response.body, 'end') + t.equal(response.statusCode, 200) + t.equal(response.headers['content-type'], 'text/plain; charset=utf-8') + t.equal(response.headers['x-custom-h2'], 'hello') + t.equal(Buffer.concat(body).toString('utf8'), 'hello h2!') +}) From 985b3816708512bafefa1544def183cc6d1536be Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Wed, 20 Sep 2023 14:55:52 +0200 Subject: [PATCH 130/259] Bumped v5.25.0 Signed-off-by: Matteo Collina --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d4baa312a24..ad40c04ad2b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "5.24.0", + "version": "5.25.0", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { From 5c2e597bd938ce99a0246f9dfbfac6d3f3a31be9 Mon Sep 17 00:00:00 2001 From: Ethan Arrowood Date: Wed, 20 Sep 2023 15:02:42 -0600 Subject: [PATCH 131/259] Add publish types script (#2273) * add publish types script * use postpublish script * 5.24.0-test.0 * 5.24.0-test.1 * uncomment * 5.24.0-test.2 * simplify automation * 5.24.0-test.3 * fix script * 5.24.0-test.4 * fix script * 5.24.0-test.5 * undici-type@5.24.0-test.5 * restore versions * delete generated file * Update update-undici-types-version.js Co-authored-by: Superchupu <53496941+SuperchupuDev@users.noreply.github.com> * fix contributor list and fix types export * this should fix types * one more adjustment * 5.24.0-test.6 * undici-types@5.24.0-test.6 * revert versions and add README for undici-types --------- Co-authored-by: Superchupu <53496941+SuperchupuDev@users.noreply.github.com> --- index.d.ts | 58 +------------------------- package.json | 32 +++++++++++++- scripts/update-undici-types-version.js | 18 ++++++++ types/README.md | 6 +++ types/index.d.ts | 57 +++++++++++++++++++++++++ types/package.json | 55 ++++++++++++++++++++++++ 6 files changed, 169 insertions(+), 57 deletions(-) create mode 100644 scripts/update-undici-types-version.js create mode 100644 types/README.md create mode 100644 types/index.d.ts create mode 100644 types/package.json diff --git a/index.d.ts b/index.d.ts index 0730677b29e..83a786d6a03 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,57 +1,3 @@ -import Dispatcher from'./types/dispatcher' -import { setGlobalDispatcher, getGlobalDispatcher } from './types/global-dispatcher' -import { setGlobalOrigin, getGlobalOrigin } from './types/global-origin' -import Pool from'./types/pool' -import { RedirectHandler, DecoratorHandler } from './types/handlers' - -import BalancedPool from './types/balanced-pool' -import Client from'./types/client' -import buildConnector from'./types/connector' -import errors from'./types/errors' -import Agent from'./types/agent' -import MockClient from'./types/mock-client' -import MockPool from'./types/mock-pool' -import MockAgent from'./types/mock-agent' -import mockErrors from'./types/mock-errors' -import ProxyAgent from'./types/proxy-agent' -import { request, pipeline, stream, connect, upgrade } from './types/api' - -export * from './types/cookies' -export * from './types/fetch' -export * from './types/file' -export * from './types/filereader' -export * from './types/formdata' -export * from './types/diagnostics-channel' -export * from './types/websocket' -export * from './types/content-type' -export * from './types/cache' -export { Interceptable } from './types/mock-interceptor' - -export { Dispatcher, BalancedPool, Pool, Client, buildConnector, errors, Agent, request, stream, pipeline, connect, upgrade, setGlobalDispatcher, getGlobalDispatcher, setGlobalOrigin, getGlobalOrigin, MockClient, MockPool, MockAgent, mockErrors, ProxyAgent, RedirectHandler, DecoratorHandler } +export * from './types/index' +import Undici from './types/index' export default Undici - -declare namespace Undici { - var Dispatcher: typeof import('./types/dispatcher').default - var Pool: typeof import('./types/pool').default; - var RedirectHandler: typeof import ('./types/handlers').RedirectHandler - var DecoratorHandler: typeof import ('./types/handlers').DecoratorHandler - var createRedirectInterceptor: typeof import ('./types/interceptors').createRedirectInterceptor - var BalancedPool: typeof import('./types/balanced-pool').default; - var Client: typeof import('./types/client').default; - var buildConnector: typeof import('./types/connector').default; - var errors: typeof import('./types/errors').default; - var Agent: typeof import('./types/agent').default; - var setGlobalDispatcher: typeof import('./types/global-dispatcher').setGlobalDispatcher; - var getGlobalDispatcher: typeof import('./types/global-dispatcher').getGlobalDispatcher; - var request: typeof import('./types/api').request; - var stream: typeof import('./types/api').stream; - var pipeline: typeof import('./types/api').pipeline; - var connect: typeof import('./types/api').connect; - var upgrade: typeof import('./types/api').upgrade; - var MockClient: typeof import('./types/mock-client').default; - var MockPool: typeof import('./types/mock-pool').default; - var MockAgent: typeof import('./types/mock-agent').default; - var mockErrors: typeof import('./types/mock-errors').default; - var fetch: typeof import('./types/fetch').fetch; - var caches: typeof import('./types/cache').caches; -} diff --git a/package.json b/package.json index ad40c04ad2b..0262ffa08fe 100644 --- a/package.json +++ b/package.json @@ -11,12 +11,41 @@ "url": "git+https://github.com/nodejs/undici.git" }, "license": "MIT", - "author": "Matteo Collina ", "contributors": [ + { + "name": "Daniele Belardi", + "url": "https://github.com/dnlup", + "author": true + }, + { + "name": "Ethan Arrowood", + "url": "https://github.com/ethan-arrowood", + "author": true + }, + { + "name": "Matteo Collina", + "url": "https://github.com/mcollina", + "author": true + }, + { + "name": "Matthew Aitken", + "url": "https://github.com/KhafraDev", + "author": true + }, { "name": "Robert Nagy", "url": "https://github.com/ronag", "author": true + }, + { + "name": "Szymon Marczak", + "url": "https://github.com/szmarczak", + "author": true + }, + { + "name": "Tomas Della Vedova", + "url": "https://github.com/delvedor", + "author": true } ], "keywords": [ @@ -64,6 +93,7 @@ "bench:run": "CONNECTIONS=1 node benchmarks/benchmark.js; CONNECTIONS=50 node benchmarks/benchmark.js", "serve:website": "docsify serve .", "prepare": "husky install", + "postpublish": "node scripts/update-undici-types-version.js && cd types && npm publish", "fuzz": "jsfuzz test/fuzzing/fuzz.js corpus" }, "devDependencies": { diff --git a/scripts/update-undici-types-version.js b/scripts/update-undici-types-version.js new file mode 100644 index 00000000000..2308e97905a --- /dev/null +++ b/scripts/update-undici-types-version.js @@ -0,0 +1,18 @@ +const fs = require('node:fs') +const path = require('node:path') +const childProcess = require('node:child_process') + +const packageJSONPath = path.join(__dirname, '..', 'package.json') +const packageJSONRaw = fs.readFileSync(packageJSONPath, 'utf-8') +const packageJSON = JSON.parse(packageJSONRaw) +const version = packageJSON.version + +const packageTypesJSONPath = path.join(__dirname, '..', 'types', 'package.json') +const packageTypesJSONRaw = fs.readFileSync(packageTypesJSONPath, 'utf-8') +const packageTypesJSON = JSON.parse(packageTypesJSONRaw) +packageTypesJSON.version = version + +fs.writeFileSync(packageTypesJSONPath, JSON.stringify(packageTypesJSON, null, 2)) + +childProcess.execSync('git add types/package.json', { cwd: path.join(__dirname, '..') }) +childProcess.execSync(`git commit -n -m 'undici-types@${version}'`, { cwd: path.join(__dirname, '..') }) diff --git a/types/README.md b/types/README.md new file mode 100644 index 00000000000..20a721c445a --- /dev/null +++ b/types/README.md @@ -0,0 +1,6 @@ +# undici-types + +This package is a dual-publish of the [undici](https://www.npmjs.com/package/undici) library types. The `undici` package **still contains types**. This package is for users who _only_ need undici types (such as for `@types/node`). It is published alongside every release of `undici`, so you can always use the same version. + +- [GitHub nodejs/undici](https://github.com/nodejs/undici) +- [Undici Documentation](https://undici.nodejs.org/#/) diff --git a/types/index.d.ts b/types/index.d.ts new file mode 100644 index 00000000000..c7532d69a07 --- /dev/null +++ b/types/index.d.ts @@ -0,0 +1,57 @@ +import Dispatcher from'./dispatcher' +import { setGlobalDispatcher, getGlobalDispatcher } from './global-dispatcher' +import { setGlobalOrigin, getGlobalOrigin } from './global-origin' +import Pool from'./pool' +import { RedirectHandler, DecoratorHandler } from './handlers' + +import BalancedPool from './balanced-pool' +import Client from'./client' +import buildConnector from'./connector' +import errors from'./errors' +import Agent from'./agent' +import MockClient from'./mock-client' +import MockPool from'./mock-pool' +import MockAgent from'./mock-agent' +import mockErrors from'./mock-errors' +import ProxyAgent from'./proxy-agent' +import { request, pipeline, stream, connect, upgrade } from './api' + +export * from './cookies' +export * from './fetch' +export * from './file' +export * from './filereader' +export * from './formdata' +export * from './diagnostics-channel' +export * from './websocket' +export * from './content-type' +export * from './cache' +export { Interceptable } from './mock-interceptor' + +export { Dispatcher, BalancedPool, Pool, Client, buildConnector, errors, Agent, request, stream, pipeline, connect, upgrade, setGlobalDispatcher, getGlobalDispatcher, setGlobalOrigin, getGlobalOrigin, MockClient, MockPool, MockAgent, mockErrors, ProxyAgent, RedirectHandler, DecoratorHandler } +export default Undici + +declare namespace Undici { + var Dispatcher: typeof import('./dispatcher').default + var Pool: typeof import('./pool').default; + var RedirectHandler: typeof import ('./handlers').RedirectHandler + var DecoratorHandler: typeof import ('./handlers').DecoratorHandler + var createRedirectInterceptor: typeof import ('./interceptors').createRedirectInterceptor + var BalancedPool: typeof import('./balanced-pool').default; + var Client: typeof import('./client').default; + var buildConnector: typeof import('./connector').default; + var errors: typeof import('./errors').default; + var Agent: typeof import('./agent').default; + var setGlobalDispatcher: typeof import('./global-dispatcher').setGlobalDispatcher; + var getGlobalDispatcher: typeof import('./global-dispatcher').getGlobalDispatcher; + var request: typeof import('./api').request; + var stream: typeof import('./api').stream; + var pipeline: typeof import('./api').pipeline; + var connect: typeof import('./api').connect; + var upgrade: typeof import('./api').upgrade; + var MockClient: typeof import('./mock-client').default; + var MockPool: typeof import('./mock-pool').default; + var MockAgent: typeof import('./mock-agent').default; + var mockErrors: typeof import('./mock-errors').default; + var fetch: typeof import('./fetch').fetch; + var caches: typeof import('./cache').caches; +} diff --git a/types/package.json b/types/package.json new file mode 100644 index 00000000000..f5690fc7e21 --- /dev/null +++ b/types/package.json @@ -0,0 +1,55 @@ +{ + "name": "undici-types", + "version": "5.24.0", + "description": "A stand-alone types package for Undici", + "homepage": "https://undici.nodejs.org", + "bugs": { + "url": "https://github.com/nodejs/undici/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/nodejs/undici.git" + }, + "license": "MIT", + "types": "index.d.ts", + "files": [ + "*.d.ts" + ], + "contributors": [ + { + "name": "Daniele Belardi", + "url": "https://github.com/dnlup", + "author": true + }, + { + "name": "Ethan Arrowood", + "url": "https://github.com/ethan-arrowood", + "author": true + }, + { + "name": "Matteo Collina", + "url": "https://github.com/mcollina", + "author": true + }, + { + "name": "Matthew Aitken", + "url": "https://github.com/KhafraDev", + "author": true + }, + { + "name": "Robert Nagy", + "url": "https://github.com/ronag", + "author": true + }, + { + "name": "Szymon Marczak", + "url": "https://github.com/szmarczak", + "author": true + }, + { + "name": "Tomas Della Vedova", + "url": "https://github.com/delvedor", + "author": true + } + ] +} From e8a667452cb00bf46a3bb62ee0d5af61723fb62a Mon Sep 17 00:00:00 2001 From: Ethan Arrowood Date: Wed, 20 Sep 2023 15:03:29 -0600 Subject: [PATCH 132/259] 5.25.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0262ffa08fe..eef22bf6a05 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "5.25.0", + "version": "5.25.1", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { From c86279c9bcf62fe28d124b124b91eb364d478a25 Mon Sep 17 00:00:00 2001 From: Ethan Arrowood Date: Wed, 20 Sep 2023 15:03:45 -0600 Subject: [PATCH 133/259] undici-types@5.25.1 --- types/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/types/package.json b/types/package.json index f5690fc7e21..16bf97c4ddf 100644 --- a/types/package.json +++ b/types/package.json @@ -1,6 +1,6 @@ { "name": "undici-types", - "version": "5.24.0", + "version": "5.25.1", "description": "A stand-alone types package for Undici", "homepage": "https://undici.nodejs.org", "bugs": { @@ -52,4 +52,4 @@ "author": true } ] -} +} \ No newline at end of file From a73260339fec7852b1c9a067b342bad0b87b20ee Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Thu, 21 Sep 2023 13:37:38 +0200 Subject: [PATCH 134/259] Add Khaf to releasers (#2276) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 051a9df2dea..3ba89890df6 100644 --- a/README.md +++ b/README.md @@ -436,6 +436,7 @@ and `undici.Agent`) which will enable the family autoselection algorithm when es * [__Ethan Arrowood__](https://github.com/ethan-arrowood), * [__Matteo Collina__](https://github.com/mcollina), * [__Robert Nagy__](https://github.com/ronag), +* [__Matthew Aitken__](https://github.com/KhafraDev), ## License From ba95ff6cf622a4ce8d6d9d42da22ebf9f15685a0 Mon Sep 17 00:00:00 2001 From: killa Date: Fri, 22 Sep 2023 15:43:00 +0800 Subject: [PATCH 135/259] fix: fix request with readable mode is object (#2279) In the stream Readable.from(buf) state, the length of 1 refers to the number of objects, not the length of buf. Therefore, when the stream mode is set to 'object,' should not use the length from the state. --- lib/core/util.js | 2 +- test/content-length.js | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/lib/core/util.js b/lib/core/util.js index 0e6197a4b29..259ba7b38a6 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -168,7 +168,7 @@ function bodyLength (body) { return 0 } else if (isStream(body)) { const state = body._readableState - return state && state.ended === true && Number.isFinite(state.length) + return state && state.objectMode === false && state.ended === true && Number.isFinite(state.length) ? state.length : null } else if (isBlobLike(body)) { diff --git a/test/content-length.js b/test/content-length.js index ecc1da34cc6..66c010a7c9e 100644 --- a/test/content-length.js +++ b/test/content-length.js @@ -300,3 +300,32 @@ test('response invalid content length with close', (t) => { }) }) }) + +test('request streaming with Readable.from(buf)', (t) => { + const server = createServer((req, res) => { + req.pipe(res) + }) + t.teardown(server.close.bind(server)) + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`) + t.teardown(client.destroy.bind(client)) + + client.request({ + path: '/', + method: 'PUT', + body: Readable.from(Buffer.from('hello')) + }, (err, data) => { + const chunks = [] + t.error(err) + data.body + .on('data', (chunk) => { + chunks.push(chunk) + }) + .on('end', () => { + t.equal(Buffer.concat(chunks).toString(), 'hello') + t.pass() + t.end() + }) + }) + }) +}) From b85ac789de76d95de6db7d442b31feed46c671ca Mon Sep 17 00:00:00 2001 From: Khafra Date: Fri, 22 Sep 2023 13:20:21 -0400 Subject: [PATCH 136/259] fix loading websockets when node is built w/ --without-ssl (#2282) --- lib/websocket/connection.js | 13 ++++++++++--- lib/websocket/frame.js | 11 +++++++++-- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/lib/websocket/connection.js b/lib/websocket/connection.js index 8c821899f65..e0fa69726b4 100644 --- a/lib/websocket/connection.js +++ b/lib/websocket/connection.js @@ -1,6 +1,5 @@ 'use strict' -const { randomBytes, createHash } = require('crypto') const diagnosticsChannel = require('diagnostics_channel') const { uid, states } = require('./constants') const { @@ -22,6 +21,14 @@ channels.open = diagnosticsChannel.channel('undici:websocket:open') channels.close = diagnosticsChannel.channel('undici:websocket:close') channels.socketError = diagnosticsChannel.channel('undici:websocket:socket_error') +/** @type {import('crypto')} */ +let crypto +try { + crypto = require('crypto') +} catch { + +} + /** * @see https://websockets.spec.whatwg.org/#concept-websocket-establish * @param {URL} url @@ -66,7 +73,7 @@ function establishWebSocketConnection (url, protocols, ws, onEstablish, options) // 5. Let keyValue be a nonce consisting of a randomly selected // 16-byte value that has been forgiving-base64-encoded and // isomorphic encoded. - const keyValue = randomBytes(16).toString('base64') + const keyValue = crypto.randomBytes(16).toString('base64') // 6. Append (`Sec-WebSocket-Key`, keyValue) to request’s // header list. @@ -148,7 +155,7 @@ function establishWebSocketConnection (url, protocols, ws, onEstablish, options) // trailing whitespace, the client MUST _Fail the WebSocket // Connection_. const secWSAccept = response.headersList.get('Sec-WebSocket-Accept') - const digest = createHash('sha1').update(keyValue + uid).digest('base64') + const digest = crypto.createHash('sha1').update(keyValue + uid).digest('base64') if (secWSAccept !== digest) { failWebsocketConnection(ws, 'Incorrect hash received in Sec-WebSocket-Accept header.') return diff --git a/lib/websocket/frame.js b/lib/websocket/frame.js index 61bfd3915ce..d867ad118b2 100644 --- a/lib/websocket/frame.js +++ b/lib/websocket/frame.js @@ -1,15 +1,22 @@ 'use strict' -const { randomBytes } = require('crypto') const { maxUnsigned16Bit } = require('./constants') +/** @type {import('crypto')} */ +let crypto +try { + crypto = require('crypto') +} catch { + +} + class WebsocketFrameSend { /** * @param {Buffer|undefined} data */ constructor (data) { this.frameData = data - this.maskKey = randomBytes(4) + this.maskKey = crypto.randomBytes(4) } createFrame (opcode) { From 4013c4b8932e73728809e4106d5c9d9d40648031 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Fri, 22 Sep 2023 19:37:05 +0200 Subject: [PATCH 137/259] Bumped v5.25.2 Signed-off-by: Matteo Collina --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index eef22bf6a05..3846b9dc398 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "5.25.1", + "version": "5.25.2", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { From 593621936d266385a18aa8bc69ad537313d28efc Mon Sep 17 00:00:00 2001 From: Yagiz Nizipli Date: Mon, 25 Sep 2023 17:37:29 -0400 Subject: [PATCH 138/259] perf: improve parse-url implementation (#2286) --- lib/core/util.js | 36 ++++++++++++++++++------------------ lib/fetch/global.js | 8 -------- test/client-errors.js | 4 ++-- 3 files changed, 20 insertions(+), 28 deletions(-) diff --git a/lib/core/util.js b/lib/core/util.js index 259ba7b38a6..769811f57f7 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -58,31 +58,31 @@ function parseURL (url) { throw new InvalidArgumentError('Invalid URL: The URL argument must be a non-null object.') } - if (url.port != null && url.port !== '' && !Number.isFinite(parseInt(url.port))) { - throw new InvalidArgumentError('Invalid URL: port must be a valid integer or a string representation of an integer.') + if (!/^https?:/.test(url.origin || url.protocol)) { + throw new InvalidArgumentError('Invalid URL protocol: the URL must start with `http:` or `https:`.') } - if (url.path != null && typeof url.path !== 'string') { - throw new InvalidArgumentError('Invalid URL path: the path must be a string or null/undefined.') - } + if (!(url instanceof URL)) { + if (url.port != null && url.port !== '' && !Number.isFinite(parseInt(url.port))) { + throw new InvalidArgumentError('Invalid URL: port must be a valid integer or a string representation of an integer.') + } - if (url.pathname != null && typeof url.pathname !== 'string') { - throw new InvalidArgumentError('Invalid URL pathname: the pathname must be a string or null/undefined.') - } + if (url.path != null && typeof url.path !== 'string') { + throw new InvalidArgumentError('Invalid URL path: the path must be a string or null/undefined.') + } - if (url.hostname != null && typeof url.hostname !== 'string') { - throw new InvalidArgumentError('Invalid URL hostname: the hostname must be a string or null/undefined.') - } + if (url.pathname != null && typeof url.pathname !== 'string') { + throw new InvalidArgumentError('Invalid URL pathname: the pathname must be a string or null/undefined.') + } - if (url.origin != null && typeof url.origin !== 'string') { - throw new InvalidArgumentError('Invalid URL origin: the origin must be a string or null/undefined.') - } + if (url.hostname != null && typeof url.hostname !== 'string') { + throw new InvalidArgumentError('Invalid URL hostname: the hostname must be a string or null/undefined.') + } - if (!/^https?:/.test(url.origin || url.protocol)) { - throw new InvalidArgumentError('Invalid URL protocol: the URL must start with `http:` or `https:`.') - } + if (url.origin != null && typeof url.origin !== 'string') { + throw new InvalidArgumentError('Invalid URL origin: the origin must be a string or null/undefined.') + } - if (!(url instanceof URL)) { const port = url.port != null ? url.port : (url.protocol === 'https:' ? 443 : 80) diff --git a/lib/fetch/global.js b/lib/fetch/global.js index 42282acdfe2..1df6f1227bc 100644 --- a/lib/fetch/global.js +++ b/lib/fetch/global.js @@ -9,14 +9,6 @@ function getGlobalOrigin () { } function setGlobalOrigin (newOrigin) { - if ( - newOrigin !== undefined && - typeof newOrigin !== 'string' && - !(newOrigin instanceof URL) - ) { - throw new Error('Invalid base url') - } - if (newOrigin === undefined) { Object.defineProperty(globalThis, globalOrigin, { value: undefined, diff --git a/test/client-errors.js b/test/client-errors.js index 936841fbb15..cec7f37d62a 100644 --- a/test/client-errors.js +++ b/test/client-errors.js @@ -256,7 +256,7 @@ errorAndChunkedEncodingPipelining(consts.ASYNC_ITERATOR) test('invalid options throws', (t) => { try { - new Client({ port: 'foobar' }) // eslint-disable-line + new Client({ port: 'foobar', protocol: 'https:' }) // eslint-disable-line t.fail() } catch (err) { t.type(err, errors.InvalidArgumentError) @@ -374,7 +374,7 @@ test('invalid options throws', (t) => { t.fail() } catch (err) { t.type(err, errors.InvalidArgumentError) - t.equal(err.message, 'Invalid URL hostname: the hostname must be a string or null/undefined.') + t.equal(err.message, 'Invalid URL protocol: the URL must start with `http:` or `https:`.') } try { From a78f8a878f8d1297f010ce5480950c91292b2377 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Thu, 28 Sep 2023 16:30:38 +0200 Subject: [PATCH 139/259] test: enable websockets inclusion in WPTReport (#2284) --- test/wpt/start-websockets.mjs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/wpt/start-websockets.mjs b/test/wpt/start-websockets.mjs index b9d8162aa33..73fd945c4d6 100644 --- a/test/wpt/start-websockets.mjs +++ b/test/wpt/start-websockets.mjs @@ -4,6 +4,8 @@ import { fileURLToPath } from 'url' import { fork } from 'child_process' import { on } from 'events' +const { WPT_REPORT } = process.env + if (process.env.CI) { // TODO(@KhafraDev): figure out *why* these tests are flaky in the CI. // process.exit(0) @@ -19,7 +21,10 @@ child.on('exit', (code) => process.exit(code)) for await (const [message] of on(child, 'message')) { if (message.server) { - const runner = new WPTRunner('websockets', message.server) + const runner = new WPTRunner('websockets', message.server, { + appendReport: !!WPT_REPORT, + reportPath: WPT_REPORT + }) runner.run() runner.once('completion', () => { From 8591eb7e89ac17a3dfdb3fbfd880a321665efbaa Mon Sep 17 00:00:00 2001 From: Dan Castillo Date: Sat, 30 Sep 2023 01:53:23 -0400 Subject: [PATCH 140/259] remove npm run test from pre-commit hook (#2296) --- .husky/pre-commit | 1 - 1 file changed, 1 deletion(-) diff --git a/.husky/pre-commit b/.husky/pre-commit index 2c1850f157c..20d0d06e583 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -2,4 +2,3 @@ . "$(dirname "$0")/_/husky.sh" npm run lint -npm run test \ No newline at end of file From 5addbb727ede7e35b5f05fbe73d2af00105bf070 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCrg=C3=BCn=20Day=C4=B1o=C4=9Flu?= Date: Sat, 30 Sep 2023 15:24:00 +0200 Subject: [PATCH 141/259] perf: use @fastify/busboy (#2211) * use @fastify/busboy * fix file handler * fix test * Revert "fix test" This reverts commit 0eada58902e281c1da47b6c448991296e659a715. * v2 * fix parseFormDataString --- lib/fetch/body.js | 10 ++++------ package.json | 2 +- test/node-fetch/utils/server.js | 4 ++-- test/utils/formdata.js | 10 +++++----- 4 files changed, 12 insertions(+), 14 deletions(-) diff --git a/lib/fetch/body.js b/lib/fetch/body.js index 105eb553157..1d9f17d7e33 100644 --- a/lib/fetch/body.js +++ b/lib/fetch/body.js @@ -1,6 +1,6 @@ 'use strict' -const Busboy = require('busboy') +const Busboy = require('@fastify/busboy') const util = require('../core/util') const { ReadableStreamFrom, @@ -385,10 +385,9 @@ function bodyMixinMethods (instance) { let busboy try { - busboy = Busboy({ + busboy = new Busboy({ headers, - preservePath: true, - defParamCharset: 'utf8' + preservePath: true }) } catch (err) { throw new DOMException(`${err}`, 'AbortError') @@ -397,8 +396,7 @@ function bodyMixinMethods (instance) { busboy.on('field', (name, value) => { responseFormData.append(name, value) }) - busboy.on('file', (name, value, info) => { - const { filename, encoding, mimeType } = info + busboy.on('file', (name, value, filename, encoding, mimeType) => { const chunks = [] if (encoding === 'base64' || encoding.toLowerCase() === 'base64') { diff --git a/package.json b/package.json index 3846b9dc398..817eb78eee3 100644 --- a/package.json +++ b/package.json @@ -161,6 +161,6 @@ ] }, "dependencies": { - "busboy": "^1.6.0" + "@fastify/busboy": "^2.0.0" } } diff --git a/test/node-fetch/utils/server.js b/test/node-fetch/utils/server.js index 46103055305..46dc9834fca 100644 --- a/test/node-fetch/utils/server.js +++ b/test/node-fetch/utils/server.js @@ -1,7 +1,7 @@ const http = require('http') const zlib = require('zlib') const { once } = require('events') -const newBusboy = require('busboy') +const Busboy = require('@fastify/busboy') module.exports = class TestServer { constructor () { @@ -435,7 +435,7 @@ module.exports = class TestServer { if (p === '/multipart') { res.statusCode = 200 res.setHeader('Content-Type', 'application/json') - const busboy = newBusboy({ headers: request.headers }) + const busboy = new Busboy({ headers: request.headers }) let body = '' busboy.on('file', async (fieldName, file, fileName) => { body += `${fieldName}=${fileName}` diff --git a/test/utils/formdata.js b/test/utils/formdata.js index 19dac3e84e5..edd88545d15 100644 --- a/test/utils/formdata.js +++ b/test/utils/formdata.js @@ -1,4 +1,4 @@ -const busboy = require('busboy') +const Busboy = require('@fastify/busboy') function parseFormDataString ( body, @@ -9,15 +9,15 @@ function parseFormDataString ( fields: [] } - const bb = busboy({ + const bb = new Busboy({ headers: { 'content-type': contentType } }) return new Promise((resolve, reject) => { - bb.on('file', (name, file, info) => { - cache.fileMap.set(name, { data: [], info }) + bb.on('file', (name, file, filename, encoding, mimeType) => { + cache.fileMap.set(name, { data: [], info: { filename, encoding, mimeType } }) file.on('data', (data) => { const old = cache.fileMap.get(name) @@ -37,7 +37,7 @@ function parseFormDataString ( }) bb.on('field', (key, value) => cache.fields.push({ key, value })) - bb.on('close', () => resolve(cache)) + bb.on('finish', () => resolve(cache)) bb.on('error', (e) => reject(e)) bb.end(body) From 5f190b13c4004a10925fa8a27e495fbfc17dc291 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Sun, 1 Oct 2023 16:51:22 +0200 Subject: [PATCH 142/259] Disable FinalizationRegistry if NODE_V8_COVERAGE is set (#2298) Signed-off-by: Matteo Collina --- lib/compat/dispatcher-weakref.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/compat/dispatcher-weakref.js b/lib/compat/dispatcher-weakref.js index dbca8580404..db97e0f6e1e 100644 --- a/lib/compat/dispatcher-weakref.js +++ b/lib/compat/dispatcher-weakref.js @@ -31,6 +31,14 @@ class CompatFinalizer { } module.exports = function () { + // FIXME: remove workaround when the Node bug is fixed + // https://github.com/nodejs/node/issues/49344#issuecomment-1741776308 + if (process.env.NODE_V8_COVERAGE) { + return { + WeakRef: CompatWeakRef, + FinalizationRegistry: CompatFinalizer + } + } return { WeakRef: global.WeakRef || CompatWeakRef, FinalizationRegistry: global.FinalizationRegistry || CompatFinalizer From 764915396f684168328544bb0778424c58e2d945 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Sun, 1 Oct 2023 16:52:28 +0200 Subject: [PATCH 143/259] Bumped v5.23.3 Signed-off-by: Matteo Collina --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 817eb78eee3..dc3374a5958 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "5.25.2", + "version": "5.25.3", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { From 96f1425ca7ab7b6f0578fe0ff9badbc16ff11ff6 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Sun, 1 Oct 2023 16:52:57 +0200 Subject: [PATCH 144/259] undici-types@5.25.3 --- types/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/package.json b/types/package.json index 16bf97c4ddf..6fcb70aa6bd 100644 --- a/types/package.json +++ b/types/package.json @@ -1,6 +1,6 @@ { "name": "undici-types", - "version": "5.25.1", + "version": "5.25.3", "description": "A stand-alone types package for Undici", "homepage": "https://undici.nodejs.org", "bugs": { From 678f026bd12d11c8ea4f06d92b23fe36904c305a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Oct 2023 22:20:28 +0000 Subject: [PATCH 145/259] build(deps): bump actions/checkout from 3.6.0 to 4.1.0 (#2299) Bumps [actions/checkout](https://github.com/actions/checkout) from 3.6.0 to 4.1.0. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/f43a0e5ff2bd294095638e18286ca9a3d1956744...8ade135a41bc03ea155e62e844d188df1ea18608) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/bench.yml | 4 ++-- .github/workflows/codeql.yml | 2 +- .github/workflows/dependency-review.yml | 2 +- .github/workflows/fuzz.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/scorecard.yml | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 295e44b2c55..200e72def08 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Code - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 with: persist-credentials: false ref: ${{ github.base_ref }} @@ -30,7 +30,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Code - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 with: persist-credentials: false - name: Setup Node diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 520750ff88a..fc1ff6aa985 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -46,7 +46,7 @@ jobs: egress-policy: audit - name: Checkout repository - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index eb24b0d0d6f..23c47bd539c 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -22,6 +22,6 @@ jobs: egress-policy: audit - name: 'Checkout Repository' - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 - name: 'Dependency Review' uses: actions/dependency-review-action@f6fff72a3217f580d5afd49a46826795305b63c7 # v3.0.8 diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index c2c918d3544..de608ceddfd 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 with: persist-credentials: false diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 62645af1bc0..7c3cf6f74c4 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -7,7 +7,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 with: persist-credentials: false - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index f641f9512b8..2b0055a6d7b 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -29,7 +29,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 with: persist-credentials: false From 5a750054ce7d473d336a7fa7ea4f42aee56afd0e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Oct 2023 22:29:48 +0000 Subject: [PATCH 146/259] build(deps): bump actions/dependency-review-action from 3.0.8 to 3.1.0 (#2301) Bumps [actions/dependency-review-action](https://github.com/actions/dependency-review-action) from 3.0.8 to 3.1.0. - [Release notes](https://github.com/actions/dependency-review-action/releases) - [Commits](https://github.com/actions/dependency-review-action/compare/f6fff72a3217f580d5afd49a46826795305b63c7...6c5ccdad469c9f8a2996bfecaec55a631a347034) --- updated-dependencies: - dependency-name: actions/dependency-review-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/dependency-review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 23c47bd539c..f29ceb3ad53 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -24,4 +24,4 @@ jobs: - name: 'Checkout Repository' uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 - name: 'Dependency Review' - uses: actions/dependency-review-action@f6fff72a3217f580d5afd49a46826795305b63c7 # v3.0.8 + uses: actions/dependency-review-action@6c5ccdad469c9f8a2996bfecaec55a631a347034 # v3.1.0 From 402446852bd65a7d2ba46973381fc8574f7165d1 Mon Sep 17 00:00:00 2001 From: Ethan Arrowood Date: Mon, 2 Oct 2023 08:37:59 -0600 Subject: [PATCH 147/259] Further automate publishing of undici-types using GitHub Actions workflow (#2290) * add workflow * improve automation * linting --- .github/workflows/publish-undici-types.yml | 21 +++++++ package.json | 1 - scripts/generate-undici-types-package-json.js | 23 ++++++++ scripts/update-undici-types-version.js | 18 ------ types/package.json | 55 ------------------- 5 files changed, 44 insertions(+), 74 deletions(-) create mode 100644 .github/workflows/publish-undici-types.yml create mode 100644 scripts/generate-undici-types-package-json.js delete mode 100644 scripts/update-undici-types-version.js delete mode 100644 types/package.json diff --git a/.github/workflows/publish-undici-types.yml b/.github/workflows/publish-undici-types.yml new file mode 100644 index 00000000000..b0afae73eb1 --- /dev/null +++ b/.github/workflows/publish-undici-types.yml @@ -0,0 +1,21 @@ +on: + push: + tags: + - 'v*' + workflow_dispatch: + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v3 + with: + node-version: '16.x' + registry-url: 'https://registry.npmjs.org' + - run: npm ci + - run: node scripts/generate-undici-types-package-json.js + - run: cd types + - run: npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/package.json b/package.json index dc3374a5958..b67d3ba0f9d 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,6 @@ "bench:run": "CONNECTIONS=1 node benchmarks/benchmark.js; CONNECTIONS=50 node benchmarks/benchmark.js", "serve:website": "docsify serve .", "prepare": "husky install", - "postpublish": "node scripts/update-undici-types-version.js && cd types && npm publish", "fuzz": "jsfuzz test/fuzzing/fuzz.js corpus" }, "devDependencies": { diff --git a/scripts/generate-undici-types-package-json.js b/scripts/generate-undici-types-package-json.js new file mode 100644 index 00000000000..250cfc08aa2 --- /dev/null +++ b/scripts/generate-undici-types-package-json.js @@ -0,0 +1,23 @@ +const fs = require('node:fs') +const path = require('node:path') + +const packageJSONPath = path.join(__dirname, '..', 'package.json') +const packageJSONRaw = fs.readFileSync(packageJSONPath, 'utf-8') +const packageJSON = JSON.parse(packageJSONRaw) + +const packageTypesJSON = { + name: 'undici-types', + version: packageJSON.version, + description: 'A stand-alone types package for Undici', + homepage: packageJSON.homepage, + bugs: packageJSON.bugs, + repository: packageJSON.repository, + license: packageJSON.license, + types: 'index.d.ts', + files: ['*.d.ts'], + contributors: packageJSON.contributors +} + +const packageTypesPath = path.join(__dirname, '..', 'types', 'package.json') + +fs.writeFileSync(packageTypesPath, JSON.stringify(packageTypesJSON, null, 2)) diff --git a/scripts/update-undici-types-version.js b/scripts/update-undici-types-version.js deleted file mode 100644 index 2308e97905a..00000000000 --- a/scripts/update-undici-types-version.js +++ /dev/null @@ -1,18 +0,0 @@ -const fs = require('node:fs') -const path = require('node:path') -const childProcess = require('node:child_process') - -const packageJSONPath = path.join(__dirname, '..', 'package.json') -const packageJSONRaw = fs.readFileSync(packageJSONPath, 'utf-8') -const packageJSON = JSON.parse(packageJSONRaw) -const version = packageJSON.version - -const packageTypesJSONPath = path.join(__dirname, '..', 'types', 'package.json') -const packageTypesJSONRaw = fs.readFileSync(packageTypesJSONPath, 'utf-8') -const packageTypesJSON = JSON.parse(packageTypesJSONRaw) -packageTypesJSON.version = version - -fs.writeFileSync(packageTypesJSONPath, JSON.stringify(packageTypesJSON, null, 2)) - -childProcess.execSync('git add types/package.json', { cwd: path.join(__dirname, '..') }) -childProcess.execSync(`git commit -n -m 'undici-types@${version}'`, { cwd: path.join(__dirname, '..') }) diff --git a/types/package.json b/types/package.json deleted file mode 100644 index 6fcb70aa6bd..00000000000 --- a/types/package.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "name": "undici-types", - "version": "5.25.3", - "description": "A stand-alone types package for Undici", - "homepage": "https://undici.nodejs.org", - "bugs": { - "url": "https://github.com/nodejs/undici/issues" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/nodejs/undici.git" - }, - "license": "MIT", - "types": "index.d.ts", - "files": [ - "*.d.ts" - ], - "contributors": [ - { - "name": "Daniele Belardi", - "url": "https://github.com/dnlup", - "author": true - }, - { - "name": "Ethan Arrowood", - "url": "https://github.com/ethan-arrowood", - "author": true - }, - { - "name": "Matteo Collina", - "url": "https://github.com/mcollina", - "author": true - }, - { - "name": "Matthew Aitken", - "url": "https://github.com/KhafraDev", - "author": true - }, - { - "name": "Robert Nagy", - "url": "https://github.com/ronag", - "author": true - }, - { - "name": "Szymon Marczak", - "url": "https://github.com/szmarczak", - "author": true - }, - { - "name": "Tomas Della Vedova", - "url": "https://github.com/delvedor", - "author": true - } - ] -} \ No newline at end of file From 7c2486d6f1560e7eb9cc127ea56dd4829998d8ea Mon Sep 17 00:00:00 2001 From: Ivan Tymoshenko Date: Tue, 3 Oct 2023 19:23:06 +0200 Subject: [PATCH 148/259] feat: disable FinalizationRegistry if NODE_V8_COVERAGE is set (#2304) --- lib/core/connect.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/core/connect.js b/lib/core/connect.js index bb71085a156..33091173fa8 100644 --- a/lib/core/connect.js +++ b/lib/core/connect.js @@ -13,7 +13,9 @@ let tls // include tls conditionally since it is not always available // re-use is enabled. let SessionCache -if (global.FinalizationRegistry) { +// FIXME: remove workaround when the Node bug is fixed +// https://github.com/nodejs/node/issues/49344#issuecomment-1741776308 +if (global.FinalizationRegistry && !process.env.NODE_V8_COVERAGE) { SessionCache = class WeakSessionCache { constructor (maxCachedSessions) { this._maxCachedSessions = maxCachedSessions From 5e654f351a9a813fed3e9feff4388b5c4fbda787 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Tue, 3 Oct 2023 19:23:45 +0200 Subject: [PATCH 149/259] Bumped v5.23.4 Signed-off-by: Matteo Collina --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b67d3ba0f9d..1d2032652ae 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "5.25.3", + "version": "5.25.4", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { From 0fde27d39d4e49b1be8e1fb7cc640350f98e6555 Mon Sep 17 00:00:00 2001 From: Ethan Arrowood Date: Wed, 4 Oct 2023 11:34:48 -0600 Subject: [PATCH 150/259] use npm install instead of npm ci (#2309) --- .github/workflows/publish-undici-types.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-undici-types.yml b/.github/workflows/publish-undici-types.yml index b0afae73eb1..27d2d24f76e 100644 --- a/.github/workflows/publish-undici-types.yml +++ b/.github/workflows/publish-undici-types.yml @@ -13,7 +13,7 @@ jobs: with: node-version: '16.x' registry-url: 'https://registry.npmjs.org' - - run: npm ci + - run: npm install - run: node scripts/generate-undici-types-package-json.js - run: cd types - run: npm publish From fcc1e39bc2771f9f8d9baa7bba47354b1b3b51d1 Mon Sep 17 00:00:00 2001 From: Ethan Arrowood Date: Thu, 5 Oct 2023 03:43:33 -0600 Subject: [PATCH 151/259] change default header to `node` (#2310) * change default header to `node` * switch on filename, add tests --- lib/fetch/index.js | 2 +- test/fetch/user-agent.js | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 test/fetch/user-agent.js diff --git a/lib/fetch/index.js b/lib/fetch/index.js index 50f1b9f3fcd..1e50b5b11b3 100644 --- a/lib/fetch/index.js +++ b/lib/fetch/index.js @@ -1344,7 +1344,7 @@ async function httpNetworkOrCacheFetch ( // user agents should append `User-Agent`/default `User-Agent` value to // httpRequest’s header list. if (!httpRequest.headersList.contains('user-agent')) { - httpRequest.headersList.append('user-agent', 'undici') + httpRequest.headersList.append('user-agent', __filename.endsWith('index.js') ? 'undici' : 'node') } // 15. If httpRequest’s cache mode is "default" and httpRequest’s header diff --git a/test/fetch/user-agent.js b/test/fetch/user-agent.js new file mode 100644 index 00000000000..48a69b4d7aa --- /dev/null +++ b/test/fetch/user-agent.js @@ -0,0 +1,25 @@ +'use strict' + +const { test } = require('tap') +const events = require('events') +const http = require('http') +const undici = require('../../') +const nodeBuild = require('../../undici-fetch.js') + +test('user-agent defaults correctly', async (t) => { + const server = http.createServer((req, res) => { + res.end(JSON.stringify({ userAgentHeader: req.headers['user-agent'] })) + }) + t.teardown(server.close.bind(server)) + + server.listen(0) + await events.once(server, 'listening') + const url = `http://localhost:${server.address().port}` + const [nodeBuildJSON, undiciJSON] = await Promise.all([ + nodeBuild.fetch(url).then((body) => body.json()), + undici.fetch(url).then((body) => body.json()) + ]) + + t.same(nodeBuildJSON.userAgentHeader, 'node') + t.same(undiciJSON.userAgentHeader, 'undici') +}) From 898040d251c8928f5f78d58133f42de5ffa60af2 Mon Sep 17 00:00:00 2001 From: Kyrylo <59825365+kyrylodolynskyi@users.noreply.github.com> Date: Thu, 5 Oct 2023 15:52:50 +0300 Subject: [PATCH 152/259] chore: change order of the pseudo-headers (#2308) --- lib/client.js | 11 +++++++--- package.json | 1 + test/http2.js | 56 ++++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 62 insertions(+), 6 deletions(-) diff --git a/lib/client.js b/lib/client.js index b5170d4f88d..cb0373bd70f 100644 --- a/lib/client.js +++ b/lib/client.js @@ -93,6 +93,7 @@ const { HTTP2_HEADER_AUTHORITY, HTTP2_HEADER_METHOD, HTTP2_HEADER_PATH, + HTTP2_HEADER_SCHEME, HTTP2_HEADER_CONTENT_LENGTH, HTTP2_HEADER_EXPECT, HTTP2_HEADER_STATUS @@ -1689,7 +1690,7 @@ function writeH2 (client, session, request) { const h2State = client[kHTTP2SessionState] headers[HTTP2_HEADER_AUTHORITY] = host || client[kHost] - headers[HTTP2_HEADER_PATH] = path + headers[HTTP2_HEADER_METHOD] = method if (method === 'CONNECT') { session.ref() @@ -1716,10 +1717,14 @@ function writeH2 (client, session, request) { }) return true - } else { - headers[HTTP2_HEADER_METHOD] = method } + // https://tools.ietf.org/html/rfc7540#section-8.3 + // :path and :scheme headers must be omited when sending CONNECT + + headers[HTTP2_HEADER_PATH] = path + headers[HTTP2_HEADER_SCHEME] = 'https' + // https://tools.ietf.org/html/rfc7231#section-4.3.1 // https://tools.ietf.org/html/rfc7231#section-4.3.2 // https://tools.ietf.org/html/rfc7231#section-4.3.5 diff --git a/package.json b/package.json index 1d2032652ae..2e87175652f 100644 --- a/package.json +++ b/package.json @@ -122,6 +122,7 @@ "pre-commit": "^1.2.2", "proxy": "^1.0.2", "proxyquire": "^2.1.3", + "semver": "^7.5.4", "sinon": "^15.0.0", "snazzy": "^9.0.0", "standard": "^17.0.0", diff --git a/test/http2.js b/test/http2.js index 538255399eb..46f2ff6755d 100644 --- a/test/http2.js +++ b/test/http2.js @@ -7,19 +7,23 @@ const { Blob } = require('node:buffer') const { Writable, pipeline, PassThrough, Readable } = require('node:stream') const { test, plan } = require('tap') +const { gte } = require('semver') const pem = require('https-pem') const { Client, Agent } = require('..') -const isGreaterThanv20 = Number(process.version.slice(1).split('.')[0]) >= 20 +const isGreaterThanv20 = gte(process.version.slice(1), '20.0.0') +// NOTE: node versions <16.14.1 have a bug which changes the order of pseudo-headers +// https://github.com/nodejs/node/pull/41735 +const hasPseudoHeadersOrderFix = gte(process.version.slice(1), '16.14.1') -plan(19) +plan(20) test('Should support H2 connection', async t => { const body = [] const server = createSecureServer(pem) - server.on('stream', (stream, headers) => { + server.on('stream', (stream, headers, _flags, rawHeaders) => { t.equal(headers['x-my-header'], 'foo') t.equal(headers[':method'], 'GET') stream.respond({ @@ -996,3 +1000,49 @@ test('Agent should support H2 connection', async t => { t.equal(response.headers['x-custom-h2'], 'hello') t.equal(Buffer.concat(body).toString('utf8'), 'hello h2!') }) + +test( + 'Should provide pseudo-headers in proper order', + { skip: !hasPseudoHeadersOrderFix }, + async t => { + const server = createSecureServer(pem) + server.on('stream', (stream, _headers, _flags, rawHeaders) => { + t.same(rawHeaders, [ + ':authority', + `localhost:${server.address().port}`, + ':method', + 'GET', + ':path', + '/', + ':scheme', + 'https' + ]) + + stream.respond({ + 'content-type': 'text/plain; charset=utf-8', + ':status': 200 + }) + stream.end() + }) + + server.listen(0) + await once(server, 'listening') + + const client = new Client(`https://localhost:${server.address().port}`, { + connect: { + rejectUnauthorized: false + }, + allowH2: true + }) + + t.teardown(server.close.bind(server)) + t.teardown(client.close.bind(client)) + + const response = await client.request({ + path: '/', + method: 'GET' + }) + + t.equal(response.statusCode, 200) + } +) From e6450127d1cf5f84f4192ce1dd516d492426cf95 Mon Sep 17 00:00:00 2001 From: Nicole <103509584+nicole0707@users.noreply.github.com> Date: Thu, 5 Oct 2023 23:57:17 +1100 Subject: [PATCH 153/259] fix: Agent.Options.factory should accept URL object or string as parameter (#2295) --- test/agent.js | 73 ++++++++++++++++++++++++++++++++++++++++++++++++ types/agent.d.ts | 2 +- 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/test/agent.js b/test/agent.js index ecdbdd2f91d..65afd8b72ac 100644 --- a/test/agent.js +++ b/test/agent.js @@ -10,6 +10,7 @@ const { request, stream, pipeline, + Pool, setGlobalDispatcher, getGlobalDispatcher } = require('../') @@ -267,6 +268,78 @@ test('multiple connections', t => { }) }) +test('agent factory supports URL parameter', (t) => { + t.plan(2) + + const noopHandler = { + onConnect () {}, + onHeaders () {}, + onData () {}, + onComplete () { + server.close() + }, + onError (err) { + throw err + } + } + + const dispatcher = new Agent({ + factory: (origin, opts) => { + t.ok(origin instanceof URL) + return new Pool(origin, opts) + } + }) + + const server = http.createServer((req, res) => { + res.setHeader('Content-Type', 'text/plain') + res.end('asd') + }) + + server.listen(0, () => { + t.doesNotThrow(() => dispatcher.dispatch({ + origin: new URL(`http://localhost:${server.address().port}`), + path: '/', + method: 'GET' + }, noopHandler)) + }) +}) + +test('agent factory supports string parameter', (t) => { + t.plan(2) + + const noopHandler = { + onConnect () {}, + onHeaders () {}, + onData () {}, + onComplete () { + server.close() + }, + onError (err) { + throw err + } + } + + const dispatcher = new Agent({ + factory: (origin, opts) => { + t.ok(typeof origin === 'string') + return new Pool(origin, opts) + } + }) + + const server = http.createServer((req, res) => { + res.setHeader('Content-Type', 'text/plain') + res.end('asd') + }) + + server.listen(0, () => { + t.doesNotThrow(() => dispatcher.dispatch({ + origin: `http://localhost:${server.address().port}`, + path: '/', + method: 'GET' + }, noopHandler)) + }) +}) + test('with globalAgent', t => { t.plan(6) const wanted = 'payload' diff --git a/types/agent.d.ts b/types/agent.d.ts index 08137358058..58081ce9372 100644 --- a/types/agent.d.ts +++ b/types/agent.d.ts @@ -17,7 +17,7 @@ declare class Agent extends Dispatcher{ declare namespace Agent { export interface Options extends Pool.Options { /** Default: `(origin, opts) => new Pool(origin, opts)`. */ - factory?(origin: URL, opts: Object): Dispatcher; + factory?(origin: string | URL, opts: Object): Dispatcher; /** Integer. Default: `0` */ maxRedirections?: number; From 2de330fe93382cc7127b43aa2c87212a1bad44ec Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Oct 2023 13:05:25 +0000 Subject: [PATCH 154/259] build(deps-dev): bump sinon from 15.2.0 to 16.1.0 (#2312) Bumps [sinon](https://github.com/sinonjs/sinon) from 15.2.0 to 16.1.0. - [Release notes](https://github.com/sinonjs/sinon/releases) - [Changelog](https://github.com/sinonjs/sinon/blob/main/docs/changelog.md) - [Commits](https://github.com/sinonjs/sinon/compare/v15.2.0...v16.1.0) --- updated-dependencies: - dependency-name: sinon dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2e87175652f..ec2c2c3d428 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "proxy": "^1.0.2", "proxyquire": "^2.1.3", "semver": "^7.5.4", - "sinon": "^15.0.0", + "sinon": "^16.1.0", "snazzy": "^9.0.0", "standard": "^17.0.0", "table": "^6.8.0", From af78c983b7dab87dbe7bf176bf23bbc9b5b28259 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Fri, 6 Oct 2023 16:19:25 +0200 Subject: [PATCH 155/259] test: handle npm ignore-scripts settings (#2313) Fixes #2287 --- package.json | 2 +- scripts/generate-pem.js | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 scripts/generate-pem.js diff --git a/package.json b/package.json index ec2c2c3d428..ef8095cec29 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "build:wasm": "node build/wasm.js --docker", "lint": "standard | snazzy", "lint:fix": "standard --fix | snazzy", - "test": "npm run test:tap && npm run test:node-fetch && npm run test:fetch && npm run test:cookies && npm run test:wpt && npm run test:websocket && npm run test:jest && npm run test:typescript", + "test": "node scripts/generate-pem && npm run test:tap && npm run test:node-fetch && npm run test:fetch && npm run test:cookies && npm run test:wpt && npm run test:websocket && npm run test:jest && npm run test:typescript", "test:cookies": "node scripts/verifyVersion 16 || tap test/cookie/*.js", "test:node-fetch": "node scripts/verifyVersion.js 16 || mocha --exit test/node-fetch", "test:fetch": "node scripts/verifyVersion.js 16 || (npm run build:node && tap --expose-gc test/fetch/*.js && tap test/webidl/*.js)", diff --git a/scripts/generate-pem.js b/scripts/generate-pem.js new file mode 100644 index 00000000000..0d7e628e209 --- /dev/null +++ b/scripts/generate-pem.js @@ -0,0 +1,3 @@ +/* istanbul ignore file */ + +require('https-pem/install') From 045d4dbce3f5768b33fe944a3ee233466c2530ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Fri, 6 Oct 2023 17:35:37 +0200 Subject: [PATCH 156/259] feat: respect `--max-http-header-size` Node.js flag (#2234) * feat: respect Node.js flag * add test * cleaner test * fix test * update docs * revert empty space * remove already default value * add * don't use `globalThis.fetch` to pass test in Node.js 16 * add test under `test/fetch` * fix lint * import from `undici-fetch` * use correct paths, use `request` for pure undici test * fix Node.js 14 test --- docs/api/Client.md | 2 +- lib/client.js | 3 ++- test/client-node-max-header-size.js | 23 +++++++++++++++++++++++ test/fetch/client-node-max-header-size.js | 23 +++++++++++++++++++++++ types/client.d.ts | 2 +- 5 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 test/client-node-max-header-size.js create mode 100644 test/fetch/client-node-max-header-size.js diff --git a/docs/api/Client.md b/docs/api/Client.md index c0987713a32..42668389a94 100644 --- a/docs/api/Client.md +++ b/docs/api/Client.md @@ -24,7 +24,7 @@ Returns: `Client` * **keepAliveMaxTimeout** `number | null` (optional) - Default: `600e3` - The maximum allowed `keepAliveTimeout`, in milliseconds, when overridden by *keep-alive* hints from the server. Defaults to 10 minutes. * **keepAliveTimeout** `number | null` (optional) - Default: `4e3` - The timeout, in milliseconds, 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. * **keepAliveTimeoutThreshold** `number | null` (optional) - Default: `1e3` - A number of milliseconds subtracted from server *keep-alive* hints when overriding `keepAliveTimeout` to account for timing inaccuracies caused by e.g. transport latency. Defaults to 1 second. -* **maxHeaderSize** `number | null` (optional) - Default: `16384` - The maximum length of request headers in bytes. Defaults to 16KiB. +* **maxHeaderSize** `number | null` (optional) - Default: `--max-http-header-size` or `16384` - The maximum length of request headers in bytes. Defaults to Node.js' --max-http-header-size or 16KiB. * **maxResponseSize** `number | null` (optional) - Default: `-1` - The maximum length of response body in bytes. Set to `-1` to disable. * **pipelining** `number | null` (optional) - Default: `1` - The amount of concurrent requests to be sent over the single TCP/TLS connection according to [RFC7230](https://tools.ietf.org/html/rfc7230#section-6.3.2). Carefully consider your workload and environment before enabling concurrent requests as pipelining may reduce performance if used incorrectly. Pipelining is sensitive to network stack settings as well as head of line blocking caused by e.g. long running requests. Set to `0` to disable keep-alive connections. * **connect** `ConnectOptions | Function | null` (optional) - Default: `null`. diff --git a/lib/client.js b/lib/client.js index cb0373bd70f..b898bfd2672 100644 --- a/lib/client.js +++ b/lib/client.js @@ -6,6 +6,7 @@ const assert = require('assert') const net = require('net') +const http = require('http') const { pipeline } = require('stream') const util = require('./core/util') const timers = require('./timers') @@ -270,7 +271,7 @@ class Client extends DispatcherBase { this[kConnector] = connect this[kSocket] = null this[kPipelining] = pipelining != null ? pipelining : 1 - this[kMaxHeadersSize] = maxHeaderSize || 16384 + this[kMaxHeadersSize] = maxHeaderSize || http.maxHeaderSize this[kKeepAliveDefaultTimeout] = keepAliveTimeout == null ? 4e3 : keepAliveTimeout this[kKeepAliveMaxTimeout] = keepAliveMaxTimeout == null ? 600e3 : keepAliveMaxTimeout this[kKeepAliveTimeoutThreshold] = keepAliveTimeoutThreshold == null ? 1e3 : keepAliveTimeoutThreshold diff --git a/test/client-node-max-header-size.js b/test/client-node-max-header-size.js new file mode 100644 index 00000000000..b5374901644 --- /dev/null +++ b/test/client-node-max-header-size.js @@ -0,0 +1,23 @@ +'use strict' + +const { execSync } = require('node:child_process') +const { test } = require('tap') + +const command = 'node -e "require(\'.\').request(\'https://httpbin.org/get\')"' + +test("respect Node.js' --max-http-header-size", async (t) => { + t.throws( + // TODO: Drop the `--unhandled-rejections=throw` once we drop Node.js 14 + () => execSync(`${command} --max-http-header-size=1 --unhandled-rejections=throw`), + /UND_ERR_HEADERS_OVERFLOW/, + 'max-http-header-size=1 should throw' + ) + + t.doesNotThrow( + () => execSync(command), + /UND_ERR_HEADERS_OVERFLOW/, + 'default max-http-header-size should not throw' + ) + + t.end() +}) diff --git a/test/fetch/client-node-max-header-size.js b/test/fetch/client-node-max-header-size.js new file mode 100644 index 00000000000..432a576b97e --- /dev/null +++ b/test/fetch/client-node-max-header-size.js @@ -0,0 +1,23 @@ +'use strict' + +const { execSync } = require('node:child_process') +const { test } = require('tap') + +const command = 'node -e "require(\'./undici-fetch.js\').fetch(\'https://httpbin.org/get\')"' + +test("respect Node.js' --max-http-header-size", async (t) => { + t.throws( + // TODO: Drop the `--unhandled-rejections=throw` once we drop Node.js 14 + () => execSync(`${command} --max-http-header-size=1 --unhandled-rejections=throw`), + /UND_ERR_HEADERS_OVERFLOW/, + 'max-http-header-size=1 should throw' + ) + + t.doesNotThrow( + () => execSync(command), + /UND_ERR_HEADERS_OVERFLOW/, + 'default max-http-header-size should not throw' + ) + + t.end() +}) diff --git a/types/client.d.ts b/types/client.d.ts index ac1779721f6..74948b15f38 100644 --- a/types/client.d.ts +++ b/types/client.d.ts @@ -23,7 +23,7 @@ export declare namespace Client { export interface Options { /** TODO */ interceptors?: OptionsInterceptors; - /** The maximum length of request headers in bytes. Default: `16384` (16KiB). */ + /** The maximum length of request headers in bytes. Default: Node.js' `--max-http-header-size` or `16384` (16KiB). */ maxHeaderSize?: number; /** The amount of time, in milliseconds, the parser will wait to receive the complete HTTP headers (Node 14 and above only). Default: `300e3` milliseconds (300s). */ headersTimeout?: number; From e5c9d703e63cd5ad691b8ce26e3f9a81c598f2e3 Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Fri, 6 Oct 2023 18:35:32 +0200 Subject: [PATCH 157/259] fix(#2311): End stream after body sent (#2314) --- lib/client.js | 6 +- test/fetch/http2.js | 162 ++++++++++++++++++++++++++++++-------------- 2 files changed, 115 insertions(+), 53 deletions(-) diff --git a/lib/client.js b/lib/client.js index b898bfd2672..be00382d0d0 100644 --- a/lib/client.js +++ b/lib/client.js @@ -2096,13 +2096,17 @@ async function writeIterable ({ h2stream, body, client, request, socket, content throw socket[kError] } - if (!h2stream.write(chunk)) { + const res = h2stream.write(chunk) + request.onBodySent(chunk) + if (!res) { await waitForDrain() } } } catch (err) { h2stream.destroy(err) } finally { + request.onRequestSent() + h2stream.end() h2stream .off('close', onDrain) .off('drain', onDrain) diff --git a/test/fetch/http2.js b/test/fetch/http2.js index e1d81d197d8..b426de3cbe5 100644 --- a/test/fetch/http2.js +++ b/test/fetch/http2.js @@ -13,24 +13,29 @@ const { Client, fetch } = require('../..') const nodeVersion = Number(process.version.split('v')[1].split('.')[0]) -plan(5) +plan(6) -test('[Fetch] Simple GET with h2', async t => { - const server = createSecureServer(pem) - const expectedRequestBody = 'hello h2!' +test('[Fetch] Issue#2311', async t => { + const expectedBody = 'hello from client!' - server.on('stream', async (stream, headers) => { - stream.respond({ + const server = createSecureServer(pem, async (req, res) => { + let body = '' + + req.setEncoding('utf8') + + res.writeHead(200, { 'content-type': 'text/plain; charset=utf-8', - 'x-custom-h2': headers['x-my-header'], - 'x-method': headers[':method'], - ':status': 200 + 'x-custom-h2': req.headers['x-my-header'] }) - stream.end(expectedRequestBody) + for await (const chunk of req) { + body += chunk + } + + res.end(body) }) - t.plan(3) + t.plan(1) server.listen() await once(server, 'listening') @@ -46,12 +51,13 @@ test('[Fetch] Simple GET with h2', async t => { `https://localhost:${server.address().port}/`, // Needs to be passed to disable the reject unauthorized { - method: 'GET', + method: 'POST', dispatcher: client, headers: { 'x-my-header': 'foo', 'content-type': 'text-plain' - } + }, + body: expectedBody } ) @@ -60,30 +66,25 @@ test('[Fetch] Simple GET with h2', async t => { t.teardown(server.close.bind(server)) t.teardown(client.close.bind(client)) - t.equal(responseBody, expectedRequestBody) - t.equal(response.headers.get('x-method'), 'GET') - t.equal(response.headers.get('x-custom-h2'), 'foo') + t.equal(responseBody, expectedBody) }) -test('[Fetch] Should handle h2 request with body (string or buffer)', async t => { +test('[Fetch] Simple GET with h2', async t => { const server = createSecureServer(pem) - const expectedBody = 'hello from client!' const expectedRequestBody = 'hello h2!' - const requestBody = [] server.on('stream', async (stream, headers) => { - stream.on('data', chunk => requestBody.push(chunk)) - stream.respond({ 'content-type': 'text/plain; charset=utf-8', 'x-custom-h2': headers['x-my-header'], + 'x-method': headers[':method'], ':status': 200 }) stream.end(expectedRequestBody) }) - t.plan(2) + t.plan(3) server.listen() await once(server, 'listening') @@ -99,13 +100,12 @@ test('[Fetch] Should handle h2 request with body (string or buffer)', async t => `https://localhost:${server.address().port}/`, // Needs to be passed to disable the reject unauthorized { - method: 'POST', + method: 'GET', dispatcher: client, headers: { 'x-my-header': 'foo', 'content-type': 'text-plain' - }, - body: expectedBody + } } ) @@ -114,21 +114,19 @@ test('[Fetch] Should handle h2 request with body (string or buffer)', async t => t.teardown(server.close.bind(server)) t.teardown(client.close.bind(client)) - t.equal(Buffer.concat(requestBody).toString('utf-8'), expectedBody) t.equal(responseBody, expectedRequestBody) + t.equal(response.headers.get('x-method'), 'GET') + t.equal(response.headers.get('x-custom-h2'), 'foo') }) -// Skipping for now, there is something odd in the way the body is handled -test('[Fetch] Should handle h2 request with body (stream)', { skip: nodeVersion === 16 }, async t => { +test('[Fetch] Should handle h2 request with body (string or buffer)', async t => { const server = createSecureServer(pem) - const expectedBody = readFileSync(__filename, 'utf-8') - const stream = createReadStream(__filename) - const requestChunks = [] + const expectedBody = 'hello from client!' + const expectedRequestBody = 'hello h2!' + const requestBody = [] server.on('stream', async (stream, headers) => { - t.equal(headers[':method'], 'PUT') - t.equal(headers[':path'], '/') - t.equal(headers[':scheme'], 'https') + stream.on('data', chunk => requestBody.push(chunk)) stream.respond({ 'content-type': 'text/plain; charset=utf-8', @@ -136,16 +134,12 @@ test('[Fetch] Should handle h2 request with body (stream)', { skip: nodeVersion ':status': 200 }) - stream.end('hello h2!') - - for await (const chunk of stream) { - requestChunks.push(chunk) - } + stream.end(expectedRequestBody) }) - t.plan(8) + t.plan(2) - server.listen(0) + server.listen() await once(server, 'listening') const client = new Client(`https://localhost:${server.address().port}`, { @@ -155,32 +149,96 @@ test('[Fetch] Should handle h2 request with body (stream)', { skip: nodeVersion allowH2: true }) - t.teardown(server.close.bind(server)) - t.teardown(client.close.bind(client)) - const response = await fetch( `https://localhost:${server.address().port}/`, // Needs to be passed to disable the reject unauthorized { - method: 'PUT', + method: 'POST', dispatcher: client, headers: { 'x-my-header': 'foo', 'content-type': 'text-plain' }, - body: Readable.toWeb(stream), - duplex: 'half' + body: expectedBody } ) const responseBody = await response.text() - t.equal(response.status, 200) - t.equal(response.headers.get('content-type'), 'text/plain; charset=utf-8') - t.equal(response.headers.get('x-custom-h2'), 'foo') - t.equal(responseBody, 'hello h2!') - t.equal(Buffer.concat(requestChunks).toString('utf-8'), expectedBody) + t.teardown(server.close.bind(server)) + t.teardown(client.close.bind(client)) + + t.equal(Buffer.concat(requestBody).toString('utf-8'), expectedBody) + t.equal(responseBody, expectedRequestBody) }) + +// Skipping for now, there is something odd in the way the body is handled +test( + '[Fetch] Should handle h2 request with body (stream)', + { skip: nodeVersion === 16 }, + async t => { + const server = createSecureServer(pem) + const expectedBody = readFileSync(__filename, 'utf-8') + const stream = createReadStream(__filename) + const requestChunks = [] + + server.on('stream', async (stream, headers) => { + t.equal(headers[':method'], 'PUT') + t.equal(headers[':path'], '/') + t.equal(headers[':scheme'], 'https') + + stream.respond({ + 'content-type': 'text/plain; charset=utf-8', + 'x-custom-h2': headers['x-my-header'], + ':status': 200 + }) + + stream.end('hello h2!') + + for await (const chunk of stream) { + requestChunks.push(chunk) + } + }) + + t.plan(8) + + server.listen(0) + await once(server, 'listening') + + const client = new Client(`https://localhost:${server.address().port}`, { + connect: { + rejectUnauthorized: false + }, + allowH2: true + }) + + t.teardown(server.close.bind(server)) + t.teardown(client.close.bind(client)) + + const response = await fetch( + `https://localhost:${server.address().port}/`, + // Needs to be passed to disable the reject unauthorized + { + method: 'PUT', + dispatcher: client, + headers: { + 'x-my-header': 'foo', + 'content-type': 'text-plain' + }, + body: Readable.toWeb(stream), + duplex: 'half' + } + ) + + const responseBody = await response.text() + + t.equal(response.status, 200) + t.equal(response.headers.get('content-type'), 'text/plain; charset=utf-8') + t.equal(response.headers.get('x-custom-h2'), 'foo') + t.equal(responseBody, 'hello h2!') + t.equal(Buffer.concat(requestChunks).toString('utf-8'), expectedBody) + } +) test('Should handle h2 request with body (Blob)', { skip: !Blob }, async t => { const server = createSecureServer(pem) const expectedBody = 'asd' From 470ee38145c5e6b367874b8b67f45143b67557c0 Mon Sep 17 00:00:00 2001 From: Khafra Date: Mon, 9 Oct 2023 18:29:20 -0400 Subject: [PATCH 158/259] disallow setting host header in fetch (#2322) --- lib/fetch/index.js | 2 ++ test/fetch/issue-2318.js | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 test/fetch/issue-2318.js diff --git a/lib/fetch/index.js b/lib/fetch/index.js index 1e50b5b11b3..8c98a646a0e 100644 --- a/lib/fetch/index.js +++ b/lib/fetch/index.js @@ -1406,6 +1406,8 @@ async function httpNetworkOrCacheFetch ( } } + httpRequest.headersList.delete('host') + // 20. If includeCredentials is true, then: if (includeCredentials) { // 1. If the user agent is not configured to block cookies for httpRequest diff --git a/test/fetch/issue-2318.js b/test/fetch/issue-2318.js new file mode 100644 index 00000000000..e4f610dc92f --- /dev/null +++ b/test/fetch/issue-2318.js @@ -0,0 +1,25 @@ +'use strict' + +const { test } = require('tap') +const { once } = require('events') +const { createServer } = require('http') +const { fetch } = require('../..') + +test('Undici overrides user-provided `Host` header', async (t) => { + t.plan(1) + + const server = createServer((req, res) => { + t.equal(req.headers.host, `localhost:${server.address().port}`) + + res.end() + }).listen(0) + + t.teardown(server.close.bind(server)) + await once(server, 'listening') + + await fetch(`http://localhost:${server.address().port}`, { + headers: { + host: 'www.idk.org' + } + }) +}) From 882ff6dae52b85fd03ddcc1c047067d594d8eb1d Mon Sep 17 00:00:00 2001 From: StepSecurity Bot Date: Tue, 10 Oct 2023 00:08:25 -0700 Subject: [PATCH 159/259] [StepSecurity] ci: Harden GitHub Actions (#2325) --- .github/workflows/publish-undici-types.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish-undici-types.yml b/.github/workflows/publish-undici-types.yml index 27d2d24f76e..95209964336 100644 --- a/.github/workflows/publish-undici-types.yml +++ b/.github/workflows/publish-undici-types.yml @@ -4,12 +4,15 @@ on: - 'v*' workflow_dispatch: +permissions: + contents: read + jobs: publish: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v3 + - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 with: node-version: '16.x' registry-url: 'https://registry.npmjs.org' From 3a9f48171d7486a558ac6f62a7c521d46fdd208b Mon Sep 17 00:00:00 2001 From: Khafra Date: Wed, 11 Oct 2023 02:30:04 -0400 Subject: [PATCH 160/259] fix fetch with coverage enabled (#2330) --- lib/compat/dispatcher-weakref.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/compat/dispatcher-weakref.js b/lib/compat/dispatcher-weakref.js index db97e0f6e1e..8cb99e21501 100644 --- a/lib/compat/dispatcher-weakref.js +++ b/lib/compat/dispatcher-weakref.js @@ -22,11 +22,13 @@ class CompatFinalizer { } register (dispatcher, key) { - dispatcher.on('disconnect', () => { - if (dispatcher[kConnected] === 0 && dispatcher[kSize] === 0) { - this.finalizer(key) - } - }) + if (dispatcher.on) { + dispatcher.on('disconnect', () => { + if (dispatcher[kConnected] === 0 && dispatcher[kSize] === 0) { + this.finalizer(key) + } + }) + } } } From 06380f6a10d25df963c06023c3190e3f9160ed8a Mon Sep 17 00:00:00 2001 From: binsee <5285894+binsee@users.noreply.github.com> Date: Wed, 11 Oct 2023 03:01:54 -0500 Subject: [PATCH 161/259] Fix stuck when using http2 POST Buffer (#2336) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 🐛 call stream.end when POST Buffer * test: ✅ add test POST Buffer for http2 * test: ✅ remove debug log --- lib/client.js | 1 + test/http2.js | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/lib/client.js b/lib/client.js index be00382d0d0..065fb563380 100644 --- a/lib/client.js +++ b/lib/client.js @@ -1862,6 +1862,7 @@ function writeH2 (client, session, request) { stream.cork() stream.write(body) stream.uncork() + stream.end() request.onBodySent(body) request.onRequestSent() } else if (util.isBlobLike(body)) { diff --git a/test/http2.js b/test/http2.js index 46f2ff6755d..7be79b021d1 100644 --- a/test/http2.js +++ b/test/http2.js @@ -17,7 +17,7 @@ const isGreaterThanv20 = gte(process.version.slice(1), '20.0.0') // https://github.com/nodejs/node/pull/41735 const hasPseudoHeadersOrderFix = gte(process.version.slice(1), '16.14.1') -plan(20) +plan(21) test('Should support H2 connection', async t => { const body = [] @@ -114,6 +114,56 @@ test('Should support H2 connection (headers as array)', async t => { t.equal(Buffer.concat(body).toString('utf8'), 'hello h2!') }) +test('Should support H2 connection(POST Buffer)', async t => { + const server = createSecureServer({ ...pem, allowHTTP1: false }) + + server.on('stream', async (stream, headers, _flags, rawHeaders) => { + t.equal(headers[':method'], 'POST') + const reqData = [] + stream.on('data', chunk => reqData.push(chunk.toString())) + await once(stream, 'end') + t.equal(reqData.join(''), 'hello!') + stream.respond({ + 'content-type': 'text/plain; charset=utf-8', + 'x-custom-h2': 'hello', + ':status': 200 + }) + stream.end('hello h2!') + }) + + server.listen(0) + await once(server, 'listening') + + const client = new Client(`https://localhost:${server.address().port}`, { + connect: { + rejectUnauthorized: false + }, + allowH2: true + }) + + t.plan(6) + t.teardown(server.close.bind(server)) + t.teardown(client.close.bind(client)) + + const sendBody = 'hello!' + const body = [] + const response = await client.request({ + path: '/', + method: 'POST', + body: sendBody + }) + + response.body.on('data', chunk => { + body.push(chunk) + }) + + await once(response.body, 'end') + t.equal(response.statusCode, 200) + t.equal(response.headers['content-type'], 'text/plain; charset=utf-8') + t.equal(response.headers['x-custom-h2'], 'hello') + t.equal(Buffer.concat(body).toString('utf8'), 'hello h2!') +}) + test('Should support H2 GOAWAY (server-side)', async t => { const body = [] const server = createSecureServer(pem) From b9d83681443405bcc4e77a4c196e559944f1dfa1 Mon Sep 17 00:00:00 2001 From: binsee <5285894+binsee@users.noreply.github.com> Date: Wed, 11 Oct 2023 03:02:33 -0500 Subject: [PATCH 162/259] =?UTF-8?q?fix:=20=F0=9F=8F=B7=EF=B8=8F=20add=20al?= =?UTF-8?q?lowH2=20to=20BuildOptions=20(#2334)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 🏷️ add allowH2 to BuildOptions * test: ✅ update test --- test/types/connector.test-d.ts | 2 +- types/connector.d.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/test/types/connector.test-d.ts b/test/types/connector.test-d.ts index bbf07e1d396..923656974d1 100644 --- a/test/types/connector.test-d.ts +++ b/test/types/connector.test-d.ts @@ -4,7 +4,7 @@ import {ConnectionOptions, TLSSocket} from 'tls' import {Socket} from 'net' import {IpcNetConnectOpts, NetConnectOpts, TcpNetConnectOpts} from "net"; -const connector = buildConnector({ rejectUnauthorized: false }) +const connector = buildConnector({ rejectUnauthorized: false, allowH2: false }) expectAssignable(new Client('', { connect (opts: buildConnector.Options, cb: buildConnector.Callback) { connector(opts, (...args) => { diff --git a/types/connector.d.ts b/types/connector.d.ts index 847284a1f2b..bd924339eb3 100644 --- a/types/connector.d.ts +++ b/types/connector.d.ts @@ -6,6 +6,7 @@ declare function buildConnector (options?: buildConnector.BuildOptions): buildCo declare namespace buildConnector { export type BuildOptions = (ConnectionOptions | TcpNetConnectOpts | IpcNetConnectOpts) & { + allowH2?: boolean; maxCachedSessions?: number | null; socketPath?: string | null; timeout?: number | null; From df9795883fb75eb97d27f86ce97a491bf023717c Mon Sep 17 00:00:00 2001 From: binsee <5285894+binsee@users.noreply.github.com> Date: Wed, 11 Oct 2023 04:05:37 -0500 Subject: [PATCH 163/259] =?UTF-8?q?fix:=20=F0=9F=90=9B=20fix=20process=20h?= =?UTF-8?q?ttp2=20header=20(#2332)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 🐛 fix process header * test: ✅ add test multiple requests to http2 * test: 💡 remove comment * test: ✅ update test plan --- lib/core/request.js | 3 ++- test/http2.js | 60 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/lib/core/request.js b/lib/core/request.js index e3b0c7b9dbf..50be01c0dc8 100644 --- a/lib/core/request.js +++ b/lib/core/request.js @@ -381,7 +381,8 @@ function processHeader (request, key, val, skipAppend = false) { key.toLowerCase() === 'content-type' ) { request.contentType = val - request.headers += processHeaderValue(key, val) + if (skipAppend) request.headers[key] = processHeaderValue(key, val, skipAppend) + else request.headers += processHeaderValue(key, val) } else if ( key.length === 17 && key.toLowerCase() === 'transfer-encoding' diff --git a/test/http2.js b/test/http2.js index 7be79b021d1..a3d24f23670 100644 --- a/test/http2.js +++ b/test/http2.js @@ -17,7 +17,7 @@ const isGreaterThanv20 = gte(process.version.slice(1), '20.0.0') // https://github.com/nodejs/node/pull/41735 const hasPseudoHeadersOrderFix = gte(process.version.slice(1), '16.14.1') -plan(21) +plan(22) test('Should support H2 connection', async t => { const body = [] @@ -67,6 +67,64 @@ test('Should support H2 connection', async t => { t.equal(Buffer.concat(body).toString('utf8'), 'hello h2!') }) +test('Should support H2 connection(multiple requests)', async t => { + const server = createSecureServer(pem) + + server.on('stream', async (stream, headers, _flags, rawHeaders) => { + t.equal(headers['x-my-header'], 'foo') + t.equal(headers[':method'], 'POST') + const reqData = [] + stream.on('data', chunk => reqData.push(chunk.toString())) + await once(stream, 'end') + const reqBody = reqData.join('') + t.equal(reqBody.length > 0, true) + stream.respond({ + 'content-type': 'text/plain; charset=utf-8', + 'x-custom-h2': 'hello', + ':status': 200 + }) + stream.end(`hello h2! ${reqBody}`) + }) + + server.listen(0) + await once(server, 'listening') + + const client = new Client(`https://localhost:${server.address().port}`, { + connect: { + rejectUnauthorized: false + }, + allowH2: true + }) + + t.plan(21) + t.teardown(server.close.bind(server)) + t.teardown(client.close.bind(client)) + + for (let i = 0; i < 3; i++) { + const sendBody = `seq ${i}` + const body = [] + const response = await client.request({ + path: '/', + method: 'POST', + headers: { + 'content-type': 'text/plain; charset=utf-8', + 'x-my-header': 'foo' + }, + body: Readable.from(sendBody) + }) + + response.body.on('data', chunk => { + body.push(chunk) + }) + + await once(response.body, 'end') + t.equal(response.statusCode, 200) + t.equal(response.headers['content-type'], 'text/plain; charset=utf-8') + t.equal(response.headers['x-custom-h2'], 'hello') + t.equal(Buffer.concat(body).toString('utf8'), `hello h2! ${sendBody}`) + } +}) + test('Should support H2 connection (headers as array)', async t => { const body = [] const server = createSecureServer(pem) From 4006aaf43ac8b30e16d6d3b89fa2e0df4b7eef33 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Wed, 11 Oct 2023 12:02:16 +0100 Subject: [PATCH 164/259] Bumped v5.26.0 Signed-off-by: Matteo Collina --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ef8095cec29..a780bc10376 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "5.25.4", + "version": "5.26.0", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { From 69ea7b94434e2a3746e6ad1477d122a8d4075c76 Mon Sep 17 00:00:00 2001 From: Ethan Arrowood Date: Wed, 11 Oct 2023 10:25:22 -0600 Subject: [PATCH 165/259] hopefully this fixes it for good (#2338) --- .github/workflows/publish-undici-types.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish-undici-types.yml b/.github/workflows/publish-undici-types.yml index 95209964336..3579907a992 100644 --- a/.github/workflows/publish-undici-types.yml +++ b/.github/workflows/publish-undici-types.yml @@ -1,3 +1,5 @@ +name: Publish undici-types + on: push: tags: @@ -18,7 +20,7 @@ jobs: registry-url: 'https://registry.npmjs.org' - run: npm install - run: node scripts/generate-undici-types-package-json.js - - run: cd types - run: npm publish + working-directory: './types' env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} From 7bcb80c0a22509ceba1b786847faba5aded1bea0 Mon Sep 17 00:00:00 2001 From: Khafra Date: Wed, 11 Oct 2023 14:24:46 -0400 Subject: [PATCH 166/259] Fix node detection omfg (#2341) * fix node detection * remove comment * change name * fix regex for windows paths --- lib/fetch/index.js | 2 +- package.json | 3 ++- scripts/esbuild-build.mjs | 24 ++++++++++++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 scripts/esbuild-build.mjs diff --git a/lib/fetch/index.js b/lib/fetch/index.js index 8c98a646a0e..c89c9b7ffcb 100644 --- a/lib/fetch/index.js +++ b/lib/fetch/index.js @@ -1344,7 +1344,7 @@ async function httpNetworkOrCacheFetch ( // user agents should append `User-Agent`/default `User-Agent` value to // httpRequest’s header list. if (!httpRequest.headersList.contains('user-agent')) { - httpRequest.headersList.append('user-agent', __filename.endsWith('index.js') ? 'undici' : 'node') + httpRequest.headersList.append('user-agent', typeof esbuildDetection === 'undefined' ? 'undici' : 'node') } // 15. If httpRequest’s cache mode is "default" and httpRequest’s header diff --git a/package.json b/package.json index a780bc10376..3436db5fe86 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "docs" ], "scripts": { - "build:node": "npx esbuild@0.14.38 index-fetch.js --bundle --platform=node --outfile=undici-fetch.js", + "build:node": "node scripts/esbuild-build.mjs", "prebuild:wasm": "node build/wasm.js --prebuild", "build:wasm": "node build/wasm.js --docker", "lint": "standard | snazzy", @@ -109,6 +109,7 @@ "delay": "^5.0.0", "dns-packet": "^5.4.0", "docsify-cli": "^4.4.3", + "esbuild": "^0.19.4", "form-data": "^4.0.0", "formdata-node": "^4.3.1", "https-pem": "^3.0.0", diff --git a/scripts/esbuild-build.mjs b/scripts/esbuild-build.mjs new file mode 100644 index 00000000000..ca5886c1a2b --- /dev/null +++ b/scripts/esbuild-build.mjs @@ -0,0 +1,24 @@ +import * as esbuild from 'esbuild' +import fs from 'node:fs' + +const bundle = { + name: 'bundle', + setup (build) { + build.onLoad({ filter: /lib(\/|\\)fetch(\/|\\)index.js/ }, async (args) => { + const text = await fs.promises.readFile(args.path, 'utf8') + + return { + contents: `var esbuildDetection = 1;${text}`, + loader: 'js' + } + }) + } +} + +await esbuild.build({ + entryPoints: ['index-fetch.js'], + bundle: true, + outfile: 'undici-fetch.js', + plugins: [bundle], + platform: 'node' +}) From c8c80b1115d668664d8cf3acec7535b0258c3079 Mon Sep 17 00:00:00 2001 From: Ethan Arrowood Date: Wed, 11 Oct 2023 12:25:50 -0600 Subject: [PATCH 167/259] 5.26.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3436db5fe86..b68fcfc58e3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "5.26.0", + "version": "5.26.1", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { From e041de359221ebeae04c469e8aff4145764e6d76 Mon Sep 17 00:00:00 2001 From: Khafra Date: Wed, 11 Oct 2023 14:56:38 -0400 Subject: [PATCH 168/259] Merge pull request from GHSA-wqq4-5wpv-mx2g * fix: delete 'cookie' and 'host' headers on cross-origin redirect * apply suggestion --- lib/fetch/index.js | 4 ++ test/fetch/redirect-cross-origin-header.js | 48 ++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 test/fetch/redirect-cross-origin-header.js diff --git a/lib/fetch/index.js b/lib/fetch/index.js index c89c9b7ffcb..5323c30abc8 100644 --- a/lib/fetch/index.js +++ b/lib/fetch/index.js @@ -1200,6 +1200,10 @@ async function httpRedirectFetch (fetchParams, response) { if (!sameOrigin(requestCurrentURL(request), locationURL)) { // https://fetch.spec.whatwg.org/#cors-non-wildcard-request-header-name request.headersList.delete('authorization') + + // "Cookie" and "Host" are forbidden request-headers, which undici doesn't implement. + request.headersList.delete('cookie') + request.headersList.delete('host') } // 14. If request’s body is non-null, then set request’s body to the first return diff --git a/test/fetch/redirect-cross-origin-header.js b/test/fetch/redirect-cross-origin-header.js new file mode 100644 index 00000000000..fca48c44ea0 --- /dev/null +++ b/test/fetch/redirect-cross-origin-header.js @@ -0,0 +1,48 @@ +'use strict' + +const { test } = require('tap') +const { createServer } = require('http') +const { once } = require('events') +const { fetch } = require('../..') + +test('Cross-origin redirects clear forbidden headers', async (t) => { + t.plan(5) + + const server1 = createServer((req, res) => { + t.equal(req.headers.cookie, undefined) + t.equal(req.headers.authorization, undefined) + + res.end('redirected') + }).listen(0) + + const server2 = createServer((req, res) => { + t.equal(req.headers.authorization, 'test') + t.equal(req.headers.cookie, 'ddd=dddd') + + res.writeHead(302, { + ...req.headers, + Location: `http://localhost:${server1.address().port}` + }) + res.end() + }).listen(0) + + t.teardown(() => { + server1.close() + server2.close() + }) + + await Promise.all([ + once(server1, 'listening'), + once(server2, 'listening') + ]) + + const res = await fetch(`http://localhost:${server2.address().port}`, { + headers: { + Authorization: 'test', + Cookie: 'ddd=dddd' + } + }) + + const text = await res.text() + t.equal(text, 'redirected') +}) From 12a62187d45f332cf39dd405f7c52b759cf40cdd Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Wed, 11 Oct 2023 19:57:39 +0100 Subject: [PATCH 169/259] Bumped v5.26.2 Signed-off-by: Matteo Collina --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b68fcfc58e3..2a3cee9ec22 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "5.26.1", + "version": "5.26.2", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { From 5351f1fdb1f49f86e356056d7beb291af0144390 Mon Sep 17 00:00:00 2001 From: Ethan Arrowood Date: Wed, 11 Oct 2023 13:11:55 -0600 Subject: [PATCH 170/259] include esbuild script in files --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 2a3cee9ec22..5f659ee3ff6 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,8 @@ "index-fetch.js", "lib", "types", - "docs" + "docs", + "scripts/esbuild-build.mjs" ], "scripts": { "build:node": "node scripts/esbuild-build.mjs", From 227b9bedf233f741b86dda4ae9d1c7ad69f5d75c Mon Sep 17 00:00:00 2001 From: Ethan Arrowood Date: Wed, 11 Oct 2023 13:12:14 -0600 Subject: [PATCH 171/259] 5.26.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5f659ee3ff6..67046ad68a3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "5.26.2", + "version": "5.26.3", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { From 81e0a9390629dbb8980e4e23640739f4f9b3fb4a Mon Sep 17 00:00:00 2001 From: Ethan Arrowood Date: Wed, 11 Oct 2023 13:15:23 -0600 Subject: [PATCH 172/259] Revert "include esbuild script in files" This reverts commit 5351f1fdb1f49f86e356056d7beb291af0144390. --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index 67046ad68a3..f8a916ad5b6 100644 --- a/package.json +++ b/package.json @@ -67,8 +67,7 @@ "index-fetch.js", "lib", "types", - "docs", - "scripts/esbuild-build.mjs" + "docs" ], "scripts": { "build:node": "node scripts/esbuild-build.mjs", From 655d3d71473a88b3dedb51ba8dc163ca9e7c01e3 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Wed, 11 Oct 2023 20:40:12 +0100 Subject: [PATCH 173/259] Revert "Revert "include esbuild script in files"" This reverts commit 81e0a9390629dbb8980e4e23640739f4f9b3fb4a. --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index f8a916ad5b6..67046ad68a3 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,8 @@ "index-fetch.js", "lib", "types", - "docs" + "docs", + "scripts/esbuild-build.mjs" ], "scripts": { "build:node": "node scripts/esbuild-build.mjs", From 026c1d5ffc38017425fd4e477bf1241f7841afc7 Mon Sep 17 00:00:00 2001 From: Khafra Date: Wed, 11 Oct 2023 17:38:13 -0400 Subject: [PATCH 174/259] use esbuild define/hooks (#2342) * use esbuild define/hooks * remove build script --- package.json | 6 ++---- scripts/esbuild-build.mjs | 24 ------------------------ 2 files changed, 2 insertions(+), 28 deletions(-) delete mode 100644 scripts/esbuild-build.mjs diff --git a/package.json b/package.json index 67046ad68a3..6c4b6f4abfd 100644 --- a/package.json +++ b/package.json @@ -67,11 +67,10 @@ "index-fetch.js", "lib", "types", - "docs", - "scripts/esbuild-build.mjs" + "docs" ], "scripts": { - "build:node": "node scripts/esbuild-build.mjs", + "build:node": "npx esbuild@0.19.4 index-fetch.js --bundle --platform=node --outfile=undici-fetch.js --define:esbuildDetection=1", "prebuild:wasm": "node build/wasm.js --prebuild", "build:wasm": "node build/wasm.js --docker", "lint": "standard | snazzy", @@ -110,7 +109,6 @@ "delay": "^5.0.0", "dns-packet": "^5.4.0", "docsify-cli": "^4.4.3", - "esbuild": "^0.19.4", "form-data": "^4.0.0", "formdata-node": "^4.3.1", "https-pem": "^3.0.0", diff --git a/scripts/esbuild-build.mjs b/scripts/esbuild-build.mjs deleted file mode 100644 index ca5886c1a2b..00000000000 --- a/scripts/esbuild-build.mjs +++ /dev/null @@ -1,24 +0,0 @@ -import * as esbuild from 'esbuild' -import fs from 'node:fs' - -const bundle = { - name: 'bundle', - setup (build) { - build.onLoad({ filter: /lib(\/|\\)fetch(\/|\\)index.js/ }, async (args) => { - const text = await fs.promises.readFile(args.path, 'utf8') - - return { - contents: `var esbuildDetection = 1;${text}`, - loader: 'js' - } - }) - } -} - -await esbuild.build({ - entryPoints: ['index-fetch.js'], - bundle: true, - outfile: 'undici-fetch.js', - plugins: [bundle], - platform: 'node' -}) From c740cbb90950c1234ae9eb511d29db1f1ee01ddd Mon Sep 17 00:00:00 2001 From: Khafra Date: Thu, 12 Oct 2023 20:55:19 -0400 Subject: [PATCH 175/259] fix request's arrayBuffer returning uint8 instead of arraybuffer (#2344) --- lib/api/readable.js | 2 +- test/client-request.js | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/api/readable.js b/lib/api/readable.js index 508fbdef928..d106568cd4b 100644 --- a/lib/api/readable.js +++ b/lib/api/readable.js @@ -268,7 +268,7 @@ function consumeEnd (consume) { pos += buf.byteLength } - resolve(dst) + resolve(dst.buffer) } else if (type === 'blob') { if (!Blob) { Blob = require('buffer').Blob diff --git a/test/client-request.js b/test/client-request.js index 703cedf6803..3e6670523b8 100644 --- a/test/client-request.js +++ b/test/client-request.js @@ -413,7 +413,7 @@ test('request blob', { skip: nodeMajor < 16 }, (t) => { }) test('request arrayBuffer', (t) => { - t.plan(1) + t.plan(2) const obj = { asd: true } const server = createServer((req, res) => { @@ -429,7 +429,10 @@ test('request arrayBuffer', (t) => { path: '/', method: 'GET' }) - t.strictSame(Buffer.from(JSON.stringify(obj)), Buffer.from(await body.arrayBuffer())) + const ab = await body.arrayBuffer() + + t.strictSame(Buffer.from(JSON.stringify(obj)), Buffer.from(ab)) + t.ok(ab instanceof ArrayBuffer) }) }) From e3de712b1d176753950fcfb04ececdd9a4ddfd06 Mon Sep 17 00:00:00 2001 From: Yoann Gendrey Date: Sat, 14 Oct 2023 06:35:41 +0200 Subject: [PATCH 176/259] fix: skip readMore call if parser is null or undefined (#2346) --- lib/client.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/client.js b/lib/client.js index 065fb563380..00f4467cc8b 100644 --- a/lib/client.js +++ b/lib/client.js @@ -1070,7 +1070,9 @@ function onParserTimeout (parser) { function onSocketReadable () { const { [kParser]: parser } = this - parser.readMore() + if (parser) { + parser.readMore() + } } function onSocketError (err) { From 40ab05029814c9778b80657d530e943d1cdfc6f2 Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Sun, 15 Oct 2023 15:43:39 +0200 Subject: [PATCH 177/259] test: first attempt for flaky fix (#2337) --- test/fetch/http2.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/fetch/http2.js b/test/fetch/http2.js index b426de3cbe5..32fb33e14eb 100644 --- a/test/fetch/http2.js +++ b/test/fetch/http2.js @@ -193,11 +193,11 @@ test( ':status': 200 }) - stream.end('hello h2!') - for await (const chunk of stream) { requestChunks.push(chunk) } + + stream.end('hello h2!') }) t.plan(8) From 52a79891f95a20fe380d70cfdf8f8f52410ff1a3 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 17 Oct 2023 11:06:03 +0200 Subject: [PATCH 178/259] test: only include WebSocket in WPT Report where it's landed (#2351) --- package.json | 2 +- test/wpt/start-websockets.mjs | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 6c4b6f4abfd..2519a2fb457 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,7 @@ "test:tdd": "tap test/*.js test/diagnostics-channel/*.js -w", "test:typescript": "node scripts/verifyVersion.js 14 || tsd && tsc --skipLibCheck test/imports/undici-import.ts", "test:websocket": "node scripts/verifyVersion.js 18 || tap test/websocket/*.js", - "test:wpt": "node scripts/verifyVersion 18 || (node test/wpt/start-fetch.mjs && node test/wpt/start-FileAPI.mjs && node test/wpt/start-mimesniff.mjs && node test/wpt/start-xhr.mjs && node --no-warnings test/wpt/start-websockets.mjs)", + "test:wpt": "node scripts/verifyVersion 18 || (node test/wpt/start-fetch.mjs && node test/wpt/start-FileAPI.mjs && node test/wpt/start-mimesniff.mjs && node test/wpt/start-xhr.mjs && node --no-warnings --expose-internals test/wpt/start-websockets.mjs)", "coverage": "nyc --reporter=text --reporter=html npm run test", "coverage:ci": "nyc --reporter=lcov npm run test", "bench": "PORT=3042 concurrently -k -s first npm:bench:server npm:bench:run", diff --git a/test/wpt/start-websockets.mjs b/test/wpt/start-websockets.mjs index 73fd945c4d6..29c9e676d3f 100644 --- a/test/wpt/start-websockets.mjs +++ b/test/wpt/start-websockets.mjs @@ -3,9 +3,18 @@ import { join } from 'path' import { fileURLToPath } from 'url' import { fork } from 'child_process' import { on } from 'events' +import options from 'internal/options' const { WPT_REPORT } = process.env +function isGlobalAvailable () { + if (typeof WebSocket !== 'undefined') { + return true + } + + return typeof options.getOptionValue('--experimental-websocket') === 'boolean' +} + if (process.env.CI) { // TODO(@KhafraDev): figure out *why* these tests are flaky in the CI. // process.exit(0) @@ -22,7 +31,7 @@ child.on('exit', (code) => process.exit(code)) for await (const [message] of on(child, 'message')) { if (message.server) { const runner = new WPTRunner('websockets', message.server, { - appendReport: !!WPT_REPORT, + appendReport: !!WPT_REPORT && isGlobalAvailable(), reportPath: WPT_REPORT }) runner.run() From 49254c3ea1cc74a01132b8c0a9274cbf94c7289e Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Tue, 17 Oct 2023 16:59:17 +0200 Subject: [PATCH 179/259] Update DispatchInterceptor.md (#2354) --- docs/api/DispatchInterceptor.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/DispatchInterceptor.md b/docs/api/DispatchInterceptor.md index 652b2e86bf9..7dfc260e32a 100644 --- a/docs/api/DispatchInterceptor.md +++ b/docs/api/DispatchInterceptor.md @@ -1,4 +1,4 @@ -#Interface: DispatchInterceptor +# Interface: DispatchInterceptor Extends: `Function` From daf349f6bc35c2e7fe600c323fb7dcfdacc000f6 Mon Sep 17 00:00:00 2001 From: Nikita Ivanov Date: Wed, 18 Oct 2023 23:11:07 +0300 Subject: [PATCH 180/259] fix: Avoid error for stream() being aborted (#2355) Co-authored-by: BobNobrain --- lib/api/api-stream.js | 10 +++++++- test/issue-2349.js | 53 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 test/issue-2349.js diff --git a/lib/api/api-stream.js b/lib/api/api-stream.js index 3a8e71a5730..c571a6f79a7 100644 --- a/lib/api/api-stream.js +++ b/lib/api/api-stream.js @@ -104,6 +104,10 @@ class StreamHandler extends AsyncResource { { callback, body: res, contentType, statusCode, statusMessage, headers } ) } else { + if (factory === null) { + return + } + res = this.runInAsyncScope(factory, null, { statusCode, headers, @@ -152,7 +156,7 @@ class StreamHandler extends AsyncResource { onData (chunk) { const { res } = this - return res.write(chunk) + return res ? res.write(chunk) : true } onComplete (trailers) { @@ -160,6 +164,10 @@ class StreamHandler extends AsyncResource { removeSignal(this) + if (!res) { + return + } + this.trailers = util.parseHeaders(trailers) res.end() diff --git a/test/issue-2349.js b/test/issue-2349.js new file mode 100644 index 00000000000..a82bb74a261 --- /dev/null +++ b/test/issue-2349.js @@ -0,0 +1,53 @@ +'use strict' + +const { test, skip } = require('tap') +const { nodeMajor } = require('../lib/core/util') +const { Writable } = require('stream') +const { MockAgent, errors, stream } = require('..') + +if (nodeMajor < 16) { + skip('only for node 16') + process.exit(0) +} + +test('stream() does not fail after request has been aborted', async (t) => { + t.plan(1) + + const mockAgent = new MockAgent() + + mockAgent.disableNetConnect() + mockAgent + .get('http://localhost:3333') + .intercept({ + path: '/' + }) + .reply(200, 'ok') + .delay(10) + + const parts = [] + const ac = new AbortController() + + setTimeout(() => ac.abort('nevermind'), 5) + + try { + await stream( + 'http://localhost:3333/', + { + opaque: { parts }, + signal: ac.signal, + dispatcher: mockAgent + }, + ({ opaque: { parts } }) => { + return new Writable({ + write (chunk, _encoding, callback) { + parts.push(chunk) + callback() + } + }) + } + ) + } catch (error) { + console.log(error) + t.equal(error instanceof errors.RequestAbortedError, true) + } +}) From 0681760ef1b24a9ddcb257b1a6a121d1825a67c0 Mon Sep 17 00:00:00 2001 From: Khafra Date: Thu, 19 Oct 2023 01:02:41 -0400 Subject: [PATCH 181/259] fix names with esbuild (#2359) --- package.json | 2 +- test/fetch/bundle.js | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 test/fetch/bundle.js diff --git a/package.json b/package.json index 2519a2fb457..f92164f9aaf 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "docs" ], "scripts": { - "build:node": "npx esbuild@0.19.4 index-fetch.js --bundle --platform=node --outfile=undici-fetch.js --define:esbuildDetection=1", + "build:node": "npx esbuild@0.19.4 index-fetch.js --bundle --platform=node --outfile=undici-fetch.js --define:esbuildDetection=1 --keep-names", "prebuild:wasm": "node build/wasm.js --prebuild", "build:wasm": "node build/wasm.js --docker", "lint": "standard | snazzy", diff --git a/test/fetch/bundle.js b/test/fetch/bundle.js new file mode 100644 index 00000000000..3b35cfc04aa --- /dev/null +++ b/test/fetch/bundle.js @@ -0,0 +1,20 @@ +'use strict' + +const { test } = require('tap') +const { Response, Request, FormData, Headers } = require('../../undici-fetch') + +test('bundle sets constructor.name and .name properly', (t) => { + t.equal(new Response().constructor.name, 'Response') + t.equal(Response.name, 'Response') + + t.equal(new Request('http://a').constructor.name, 'Request') + t.equal(Request.name, 'Request') + + t.equal(new Headers().constructor.name, 'Headers') + t.equal(Headers.name, 'Headers') + + t.equal(new FormData().constructor.name, 'FormData') + t.equal(FormData.name, 'FormData') + + t.end() +}) From dea70e27e4d14952eb7b96da021eb44d24d1159e Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Thu, 19 Oct 2023 10:45:52 +0200 Subject: [PATCH 182/259] Bumped v5.26.4 Signed-off-by: Matteo Collina --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f92164f9aaf..31e157de002 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "5.26.3", + "version": "5.26.4", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { From 63afd9b5e28380dc86de29ade69adaad7efcd231 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Thu, 19 Oct 2023 16:25:23 +0200 Subject: [PATCH 183/259] Drop race condition in connect-timeout test (#2360) Signed-off-by: Matteo Collina --- test/connect-timeout.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/connect-timeout.js b/test/connect-timeout.js index c904a7335a2..a736a540170 100644 --- a/test/connect-timeout.js +++ b/test/connect-timeout.js @@ -8,7 +8,7 @@ const sleep = require('atomic-sleep') test('priotorise socket errors over timeouts', (t) => { t.plan(1) const connectTimeout = 1000 - const client = new Pool('http://foobar.bar:1234', { connectTimeout: 1 }) + const client = new Pool('http://foobar.bar:1234', { connectTimeout: 2 }) client.request({ method: 'GET', path: '/foobar' }) .then(() => t.fail()) From 24db5d4eaf8b747e7c9bde4faf797943b6f73c8d Mon Sep 17 00:00:00 2001 From: Igor Savin Date: Mon, 23 Oct 2023 08:42:34 +0300 Subject: [PATCH 184/259] Remove a couple of unnecessary async functions (#2367) --- index-fetch.js | 8 +++---- lib/fetch/index.js | 53 +++++++++++++++++++++++----------------------- 2 files changed, 30 insertions(+), 31 deletions(-) diff --git a/index-fetch.js b/index-fetch.js index 23ac5306007..ba31a65f25c 100644 --- a/index-fetch.js +++ b/index-fetch.js @@ -2,13 +2,11 @@ const fetchImpl = require('./lib/fetch').fetch -module.exports.fetch = async function fetch (resource, init = undefined) { - try { - return await fetchImpl(resource, init) - } catch (err) { +module.exports.fetch = function fetch (resource, init = undefined) { + return fetchImpl(resource, init).catch((err) => { Error.captureStackTrace(err, this) throw err - } + }) } module.exports.FormData = require('./lib/fetch/formdata').FormData module.exports.Headers = require('./lib/fetch/headers').Headers diff --git a/lib/fetch/index.js b/lib/fetch/index.js index 5323c30abc8..10d84a8d192 100644 --- a/lib/fetch/index.js +++ b/lib/fetch/index.js @@ -121,7 +121,7 @@ class Fetch extends EE { } // https://fetch.spec.whatwg.org/#fetch-method -async function fetch (input, init = {}) { +function fetch (input, init = {}) { webidl.argumentLengthCheck(arguments, 1, { header: 'globalThis.fetch' }) // 1. Let p be a new promise. @@ -204,7 +204,7 @@ async function fetch (input, init = {}) { const processResponse = (response) => { // 1. If locallyAborted is true, terminate these substeps. if (locallyAborted) { - return + return Promise.resolve() } // 2. If response’s aborted flag is set, then: @@ -217,7 +217,7 @@ async function fetch (input, init = {}) { // deserializedError. abortFetch(p, request, responseObject, controller.serializedAbortReason) - return + return Promise.resolve() } // 3. If response is a network error, then reject p with a TypeError @@ -226,7 +226,7 @@ async function fetch (input, init = {}) { p.reject( Object.assign(new TypeError('fetch failed'), { cause: response.error }) ) - return + return Promise.resolve() } // 4. Set responseObject to the result of creating a Response object, @@ -776,13 +776,13 @@ async function mainFetch (fetchParams, recursive = false) { // https://fetch.spec.whatwg.org/#concept-scheme-fetch // given a fetch params fetchParams -async function schemeFetch (fetchParams) { +function schemeFetch (fetchParams) { // Note: since the connection is destroyed on redirect, which sets fetchParams to a // cancelled state, we do not want this condition to trigger *unless* there have been // no redirects. See https://github.com/nodejs/undici/issues/1776 // 1. If fetchParams is canceled, then return the appropriate network error for fetchParams. if (isCancelled(fetchParams) && fetchParams.request.redirectCount === 0) { - return makeAppropriateNetworkError(fetchParams) + return Promise.resolve(makeAppropriateNetworkError(fetchParams)) } // 2. Let request be fetchParams’s request. @@ -798,7 +798,7 @@ async function schemeFetch (fetchParams) { // and body is the empty byte sequence as a body. // Otherwise, return a network error. - return makeNetworkError('about scheme is not supported') + return Promise.resolve(makeNetworkError('about scheme is not supported')) } case 'blob:': { if (!resolveObjectURL) { @@ -811,7 +811,7 @@ async function schemeFetch (fetchParams) { // https://github.com/web-platform-tests/wpt/blob/7b0ebaccc62b566a1965396e5be7bb2bc06f841f/FileAPI/url/resources/fetch-tests.js#L52-L56 // Buffer.resolveObjectURL does not ignore URL queries. if (blobURLEntry.search.length !== 0) { - return makeNetworkError('NetworkError when attempting to fetch resource.') + return Promise.resolve(makeNetworkError('NetworkError when attempting to fetch resource.')) } const blobURLEntryObject = resolveObjectURL(blobURLEntry.toString()) @@ -819,7 +819,7 @@ async function schemeFetch (fetchParams) { // 2. If request’s method is not `GET`, blobURLEntry is null, or blobURLEntry’s // object is not a Blob object, then return a network error. if (request.method !== 'GET' || !isBlobLike(blobURLEntryObject)) { - return makeNetworkError('invalid method') + return Promise.resolve(makeNetworkError('invalid method')) } // 3. Let bodyWithType be the result of safely extracting blobURLEntry’s object. @@ -846,7 +846,7 @@ async function schemeFetch (fetchParams) { response.body = body - return response + return Promise.resolve(response) } case 'data:': { // 1. Let dataURLStruct be the result of running the @@ -857,7 +857,7 @@ async function schemeFetch (fetchParams) { // 2. If dataURLStruct is failure, then return a // network error. if (dataURLStruct === 'failure') { - return makeNetworkError('failed to fetch the data URL') + return Promise.resolve(makeNetworkError('failed to fetch the data URL')) } // 3. Let mimeType be dataURLStruct’s MIME type, serialized. @@ -866,28 +866,28 @@ async function schemeFetch (fetchParams) { // 4. Return a response whose status message is `OK`, // header list is « (`Content-Type`, mimeType) », // and body is dataURLStruct’s body as a body. - return makeResponse({ + return Promise.resolve(makeResponse({ statusText: 'OK', headersList: [ ['content-type', { name: 'Content-Type', value: mimeType }] ], body: safelyExtractBody(dataURLStruct.body)[0] - }) + })) } case 'file:': { // For now, unfortunate as it is, file URLs are left as an exercise for the reader. // When in doubt, return a network error. - return makeNetworkError('not implemented... yet...') + return Promise.resolve(makeNetworkError('not implemented... yet...')) } case 'http:': case 'https:': { // Return the result of running HTTP fetch given fetchParams. - return await httpFetch(fetchParams) + return httpFetch(fetchParams) .catch((err) => makeNetworkError(err)) } default: { - return makeNetworkError('unknown scheme') + return Promise.resolve(makeNetworkError('unknown scheme')) } } } @@ -906,7 +906,7 @@ function finalizeResponse (fetchParams, response) { } // https://fetch.spec.whatwg.org/#fetch-finale -async function fetchFinale (fetchParams, response) { +function fetchFinale (fetchParams, response) { // 1. If response is a network error, then: if (response.type === 'error') { // 1. Set response’s URL list to « fetchParams’s request’s URL list[0] ». @@ -990,8 +990,9 @@ async function fetchFinale (fetchParams, response) { } else { // 4. Otherwise, fully read response’s body given processBody, processBodyError, // and fetchParams’s task destination. - await fullyReadBody(response.body, processBody, processBodyError) + return fullyReadBody(response.body, processBody, processBodyError) } + return Promise.resolve() } } @@ -1099,7 +1100,7 @@ async function httpFetch (fetchParams) { } // https://fetch.spec.whatwg.org/#http-redirect-fetch -async function httpRedirectFetch (fetchParams, response) { +function httpRedirectFetch (fetchParams, response) { // 1. Let request be fetchParams’s request. const request = fetchParams.request @@ -1125,18 +1126,18 @@ async function httpRedirectFetch (fetchParams, response) { } } catch (err) { // 5. If locationURL is failure, then return a network error. - return makeNetworkError(err) + return Promise.resolve(makeNetworkError(err)) } // 6. If locationURL’s scheme is not an HTTP(S) scheme, then return a network // error. if (!urlIsHttpHttpsScheme(locationURL)) { - return makeNetworkError('URL scheme must be a HTTP(S) scheme') + return Promise.resolve(makeNetworkError('URL scheme must be a HTTP(S) scheme')) } // 7. If request’s redirect count is 20, then return a network error. if (request.redirectCount === 20) { - return makeNetworkError('redirect count exceeded') + return Promise.resolve(makeNetworkError('redirect count exceeded')) } // 8. Increase request’s redirect count by 1. @@ -1150,7 +1151,7 @@ async function httpRedirectFetch (fetchParams, response) { (locationURL.username || locationURL.password) && !sameOrigin(request, locationURL) ) { - return makeNetworkError('cross origin not allowed for request mode "cors"') + return Promise.resolve(makeNetworkError('cross origin not allowed for request mode "cors"')) } // 10. If request’s response tainting is "cors" and locationURL includes @@ -1159,9 +1160,9 @@ async function httpRedirectFetch (fetchParams, response) { request.responseTainting === 'cors' && (locationURL.username || locationURL.password) ) { - return makeNetworkError( + return Promise.resolve(makeNetworkError( 'URL cannot contain credentials for request mode "cors"' - ) + )) } // 11. If actualResponse’s status is not 303, request’s body is non-null, @@ -1171,7 +1172,7 @@ async function httpRedirectFetch (fetchParams, response) { request.body != null && request.body.source == null ) { - return makeNetworkError() + return Promise.resolve(makeNetworkError()) } // 12. If one of the following is true From c31bd88979c04e72ff5f2869b71c630720422ab8 Mon Sep 17 00:00:00 2001 From: Ethan Arrowood Date: Mon, 23 Oct 2023 01:05:31 -0600 Subject: [PATCH 185/259] Update namespace type with Fetch exports (#2361) * adds fetch classes to default namespace type * update tests --- test/types/index.test-d.ts | 8 +++++++- types/index.d.ts | 6 ++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/test/types/index.test-d.ts b/test/types/index.test-d.ts index 4c644045ca0..3827e611956 100644 --- a/test/types/index.test-d.ts +++ b/test/types/index.test-d.ts @@ -1,5 +1,5 @@ import { expectAssignable } from 'tsd' -import Undici, {Pool, Client, errors, fetch, Interceptable, RedirectHandler, DecoratorHandler} from '../..' +import Undici, {Pool, Client, errors, fetch, Interceptable, RedirectHandler, DecoratorHandler, Headers, Response, Request, FormData, File, FileReader} from '../..' import Dispatcher from "../../types/dispatcher"; expectAssignable(new Undici.Pool('', {})) @@ -7,6 +7,12 @@ expectAssignable(new Undici.Client('', {})) expectAssignable(new Undici.MockAgent().get('')) expectAssignable(Undici.errors) expectAssignable(Undici.fetch) +expectAssignable(Undici.Headers) +expectAssignable(Undici.Response) +expectAssignable(Undici.Request) +expectAssignable(Undici.FormData) +expectAssignable(Undici.File) +expectAssignable(Undici.FileReader) const client = new Undici.Client('', {}) const handler: Dispatcher.DispatchHandlers = {} diff --git a/types/index.d.ts b/types/index.d.ts index c7532d69a07..4589845b4a9 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -53,5 +53,11 @@ declare namespace Undici { var MockAgent: typeof import('./mock-agent').default; var mockErrors: typeof import('./mock-errors').default; var fetch: typeof import('./fetch').fetch; + var Headers: typeof import('./fetch').Headers; + var Response: typeof import('./fetch').Response; + var Request: typeof import('./fetch').Request; + var FormData: typeof import('./formdata').FormData; + var File: typeof import('./file').File; + var FileReader: typeof import('./filereader').FileReader; var caches: typeof import('./cache').caches; } From 9197790ae0d015b40b75fd0c5cdb7420704b5272 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 23 Oct 2023 09:25:50 +0200 Subject: [PATCH 186/259] Bumped v5.26.5 Signed-off-by: Matteo Collina --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 31e157de002..1d610c23025 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "5.26.4", + "version": "5.26.5", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { From 8050ec0224a51d44f776364820e6a16112fb4781 Mon Sep 17 00:00:00 2001 From: Igor Savin Date: Mon, 23 Oct 2023 17:00:56 +0300 Subject: [PATCH 187/259] Use sets and reusable TextEncoder/TextDecoder instances (#2368) * Use sets and reusable TextEncoder/TextDecoder instances * Do not reuse streaming decoder --- lib/fetch/body.js | 21 ++++++++++++--------- lib/fetch/constants.js | 17 ++++++++++++++++- lib/fetch/file.js | 3 ++- lib/fetch/index.js | 17 +++++++++-------- lib/fetch/request.js | 8 ++++---- lib/fetch/response.js | 7 ++++--- lib/fetch/util.js | 8 ++++---- 7 files changed, 51 insertions(+), 30 deletions(-) diff --git a/lib/fetch/body.js b/lib/fetch/body.js index 1d9f17d7e33..fd8481b796d 100644 --- a/lib/fetch/body.js +++ b/lib/fetch/body.js @@ -26,6 +26,8 @@ let ReadableStream = globalThis.ReadableStream /** @type {globalThis['File']} */ const File = NativeFile ?? UndiciFile +const textEncoder = new TextEncoder() +const textDecoder = new TextDecoder() // https://fetch.spec.whatwg.org/#concept-bodyinit-extract function extractBody (object, keepalive = false) { @@ -49,7 +51,7 @@ function extractBody (object, keepalive = false) { stream = new ReadableStream({ async pull (controller) { controller.enqueue( - typeof source === 'string' ? new TextEncoder().encode(source) : source + typeof source === 'string' ? textEncoder.encode(source) : source ) queueMicrotask(() => readableStreamClose(controller)) }, @@ -119,7 +121,6 @@ function extractBody (object, keepalive = false) { // - That the content-length is calculated in advance. // - And that all parts are pre-encoded and ready to be sent. - const enc = new TextEncoder() const blobParts = [] const rn = new Uint8Array([13, 10]) // '\r\n' length = 0 @@ -127,13 +128,13 @@ function extractBody (object, keepalive = false) { for (const [name, value] of object) { if (typeof value === 'string') { - const chunk = enc.encode(prefix + + const chunk = textEncoder.encode(prefix + `; name="${escape(normalizeLinefeeds(name))}"` + `\r\n\r\n${normalizeLinefeeds(value)}\r\n`) blobParts.push(chunk) length += chunk.byteLength } else { - const chunk = enc.encode(`${prefix}; name="${escape(normalizeLinefeeds(name))}"` + + const chunk = textEncoder.encode(`${prefix}; name="${escape(normalizeLinefeeds(name))}"` + (value.name ? `; filename="${escape(value.name)}"` : '') + '\r\n' + `Content-Type: ${ value.type || 'application/octet-stream' @@ -147,7 +148,7 @@ function extractBody (object, keepalive = false) { } } - const chunk = enc.encode(`--${boundary}--`) + const chunk = textEncoder.encode(`--${boundary}--`) blobParts.push(chunk) length += chunk.byteLength if (hasUnknownSizeValue) { @@ -443,14 +444,16 @@ function bodyMixinMethods (instance) { let text = '' // application/x-www-form-urlencoded parser will keep the BOM. // https://url.spec.whatwg.org/#concept-urlencoded-parser - const textDecoder = new TextDecoder('utf-8', { ignoreBOM: true }) + // Note that streaming decoder is stateful and cannot be reused + const streamingDecoder = new TextDecoder('utf-8', { ignoreBOM: true }) + for await (const chunk of consumeBody(this[kState].body)) { if (!isUint8Array(chunk)) { throw new TypeError('Expected Uint8Array chunk') } - text += textDecoder.decode(chunk, { stream: true }) + text += streamingDecoder.decode(chunk, { stream: true }) } - text += textDecoder.decode() + text += streamingDecoder.decode() entries = new URLSearchParams(text) } catch (err) { // istanbul ignore next: Unclear when new URLSearchParams can fail on a string. @@ -565,7 +568,7 @@ function utf8DecodeBytes (buffer) { // 3. Process a queue with an instance of UTF-8’s // decoder, ioQueue, output, and "replacement". - const output = new TextDecoder().decode(buffer) + const output = textDecoder.decode(buffer) // 4. Return output. return output diff --git a/lib/fetch/constants.js b/lib/fetch/constants.js index a5294a994fb..218fcbee4da 100644 --- a/lib/fetch/constants.js +++ b/lib/fetch/constants.js @@ -3,10 +3,12 @@ const { MessageChannel, receiveMessageOnPort } = require('worker_threads') const corsSafeListedMethods = ['GET', 'HEAD', 'POST'] +const corsSafeListedMethodsSet = new Set(corsSafeListedMethods) const nullBodyStatus = [101, 204, 205, 304] const redirectStatus = [301, 302, 303, 307, 308] +const redirectStatusSet = new Set(redirectStatus) // https://fetch.spec.whatwg.org/#block-bad-port const badPorts = [ @@ -18,6 +20,8 @@ const badPorts = [ '10080' ] +const badPortsSet = new Set(badPorts) + // https://w3c.github.io/webappsec-referrer-policy/#referrer-policies const referrerPolicy = [ '', @@ -30,10 +34,12 @@ const referrerPolicy = [ 'strict-origin-when-cross-origin', 'unsafe-url' ] +const referrerPolicySet = new Set(referrerPolicy) const requestRedirect = ['follow', 'manual', 'error'] const safeMethods = ['GET', 'HEAD', 'OPTIONS', 'TRACE'] +const safeMethodsSet = new Set(safeMethods) const requestMode = ['navigate', 'same-origin', 'no-cors', 'cors'] @@ -68,6 +74,7 @@ const requestDuplex = [ // http://fetch.spec.whatwg.org/#forbidden-method const forbiddenMethods = ['CONNECT', 'TRACE', 'TRACK'] +const forbiddenMethodsSet = new Set(forbiddenMethods) const subresource = [ 'audio', @@ -83,6 +90,7 @@ const subresource = [ 'xslt', '' ] +const subresourceSet = new Set(subresource) /** @type {globalThis['DOMException']} */ const DOMException = globalThis.DOMException ?? (() => { @@ -132,5 +140,12 @@ module.exports = { nullBodyStatus, safeMethods, badPorts, - requestDuplex + requestDuplex, + subresourceSet, + badPortsSet, + redirectStatusSet, + corsSafeListedMethodsSet, + safeMethodsSet, + forbiddenMethodsSet, + referrerPolicySet } diff --git a/lib/fetch/file.js b/lib/fetch/file.js index 81bb7b2441a..3133d255ecd 100644 --- a/lib/fetch/file.js +++ b/lib/fetch/file.js @@ -7,6 +7,7 @@ const { isBlobLike } = require('./util') const { webidl } = require('./webidl') const { parseMIMEType, serializeAMimeType } = require('./dataURL') const { kEnumerableProperty } = require('../core/util') +const encoder = new TextEncoder() class File extends Blob { constructor (fileBits, fileName, options = {}) { @@ -280,7 +281,7 @@ function processBlobParts (parts, options) { } // 3. Append the result of UTF-8 encoding s to bytes. - bytes.push(new TextEncoder().encode(s)) + bytes.push(encoder.encode(s)) } else if ( types.isAnyArrayBuffer(element) || types.isTypedArray(element) diff --git a/lib/fetch/index.js b/lib/fetch/index.js index 10d84a8d192..298b3ddb27c 100644 --- a/lib/fetch/index.js +++ b/lib/fetch/index.js @@ -46,11 +46,11 @@ const { kState, kHeaders, kGuard, kRealm } = require('./symbols') const assert = require('assert') const { safelyExtractBody } = require('./body') const { - redirectStatus, + redirectStatusSet, nullBodyStatus, - safeMethods, + safeMethodsSet, requestBodyHeader, - subresource, + subresourceSet, DOMException } = require('./constants') const { kHeadersList } = require('../core/symbols') @@ -62,6 +62,7 @@ const { TransformStream } = require('stream/web') const { getGlobalDispatcher } = require('../global') const { webidl } = require('./webidl') const { STATUS_CODES } = require('http') +const GET_OR_HEAD = ['GET', 'HEAD'] /** @type {import('buffer').resolveObjectURL} */ let resolveObjectURL @@ -509,7 +510,7 @@ function fetching ({ } // 15. If request is a subresource request, then: - if (subresource.includes(request.destination)) { + if (subresourceSet.has(request.destination)) { // TODO } @@ -1063,7 +1064,7 @@ async function httpFetch (fetchParams) { } // 8. If actualResponse’s status is a redirect status, then: - if (redirectStatus.includes(actualResponse.status)) { + if (redirectStatusSet.has(actualResponse.status)) { // 1. If actualResponse’s status is not 303, request’s body is not null, // and the connection uses HTTP/2, then user agents may, and are even // encouraged to, transmit an RST_STREAM frame. @@ -1181,7 +1182,7 @@ function httpRedirectFetch (fetchParams, response) { if ( ([301, 302].includes(actualResponse.status) && request.method === 'POST') || (actualResponse.status === 303 && - !['GET', 'HEAD'].includes(request.method)) + !GET_OR_HEAD.includes(request.method)) ) { // then: // 1. Set request’s method to `GET` and request’s body to null. @@ -1465,7 +1466,7 @@ async function httpNetworkOrCacheFetch ( // responses in httpCache, as per the "Invalidation" chapter of HTTP // Caching, and set storedResponse to null. [HTTP-CACHING] if ( - !safeMethods.includes(httpRequest.method) && + !safeMethodsSet.has(httpRequest.method) && forwardResponse.status >= 200 && forwardResponse.status <= 399 ) { @@ -2025,7 +2026,7 @@ async function httpNetworkFetch ( const willFollow = request.redirect === 'follow' && location && - redirectStatus.includes(status) + redirectStatusSet.has(status) // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding if (request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status) && !willFollow) { diff --git a/lib/fetch/request.js b/lib/fetch/request.js index 912bd5b8c98..60e654eca11 100644 --- a/lib/fetch/request.js +++ b/lib/fetch/request.js @@ -13,8 +13,8 @@ const { makePolicyContainer } = require('./util') const { - forbiddenMethods, - corsSafeListedMethods, + forbiddenMethodsSet, + corsSafeListedMethodsSet, referrerPolicy, requestRedirect, requestMode, @@ -319,7 +319,7 @@ class Request { throw TypeError(`'${init.method}' is not a valid HTTP method.`) } - if (forbiddenMethods.indexOf(method.toUpperCase()) !== -1) { + if (forbiddenMethodsSet.has(method.toUpperCase())) { throw TypeError(`'${init.method}' HTTP method is unsupported.`) } @@ -404,7 +404,7 @@ class Request { if (mode === 'no-cors') { // 1. If this’s request’s method is not a CORS-safelisted method, // then throw a TypeError. - if (!corsSafeListedMethods.includes(request.method)) { + if (!corsSafeListedMethodsSet.has(request.method)) { throw new TypeError( `'${request.method} is unsupported in no-cors mode.` ) diff --git a/lib/fetch/response.js b/lib/fetch/response.js index 88deb71a062..23cf55c51dc 100644 --- a/lib/fetch/response.js +++ b/lib/fetch/response.js @@ -14,7 +14,7 @@ const { isomorphicEncode } = require('./util') const { - redirectStatus, + redirectStatusSet, nullBodyStatus, DOMException } = require('./constants') @@ -28,6 +28,7 @@ const assert = require('assert') const { types } = require('util') const ReadableStream = globalThis.ReadableStream || require('stream/web').ReadableStream +const textEncoder = new TextEncoder('utf-8') // https://fetch.spec.whatwg.org/#response-class class Response { @@ -57,7 +58,7 @@ class Response { } // 1. Let bytes the result of running serialize a JavaScript value to JSON bytes on data. - const bytes = new TextEncoder('utf-8').encode( + const bytes = textEncoder.encode( serializeJavascriptValueToJSONString(data) ) @@ -102,7 +103,7 @@ class Response { } // 3. If status is not a redirect status, then throw a RangeError. - if (!redirectStatus.includes(status)) { + if (!redirectStatusSet.has(status)) { throw new RangeError('Invalid status code ' + status) } diff --git a/lib/fetch/util.js b/lib/fetch/util.js index fcbba84bc9a..033fa206aed 100644 --- a/lib/fetch/util.js +++ b/lib/fetch/util.js @@ -1,6 +1,6 @@ 'use strict' -const { redirectStatus, badPorts, referrerPolicy: referrerPolicyTokens } = require('./constants') +const { redirectStatusSet, referrerPolicySet: referrerPolicyTokens, badPortsSet } = require('./constants') const { getGlobalOrigin } = require('./global') const { performance } = require('perf_hooks') const { isBlobLike, toUSVString, ReadableStreamFrom } = require('../core/util') @@ -29,7 +29,7 @@ function responseURL (response) { // https://fetch.spec.whatwg.org/#concept-response-location-url function responseLocationURL (response, requestFragment) { // 1. If response’s status is not a redirect status, then return null. - if (!redirectStatus.includes(response.status)) { + if (!redirectStatusSet.has(response.status)) { return null } @@ -64,7 +64,7 @@ function requestBadPort (request) { // 2. If url’s scheme is an HTTP(S) scheme and url’s port is a bad port, // then return blocked. - if (urlIsHttpHttpsScheme(url) && badPorts.includes(url.port)) { + if (urlIsHttpHttpsScheme(url) && badPortsSet.has(url.port)) { return 'blocked' } @@ -206,7 +206,7 @@ function setRequestReferrerPolicyOnRedirect (request, actualResponse) { // The left-most policy is the fallback. for (let i = policyHeader.length; i !== 0; i--) { const token = policyHeader[i - 1].trim() - if (referrerPolicyTokens.includes(token)) { + if (referrerPolicyTokens.has(token)) { policy = token break } From 67e714d0d55848a56616fa98394c207a835447c8 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Wed, 25 Oct 2023 19:43:21 +0200 Subject: [PATCH 188/259] feat: forward onRequestSent to handler (#2375) --- lib/core/request.js | 8 ++++++++ test/client-dispatch.js | 13 +++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/lib/core/request.js b/lib/core/request.js index 50be01c0dc8..43973884f1a 100644 --- a/lib/core/request.js +++ b/lib/core/request.js @@ -222,6 +222,14 @@ class Request { if (channels.bodySent.hasSubscribers) { channels.bodySent.publish({ request: this }) } + + if (this[kHandler].onRequestSent) { + try { + this[kHandler].onRequestSent() + } catch (err) { + this.onError(err) + } + } } onConnect (abort) { diff --git a/test/client-dispatch.js b/test/client-dispatch.js index 2196e5d53f5..c3de37ae2a9 100644 --- a/test/client-dispatch.js +++ b/test/client-dispatch.js @@ -671,6 +671,8 @@ test('dispatch onBodySent not a function', (t) => { }) test('dispatch onBodySent buffer', (t) => { + t.plan(3) + const server = http.createServer((req, res) => { res.end('ad') }) @@ -688,6 +690,9 @@ test('dispatch onBodySent buffer', (t) => { onBodySent (chunk) { t.equal(chunk.toString(), body) }, + onRequestSent () { + t.pass() + }, onError (err) { throw err }, @@ -695,13 +700,14 @@ test('dispatch onBodySent buffer', (t) => { onHeaders () {}, onData () {}, onComplete () { - t.end() + t.pass() } }) }) }) test('dispatch onBodySent stream', (t) => { + t.plan(8) const server = http.createServer((req, res) => { res.end('ad') }) @@ -723,6 +729,9 @@ test('dispatch onBodySent stream', (t) => { t.equal(chunks[currentChunk++], chunk) sentBytes += Buffer.byteLength(chunk) }, + onRequestSent () { + t.pass() + }, onError (err) { throw err }, @@ -732,7 +741,7 @@ test('dispatch onBodySent stream', (t) => { onComplete () { t.equal(currentChunk, chunks.length) t.equal(sentBytes, toSendBytes) - t.end() + t.pass() } }) }) From 3ec35ee23a7f2693bd6f9f707f6a20dc6de7a6b0 Mon Sep 17 00:00:00 2001 From: Khafra Date: Wed, 25 Oct 2023 15:36:18 -0400 Subject: [PATCH 189/259] skip bundle test on node 16 (#2377) * skip bundle test on node 16 * skip more tests * fix --- test/fetch/bundle.js | 9 ++++++++- test/fetch/client-node-max-header-size.js | 8 +++++++- test/fetch/issue-1447.js | 8 +++++++- test/fetch/user-agent.js | 9 ++++++++- 4 files changed, 30 insertions(+), 4 deletions(-) diff --git a/test/fetch/bundle.js b/test/fetch/bundle.js index 3b35cfc04aa..0a0de22a031 100644 --- a/test/fetch/bundle.js +++ b/test/fetch/bundle.js @@ -1,6 +1,13 @@ 'use strict' -const { test } = require('tap') +const { test, skip } = require('tap') +const { nodeMajor } = require('../../lib/core/util') + +if (nodeMajor === 16) { + skip('esbuild uses static blocks with --keep-names which node 16.8 does not have') + process.exit() +} + const { Response, Request, FormData, Headers } = require('../../undici-fetch') test('bundle sets constructor.name and .name properly', (t) => { diff --git a/test/fetch/client-node-max-header-size.js b/test/fetch/client-node-max-header-size.js index 432a576b97e..737bae8ed6e 100644 --- a/test/fetch/client-node-max-header-size.js +++ b/test/fetch/client-node-max-header-size.js @@ -1,7 +1,13 @@ 'use strict' const { execSync } = require('node:child_process') -const { test } = require('tap') +const { test, skip } = require('tap') +const { nodeMajor } = require('../../lib/core/util') + +if (nodeMajor === 16) { + skip('esbuild uses static blocks with --keep-names which node 16.8 does not have') + process.exit() +} const command = 'node -e "require(\'./undici-fetch.js\').fetch(\'https://httpbin.org/get\')"' diff --git a/test/fetch/issue-1447.js b/test/fetch/issue-1447.js index cfa5e94c0ed..503b34406d2 100644 --- a/test/fetch/issue-1447.js +++ b/test/fetch/issue-1447.js @@ -1,6 +1,12 @@ 'use strict' -const { test } = require('tap') +const { test, skip } = require('tap') +const { nodeMajor } = require('../../lib/core/util') + +if (nodeMajor === 16) { + skip('esbuild uses static blocks with --keep-names which node 16.8 does not have') + process.exit() +} const undici = require('../..') const { fetch: theoreticalGlobalFetch } = require('../../undici-fetch') diff --git a/test/fetch/user-agent.js b/test/fetch/user-agent.js index 48a69b4d7aa..2e37ea5883d 100644 --- a/test/fetch/user-agent.js +++ b/test/fetch/user-agent.js @@ -1,9 +1,16 @@ 'use strict' -const { test } = require('tap') +const { test, skip } = require('tap') const events = require('events') const http = require('http') const undici = require('../../') +const { nodeMajor } = require('../../lib/core/util') + +if (nodeMajor === 16) { + skip('esbuild uses static blocks with --keep-names which node 16.8 does not have') + process.exit() +} + const nodeBuild = require('../../undici-fetch.js') test('user-agent defaults correctly', async (t) => { From 6df0fe04f0631203a2433d77895b6e1a1a13727a Mon Sep 17 00:00:00 2001 From: Khafra Date: Thu, 26 Oct 2023 01:07:14 -0400 Subject: [PATCH 190/259] fix windows CI (#2379) --- package.json | 2 +- test/wpt/start-websockets.mjs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 1d610c23025..1b09c27dca6 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,7 @@ "test:tdd": "tap test/*.js test/diagnostics-channel/*.js -w", "test:typescript": "node scripts/verifyVersion.js 14 || tsd && tsc --skipLibCheck test/imports/undici-import.ts", "test:websocket": "node scripts/verifyVersion.js 18 || tap test/websocket/*.js", - "test:wpt": "node scripts/verifyVersion 18 || (node test/wpt/start-fetch.mjs && node test/wpt/start-FileAPI.mjs && node test/wpt/start-mimesniff.mjs && node test/wpt/start-xhr.mjs && node --no-warnings --expose-internals test/wpt/start-websockets.mjs)", + "test:wpt": "node scripts/verifyVersion 18 || (node test/wpt/start-fetch.mjs && node test/wpt/start-FileAPI.mjs && node test/wpt/start-mimesniff.mjs && node test/wpt/start-xhr.mjs && node test/wpt/start-websockets.mjs)", "coverage": "nyc --reporter=text --reporter=html npm run test", "coverage:ci": "nyc --reporter=lcov npm run test", "bench": "PORT=3042 concurrently -k -s first npm:bench:server npm:bench:run", diff --git a/test/wpt/start-websockets.mjs b/test/wpt/start-websockets.mjs index 29c9e676d3f..ee364ecf6c5 100644 --- a/test/wpt/start-websockets.mjs +++ b/test/wpt/start-websockets.mjs @@ -3,7 +3,6 @@ import { join } from 'path' import { fileURLToPath } from 'url' import { fork } from 'child_process' import { on } from 'events' -import options from 'internal/options' const { WPT_REPORT } = process.env @@ -12,7 +11,7 @@ function isGlobalAvailable () { return true } - return typeof options.getOptionValue('--experimental-websocket') === 'boolean' + return process.execArgv.includes('--experimental-websocket') } if (process.env.CI) { From 41c253d0c23fd1cf63b8033d8ab61c2cf13e8c6e Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Thu, 26 Oct 2023 13:47:21 +0200 Subject: [PATCH 191/259] 5.27.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1b09c27dca6..454271d7b46 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "5.26.5", + "version": "5.27.0", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { From a3a437eeebeb8fcca9014107b9ee748402ba2b1e Mon Sep 17 00:00:00 2001 From: Khafra Date: Thu, 26 Oct 2023 09:14:04 -0400 Subject: [PATCH 192/259] add regression test (#2376) refs: https://github.com/nodejs/node/issues/50263#issuecomment-1779670104 --- test/fetch/bundle.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/fetch/bundle.js b/test/fetch/bundle.js index 0a0de22a031..aa1257a49b4 100644 --- a/test/fetch/bundle.js +++ b/test/fetch/bundle.js @@ -25,3 +25,17 @@ test('bundle sets constructor.name and .name properly', (t) => { t.end() }) + +test('regression test for https://github.com/nodejs/node/issues/50263', (t) => { + const request = new Request('https://a', { + headers: { + test: 'abc' + }, + method: 'POST' + }) + + const request1 = new Request(request, { body: 'does not matter' }) + + t.equal(request1.headers.get('test'), 'abc') + t.end() +}) From fc29aa093d98604190c89ff66bfb7e5285a72693 Mon Sep 17 00:00:00 2001 From: Paul Xue Date: Thu, 26 Oct 2023 16:06:00 -0400 Subject: [PATCH 193/259] fix: define conditions when content-length should be sent (#2305) --- lib/client.js | 13 +- test/content-length.js | 204 ++++++++++++++++++++++++------- test/no-strict-content-length.js | 4 - 3 files changed, 170 insertions(+), 51 deletions(-) diff --git a/lib/client.js b/lib/client.js index 00f4467cc8b..0ac23f3ee99 100644 --- a/lib/client.js +++ b/lib/client.js @@ -1499,6 +1499,11 @@ function _resume (client, sync) { } } +// https://www.rfc-editor.org/rfc/rfc7230#section-3.3.2 +function shouldSendContentLength (method) { + return method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS' && method !== 'TRACE' && method !== 'CONNECT' +} + function write (client, request) { if (client[kHTTPConnVersion] === 'h2') { writeH2(client, client[kHTTP2Session], request) @@ -1542,7 +1547,9 @@ function write (client, request) { contentLength = null } - if (request.contentLength !== null && request.contentLength !== contentLength) { + // https://github.com/nodejs/undici/issues/2046 + // A user agent may send a Content-Length header with 0 value, this should be allowed. + if (shouldSendContentLength(method) && contentLength > 0 && request.contentLength !== null && request.contentLength !== contentLength) { if (client[kStrictContentLength]) { errorRequest(client, request, new RequestContentLengthMismatchError()) return false @@ -1763,7 +1770,9 @@ function writeH2 (client, session, request) { contentLength = null } - if (request.contentLength != null && request.contentLength !== contentLength) { + // https://github.com/nodejs/undici/issues/2046 + // A user agent may send a Content-Length header with 0 value, this should be allowed. + if (shouldSendContentLength(method) && contentLength > 0 && request.contentLength != null && request.contentLength !== contentLength) { if (client[kStrictContentLength]) { errorRequest(client, request, new RequestContentLengthMismatchError()) return false diff --git a/test/content-length.js b/test/content-length.js index 66c010a7c9e..9ce74051d8b 100644 --- a/test/content-length.js +++ b/test/content-length.js @@ -7,7 +7,7 @@ const { Readable } = require('stream') const { maybeWrapStream, consts } = require('./utils/async-iterators') test('request invalid content-length', (t) => { - t.plan(10) + t.plan(7) const server = createServer((req, res) => { res.end() @@ -61,54 +61,13 @@ test('request invalid content-length', (t) => { t.type(err, errors.RequestContentLengthMismatchError) }) - client.request({ - path: '/', - method: 'HEAD', - headers: { - 'content-length': 10 - } - }, (err, data) => { - t.type(err, errors.RequestContentLengthMismatchError) - }) - - client.request({ - path: '/', - method: 'GET', - headers: { - 'content-length': 0 - } - }, (err, data) => { - t.type(err, errors.RequestContentLengthMismatchError) - }) - client.request({ path: '/', method: 'GET', headers: { 'content-length': 4 }, - body: new Readable({ - read () { - this.push('asd') - this.push(null) - } - }) - }, (err, data) => { - t.type(err, errors.RequestContentLengthMismatchError) - }) - - client.request({ - path: '/', - method: 'GET', - headers: { - 'content-length': 4 - }, - body: new Readable({ - read () { - this.push('asasdasdasdd') - this.push(null) - } - }) + body: ['asd'] }, (err, data) => { t.type(err, errors.RequestContentLengthMismatchError) }) @@ -119,14 +78,14 @@ test('request invalid content-length', (t) => { headers: { 'content-length': 4 }, - body: ['asd'] + body: ['asasdasdasdd'] }, (err, data) => { t.type(err, errors.RequestContentLengthMismatchError) }) client.request({ path: '/', - method: 'GET', + method: 'DELETE', headers: { 'content-length': 4 }, @@ -329,3 +288,158 @@ test('request streaming with Readable.from(buf)', (t) => { }) }) }) + +test('request DELETE, content-length=0, with body', (t) => { + t.plan(5) + const server = createServer((req, res) => { + res.end() + }) + server.on('request', (req, res) => { + t.equal(req.headers['content-length'], undefined) + }) + t.teardown(server.close.bind(server)) + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`) + t.teardown(client.destroy.bind(client)) + + client.request({ + path: '/', + method: 'DELETE', + headers: { + 'content-length': 0 + }, + body: new Readable({ + read () { + this.push('asd') + this.push(null) + } + }) + }, (err) => { + t.type(err, errors.RequestContentLengthMismatchError) + }) + + client.request({ + path: '/', + method: 'DELETE', + headers: { + 'content-length': 0 + } + }, (err, resp) => { + t.equal(resp.headers['content-length'], '0') + t.error(err) + }) + + client.on('disconnect', () => { + t.pass() + }) + }) +}) + +test('content-length shouldSendContentLength=false', (t) => { + t.plan(15) + const server = createServer((req, res) => { + res.end() + }) + server.on('request', (req, res) => { + switch (req.url) { + case '/put0': + t.equal(req.headers['content-length'], '0') + break + case '/head': + t.equal(req.headers['content-length'], undefined) + break + case '/get': + t.equal(req.headers['content-length'], undefined) + break + } + }) + t.teardown(server.close.bind(server)) + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`) + t.teardown(client.destroy.bind(client)) + + client.request({ + path: '/put0', + method: 'PUT', + headers: { + 'content-length': 0 + } + }, (err, resp) => { + t.equal(resp.headers['content-length'], '0') + t.error(err) + }) + + client.request({ + path: '/head', + method: 'HEAD', + headers: { + 'content-length': 10 + } + }, (err, resp) => { + t.equal(resp.headers['content-length'], undefined) + t.error(err) + }) + + client.request({ + path: '/get', + method: 'GET', + headers: { + 'content-length': 0 + } + }, (err) => { + t.error(err) + }) + + client.request({ + path: '/', + method: 'GET', + headers: { + 'content-length': 4 + }, + body: new Readable({ + read () { + this.push('asd') + this.push(null) + } + }) + }, (err) => { + t.error(err) + }) + + client.request({ + path: '/', + method: 'GET', + headers: { + 'content-length': 4 + }, + body: new Readable({ + read () { + this.push('asasdasdasdd') + this.push(null) + } + }) + }, (err) => { + t.error(err) + }) + + client.request({ + path: '/', + method: 'HEAD', + headers: { + 'content-length': 4 + }, + body: new Readable({ + read () { + this.push('asasdasdasdd') + this.push(null) + } + }) + }, (err) => { + t.error(err) + }) + + client.on('disconnect', () => { + t.pass() + }) + }) +}) diff --git a/test/no-strict-content-length.js b/test/no-strict-content-length.js index 1d0eae6c789..993b0fdcf7a 100644 --- a/test/no-strict-content-length.js +++ b/test/no-strict-content-length.js @@ -90,7 +90,6 @@ tap.test('strictContentLength: false', (t) => { 'content-length': 10 } }, (err, data) => { - assertEmitWarningCalledAndReset() t.error(err) }) @@ -101,7 +100,6 @@ tap.test('strictContentLength: false', (t) => { 'content-length': 0 } }, (err, data) => { - assertEmitWarningCalledAndReset() t.error(err) }) @@ -118,7 +116,6 @@ tap.test('strictContentLength: false', (t) => { } }) }, (err, data) => { - assertEmitWarningCalledAndReset() t.error(err) }) @@ -135,7 +132,6 @@ tap.test('strictContentLength: false', (t) => { } }) }, (err, data) => { - assertEmitWarningCalledAndReset() t.error(err) }) }) From 3a77cbb471b76175898d8b094beb9b40ceadd671 Mon Sep 17 00:00:00 2001 From: Eva Date: Fri, 27 Oct 2023 11:36:39 +0300 Subject: [PATCH 194/259] refactor: removed unnecessary check (#2381) --- lib/pool.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pool.js b/lib/pool.js index 08509958069..e3cd3399e6b 100644 --- a/lib/pool.js +++ b/lib/pool.js @@ -57,7 +57,7 @@ class Pool extends PoolBase { maxCachedSessions, allowH2, socketPath, - timeout: connectTimeout == null ? 10e3 : connectTimeout, + timeout: connectTimeout, ...(util.nodeHasAutoSelectFamily && autoSelectFamily ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined), ...connect }) From 45b904c3cbac0340e9373b7a0b9a49297b12fd35 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Thu, 2 Nov 2023 18:55:27 +0100 Subject: [PATCH 195/259] fix: stream body handling (#2391) --- lib/client.js | 24 +++++------------------- lib/core/request.js | 41 ++++++++++++++++++++++++++++++++++++++++- lib/core/util.js | 2 +- 3 files changed, 46 insertions(+), 21 deletions(-) diff --git a/lib/client.js b/lib/client.js index 0ac23f3ee99..968a7f92071 100644 --- a/lib/client.js +++ b/lib/client.js @@ -1462,23 +1462,7 @@ function _resume (client, sync) { return } - if (util.isStream(request.body) && util.bodyLength(request.body) === 0) { - request.body - .on('data', /* istanbul ignore next */ function () { - /* istanbul ignore next */ - assert(false) - }) - .on('error', function (err) { - errorRequest(client, request, err) - }) - .on('end', function () { - util.destroy(this) - }) - - request.body = null - } - - if (client[kRunning] > 0 && + if (client[kRunning] > 0 && util.bodyLength(request.body) !== 0 && (util.isStream(request.body) || util.isAsyncIterable(request.body))) { // Request with stream or iterator body can error while other requests // are inflight and indirectly error those as well. @@ -1532,7 +1516,9 @@ function write (client, request) { body.read(0) } - let contentLength = util.bodyLength(body) + const bodyLength = util.bodyLength(body) + + let contentLength = bodyLength if (contentLength === null) { contentLength = request.contentLength @@ -1630,7 +1616,7 @@ function write (client, request) { } /* istanbul ignore else: assertion */ - if (!body) { + if (!body || bodyLength === 0) { if (contentLength === 0) { socket.write(`${header}content-length: 0\r\n\r\n`, 'latin1') } else { diff --git a/lib/core/request.js b/lib/core/request.js index 43973884f1a..1b938bd5747 100644 --- a/lib/core/request.js +++ b/lib/core/request.js @@ -112,10 +112,28 @@ class Request { this.method = method + this.abort = null + if (body == null) { this.body = null } else if (util.isStream(body)) { this.body = body + + if (!this.body._readableState?.autoDestroy) { + this.endHandler = function autoDestroy () { + util.destroy(this) + } + this.body.on('end', this.endHandler) + } + + this.errorHandler = err => { + if (this.abort) { + this.abort(err) + } else { + this.error = err + } + } + this.body.on('error', this.errorHandler) } else if (util.isBuffer(body)) { this.body = body.byteLength ? body : null } else if (ArrayBuffer.isView(body)) { @@ -236,7 +254,12 @@ class Request { assert(!this.aborted) assert(!this.completed) - return this[kHandler].onConnect(abort) + if (this.error) { + abort(this.error) + } else { + this.abort = abort + return this[kHandler].onConnect(abort) + } } onHeaders (statusCode, headers, resume, statusText) { @@ -265,6 +288,8 @@ class Request { } onComplete (trailers) { + this.onFinally() + assert(!this.aborted) this.completed = true @@ -275,6 +300,8 @@ class Request { } onError (error) { + this.onFinally() + if (channels.error.hasSubscribers) { channels.error.publish({ request: this, error }) } @@ -286,6 +313,18 @@ class Request { return this[kHandler].onError(error) } + onFinally () { + if (this.errorHandler) { + this.body.off('error', this.errorHandler) + this.errorHandler = null + } + + if (this.endHandler) { + this.body.off('end', this.endHandler) + this.endHandler = null + } + } + // TODO: adjust to support H2 addHeader (key, value) { processHeader(this, key, value) diff --git a/lib/core/util.js b/lib/core/util.js index 769811f57f7..f2ead039363 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -190,7 +190,7 @@ function isReadableAborted (stream) { } function destroy (stream, err) { - if (!isStream(stream) || isDestroyed(stream)) { + if (stream == null || !isStream(stream) || isDestroyed(stream)) { return } From 2aedba485b539335b7ade6977615f9f94173eab2 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Fri, 3 Nov 2023 17:53:36 +0100 Subject: [PATCH 196/259] 5.27.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 454271d7b46..2f79081928d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "5.27.0", + "version": "5.27.1", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { From 87c54cb1fed24e4106325f2d5047d1bd48fe45e1 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Fri, 3 Nov 2023 21:34:57 +0100 Subject: [PATCH 197/259] fix: avoid optional chaining --- lib/core/request.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/core/request.js b/lib/core/request.js index 1b938bd5747..7db05ce65ae 100644 --- a/lib/core/request.js +++ b/lib/core/request.js @@ -119,7 +119,8 @@ class Request { } else if (util.isStream(body)) { this.body = body - if (!this.body._readableState?.autoDestroy) { + const rState = this.body._readableState + if (!rState || !rState.autoDestroy) { this.endHandler = function autoDestroy () { util.destroy(this) } From 1541173d7a728eaf88bcd87263cef2ea0d993e74 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Fri, 3 Nov 2023 21:35:32 +0100 Subject: [PATCH 198/259] 5.27.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2f79081928d..ea1228f94b9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "5.27.1", + "version": "5.27.2", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { From a054593dc129d6960dcd7773805e39cb59d350b5 Mon Sep 17 00:00:00 2001 From: Michele Doria <85507169+mdoria12@users.noreply.github.com> Date: Sat, 4 Nov 2023 17:49:28 +0100 Subject: [PATCH 199/259] fix(parseHeaders): util.parseHeaders handle correctly array of buffer as value (#2398) --- lib/core/util.js | 2 +- test/mock-agent.js | 6 +++++- test/util.js | 3 ++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/core/util.js b/lib/core/util.js index f2ead039363..6b9cb1df27a 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -228,7 +228,7 @@ function parseHeaders (headers, obj = {}) { if (!val) { if (Array.isArray(headers[i + 1])) { - obj[key] = headers[i + 1] + obj[key] = headers[i + 1].map(x => x.toString('utf8')) } else { obj[key] = headers[i + 1].toString('utf8') } diff --git a/test/mock-agent.js b/test/mock-agent.js index 4145432c2c6..d96a511a167 100644 --- a/test/mock-agent.js +++ b/test/mock-agent.js @@ -2594,5 +2594,9 @@ test('MockAgent - headers should be array of strings', async (t) => { method: 'GET' }) - t.equal(headers['set-cookie'].length, 3) + t.same(headers['set-cookie'], [ + 'foo=bar', + 'bar=baz', + 'baz=qux' + ]) }) diff --git a/test/util.js b/test/util.js index 48a21a1141f..794c68e3f77 100644 --- a/test/util.js +++ b/test/util.js @@ -83,12 +83,13 @@ test('validateHandler', (t) => { }) test('parseHeaders', (t) => { - t.plan(5) + t.plan(6) t.same(util.parseHeaders(['key', 'value']), { key: 'value' }) t.same(util.parseHeaders([Buffer.from('key'), Buffer.from('value')]), { key: 'value' }) t.same(util.parseHeaders(['Key', 'Value']), { key: 'Value' }) t.same(util.parseHeaders(['Key', 'value', 'key', 'Value']), { key: ['value', 'Value'] }) t.same(util.parseHeaders(['key', ['value1', 'value2', 'value3']]), { key: ['value1', 'value2', 'value3'] }) + t.same(util.parseHeaders([Buffer.from('key'), [Buffer.from('value1'), Buffer.from('value2'), Buffer.from('value3')]]), { key: ['value1', 'value2', 'value3'] }) }) test('parseRawHeaders', (t) => { From deccde64ac5108c40cb5ce23591335eab75c23ef Mon Sep 17 00:00:00 2001 From: Dan Castillo Date: Sun, 5 Nov 2023 03:59:22 -0500 Subject: [PATCH 200/259] docs: add license to undici-types (#2401) --- scripts/generate-undici-types-package-json.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/generate-undici-types-package-json.js b/scripts/generate-undici-types-package-json.js index 250cfc08aa2..78095ae6d5e 100644 --- a/scripts/generate-undici-types-package-json.js +++ b/scripts/generate-undici-types-package-json.js @@ -5,6 +5,9 @@ const packageJSONPath = path.join(__dirname, '..', 'package.json') const packageJSONRaw = fs.readFileSync(packageJSONPath, 'utf-8') const packageJSON = JSON.parse(packageJSONRaw) +const licensePath = path.join(__dirname, '..', 'LICENSE') +const licenseRaw = fs.readFileSync(licensePath, 'utf-8') + const packageTypesJSON = { name: 'undici-types', version: packageJSON.version, @@ -19,5 +22,7 @@ const packageTypesJSON = { } const packageTypesPath = path.join(__dirname, '..', 'types', 'package.json') +const licenseTypesPath = path.join(__dirname, '..', 'types', 'LICENSE') fs.writeFileSync(packageTypesPath, JSON.stringify(packageTypesJSON, null, 2)) +fs.writeFileSync(licenseTypesPath, licenseRaw) From 7b5c851885e5f85da01dd3de8d316c394c5ca76b Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Mon, 6 Nov 2023 20:48:00 +0100 Subject: [PATCH 201/259] perf: optimize Readable.dump (#2402) --- lib/api/readable.js | 65 ++++++++++++++++++++++++++++----------------- lib/core/util.js | 9 ------- 2 files changed, 40 insertions(+), 34 deletions(-) diff --git a/lib/api/readable.js b/lib/api/readable.js index d106568cd4b..89913eaa621 100644 --- a/lib/api/readable.js +++ b/lib/api/readable.js @@ -16,6 +16,8 @@ const kBody = Symbol('kBody') const kAbort = Symbol('abort') const kContentType = Symbol('kContentType') +const noop = () => {} + module.exports = class BodyReadable extends Readable { constructor ({ resume, @@ -149,37 +151,50 @@ module.exports = class BodyReadable extends Readable { return this[kBody] } - async dump (opts) { + dump (opts) { let limit = opts && Number.isFinite(opts.limit) ? opts.limit : 262144 const signal = opts && opts.signal - const abortFn = () => { - this.destroy() - } - let signalListenerCleanup + if (signal) { - if (typeof signal !== 'object' || !('aborted' in signal)) { - throw new InvalidArgumentError('signal must be an AbortSignal') - } - util.throwIfAborted(signal) - signalListenerCleanup = util.addAbortListener(signal, abortFn) - } - try { - for await (const chunk of this) { - util.throwIfAborted(signal) - limit -= Buffer.byteLength(chunk) - if (limit < 0) { - return + try { + if (typeof signal !== 'object' || !('aborted' in signal)) { + throw new InvalidArgumentError('signal must be an AbortSignal') } + util.throwIfAborted(signal) + } catch (err) { + return Promise.reject(err) } - } catch { - util.throwIfAborted(signal) - } finally { - if (typeof signalListenerCleanup === 'function') { - signalListenerCleanup() - } else if (signalListenerCleanup) { - signalListenerCleanup[Symbol.dispose]() - } } + + if (this.closed) { + return Promise.resolve(null) + } + + return new Promise((resolve, reject) => { + const signalListenerCleanup = signal + ? util.addAbortListener(signal, () => { + this.destroy() + }) + : noop + + this + .on('close', function () { + signalListenerCleanup() + if (signal?.aborted) { + reject(signal.reason || Object.assign(new Error('The operation was aborted'), { name: 'AbortError' })) + } else { + resolve(null) + } + }) + .on('error', noop) + .on('data', function (chunk) { + limit -= chunk.length + if (limit <= 0) { + this.destroy() + } + }) + .resume() + }) } } diff --git a/lib/core/util.js b/lib/core/util.js index 6b9cb1df27a..7fe2229c3c4 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -431,16 +431,7 @@ function throwIfAborted (signal) { } } -let events function addAbortListener (signal, listener) { - if (typeof Symbol.dispose === 'symbol') { - if (!events) { - events = require('events') - } - if (typeof events.addAbortListener === 'function' && 'aborted' in signal) { - return events.addAbortListener(signal, listener) - } - } if ('addEventListener' in signal) { signal.addEventListener('abort', listener, { once: true }) return () => signal.removeEventListener('abort', listener) From 0f319a0dfffc228e99321954e71a0837a51b3973 Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Tue, 7 Nov 2023 22:15:22 +0900 Subject: [PATCH 202/259] perf(headers): Improve Headers (#2397) * perf(headers): Improve Headers * perf: reduce variables in the for loop * perf: use `===` * perf: use `===` * feat: Revert `avoid re-stringify` * perf: use `substring` * fix: fixing trimming issues * perf: avoid two duplicate calls of `ByteString`. * fix: comment position --- lib/fetch/headers.js | 116 +++++++++++++++++++++++++------------------ lib/fetch/webidl.js | 6 +-- 2 files changed, 69 insertions(+), 53 deletions(-) diff --git a/lib/fetch/headers.js b/lib/fetch/headers.js index aa5e73e5d27..bf17d690a91 100644 --- a/lib/fetch/headers.js +++ b/lib/fetch/headers.js @@ -16,6 +16,13 @@ const assert = require('assert') const kHeadersMap = Symbol('headers map') const kHeadersSortedMap = Symbol('headers map sorted') +/** + * @param {number} code + */ +function isHTTPWhiteSpaceCharCode (code) { + return code === 0x00a || code === 0x00d || code === 0x009 || code === 0x020 +} + /** * @see https://fetch.spec.whatwg.org/#concept-header-value-normalize * @param {string} potentialValue @@ -24,12 +31,12 @@ function headerValueNormalize (potentialValue) { // To normalize a byte sequence potentialValue, remove // any leading and trailing HTTP whitespace bytes from // potentialValue. + let i = 0; let j = potentialValue.length + + while (j > i && isHTTPWhiteSpaceCharCode(potentialValue.charCodeAt(j - 1))) --j + while (j > i && isHTTPWhiteSpaceCharCode(potentialValue.charCodeAt(i))) ++i - // Trimming the end with `.replace()` and a RegExp is typically subject to - // ReDoS. This is safer and faster. - let i = potentialValue.length - while (/[\r\n\t ]/.test(potentialValue.charAt(--i))); - return potentialValue.slice(0, i + 1).replace(/^[\r\n\t ]+/, '') + return i === 0 && j === potentialValue.length ? potentialValue : potentialValue.substring(i, j) } function fill (headers, object) { @@ -38,7 +45,8 @@ function fill (headers, object) { // 1. If object is a sequence, then for each header in object: // Note: webidl conversion to array has already been done. if (Array.isArray(object)) { - for (const header of object) { + for (let i = 0; i < object.length; ++i) { + const header = object[i] // 1. If header does not contain exactly two items, then throw a TypeError. if (header.length !== 2) { throw webidl.errors.exception({ @@ -48,15 +56,16 @@ function fill (headers, object) { } // 2. Append (header’s first item, header’s second item) to headers. - headers.append(header[0], header[1]) + appendHeader(headers, header[0], header[1]) } } else if (typeof object === 'object' && object !== null) { // Note: null should throw // 2. Otherwise, object is a record, then for each key → value in object, // append (key, value) to headers - for (const [key, value] of Object.entries(object)) { - headers.append(key, value) + const keys = Object.keys(object) + for (let i = 0; i < keys.length; ++i) { + appendHeader(headers, keys[i], object[keys[i]]) } } else { throw webidl.errors.conversionFailed({ @@ -67,6 +76,50 @@ function fill (headers, object) { } } +/** + * @see https://fetch.spec.whatwg.org/#concept-headers-append + */ +function appendHeader (headers, name, value) { + // 1. Normalize value. + value = headerValueNormalize(value) + + // 2. If name is not a header name or value is not a + // header value, then throw a TypeError. + if (!isValidHeaderName(name)) { + throw webidl.errors.invalidArgument({ + prefix: 'Headers.append', + value: name, + type: 'header name' + }) + } else if (!isValidHeaderValue(value)) { + throw webidl.errors.invalidArgument({ + prefix: 'Headers.append', + value, + type: 'header value' + }) + } + + // 3. If headers’s guard is "immutable", then throw a TypeError. + // 4. Otherwise, if headers’s guard is "request" and name is a + // forbidden header name, return. + // Note: undici does not implement forbidden header names + if (headers[kGuard] === 'immutable') { + throw new TypeError('immutable') + } else if (headers[kGuard] === 'request-no-cors') { + // 5. Otherwise, if headers’s guard is "request-no-cors": + // TODO + } + + // 6. Otherwise, if headers’s guard is "response" and name is a + // forbidden response-header name, return. + + // 7. Append (name, value) to headers’s header list. + return headers[kHeadersList].append(name, value) + + // 8. If headers’s guard is "request-no-cors", then remove + // privileged no-CORS request headers from headers +} + class HeadersList { /** @type {[string, string][]|null} */ cookies = null @@ -212,43 +265,7 @@ class Headers { name = webidl.converters.ByteString(name) value = webidl.converters.ByteString(value) - // 1. Normalize value. - value = headerValueNormalize(value) - - // 2. If name is not a header name or value is not a - // header value, then throw a TypeError. - if (!isValidHeaderName(name)) { - throw webidl.errors.invalidArgument({ - prefix: 'Headers.append', - value: name, - type: 'header name' - }) - } else if (!isValidHeaderValue(value)) { - throw webidl.errors.invalidArgument({ - prefix: 'Headers.append', - value, - type: 'header value' - }) - } - - // 3. If headers’s guard is "immutable", then throw a TypeError. - // 4. Otherwise, if headers’s guard is "request" and name is a - // forbidden header name, return. - // Note: undici does not implement forbidden header names - if (this[kGuard] === 'immutable') { - throw new TypeError('immutable') - } else if (this[kGuard] === 'request-no-cors') { - // 5. Otherwise, if headers’s guard is "request-no-cors": - // TODO - } - - // 6. Otherwise, if headers’s guard is "response" and name is a - // forbidden response-header name, return. - - // 7. Append (name, value) to headers’s header list. - // 8. If headers’s guard is "request-no-cors", then remove - // privileged no-CORS request headers from headers - return this[kHeadersList].append(name, value) + return appendHeader(this, name, value) } // https://fetch.spec.whatwg.org/#dom-headers-delete @@ -422,7 +439,8 @@ class Headers { const cookies = this[kHeadersList].cookies // 3. For each name of names: - for (const [name, value] of names) { + for (let i = 0; i < names.length; ++i) { + const [name, value] = names[i] // 1. If name is `set-cookie`, then: if (name === 'set-cookie') { // 1. Let values be a list of all values of headers in list whose name @@ -430,8 +448,8 @@ class Headers { // 2. For each value of values: // 1. Append (name, value) to headers. - for (const value of cookies) { - headers.push([name, value]) + for (let j = 0; j < cookies.length; ++j) { + headers.push([name, cookies[j]]) } } else { // 2. Otherwise: diff --git a/lib/fetch/webidl.js b/lib/fetch/webidl.js index 38a05e65759..6fcf2ab6ad4 100644 --- a/lib/fetch/webidl.js +++ b/lib/fetch/webidl.js @@ -427,12 +427,10 @@ webidl.converters.ByteString = function (V) { // 2. If the value of any element of x is greater than // 255, then throw a TypeError. for (let index = 0; index < x.length; index++) { - const charCode = x.charCodeAt(index) - - if (charCode > 255) { + if (x.charCodeAt(index) > 255) { throw new TypeError( 'Cannot convert argument to a ByteString because the character at ' + - `index ${index} has a value of ${charCode} which is greater than 255.` + `index ${index} has a value of ${x.charCodeAt(index)} which is greater than 255.` ) } } From a593323a225f35cced4742955f22a6ebd8f20bdb Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 8 Nov 2023 03:29:03 +0100 Subject: [PATCH 203/259] test: re-enable conditional WPT Report for websockets (#2407) --- test/wpt/start-websockets.mjs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/wpt/start-websockets.mjs b/test/wpt/start-websockets.mjs index ee364ecf6c5..616584d0f97 100644 --- a/test/wpt/start-websockets.mjs +++ b/test/wpt/start-websockets.mjs @@ -11,7 +11,10 @@ function isGlobalAvailable () { return true } - return process.execArgv.includes('--experimental-websocket') + const [nodeMajor] = process.versions.node.split('.').map(v => Number(v)) + + // TODO: keep this up to date when backports to earlier majors happen + return nodeMajor >= 21 } if (process.env.CI) { From 57fd3430b61b9f8a607447a9c85a711e5f6b7c34 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Wed, 8 Nov 2023 12:28:57 +0100 Subject: [PATCH 204/259] fix: delay abort on 'close' (#2408) Some legacy streams will incorrectly emit 'close' before 'end' in the successfull case. --- lib/client.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/client.js b/lib/client.js index 968a7f92071..ac2b104d276 100644 --- a/lib/client.js +++ b/lib/client.js @@ -1972,7 +1972,11 @@ function writeStream ({ h2stream, body, client, request, socket, contentLength, } } const onAbort = function () { - onFinished(new RequestAbortedError()) + if (finished) { + return + } + const err = new RequestAbortedError() + queueMicrotask(() => onFinished(err)) } const onFinished = function (err) { if (finished) { From b8483ab72142b75a7a764ae1c76a213b83cdc2fd Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Thu, 9 Nov 2023 00:17:07 +0900 Subject: [PATCH 205/259] refactor: use substring (#2411) --- lib/client.js | 2 +- lib/core/util.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/client.js b/lib/client.js index ac2b104d276..70001247553 100644 --- a/lib/client.js +++ b/lib/client.js @@ -1183,7 +1183,7 @@ async function connect (client) { const idx = hostname.indexOf(']') assert(idx !== -1) - const ip = hostname.substr(1, idx - 1) + const ip = hostname.substring(1, idx) assert(net.isIP(ip)) hostname = ip diff --git a/lib/core/util.js b/lib/core/util.js index 7fe2229c3c4..52c6b985883 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -125,13 +125,13 @@ function getHostname (host) { const idx = host.indexOf(']') assert(idx !== -1) - return host.substr(1, idx - 1) + return host.substring(1, idx) } const idx = host.indexOf(':') if (idx === -1) return host - return host.substr(0, idx) + return host.substring(0, idx) } // IP addresses are not valid server names per RFC6066 From 9c1ab5a63933a743a2da6697ee55eb4ae89c8517 Mon Sep 17 00:00:00 2001 From: Khafra Date: Fri, 10 Nov 2023 04:14:16 -0500 Subject: [PATCH 206/259] add additional http2 test with fetch (#2419) --- test/fetch/http2.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/fetch/http2.js b/test/fetch/http2.js index 32fb33e14eb..83860c8fefb 100644 --- a/test/fetch/http2.js +++ b/test/fetch/http2.js @@ -84,7 +84,7 @@ test('[Fetch] Simple GET with h2', async t => { stream.end(expectedRequestBody) }) - t.plan(3) + t.plan(4) server.listen() await once(server, 'listening') @@ -117,6 +117,9 @@ test('[Fetch] Simple GET with h2', async t => { t.equal(responseBody, expectedRequestBody) t.equal(response.headers.get('x-method'), 'GET') t.equal(response.headers.get('x-custom-h2'), 'foo') + + // See https://fetch.spec.whatwg.org/#concept-response-status-message + t.equal(response.statusText, '') }) test('[Fetch] Should handle h2 request with body (string or buffer)', async t => { From a1342a7918fb97a0970391adb81afd503459d475 Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Fri, 10 Nov 2023 18:14:28 +0900 Subject: [PATCH 207/259] fix: HTTPToken check (#2410) * fix: HTTPToken check * fix: lint * fix: h2 test * fix: h2 test * feat: Revert `header name check` * test: skip h2 --- lib/fetch/util.js | 71 +++++++++++++++++++++++-------------------- test/fetch/headers.js | 8 +++++ test/fetch/http2.js | 5 ++- 3 files changed, 50 insertions(+), 34 deletions(-) diff --git a/lib/fetch/util.js b/lib/fetch/util.js index 033fa206aed..bc6fd50c68a 100644 --- a/lib/fetch/util.js +++ b/lib/fetch/util.js @@ -103,52 +103,57 @@ function isValidReasonPhrase (statusText) { return true } -function isTokenChar (c) { - return !( - c >= 0x7f || - c <= 0x20 || - c === '(' || - c === ')' || - c === '<' || - c === '>' || - c === '@' || - c === ',' || - c === ';' || - c === ':' || - c === '\\' || - c === '"' || - c === '/' || - c === '[' || - c === ']' || - c === '?' || - c === '=' || - c === '{' || - c === '}' - ) +/** + * @see https://tools.ietf.org/html/rfc7230#section-3.2.6 + * @param {number} c + */ +function isTokenCharCode (c) { + switch (c) { + case 0x22: + case 0x28: + case 0x29: + case 0x2c: + case 0x2f: + case 0x3a: + case 0x3b: + case 0x3c: + case 0x3d: + case 0x3e: + case 0x3f: + case 0x40: + case 0x5b: + case 0x5c: + case 0x5d: + case 0x7b: + case 0x7d: + // DQUOTE and "(),/:;<=>?@[\]{}" + return false + default: + // VCHAR %x21-7E + return c >= 0x21 && c <= 0x7e + } } -// See RFC 7230, Section 3.2.6. -// https://github.com/chromium/chromium/blob/d7da0240cae77824d1eda25745c4022757499131/third_party/blink/renderer/platform/network/http_parsers.cc#L321 +/** + * @param {string} characters + */ function isValidHTTPToken (characters) { - if (!characters || typeof characters !== 'string') { + if (characters.length === 0) { return false } for (let i = 0; i < characters.length; ++i) { - const c = characters.charCodeAt(i) - if (c > 0x7f || !isTokenChar(c)) { + if (!isTokenCharCode(characters.charCodeAt(i))) { return false } } return true } -// https://fetch.spec.whatwg.org/#header-name -// https://github.com/chromium/chromium/blob/b3d37e6f94f87d59e44662d6078f6a12de845d17/net/http/http_util.cc#L342 +/** + * @see https://fetch.spec.whatwg.org/#header-name + * @param {string} potentialValue + */ function isValidHeaderName (potentialValue) { - if (potentialValue.length === 0) { - return false - } - return isValidHTTPToken(potentialValue) } diff --git a/test/fetch/headers.js b/test/fetch/headers.js index b91609dc383..c4b4e03d297 100644 --- a/test/fetch/headers.js +++ b/test/fetch/headers.js @@ -638,6 +638,14 @@ tap.test('request-no-cors guard', (t) => { }) tap.test('invalid headers', (t) => { + t.doesNotThrow(() => new Headers({ "abcdefghijklmnopqrstuvwxyz0123456789!#$%&'*+-.^_`|~": 'test' })) + + const chars = '"(),/:;<=>?@[\\]{}'.split('') + + for (const char of chars) { + t.throws(() => new Headers({ [char]: 'test' }), TypeError, `The string "${char}" should throw an error.`) + } + for (const byte of ['\r', '\n', '\t', ' ', String.fromCharCode(128), '']) { t.throws(() => { new Headers().set(byte, 'test') diff --git a/test/fetch/http2.js b/test/fetch/http2.js index 83860c8fefb..521c97bea97 100644 --- a/test/fetch/http2.js +++ b/test/fetch/http2.js @@ -6,13 +6,16 @@ const { once } = require('node:events') const { Blob } = require('node:buffer') const { Readable } = require('node:stream') -const { test, plan } = require('tap') +const { test, plan, skip } = require('tap') const pem = require('https-pem') const { Client, fetch } = require('../..') const nodeVersion = Number(process.version.split('v')[1].split('.')[0]) +skip('Skip H2 test due to pseudo-header issue.') +process.exit(0) + plan(6) test('[Fetch] Issue#2311', async t => { From a3af41c12b9c94dca659f8029d796a530a05eddd Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Sat, 11 Nov 2023 00:30:17 +0900 Subject: [PATCH 208/259] perf: optimize HeadersList.get (#2420) * perf: optimize HeadersList.get * simple --- lib/fetch/headers.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/fetch/headers.js b/lib/fetch/headers.js index bf17d690a91..bfc0b62f169 100644 --- a/lib/fetch/headers.js +++ b/lib/fetch/headers.js @@ -208,15 +208,13 @@ class HeadersList { // https://fetch.spec.whatwg.org/#concept-header-list-get get (name) { - // 1. If list does not contain name, then return null. - if (!this.contains(name)) { - return null - } + const value = this[kHeadersMap].get(name.toLowerCase()) + // 1. If list does not contain name, then return null. // 2. Return the values of all headers in list whose name // is a byte-case-insensitive match for name, // separated from each other by 0x2C 0x20, in order. - return this[kHeadersMap].get(name.toLowerCase())?.value ?? null + return value === undefined ? null : value.value } * [Symbol.iterator] () { From ecac429629c1b70638178ae3600f02e807f61cd6 Mon Sep 17 00:00:00 2001 From: Khafra Date: Fri, 10 Nov 2023 20:52:14 -0500 Subject: [PATCH 209/259] properly handle pseudo-headers in fetch (#2422) properly handle pseudo-headers in fetch --- lib/fetch/index.js | 6 +++--- test/fetch/http2.js | 11 ++++++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/fetch/index.js b/lib/fetch/index.js index 298b3ddb27c..2c68e97f64f 100644 --- a/lib/fetch/index.js +++ b/lib/fetch/index.js @@ -2002,7 +2002,7 @@ async function httpNetworkFetch ( location = val } - headers.append(key, val) + headers[kHeadersList].append(key, val) } } else { const keys = Object.keys(headersList) @@ -2016,7 +2016,7 @@ async function httpNetworkFetch ( location = val } - headers.append(key, val) + headers[kHeadersList].append(key, val) } } @@ -2120,7 +2120,7 @@ async function httpNetworkFetch ( const key = headersList[n + 0].toString('latin1') const val = headersList[n + 1].toString('latin1') - headers.append(key, val) + headers[kHeadersList].append(key, val) } resolve({ diff --git a/test/fetch/http2.js b/test/fetch/http2.js index 521c97bea97..9b3c95b8f1f 100644 --- a/test/fetch/http2.js +++ b/test/fetch/http2.js @@ -6,16 +6,13 @@ const { once } = require('node:events') const { Blob } = require('node:buffer') const { Readable } = require('node:stream') -const { test, plan, skip } = require('tap') +const { test, plan } = require('tap') const pem = require('https-pem') const { Client, fetch } = require('../..') const nodeVersion = Number(process.version.split('v')[1].split('.')[0]) -skip('Skip H2 test due to pseudo-header issue.') -process.exit(0) - plan(6) test('[Fetch] Issue#2311', async t => { @@ -87,7 +84,7 @@ test('[Fetch] Simple GET with h2', async t => { stream.end(expectedRequestBody) }) - t.plan(4) + t.plan(5) server.listen() await once(server, 'listening') @@ -120,6 +117,10 @@ test('[Fetch] Simple GET with h2', async t => { t.equal(responseBody, expectedRequestBody) t.equal(response.headers.get('x-method'), 'GET') t.equal(response.headers.get('x-custom-h2'), 'foo') + // https://github.com/nodejs/undici/issues/2415 + t.throws(() => { + response.headers.get(':status') + }, TypeError) // See https://fetch.spec.whatwg.org/#concept-response-status-message t.equal(response.statusText, '') From 987b92e3c7e9ab08d943fdebfbc6a13442a3af91 Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Sun, 12 Nov 2023 00:02:21 +0900 Subject: [PATCH 210/259] perf(headers): if the guard is immutable (#2424) --- lib/fetch/headers.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/lib/fetch/headers.js b/lib/fetch/headers.js index bfc0b62f169..f37c5c99e03 100644 --- a/lib/fetch/headers.js +++ b/lib/fetch/headers.js @@ -471,6 +471,12 @@ class Headers { keys () { webidl.brandCheck(this, Headers) + if (this[kGuard] === 'immutable') { + const value = this[kHeadersSortedMap] + return makeIterator(() => value, 'Headers', + 'key') + } + return makeIterator( () => [...this[kHeadersSortedMap].values()], 'Headers', @@ -481,6 +487,12 @@ class Headers { values () { webidl.brandCheck(this, Headers) + if (this[kGuard] === 'immutable') { + const value = this[kHeadersSortedMap] + return makeIterator(() => value, 'Headers', + 'value') + } + return makeIterator( () => [...this[kHeadersSortedMap].values()], 'Headers', @@ -491,6 +503,12 @@ class Headers { entries () { webidl.brandCheck(this, Headers) + if (this[kGuard] === 'immutable') { + const value = this[kHeadersSortedMap] + return makeIterator(() => value, 'Headers', + 'key+value') + } + return makeIterator( () => [...this[kHeadersSortedMap].values()], 'Headers', From 91ca13ccd065d50c47279e9fdac0c152464baee9 Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Sun, 12 Nov 2023 18:45:28 +0900 Subject: [PATCH 211/259] fix(mock-agent): send stream body (#2425) --- lib/fetch/index.js | 2 +- test/mock-agent.js | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/lib/fetch/index.js b/lib/fetch/index.js index 2c68e97f64f..c109a01bf1f 100644 --- a/lib/fetch/index.js +++ b/lib/fetch/index.js @@ -1957,7 +1957,7 @@ async function httpNetworkFetch ( path: url.pathname + url.search, origin: url.origin, method: request.method, - body: fetchParams.controller.dispatcher.isMockActive ? request.body && request.body.source : body, + body: fetchParams.controller.dispatcher.isMockActive ? request.body && (request.body.source || request.body.stream) : body, headers: request.headersList.entries, maxRedirections: 0, upgrade: request.mode === 'websocket' ? 'websocket' : undefined diff --git a/test/mock-agent.js b/test/mock-agent.js index d96a511a167..c9ffda443aa 100644 --- a/test/mock-agent.js +++ b/test/mock-agent.js @@ -2600,3 +2600,38 @@ test('MockAgent - headers should be array of strings', async (t) => { 'baz=qux' ]) }) + +// https://github.com/nodejs/undici/issues/2418 +test('MockAgent - Sending ReadableStream body', { skip: nodeMajor < 16 }, async (t) => { + t.plan(1) + const { fetch } = require('..') + const ReadableStream = globalThis.ReadableStream || require('stream/web').ReadableStream + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + + const server = createServer((req, res) => { + res.setHeader('content-type', 'text/plain') + req.pipe(res) + }) + + t.teardown(mockAgent.close.bind(mockAgent)) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const url = `http://localhost:${server.address().port}` + + const response = await fetch(url, { + method: 'POST', + body: new ReadableStream({ + start (controller) { + controller.enqueue('test') + controller.close() + } + }), + duplex: 'half' + }) + + t.same(await response.text(), 'test') +}) From 99932f152e3b1b8ac73486f0ecfbbd89fabf090b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Nov 2023 09:43:49 +0100 Subject: [PATCH 212/259] build(deps): bump github/codeql-action from 2.21.5 to 2.22.5 (#2394) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 6 +++--- .github/workflows/scorecard.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index fc1ff6aa985..0ef4cda43b5 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -50,7 +50,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@00e563ead9f72a8461b24876bee2d0c2e8bd2ee8 # v2.3.3 + uses: github/codeql-action/init@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.3.3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -60,7 +60,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@00e563ead9f72a8461b24876bee2d0c2e8bd2ee8 # v2.3.3 + uses: github/codeql-action/autobuild@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.3.3 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -73,6 +73,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@00e563ead9f72a8461b24876bee2d0c2e8bd2ee8 # v2.3.3 + uses: github/codeql-action/analyze@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.3.3 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 2b0055a6d7b..91688cfced3 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -51,6 +51,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@00e563ead9f72a8461b24876bee2d0c2e8bd2ee8 # v2.21.5 + uses: github/codeql-action/upload-sarif@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5 with: sarif_file: results.sarif From f996014708489a81f4c5f0bb803ebe2835b5f7de Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Mon, 13 Nov 2023 10:16:14 +0100 Subject: [PATCH 213/259] feat(#2264): Expose Retry Handler (#2281) * feat: initial implementation * feat: handle simple scenario * feat: enhance default retry * feat: enhance err * feat: add support for retry-after header * feat: add support for weak etag check * ts: adjust types * refactor: reduce magic * docs: add RetryAfter documentation * refactor: small adjustments * refactor: apply review suggestions * refactor: apply review * feat: set retry async * refactor: apply reviews --- docs/api/RetryHandler.md | 108 +++++++ index.js | 2 + lib/core/errors.js | 16 +- lib/core/symbols.js | 3 +- lib/core/util.js | 19 +- lib/handler/RetryHandler.js | 359 ++++++++++++++++++++++++ package.json | 1 + test/retry-handler.js | 544 ++++++++++++++++++++++++++++++++++++ types/index.d.ts | 4 +- types/retry-handler.d.ts | 116 ++++++++ 10 files changed, 1168 insertions(+), 4 deletions(-) create mode 100644 docs/api/RetryHandler.md create mode 100644 lib/handler/RetryHandler.js create mode 100644 test/retry-handler.js create mode 100644 types/retry-handler.d.ts diff --git a/docs/api/RetryHandler.md b/docs/api/RetryHandler.md new file mode 100644 index 00000000000..2323ce47911 --- /dev/null +++ b/docs/api/RetryHandler.md @@ -0,0 +1,108 @@ +# Class: RetryHandler + +Extends: `undici.DispatcherHandlers` + +A handler class that implements the retry logic for a request. + +## `new RetryHandler(dispatchOptions, retryHandlers, [retryOptions])` + +Arguments: + +- **options** `Dispatch.DispatchOptions & RetryOptions` (required) - It is an intersection of `Dispatcher.DispatchOptions` and `RetryOptions`. +- **retryHandlers** `RetryHandlers` (required) - Object containing the `dispatch` to be used on every retry, and `handler` for handling the `dispatch` lifecycle. + +Returns: `retryHandler` + +### Parameter: `Dispatch.DispatchOptions & RetryOptions` + +Extends: [`Dispatch.DispatchOptions`](Dispatcher.md#parameter-dispatchoptions). + +#### `RetryOptions` + +- **retry** `(err: Error, context: RetryContext, callback: (err?: Error | null) => void) => void` (optional) - Function to be called after every retry. It should pass error if no more retries should be performed. +- **maxRetries** `number` (optional) - Maximum number of retries. Default: `5` +- **maxTimeout** `number` (optional) - Maximum number of milliseconds to wait before retrying. Default: `30000` (30 seconds) +- **minTimeout** `number` (optional) - Minimum number of milliseconds to wait before retrying. Default: `500` (half a second) +- **timeoutFactor** `number` (optional) - Factor to multiply the timeout by for each retry attempt. Default: `2` +- **retryAfter** `boolean` (optional) - It enables automatic retry after the `Retry-After` header is received. Default: `true` +- +- **methods** `string[]` (optional) - Array of HTTP methods to retry. Default: `['GET', 'PUT', 'HEAD', 'OPTIONS', 'DELETE']` +- **statusCodes** `number[]` (optional) - Array of HTTP status codes to retry. Default: `[429, 500, 502, 503, 504]` +- **errorCodes** `string[]` (optional) - Array of Error codes to retry. Default: `['ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND', 'ENETDOWN','ENETUNREACH', 'EHOSTDOWN', + +**`RetryContext`** + +- `state`: `RetryState` - Current retry state. It can be mutated. +- `opts`: `Dispatch.DispatchOptions & RetryOptions` - Options passed to the retry handler. + +### Parameter `RetryHandlers` + +- **dispatch** `(options: Dispatch.DispatchOptions, handlers: Dispatch.DispatchHandlers) => Promise` (required) - Dispatch function to be called after every retry. +- **handler** Extends [`Dispatch.DispatchHandlers`](Dispatcher.md#dispatcherdispatchoptions-handler) (required) - Handler function to be called after the request is successful or the retries are exhausted. + +Examples: + +```js +const client = new Client(`http://localhost:${server.address().port}`); +const chunks = []; +const handler = new RetryHandler( + { + ...dispatchOptions, + retryOptions: { + // custom retry function + retry: function (err, state, callback) { + counter++; + + if (err.code && err.code === "UND_ERR_DESTROYED") { + callback(err); + return; + } + + if (err.statusCode === 206) { + callback(err); + return; + } + + setTimeout(() => callback(null), 1000); + }, + }, + }, + { + dispatch: (...args) => { + return client.dispatch(...args); + }, + handler: { + onConnect() {}, + onBodySent() {}, + onHeaders(status, _rawHeaders, resume, _statusMessage) { + // do something with headers + }, + onData(chunk) { + chunks.push(chunk); + return true; + }, + onComplete() {}, + onError() { + // handle error properly + }, + }, + } +); +``` + +#### Example - Basic RetryHandler with defaults + +```js +const client = new Client(`http://localhost:${server.address().port}`); +const handler = new RetryHandler(dispatchOptions, { + dispatch: client.dispatch.bind(client), + handler: { + onConnect() {}, + onBodySent() {}, + onHeaders(status, _rawHeaders, resume, _statusMessage) {}, + onData(chunk) {}, + onComplete() {}, + onError(err) {}, + }, +}); +``` diff --git a/index.js b/index.js index 7c0c8adcd6c..26302cc8efa 100644 --- a/index.js +++ b/index.js @@ -15,6 +15,7 @@ const MockAgent = require('./lib/mock/mock-agent') const MockPool = require('./lib/mock/mock-pool') const mockErrors = require('./lib/mock/mock-errors') const ProxyAgent = require('./lib/proxy-agent') +const RetryHandler = require('./lib/handler/RetryHandler') const { getGlobalDispatcher, setGlobalDispatcher } = require('./lib/global') const DecoratorHandler = require('./lib/handler/DecoratorHandler') const RedirectHandler = require('./lib/handler/RedirectHandler') @@ -36,6 +37,7 @@ module.exports.Pool = Pool module.exports.BalancedPool = BalancedPool module.exports.Agent = Agent module.exports.ProxyAgent = ProxyAgent +module.exports.RetryHandler = RetryHandler module.exports.DecoratorHandler = DecoratorHandler module.exports.RedirectHandler = RedirectHandler diff --git a/lib/core/errors.js b/lib/core/errors.js index 653782d9b5e..7af704b462a 100644 --- a/lib/core/errors.js +++ b/lib/core/errors.js @@ -193,6 +193,19 @@ class ResponseExceededMaxSizeError extends UndiciError { } } +class RequestRetryError extends UndiciError { + constructor (message, code, { headers, data }) { + super(message) + Error.captureStackTrace(this, RequestRetryError) + this.name = 'RequestRetryError' + this.message = message || 'Request retry error' + this.code = 'UND_ERR_REQ_RETRY' + this.statusCode = code + this.data = data + this.headers = headers + } +} + module.exports = { HTTPParserError, UndiciError, @@ -212,5 +225,6 @@ module.exports = { NotSupportedError, ResponseContentLengthMismatchError, BalancedPoolMissingUpstreamError, - ResponseExceededMaxSizeError + ResponseExceededMaxSizeError, + RequestRetryError } diff --git a/lib/core/symbols.js b/lib/core/symbols.js index c2492f4355f..1d5dc4e3db0 100644 --- a/lib/core/symbols.js +++ b/lib/core/symbols.js @@ -57,5 +57,6 @@ module.exports = { kHTTP2BuildRequest: Symbol('http2 build request'), kHTTP1BuildRequest: Symbol('http1 build request'), kHTTP2CopyHeaders: Symbol('http2 copy headers'), - kHTTPConnVersion: Symbol('http connection version') + kHTTPConnVersion: Symbol('http connection version'), + kRetryHandlerDefaultRetry: Symbol('retry agent default retry') } diff --git a/lib/core/util.js b/lib/core/util.js index 52c6b985883..8d5450ba0c0 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -455,6 +455,21 @@ function toUSVString (val) { return `${val}` } +// Parsed accordingly to RFC 9110 +// https://www.rfc-editor.org/rfc/rfc9110#field.content-range +function parseRangeHeader (range) { + if (range == null || range === '') return { start: 0, end: null, size: null } + + const m = range ? range.match(/^bytes (\d+)-(\d+)\/(\d+)?$/) : null + return m + ? { + start: parseInt(m[1]), + end: m[2] ? parseInt(m[2]) : null, + size: m[3] ? parseInt(m[3]) : null + } + : null +} + const kEnumerableProperty = Object.create(null) kEnumerableProperty.enumerable = true @@ -488,7 +503,9 @@ module.exports = { buildURL, throwIfAborted, addAbortListener, + parseRangeHeader, nodeMajor, nodeMinor, - nodeHasAutoSelectFamily: nodeMajor > 18 || (nodeMajor === 18 && nodeMinor >= 13) + nodeHasAutoSelectFamily: nodeMajor > 18 || (nodeMajor === 18 && nodeMinor >= 13), + safeHTTPMethods: ['GET', 'HEAD', 'OPTIONS', 'TRACE'] } diff --git a/lib/handler/RetryHandler.js b/lib/handler/RetryHandler.js new file mode 100644 index 00000000000..5819c7a9b13 --- /dev/null +++ b/lib/handler/RetryHandler.js @@ -0,0 +1,359 @@ +const assert = require('node:assert') + +const { kRetryHandlerDefaultRetry } = require('../core/symbols') +const { RequestRetryError } = require('../core/errors') +const { isDisturbed, parseHeaders, parseRangeHeader } = require('../core/util') + +function calculateRetryAfterHeader (retryAfter) { + const current = Date.now() + const diff = new Date(retryAfter).getTime() - current + + return diff +} + +class RetryHandler { + constructor (opts, handlers) { + const { retryOptions, ...dispatchOpts } = opts + const { + // Retry scoped + retry: retryFn, + maxRetries, + maxTimeout, + minTimeout, + timeoutFactor, + // Response scoped + methods, + errorCodes, + retryAfter, + statusCodes + } = retryOptions ?? {} + + this.dispatch = handlers.dispatch + this.handler = handlers.handler + this.opts = dispatchOpts + this.abort = null + this.aborted = false + this.retryOpts = { + retry: retryFn ?? RetryHandler[kRetryHandlerDefaultRetry], + retryAfter: retryAfter ?? true, + maxTimeout: maxTimeout ?? 30 * 1000, // 30s, + timeout: minTimeout ?? 500, // .5s + timeoutFactor: timeoutFactor ?? 2, + maxRetries: maxRetries ?? 5, + // What errors we should retry + methods: methods ?? ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE', 'TRACE'], + // Indicates which errors to retry + statusCodes: statusCodes ?? [500, 502, 503, 504, 429], + // List of errors to retry + errorCodes: errorCodes ?? [ + 'ECONNRESET', + 'ECONNREFUSED', + 'ENOTFOUND', + 'ENETDOWN', + 'ENETUNREACH', + 'EHOSTDOWN', + 'EHOSTUNREACH', + 'EPIPE' + ] + } + + this.retryCount = 0 + this.start = 0 + this.end = null + this.etag = null + this.resume = null + + // Handle possible onConnect duplication + this.handler.onConnect(reason => { + this.aborted = true + if (this.abort) { + this.abort(reason) + } else { + this.reason = reason + } + }) + } + + onRequestSent () { + if (this.handler.onRequestSent) { + this.handler.onRequestSent() + } + } + + onUpgrade (statusCode, headers, socket) { + if (this.handler.onUpgrade) { + this.handler.onUpgrade(statusCode, headers, socket) + } + } + + onConnect (abort) { + if (this.aborted) { + abort(this.reason) + } else { + this.abort = abort + } + } + + onBodySent (chunk) { + return this.handler.onBodySent(chunk) + } + + static [kRetryHandlerDefaultRetry] (err, { state, opts }, cb) { + const { statusCode, code, headers } = err + const { method, retryOptions } = opts + const { + maxRetries, + timeout, + maxTimeout, + timeoutFactor, + statusCodes, + errorCodes, + methods + } = retryOptions + let { counter, currentTimeout } = state + + currentTimeout = + currentTimeout != null && currentTimeout > 0 ? currentTimeout : timeout + + // Any code that is not a Undici's originated and allowed to retry + if ( + code && + code !== 'UND_ERR_REQ_RETRY' && + code !== 'UND_ERR_SOCKET' && + !errorCodes.includes(code) + ) { + cb(err) + return + } + + // If a set of method are provided and the current method is not in the list + if (Array.isArray(methods) && !methods.includes(method)) { + cb(err) + return + } + + // If a set of status code are provided and the current status code is not in the list + if ( + statusCode != null && + Array.isArray(statusCodes) && + !statusCodes.includes(statusCode) + ) { + cb(err) + return + } + + // If we reached the max number of retries + if (counter > maxRetries) { + cb(err) + return + } + + let retryAfterHeader = headers != null && headers['retry-after'] + if (retryAfterHeader) { + retryAfterHeader = Number(retryAfterHeader) + retryAfterHeader = isNaN(retryAfterHeader) + ? calculateRetryAfterHeader(retryAfterHeader) + : retryAfterHeader * 1e3 // Retry-After is in seconds + } + + const retryTimeout = + retryAfterHeader > 0 + ? Math.min(retryAfterHeader, maxTimeout) + : Math.min(currentTimeout * timeoutFactor ** counter, maxTimeout) + + state.currentTimeout = retryTimeout + + setTimeout(() => cb(null), retryTimeout) + } + + onHeaders (statusCode, rawHeaders, resume, statusMessage) { + const headers = parseHeaders(rawHeaders) + + this.retryCount += 1 + + if (statusCode >= 300) { + this.abort( + new RequestRetryError('Request failed', statusCode, { + headers, + count: this.retryCount + }) + ) + return false + } + + // Checkpoint for resume from where we left it + if (this.resume != null) { + this.resume = null + + if (statusCode !== 206) { + return true + } + + const contentRange = parseRangeHeader(headers['content-range']) + // If no content range + if (!contentRange) { + this.abort( + new RequestRetryError('Content-Range mismatch', statusCode, { + headers, + count: this.retryCount + }) + ) + return false + } + + // Let's start with a weak etag check + if (this.etag != null && this.etag !== headers.etag) { + this.abort( + new RequestRetryError('ETag mismatch', statusCode, { + headers, + count: this.retryCount + }) + ) + return false + } + + const { start, size, end = size } = contentRange + + assert(this.start === start, 'content-range mismatch') + assert(this.end == null || this.end === end, 'content-range mismatch') + + this.resume = resume + return true + } + + if (this.end == null) { + if (statusCode === 206) { + // First time we receive 206 + const range = parseRangeHeader(headers['content-range']) + + if (range == null) { + return this.handler.onHeaders( + statusCode, + rawHeaders, + resume, + statusMessage + ) + } + + const { start, size, end = size } = range + + assert( + start != null && Number.isFinite(start) && this.start !== start, + 'content-range mismatch' + ) + assert(Number.isFinite(start)) + assert( + end != null && Number.isFinite(end) && this.end !== end, + 'invalid content-length' + ) + + this.start = start + this.end = end + } + + // We make our best to checkpoint the body for further range headers + if (this.end == null) { + const contentLength = headers['content-length'] + this.end = contentLength != null ? Number(contentLength) : null + } + + assert(Number.isFinite(this.start)) + assert( + this.end == null || Number.isFinite(this.end), + 'invalid content-length' + ) + + this.resume = resume + this.etag = headers.etag != null ? headers.etag : null + + return this.handler.onHeaders( + statusCode, + rawHeaders, + resume, + statusMessage + ) + } + + const err = new RequestRetryError('Request failed', statusCode, { + headers, + count: this.retryCount + }) + // Meant to just indicate weather or not trigger a retry + // It should return null to indicate we exhausted the retries + const retryAfter = this.retryOpts.retry( + err, + { counter: this.retryCount, currentTimeout: this.retryAfter }, + { retry: this.retryOpts, ...this.opts } + ) + + assert( + retryAfter == null || Number.isFinite(retryAfter), + 'invalid retryAfter' + ) + + if (retryAfter == null) { + return this.handler.onHeaders( + statusCode, + rawHeaders, + resume, + statusMessage + ) + } + + this.retryAfter = retryAfter + + this.abort(err) + + return false + } + + onData (chunk) { + this.start += chunk.length + + return this.handler.onData(chunk) + } + + onComplete (rawTrailers) { + this.retryCount = 0 + return this.handler.onComplete(rawTrailers) + } + + onError (err) { + if (this.aborted || isDisturbed(this.opts.body)) { + return this.handler.onError(err) + } + + this.retryOpts.retry( + err, + { + state: { counter: this.retryCount++, currentTimeout: this.retryAfter }, + opts: { retryOptions: this.retryOpts, ...this.opts } + }, + onRetry.bind(this) + ) + + function onRetry (err) { + if (err != null || this.aborted || isDisturbed(this.opts.body)) { + return this.handler.onError(err) + } + + if (this.start !== 0) { + this.opts = { + ...this.opts, + headers: { + ...this.opts.headers, + range: `bytes=${this.start}-${this.end ?? ''}` + } + } + } + + try { + this.dispatch(this.opts, this) + } catch (err) { + this.handler.onError(err) + } + } + } +} + +module.exports = RetryHandler diff --git a/package.json b/package.json index ea1228f94b9..91f71169a91 100644 --- a/package.json +++ b/package.json @@ -118,6 +118,7 @@ "jsdom": "^22.1.0", "jsfuzz": "^1.0.15", "mocha": "^10.0.0", + "mockttp": "^3.9.2", "p-timeout": "^3.2.0", "pre-commit": "^1.2.2", "proxy": "^1.0.2", diff --git a/test/retry-handler.js b/test/retry-handler.js new file mode 100644 index 00000000000..ef2c47eeb33 --- /dev/null +++ b/test/retry-handler.js @@ -0,0 +1,544 @@ +'use strict' +const { createServer } = require('node:http') +const { once } = require('node:events') + +const tap = require('tap') + +const { RetryHandler, Client } = require('..') + +tap.test('Should retry status code', t => { + let counter = 0 + const chunks = [] + const server = createServer() + const dispatchOptions = { + retryOptions: { + retry: (err, { state, opts }, done) => { + counter++ + + if ( + err.statusCode === 500 || + err.message.includes('other side closed') + ) { + setTimeout(done, 500) + return + } + + return done(err) + } + }, + method: 'GET', + path: '/', + headers: { + 'content-type': 'application/json' + } + } + + t.plan(4) + + server.on('request', (req, res) => { + switch (counter) { + case 0: + req.destroy() + return + case 1: + res.writeHead(500) + res.end('failed') + return + case 2: + res.writeHead(200) + res.end('hello world!') + return + default: + t.fail() + } + }) + + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`) + const handler = new RetryHandler(dispatchOptions, { + dispatch: client.dispatch.bind(client), + handler: { + onConnect () { + t.pass() + }, + onBodySent () { + t.pass() + }, + onHeaders (status, _rawHeaders, resume, _statusMessage) { + t.equal(status, 200) + return true + }, + onData (chunk) { + chunks.push(chunk) + return true + }, + onComplete () { + t.equal(Buffer.concat(chunks).toString('utf-8'), 'hello world!') + t.equal(counter, 2) + }, + onError () { + t.fail() + } + } + }) + + t.teardown(async () => { + await client.close() + server.close() + + await once(server, 'close') + }) + + client.dispatch( + { + method: 'GET', + path: '/', + headers: { + 'content-type': 'application/json' + } + }, + handler + ) + }) +}) + +tap.test('Should use retry-after header for retries', t => { + let counter = 0 + const chunks = [] + const server = createServer() + let checkpoint + const dispatchOptions = { + method: 'PUT', + path: '/', + headers: { + 'content-type': 'application/json' + } + } + + t.plan(4) + + server.on('request', (req, res) => { + switch (counter) { + case 0: + res.writeHead(429, { + 'retry-after': 1 + }) + res.end('rate limit') + checkpoint = Date.now() + counter++ + return + case 1: + res.writeHead(200) + res.end('hello world!') + t.ok(Date.now() - checkpoint >= 500) + counter++ + return + default: + t.fail('unexpected request') + } + }) + + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`) + const handler = new RetryHandler(dispatchOptions, { + dispatch: client.dispatch.bind(client), + handler: { + onConnect () { + t.pass() + }, + onBodySent () { + t.pass() + }, + onHeaders (status, _rawHeaders, resume, _statusMessage) { + t.equal(status, 200) + return true + }, + onData (chunk) { + chunks.push(chunk) + return true + }, + onComplete () { + t.equal(Buffer.concat(chunks).toString('utf-8'), 'hello world!') + }, + onError (err) { + t.error(err) + } + } + }) + + t.teardown(async () => { + await client.close() + server.close() + + await once(server, 'close') + }) + + client.dispatch( + { + method: 'PUT', + path: '/', + headers: { + 'content-type': 'application/json' + } + }, + handler + ) + }) +}) + +tap.test('Should use retry-after header for retries (date)', t => { + let counter = 0 + const chunks = [] + const server = createServer() + let checkpoint + const dispatchOptions = { + method: 'PUT', + path: '/', + headers: { + 'content-type': 'application/json' + } + } + + t.plan(4) + + server.on('request', (req, res) => { + switch (counter) { + case 0: + res.writeHead(429, { + 'retry-after': new Date( + new Date().setSeconds(new Date().getSeconds() + 1) + ).toUTCString() + }) + res.end('rate limit') + checkpoint = Date.now() + counter++ + return + case 1: + res.writeHead(200) + res.end('hello world!') + t.ok(Date.now() - checkpoint >= 1) + counter++ + return + default: + t.fail('unexpected request') + } + }) + + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`) + const handler = new RetryHandler(dispatchOptions, { + dispatch: client.dispatch.bind(client), + handler: { + onConnect () { + t.pass() + }, + onBodySent () { + t.pass() + }, + onHeaders (status, _rawHeaders, resume, _statusMessage) { + t.equal(status, 200) + return true + }, + onData (chunk) { + chunks.push(chunk) + return true + }, + onComplete () { + t.equal(Buffer.concat(chunks).toString('utf-8'), 'hello world!') + }, + onError (err) { + t.error(err) + } + } + }) + + t.teardown(async () => { + await client.close() + server.close() + + await once(server, 'close') + }) + + client.dispatch( + { + method: 'PUT', + path: '/', + headers: { + 'content-type': 'application/json' + } + }, + handler + ) + }) +}) + +tap.test('Should retry with defaults', t => { + let counter = 0 + const chunks = [] + const server = createServer() + const dispatchOptions = { + method: 'GET', + path: '/', + headers: { + 'content-type': 'application/json' + } + } + + server.on('request', (req, res) => { + switch (counter) { + case 0: + req.destroy() + counter++ + return + case 1: + res.writeHead(500) + res.end('failed') + counter++ + return + case 2: + res.writeHead(200) + res.end('hello world!') + counter++ + return + default: + t.fail() + } + }) + + t.plan(3) + + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`) + const handler = new RetryHandler(dispatchOptions, { + dispatch: client.dispatch.bind(client), + handler: { + onConnect () { + t.pass() + }, + onBodySent () { + t.pass() + }, + onHeaders (status, _rawHeaders, resume, _statusMessage) { + t.equal(status, 200) + return true + }, + onData (chunk) { + chunks.push(chunk) + return true + }, + onComplete () { + t.equal(Buffer.concat(chunks).toString('utf-8'), 'hello world!') + }, + onError (err) { + t.error(err) + } + } + }) + + t.teardown(async () => { + await client.close() + server.close() + + await once(server, 'close') + }) + + client.dispatch( + { + method: 'GET', + path: '/', + headers: { + 'content-type': 'application/json' + } + }, + handler + ) + }) +}) + +tap.test('Should handle 206 partial content', t => { + const chunks = [] + let counter = 0 + + // Took from: https://github.com/nxtedition/nxt-lib/blob/4b001ebc2f22cf735a398f35ff800dd553fe5933/test/undici/retry.js#L47 + let x = 0 + const server = createServer((req, res) => { + if (x === 0) { + t.pass() + res.setHeader('etag', 'asd') + res.write('abc') + setTimeout(() => { + res.destroy() + }, 1e2) + } else if (x === 1) { + t.same(req.headers.range, 'bytes=3-') + res.setHeader('content-range', 'bytes 3-6/6') + res.setHeader('etag', 'asd') + res.statusCode = 206 + res.end('def') + } + x++ + }) + + const dispatchOptions = { + retryOptions: { + retry: function (err, _, done) { + counter++ + + if (err.code && err.code === 'UND_ERR_DESTROYED') { + return done(false) + } + + if (err.statusCode === 206) return done(err) + + setTimeout(done, 800) + } + }, + method: 'GET', + path: '/', + headers: { + 'content-type': 'application/json' + } + } + + t.plan(8) + + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`) + const handler = new RetryHandler(dispatchOptions, { + dispatch: (...args) => { + return client.dispatch(...args) + }, + handler: { + onRequestSent () { + t.pass() + }, + onConnect () { + t.pass() + }, + onBodySent () { + t.pass() + }, + onHeaders (status, _rawHeaders, resume, _statusMessage) { + t.equal(status, 200) + return true + }, + onData (chunk) { + chunks.push(chunk) + return true + }, + onComplete () { + t.equal(Buffer.concat(chunks).toString('utf-8'), 'abcdef') + t.equal(counter, 1) + }, + onError () { + t.fail() + } + } + }) + + client.dispatch( + { + method: 'GET', + path: '/', + headers: { + 'content-type': 'application/json' + } + }, + handler + ) + + t.teardown(async () => { + await client.close() + + server.close() + await once(server, 'close') + }) + }) +}) + +tap.test('Should handle 206 partial content - bad-etag', t => { + const chunks = [] + + // Took from: https://github.com/nxtedition/nxt-lib/blob/4b001ebc2f22cf735a398f35ff800dd553fe5933/test/undici/retry.js#L47 + let x = 0 + const server = createServer((req, res) => { + if (x === 0) { + t.pass() + res.setHeader('etag', 'asd') + res.write('abc') + setTimeout(() => { + res.destroy() + }, 1e2) + } else if (x === 1) { + t.same(req.headers.range, 'bytes=3-') + res.setHeader('content-range', 'bytes 3-6/6') + res.setHeader('etag', 'erwsd') + res.statusCode = 206 + res.end('def') + } + x++ + }) + + const dispatchOptions = { + method: 'GET', + path: '/', + headers: { + 'content-type': 'application/json' + } + } + + t.plan(6) + + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`) + const handler = new RetryHandler( + dispatchOptions, + { + dispatch: (...args) => { + return client.dispatch(...args) + }, + handler: { + onConnect () { + t.pass() + }, + onBodySent () { + t.pass() + }, + onHeaders (status, _rawHeaders, resume, _statusMessage) { + t.pass() + return true + }, + onData (chunk) { + chunks.push(chunk) + return true + }, + onComplete () { + t.error('should not complete') + }, + onError (err) { + t.equal(Buffer.concat(chunks).toString('utf-8'), 'abc') + t.equal(err.code, 'UND_ERR_REQ_RETRY') + } + } + } + ) + + client.dispatch( + { + method: 'GET', + path: '/', + headers: { + 'content-type': 'application/json' + } + }, + handler + ) + + t.teardown(async () => { + await client.close() + + server.close() + await once(server, 'close') + }) + }) +}) diff --git a/types/index.d.ts b/types/index.d.ts index 4589845b4a9..0ea8bdc217d 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -14,6 +14,7 @@ import MockPool from'./mock-pool' import MockAgent from'./mock-agent' import mockErrors from'./mock-errors' import ProxyAgent from'./proxy-agent' +import RetryHandler from'./retry-handler' import { request, pipeline, stream, connect, upgrade } from './api' export * from './cookies' @@ -27,7 +28,7 @@ export * from './content-type' export * from './cache' export { Interceptable } from './mock-interceptor' -export { Dispatcher, BalancedPool, Pool, Client, buildConnector, errors, Agent, request, stream, pipeline, connect, upgrade, setGlobalDispatcher, getGlobalDispatcher, setGlobalOrigin, getGlobalOrigin, MockClient, MockPool, MockAgent, mockErrors, ProxyAgent, RedirectHandler, DecoratorHandler } +export { Dispatcher, BalancedPool, Pool, Client, buildConnector, errors, Agent, request, stream, pipeline, connect, upgrade, setGlobalDispatcher, getGlobalDispatcher, setGlobalOrigin, getGlobalOrigin, MockClient, MockPool, MockAgent, mockErrors, ProxyAgent, RedirectHandler, DecoratorHandler, RetryHandler } export default Undici declare namespace Undici { @@ -35,6 +36,7 @@ declare namespace Undici { var Pool: typeof import('./pool').default; var RedirectHandler: typeof import ('./handlers').RedirectHandler var DecoratorHandler: typeof import ('./handlers').DecoratorHandler + var RetryHandler: typeof import ('./retry-handler').default var createRedirectInterceptor: typeof import ('./interceptors').createRedirectInterceptor var BalancedPool: typeof import('./balanced-pool').default; var Client: typeof import('./client').default; diff --git a/types/retry-handler.d.ts b/types/retry-handler.d.ts new file mode 100644 index 00000000000..0528eb44279 --- /dev/null +++ b/types/retry-handler.d.ts @@ -0,0 +1,116 @@ +import Dispatcher from "./dispatcher"; + +export default RetryHandler; + +declare class RetryHandler implements Dispatcher.DispatchHandlers { + constructor( + options: Dispatcher.DispatchOptions & { + retryOptions?: RetryHandler.RetryOptions; + }, + retryHandlers: RetryHandler.RetryHandlers + ); +} + +declare namespace RetryHandler { + export type RetryState = { counter: number; currentTimeout: number }; + + export type RetryContext = { + state: RetryState; + opts: Dispatcher.DispatchOptions & { + retryOptions?: RetryHandler.RetryOptions; + }; + } + + export type OnRetryCallback = (result?: Error | null) => void; + + export type RetryCallback = ( + err: Error, + context: { + state: RetryState; + opts: Dispatcher.DispatchOptions & { + retryOptions?: RetryHandler.RetryOptions; + }; + }, + callback: OnRetryCallback + ) => number | null; + + export interface RetryOptions { + /** + * Callback to be invoked on every retry iteration. + * It receives the error, current state of the retry object and the options object + * passed when instantiating the retry handler. + * + * @type {RetryCallback} + * @memberof RetryOptions + */ + retry?: RetryCallback; + /** + * Maximum number of retries to allow. + * + * @type {number} + * @memberof RetryOptions + * @default 5 + */ + maxRetries?: number; + /** + * Max number of milliseconds allow between retries + * + * @type {number} + * @memberof RetryOptions + * @default 30000 + */ + maxTimeout?: number; + /** + * Initial number of milliseconds to wait before retrying for the first time. + * + * @type {number} + * @memberof RetryOptions + * @default 500 + */ + minTimeout?: number; + /** + * Factior to multiply the timeout factor between retries. + * + * @type {number} + * @memberof RetryOptions + * @default 2 + */ + timeoutFactor?: number; + /** + * It enables to automatically infer timeout between retries based on the `Retry-After` header. + * + * @type {boolean} + * @memberof RetryOptions + * @default true + */ + retryAfter?: boolean; + /** + * HTTP methods to retry. + * + * @type {Dispatcher.HttpMethod[]} + * @memberof RetryOptions + * @default ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE', 'TRACE'], + */ + methods?: Dispatcher.HttpMethod[]; + /** + * Error codes to be retried. e.g. `ECONNRESET`, `ENOTFOUND`, `ETIMEDOUT`, `ECONNREFUSED`, etc. + * + * @type {string[]} + * @default ['ECONNRESET','ECONNREFUSED','ENOTFOUND','ENETDOWN','ENETUNREACH','EHOSTDOWN','EHOSTUNREACH','EPIPE'] + */ + errorCodes?: string[]; + /** + * HTTP status codes to be retried. + * + * @type {number[]} + * @memberof RetryOptions + * @default [500, 502, 503, 504, 429], + */ + statusCodes?: number[]; + } + + export interface RetryHandlers { + dispatch: Dispatcher["dispatch"]; + handler: Dispatcher.DispatchHandlers; + } +} From 5555e5e8e0b286ad65e91b9c85050912e83b9a05 Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Tue, 14 Nov 2023 09:04:18 +0900 Subject: [PATCH 214/259] fix: implement `Headers#set` correctly (#2432) * fix: implement `Headers#set` correctly * fix: HeadersList * test: add * test: fix --- lib/fetch/headers.js | 4 ++-- test/fetch/headers.js | 12 +++++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/fetch/headers.js b/lib/fetch/headers.js index f37c5c99e03..035560428bc 100644 --- a/lib/fetch/headers.js +++ b/lib/fetch/headers.js @@ -190,7 +190,7 @@ class HeadersList { // the first such header to value and remove the // others. // 2. Otherwise, append header (name, value) to list. - return this[kHeadersMap].set(lowercaseName, { name, value }) + this[kHeadersMap].set(lowercaseName, { name, value }) } // https://fetch.spec.whatwg.org/#concept-header-list-delete @@ -401,7 +401,7 @@ class Headers { // 7. Set (name, value) in this’s header list. // 8. If this’s guard is "request-no-cors", then remove // privileged no-CORS request headers from this - return this[kHeadersList].set(name, value) + this[kHeadersList].set(name, value) } // https://fetch.spec.whatwg.org/#dom-headers-getsetcookie diff --git a/test/fetch/headers.js b/test/fetch/headers.js index c4b4e03d297..5da1a1ded1f 100644 --- a/test/fetch/headers.js +++ b/test/fetch/headers.js @@ -250,7 +250,7 @@ tap.test('Headers has', t => { }) tap.test('Headers set', t => { - t.plan(4) + t.plan(5) t.test('sets valid header entry to instance', t => { t.plan(2) @@ -294,6 +294,16 @@ tap.test('Headers set', t => { t.throws(() => headers.set('undici'), 'throws on missing value') t.throws(() => headers.set('invalid @ header ? name', 'valid value'), 'throws on invalid name') }) + + // https://github.com/nodejs/undici/issues/2431 + t.test('`Headers#set` returns undefined', t => { + t.plan(2) + const headers = new Headers() + + t.same(headers.set('a', 'b'), undefined) + + t.notOk(headers.set('c', 'd') instanceof Map) + }) }) tap.test('Headers forEach', t => { From ba4ca327843de62a83c1f9c32acc303bd6b8545f Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Tue, 14 Nov 2023 09:04:40 +0900 Subject: [PATCH 215/259] fix: implement `Headers#delete` correctly (#2430) * fix: implement `Headers#delete` correctly * fix: HeadersList * test: add * test: add comment * revert --- lib/fetch/headers.js | 4 ++-- test/fetch/headers.js | 11 ++++++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/fetch/headers.js b/lib/fetch/headers.js index 035560428bc..cfdc8868efa 100644 --- a/lib/fetch/headers.js +++ b/lib/fetch/headers.js @@ -203,7 +203,7 @@ class HeadersList { this.cookies = null } - return this[kHeadersMap].delete(name) + this[kHeadersMap].delete(name) } // https://fetch.spec.whatwg.org/#concept-header-list-get @@ -308,7 +308,7 @@ class Headers { // 7. Delete name from this’s header list. // 8. If this’s guard is "request-no-cors", then remove // privileged no-CORS request headers from this. - return this[kHeadersList].delete(name) + this[kHeadersList].delete(name) } // https://fetch.spec.whatwg.org/#dom-headers-get diff --git a/test/fetch/headers.js b/test/fetch/headers.js index 5da1a1ded1f..48461103d78 100644 --- a/test/fetch/headers.js +++ b/test/fetch/headers.js @@ -160,7 +160,7 @@ tap.test('Headers append', t => { }) tap.test('Headers delete', t => { - t.plan(3) + t.plan(4) t.test('deletes valid header entry from instance', t => { t.plan(3) @@ -193,6 +193,15 @@ tap.test('Headers delete', t => { t.throws(() => headers.delete(), 'throws on missing namee') t.throws(() => headers.delete('invalid @ header ? name'), 'throws on invalid name') }) + + // https://github.com/nodejs/undici/issues/2429 + t.test('`Headers#delete` returns undefined', t => { + t.plan(2) + const headers = new Headers({ test: 'test' }) + + t.same(headers.delete('test'), undefined) + t.same(headers.delete('test2'), undefined) + }) }) tap.test('Headers get', t => { From b37bbf7426cfa3cbf7cbe1039800c3dc35733ba8 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 15 Nov 2023 17:14:43 +0100 Subject: [PATCH 216/259] test: update websocket wpt availability (#2437) --- test/wpt/start-websockets.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/wpt/start-websockets.mjs b/test/wpt/start-websockets.mjs index 616584d0f97..79aa297b265 100644 --- a/test/wpt/start-websockets.mjs +++ b/test/wpt/start-websockets.mjs @@ -11,10 +11,10 @@ function isGlobalAvailable () { return true } - const [nodeMajor] = process.versions.node.split('.').map(v => Number(v)) + const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(v => Number(v)) // TODO: keep this up to date when backports to earlier majors happen - return nodeMajor >= 21 + return nodeMajor >= 21 || (nodeMajor === 20 && nodeMinor >= 10) } if (process.env.CI) { From 42c05a84e01ddd7290aa24b939f09a8a13614dae Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Sun, 19 Nov 2023 08:00:31 +0900 Subject: [PATCH 217/259] fix: type comment position (#2443) --- lib/client.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/client.js b/lib/client.js index 70001247553..e002ee364a3 100644 --- a/lib/client.js +++ b/lib/client.js @@ -1682,6 +1682,7 @@ function writeH2 (client, session, request) { return false } + /** @type {import('node:http2').ClientHttp2Stream} */ let stream const h2State = client[kHTTP2SessionState] @@ -1777,14 +1778,10 @@ function writeH2 (client, session, request) { const shouldEndStream = method === 'GET' || method === 'HEAD' if (expectContinue) { headers[HTTP2_HEADER_EXPECT] = '100-continue' - /** - * @type {import('node:http2').ClientHttp2Stream} - */ stream = session.request(headers, { endStream: shouldEndStream, signal }) stream.once('continue', writeBodyH2) } else { - /** @type {import('node:http2').ClientHttp2Stream} */ stream = session.request(headers, { endStream: shouldEndStream, signal From 6631655b4617bd4f9954347b0e99b3ba77534cf1 Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Sun, 19 Nov 2023 16:09:14 +0900 Subject: [PATCH 218/259] fix: `onHeaders` type declaration (#2444) --- types/dispatcher.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/dispatcher.d.ts b/types/dispatcher.d.ts index 816db19d20d..efc53eea791 100644 --- a/types/dispatcher.d.ts +++ b/types/dispatcher.d.ts @@ -211,7 +211,7 @@ declare namespace Dispatcher { /** Invoked when request is upgraded either due to a `Upgrade` header or `CONNECT` method. */ onUpgrade?(statusCode: number, headers: Buffer[] | string[] | null, socket: Duplex): void; /** Invoked when statusCode and headers have been received. May be invoked multiple times due to 1xx informational headers. */ - onHeaders?(statusCode: number, headers: Buffer[] | string[] | null, resume: () => void): boolean; + onHeaders?(statusCode: number, headers: Buffer[] | string[] | null, resume: () => void, statusText: string): boolean; /** Invoked when response payload data is received. */ onData?(chunk: Buffer): boolean; /** Invoked when response payload and trailers have been received and the request has completed. */ From 90831aecb914fdcd009b6dbbb0a38425ed27ff46 Mon Sep 17 00:00:00 2001 From: Khafra Date: Sun, 19 Nov 2023 12:07:02 -0500 Subject: [PATCH 219/259] remove http2 status pseudo header from headers (#2438) * remove http2 status pseudo header from headers Co-authored-by: tsctx <91457664+tsctx@users.noreply.github.com> fixup * add test Co-authored-by: tsctx <91457664+tsctx@users.noreply.github.com> --------- Co-authored-by: tsctx <91457664+tsctx@users.noreply.github.com> --- lib/client.js | 4 +++- test/fetch/http2.js | 42 ++++++++++++++++++++++++++++++++++++++++-- test/http2.js | 37 ++++++++++++++++++++++++++++++++++++- 3 files changed, 79 insertions(+), 4 deletions(-) diff --git a/lib/client.js b/lib/client.js index e002ee364a3..cafc03a09ff 100644 --- a/lib/client.js +++ b/lib/client.js @@ -1793,7 +1793,9 @@ function writeH2 (client, session, request) { ++h2State.openStreams stream.once('response', headers => { - if (request.onHeaders(Number(headers[HTTP2_HEADER_STATUS]), headers, stream.resume.bind(stream), '') === false) { + const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers + + if (request.onHeaders(Number(statusCode), realHeaders, stream.resume.bind(stream), '') === false) { stream.pause() } }) diff --git a/test/fetch/http2.js b/test/fetch/http2.js index 9b3c95b8f1f..9f6997f821b 100644 --- a/test/fetch/http2.js +++ b/test/fetch/http2.js @@ -9,11 +9,11 @@ const { Readable } = require('node:stream') const { test, plan } = require('tap') const pem = require('https-pem') -const { Client, fetch } = require('../..') +const { Client, fetch, Headers } = require('../..') const nodeVersion = Number(process.version.split('v')[1].split('.')[0]) -plan(6) +plan(7) test('[Fetch] Issue#2311', async t => { const expectedBody = 'hello from client!' @@ -375,3 +375,41 @@ test( t.equal(Buffer.concat(requestChunks).toString('utf-8'), expectedBody) } ) + +test('Issue#2415', async (t) => { + t.plan(1) + const server = createSecureServer(pem) + + server.on('stream', async (stream, headers) => { + stream.respond({ + ':status': 200 + }) + stream.end('test') + }) + + server.listen() + await once(server, 'listening') + + const client = new Client(`https://localhost:${server.address().port}`, { + connect: { + rejectUnauthorized: false + }, + allowH2: true + }) + + const response = await fetch( + `https://localhost:${server.address().port}/`, + // Needs to be passed to disable the reject unauthorized + { + method: 'GET', + dispatcher: client + } + ) + + await response.text() + + t.teardown(server.close.bind(server)) + t.teardown(client.close.bind(client)) + + t.doesNotThrow(() => new Headers(response.headers)) +}) diff --git a/test/http2.js b/test/http2.js index a3d24f23670..71b77493255 100644 --- a/test/http2.js +++ b/test/http2.js @@ -17,7 +17,7 @@ const isGreaterThanv20 = gte(process.version.slice(1), '20.0.0') // https://github.com/nodejs/node/pull/41735 const hasPseudoHeadersOrderFix = gte(process.version.slice(1), '16.14.1') -plan(22) +plan(23) test('Should support H2 connection', async t => { const body = [] @@ -1154,3 +1154,38 @@ test( t.equal(response.statusCode, 200) } ) + +test('The h2 pseudo-headers is not included in the headers', async t => { + const server = createSecureServer(pem) + + server.on('stream', (stream, headers) => { + stream.respond({ + ':status': 200 + }) + stream.end('hello h2!') + }) + + server.listen(0) + await once(server, 'listening') + + const client = new Client(`https://localhost:${server.address().port}`, { + connect: { + rejectUnauthorized: false + }, + allowH2: true + }) + + t.plan(2) + t.teardown(server.close.bind(server)) + t.teardown(client.close.bind(client)) + + const response = await client.request({ + path: '/', + method: 'GET' + }) + + await response.body.text() + + t.equal(response.statusCode, 200) + t.equal(response.headers[':status'], undefined) +}) From 5d77bf256b979ae0a8463f6096ee32d27b17689e Mon Sep 17 00:00:00 2001 From: Oliver Salzburg Date: Mon, 20 Nov 2023 20:45:54 +0100 Subject: [PATCH 220/259] docs: Clarify `path` matching in `intercept()` (#2426) The behavior of `query` was unclear, as it only applies to the case where `path` is provided as a `string`. It also wasn't clear that the `RegExp` and callback case will receive the query as part of the path. The additional guidance should help with both. To further clarify the use case, an example for the callback scenario was added. Closes: #2385 --- docs/api/MockPool.md | 42 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/docs/api/MockPool.md b/docs/api/MockPool.md index de53914002e..96a986f57bb 100644 --- a/docs/api/MockPool.md +++ b/docs/api/MockPool.md @@ -35,8 +35,7 @@ const mockPool = mockAgent.get('http://localhost:3000') ### `MockPool.intercept(options)` -This method defines the interception rules for matching against requests for a MockPool or MockPool. We can intercept multiple times on a single instance, but each intercept is only used once. -For example if you expect to make 2 requests inside a test, you need to call `intercept()` twice. Assuming you use `disableNetConnect()` you will get `MockNotMatchedError` on the second request when you only call `intercept()` once. +This method defines the interception rules for matching against requests for a MockPool or MockPool. We can intercept multiple times on a single instance, but each intercept is only used once. For example if you expect to make 2 requests inside a test, you need to call `intercept()` twice. Assuming you use `disableNetConnect()` you will get `MockNotMatchedError` on the second request when you only call `intercept()` once. When defining interception rules, all the rules must pass for a request to be intercepted. If a request is not intercepted, a real request will be attempted. @@ -54,11 +53,11 @@ Returns: `MockInterceptor` corresponding to the input options. ### Parameter: `MockPoolInterceptOptions` -* **path** `string | RegExp | (path: string) => boolean` - a matcher for the HTTP request path. +* **path** `string | RegExp | (path: string) => boolean` - a matcher for the HTTP request path. When a `RegExp` or callback is used, it will match against the request path including all query parameters in alphabetical order. When a `string` is provided, the query parameters can be conveniently specified through the `MockPoolInterceptOptions.query` setting. * **method** `string | RegExp | (method: string) => boolean` - (optional) - a matcher for the HTTP request method. Defaults to `GET`. * **body** `string | RegExp | (body: string) => boolean` - (optional) - a matcher for the HTTP request body. * **headers** `Record boolean`> - (optional) - a matcher for the HTTP request headers. To be intercepted, a request must match all defined headers. Extra headers not defined here may (or may not) be included in the request and do not affect the interception in any way. -* **query** `Record | null` - (optional) - a matcher for the HTTP request query string params. +* **query** `Record | null` - (optional) - a matcher for the HTTP request query string params. Only applies when a `string` was provided for `MockPoolInterceptOptions.path`. ### Return: `MockInterceptor` @@ -458,6 +457,41 @@ const result3 = await request('http://localhost:3000/foo') // Will not match and make attempt a real request ``` +#### Example - Mocked request with path callback + +```js +import { MockAgent, setGlobalDispatcher, request } from 'undici' +import querystring from 'querystring' + +const mockAgent = new MockAgent() +setGlobalDispatcher(mockAgent) + +const mockPool = mockAgent.get('http://localhost:3000') + +const matchPath = requestPath => { + const [pathname, search] = requestPath.split('?') + const requestQuery = querystring.parse(search) + + if (!pathname.startsWith('/foo')) { + return false + } + + if (!Object.keys(requestQuery).includes('foo') || requestQuery.foo !== 'bar') { + return false + } + + return true +} + +mockPool.intercept({ + path: matchPath, + method: 'GET' +}).reply(200, 'foo') + +const result = await request('http://localhost:3000/foo?foo=bar') +// Will match and return mocked data +``` + ### `MockPool.close()` Closes the mock pool and de-registers from associated MockAgent. From 0cc7e87a9b44b1276d6eb5be118e9ea73efc761a Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Tue, 21 Nov 2023 12:47:03 +0900 Subject: [PATCH 221/259] fix: set-cookie clone (#2446) --- lib/fetch/headers.js | 2 +- test/fetch/request.js | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/fetch/headers.js b/lib/fetch/headers.js index cfdc8868efa..69acaaad996 100644 --- a/lib/fetch/headers.js +++ b/lib/fetch/headers.js @@ -128,7 +128,7 @@ class HeadersList { if (init instanceof HeadersList) { this[kHeadersMap] = new Map(init[kHeadersMap]) this[kHeadersSortedMap] = init[kHeadersSortedMap] - this.cookies = init.cookies + this.cookies = init.cookies === null ? null : [...init.cookies] } else { this[kHeadersMap] = new Map(init) this[kHeadersSortedMap] = null diff --git a/test/fetch/request.js b/test/fetch/request.js index acd3e6ab439..de593e8c053 100644 --- a/test/fetch/request.js +++ b/test/fetch/request.js @@ -487,4 +487,14 @@ test('request.referrer', (t) => { t.end() }) +// https://github.com/nodejs/undici/issues/2445 +test('Clone the set-cookie header when Request is passed as the first parameter and no header is passed.', (t) => { + t.plan(2) + const request = new Request('http://localhost', { headers: { 'set-cookie': 'A' } }) + const request2 = new Request(request) + request2.headers.append('set-cookie', 'B') + t.equal(request.headers.getSetCookie().join(', '), request.headers.get('set-cookie')) + t.equal(request2.headers.getSetCookie().join(', '), request2.headers.get('set-cookie')) +}) + teardown(() => process.exit()) From 99c38c6c50d80b2a59157c483cab0106ac6f2067 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Nie=C3=9Fen?= Date: Wed, 22 Nov 2023 00:04:38 +0100 Subject: [PATCH 222/259] docs: fix typo in maxConcurrentStreams (#2450) --- docs/api/Client.md | 2 +- types/client.d.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api/Client.md b/docs/api/Client.md index 42668389a94..b9e26f09752 100644 --- a/docs/api/Client.md +++ b/docs/api/Client.md @@ -33,7 +33,7 @@ Returns: `Client` * **autoSelectFamily**: `boolean` (optional) - Default: depends on local Node version, on Node 18.13.0 and above is `false`. Enables a family autodetection algorithm that loosely implements section 5 of [RFC 8305](https://tools.ietf.org/html/rfc8305#section-5). See [here](https://nodejs.org/api/net.html#socketconnectoptions-connectlistener) for more details. This option is ignored if not supported by the current Node version. * **autoSelectFamilyAttemptTimeout**: `number` - Default: depends on local Node version, on Node 18.13.0 and above is `250`. The amount of time in milliseconds to wait for a connection attempt to finish before trying the next address when using the `autoSelectFamily` option. See [here](https://nodejs.org/api/net.html#socketconnectoptions-connectlistener) for more details. * **allowH2**: `boolean` - Default: `false`. Enables support for H2 if the server has assigned bigger priority to it through ALPN negotiation. -* **maxConcurrentStreams**: `number` - Default: `100`. Dictates the maximum number of concurrent streams for a single H2 session. It can be overriden by a SETTINGS remote frame. +* **maxConcurrentStreams**: `number` - Default: `100`. Dictates the maximum number of concurrent streams for a single H2 session. It can be overridden by a SETTINGS remote frame. #### Parameter: `ConnectOptions` diff --git a/types/client.d.ts b/types/client.d.ts index 74948b15f38..56e78cc9765 100644 --- a/types/client.d.ts +++ b/types/client.d.ts @@ -77,7 +77,7 @@ export declare namespace Client { */ allowH2?: boolean; /** - * @description Dictates the maximum number of concurrent streams for a single H2 session. It can be overriden by a SETTINGS remote frame. + * @description Dictates the maximum number of concurrent streams for a single H2 session. It can be overridden by a SETTINGS remote frame. * @default 100 */ maxConcurrentStreams?: number From 039fa15389eb40dd5a0e486a498bd42f33ba5b12 Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Wed, 22 Nov 2023 10:04:31 +0100 Subject: [PATCH 223/259] refactor: remove leftovers (#2451) --- lib/handler/RetryHandler.js | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/lib/handler/RetryHandler.js b/lib/handler/RetryHandler.js index 5819c7a9b13..a8112b36ee4 100644 --- a/lib/handler/RetryHandler.js +++ b/lib/handler/RetryHandler.js @@ -278,29 +278,6 @@ class RetryHandler { headers, count: this.retryCount }) - // Meant to just indicate weather or not trigger a retry - // It should return null to indicate we exhausted the retries - const retryAfter = this.retryOpts.retry( - err, - { counter: this.retryCount, currentTimeout: this.retryAfter }, - { retry: this.retryOpts, ...this.opts } - ) - - assert( - retryAfter == null || Number.isFinite(retryAfter), - 'invalid retryAfter' - ) - - if (retryAfter == null) { - return this.handler.onHeaders( - statusCode, - rawHeaders, - resume, - statusMessage - ) - } - - this.retryAfter = retryAfter this.abort(err) From e495094b3700690f771089f1a3c338dd4da01b63 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Wed, 22 Nov 2023 11:34:34 +0100 Subject: [PATCH 224/259] fix: bad error handling --- lib/core/request.js | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/lib/core/request.js b/lib/core/request.js index 7db05ce65ae..fbbe45a6d9c 100644 --- a/lib/core/request.js +++ b/lib/core/request.js @@ -229,11 +229,7 @@ class Request { onBodySent (chunk) { if (this[kHandler].onBodySent) { - try { - this[kHandler].onBodySent(chunk) - } catch (err) { - this.onError(err) - } + return this[kHandler].onBodySent(chunk) } } @@ -243,11 +239,7 @@ class Request { } if (this[kHandler].onRequestSent) { - try { - this[kHandler].onRequestSent() - } catch (err) { - this.onError(err) - } + return this[kHandler].onRequestSent() } } From 9e80fb10cc5f5689bcf0584d455ac512de257feb Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Thu, 23 Nov 2023 21:36:04 +0900 Subject: [PATCH 225/259] refactor: add missing new operator (#2452) --- lib/fetch/request.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/fetch/request.js b/lib/fetch/request.js index 60e654eca11..336d361ce20 100644 --- a/lib/fetch/request.js +++ b/lib/fetch/request.js @@ -316,11 +316,11 @@ class Request { // 2. If method is not a method or method is a forbidden method, then // throw a TypeError. if (!isValidHTTPToken(init.method)) { - throw TypeError(`'${init.method}' is not a valid HTTP method.`) + throw new TypeError(`'${init.method}' is not a valid HTTP method.`) } if (forbiddenMethodsSet.has(method.toUpperCase())) { - throw TypeError(`'${init.method}' HTTP method is unsupported.`) + throw new TypeError(`'${init.method}' HTTP method is unsupported.`) } // 3. Normalize method. From 66029d1b317c0cfe38543553055cc86c658d7635 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Fri, 24 Nov 2023 09:59:38 +0100 Subject: [PATCH 226/259] Bumped v5.28.0 Signed-off-by: Matteo Collina --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 91f71169a91..a4d2d622737 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "5.27.2", + "version": "5.28.0", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { From 17c7a7330b6a0013d6061e7da0bb9ed5b92efaf3 Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Sun, 26 Nov 2023 16:10:12 +0900 Subject: [PATCH 227/259] perf: Improve `normalizeMethod` (#2456) * perf: Improve `normalizeMethod` * remove comment * test: add * test: remove * perf: use Map * Revert "perf: use Map" * Revert "test: remove" This reverts commit 3a37e7bc287aad76feea3b7aca94902df7147085. * perf: direct reference --- lib/fetch/request.js | 11 ++++++----- lib/fetch/util.js | 30 +++++++++++++++++++++++++----- test/fetch/request.js | 7 +++++++ 3 files changed, 38 insertions(+), 10 deletions(-) diff --git a/lib/fetch/request.js b/lib/fetch/request.js index 336d361ce20..5e1114636dc 100644 --- a/lib/fetch/request.js +++ b/lib/fetch/request.js @@ -10,7 +10,8 @@ const { isValidHTTPToken, sameOrigin, normalizeMethod, - makePolicyContainer + makePolicyContainer, + normalizeMethodRecord } = require('./util') const { forbiddenMethodsSet, @@ -315,16 +316,16 @@ class Request { // 2. If method is not a method or method is a forbidden method, then // throw a TypeError. - if (!isValidHTTPToken(init.method)) { - throw new TypeError(`'${init.method}' is not a valid HTTP method.`) + if (!isValidHTTPToken(method)) { + throw new TypeError(`'${method}' is not a valid HTTP method.`) } if (forbiddenMethodsSet.has(method.toUpperCase())) { - throw new TypeError(`'${init.method}' HTTP method is unsupported.`) + throw new TypeError(`'${method}' HTTP method is unsupported.`) } // 3. Normalize method. - method = normalizeMethod(init.method) + method = normalizeMethodRecord[method] ?? normalizeMethod(method) // 4. Set request’s method to method. request.method = method diff --git a/lib/fetch/util.js b/lib/fetch/util.js index bc6fd50c68a..b12142c7f42 100644 --- a/lib/fetch/util.js +++ b/lib/fetch/util.js @@ -698,11 +698,30 @@ function isCancelled (fetchParams) { fetchParams.controller.state === 'terminated' } -// https://fetch.spec.whatwg.org/#concept-method-normalize +const normalizeMethodRecord = { + delete: 'DELETE', + DELETE: 'DELETE', + get: 'GET', + GET: 'GET', + head: 'HEAD', + HEAD: 'HEAD', + options: 'OPTIONS', + OPTIONS: 'OPTIONS', + post: 'POST', + POST: 'POST', + put: 'PUT', + PUT: 'PUT' +} + +// Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`. +Object.setPrototypeOf(normalizeMethodRecord, null) + +/** + * @see https://fetch.spec.whatwg.org/#concept-method-normalize + * @param {string} method + */ function normalizeMethod (method) { - return /^(DELETE|GET|HEAD|OPTIONS|POST|PUT)$/i.test(method) - ? method.toUpperCase() - : method + return normalizeMethodRecord[method.toLowerCase()] ?? method } // https://infra.spec.whatwg.org/#serialize-a-javascript-value-to-a-json-string @@ -1047,5 +1066,6 @@ module.exports = { urlIsLocal, urlHasHttpsScheme, urlIsHttpHttpsScheme, - readAllBytes + readAllBytes, + normalizeMethodRecord } diff --git a/test/fetch/request.js b/test/fetch/request.js index de593e8c053..4f66de4ec73 100644 --- a/test/fetch/request.js +++ b/test/fetch/request.js @@ -497,4 +497,11 @@ test('Clone the set-cookie header when Request is passed as the first parameter t.equal(request2.headers.getSetCookie().join(', '), request2.headers.get('set-cookie')) }) +// Tests for optimization introduced in https://github.com/nodejs/undici/pull/2456 +test('keys to object prototypes method', (t) => { + t.plan(1) + const request = new Request('http://localhost', { method: 'hasOwnProperty' }) + t.ok(typeof request.method === 'string') +}) + teardown(() => process.exit()) From ac30400a191a78c6148c50e7ff70390a4ea93f90 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Sun, 26 Nov 2023 10:25:50 +0100 Subject: [PATCH 228/259] fix: dispatch error handling (#2459) --- lib/client.js | 31 ++++++++++++------------------- lib/core/request.js | 34 +++++++++++++++++++++++++++++----- 2 files changed, 41 insertions(+), 24 deletions(-) diff --git a/lib/client.js b/lib/client.js index cafc03a09ff..22cb39039da 100644 --- a/lib/client.js +++ b/lib/client.js @@ -917,11 +917,9 @@ class Parser { socket[kReset] = true } - let pause - try { - pause = request.onHeaders(statusCode, headers, this.resume, statusText) === false - } catch (err) { - util.destroy(socket, err) + const pause = request.onHeaders(statusCode, headers, this.resume, statusText) === false + + if (request.aborted) { return -1 } @@ -968,13 +966,8 @@ class Parser { this.bytesRead += buf.length - try { - if (request.onData(buf) === false) { - return constants.ERROR.PAUSED - } - } catch (err) { - util.destroy(socket, err) - return -1 + if (request.onData(buf) === false) { + return constants.ERROR.PAUSED } } @@ -1015,11 +1008,7 @@ class Parser { return -1 } - try { - request.onComplete(headers) - } catch (err) { - errorRequest(client, request, err) - } + request.onComplete(headers) client[kQueue][client[kRunningIdx]++] = null @@ -1805,13 +1794,17 @@ function writeH2 (client, session, request) { }) stream.on('data', (chunk) => { - if (request.onData(chunk) === false) stream.pause() + if (request.onData(chunk) === false) { + stream.pause() + } }) stream.once('close', () => { h2State.openStreams -= 1 // TODO(HTTP/2): unref only if current streams count is 0 - if (h2State.openStreams === 0) session.unref() + if (h2State.openStreams === 0) { + session.unref() + } }) stream.once('error', function (err) { diff --git a/lib/core/request.js b/lib/core/request.js index fbbe45a6d9c..3697e6a3acc 100644 --- a/lib/core/request.js +++ b/lib/core/request.js @@ -229,7 +229,11 @@ class Request { onBodySent (chunk) { if (this[kHandler].onBodySent) { - return this[kHandler].onBodySent(chunk) + try { + return this[kHandler].onBodySent(chunk) + } catch (err) { + this.abort(err) + } } } @@ -239,7 +243,11 @@ class Request { } if (this[kHandler].onRequestSent) { - return this[kHandler].onRequestSent() + try { + return this[kHandler].onRequestSent() + } catch (err) { + this.abort(err) + } } } @@ -263,14 +271,23 @@ class Request { channels.headers.publish({ request: this, response: { statusCode, headers, statusText } }) } - return this[kHandler].onHeaders(statusCode, headers, resume, statusText) + try { + return this[kHandler].onHeaders(statusCode, headers, resume, statusText) + } catch (err) { + this.abort(err) + } } onData (chunk) { assert(!this.aborted) assert(!this.completed) - return this[kHandler].onData(chunk) + try { + return this[kHandler].onData(chunk) + } catch (err) { + this.abort(err) + return false + } } onUpgrade (statusCode, headers, socket) { @@ -289,7 +306,13 @@ class Request { if (channels.trailers.hasSubscribers) { channels.trailers.publish({ request: this, trailers }) } - return this[kHandler].onComplete(trailers) + + try { + return this[kHandler].onComplete(trailers) + } catch (err) { + // TODO (fix): This might be a bad idea? + this.onError(err) + } } onError (error) { @@ -303,6 +326,7 @@ class Request { return } this.aborted = true + return this[kHandler].onError(error) } From 0c45ff7807ffc8be0cbff7a28a23fccc43b00514 Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Mon, 27 Nov 2023 00:23:03 +0900 Subject: [PATCH 229/259] perf(request): optimize if headers are given (#2454) * perf(request): optimize if headers are given * perf: use shallow copy * fix * spec * make maintenance easier * fix: comment position * fix * short * fix * add comment * use append * fix: typo * refactor --- lib/fetch/request.js | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/lib/fetch/request.js b/lib/fetch/request.js index 5e1114636dc..3b813aa77df 100644 --- a/lib/fetch/request.js +++ b/lib/fetch/request.js @@ -184,8 +184,10 @@ class Request { urlList: [...request.urlList] }) + const initHasKey = Object.keys(init).length !== 0 + // 13. If init is not empty, then: - if (Object.keys(init).length > 0) { + if (initHasKey) { // 1. If request’s mode is "navigate", then set it to "same-origin". if (request.mode === 'navigate') { request.mode = 'same-origin' @@ -416,25 +418,25 @@ class Request { } // 32. If init is not empty, then: - if (Object.keys(init).length !== 0) { + if (initHasKey) { + /** @type {HeadersList} */ + const headersList = this[kHeaders][kHeadersList] // 1. Let headers be a copy of this’s headers and its associated header // list. - let headers = new Headers(this[kHeaders]) - // 2. If init["headers"] exists, then set headers to init["headers"]. - if (init.headers !== undefined) { - headers = init.headers - } + const headers = init.headers !== undefined ? init.headers : new HeadersList(headersList) // 3. Empty this’s headers’s header list. - this[kHeaders][kHeadersList].clear() + headersList.clear() // 4. If headers is a Headers object, then for each header in its header // list, append header’s name/header’s value to this’s headers. - if (headers.constructor.name === 'Headers') { + if (headers instanceof HeadersList) { for (const [key, val] of headers) { - this[kHeaders].append(key, val) + headersList.append(key, val) } + // Note: Copy the `set-cookie` meta-data. + headersList.cookies = headers.cookies } else { // 5. Otherwise, fill this’s headers with headers. fillHeaders(this[kHeaders], headers) From e7ab79a5cfad66d5a1779854ba047a4133ccb12c Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Mon, 27 Nov 2023 07:42:24 +0100 Subject: [PATCH 230/259] chore: less async await (#2463) --- lib/agent.js | 8 +++--- lib/api/readable.js | 8 +++--- lib/client.js | 4 +-- lib/fetch/body.js | 4 +-- lib/fetch/index.js | 4 +-- lib/pool-base.js | 4 +-- lib/proxy-agent.js | 66 +++++++++++++++++++++------------------------ 7 files changed, 48 insertions(+), 50 deletions(-) diff --git a/lib/agent.js b/lib/agent.js index 0b18f2a91bd..8f2e1750d55 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -118,7 +118,7 @@ class Agent extends DispatcherBase { return dispatcher.dispatch(opts, handler) } - async [kClose] () { + [kClose] () { const closePromises = [] for (const ref of this[kClients].values()) { const client = ref.deref() @@ -128,10 +128,10 @@ class Agent extends DispatcherBase { } } - await Promise.all(closePromises) + return Promise.all(closePromises) } - async [kDestroy] (err) { + [kDestroy] (err) { const destroyPromises = [] for (const ref of this[kClients].values()) { const client = ref.deref() @@ -141,7 +141,7 @@ class Agent extends DispatcherBase { } } - await Promise.all(destroyPromises) + return Promise.all(destroyPromises) } } diff --git a/lib/api/readable.js b/lib/api/readable.js index 89913eaa621..eefcf279f4f 100644 --- a/lib/api/readable.js +++ b/lib/api/readable.js @@ -209,12 +209,14 @@ function isUnusable (self) { return util.isDisturbed(self) || isLocked(self) } -async function consume (stream, type) { +function consume (stream, type) { if (isUnusable(stream)) { - throw new TypeError('unusable') + return Promise.reject(new TypeError('unusable')) } - assert(!stream[kConsume]) + if (stream[kConsume]) { + return Promise.reject(new assert.AssertionError('null != true')) + } return new Promise((resolve, reject) => { stream[kConsume] = { diff --git a/lib/client.js b/lib/client.js index 22cb39039da..09cc57a1e4b 100644 --- a/lib/client.js +++ b/lib/client.js @@ -380,7 +380,7 @@ class Client extends DispatcherBase { return this[kNeedDrain] < 2 } - async [kClose] () { + [kClose] () { // TODO: for H2 we need to gracefully flush the remaining enqueued // request and close each stream. return new Promise((resolve) => { @@ -392,7 +392,7 @@ class Client extends DispatcherBase { }) } - async [kDestroy] (err) { + [kDestroy] (err) { return new Promise((resolve) => { const requests = this[kQueue].splice(this[kPendingIdx]) for (let i = 0; i < requests.length; i++) { diff --git a/lib/fetch/body.js b/lib/fetch/body.js index fd8481b796d..6598ea9ab30 100644 --- a/lib/fetch/body.js +++ b/lib/fetch/body.js @@ -234,8 +234,8 @@ function extractBody (object, keepalive = false) { } return controller.desiredSize > 0 }, - async cancel (reason) { - await iterator.return() + cancel (reason) { + return iterator.return() }, type: undefined }) diff --git a/lib/fetch/index.js b/lib/fetch/index.js index c109a01bf1f..2c6323e5d3b 100644 --- a/lib/fetch/index.js +++ b/lib/fetch/index.js @@ -1808,10 +1808,10 @@ async function httpNetworkFetch ( fetchParams.controller.controller = controller }, async pull (controller) { - await pullAlgorithm(controller) + pullAlgorithm(controller) }, async cancel (reason) { - await cancelAlgorithm(reason) + cancelAlgorithm(reason) } }, { diff --git a/lib/pool-base.js b/lib/pool-base.js index 2a909eee083..3c88562ac95 100644 --- a/lib/pool-base.js +++ b/lib/pool-base.js @@ -111,7 +111,7 @@ class PoolBase extends DispatcherBase { return this[kStats] } - async [kClose] () { + [kClose] () { if (this[kQueue].isEmpty()) { return Promise.all(this[kClients].map(c => c.close())) } else { @@ -121,7 +121,7 @@ class PoolBase extends DispatcherBase { } } - async [kDestroy] (err) { + [kDestroy] (err) { while (true) { const item = this[kQueue].shift() if (!item) { diff --git a/lib/proxy-agent.js b/lib/proxy-agent.js index c710948cc5b..e9e46262306 100644 --- a/lib/proxy-agent.js +++ b/lib/proxy-agent.js @@ -82,40 +82,38 @@ class ProxyAgent extends DispatcherBase { this[kClient] = clientFactory(resolvedUrl, { connect }) this[kAgent] = new Agent({ ...opts, - connect: async (opts, callback) => { + connect: (opts, callback) => { let requestedHost = opts.host if (!opts.port) { requestedHost += `:${defaultProtocolPort(opts.protocol)}` } - try { - const { socket, statusCode } = await this[kClient].connect({ - origin, - port, - path: requestedHost, - signal: opts.signal, - headers: { - ...this[kProxyHeaders], - host - } - }) - if (statusCode !== 200) { - socket.on('error', () => {}).destroy() - callback(new RequestAbortedError('Proxy response !== 200 when HTTP Tunneling')) - } - if (opts.protocol !== 'https:') { - callback(null, socket) - return + this[kClient].connect({ + origin, + port, + path: requestedHost, + signal: opts.signal, + headers: { + ...this[kProxyHeaders], + host } - let servername - if (this[kRequestTls]) { - servername = this[kRequestTls].servername - } else { - servername = opts.servername - } - this[kConnectEndpoint]({ ...opts, servername, httpSocket: socket }, callback) - } catch (err) { - callback(err) - } + }).catch(callback) + .then(({ socket, statusCode }) => { + if (statusCode !== 200) { + socket.on('error', () => { }).destroy() + callback(new RequestAbortedError('Proxy response !== 200 when HTTP Tunneling')) + } + if (opts.protocol !== 'https:') { + callback(null, socket) + return + } + let servername + if (this[kRequestTls]) { + servername = this[kRequestTls].servername + } else { + servername = opts.servername + } + this[kConnectEndpoint]({ ...opts, servername, httpSocket: socket }, callback) + }).catch(callback) } }) } @@ -136,14 +134,12 @@ class ProxyAgent extends DispatcherBase { ) } - async [kClose] () { - await this[kAgent].close() - await this[kClient].close() + [kClose] () { + return Promise.all([this[kAgent].close, this[kClient].close]) } - async [kDestroy] () { - await this[kAgent].destroy() - await this[kClient].destroy() + [kDestroy] () { + return Promise.all([this[kAgent].destroy, this[kClient].destroy]) } } From 600a95280b4dfc79d8996151479974463185a6e4 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 27 Nov 2023 10:47:13 +0100 Subject: [PATCH 231/259] Revert "chore: less async await (#2463)" This reverts commit e7ab79a5cfad66d5a1779854ba047a4133ccb12c. --- lib/agent.js | 8 +++--- lib/api/readable.js | 8 +++--- lib/client.js | 4 +-- lib/fetch/body.js | 4 +-- lib/fetch/index.js | 4 +-- lib/pool-base.js | 4 +-- lib/proxy-agent.js | 66 ++++++++++++++++++++++++--------------------- 7 files changed, 50 insertions(+), 48 deletions(-) diff --git a/lib/agent.js b/lib/agent.js index 8f2e1750d55..0b18f2a91bd 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -118,7 +118,7 @@ class Agent extends DispatcherBase { return dispatcher.dispatch(opts, handler) } - [kClose] () { + async [kClose] () { const closePromises = [] for (const ref of this[kClients].values()) { const client = ref.deref() @@ -128,10 +128,10 @@ class Agent extends DispatcherBase { } } - return Promise.all(closePromises) + await Promise.all(closePromises) } - [kDestroy] (err) { + async [kDestroy] (err) { const destroyPromises = [] for (const ref of this[kClients].values()) { const client = ref.deref() @@ -141,7 +141,7 @@ class Agent extends DispatcherBase { } } - return Promise.all(destroyPromises) + await Promise.all(destroyPromises) } } diff --git a/lib/api/readable.js b/lib/api/readable.js index eefcf279f4f..89913eaa621 100644 --- a/lib/api/readable.js +++ b/lib/api/readable.js @@ -209,14 +209,12 @@ function isUnusable (self) { return util.isDisturbed(self) || isLocked(self) } -function consume (stream, type) { +async function consume (stream, type) { if (isUnusable(stream)) { - return Promise.reject(new TypeError('unusable')) + throw new TypeError('unusable') } - if (stream[kConsume]) { - return Promise.reject(new assert.AssertionError('null != true')) - } + assert(!stream[kConsume]) return new Promise((resolve, reject) => { stream[kConsume] = { diff --git a/lib/client.js b/lib/client.js index 09cc57a1e4b..22cb39039da 100644 --- a/lib/client.js +++ b/lib/client.js @@ -380,7 +380,7 @@ class Client extends DispatcherBase { return this[kNeedDrain] < 2 } - [kClose] () { + async [kClose] () { // TODO: for H2 we need to gracefully flush the remaining enqueued // request and close each stream. return new Promise((resolve) => { @@ -392,7 +392,7 @@ class Client extends DispatcherBase { }) } - [kDestroy] (err) { + async [kDestroy] (err) { return new Promise((resolve) => { const requests = this[kQueue].splice(this[kPendingIdx]) for (let i = 0; i < requests.length; i++) { diff --git a/lib/fetch/body.js b/lib/fetch/body.js index 6598ea9ab30..fd8481b796d 100644 --- a/lib/fetch/body.js +++ b/lib/fetch/body.js @@ -234,8 +234,8 @@ function extractBody (object, keepalive = false) { } return controller.desiredSize > 0 }, - cancel (reason) { - return iterator.return() + async cancel (reason) { + await iterator.return() }, type: undefined }) diff --git a/lib/fetch/index.js b/lib/fetch/index.js index 2c6323e5d3b..c109a01bf1f 100644 --- a/lib/fetch/index.js +++ b/lib/fetch/index.js @@ -1808,10 +1808,10 @@ async function httpNetworkFetch ( fetchParams.controller.controller = controller }, async pull (controller) { - pullAlgorithm(controller) + await pullAlgorithm(controller) }, async cancel (reason) { - cancelAlgorithm(reason) + await cancelAlgorithm(reason) } }, { diff --git a/lib/pool-base.js b/lib/pool-base.js index 3c88562ac95..2a909eee083 100644 --- a/lib/pool-base.js +++ b/lib/pool-base.js @@ -111,7 +111,7 @@ class PoolBase extends DispatcherBase { return this[kStats] } - [kClose] () { + async [kClose] () { if (this[kQueue].isEmpty()) { return Promise.all(this[kClients].map(c => c.close())) } else { @@ -121,7 +121,7 @@ class PoolBase extends DispatcherBase { } } - [kDestroy] (err) { + async [kDestroy] (err) { while (true) { const item = this[kQueue].shift() if (!item) { diff --git a/lib/proxy-agent.js b/lib/proxy-agent.js index e9e46262306..c710948cc5b 100644 --- a/lib/proxy-agent.js +++ b/lib/proxy-agent.js @@ -82,38 +82,40 @@ class ProxyAgent extends DispatcherBase { this[kClient] = clientFactory(resolvedUrl, { connect }) this[kAgent] = new Agent({ ...opts, - connect: (opts, callback) => { + connect: async (opts, callback) => { let requestedHost = opts.host if (!opts.port) { requestedHost += `:${defaultProtocolPort(opts.protocol)}` } - this[kClient].connect({ - origin, - port, - path: requestedHost, - signal: opts.signal, - headers: { - ...this[kProxyHeaders], - host - } - }).catch(callback) - .then(({ socket, statusCode }) => { - if (statusCode !== 200) { - socket.on('error', () => { }).destroy() - callback(new RequestAbortedError('Proxy response !== 200 when HTTP Tunneling')) - } - if (opts.protocol !== 'https:') { - callback(null, socket) - return + try { + const { socket, statusCode } = await this[kClient].connect({ + origin, + port, + path: requestedHost, + signal: opts.signal, + headers: { + ...this[kProxyHeaders], + host } - let servername - if (this[kRequestTls]) { - servername = this[kRequestTls].servername - } else { - servername = opts.servername - } - this[kConnectEndpoint]({ ...opts, servername, httpSocket: socket }, callback) - }).catch(callback) + }) + if (statusCode !== 200) { + socket.on('error', () => {}).destroy() + callback(new RequestAbortedError('Proxy response !== 200 when HTTP Tunneling')) + } + if (opts.protocol !== 'https:') { + callback(null, socket) + return + } + let servername + if (this[kRequestTls]) { + servername = this[kRequestTls].servername + } else { + servername = opts.servername + } + this[kConnectEndpoint]({ ...opts, servername, httpSocket: socket }, callback) + } catch (err) { + callback(err) + } } }) } @@ -134,12 +136,14 @@ class ProxyAgent extends DispatcherBase { ) } - [kClose] () { - return Promise.all([this[kAgent].close, this[kClient].close]) + async [kClose] () { + await this[kAgent].close() + await this[kClient].close() } - [kDestroy] () { - return Promise.all([this[kAgent].destroy, this[kClient].destroy]) + async [kDestroy] () { + await this[kAgent].destroy() + await this[kClient].destroy() } } From 286bb4463b05e01e809737214e8eb1c161b78240 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 27 Nov 2023 10:48:11 +0100 Subject: [PATCH 232/259] Bumped v5.28.1 Signed-off-by: Matteo Collina --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a4d2d622737..f30f5070ff2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "5.28.0", + "version": "5.28.1", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { From 02807b685b4341d5293ba3146de456fd820f7746 Mon Sep 17 00:00:00 2001 From: Chau Giang Date: Mon, 27 Nov 2023 19:29:51 +0700 Subject: [PATCH 233/259] fix: remove optional chainning for compatible with Nodejs12 and below (#2470) --- lib/api/readable.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/api/readable.js b/lib/api/readable.js index 89913eaa621..5269dfae50c 100644 --- a/lib/api/readable.js +++ b/lib/api/readable.js @@ -180,7 +180,7 @@ module.exports = class BodyReadable extends Readable { this .on('close', function () { signalListenerCleanup() - if (signal?.aborted) { + if (signal && signal.aborted) { reject(signal.reason || Object.assign(new Error('The operation was aborted'), { name: 'AbortError' })) } else { resolve(null) From 6298bfada98b1e8c6a23a62eb9c7e325f16f518a Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Mon, 27 Nov 2023 22:45:22 +0900 Subject: [PATCH 234/259] fix: remove `node:` prefix (#2471) --- lib/handler/RetryHandler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/handler/RetryHandler.js b/lib/handler/RetryHandler.js index a8112b36ee4..306948bf8cb 100644 --- a/lib/handler/RetryHandler.js +++ b/lib/handler/RetryHandler.js @@ -1,4 +1,4 @@ -const assert = require('node:assert') +const assert = require('assert') const { kRetryHandlerDefaultRetry } = require('../core/symbols') const { RequestRetryError } = require('../core/errors') From c182c32183c77330bda0d1a433fedebc149f1e6c Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Mon, 27 Nov 2023 23:39:24 +0900 Subject: [PATCH 235/259] perf: avoid Headers initialization (#2468) --- lib/cache/symbols.js | 2 +- lib/core/symbols.js | 3 ++- lib/fetch/headers.js | 5 ++++- lib/fetch/request.js | 11 +++++------ lib/fetch/response.js | 4 ++-- 5 files changed, 14 insertions(+), 11 deletions(-) diff --git a/lib/cache/symbols.js b/lib/cache/symbols.js index f9b19740af8..40448d6001e 100644 --- a/lib/cache/symbols.js +++ b/lib/cache/symbols.js @@ -1,5 +1,5 @@ 'use strict' module.exports = { - kConstruct: Symbol('constructable') + kConstruct: require('../core/symbols').kConstruct } diff --git a/lib/core/symbols.js b/lib/core/symbols.js index 1d5dc4e3db0..68d8566fac0 100644 --- a/lib/core/symbols.js +++ b/lib/core/symbols.js @@ -58,5 +58,6 @@ module.exports = { kHTTP1BuildRequest: Symbol('http1 build request'), kHTTP2CopyHeaders: Symbol('http2 copy headers'), kHTTPConnVersion: Symbol('http connection version'), - kRetryHandlerDefaultRetry: Symbol('retry agent default retry') + kRetryHandlerDefaultRetry: Symbol('retry agent default retry'), + kConstruct: Symbol('constructable') } diff --git a/lib/fetch/headers.js b/lib/fetch/headers.js index 69acaaad996..2f1c0be5a47 100644 --- a/lib/fetch/headers.js +++ b/lib/fetch/headers.js @@ -2,7 +2,7 @@ 'use strict' -const { kHeadersList } = require('../core/symbols') +const { kHeadersList, kConstruct } = require('../core/symbols') const { kGuard } = require('./symbols') const { kEnumerableProperty } = require('../core/util') const { @@ -240,6 +240,9 @@ class HeadersList { // https://fetch.spec.whatwg.org/#headers-class class Headers { constructor (init = undefined) { + if (init === kConstruct) { + return + } this[kHeadersList] = new HeadersList() // The new Headers(init) constructor steps are: diff --git a/lib/fetch/request.js b/lib/fetch/request.js index 3b813aa77df..51896f58435 100644 --- a/lib/fetch/request.js +++ b/lib/fetch/request.js @@ -28,13 +28,12 @@ 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 { kHeadersList, kConstruct } = require('../core/symbols') const assert = require('assert') const { getMaxListeners, setMaxListeners, getEventListeners, defaultMaxListeners } = require('events') let TransformStream = globalThis.TransformStream -const kInit = Symbol('init') const kAbortController = Symbol('abortController') const requestFinalizer = new FinalizationRegistry(({ signal, abort }) => { @@ -45,7 +44,7 @@ const requestFinalizer = new FinalizationRegistry(({ signal, abort }) => { class Request { // https://fetch.spec.whatwg.org/#dom-request constructor (input, init = {}) { - if (input === kInit) { + if (input === kConstruct) { return } @@ -398,7 +397,7 @@ class Request { // 30. Set this’s headers to a new Headers object with this’s relevant // Realm, whose header list is request’s header list and guard is // "request". - this[kHeaders] = new Headers() + this[kHeaders] = new Headers(kConstruct) this[kHeaders][kHeadersList] = request.headersList this[kHeaders][kGuard] = 'request' this[kHeaders][kRealm] = this[kRealm] @@ -725,10 +724,10 @@ class Request { // 3. Let clonedRequestObject be the result of creating a Request object, // given clonedRequest, this’s headers’s guard, and this’s relevant Realm. - const clonedRequestObject = new Request(kInit) + const clonedRequestObject = new Request(kConstruct) clonedRequestObject[kState] = clonedRequest clonedRequestObject[kRealm] = this[kRealm] - clonedRequestObject[kHeaders] = new Headers() + clonedRequestObject[kHeaders] = new Headers(kConstruct) clonedRequestObject[kHeaders][kHeadersList] = clonedRequest.headersList clonedRequestObject[kHeaders][kGuard] = this[kHeaders][kGuard] clonedRequestObject[kHeaders][kRealm] = this[kHeaders][kRealm] diff --git a/lib/fetch/response.js b/lib/fetch/response.js index 23cf55c51dc..5d23475f14e 100644 --- a/lib/fetch/response.js +++ b/lib/fetch/response.js @@ -23,7 +23,7 @@ const { webidl } = require('./webidl') const { FormData } = require('./formdata') const { getGlobalOrigin } = require('./global') const { URLSerializer } = require('./dataURL') -const { kHeadersList } = require('../core/symbols') +const { kHeadersList, kConstruct } = require('../core/symbols') const assert = require('assert') const { types } = require('util') @@ -144,7 +144,7 @@ class Response { // 2. Set this’s headers to a new Headers object with this’s relevant // Realm, whose header list is this’s response’s header list and guard // is "response". - this[kHeaders] = new Headers() + this[kHeaders] = new Headers(kConstruct) this[kHeaders][kGuard] = 'response' this[kHeaders][kHeadersList] = this[kState].headersList this[kHeaders][kRealm] = this[kRealm] From 56efa962f682bb24836ae66c52203ff3ec131ba7 Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Tue, 28 Nov 2023 00:11:15 +0900 Subject: [PATCH 236/259] fix: handle SharedArrayBuffer correctly (#2466) * fix: handle SharedArrayBuffer correctly * format * test: add * fix: test * fixup * use ArrayBuffer.isView * fixup * fixup * test: add Request * fixup --- lib/fetch/response.js | 6 +----- test/fetch/request.js | 7 +++++++ test/fetch/response.js | 7 +++++++ 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/lib/fetch/response.js b/lib/fetch/response.js index 5d23475f14e..73386123e33 100644 --- a/lib/fetch/response.js +++ b/lib/fetch/response.js @@ -514,11 +514,7 @@ webidl.converters.XMLHttpRequestBodyInit = function (V) { return webidl.converters.Blob(V, { strict: false }) } - if ( - types.isAnyArrayBuffer(V) || - types.isTypedArray(V) || - types.isDataView(V) - ) { + if (types.isArrayBuffer(V) || types.isTypedArray(V) || types.isDataView(V)) { return webidl.converters.BufferSource(V) } diff --git a/test/fetch/request.js b/test/fetch/request.js index 4f66de4ec73..db2c8e868b6 100644 --- a/test/fetch/request.js +++ b/test/fetch/request.js @@ -504,4 +504,11 @@ test('keys to object prototypes method', (t) => { t.ok(typeof request.method === 'string') }) +// https://github.com/nodejs/undici/issues/2465 +test('Issue#2465', async (t) => { + t.plan(1) + const request = new Request('http://localhost', { body: new SharedArrayBuffer(0), method: 'POST' }) + t.equal(await request.text(), '[object SharedArrayBuffer]') +}) + teardown(() => process.exit()) diff --git a/test/fetch/response.js b/test/fetch/response.js index 2342f0927ff..422c7ef2e02 100644 --- a/test/fetch/response.js +++ b/test/fetch/response.js @@ -248,3 +248,10 @@ test('constructing Response with third party FormData body', async (t) => { t.equal(contentType[0], 'multipart/form-data; boundary') t.ok((await res.text()).startsWith(`--${contentType[1]}`)) }) + +// https://github.com/nodejs/undici/issues/2465 +test('Issue#2465', async (t) => { + t.plan(1) + const response = new Response(new SharedArrayBuffer(0)) + t.equal(await response.text(), '[object SharedArrayBuffer]') +}) From 0437f69d186e7144e726f0657e5cda08b418606b Mon Sep 17 00:00:00 2001 From: Szymon Gebler Date: Mon, 27 Nov 2023 20:12:31 +0100 Subject: [PATCH 237/259] Add `null` to `signal` in `RequestInit` (#2455) --- test/types/fetch.test-d.ts | 2 +- types/fetch.d.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/types/fetch.test-d.ts b/test/types/fetch.test-d.ts index e11296aa85c..59fb49fc3e8 100644 --- a/test/types/fetch.test-d.ts +++ b/test/types/fetch.test-d.ts @@ -42,7 +42,7 @@ expectType(requestInit.headers) expectType(requestInit.body) expectType(requestInit.redirect) expectType(requestInit.integrity) -expectType(requestInit.signal) +expectType(requestInit.signal) expectType(requestInit.credentials) expectType(requestInit.mode) expectType(requestInit.referrer); diff --git a/types/fetch.d.ts b/types/fetch.d.ts index fa4619c9182..440f2b00397 100644 --- a/types/fetch.d.ts +++ b/types/fetch.d.ts @@ -108,7 +108,7 @@ export interface RequestInit { body?: BodyInit redirect?: RequestRedirect integrity?: string - signal?: AbortSignal + signal?: AbortSignal | null credentials?: RequestCredentials mode?: RequestMode referrer?: string From c5d73ca7186e30c6849ef39be29519fbc289a488 Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Wed, 29 Nov 2023 03:25:47 +0900 Subject: [PATCH 238/259] fix: correctly handle data URL with hashes. (#2475) * fix: correctly handle data URL with hashes. * fix: lint * test: better name * suggestion change * perf: avoid substring * fixup * test: better --- lib/fetch/dataURL.js | 13 +++++-------- test/fetch/data-uri.js | 21 +++++++++++++++++++++ 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/lib/fetch/dataURL.js b/lib/fetch/dataURL.js index 6df4fcc8cc6..7b6a606106d 100644 --- a/lib/fetch/dataURL.js +++ b/lib/fetch/dataURL.js @@ -119,17 +119,14 @@ function dataURLProcessor (dataURL) { * @param {boolean} excludeFragment */ function URLSerializer (url, excludeFragment = false) { - const href = url.href - if (!excludeFragment) { - return href + return url.href } - const hash = href.lastIndexOf('#') - if (hash === -1) { - return href - } - return href.slice(0, hash) + const href = url.href + const hashLength = url.hash.length + + return hashLength === 0 ? href : href.substring(0, href.length - hashLength) } // https://infra.spec.whatwg.org/#collect-a-sequence-of-code-points diff --git a/test/fetch/data-uri.js b/test/fetch/data-uri.js index d4ca7ebab56..6191bfe6aa5 100644 --- a/test/fetch/data-uri.js +++ b/test/fetch/data-uri.js @@ -191,3 +191,24 @@ test('https://domain.com/?', (t) => { const serialized = URLSerializer(new URL(domain)) t.equal(serialized, domain) }) + +// https://github.com/nodejs/undici/issues/2474 +test('hash url', (t) => { + t.plan(1) + const domain = 'https://domain.com/#a#b' + const url = new URL(domain) + const serialized = URLSerializer(url, true) + t.equal(serialized, url.href.substring(0, url.href.length - url.hash.length)) +}) + +// https://github.com/nodejs/undici/issues/2474 +test('data url that includes the hash', async (t) => { + t.plan(1) + const dataURL = 'data:,node#js#' + try { + const res = await fetch(dataURL) + t.equal(await res.text(), 'node') + } catch (error) { + t.fail(`failed to fetch ${dataURL}`) + } +}) From 19c69a045467182403815d854ecbf936574d3300 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torstein=20Bj=C3=B8rnstad?= Date: Wed, 29 Nov 2023 16:58:49 +0100 Subject: [PATCH 239/259] fix: check response for timinginfo allow flag (#2477) * fix: check response for timinginfo allow flag * Update test/fetch/resource-timing.js --------- Co-authored-by: Khafra --- lib/fetch/index.js | 2 +- test/fetch/resource-timing.js | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/lib/fetch/index.js b/lib/fetch/index.js index c109a01bf1f..17c3d87ea62 100644 --- a/lib/fetch/index.js +++ b/lib/fetch/index.js @@ -286,7 +286,7 @@ function finalizeAndReportTiming (response, initiatorType = 'other') { } // 8. If response’s timing allow passed flag is not set, then: - if (!timingInfo.timingAllowPassed) { + if (!response.timingAllowPassed) { // 1. Set timingInfo to a the result of creating an opaque timing info for timingInfo. timingInfo = createOpaqueTimingInfo({ startTime: timingInfo.startTime diff --git a/test/fetch/resource-timing.js b/test/fetch/resource-timing.js index 25b3bcaafbb..d266f28bdc8 100644 --- a/test/fetch/resource-timing.js +++ b/test/fetch/resource-timing.js @@ -46,3 +46,27 @@ test('should create a PerformanceResourceTiming after each fetch request', { ski t.teardown(server.close.bind(server)) }) + +test('should include encodedBodySize in performance entry', { skip }, (t) => { + t.plan(4) + const obs = new PerformanceObserver(list => { + const [entry] = list.getEntries() + t.equal(entry.encodedBodySize, 2) + t.equal(entry.decodedBodySize, 2) + t.equal(entry.transferSize, 2 + 300) + + obs.disconnect() + performance.clearResourceTimings() + }) + + obs.observe({ entryTypes: ['resource'] }) + + const server = createServer((req, res) => { + res.end('ok') + }).listen(0, async () => { + const body = await fetch(`http://localhost:${server.address().port}`) + t.strictSame('ok', await body.text()) + }) + + t.teardown(server.close.bind(server)) +}) From ed15e984df1645517ffe0f482685633267f46f0c Mon Sep 17 00:00:00 2001 From: MzUgM <108896003+MzUgM@users.noreply.github.com> Date: Thu, 30 Nov 2023 03:29:45 +0000 Subject: [PATCH 240/259] Make call to onBodySent conditional in RetryHandler (#2478) * failing test RequestHandler does not have `onBodySent` but `RetryHandler` always sends to `onBodySent` which causes a problem for users who use `RetryHandler` via `request()` * is this a fix? * refactor: clean up test case --- lib/api/api-request.js | 1 + lib/handler/RetryHandler.js | 2 +- test/retry-handler.js | 78 +++++++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 1 deletion(-) diff --git a/lib/api/api-request.js b/lib/api/api-request.js index f130ecc9867..d4281ce2449 100644 --- a/lib/api/api-request.js +++ b/lib/api/api-request.js @@ -177,3 +177,4 @@ function request (opts, callback) { } module.exports = request +module.exports.RequestHandler = RequestHandler diff --git a/lib/handler/RetryHandler.js b/lib/handler/RetryHandler.js index 306948bf8cb..371044719fd 100644 --- a/lib/handler/RetryHandler.js +++ b/lib/handler/RetryHandler.js @@ -95,7 +95,7 @@ class RetryHandler { } onBodySent (chunk) { - return this.handler.onBodySent(chunk) + if (this.handler.onBodySent) return this.handler.onBodySent(chunk) } static [kRetryHandlerDefaultRetry] (err, { state, opts }, cb) { diff --git a/test/retry-handler.js b/test/retry-handler.js index ef2c47eeb33..a4577a6a3e1 100644 --- a/test/retry-handler.js +++ b/test/retry-handler.js @@ -5,6 +5,7 @@ const { once } = require('node:events') const tap = require('tap') const { RetryHandler, Client } = require('..') +const { RequestHandler } = require('../lib/api/api-request') tap.test('Should retry status code', t => { let counter = 0 @@ -542,3 +543,80 @@ tap.test('Should handle 206 partial content - bad-etag', t => { }) }) }) + +tap.test('retrying a request with a body', t => { + let counter = 0 + const server = createServer() + const dispatchOptions = { + retryOptions: { + retry: (err, { state, opts }, done) => { + counter++ + + if ( + err.statusCode === 500 || + err.message.includes('other side closed') + ) { + setTimeout(done, 500) + return + } + + return done(err) + } + }, + method: 'POST', + path: '/', + headers: { + 'content-type': 'application/json' + }, + body: JSON.stringify({ hello: 'world' }) + } + + t.plan(1) + + server.on('request', (req, res) => { + switch (counter) { + case 0: + req.destroy() + return + case 1: + res.writeHead(500) + res.end('failed') + return + case 2: + res.writeHead(200) + res.end('hello world!') + return + default: + t.fail() + } + }) + + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`) + const handler = new RetryHandler(dispatchOptions, { + dispatch: client.dispatch.bind(client), + handler: new RequestHandler(dispatchOptions, (err, data) => { + t.error(err) + }) + }) + + t.teardown(async () => { + await client.close() + server.close() + + await once(server, 'close') + }) + + client.dispatch( + { + method: 'POST', + path: '/', + headers: { + 'content-type': 'application/json' + }, + body: JSON.stringify({ hello: 'world' }) + }, + handler + ) + }) +}) From 28759f406ff808afa7a102e9e248291123ef59cb Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Thu, 30 Nov 2023 19:40:27 +0900 Subject: [PATCH 241/259] refactor: better integrity check (#2462) * refactor: better integrity check * better --- lib/fetch/request.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/fetch/request.js b/lib/fetch/request.js index 51896f58435..6fe4dff64c4 100644 --- a/lib/fetch/request.js +++ b/lib/fetch/request.js @@ -301,7 +301,7 @@ class Request { } // 23. If init["integrity"] exists, then set request’s integrity metadata to it. - if (init.integrity !== undefined && init.integrity != null) { + if (init.integrity != null) { request.integrity = String(init.integrity) } From 08183ea1d25964de9eac3b9944b0c933fe693e6f Mon Sep 17 00:00:00 2001 From: Matt Way Date: Thu, 30 Nov 2023 22:33:10 +1000 Subject: [PATCH 242/259] fix: Added support for inline URL username:password proxy auth (#2473) * added support for inline URL username:password * Update lib/proxy-agent.js --------- Co-authored-by: Robert Nagy --- lib/proxy-agent.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/proxy-agent.js b/lib/proxy-agent.js index c710948cc5b..e3c0f6f3d46 100644 --- a/lib/proxy-agent.js +++ b/lib/proxy-agent.js @@ -65,6 +65,9 @@ class ProxyAgent extends DispatcherBase { this[kProxyTls] = opts.proxyTls this[kProxyHeaders] = opts.headers || {} + const resolvedUrl = new URL(opts.uri) + const { origin, port, host, username, password } = resolvedUrl + if (opts.auth && opts.token) { throw new InvalidArgumentError('opts.auth cannot be used in combination with opts.token') } else if (opts.auth) { @@ -72,11 +75,10 @@ class ProxyAgent extends DispatcherBase { this[kProxyHeaders]['proxy-authorization'] = `Basic ${opts.auth}` } else if (opts.token) { this[kProxyHeaders]['proxy-authorization'] = opts.token + } else if (username && password) { + this[kProxyHeaders]['proxy-authorization'] = `Basic ${Buffer.from(`${decodeURIComponent(username)}:${decodeURIComponent(password)}`).toString('base64')}` } - const resolvedUrl = new URL(opts.uri) - const { origin, port, host } = resolvedUrl - const connect = buildConnector({ ...opts.proxyTls }) this[kConnectEndpoint] = buildConnector({ ...opts.requestTls }) this[kClient] = clientFactory(resolvedUrl, { connect }) @@ -100,7 +102,7 @@ class ProxyAgent extends DispatcherBase { }) if (statusCode !== 200) { socket.on('error', () => {}).destroy() - callback(new RequestAbortedError('Proxy response !== 200 when HTTP Tunneling')) + callback(new RequestAbortedError(`Proxy response (${statusCode}) !== 200 when HTTP Tunneling`)) } if (opts.protocol !== 'https:') { callback(null, socket) From 80979edc78c458e87786e25194bc64ed0a2184b4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 Nov 2023 13:33:26 +0100 Subject: [PATCH 243/259] build(deps-dev): bump jsdom from 22.1.0 to 23.0.0 (#2472) Bumps [jsdom](https://github.com/jsdom/jsdom) from 22.1.0 to 23.0.0. - [Release notes](https://github.com/jsdom/jsdom/releases) - [Changelog](https://github.com/jsdom/jsdom/blob/main/Changelog.md) - [Commits](https://github.com/jsdom/jsdom/compare/22.1.0...23.0.0) --- updated-dependencies: - dependency-name: jsdom dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f30f5070ff2..d79d0841df7 100644 --- a/package.json +++ b/package.json @@ -115,7 +115,7 @@ "husky": "^8.0.1", "import-fresh": "^3.3.0", "jest": "^29.0.2", - "jsdom": "^22.1.0", + "jsdom": "^23.0.0", "jsfuzz": "^1.0.15", "mocha": "^10.0.0", "mockttp": "^3.9.2", From ea2f606e6b101fcbc578a407c8d4f9d10d17756e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 Nov 2023 13:33:39 +0100 Subject: [PATCH 244/259] build(deps-dev): bump sinon from 16.1.3 to 17.0.1 (#2405) Bumps [sinon](https://github.com/sinonjs/sinon) from 16.1.3 to 17.0.1. - [Release notes](https://github.com/sinonjs/sinon/releases) - [Changelog](https://github.com/sinonjs/sinon/blob/main/docs/changelog.md) - [Commits](https://github.com/sinonjs/sinon/compare/v16.1.3...v17.0.1) --- updated-dependencies: - dependency-name: sinon dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d79d0841df7..f092c147ee1 100644 --- a/package.json +++ b/package.json @@ -124,7 +124,7 @@ "proxy": "^1.0.2", "proxyquire": "^2.1.3", "semver": "^7.5.4", - "sinon": "^16.1.0", + "sinon": "^17.0.1", "snazzy": "^9.0.0", "standard": "^17.0.0", "table": "^6.8.0", From a393a86d09581945ce4e601d2359023e901b2dd0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 Nov 2023 13:33:54 +0100 Subject: [PATCH 245/259] build(deps): bump ossf/scorecard-action from 2.2.0 to 2.3.1 (#2396) Bumps [ossf/scorecard-action](https://github.com/ossf/scorecard-action) from 2.2.0 to 2.3.1. - [Release notes](https://github.com/ossf/scorecard-action/releases) - [Changelog](https://github.com/ossf/scorecard-action/blob/main/RELEASE.md) - [Commits](https://github.com/ossf/scorecard-action/compare/08b4669551908b1024bb425080c797723083c031...0864cf19026789058feabb7e87baa5f140aac736) --- updated-dependencies: - dependency-name: ossf/scorecard-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/scorecard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 91688cfced3..8b46ffa7c63 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -34,7 +34,7 @@ jobs: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@08b4669551908b1024bb425080c797723083c031 # v2.2.0 + uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 with: results_file: results.sarif results_format: sarif From 1f6d1597648d332c0705befec74387631d5df9ff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 Nov 2023 13:34:00 +0100 Subject: [PATCH 246/259] build(deps): bump actions/setup-node from 3.8.1 to 4.0.0 (#2395) Bumps [actions/setup-node](https://github.com/actions/setup-node) from 3.8.1 to 4.0.0. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d...8f152de45cc393bb48ce5d89d36b731f54556e65) --- updated-dependencies: - dependency-name: actions/setup-node dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/bench.yml | 4 ++-- .github/workflows/fuzz.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/publish-undici-types.yml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 200e72def08..281bdc62246 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -17,7 +17,7 @@ jobs: persist-credentials: false ref: ${{ github.base_ref }} - name: Setup Node - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 + uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0 with: node-version: lts/* - name: Install Modules @@ -34,7 +34,7 @@ jobs: with: persist-credentials: false - name: Setup Node - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 + uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0 with: node-version: lts/* - name: Install Modules diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index de608ceddfd..ff8d4b601c4 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -16,7 +16,7 @@ jobs: persist-credentials: false - name: Setup Node - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 + uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0 with: node-version: lts/* diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 7c3cf6f74c4..2eb2b6b5ed8 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -10,7 +10,7 @@ jobs: - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 with: persist-credentials: false - - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 + - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0 with: node-version: lts/* - run: npm install diff --git a/.github/workflows/publish-undici-types.yml b/.github/workflows/publish-undici-types.yml index 3579907a992..3f8fea32ce5 100644 --- a/.github/workflows/publish-undici-types.yml +++ b/.github/workflows/publish-undici-types.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 - - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 + - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0 with: node-version: '16.x' registry-url: 'https://registry.npmjs.org' From 97881779e6ba41d2fdbfe27b5c9cc0563dc60134 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 Nov 2023 13:34:13 +0100 Subject: [PATCH 247/259] build(deps): bump step-security/harden-runner from 2.5.0 to 2.6.0 (#2392) Bumps [step-security/harden-runner](https://github.com/step-security/harden-runner) from 2.5.0 to 2.6.0. - [Release notes](https://github.com/step-security/harden-runner/releases) - [Commits](https://github.com/step-security/harden-runner/compare/cba0d00b1fc9a034e1e642ea0f1103c282990604...1b05615854632b887b69ae1be8cbefe72d3ae423) --- updated-dependencies: - dependency-name: step-security/harden-runner dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 2 +- .github/workflows/dependency-review.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 0ef4cda43b5..3c44e6607fa 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -41,7 +41,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@cba0d00b1fc9a034e1e642ea0f1103c282990604 # v2.5.0 + uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v2.6.0 with: egress-policy: audit diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index f29ceb3ad53..0e356c7c203 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@cba0d00b1fc9a034e1e642ea0f1103c282990604 # v2.5.0 + uses: step-security/harden-runner@1b05615854632b887b69ae1be8cbefe72d3ae423 # v2.6.0 with: egress-policy: audit From 169c157f9a576e4422a20060f57db1dc4693b373 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 Nov 2023 13:34:29 +0100 Subject: [PATCH 248/259] build(deps-dev): bump formdata-node from 4.4.1 to 6.0.3 (#2389) Bumps [formdata-node](https://github.com/octet-stream/form-data) from 4.4.1 to 6.0.3. - [Release notes](https://github.com/octet-stream/form-data/releases) - [Changelog](https://github.com/octet-stream/form-data/blob/main/CHANGELOG.md) - [Commits](https://github.com/octet-stream/form-data/compare/v4.4.1...v6.0.3) --- updated-dependencies: - dependency-name: formdata-node dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f092c147ee1..fe1b2ef7134 100644 --- a/package.json +++ b/package.json @@ -110,7 +110,7 @@ "dns-packet": "^5.4.0", "docsify-cli": "^4.4.3", "form-data": "^4.0.0", - "formdata-node": "^4.3.1", + "formdata-node": "^6.0.3", "https-pem": "^3.0.0", "husky": "^8.0.1", "import-fresh": "^3.3.0", From fcdfe878d792c4347b81179bc31a2d1b1f06e8fb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 Nov 2023 13:35:26 +0100 Subject: [PATCH 249/259] build(deps): bump actions/upload-artifact from 3.1.2 to 3.1.3 (#2302) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3.1.2 to 3.1.3. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/0b7f8abb1508181956e8e162db84b466c27e18ce...a8a3f3ad30e3422c9c7b888a15615d19a852ae32) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/fuzz.yml | 2 +- .github/workflows/scorecard.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index ff8d4b601c4..29d7490194d 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -29,7 +29,7 @@ jobs: run: | npm run fuzz - - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 + - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 if: ${{ failure() }} with: name: undici-fuzz-results-${{ github.sha }} diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 8b46ffa7c63..f52ad55549c 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -43,7 +43,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 + uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 with: name: SARIF file path: results.sarif From 9a14e5f32a118fa93e769cc15ae8de9de552f2e4 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Thu, 30 Nov 2023 16:40:41 +0100 Subject: [PATCH 250/259] Bumped v5.28.2 Signed-off-by: Matteo Collina --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fe1b2ef7134..2b64daf41e2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "5.28.1", + "version": "5.28.2", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { From d3aa574b1259c1d8d329a0f0f495ee82882b1458 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 5 Feb 2024 09:35:32 +0100 Subject: [PATCH 251/259] Merge pull request from GHSA-3787-6prv-h9w3 Signed-off-by: Matteo Collina --- lib/fetch/index.js | 3 +++ test/fetch/redirect-cross-origin-header.js | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/fetch/index.js b/lib/fetch/index.js index 17c3d87ea62..dea206965a9 100644 --- a/lib/fetch/index.js +++ b/lib/fetch/index.js @@ -1203,6 +1203,9 @@ function httpRedirectFetch (fetchParams, response) { // https://fetch.spec.whatwg.org/#cors-non-wildcard-request-header-name request.headersList.delete('authorization') + // https://fetch.spec.whatwg.org/#authentication-entries + request.headersList.delete('proxy-authorization', true) + // "Cookie" and "Host" are forbidden request-headers, which undici doesn't implement. request.headersList.delete('cookie') request.headersList.delete('host') diff --git a/test/fetch/redirect-cross-origin-header.js b/test/fetch/redirect-cross-origin-header.js index fca48c44ea0..8c3e674968b 100644 --- a/test/fetch/redirect-cross-origin-header.js +++ b/test/fetch/redirect-cross-origin-header.js @@ -6,11 +6,12 @@ const { once } = require('events') const { fetch } = require('../..') test('Cross-origin redirects clear forbidden headers', async (t) => { - t.plan(5) + t.plan(6) const server1 = createServer((req, res) => { t.equal(req.headers.cookie, undefined) t.equal(req.headers.authorization, undefined) + t.equal(req.headers['proxy-authorization'], undefined) res.end('redirected') }).listen(0) @@ -39,7 +40,8 @@ test('Cross-origin redirects clear forbidden headers', async (t) => { const res = await fetch(`http://localhost:${server2.address().port}`, { headers: { Authorization: 'test', - Cookie: 'ddd=dddd' + Cookie: 'ddd=dddd', + 'Proxy-Authorization': 'test' } }) From 8ec52cde66e288ea98f9f801c29e6e845bf4c5f1 Mon Sep 17 00:00:00 2001 From: SUZUKI Sosuke Date: Fri, 12 Jan 2024 01:02:07 +0900 Subject: [PATCH 252/259] Fix tests for Node.js v21 (#2609) --- test/mock-interceptor-unused-assertions.js | 57 ++++++++++++++++++++-- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/test/mock-interceptor-unused-assertions.js b/test/mock-interceptor-unused-assertions.js index bfa2275777f..120a2ddedba 100644 --- a/test/mock-interceptor-unused-assertions.js +++ b/test/mock-interceptor-unused-assertions.js @@ -3,6 +3,11 @@ const { test, beforeEach, afterEach } = require('tap') const { MockAgent, setGlobalDispatcher } = require('..') const PendingInterceptorsFormatter = require('../lib/mock/pending-interceptors-formatter') +const util = require('../lib/core/util') + +// Since Node.js v21 `console.table` rows are aligned to the left +// https://github.com/nodejs/node/pull/50135 +const tableRowsAlignedToLeft = util.nodeMajor >= 21 // Avoid colors in the output for inline snapshots. const pendingInterceptorsFormatter = new PendingInterceptorsFormatter({ disableColors: true }) @@ -40,7 +45,17 @@ test('1 pending interceptor', t => { const err = t.throws(() => mockAgentWithOneInterceptor().assertNoPendingInterceptors({ pendingInterceptorsFormatter })) - t.same(err.message, ` + t.same(err.message, tableRowsAlignedToLeft + ? ` +1 interceptor is pending: + +┌─────────┬────────┬───────────────────────┬──────┬─────────────┬────────────┬─────────────┬───────────┐ +│ (index) │ Method │ Origin │ Path │ Status code │ Persistent │ Invocations │ Remaining │ +├─────────┼────────┼───────────────────────┼──────┼─────────────┼────────────┼─────────────┼───────────┤ +│ 0 │ 'GET' │ 'https://example.com' │ '/' │ 200 │ '❌' │ 0 │ 1 │ +└─────────┴────────┴───────────────────────┴──────┴─────────────┴────────────┴─────────────┴───────────┘ +`.trim() + : ` 1 interceptor is pending: ┌─────────┬────────┬───────────────────────┬──────┬─────────────┬────────────┬─────────────┬───────────┐ @@ -61,7 +76,18 @@ test('2 pending interceptors', t => { .reply(204, 'OK') const err = t.throws(() => withTwoInterceptors.assertNoPendingInterceptors({ pendingInterceptorsFormatter })) - t.same(err.message, ` + t.same(err.message, tableRowsAlignedToLeft + ? ` +2 interceptors are pending: + +┌─────────┬────────┬──────────────────────────┬──────────────┬─────────────┬────────────┬─────────────┬───────────┐ +│ (index) │ Method │ Origin │ Path │ Status code │ Persistent │ Invocations │ Remaining │ +├─────────┼────────┼──────────────────────────┼──────────────┼─────────────┼────────────┼─────────────┼───────────┤ +│ 0 │ 'GET' │ 'https://example.com' │ '/' │ 200 │ '❌' │ 0 │ 1 │ +│ 1 │ 'GET' │ 'https://localhost:9999' │ '/some/path' │ 204 │ '❌' │ 0 │ 1 │ +└─────────┴────────┴──────────────────────────┴──────────────┴─────────────┴────────────┴─────────────┴───────────┘ +`.trim() + : ` 2 interceptors are pending: ┌─────────┬────────┬──────────────────────────┬──────────────┬─────────────┬────────────┬─────────────┬───────────┐ @@ -123,7 +149,20 @@ test('Variations of persist(), times(), and pending status', async t => { const err = t.throws(() => agent.assertNoPendingInterceptors({ pendingInterceptorsFormatter })) - t.same(err.message, ` + t.same(err.message, tableRowsAlignedToLeft + ? ` +4 interceptors are pending: + +┌─────────┬────────┬──────────────────────────┬──────────────────────┬─────────────┬────────────┬─────────────┬───────────┐ +│ (index) │ Method │ Origin │ Path │ Status code │ Persistent │ Invocations │ Remaining │ +├─────────┼────────┼──────────────────────────┼──────────────────────┼─────────────┼────────────┼─────────────┼───────────┤ +│ 0 │ 'GET' │ 'https://example.com' │ '/' │ 200 │ '❌' │ 0 │ 1 │ +│ 1 │ 'GET' │ 'https://localhost:9999' │ '/persistent/unused' │ 200 │ '✅' │ 0 │ Infinity │ +│ 2 │ 'GET' │ 'https://localhost:9999' │ '/times/partial' │ 200 │ '❌' │ 1 │ 4 │ +│ 3 │ 'GET' │ 'https://localhost:9999' │ '/times/unused' │ 200 │ '❌' │ 0 │ 2 │ +└─────────┴────────┴──────────────────────────┴──────────────────────┴─────────────┴────────────┴─────────────┴───────────┘ +`.trim() + : ` 4 interceptors are pending: ┌─────────┬────────┬──────────────────────────┬──────────────────────┬─────────────┬────────────┬─────────────┬───────────┐ @@ -172,7 +211,17 @@ test('defaults to rendering output with terminal color when process.env.CI is un const err = t.throws( () => mockAgentWithOneInterceptor().assertNoPendingInterceptors()) - t.same(err.message, ` + t.same(err.message, tableRowsAlignedToLeft + ? ` +1 interceptor is pending: + +┌─────────┬────────┬───────────────────────┬──────┬─────────────┬────────────┬─────────────┬───────────┐ +│ (index) │ Method │ Origin │ Path │ Status code │ Persistent │ Invocations │ Remaining │ +├─────────┼────────┼───────────────────────┼──────┼─────────────┼────────────┼─────────────┼───────────┤ +│ 0 │ \u001b[32m'GET'\u001b[39m │ \u001b[32m'https://example.com'\u001b[39m │ \u001b[32m'/'\u001b[39m │ \u001b[33m200\u001b[39m │ \u001b[32m'❌'\u001b[39m │ \u001b[33m0\u001b[39m │ \u001b[33m1\u001b[39m │ +└─────────┴────────┴───────────────────────┴──────┴─────────────┴────────────┴─────────────┴───────────┘ +`.trim() + : ` 1 interceptor is pending: ┌─────────┬────────┬───────────────────────┬──────┬─────────────┬────────────┬─────────────┬───────────┐ From 20c65b89f4fda588ebb3f2abf51c55726880820e Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Tue, 16 Jan 2024 19:34:08 +0100 Subject: [PATCH 253/259] Fix tests for Node.js v20.11.0 (#2618) Signed-off-by: Matteo Collina --- test/mock-interceptor-unused-assertions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/mock-interceptor-unused-assertions.js b/test/mock-interceptor-unused-assertions.js index 120a2ddedba..2fe8b824f9f 100644 --- a/test/mock-interceptor-unused-assertions.js +++ b/test/mock-interceptor-unused-assertions.js @@ -7,7 +7,7 @@ const util = require('../lib/core/util') // Since Node.js v21 `console.table` rows are aligned to the left // https://github.com/nodejs/node/pull/50135 -const tableRowsAlignedToLeft = util.nodeMajor >= 21 +const tableRowsAlignedToLeft = util.nodeMajor >= 21 || (util.nodeMajor === 20 && util.nodeMinor >= 11) // Avoid colors in the output for inline snapshots. const pendingInterceptorsFormatter = new PendingInterceptorsFormatter({ disableColors: true }) From e71cb4c88faae5670a129fde5552266afc2dbc39 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 5 Feb 2024 12:25:23 +0100 Subject: [PATCH 254/259] Bumped v5.28.3 Signed-off-by: Matteo Collina --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2b64daf41e2..15a94360d8d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "5.28.2", + "version": "5.28.3", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { From 0e9d54b2c2a5ec0b58937114c857a9ed9fe22d5b Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 5 Feb 2024 12:51:56 +0100 Subject: [PATCH 255/259] skip failing test due to Node.js changes Signed-off-by: Matteo Collina --- test/balanced-pool.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/balanced-pool.js b/test/balanced-pool.js index d20f926e931..297f207718b 100644 --- a/test/balanced-pool.js +++ b/test/balanced-pool.js @@ -2,7 +2,6 @@ const { test } = require('tap') const { BalancedPool, Pool, Client, errors } = require('..') -const { nodeMajor } = require('../lib/core/util') const { createServer } = require('http') const { promisify } = require('util') @@ -437,7 +436,7 @@ const cases = [ expectedRatios: [0.34, 0.34, 0.32], // Skip because the behavior of Node.js has changed - skip: nodeMajor >= 19 + skip: true }, // 8 From 723c4e728051aefd5eb5ae7193dfb18046009f83 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 5 Feb 2024 13:02:39 +0100 Subject: [PATCH 256/259] Revert "build(deps-dev): bump formdata-node from 4.4.1 to 6.0.3 (#2389)" This reverts commit 169c157f9a576e4422a20060f57db1dc4693b373. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 15a94360d8d..2c280f68dca 100644 --- a/package.json +++ b/package.json @@ -110,7 +110,7 @@ "dns-packet": "^5.4.0", "docsify-cli": "^4.4.3", "form-data": "^4.0.0", - "formdata-node": "^6.0.3", + "formdata-node": "^4.3.1", "https-pem": "^3.0.0", "husky": "^8.0.1", "import-fresh": "^3.3.0", From 64e3402da4e032e68de46acb52800c9a06aaea3f Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Tue, 2 Apr 2024 12:41:40 +0200 Subject: [PATCH 257/259] Merge pull request from GHSA-m4v8-wqvr-p9f7 Signed-off-by: Matteo Collina --- lib/core/constants.js | 118 +++++++++++++++++++++++++++ lib/core/util.js | 11 +++ lib/handler/RedirectHandler.js | 17 ++-- test/redirect-cross-origin-header.js | 51 ++++++++++++ 4 files changed, 191 insertions(+), 6 deletions(-) create mode 100644 lib/core/constants.js create mode 100644 test/redirect-cross-origin-header.js diff --git a/lib/core/constants.js b/lib/core/constants.js new file mode 100644 index 00000000000..6ec770dd533 --- /dev/null +++ b/lib/core/constants.js @@ -0,0 +1,118 @@ +'use strict' + +/** @type {Record} */ +const headerNameLowerCasedRecord = {} + +// https://developer.mozilla.org/docs/Web/HTTP/Headers +const wellknownHeaderNames = [ + 'Accept', + 'Accept-Encoding', + 'Accept-Language', + 'Accept-Ranges', + 'Access-Control-Allow-Credentials', + 'Access-Control-Allow-Headers', + 'Access-Control-Allow-Methods', + 'Access-Control-Allow-Origin', + 'Access-Control-Expose-Headers', + 'Access-Control-Max-Age', + 'Access-Control-Request-Headers', + 'Access-Control-Request-Method', + 'Age', + 'Allow', + 'Alt-Svc', + 'Alt-Used', + 'Authorization', + 'Cache-Control', + 'Clear-Site-Data', + 'Connection', + 'Content-Disposition', + 'Content-Encoding', + 'Content-Language', + 'Content-Length', + 'Content-Location', + 'Content-Range', + 'Content-Security-Policy', + 'Content-Security-Policy-Report-Only', + 'Content-Type', + 'Cookie', + 'Cross-Origin-Embedder-Policy', + 'Cross-Origin-Opener-Policy', + 'Cross-Origin-Resource-Policy', + 'Date', + 'Device-Memory', + 'Downlink', + 'ECT', + 'ETag', + 'Expect', + 'Expect-CT', + 'Expires', + 'Forwarded', + 'From', + 'Host', + 'If-Match', + 'If-Modified-Since', + 'If-None-Match', + 'If-Range', + 'If-Unmodified-Since', + 'Keep-Alive', + 'Last-Modified', + 'Link', + 'Location', + 'Max-Forwards', + 'Origin', + 'Permissions-Policy', + 'Pragma', + 'Proxy-Authenticate', + 'Proxy-Authorization', + 'RTT', + 'Range', + 'Referer', + 'Referrer-Policy', + 'Refresh', + 'Retry-After', + 'Sec-WebSocket-Accept', + 'Sec-WebSocket-Extensions', + 'Sec-WebSocket-Key', + 'Sec-WebSocket-Protocol', + 'Sec-WebSocket-Version', + 'Server', + 'Server-Timing', + 'Service-Worker-Allowed', + 'Service-Worker-Navigation-Preload', + 'Set-Cookie', + 'SourceMap', + 'Strict-Transport-Security', + 'Supports-Loading-Mode', + 'TE', + 'Timing-Allow-Origin', + 'Trailer', + 'Transfer-Encoding', + 'Upgrade', + 'Upgrade-Insecure-Requests', + 'User-Agent', + 'Vary', + 'Via', + 'WWW-Authenticate', + 'X-Content-Type-Options', + 'X-DNS-Prefetch-Control', + 'X-Frame-Options', + 'X-Permitted-Cross-Domain-Policies', + 'X-Powered-By', + 'X-Requested-With', + 'X-XSS-Protection' +] + +for (let i = 0; i < wellknownHeaderNames.length; ++i) { + const key = wellknownHeaderNames[i] + const lowerCasedKey = key.toLowerCase() + headerNameLowerCasedRecord[key] = headerNameLowerCasedRecord[lowerCasedKey] = + lowerCasedKey +} + +// Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`. +Object.setPrototypeOf(headerNameLowerCasedRecord, null) + +module.exports = { + wellknownHeaderNames, + headerNameLowerCasedRecord +} diff --git a/lib/core/util.js b/lib/core/util.js index 8d5450ba0c0..5741650ea2e 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -9,6 +9,7 @@ const { InvalidArgumentError } = require('./errors') const { Blob } = require('buffer') const nodeUtil = require('util') const { stringify } = require('querystring') +const { headerNameLowerCasedRecord } = require('./constants') const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(v => Number(v)) @@ -218,6 +219,15 @@ function parseKeepAliveTimeout (val) { return m ? parseInt(m[1], 10) * 1000 : null } +/** + * Retrieves a header name and returns its lowercase value. + * @param {string | Buffer} value Header name + * @returns {string} + */ +function headerNameToString (value) { + return headerNameLowerCasedRecord[value] || value.toLowerCase() +} + function parseHeaders (headers, obj = {}) { // For H2 support if (!Array.isArray(headers)) return headers @@ -489,6 +499,7 @@ module.exports = { isIterable, isAsyncIterable, isDestroyed, + headerNameToString, parseRawHeaders, parseHeaders, parseKeepAliveTimeout, diff --git a/lib/handler/RedirectHandler.js b/lib/handler/RedirectHandler.js index baca27ed147..933725fbd01 100644 --- a/lib/handler/RedirectHandler.js +++ b/lib/handler/RedirectHandler.js @@ -184,12 +184,17 @@ function parseLocation (statusCode, headers) { // https://tools.ietf.org/html/rfc7231#section-6.4.4 function shouldRemoveHeader (header, removeContent, unknownOrigin) { - return ( - (header.length === 4 && header.toString().toLowerCase() === 'host') || - (removeContent && header.toString().toLowerCase().indexOf('content-') === 0) || - (unknownOrigin && header.length === 13 && header.toString().toLowerCase() === 'authorization') || - (unknownOrigin && header.length === 6 && header.toString().toLowerCase() === 'cookie') - ) + if (header.length === 4) { + return util.headerNameToString(header) === 'host' + } + if (removeContent && util.headerNameToString(header).startsWith('content-')) { + return true + } + if (unknownOrigin && (header.length === 13 || header.length === 6 || header.length === 19)) { + const name = util.headerNameToString(header) + return name === 'authorization' || name === 'cookie' || name === 'proxy-authorization' + } + return false } // https://tools.ietf.org/html/rfc7231#section-6.4 diff --git a/test/redirect-cross-origin-header.js b/test/redirect-cross-origin-header.js new file mode 100644 index 00000000000..8313e00c019 --- /dev/null +++ b/test/redirect-cross-origin-header.js @@ -0,0 +1,51 @@ +'use strict' + +const { test } = require('tap') +const { createServer } = require('node:http') +const { once } = require('node:events') +const { request } = require('..') + +test('Cross-origin redirects clear forbidden headers', async (t) => { + t.plan(6) + + const server1 = createServer((req, res) => { + t.equal(req.headers.cookie, undefined) + t.equal(req.headers.authorization, undefined) + t.equal(req.headers['proxy-authorization'], undefined) + + res.end('redirected') + }).listen(0) + + const server2 = createServer((req, res) => { + t.equal(req.headers.authorization, 'test') + t.equal(req.headers.cookie, 'ddd=dddd') + + res.writeHead(302, { + ...req.headers, + Location: `http://localhost:${server1.address().port}` + }) + res.end() + }).listen(0) + + t.teardown(() => { + server1.close() + server2.close() + }) + + await Promise.all([ + once(server1, 'listening'), + once(server2, 'listening') + ]) + + const res = await request(`http://localhost:${server2.address().port}`, { + maxRedirections: 1, + headers: { + Authorization: 'test', + Cookie: 'ddd=dddd', + 'Proxy-Authorization': 'test' + } + }) + + const text = await res.body.text() + t.equal(text, 'redirected') +}) From 2b39440bd9ded841c93dd72138f3b1763ae26055 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Tue, 2 Apr 2024 12:43:48 +0200 Subject: [PATCH 258/259] Merge pull request from GHSA-9qxr-qj54-h672 Co-authored-by: uzlopak Signed-off-by: Matteo Collina --- benchmarks/fetch/bytes-match.mjs | 24 ++++ lib/fetch/util.js | 143 ++++++++++++++++------ test/fetch/integrity.js | 197 +++++++++++++++++++++++++++++++ test/fetch/util.js | 73 ++++++++++++ 4 files changed, 402 insertions(+), 35 deletions(-) create mode 100644 benchmarks/fetch/bytes-match.mjs diff --git a/benchmarks/fetch/bytes-match.mjs b/benchmarks/fetch/bytes-match.mjs new file mode 100644 index 00000000000..6c6b263499d --- /dev/null +++ b/benchmarks/fetch/bytes-match.mjs @@ -0,0 +1,24 @@ +import { createHash } from 'node:crypto' +import { bench, run } from 'mitata' +import { bytesMatch } from '../../lib/web/fetch/util.js' + +const body = Buffer.from('Hello world!') +const validSha256Base64 = `sha256-${createHash('sha256').update(body).digest('base64')}` +const invalidSha256Base64 = `sha256-${createHash('sha256').update(body).digest('base64')}` +const validSha256Base64Url = `sha256-${createHash('sha256').update(body).digest('base64url')}` +const invalidSha256Base64Url = `sha256-${createHash('sha256').update(body).digest('base64url')}` + +bench('bytesMatch valid sha256 and base64', () => { + bytesMatch(body, validSha256Base64) +}) +bench('bytesMatch invalid sha256 and base64', () => { + bytesMatch(body, invalidSha256Base64) +}) +bench('bytesMatch valid sha256 and base64url', () => { + bytesMatch(body, validSha256Base64Url) +}) +bench('bytesMatch invalid sha256 and base64url', () => { + bytesMatch(body, invalidSha256Base64Url) +}) + +await run() diff --git a/lib/fetch/util.js b/lib/fetch/util.js index b12142c7f42..ede4ec4897a 100644 --- a/lib/fetch/util.js +++ b/lib/fetch/util.js @@ -7,14 +7,18 @@ const { isBlobLike, toUSVString, ReadableStreamFrom } = require('../core/util') const assert = require('assert') const { isUint8Array } = require('util/types') +let supportedHashes = [] + // https://nodejs.org/api/crypto.html#determining-if-crypto-support-is-unavailable /** @type {import('crypto')|undefined} */ let crypto try { crypto = require('crypto') + const possibleRelevantHashes = ['sha256', 'sha384', 'sha512'] + supportedHashes = crypto.getHashes().filter((hash) => possibleRelevantHashes.includes(hash)) +/* c8 ignore next 3 */ } catch { - } function responseURL (response) { @@ -542,66 +546,56 @@ function bytesMatch (bytes, metadataList) { return true } - // 3. If parsedMetadata is the empty set, return true. + // 3. If response is not eligible for integrity validation, return false. + // TODO + + // 4. If parsedMetadata is the empty set, return true. if (parsedMetadata.length === 0) { return true } - // 4. Let metadata be the result of getting the strongest + // 5. Let metadata be the result of getting the strongest // metadata from parsedMetadata. - 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) + const strongest = getStrongestMetadata(parsedMetadata) + const metadata = filterMetadataListByAlgorithm(parsedMetadata, strongest) - // 5. For each item in metadata: + // 6. 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. - let expectedValue = item.hash + const expectedValue = item.hash // See https://github.com/web-platform-tests/wpt/commit/e4c5cc7a5e48093220528dfdd1c4012dc3837a0e // "be liberal with padding". This is annoying, and it's not even in the spec. - if (expectedValue.endsWith('==')) { - expectedValue = expectedValue.slice(0, -2) - } - // 3. Let actualValue be the result of applying algorithm to bytes. let actualValue = crypto.createHash(algorithm).update(bytes).digest('base64') - if (actualValue.endsWith('==')) { - actualValue = actualValue.slice(0, -2) + if (actualValue[actualValue.length - 1] === '=') { + if (actualValue[actualValue.length - 2] === '=') { + actualValue = actualValue.slice(0, -2) + } else { + actualValue = actualValue.slice(0, -1) + } } // 4. If actualValue is a case-sensitive match for expectedValue, // return true. - if (actualValue === expectedValue) { - return true - } - - let actualBase64URL = crypto.createHash(algorithm).update(bytes).digest('base64url') - - if (actualBase64URL.endsWith('==')) { - actualBase64URL = actualBase64URL.slice(0, -2) - } - - if (actualBase64URL === expectedValue) { + if (compareBase64Mixed(actualValue, expectedValue)) { return true } } - // 6. Return false. + // 7. Return false. return false } // https://w3c.github.io/webappsec-subresource-integrity/#grammardef-hash-with-options // 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 +const parseHashWithOptions = /(?sha256|sha384|sha512)-((?[A-Za-z0-9+/]+|[A-Za-z0-9_-]+)={0,2}(?:\s|$)( +[!-~]*)?)?/i /** * @see https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata @@ -615,8 +609,6 @@ function parseMetadata (metadata) { // 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. @@ -626,7 +618,11 @@ function parseMetadata (metadata) { const parsedToken = parseHashWithOptions.exec(token) // 3. If token does not parse, continue to the next token. - if (parsedToken === null || parsedToken.groups === undefined) { + if ( + parsedToken === null || + parsedToken.groups === undefined || + parsedToken.groups.algo === 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 @@ -635,11 +631,11 @@ function parseMetadata (metadata) { } // 4. Let algorithm be the hash-algo component of token. - const algorithm = parsedToken.groups.algo + const algorithm = parsedToken.groups.algo.toLowerCase() // 5. If algorithm is a hash function recognized by the user // agent, add the parsed token to result. - if (supportedHashes.includes(algorithm.toLowerCase())) { + if (supportedHashes.includes(algorithm)) { result.push(parsedToken.groups) } } @@ -652,6 +648,82 @@ function parseMetadata (metadata) { return result } +/** + * @param {{ algo: 'sha256' | 'sha384' | 'sha512' }[]} metadataList + */ +function getStrongestMetadata (metadataList) { + // Let algorithm be the algo component of the first item in metadataList. + // Can be sha256 + let algorithm = metadataList[0].algo + // If the algorithm is sha512, then it is the strongest + // and we can return immediately + if (algorithm[3] === '5') { + return algorithm + } + + for (let i = 1; i < metadataList.length; ++i) { + const metadata = metadataList[i] + // If the algorithm is sha512, then it is the strongest + // and we can break the loop immediately + if (metadata.algo[3] === '5') { + algorithm = 'sha512' + break + // If the algorithm is sha384, then a potential sha256 or sha384 is ignored + } else if (algorithm[3] === '3') { + continue + // algorithm is sha256, check if algorithm is sha384 and if so, set it as + // the strongest + } else if (metadata.algo[3] === '3') { + algorithm = 'sha384' + } + } + return algorithm +} + +function filterMetadataListByAlgorithm (metadataList, algorithm) { + if (metadataList.length === 1) { + return metadataList + } + + let pos = 0 + for (let i = 0; i < metadataList.length; ++i) { + if (metadataList[i].algo === algorithm) { + metadataList[pos++] = metadataList[i] + } + } + + metadataList.length = pos + + return metadataList +} + +/** + * Compares two base64 strings, allowing for base64url + * in the second string. + * +* @param {string} actualValue always base64 + * @param {string} expectedValue base64 or base64url + * @returns {boolean} + */ +function compareBase64Mixed (actualValue, expectedValue) { + if (actualValue.length !== expectedValue.length) { + return false + } + for (let i = 0; i < actualValue.length; ++i) { + if (actualValue[i] !== expectedValue[i]) { + if ( + (actualValue[i] === '+' && expectedValue[i] === '-') || + (actualValue[i] === '/' && expectedValue[i] === '_') + ) { + continue + } + return false + } + } + + return true +} + // https://w3c.github.io/webappsec-upgrade-insecure-requests/#upgrade-request function tryUpgradeRequestToAPotentiallyTrustworthyURL (request) { // TODO @@ -1067,5 +1139,6 @@ module.exports = { urlHasHttpsScheme, urlIsHttpHttpsScheme, readAllBytes, - normalizeMethodRecord + normalizeMethodRecord, + parseMetadata } diff --git a/test/fetch/integrity.js b/test/fetch/integrity.js index f91f69314ee..90af8918cc5 100644 --- a/test/fetch/integrity.js +++ b/test/fetch/integrity.js @@ -4,6 +4,16 @@ const { test } = require('tap') const { createServer } = require('http') const { createHash, getHashes } = require('crypto') const { gzipSync } = require('zlib') +/* +======= +const { test, after } = require('node:test') +const { tspl } = require('@matteo.collina/tspl') +const assert = require('node:assert') +const { createServer } = require('node:http') +const { createHash, getHashes } = require('node:crypto') +const { gzipSync } = require('node:zlib') +>>>>>>> d542b8cd (Merge pull request from GHSA-9qxr-qj54-h672) +*/ const { fetch, setGlobalDispatcher, Agent } = require('../..') const { once } = require('events') @@ -148,3 +158,190 @@ test('request with sha512 hash', { skip: !supportedHashes.includes('sha512') }, integrity: 'sha512-ypeBEsobvcr6wjGzmiPcTaeG7/gUfE5yuYB3ha/uSLs=' })) }) + +test('request with correct integrity checksum (base64url)', (t) => { + t.plan(1) + const body = 'Hello world!' + const hash = createHash('sha256').update(body).digest('base64url') + + const server = createServer((req, res) => { + res.end(body) + }) + + t.teardown(server.close.bind(server)) + + server.listen(0, async () => { + const response = await fetch(`http://localhost:${server.address().port}`, { + integrity: `sha256-${hash}` + }) + t.equal(body, await response.text()) + }) +}) + +test('request with incorrect integrity checksum (base64url)', (t) => { + t.plan(1) + + const body = 'Hello world!' + const hash = createHash('sha256').update('invalid').digest('base64url') + + const server = createServer((req, res) => { + res.end(body) + }) + + t.teardown(server.close.bind(server)) + + server.listen(0, async () => { + await t.rejects(fetch(`http://localhost:${server.address().port}`, { + integrity: `sha256-${hash}` + })) + }) +}) + +test('request with incorrect integrity checksum (only dash)', (t) => { + t.plan(1) + + const body = 'Hello world!' + + const server = createServer((req, res) => { + res.end(body) + }) + + t.teardown(server.close.bind(server)) + + server.listen(0, async () => { + await t.rejects(fetch(`http://localhost:${server.address().port}`, { + integrity: 'sha256--' + })) + }) +}) + +test('request with incorrect integrity checksum (non-ascii character)', (t) => { + t.plan(1) + + const body = 'Hello world!' + + const server = createServer((req, res) => { + res.end(body) + }) + + t.teardown(server.close.bind(server)) + + server.listen(0, async () => { + await t.rejects(() => fetch(`http://localhost:${server.address().port}`, { + integrity: 'sha256-ä' + })) + }) +}) + +test('request with incorrect stronger integrity checksum (non-ascii character)', (t) => { + t.plan(2) + + const body = 'Hello world!' + const sha256 = createHash('sha256').update(body).digest('base64') + const sha384 = 'ä' + + const server = createServer((req, res) => { + res.end(body) + }) + + t.teardown(server.close.bind(server)) + + server.listen(0, async () => { + await t.rejects(() => fetch(`http://localhost:${server.address().port}`, { + integrity: `sha256-${sha256} sha384-${sha384}` + })) + await t.rejects(() => fetch(`http://localhost:${server.address().port}`, { + integrity: `sha384-${sha384} sha256-${sha256}` + })) + }) +}) + +test('request with correct integrity checksum (base64). mixed', (t) => { + t.plan(6) + + const body = 'Hello world!' + const sha256 = createHash('sha256').update(body).digest('base64') + const sha384 = createHash('sha384').update(body).digest('base64') + const sha512 = createHash('sha512').update(body).digest('base64') + + const server = createServer((req, res) => { + res.end(body) + }) + + t.teardown(server.close.bind(server)) + + server.listen(0, async () => { + let response + response = await fetch(`http://localhost:${server.address().port}`, { + integrity: `sha256-${sha256} sha512-${sha512}` + }) + t.equal(body, await response.text()) + response = await fetch(`http://localhost:${server.address().port}`, { + integrity: `sha512-${sha512} sha256-${sha256}` + }) + + t.equal(body, await response.text()) + response = await fetch(`http://localhost:${server.address().port}`, { + integrity: `sha384-${sha384} sha512-${sha512}` + }) + t.equal(body, await response.text()) + response = await fetch(`http://localhost:${server.address().port}`, { + integrity: `sha384-${sha384} sha512-${sha512}` + }) + t.equal(body, await response.text()) + + response = await fetch(`http://localhost:${server.address().port}`, { + integrity: `sha256-${sha256} sha384-${sha384}` + }) + t.equal(body, await response.text()) + response = await fetch(`http://localhost:${server.address().port}`, { + integrity: `sha384-${sha384} sha256-${sha256}` + }) + t.equal(body, await response.text()) + }) +}) + +test('request with correct integrity checksum (base64url). mixed', (t) => { + t.plan(6) + + const body = 'Hello world!' + const sha256 = createHash('sha256').update(body).digest('base64url') + const sha384 = createHash('sha384').update(body).digest('base64url') + const sha512 = createHash('sha512').update(body).digest('base64url') + + const server = createServer((req, res) => { + res.end(body) + }) + + t.teardown(server.close.bind(server)) + + server.listen(0, async () => { + let response + response = await fetch(`http://localhost:${server.address().port}`, { + integrity: `sha256-${sha256} sha512-${sha512}` + }) + t.equal(body, await response.text()) + response = await fetch(`http://localhost:${server.address().port}`, { + integrity: `sha512-${sha512} sha256-${sha256}` + }) + + t.equal(body, await response.text()) + response = await fetch(`http://localhost:${server.address().port}`, { + integrity: `sha384-${sha384} sha512-${sha512}` + }) + t.equal(body, await response.text()) + response = await fetch(`http://localhost:${server.address().port}`, { + integrity: `sha384-${sha384} sha512-${sha512}` + }) + t.equal(body, await response.text()) + + response = await fetch(`http://localhost:${server.address().port}`, { + integrity: `sha256-${sha256} sha384-${sha384}` + }) + t.equal(body, await response.text()) + response = await fetch(`http://localhost:${server.address().port}`, { + integrity: `sha384-${sha384} sha256-${sha256}` + }) + t.equal(body, await response.text()) + }) +}) diff --git a/test/fetch/util.js b/test/fetch/util.js index 02b75bc7783..f728d8673ac 100644 --- a/test/fetch/util.js +++ b/test/fetch/util.js @@ -5,6 +5,7 @@ const { test } = t const util = require('../../lib/fetch/util') const { HeadersList } = require('../../lib/fetch/headers') +const { createHash } = require('crypto') test('responseURL', (t) => { t.plan(2) @@ -279,3 +280,75 @@ test('setRequestReferrerPolicyOnRedirect', nested => { t.equal(request.referrerPolicy, initial) }) }) + +test('parseMetadata', (t) => { + t.test('should parse valid metadata with option', (t) => { + const body = 'Hello world!' + const hash256 = createHash('sha256').update(body).digest('base64') + const hash384 = createHash('sha384').update(body).digest('base64') + const hash512 = createHash('sha512').update(body).digest('base64') + + const validMetadata = `sha256-${hash256} !@ sha384-${hash384} !@ sha512-${hash512} !@` + const result = util.parseMetadata(validMetadata) + + t.same(result, [ + { algo: 'sha256', hash: hash256.replace(/=/g, '') }, + { algo: 'sha384', hash: hash384.replace(/=/g, '') }, + { algo: 'sha512', hash: hash512.replace(/=/g, '') } + ]) + t.end() + }) + + t.test('should parse valid metadata with non ASCII chars option', (t) => { + const body = 'Hello world!' + const hash256 = createHash('sha256').update(body).digest('base64') + const hash384 = createHash('sha384').update(body).digest('base64') + const hash512 = createHash('sha512').update(body).digest('base64') + + const validMetadata = `sha256-${hash256} !© sha384-${hash384} !€ sha512-${hash512} !µ` + const result = util.parseMetadata(validMetadata) + + t.same(result, [ + { algo: 'sha256', hash: hash256.replace(/=/g, '') }, + { algo: 'sha384', hash: hash384.replace(/=/g, '') }, + { algo: 'sha512', hash: hash512.replace(/=/g, '') } + ]) + t.end() + }) + + t.test('should parse valid metadata without option', (t) => { + const body = 'Hello world!' + const hash256 = createHash('sha256').update(body).digest('base64') + const hash384 = createHash('sha384').update(body).digest('base64') + const hash512 = createHash('sha512').update(body).digest('base64') + + const validMetadata = `sha256-${hash256} sha384-${hash384} sha512-${hash512}` + const result = util.parseMetadata(validMetadata) + + t.same(result, [ + { algo: 'sha256', hash: hash256.replace(/=/g, '') }, + { algo: 'sha384', hash: hash384.replace(/=/g, '') }, + { algo: 'sha512', hash: hash512.replace(/=/g, '') } + ]) + t.end() + }) + + t.test('should set hash as undefined when invalid base64 chars are provided', (t) => { + const body = 'Hello world!' + const hash256 = createHash('sha256').update(body).digest('base64') + const invalidHash384 = 'zifp5hE1Xl5LQQqQz[]Bq/iaq9Wb6jVb//T7EfTmbXD2aEP5c2ZdJr9YTDfcTE1ZH+' + const hash512 = createHash('sha512').update(body).digest('base64') + + const validMetadata = `sha256-${hash256} sha384-${invalidHash384} sha512-${hash512}` + const result = util.parseMetadata(validMetadata) + + t.same(result, [ + { algo: 'sha256', hash: hash256.replace(/=/g, '') }, + { algo: 'sha384', hash: undefined }, + { algo: 'sha512', hash: hash512.replace(/=/g, '') } + ]) + t.end() + }) + + t.end() +}) From fb983069071f52e0a7ea0e71078459c765aae172 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Tue, 2 Apr 2024 17:35:50 +0100 Subject: [PATCH 259/259] Bumped v5.28.4 Signed-off-by: Matteo Collina --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2c280f68dca..65a2d9833cf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "5.28.3", + "version": "5.28.4", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": {