From d8d3c940e1ed4ae6b813f444664f48551e9c52f3 Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Wed, 11 Jan 2023 12:32:22 +0100 Subject: [PATCH 001/123] docs(Dispatcher): adjust documentation for reset flag (#1852) --- docs/api/Dispatcher.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/Dispatcher.md b/docs/api/Dispatcher.md index d9499bec6ed..54749964d67 100644 --- a/docs/api/Dispatcher.md +++ b/docs/api/Dispatcher.md @@ -192,7 +192,7 @@ Returns: `Boolean` - `false` if dispatcher is busy and further dispatch calls wo * **origin** `string | URL` * **path** `string` * **method** `string` -* **reset** `boolean` (optional) - Default: `false` - Indicates whether the request should attempt to create a long-living connection by sending the `connection: keep-alive` header, or close it immediately after response by sending `connection: close`. +* **reset** `boolean` (optional) - Default: `false` - If `false`, the request will attempt to create a long-living connection by sending the `connection: keep-alive` header,otherwise will attempt to close it immediately after response by sending `connection: close` within the request and closing the socket afterwards. * **body** `string | Buffer | Uint8Array | stream.Readable | Iterable | AsyncIterable | null` (optional) - Default: `null` * **headers** `UndiciHeaders | string[]` (optional) - Default: `null`. * **query** `Record | null` (optional) - Default: `null` - Query string params to be embedded in the request URL. Note that both keys and values of query are encoded using `encodeURIComponent`. If for some reason you need to send them unencoded, embed query params into path directly instead. From e926fa4245d12f8fa17a7ecfd80b55793d74f3c3 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Wed, 11 Jan 2023 12:14:29 +0000 Subject: [PATCH 002/123] Fix broken interceptor test (#1853) Signed-off-by: Matteo Collina Signed-off-by: Matteo Collina --- test/jest/interceptor.test.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/test/jest/interceptor.test.js b/test/jest/interceptor.test.js index c3a9bdeb682..73d70b7c9cd 100644 --- a/test/jest/interceptor.test.js +++ b/test/jest/interceptor.test.js @@ -5,6 +5,8 @@ const { Agent, request } = require('../../index') const DecoratorHandler = require('../../lib/handler/DecoratorHandler') /* global expect */ +const defaultOpts = { keepAliveTimeout: 10, keepAliveMaxTimeout: 10 } + describe('interceptors', () => { let server beforeEach(async () => { @@ -15,7 +17,7 @@ describe('interceptors', () => { await new Promise((resolve) => { server.listen(0, resolve) }) }) afterEach(async () => { - await server.close() + await new Promise((resolve) => server.close(resolve)) }) test('interceptors are applied on client from an agent', async () => { @@ -29,8 +31,7 @@ describe('interceptors', () => { } } - // await new Promise(resolve => server.listen(0, () => resolve())) - const opts = { interceptors: { Client: [buildInterceptor] } } + const opts = { interceptors: { Client: [buildInterceptor] }, ...defaultOpts } const agent = new Agent(opts) const origin = new URL(`http://localhost:${server.address().port}`) await Promise.all([ @@ -58,7 +59,7 @@ describe('interceptors', () => { } } - const opts = { interceptors: { Pool: [setHeaderInterceptor, assertHeaderInterceptor] } } + const opts = { interceptors: { Pool: [setHeaderInterceptor, assertHeaderInterceptor] }, ...defaultOpts } const agent = new Agent(opts) const origin = new URL(`http://localhost:${server.address().port}`) await request(origin, { dispatcher: agent, headers: [] }) @@ -90,7 +91,7 @@ describe('interceptors', () => { } } - const opts = { interceptors: { Agent: [assertHeaderInterceptor, clearResponseHeadersInterceptor] } } + const opts = { interceptors: { Agent: [assertHeaderInterceptor, clearResponseHeadersInterceptor] }, ...defaultOpts } const agent = new Agent(opts) const origin = new URL(`http://localhost:${server.address().port}`) await request(origin, { dispatcher: agent, headers: [] }) @@ -178,7 +179,7 @@ describe('interceptors with NtlmRequestHandler', () => { await new Promise((resolve) => { server.listen(0, resolve) }) }) afterEach(async () => { - await server.close() + await new Promise((resolve) => server.close(resolve)) }) test('Retry interceptor on Client will use the same socket', async () => { @@ -187,7 +188,7 @@ describe('interceptors with NtlmRequestHandler', () => { return dispatch(opts, new FakeNtlmRequestHandler(dispatch, opts, handler)) } } - const opts = { interceptors: { Client: [interceptor] } } + const opts = { interceptors: { Client: [interceptor] }, ...defaultOpts } const agent = new Agent(opts) const origin = new URL(`http://localhost:${server.address().port}`) const { statusCode } = await request(origin, { dispatcher: agent, headers: [] }) From 8c90b01a0ed0470d0b635b6bce9d17990f811246 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Wed, 11 Jan 2023 13:15:09 +0100 Subject: [PATCH 003/123] Bumped v5.15.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 a53b5d04399..44e214624fb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "5.14.0", + "version": "5.15.0", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { From d5af7ddfd4673694c314e88b61874d9ff437e22f Mon Sep 17 00:00:00 2001 From: Khafra Date: Thu, 12 Jan 2023 04:12:49 -0500 Subject: [PATCH 004/123] fix(websocket): simplify typedarray copying (#1854) * fix(websocket): simplify typedarray copying * fix: simplify even further * and again --- lib/websocket/websocket.js | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/lib/websocket/websocket.js b/lib/websocket/websocket.js index 366b6aa3e27..79c9be439c1 100644 --- a/lib/websocket/websocket.js +++ b/lib/websocket/websocket.js @@ -309,23 +309,14 @@ class WebSocket extends EventTarget { // not throw an exception must increase the bufferedAmount attribute // by the length of data’s buffer in bytes. - const ab = new ArrayBuffer(data.byteLength) + const ab = Buffer.from(data, data.byteOffset, data.byteLength) - if (Buffer.isBuffer(data)) { - // new Buffer signature is deprecated - Buffer.from(ab).set(data) - } else { - new data.constructor(ab).set(data) - } - - const value = Buffer.from(ab) - - const frame = new WebsocketFrameSend(value) + const frame = new WebsocketFrameSend(ab) const buffer = frame.createFrame(opcodes.BINARY) - this.#bufferedAmount += value.byteLength + this.#bufferedAmount += ab.byteLength socket.write(buffer, () => { - this.#bufferedAmount -= value.byteLength + this.#bufferedAmount -= ab.byteLength }) } else if (isBlobLike(data)) { // If the WebSocket connection is established, and the WebSocket From f9aa0f186c9b1bc2cf2f492159da7c07113198d2 Mon Sep 17 00:00:00 2001 From: Khafra Date: Tue, 17 Jan 2023 13:45:55 -0500 Subject: [PATCH 005/123] fix: wpts on node v18.13.0+ (#1859) --- lib/fetch/formdata.js | 2 +- test/wpt/runner/runner/worker.mjs | 3 ++- test/wpt/status/FileAPI.status.json | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/fetch/formdata.js b/lib/fetch/formdata.js index a0c9e2f9373..a957a80e02a 100644 --- a/lib/fetch/formdata.js +++ b/lib/fetch/formdata.js @@ -259,7 +259,7 @@ function makeEntry (name, value, filename) { lastModified: value.lastModified } - value = value instanceof File + value = (NativeFile && value instanceof NativeFile) || value instanceof UndiciFile ? new File([value], filename, options) : new FileLike(value, filename, options) } diff --git a/test/wpt/runner/runner/worker.mjs b/test/wpt/runner/runner/worker.mjs index bf50dca4396..6be596c3b69 100644 --- a/test/wpt/runner/runner/worker.mjs +++ b/test/wpt/runner/runner/worker.mjs @@ -2,6 +2,7 @@ import { join } from 'node:path' import { runInThisContext } from 'node:vm' import { parentPort, workerData } from 'node:worker_threads' import { readFileSync } from 'node:fs' +import buffer from 'node:buffer' import { setGlobalOrigin, Response, @@ -34,7 +35,7 @@ Object.defineProperties(globalThis, { }, File: { ...globalPropertyDescriptors, - value: File + value: buffer.File ?? File }, FormData: { ...globalPropertyDescriptors, diff --git a/test/wpt/status/FileAPI.status.json b/test/wpt/status/FileAPI.status.json index d0d9825c6ef..844608acd9a 100644 --- a/test/wpt/status/FileAPI.status.json +++ b/test/wpt/status/FileAPI.status.json @@ -1,6 +1,6 @@ { "File-constructor.any.js": { - "fail": [ + "flaky": [ "Using type in File constructor: nonparsable" ] }, From 61246a5e61f004c78d36bd39efbd348c24e1a3e8 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Tue, 17 Jan 2023 23:45:13 +0100 Subject: [PATCH 006/123] perf: allow keep alive for HEAD requests (#1858) --- lib/client.js | 17 +++++--- lib/core/request.js | 2 +- test/client-head-reset-override.js | 62 ++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 6 deletions(-) create mode 100644 test/client-head-reset-override.js diff --git a/lib/client.js b/lib/client.js index 1625b47eafe..41fd4a753f8 100644 --- a/lib/client.js +++ b/lib/client.js @@ -441,6 +441,7 @@ class Parser { this.keepAlive = '' this.contentLength = '' + this.connection = '' this.maxResponseSize = client[kMaxResponseSize] } @@ -616,6 +617,8 @@ class Parser { const key = this.headers[len - 2] if (key.length === 10 && key.toString().toLowerCase() === 'keep-alive') { this.keepAlive += buf.toString() + } else if (key.length === 10 && key.toString().toLowerCase() === 'connection') { + this.connection += buf.toString() } else if (key.length === 14 && key.toString().toLowerCase() === 'content-length') { this.contentLength += buf.toString() } @@ -709,7 +712,11 @@ class Parser { assert.strictEqual(this.timeoutType, TIMEOUT_HEADERS) this.statusCode = statusCode - this.shouldKeepAlive = shouldKeepAlive + this.shouldKeepAlive = ( + shouldKeepAlive || + // Override llhttp value which does not allow keepAlive for HEAD. + (request.method === 'HEAD' && !socket[kReset] && this.connection.toLowerCase() === 'keep-alive') + ) if (this.statusCode >= 200) { const bodyTimeout = request.bodyTimeout != null @@ -739,7 +746,7 @@ class Parser { this.headers = [] this.headersSize = 0 - if (shouldKeepAlive && client[kPipelining]) { + if (this.shouldKeepAlive && client[kPipelining]) { const keepAliveTimeout = this.keepAlive ? util.parseKeepAliveTimeout(this.keepAlive) : null if (keepAliveTimeout != null) { @@ -769,7 +776,6 @@ class Parser { } if (request.method === 'HEAD') { - assert(socket[kReset]) return 1 } @@ -843,6 +849,7 @@ class Parser { this.bytesRead = 0 this.contentLength = '' this.keepAlive = '' + this.connection = '' assert(this.headers.length % 2 === 0) this.headers = [] @@ -1376,8 +1383,8 @@ function write (client, request) { socket[kReset] = true } - if (reset) { - socket[kReset] = true + if (reset != null) { + socket[kReset] = reset } if (client[kMaxRequests] && socket[kCounter]++ >= client[kMaxRequests]) { diff --git a/lib/core/request.js b/lib/core/request.js index e8a8f2a43dc..bee1a872960 100644 --- a/lib/core/request.js +++ b/lib/core/request.js @@ -144,7 +144,7 @@ class Request { this.blocking = blocking == null ? false : blocking - this.reset = reset == null ? false : reset + this.reset = reset == null ? null : reset this.host = null diff --git a/test/client-head-reset-override.js b/test/client-head-reset-override.js new file mode 100644 index 00000000000..a7d79e25d75 --- /dev/null +++ b/test/client-head-reset-override.js @@ -0,0 +1,62 @@ +'use strict' + +const { createServer } = require('http') +const { test } = require('tap') +const { Client } = require('..') + +test('override HEAD reset', (t) => { + const expected = 'testing123' + const server = createServer((req, res) => { + if (req.method === 'GET') { + res.write(expected) + } + res.end() + }) + t.teardown(server.close.bind(server)) + + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`) + t.teardown(client.close.bind(client)) + + let done + client.on('disconnect', () => { + if (!done) { + t.fail() + } + }) + + client.request({ + path: '/', + method: 'HEAD', + reset: false + }, (err, res) => { + t.error(err) + res.body.resume() + }) + + client.request({ + path: '/', + method: 'HEAD', + reset: false + }, (err, res) => { + t.error(err) + res.body.resume() + }) + + client.request({ + path: '/', + method: 'GET', + reset: false + }, (err, res) => { + t.error(err) + let str = '' + res.body.on('data', (data) => { + str += data + }).on('end', () => { + t.same(str, expected) + done = true + t.end() + }) + }) + }) +}) From 16c41280bc98bdce692a540e501eefbea90ebb65 Mon Sep 17 00:00:00 2001 From: Khafra Date: Wed, 18 Jan 2023 15:40:34 -0500 Subject: [PATCH 007/123] fix: flaky abort test (#1863) --- test/fetch/abort.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/fetch/abort.js b/test/fetch/abort.js index 3bb9efc8ba2..55c9b45d90a 100644 --- a/test/fetch/abort.js +++ b/test/fetch/abort.js @@ -90,9 +90,7 @@ test('Allow the usage of custom implementation of AbortController', async (t) => }) test('allows aborting with custom errors', { skip: process.version.startsWith('v16.') }, async (t) => { - const server = createServer((req, res) => { - setTimeout(() => res.end(), 5000) - }).listen(0) + const server = createServer().listen(0) t.teardown(server.close.bind(server)) await once(server, 'listening') From c9d4a6bd43334668fbb8da222aa3d694458527c5 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Thu, 19 Jan 2023 12:31:48 +0100 Subject: [PATCH 008/123] fix: flaky abort test --- test/fetch/abort.js | 53 -------------------------------------- test/fetch/abort2.js | 60 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 53 deletions(-) create mode 100644 test/fetch/abort2.js diff --git a/test/fetch/abort.js b/test/fetch/abort.js index 55c9b45d90a..03ee3b44ab2 100644 --- a/test/fetch/abort.js +++ b/test/fetch/abort.js @@ -8,59 +8,6 @@ const { DOMException } = require('../../lib/fetch/constants') const { AbortController: NPMAbortController } = require('abort-controller') -/* global AbortController */ - -test('parallel fetch with the same AbortController works as expected', async (t) => { - const body = { - fixes: 1389, - bug: 'Ensure request is not aborted before enqueueing bytes into stream.' - } - - const server = createServer((req, res) => { - res.statusCode = 200 - res.end(JSON.stringify(body)) - }) - - t.teardown(server.close.bind(server)) - - const abortController = new AbortController() - - async function makeRequest () { - const result = await fetch(`http://localhost:${server.address().port}`, { - signal: abortController.signal - }).then(response => response.json()) - - abortController.abort() - return result - } - - server.listen(0) - await once(server, 'listening') - - const requests = Array.from({ length: 10 }, makeRequest) - const result = await Promise.allSettled(requests) - - // since the requests are running parallel, any of them could resolve first. - // therefore we cannot rely on the order of the requests sent. - const { resolved, rejected } = result.reduce((a, b) => { - if (b.status === 'rejected') { - a.rejected.push(b) - } else { - a.resolved.push(b) - } - - return a - }, { resolved: [], rejected: [] }) - - t.equal(rejected.length, 9) // out of 10 requests, only 1 should succeed - t.equal(resolved.length, 1) - - t.ok(rejected.every(rej => rej.reason?.code === DOMException.ABORT_ERR)) - t.same(resolved[0].value, body) - - t.end() -}) - test('Allow the usage of custom implementation of AbortController', async (t) => { const body = { fixes: 1605 diff --git a/test/fetch/abort2.js b/test/fetch/abort2.js new file mode 100644 index 00000000000..5f3853bcb7a --- /dev/null +++ b/test/fetch/abort2.js @@ -0,0 +1,60 @@ +'use strict' + +const { test } = require('tap') +const { fetch } = require('../..') +const { createServer } = require('http') +const { once } = require('events') +const { DOMException } = require('../../lib/fetch/constants') + +/* global AbortController */ + +test('parallel fetch with the same AbortController works as expected', async (t) => { + const body = { + fixes: 1389, + bug: 'Ensure request is not aborted before enqueueing bytes into stream.' + } + + const server = createServer((req, res) => { + res.statusCode = 200 + res.end(JSON.stringify(body)) + }) + + t.teardown(server.close.bind(server)) + + const abortController = new AbortController() + + async function makeRequest () { + const result = await fetch(`http://localhost:${server.address().port}`, { + signal: abortController.signal + }).then(response => response.json()) + + abortController.abort() + return result + } + + server.listen(0) + await once(server, 'listening') + + const requests = Array.from({ length: 10 }, makeRequest) + const result = await Promise.allSettled(requests) + + // since the requests are running parallel, any of them could resolve first. + // therefore we cannot rely on the order of the requests sent. + const { resolved, rejected } = result.reduce((a, b) => { + if (b.status === 'rejected') { + a.rejected.push(b) + } else { + a.resolved.push(b) + } + + return a + }, { resolved: [], rejected: [] }) + + t.equal(rejected.length, 9) // out of 10 requests, only 1 should succeed + t.equal(resolved.length, 1) + + t.ok(rejected.every(rej => rej.reason?.code === DOMException.ABORT_ERR)) + t.same(resolved[0].value, body) + + t.end() +}) From 9d5f23177408dc16d3d4cbb8cebf463081c54e16 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Thu, 19 Jan 2023 12:44:11 +0100 Subject: [PATCH 009/123] 5.15.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 44e214624fb..02c3bf3857e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "5.15.0", + "version": "5.15.1", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { From 2d2512ce46dcc1a5d09c5823ac6e1ea5f99643c6 Mon Sep 17 00:00:00 2001 From: Khafra Date: Thu, 19 Jan 2023 15:13:56 -0500 Subject: [PATCH 010/123] fix: don't load websockets w/o crypto support (#1868) --- index.js | 10 +++++++++- lib/websocket/connection.js | 1 - 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 3e5be33f3e0..48d9beed1b8 100644 --- a/index.js +++ b/index.js @@ -24,6 +24,14 @@ const nodeVersion = process.versions.node.split('.') const nodeMajor = Number(nodeVersion[0]) const nodeMinor = Number(nodeVersion[1]) +let hasCrypto +try { + require('crypto') + hasCrypto = true +} catch { + hasCrypto = false +} + Object.assign(Dispatcher.prototype, api) module.exports.Dispatcher = Dispatcher @@ -128,7 +136,7 @@ if (nodeMajor >= 16) { module.exports.setCookie = setCookie } -if (nodeMajor >= 18) { +if (nodeMajor >= 18 && hasCrypto) { const { WebSocket } = require('./lib/websocket/websocket') module.exports.WebSocket = WebSocket diff --git a/lib/websocket/connection.js b/lib/websocket/connection.js index 43ecc1445f8..df8e551c26e 100644 --- a/lib/websocket/connection.js +++ b/lib/websocket/connection.js @@ -1,6 +1,5 @@ 'use strict' -// TODO: crypto isn't available in all environments const { randomBytes, createHash } = require('crypto') const diagnosticsChannel = require('diagnostics_channel') const { uid, states } = require('./constants') From 22d2f70f717a016812f5bc9d208f1f1f5ba6a25e Mon Sep 17 00:00:00 2001 From: Kristoffer K Date: Fri, 20 Jan 2023 00:11:10 +0100 Subject: [PATCH 011/123] fix: don't include websockets in node bundle (#1869) --- lib/fetch/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/fetch/index.js b/lib/fetch/index.js index ddcf3c18f91..eb706b557ba 100644 --- a/lib/fetch/index.js +++ b/lib/fetch/index.js @@ -56,7 +56,7 @@ const { Readable, pipeline } = require('stream') const { isErrored, isReadable } = require('../core/util') const { dataURLProcessor, serializeAMimeType } = require('./dataURL') const { TransformStream } = require('stream/web') -const { getGlobalDispatcher } = require('../../index') +const { getGlobalDispatcher } = require('../global') const { webidl } = require('./webidl') const { STATUS_CODES } = require('http') From f61a902fcf138e0b13c68ab7496ee0c15e708f3a Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Fri, 20 Jan 2023 10:07:47 +0100 Subject: [PATCH 012/123] fix: don't set socket on client before init Refs: https://github.com/nodejs/undici/discussions/1633 --- lib/client.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/client.js b/lib/client.js index 41fd4a753f8..1317fc4d686 100644 --- a/lib/client.js +++ b/lib/client.js @@ -1074,8 +1074,6 @@ async function connect (client) { assert(socket) - client[kSocket] = socket - socket[kNoRef] = false socket[kWriting] = false socket[kReset] = false @@ -1091,6 +1089,8 @@ async function connect (client) { .on('end', onSocketEnd) .on('close', onSocketClose) + client[kSocket] = socket + if (channels.connected.hasSubscribers) { channels.connected.publish({ connectParams: { @@ -1176,7 +1176,7 @@ function _resume (client, sync) { const socket = client[kSocket] - if (socket) { + if (socket && !socket.destroyed) { if (client[kSize] === 0) { if (!socket[kNoRef] && socket.unref) { socket.unref() @@ -1243,7 +1243,7 @@ function _resume (client, sync) { if (!socket) { connect(client) - continue + return } if (socket.destroyed || socket[kWriting] || socket[kReset] || socket[kBlocking]) { From d6ba9e5b10747c77c9cbddeaf912b4fae47d8ac9 Mon Sep 17 00:00:00 2001 From: Sean Kelly Date: Fri, 20 Jan 2023 09:29:38 -0800 Subject: [PATCH 013/123] Fix fetch parameters not being applied correctly (#1870) * Fix default fetch parameters * Preserve existing behavior with 300 second timeout if not defined * Add test for 300 second timeout as default * Cleanup old unused tests * Simplify how fetch utilizes timeouts from agent --- lib/fetch/index.js | 2 -- test/fetch/fetch-timeouts.js | 49 +++++++++++++++++++++++++++++++++ test/node-fetch/main.js | 16 ----------- test/node-fetch/utils/server.js | 16 ----------- 4 files changed, 49 insertions(+), 34 deletions(-) create mode 100644 test/fetch/fetch-timeouts.js diff --git a/lib/fetch/index.js b/lib/fetch/index.js index eb706b557ba..ae60a5af57c 100644 --- a/lib/fetch/index.js +++ b/lib/fetch/index.js @@ -1951,8 +1951,6 @@ async function httpNetworkFetch ( body: fetchParams.controller.dispatcher.isMockActive ? request.body && request.body.source : body, headers: request.headersList[kHeadersCaseInsensitive], maxRedirections: 0, - bodyTimeout: 300_000, - headersTimeout: 300_000, upgrade: request.mode === 'websocket' ? 'websocket' : undefined }, { diff --git a/test/fetch/fetch-timeouts.js b/test/fetch/fetch-timeouts.js new file mode 100644 index 00000000000..adbf888ebba --- /dev/null +++ b/test/fetch/fetch-timeouts.js @@ -0,0 +1,49 @@ +'use strict' + +const { test } = require('tap') + +const { fetch, Agent } = require('../..') +const { createServer } = require('http') +const FakeTimers = require('@sinonjs/fake-timers') + +test('Fetch very long request, timeout overridden so no error', (t) => { + const minutes = 6 + const msToDelay = 1000 * 60 * minutes + + t.setTimeout(undefined) + t.plan(1) + + const clock = FakeTimers.install() + t.teardown(clock.uninstall.bind(clock)) + + const server = createServer((req, res) => { + setTimeout(() => { + res.end('hello') + }, msToDelay) + clock.tick(msToDelay + 1) + }) + t.teardown(server.close.bind(server)) + + server.listen(0, () => { + fetch(`http://localhost:${server.address().port}`, { + path: '/', + method: 'GET', + dispatcher: new Agent({ + headersTimeout: 0, + connectTimeout: 0, + bodyTimeout: 0 + }) + }) + .then((response) => response.text()) + .then((response) => { + t.equal('hello', response) + t.end() + }) + .catch((err) => { + // This should not happen, a timeout error should not occur + t.error(err) + }) + + clock.tick(msToDelay - 1) + }) +}) diff --git a/test/node-fetch/main.js b/test/node-fetch/main.js index acefe825ea4..ab3ee9f70ae 100644 --- a/test/node-fetch/main.js +++ b/test/node-fetch/main.js @@ -1660,20 +1660,4 @@ describe('node-fetch', () => { expect(res.ok).to.be.false }) }) - - // it('should not time out waiting for a response 60 seconds', function () { - // this.timeout(65_000) - // return fetch(`${base}timeout60s`).then(res => { - // expect(res.status).to.equal(200) - // expect(res.ok).to.be.true - // return res.text().then(result => { - // expect(result).to.equal('text') - // }) - // }) - // }) - - // it('should time out waiting for more than 300 seconds', function () { - // this.timeout(305_000) - // return expect(fetch(`${base}timeout300s`)).to.eventually.be.rejectedWith(TypeError) - // }) }) diff --git a/test/node-fetch/utils/server.js b/test/node-fetch/utils/server.js index e846465dbf7..46103055305 100644 --- a/test/node-fetch/utils/server.js +++ b/test/node-fetch/utils/server.js @@ -227,22 +227,6 @@ module.exports = class TestServer { }, 1000) } - if (p === '/timeout60s') { - setTimeout(() => { - res.statusCode = 200 - res.setHeader('Content-Type', 'text/plain') - res.end('text') - }, 60_000) - } - - if (p === '/timeout300s') { - setTimeout(() => { - res.statusCode = 200 - res.setHeader('Content-Type', 'text/plain') - res.end('text') - }, 300_000) - } - if (p === '/slow') { res.statusCode = 200 res.setHeader('Content-Type', 'text/plain') From 5883598a02d57892439c5fe476dd4288dca05220 Mon Sep 17 00:00:00 2001 From: Debadree Chatterjee Date: Sat, 21 Jan 2023 00:35:49 +0530 Subject: [PATCH 014/123] fix: typo in contributing.md (#1874) --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d5be39b6dbe..7aec65f8164 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,7 +50,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 18134081b1058fcaf62e5e689fcf92d91394dc0f Mon Sep 17 00:00:00 2001 From: Carlos Fuentes Date: Fri, 20 Jan 2023 20:08:09 +0100 Subject: [PATCH 015/123] chore(performance#38): Performance improvements (#1871) --- lib/fetch/dataURL.js | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/lib/fetch/dataURL.js b/lib/fetch/dataURL.js index 74b6d9ec472..45a058376ff 100644 --- a/lib/fetch/dataURL.js +++ b/lib/fetch/dataURL.js @@ -5,6 +5,12 @@ const { isValidHTTPToken, isomorphicDecode } = require('./util') const encoder = new TextEncoder() +// Regex +const HTTP_TOKEN_CODEPOINTS = /^[!#$%&'*+-.^_|~A-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 + // https://fetch.spec.whatwg.org/#data-url-processor /** @param {URL} dataURL */ function dataURLProcessor (dataURL) { @@ -217,7 +223,7 @@ function parseMIMEType (input) { // 4. If type is the empty string or does not solely // contain HTTP token code points, then return failure. // https://mimesniff.spec.whatwg.org/#http-token-code-point - if (type.length === 0 || !/^[!#$%&'*+-.^_|~A-z0-9]+$/.test(type)) { + if (type.length === 0 || !HTTP_TOKEN_CODEPOINTS.test(type)) { return 'failure' } @@ -244,7 +250,7 @@ function parseMIMEType (input) { // 9. If subtype is the empty string or does not solely // contain HTTP token code points, then return failure. - if (subtype.length === 0 || !/^[!#$%&'*+-.^_|~A-z0-9]+$/.test(subtype)) { + if (subtype.length === 0 || !HTTP_TOKEN_CODEPOINTS.test(subtype)) { return 'failure' } @@ -258,9 +264,7 @@ function parseMIMEType (input) { /** @type {Map} */ parameters: new Map(), // https://mimesniff.spec.whatwg.org/#mime-type-essence - get essence () { - return `${this.type}/${this.subtype}` - } + essence: `${type}/${subtype}` } // 11. While position is not past the end of input: @@ -272,7 +276,7 @@ function parseMIMEType (input) { // whitespace from input given position. collectASequenceOfCodePoints( // https://fetch.spec.whatwg.org/#http-whitespace - (char) => /(\u000A|\u000D|\u0009|\u0020)/.test(char), // eslint-disable-line + char => HTTP_WHITESPACE_REGEX.test(char), input, position ) @@ -355,9 +359,8 @@ function parseMIMEType (input) { // then set mimeType’s parameters[parameterName] to parameterValue. if ( parameterName.length !== 0 && - /^[!#$%&'*+-.^_|~A-z0-9]+$/.test(parameterName) && - // https://mimesniff.spec.whatwg.org/#http-quoted-string-token-code-point - !/^(\u0009|\x{0020}-\x{007E}|\x{0080}-\x{00FF})+$/.test(parameterValue) && // eslint-disable-line + HTTP_TOKEN_CODEPOINTS.test(parameterName) && + !HTTP_QUOTED_STRING_TOKENS.test(parameterValue) && !mimeType.parameters.has(parameterName) ) { mimeType.parameters.set(parameterName, parameterValue) From d7602f6258ce0e50b83cf7eb07d159af5bd4bb7e Mon Sep 17 00:00:00 2001 From: Mateo Nunez Date: Sat, 21 Jan 2023 14:32:40 +0100 Subject: [PATCH 016/123] perf(util): improve isFormDataLike checks (#1875) --- lib/core/util.js | 17 ++++++++- package.json | 1 + test/fetch/formdata.js | 80 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 1 deletion(-) diff --git a/lib/core/util.js b/lib/core/util.js index c2dcf79fb80..6f38ffc8a4d 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -354,8 +354,23 @@ function ReadableStreamFrom (iterable) { ) } +// The chunk should be a FormData instance and contains +// all the required methods. function isFormDataLike (chunk) { - return chunk && chunk.constructor && chunk.constructor.name === 'FormData' + return (chunk && + chunk.constructor && chunk.constructor.name === 'FormData' && + typeof chunk === 'object' && + (typeof chunk.append === 'function' && + typeof chunk.delete === 'function' && + typeof chunk.get === 'function' && + typeof chunk.getAll === 'function' && + typeof chunk.has === 'function' && + typeof chunk.set === 'function' && + typeof chunk.entries === 'function' && + typeof chunk.keys === 'function' && + typeof chunk.values === 'function' && + typeof chunk.forEach === 'function') + ) } const kEnumerableProperty = Object.create(null) diff --git a/package.json b/package.json index 02c3bf3857e..543ab94c7ad 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "cronometro": "^1.0.5", "delay": "^5.0.0", "docsify-cli": "^4.4.3", + "form-data": "^4.0.0", "formdata-node": "^4.3.1", "https-pem": "^3.0.0", "husky": "^8.0.1", diff --git a/test/fetch/formdata.js b/test/fetch/formdata.js index e91893f1aa6..156c11fb77d 100644 --- a/test/fetch/formdata.js +++ b/test/fetch/formdata.js @@ -4,6 +4,8 @@ const { test } = require('tap') const { FormData, File, Response } = require('../../') const { Blob: ThirdPartyBlob } = require('formdata-node') const { Blob } = require('buffer') +const { isFormDataLike } = require('../../lib/core/util') +const ThirdPartyFormDataInvalid = require('form-data') test('arg validation', (t) => { const form = new FormData() @@ -285,6 +287,84 @@ test('formData.constructor.name', (t) => { t.end() }) +test('formData should be an instance of FormData', (t) => { + t.plan(3) + + t.test('Invalid class FormData', (t) => { + class FormData { + constructor () { + this.data = [] + } + + append (key, value) { + this.data.push([key, value]) + } + + get (key) { + return this.data.find(([k]) => k === key) + } + } + + const form = new FormData() + t.equal(isFormDataLike(form), false) + t.end() + }) + + t.test('Invalid function FormData', (t) => { + function FormData () { + const data = [] + return { + append (key, value) { + data.push([key, value]) + }, + get (key) { + return data.find(([k]) => k === key) + } + } + } + + const form = new FormData() + t.equal(isFormDataLike(form), false) + t.end() + }) + + test('Invalid third-party FormData', (t) => { + const form = new ThirdPartyFormDataInvalid() + t.equal(isFormDataLike(form), false) + t.end() + }) + + t.test('Valid FormData', (t) => { + const form = new FormData() + t.equal(isFormDataLike(form), true) + t.end() + }) +}) + +test('FormData should be compatible with third-party libraries', (t) => { + t.plan(1) + + class FormData { + constructor () { + this.data = [] + } + + append () {} + delete () {} + get () {} + getAll () {} + has () {} + set () {} + entries () {} + keys () {} + values () {} + forEach () {} + } + + const form = new FormData() + t.equal(isFormDataLike(form), true) +}) + test('arguments', (t) => { t.equal(FormData.constructor.length, 1) t.equal(FormData.prototype.append.length, 2) From 9457c9719029945ef9ff36b71d58557443730942 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Sun, 22 Jan 2023 10:26:09 +0100 Subject: [PATCH 017/123] 5.15.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 543ab94c7ad..bfbbe4c2eee 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "5.15.1", + "version": "5.15.2", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { From 69ede10981ba8fa8a45bf2886a0c9c1644b4ef4e Mon Sep 17 00:00:00 2001 From: Sebastian Mayr Date: Sun, 22 Jan 2023 17:14:18 -0500 Subject: [PATCH 018/123] Add feature to specify custom headers for proxies (#1877) --- lib/proxy-agent.js | 2 +- test/proxy-agent.js | 33 +++++++++++++++++++++++++++++++++ types/proxy-agent.d.ts | 3 +++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/lib/proxy-agent.js b/lib/proxy-agent.js index c2e7f268b70..128daddbef2 100644 --- a/lib/proxy-agent.js +++ b/lib/proxy-agent.js @@ -53,7 +53,7 @@ class ProxyAgent extends DispatcherBase { this[kRequestTls] = opts.requestTls this[kProxyTls] = opts.proxyTls - this[kProxyHeaders] = {} + this[kProxyHeaders] = opts.headers || {} if (opts.auth && opts.token) { throw new InvalidArgumentError('opts.auth cannot be used in combination with opts.token') diff --git a/test/proxy-agent.js b/test/proxy-agent.js index 03156395eff..8c6b5999278 100644 --- a/test/proxy-agent.js +++ b/test/proxy-agent.js @@ -197,6 +197,39 @@ test('use proxy-agent with token', async (t) => { proxyAgent.close() }) +test('use proxy-agent with custom headers', async (t) => { + t.plan(2) + const server = await buildServer() + const proxy = await buildProxy() + + const serverUrl = `http://localhost:${server.address().port}` + const proxyUrl = `http://localhost:${proxy.address().port}` + const proxyAgent = new ProxyAgent({ + uri: proxyUrl, + headers: { + 'User-Agent': 'Foobar/1.0.0' + } + }) + + proxy.on('connect', (req) => { + t.equal(req.headers['user-agent'], 'Foobar/1.0.0') + }) + + server.on('request', (req, res) => { + t.equal(req.headers['user-agent'], 'BarBaz/1.0.0') + res.end() + }) + + await request(serverUrl + '/hello?foo=bar', { + headers: { 'user-agent': 'BarBaz/1.0.0' }, + dispatcher: proxyAgent + }) + + server.close() + proxy.close() + proxyAgent.close() +}) + test('sending proxy-authorization in request headers should throw', async (t) => { t.plan(3) const server = await buildServer() diff --git a/types/proxy-agent.d.ts b/types/proxy-agent.d.ts index f5874716f72..55058734f78 100644 --- a/types/proxy-agent.d.ts +++ b/types/proxy-agent.d.ts @@ -1,3 +1,5 @@ +import { IncomingHttpHeaders } from 'http' + import Agent from './agent' import buildConnector from './connector'; import Dispatcher from './dispatcher' @@ -19,6 +21,7 @@ declare namespace ProxyAgent { */ auth?: string; token?: string; + headers?: IncomingHttpHeaders; requestTls?: buildConnector.BuildOptions; proxyTls?: buildConnector.BuildOptions; } From d0a99c247641ecea9879f729adf7576a2cc5df18 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Mon, 23 Jan 2023 07:21:43 +0100 Subject: [PATCH 019/123] fix: don't swallow llhttp loading error --- lib/client.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/client.js b/lib/client.js index 1317fc4d686..6300c1dd8bb 100644 --- a/lib/client.js +++ b/lib/client.js @@ -404,8 +404,7 @@ async function lazyllhttp () { let llhttpInstance = null let llhttpPromise = lazyllhttp() - .catch(() => { - }) +llhttpPromise.catch() let currentParser = null let currentBufferRef = null From 81b1521c21b5bbfcca06e8aacae4f7c47ac15e7a Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Mon, 23 Jan 2023 07:22:47 +0100 Subject: [PATCH 020/123] 5.16.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bfbbe4c2eee..9c4d7dae93b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "5.15.2", + "version": "5.16.0", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { From bdb1d088edbec1751cd54fc2db592841e756af71 Mon Sep 17 00:00:00 2001 From: Khafra Date: Tue, 24 Jan 2023 10:31:31 -0500 Subject: [PATCH 021/123] fix(wpts): Blob is a global getter in >=v19.x.x (#1880) --- test/wpt/runner/runner/worker.mjs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/wpt/runner/runner/worker.mjs b/test/wpt/runner/runner/worker.mjs index 6be596c3b69..924b3707cc5 100644 --- a/test/wpt/runner/runner/worker.mjs +++ b/test/wpt/runner/runner/worker.mjs @@ -64,6 +64,11 @@ Object.defineProperties(globalThis, { CloseEvent: { ...globalPropertyDescriptors, value: CloseEvent + }, + Blob: { + ...globalPropertyDescriptors, + // See https://github.com/nodejs/node/pull/45659 + value: buffer.Blob } }) From f02ab5b18a7e5cfcf0ffafaf8dc7a88b1f8430e3 Mon Sep 17 00:00:00 2001 From: Rafael Gonzaga Date: Wed, 25 Jan 2023 05:11:35 +0000 Subject: [PATCH 022/123] doc: fix anchor links dispatcher.stream (#1881) --- docs/api/Dispatcher.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/Dispatcher.md b/docs/api/Dispatcher.md index 54749964d67..6bb97b47208 100644 --- a/docs/api/Dispatcher.md +++ b/docs/api/Dispatcher.md @@ -631,7 +631,7 @@ try { A faster version of `Dispatcher.request`. This method expects the second argument `factory` to return a [`stream.Writable`](https://nodejs.org/api/stream.html#stream_class_stream_writable) stream which the response will be written to. This improves performance by avoiding creating an intermediate [`stream.Readable`](https://nodejs.org/api/stream.html#stream_readable_streams) stream when the user expects to directly pipe the response body to a [`stream.Writable`](https://nodejs.org/api/stream.html#stream_class_stream_writable) stream. -As demonstrated in [Example 1 - Basic GET stream request](#example-1-basic-get-stream-request), it is recommended to use the `option.opaque` property to avoid creating a closure for the `factory` method. This pattern works well with Node.js Web Frameworks such as [Fastify](https://fastify.io). See [Example 2 - Stream to Fastify Response](#example-2-stream-to-fastify-response) for more details. +As demonstrated in [Example 1 - Basic GET stream request](#example-1---basic-get-stream-request), it is recommended to use the `option.opaque` property to avoid creating a closure for the `factory` method. This pattern works well with Node.js Web Frameworks such as [Fastify](https://fastify.io). See [Example 2 - Stream to Fastify Response](#example-2---stream-to-fastify-response) for more details. Arguments: From 25a8aac24ec5b1a4a45cd2abf316a52412fad8f1 Mon Sep 17 00:00:00 2001 From: Khafra Date: Thu, 26 Jan 2023 02:32:25 -0500 Subject: [PATCH 023/123] wpt: make runner more resilient (#1884) * wpt: make runner more resilient * wpt: cleaner exit, hopefully --- package.json | 2 +- test/wpt/runner/runner/runner.mjs | 13 ++++++++++--- test/wpt/server/server.mjs | 2 +- test/wpt/start-FileAPI.mjs | 8 +++++--- test/wpt/start-fetch.mjs | 8 +++++--- test/wpt/start-mimesniff.mjs | 8 +++++--- test/wpt/start-websockets.mjs | 4 ++-- 7 files changed, 29 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 9c4d7dae93b..d84e47a9722 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "test:tdd": "tap test/*.js test/diagnostics-channel/*.js -w", "test:typescript": "tsd && tsc 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 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 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/runner/runner/runner.mjs b/test/wpt/runner/runner/runner.mjs index da5cb96899c..3e558de3e0b 100644 --- a/test/wpt/runner/runner/runner.mjs +++ b/test/wpt/runner/runner/runner.mjs @@ -1,6 +1,7 @@ import { deepStrictEqual } from 'node:assert' -import { EventEmitter } from 'node:events' +import { EventEmitter, once } from 'node:events' import { readdirSync, readFileSync, statSync } from 'node:fs' +import { cpus } from 'node:os' import { basename, isAbsolute, join, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { Worker } from 'node:worker_threads' @@ -82,10 +83,11 @@ export class WPTRunner extends EventEmitter { return [...files] } - run () { + async run () { const workerPath = fileURLToPath(join(import.meta.url, '../worker.mjs')) /** @type {Set} */ const activeWorkers = new Set() + let finishedFiles = 0 for (const test of this.#files) { const code = test.includes('.sub.') @@ -95,6 +97,7 @@ export class WPTRunner extends EventEmitter { if (this.#status[basename(test)]?.skip) { this.#stats.skipped += 1 + finishedFiles++ continue } @@ -131,10 +134,14 @@ export class WPTRunner extends EventEmitter { activeWorkers.delete(worker) clearTimeout(timeout) - if (activeWorkers.size === 0) { + if (++finishedFiles === this.#files.length) { this.handleRunnerCompletion() } }) + + if (activeWorkers.size === cpus().length) { + await once(worker, 'exit') + } } } diff --git a/test/wpt/server/server.mjs b/test/wpt/server/server.mjs index 13b8856a1f4..9df64e75010 100644 --- a/test/wpt/server/server.mjs +++ b/test/wpt/server/server.mjs @@ -354,7 +354,7 @@ send({ server: `http://localhost:${server.address().port}` }) process.on('message', (message) => { if (message === 'shutdown') { - server.close((err) => err ? send(err) : send({ message: 'shutdown' })) + server.close((err) => process.exit(err ? 1 : 0)) } }) diff --git a/test/wpt/start-FileAPI.mjs b/test/wpt/start-FileAPI.mjs index e601afc1120..fc85158ae13 100644 --- a/test/wpt/start-FileAPI.mjs +++ b/test/wpt/start-FileAPI.mjs @@ -10,15 +10,17 @@ 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('FileAPI', message.server) runner.run() runner.once('completion', () => { - child.send('shutdown') + if (child.connected) { + child.send('shutdown') + } }) - } else if (message.message === 'shutdown') { - process.exit() } } diff --git a/test/wpt/start-fetch.mjs b/test/wpt/start-fetch.mjs index 55d3f2e53ae..dbf66af59b8 100644 --- a/test/wpt/start-fetch.mjs +++ b/test/wpt/start-fetch.mjs @@ -10,15 +10,17 @@ 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('fetch', message.server) runner.run() runner.once('completion', () => { - child.send('shutdown') + if (child.connected) { + child.send('shutdown') + } }) - } else if (message.message === 'shutdown') { - process.exit() } } diff --git a/test/wpt/start-mimesniff.mjs b/test/wpt/start-mimesniff.mjs index 90e6bdd6c04..396e93dfa9a 100644 --- a/test/wpt/start-mimesniff.mjs +++ b/test/wpt/start-mimesniff.mjs @@ -10,15 +10,17 @@ 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('mimesniff', message.server) runner.run() runner.once('completion', () => { - child.send('shutdown') + if (child.connected) { + child.send('shutdown') + } }) - } else if (message.message === 'shutdown') { - process.exit() } } diff --git a/test/wpt/start-websockets.mjs b/test/wpt/start-websockets.mjs index a7078c6d8d3..603c2f31681 100644 --- a/test/wpt/start-websockets.mjs +++ b/test/wpt/start-websockets.mjs @@ -10,6 +10,8 @@ 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('websockets', message.server) @@ -20,7 +22,5 @@ for await (const [message] of on(child, 'message')) { child.send('shutdown') } }) - } else if (message.message === 'shutdown') { - process.exit() } } From 47f574d1ac97afa3b1f17c0f8610b73b16cba9b9 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Thu, 26 Jan 2023 11:35:45 +0100 Subject: [PATCH 024/123] Make test pass in v19.x (#1879) * Make test pass in v19.x Signed-off-by: Matteo Collina * fixup Signed-off-by: Matteo Collina * enable v19 Signed-off-by: Matteo Collina * fixup Signed-off-by: Matteo Collina * fixup Signed-off-by: Matteo Collina * github CI timeout Signed-off-by: Matteo Collina Signed-off-by: Matteo Collina --- .github/workflows/nodejs.yml | 2 +- test/balanced-pool.js | 11 +++++++---- test/fetch/abort.js | 26 ++++++++++++++++++++++++-- 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index bd8de1c3450..62f85400918 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -9,6 +9,7 @@ on: [push, pull_request] jobs: build: name: Test + timeout-minutes: 15 uses: pkgjs/action/.github/workflows/node-test.yaml@v0.1 with: runs-on: ubuntu-latest, windows-latest @@ -22,7 +23,6 @@ jobs: exclude: | - runs-on: windows-latest node-version: 16 - - node-version: 19 automerge: if: > github.event_name == 'pull_request' && github.event.pull_request.user.login == 'dependabot[bot]' diff --git a/test/balanced-pool.js b/test/balanced-pool.js index 7e28da7c72d..be544887ef9 100644 --- a/test/balanced-pool.js +++ b/test/balanced-pool.js @@ -4,6 +4,7 @@ const { test } = require('tap') const { BalancedPool, Pool, Client, errors } = require('..') const { createServer } = require('http') const { promisify } = require('util') +const semver = require('semver') test('throws when factory is not a function', (t) => { t.plan(2) @@ -433,7 +434,10 @@ const cases = [ expected: ['A', 'B', 'C', 'A', 'B', 'C/connectionRefused', 'A', 'B', 'A', 'B', 'A', 'B', 'C', 'A', 'B', 'C'], expectedConnectionRefusedErrors: 1, expectedSocketErrors: 0, - expectedRatios: [0.34, 0.34, 0.32] + expectedRatios: [0.34, 0.34, 0.32], + + // Skip because the behavior of Node.js has changed + skip: semver.satisfies(process.version, '>= 19.0.0') }, // 8 @@ -476,8 +480,8 @@ const cases = [ ] -for (const [index, { config, expected, expectedRatios, iterations = 9, expectedConnectionRefusedErrors = 0, expectedSocketErrors = 0, maxWeightPerServer, errorPenalty = 10 }] of cases.entries()) { - test(`weighted round robin - case ${index}`, async (t) => { +for (const [index, { config, expected, expectedRatios, iterations = 9, expectedConnectionRefusedErrors = 0, expectedSocketErrors = 0, maxWeightPerServer, errorPenalty = 10, only = false, skip = false }] of cases.entries()) { + test(`weighted round robin - case ${index}`, { only, skip }, async (t) => { // cerate an array to store succesfull reqeusts const requestLog = [] @@ -512,7 +516,6 @@ for (const [index, { config, expected, expectedRatios, iterations = 9, expectedC 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) - serverWithError.requestsCount++ if (e.code === 'ECONNREFUSED') { diff --git a/test/fetch/abort.js b/test/fetch/abort.js index 03ee3b44ab2..1b9f1e68e8d 100644 --- a/test/fetch/abort.js +++ b/test/fetch/abort.js @@ -5,6 +5,7 @@ const { fetch } = require('../..') const { createServer } = require('http') const { once } = require('events') const { DOMException } = require('../../lib/fetch/constants') +const semver = require('semver') const { AbortController: NPMAbortController } = require('abort-controller') @@ -36,13 +37,13 @@ test('Allow the usage of custom implementation of AbortController', async (t) => } }) -test('allows aborting with custom errors', { skip: process.version.startsWith('v16.') }, async (t) => { +test('allows aborting with custom errors', { skip: semver.satisfies(process.version, '16.x') }, async (t) => { const server = createServer().listen(0) t.teardown(server.close.bind(server)) await once(server, 'listening') - t.test('Using AbortSignal.timeout', async (t) => { + t.test('Using AbortSignal.timeout without cause', { skip: semver.satisfies(process.version, '>= 19.0.0') }, async (t) => { await t.rejects( fetch(`http://localhost:${server.address().port}`, { signal: AbortSignal.timeout(50) @@ -54,6 +55,27 @@ test('allows aborting with custom errors', { skip: process.version.startsWith('v ) }) + t.test('Using AbortSignal.timeout with cause', { skip: semver.satisfies(process.version, '< 19.0.0') }, async (t) => { + t.plan(2) + + try { + await fetch(`http://localhost:${server.address().port}`, { + signal: AbortSignal.timeout(50) + }) + } catch (err) { + if (err.name === 'TypeError') { + const cause = err.cause + t.equal(cause.name, 'HeadersTimeoutError') + t.equal(cause.code, 'UND_ERR_HEADERS_TIMEOUT') + } else if (err.name === 'TimeoutError') { + t.equal(err.code, DOMException.TIMEOUT_ERR) + t.equal(err.cause, undefined) + } else { + t.error(err) + } + } + }) + t.test('Error defaults to an AbortError DOMException', async (t) => { const ac = new AbortController() ac.abort() // no reason From 196c4da58dddc8850aea4ca63fe5d483a8a159c4 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Fri, 27 Jan 2023 14:35:57 +0100 Subject: [PATCH 025/123] remove timeout-minutes in github action Signed-off-by: Matteo Collina fix https://github.com/nodejs/undici/issues/1890 --- .github/workflows/nodejs.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 62f85400918..fee9d7aace4 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -9,7 +9,6 @@ on: [push, pull_request] jobs: build: name: Test - timeout-minutes: 15 uses: pkgjs/action/.github/workflows/node-test.yaml@v0.1 with: runs-on: ubuntu-latest, windows-latest From a9dabd54ae1a7894e65d1da13e103edb22be9444 Mon Sep 17 00:00:00 2001 From: pan93412 Date: Sun, 29 Jan 2023 16:43:16 +0800 Subject: [PATCH 026/123] Correct the type of DispatchOptions["headers"] (#1896) --- docs/api/Dispatcher.md | 12 ++++++------ types/dispatcher.d.ts | 2 +- types/errors.d.ts | 2 +- types/header.d.ts | 4 ++++ types/mock-interceptor.d.ts | 2 +- types/proxy-agent.d.ts | 3 +-- 6 files changed, 14 insertions(+), 11 deletions(-) create mode 100644 types/header.d.ts diff --git a/docs/api/Dispatcher.md b/docs/api/Dispatcher.md index 6bb97b47208..25c980d612b 100644 --- a/docs/api/Dispatcher.md +++ b/docs/api/Dispatcher.md @@ -74,7 +74,7 @@ Returns: `void | Promise` - Only returns a `Promise` if no `callbac #### Parameter: `ConnectData` * **statusCode** `number` -* **headers** `http.IncomingHttpHeaders` +* **headers** `Record` * **socket** `stream.Duplex` * **opaque** `unknown` @@ -383,7 +383,7 @@ Extends: [`RequestOptions`](#parameter-requestoptions) #### Parameter: PipelineHandlerData * **statusCode** `number` -* **headers** `IncomingHttpHeaders` +* **headers** `Record` * **opaque** `unknown` * **body** `stream.Readable` * **context** `object` @@ -477,7 +477,7 @@ The `RequestOptions.method` property should not be value `'CONNECT'`. #### Parameter: `ResponseData` * **statusCode** `number` -* **headers** `http.IncomingHttpHeaders` - Note that all header keys are lower-cased, e. g. `content-type`. +* **headers** `Record` - Note that all header keys are lower-cased, e. g. `content-type`. * **body** `stream.Readable` which also implements [the body mixin from the Fetch Standard](https://fetch.spec.whatwg.org/#body-mixin). * **trailers** `Record` - This object starts out as empty and will be mutated to contain trailers after `body` has emitted `'end'`. @@ -644,7 +644,7 @@ Returns: `void | Promise` - Only returns a `Promise` if no `callback #### Parameter: `StreamFactoryData` * **statusCode** `number` -* **headers** `http.IncomingHttpHeaders` +* **headers** `Record` * **opaque** `unknown` * **onInfo** `({statusCode: number, headers: Record}) => void | null` (optional) - Default: `null` - Callback collecting all the info headers (HTTP 100-199) received. @@ -853,9 +853,9 @@ Emitted when dispatcher is no longer busy. ## Parameter: `UndiciHeaders` -* `http.IncomingHttpHeaders | string[] | null` +* `Record | string[] | null` -Header arguments such as `options.headers` in [`Client.dispatch`](Client.md#clientdispatchoptions-handlers) can be specified in two forms; either as an object specified by the `http.IncomingHttpHeaders` type, or an array of strings. An array representation of a header list must have an even length or an `InvalidArgumentError` will be thrown. +Header arguments such as `options.headers` in [`Client.dispatch`](Client.md#clientdispatchoptions-handlers) can be specified in two forms; either as an object specified by the `Record` (`IncomingHttpHeaders`) type, or an array of strings. An array representation of a header list must have an even length or an `InvalidArgumentError` will be thrown. Keys are lowercase and values are not modified. diff --git a/types/dispatcher.d.ts b/types/dispatcher.d.ts index 05a2ab5e76e..2e427a73dd4 100644 --- a/types/dispatcher.d.ts +++ b/types/dispatcher.d.ts @@ -1,8 +1,8 @@ import { URL } from 'url' import { Duplex, Readable, Writable } from 'stream' import { EventEmitter } from 'events' -import { IncomingHttpHeaders } from 'http' import { Blob } from 'buffer' +import { IncomingHttpHeaders } from './header' import BodyReadable from './readable' import { FormData } from './formdata' import Errors from './errors' diff --git a/types/errors.d.ts b/types/errors.d.ts index c4ef4b6f633..f05fae1e5b3 100644 --- a/types/errors.d.ts +++ b/types/errors.d.ts @@ -1,4 +1,4 @@ -import {IncomingHttpHeaders} from "http"; +import { IncomingHttpHeaders } from "./header"; import Client from './client' export default Errors diff --git a/types/header.d.ts b/types/header.d.ts new file mode 100644 index 00000000000..3fd9483e27b --- /dev/null +++ b/types/header.d.ts @@ -0,0 +1,4 @@ +/** + * The header type declaration of `undici`. + */ +export type IncomingHttpHeaders = Record; diff --git a/types/mock-interceptor.d.ts b/types/mock-interceptor.d.ts index 9dc423c0600..6b3961c0482 100644 --- a/types/mock-interceptor.d.ts +++ b/types/mock-interceptor.d.ts @@ -1,4 +1,4 @@ -import { IncomingHttpHeaders } from 'http' +import { IncomingHttpHeaders } from './header' import Dispatcher from './dispatcher'; import { BodyInit, Headers } from './fetch' diff --git a/types/proxy-agent.d.ts b/types/proxy-agent.d.ts index 55058734f78..d312cb3f9a5 100644 --- a/types/proxy-agent.d.ts +++ b/types/proxy-agent.d.ts @@ -1,8 +1,7 @@ -import { IncomingHttpHeaders } from 'http' - import Agent from './agent' import buildConnector from './connector'; import Dispatcher from './dispatcher' +import { IncomingHttpHeaders } from './header' export default ProxyAgent From 67e97966a1bdd4bb658b2524efcf258c0b432748 Mon Sep 17 00:00:00 2001 From: Khafra Date: Sun, 29 Jan 2023 09:40:17 -0500 Subject: [PATCH 027/123] perf(content-type parser): faster string collector (#1894) --- lib/fetch/dataURL.js | 39 +++++++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/lib/fetch/dataURL.js b/lib/fetch/dataURL.js index 45a058376ff..c950167d295 100644 --- a/lib/fetch/dataURL.js +++ b/lib/fetch/dataURL.js @@ -31,8 +31,8 @@ function dataURLProcessor (dataURL) { // 5. Let mimeType be the result of collecting a // sequence of code points that are not equal // to U+002C (,), given position. - let mimeType = collectASequenceOfCodePoints( - (char) => char !== ',', + let mimeType = collectASequenceOfCodePointsFast( + ',', input, position ) @@ -145,6 +145,25 @@ function collectASequenceOfCodePoints (condition, input, position) { return result } +/** + * A faster collectASequenceOfCodePoints that only works when comparing a single character. + * @param {string} char + * @param {string} input + * @param {{ position: number }} position + */ +function collectASequenceOfCodePointsFast (char, input, position) { + const idx = input.indexOf(char, position.position) + const start = position.position + + if (idx === -1) { + position.position = input.length + return input.slice(start) + } + + position.position = idx + return input.slice(start, position.position) +} + // https://url.spec.whatwg.org/#string-percent-decode /** @param {string} input */ function stringPercentDecode (input) { @@ -214,8 +233,8 @@ function parseMIMEType (input) { // 3. Let type be the result of collecting a sequence // of code points that are not U+002F (/) from // input, given position. - const type = collectASequenceOfCodePoints( - (char) => char !== '/', + const type = collectASequenceOfCodePointsFast( + '/', input, position ) @@ -239,8 +258,8 @@ function parseMIMEType (input) { // 7. Let subtype be the result of collecting a sequence of // code points that are not U+003B (;) from input, given // position. - let subtype = collectASequenceOfCodePoints( - (char) => char !== ';', + let subtype = collectASequenceOfCodePointsFast( + ';', input, position ) @@ -324,8 +343,8 @@ function parseMIMEType (input) { // 2. Collect a sequence of code points that are not // U+003B (;) from input, given position. - collectASequenceOfCodePoints( - (char) => char !== ';', + collectASequenceOfCodePointsFast( + ';', input, position ) @@ -335,8 +354,8 @@ function parseMIMEType (input) { // 1. Set parameterValue to the result of collecting // a sequence of code points that are not U+003B (;) // from input, given position. - parameterValue = collectASequenceOfCodePoints( - (char) => char !== ';', + parameterValue = collectASequenceOfCodePointsFast( + ';', input, position ) From f4693d6d70941c77180bc456e2ee4bc2e5c75834 Mon Sep 17 00:00:00 2001 From: Khafra Date: Sun, 29 Jan 2023 17:22:03 -0500 Subject: [PATCH 028/123] feat: expose content-type parser (#1895) * feat: expose content-type parser * fix: add docs --- docs/api/ContentType.md | 57 +++++++++++++++++++++++++++++++++++++++++ docsify/sidebar.md | 1 + index.d.ts | 1 + index.js | 5 ++++ types/content-type.d.ts | 21 +++++++++++++++ 5 files changed, 85 insertions(+) create mode 100644 docs/api/ContentType.md create mode 100644 types/content-type.d.ts diff --git a/docs/api/ContentType.md b/docs/api/ContentType.md new file mode 100644 index 00000000000..2bcc9f71ca3 --- /dev/null +++ b/docs/api/ContentType.md @@ -0,0 +1,57 @@ +# MIME Type Parsing + +## `MIMEType` interface + +* **type** `string` +* **subtype** `string` +* **parameters** `Map` +* **essence** `string` + +## `parseMIMEType(input)` + +Implements [parse a MIME type](https://mimesniff.spec.whatwg.org/#parse-a-mime-type). + +Parses a MIME type, returning its type, subtype, and any associated parameters. If the parser can't parse an input it returns the string literal `'failure'`. + +```js +import { parseMIMEType } from 'undici' + +parseMIMEType('text/html; charset=gbk') +// { +// type: 'text', +// subtype: 'html', +// parameters: Map(1) { 'charset' => 'gbk' }, +// essence: 'text/html' +// } +``` + +Arguments: + +* **input** `string` + +Returns: `MIMEType|'failure'` + +## `serializeAMimeType(input)` + +Implements [serialize a MIME type](https://mimesniff.spec.whatwg.org/#serialize-a-mime-type). + +Serializes a MIMEType object. + +```js +import { serializeAMimeType } from 'undici' + +serializeAMimeType({ + type: 'text', + subtype: 'html', + parameters: new Map([['charset', 'gbk']]), + essence: 'text/html' +}) +// text/html;charset=gbk + +``` + +Arguments: + +* **mimeType** `MIMEType` + +Returns: `string` diff --git a/docsify/sidebar.md b/docsify/sidebar.md index 6c110f47f6e..015e6f48842 100644 --- a/docsify/sidebar.md +++ b/docsify/sidebar.md @@ -19,6 +19,7 @@ * [API Lifecycle](/docs/api/api-lifecycle.md "Undici API - Lifecycle") * [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") * 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 e914634f1d0..d67de97241d 100644 --- a/index.d.ts +++ b/index.d.ts @@ -23,6 +23,7 @@ export * from './types/filereader' export * from './types/formdata' export * from './types/diagnostics-channel' export * from './types/websocket' +export * from './types/content-type' 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 } diff --git a/index.js b/index.js index 48d9beed1b8..a970b937423 100644 --- a/index.js +++ b/index.js @@ -134,6 +134,11 @@ if (nodeMajor >= 16) { module.exports.getCookies = getCookies module.exports.getSetCookies = getSetCookies module.exports.setCookie = setCookie + + const { parseMIMEType, serializeAMimeType } = require('./lib/fetch/dataURL') + + module.exports.parseMIMEType = parseMIMEType + module.exports.serializeAMimeType = serializeAMimeType } if (nodeMajor >= 18 && hasCrypto) { diff --git a/types/content-type.d.ts b/types/content-type.d.ts new file mode 100644 index 00000000000..f2a87f1b751 --- /dev/null +++ b/types/content-type.d.ts @@ -0,0 +1,21 @@ +/// + +interface MIMEType { + type: string + subtype: string + parameters: Map + essence: string +} + +/** + * Parse a string to a {@link MIMEType} object. Returns `failure` if the string + * couldn't be parsed. + * @see https://mimesniff.spec.whatwg.org/#parse-a-mime-type + */ +export function parseMIMEType (input: string): 'failure' | MIMEType + +/** + * Convert a MIMEType object to a string. + * @see https://mimesniff.spec.whatwg.org/#serialize-a-mime-type + */ +export function serializeAMimeType (mimeType: MIMEType): string From 0c7510265478870959e156122e14efe41edbd420 Mon Sep 17 00:00:00 2001 From: Sean Kelly Date: Sun, 29 Jan 2023 14:23:08 -0800 Subject: [PATCH 029/123] Update DispatchOptions type (#1889) --- types/dispatcher.d.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/types/dispatcher.d.ts b/types/dispatcher.d.ts index 2e427a73dd4..68135d1e5c1 100644 --- a/types/dispatcher.d.ts +++ b/types/dispatcher.d.ts @@ -105,6 +105,8 @@ declare namespace Dispatcher { query?: Record; /** Whether the requests can be safely retried or not. If `false` the request won't be sent until all preceding requests in the pipeline have completed. Default: `true` if `method` is `HEAD` or `GET`. */ idempotent?: boolean; + /** 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. */ + 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 30 seconds. */ From 055780a930cc40c138152e5ddcdfbf858cb7808f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20C=C3=A1rdenas?= Date: Mon, 30 Jan 2023 07:03:18 -0600 Subject: [PATCH 030/123] fix: update error type definitions (#1888) --- test/types/errors.test-d.ts | 58 +++++++++++++++++++++++++++++-------- types/errors.d.ts | 45 +++++++++++++++++++++------- 2 files changed, 80 insertions(+), 23 deletions(-) diff --git a/test/types/errors.test-d.ts b/test/types/errors.test-d.ts index 53b544dfcd1..8708519504b 100644 --- a/test/types/errors.test-d.ts +++ b/test/types/errors.test-d.ts @@ -4,29 +4,44 @@ import Client from '../../types/client' expectAssignable(new errors.UndiciError()) +expectAssignable(new errors.ConnectTimeoutError()) +expectAssignable(new errors.ConnectTimeoutError()) +expectAssignable<'ConnectTimeoutError'>(new errors.ConnectTimeoutError().name) +expectAssignable<'UND_ERR_CONNECT_TIMEOUT'>(new errors.ConnectTimeoutError().code) + expectAssignable(new errors.HeadersTimeoutError()) expectAssignable(new errors.HeadersTimeoutError()) expectAssignable<'HeadersTimeoutError'>(new errors.HeadersTimeoutError().name) expectAssignable<'UND_ERR_HEADERS_TIMEOUT'>(new errors.HeadersTimeoutError().code) +expectAssignable(new errors.HeadersOverflowError()) +expectAssignable(new errors.HeadersOverflowError()) +expectAssignable<'HeadersOverflowError'>(new errors.HeadersOverflowError().name) +expectAssignable<'UND_ERR_HEADERS_OVERFLOW'>(new errors.HeadersOverflowError().code) + expectAssignable(new errors.BodyTimeoutError()) expectAssignable(new errors.BodyTimeoutError()) expectAssignable<'BodyTimeoutError'>(new errors.BodyTimeoutError().name) expectAssignable<'UND_ERR_BODY_TIMEOUT'>(new errors.BodyTimeoutError().code) -expectAssignable(new errors.SocketTimeoutError()) -expectAssignable(new errors.SocketTimeoutError()) -expectAssignable<'SocketTimeoutError'>(new errors.SocketTimeoutError().name) -expectAssignable<'UND_ERR_SOCKET_TIMEOUT'>(new errors.SocketTimeoutError().code) +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.InvalidReturnError()) -expectAssignable(new errors.InvalidReturnError()) -expectAssignable<'InvalidReturnError'>(new errors.InvalidReturnError().name) -expectAssignable<'UND_ERR_INVALID_RETURN_VALUE'>(new errors.InvalidReturnError().code) +expectAssignable(new errors.InvalidArgumentError()) +expectAssignable(new errors.InvalidArgumentError()) +expectAssignable<'InvalidArgumentError'>(new errors.InvalidArgumentError().name) +expectAssignable<'UND_ERR_INVALID_ARG'>(new errors.InvalidArgumentError().code) + +expectAssignable(new errors.InvalidReturnValueError()) +expectAssignable(new errors.InvalidReturnValueError()) +expectAssignable<'InvalidReturnValueError'>(new errors.InvalidReturnValueError().name) +expectAssignable<'UND_ERR_INVALID_RETURN_VALUE'>(new errors.InvalidReturnValueError().code) expectAssignable(new errors.RequestAbortedError()) expectAssignable(new errors.RequestAbortedError()) -expectAssignable<'RequestAbortedError'>(new errors.RequestAbortedError().name) +expectAssignable<'AbortError'>(new errors.RequestAbortedError().name) expectAssignable<'UND_ERR_ABORTED'>(new errors.RequestAbortedError().code) expectAssignable(new errors.InformationalError()) @@ -39,6 +54,11 @@ expectAssignable(new errors.RequestCon expectAssignable<'RequestContentLengthMismatchError'>(new errors.RequestContentLengthMismatchError().name) expectAssignable<'UND_ERR_REQ_CONTENT_LENGTH_MISMATCH'>(new errors.RequestContentLengthMismatchError().code) +expectAssignable(new errors.ResponseContentLengthMismatchError()) +expectAssignable(new errors.ResponseContentLengthMismatchError()) +expectAssignable<'ResponseContentLengthMismatchError'>(new errors.ResponseContentLengthMismatchError().name) +expectAssignable<'UND_ERR_RES_CONTENT_LENGTH_MISMATCH'>(new errors.ResponseContentLengthMismatchError().code) + expectAssignable(new errors.ClientDestroyedError()) expectAssignable(new errors.ClientDestroyedError()) expectAssignable<'ClientDestroyedError'>(new errors.ClientDestroyedError().name) @@ -60,15 +80,29 @@ expectAssignable(new errors.NotSupportedError()) expectAssignable<'NotSupportedError'>(new errors.NotSupportedError().name) expectAssignable<'UND_ERR_NOT_SUPPORTED'>(new errors.NotSupportedError().code) +expectAssignable(new errors.BalancedPoolMissingUpstreamError()) +expectAssignable(new errors.BalancedPoolMissingUpstreamError()) +expectAssignable<'MissingUpstreamError'>(new errors.BalancedPoolMissingUpstreamError().name) +expectAssignable<'UND_ERR_BPL_MISSING_UPSTREAM'>(new errors.BalancedPoolMissingUpstreamError().code) + +expectAssignable(new errors.HTTPParserError()) +expectAssignable(new errors.HTTPParserError()) +expectAssignable<'HTTPParserError'>(new errors.HTTPParserError().name) + +expectAssignable(new errors.ResponseExceededMaxSizeError()) +expectAssignable(new errors.ResponseExceededMaxSizeError()) +expectAssignable<'ResponseExceededMaxSizeError'>(new errors.ResponseExceededMaxSizeError().name) +expectAssignable<'UND_ERR_RES_EXCEEDED_MAX_SIZE'>(new errors.ResponseExceededMaxSizeError().code) + { // @ts-ignore - function f (): errors.HeadersTimeoutError | errors.SocketTimeoutError { return } + function f (): errors.HeadersTimeoutError | errors.ConnectTimeoutError { return } const e = f() if (e.code === 'UND_ERR_HEADERS_TIMEOUT') { expectAssignable(e) - } else if (e.code === 'UND_ERR_SOCKET_TIMEOUT') { - expectAssignable(e) + } else if (e.code === 'UND_ERR_CONNECT_TIMEOUT') { + expectAssignable(e) } } diff --git a/types/errors.d.ts b/types/errors.d.ts index f05fae1e5b3..fd2ce7c3a99 100644 --- a/types/errors.d.ts +++ b/types/errors.d.ts @@ -6,12 +6,24 @@ export default Errors declare namespace Errors { export class UndiciError extends Error { } + /** Connect timeout error. */ + export class ConnectTimeoutError extends UndiciError { + name: 'ConnectTimeoutError'; + code: 'UND_ERR_CONNECT_TIMEOUT'; + } + /** A header exceeds the `headersTimeout` option. */ export class HeadersTimeoutError extends UndiciError { name: 'HeadersTimeoutError'; code: 'UND_ERR_HEADERS_TIMEOUT'; } + /** Headers overflow error. */ + export class HeadersOverflowError extends UndiciError { + name: 'HeadersOverflowError' + code: 'UND_ERR_HEADERS_OVERFLOW' + } + /** A body exceeds the `bodyTimeout` option. */ export class BodyTimeoutError extends UndiciError { name: 'BodyTimeoutError'; @@ -27,12 +39,6 @@ declare namespace Errors { headers: IncomingHttpHeaders | string[] | null; } - /** A socket exceeds the `socketTimeout` option. */ - export class SocketTimeoutError extends UndiciError { - name: 'SocketTimeoutError'; - code: 'UND_ERR_SOCKET_TIMEOUT'; - } - /** Passed an invalid argument. */ export class InvalidArgumentError extends UndiciError { name: 'InvalidArgumentError'; @@ -40,14 +46,14 @@ declare namespace Errors { } /** Returned an invalid value. */ - export class InvalidReturnError extends UndiciError { - name: 'InvalidReturnError'; + export class InvalidReturnValueError extends UndiciError { + name: 'InvalidReturnValueError'; code: 'UND_ERR_INVALID_RETURN_VALUE'; } /** The request has been aborted by the user. */ export class RequestAbortedError extends UndiciError { - name: 'RequestAbortedError'; + name: 'AbortError'; code: 'UND_ERR_ABORTED'; } @@ -57,12 +63,18 @@ declare namespace Errors { code: 'UND_ERR_INFO'; } - /** Body does not match content-length header. */ + /** Request body length does not match content-length header. */ export class RequestContentLengthMismatchError extends UndiciError { name: 'RequestContentLengthMismatchError'; code: 'UND_ERR_REQ_CONTENT_LENGTH_MISMATCH'; } + /** Response body length does not match content-length header. */ + export class ResponseContentLengthMismatchError extends UndiciError { + name: 'ResponseContentLengthMismatchError'; + code: 'UND_ERR_RES_CONTENT_LENGTH_MISMATCH'; + } + /** Trying to use a destroyed client. */ export class ClientDestroyedError extends UndiciError { name: 'ClientDestroyedError'; @@ -88,7 +100,18 @@ declare namespace Errors { code: 'UND_ERR_NOT_SUPPORTED'; } - /** The response exceed the length allowed */ + /** No upstream has been added to the BalancedPool. */ + export class BalancedPoolMissingUpstreamError extends UndiciError { + name: 'MissingUpstreamError'; + code: 'UND_ERR_BPL_MISSING_UPSTREAM'; + } + + export class HTTPParserError extends UndiciError { + name: 'HTTPParserError'; + code: string; + } + + /** The response exceed the length allowed. */ export class ResponseExceededMaxSizeError extends UndiciError { name: 'ResponseExceededMaxSizeError'; code: 'UND_ERR_RES_EXCEEDED_MAX_SIZE'; From db0b5ca199a001da7908b5d82187145cf225a702 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Wed, 1 Feb 2023 13:03:59 +0100 Subject: [PATCH 031/123] fix: ensure connection header is a string (#1900) --- lib/core/request.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/core/request.js b/lib/core/request.js index bee1a872960..59e0b87f39d 100644 --- a/lib/core/request.js +++ b/lib/core/request.js @@ -327,7 +327,7 @@ function processHeader (request, key, val) { key.length === 10 && key.toLowerCase() === 'connection' ) { - const value = val.toLowerCase() + const value = typeof val === 'string' ? val.toLowerCase() : null if (value !== 'close' && value !== 'keep-alive') { throw new InvalidArgumentError('invalid connection header') } else if (value === 'close') { From 2b260c997ad4efe4ed2064b264b4b546a59e7a67 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Wed, 1 Feb 2023 17:37:21 +0100 Subject: [PATCH 032/123] fix: throw if invalid content-type header (#1901) --- lib/core/request.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/core/request.js b/lib/core/request.js index 59e0b87f39d..e476fc1bcc1 100644 --- a/lib/core/request.js +++ b/lib/core/request.js @@ -284,6 +284,7 @@ function processHeaderValue (key, val) { } else if (headerCharRegex.exec(val) !== null) { throw new InvalidArgumentError(`invalid ${key} header`) } + return `${key}: ${val}\r\n` } @@ -313,11 +314,10 @@ function processHeader (request, key, val) { } else if ( request.contentType === null && key.length === 12 && - key.toLowerCase() === 'content-type' && - headerCharRegex.exec(val) === null + key.toLowerCase() === 'content-type' ) { request.contentType = val - request.headers += `${key}: ${val}\r\n` + request.headers += processHeaderValue(key, val) } else if ( key.length === 17 && key.toLowerCase() === 'transfer-encoding' From 09863812bc2bf68177480c0b8abe914e5f38ca8e Mon Sep 17 00:00:00 2001 From: Khafra Date: Fri, 3 Feb 2023 15:31:31 -0500 Subject: [PATCH 033/123] fix(fetch): use semicolon for Cookie header delimiter (#1906) --- lib/fetch/headers.js | 6 +++++- test/fetch/cookies.js | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/lib/fetch/headers.js b/lib/fetch/headers.js index f3955f2a47a..0febd114db5 100644 --- a/lib/fetch/headers.js +++ b/lib/fetch/headers.js @@ -104,7 +104,11 @@ class HeadersList { // 2. Append (name, value) to list. if (exists) { - this[kHeadersMap].set(lowercaseName, { name: exists.name, value: `${exists.value}, ${value}` }) + const delimiter = lowercaseName === 'cookie' ? '; ' : ', ' + this[kHeadersMap].set(lowercaseName, { + name: exists.name, + value: `${exists.value}${delimiter}${value}` + }) } else { this[kHeadersMap].set(lowercaseName, { name, value }) } diff --git a/test/fetch/cookies.js b/test/fetch/cookies.js index d17a8827df5..18b001d8644 100644 --- a/test/fetch/cookies.js +++ b/test/fetch/cookies.js @@ -48,3 +48,22 @@ test('Can send cookies to a server with fetch - issue #1463', async (t) => { t.end() }) + +test('Cookie header is delimited with a semicolon rather than a comma - issue #1905', async (t) => { + t.plan(1) + + const server = createServer((req, res) => { + t.equal(req.headers.cookie, 'FOO=lorem-ipsum-dolor-sit-amet; BAR=the-quick-brown-fox') + res.end() + }).listen(0) + + t.teardown(server.close.bind(server)) + await once(server, 'listening') + + await fetch(`http://localhost:${server.address().port}`, { + headers: [ + ['cookie', 'FOO=lorem-ipsum-dolor-sit-amet'], + ['cookie', 'BAR=the-quick-brown-fox'] + ] + }) +}) From 208f7cf8629fcadca554dcd935dda3afaeb87654 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Sat, 4 Feb 2023 04:32:54 +0100 Subject: [PATCH 034/123] perf: use FastBuffer (#1907) --- lib/client.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/lib/client.js b/lib/client.js index 6300c1dd8bb..2dd689a6f09 100644 --- a/lib/client.js +++ b/lib/client.js @@ -65,6 +65,7 @@ const { kLocalAddress, kMaxResponseSize } = require('./core/symbols') +const FastBuffer = Buffer[Symbol.species] const kClosedResolve = Symbol('kClosedResolve') @@ -363,8 +364,7 @@ async function lazyllhttp () { wasm_on_status: (p, at, len) => { assert.strictEqual(currentParser.ptr, p) const start = at - currentBufferPtr - const end = start + len - return currentParser.onStatus(currentBufferRef.slice(start, end)) || 0 + return currentParser.onStatus(new FastBuffer(currentBufferRef.buffer, start, len)) || 0 }, wasm_on_message_begin: (p) => { assert.strictEqual(currentParser.ptr, p) @@ -373,14 +373,12 @@ async function lazyllhttp () { wasm_on_header_field: (p, at, len) => { assert.strictEqual(currentParser.ptr, p) const start = at - currentBufferPtr - const end = start + len - return currentParser.onHeaderField(currentBufferRef.slice(start, end)) || 0 + return currentParser.onHeaderField(new FastBuffer(currentBufferRef.buffer, start, len)) || 0 }, wasm_on_header_value: (p, at, len) => { assert.strictEqual(currentParser.ptr, p) const start = at - currentBufferPtr - const end = start + len - return currentParser.onHeaderValue(currentBufferRef.slice(start, end)) || 0 + return currentParser.onHeaderValue(new FastBuffer(currentBufferRef.buffer, start, len)) || 0 }, wasm_on_headers_complete: (p, statusCode, upgrade, shouldKeepAlive) => { assert.strictEqual(currentParser.ptr, p) @@ -389,8 +387,7 @@ async function lazyllhttp () { wasm_on_body: (p, at, len) => { assert.strictEqual(currentParser.ptr, p) const start = at - currentBufferPtr - const end = start + len - return currentParser.onBody(currentBufferRef.slice(start, end)) || 0 + return currentParser.onBody(new FastBuffer(currentBufferRef.buffer, start, len)) || 0 }, wasm_on_message_complete: (p) => { assert.strictEqual(currentParser.ptr, p) From 16b7a68be363c463cba00c85a9006ee938ec1d77 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Sat, 4 Feb 2023 12:10:53 +0100 Subject: [PATCH 035/123] 5.17.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d84e47a9722..3cfd02005d2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "5.16.0", + "version": "5.17.0", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { From d2be675575512794dcd41b9683b209fc15368154 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Sat, 4 Feb 2023 12:44:01 +0100 Subject: [PATCH 036/123] fix: bad buffer slice --- lib/client.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/client.js b/lib/client.js index 2dd689a6f09..496c568d732 100644 --- a/lib/client.js +++ b/lib/client.js @@ -363,7 +363,7 @@ async function lazyllhttp () { }, wasm_on_status: (p, at, len) => { assert.strictEqual(currentParser.ptr, p) - const start = at - currentBufferPtr + const start = at - currentBufferPtr + currentBufferRef.byteOffset return currentParser.onStatus(new FastBuffer(currentBufferRef.buffer, start, len)) || 0 }, wasm_on_message_begin: (p) => { @@ -372,12 +372,12 @@ async function lazyllhttp () { }, wasm_on_header_field: (p, at, len) => { assert.strictEqual(currentParser.ptr, p) - const start = at - currentBufferPtr + const start = at - currentBufferPtr + currentBufferRef.byteOffset return currentParser.onHeaderField(new FastBuffer(currentBufferRef.buffer, start, len)) || 0 }, wasm_on_header_value: (p, at, len) => { assert.strictEqual(currentParser.ptr, p) - const start = at - currentBufferPtr + const start = at - currentBufferPtr + currentBufferRef.byteOffset return currentParser.onHeaderValue(new FastBuffer(currentBufferRef.buffer, start, len)) || 0 }, wasm_on_headers_complete: (p, statusCode, upgrade, shouldKeepAlive) => { @@ -386,7 +386,7 @@ async function lazyllhttp () { }, wasm_on_body: (p, at, len) => { assert.strictEqual(currentParser.ptr, p) - const start = at - currentBufferPtr + const start = at - currentBufferPtr + currentBufferRef.byteOffset return currentParser.onBody(new FastBuffer(currentBufferRef.buffer, start, len)) || 0 }, wasm_on_message_complete: (p) => { From ce6a53bcd3ba54b761a0f4bda350b8d0e4283d66 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Sat, 4 Feb 2023 12:47:18 +0100 Subject: [PATCH 037/123] 5.17.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3cfd02005d2..8ced1d9222c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "5.17.0", + "version": "5.17.1", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { From b4c0e5a4efe6d48c7ee11a77668a70ba7ee11f71 Mon Sep 17 00:00:00 2001 From: Sean Kelly Date: Sat, 4 Feb 2023 11:27:46 -0800 Subject: [PATCH 038/123] Add ability to set TCP keepalive (#1904) --- docs/api/Client.md | 2 ++ lib/core/connect.js | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/docs/api/Client.md b/docs/api/Client.md index 2b5f9e526bb..1aca7d09d31 100644 --- a/docs/api/Client.md +++ b/docs/api/Client.md @@ -38,6 +38,8 @@ Furthermore, the following options can be passed: * **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` * **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 ### Example - Basic Client instantiation diff --git a/lib/core/connect.js b/lib/core/connect.js index 89561f16fbe..f3b5cc33edd 100644 --- a/lib/core/connect.js +++ b/lib/core/connect.js @@ -120,6 +120,12 @@ function buildConnector ({ maxCachedSessions, socketPath, timeout, ...opts }) { }) } + // Set TCP keep alive options on the socket here instead of in connect() for the case of assigning the socket + if (options.keepAlive == null || options.keepAlive) { + const keepAliveInitialDelay = options.keepAliveInitialDelay === undefined ? 60e3 : options.keepAliveInitialDelay + socket.setKeepAlive(true, keepAliveInitialDelay) + } + const cancelTimeout = setupTimeout(() => onConnectTimeout(socket), timeout) socket From b06f19f3add31613203c857a65660dd63d0bb42b Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Sun, 5 Feb 2023 20:15:33 +0100 Subject: [PATCH 039/123] use faster timers (#1908) * refactor: refreshTimeout * perf: fast timers * fixup * Revert "fixup" This reverts commit 2fc8eaa7195c931481d902a3f2583f38317td0be8. * fixup * fixup * fixup * fixup * fixup * fixup * fixup * fixup * fixup * Update lib/timers.js * Update lib/timers.js * Update lib/timers.js --- lib/client.js | 7 ++- lib/timers.js | 89 ++++++++++++++++++++++++++++ test/client-keep-alive.js | 7 +++ test/client-reconnect.js | 7 +++ test/client-timeout.js | 15 ++++- test/fetch/fetch-timeouts.js | 7 +++ test/request-timeout.js | 109 +++++++++++++++++++++++++++++++++++ test/socket-timeout.js | 7 +++ 8 files changed, 244 insertions(+), 4 deletions(-) create mode 100644 lib/timers.js diff --git a/lib/client.js b/lib/client.js index 496c568d732..983e7034795 100644 --- a/lib/client.js +++ b/lib/client.js @@ -5,6 +5,7 @@ const assert = require('assert') const net = require('net') const util = require('./core/util') +const timers = require('./timers') const Request = require('./core/request') const DispatcherBase = require('./dispatcher-base') const { @@ -444,9 +445,9 @@ class Parser { setTimeout (value, type) { this.timeoutType = type if (value !== this.timeoutValue) { - clearTimeout(this.timeout) + timers.clearTimeout(this.timeout) if (value) { - this.timeout = setTimeout(onParserTimeout, value, this) + this.timeout = timers.setTimeout(onParserTimeout, value, this) // istanbul ignore else: only for jest if (this.timeout.unref) { this.timeout.unref() @@ -562,7 +563,7 @@ class Parser { this.llhttp.llhttp_free(this.ptr) this.ptr = null - clearTimeout(this.timeout) + timers.clearTimeout(this.timeout) this.timeout = null this.timeoutValue = null this.timeoutType = null diff --git a/lib/timers.js b/lib/timers.js new file mode 100644 index 00000000000..f96bc62f286 --- /dev/null +++ b/lib/timers.js @@ -0,0 +1,89 @@ +'use strict' + +let fastNow = Date.now() +let fastNowTimeout + +const fastTimers = [] + +function onTimeout () { + fastNow = Date.now() + + let len = fastTimers.length + let idx = 0 + while (idx < len) { + const timer = fastTimers[idx] + + if (timer.expires && fastNow >= timer.expires) { + timer.expires = 0 + timer.callback(timer.opaque) + } + + if (timer.expires === 0) { + timer.active = false + if (idx !== len - 1) { + fastTimers[idx] = fastTimers.pop() + } else { + fastTimers.pop() + } + len -= 1 + } else { + idx += 1 + } + } + + if (fastTimers.length > 0) { + refreshTimeout() + } +} + +function refreshTimeout () { + if (fastNowTimeout && fastNowTimeout.refresh) { + fastNowTimeout.refresh() + } else { + clearTimeout(fastNowTimeout) + fastNowTimeout = setTimeout(onTimeout, 1e3) + if (fastNowTimeout.unref) { + fastNowTimeout.unref() + } + } +} + +class Timeout { + constructor (callback, delay, opaque) { + this.callback = callback + this.delay = delay + this.opaque = opaque + this.expires = 0 + this.active = false + + this.refresh() + } + + refresh () { + if (!this.active) { + this.active = true + fastTimers.push(this) + if (!fastNowTimeout || fastTimers.length === 1) { + refreshTimeout() + fastNow = Date.now() + } + } + + this.expires = fastNow + this.delay + } + + clear () { + this.expires = 0 + } +} + +module.exports = { + setTimeout (callback, delay, opaque) { + return new Timeout(callback, delay, opaque) + }, + clearTimeout (timeout) { + if (timeout && timeout.clear) { + timeout.clear() + } + } +} diff --git a/test/client-keep-alive.js b/test/client-keep-alive.js index b0f81606067..5360675c65c 100644 --- a/test/client-keep-alive.js +++ b/test/client-keep-alive.js @@ -2,6 +2,7 @@ const { test } = require('tap') const { Client } = require('..') +const timers = require('../lib/timers') const { kConnect } = require('../lib/core/symbols') const { createServer } = require('net') const http = require('http') @@ -47,6 +48,12 @@ test('keep-alive header 0', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + const server = createServer((socket) => { socket.write('HTTP/1.1 200 OK\r\n') socket.write('Content-Length: 0\r\n') diff --git a/test/client-reconnect.js b/test/client-reconnect.js index 6abb8e403f3..ae1a206de63 100644 --- a/test/client-reconnect.js +++ b/test/client-reconnect.js @@ -4,6 +4,7 @@ const { test } = require('tap') const { Client } = require('..') const { createServer } = require('http') const FakeTimers = require('@sinonjs/fake-timers') +const timers = require('../lib/timers') test('multiple reconnect', (t) => { t.plan(5) @@ -12,6 +13,12 @@ test('multiple reconnect', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + const server = createServer((req, res) => { n === 0 ? res.destroy() : res.end('ok') }) diff --git a/test/client-timeout.js b/test/client-timeout.js index eedf1f48c97..5f1686a0251 100644 --- a/test/client-timeout.js +++ b/test/client-timeout.js @@ -5,6 +5,7 @@ const { Client, errors } = require('..') const { createServer } = require('http') const { Readable } = require('stream') const FakeTimers = require('@sinonjs/fake-timers') +const timers = require('../lib/timers') test('refresh timeout on pause', (t) => { t.plan(1) @@ -51,6 +52,12 @@ test('start headers timeout after request body', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + const server = createServer((req, res) => { }) t.teardown(server.close.bind(server)) @@ -101,6 +108,12 @@ test('start headers timeout after async iterator request body', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + const server = createServer((req, res) => { }) t.teardown(server.close.bind(server)) @@ -167,7 +180,7 @@ test('parser resume with no body timeout', (t) => { onConnect () { }, onHeaders (statusCode, headers, resume) { - setTimeout(resume, 100) + setTimeout(resume, 2000) return false }, onData () { diff --git a/test/fetch/fetch-timeouts.js b/test/fetch/fetch-timeouts.js index adbf888ebba..b659aaa08d6 100644 --- a/test/fetch/fetch-timeouts.js +++ b/test/fetch/fetch-timeouts.js @@ -3,6 +3,7 @@ const { test } = require('tap') const { fetch, Agent } = require('../..') +const timers = require('../../lib/timers') const { createServer } = require('http') const FakeTimers = require('@sinonjs/fake-timers') @@ -16,6 +17,12 @@ test('Fetch very long request, timeout overridden so no error', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + const server = createServer((req, res) => { setTimeout(() => { res.end('hello') diff --git a/test/request-timeout.js b/test/request-timeout.js index 2d2e826acac..972ebd0b373 100644 --- a/test/request-timeout.js +++ b/test/request-timeout.js @@ -4,6 +4,7 @@ const { test } = require('tap') const { createReadStream, writeFileSync, unlinkSync } = require('fs') const { Client, errors } = require('..') const { kConnect } = require('../lib/core/symbols') +const timers = require('../lib/timers') const { createServer } = require('http') const EventEmitter = require('events') const FakeTimers = require('@sinonjs/fake-timers') @@ -65,6 +66,12 @@ test('body timeout', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + const server = createServer((req, res) => { res.write('hello') }) @@ -93,6 +100,12 @@ test('overridden request timeout', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + const server = createServer((req, res) => { setTimeout(() => { res.end('hello') @@ -119,6 +132,12 @@ test('overridden body timeout', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + const server = createServer((req, res) => { res.write('hello') }) @@ -147,6 +166,12 @@ test('With EE signal', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + const server = createServer((req, res) => { setTimeout(() => { res.end('hello') @@ -176,6 +201,12 @@ test('With abort-controller signal', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + const server = createServer((req, res) => { setTimeout(() => { res.end('hello') @@ -205,6 +236,12 @@ test('Abort before timeout (EE)', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + const ee = new EventEmitter() const server = createServer((req, res) => { setTimeout(() => { @@ -234,6 +271,12 @@ test('Abort before timeout (abort-controller)', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + const abortController = new AbortController() const server = createServer((req, res) => { setTimeout(() => { @@ -263,6 +306,12 @@ test('Timeout with pipelining', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + const server = createServer((req, res) => { setTimeout(() => { res.end('hello') @@ -298,6 +347,12 @@ test('Global option', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + const server = createServer((req, res) => { setTimeout(() => { res.end('hello') @@ -326,6 +381,12 @@ test('Request options overrides global option', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + const server = createServer((req, res) => { setTimeout(() => { res.end('hello') @@ -377,6 +438,12 @@ test('client.close should wait for the timeout', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + const server = createServer((req, res) => { }) t.teardown(server.close.bind(server)) @@ -449,6 +516,12 @@ test('Disable request timeout', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + const server = createServer((req, res) => { setTimeout(() => { res.end('hello') @@ -485,6 +558,12 @@ test('Disable request timeout for a single request', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + const server = createServer((req, res) => { setTimeout(() => { res.end('hello') @@ -521,6 +600,12 @@ test('stream timeout', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + const server = createServer((req, res) => { setTimeout(() => { res.end('hello') @@ -551,6 +636,12 @@ test('stream custom timeout', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + const server = createServer((req, res) => { setTimeout(() => { res.end('hello') @@ -583,6 +674,12 @@ test('pipeline timeout', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + const server = createServer((req, res) => { setTimeout(() => { req.pipe(res) @@ -632,6 +729,12 @@ test('pipeline timeout', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + const server = createServer((req, res) => { setTimeout(() => { req.pipe(res) @@ -683,6 +786,12 @@ test('client.close should not deadlock', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + const server = createServer((req, res) => { }) t.teardown(server.close.bind(server)) diff --git a/test/socket-timeout.js b/test/socket-timeout.js index 6ce58369643..8019c74198a 100644 --- a/test/socket-timeout.js +++ b/test/socket-timeout.js @@ -2,6 +2,7 @@ const { test } = require('tap') const { Client, errors } = require('..') +const timers = require('../lib/timers') const { createServer } = require('http') const FakeTimers = require('@sinonjs/fake-timers') @@ -64,6 +65,12 @@ test('Disable socket timeout', (t) => { const clock = FakeTimers.install() t.teardown(clock.uninstall.bind(clock)) + const orgTimers = { ...timers } + Object.assign(timers, { setTimeout, clearTimeout }) + t.teardown(() => { + Object.assign(timers, orgTimers) + }) + server.once('request', (req, res) => { setTimeout(() => { res.end('hello') From 3bc7b4c4c10acf9ef08d97331672c9417954c776 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Mon, 6 Feb 2023 06:59:41 +0100 Subject: [PATCH 040/123] fix: ensure header value is a string (#1899) * fix: ensure header value is a string * fixup * fixup * fixup * fixup --- lib/core/request.js | 8 ++++++-- test/client-dispatch.js | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/core/request.js b/lib/core/request.js index e476fc1bcc1..7271bc64f24 100644 --- a/lib/core/request.js +++ b/lib/core/request.js @@ -279,9 +279,13 @@ class Request { } function processHeaderValue (key, val) { - if (val && (typeof val === 'object' && !Array.isArray(val))) { + if (val && typeof val === 'object') { throw new InvalidArgumentError(`invalid ${key} header`) - } else if (headerCharRegex.exec(val) !== null) { + } + + val = val != null ? `${val}` : '' + + if (headerCharRegex.exec(val) !== null) { throw new InvalidArgumentError(`invalid ${key} header`) } diff --git a/test/client-dispatch.js b/test/client-dispatch.js index 9e59fdb2fb5..2196e5d53f5 100644 --- a/test/client-dispatch.js +++ b/test/client-dispatch.js @@ -94,7 +94,7 @@ test('basic dispatch get', (t) => { t.equal(`localhost:${server.address().port}`, req.headers.host) t.equal(undefined, req.headers.foo) t.equal('bar', req.headers.bar) - t.equal('null', req.headers.baz) + t.equal('', req.headers.baz) t.equal(undefined, req.headers['content-length']) res.end('hello') }) From 6641f9380dde2de287d615ca555dbbeeffaa3f6c Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Mon, 6 Feb 2023 07:02:43 +0100 Subject: [PATCH 041/123] fix: flaky test --- test/client-keep-alive.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/client-keep-alive.js b/test/client-keep-alive.js index 5360675c65c..e752995f1f9 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() - }, 2e3) + }, 3e3) client.on('disconnect', () => { t.pass() clearTimeout(timeout) @@ -142,7 +142,7 @@ test('keep-alive header no postfix', (t) => { body.on('end', () => { const timeout = setTimeout(() => { t.fail() - }, 2e3) + }, 3e3) client.on('disconnect', () => { t.pass() clearTimeout(timeout) @@ -178,7 +178,7 @@ test('keep-alive not timeout', (t) => { body.on('end', () => { const timeout = setTimeout(() => { t.fail() - }, 2e3) + }, 3e3) client.on('disconnect', () => { t.pass() clearTimeout(timeout) @@ -215,7 +215,7 @@ test('keep-alive threshold', (t) => { body.on('end', () => { const timeout = setTimeout(() => { t.fail() - }, 2e3) + }, 3e3) client.on('disconnect', () => { t.pass() clearTimeout(timeout) @@ -252,7 +252,7 @@ test('keep-alive max keepalive', (t) => { body.on('end', () => { const timeout = setTimeout(() => { t.fail() - }, 2e3) + }, 3e3) client.on('disconnect', () => { t.pass() clearTimeout(timeout) From 9dceb21156f85de1e0757785dc1da4cbe6eb9853 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Mon, 6 Feb 2023 08:06:58 +0100 Subject: [PATCH 042/123] 5.18.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8ced1d9222c..af56b2ab98f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "5.17.1", + "version": "5.18.0", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { From 7827031d2a7fd99f8ab9d5eddce127c6f700dd35 Mon Sep 17 00:00:00 2001 From: Khafra Date: Tue, 7 Feb 2023 02:35:35 -0500 Subject: [PATCH 043/123] fix(fetch): raise AbortSignal max event listeners (#1910) --- lib/fetch/request.js | 6 ++++++ test/fetch/issue-node-46525.js | 28 ++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 test/fetch/issue-node-46525.js diff --git a/lib/fetch/request.js b/lib/fetch/request.js index 7264d152f87..eca7b060e0e 100644 --- a/lib/fetch/request.js +++ b/lib/fetch/request.js @@ -28,6 +28,7 @@ const { getGlobalOrigin } = require('./global') const { URLSerializer } = require('./dataURL') const { kHeadersList } = require('../core/symbols') const assert = require('assert') +const { setMaxListeners, getEventListeners, defaultMaxListeners } = require('events') let TransformStream = globalThis.TransformStream @@ -352,6 +353,11 @@ class Request { const abort = function () { acRef.deref()?.abort(this.reason) } + + if (getEventListeners(signal, 'abort').length >= defaultMaxListeners) { + setMaxListeners(100, signal) + } + signal.addEventListener('abort', abort, { once: true }) requestFinalizer.register(this, { signal, abort }) } diff --git a/test/fetch/issue-node-46525.js b/test/fetch/issue-node-46525.js new file mode 100644 index 00000000000..6fd9810c52b --- /dev/null +++ b/test/fetch/issue-node-46525.js @@ -0,0 +1,28 @@ +'use strict' + +const { once } = require('events') +const { createServer } = require('http') +const { test } = require('tap') +const { fetch } = require('../..') + +// https://github.com/nodejs/node/issues/46525 +test('No warning when reusing AbortController', async (t) => { + function onWarning (error) { + t.error(error, 'Got warning') + } + + const server = createServer((req, res) => res.end()).listen(0) + + await once(server, 'listening') + + process.on('warning', onWarning) + t.teardown(() => { + process.off('warning', onWarning) + return server.close() + }) + + const controller = new AbortController() + for (let i = 0; i < 15; i++) { + await fetch(`http://localhost:${server.address().port}`, { signal: controller.signal }) + } +}) From c0ba75f784765e81722d0c2b31c08d273e04b0f6 Mon Sep 17 00:00:00 2001 From: KaKa Date: Wed, 8 Feb 2023 11:29:31 +0800 Subject: [PATCH 044/123] fix: content-disposition header parsing (#1911) --- lib/core/util.js | 23 +++++++++++-- lib/mock/mock-utils.js | 6 +++- test/issue-1903.js | 77 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+), 4 deletions(-) create mode 100644 test/issue-1903.js diff --git a/lib/core/util.js b/lib/core/util.js index 6f38ffc8a4d..3b9b56cb64d 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -213,25 +213,42 @@ function parseHeaders (headers, obj = {}) { for (let i = 0; i < headers.length; i += 2) { 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() + obj[key] = headers[i + 1].toString(encoding) } } else { if (!Array.isArray(val)) { val = [val] obj[key] = val } - val.push(headers[i + 1].toString()) + val.push(headers[i + 1].toString(encoding)) } } return obj } function parseRawHeaders (headers) { - return headers.map(header => header.toString()) + const ret = [] + for (let n = 0; n < headers.length; n += 2) { + const key = headers[n + 0].toString() + + const encoding = key.length === 19 && key.toLowerCase() === 'content-disposition' + ? 'latin1' + : 'utf8' + + const val = headers[n + 1].toString(encoding) + + ret.push(key, val) + } + return ret } function isBuffer (buffer) { diff --git a/lib/mock/mock-utils.js b/lib/mock/mock-utils.js index 9dc414d0a35..42ea185cc0e 100644 --- a/lib/mock/mock-utils.js +++ b/lib/mock/mock-utils.js @@ -188,7 +188,11 @@ function buildKey (opts) { } function generateKeyValues (data) { - return Object.entries(data).reduce((keyValuePairs, [key, value]) => [...keyValuePairs, key, value], []) + return Object.entries(data).reduce((keyValuePairs, [key, value]) => [ + ...keyValuePairs, + Buffer.from(`${key}`), + Array.isArray(value) ? value.map(x => Buffer.from(`${x}`)) : Buffer.from(`${value}`) + ], []) } /** diff --git a/test/issue-1903.js b/test/issue-1903.js new file mode 100644 index 00000000000..8478b2d53dd --- /dev/null +++ b/test/issue-1903.js @@ -0,0 +1,77 @@ +'use strict' + +const { createServer } = require('http') +const { test } = require('tap') +const { request } = require('..') + +function createPromise () { + const result = {} + result.promise = new Promise((resolve) => { + result.resolve = resolve + }) + return result +} + +test('should parse content-disposition consistencely', async (t) => { + t.plan(5) + + // create promise to allow server spinup in parallel + const spinup1 = createPromise() + const spinup2 = createPromise() + const spinup3 = createPromise() + + // variables to store content-disposition header + const header = [] + + const server = createServer((req, res) => { + res.writeHead(200, { + 'content-length': 2, + 'content-disposition': "attachment; filename='år.pdf'" + }) + header.push("attachment; filename='år.pdf'") + res.end('OK', spinup1.resolve) + }) + t.teardown(server.close.bind(server)) + server.listen(0, spinup1.resolve) + + const proxy1 = createServer(async (req, res) => { + const { statusCode, headers, body } = await request(`http://localhost:${server.address().port}`, { + method: 'GET' + }) + header.push(headers['content-disposition']) + delete headers['transfer-encoding'] + res.writeHead(statusCode, headers) + body.pipe(res) + }) + t.teardown(proxy1.close.bind(proxy1)) + proxy1.listen(0, spinup2.resolve) + + const proxy2 = createServer(async (req, res) => { + const { statusCode, headers, body } = await request(`http://localhost:${proxy1.address().port}`, { + method: 'GET' + }) + header.push(headers['content-disposition']) + delete headers['transfer-encoding'] + res.writeHead(statusCode, headers) + body.pipe(res) + }) + t.teardown(proxy2.close.bind(proxy2)) + proxy2.listen(0, spinup3.resolve) + + // wait until all server spinup + await Promise.all([spinup1.promise, spinup2.promise, spinup3.promise]) + + const { statusCode, headers, body } = await request(`http://localhost:${proxy2.address().port}`, { + method: 'GET' + }) + header.push(headers['content-disposition']) + t.equal(statusCode, 200) + t.equal(await body.text(), 'OK') + + // we check header + // must not be the same in first proxy + t.notSame(header[0], header[1]) + // chaining always the same value + t.equal(header[1], header[2]) + t.equal(header[2], header[3]) +}) From 0a5953501640c8efb3c5979a3ca5b53bc9793876 Mon Sep 17 00:00:00 2001 From: Khafra Date: Wed, 8 Feb 2023 12:05:27 -0500 Subject: [PATCH 045/123] fix: remove test (#1916) --- test/fetch/issue-1137.js | 24 ------------------------ 1 file changed, 24 deletions(-) delete mode 100644 test/fetch/issue-1137.js diff --git a/test/fetch/issue-1137.js b/test/fetch/issue-1137.js deleted file mode 100644 index 2b1585f9c3b..00000000000 --- a/test/fetch/issue-1137.js +++ /dev/null @@ -1,24 +0,0 @@ -'use strict' - -const { fetch } = require('../..') -const { test } = require('tap') - -/* global AbortController */ -const controller = new AbortController() - -test('abort fetch', (t) => { - fetch( - 'https://speed.hetzner.de/100MB.bin', { - signal: controller.signal - }).then(response => { - (async () => { - try { - await response.text() - } catch (err) { - t.equal(err.name, 'AbortError') - } - t.end() - })() - controller.abort() - }) -}) From ba5ef44b71eff5a86a8473850a326ff7392664d3 Mon Sep 17 00:00:00 2001 From: Khafra Date: Wed, 8 Feb 2023 15:04:43 -0500 Subject: [PATCH 046/123] feat: add Headers.prototype.getSetCookie (#1915) --- lib/cookies/index.js | 3 +- lib/fetch/headers.js | 67 +++++- test/wpt/status/fetch.status.json | 5 + .../fetch/api/headers/header-setcookie.any.js | 224 ++++++++++++++++++ 4 files changed, 290 insertions(+), 9 deletions(-) create mode 100644 test/wpt/tests/fetch/api/headers/header-setcookie.any.js diff --git a/lib/cookies/index.js b/lib/cookies/index.js index def04f5bc4a..c9c1f28ee1f 100644 --- a/lib/cookies/index.js +++ b/lib/cookies/index.js @@ -83,7 +83,8 @@ function getSetCookies (headers) { return [] } - return cookies.map((pair) => parseSetCookie(pair[1])) + // In older versions of undici, cookies is a list of name:value. + return cookies.map((pair) => parseSetCookie(Array.isArray(pair) ? pair[1] : pair)) } /** diff --git a/lib/fetch/headers.js b/lib/fetch/headers.js index 0febd114db5..af2c64bad8d 100644 --- a/lib/fetch/headers.js +++ b/lib/fetch/headers.js @@ -11,6 +11,7 @@ const { isValidHeaderValue } = require('./util') const { webidl } = require('./webidl') +const assert = require('assert') const kHeadersMap = Symbol('headers map') const kHeadersSortedMap = Symbol('headers map sorted') @@ -115,7 +116,7 @@ class HeadersList { if (lowercaseName === 'set-cookie') { this.cookies ??= [] - this.cookies.push([name, value]) + this.cookies.push(value) } } @@ -125,7 +126,7 @@ class HeadersList { const lowercaseName = name.toLowerCase() if (lowercaseName === 'set-cookie') { - this.cookies = [[name, value]] + this.cookies = [value] } // 1. If list contains name, then set the value of @@ -383,18 +384,68 @@ class Headers { return this[kHeadersList].set(name, value) } + // https://fetch.spec.whatwg.org/#dom-headers-getsetcookie + getSetCookie () { + webidl.brandCheck(this, Headers) + + // 1. If this’s header list does not contain `Set-Cookie`, then return « ». + // 2. Return the values of all headers in this’s header list whose name is + // a byte-case-insensitive match for `Set-Cookie`, in order. + + return this[kHeadersList].cookies ?? [] + } + + // https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine get [kHeadersSortedMap] () { - if (!this[kHeadersList][kHeadersSortedMap]) { - this[kHeadersList][kHeadersSortedMap] = new Map([...this[kHeadersList]].sort((a, b) => a[0] < b[0] ? -1 : 1)) + if (this[kHeadersList][kHeadersSortedMap]) { + return this[kHeadersList][kHeadersSortedMap] } - return this[kHeadersList][kHeadersSortedMap] + + // 1. Let headers be an empty list of headers with the key being the name + // and value the value. + const headers = [] + + // 2. Let names be the result of convert header names to a sorted-lowercase + // set with all the names of the headers in list. + const names = [...this[kHeadersList]].sort((a, b) => a[0] < b[0] ? -1 : 1) + const cookies = this[kHeadersList].cookies + + // 3. For each name of names: + for (const [name, value] of names) { + // 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 + // is a byte-case-insensitive match for name, in order. + + // 2. For each value of values: + // 1. Append (name, value) to headers. + for (const value of cookies) { + headers.push([name, value]) + } + } else { + // 2. Otherwise: + + // 1. Let value be the result of getting name from list. + + // 2. Assert: value is non-null. + assert(value !== null) + + // 3. Append (name, value) to headers. + headers.push([name, value]) + } + } + + this[kHeadersList][kHeadersSortedMap] = headers + + // 4. Return headers. + return headers } keys () { webidl.brandCheck(this, Headers) return makeIterator( - () => [...this[kHeadersSortedMap].entries()], + () => [...this[kHeadersSortedMap].values()], 'Headers', 'key' ) @@ -404,7 +455,7 @@ class Headers { webidl.brandCheck(this, Headers) return makeIterator( - () => [...this[kHeadersSortedMap].entries()], + () => [...this[kHeadersSortedMap].values()], 'Headers', 'value' ) @@ -414,7 +465,7 @@ class Headers { webidl.brandCheck(this, Headers) return makeIterator( - () => [...this[kHeadersSortedMap].entries()], + () => [...this[kHeadersSortedMap].values()], 'Headers', 'key+value' ) diff --git a/test/wpt/status/fetch.status.json b/test/wpt/status/fetch.status.json index 1a6a5084746..380f0153f7c 100644 --- a/test/wpt/status/fetch.status.json +++ b/test/wpt/status/fetch.status.json @@ -206,5 +206,10 @@ "fetch() with value %1E", "fetch() with value %1F" ] + }, + "header-setcookie.any.js": { + "fail": [ + "Set-Cookie is a forbidden response header" + ] } } diff --git a/test/wpt/tests/fetch/api/headers/header-setcookie.any.js b/test/wpt/tests/fetch/api/headers/header-setcookie.any.js new file mode 100644 index 00000000000..b8a3348f5f5 --- /dev/null +++ b/test/wpt/tests/fetch/api/headers/header-setcookie.any.js @@ -0,0 +1,224 @@ +// META: title=Headers set-cookie special cases +// META: global=window,worker + +const headerList = [ + ["set-cookie", "foo=bar"], + ["Set-Cookie", "fizz=buzz; domain=example.com"], +]; + +const setCookie2HeaderList = [ + ["set-cookie2", "foo2=bar2"], + ["Set-Cookie2", "fizz2=buzz2; domain=example2.com"], +]; + +function assert_nested_array_equals(actual, expected) { + assert_equals(actual.length, expected.length, "Array length is not equal"); + for (let i = 0; i < expected.length; i++) { + assert_array_equals(actual[i], expected[i]); + } +} + +test(function () { + const headers = new Headers(headerList); + assert_equals( + headers.get("set-cookie"), + "foo=bar, fizz=buzz; domain=example.com", + ); +}, "Headers.prototype.get combines set-cookie headers in order"); + +test(function () { + const headers = new Headers(headerList); + const list = [...headers]; + assert_nested_array_equals(list, [ + ["set-cookie", "foo=bar"], + ["set-cookie", "fizz=buzz; domain=example.com"], + ]); +}, "Headers iterator does not combine set-cookie headers"); + +test(function () { + const headers = new Headers(setCookie2HeaderList); + const list = [...headers]; + assert_nested_array_equals(list, [ + ["set-cookie2", "foo2=bar2, fizz2=buzz2; domain=example2.com"], + ]); +}, "Headers iterator does not special case set-cookie2 headers"); + +test(function () { + const headers = new Headers([...headerList, ...setCookie2HeaderList]); + const list = [...headers]; + assert_nested_array_equals(list, [ + ["set-cookie", "foo=bar"], + ["set-cookie", "fizz=buzz; domain=example.com"], + ["set-cookie2", "foo2=bar2, fizz2=buzz2; domain=example2.com"], + ]); +}, "Headers iterator does not combine set-cookie & set-cookie2 headers"); + +test(function () { + // Values are in non alphabetic order, and the iterator should yield in the + // headers in the exact order of the input. + const headers = new Headers([ + ["set-cookie", "z=z"], + ["set-cookie", "a=a"], + ["set-cookie", "n=n"], + ]); + const list = [...headers]; + assert_nested_array_equals(list, [ + ["set-cookie", "z=z"], + ["set-cookie", "a=a"], + ["set-cookie", "n=n"], + ]); +}, "Headers iterator preserves set-cookie ordering"); + +test( + function () { + const headers = new Headers([ + ["xylophone-header", "1"], + ["best-header", "2"], + ["set-cookie", "3"], + ["a-cool-header", "4"], + ["set-cookie", "5"], + ["a-cool-header", "6"], + ["best-header", "7"], + ]); + const list = [...headers]; + assert_nested_array_equals(list, [ + ["a-cool-header", "4, 6"], + ["best-header", "2, 7"], + ["set-cookie", "3"], + ["set-cookie", "5"], + ["xylophone-header", "1"], + ]); + }, + "Headers iterator preserves per header ordering, but sorts keys alphabetically", +); + +test( + function () { + const headers = new Headers([ + ["xylophone-header", "7"], + ["best-header", "6"], + ["set-cookie", "5"], + ["a-cool-header", "4"], + ["set-cookie", "3"], + ["a-cool-header", "2"], + ["best-header", "1"], + ]); + const list = [...headers]; + assert_nested_array_equals(list, [ + ["a-cool-header", "4, 2"], + ["best-header", "6, 1"], + ["set-cookie", "5"], + ["set-cookie", "3"], + ["xylophone-header", "7"], + ]); + }, + "Headers iterator preserves per header ordering, but sorts keys alphabetically (and ignores value ordering)", +); + +test(function () { + const headers = new Headers([["fizz", "buzz"], ["X-Header", "test"]]); + const iterator = headers[Symbol.iterator](); + assert_array_equals(iterator.next().value, ["fizz", "buzz"]); + headers.append("Set-Cookie", "a=b"); + assert_array_equals(iterator.next().value, ["set-cookie", "a=b"]); + headers.append("Accept", "text/html"); + assert_array_equals(iterator.next().value, ["set-cookie", "a=b"]); + assert_array_equals(iterator.next().value, ["x-header", "test"]); + headers.append("set-cookie", "c=d"); + assert_array_equals(iterator.next().value, ["x-header", "test"]); + assert_true(iterator.next().done); +}, "Headers iterator is correctly updated with set-cookie changes"); + +test(function () { + const headers = new Headers(headerList); + assert_true(headers.has("sEt-cOoKiE")); +}, "Headers.prototype.has works for set-cookie"); + +test(function () { + const headers = new Headers(setCookie2HeaderList); + headers.append("set-Cookie", "foo=bar"); + headers.append("sEt-cOoKiE", "fizz=buzz"); + const list = [...headers]; + assert_nested_array_equals(list, [ + ["set-cookie", "foo=bar"], + ["set-cookie", "fizz=buzz"], + ["set-cookie2", "foo2=bar2, fizz2=buzz2; domain=example2.com"], + ]); +}, "Headers.prototype.append works for set-cookie"); + +test(function () { + const headers = new Headers(headerList); + headers.set("set-cookie", "foo2=bar2"); + const list = [...headers]; + assert_nested_array_equals(list, [ + ["set-cookie", "foo2=bar2"], + ]); +}, "Headers.prototype.set works for set-cookie"); + +test(function () { + const headers = new Headers(headerList); + headers.delete("set-Cookie"); + const list = [...headers]; + assert_nested_array_equals(list, []); +}, "Headers.prototype.delete works for set-cookie"); + +test(function () { + const headers = new Headers(); + assert_array_equals(headers.getSetCookie(), []); +}, "Headers.prototype.getSetCookie with no headers present"); + +test(function () { + const headers = new Headers([headerList[0]]); + assert_array_equals(headers.getSetCookie(), ["foo=bar"]); +}, "Headers.prototype.getSetCookie with one header"); + +test(function () { + const headers = new Headers({ "Set-Cookie": "foo=bar" }); + assert_array_equals(headers.getSetCookie(), ["foo=bar"]); +}, "Headers.prototype.getSetCookie with one header created from an object"); + +test(function () { + const headers = new Headers(headerList); + assert_array_equals(headers.getSetCookie(), [ + "foo=bar", + "fizz=buzz; domain=example.com", + ]); +}, "Headers.prototype.getSetCookie with multiple headers"); + +test(function () { + const headers = new Headers([["set-cookie", ""]]); + assert_array_equals(headers.getSetCookie(), [""]); +}, "Headers.prototype.getSetCookie with an empty header"); + +test(function () { + const headers = new Headers([["set-cookie", "x"], ["set-cookie", "x"]]); + assert_array_equals(headers.getSetCookie(), ["x", "x"]); +}, "Headers.prototype.getSetCookie with two equal headers"); + +test(function () { + const headers = new Headers([ + ["set-cookie2", "x"], + ["set-cookie", "y"], + ["set-cookie2", "z"], + ]); + assert_array_equals(headers.getSetCookie(), ["y"]); +}, "Headers.prototype.getSetCookie ignores set-cookie2 headers"); + +test(function () { + // Values are in non alphabetic order, and the iterator should yield in the + // headers in the exact order of the input. + const headers = new Headers([ + ["set-cookie", "z=z"], + ["set-cookie", "a=a"], + ["set-cookie", "n=n"], + ]); + assert_array_equals(headers.getSetCookie(), ["z=z", "a=a", "n=n"]); +}, "Headers.prototype.getSetCookie preserves header ordering"); + +test(function () { + const response = new Response(); + response.headers.append("Set-Cookie", "foo=bar"); + assert_array_equals(response.headers.getSetCookie(), []); + response.headers.append("sEt-cOokIe", "bar=baz"); + assert_array_equals(response.headers.getSetCookie(), []); +}, "Set-Cookie is a forbidden response header"); From 87fa73498d6014a33989179cfaa4347dcb29600f Mon Sep 17 00:00:00 2001 From: Khafra Date: Thu, 9 Feb 2023 00:15:58 -0500 Subject: [PATCH 047/123] fix(headers): clone getSetCookie list & add getSetCookie type (#1917) --- lib/fetch/headers.js | 8 +++++++- test/fetch/headers.js | 18 ++++++++++++++++++ test/types/fetch.test-d.ts | 4 +++- types/fetch.d.ts | 1 + 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/lib/fetch/headers.js b/lib/fetch/headers.js index af2c64bad8d..634b81fc74a 100644 --- a/lib/fetch/headers.js +++ b/lib/fetch/headers.js @@ -392,7 +392,13 @@ class Headers { // 2. Return the values of all headers in this’s header list whose name is // a byte-case-insensitive match for `Set-Cookie`, in order. - return this[kHeadersList].cookies ?? [] + const list = this[kHeadersList].cookies + + if (list) { + return [...list] + } + + return [] } // https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine diff --git a/test/fetch/headers.js b/test/fetch/headers.js index f4508d01fd9..7ae85b815b5 100644 --- a/test/fetch/headers.js +++ b/test/fetch/headers.js @@ -665,3 +665,21 @@ tap.test('invalid headers', (t) => { t.end() }) + +tap.test('Headers.prototype.getSetCookie', (t) => { + t.test('Mutating the returned list does not affect the set-cookie list', (t) => { + const h = new Headers([ + ['set-cookie', 'a=b'], + ['set-cookie', 'c=d'] + ]) + + const old = h.getSetCookie() + h.getSetCookie().push('oh=no') + const now = h.getSetCookie() + + t.same(old, now) + t.end() + }) + + t.end() +}) diff --git a/test/types/fetch.test-d.ts b/test/types/fetch.test-d.ts index a76d6d7043c..e11296aa85c 100644 --- a/test/types/fetch.test-d.ts +++ b/test/types/fetch.test-d.ts @@ -168,4 +168,6 @@ expectType(response.clone()) expectType(new Request('https://example.com', { body: 'Hello, world', duplex: 'half' })) expectAssignable({ duplex: 'half' }) -expectNotAssignable({ duplex: 'not valid' }) \ No newline at end of file +expectNotAssignable({ duplex: 'not valid' }) + +expectType(headers.getSetCookie()) diff --git a/types/fetch.d.ts b/types/fetch.d.ts index 58bd4d2a769..fa4619c9182 100644 --- a/types/fetch.d.ts +++ b/types/fetch.d.ts @@ -59,6 +59,7 @@ export declare class Headers implements SpecIterable<[string, string]> { readonly get: (name: string) => string | null readonly has: (name: string) => boolean readonly set: (name: string, value: string) => void + readonly getSetCookie: () => string[] readonly forEach: ( callbackfn: (value: string, key: string, iterable: Headers) => void, thisArg?: unknown From e155c6db5cec9bc577d548fa7c7378013631c79c Mon Sep 17 00:00:00 2001 From: Guillaume <20539361+p9f@users.noreply.github.com> Date: Fri, 10 Feb 2023 14:51:41 +0000 Subject: [PATCH 048/123] doc(mock): update out-of-date reply documentation (#1913) --- 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 c86f9a30adb..923c157aa64 100644 --- a/docs/api/MockPool.md +++ b/docs/api/MockPool.md @@ -63,7 +63,8 @@ Returns: `MockInterceptor` corresponding to the input options. We can define the behaviour of an intercepted request with the following options. -* **reply** `(statusCode: number, replyData: string | Buffer | object | MockInterceptor.MockResponseDataHandler, responseOptions?: MockResponseOptions) => MockScope` - define a reply for a matching request. You can define this as a callback to read incoming request data. Default for `responseOptions` is `{}`. +* **reply** `(statusCode: number, replyData: string | Buffer | object | MockInterceptor.MockResponseDataHandler, responseOptions?: MockResponseOptions) => MockScope` - define a reply for a matching request. You can define the replyData as a callback to read incoming request data. Default for `responseOptions` is `{}`. +* **reply** `(callback: MockInterceptor.MockReplyOptionsCallback) => MockScope` - define a reply for a matching request, allowing dynamic mocking of all reply options rather than just the data. * **replyWithError** `(error: Error) => MockScope` - define an error for a matching request to throw. * **defaultReplyHeaders** `(headers: Record) => MockInterceptor` - define default headers to be included in subsequent replies. These are in addition to headers on a specific reply. * **defaultReplyTrailers** `(trailers: Record) => MockInterceptor` - define default trailers to be included in subsequent replies. These are in addition to trailers on a specific reply. From aebb232d22e9adafce015b985093114a95b560f0 Mon Sep 17 00:00:00 2001 From: Alex <2273103+SkeLLLa@users.noreply.github.com> Date: Mon, 13 Feb 2023 08:41:38 +0100 Subject: [PATCH 049/123] fix(types): add missing keepAlive params (#1918) --- test/types/connector.test-d.ts | 2 ++ types/connector.d.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/test/types/connector.test-d.ts b/test/types/connector.test-d.ts index 1437d6f333d..bbf07e1d396 100644 --- a/test/types/connector.test-d.ts +++ b/test/types/connector.test-d.ts @@ -25,6 +25,8 @@ expectAssignable(new Client('', { expectAssignable({ checkServerIdentity: () => undefined, // Test if ConnectionOptions is assignable localPort: 1234, // Test if TcpNetConnectOpts is assignable + keepAlive: true, + keepAliveInitialDelay: 12345, }); expectAssignable({ diff --git a/types/connector.d.ts b/types/connector.d.ts index 2b28771af2a..d53d7952b5c 100644 --- a/types/connector.d.ts +++ b/types/connector.d.ts @@ -10,6 +10,8 @@ declare namespace buildConnector { socketPath?: string | null; timeout?: number | null; port?: number; + keepAlive?: boolean | null; + keepAliveInitialDelay?: number | null; } export interface Options { From f7c6c6a4a2aef7ee3b8207c4eeab700cb0cfc7dc Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 13 Feb 2023 11:21:33 +0100 Subject: [PATCH 050/123] Make the fetch() abort test pass locally, on Linux and Mac, Node 18 and 19 (#1927) Signed-off-by: Matteo Collina --- test/fetch/abort.js | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/test/fetch/abort.js b/test/fetch/abort.js index 1b9f1e68e8d..c8472689fb7 100644 --- a/test/fetch/abort.js +++ b/test/fetch/abort.js @@ -43,25 +43,14 @@ test('allows aborting with custom errors', { skip: semver.satisfies(process.vers t.teardown(server.close.bind(server)) await once(server, 'listening') - t.test('Using AbortSignal.timeout without cause', { skip: semver.satisfies(process.version, '>= 19.0.0') }, async (t) => { - await t.rejects( - fetch(`http://localhost:${server.address().port}`, { - signal: AbortSignal.timeout(50) - }), - { - name: 'TimeoutError', - code: DOMException.TIMEOUT_ERR - } - ) - }) - - t.test('Using AbortSignal.timeout with cause', { skip: semver.satisfies(process.version, '< 19.0.0') }, async (t) => { + t.test('Using AbortSignal.timeout with cause', async (t) => { t.plan(2) try { await fetch(`http://localhost:${server.address().port}`, { signal: AbortSignal.timeout(50) }) + t.fail('should throw') } catch (err) { if (err.name === 'TypeError') { const cause = err.cause From f5c89e5c87c7d702996b152c4ad86302b60c4181 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 13 Feb 2023 11:23:03 +0100 Subject: [PATCH 051/123] Bumped v5.19.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 af56b2ab98f..af4e624f718 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "5.18.0", + "version": "5.19.0", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { From a2eff05401358f6595138df963837c24348f2034 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Mon, 13 Feb 2023 12:17:57 +0100 Subject: [PATCH 052/123] Merge pull request from GHSA-5r9g-qh6m-jxff --- lib/core/request.js | 3 +++ test/headers-crlf.js | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 test/headers-crlf.js diff --git a/lib/core/request.js b/lib/core/request.js index 7271bc64f24..adb5bfcb3a8 100644 --- a/lib/core/request.js +++ b/lib/core/request.js @@ -304,6 +304,9 @@ function processHeader (request, key, val) { key.length === 4 && key.toLowerCase() === 'host' ) { + if (headerCharRegex.exec(val) !== null) { + throw new InvalidArgumentError(`invalid ${key} header`) + } // Consumed by Client request.host = val } else if ( diff --git a/test/headers-crlf.js b/test/headers-crlf.js new file mode 100644 index 00000000000..fa90eaadd11 --- /dev/null +++ b/test/headers-crlf.js @@ -0,0 +1,37 @@ +'use strict' + +const { test } = require('tap') +const { Client } = require('..') +const { createServer } = require('http') +const EE = require('events') + +test('CRLF Injection in Nodejs ‘undici’ via host', (t) => { + t.plan(1) + + const server = createServer(async (req, res) => { + res.end() + }) + t.teardown(server.close.bind(server)) + + server.listen(0, async () => { + const client = new Client(`http://localhost:${server.address().port}`) + t.teardown(client.close.bind(client)) + + const unsanitizedContentTypeInput = '12 \r\n\r\naaa:aaa' + + try { + const { body } = await client.request({ + path: '/', + method: 'POST', + headers: { + 'content-type': 'application/json', + 'host': unsanitizedContentTypeInput + }, + body: 'asd' + }) + await body.dump() + } catch (err) { + t.same(err.code, 'UND_ERR_INVALID_ARG') + } + }) +}) From f2324e549943f0b0937b09fb1c0c16cc7c93abdf Mon Sep 17 00:00:00 2001 From: Rich Trott Date: Mon, 13 Feb 2023 03:23:21 -0800 Subject: [PATCH 053/123] Merge pull request from GHSA-r6ch-mqf9-qc9w Refs: https://hackerone.com/bugs?report_id=1784449 Co-authored-by: Matteo Collina --- lib/fetch/headers.js | 10 ++++++---- test/fetch/headers.js | 14 +++++++++++++- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/lib/fetch/headers.js b/lib/fetch/headers.js index 634b81fc74a..ea3b9a14a43 100644 --- a/lib/fetch/headers.js +++ b/lib/fetch/headers.js @@ -24,10 +24,12 @@ function headerValueNormalize (potentialValue) { // To normalize a byte sequence potentialValue, remove // any leading and trailing HTTP whitespace bytes from // potentialValue. - return potentialValue.replace( - /^[\r\n\t ]+|[\r\n\t ]+$/g, - '' - ) + + // 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 ]+/, '') } function fill (headers, object) { diff --git a/test/fetch/headers.js b/test/fetch/headers.js index 7ae85b815b5..1f9196c4697 100644 --- a/test/fetch/headers.js +++ b/test/fetch/headers.js @@ -666,6 +666,18 @@ tap.test('invalid headers', (t) => { t.end() }) +tap.test('headers that might cause a ReDoS', (t) => { + t.doesNotThrow(() => { + // This test will time out if the ReDoS attack is successful. + const headers = new Headers() + const attack = 'a' + '\t'.repeat(500_000) + '\ta' + headers.append('fhqwhgads', attack) + }) + + t.end() +}) + + tap.test('Headers.prototype.getSetCookie', (t) => { t.test('Mutating the returned list does not affect the set-cookie list', (t) => { const h = new Headers([ @@ -682,4 +694,4 @@ tap.test('Headers.prototype.getSetCookie', (t) => { }) t.end() -}) +}) \ No newline at end of file From 6c32c0fd5b874328e5e1f635e2cc431aa21cddab Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 13 Feb 2023 12:24:55 +0100 Subject: [PATCH 054/123] lint fixes Signed-off-by: Matteo Collina --- test/fetch/headers.js | 3 +-- test/headers-crlf.js | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/test/fetch/headers.js b/test/fetch/headers.js index 1f9196c4697..19beb98bdba 100644 --- a/test/fetch/headers.js +++ b/test/fetch/headers.js @@ -677,7 +677,6 @@ tap.test('headers that might cause a ReDoS', (t) => { t.end() }) - tap.test('Headers.prototype.getSetCookie', (t) => { t.test('Mutating the returned list does not affect the set-cookie list', (t) => { const h = new Headers([ @@ -694,4 +693,4 @@ tap.test('Headers.prototype.getSetCookie', (t) => { }) t.end() -}) \ No newline at end of file +}) diff --git a/test/headers-crlf.js b/test/headers-crlf.js index fa90eaadd11..b24fd391491 100644 --- a/test/headers-crlf.js +++ b/test/headers-crlf.js @@ -3,7 +3,6 @@ const { test } = require('tap') const { Client } = require('..') const { createServer } = require('http') -const EE = require('events') test('CRLF Injection in Nodejs ‘undici’ via host', (t) => { t.plan(1) @@ -17,7 +16,7 @@ test('CRLF Injection in Nodejs ‘undici’ via host', (t) => { const client = new Client(`http://localhost:${server.address().port}`) t.teardown(client.close.bind(client)) - const unsanitizedContentTypeInput = '12 \r\n\r\naaa:aaa' + const unsanitizedContentTypeInput = '12 \r\n\r\naaa:aaa' try { const { body } = await client.request({ @@ -25,7 +24,7 @@ test('CRLF Injection in Nodejs ‘undici’ via host', (t) => { method: 'POST', headers: { 'content-type': 'application/json', - 'host': unsanitizedContentTypeInput + host: unsanitizedContentTypeInput }, body: 'asd' }) From 984d53bad97c98529424a7f3bef6be1d0e76d039 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 13 Feb 2023 12:26:57 +0100 Subject: [PATCH 055/123] Bumped v5.19.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 af4e624f718..c664a9af9b3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "5.19.0", + "version": "5.19.1", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { From 2971280f3167f759d1474bd85961d6dac577d4a6 Mon Sep 17 00:00:00 2001 From: Khafra Date: Tue, 14 Feb 2023 21:58:16 -0500 Subject: [PATCH 056/123] perf: improve cookie parsing performance (#1931) --- lib/cookies/parse.js | 16 ++++++++-------- lib/fetch/dataURL.js | 1 + 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/cookies/parse.js b/lib/cookies/parse.js index 6a1e37be6b7..aae2750360b 100644 --- a/lib/cookies/parse.js +++ b/lib/cookies/parse.js @@ -2,7 +2,7 @@ const { maxNameValuePairSize, maxAttributeValueSize } = require('./constants') const { isCTLExcludingHtab } = require('./util') -const { collectASequenceOfCodePoints } = require('../fetch/dataURL') +const { collectASequenceOfCodePointsFast } = require('../fetch/dataURL') const assert = require('assert') /** @@ -32,7 +32,7 @@ function parseSetCookie (header) { // (including the %x3B (";") in question). const position = { position: 0 } - nameValuePair = collectASequenceOfCodePoints((char) => char !== ';', header, position) + nameValuePair = collectASequenceOfCodePointsFast(';', header, position) unparsedAttributes = header.slice(position.position) } else { // Otherwise: @@ -54,8 +54,8 @@ function parseSetCookie (header) { // empty) value string consists of the characters after the first // %x3D ("=") character. const position = { position: 0 } - name = collectASequenceOfCodePoints( - (char) => char !== '=', + name = collectASequenceOfCodePointsFast( + '=', nameValuePair, position ) @@ -106,8 +106,8 @@ function parseUnparsedAttributes (unparsedAttributes, cookieAttributeList = {}) if (unparsedAttributes.includes(';')) { // 1. Consume the characters of the unparsed-attributes up to, but // not including, the first %x3B (";") character. - cookieAv = collectASequenceOfCodePoints( - (char) => char !== ';', + cookieAv = collectASequenceOfCodePointsFast( + ';', unparsedAttributes, { position: 0 } ) @@ -134,8 +134,8 @@ function parseUnparsedAttributes (unparsedAttributes, cookieAttributeList = {}) // character. const position = { position: 0 } - attributeName = collectASequenceOfCodePoints( - (char) => char !== '=', + attributeName = collectASequenceOfCodePointsFast( + '=', cookieAv, position ) diff --git a/lib/fetch/dataURL.js b/lib/fetch/dataURL.js index c950167d295..0d4a46956db 100644 --- a/lib/fetch/dataURL.js +++ b/lib/fetch/dataURL.js @@ -556,6 +556,7 @@ module.exports = { dataURLProcessor, URLSerializer, collectASequenceOfCodePoints, + collectASequenceOfCodePointsFast, stringPercentDecode, parseMIMEType, collectAnHTTPQuotedString, From f73ec635a1bd1e6601c5eeec72d3b73eea30aa28 Mon Sep 17 00:00:00 2001 From: Khafra Date: Wed, 15 Feb 2023 03:07:38 -0500 Subject: [PATCH 057/123] fix: disable websocket wpts in ci :( (#1932) --- test/wpt/runner/runner/runner.mjs | 15 ++++++--------- test/wpt/start-websockets.mjs | 5 +++++ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/test/wpt/runner/runner/runner.mjs b/test/wpt/runner/runner/runner.mjs index 3e558de3e0b..744f89975ba 100644 --- a/test/wpt/runner/runner/runner.mjs +++ b/test/wpt/runner/runner/runner.mjs @@ -115,12 +115,8 @@ export class WPTRunner extends EventEmitter { }) activeWorkers.add(worker) - const timeout = setTimeout( - () => { - console.warn('Test timed out:', test) - }, - meta.timeout === 'long' ? 60_000 : 10_000 - ) + // These values come directly from the web-platform-tests + const timeout = meta.timeout === 'long' ? 60_000 : 10_000 worker.on('message', (message) => { if (message.type === 'result') { @@ -132,15 +128,16 @@ export class WPTRunner extends EventEmitter { worker.once('exit', () => { activeWorkers.delete(worker) - clearTimeout(timeout) if (++finishedFiles === this.#files.length) { this.handleRunnerCompletion() } }) - if (activeWorkers.size === cpus().length) { - await once(worker, 'exit') + if (activeWorkers.size >= cpus().length) { + await once(worker, 'exit', { + signal: AbortSignal.timeout(timeout) + }) } } } diff --git a/test/wpt/start-websockets.mjs b/test/wpt/start-websockets.mjs index 603c2f31681..9fe485bb1c1 100644 --- a/test/wpt/start-websockets.mjs +++ b/test/wpt/start-websockets.mjs @@ -4,6 +4,11 @@ import { fileURLToPath } from 'url' import { fork } from 'child_process' import { on } from 'events' +if (process.env.CI) { + // TODO(@KhafraDev): figure out *why* these tests are flaky in the CI. + process.exit(0) +} + const serverPath = fileURLToPath(join(import.meta.url, '../server/websocket.mjs')) const child = fork(serverPath, [], { From c2387e8071e4ed7e32b512ee6f78375ac5c227b8 Mon Sep 17 00:00:00 2001 From: pan93412 Date: Wed, 15 Feb 2023 16:48:29 +0800 Subject: [PATCH 058/123] =?UTF-8?q?fix:=20Allow=20=E2=80=9Cundefined?= =?UTF-8?q?=E2=80=9C=20as=20value=20in=20headers=20(#1929)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: Allow “undefined“ as value in headers https://github.com/nodejs/undici/pull/1896#discussion_r1104301293 Signed-off-by: pan93412 * test: Add test for our IncomingHttpHeaders Co-Authored-By: Simen Bekkhus Signed-off-by: pan93412 * chore: Upgrade TypeScript to 4.9.5 Signed-off-by: pan93412 --------- Signed-off-by: pan93412 Co-authored-by: Simen Bekkhus --- docs/api/Dispatcher.md | 10 +++++----- package.json | 2 +- test/types/dispatcher.test-d.ts | 8 ++++++++ test/types/header.test-d.ts | 16 ++++++++++++++++ types/header.d.ts | 2 +- 5 files changed, 31 insertions(+), 7 deletions(-) create mode 100644 test/types/header.test-d.ts diff --git a/docs/api/Dispatcher.md b/docs/api/Dispatcher.md index 25c980d612b..fa8ae2dd6fc 100644 --- a/docs/api/Dispatcher.md +++ b/docs/api/Dispatcher.md @@ -74,7 +74,7 @@ Returns: `void | Promise` - Only returns a `Promise` if no `callbac #### Parameter: `ConnectData` * **statusCode** `number` -* **headers** `Record` +* **headers** `Record` * **socket** `stream.Duplex` * **opaque** `unknown` @@ -383,7 +383,7 @@ Extends: [`RequestOptions`](#parameter-requestoptions) #### Parameter: PipelineHandlerData * **statusCode** `number` -* **headers** `Record` +* **headers** `Record` * **opaque** `unknown` * **body** `stream.Readable` * **context** `object` @@ -644,7 +644,7 @@ Returns: `void | Promise` - Only returns a `Promise` if no `callback #### Parameter: `StreamFactoryData` * **statusCode** `number` -* **headers** `Record` +* **headers** `Record` * **opaque** `unknown` * **onInfo** `({statusCode: number, headers: Record}) => void | null` (optional) - Default: `null` - Callback collecting all the info headers (HTTP 100-199) received. @@ -853,9 +853,9 @@ Emitted when dispatcher is no longer busy. ## Parameter: `UndiciHeaders` -* `Record | string[] | null` +* `Record | string[] | null` -Header arguments such as `options.headers` in [`Client.dispatch`](Client.md#clientdispatchoptions-handlers) can be specified in two forms; either as an object specified by the `Record` (`IncomingHttpHeaders`) type, or an array of strings. An array representation of a header list must have an even length or an `InvalidArgumentError` will be thrown. +Header arguments such as `options.headers` in [`Client.dispatch`](Client.md#clientdispatchoptions-handlers) can be specified in two forms; either as an object specified by the `Record` (`IncomingHttpHeaders`) type, or an array of strings. An array representation of a header list must have an even length or an `InvalidArgumentError` will be thrown. Keys are lowercase and values are not modified. diff --git a/package.json b/package.json index c664a9af9b3..e3c2d12ba9f 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,7 @@ "table": "^6.8.0", "tap": "^16.1.0", "tsd": "^0.25.0", - "typescript": "^4.8.4", + "typescript": "^4.9.5", "wait-on": "^6.0.0", "ws": "^8.11.0" }, diff --git a/test/types/dispatcher.test-d.ts b/test/types/dispatcher.test-d.ts index 94c66ee3abc..2f31b91c987 100644 --- a/test/types/dispatcher.test-d.ts +++ b/test/types/dispatcher.test-d.ts @@ -1,3 +1,4 @@ +import { IncomingHttpHeaders } from 'http' import { Duplex, Readable, Writable } from 'stream' import { expectAssignable, expectType } from 'tsd' import { Dispatcher } from '../..' @@ -9,11 +10,18 @@ expectAssignable(new Dispatcher()) { const dispatcher = new Dispatcher() + const nodeCoreHeaders = { + authorization: undefined, + ['content-type']: 'application/json' + } satisfies IncomingHttpHeaders; + // dispatch expectAssignable(dispatcher.dispatch({ path: '', method: 'GET' }, {})) expectAssignable(dispatcher.dispatch({ origin: '', path: '', method: 'GET' }, {})) + expectAssignable(dispatcher.dispatch({ origin: '', path: '', method: 'GET', headers: { authorization: undefined } }, {})) expectAssignable(dispatcher.dispatch({ origin: '', path: '', method: 'GET', headers: [] }, {})) expectAssignable(dispatcher.dispatch({ origin: '', path: '', method: 'GET', headers: {} }, {})) + expectAssignable(dispatcher.dispatch({ origin: '', path: '', method: 'GET', headers: nodeCoreHeaders }, {})) expectAssignable(dispatcher.dispatch({ origin: '', path: '', method: 'GET', headers: null, reset: true }, {})) expectAssignable(dispatcher.dispatch({ origin: new URL('http://localhost'), path: '', method: 'GET' }, {})) diff --git a/test/types/header.test-d.ts b/test/types/header.test-d.ts new file mode 100644 index 00000000000..38ac9f6afdc --- /dev/null +++ b/test/types/header.test-d.ts @@ -0,0 +1,16 @@ +import { IncomingHttpHeaders as CoreIncomingHttpHeaders } from "http"; +import { expectAssignable, expectNotAssignable } from "tsd"; +import { IncomingHttpHeaders } from "../../types/header"; + +const headers = { + authorization: undefined, + ["content-type"]: "application/json", +} satisfies CoreIncomingHttpHeaders; + +expectAssignable(headers); + +// It is why we do not need to add ` | null` to `IncomingHttpHeaders`: +expectNotAssignable({ + authorization: null, + ["content-type"]: "application/json", +}); diff --git a/types/header.d.ts b/types/header.d.ts index 3fd9483e27b..bfdb3296d4d 100644 --- a/types/header.d.ts +++ b/types/header.d.ts @@ -1,4 +1,4 @@ /** * The header type declaration of `undici`. */ -export type IncomingHttpHeaders = Record; +export type IncomingHttpHeaders = Record; From eae68072260fd5080985c1b94d1f54e38a3fb789 Mon Sep 17 00:00:00 2001 From: Paolo Insogna Date: Wed, 15 Feb 2023 09:48:48 +0100 Subject: [PATCH 059/123] feat: Support autoSelectFamily when connecting. (#1914) * feat: Support autoSelectFamily when connecting. * test: Fixed test. --- README.md | 12 +++ docs/api/Client.md | 2 + index.js | 10 +- lib/client.js | 12 ++- lib/core/request.js | 6 +- lib/core/util.js | 7 +- lib/fetch/index.js | 6 +- lib/pool.js | 3 + package.json | 2 +- test/autoselectfamily.js | 187 ++++++++++++++++++++++++++++++++++ test/balanced-pool.js | 4 +- test/client-errors.js | 8 ++ test/client-pipeline.js | 3 +- test/client-request.js | 3 +- test/esm-wrapper.js | 4 +- test/fetch/abort.js | 4 +- test/fetch/request.js | 3 +- test/fetch/resource-timing.js | 5 +- test/mock-agent.js | 3 +- test/mock-pool.js | 3 +- test/mock-utils.js | 3 +- test/proxy-agent.js | 3 +- test/redirect-request.js | 3 +- test/request-timeout.js | 3 +- test/tls-client-cert.js | 2 +- test/tls-session-reuse.js | 3 +- test/util.js | 2 +- types/client.d.ts | 6 +- 28 files changed, 263 insertions(+), 49 deletions(-) create mode 100644 test/autoselectfamily.js diff --git a/README.md b/README.md index bd991008a90..e49064fa297 100644 --- a/README.md +++ b/README.md @@ -405,6 +405,18 @@ implementations in Deno and Cloudflare Workers. Refs: https://fetch.spec.whatwg.org/#atomic-http-redirect-handling +## Workarounds + +### Network address family autoselection. + +If you experience problem when connecting to a remote server that is resolved by your DNS servers to a IPv6 (AAAA record) +first, there are chances that your local router or ISP might have problem connecting to IPv6 networks. In that case +undici will throw an error with code `UND_ERR_CONNECT_TIMEOUT`. + +If the target server resolves to both a IPv6 and IPv4 (A records) address and you are using a compatible Node version +(18.3.0 and above), you can fix the problem by providing the `autoSelectFamily` option (support by both `undici.request` +and `undici.Agent`) which will enable the family autoselection algorithm when establishing the connection. + ## Collaborators * [__Daniele Belardi__](https://github.com/dnlup), diff --git a/docs/api/Client.md b/docs/api/Client.md index 1aca7d09d31..970ed1eee65 100644 --- a/docs/api/Client.md +++ b/docs/api/Client.md @@ -28,6 +28,8 @@ Returns: `Client` * **connect** `ConnectOptions | Function | null` (optional) - Default: `null`. * **strictContentLength** `Boolean` (optional) - Default: `true` - Whether to treat request content length mismatches as errors. If true, an error is thrown when the request content-length header doesn't match the length of the request body. * **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. #### Parameter: `ConnectOptions` diff --git a/index.js b/index.js index a970b937423..02ac246fa45 100644 --- a/index.js +++ b/index.js @@ -20,10 +20,6 @@ const DecoratorHandler = require('./lib/handler/DecoratorHandler') const RedirectHandler = require('./lib/handler/RedirectHandler') const createRedirectInterceptor = require('./lib/interceptor/redirectInterceptor') -const nodeVersion = process.versions.node.split('.') -const nodeMajor = Number(nodeVersion[0]) -const nodeMinor = Number(nodeVersion[1]) - let hasCrypto try { require('crypto') @@ -100,7 +96,7 @@ function makeDispatcher (fn) { module.exports.setGlobalDispatcher = setGlobalDispatcher module.exports.getGlobalDispatcher = getGlobalDispatcher -if (nodeMajor > 16 || (nodeMajor === 16 && nodeMinor >= 8)) { +if (util.nodeMajor > 16 || (util.nodeMajor === 16 && util.nodeMinor >= 8)) { let fetchImpl = null module.exports.fetch = async function fetch (resource) { if (!fetchImpl) { @@ -127,7 +123,7 @@ if (nodeMajor > 16 || (nodeMajor === 16 && nodeMinor >= 8)) { module.exports.getGlobalOrigin = getGlobalOrigin } -if (nodeMajor >= 16) { +if (util.nodeMajor >= 16) { const { deleteCookie, getCookies, getSetCookies, setCookie } = require('./lib/cookies') module.exports.deleteCookie = deleteCookie @@ -141,7 +137,7 @@ if (nodeMajor >= 16) { module.exports.serializeAMimeType = serializeAMimeType } -if (nodeMajor >= 18 && hasCrypto) { +if (util.nodeMajor >= 18 && hasCrypto) { const { WebSocket } = require('./lib/websocket/websocket') module.exports.WebSocket = WebSocket diff --git a/lib/client.js b/lib/client.js index 983e7034795..49d51f3400c 100644 --- a/lib/client.js +++ b/lib/client.js @@ -109,7 +109,9 @@ class Client extends DispatcherBase { connect, maxRequestsPerClient, localAddress, - maxResponseSize + maxResponseSize, + autoSelectFamily, + autoSelectFamilyAttemptTimeout } = {}) { super() @@ -185,12 +187,20 @@ class Client extends DispatcherBase { throw new InvalidArgumentError('maxResponseSize must be a positive number') } + if ( + autoSelectFamilyAttemptTimeout != null && + (!Number.isInteger(autoSelectFamilyAttemptTimeout) || autoSelectFamilyAttemptTimeout < -1) + ) { + throw new InvalidArgumentError('autoSelectFamilyAttemptTimeout must be a positive number') + } + if (typeof connect !== 'function') { connect = buildConnector({ ...tls, maxCachedSessions, socketPath, timeout: connectTimeout, + ...(util.nodeHasAutoSelectFamily && autoSelectFamily ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined), ...connect }) } diff --git a/lib/core/request.js b/lib/core/request.js index adb5bfcb3a8..c82159e5ba0 100644 --- a/lib/core/request.js +++ b/lib/core/request.js @@ -34,10 +34,6 @@ const channels = {} let extractBody -const nodeVersion = process.versions.node.split('.') -const nodeMajor = Number(nodeVersion[0]) -const nodeMinor = Number(nodeVersion[1]) - try { const diagnosticsChannel = require('diagnostics_channel') channels.create = diagnosticsChannel.channel('undici:request:create') @@ -172,7 +168,7 @@ class Request { } if (util.isFormDataLike(this.body)) { - if (nodeMajor < 16 || (nodeMajor === 16 && nodeMinor < 8)) { + if (util.nodeMajor < 16 || (util.nodeMajor === 16 && util.nodeMinor < 8)) { throw new InvalidArgumentError('Form-Data bodies are only supported in node v16.8 and newer.') } diff --git a/lib/core/util.js b/lib/core/util.js index 3b9b56cb64d..ef9b4570dcd 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -10,6 +10,8 @@ const { Blob } = require('buffer') const nodeUtil = require('util') const { stringify } = require('querystring') +const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(v => Number(v)) + function nop () {} function isStream (obj) { @@ -420,5 +422,8 @@ module.exports = { validateHandler, getSocketInfo, isFormDataLike, - buildURL + buildURL, + nodeMajor, + nodeMinor, + nodeHasAutoSelectFamily: nodeMajor > 18 || (nodeMajor === 18 && nodeMinor >= 13) } diff --git a/lib/fetch/index.js b/lib/fetch/index.js index ae60a5af57c..6e356de8128 100644 --- a/lib/fetch/index.js +++ b/lib/fetch/index.js @@ -53,7 +53,7 @@ const { const { kHeadersList } = require('../core/symbols') const EE = require('events') const { Readable, pipeline } = require('stream') -const { isErrored, isReadable } = require('../core/util') +const { isErrored, isReadable, nodeMajor, nodeMinor } = require('../core/util') const { dataURLProcessor, serializeAMimeType } = require('./dataURL') const { TransformStream } = require('stream/web') const { getGlobalDispatcher } = require('../global') @@ -64,10 +64,6 @@ const { STATUS_CODES } = require('http') let resolveObjectURL let ReadableStream = globalThis.ReadableStream -const nodeVersion = process.versions.node.split('.') -const nodeMajor = Number(nodeVersion[0]) -const nodeMinor = Number(nodeVersion[1]) - class Fetch extends EE { constructor (dispatcher) { super() diff --git a/lib/pool.js b/lib/pool.js index c1c20dd6b87..93b3158f21a 100644 --- a/lib/pool.js +++ b/lib/pool.js @@ -32,6 +32,8 @@ class Pool extends PoolBase { tls, maxCachedSessions, socketPath, + autoSelectFamily, + autoSelectFamilyAttemptTimeout, ...options } = {}) { super() @@ -54,6 +56,7 @@ class Pool extends PoolBase { maxCachedSessions, socketPath, timeout: connectTimeout == null ? 10e3 : connectTimeout, + ...(util.nodeHasAutoSelectFamily && autoSelectFamily ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined), ...connect }) } diff --git a/package.json b/package.json index e3c2d12ba9f..1109bcb6b84 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "concurrently": "^7.1.0", "cronometro": "^1.0.5", "delay": "^5.0.0", + "dns-packet": "^5.4.0", "docsify-cli": "^4.4.3", "form-data": "^4.0.0", "formdata-node": "^4.3.1", @@ -91,7 +92,6 @@ "pre-commit": "^1.2.2", "proxy": "^1.0.2", "proxyquire": "^2.1.3", - "semver": "^7.3.5", "sinon": "^15.0.0", "snazzy": "^9.0.0", "standard": "^17.0.0", diff --git a/test/autoselectfamily.js b/test/autoselectfamily.js new file mode 100644 index 00000000000..03f15545cf3 --- /dev/null +++ b/test/autoselectfamily.js @@ -0,0 +1,187 @@ +'use strict' + +const { test } = 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') + +function _lookup (resolver, hostname, options, cb) { + resolver.resolve(hostname, 'ANY', (err, replies) => { + if (err) { + return cb(err) + } + + const hosts = replies + .map((r) => ({ address: r.address, family: r.type === 'AAAA' ? 6 : 4 })) + .sort((a, b) => b.family - a.family) + + if (options.all === true) { + return cb(null, hosts) + } + + return cb(null, hosts[0].address, hosts[0].family) + }) +} + +function createDnsServer (ipv6Addr, ipv4Addr, cb) { + // Create a DNS server which replies with a AAAA and a A record for the same host + const socket = dgram.createSocket('udp4') + + socket.on('message', (msg, { address, port }) => { + const parsed = dnsPacket.decode(msg) + + const response = dnsPacket.encode({ + type: 'answer', + id: parsed.id, + questions: parsed.questions, + answers: [ + { type: 'AAAA', class: 'IN', name: 'example.org', data: '::1', ttl: 123 }, + { type: 'A', class: 'IN', name: 'example.org', data: '127.0.0.1', ttl: 123 } + ] + }) + + socket.send(response, port, address) + }) + + socket.bind(0, () => { + const resolver = new Resolver() + resolver.setServers([`127.0.0.1:${socket.address().port}`]) + + cb(null, { dnsServer: socket, lookup: _lookup.bind(null, resolver) }) + }) +} + +if (nodeHasAutoSelectFamily) { + 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') + }) + + t.teardown(() => { + server.close() + dnsServer.close() + }) + + 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) + + let response = Buffer.alloc(0) + + body.on('data', chunk => { + response = Buffer.concat([response, chunk]) + }) + + 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) + + createDnsServer('::1', '127.0.0.1', function (_, { dnsServer, lookup }) { + const server = createServer((req, res) => { + res.end('hello') + }) + + 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 }) + + t.teardown(client.destroy.bind(client)) + + client.request({ + path: '/', + method: 'GET' + }, (err, { statusCode, body }) => { + t.error(err) + + let response = Buffer.alloc(0) + + body.on('data', chunk => { + response = Buffer.concat([response, chunk]) + }) + + 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) + + createDnsServer('::1', '127.0.0.1', function (_, { dnsServer, lookup }) { + const server = createServer((req, res) => { + res.end('hello') + }) + + t.teardown(() => { + server.close() + dnsServer.close() + }) + + server.listen(0, '127.0.0.1', () => { + const agent = new Agent({ connect: { lookup } }) + + request(`http://example.org:${server.address().port}`, { + method: 'GET', + dispatcher: agent + }, (err, { statusCode, body }) => { + t.strictSame(err.code, 'ECONNREFUSED') + }) + }) + }) +}) + +test('with autoSelectFamily disabled the request fails when using a client', (t) => { + t.plan(1) + + createDnsServer('::1', '127.0.0.1', function (_, { dnsServer, lookup }) { + const server = createServer((req, res) => { + res.end('hello') + }) + + 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 } }) + t.teardown(client.destroy.bind(client)) + + client.request({ + path: '/', + method: 'GET' + }, (err, { statusCode, body }) => { + t.strictSame(err.code, 'ECONNREFUSED') + }) + }) + }) +}) diff --git a/test/balanced-pool.js b/test/balanced-pool.js index be544887ef9..669e04a571c 100644 --- a/test/balanced-pool.js +++ b/test/balanced-pool.js @@ -2,9 +2,9 @@ const { test } = require('tap') const { BalancedPool, Pool, Client, errors } = require('..') +const { nodeMajor } = require('../lib/core/util') const { createServer } = require('http') const { promisify } = require('util') -const semver = require('semver') test('throws when factory is not a function', (t) => { t.plan(2) @@ -437,7 +437,7 @@ const cases = [ expectedRatios: [0.34, 0.34, 0.32], // Skip because the behavior of Node.js has changed - skip: semver.satisfies(process.version, '>= 19.0.0') + skip: nodeMajor >= 19 }, // 8 diff --git a/test/client-errors.js b/test/client-errors.js index f96d7ecbb9f..fc6e534c824 100644 --- a/test/client-errors.js +++ b/test/client-errors.js @@ -533,6 +533,14 @@ test('invalid options throws', (t) => { t.equal(err.message, 'maxRequestsPerClient must be a positive number') } + try { + new Client(new URL('http://localhost:200'), { autoSelectFamilyAttemptTimeout: 'foo' }) // eslint-disable-line + t.fail() + } catch (err) { + t.type(err, errors.InvalidArgumentError) + t.equal(err.message, 'autoSelectFamilyAttemptTimeout must be a positive number') + } + t.end() }) diff --git a/test/client-pipeline.js b/test/client-pipeline.js index 5b03617edad..9b677a02774 100644 --- a/test/client-pipeline.js +++ b/test/client-pipeline.js @@ -11,6 +11,7 @@ const { Writable, PassThrough } = require('stream') +const { nodeMajor } = require('../lib/core/util') test('pipeline get', (t) => { t.plan(17) @@ -535,7 +536,7 @@ test('pipeline abort piped res', (t) => { }) .on('error', (err) => { // Node < 13 doesn't always detect premature close. - if (process.versions.node.split('.')[0] < 13) { + if (nodeMajor < 13) { t.ok(err) } else { t.equal(err.code, 'UND_ERR_ABORTED') diff --git a/test/client-request.js b/test/client-request.js index 74976bb5925..ff564604d42 100644 --- a/test/client-request.js +++ b/test/client-request.js @@ -9,10 +9,9 @@ const { Readable } = require('stream') const net = require('net') const { promisify } = require('util') const { NotSupportedError } = require('../lib/core/errors') +const { nodeMajor } = require('../lib/core/util') const { parseFormDataString } = require('./utils/formdata') -const nodeMajor = Number(process.versions.node.split('.')[0]) - test('request dump', (t) => { t.plan(3) diff --git a/test/esm-wrapper.js b/test/esm-wrapper.js index 997d7acbfb9..a593fbdb531 100644 --- a/test/esm-wrapper.js +++ b/test/esm-wrapper.js @@ -1,7 +1,7 @@ 'use strict' -const semver = require('semver') +const { nodeMajor, nodeMinor } = require('../lib/core/util') -if (!semver.satisfies(process.version, '>= v14.13.0 || ^12.20.0')) { +if (!((nodeMajor > 14 || (nodeMajor === 14 && nodeMajor > 13)) || (nodeMajor === 12 && nodeMinor > 20))) { require('tap') // shows skipped } else { ;(async () => { diff --git a/test/fetch/abort.js b/test/fetch/abort.js index c8472689fb7..e1ca1ebf9ea 100644 --- a/test/fetch/abort.js +++ b/test/fetch/abort.js @@ -5,7 +5,7 @@ const { fetch } = require('../..') const { createServer } = require('http') const { once } = require('events') const { DOMException } = require('../../lib/fetch/constants') -const semver = require('semver') +const { nodeMajor } = require('../../lib/core/util') const { AbortController: NPMAbortController } = require('abort-controller') @@ -37,7 +37,7 @@ test('Allow the usage of custom implementation of AbortController', async (t) => } }) -test('allows aborting with custom errors', { skip: semver.satisfies(process.version, '16.x') }, async (t) => { +test('allows aborting with custom errors', { skip: nodeMajor === 16 }, async (t) => { const server = createServer().listen(0) t.teardown(server.close.bind(server)) diff --git a/test/fetch/request.js b/test/fetch/request.js index de91df91479..29d88b2ec66 100644 --- a/test/fetch/request.js +++ b/test/fetch/request.js @@ -13,7 +13,8 @@ const { Blob: ThirdPartyBlob, FormData: ThirdPartyFormData } = require('formdata-node') -const hasSignalReason = !!~process.version.localeCompare('v16.14.0', undefined, { numeric: true }) + +const hasSignalReason = 'reason' in AbortSignal.prototype test('arg validation', async (t) => { // constructor diff --git a/test/fetch/resource-timing.js b/test/fetch/resource-timing.js index 2481887bab6..c8732198c30 100644 --- a/test/fetch/resource-timing.js +++ b/test/fetch/resource-timing.js @@ -2,6 +2,7 @@ const { test } = require('tap') const { createServer } = require('http') +const { nodeMajor, nodeMinor } = require('../../lib/core/util') const { fetch } = require('../..') const { @@ -9,9 +10,7 @@ const { performance } = require('perf_hooks') -const semver = require('semver') - -const skip = semver.lt(process.version, '18.2.0') +const skip = nodeMajor < 18 || (nodeMajor === 18 && nodeMinor < 2) test('should create a PerformanceResourceTiming after each fetch request', { skip }, (t) => { t.plan(4) diff --git a/test/mock-agent.js b/test/mock-agent.js index f83f9406be9..4145432c2c6 100644 --- a/test/mock-agent.js +++ b/test/mock-agent.js @@ -7,14 +7,13 @@ const { request, setGlobalDispatcher, MockAgent, Agent } = require('..') const { getResponse } = require('../lib/mock/mock-utils') const { kClients, kConnected } = require('../lib/core/symbols') const { InvalidArgumentError, ClientDestroyedError } = require('../lib/core/errors') +const { nodeMajor } = require('../lib/core/util') const MockClient = require('../lib/mock/mock-client') const MockPool = require('../lib/mock/mock-pool') const { kAgent } = require('../lib/mock/mock-symbols') const Dispatcher = require('../lib/dispatcher') const { MockNotMatchedError } = require('../lib/mock/mock-errors') -const nodeMajor = Number(process.versions.node.split('.')[0]) - test('MockAgent - constructor', t => { t.plan(5) diff --git a/test/mock-pool.js b/test/mock-pool.js index 3e6907192e4..0ac1aac49ce 100644 --- a/test/mock-pool.js +++ b/test/mock-pool.js @@ -5,14 +5,13 @@ const { createServer } = require('http') const { promisify } = require('util') const { MockAgent, MockPool, getGlobalDispatcher, setGlobalDispatcher, request } = require('..') const { kUrl } = require('../lib/core/symbols') +const { nodeMajor } = require('../lib/core/util') const { kDispatches } = require('../lib/mock/mock-symbols') const { InvalidArgumentError } = require('../lib/core/errors') const { MockInterceptor } = require('../lib/mock/mock-interceptor') const { getResponse } = require('../lib/mock/mock-utils') const Dispatcher = require('../lib/dispatcher') -const nodeMajor = Number(process.versions.node.split('.', 1)[0]) - test('MockPool - constructor', t => { t.plan(3) diff --git a/test/mock-utils.js b/test/mock-utils.js index 5dd9e3cddf0..7799803ad2f 100644 --- a/test/mock-utils.js +++ b/test/mock-utils.js @@ -1,6 +1,7 @@ 'use strict' const { test } = require('tap') +const { nodeMajor } = require('../lib/core/util') const { MockNotMatchedError } = require('../lib/mock/mock-errors') const { deleteMockDispatch, @@ -144,7 +145,7 @@ test('getHeaderByName', (t) => { t.equal(getHeaderByName(headersArray, 'key'), 'value') t.equal(getHeaderByName(headersArray, 'anotherKey'), undefined) - if (Number(process.versions.node.split('.')[0]) >= 16) { + if (nodeMajor >= 16) { const { Headers } = require('../index') const headers = new Headers([ diff --git a/test/proxy-agent.js b/test/proxy-agent.js index 8c6b5999278..d760f66347e 100644 --- a/test/proxy-agent.js +++ b/test/proxy-agent.js @@ -3,6 +3,7 @@ const { test } = require('tap') const { request, fetch, setGlobalDispatcher, getGlobalDispatcher } = require('..') const { InvalidArgumentError } = require('../lib/core/errors') +const { nodeMajor } = require('../lib/core/util') const { readFileSync } = require('fs') const { join } = require('path') const ProxyAgent = require('../lib/proxy-agent') @@ -10,8 +11,6 @@ const { createServer } = require('http') const https = require('https') const proxy = require('proxy') -const nodeMajor = Number(process.versions.node.split('.', 1)[0]) - test('should throw error when no uri is provided', (t) => { t.plan(2) t.throws(() => new ProxyAgent(), InvalidArgumentError) diff --git a/test/redirect-request.js b/test/redirect-request.js index 2915f1e224a..f996bfa8231 100644 --- a/test/redirect-request.js +++ b/test/redirect-request.js @@ -2,6 +2,7 @@ const t = require('tap') const undici = require('..') +const { nodeMajor } = require('../lib/core/util') const { startRedirectingServer, startRedirectingWithBodyServer, @@ -13,8 +14,6 @@ const { } = require('./utils/redirecting-servers') const { createReadable, createReadableStream } = require('./utils/stream') -const nodeMajor = Number(process.versions.node.split('.')[0]) - for (const factory of [ (server, opts) => new undici.Agent(opts), (server, opts) => new undici.Pool(`http://${server}`, opts), diff --git a/test/request-timeout.js b/test/request-timeout.js index 972ebd0b373..25e92294e84 100644 --- a/test/request-timeout.js +++ b/test/request-timeout.js @@ -4,6 +4,7 @@ const { test } = require('tap') const { createReadStream, writeFileSync, unlinkSync } = require('fs') const { Client, errors } = require('..') const { kConnect } = require('../lib/core/symbols') +const { nodeMajor } = require('../lib/core/util') const timers = require('../lib/timers') const { createServer } = require('http') const EventEmitter = require('events') @@ -16,8 +17,6 @@ const { PassThrough } = require('stream') -const nodeMajor = Number(process.versions.node.split('.')[0]) - test('request timeout', (t) => { t.plan(1) diff --git a/test/tls-client-cert.js b/test/tls-client-cert.js index ecbbbec1850..8ae301d8fca 100644 --- a/test/tls-client-cert.js +++ b/test/tls-client-cert.js @@ -6,7 +6,7 @@ const https = require('https') const { test } = require('tap') const { Client } = require('..') const { kSocket } = require('../lib/core/symbols') -const nodeMajor = Number(process.versions.node.split('.')[0]) +const { nodeMajor } = require('../lib/core/util') const serverOptions = { ca: [ diff --git a/test/tls-session-reuse.js b/test/tls-session-reuse.js index 161d48a360d..147e92fd76b 100644 --- a/test/tls-session-reuse.js +++ b/test/tls-session-reuse.js @@ -7,8 +7,7 @@ const crypto = require('crypto') const { test } = require('tap') const { Client, Pool } = require('..') const { kSocket } = require('../lib/core/symbols') - -const nodeMajor = Number(process.versions.node.split('.')[0]) +const { nodeMajor } = require('../lib/core/util') const options = { key: readFileSync(join(__dirname, 'fixtures', 'key.pem'), 'utf8'), diff --git a/test/util.js b/test/util.js index cbc94d63ba0..48a21a1141f 100644 --- a/test/util.js +++ b/test/util.js @@ -96,7 +96,7 @@ test('parseRawHeaders', (t) => { t.same(util.parseRawHeaders(['key', 'value', Buffer.from('key'), Buffer.from('value')]), ['key', 'value', 'key', 'value']) }) -test('buildURL', { skip: process.version.startsWith('v12.') }, (t) => { +test('buildURL', { skip: util.nodeMajor >= 12 }, (t) => { const tests = [ [{ id: BigInt(123456) }, 'id=123456'], [{ date: new Date() }, 'date='], diff --git a/types/client.d.ts b/types/client.d.ts index 3a954ac83a1..d147233408d 100644 --- a/types/client.d.ts +++ b/types/client.d.ts @@ -43,7 +43,11 @@ declare namespace Client { maxRequestsPerClient?: number; /** Max response body size in bytes, -1 is disabled */ maxResponseSize?: number | null; - + /** 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; + interceptors?: {Client: readonly DispatchInterceptor[] | undefined} } From 8b8bfa70b931a3a479e312e5a80db7a8653d1a3b Mon Sep 17 00:00:00 2001 From: Khafra Date: Thu, 16 Feb 2023 14:05:20 -0500 Subject: [PATCH 060/123] fix: copy cookies when cloning haders (#1936) --- lib/fetch/headers.js | 1 + test/fetch/headers.js | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/lib/fetch/headers.js b/lib/fetch/headers.js index ea3b9a14a43..5093ef8726f 100644 --- a/lib/fetch/headers.js +++ b/lib/fetch/headers.js @@ -75,6 +75,7 @@ class HeadersList { if (init instanceof HeadersList) { this[kHeadersMap] = new Map(init[kHeadersMap]) this[kHeadersSortedMap] = init[kHeadersSortedMap] + this.cookies = init.cookies } else { this[kHeadersMap] = new Map(init) this[kHeadersSortedMap] = null diff --git a/test/fetch/headers.js b/test/fetch/headers.js index 19beb98bdba..b91609dc383 100644 --- a/test/fetch/headers.js +++ b/test/fetch/headers.js @@ -3,6 +3,9 @@ const tap = require('tap') const { Headers, fill } = require('../../lib/fetch/headers') const { kGuard } = require('../../lib/fetch/symbols') +const { once } = require('events') +const { fetch } = require('../..') +const { createServer } = require('http') tap.test('Headers initialization', t => { t.plan(8) @@ -692,5 +695,22 @@ tap.test('Headers.prototype.getSetCookie', (t) => { t.end() }) + // https://github.com/nodejs/undici/issues/1935 + t.test('When Headers are cloned, so are the cookies', async (t) => { + const server = createServer((req, res) => { + res.setHeader('Set-Cookie', 'test=onetwo') + res.end('Hello World!') + }).listen(0) + + await once(server, 'listening') + t.teardown(server.close.bind(server)) + + const res = await fetch(`http://localhost:${server.address().port}`) + const entries = Object.fromEntries(res.headers.entries()) + + t.same(res.headers.getSetCookie(), ['test=onetwo']) + t.ok('set-cookie' in entries) + }) + t.end() }) From eaf4dc9afd29e54c58c2d42f65edc0350293e55a Mon Sep 17 00:00:00 2001 From: Khafra Date: Thu, 16 Feb 2023 16:50:47 -0500 Subject: [PATCH 061/123] test: more logs in wpt runner (#1933) --- test/wpt/runner/runner/runner.mjs | 34 +++++++++++++++++++++---------- test/wpt/runner/runner/util.mjs | 14 +++++++++++++ test/wpt/start-websockets.mjs | 2 +- 3 files changed, 38 insertions(+), 12 deletions(-) diff --git a/test/wpt/runner/runner/runner.mjs b/test/wpt/runner/runner/runner.mjs index 744f89975ba..6e8bebb2460 100644 --- a/test/wpt/runner/runner/runner.mjs +++ b/test/wpt/runner/runner/runner.mjs @@ -1,11 +1,10 @@ import { deepStrictEqual } from 'node:assert' import { EventEmitter, once } from 'node:events' import { readdirSync, readFileSync, statSync } from 'node:fs' -import { cpus } from 'node:os' import { basename, isAbsolute, join, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { Worker } from 'node:worker_threads' -import { parseMeta, handlePipes, normalizeName } from './util.mjs' +import { parseMeta, handlePipes, normalizeName, colors } from './util.mjs' const basePath = fileURLToPath(join(import.meta.url, '../../..')) const testPath = join(basePath, 'tests') @@ -80,14 +79,14 @@ export class WPTRunner extends EventEmitter { } } - return [...files] + return [...files].sort() } async run () { const workerPath = fileURLToPath(join(import.meta.url, '../worker.mjs')) /** @type {Set} */ const activeWorkers = new Set() - let finishedFiles = 0 + let finishedFiles = 1 for (const test of this.#files) { const code = test.includes('.sub.') @@ -97,10 +96,17 @@ export class WPTRunner extends EventEmitter { if (this.#status[basename(test)]?.skip) { this.#stats.skipped += 1 + + console.log('='.repeat(96)) + console.log(colors(`[${finishedFiles}/${this.#files.length}] SKIPPED - ${test}`, 'yellow')) + console.log('='.repeat(96)) + finishedFiles++ continue } + const start = performance.now() + const worker = new Worker(workerPath, { workerData: { // Code to load before the test harness and tests. @@ -126,20 +132,26 @@ export class WPTRunner extends EventEmitter { } }) - worker.once('exit', () => { + try { activeWorkers.delete(worker) - if (++finishedFiles === this.#files.length) { - this.handleRunnerCompletion() - } - }) - - if (activeWorkers.size >= cpus().length) { await once(worker, 'exit', { signal: AbortSignal.timeout(timeout) }) + + console.log('='.repeat(96)) + console.log(colors(`[${finishedFiles}/${this.#files.length}] PASSED - ${test}`, 'green')) + console.log(`Test took ${(performance.now() - start).toFixed(2)}ms`) + console.log('='.repeat(96)) + + finishedFiles++ + } catch (e) { + console.log(`${test} timed out after ${timeout}ms`) + throw e } } + + this.handleRunnerCompletion() } /** diff --git a/test/wpt/runner/runner/util.mjs b/test/wpt/runner/runner/util.mjs index c45fdc1df44..1d75029b6dc 100644 --- a/test/wpt/runner/runner/util.mjs +++ b/test/wpt/runner/runner/util.mjs @@ -1,5 +1,7 @@ +import assert from 'node:assert' import { exit } from 'node:process' import { inspect } from 'node:util' +import tty from 'node:tty' /** * Parse the `Meta:` tags sometimes included in tests. @@ -132,3 +134,15 @@ export function normalizeName (name) { } }) } + +export function colors (str, color) { + assert(Object.hasOwn(inspect.colors, color), `Missing color ${color}`) + + if (!tty.WriteStream.prototype.hasColors()) { + return str + } + + const [start, end] = inspect.colors[color] + + return `\u001b[${start}m${str}\u001b[${end}m` +} diff --git a/test/wpt/start-websockets.mjs b/test/wpt/start-websockets.mjs index 9fe485bb1c1..a8c1067ca5a 100644 --- a/test/wpt/start-websockets.mjs +++ b/test/wpt/start-websockets.mjs @@ -6,7 +6,7 @@ import { on } from 'events' if (process.env.CI) { // TODO(@KhafraDev): figure out *why* these tests are flaky in the CI. - process.exit(0) + // process.exit(0) } const serverPath = fileURLToPath(join(import.meta.url, '../server/websocket.mjs')) From 30dafe33adbc88a33ab6d53a8976ac86bf80a339 Mon Sep 17 00:00:00 2001 From: Sergiy Kyrylkov Date: Fri, 17 Feb 2023 16:36:52 +0100 Subject: [PATCH 062/123] feat: change headersTimeout and bodyTimeout to 300s (#1937) * feat: change bodyTimeout and headersTimeout to 300s * feat: revert bodyTimeout to 30s * fix: update headersTimeout default to 300e3 * fix: update timeout test * Revert "fix: update timeout test" This reverts commit 78a0552d1b1d65bae4a215aab5ecdfd7f3ee550d. * fix: address request-timeout.js failures * fix: update Dispatcher headersTimeout docs to 300s * feat: change bodyTimeout to 300s --- docs/api/Client.md | 4 ++-- docs/api/Dispatcher.md | 4 ++-- lib/client.js | 4 ++-- test/request-timeout.js | 8 ++++---- types/client.d.ts | 4 ++-- types/dispatcher.d.ts | 4 ++-- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/api/Client.md b/docs/api/Client.md index 970ed1eee65..fc7c5d26e8f 100644 --- a/docs/api/Client.md +++ b/docs/api/Client.md @@ -17,8 +17,8 @@ Returns: `Client` ### Parameter: `ClientOptions` -* **bodyTimeout** `number | null` (optional) - Default: `30e3` - The timeout after which a request will time out, in milliseconds. Monitors time between receiving body data. Use `0` to disable it entirely. Defaults to 30 seconds. -* **headersTimeout** `number | null` (optional) - Default: `30e3` - The amount of time the parser will wait to receive the complete HTTP headers while not sending the request. Defaults to 30 seconds. +* **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. diff --git a/docs/api/Dispatcher.md b/docs/api/Dispatcher.md index fa8ae2dd6fc..a50642948aa 100644 --- a/docs/api/Dispatcher.md +++ b/docs/api/Dispatcher.md @@ -199,8 +199,8 @@ Returns: `Boolean` - `false` if dispatcher is busy and further dispatch calls wo * **idempotent** `boolean` (optional) - Default: `true` if `method` is `'HEAD'` or `'GET'` - Whether the requests can be safely retried or not. If `false` the request won't be sent until all preceding requests in the pipeline has completed. * **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 30 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 30 seconds. +* **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. #### Parameter: `DispatchHandler` diff --git a/lib/client.js b/lib/client.js index 49d51f3400c..9dbd2eefe47 100644 --- a/lib/client.js +++ b/lib/client.js @@ -222,8 +222,8 @@ class Client extends DispatcherBase { this[kResuming] = 0 // 0, idle, 1, scheduled, 2 resuming this[kNeedDrain] = 0 // 0, idle, 1, scheduled, 2 resuming this[kHostHeader] = `host: ${this[kUrl].hostname}${this[kUrl].port ? `:${this[kUrl].port}` : ''}\r\n` - this[kBodyTimeout] = bodyTimeout != null ? bodyTimeout : 30e3 - this[kHeadersTimeout] = headersTimeout != null ? headersTimeout : 30e3 + this[kBodyTimeout] = bodyTimeout != null ? bodyTimeout : 300e3 + this[kHeadersTimeout] = headersTimeout != null ? headersTimeout : 300e3 this[kStrictContentLength] = strictContentLength == null ? true : strictContentLength this[kMaxRedirections] = maxRedirections this[kMaxRequests] = maxRequestsPerClient diff --git a/test/request-timeout.js b/test/request-timeout.js index 25e92294e84..3ec5c107e79 100644 --- a/test/request-timeout.js +++ b/test/request-timeout.js @@ -608,8 +608,8 @@ test('stream timeout', (t) => { const server = createServer((req, res) => { setTimeout(() => { res.end('hello') - }, 31e3) - clock.tick(31e3) + }, 301e3) + clock.tick(301e3) }) t.teardown(server.close.bind(server)) @@ -682,8 +682,8 @@ test('pipeline timeout', (t) => { const server = createServer((req, res) => { setTimeout(() => { req.pipe(res) - }, 31e3) - clock.tick(31e3) + }, 301e3) + clock.tick(301e3) }) t.teardown(server.close.bind(server)) diff --git a/types/client.d.ts b/types/client.d.ts index d147233408d..871e3c2448a 100644 --- a/types/client.d.ts +++ b/types/client.d.ts @@ -31,9 +31,9 @@ declare namespace Client { connect?: buildConnector.BuildOptions | buildConnector.connector | null; /** The maximum length of request headers in bytes. Default: `16384` (16KiB). */ maxHeaderSize?: 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. Default: `30e3` milliseconds (30s). */ + /** The timeout after which a request will time out, in milliseconds. Monitors time between receiving body data. Use `0` to disable it entirely. Default: `300e3` milliseconds (300s). */ bodyTimeout?: number | null; - /** The amount of time the parser will wait to receive the complete HTTP headers (Node 14 and above only). Default: `30e3` milliseconds (30s). */ + /** The amount of time the parser will wait to receive the complete HTTP headers (Node 14 and above only). Default: `300e3` milliseconds (300s). */ headersTimeout?: number | null; /** If `true`, an error is thrown when the request content-length header doesn't match the length of the request body. Default: `true`. */ strictContentLength?: boolean; diff --git a/types/dispatcher.d.ts b/types/dispatcher.d.ts index 68135d1e5c1..c6b0c8875eb 100644 --- a/types/dispatcher.d.ts +++ b/types/dispatcher.d.ts @@ -109,9 +109,9 @@ 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 30 seconds. */ + /** The amount of time 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 30 seconds. */ + /** 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; /** Whether the request should stablish a keep-alive or not. Default `false` */ reset?: boolean; From 28b9dea3fdcc453e25b3d305d5f004d85330cff1 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Sat, 18 Feb 2023 09:59:19 +0100 Subject: [PATCH 063/123] Bumped v5.20.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 1109bcb6b84..37baa25665b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "5.19.1", + "version": "5.20.0", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { From d8d9a96b6b8f856d9e01a1edfe22c3d83c6996ec Mon Sep 17 00:00:00 2001 From: Rafael Gonzaga Date: Sun, 19 Feb 2023 22:09:38 -0300 Subject: [PATCH 064/123] Create scorecard.yml (#1942) --- .github/workflows/scorecard.yml | 56 +++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 .github/workflows/scorecard.yml diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 00000000000..ab9c3e40b34 --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,56 @@ +# This workflow uses actions that are not certified by GitHub. They are provided +# by a third-party and are governed by separate terms of service, privacy +# policy, and support documentation. + +name: Scorecard supply-chain security +on: + # For Branch-Protection check. Only the default branch is supported. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection + branch_protection_rule: + # To guarantee Maintained check is occasionally updated. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained + schedule: + - cron: '16 10 * * 2' + push: + branches: [ "main" ] + +# Declare default permissions as read only. +permissions: read-all + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + permissions: + # Needed to upload the results to code-scanning dashboard. + security-events: write + # Needed to publish results and get a badge (see publish_results below). + id-token: write + + steps: + - name: "Checkout code" + uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v3.1.0 + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@e38b1902ae4f44df626f11ba0734b14fb91f8f86 # v2.1.2 + with: + results_file: results.sarif + results_format: sarif + publish_results: true + + # 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 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard. + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2.2.4 + with: + sarif_file: results.sarif From d04ec2b1b4145f9068fb97a43ea35f0bb3197d15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominykas=20Bly=C5=BE=C4=97?= Date: Tue, 21 Feb 2023 11:40:12 +0200 Subject: [PATCH 065/123] ci: timeout CI jobs after 15 minutes (#1946) --- .github/workflows/nodejs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index fee9d7aace4..b676ba08207 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -13,6 +13,7 @@ jobs: with: runs-on: ubuntu-latest, windows-latest test-command: npm run coverage:ci + timeout-minutes: 15 post-test-steps: | - name: Coverage Report uses: codecov/codecov-action@v3 From 5851019e063eac3e3db769bde154c6b6a98ff655 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 21 Feb 2023 16:52:44 +0100 Subject: [PATCH 066/123] test(wpt): respect variants (#1951) --- test/wpt/runner/runner/runner.mjs | 88 ++++++++++++++++++------------- test/wpt/runner/runner/util.mjs | 6 ++- 2 files changed, 57 insertions(+), 37 deletions(-) diff --git a/test/wpt/runner/runner/runner.mjs b/test/wpt/runner/runner/runner.mjs index 6e8bebb2460..1a17ca6f7a0 100644 --- a/test/wpt/runner/runner/runner.mjs +++ b/test/wpt/runner/runner/runner.mjs @@ -87,18 +87,27 @@ export class WPTRunner extends EventEmitter { /** @type {Set} */ const activeWorkers = new Set() let finishedFiles = 1 + let total = this.#files.length - for (const test of this.#files) { + const files = this.#files.map((test) => { const code = test.includes('.sub.') ? handlePipes(readFileSync(test, 'utf-8'), this.#url) : readFileSync(test, 'utf-8') const meta = this.resolveMeta(code, test) + if (meta.variant.length) { + total += meta.variant.length - 1 + } + + return [test, code, meta] + }) + + for (const [test, code, meta] of files) { if (this.#status[basename(test)]?.skip) { this.#stats.skipped += 1 console.log('='.repeat(96)) - console.log(colors(`[${finishedFiles}/${this.#files.length}] SKIPPED - ${test}`, 'yellow')) + console.log(colors(`[${finishedFiles}/${total}] SKIPPED - ${test}`, 'yellow')) console.log('='.repeat(96)) finishedFiles++ @@ -107,47 +116,54 @@ export class WPTRunner extends EventEmitter { const start = performance.now() - const worker = new Worker(workerPath, { - workerData: { - // Code to load before the test harness and tests. - initScripts: this.#initScripts, - // The test file. - test: code, - // Parsed META tag information - meta, - url: this.#url, - path: test + for (const variant of meta.variant.length ? meta.variant : ['']) { + const url = new URL(this.#url) + if (variant) { + url.search = variant } - }) + const worker = new Worker(workerPath, { + workerData: { + // Code to load before the test harness and tests. + initScripts: this.#initScripts, + // The test file. + test: code, + // Parsed META tag information + meta, + url: url.href, + path: test + } + }) - activeWorkers.add(worker) - // These values come directly from the web-platform-tests - const timeout = meta.timeout === 'long' ? 60_000 : 10_000 + activeWorkers.add(worker) + // These values come directly from the web-platform-tests + const timeout = meta.timeout === 'long' ? 60_000 : 10_000 - worker.on('message', (message) => { - if (message.type === 'result') { - this.handleIndividualTestCompletion(message, basename(test)) - } else if (message.type === 'completion') { - this.handleTestCompletion(worker) - } - }) + worker.on('message', (message) => { + if (message.type === 'result') { + this.handleIndividualTestCompletion(message, basename(test)) + } else if (message.type === 'completion') { + this.handleTestCompletion(worker) + } + }) - try { - activeWorkers.delete(worker) + try { + activeWorkers.delete(worker) - await once(worker, 'exit', { - signal: AbortSignal.timeout(timeout) - }) + await once(worker, 'exit', { + signal: AbortSignal.timeout(timeout) + }) - console.log('='.repeat(96)) - console.log(colors(`[${finishedFiles}/${this.#files.length}] PASSED - ${test}`, 'green')) - console.log(`Test took ${(performance.now() - start).toFixed(2)}ms`) - console.log('='.repeat(96)) + console.log('='.repeat(96)) + console.log(colors(`[${finishedFiles}/${total}] PASSED - ${test}`, 'green')) + if (variant) console.log('Variant:', variant) + console.log(`Test took ${(performance.now() - start).toFixed(2)}ms`) + console.log('='.repeat(96)) - finishedFiles++ - } catch (e) { - console.log(`${test} timed out after ${timeout}ms`) - throw e + finishedFiles++ + } catch (e) { + console.log(`${test} timed out after ${timeout}ms`) + throw e + } } } diff --git a/test/wpt/runner/runner/util.mjs b/test/wpt/runner/runner/util.mjs index 1d75029b6dc..aa6ad53afd7 100644 --- a/test/wpt/runner/runner/util.mjs +++ b/test/wpt/runner/runner/util.mjs @@ -25,7 +25,9 @@ export function parseMeta (fileContents) { /** @type {string[]} */ global: [], /** @type {string[]} */ - scripts: [] + scripts: [], + /** @type {string[]} */ + variant: [] } for (const line of lines) { @@ -42,6 +44,8 @@ export function parseMeta (fileContents) { switch (groups.type) { case 'variant': + meta[groups.type].push(groups.match) + break case 'title': case 'timeout': { meta[groups.type] = groups.match From ab494d2699d68ed3d8a13ea11e9862662a5893a7 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Tue, 21 Feb 2023 17:15:51 +0100 Subject: [PATCH 067/123] fix: improve isFormDataLike compat (#1953) Fixes: https://github.com/nodejs/undici/issues/1952 --- lib/core/util.js | 25 +++++++++++-------------- test/fetch/formdata.js | 4 ++++ 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/lib/core/util.js b/lib/core/util.js index ef9b4570dcd..334bc26c776 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -375,20 +375,17 @@ function ReadableStreamFrom (iterable) { // The chunk should be a FormData instance and contains // all the required methods. -function isFormDataLike (chunk) { - return (chunk && - chunk.constructor && chunk.constructor.name === 'FormData' && - typeof chunk === 'object' && - (typeof chunk.append === 'function' && - typeof chunk.delete === 'function' && - typeof chunk.get === 'function' && - typeof chunk.getAll === 'function' && - typeof chunk.has === 'function' && - typeof chunk.set === 'function' && - typeof chunk.entries === 'function' && - typeof chunk.keys === 'function' && - typeof chunk.values === 'function' && - typeof chunk.forEach === 'function') +function isFormDataLike (object) { + return ( + object && + typeof object === 'object' && + typeof object.append === 'function' && + typeof object.delete === 'function' && + typeof object.get === 'function' && + typeof object.getAll === 'function' && + typeof object.has === 'function' && + typeof object.set === 'function' && + object[Symbol.toStringTag] === 'FormData' ) } diff --git a/test/fetch/formdata.js b/test/fetch/formdata.js index 156c11fb77d..fed95bfb3da 100644 --- a/test/fetch/formdata.js +++ b/test/fetch/formdata.js @@ -349,6 +349,10 @@ test('FormData should be compatible with third-party libraries', (t) => { this.data = [] } + get [Symbol.toStringTag] () { + return 'FormData' + } + append () {} delete () {} get () {} From bc8e2b801b0e93d7d5b8482beb8ab33967162dd0 Mon Sep 17 00:00:00 2001 From: Khafra Date: Tue, 21 Feb 2023 19:21:14 -0500 Subject: [PATCH 068/123] fix: flaky fetch tests (#1956) --- test/fetch/data-uri.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/fetch/data-uri.js b/test/fetch/data-uri.js index 001eeb052bc..d4ca7ebab56 100644 --- a/test/fetch/data-uri.js +++ b/test/fetch/data-uri.js @@ -180,14 +180,14 @@ test('too long base64 url', async (t) => { test('https://domain.com/#', (t) => { t.plan(1) - const domain = 'https://domain.com/#' + const domain = 'https://domain.com/#a' const serialized = URLSerializer(new URL(domain)) t.equal(serialized, domain) }) test('https://domain.com/?', (t) => { t.plan(1) - const domain = 'https://domain.com/?' + const domain = 'https://domain.com/?a=b' const serialized = URLSerializer(new URL(domain)) t.equal(serialized, domain) }) From 474f54f808f0bbedf53255f67e63e5896bf7bff9 Mon Sep 17 00:00:00 2001 From: Khafra Date: Wed, 22 Feb 2023 01:42:26 -0500 Subject: [PATCH 069/123] test(wpt): include all testing files (#1954) * test(wpt): include all testing files * lint * fix: mark flaky/failing tests by entire file path * fix: pass 16/20 tests in request-headers.any.js * fix: patch referer header never being set * fix some tests * fix: node-fetch test --- CONTRIBUTING.md | 39 + lib/fetch/request.js | 5 +- lib/fetch/util.js | 179 +- test/wpt/runner/runner/runner.mjs | 63 +- test/wpt/runner/runner/util.mjs | 20 + test/wpt/server/server.mjs | 22 + test/wpt/start-xhr.mjs | 2 +- test/wpt/status/FileAPI.status.json | 61 +- test/wpt/status/fetch.status.json | 596 +- test/wpt/status/mimesniff.status.json | 10 +- test/wpt/status/websockets.status.json | 54 +- test/wpt/status/xhr.status.json | 1 - test/wpt/status/xhr/formdata.status.json | 1 + .../Blob-methods-from-detached-frame.html | 59 + .../cross-partition.tentative.https.html | 276 + .../FileAPI/BlobURL/support/file_test2.txt | 0 .../tests/FileAPI/BlobURL/test2-manual.html | 62 + .../progress_event_bubbles_cancelable.html | 33 + .../FileAPI/FileReader/support/file_test1.txt | 0 .../FileReader/test_errors-manual.html | 72 + .../test_notreadableerrors-manual.html | 42 + .../test_securityerrors-manual.html | 40 + .../wpt/tests/FileAPI/FileReader/workers.html | 27 + .../tests/FileAPI/FileReaderSync.worker.js | 56 + test/wpt/tests/FileAPI/META.yml | 6 + .../FileAPI/blob/Blob-array-buffer.any.js | 45 + .../blob/Blob-constructor-dom.window.js | 53 + .../blob/Blob-constructor-endings.html | 104 + .../FileAPI/blob/Blob-constructor.any.js | 468 ++ .../FileAPI/blob/Blob-in-worker.worker.js | 9 + .../FileAPI/blob/Blob-slice-overflow.any.js | 32 + test/wpt/tests/FileAPI/blob/Blob-slice.any.js | 231 + .../FileAPI/blob/Blob-stream-byob-crash.html | 11 + .../wpt/tests/FileAPI/blob/Blob-stream.any.js | 83 + test/wpt/tests/FileAPI/blob/Blob-text.any.js | 64 + .../file/File-constructor-endings.html | 104 + .../Worker-read-file-constructor.worker.js | 15 + .../file/resources/echo-content-escaped.py | 26 + .../FileAPI/file/send-file-form-controls.html | 113 + .../file/send-file-form-iso-2022-jp.html | 65 + .../file/send-file-form-punctuation.html | 226 + .../FileAPI/file/send-file-form-utf-8.html | 62 + .../file/send-file-form-windows-1252.html | 62 + .../file/send-file-form-x-user-defined.html | 63 + .../tests/FileAPI/file/send-file-form.html | 25 + .../FileAPI/filelist-section/filelist.html | 57 + ...lelist_multiple_selected_files-manual.html | 64 + .../filelist_selected_file-manual.html | 64 + .../filelist-section/support/upload.txt | 1 + .../filelist-section/support/upload.zip | Bin 0 -> 220 bytes test/wpt/tests/FileAPI/historical.https.html | 65 + test/wpt/tests/FileAPI/idlharness-manual.html | 45 + test/wpt/tests/FileAPI/idlharness.html | 37 + test/wpt/tests/FileAPI/idlharness.worker.js | 17 + test/wpt/tests/FileAPI/progress-manual.html | 49 + .../filereader_file-manual.html | 69 + .../filereader_file_img-manual.html | 47 + .../filereader_result.any.js | 82 + .../support/blue-100x100.png | Bin 0 -> 227 bytes test/wpt/tests/FileAPI/support/Blob.js | 70 + .../support/document-domain-setter.sub.html | 7 + .../tests/FileAPI/support/empty-document.html | 3 + .../support/historical-serviceworker.js | 5 + .../tests/FileAPI/support/incumbent.sub.html | 22 + .../FileAPI/support/send-file-form-helper.js | 282 + test/wpt/tests/FileAPI/support/upload.txt | 1 + .../wpt/tests/FileAPI/support/url-origin.html | 6 + test/wpt/tests/FileAPI/unicode.html | 46 + .../FileAPI/url/cross-global-revoke.sub.html | 62 + ...multi-global-origin-serialization.sub.html | 26 + .../FileAPI/url/resources/create-helper.html | 7 + .../FileAPI/url/resources/create-helper.js | 4 + .../FileAPI/url/resources/fetch-tests.js | 71 + .../FileAPI/url/resources/revoke-helper.html | 7 + .../FileAPI/url/resources/revoke-helper.js | 9 + .../tests/FileAPI/url/sandboxed-iframe.html | 32 + .../tests/FileAPI/url/unicode-origin.sub.html | 23 + .../tests/FileAPI/url/url-charset.window.js | 34 + test/wpt/tests/FileAPI/url/url-format.any.js | 70 + .../FileAPI/url/url-in-tags-revoke.window.js | 115 + .../tests/FileAPI/url/url-in-tags.window.js | 48 + test/wpt/tests/FileAPI/url/url-lifetime.html | 56 + .../tests/FileAPI/url/url-reload.window.js | 36 + .../tests/FileAPI/url/url-with-fetch.any.js | 72 + .../wpt/tests/FileAPI/url/url-with-xhr.any.js | 68 + .../url/url_createobjecturl_file-manual.html | 45 + .../url_createobjecturl_file_img-manual.html | 28 + .../url/url_xmlhttprequest_img-ref.html | 12 + .../FileAPI/url/url_xmlhttprequest_img.html | 27 + test/wpt/tests/LICENSE.md | 11 - test/wpt/tests/common/CustomCorsResponse.py | 30 + test/wpt/tests/common/META.yml | 3 + test/wpt/tests/common/PrefixedLocalStorage.js | 116 + .../common/PrefixedLocalStorage.js.headers | 1 + test/wpt/tests/common/PrefixedPostMessage.js | 100 + .../common/PrefixedPostMessage.js.headers | 1 + test/wpt/tests/common/README.md | 10 + test/wpt/tests/common/__init__.py | 0 test/wpt/tests/common/arrays.js | 31 + test/wpt/tests/common/blank-with-cors.html | 0 .../tests/common/blank-with-cors.html.headers | 1 + test/wpt/tests/common/blank.html | 0 test/wpt/tests/common/custom-cors-response.js | 32 + test/wpt/tests/common/dispatcher/README.md | 228 + .../wpt/tests/common/dispatcher/dispatcher.js | 256 + .../wpt/tests/common/dispatcher/dispatcher.py | 53 + .../dispatcher/executor-service-worker.js | 24 + .../common/dispatcher/executor-worker.js | 12 + .../wpt/tests/common/dispatcher/executor.html | 15 + .../common/dispatcher/remote-executor.html | 12 + test/wpt/tests/common/domain-setter.sub.html | 8 + test/wpt/tests/common/dummy.xhtml | 2 + test/wpt/tests/common/dummy.xml | 1 + test/wpt/tests/common/echo.py | 6 + test/wpt/tests/common/gc.js | 52 + .../tests/common/get-host-info.sub.js.headers | 1 + test/wpt/tests/common/media.js | 55 + test/wpt/tests/common/media.js.headers | 1 + test/wpt/tests/common/object-association.js | 74 + .../common/object-association.js.headers | 1 + .../common/performance-timeline-utils.js | 56 + .../performance-timeline-utils.js.headers | 1 + test/wpt/tests/common/proxy-all.sub.pac | 3 + test/wpt/tests/common/redirect-opt-in.py | 20 + test/wpt/tests/common/redirect.py | 19 + test/wpt/tests/common/refresh.py | 11 + test/wpt/tests/common/reftest-wait.js | 39 + test/wpt/tests/common/reftest-wait.js.headers | 1 + test/wpt/tests/common/rendering-utils.js | 19 + test/wpt/tests/common/sab.js | 21 + .../tests/common/security-features/README.md | 460 ++ .../common/security-features/__init__.py | 0 .../security-features/resources/common.sub.js | 1303 +++++ .../resources/common.sub.js.headers | 1 + .../security-features/scope/__init__.py | 0 .../security-features/scope/document.py | 36 + .../scope/template/document.html.template | 30 + .../scope/template/worker.js.template | 29 + .../common/security-features/scope/util.py | 43 + .../common/security-features/scope/worker.py | 44 + .../security-features/subresource/__init__.py | 0 .../security-features/subresource/audio.py | 18 + .../security-features/subresource/document.py | 12 + .../security-features/subresource/empty.py | 14 + .../security-features/subresource/font.py | 76 + .../security-features/subresource/image.py | 116 + .../security-features/subresource/referrer.py | 4 + .../security-features/subresource/script.py | 14 + .../subresource/shared-worker.py | 13 + .../subresource/static-import.py | 19 + .../subresource/stylesheet.py | 61 + .../subresource/subresource.py | 199 + .../security-features/subresource/svg.py | 37 + .../template/document.html.template | 16 + .../subresource/template/font.css.template | 9 + .../subresource/template/image.css.template | 3 + .../subresource/template/script.js.template | 3 + .../template/shared-worker.js.template | 5 + .../template/static-import.js.template | 1 + .../subresource/template/svg.css.template | 3 + .../template/svg.embedded.template | 5 + .../subresource/template/worker.js.template | 3 + .../security-features/subresource/video.py | 17 + .../security-features/subresource/worker.py | 13 + .../security-features/subresource/xhr.py | 16 + .../tools/format_spec_src_json.py | 24 + .../security-features/tools/generate.py | 464 ++ .../security-features/tools/spec.src.json | 533 ++ .../security-features/tools/spec_validator.py | 253 + .../tools/template/disclaimer.template | 1 + .../tools/template/spec_json.js.template | 1 + .../tools/template/test.debug.html.template | 26 + .../tools/template/test.release.html.template | 22 + .../common/security-features/tools/util.py | 230 + .../tests/common/security-features/types.md | 62 + test/wpt/tests/common/slow-redirect.py | 29 + test/wpt/tests/common/slow.py | 6 + test/wpt/tests/common/square.png | Bin 0 -> 18299 bytes test/wpt/tests/common/stringifiers.js | 57 + test/wpt/tests/common/stringifiers.js.headers | 1 + test/wpt/tests/common/subset-tests-by-key.js | 83 + test/wpt/tests/common/subset-tests.js | 60 + .../test-setting-immutable-prototype.js | 67 + ...est-setting-immutable-prototype.js.headers | 1 + test/wpt/tests/common/text-plain.txt | 4 + .../common/third_party/reftest-analyzer.xhtml | 934 ++++ test/wpt/tests/common/utils.js.headers | 1 + test/wpt/tests/common/window-name-setter.html | 12 + test/wpt/tests/common/worklet-reftest.js | 50 + .../tests/common/worklet-reftest.js.headers | 1 + test/wpt/tests/fetch/META.yml | 7 + test/wpt/tests/fetch/README.md | 6 + .../tests/fetch/api/abort/cache.https.any.js | 47 + .../fetch/api/abort/destroyed-context.html | 27 + test/wpt/tests/fetch/api/abort/general.any.js | 1 - test/wpt/tests/fetch/api/abort/keepalive.html | 85 + .../serviceworker-intercepted.https.html | 212 + .../fetch/api/basic/block-mime-as-script.html | 43 + .../fetch/api/basic/conditional-get.any.js | 38 + test/wpt/tests/fetch/api/basic/keepalive.html | 106 + .../fetch/api/basic/mediasource.window.js | 5 + .../fetch/api/basic/mode-no-cors.sub.any.js | 29 + .../fetch/api/basic/mode-same-origin.any.js | 28 + .../wpt/tests/fetch/api/basic/referrer.any.js | 29 + .../basic/request-forbidden-headers.any.js | 100 + .../fetch/api/basic/request-headers.any.js | 82 + .../request-referrer-redirected-worker.html | 17 + .../fetch/api/basic/request-referrer.any.js | 24 + .../fetch/api/basic/request-upload.h2.any.js | 186 + .../tests/fetch/api/basic/status.h2.any.js | 17 + .../wpt/tests/fetch/api/body/mime-type.any.js | 2 +- .../tests/fetch/api/cors/cors-basic.any.js | 37 + .../api/cors/cors-cookies-redirect.any.js | 49 + .../tests/fetch/api/cors/cors-cookies.any.js | 56 + .../api/cors/cors-expose-star.sub.any.js | 41 + .../fetch/api/cors/cors-filtering.sub.any.js | 69 + .../api/cors/cors-multiple-origins.sub.any.js | 22 + .../fetch/api/cors/cors-no-preflight.any.js | 41 + .../tests/fetch/api/cors/cors-origin.any.js | 51 + .../api/cors/cors-preflight-cache.any.js | 46 + .../cors-preflight-not-cors-safelisted.any.js | 19 + .../api/cors/cors-preflight-redirect.any.js | 37 + .../api/cors/cors-preflight-referrer.any.js | 51 + .../cors-preflight-response-validation.any.js | 33 + .../fetch/api/cors/cors-preflight-star.any.js | 82 + .../api/cors/cors-preflight-status.any.js | 37 + .../fetch/api/cors/cors-preflight.any.js | 62 + .../api/cors/cors-redirect-credentials.any.js | 52 + .../api/cors/cors-redirect-preflight.any.js | 46 + .../tests/fetch/api/cors/cors-redirect.any.js | 42 + .../tests/fetch/api/cors/data-url-iframe.html | 58 + .../api/cors/data-url-shared-worker.html | 53 + .../tests/fetch/api/cors/data-url-worker.html | 50 + .../fetch/api/cors/resources/corspreflight.js | 58 + .../cors/resources/not-cors-safelisted.json | 13 + .../fetch/api/cors/sandboxed-iframe.html | 14 + .../tests/fetch/api/crashtests/request.html | 8 + .../credentials/authentication-basic.any.js | 17 + .../authentication-redirection.any.js | 2 +- .../fetch/api/credentials/cookies.any.js | 49 + .../fetch/api/headers/header-setcookie.any.js | 42 + .../fetch/api/headers/headers-no-cors.any.js | 59 + .../api/policies/csp-blocked-worker.html | 16 + .../tests/fetch/api/policies/csp-blocked.html | 15 + .../api/policies/csp-blocked.html.headers | 1 + .../tests/fetch/api/policies/csp-blocked.js | 13 + .../fetch/api/policies/csp-blocked.js.headers | 1 + .../tests/fetch/api/policies/nested-policy.js | 1 + .../api/policies/nested-policy.js.headers | 1 + ...rrer-no-referrer-service-worker.https.html | 18 + .../policies/referrer-no-referrer-worker.html | 17 + .../api/policies/referrer-no-referrer.html | 15 + .../referrer-no-referrer.html.headers | 1 + .../api/policies/referrer-no-referrer.js | 19 + .../policies/referrer-no-referrer.js.headers | 1 + .../referrer-origin-service-worker.https.html | 18 + ...hen-cross-origin-service-worker.https.html | 17 + ...errer-origin-when-cross-origin-worker.html | 16 + .../referrer-origin-when-cross-origin.html | 16 + ...rrer-origin-when-cross-origin.html.headers | 1 + .../referrer-origin-when-cross-origin.js | 21 + ...ferrer-origin-when-cross-origin.js.headers | 1 + .../api/policies/referrer-origin-worker.html | 17 + .../fetch/api/policies/referrer-origin.html | 16 + .../api/policies/referrer-origin.html.headers | 1 + .../fetch/api/policies/referrer-origin.js | 30 + .../api/policies/referrer-origin.js.headers | 1 + ...errer-unsafe-url-service-worker.https.html | 18 + .../policies/referrer-unsafe-url-worker.html | 17 + .../api/policies/referrer-unsafe-url.html | 16 + .../policies/referrer-unsafe-url.html.headers | 1 + .../fetch/api/policies/referrer-unsafe-url.js | 21 + .../policies/referrer-unsafe-url.js.headers | 1 + .../redirect/redirect-empty-location.any.js | 21 + .../redirect-location-escape.tentative.any.js | 46 + .../fetch/api/redirect/redirect-mode.any.js | 59 + .../fetch/api/redirect/redirect-origin.any.js | 42 + .../redirect-referrer-override.any.js | 104 + .../api/redirect/redirect-referrer.any.js | 66 + .../api/redirect/redirect-upload.h2.any.js | 33 + .../fetch-destination-frame.https.html | 51 + .../fetch-destination-iframe.https.html | 51 + ...fetch-destination-no-load-event.https.html | 124 + .../fetch-destination-prefetch.https.html | 46 + .../fetch-destination-worker.https.html | 60 + .../destination/fetch-destination.https.html | 435 ++ .../api/request/destination/resources/dummy | 0 .../request/destination/resources/dummy.es | 0 .../destination/resources/dummy.es.headers | 1 + .../request/destination/resources/dummy.html | 0 .../request/destination/resources/dummy.png | Bin 0 -> 18299 bytes .../request/destination/resources/dummy.ttf | Bin 0 -> 2528 bytes .../destination/resources/dummy_audio.mp3 | Bin 0 -> 20498 bytes .../destination/resources/dummy_audio.oga | Bin 0 -> 18541 bytes .../destination/resources/dummy_video.mp4 | Bin 0 -> 67369 bytes .../destination/resources/dummy_video.ogv | Bin 0 -> 94372 bytes .../destination/resources/empty.https.html | 0 .../fetch-destination-worker-frame.js | 20 + .../fetch-destination-worker-iframe.js | 20 + .../fetch-destination-worker-no-load-event.js | 20 + .../resources/fetch-destination-worker.js | 12 + .../request/destination/resources/importer.js | 1 + .../multi-globals/current/current.html | 3 + .../multi-globals/incumbent/incumbent.html | 14 + .../request/multi-globals/url-parsing.html | 27 + .../request-cache-default-conditional.any.js | 170 + .../api/request/request-cache-default.any.js | 39 + .../request/request-cache-force-cache.any.js | 67 + .../api/request/request-cache-no-cache.any.js | 25 + .../api/request/request-cache-no-store.any.js | 37 + .../request-cache-only-if-cached.any.js | 66 + .../api/request/request-cache-reload.any.js | 51 + .../tests/fetch/api/request/request-cache.js | 223 + .../fetch/api/request/request-clone.sub.html | 63 + .../fetch/api/request/request-headers.any.js | 2 + .../api/request/request-init-001.sub.html | 112 + .../api/request/request-init-003.sub.html | 84 + .../api/request/request-init-priority.any.js | 26 + .../api/request/request-keepalive-quota.html | 97 + .../request-reset-attributes.https.html | 96 + .../api/request/request-structure.any.js | 12 +- .../fetch/api/request/resources/cache.py | 67 + .../fetch/api/request/resources/hello.txt | 1 + .../request-reset-attributes-worker.js | 19 + .../tests/fetch/api/request/url-encoding.html | 25 + .../fetch/api/resources/authentication.py | 14 + .../fetch/api/resources/bad-chunk-encoding.py | 13 + test/wpt/tests/fetch/api/resources/basic.html | 5 + test/wpt/tests/fetch/api/resources/cache.py | 18 + .../tests/fetch/api/resources/clean-stash.py | 6 + .../fetch/api/resources/cors-top.txt.headers | 1 + .../resources/dump-authorization-header.py | 14 + .../fetch/api/resources/echo-content.h2.py | 7 + .../tests/fetch/api/resources/echo-content.py | 12 + .../api/resources/infinite-slow-response.py | 35 + .../fetch/api/resources/inspect-headers.py | 24 + .../fetch/api/resources/keepalive-iframe.html | 25 + .../fetch/api/resources/keepalive-window.html | 34 + test/wpt/tests/fetch/api/resources/method.py | 18 + .../tests/fetch/api/resources/preflight.py | 78 + .../api/resources/redirect-empty-location.py | 3 + .../tests/fetch/api/resources/redirect.h2.py | 14 + .../wpt/tests/fetch/api/resources/redirect.py | 73 + .../fetch/api/resources/sandboxed-iframe.html | 34 + .../fetch/api/resources/script-with-header.py | 7 + .../tests/fetch/api/resources/stash-put.py | 17 + .../tests/fetch/api/resources/stash-take.py | 9 + test/wpt/tests/fetch/api/resources/status.py | 11 + .../fetch/api/resources/sw-intercept-abort.js | 19 + .../tests/fetch/api/resources/sw-intercept.js | 10 + test/wpt/tests/fetch/api/resources/trickle.py | 15 + test/wpt/tests/fetch/api/resources/utils.js | 6 +- .../multi-globals/current/current.html | 3 + .../multi-globals/incumbent/incumbent.html | 16 + .../multi-globals/relevant/relevant.html | 2 + .../response/multi-globals/url-parsing.html | 27 + .../response-body-read-task-handling.html | 54 + .../response/response-clone-iframe.window.js | 32 + .../response/response-consume-stream.any.js | 60 +- .../fetch/api/response/response-consume.html | 317 ++ .../response-stream-with-broken-then.any.js | 117 + .../network-partition-key.html | 264 + ...network-partition-about-blank-checker.html | 35 + .../resources/network-partition-checker.html | 30 + .../network-partition-iframe-checker.html | 22 + .../resources/network-partition-key.js | 47 + .../resources/network-partition-key.py | 130 + .../network-partition-worker-checker.html | 24 + .../resources/network-partition-worker.js | 15 + .../resources/bad-gzip-body.py | 3 + .../api-and-duplicate-headers.any.js | 23 + .../fetch/content-length/content-length.html | 14 + .../content-length.html.headers | 1 + .../fetch/content-length/parsing.window.js | 18 + .../resources/content-length.py | 10 + .../resources/content-lengths.json | 126 + .../resources/identical-duplicates.asis | 9 + .../fetch/content-length/too-long.window.js | 4 + test/wpt/tests/fetch/content-type/README.md | 20 + .../fetch/content-type/multipart.window.js | 33 + .../content-type/resources/content-type.py | 18 + .../content-type/resources/content-types.json | 122 + .../resources/script-content-types.json | 92 + .../fetch/content-type/response.window.js | 72 + .../tests/fetch/content-type/script.window.js | 48 + test/wpt/tests/fetch/corb/README.md | 67 + .../img-html-correctly-labeled.sub-ref.html | 4 + .../corb/img-html-correctly-labeled.sub.html | 11 + ...img-mime-types-coverage.tentative.sub.html | 85 + ...led-as-html-nosniff.tentative.sub-ref.html | 4 + ...labeled-as-html-nosniff.tentative.sub.html | 11 + .../img-png-mislabeled-as-html.sub-ref.html | 4 + .../corb/img-png-mislabeled-as-html.sub.html | 10 + ...g-svg-doctype-html-mimetype-empty.sub.html | 7 + ...img-svg-doctype-html-mimetype-svg.sub.html | 11 + .../fetch/corb/img-svg-invalid.sub-ref.html | 5 + .../corb/img-svg-labeled-as-dash.sub.html | 6 + .../corb/img-svg-labeled-as-svg-xml.sub.html | 6 + .../fetch/corb/img-svg-xml-decl.sub.html | 6 + .../wpt/tests/fetch/corb/img-svg.sub-ref.html | 5 + ...labeled-as-html-nosniff.tentative.sub.html | 24 + .../css-mislabeled-as-html-nosniff.css | 1 + ...css-mislabeled-as-html-nosniff.css.headers | 2 + .../corb/resources/css-mislabeled-as-html.css | 1 + .../css-mislabeled-as-html.css.headers | 1 + .../css-with-json-parser-breaker.css | 3 + .../corb/resources/empty-labeled-as-png.png | 0 .../empty-labeled-as-png.png.headers | 1 + .../resources/html-correctly-labeled.html | 10 + .../html-correctly-labeled.html.headers | 1 + .../fetch/corb/resources/html-js-polyglot.js | 9 + .../resources/html-js-polyglot.js.headers | 1 + .../fetch/corb/resources/html-js-polyglot2.js | 10 + .../resources/html-js-polyglot2.js.headers | 1 + .../js-mislabeled-as-html-nosniff.js | 1 + .../js-mislabeled-as-html-nosniff.js.headers | 2 + .../corb/resources/js-mislabeled-as-html.js | 1 + .../js-mislabeled-as-html.js.headers | 1 + .../corb/resources/png-correctly-labeled.png | Bin 0 -> 1010 bytes .../png-correctly-labeled.png.headers | 1 + .../png-mislabeled-as-html-nosniff.png | Bin 0 -> 1010 bytes ...png-mislabeled-as-html-nosniff.png.headers | 2 + .../corb/resources/png-mislabeled-as-html.png | Bin 0 -> 1010 bytes .../png-mislabeled-as-html.png.headers | 1 + .../corb/resources/response_block_probe.js | 1 + .../resources/response_block_probe.js.headers | 1 + .../corb/resources/sniffable-resource.py | 11 + ...ts-html-containing-blob-url-to-parent.html | 16 + .../svg-doctype-html-mimetype-empty.svg | 4 + ...vg-doctype-html-mimetype-empty.svg.headers | 1 + .../svg-doctype-html-mimetype-svg.svg | 4 + .../svg-doctype-html-mimetype-svg.svg.headers | 1 + .../corb/resources/svg-labeled-as-dash.svg | 3 + .../resources/svg-labeled-as-dash.svg.headers | 1 + .../corb/resources/svg-labeled-as-svg-xml.svg | 3 + .../svg-labeled-as-svg-xml.svg.headers | 1 + .../fetch/corb/resources/svg-xml-decl.svg | 4 + test/wpt/tests/fetch/corb/resources/svg.svg | 3 + .../fetch/corb/resources/svg.svg.headers | 1 + .../response_block.tentative.sub.https.html | 44 + ...-html-correctly-labeled.tentative.sub.html | 32 + .../corb/script-html-js-polyglot.sub.html | 32 + ...pt-html-via-cross-origin-blob-url.sub.html | 38 + ...ipt-js-mislabeled-as-html-nosniff.sub.html | 33 + .../script-js-mislabeled-as-html.sub.html | 25 + ...ith-json-parser-breaker.tentative.sub.html | 85 + ...with-nonsniffable-types.tentative.sub.html | 84 + ...le-css-mislabeled-as-html-nosniff.sub.html | 42 + .../style-css-mislabeled-as-html.sub.html | 36 + ...tyle-css-with-json-parser-breaker.sub.html | 38 + .../style-html-correctly-labeled.sub.html | 41 + .../fetch-in-iframe.html | 67 + .../cross-origin-resource-policy/fetch.any.js | 76 + .../fetch.https.any.js | 56 + .../iframe-loads.html | 46 + .../image-loads.html | 54 + .../resources/green.png | Bin 0 -> 87 bytes .../resources/hello.py | 6 + .../resources/iframe.py | 5 + .../resources/iframeFetch.html | 19 + .../resources/image.py | 22 + .../resources/redirect.py | 6 + .../resources/script.py | 6 + .../scheme-restriction.any.js | 7 + .../scheme-restriction.https.window.js | 13 + .../script-loads.html | 52 + .../syntax.any.js | 19 + test/wpt/tests/fetch/data-urls/README.md | 11 + test/wpt/tests/fetch/h1-parsing/README.md | 5 + .../tests/fetch/h1-parsing/lone-cr.window.js | 23 + .../resources-with-0x00-in-header.window.js | 31 + .../fetch/h1-parsing/resources/README.md | 6 + .../resources/blue-with-0x00-in-a-header.asis | Bin 0 -> 546 bytes .../resources/document-with-0x00-in-header.py | 4 + .../fetch/h1-parsing/resources/message.py | 3 + .../resources/script-with-0x00-in-header.py | 4 + .../fetch/h1-parsing/resources/status-code.py | 6 + .../fetch/h1-parsing/status-code.window.js | 98 + .../tests/fetch/http-cache/304-update.any.js | 146 + test/wpt/tests/fetch/http-cache/README.md | 72 + .../http-cache/basic-auth-cache-test-ref.html | 6 + .../http-cache/basic-auth-cache-test.html | 27 + .../tests/fetch/http-cache/cache-mode.any.js | 61 + .../tests/fetch/http-cache/cc-request.any.js | 202 + .../http-cache/credentials.tentative.any.js | 62 + .../tests/fetch/http-cache/freshness.any.js | 215 + .../tests/fetch/http-cache/heuristic.any.js | 93 + test/wpt/tests/fetch/http-cache/http-cache.js | 274 + .../tests/fetch/http-cache/invalidate.any.js | 235 + .../wpt/tests/fetch/http-cache/partial.any.js | 208 + .../tests/fetch/http-cache/post-patch.any.js | 46 + .../fetch/http-cache/resources/http-cache.py | 124 + .../http-cache/resources/securedimage.py | 19 + .../split-cache-popup-with-iframe.html | 34 + .../resources/split-cache-popup.html | 28 + .../tests/fetch/http-cache/split-cache.html | 158 + test/wpt/tests/fetch/http-cache/status.any.js | 60 + test/wpt/tests/fetch/http-cache/vary.any.js | 313 ++ ...vas-remote-read-remote-image-redirect.html | 28 + test/wpt/tests/fetch/metadata/META.yml | 4 + test/wpt/tests/fetch/metadata/README.md | 9 + .../fetch/metadata/audio-worklet.https.html | 20 + .../metadata/embed.https.sub.tentative.html | 63 + .../metadata/fetch-preflight.https.sub.any.js | 29 + .../fetch/metadata/fetch.https.sub.any.js | 58 + .../appcache-manifest.https.sub.html | 341 ++ .../generated/audioworklet.https.sub.html | 271 + .../css-font-face.https.sub.tentative.html | 230 + .../css-font-face.sub.tentative.html | 196 + .../css-images.https.sub.tentative.html | 1384 +++++ .../generated/css-images.sub.tentative.html | 1099 ++++ .../generated/element-a.https.sub.html | 482 ++ .../metadata/generated/element-a.sub.html | 342 ++ .../generated/element-area.https.sub.html | 482 ++ .../metadata/generated/element-area.sub.html | 342 ++ .../generated/element-audio.https.sub.html | 325 ++ .../metadata/generated/element-audio.sub.html | 229 + .../generated/element-embed.https.sub.html | 224 + .../metadata/generated/element-embed.sub.html | 190 + .../generated/element-frame.https.sub.html | 309 ++ .../metadata/generated/element-frame.sub.html | 250 + .../generated/element-iframe.https.sub.html | 309 ++ .../generated/element-iframe.sub.html | 250 + ...ment-img-environment-change.https.sub.html | 357 ++ .../element-img-environment-change.sub.html | 270 + .../generated/element-img.https.sub.html | 645 +++ .../metadata/generated/element-img.sub.html | 456 ++ .../element-input-image.https.sub.html | 229 + .../generated/element-input-image.sub.html | 184 + .../element-link-icon.https.sub.html | 371 ++ .../generated/element-link-icon.sub.html | 279 + ...ment-link-prefetch.https.optional.sub.html | 559 ++ .../element-link-prefetch.optional.sub.html | 275 + ...ement-meta-refresh.https.optional.sub.html | 276 + .../element-meta-refresh.optional.sub.html | 225 + .../generated/element-picture.https.sub.html | 997 ++++ .../generated/element-picture.sub.html | 721 +++ .../generated/element-script.https.sub.html | 593 ++ .../generated/element-script.sub.html | 488 ++ .../element-video-poster.https.sub.html | 243 + .../generated/element-video-poster.sub.html | 198 + .../generated/element-video.https.sub.html | 325 ++ .../metadata/generated/element-video.sub.html | 229 + .../fetch-via-serviceworker.https.sub.html | 683 +++ .../metadata/generated/fetch.https.sub.html | 302 + .../fetch/metadata/generated/fetch.sub.html | 220 + .../generated/form-submission.https.sub.html | 522 ++ .../generated/form-submission.sub.html | 400 ++ .../generated/header-link.https.sub.html | 529 ++ .../header-link.https.sub.tentative.html | 51 + .../metadata/generated/header-link.sub.html | 460 ++ .../header-refresh.https.optional.sub.html | 273 + .../header-refresh.optional.sub.html | 222 + ...cript-module-import-dynamic.https.sub.html | 254 + .../script-module-import-dynamic.sub.html | 214 + ...script-module-import-static.https.sub.html | 288 + .../script-module-import-static.sub.html | 246 + .../generated/serviceworker.https.sub.html | 170 + .../generated/svg-image.https.sub.html | 367 ++ .../metadata/generated/svg-image.sub.html | 265 + .../generated/window-history.https.sub.html | 237 + .../generated/window-history.sub.html | 360 ++ .../generated/window-location.https.sub.html | 1184 ++++ .../generated/window-location.sub.html | 894 +++ ...orker-dedicated-constructor.https.sub.html | 118 + .../worker-dedicated-constructor.sub.html | 204 + ...ker-dedicated-importscripts.https.sub.html | 268 + .../worker-dedicated-importscripts.sub.html | 228 + .../fetch/metadata/navigation.https.sub.html | 23 + .../fetch/metadata/object.https.sub.html | 62 + .../fetch/metadata/paint-worklet.https.html | 19 + .../fetch/metadata/portal.https.sub.html | 50 + .../fetch/metadata/preload.https.sub.html | 50 + ...-redirect-https-downgrade-upgrade.sub.html | 18 + .../redirect/redirect-http-upgrade.sub.html | 17 + .../redirect-https-downgrade.sub.html | 17 + .../fetch/metadata/report.https.sub.html | 33 + .../report.https.sub.html.sub.headers | 3 + .../resources/appcache-iframe.sub.html | 15 + .../metadata/resources/dedicatedWorker.js | 1 + .../fetch/metadata/resources/echo-as-json.py | 29 + .../metadata/resources/echo-as-script.py | 14 + .../fetch/metadata/resources/es-module.sub.js | 1 + .../fetch-via-serviceworker--fallback--sw.js | 3 + ...etch-via-serviceworker--respondWith--sw.js | 3 + .../fetch-via-serviceworker-frame.html | 3 + .../fetch/metadata/resources/header-link.py | 15 + .../tests/fetch/metadata/resources/helper.js | 42 + .../fetch/metadata/resources/helper.sub.js | 67 + .../metadata/resources/message-opener.html | 17 + .../fetch/metadata/resources/post-to-owner.py | 36 + .../fetch/metadata/resources/record-header.py | 145 + .../metadata/resources/record-headers.py | 73 + .../resources/redirectTestHelper.sub.js | 167 + .../serviceworker-accessors-frame.html | 3 + .../resources/serviceworker-accessors.sw.js | 14 + .../fetch/metadata/resources/sharedWorker.js | 9 + .../resources/unload-with-beacon.html | 12 + .../metadata/resources/xslt-test.sub.xml | 12 + .../serviceworker-accessors.https.sub.html | 51 + .../metadata/sharedworker.https.sub.html | 40 + .../tests/fetch/metadata/style.https.sub.html | 86 + test/wpt/tests/fetch/metadata/tools/README.md | 126 + .../metadata/tools/fetch-metadata.conf.yml | 806 +++ .../tests/fetch/metadata/tools/generate.py | 195 + .../appcache-manifest.sub.https.html | 63 + .../templates/audioworklet.https.sub.html | 53 + .../tools/templates/css-font-face.sub.html | 60 + .../tools/templates/css-images.sub.html | 137 + .../tools/templates/element-a.sub.html | 72 + .../tools/templates/element-area.sub.html | 72 + .../tools/templates/element-audio.sub.html | 51 + .../tools/templates/element-embed.sub.html | 54 + .../tools/templates/element-frame.sub.html | 62 + .../tools/templates/element-iframe.sub.html | 62 + .../element-img-environment-change.sub.html | 78 + .../tools/templates/element-img.sub.html | 52 + .../templates/element-input-image.sub.html | 48 + .../templates/element-link-icon.sub.html | 75 + .../element-link-prefetch.optional.sub.html | 71 + .../element-meta-refresh.optional.sub.html | 60 + .../tools/templates/element-picture.sub.html | 101 + .../tools/templates/element-script.sub.html | 54 + .../templates/element-video-poster.sub.html | 62 + .../tools/templates/element-video.sub.html | 51 + .../fetch-via-serviceworker.https.sub.html | 88 + .../metadata/tools/templates/fetch.sub.html | 42 + .../tools/templates/form-submission.sub.html | 87 + .../tools/templates/header-link.sub.html | 56 + .../header-refresh.optional.sub.html | 59 + .../script-module-import-dynamic.sub.html | 35 + .../script-module-import-static.sub.html | 53 + .../templates/serviceworker.https.sub.html | 72 + .../tools/templates/svg-image.sub.html | 75 + .../tools/templates/window-history.sub.html | 134 + .../tools/templates/window-location.sub.html | 128 + .../worker-dedicated-constructor.sub.html | 49 + .../worker-dedicated-importscripts.sub.html | 54 + .../tests/fetch/metadata/track.https.sub.html | 119 + .../metadata/trailing-dot.https.sub.any.js | 30 + .../fetch/metadata/unload.https.sub.html | 64 + .../fetch/metadata/window-open.https.sub.html | 199 + .../fetch/metadata/worker.https.sub.html | 24 + .../tests/fetch/metadata/xslt.https.sub.html | 25 + test/wpt/tests/fetch/nosniff/image.html | 39 + .../tests/fetch/nosniff/importscripts.html | 14 + test/wpt/tests/fetch/nosniff/importscripts.js | 28 + .../fetch/nosniff/parsing-nosniff.window.js | 27 + test/wpt/tests/fetch/nosniff/resources/css.py | 23 + .../tests/fetch/nosniff/resources/image.py | 24 + test/wpt/tests/fetch/nosniff/resources/js.py | 17 + .../tests/fetch/nosniff/resources/nosniff.py | 11 + .../tests/fetch/nosniff/resources/worker.py | 16 + .../resources/x-content-type-options.json | 62 + test/wpt/tests/fetch/nosniff/script.html | 43 + test/wpt/tests/fetch/nosniff/stylesheet.html | 60 + test/wpt/tests/fetch/nosniff/worker.html | 28 + test/wpt/tests/fetch/orb/resources/data.json | 3 + test/wpt/tests/fetch/orb/resources/font.ttf | Bin 0 -> 2528 bytes test/wpt/tests/fetch/orb/resources/image.png | Bin 0 -> 1010 bytes .../tests/fetch/orb/resources/js-unlabeled.js | 1 + .../orb/resources/png-mislabeled-as-html.png | Bin 0 -> 1010 bytes .../png-mislabeled-as-html.png.headers | 1 + .../fetch/orb/resources/png-unlabeled.png | Bin 0 -> 1010 bytes test/wpt/tests/fetch/orb/resources/script.js | 4 + test/wpt/tests/fetch/orb/resources/sound.mp3 | Bin 0 -> 539 bytes test/wpt/tests/fetch/orb/resources/text.txt | 1 + test/wpt/tests/fetch/orb/resources/utils.js | 18 + .../compressed-image-sniffing.sub.html | 20 + .../orb/tentative/content-range.sub.any.js | 31 + ...img-mime-types-coverage.tentative.sub.html | 126 + .../img-png-mislabeled-as-html.sub-ref.html | 5 + .../img-png-mislabeled-as-html.sub.html | 7 + .../tentative/img-png-unlabeled.sub-ref.html | 5 + .../orb/tentative/img-png-unlabeled.sub.html | 7 + .../orb/tentative/known-mime-type.sub.any.js | 41 + .../fetch/orb/tentative/nosniff.sub.any.js | 59 + .../script-js-unlabeled-gziped.sub.html | 24 + .../orb/tentative/script-unlabeled.sub.html | 24 + .../fetch/orb/tentative/status.sub.any.js | 33 + .../tests/fetch/orb/tentative/status.sub.html | 17 + .../tentative/unknown-mime-type.sub.any.js | 28 + .../wpt/tests/fetch/origin/assorted.window.js | 211 + .../origin/resources/redirect-and-stash.py | 32 + .../fetch/origin/resources/referrer-policy.py | 7 + .../fetch/private-network-access/META.yml | 7 + .../fetch/private-network-access/README.md | 10 + ...fetch-from-treat-as-public.https.window.js | 68 + .../fetch.https.window.js | 271 + .../private-network-access/fetch.window.js | 183 + .../iframe.tentative.https.window.js | 229 + .../iframe.tentative.window.js | 110 + ...ed-content-fetch.tentative.https.window.js | 277 + .../nested-worker.https.window.js | 36 + .../nested-worker.window.js | 36 + .../preflight-cache.https.window.js | 88 + .../redirect.https.window.js | 232 + .../resources/executor.html | 9 + .../resources/fetcher.html | 21 + .../resources/fetcher.js | 20 + .../resources/iframed.html | 7 + .../resources/iframer.html | 9 + .../resources/preflight.py | 169 + .../resources/service-worker-bridge.html | 155 + .../resources/service-worker.js | 18 + .../resources/shared-fetcher.js | 23 + .../resources/shared-worker-fetcher.html | 19 + .../resources/socket-opener.html | 15 + .../resources/support.sub.js | 673 +++ .../resources/worker-blob-fetcher.html | 45 + .../resources/worker-fetcher.html | 18 + .../resources/worker-fetcher.js | 11 + .../resources/xhr-sender.html | 33 + ...ce-worker-background-fetch.https.window.js | 142 + .../service-worker-fetch.https.window.js | 217 + .../service-worker-update.https.window.js | 121 + .../service-worker.https.window.js | 107 + .../shared-worker-fetch.https.window.js | 152 + .../shared-worker-fetch.window.js | 142 + .../shared-worker.https.window.js | 58 + .../shared-worker.window.js | 34 + .../websocket.https.window.js | 40 + .../websocket.window.js | 40 + .../worker-blob-fetch.window.js | 143 + .../worker-fetch.https.window.js | 151 + .../worker-fetch.window.js | 142 + .../worker.https.window.js | 61 + .../private-network-access/worker.window.js | 37 + .../xhr-from-treat-as-public.https.window.js | 74 + .../xhr.https.window.js | 142 + .../private-network-access/xhr.window.js | 183 + test/wpt/tests/fetch/range/blob.any.js | 197 + test/wpt/tests/fetch/range/data.any.js | 29 + test/wpt/tests/fetch/range/general.any.js | 140 + test/wpt/tests/fetch/range/general.window.js | 29 + .../range/non-matching-range-response.html | 34 + .../tests/fetch/range/resources/basic.html | 1 + .../tests/fetch/range/resources/long-wav.py | 134 + .../fetch/range/resources/partial-script.py | 29 + .../fetch/range/resources/partial-text.py | 53 + .../tests/fetch/range/resources/range-sw.js | 218 + .../tests/fetch/range/resources/stash-take.py | 7 + test/wpt/tests/fetch/range/resources/utils.js | 36 + .../fetch/range/resources/video-with-range.py | 43 + test/wpt/tests/fetch/range/sw.https.window.js | 228 + .../302-found-post-handler.py | 15 + .../redirect-navigate/302-found-post.html | 20 + .../redirect-navigate/preserve-fragment.html | 202 + .../resources/destination.html | 28 + test/wpt/tests/fetch/redirects/data.window.js | 25 + .../redirects/subresource-fragments.html | 39 + .../tests/fetch/security/1xx-response.any.js | 28 + ...kup-mitigation-data-url.tentative.sub.html | 229 + .../dangling-markup-mitigation.tentative.html | 147 + .../embedded-credentials.tentative.sub.html | 89 + ...edirect-to-url-with-credentials.https.html | 68 + .../embedded-credential-window.sub.html | 19 + .../fetch-sw.https.html | 65 + .../fetch/stale-while-revalidate/fetch.any.js | 32 + .../resources/stale-css.py | 28 + .../resources/stale-image.py | 40 + .../resources/stale-script.py | 32 + .../revalidate-not-blocked-by-csp.html | 69 + .../stale-while-revalidate/stale-css.html | 51 + .../stale-while-revalidate/stale-image.html | 55 + .../stale-while-revalidate/stale-script.html | 59 + .../stale-while-revalidate/sw-intercept.js | 14 + .../interfaces/ANGLE_instanced_arrays.idl | 12 + test/wpt/tests/interfaces/CSP.idl | 56 + test/wpt/tests/interfaces/DOM-Parsing.idl | 26 + .../wpt/tests/interfaces/EXT_blend_minmax.idl | 10 + .../interfaces/EXT_clip_cull_distance.idl | 20 + .../interfaces/EXT_color_buffer_float.idl | 8 + .../EXT_color_buffer_half_float.idl | 12 + .../interfaces/EXT_disjoint_timer_query.idl | 30 + .../EXT_disjoint_timer_query_webgl2.idl | 14 + test/wpt/tests/interfaces/EXT_float_blend.idl | 8 + test/wpt/tests/interfaces/EXT_frag_depth.idl | 8 + test/wpt/tests/interfaces/EXT_sRGB.idl | 12 + .../interfaces/EXT_shader_texture_lod.idl | 8 + .../EXT_texture_compression_bptc.idl | 12 + .../EXT_texture_compression_rgtc.idl | 12 + .../EXT_texture_filter_anisotropic.idl | 10 + .../tests/interfaces/EXT_texture_norm16.idl | 16 + test/wpt/tests/interfaces/FedCM.idl | 81 + test/wpt/tests/interfaces/IndexedDB.idl | 226 + .../KHR_parallel_shader_compile.idl | 9 + test/wpt/tests/interfaces/META.yml | 2 + .../interfaces/OES_draw_buffers_indexed.idl | 26 + .../interfaces/OES_element_index_uint.idl | 8 + .../interfaces/OES_fbo_render_mipmap.idl | 8 + .../interfaces/OES_standard_derivatives.idl | 9 + .../tests/interfaces/OES_texture_float.idl | 7 + .../interfaces/OES_texture_float_linear.idl | 7 + .../interfaces/OES_texture_half_float.idl | 9 + .../OES_texture_half_float_linear.idl | 7 + .../interfaces/OES_vertex_array_object.idl | 18 + test/wpt/tests/interfaces/OVR_multiview2.idl | 14 + test/wpt/tests/interfaces/README.md | 3 + test/wpt/tests/interfaces/SVG.idl | 693 +++ ...WEBGL_blend_equation_advanced_coherent.idl | 23 + .../interfaces/WEBGL_color_buffer_float.idl | 11 + .../WEBGL_compressed_texture_astc.idl | 41 + .../WEBGL_compressed_texture_etc.idl | 19 + .../WEBGL_compressed_texture_etc1.idl | 10 + .../WEBGL_compressed_texture_pvrtc.idl | 13 + .../WEBGL_compressed_texture_s3tc.idl | 13 + .../WEBGL_compressed_texture_s3tc_srgb.idl | 13 + .../interfaces/WEBGL_debug_renderer_info.idl | 12 + .../tests/interfaces/WEBGL_debug_shaders.idl | 11 + .../tests/interfaces/WEBGL_depth_texture.idl | 9 + .../tests/interfaces/WEBGL_draw_buffers.idl | 46 + ...aw_instanced_base_vertex_base_instance.idl | 14 + .../tests/interfaces/WEBGL_lose_context.idl | 10 + .../wpt/tests/interfaces/WEBGL_multi_draw.idl | 32 + ...aw_instanced_base_vertex_base_instance.idl | 26 + test/wpt/tests/interfaces/WebCryptoAPI.idl | 237 + test/wpt/tests/interfaces/accelerometer.idl | 40 + test/wpt/tests/interfaces/ambient-light.idl | 14 + test/wpt/tests/interfaces/anchors.idl | 35 + .../interfaces/attribution-reporting-api.idl | 12 + test/wpt/tests/interfaces/audio-output.idl | 17 + .../tests/interfaces/autoplay-detection.idl | 19 + .../wpt/tests/interfaces/background-fetch.idl | 89 + test/wpt/tests/interfaces/background-sync.idl | 30 + test/wpt/tests/interfaces/badging.idl | 19 + test/wpt/tests/interfaces/battery-status.idl | 21 + test/wpt/tests/interfaces/beacon.idl | 8 + .../interfaces/capture-handle-identity.idl | 27 + test/wpt/tests/interfaces/clipboard-apis.idl | 51 + test/wpt/tests/interfaces/close-watcher.idl | 19 + test/wpt/tests/interfaces/compat.idl | 13 + test/wpt/tests/interfaces/compression.idl | 16 + .../wpt/tests/interfaces/compute-pressure.idl | 40 + test/wpt/tests/interfaces/console.idl | 34 + test/wpt/tests/interfaces/contact-picker.idl | 44 + test/wpt/tests/interfaces/content-index.idl | 46 + test/wpt/tests/interfaces/cookie-store.idl | 110 + .../interfaces/credential-management.idl | 105 + .../interfaces/csp-embedded-enforcement.idl | 8 + test/wpt/tests/interfaces/csp-next.idl | 21 + .../interfaces/css-animation-worklet.idl | 37 + .../wpt/tests/interfaces/css-animations-2.idl | 9 + test/wpt/tests/interfaces/css-animations.idl | 47 + test/wpt/tests/interfaces/css-cascade.idl | 18 + test/wpt/tests/interfaces/css-color-5.idl | 12 + test/wpt/tests/interfaces/css-conditional.idl | 27 + test/wpt/tests/interfaces/css-contain-3.idl | 10 + test/wpt/tests/interfaces/css-contain.idl | 13 + .../tests/interfaces/css-counter-styles.idl | 23 + .../wpt/tests/interfaces/css-font-loading.idl | 134 + test/wpt/tests/interfaces/css-fonts.idl | 36 + .../tests/interfaces/css-highlight-api.idl | 27 + test/wpt/tests/interfaces/css-images-4.idl | 8 + test/wpt/tests/interfaces/css-layout-api.idl | 144 + test/wpt/tests/interfaces/css-masking.idl | 20 + test/wpt/tests/interfaces/css-nav.idl | 48 + test/wpt/tests/interfaces/css-nesting.idl | 10 + test/wpt/tests/interfaces/css-paint-api.idl | 39 + test/wpt/tests/interfaces/css-parser-api.idl | 76 + .../interfaces/css-properties-values-api.idl | 23 + test/wpt/tests/interfaces/css-pseudo.idl | 16 + test/wpt/tests/interfaces/css-regions.idl | 29 + .../wpt/tests/interfaces/css-shadow-parts.idl | 8 + .../tests/interfaces/css-toggle.tentative.idl | 51 + .../tests/interfaces/css-transitions-2.idl | 9 + test/wpt/tests/interfaces/css-transitions.idl | 25 + test/wpt/tests/interfaces/css-typed-om.idl | 425 ++ .../tests/interfaces/css-view-transitions.idl | 18 + test/wpt/tests/interfaces/cssom-view.idl | 200 + test/wpt/tests/interfaces/cssom.idl | 167 + .../interfaces/custom-state-pseudo-class.idl | 14 + test/wpt/tests/interfaces/datacue.idl | 12 + .../interfaces/deprecation-reporting.idl | 15 + test/wpt/tests/interfaces/device-memory.idl | 14 + test/wpt/tests/interfaces/device-posture.idl | 21 + test/wpt/tests/interfaces/digital-goods.idl | 44 + test/wpt/tests/interfaces/edit-context.idl | 113 + test/wpt/tests/interfaces/element-timing.idl | 22 + test/wpt/tests/interfaces/encoding.idl | 59 + test/wpt/tests/interfaces/encrypted-media.idl | 125 + test/wpt/tests/interfaces/entries-api.idl | 71 + test/wpt/tests/interfaces/event-timing.idl | 29 + test/wpt/tests/interfaces/eyedropper-api.idl | 18 + test/wpt/tests/interfaces/fetch.idl | 2 + test/wpt/tests/interfaces/fido.idl | 47 + .../tests/interfaces/file-system-access.idl | 72 + test/wpt/tests/interfaces/filter-effects.idl | 341 ++ .../wpt/tests/interfaces/font-metrics-api.idl | 42 + test/wpt/tests/interfaces/fs.idl | 97 + test/wpt/tests/interfaces/fullscreen.idl | 35 + .../tests/interfaces/gamepad-extensions.idl | 71 + test/wpt/tests/interfaces/gamepad.idl | 49 + test/wpt/tests/interfaces/generic-sensor.idl | 60 + .../tests/interfaces/geolocation-sensor.idl | 47 + test/wpt/tests/interfaces/geolocation.idl | 65 + test/wpt/tests/interfaces/geometry.idl | 290 + .../interfaces/get-installed-related-apps.idl | 16 + test/wpt/tests/interfaces/gyroscope.idl | 24 + test/wpt/tests/interfaces/hr-time.idl | 19 + .../tests/interfaces/html-media-capture.idl | 8 + test/wpt/tests/interfaces/html.idl | 46 +- test/wpt/tests/interfaces/idle-detection.idl | 31 + test/wpt/tests/interfaces/image-capture.idl | 160 + test/wpt/tests/interfaces/image-resource.idl | 11 + test/wpt/tests/interfaces/ink-enhancement.idl | 32 + .../interfaces/input-device-capabilities.idl | 24 + test/wpt/tests/interfaces/input-events.idl | 14 + .../interfaces/intersection-observer.idl | 46 + .../interfaces/intervention-reporting.idl | 14 + .../wpt/tests/interfaces/is-input-pending.idl | 16 + .../tests/interfaces/js-self-profiling.idl | 44 + test/wpt/tests/interfaces/keyboard-lock.idl | 14 + test/wpt/tests/interfaces/keyboard-map.idl | 15 + .../interfaces/largest-contentful-paint.idl | 15 + .../tests/interfaces/layout-instability.idl | 20 + .../tests/interfaces/local-font-access.idl | 24 + test/wpt/tests/interfaces/longtasks.idl | 19 + test/wpt/tests/interfaces/magnetometer.idl | 46 + .../tests/interfaces/manifest-incubations.idl | 24 + test/wpt/tests/interfaces/mathml-core.idl | 9 + .../tests/interfaces/media-capabilities.idl | 115 + .../interfaces/media-playback-quality.idl | 18 + test/wpt/tests/interfaces/media-source.idl | 91 + .../interfaces/mediacapture-automation.idl | 36 + .../interfaces/mediacapture-fromelement.idl | 17 + .../mediacapture-handle-actions.idl | 31 + .../tests/interfaces/mediacapture-region.idl | 15 + .../tests/interfaces/mediacapture-streams.idl | 248 + .../interfaces/mediacapture-transform.idl | 23 + .../interfaces/mediacapture-viewport.idl | 14 + test/wpt/tests/interfaces/mediasession.idl | 84 + .../interfaces/mediastream-recording.idl | 60 + test/wpt/tests/interfaces/model-element.idl | 9 + .../wpt/tests/interfaces/mst-content-hint.idl | 18 + test/wpt/tests/interfaces/navigation-api.idl | 158 + .../tests/interfaces/navigation-timing.idl | 71 + test/wpt/tests/interfaces/netinfo.idl | 43 + test/wpt/tests/interfaces/notifications.idl | 101 + .../tests/interfaces/orientation-event.idl | 82 + .../tests/interfaces/orientation-sensor.idl | 35 + test/wpt/tests/interfaces/page-lifecycle.idl | 19 + test/wpt/tests/interfaces/paint-timing.idl | 7 + .../tests/interfaces/parakeet.tentative.idl | 32 + test/wpt/tests/interfaces/payment-handler.idl | 131 + test/wpt/tests/interfaces/payment-request.idl | 112 + .../interfaces/performance-measure-memory.idl | 30 + .../tests/interfaces/performance-timeline.idl | 49 + .../interfaces/periodic-background-sync.idl | 34 + .../tests/interfaces/permissions-policy.idl | 29 + .../tests/interfaces/permissions-request.idl | 8 + .../tests/interfaces/permissions-revoke.idl | 8 + test/wpt/tests/interfaces/permissions.idl | 41 + .../tests/interfaces/picture-in-picture.idl | 41 + test/wpt/tests/interfaces/pointerevents.idl | 64 + test/wpt/tests/interfaces/pointerlock.idl | 28 + test/wpt/tests/interfaces/portals.idl | 50 + .../tests/interfaces/prefer-current-tab.idl | 8 + .../interfaces/prerendering-revamped.idl | 15 + .../wpt/tests/interfaces/presentation-api.idl | 95 + test/wpt/tests/interfaces/priority-hints.idl | 20 + .../interfaces/private-click-measurement.idl | 8 + test/wpt/tests/interfaces/proximity.idl | 18 + test/wpt/tests/interfaces/push-api.idl | 93 + .../tests/interfaces/raw-camera-access.idl | 18 + test/wpt/tests/interfaces/remote-playback.idl | 32 + test/wpt/tests/interfaces/reporting.idl | 39 + .../tests/interfaces/requestidlecallback.idl | 20 + test/wpt/tests/interfaces/resize-observer.idl | 37 + test/wpt/tests/interfaces/resource-timing.idl | 39 + test/wpt/tests/interfaces/sanitizer-api.idl | 38 + .../interfaces/sanitizer-api.tentative.idl | 17 + test/wpt/tests/interfaces/savedata.idl | 10 + test/wpt/tests/interfaces/scheduling-apis.idl | 57 + test/wpt/tests/interfaces/screen-capture.idl | 85 + .../tests/interfaces/screen-orientation.idl | 35 + .../wpt/tests/interfaces/screen-wake-lock.idl | 24 + .../tests/interfaces/scroll-animations.idl | 41 + .../interfaces/scroll-to-text-fragment.idl | 12 + .../secure-payment-confirmation.idl | 59 + test/wpt/tests/interfaces/selection-api.idl | 45 + test/wpt/tests/interfaces/serial.idl | 85 + test/wpt/tests/interfaces/server-timing.idl | 17 + test/wpt/tests/interfaces/service-workers.idl | 240 + .../tests/interfaces/shape-detection-api.idl | 69 + test/wpt/tests/interfaces/speech-api.idl | 202 + test/wpt/tests/interfaces/storage-access.idl | 9 + .../interfaces/storage-buckets.tentative.idl | 36 + test/wpt/tests/interfaces/storage.idl | 25 + test/wpt/tests/interfaces/streams.idl | 222 + .../tests/interfaces/sub-apps.tentative.idl | 17 + test/wpt/tests/interfaces/svg-animations.idl | 68 + test/wpt/tests/interfaces/testutils.idl | 9 + .../tests/interfaces/text-detection-api.idl | 18 + test/wpt/tests/interfaces/touch-events.idl | 79 + test/wpt/tests/interfaces/trusted-types.idl | 71 + test/wpt/tests/interfaces/ua-client-hints.idl | 44 + test/wpt/tests/interfaces/uievents.idl | 248 + test/wpt/tests/interfaces/urlpattern.idl | 59 + test/wpt/tests/interfaces/user-timing.idl | 34 + test/wpt/tests/interfaces/vibration.idl | 10 + test/wpt/tests/interfaces/video-rvfc.idl | 27 + .../wpt/tests/interfaces/virtual-keyboard.idl | 21 + .../interfaces/virtual-keyboard.tentative.idl | 15 + test/wpt/tests/interfaces/wai-aria.idl | 59 + test/wpt/tests/interfaces/wasm-js-api.idl | 110 + test/wpt/tests/interfaces/wasm-web-api.idl | 11 + .../wpt/tests/interfaces/web-animations-2.idl | 101 + test/wpt/tests/interfaces/web-animations.idl | 150 + test/wpt/tests/interfaces/web-app-launch.idl | 19 + test/wpt/tests/interfaces/web-bluetooth.idl | 251 + test/wpt/tests/interfaces/web-locks.idl | 50 + test/wpt/tests/interfaces/web-nfc.idl | 81 + test/wpt/tests/interfaces/web-otp.idl | 21 + test/wpt/tests/interfaces/web-share.idl | 16 + test/wpt/tests/interfaces/webaudio.idl | 674 +++ test/wpt/tests/interfaces/webauthn.idl | 349 ++ .../webcodecs-aac-codec-registration.idl | 17 + .../webcodecs-avc-codec-registration.idl | 17 + .../webcodecs-flac-codec-registration.idl | 13 + .../webcodecs-hevc-codec-registration.idl | 17 + .../webcodecs-opus-codec-registration.idl | 22 + test/wpt/tests/interfaces/webcodecs.idl | 494 ++ .../interfaces/webcrypto-secure-curves.idl | 8 + test/wpt/tests/interfaces/webdriver.idl | 9 + test/wpt/tests/interfaces/webgl1.idl | 745 +++ test/wpt/tests/interfaces/webgl2.idl | 582 ++ test/wpt/tests/interfaces/webgpu.idl | 1265 +++++ test/wpt/tests/interfaces/webhid.idl | 127 + test/wpt/tests/interfaces/webidl.idl | 50 + test/wpt/tests/interfaces/webmidi.idl | 91 + test/wpt/tests/interfaces/webnn.idl | 542 ++ .../interfaces/webrtc-encoded-transform.idl | 126 + test/wpt/tests/interfaces/webrtc-ice.idl | 24 + test/wpt/tests/interfaces/webrtc-identity.idl | 97 + test/wpt/tests/interfaces/webrtc-priority.idl | 24 + test/wpt/tests/interfaces/webrtc-stats.idl | 288 + test/wpt/tests/interfaces/webrtc-svc.idl | 8 + test/wpt/tests/interfaces/webrtc.idl | 628 +++ test/wpt/tests/interfaces/webtransport.idl | 142 + test/wpt/tests/interfaces/webusb.idl | 249 + test/wpt/tests/interfaces/webvr.tentative.idl | 204 + test/wpt/tests/interfaces/webvtt.idl | 40 + test/wpt/tests/interfaces/webxr-ar-module.idl | 29 + .../tests/interfaces/webxr-depth-sensing.idl | 57 + .../tests/interfaces/webxr-dom-overlays.idl | 31 + .../interfaces/webxr-gamepads-module.idl | 8 + .../wpt/tests/interfaces/webxr-hand-input.idl | 66 + test/wpt/tests/interfaces/webxr-hit-test.idl | 69 + .../interfaces/webxr-lighting-estimation.idl | 39 + test/wpt/tests/interfaces/webxr.idl | 294 + test/wpt/tests/interfaces/webxrlayers.idl | 212 + .../interfaces/window-controls-overlay.idl | 28 + .../wpt/tests/interfaces/window-placement.idl | 42 + test/wpt/tests/interfaces/xhr.idl | 99 + test/wpt/tests/mimesniff/META.yml | 3 + test/wpt/tests/mimesniff/README.md | 4 + .../mimesniff/media/media-sniff.window.js | 32 + .../tests/mimesniff/media/resources/flac.flac | Bin 0 -> 8493 bytes .../mimesniff/media/resources/make-vectors.sh | 10 + .../mimesniff/media/resources/mp3-raw.mp3 | Bin 0 -> 417 bytes .../media/resources/mp3-with-id3.mp3 | Bin 0 -> 644 bytes .../tests/mimesniff/media/resources/mp4.mp4 | Bin 0 -> 1231 bytes .../tests/mimesniff/media/resources/ogg.ogg | Bin 0 -> 3594 bytes .../tests/mimesniff/media/resources/wav.wav | Bin 0 -> 486 bytes .../tests/mimesniff/media/resources/webm.webm | Bin 0 -> 877 bytes test/wpt/tests/mimesniff/mime-types/README.md | 47 + .../mime-types/charset-parameter.window.js | 61 + .../resources/generated-mime-types.py | 48 + .../mime-types/resources/mime-charset.py | 19 + .../mime-types/resources/mime-groups.json | 159 + test/wpt/tests/resources/.htaccess | 2 + test/wpt/tests/resources/META.yml | 2 + .../SVGAnimationTestCase-testharness.js | 102 + test/wpt/tests/resources/accesskey.js | 34 + test/wpt/tests/resources/blank.html | 16 + test/wpt/tests/resources/channel.sub.js | 1097 ++++ test/wpt/tests/resources/check-layout-th.js | 252 + test/wpt/tests/resources/check-layout.js | 245 + test/wpt/tests/resources/chromium/README.md | 7 + .../chromium/contacts_manager_mock.js | 90 + .../chromium/content-index-helpers.js | 9 + .../chromium/enable-hyperlink-auditing.js | 2 + test/wpt/tests/resources/chromium/fake-hid.js | 297 + .../tests/resources/chromium/fake-serial.js | 443 ++ .../chromium/generic_sensor_mocks.js | 519 ++ .../chromium/generic_sensor_mocks.js.headers | 1 + .../chromium/mock-barcodedetection.js | 136 + .../chromium/mock-barcodedetection.js.headers | 1 + .../resources/chromium/mock-direct-sockets.js | 75 + .../resources/chromium/mock-facedetection.js | 130 + .../chromium/mock-facedetection.js.headers | 1 + .../resources/chromium/mock-idle-detection.js | 80 + .../resources/chromium/mock-imagecapture.js | 309 ++ .../resources/chromium/mock-managed-config.js | 91 + .../chromium/mock-pressure-service.js | 141 + .../chromium/mock-pressure-service.js.headers | 1 + .../tests/resources/chromium/mock-subapps.js | 86 + .../resources/chromium/mock-textdetection.js | 92 + .../chromium/mock-textdetection.js.headers | 1 + test/wpt/tests/resources/chromium/nfc-mock.js | 437 ++ .../resources/chromium/web-bluetooth-test.js | 629 +++ .../chromium/web-bluetooth-test.js.headers | 1 + .../resources/chromium/webusb-child-test.js | 40 + .../chromium/webusb-child-test.js.headers | 1 + .../tests/resources/chromium/webusb-test.js | 582 ++ .../resources/chromium/webusb-test.js.headers | 1 + .../chromium/webxr-test-math-helper.js | 298 + .../webxr-test-math-helper.js.headers | 1 + .../tests/resources/chromium/webxr-test.js | 2126 +++++++ .../resources/chromium/webxr-test.js.headers | 1 + .../declarative-shadow-dom-polyfill.js | 16 + .../tests/resources/idlharness-shadowrealm.js | 61 + test/wpt/tests/resources/idlharness.js | 223 +- .../wpt/tests/resources/idlharness.js.headers | 2 + test/wpt/tests/resources/readme.md | 14 + test/wpt/tests/resources/sriharness.js | 226 + test/wpt/tests/resources/test-only-api.js | 31 + .../tests/resources/test-only-api.js.headers | 2 + test/wpt/tests/resources/test-only-api.m.js | 5 + .../resources/test-only-api.m.js.headers | 2 + test/wpt/tests/resources/test/README.md | 83 + test/wpt/tests/resources/test/conftest.py | 269 + test/wpt/tests/resources/test/harness.html | 26 + test/wpt/tests/resources/test/idl-helper.js | 24 + .../resources/test/nested-testharness.js | 80 + .../wpt/tests/resources/test/requirements.txt | 1 + .../test/tests/functional/abortsignal.html | 49 + .../test/tests/functional/add_cleanup.html | 91 + .../tests/functional/add_cleanup_async.html | 85 + .../add_cleanup_async_bad_return.html | 50 + .../add_cleanup_async_rejection.html | 94 + ...dd_cleanup_async_rejection_after_load.html | 52 + .../functional/add_cleanup_async_timeout.html | 57 + .../functional/add_cleanup_bad_return.html | 61 + .../tests/functional/add_cleanup_count.html | 39 + .../tests/functional/add_cleanup_err.html | 45 + .../functional/add_cleanup_err_multi.html | 52 + .../functional/add_cleanup_sync_queue.html | 55 + .../test/tests/functional/api-tests-1.html | 991 ++++ .../test/tests/functional/api-tests-2.html | 62 + .../test/tests/functional/api-tests-3.html | 34 + .../tests/functional/assert-array-equals.html | 162 + .../test/tests/functional/force_timeout.html | 60 + .../tests/functional/generate-callback.html | 153 + .../test_partial_interface_of.html | 90 + .../IdlInterface/test_exposed_wildcard.html | 233 + .../test_immutable_prototype.html | 298 + .../IdlInterface/test_interface_mixin.html | 131 + .../test_partial_interface_of.html | 188 + .../test_primary_interface_of.html | 116 + .../IdlInterface/test_to_json_operation.html | 177 + .../IdlNamespace/test_attribute.html | 100 + .../IdlNamespace/test_operation.html | 242 + .../IdlNamespace/test_partial_namespace.html | 125 + .../tests/functional/iframe-callback.html | 116 + .../functional/iframe-consolidate-errors.html | 50 + .../functional/iframe-consolidate-tests.html | 85 + .../test/tests/functional/iframe-msg.html | 84 + .../test/tests/functional/log-insertion.html | 46 + .../test/tests/functional/no-title.html | 146 + .../test/tests/functional/order.html | 36 + .../test/tests/functional/promise-async.html | 172 + .../tests/functional/promise-with-sync.html | 79 + .../test/tests/functional/promise.html | 219 + .../test/tests/functional/queue.html | 130 + .../tests/functional/setup-function-worker.js | 14 + .../functional/setup-worker-service.html | 86 + .../functional/single-page-test-fail.html | 28 + .../single-page-test-no-assertions.html | 25 + .../functional/single-page-test-no-body.html | 26 + .../functional/single-page-test-pass.html | 28 + .../test/tests/functional/step_wait.html | 57 + .../test/tests/functional/step_wait_func.html | 49 + .../task-scheduling-promise-test.html | 241 + .../functional/task-scheduling-test.html | 141 + .../functional/uncaught-exception-handle.html | 33 + .../functional/uncaught-exception-ignore.html | 35 + .../worker-dedicated-uncaught-allow.html | 45 + .../worker-dedicated-uncaught-single.html | 56 + .../functional/worker-dedicated.sub.html | 88 + .../test/tests/functional/worker-error.js | 8 + .../test/tests/functional/worker-service.html | 115 + .../test/tests/functional/worker-shared.html | 73 + .../tests/functional/worker-uncaught-allow.js | 19 + .../functional/worker-uncaught-single.js | 8 + .../resources/test/tests/functional/worker.js | 34 + .../tests/unit/IdlArray/is_json_type.html | 192 + .../get_reverse_inheritance_stack.html | 47 + .../test_partial_dictionary.html | 39 + .../tests/unit/IdlInterface/constructors.html | 26 + .../default_to_json_operation.html | 114 + .../do_member_unscopable_asserts.html | 56 + .../IdlInterface/get_interface_object.html | 22 + .../get_interface_object_owner.html | 21 + .../IdlInterface/get_legacy_namespace.html | 20 + .../unit/IdlInterface/get_qualified_name.html | 20 + .../get_reverse_inheritance_stack.html | 49 + ...has_default_to_json_regular_operation.html | 47 + .../has_to_json_regular_operation.html | 31 + .../should_have_interface_object.html | 30 + .../test_primary_interface_of_undefined.html | 29 + .../is_to_json_regular_operation.html | 42 + .../unit/IdlInterfaceMember/toString.html | 36 + .../test/tests/unit/assert_implements.html | 43 + .../unit/assert_implements_optional.html | 43 + .../test/tests/unit/assert_object_equals.html | 152 + .../unit/async-test-return-restrictions.html | 135 + .../resources/test/tests/unit/basic.html | 48 + .../unit/exceptional-cases-timeouts.html | 120 + .../test/tests/unit/exceptional-cases.html | 392 ++ .../test/tests/unit/format-value.html | 123 + .../resources/test/tests/unit/helpers.js | 21 + .../resources/test/tests/unit/late-test.html | 54 + .../tests/unit/promise_setup-timeout.html | 28 + .../test/tests/unit/promise_setup.html | 333 ++ .../test/tests/unit/single_test.html | 94 + .../tests/unit/test-return-restrictions.html | 156 + .../test/tests/unit/throwing-assertions.html | 268 + .../test/tests/unit/unpaired-surrogates.html | 143 + test/wpt/tests/resources/test/tox.ini | 13 + test/wpt/tests/resources/test/wptserver.py | 58 + .../wpt/tests/resources/testdriver-actions.js | 599 ++ test/wpt/tests/resources/testdriver-vendor.js | 0 .../resources/testdriver-vendor.js.headers | 2 + test/wpt/tests/resources/testdriver.js | 776 +++ .../wpt/tests/resources/testdriver.js.headers | 2 + test/wpt/tests/resources/testharness.js | 4929 +++++++++++++++++ .../tests/resources/testharness.js.headers | 2 + test/wpt/tests/resources/testharnessreport.js | 57 + .../resources/testharnessreport.js.headers | 2 + test/wpt/tests/resources/webidl2/build.sh | 12 + .../wpt/tests/resources/webidl2/lib/README.md | 4 + .../tests/resources/webidl2/lib/VERSION.md | 1 + .../resources/webidl2/lib/webidl2.js.headers | 1 + test/wpt/tests/websockets/META.yml | 6 + test/wpt/tests/websockets/README.md | 1 + test/wpt/tests/websockets/Send-data.worker.js | 26 + test/wpt/tests/websockets/binary/001.html | 27 + test/wpt/tests/websockets/binary/002.html | 28 + test/wpt/tests/websockets/binary/004.html | 27 + test/wpt/tests/websockets/binary/005.html | 26 + .../websockets/closing-handshake/002.html | 23 + .../websockets/closing-handshake/003.html | 24 + .../websockets/closing-handshake/004.html | 25 + .../wpt/tests/websockets/constructor/001.html | 14 + .../wpt/tests/websockets/constructor/002.html | 21 + .../wpt/tests/websockets/constructor/004.html | 36 + .../wpt/tests/websockets/constructor/005.html | 14 + .../wpt/tests/websockets/constructor/006.html | 29 + .../wpt/tests/websockets/constructor/007.html | 17 + .../wpt/tests/websockets/constructor/008.html | 15 + .../wpt/tests/websockets/constructor/009.html | 24 + .../wpt/tests/websockets/constructor/010.html | 22 + .../wpt/tests/websockets/constructor/011.html | 28 + .../wpt/tests/websockets/constructor/012.html | 20 + .../wpt/tests/websockets/constructor/013.html | 42 + .../wpt/tests/websockets/constructor/014.html | 39 + .../wpt/tests/websockets/constructor/016.html | 20 + .../wpt/tests/websockets/constructor/017.html | 19 + .../wpt/tests/websockets/constructor/018.html | 20 + .../wpt/tests/websockets/constructor/019.html | 21 + .../wpt/tests/websockets/constructor/020.html | 21 + .../wpt/tests/websockets/constructor/021.html | 12 + .../wpt/tests/websockets/constructor/022.html | 23 + test/wpt/tests/websockets/cookies/001.html | 28 + test/wpt/tests/websockets/cookies/002.html | 26 + test/wpt/tests/websockets/cookies/003.html | 34 + test/wpt/tests/websockets/cookies/004.html | 31 + test/wpt/tests/websockets/cookies/005.html | 35 + test/wpt/tests/websockets/cookies/006.html | 35 + test/wpt/tests/websockets/cookies/007.html | 36 + .../websockets/cookies/support/set-cookie.py | 7 + .../support/websocket-cookies-helper.sub.js | 57 + .../third-party-cookie-accepted.https.html | 25 + .../websockets/extended-payload-length.html | 72 + .../websockets/handlers/basic_auth_wsh.py | 26 + .../handlers/delayed-passive-close_wsh.py | 27 + .../websockets/handlers/echo-cookie_wsh.py | 12 + .../websockets/handlers/echo-query_v13_wsh.py | 11 + .../websockets/handlers/echo-query_wsh.py | 9 + .../handlers/echo_close_data_wsh.py | 20 + .../websockets/handlers/echo_exit_wsh.py | 19 + .../tests/websockets/handlers/echo_raw_wsh.py | 16 + .../wpt/tests/websockets/handlers/echo_wsh.py | 36 + .../websockets/handlers/empty-message_wsh.py | 13 + .../handlers/handshake_no_extensions_wsh.py | 9 + .../handlers/handshake_no_protocol_wsh.py | 8 + .../handlers/handshake_protocol_wsh.py | 7 + .../handlers/handshake_sleep_2_wsh.py | 9 + .../tests/websockets/handlers/invalid_wsh.py | 8 + .../websockets/handlers/msg_channel_wsh.py | 234 + .../tests/websockets/handlers/origin_wsh.py | 11 + .../websockets/handlers/protocol_array_wsh.py | 14 + .../tests/websockets/handlers/protocol_wsh.py | 12 + .../handlers/receive-backpressure_wsh.py | 14 + .../tests/websockets/handlers/referrer_wsh.py | 12 + .../handlers/send-backpressure_wsh.py | 39 + .../handlers/set-cookie-secure_wsh.py | 11 + .../handlers/set-cookie_http_wsh.py | 11 + .../websockets/handlers/set-cookie_wsh.py | 11 + .../handlers/set-cookies-samesite_wsh.py | 25 + .../handlers/simple_handshake_wsh.py | 35 + .../websockets/handlers/sleep_10_v13_wsh.py | 24 + .../handlers/stash_responder_blocking_wsh.py | 45 + .../handlers/stash_responder_wsh.py | 45 + .../handlers/wrong_accept_key_wsh.py | 19 + .../interfaces/CloseEvent/clean-close.html | 24 + .../interfaces/CloseEvent/constructor.html | 35 + .../interfaces/CloseEvent/historical.html | 12 + .../bufferedAmount-arraybuffer.html | 27 + .../bufferedAmount/bufferedAmount-blob.html | 28 + .../bufferedAmount-defineProperty-getter.html | 18 + .../bufferedAmount-defineProperty-setter.html | 20 + .../bufferedAmount-deleting.html | 23 + .../bufferedAmount-getting.html | 54 + .../bufferedAmount-initial.html | 15 + .../bufferedAmount/bufferedAmount-large.html | 29 + .../bufferedAmount-readonly.html | 16 + .../bufferedAmount-unicode.html | 25 + .../WebSocket/close/close-basic.html | 26 + .../WebSocket/close/close-connecting.html | 25 + .../WebSocket/close/close-multiple.html | 29 + .../WebSocket/close/close-nested.html | 28 + .../WebSocket/close/close-replace.html | 15 + .../WebSocket/close/close-return.html | 14 + .../interfaces/WebSocket/constants/001.html | 17 + .../interfaces/WebSocket/constants/002.html | 24 + .../interfaces/WebSocket/constants/003.html | 22 + .../interfaces/WebSocket/constants/004.html | 21 + .../interfaces/WebSocket/constants/005.html | 20 + .../interfaces/WebSocket/constants/006.html | 20 + .../interfaces/WebSocket/events/001.html | 18 + .../interfaces/WebSocket/events/002.html | 20 + .../interfaces/WebSocket/events/003.html | 21 + .../interfaces/WebSocket/events/004.html | 16 + .../interfaces/WebSocket/events/006.html | 17 + .../interfaces/WebSocket/events/007.html | 22 + .../interfaces/WebSocket/events/008.html | 24 + .../interfaces/WebSocket/events/009.html | 21 + .../interfaces/WebSocket/events/010.html | 21 + .../interfaces/WebSocket/events/011.html | 18 + .../interfaces/WebSocket/events/012.html | 18 + .../interfaces/WebSocket/events/013.html | 20 + .../interfaces/WebSocket/events/014.html | 21 + .../interfaces/WebSocket/events/015.html | 36 + .../interfaces/WebSocket/events/016.html | 39 + .../interfaces/WebSocket/events/017.html | 56 + .../interfaces/WebSocket/events/018.html | 52 + .../interfaces/WebSocket/events/019.html | 31 + .../interfaces/WebSocket/events/020.html | 17 + .../interfaces/WebSocket/extensions/001.html | 14 + .../WebSocket/protocol/protocol-initial.html | 14 + .../interfaces/WebSocket/readyState/001.html | 13 + .../interfaces/WebSocket/readyState/002.html | 15 + .../interfaces/WebSocket/readyState/003.html | 18 + .../interfaces/WebSocket/readyState/004.html | 17 + .../interfaces/WebSocket/readyState/005.html | 19 + .../interfaces/WebSocket/readyState/006.html | 19 + .../interfaces/WebSocket/readyState/007.html | 19 + .../interfaces/WebSocket/readyState/008.html | 21 + .../interfaces/WebSocket/send/001.html | 15 + .../interfaces/WebSocket/send/002.html | 15 + .../interfaces/WebSocket/send/003.html | 15 + .../interfaces/WebSocket/send/004.html | 25 + .../interfaces/WebSocket/send/005.html | 19 + .../interfaces/WebSocket/send/006.html | 28 + .../interfaces/WebSocket/send/007.html | 27 + .../interfaces/WebSocket/send/008.html | 25 + .../interfaces/WebSocket/send/009.html | 27 + .../interfaces/WebSocket/send/010.html | 42 + .../interfaces/WebSocket/send/011.html | 28 + .../interfaces/WebSocket/send/012.html | 28 + .../interfaces/WebSocket/url/001.html | 13 + .../interfaces/WebSocket/url/002.html | 15 + .../interfaces/WebSocket/url/003.html | 17 + .../interfaces/WebSocket/url/004.html | 17 + .../interfaces/WebSocket/url/005.html | 17 + .../interfaces/WebSocket/url/006.html | 19 + .../interfaces/WebSocket/url/resolve.html | 14 + .../keeping-connection-open/001.html | 29 + .../multi-globals/message-received.html | 33 + .../multi-globals/support/incumbent.sub.html | 24 + .../multi-globals/support/relevant.html | 2 + .../websockets/opening-handshake/001.html | 20 + .../websockets/opening-handshake/002.html | 24 + .../003-sets-origin.worker.js | 17 + .../websockets/opening-handshake/003.html | 27 + .../websockets/opening-handshake/005.html | 25 + ...remove-own-iframe-during-onerror.window.js | 23 + test/wpt/tests/websockets/security/001.html | 16 + test/wpt/tests/websockets/security/002.html | 20 + test/wpt/tests/websockets/security/check.py | 2 + .../websockets/stream/tentative/README.md | 9 + .../websockets/stream/tentative/abort.any.js | 48 + .../tentative/backpressure-receive.any.js | 40 + .../stream/tentative/backpressure-send.any.js | 25 + .../websockets/stream/tentative/close.any.js | 186 + .../stream/tentative/constructor.any.js | 68 + .../tentative/resources/url-constants.js | 8 + .../websockets/unload-a-document/001-1.html | 25 + .../websockets/unload-a-document/001-2.html | 4 + .../websockets/unload-a-document/001.html | 26 + .../websockets/unload-a-document/002-1.html | 32 + .../websockets/unload-a-document/002-2.html | 4 + .../websockets/unload-a-document/002.html | 27 + .../websockets/unload-a-document/003.html | 14 + .../websockets/unload-a-document/004.html | 16 + .../websockets/unload-a-document/005-1.html | 22 + .../websockets/unload-a-document/005.html | 21 + test/wpt/tests/xhr/META.yml | 7 + test/wpt/tests/xhr/README.md | 7 + .../xhr/XMLHttpRequest-withCredentials.any.js | 40 + test/wpt/tests/xhr/abort-after-receive.any.js | 30 + test/wpt/tests/xhr/abort-after-send.any.js | 29 + test/wpt/tests/xhr/abort-after-stop.window.js | 22 + test/wpt/tests/xhr/abort-after-timeout.any.js | 43 + .../wpt/tests/xhr/abort-during-done.window.js | 78 + .../abort-during-headers-received.window.js | 41 + .../tests/xhr/abort-during-loading.window.js | 41 + test/wpt/tests/xhr/abort-during-open.any.js | 18 + .../xhr/abort-during-readystatechange.any.js | 19 + test/wpt/tests/xhr/abort-during-unsent.any.js | 19 + test/wpt/tests/xhr/abort-during-upload.any.js | 17 + test/wpt/tests/xhr/abort-event-abort.any.js | 32 + .../tests/xhr/abort-event-listeners.any.js | 13 + test/wpt/tests/xhr/abort-event-loadend.any.js | 30 + test/wpt/tests/xhr/abort-event-order.htm | 52 + .../tests/xhr/abort-upload-event-abort.any.js | 31 + .../xhr/abort-upload-event-loadend.any.js | 31 + ...rol-and-redirects-async-same-origin.any.js | 61 + .../access-control-and-redirects-async.any.js | 79 + .../xhr/access-control-and-redirects.any.js | 50 + ...-access-control-origin-header-data-url.htm | 43 + ...-allow-access-control-origin-header.any.js | 13 + .../access-control-basic-allow-async.any.js | 19 + ...ow-non-cors-safelisted-method-async.any.js | 17 + ...ic-allow-non-cors-safelisted-method.any.js | 14 + ...flight-cache-invalidation-by-header.any.js | 38 + ...flight-cache-invalidation-by-method.any.js | 37 + ...basic-allow-preflight-cache-timeout.any.js | 37 + ...control-basic-allow-preflight-cache.any.js | 35 + .../access-control-basic-allow-star.any.js | 12 + .../xhr/access-control-basic-allow.any.js | 12 + ...-basic-cors-safelisted-request-headers.htm | 31 + ...basic-cors-safelisted-response-headers.htm | 32 + .../tests/xhr/access-control-basic-denied.htm | 30 + ...cess-control-basic-get-fail-non-simple.htm | 26 + ...basic-non-cors-safelisted-content-type.htm | 30 + ...rol-basic-post-success-no-content-type.htm | 26 + ...-with-non-cors-safelisted-content-type.htm | 37 + .../access-control-basic-preflight-denied.htm | 31 + ...ss-control-expose-headers-on-redirect.html | 33 + ...-control-preflight-async-header-denied.htm | 39 + ...-control-preflight-async-method-denied.htm | 38 + ...-control-preflight-async-not-supported.htm | 37 + ...ess-control-preflight-credential-async.htm | 29 + ...cess-control-preflight-credential-sync.htm | 24 + ...access-control-preflight-headers-async.htm | 35 + .../access-control-preflight-headers-sync.htm | 29 + ...-request-allow-headers-returns-star.any.js | 26 + ...rol-preflight-request-header-lowercase.htm | 29 + ...light-request-header-returns-origin.any.js | 26 + ...ontrol-preflight-request-header-sorted.htm | 28 + ...ntrol-preflight-request-headers-origin.htm | 29 + ...l-preflight-request-invalid-status-301.htm | 28 + ...l-preflight-request-invalid-status-400.htm | 28 + ...l-preflight-request-invalid-status-501.htm | 28 + ...flight-request-must-not-contain-cookie.htm | 57 + ...s-control-preflight-sync-header-denied.htm | 34 + ...s-control-preflight-sync-method-denied.htm | 33 + ...s-control-preflight-sync-not-supported.htm | 33 + ...ccess-control-recursive-failed-request.htm | 38 + ...access-control-response-with-body-sync.htm | 25 + .../xhr/access-control-response-with-body.htm | 29 + ...-control-response-with-exposed-headers.htm | 38 + ...rol-sandboxed-iframe-allow-origin-null.htm | 32 + .../access-control-sandboxed-iframe-allow.htm | 32 + ...ndboxed-iframe-denied-without-wildcard.htm | 43 + ...access-control-sandboxed-iframe-denied.htm | 41 + .../xhr/allow-lists-starting-with-comma.htm | 33 + .../tests/xhr/anonymous-mode-unsupported.htm | 40 + .../close-worker-with-xhr-in-progress.html | 26 + .../tests/xhr/content-type-unmodified.any.js | 16 + test/wpt/tests/xhr/cookies.http.html | 41 + .../wpt/tests/xhr/cors-expose-star.sub.any.js | 52 + test/wpt/tests/xhr/cors-upload.any.js | 59 + test/wpt/tests/xhr/data-uri.htm | 41 + test/wpt/tests/xhr/event-abort.any.js | 15 + test/wpt/tests/xhr/event-error-order.sub.html | 35 + test/wpt/tests/xhr/event-error.sub.any.js | 28 + test/wpt/tests/xhr/event-load.any.js | 21 + test/wpt/tests/xhr/event-loadend.any.js | 19 + .../tests/xhr/event-loadstart-upload.any.js | 19 + test/wpt/tests/xhr/event-loadstart.any.js | 17 + test/wpt/tests/xhr/event-progress.any.js | 18 + .../xhr/event-readystate-sync-open.any.js | 23 + .../xhr/event-readystatechange-loaded.any.js | 23 + test/wpt/tests/xhr/event-timeout-order.any.js | 21 + test/wpt/tests/xhr/event-timeout.any.js | 18 + .../event-upload-progress-crossorigin.any.js | 26 + .../tests/xhr/event-upload-progress.any.js | 30 + .../firing-events-http-content-length.html | 32 + .../firing-events-http-no-content-length.html | 35 + test/wpt/tests/xhr/folder.txt | 1 + test/wpt/tests/xhr/formdata.html | 90 + .../xhr/formdata/append-formelement.html | 52 + .../xhr/formdata/constructor-formelement.html | 150 + .../xhr/formdata/constructor-submitter.html | 100 + .../xhr/formdata/delete-formelement.html | 41 + .../tests/xhr/formdata/get-formelement.html | 34 + .../tests/xhr/formdata/has-formelement.html | 25 + test/wpt/tests/xhr/formdata/iteration.any.js | 65 + test/wpt/tests/xhr/formdata/set-blob.any.js | 21 +- .../tests/xhr/formdata/set-formelement.html | 51 + .../xhr/getallresponseheaders-cookies.htm | 38 + .../xhr/getallresponseheaders-status.htm | 33 + test/wpt/tests/xhr/getallresponseheaders.htm | 35 + .../getresponseheader-case-insensitive.htm | 34 + .../xhr/getresponseheader-chunked-trailer.htm | 32 + .../getresponseheader-cookies-and-more.htm | 36 + .../xhr/getresponseheader-error-state.htm | 36 + .../xhr/getresponseheader-server-date.htm | 29 + .../getresponseheader-special-characters.htm | 34 + .../getresponseheader-unsent-opened-state.htm | 32 + test/wpt/tests/xhr/getresponseheader.any.js | 18 + .../wpt/tests/xhr/header-user-agent-async.htm | 26 + test/wpt/tests/xhr/header-user-agent-sync.htm | 20 + .../tests/xhr/headers-normalize-response.htm | 43 + test/wpt/tests/xhr/historical.html | 15 + test/wpt/tests/xhr/idlharness.any.js | 28 + test/wpt/tests/xhr/json.any.js | 23 + test/wpt/tests/xhr/loadstart-and-state.html | 40 + test/wpt/tests/xhr/open-after-abort.htm | 77 + .../tests/xhr/open-after-setrequestheader.htm | 33 + test/wpt/tests/xhr/open-after-stop.window.js | 43 + .../wpt/tests/xhr/open-during-abort-event.htm | 56 + .../xhr/open-during-abort-processing.htm | 62 + test/wpt/tests/xhr/open-during-abort.htm | 33 + test/wpt/tests/xhr/open-method-bogus.htm | 28 + .../xhr/open-method-case-insensitive.htm | 29 + .../tests/xhr/open-method-case-sensitive.htm | 31 + test/wpt/tests/xhr/open-method-insecure.htm | 29 + .../xhr/open-method-responsetype-set-sync.htm | 32 + test/wpt/tests/xhr/open-open-send.htm | 33 + test/wpt/tests/xhr/open-open-sync-send.htm | 31 + .../tests/xhr/open-parameters-toString.htm | 54 + test/wpt/tests/xhr/open-referer.htm | 20 + test/wpt/tests/xhr/open-send-during-abort.htm | 27 + test/wpt/tests/xhr/open-send-open.htm | 33 + test/wpt/tests/xhr/open-sync-open-send.htm | 41 + .../tests/xhr/open-url-about-blank-window.htm | 23 + .../xhr/open-url-base-inserted-after-open.htm | 24 + test/wpt/tests/xhr/open-url-base-inserted.htm | 24 + test/wpt/tests/xhr/open-url-base.htm | 22 + test/wpt/tests/xhr/open-url-encoding.htm | 26 + test/wpt/tests/xhr/open-url-fragment.htm | 38 + .../xhr/open-url-javascript-window-2.htm | 19 + .../tests/xhr/open-url-javascript-window.htm | 28 + .../wpt/tests/xhr/open-url-multi-window-2.htm | 25 + .../wpt/tests/xhr/open-url-multi-window-3.htm | 25 + .../wpt/tests/xhr/open-url-multi-window-4.htm | 50 + .../wpt/tests/xhr/open-url-multi-window-5.htm | 32 + .../wpt/tests/xhr/open-url-multi-window-6.htm | 41 + test/wpt/tests/xhr/open-url-multi-window.htm | 31 + ...pen-url-redirected-sharedworker-origin.htm | 11 + .../xhr/open-url-redirected-worker-origin.htm | 11 + test/wpt/tests/xhr/open-url-worker-origin.htm | 9 + test/wpt/tests/xhr/open-url-worker-simple.htm | 25 + .../open-user-password-non-same-origin.htm | 25 + test/wpt/tests/xhr/over-1-meg.any.js | 16 + test/wpt/tests/xhr/overridemimetype-blob.html | 57 + .../xhr/overridemimetype-done-state.any.js | 20 + .../xhr/overridemimetype-edge-cases.window.js | 50 + ...-headers-received-state-force-shiftjis.htm | 34 + .../overridemimetype-invalid-mime-type.htm | 41 + .../xhr/overridemimetype-loading-state.htm | 32 + ...verridemimetype-open-state-force-utf-8.htm | 27 + .../overridemimetype-open-state-force-xml.htm | 34 + ...imetype-unsent-state-force-shiftjis.any.js | 12 + .../xhr/preserve-ua-header-on-redirect.htm | 43 + .../progress-events-response-data-gzip.htm | 83 + .../tests/xhr/progressevent-constructor.html | 47 + .../tests/xhr/progressevent-interface.html | 49 + .../tests/xhr/request-content-length.any.js | 31 + .../tests/xhr/resources/accept-language.py | 3 + test/wpt/tests/xhr/resources/accept.py | 2 + .../resources/access-control-allow-lists.py | 26 + .../access-control-allow-with-body.py | 15 + .../resources/access-control-auth-basic.py | 17 + ...cess-control-basic-allow-no-credentials.py | 5 + .../access-control-basic-allow-star.py | 5 + .../resources/access-control-basic-allow.py | 6 + ...l-basic-cors-safelisted-request-headers.py | 16 + ...-basic-cors-safelisted-response-headers.py | 19 + .../resources/access-control-basic-denied.py | 5 + ...ess-control-basic-options-not-supported.py | 12 + ...trol-basic-preflight-cache-invalidation.py | 49 + ...s-control-basic-preflight-cache-timeout.py | 50 + .../access-control-basic-preflight-cache.py | 50 + .../access-control-basic-put-allow.py | 22 + .../xhr/resources/access-control-cookie.py | 16 + .../resources/access-control-origin-header.py | 8 + .../access-control-preflight-denied.py | 49 + ...ight-request-allow-headers-returns-star.py | 12 + ...trol-preflight-request-header-lowercase.py | 16 + ...preflight-request-header-returns-origin.py | 12 + ...control-preflight-request-header-sorted.py | 18 + ...ontrol-preflight-request-headers-origin.py | 12 + ...ontrol-preflight-request-invalid-status.py | 16 + ...eflight-request-must-not-contain-cookie.py | 12 + .../access-control-sandboxed-iframe.html | 24 + test/wpt/tests/xhr/resources/auth1/auth.py | 12 + test/wpt/tests/xhr/resources/auth10/auth.py | 12 + test/wpt/tests/xhr/resources/auth11/auth.py | 12 + test/wpt/tests/xhr/resources/auth2/auth.py | 12 + .../tests/xhr/resources/auth2/corsenabled.py | 18 + test/wpt/tests/xhr/resources/auth3/auth.py | 12 + test/wpt/tests/xhr/resources/auth4/auth.py | 12 + test/wpt/tests/xhr/resources/auth5/auth.py | 15 + test/wpt/tests/xhr/resources/auth6/auth.py | 15 + .../tests/xhr/resources/auth7/corsenabled.py | 20 + .../auth8/corsenabled-no-authorize.py | 20 + test/wpt/tests/xhr/resources/auth9/auth.py | 12 + .../wpt/tests/xhr/resources/authentication.py | 24 + .../tests/xhr/resources/bad-chunk-encoding.py | 17 + test/wpt/tests/xhr/resources/base.xml | 1 + test/wpt/tests/xhr/resources/chunked.py | 17 + test/wpt/tests/xhr/resources/conditional.py | 29 + test/wpt/tests/xhr/resources/content.py | 20 + test/wpt/tests/xhr/resources/corsenabled.py | 25 + test/wpt/tests/xhr/resources/delay.py | 7 + .../tests/xhr/resources/echo-content-cors.py | 23 + .../tests/xhr/resources/echo-content-type.py | 6 + test/wpt/tests/xhr/resources/echo-headers.py | 7 + test/wpt/tests/xhr/resources/echo-method.py | 16 + .../xhr/resources/empty-div-utf8-html.py | 5 + test/wpt/tests/xhr/resources/folder.txt | 1 + test/wpt/tests/xhr/resources/form.py | 2 + .../wpt/tests/xhr/resources/get-set-cookie.py | 18 + test/wpt/tests/xhr/resources/gzip.py | 24 + .../tests/xhr/resources/header-user-agent.py | 15 + test/wpt/tests/xhr/resources/headers.asis | 6 + test/wpt/tests/xhr/resources/headers.py | 12 + test/wpt/tests/xhr/resources/image.gif | Bin 0 -> 167145 bytes test/wpt/tests/xhr/resources/img-utf8-html.py | 5 + test/wpt/tests/xhr/resources/img.jpg | Bin 0 -> 108761 bytes .../tests/xhr/resources/infinite-redirects.py | 24 + test/wpt/tests/xhr/resources/init.htm | 20 + .../tests/xhr/resources/inspect-headers.py | 36 + .../tests/xhr/resources/invalid-utf8-html.py | 5 + test/wpt/tests/xhr/resources/last-modified.py | 9 + .../no-custom-header-on-preflight.py | 27 + .../wpt/tests/xhr/resources/nocors/folder.txt | 1 + test/wpt/tests/xhr/resources/over-1-meg.txt | 1 + test/wpt/tests/xhr/resources/parse-headers.py | 6 + test/wpt/tests/xhr/resources/pass.txt | 1 + test/wpt/tests/xhr/resources/redirect-cors.py | 20 + test/wpt/tests/xhr/resources/redirect.py | 16 + test/wpt/tests/xhr/resources/requri.py | 5 + test/wpt/tests/xhr/resources/reset-token.py | 5 + .../responseType-document-in-worker.js | 9 + .../responseXML-unavailable-in-worker.js | 9 + ...after-setting-document-domain-window-1.htm | 23 + ...after-setting-document-domain-window-2.htm | 20 + ...r-setting-document-domain-window-helper.js | 32 + .../wpt/tests/xhr/resources/shift-jis-html.py | 6 + test/wpt/tests/xhr/resources/status.py | 11 + test/wpt/tests/xhr/resources/top.txt | 1 + test/wpt/tests/xhr/resources/trickle.py | 15 + test/wpt/tests/xhr/resources/upload.py | 17 + test/wpt/tests/xhr/resources/utf16.txt | Bin 0 -> 18 bytes test/wpt/tests/xhr/resources/well-formed.xml | 4 + test/wpt/tests/xhr/resources/win-1252-html.py | 5 + test/wpt/tests/xhr/resources/win-1252-xml.py | 5 + .../resources/workerxhr-origin-referrer.js | 63 + .../tests/xhr/resources/workerxhr-simple.js | 9 + .../resources/xmlhttprequest-event-order.js | 83 + .../xmlhttprequest-timeout-aborted.js | 15 + .../xmlhttprequest-timeout-abortedonmain.js | 8 + .../xmlhttprequest-timeout-overrides.js | 12 + ...xmlhttprequest-timeout-overridesexpires.js | 12 + .../xmlhttprequest-timeout-runner.js | 21 + .../xmlhttprequest-timeout-simple.js | 6 + .../xmlhttprequest-timeout-synconmain.js | 2 + .../xmlhttprequest-timeout-synconworker.js | 11 + .../resources/xmlhttprequest-timeout-twice.js | 6 + .../xhr/resources/xmlhttprequest-timeout.js | 333 ++ test/wpt/tests/xhr/resources/zlib.py | 19 + .../wpt/tests/xhr/response-body-errors.any.js | 23 + .../tests/xhr/response-data-arraybuffer.htm | 54 + test/wpt/tests/xhr/response-data-blob.htm | 55 + test/wpt/tests/xhr/response-data-deflate.htm | 42 + test/wpt/tests/xhr/response-data-gzip.htm | 42 + test/wpt/tests/xhr/response-data-progress.htm | 52 + .../xhr/response-invalid-responsetype.htm | 38 + test/wpt/tests/xhr/response-json.htm | 61 + test/wpt/tests/xhr/response-method.htm | 21 + test/wpt/tests/xhr/responseText-status.html | 33 + .../xhr/responseType-document-in-worker.html | 13 + .../responseXML-unavailable-in-worker.html | 13 + .../tests/xhr/responsedocument-decoding.htm | 39 + test/wpt/tests/xhr/responsetext-decoding.htm | 93 + test/wpt/tests/xhr/responsetype.any.js | 135 + test/wpt/tests/xhr/responseurl.html | 37 + test/wpt/tests/xhr/responsexml-basic.htm | 33 + .../xhr/responsexml-document-properties.htm | 123 + test/wpt/tests/xhr/responsexml-get-twice.htm | 66 + test/wpt/tests/xhr/responsexml-media-type.htm | 41 + .../xhr/responsexml-non-document-types.htm | 45 + .../tests/xhr/responsexml-non-well-formed.htm | 30 + .../tests/xhr/security-consideration.sub.html | 36 + test/wpt/tests/xhr/send-accept-language.htm | 27 + test/wpt/tests/xhr/send-accept.htm | 24 + .../send-after-setting-document-domain.htm | 39 + ...-authentication-basic-cors-not-enabled.htm | 28 + .../xhr/send-authentication-basic-cors.htm | 35 + ...nd-authentication-basic-repeat-no-args.htm | 33 + ...n-basic-setrequestheader-and-arguments.htm | 36 + ...asic-setrequestheader-existing-session.htm | 53 + ...-authentication-basic-setrequestheader.htm | 36 + .../tests/xhr/send-authentication-basic.htm | 27 + ...thentication-competing-names-passwords.htm | 50 + ...entication-cors-basic-setrequestheader.htm | 30 + ...tication-cors-setrequestheader-no-cred.htm | 61 + ...authentication-existing-session-manual.htm | 33 + .../send-authentication-prompt-2-manual.htm | 25 + .../xhr/send-authentication-prompt-manual.htm | 25 + .../xhr/send-blob-with-no-mime-type.html | 61 + test/wpt/tests/xhr/send-conditional-cors.htm | 42 + test/wpt/tests/xhr/send-conditional.htm | 34 + .../tests/xhr/send-content-type-charset.htm | 115 + .../tests/xhr/send-content-type-string.htm | 26 + .../tests/xhr/send-data-arraybuffer.any.js | 31 + .../xhr/send-data-arraybufferview.any.js | 18 + test/wpt/tests/xhr/send-data-blob.htm | 62 + test/wpt/tests/xhr/send-data-es-object.any.js | 58 + test/wpt/tests/xhr/send-data-formdata.any.js | 21 + .../xhr/send-data-sharedarraybuffer.any.js | 27 + .../send-data-string-invalid-unicode.any.js | 46 + .../xhr/send-data-unexpected-tostring.htm | 56 + test/wpt/tests/xhr/send-entity-body-basic.htm | 28 + .../xhr/send-entity-body-document-bogus.htm | 26 + .../tests/xhr/send-entity-body-document.htm | 92 + test/wpt/tests/xhr/send-entity-body-empty.htm | 26 + .../xhr/send-entity-body-get-head-async.htm | 39 + .../tests/xhr/send-entity-body-get-head.htm | 36 + test/wpt/tests/xhr/send-entity-body-none.htm | 40 + .../send-network-error-async-events.sub.htm | 47 + .../send-network-error-sync-events.sub.htm | 45 + .../xhr/send-no-response-event-loadend.htm | 48 + .../xhr/send-no-response-event-loadstart.htm | 48 + .../xhr/send-no-response-event-order.htm | 45 + test/wpt/tests/xhr/send-non-same-origin.htm | 33 + test/wpt/tests/xhr/send-receive-utf16.htm | 37 + .../tests/xhr/send-redirect-bogus-sync.htm | 26 + test/wpt/tests/xhr/send-redirect-bogus.htm | 36 + .../tests/xhr/send-redirect-infinite-sync.htm | 24 + test/wpt/tests/xhr/send-redirect-infinite.htm | 35 + .../tests/xhr/send-redirect-no-location.htm | 40 + .../tests/xhr/send-redirect-post-upload.htm | 140 + test/wpt/tests/xhr/send-redirect-to-cors.htm | 92 + .../tests/xhr/send-redirect-to-non-cors.htm | 37 + test/wpt/tests/xhr/send-redirect.htm | 36 + .../tests/xhr/send-response-event-order.htm | 40 + .../send-response-upload-event-loadend.htm | 40 + .../send-response-upload-event-loadstart.htm | 39 + .../send-response-upload-event-progress.htm | 39 + test/wpt/tests/xhr/send-send.any.js | 7 + test/wpt/tests/xhr/send-sync-blocks-async.htm | 53 + .../xhr/send-sync-no-response-event-load.htm | 38 + .../send-sync-no-response-event-loadend.htm | 38 + .../xhr/send-sync-no-response-event-order.htm | 51 + .../xhr/send-sync-response-event-order.htm | 35 + test/wpt/tests/xhr/send-sync-timeout.htm | 29 + test/wpt/tests/xhr/send-timeout-events.htm | 62 + test/wpt/tests/xhr/send-usp.any.js | 46 + .../tests/xhr/setrequestheader-after-send.htm | 27 + .../setrequestheader-allow-empty-value.htm | 26 + ...equestheader-allow-whitespace-in-value.htm | 27 + .../xhr/setrequestheader-before-open.htm | 18 + .../tests/xhr/setrequestheader-bogus-name.htm | 59 + .../xhr/setrequestheader-bogus-value.htm | 36 + .../xhr/setrequestheader-case-insensitive.htm | 34 + .../xhr/setrequestheader-combining.window.js | 12 + .../xhr/setrequestheader-content-type.htm | 220 + .../xhr/setrequestheader-header-allowed.htm | 34 + .../xhr/setrequestheader-header-forbidden.htm | 95 + ...setrequestheader-open-setrequestheader.htm | 53 + test/wpt/tests/xhr/status-async.htm | 62 + test/wpt/tests/xhr/status-basic.htm | 51 + test/wpt/tests/xhr/status-error.htm | 87 + test/wpt/tests/xhr/status.h2.window.js | 21 + test/wpt/tests/xhr/sync-no-progress.any.js | 13 + test/wpt/tests/xhr/sync-no-timeout.any.js | 16 + .../tests/xhr/sync-xhr-and-window-onload.html | 25 + .../sync-xhr-supported-by-feature-policy.html | 11 + test/wpt/tests/xhr/template-element.html | 36 + .../wpt/tests/xhr/thrown-error-in-events.html | 60 + test/wpt/tests/xhr/timeout-cors-async.htm | 43 + .../tests/xhr/timeout-multiple-fetches.html | 32 + test/wpt/tests/xhr/timeout-sync.htm | 25 + .../xhr/xhr-authorization-redirect.any.js | 28 + .../wpt/tests/xhr/xhr-timeout-longtask.any.js | 14 + test/wpt/tests/xhr/xmlhttprequest-basic.htm | 45 + .../tests/xhr/xmlhttprequest-eventtarget.htm | 48 + .../xhr/xmlhttprequest-network-error-sync.htm | 34 + .../xhr/xmlhttprequest-network-error.htm | 39 + ...est-sync-block-defer-scripts-subframe.html | 17 + ...lhttprequest-sync-block-defer-scripts.html | 15 + .../xmlhttprequest-sync-block-scripts.html | 22 + ...quest-sync-default-feature-policy.sub.html | 32 + ...t-sync-not-hang-scriptloader-subframe.html | 17 + ...ttprequest-sync-not-hang-scriptloader.html | 16 + .../xhr/xmlhttprequest-timeout-aborted.html | 29 + .../xmlhttprequest-timeout-abortedonmain.html | 25 + .../xhr/xmlhttprequest-timeout-overrides.html | 26 + ...lhttprequest-timeout-overridesexpires.html | 26 + .../xhr/xmlhttprequest-timeout-reused.html | 49 + .../xhr/xmlhttprequest-timeout-simple.html | 27 + .../xmlhttprequest-timeout-synconmain.html | 23 + .../xhr/xmlhttprequest-timeout-twice.html | 28 + ...xmlhttprequest-timeout-worker-aborted.html | 31 + ...lhttprequest-timeout-worker-overrides.html | 27 + ...quest-timeout-worker-overridesexpires.html | 28 + .../xmlhttprequest-timeout-worker-simple.html | 29 + ...tprequest-timeout-worker-synconworker.html | 28 + .../xmlhttprequest-timeout-worker-twice.html | 29 + test/wpt/tests/xhr/xmlhttprequest-unsent.htm | 36 + 1829 files changed, 124557 insertions(+), 522 deletions(-) delete mode 100644 test/wpt/status/xhr.status.json create mode 100644 test/wpt/status/xhr/formdata.status.json create mode 100644 test/wpt/tests/FileAPI/Blob-methods-from-detached-frame.html create mode 100644 test/wpt/tests/FileAPI/BlobURL/cross-partition.tentative.https.html create mode 100644 test/wpt/tests/FileAPI/BlobURL/support/file_test2.txt create mode 100644 test/wpt/tests/FileAPI/BlobURL/test2-manual.html create mode 100644 test/wpt/tests/FileAPI/FileReader/progress_event_bubbles_cancelable.html create mode 100644 test/wpt/tests/FileAPI/FileReader/support/file_test1.txt create mode 100644 test/wpt/tests/FileAPI/FileReader/test_errors-manual.html create mode 100644 test/wpt/tests/FileAPI/FileReader/test_notreadableerrors-manual.html create mode 100644 test/wpt/tests/FileAPI/FileReader/test_securityerrors-manual.html create mode 100644 test/wpt/tests/FileAPI/FileReader/workers.html create mode 100644 test/wpt/tests/FileAPI/FileReaderSync.worker.js create mode 100644 test/wpt/tests/FileAPI/META.yml create mode 100644 test/wpt/tests/FileAPI/blob/Blob-array-buffer.any.js create mode 100644 test/wpt/tests/FileAPI/blob/Blob-constructor-dom.window.js create mode 100644 test/wpt/tests/FileAPI/blob/Blob-constructor-endings.html create mode 100644 test/wpt/tests/FileAPI/blob/Blob-constructor.any.js create mode 100644 test/wpt/tests/FileAPI/blob/Blob-in-worker.worker.js create mode 100644 test/wpt/tests/FileAPI/blob/Blob-slice-overflow.any.js create mode 100644 test/wpt/tests/FileAPI/blob/Blob-slice.any.js create mode 100644 test/wpt/tests/FileAPI/blob/Blob-stream-byob-crash.html create mode 100644 test/wpt/tests/FileAPI/blob/Blob-stream.any.js create mode 100644 test/wpt/tests/FileAPI/blob/Blob-text.any.js create mode 100644 test/wpt/tests/FileAPI/file/File-constructor-endings.html create mode 100644 test/wpt/tests/FileAPI/file/Worker-read-file-constructor.worker.js create mode 100644 test/wpt/tests/FileAPI/file/resources/echo-content-escaped.py create mode 100644 test/wpt/tests/FileAPI/file/send-file-form-controls.html create mode 100644 test/wpt/tests/FileAPI/file/send-file-form-iso-2022-jp.html create mode 100644 test/wpt/tests/FileAPI/file/send-file-form-punctuation.html create mode 100644 test/wpt/tests/FileAPI/file/send-file-form-utf-8.html create mode 100644 test/wpt/tests/FileAPI/file/send-file-form-windows-1252.html create mode 100644 test/wpt/tests/FileAPI/file/send-file-form-x-user-defined.html create mode 100644 test/wpt/tests/FileAPI/file/send-file-form.html create mode 100644 test/wpt/tests/FileAPI/filelist-section/filelist.html create mode 100644 test/wpt/tests/FileAPI/filelist-section/filelist_multiple_selected_files-manual.html create mode 100644 test/wpt/tests/FileAPI/filelist-section/filelist_selected_file-manual.html create mode 100644 test/wpt/tests/FileAPI/filelist-section/support/upload.txt create mode 100644 test/wpt/tests/FileAPI/filelist-section/support/upload.zip create mode 100644 test/wpt/tests/FileAPI/historical.https.html create mode 100644 test/wpt/tests/FileAPI/idlharness-manual.html create mode 100644 test/wpt/tests/FileAPI/idlharness.html create mode 100644 test/wpt/tests/FileAPI/idlharness.worker.js create mode 100644 test/wpt/tests/FileAPI/progress-manual.html create mode 100644 test/wpt/tests/FileAPI/reading-data-section/filereader_file-manual.html create mode 100644 test/wpt/tests/FileAPI/reading-data-section/filereader_file_img-manual.html create mode 100644 test/wpt/tests/FileAPI/reading-data-section/filereader_result.any.js create mode 100644 test/wpt/tests/FileAPI/reading-data-section/support/blue-100x100.png create mode 100644 test/wpt/tests/FileAPI/support/Blob.js create mode 100644 test/wpt/tests/FileAPI/support/document-domain-setter.sub.html create mode 100644 test/wpt/tests/FileAPI/support/empty-document.html create mode 100644 test/wpt/tests/FileAPI/support/historical-serviceworker.js create mode 100644 test/wpt/tests/FileAPI/support/incumbent.sub.html create mode 100644 test/wpt/tests/FileAPI/support/send-file-form-helper.js create mode 100644 test/wpt/tests/FileAPI/support/upload.txt create mode 100644 test/wpt/tests/FileAPI/support/url-origin.html create mode 100644 test/wpt/tests/FileAPI/unicode.html create mode 100644 test/wpt/tests/FileAPI/url/cross-global-revoke.sub.html create mode 100644 test/wpt/tests/FileAPI/url/multi-global-origin-serialization.sub.html create mode 100644 test/wpt/tests/FileAPI/url/resources/create-helper.html create mode 100644 test/wpt/tests/FileAPI/url/resources/create-helper.js create mode 100644 test/wpt/tests/FileAPI/url/resources/fetch-tests.js create mode 100644 test/wpt/tests/FileAPI/url/resources/revoke-helper.html create mode 100644 test/wpt/tests/FileAPI/url/resources/revoke-helper.js create mode 100644 test/wpt/tests/FileAPI/url/sandboxed-iframe.html create mode 100644 test/wpt/tests/FileAPI/url/unicode-origin.sub.html create mode 100644 test/wpt/tests/FileAPI/url/url-charset.window.js create mode 100644 test/wpt/tests/FileAPI/url/url-format.any.js create mode 100644 test/wpt/tests/FileAPI/url/url-in-tags-revoke.window.js create mode 100644 test/wpt/tests/FileAPI/url/url-in-tags.window.js create mode 100644 test/wpt/tests/FileAPI/url/url-lifetime.html create mode 100644 test/wpt/tests/FileAPI/url/url-reload.window.js create mode 100644 test/wpt/tests/FileAPI/url/url-with-fetch.any.js create mode 100644 test/wpt/tests/FileAPI/url/url-with-xhr.any.js create mode 100644 test/wpt/tests/FileAPI/url/url_createobjecturl_file-manual.html create mode 100644 test/wpt/tests/FileAPI/url/url_createobjecturl_file_img-manual.html create mode 100644 test/wpt/tests/FileAPI/url/url_xmlhttprequest_img-ref.html create mode 100644 test/wpt/tests/FileAPI/url/url_xmlhttprequest_img.html delete mode 100644 test/wpt/tests/LICENSE.md create mode 100644 test/wpt/tests/common/CustomCorsResponse.py create mode 100644 test/wpt/tests/common/META.yml create mode 100644 test/wpt/tests/common/PrefixedLocalStorage.js create mode 100644 test/wpt/tests/common/PrefixedLocalStorage.js.headers create mode 100644 test/wpt/tests/common/PrefixedPostMessage.js create mode 100644 test/wpt/tests/common/PrefixedPostMessage.js.headers create mode 100644 test/wpt/tests/common/README.md create mode 100644 test/wpt/tests/common/__init__.py create mode 100644 test/wpt/tests/common/arrays.js create mode 100644 test/wpt/tests/common/blank-with-cors.html create mode 100644 test/wpt/tests/common/blank-with-cors.html.headers create mode 100644 test/wpt/tests/common/blank.html create mode 100644 test/wpt/tests/common/custom-cors-response.js create mode 100644 test/wpt/tests/common/dispatcher/README.md create mode 100644 test/wpt/tests/common/dispatcher/dispatcher.js create mode 100644 test/wpt/tests/common/dispatcher/dispatcher.py create mode 100644 test/wpt/tests/common/dispatcher/executor-service-worker.js create mode 100644 test/wpt/tests/common/dispatcher/executor-worker.js create mode 100644 test/wpt/tests/common/dispatcher/executor.html create mode 100644 test/wpt/tests/common/dispatcher/remote-executor.html create mode 100644 test/wpt/tests/common/domain-setter.sub.html create mode 100644 test/wpt/tests/common/dummy.xhtml create mode 100644 test/wpt/tests/common/dummy.xml create mode 100644 test/wpt/tests/common/echo.py create mode 100644 test/wpt/tests/common/gc.js create mode 100644 test/wpt/tests/common/get-host-info.sub.js.headers create mode 100644 test/wpt/tests/common/media.js create mode 100644 test/wpt/tests/common/media.js.headers create mode 100644 test/wpt/tests/common/object-association.js create mode 100644 test/wpt/tests/common/object-association.js.headers create mode 100644 test/wpt/tests/common/performance-timeline-utils.js create mode 100644 test/wpt/tests/common/performance-timeline-utils.js.headers create mode 100644 test/wpt/tests/common/proxy-all.sub.pac create mode 100644 test/wpt/tests/common/redirect-opt-in.py create mode 100644 test/wpt/tests/common/redirect.py create mode 100644 test/wpt/tests/common/refresh.py create mode 100644 test/wpt/tests/common/reftest-wait.js create mode 100644 test/wpt/tests/common/reftest-wait.js.headers create mode 100644 test/wpt/tests/common/rendering-utils.js create mode 100644 test/wpt/tests/common/sab.js create mode 100644 test/wpt/tests/common/security-features/README.md create mode 100644 test/wpt/tests/common/security-features/__init__.py create mode 100644 test/wpt/tests/common/security-features/resources/common.sub.js create mode 100644 test/wpt/tests/common/security-features/resources/common.sub.js.headers create mode 100644 test/wpt/tests/common/security-features/scope/__init__.py create mode 100644 test/wpt/tests/common/security-features/scope/document.py create mode 100644 test/wpt/tests/common/security-features/scope/template/document.html.template create mode 100644 test/wpt/tests/common/security-features/scope/template/worker.js.template create mode 100644 test/wpt/tests/common/security-features/scope/util.py create mode 100644 test/wpt/tests/common/security-features/scope/worker.py create mode 100644 test/wpt/tests/common/security-features/subresource/__init__.py create mode 100644 test/wpt/tests/common/security-features/subresource/audio.py create mode 100644 test/wpt/tests/common/security-features/subresource/document.py create mode 100644 test/wpt/tests/common/security-features/subresource/empty.py create mode 100644 test/wpt/tests/common/security-features/subresource/font.py create mode 100644 test/wpt/tests/common/security-features/subresource/image.py create mode 100644 test/wpt/tests/common/security-features/subresource/referrer.py create mode 100644 test/wpt/tests/common/security-features/subresource/script.py create mode 100644 test/wpt/tests/common/security-features/subresource/shared-worker.py create mode 100644 test/wpt/tests/common/security-features/subresource/static-import.py create mode 100644 test/wpt/tests/common/security-features/subresource/stylesheet.py create mode 100644 test/wpt/tests/common/security-features/subresource/subresource.py create mode 100644 test/wpt/tests/common/security-features/subresource/svg.py create mode 100644 test/wpt/tests/common/security-features/subresource/template/document.html.template create mode 100644 test/wpt/tests/common/security-features/subresource/template/font.css.template create mode 100644 test/wpt/tests/common/security-features/subresource/template/image.css.template create mode 100644 test/wpt/tests/common/security-features/subresource/template/script.js.template create mode 100644 test/wpt/tests/common/security-features/subresource/template/shared-worker.js.template create mode 100644 test/wpt/tests/common/security-features/subresource/template/static-import.js.template create mode 100644 test/wpt/tests/common/security-features/subresource/template/svg.css.template create mode 100644 test/wpt/tests/common/security-features/subresource/template/svg.embedded.template create mode 100644 test/wpt/tests/common/security-features/subresource/template/worker.js.template create mode 100644 test/wpt/tests/common/security-features/subresource/video.py create mode 100644 test/wpt/tests/common/security-features/subresource/worker.py create mode 100644 test/wpt/tests/common/security-features/subresource/xhr.py create mode 100644 test/wpt/tests/common/security-features/tools/format_spec_src_json.py create mode 100644 test/wpt/tests/common/security-features/tools/generate.py create mode 100644 test/wpt/tests/common/security-features/tools/spec.src.json create mode 100644 test/wpt/tests/common/security-features/tools/spec_validator.py create mode 100644 test/wpt/tests/common/security-features/tools/template/disclaimer.template create mode 100644 test/wpt/tests/common/security-features/tools/template/spec_json.js.template create mode 100644 test/wpt/tests/common/security-features/tools/template/test.debug.html.template create mode 100644 test/wpt/tests/common/security-features/tools/template/test.release.html.template create mode 100644 test/wpt/tests/common/security-features/tools/util.py create mode 100644 test/wpt/tests/common/security-features/types.md create mode 100644 test/wpt/tests/common/slow-redirect.py create mode 100644 test/wpt/tests/common/slow.py create mode 100644 test/wpt/tests/common/square.png create mode 100644 test/wpt/tests/common/stringifiers.js create mode 100644 test/wpt/tests/common/stringifiers.js.headers create mode 100644 test/wpt/tests/common/subset-tests-by-key.js create mode 100644 test/wpt/tests/common/subset-tests.js create mode 100644 test/wpt/tests/common/test-setting-immutable-prototype.js create mode 100644 test/wpt/tests/common/test-setting-immutable-prototype.js.headers create mode 100644 test/wpt/tests/common/text-plain.txt create mode 100644 test/wpt/tests/common/third_party/reftest-analyzer.xhtml create mode 100644 test/wpt/tests/common/utils.js.headers create mode 100644 test/wpt/tests/common/window-name-setter.html create mode 100644 test/wpt/tests/common/worklet-reftest.js create mode 100644 test/wpt/tests/common/worklet-reftest.js.headers create mode 100644 test/wpt/tests/fetch/META.yml create mode 100644 test/wpt/tests/fetch/README.md create mode 100644 test/wpt/tests/fetch/api/abort/cache.https.any.js create mode 100644 test/wpt/tests/fetch/api/abort/destroyed-context.html create mode 100644 test/wpt/tests/fetch/api/abort/keepalive.html create mode 100644 test/wpt/tests/fetch/api/abort/serviceworker-intercepted.https.html create mode 100644 test/wpt/tests/fetch/api/basic/block-mime-as-script.html create mode 100644 test/wpt/tests/fetch/api/basic/conditional-get.any.js create mode 100644 test/wpt/tests/fetch/api/basic/keepalive.html create mode 100644 test/wpt/tests/fetch/api/basic/mediasource.window.js create mode 100644 test/wpt/tests/fetch/api/basic/mode-no-cors.sub.any.js create mode 100644 test/wpt/tests/fetch/api/basic/mode-same-origin.any.js create mode 100644 test/wpt/tests/fetch/api/basic/referrer.any.js create mode 100644 test/wpt/tests/fetch/api/basic/request-forbidden-headers.any.js create mode 100644 test/wpt/tests/fetch/api/basic/request-headers.any.js create mode 100644 test/wpt/tests/fetch/api/basic/request-referrer-redirected-worker.html create mode 100644 test/wpt/tests/fetch/api/basic/request-referrer.any.js create mode 100644 test/wpt/tests/fetch/api/basic/request-upload.h2.any.js create mode 100644 test/wpt/tests/fetch/api/basic/status.h2.any.js create mode 100644 test/wpt/tests/fetch/api/cors/cors-basic.any.js create mode 100644 test/wpt/tests/fetch/api/cors/cors-cookies-redirect.any.js create mode 100644 test/wpt/tests/fetch/api/cors/cors-cookies.any.js create mode 100644 test/wpt/tests/fetch/api/cors/cors-expose-star.sub.any.js create mode 100644 test/wpt/tests/fetch/api/cors/cors-filtering.sub.any.js create mode 100644 test/wpt/tests/fetch/api/cors/cors-multiple-origins.sub.any.js create mode 100644 test/wpt/tests/fetch/api/cors/cors-no-preflight.any.js create mode 100644 test/wpt/tests/fetch/api/cors/cors-origin.any.js create mode 100644 test/wpt/tests/fetch/api/cors/cors-preflight-cache.any.js create mode 100644 test/wpt/tests/fetch/api/cors/cors-preflight-not-cors-safelisted.any.js create mode 100644 test/wpt/tests/fetch/api/cors/cors-preflight-redirect.any.js create mode 100644 test/wpt/tests/fetch/api/cors/cors-preflight-referrer.any.js create mode 100644 test/wpt/tests/fetch/api/cors/cors-preflight-response-validation.any.js create mode 100644 test/wpt/tests/fetch/api/cors/cors-preflight-star.any.js create mode 100644 test/wpt/tests/fetch/api/cors/cors-preflight-status.any.js create mode 100644 test/wpt/tests/fetch/api/cors/cors-preflight.any.js create mode 100644 test/wpt/tests/fetch/api/cors/cors-redirect-credentials.any.js create mode 100644 test/wpt/tests/fetch/api/cors/cors-redirect-preflight.any.js create mode 100644 test/wpt/tests/fetch/api/cors/cors-redirect.any.js create mode 100644 test/wpt/tests/fetch/api/cors/data-url-iframe.html create mode 100644 test/wpt/tests/fetch/api/cors/data-url-shared-worker.html create mode 100644 test/wpt/tests/fetch/api/cors/data-url-worker.html create mode 100644 test/wpt/tests/fetch/api/cors/resources/corspreflight.js create mode 100644 test/wpt/tests/fetch/api/cors/resources/not-cors-safelisted.json create mode 100644 test/wpt/tests/fetch/api/cors/sandboxed-iframe.html create mode 100644 test/wpt/tests/fetch/api/crashtests/request.html create mode 100644 test/wpt/tests/fetch/api/credentials/authentication-basic.any.js create mode 100644 test/wpt/tests/fetch/api/credentials/cookies.any.js create mode 100644 test/wpt/tests/fetch/api/headers/headers-no-cors.any.js create mode 100644 test/wpt/tests/fetch/api/policies/csp-blocked-worker.html create mode 100644 test/wpt/tests/fetch/api/policies/csp-blocked.html create mode 100644 test/wpt/tests/fetch/api/policies/csp-blocked.html.headers create mode 100644 test/wpt/tests/fetch/api/policies/csp-blocked.js create mode 100644 test/wpt/tests/fetch/api/policies/csp-blocked.js.headers create mode 100644 test/wpt/tests/fetch/api/policies/nested-policy.js create mode 100644 test/wpt/tests/fetch/api/policies/nested-policy.js.headers create mode 100644 test/wpt/tests/fetch/api/policies/referrer-no-referrer-service-worker.https.html create mode 100644 test/wpt/tests/fetch/api/policies/referrer-no-referrer-worker.html create mode 100644 test/wpt/tests/fetch/api/policies/referrer-no-referrer.html create mode 100644 test/wpt/tests/fetch/api/policies/referrer-no-referrer.html.headers create mode 100644 test/wpt/tests/fetch/api/policies/referrer-no-referrer.js create mode 100644 test/wpt/tests/fetch/api/policies/referrer-no-referrer.js.headers create mode 100644 test/wpt/tests/fetch/api/policies/referrer-origin-service-worker.https.html create mode 100644 test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin-service-worker.https.html create mode 100644 test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin-worker.html create mode 100644 test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin.html create mode 100644 test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin.html.headers create mode 100644 test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin.js create mode 100644 test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin.js.headers create mode 100644 test/wpt/tests/fetch/api/policies/referrer-origin-worker.html create mode 100644 test/wpt/tests/fetch/api/policies/referrer-origin.html create mode 100644 test/wpt/tests/fetch/api/policies/referrer-origin.html.headers create mode 100644 test/wpt/tests/fetch/api/policies/referrer-origin.js create mode 100644 test/wpt/tests/fetch/api/policies/referrer-origin.js.headers create mode 100644 test/wpt/tests/fetch/api/policies/referrer-unsafe-url-service-worker.https.html create mode 100644 test/wpt/tests/fetch/api/policies/referrer-unsafe-url-worker.html create mode 100644 test/wpt/tests/fetch/api/policies/referrer-unsafe-url.html create mode 100644 test/wpt/tests/fetch/api/policies/referrer-unsafe-url.html.headers create mode 100644 test/wpt/tests/fetch/api/policies/referrer-unsafe-url.js create mode 100644 test/wpt/tests/fetch/api/policies/referrer-unsafe-url.js.headers create mode 100644 test/wpt/tests/fetch/api/redirect/redirect-empty-location.any.js create mode 100644 test/wpt/tests/fetch/api/redirect/redirect-location-escape.tentative.any.js create mode 100644 test/wpt/tests/fetch/api/redirect/redirect-mode.any.js create mode 100644 test/wpt/tests/fetch/api/redirect/redirect-origin.any.js create mode 100644 test/wpt/tests/fetch/api/redirect/redirect-referrer-override.any.js create mode 100644 test/wpt/tests/fetch/api/redirect/redirect-referrer.any.js create mode 100644 test/wpt/tests/fetch/api/redirect/redirect-upload.h2.any.js create mode 100644 test/wpt/tests/fetch/api/request/destination/fetch-destination-frame.https.html create mode 100644 test/wpt/tests/fetch/api/request/destination/fetch-destination-iframe.https.html create mode 100644 test/wpt/tests/fetch/api/request/destination/fetch-destination-no-load-event.https.html create mode 100644 test/wpt/tests/fetch/api/request/destination/fetch-destination-prefetch.https.html create mode 100644 test/wpt/tests/fetch/api/request/destination/fetch-destination-worker.https.html create mode 100644 test/wpt/tests/fetch/api/request/destination/fetch-destination.https.html create mode 100644 test/wpt/tests/fetch/api/request/destination/resources/dummy create mode 100644 test/wpt/tests/fetch/api/request/destination/resources/dummy.es create mode 100644 test/wpt/tests/fetch/api/request/destination/resources/dummy.es.headers create mode 100644 test/wpt/tests/fetch/api/request/destination/resources/dummy.html create mode 100644 test/wpt/tests/fetch/api/request/destination/resources/dummy.png create mode 100644 test/wpt/tests/fetch/api/request/destination/resources/dummy.ttf create mode 100644 test/wpt/tests/fetch/api/request/destination/resources/dummy_audio.mp3 create mode 100644 test/wpt/tests/fetch/api/request/destination/resources/dummy_audio.oga create mode 100644 test/wpt/tests/fetch/api/request/destination/resources/dummy_video.mp4 create mode 100644 test/wpt/tests/fetch/api/request/destination/resources/dummy_video.ogv create mode 100644 test/wpt/tests/fetch/api/request/destination/resources/empty.https.html create mode 100644 test/wpt/tests/fetch/api/request/destination/resources/fetch-destination-worker-frame.js create mode 100644 test/wpt/tests/fetch/api/request/destination/resources/fetch-destination-worker-iframe.js create mode 100644 test/wpt/tests/fetch/api/request/destination/resources/fetch-destination-worker-no-load-event.js create mode 100644 test/wpt/tests/fetch/api/request/destination/resources/fetch-destination-worker.js create mode 100644 test/wpt/tests/fetch/api/request/destination/resources/importer.js create mode 100644 test/wpt/tests/fetch/api/request/multi-globals/current/current.html create mode 100644 test/wpt/tests/fetch/api/request/multi-globals/incumbent/incumbent.html create mode 100644 test/wpt/tests/fetch/api/request/multi-globals/url-parsing.html create mode 100644 test/wpt/tests/fetch/api/request/request-cache-default-conditional.any.js create mode 100644 test/wpt/tests/fetch/api/request/request-cache-default.any.js create mode 100644 test/wpt/tests/fetch/api/request/request-cache-force-cache.any.js create mode 100644 test/wpt/tests/fetch/api/request/request-cache-no-cache.any.js create mode 100644 test/wpt/tests/fetch/api/request/request-cache-no-store.any.js create mode 100644 test/wpt/tests/fetch/api/request/request-cache-only-if-cached.any.js create mode 100644 test/wpt/tests/fetch/api/request/request-cache-reload.any.js create mode 100644 test/wpt/tests/fetch/api/request/request-cache.js create mode 100644 test/wpt/tests/fetch/api/request/request-clone.sub.html create mode 100644 test/wpt/tests/fetch/api/request/request-init-001.sub.html create mode 100644 test/wpt/tests/fetch/api/request/request-init-003.sub.html create mode 100644 test/wpt/tests/fetch/api/request/request-init-priority.any.js create mode 100644 test/wpt/tests/fetch/api/request/request-keepalive-quota.html create mode 100644 test/wpt/tests/fetch/api/request/request-reset-attributes.https.html create mode 100644 test/wpt/tests/fetch/api/request/resources/cache.py create mode 100644 test/wpt/tests/fetch/api/request/resources/hello.txt create mode 100644 test/wpt/tests/fetch/api/request/resources/request-reset-attributes-worker.js create mode 100644 test/wpt/tests/fetch/api/request/url-encoding.html create mode 100644 test/wpt/tests/fetch/api/resources/authentication.py create mode 100644 test/wpt/tests/fetch/api/resources/bad-chunk-encoding.py create mode 100644 test/wpt/tests/fetch/api/resources/basic.html create mode 100644 test/wpt/tests/fetch/api/resources/cache.py create mode 100644 test/wpt/tests/fetch/api/resources/clean-stash.py create mode 100644 test/wpt/tests/fetch/api/resources/cors-top.txt.headers create mode 100644 test/wpt/tests/fetch/api/resources/dump-authorization-header.py create mode 100644 test/wpt/tests/fetch/api/resources/echo-content.h2.py create mode 100644 test/wpt/tests/fetch/api/resources/echo-content.py create mode 100644 test/wpt/tests/fetch/api/resources/infinite-slow-response.py create mode 100644 test/wpt/tests/fetch/api/resources/inspect-headers.py create mode 100644 test/wpt/tests/fetch/api/resources/keepalive-iframe.html create mode 100644 test/wpt/tests/fetch/api/resources/keepalive-window.html create mode 100644 test/wpt/tests/fetch/api/resources/method.py create mode 100644 test/wpt/tests/fetch/api/resources/preflight.py create mode 100644 test/wpt/tests/fetch/api/resources/redirect-empty-location.py create mode 100644 test/wpt/tests/fetch/api/resources/redirect.h2.py create mode 100644 test/wpt/tests/fetch/api/resources/redirect.py create mode 100644 test/wpt/tests/fetch/api/resources/sandboxed-iframe.html create mode 100644 test/wpt/tests/fetch/api/resources/script-with-header.py create mode 100644 test/wpt/tests/fetch/api/resources/stash-put.py create mode 100644 test/wpt/tests/fetch/api/resources/stash-take.py create mode 100644 test/wpt/tests/fetch/api/resources/status.py create mode 100644 test/wpt/tests/fetch/api/resources/sw-intercept-abort.js create mode 100644 test/wpt/tests/fetch/api/resources/sw-intercept.js create mode 100644 test/wpt/tests/fetch/api/resources/trickle.py create mode 100644 test/wpt/tests/fetch/api/response/multi-globals/current/current.html create mode 100644 test/wpt/tests/fetch/api/response/multi-globals/incumbent/incumbent.html create mode 100644 test/wpt/tests/fetch/api/response/multi-globals/relevant/relevant.html create mode 100644 test/wpt/tests/fetch/api/response/multi-globals/url-parsing.html create mode 100644 test/wpt/tests/fetch/api/response/response-body-read-task-handling.html create mode 100644 test/wpt/tests/fetch/api/response/response-clone-iframe.window.js create mode 100644 test/wpt/tests/fetch/api/response/response-consume.html create mode 100644 test/wpt/tests/fetch/api/response/response-stream-with-broken-then.any.js create mode 100644 test/wpt/tests/fetch/connection-pool/network-partition-key.html create mode 100644 test/wpt/tests/fetch/connection-pool/resources/network-partition-about-blank-checker.html create mode 100644 test/wpt/tests/fetch/connection-pool/resources/network-partition-checker.html create mode 100644 test/wpt/tests/fetch/connection-pool/resources/network-partition-iframe-checker.html create mode 100644 test/wpt/tests/fetch/connection-pool/resources/network-partition-key.js create mode 100644 test/wpt/tests/fetch/connection-pool/resources/network-partition-key.py create mode 100644 test/wpt/tests/fetch/connection-pool/resources/network-partition-worker-checker.html create mode 100644 test/wpt/tests/fetch/connection-pool/resources/network-partition-worker.js create mode 100644 test/wpt/tests/fetch/content-encoding/resources/bad-gzip-body.py create mode 100644 test/wpt/tests/fetch/content-length/api-and-duplicate-headers.any.js create mode 100644 test/wpt/tests/fetch/content-length/content-length.html create mode 100644 test/wpt/tests/fetch/content-length/content-length.html.headers create mode 100644 test/wpt/tests/fetch/content-length/parsing.window.js create mode 100644 test/wpt/tests/fetch/content-length/resources/content-length.py create mode 100644 test/wpt/tests/fetch/content-length/resources/content-lengths.json create mode 100644 test/wpt/tests/fetch/content-length/resources/identical-duplicates.asis create mode 100644 test/wpt/tests/fetch/content-length/too-long.window.js create mode 100644 test/wpt/tests/fetch/content-type/README.md create mode 100644 test/wpt/tests/fetch/content-type/multipart.window.js create mode 100644 test/wpt/tests/fetch/content-type/resources/content-type.py create mode 100644 test/wpt/tests/fetch/content-type/resources/content-types.json create mode 100644 test/wpt/tests/fetch/content-type/resources/script-content-types.json create mode 100644 test/wpt/tests/fetch/content-type/response.window.js create mode 100644 test/wpt/tests/fetch/content-type/script.window.js create mode 100644 test/wpt/tests/fetch/corb/README.md create mode 100644 test/wpt/tests/fetch/corb/img-html-correctly-labeled.sub-ref.html create mode 100644 test/wpt/tests/fetch/corb/img-html-correctly-labeled.sub.html create mode 100644 test/wpt/tests/fetch/corb/img-mime-types-coverage.tentative.sub.html create mode 100644 test/wpt/tests/fetch/corb/img-png-mislabeled-as-html-nosniff.tentative.sub-ref.html create mode 100644 test/wpt/tests/fetch/corb/img-png-mislabeled-as-html-nosniff.tentative.sub.html create mode 100644 test/wpt/tests/fetch/corb/img-png-mislabeled-as-html.sub-ref.html create mode 100644 test/wpt/tests/fetch/corb/img-png-mislabeled-as-html.sub.html create mode 100644 test/wpt/tests/fetch/corb/img-svg-doctype-html-mimetype-empty.sub.html create mode 100644 test/wpt/tests/fetch/corb/img-svg-doctype-html-mimetype-svg.sub.html create mode 100644 test/wpt/tests/fetch/corb/img-svg-invalid.sub-ref.html create mode 100644 test/wpt/tests/fetch/corb/img-svg-labeled-as-dash.sub.html create mode 100644 test/wpt/tests/fetch/corb/img-svg-labeled-as-svg-xml.sub.html create mode 100644 test/wpt/tests/fetch/corb/img-svg-xml-decl.sub.html create mode 100644 test/wpt/tests/fetch/corb/img-svg.sub-ref.html create mode 100644 test/wpt/tests/fetch/corb/preload-image-png-mislabeled-as-html-nosniff.tentative.sub.html create mode 100644 test/wpt/tests/fetch/corb/resources/css-mislabeled-as-html-nosniff.css create mode 100644 test/wpt/tests/fetch/corb/resources/css-mislabeled-as-html-nosniff.css.headers create mode 100644 test/wpt/tests/fetch/corb/resources/css-mislabeled-as-html.css create mode 100644 test/wpt/tests/fetch/corb/resources/css-mislabeled-as-html.css.headers create mode 100644 test/wpt/tests/fetch/corb/resources/css-with-json-parser-breaker.css create mode 100644 test/wpt/tests/fetch/corb/resources/empty-labeled-as-png.png create mode 100644 test/wpt/tests/fetch/corb/resources/empty-labeled-as-png.png.headers create mode 100644 test/wpt/tests/fetch/corb/resources/html-correctly-labeled.html create mode 100644 test/wpt/tests/fetch/corb/resources/html-correctly-labeled.html.headers create mode 100644 test/wpt/tests/fetch/corb/resources/html-js-polyglot.js create mode 100644 test/wpt/tests/fetch/corb/resources/html-js-polyglot.js.headers create mode 100644 test/wpt/tests/fetch/corb/resources/html-js-polyglot2.js create mode 100644 test/wpt/tests/fetch/corb/resources/html-js-polyglot2.js.headers create mode 100644 test/wpt/tests/fetch/corb/resources/js-mislabeled-as-html-nosniff.js create mode 100644 test/wpt/tests/fetch/corb/resources/js-mislabeled-as-html-nosniff.js.headers create mode 100644 test/wpt/tests/fetch/corb/resources/js-mislabeled-as-html.js create mode 100644 test/wpt/tests/fetch/corb/resources/js-mislabeled-as-html.js.headers create mode 100644 test/wpt/tests/fetch/corb/resources/png-correctly-labeled.png create mode 100644 test/wpt/tests/fetch/corb/resources/png-correctly-labeled.png.headers create mode 100644 test/wpt/tests/fetch/corb/resources/png-mislabeled-as-html-nosniff.png create mode 100644 test/wpt/tests/fetch/corb/resources/png-mislabeled-as-html-nosniff.png.headers create mode 100644 test/wpt/tests/fetch/corb/resources/png-mislabeled-as-html.png create mode 100644 test/wpt/tests/fetch/corb/resources/png-mislabeled-as-html.png.headers create mode 100644 test/wpt/tests/fetch/corb/resources/response_block_probe.js create mode 100644 test/wpt/tests/fetch/corb/resources/response_block_probe.js.headers create mode 100644 test/wpt/tests/fetch/corb/resources/sniffable-resource.py create mode 100644 test/wpt/tests/fetch/corb/resources/subframe-that-posts-html-containing-blob-url-to-parent.html create mode 100644 test/wpt/tests/fetch/corb/resources/svg-doctype-html-mimetype-empty.svg create mode 100644 test/wpt/tests/fetch/corb/resources/svg-doctype-html-mimetype-empty.svg.headers create mode 100644 test/wpt/tests/fetch/corb/resources/svg-doctype-html-mimetype-svg.svg create mode 100644 test/wpt/tests/fetch/corb/resources/svg-doctype-html-mimetype-svg.svg.headers create mode 100644 test/wpt/tests/fetch/corb/resources/svg-labeled-as-dash.svg create mode 100644 test/wpt/tests/fetch/corb/resources/svg-labeled-as-dash.svg.headers create mode 100644 test/wpt/tests/fetch/corb/resources/svg-labeled-as-svg-xml.svg create mode 100644 test/wpt/tests/fetch/corb/resources/svg-labeled-as-svg-xml.svg.headers create mode 100644 test/wpt/tests/fetch/corb/resources/svg-xml-decl.svg create mode 100644 test/wpt/tests/fetch/corb/resources/svg.svg create mode 100644 test/wpt/tests/fetch/corb/resources/svg.svg.headers create mode 100644 test/wpt/tests/fetch/corb/response_block.tentative.sub.https.html create mode 100644 test/wpt/tests/fetch/corb/script-html-correctly-labeled.tentative.sub.html create mode 100644 test/wpt/tests/fetch/corb/script-html-js-polyglot.sub.html create mode 100644 test/wpt/tests/fetch/corb/script-html-via-cross-origin-blob-url.sub.html create mode 100644 test/wpt/tests/fetch/corb/script-js-mislabeled-as-html-nosniff.sub.html create mode 100644 test/wpt/tests/fetch/corb/script-js-mislabeled-as-html.sub.html create mode 100644 test/wpt/tests/fetch/corb/script-resource-with-json-parser-breaker.tentative.sub.html create mode 100644 test/wpt/tests/fetch/corb/script-resource-with-nonsniffable-types.tentative.sub.html create mode 100644 test/wpt/tests/fetch/corb/style-css-mislabeled-as-html-nosniff.sub.html create mode 100644 test/wpt/tests/fetch/corb/style-css-mislabeled-as-html.sub.html create mode 100644 test/wpt/tests/fetch/corb/style-css-with-json-parser-breaker.sub.html create mode 100644 test/wpt/tests/fetch/corb/style-html-correctly-labeled.sub.html create mode 100644 test/wpt/tests/fetch/cross-origin-resource-policy/fetch-in-iframe.html create mode 100644 test/wpt/tests/fetch/cross-origin-resource-policy/fetch.any.js create mode 100644 test/wpt/tests/fetch/cross-origin-resource-policy/fetch.https.any.js create mode 100644 test/wpt/tests/fetch/cross-origin-resource-policy/iframe-loads.html create mode 100644 test/wpt/tests/fetch/cross-origin-resource-policy/image-loads.html create mode 100644 test/wpt/tests/fetch/cross-origin-resource-policy/resources/green.png create mode 100644 test/wpt/tests/fetch/cross-origin-resource-policy/resources/hello.py create mode 100644 test/wpt/tests/fetch/cross-origin-resource-policy/resources/iframe.py create mode 100644 test/wpt/tests/fetch/cross-origin-resource-policy/resources/iframeFetch.html create mode 100644 test/wpt/tests/fetch/cross-origin-resource-policy/resources/image.py create mode 100644 test/wpt/tests/fetch/cross-origin-resource-policy/resources/redirect.py create mode 100644 test/wpt/tests/fetch/cross-origin-resource-policy/resources/script.py create mode 100644 test/wpt/tests/fetch/cross-origin-resource-policy/scheme-restriction.any.js create mode 100644 test/wpt/tests/fetch/cross-origin-resource-policy/scheme-restriction.https.window.js create mode 100644 test/wpt/tests/fetch/cross-origin-resource-policy/script-loads.html create mode 100644 test/wpt/tests/fetch/cross-origin-resource-policy/syntax.any.js create mode 100644 test/wpt/tests/fetch/data-urls/README.md create mode 100644 test/wpt/tests/fetch/h1-parsing/README.md create mode 100644 test/wpt/tests/fetch/h1-parsing/lone-cr.window.js create mode 100644 test/wpt/tests/fetch/h1-parsing/resources-with-0x00-in-header.window.js create mode 100644 test/wpt/tests/fetch/h1-parsing/resources/README.md create mode 100644 test/wpt/tests/fetch/h1-parsing/resources/blue-with-0x00-in-a-header.asis create mode 100644 test/wpt/tests/fetch/h1-parsing/resources/document-with-0x00-in-header.py create mode 100644 test/wpt/tests/fetch/h1-parsing/resources/message.py create mode 100644 test/wpt/tests/fetch/h1-parsing/resources/script-with-0x00-in-header.py create mode 100644 test/wpt/tests/fetch/h1-parsing/resources/status-code.py create mode 100644 test/wpt/tests/fetch/h1-parsing/status-code.window.js create mode 100644 test/wpt/tests/fetch/http-cache/304-update.any.js create mode 100644 test/wpt/tests/fetch/http-cache/README.md create mode 100644 test/wpt/tests/fetch/http-cache/basic-auth-cache-test-ref.html create mode 100644 test/wpt/tests/fetch/http-cache/basic-auth-cache-test.html create mode 100644 test/wpt/tests/fetch/http-cache/cache-mode.any.js create mode 100644 test/wpt/tests/fetch/http-cache/cc-request.any.js create mode 100644 test/wpt/tests/fetch/http-cache/credentials.tentative.any.js create mode 100644 test/wpt/tests/fetch/http-cache/freshness.any.js create mode 100644 test/wpt/tests/fetch/http-cache/heuristic.any.js create mode 100644 test/wpt/tests/fetch/http-cache/http-cache.js create mode 100644 test/wpt/tests/fetch/http-cache/invalidate.any.js create mode 100644 test/wpt/tests/fetch/http-cache/partial.any.js create mode 100644 test/wpt/tests/fetch/http-cache/post-patch.any.js create mode 100644 test/wpt/tests/fetch/http-cache/resources/http-cache.py create mode 100644 test/wpt/tests/fetch/http-cache/resources/securedimage.py create mode 100644 test/wpt/tests/fetch/http-cache/resources/split-cache-popup-with-iframe.html create mode 100644 test/wpt/tests/fetch/http-cache/resources/split-cache-popup.html create mode 100644 test/wpt/tests/fetch/http-cache/split-cache.html create mode 100644 test/wpt/tests/fetch/http-cache/status.any.js create mode 100644 test/wpt/tests/fetch/http-cache/vary.any.js create mode 100644 test/wpt/tests/fetch/images/canvas-remote-read-remote-image-redirect.html create mode 100644 test/wpt/tests/fetch/metadata/META.yml create mode 100644 test/wpt/tests/fetch/metadata/README.md create mode 100644 test/wpt/tests/fetch/metadata/audio-worklet.https.html create mode 100644 test/wpt/tests/fetch/metadata/embed.https.sub.tentative.html create mode 100644 test/wpt/tests/fetch/metadata/fetch-preflight.https.sub.any.js create mode 100644 test/wpt/tests/fetch/metadata/fetch.https.sub.any.js create mode 100644 test/wpt/tests/fetch/metadata/generated/appcache-manifest.https.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/audioworklet.https.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/css-font-face.https.sub.tentative.html create mode 100644 test/wpt/tests/fetch/metadata/generated/css-font-face.sub.tentative.html create mode 100644 test/wpt/tests/fetch/metadata/generated/css-images.https.sub.tentative.html create mode 100644 test/wpt/tests/fetch/metadata/generated/css-images.sub.tentative.html create mode 100644 test/wpt/tests/fetch/metadata/generated/element-a.https.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/element-a.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/element-area.https.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/element-area.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/element-audio.https.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/element-audio.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/element-embed.https.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/element-embed.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/element-frame.https.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/element-frame.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/element-iframe.https.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/element-iframe.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/element-img-environment-change.https.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/element-img-environment-change.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/element-img.https.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/element-img.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/element-input-image.https.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/element-input-image.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/element-link-icon.https.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/element-link-icon.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/element-link-prefetch.https.optional.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/element-link-prefetch.optional.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/element-meta-refresh.https.optional.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/element-meta-refresh.optional.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/element-picture.https.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/element-picture.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/element-script.https.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/element-script.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/element-video-poster.https.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/element-video-poster.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/element-video.https.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/element-video.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/fetch-via-serviceworker.https.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/fetch.https.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/fetch.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/form-submission.https.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/form-submission.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/header-link.https.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/header-link.https.sub.tentative.html create mode 100644 test/wpt/tests/fetch/metadata/generated/header-link.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/header-refresh.https.optional.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/header-refresh.optional.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/script-module-import-dynamic.https.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/script-module-import-dynamic.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/script-module-import-static.https.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/script-module-import-static.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/serviceworker.https.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/svg-image.https.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/svg-image.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/window-history.https.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/window-history.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/window-location.https.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/window-location.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/worker-dedicated-constructor.https.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/worker-dedicated-constructor.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/worker-dedicated-importscripts.https.sub.html create mode 100644 test/wpt/tests/fetch/metadata/generated/worker-dedicated-importscripts.sub.html create mode 100644 test/wpt/tests/fetch/metadata/navigation.https.sub.html create mode 100644 test/wpt/tests/fetch/metadata/object.https.sub.html create mode 100644 test/wpt/tests/fetch/metadata/paint-worklet.https.html create mode 100644 test/wpt/tests/fetch/metadata/portal.https.sub.html create mode 100644 test/wpt/tests/fetch/metadata/preload.https.sub.html create mode 100644 test/wpt/tests/fetch/metadata/redirect/multiple-redirect-https-downgrade-upgrade.sub.html create mode 100644 test/wpt/tests/fetch/metadata/redirect/redirect-http-upgrade.sub.html create mode 100644 test/wpt/tests/fetch/metadata/redirect/redirect-https-downgrade.sub.html create mode 100644 test/wpt/tests/fetch/metadata/report.https.sub.html create mode 100644 test/wpt/tests/fetch/metadata/report.https.sub.html.sub.headers create mode 100644 test/wpt/tests/fetch/metadata/resources/appcache-iframe.sub.html create mode 100644 test/wpt/tests/fetch/metadata/resources/dedicatedWorker.js create mode 100644 test/wpt/tests/fetch/metadata/resources/echo-as-json.py create mode 100644 test/wpt/tests/fetch/metadata/resources/echo-as-script.py create mode 100644 test/wpt/tests/fetch/metadata/resources/es-module.sub.js create mode 100644 test/wpt/tests/fetch/metadata/resources/fetch-via-serviceworker--fallback--sw.js create mode 100644 test/wpt/tests/fetch/metadata/resources/fetch-via-serviceworker--respondWith--sw.js create mode 100644 test/wpt/tests/fetch/metadata/resources/fetch-via-serviceworker-frame.html create mode 100644 test/wpt/tests/fetch/metadata/resources/header-link.py create mode 100644 test/wpt/tests/fetch/metadata/resources/helper.js create mode 100644 test/wpt/tests/fetch/metadata/resources/helper.sub.js create mode 100644 test/wpt/tests/fetch/metadata/resources/message-opener.html create mode 100644 test/wpt/tests/fetch/metadata/resources/post-to-owner.py create mode 100644 test/wpt/tests/fetch/metadata/resources/record-header.py create mode 100644 test/wpt/tests/fetch/metadata/resources/record-headers.py create mode 100644 test/wpt/tests/fetch/metadata/resources/redirectTestHelper.sub.js create mode 100644 test/wpt/tests/fetch/metadata/resources/serviceworker-accessors-frame.html create mode 100644 test/wpt/tests/fetch/metadata/resources/serviceworker-accessors.sw.js create mode 100644 test/wpt/tests/fetch/metadata/resources/sharedWorker.js create mode 100644 test/wpt/tests/fetch/metadata/resources/unload-with-beacon.html create mode 100644 test/wpt/tests/fetch/metadata/resources/xslt-test.sub.xml create mode 100644 test/wpt/tests/fetch/metadata/serviceworker-accessors.https.sub.html create mode 100644 test/wpt/tests/fetch/metadata/sharedworker.https.sub.html create mode 100644 test/wpt/tests/fetch/metadata/style.https.sub.html create mode 100644 test/wpt/tests/fetch/metadata/tools/README.md create mode 100644 test/wpt/tests/fetch/metadata/tools/fetch-metadata.conf.yml create mode 100644 test/wpt/tests/fetch/metadata/tools/generate.py create mode 100644 test/wpt/tests/fetch/metadata/tools/templates/appcache-manifest.sub.https.html create mode 100644 test/wpt/tests/fetch/metadata/tools/templates/audioworklet.https.sub.html create mode 100644 test/wpt/tests/fetch/metadata/tools/templates/css-font-face.sub.html create mode 100644 test/wpt/tests/fetch/metadata/tools/templates/css-images.sub.html create mode 100644 test/wpt/tests/fetch/metadata/tools/templates/element-a.sub.html create mode 100644 test/wpt/tests/fetch/metadata/tools/templates/element-area.sub.html create mode 100644 test/wpt/tests/fetch/metadata/tools/templates/element-audio.sub.html create mode 100644 test/wpt/tests/fetch/metadata/tools/templates/element-embed.sub.html create mode 100644 test/wpt/tests/fetch/metadata/tools/templates/element-frame.sub.html create mode 100644 test/wpt/tests/fetch/metadata/tools/templates/element-iframe.sub.html create mode 100644 test/wpt/tests/fetch/metadata/tools/templates/element-img-environment-change.sub.html create mode 100644 test/wpt/tests/fetch/metadata/tools/templates/element-img.sub.html create mode 100644 test/wpt/tests/fetch/metadata/tools/templates/element-input-image.sub.html create mode 100644 test/wpt/tests/fetch/metadata/tools/templates/element-link-icon.sub.html create mode 100644 test/wpt/tests/fetch/metadata/tools/templates/element-link-prefetch.optional.sub.html create mode 100644 test/wpt/tests/fetch/metadata/tools/templates/element-meta-refresh.optional.sub.html create mode 100644 test/wpt/tests/fetch/metadata/tools/templates/element-picture.sub.html create mode 100644 test/wpt/tests/fetch/metadata/tools/templates/element-script.sub.html create mode 100644 test/wpt/tests/fetch/metadata/tools/templates/element-video-poster.sub.html create mode 100644 test/wpt/tests/fetch/metadata/tools/templates/element-video.sub.html create mode 100644 test/wpt/tests/fetch/metadata/tools/templates/fetch-via-serviceworker.https.sub.html create mode 100644 test/wpt/tests/fetch/metadata/tools/templates/fetch.sub.html create mode 100644 test/wpt/tests/fetch/metadata/tools/templates/form-submission.sub.html create mode 100644 test/wpt/tests/fetch/metadata/tools/templates/header-link.sub.html create mode 100644 test/wpt/tests/fetch/metadata/tools/templates/header-refresh.optional.sub.html create mode 100644 test/wpt/tests/fetch/metadata/tools/templates/script-module-import-dynamic.sub.html create mode 100644 test/wpt/tests/fetch/metadata/tools/templates/script-module-import-static.sub.html create mode 100644 test/wpt/tests/fetch/metadata/tools/templates/serviceworker.https.sub.html create mode 100644 test/wpt/tests/fetch/metadata/tools/templates/svg-image.sub.html create mode 100644 test/wpt/tests/fetch/metadata/tools/templates/window-history.sub.html create mode 100644 test/wpt/tests/fetch/metadata/tools/templates/window-location.sub.html create mode 100644 test/wpt/tests/fetch/metadata/tools/templates/worker-dedicated-constructor.sub.html create mode 100644 test/wpt/tests/fetch/metadata/tools/templates/worker-dedicated-importscripts.sub.html create mode 100644 test/wpt/tests/fetch/metadata/track.https.sub.html create mode 100644 test/wpt/tests/fetch/metadata/trailing-dot.https.sub.any.js create mode 100644 test/wpt/tests/fetch/metadata/unload.https.sub.html create mode 100644 test/wpt/tests/fetch/metadata/window-open.https.sub.html create mode 100644 test/wpt/tests/fetch/metadata/worker.https.sub.html create mode 100644 test/wpt/tests/fetch/metadata/xslt.https.sub.html create mode 100644 test/wpt/tests/fetch/nosniff/image.html create mode 100644 test/wpt/tests/fetch/nosniff/importscripts.html create mode 100644 test/wpt/tests/fetch/nosniff/importscripts.js create mode 100644 test/wpt/tests/fetch/nosniff/parsing-nosniff.window.js create mode 100644 test/wpt/tests/fetch/nosniff/resources/css.py create mode 100644 test/wpt/tests/fetch/nosniff/resources/image.py create mode 100644 test/wpt/tests/fetch/nosniff/resources/js.py create mode 100644 test/wpt/tests/fetch/nosniff/resources/nosniff.py create mode 100644 test/wpt/tests/fetch/nosniff/resources/worker.py create mode 100644 test/wpt/tests/fetch/nosniff/resources/x-content-type-options.json create mode 100644 test/wpt/tests/fetch/nosniff/script.html create mode 100644 test/wpt/tests/fetch/nosniff/stylesheet.html create mode 100644 test/wpt/tests/fetch/nosniff/worker.html create mode 100644 test/wpt/tests/fetch/orb/resources/data.json create mode 100644 test/wpt/tests/fetch/orb/resources/font.ttf create mode 100644 test/wpt/tests/fetch/orb/resources/image.png create mode 100644 test/wpt/tests/fetch/orb/resources/js-unlabeled.js create mode 100644 test/wpt/tests/fetch/orb/resources/png-mislabeled-as-html.png create mode 100644 test/wpt/tests/fetch/orb/resources/png-mislabeled-as-html.png.headers create mode 100644 test/wpt/tests/fetch/orb/resources/png-unlabeled.png create mode 100644 test/wpt/tests/fetch/orb/resources/script.js create mode 100644 test/wpt/tests/fetch/orb/resources/sound.mp3 create mode 100644 test/wpt/tests/fetch/orb/resources/text.txt create mode 100644 test/wpt/tests/fetch/orb/resources/utils.js create mode 100644 test/wpt/tests/fetch/orb/tentative/compressed-image-sniffing.sub.html create mode 100644 test/wpt/tests/fetch/orb/tentative/content-range.sub.any.js create mode 100644 test/wpt/tests/fetch/orb/tentative/img-mime-types-coverage.tentative.sub.html create mode 100644 test/wpt/tests/fetch/orb/tentative/img-png-mislabeled-as-html.sub-ref.html create mode 100644 test/wpt/tests/fetch/orb/tentative/img-png-mislabeled-as-html.sub.html create mode 100644 test/wpt/tests/fetch/orb/tentative/img-png-unlabeled.sub-ref.html create mode 100644 test/wpt/tests/fetch/orb/tentative/img-png-unlabeled.sub.html create mode 100644 test/wpt/tests/fetch/orb/tentative/known-mime-type.sub.any.js create mode 100644 test/wpt/tests/fetch/orb/tentative/nosniff.sub.any.js create mode 100644 test/wpt/tests/fetch/orb/tentative/script-js-unlabeled-gziped.sub.html create mode 100644 test/wpt/tests/fetch/orb/tentative/script-unlabeled.sub.html create mode 100644 test/wpt/tests/fetch/orb/tentative/status.sub.any.js create mode 100644 test/wpt/tests/fetch/orb/tentative/status.sub.html create mode 100644 test/wpt/tests/fetch/orb/tentative/unknown-mime-type.sub.any.js create mode 100644 test/wpt/tests/fetch/origin/assorted.window.js create mode 100644 test/wpt/tests/fetch/origin/resources/redirect-and-stash.py create mode 100644 test/wpt/tests/fetch/origin/resources/referrer-policy.py create mode 100644 test/wpt/tests/fetch/private-network-access/META.yml create mode 100644 test/wpt/tests/fetch/private-network-access/README.md create mode 100644 test/wpt/tests/fetch/private-network-access/fetch-from-treat-as-public.https.window.js create mode 100644 test/wpt/tests/fetch/private-network-access/fetch.https.window.js create mode 100644 test/wpt/tests/fetch/private-network-access/fetch.window.js create mode 100644 test/wpt/tests/fetch/private-network-access/iframe.tentative.https.window.js create mode 100644 test/wpt/tests/fetch/private-network-access/iframe.tentative.window.js create mode 100644 test/wpt/tests/fetch/private-network-access/mixed-content-fetch.tentative.https.window.js create mode 100644 test/wpt/tests/fetch/private-network-access/nested-worker.https.window.js create mode 100644 test/wpt/tests/fetch/private-network-access/nested-worker.window.js create mode 100644 test/wpt/tests/fetch/private-network-access/preflight-cache.https.window.js create mode 100644 test/wpt/tests/fetch/private-network-access/redirect.https.window.js create mode 100644 test/wpt/tests/fetch/private-network-access/resources/executor.html create mode 100644 test/wpt/tests/fetch/private-network-access/resources/fetcher.html create mode 100644 test/wpt/tests/fetch/private-network-access/resources/fetcher.js create mode 100644 test/wpt/tests/fetch/private-network-access/resources/iframed.html create mode 100644 test/wpt/tests/fetch/private-network-access/resources/iframer.html create mode 100644 test/wpt/tests/fetch/private-network-access/resources/preflight.py create mode 100644 test/wpt/tests/fetch/private-network-access/resources/service-worker-bridge.html create mode 100644 test/wpt/tests/fetch/private-network-access/resources/service-worker.js create mode 100644 test/wpt/tests/fetch/private-network-access/resources/shared-fetcher.js create mode 100644 test/wpt/tests/fetch/private-network-access/resources/shared-worker-fetcher.html create mode 100644 test/wpt/tests/fetch/private-network-access/resources/socket-opener.html create mode 100644 test/wpt/tests/fetch/private-network-access/resources/support.sub.js create mode 100644 test/wpt/tests/fetch/private-network-access/resources/worker-blob-fetcher.html create mode 100644 test/wpt/tests/fetch/private-network-access/resources/worker-fetcher.html create mode 100644 test/wpt/tests/fetch/private-network-access/resources/worker-fetcher.js create mode 100644 test/wpt/tests/fetch/private-network-access/resources/xhr-sender.html create mode 100644 test/wpt/tests/fetch/private-network-access/service-worker-background-fetch.https.window.js create mode 100644 test/wpt/tests/fetch/private-network-access/service-worker-fetch.https.window.js create mode 100644 test/wpt/tests/fetch/private-network-access/service-worker-update.https.window.js create mode 100644 test/wpt/tests/fetch/private-network-access/service-worker.https.window.js create mode 100644 test/wpt/tests/fetch/private-network-access/shared-worker-fetch.https.window.js create mode 100644 test/wpt/tests/fetch/private-network-access/shared-worker-fetch.window.js create mode 100644 test/wpt/tests/fetch/private-network-access/shared-worker.https.window.js create mode 100644 test/wpt/tests/fetch/private-network-access/shared-worker.window.js create mode 100644 test/wpt/tests/fetch/private-network-access/websocket.https.window.js create mode 100644 test/wpt/tests/fetch/private-network-access/websocket.window.js create mode 100644 test/wpt/tests/fetch/private-network-access/worker-blob-fetch.window.js create mode 100644 test/wpt/tests/fetch/private-network-access/worker-fetch.https.window.js create mode 100644 test/wpt/tests/fetch/private-network-access/worker-fetch.window.js create mode 100644 test/wpt/tests/fetch/private-network-access/worker.https.window.js create mode 100644 test/wpt/tests/fetch/private-network-access/worker.window.js create mode 100644 test/wpt/tests/fetch/private-network-access/xhr-from-treat-as-public.https.window.js create mode 100644 test/wpt/tests/fetch/private-network-access/xhr.https.window.js create mode 100644 test/wpt/tests/fetch/private-network-access/xhr.window.js create mode 100644 test/wpt/tests/fetch/range/blob.any.js create mode 100644 test/wpt/tests/fetch/range/data.any.js create mode 100644 test/wpt/tests/fetch/range/general.any.js create mode 100644 test/wpt/tests/fetch/range/general.window.js create mode 100644 test/wpt/tests/fetch/range/non-matching-range-response.html create mode 100644 test/wpt/tests/fetch/range/resources/basic.html create mode 100644 test/wpt/tests/fetch/range/resources/long-wav.py create mode 100644 test/wpt/tests/fetch/range/resources/partial-script.py create mode 100644 test/wpt/tests/fetch/range/resources/partial-text.py create mode 100644 test/wpt/tests/fetch/range/resources/range-sw.js create mode 100644 test/wpt/tests/fetch/range/resources/stash-take.py create mode 100644 test/wpt/tests/fetch/range/resources/utils.js create mode 100644 test/wpt/tests/fetch/range/resources/video-with-range.py create mode 100644 test/wpt/tests/fetch/range/sw.https.window.js create mode 100644 test/wpt/tests/fetch/redirect-navigate/302-found-post-handler.py create mode 100644 test/wpt/tests/fetch/redirect-navigate/302-found-post.html create mode 100644 test/wpt/tests/fetch/redirect-navigate/preserve-fragment.html create mode 100644 test/wpt/tests/fetch/redirect-navigate/resources/destination.html create mode 100644 test/wpt/tests/fetch/redirects/data.window.js create mode 100644 test/wpt/tests/fetch/redirects/subresource-fragments.html create mode 100644 test/wpt/tests/fetch/security/1xx-response.any.js create mode 100644 test/wpt/tests/fetch/security/dangling-markup-mitigation-data-url.tentative.sub.html create mode 100644 test/wpt/tests/fetch/security/dangling-markup-mitigation.tentative.html create mode 100644 test/wpt/tests/fetch/security/embedded-credentials.tentative.sub.html create mode 100644 test/wpt/tests/fetch/security/redirect-to-url-with-credentials.https.html create mode 100644 test/wpt/tests/fetch/security/support/embedded-credential-window.sub.html create mode 100644 test/wpt/tests/fetch/stale-while-revalidate/fetch-sw.https.html create mode 100644 test/wpt/tests/fetch/stale-while-revalidate/fetch.any.js create mode 100644 test/wpt/tests/fetch/stale-while-revalidate/resources/stale-css.py create mode 100644 test/wpt/tests/fetch/stale-while-revalidate/resources/stale-image.py create mode 100644 test/wpt/tests/fetch/stale-while-revalidate/resources/stale-script.py create mode 100644 test/wpt/tests/fetch/stale-while-revalidate/revalidate-not-blocked-by-csp.html create mode 100644 test/wpt/tests/fetch/stale-while-revalidate/stale-css.html create mode 100644 test/wpt/tests/fetch/stale-while-revalidate/stale-image.html create mode 100644 test/wpt/tests/fetch/stale-while-revalidate/stale-script.html create mode 100644 test/wpt/tests/fetch/stale-while-revalidate/sw-intercept.js create mode 100644 test/wpt/tests/interfaces/ANGLE_instanced_arrays.idl create mode 100644 test/wpt/tests/interfaces/CSP.idl create mode 100644 test/wpt/tests/interfaces/DOM-Parsing.idl create mode 100644 test/wpt/tests/interfaces/EXT_blend_minmax.idl create mode 100644 test/wpt/tests/interfaces/EXT_clip_cull_distance.idl create mode 100644 test/wpt/tests/interfaces/EXT_color_buffer_float.idl create mode 100644 test/wpt/tests/interfaces/EXT_color_buffer_half_float.idl create mode 100644 test/wpt/tests/interfaces/EXT_disjoint_timer_query.idl create mode 100644 test/wpt/tests/interfaces/EXT_disjoint_timer_query_webgl2.idl create mode 100644 test/wpt/tests/interfaces/EXT_float_blend.idl create mode 100644 test/wpt/tests/interfaces/EXT_frag_depth.idl create mode 100644 test/wpt/tests/interfaces/EXT_sRGB.idl create mode 100644 test/wpt/tests/interfaces/EXT_shader_texture_lod.idl create mode 100644 test/wpt/tests/interfaces/EXT_texture_compression_bptc.idl create mode 100644 test/wpt/tests/interfaces/EXT_texture_compression_rgtc.idl create mode 100644 test/wpt/tests/interfaces/EXT_texture_filter_anisotropic.idl create mode 100644 test/wpt/tests/interfaces/EXT_texture_norm16.idl create mode 100644 test/wpt/tests/interfaces/FedCM.idl create mode 100644 test/wpt/tests/interfaces/IndexedDB.idl create mode 100644 test/wpt/tests/interfaces/KHR_parallel_shader_compile.idl create mode 100644 test/wpt/tests/interfaces/META.yml create mode 100644 test/wpt/tests/interfaces/OES_draw_buffers_indexed.idl create mode 100644 test/wpt/tests/interfaces/OES_element_index_uint.idl create mode 100644 test/wpt/tests/interfaces/OES_fbo_render_mipmap.idl create mode 100644 test/wpt/tests/interfaces/OES_standard_derivatives.idl create mode 100644 test/wpt/tests/interfaces/OES_texture_float.idl create mode 100644 test/wpt/tests/interfaces/OES_texture_float_linear.idl create mode 100644 test/wpt/tests/interfaces/OES_texture_half_float.idl create mode 100644 test/wpt/tests/interfaces/OES_texture_half_float_linear.idl create mode 100644 test/wpt/tests/interfaces/OES_vertex_array_object.idl create mode 100644 test/wpt/tests/interfaces/OVR_multiview2.idl create mode 100644 test/wpt/tests/interfaces/README.md create mode 100644 test/wpt/tests/interfaces/SVG.idl create mode 100644 test/wpt/tests/interfaces/WEBGL_blend_equation_advanced_coherent.idl create mode 100644 test/wpt/tests/interfaces/WEBGL_color_buffer_float.idl create mode 100644 test/wpt/tests/interfaces/WEBGL_compressed_texture_astc.idl create mode 100644 test/wpt/tests/interfaces/WEBGL_compressed_texture_etc.idl create mode 100644 test/wpt/tests/interfaces/WEBGL_compressed_texture_etc1.idl create mode 100644 test/wpt/tests/interfaces/WEBGL_compressed_texture_pvrtc.idl create mode 100644 test/wpt/tests/interfaces/WEBGL_compressed_texture_s3tc.idl create mode 100644 test/wpt/tests/interfaces/WEBGL_compressed_texture_s3tc_srgb.idl create mode 100644 test/wpt/tests/interfaces/WEBGL_debug_renderer_info.idl create mode 100644 test/wpt/tests/interfaces/WEBGL_debug_shaders.idl create mode 100644 test/wpt/tests/interfaces/WEBGL_depth_texture.idl create mode 100644 test/wpt/tests/interfaces/WEBGL_draw_buffers.idl create mode 100644 test/wpt/tests/interfaces/WEBGL_draw_instanced_base_vertex_base_instance.idl create mode 100644 test/wpt/tests/interfaces/WEBGL_lose_context.idl create mode 100644 test/wpt/tests/interfaces/WEBGL_multi_draw.idl create mode 100644 test/wpt/tests/interfaces/WEBGL_multi_draw_instanced_base_vertex_base_instance.idl create mode 100644 test/wpt/tests/interfaces/WebCryptoAPI.idl create mode 100644 test/wpt/tests/interfaces/accelerometer.idl create mode 100644 test/wpt/tests/interfaces/ambient-light.idl create mode 100644 test/wpt/tests/interfaces/anchors.idl create mode 100644 test/wpt/tests/interfaces/attribution-reporting-api.idl create mode 100644 test/wpt/tests/interfaces/audio-output.idl create mode 100644 test/wpt/tests/interfaces/autoplay-detection.idl create mode 100644 test/wpt/tests/interfaces/background-fetch.idl create mode 100644 test/wpt/tests/interfaces/background-sync.idl create mode 100644 test/wpt/tests/interfaces/badging.idl create mode 100644 test/wpt/tests/interfaces/battery-status.idl create mode 100644 test/wpt/tests/interfaces/beacon.idl create mode 100644 test/wpt/tests/interfaces/capture-handle-identity.idl create mode 100644 test/wpt/tests/interfaces/clipboard-apis.idl create mode 100644 test/wpt/tests/interfaces/close-watcher.idl create mode 100644 test/wpt/tests/interfaces/compat.idl create mode 100644 test/wpt/tests/interfaces/compression.idl create mode 100644 test/wpt/tests/interfaces/compute-pressure.idl create mode 100644 test/wpt/tests/interfaces/console.idl create mode 100644 test/wpt/tests/interfaces/contact-picker.idl create mode 100644 test/wpt/tests/interfaces/content-index.idl create mode 100644 test/wpt/tests/interfaces/cookie-store.idl create mode 100644 test/wpt/tests/interfaces/credential-management.idl create mode 100644 test/wpt/tests/interfaces/csp-embedded-enforcement.idl create mode 100644 test/wpt/tests/interfaces/csp-next.idl create mode 100644 test/wpt/tests/interfaces/css-animation-worklet.idl create mode 100644 test/wpt/tests/interfaces/css-animations-2.idl create mode 100644 test/wpt/tests/interfaces/css-animations.idl create mode 100644 test/wpt/tests/interfaces/css-cascade.idl create mode 100644 test/wpt/tests/interfaces/css-color-5.idl create mode 100644 test/wpt/tests/interfaces/css-conditional.idl create mode 100644 test/wpt/tests/interfaces/css-contain-3.idl create mode 100644 test/wpt/tests/interfaces/css-contain.idl create mode 100644 test/wpt/tests/interfaces/css-counter-styles.idl create mode 100644 test/wpt/tests/interfaces/css-font-loading.idl create mode 100644 test/wpt/tests/interfaces/css-fonts.idl create mode 100644 test/wpt/tests/interfaces/css-highlight-api.idl create mode 100644 test/wpt/tests/interfaces/css-images-4.idl create mode 100644 test/wpt/tests/interfaces/css-layout-api.idl create mode 100644 test/wpt/tests/interfaces/css-masking.idl create mode 100644 test/wpt/tests/interfaces/css-nav.idl create mode 100644 test/wpt/tests/interfaces/css-nesting.idl create mode 100644 test/wpt/tests/interfaces/css-paint-api.idl create mode 100644 test/wpt/tests/interfaces/css-parser-api.idl create mode 100644 test/wpt/tests/interfaces/css-properties-values-api.idl create mode 100644 test/wpt/tests/interfaces/css-pseudo.idl create mode 100644 test/wpt/tests/interfaces/css-regions.idl create mode 100644 test/wpt/tests/interfaces/css-shadow-parts.idl create mode 100644 test/wpt/tests/interfaces/css-toggle.tentative.idl create mode 100644 test/wpt/tests/interfaces/css-transitions-2.idl create mode 100644 test/wpt/tests/interfaces/css-transitions.idl create mode 100644 test/wpt/tests/interfaces/css-typed-om.idl create mode 100644 test/wpt/tests/interfaces/css-view-transitions.idl create mode 100644 test/wpt/tests/interfaces/cssom-view.idl create mode 100644 test/wpt/tests/interfaces/cssom.idl create mode 100644 test/wpt/tests/interfaces/custom-state-pseudo-class.idl create mode 100644 test/wpt/tests/interfaces/datacue.idl create mode 100644 test/wpt/tests/interfaces/deprecation-reporting.idl create mode 100644 test/wpt/tests/interfaces/device-memory.idl create mode 100644 test/wpt/tests/interfaces/device-posture.idl create mode 100644 test/wpt/tests/interfaces/digital-goods.idl create mode 100644 test/wpt/tests/interfaces/edit-context.idl create mode 100644 test/wpt/tests/interfaces/element-timing.idl create mode 100644 test/wpt/tests/interfaces/encoding.idl create mode 100644 test/wpt/tests/interfaces/encrypted-media.idl create mode 100644 test/wpt/tests/interfaces/entries-api.idl create mode 100644 test/wpt/tests/interfaces/event-timing.idl create mode 100644 test/wpt/tests/interfaces/eyedropper-api.idl create mode 100644 test/wpt/tests/interfaces/fido.idl create mode 100644 test/wpt/tests/interfaces/file-system-access.idl create mode 100644 test/wpt/tests/interfaces/filter-effects.idl create mode 100644 test/wpt/tests/interfaces/font-metrics-api.idl create mode 100644 test/wpt/tests/interfaces/fs.idl create mode 100644 test/wpt/tests/interfaces/fullscreen.idl create mode 100644 test/wpt/tests/interfaces/gamepad-extensions.idl create mode 100644 test/wpt/tests/interfaces/gamepad.idl create mode 100644 test/wpt/tests/interfaces/generic-sensor.idl create mode 100644 test/wpt/tests/interfaces/geolocation-sensor.idl create mode 100644 test/wpt/tests/interfaces/geolocation.idl create mode 100644 test/wpt/tests/interfaces/geometry.idl create mode 100644 test/wpt/tests/interfaces/get-installed-related-apps.idl create mode 100644 test/wpt/tests/interfaces/gyroscope.idl create mode 100644 test/wpt/tests/interfaces/hr-time.idl create mode 100644 test/wpt/tests/interfaces/html-media-capture.idl create mode 100644 test/wpt/tests/interfaces/idle-detection.idl create mode 100644 test/wpt/tests/interfaces/image-capture.idl create mode 100644 test/wpt/tests/interfaces/image-resource.idl create mode 100644 test/wpt/tests/interfaces/ink-enhancement.idl create mode 100644 test/wpt/tests/interfaces/input-device-capabilities.idl create mode 100644 test/wpt/tests/interfaces/input-events.idl create mode 100644 test/wpt/tests/interfaces/intersection-observer.idl create mode 100644 test/wpt/tests/interfaces/intervention-reporting.idl create mode 100644 test/wpt/tests/interfaces/is-input-pending.idl create mode 100644 test/wpt/tests/interfaces/js-self-profiling.idl create mode 100644 test/wpt/tests/interfaces/keyboard-lock.idl create mode 100644 test/wpt/tests/interfaces/keyboard-map.idl create mode 100644 test/wpt/tests/interfaces/largest-contentful-paint.idl create mode 100644 test/wpt/tests/interfaces/layout-instability.idl create mode 100644 test/wpt/tests/interfaces/local-font-access.idl create mode 100644 test/wpt/tests/interfaces/longtasks.idl create mode 100644 test/wpt/tests/interfaces/magnetometer.idl create mode 100644 test/wpt/tests/interfaces/manifest-incubations.idl create mode 100644 test/wpt/tests/interfaces/mathml-core.idl create mode 100644 test/wpt/tests/interfaces/media-capabilities.idl create mode 100644 test/wpt/tests/interfaces/media-playback-quality.idl create mode 100644 test/wpt/tests/interfaces/media-source.idl create mode 100644 test/wpt/tests/interfaces/mediacapture-automation.idl create mode 100644 test/wpt/tests/interfaces/mediacapture-fromelement.idl create mode 100644 test/wpt/tests/interfaces/mediacapture-handle-actions.idl create mode 100644 test/wpt/tests/interfaces/mediacapture-region.idl create mode 100644 test/wpt/tests/interfaces/mediacapture-streams.idl create mode 100644 test/wpt/tests/interfaces/mediacapture-transform.idl create mode 100644 test/wpt/tests/interfaces/mediacapture-viewport.idl create mode 100644 test/wpt/tests/interfaces/mediasession.idl create mode 100644 test/wpt/tests/interfaces/mediastream-recording.idl create mode 100644 test/wpt/tests/interfaces/model-element.idl create mode 100644 test/wpt/tests/interfaces/mst-content-hint.idl create mode 100644 test/wpt/tests/interfaces/navigation-api.idl create mode 100644 test/wpt/tests/interfaces/navigation-timing.idl create mode 100644 test/wpt/tests/interfaces/netinfo.idl create mode 100644 test/wpt/tests/interfaces/notifications.idl create mode 100644 test/wpt/tests/interfaces/orientation-event.idl create mode 100644 test/wpt/tests/interfaces/orientation-sensor.idl create mode 100644 test/wpt/tests/interfaces/page-lifecycle.idl create mode 100644 test/wpt/tests/interfaces/paint-timing.idl create mode 100644 test/wpt/tests/interfaces/parakeet.tentative.idl create mode 100644 test/wpt/tests/interfaces/payment-handler.idl create mode 100644 test/wpt/tests/interfaces/payment-request.idl create mode 100644 test/wpt/tests/interfaces/performance-measure-memory.idl create mode 100644 test/wpt/tests/interfaces/performance-timeline.idl create mode 100644 test/wpt/tests/interfaces/periodic-background-sync.idl create mode 100644 test/wpt/tests/interfaces/permissions-policy.idl create mode 100644 test/wpt/tests/interfaces/permissions-request.idl create mode 100644 test/wpt/tests/interfaces/permissions-revoke.idl create mode 100644 test/wpt/tests/interfaces/permissions.idl create mode 100644 test/wpt/tests/interfaces/picture-in-picture.idl create mode 100644 test/wpt/tests/interfaces/pointerevents.idl create mode 100644 test/wpt/tests/interfaces/pointerlock.idl create mode 100644 test/wpt/tests/interfaces/portals.idl create mode 100644 test/wpt/tests/interfaces/prefer-current-tab.idl create mode 100644 test/wpt/tests/interfaces/prerendering-revamped.idl create mode 100644 test/wpt/tests/interfaces/presentation-api.idl create mode 100644 test/wpt/tests/interfaces/priority-hints.idl create mode 100644 test/wpt/tests/interfaces/private-click-measurement.idl create mode 100644 test/wpt/tests/interfaces/proximity.idl create mode 100644 test/wpt/tests/interfaces/push-api.idl create mode 100644 test/wpt/tests/interfaces/raw-camera-access.idl create mode 100644 test/wpt/tests/interfaces/remote-playback.idl create mode 100644 test/wpt/tests/interfaces/reporting.idl create mode 100644 test/wpt/tests/interfaces/requestidlecallback.idl create mode 100644 test/wpt/tests/interfaces/resize-observer.idl create mode 100644 test/wpt/tests/interfaces/resource-timing.idl create mode 100644 test/wpt/tests/interfaces/sanitizer-api.idl create mode 100644 test/wpt/tests/interfaces/sanitizer-api.tentative.idl create mode 100644 test/wpt/tests/interfaces/savedata.idl create mode 100644 test/wpt/tests/interfaces/scheduling-apis.idl create mode 100644 test/wpt/tests/interfaces/screen-capture.idl create mode 100644 test/wpt/tests/interfaces/screen-orientation.idl create mode 100644 test/wpt/tests/interfaces/screen-wake-lock.idl create mode 100644 test/wpt/tests/interfaces/scroll-animations.idl create mode 100644 test/wpt/tests/interfaces/scroll-to-text-fragment.idl create mode 100644 test/wpt/tests/interfaces/secure-payment-confirmation.idl create mode 100644 test/wpt/tests/interfaces/selection-api.idl create mode 100644 test/wpt/tests/interfaces/serial.idl create mode 100644 test/wpt/tests/interfaces/server-timing.idl create mode 100644 test/wpt/tests/interfaces/service-workers.idl create mode 100644 test/wpt/tests/interfaces/shape-detection-api.idl create mode 100644 test/wpt/tests/interfaces/speech-api.idl create mode 100644 test/wpt/tests/interfaces/storage-access.idl create mode 100644 test/wpt/tests/interfaces/storage-buckets.tentative.idl create mode 100644 test/wpt/tests/interfaces/storage.idl create mode 100644 test/wpt/tests/interfaces/streams.idl create mode 100644 test/wpt/tests/interfaces/sub-apps.tentative.idl create mode 100644 test/wpt/tests/interfaces/svg-animations.idl create mode 100644 test/wpt/tests/interfaces/testutils.idl create mode 100644 test/wpt/tests/interfaces/text-detection-api.idl create mode 100644 test/wpt/tests/interfaces/touch-events.idl create mode 100644 test/wpt/tests/interfaces/trusted-types.idl create mode 100644 test/wpt/tests/interfaces/ua-client-hints.idl create mode 100644 test/wpt/tests/interfaces/uievents.idl create mode 100644 test/wpt/tests/interfaces/urlpattern.idl create mode 100644 test/wpt/tests/interfaces/user-timing.idl create mode 100644 test/wpt/tests/interfaces/vibration.idl create mode 100644 test/wpt/tests/interfaces/video-rvfc.idl create mode 100644 test/wpt/tests/interfaces/virtual-keyboard.idl create mode 100644 test/wpt/tests/interfaces/virtual-keyboard.tentative.idl create mode 100644 test/wpt/tests/interfaces/wai-aria.idl create mode 100644 test/wpt/tests/interfaces/wasm-js-api.idl create mode 100644 test/wpt/tests/interfaces/wasm-web-api.idl create mode 100644 test/wpt/tests/interfaces/web-animations-2.idl create mode 100644 test/wpt/tests/interfaces/web-animations.idl create mode 100644 test/wpt/tests/interfaces/web-app-launch.idl create mode 100644 test/wpt/tests/interfaces/web-bluetooth.idl create mode 100644 test/wpt/tests/interfaces/web-locks.idl create mode 100644 test/wpt/tests/interfaces/web-nfc.idl create mode 100644 test/wpt/tests/interfaces/web-otp.idl create mode 100644 test/wpt/tests/interfaces/web-share.idl create mode 100644 test/wpt/tests/interfaces/webaudio.idl create mode 100644 test/wpt/tests/interfaces/webauthn.idl create mode 100644 test/wpt/tests/interfaces/webcodecs-aac-codec-registration.idl create mode 100644 test/wpt/tests/interfaces/webcodecs-avc-codec-registration.idl create mode 100644 test/wpt/tests/interfaces/webcodecs-flac-codec-registration.idl create mode 100644 test/wpt/tests/interfaces/webcodecs-hevc-codec-registration.idl create mode 100644 test/wpt/tests/interfaces/webcodecs-opus-codec-registration.idl create mode 100644 test/wpt/tests/interfaces/webcodecs.idl create mode 100644 test/wpt/tests/interfaces/webcrypto-secure-curves.idl create mode 100644 test/wpt/tests/interfaces/webdriver.idl create mode 100644 test/wpt/tests/interfaces/webgl1.idl create mode 100644 test/wpt/tests/interfaces/webgl2.idl create mode 100644 test/wpt/tests/interfaces/webgpu.idl create mode 100644 test/wpt/tests/interfaces/webhid.idl create mode 100644 test/wpt/tests/interfaces/webidl.idl create mode 100644 test/wpt/tests/interfaces/webmidi.idl create mode 100644 test/wpt/tests/interfaces/webnn.idl create mode 100644 test/wpt/tests/interfaces/webrtc-encoded-transform.idl create mode 100644 test/wpt/tests/interfaces/webrtc-ice.idl create mode 100644 test/wpt/tests/interfaces/webrtc-identity.idl create mode 100644 test/wpt/tests/interfaces/webrtc-priority.idl create mode 100644 test/wpt/tests/interfaces/webrtc-stats.idl create mode 100644 test/wpt/tests/interfaces/webrtc-svc.idl create mode 100644 test/wpt/tests/interfaces/webrtc.idl create mode 100644 test/wpt/tests/interfaces/webtransport.idl create mode 100644 test/wpt/tests/interfaces/webusb.idl create mode 100644 test/wpt/tests/interfaces/webvr.tentative.idl create mode 100644 test/wpt/tests/interfaces/webvtt.idl create mode 100644 test/wpt/tests/interfaces/webxr-ar-module.idl create mode 100644 test/wpt/tests/interfaces/webxr-depth-sensing.idl create mode 100644 test/wpt/tests/interfaces/webxr-dom-overlays.idl create mode 100644 test/wpt/tests/interfaces/webxr-gamepads-module.idl create mode 100644 test/wpt/tests/interfaces/webxr-hand-input.idl create mode 100644 test/wpt/tests/interfaces/webxr-hit-test.idl create mode 100644 test/wpt/tests/interfaces/webxr-lighting-estimation.idl create mode 100644 test/wpt/tests/interfaces/webxr.idl create mode 100644 test/wpt/tests/interfaces/webxrlayers.idl create mode 100644 test/wpt/tests/interfaces/window-controls-overlay.idl create mode 100644 test/wpt/tests/interfaces/window-placement.idl create mode 100644 test/wpt/tests/interfaces/xhr.idl create mode 100644 test/wpt/tests/mimesniff/META.yml create mode 100644 test/wpt/tests/mimesniff/README.md create mode 100644 test/wpt/tests/mimesniff/media/media-sniff.window.js create mode 100644 test/wpt/tests/mimesniff/media/resources/flac.flac create mode 100644 test/wpt/tests/mimesniff/media/resources/make-vectors.sh create mode 100644 test/wpt/tests/mimesniff/media/resources/mp3-raw.mp3 create mode 100644 test/wpt/tests/mimesniff/media/resources/mp3-with-id3.mp3 create mode 100644 test/wpt/tests/mimesniff/media/resources/mp4.mp4 create mode 100644 test/wpt/tests/mimesniff/media/resources/ogg.ogg create mode 100644 test/wpt/tests/mimesniff/media/resources/wav.wav create mode 100644 test/wpt/tests/mimesniff/media/resources/webm.webm create mode 100644 test/wpt/tests/mimesniff/mime-types/README.md create mode 100644 test/wpt/tests/mimesniff/mime-types/charset-parameter.window.js create mode 100644 test/wpt/tests/mimesniff/mime-types/resources/generated-mime-types.py create mode 100644 test/wpt/tests/mimesniff/mime-types/resources/mime-charset.py create mode 100644 test/wpt/tests/mimesniff/mime-types/resources/mime-groups.json create mode 100644 test/wpt/tests/resources/.htaccess create mode 100644 test/wpt/tests/resources/META.yml create mode 100644 test/wpt/tests/resources/SVGAnimationTestCase-testharness.js create mode 100644 test/wpt/tests/resources/accesskey.js create mode 100644 test/wpt/tests/resources/blank.html create mode 100644 test/wpt/tests/resources/channel.sub.js create mode 100644 test/wpt/tests/resources/check-layout-th.js create mode 100644 test/wpt/tests/resources/check-layout.js create mode 100644 test/wpt/tests/resources/chromium/README.md create mode 100644 test/wpt/tests/resources/chromium/contacts_manager_mock.js create mode 100644 test/wpt/tests/resources/chromium/content-index-helpers.js create mode 100644 test/wpt/tests/resources/chromium/enable-hyperlink-auditing.js create mode 100644 test/wpt/tests/resources/chromium/fake-hid.js create mode 100644 test/wpt/tests/resources/chromium/fake-serial.js create mode 100644 test/wpt/tests/resources/chromium/generic_sensor_mocks.js create mode 100644 test/wpt/tests/resources/chromium/generic_sensor_mocks.js.headers create mode 100644 test/wpt/tests/resources/chromium/mock-barcodedetection.js create mode 100644 test/wpt/tests/resources/chromium/mock-barcodedetection.js.headers create mode 100644 test/wpt/tests/resources/chromium/mock-direct-sockets.js create mode 100644 test/wpt/tests/resources/chromium/mock-facedetection.js create mode 100644 test/wpt/tests/resources/chromium/mock-facedetection.js.headers create mode 100644 test/wpt/tests/resources/chromium/mock-idle-detection.js create mode 100644 test/wpt/tests/resources/chromium/mock-imagecapture.js create mode 100644 test/wpt/tests/resources/chromium/mock-managed-config.js create mode 100644 test/wpt/tests/resources/chromium/mock-pressure-service.js create mode 100644 test/wpt/tests/resources/chromium/mock-pressure-service.js.headers create mode 100644 test/wpt/tests/resources/chromium/mock-subapps.js create mode 100644 test/wpt/tests/resources/chromium/mock-textdetection.js create mode 100644 test/wpt/tests/resources/chromium/mock-textdetection.js.headers create mode 100644 test/wpt/tests/resources/chromium/nfc-mock.js create mode 100644 test/wpt/tests/resources/chromium/web-bluetooth-test.js create mode 100644 test/wpt/tests/resources/chromium/web-bluetooth-test.js.headers create mode 100644 test/wpt/tests/resources/chromium/webusb-child-test.js create mode 100644 test/wpt/tests/resources/chromium/webusb-child-test.js.headers create mode 100644 test/wpt/tests/resources/chromium/webusb-test.js create mode 100644 test/wpt/tests/resources/chromium/webusb-test.js.headers create mode 100644 test/wpt/tests/resources/chromium/webxr-test-math-helper.js create mode 100644 test/wpt/tests/resources/chromium/webxr-test-math-helper.js.headers create mode 100644 test/wpt/tests/resources/chromium/webxr-test.js create mode 100644 test/wpt/tests/resources/chromium/webxr-test.js.headers create mode 100644 test/wpt/tests/resources/declarative-shadow-dom-polyfill.js create mode 100644 test/wpt/tests/resources/idlharness-shadowrealm.js create mode 100644 test/wpt/tests/resources/idlharness.js.headers create mode 100644 test/wpt/tests/resources/readme.md create mode 100644 test/wpt/tests/resources/sriharness.js create mode 100644 test/wpt/tests/resources/test-only-api.js create mode 100644 test/wpt/tests/resources/test-only-api.js.headers create mode 100644 test/wpt/tests/resources/test-only-api.m.js create mode 100644 test/wpt/tests/resources/test-only-api.m.js.headers create mode 100644 test/wpt/tests/resources/test/README.md create mode 100644 test/wpt/tests/resources/test/conftest.py create mode 100644 test/wpt/tests/resources/test/harness.html create mode 100644 test/wpt/tests/resources/test/idl-helper.js create mode 100644 test/wpt/tests/resources/test/nested-testharness.js create mode 100644 test/wpt/tests/resources/test/requirements.txt create mode 100644 test/wpt/tests/resources/test/tests/functional/abortsignal.html create mode 100644 test/wpt/tests/resources/test/tests/functional/add_cleanup.html create mode 100644 test/wpt/tests/resources/test/tests/functional/add_cleanup_async.html create mode 100644 test/wpt/tests/resources/test/tests/functional/add_cleanup_async_bad_return.html create mode 100644 test/wpt/tests/resources/test/tests/functional/add_cleanup_async_rejection.html create mode 100644 test/wpt/tests/resources/test/tests/functional/add_cleanup_async_rejection_after_load.html create mode 100644 test/wpt/tests/resources/test/tests/functional/add_cleanup_async_timeout.html create mode 100644 test/wpt/tests/resources/test/tests/functional/add_cleanup_bad_return.html create mode 100644 test/wpt/tests/resources/test/tests/functional/add_cleanup_count.html create mode 100644 test/wpt/tests/resources/test/tests/functional/add_cleanup_err.html create mode 100644 test/wpt/tests/resources/test/tests/functional/add_cleanup_err_multi.html create mode 100644 test/wpt/tests/resources/test/tests/functional/add_cleanup_sync_queue.html create mode 100644 test/wpt/tests/resources/test/tests/functional/api-tests-1.html create mode 100644 test/wpt/tests/resources/test/tests/functional/api-tests-2.html create mode 100644 test/wpt/tests/resources/test/tests/functional/api-tests-3.html create mode 100644 test/wpt/tests/resources/test/tests/functional/assert-array-equals.html create mode 100644 test/wpt/tests/resources/test/tests/functional/force_timeout.html create mode 100644 test/wpt/tests/resources/test/tests/functional/generate-callback.html create mode 100644 test/wpt/tests/resources/test/tests/functional/idlharness/IdlDictionary/test_partial_interface_of.html create mode 100644 test/wpt/tests/resources/test/tests/functional/idlharness/IdlInterface/test_exposed_wildcard.html create mode 100644 test/wpt/tests/resources/test/tests/functional/idlharness/IdlInterface/test_immutable_prototype.html create mode 100644 test/wpt/tests/resources/test/tests/functional/idlharness/IdlInterface/test_interface_mixin.html create mode 100644 test/wpt/tests/resources/test/tests/functional/idlharness/IdlInterface/test_partial_interface_of.html create mode 100644 test/wpt/tests/resources/test/tests/functional/idlharness/IdlInterface/test_primary_interface_of.html create mode 100644 test/wpt/tests/resources/test/tests/functional/idlharness/IdlInterface/test_to_json_operation.html create mode 100644 test/wpt/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_attribute.html create mode 100644 test/wpt/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_operation.html create mode 100644 test/wpt/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_partial_namespace.html create mode 100644 test/wpt/tests/resources/test/tests/functional/iframe-callback.html create mode 100644 test/wpt/tests/resources/test/tests/functional/iframe-consolidate-errors.html create mode 100644 test/wpt/tests/resources/test/tests/functional/iframe-consolidate-tests.html create mode 100644 test/wpt/tests/resources/test/tests/functional/iframe-msg.html create mode 100644 test/wpt/tests/resources/test/tests/functional/log-insertion.html create mode 100644 test/wpt/tests/resources/test/tests/functional/no-title.html create mode 100644 test/wpt/tests/resources/test/tests/functional/order.html create mode 100644 test/wpt/tests/resources/test/tests/functional/promise-async.html create mode 100644 test/wpt/tests/resources/test/tests/functional/promise-with-sync.html create mode 100644 test/wpt/tests/resources/test/tests/functional/promise.html create mode 100644 test/wpt/tests/resources/test/tests/functional/queue.html create mode 100644 test/wpt/tests/resources/test/tests/functional/setup-function-worker.js create mode 100644 test/wpt/tests/resources/test/tests/functional/setup-worker-service.html create mode 100644 test/wpt/tests/resources/test/tests/functional/single-page-test-fail.html create mode 100644 test/wpt/tests/resources/test/tests/functional/single-page-test-no-assertions.html create mode 100644 test/wpt/tests/resources/test/tests/functional/single-page-test-no-body.html create mode 100644 test/wpt/tests/resources/test/tests/functional/single-page-test-pass.html create mode 100644 test/wpt/tests/resources/test/tests/functional/step_wait.html create mode 100644 test/wpt/tests/resources/test/tests/functional/step_wait_func.html create mode 100644 test/wpt/tests/resources/test/tests/functional/task-scheduling-promise-test.html create mode 100644 test/wpt/tests/resources/test/tests/functional/task-scheduling-test.html create mode 100644 test/wpt/tests/resources/test/tests/functional/uncaught-exception-handle.html create mode 100644 test/wpt/tests/resources/test/tests/functional/uncaught-exception-ignore.html create mode 100644 test/wpt/tests/resources/test/tests/functional/worker-dedicated-uncaught-allow.html create mode 100644 test/wpt/tests/resources/test/tests/functional/worker-dedicated-uncaught-single.html create mode 100644 test/wpt/tests/resources/test/tests/functional/worker-dedicated.sub.html create mode 100644 test/wpt/tests/resources/test/tests/functional/worker-error.js create mode 100644 test/wpt/tests/resources/test/tests/functional/worker-service.html create mode 100644 test/wpt/tests/resources/test/tests/functional/worker-shared.html create mode 100644 test/wpt/tests/resources/test/tests/functional/worker-uncaught-allow.js create mode 100644 test/wpt/tests/resources/test/tests/functional/worker-uncaught-single.js create mode 100644 test/wpt/tests/resources/test/tests/functional/worker.js create mode 100644 test/wpt/tests/resources/test/tests/unit/IdlArray/is_json_type.html create mode 100644 test/wpt/tests/resources/test/tests/unit/IdlDictionary/get_reverse_inheritance_stack.html create mode 100644 test/wpt/tests/resources/test/tests/unit/IdlDictionary/test_partial_dictionary.html create mode 100644 test/wpt/tests/resources/test/tests/unit/IdlInterface/constructors.html create mode 100644 test/wpt/tests/resources/test/tests/unit/IdlInterface/default_to_json_operation.html create mode 100644 test/wpt/tests/resources/test/tests/unit/IdlInterface/do_member_unscopable_asserts.html create mode 100644 test/wpt/tests/resources/test/tests/unit/IdlInterface/get_interface_object.html create mode 100644 test/wpt/tests/resources/test/tests/unit/IdlInterface/get_interface_object_owner.html create mode 100644 test/wpt/tests/resources/test/tests/unit/IdlInterface/get_legacy_namespace.html create mode 100644 test/wpt/tests/resources/test/tests/unit/IdlInterface/get_qualified_name.html create mode 100644 test/wpt/tests/resources/test/tests/unit/IdlInterface/get_reverse_inheritance_stack.html create mode 100644 test/wpt/tests/resources/test/tests/unit/IdlInterface/has_default_to_json_regular_operation.html create mode 100644 test/wpt/tests/resources/test/tests/unit/IdlInterface/has_to_json_regular_operation.html create mode 100644 test/wpt/tests/resources/test/tests/unit/IdlInterface/should_have_interface_object.html create mode 100644 test/wpt/tests/resources/test/tests/unit/IdlInterface/test_primary_interface_of_undefined.html create mode 100644 test/wpt/tests/resources/test/tests/unit/IdlInterfaceMember/is_to_json_regular_operation.html create mode 100644 test/wpt/tests/resources/test/tests/unit/IdlInterfaceMember/toString.html create mode 100644 test/wpt/tests/resources/test/tests/unit/assert_implements.html create mode 100644 test/wpt/tests/resources/test/tests/unit/assert_implements_optional.html create mode 100644 test/wpt/tests/resources/test/tests/unit/assert_object_equals.html create mode 100644 test/wpt/tests/resources/test/tests/unit/async-test-return-restrictions.html create mode 100644 test/wpt/tests/resources/test/tests/unit/basic.html create mode 100644 test/wpt/tests/resources/test/tests/unit/exceptional-cases-timeouts.html create mode 100644 test/wpt/tests/resources/test/tests/unit/exceptional-cases.html create mode 100644 test/wpt/tests/resources/test/tests/unit/format-value.html create mode 100644 test/wpt/tests/resources/test/tests/unit/helpers.js create mode 100644 test/wpt/tests/resources/test/tests/unit/late-test.html create mode 100644 test/wpt/tests/resources/test/tests/unit/promise_setup-timeout.html create mode 100644 test/wpt/tests/resources/test/tests/unit/promise_setup.html create mode 100644 test/wpt/tests/resources/test/tests/unit/single_test.html create mode 100644 test/wpt/tests/resources/test/tests/unit/test-return-restrictions.html create mode 100644 test/wpt/tests/resources/test/tests/unit/throwing-assertions.html create mode 100644 test/wpt/tests/resources/test/tests/unit/unpaired-surrogates.html create mode 100644 test/wpt/tests/resources/test/tox.ini create mode 100644 test/wpt/tests/resources/test/wptserver.py create mode 100644 test/wpt/tests/resources/testdriver-actions.js create mode 100644 test/wpt/tests/resources/testdriver-vendor.js create mode 100644 test/wpt/tests/resources/testdriver-vendor.js.headers create mode 100644 test/wpt/tests/resources/testdriver.js create mode 100644 test/wpt/tests/resources/testdriver.js.headers create mode 100644 test/wpt/tests/resources/testharness.js create mode 100644 test/wpt/tests/resources/testharness.js.headers create mode 100644 test/wpt/tests/resources/testharnessreport.js create mode 100644 test/wpt/tests/resources/testharnessreport.js.headers create mode 100644 test/wpt/tests/resources/webidl2/build.sh create mode 100644 test/wpt/tests/resources/webidl2/lib/README.md create mode 100644 test/wpt/tests/resources/webidl2/lib/VERSION.md create mode 100644 test/wpt/tests/resources/webidl2/lib/webidl2.js.headers create mode 100644 test/wpt/tests/websockets/META.yml create mode 100644 test/wpt/tests/websockets/README.md create mode 100644 test/wpt/tests/websockets/Send-data.worker.js create mode 100644 test/wpt/tests/websockets/binary/001.html create mode 100644 test/wpt/tests/websockets/binary/002.html create mode 100644 test/wpt/tests/websockets/binary/004.html create mode 100644 test/wpt/tests/websockets/binary/005.html create mode 100644 test/wpt/tests/websockets/closing-handshake/002.html create mode 100644 test/wpt/tests/websockets/closing-handshake/003.html create mode 100644 test/wpt/tests/websockets/closing-handshake/004.html create mode 100644 test/wpt/tests/websockets/constructor/001.html create mode 100644 test/wpt/tests/websockets/constructor/002.html create mode 100644 test/wpt/tests/websockets/constructor/004.html create mode 100644 test/wpt/tests/websockets/constructor/005.html create mode 100644 test/wpt/tests/websockets/constructor/006.html create mode 100644 test/wpt/tests/websockets/constructor/007.html create mode 100644 test/wpt/tests/websockets/constructor/008.html create mode 100644 test/wpt/tests/websockets/constructor/009.html create mode 100644 test/wpt/tests/websockets/constructor/010.html create mode 100644 test/wpt/tests/websockets/constructor/011.html create mode 100644 test/wpt/tests/websockets/constructor/012.html create mode 100644 test/wpt/tests/websockets/constructor/013.html create mode 100644 test/wpt/tests/websockets/constructor/014.html create mode 100644 test/wpt/tests/websockets/constructor/016.html create mode 100644 test/wpt/tests/websockets/constructor/017.html create mode 100644 test/wpt/tests/websockets/constructor/018.html create mode 100644 test/wpt/tests/websockets/constructor/019.html create mode 100644 test/wpt/tests/websockets/constructor/020.html create mode 100644 test/wpt/tests/websockets/constructor/021.html create mode 100644 test/wpt/tests/websockets/constructor/022.html create mode 100644 test/wpt/tests/websockets/cookies/001.html create mode 100644 test/wpt/tests/websockets/cookies/002.html create mode 100644 test/wpt/tests/websockets/cookies/003.html create mode 100644 test/wpt/tests/websockets/cookies/004.html create mode 100644 test/wpt/tests/websockets/cookies/005.html create mode 100644 test/wpt/tests/websockets/cookies/006.html create mode 100644 test/wpt/tests/websockets/cookies/007.html create mode 100644 test/wpt/tests/websockets/cookies/support/set-cookie.py create mode 100644 test/wpt/tests/websockets/cookies/support/websocket-cookies-helper.sub.js create mode 100644 test/wpt/tests/websockets/cookies/third-party-cookie-accepted.https.html create mode 100644 test/wpt/tests/websockets/extended-payload-length.html create mode 100644 test/wpt/tests/websockets/handlers/basic_auth_wsh.py create mode 100644 test/wpt/tests/websockets/handlers/delayed-passive-close_wsh.py create mode 100644 test/wpt/tests/websockets/handlers/echo-cookie_wsh.py create mode 100644 test/wpt/tests/websockets/handlers/echo-query_v13_wsh.py create mode 100644 test/wpt/tests/websockets/handlers/echo-query_wsh.py create mode 100644 test/wpt/tests/websockets/handlers/echo_close_data_wsh.py create mode 100644 test/wpt/tests/websockets/handlers/echo_exit_wsh.py create mode 100644 test/wpt/tests/websockets/handlers/echo_raw_wsh.py create mode 100644 test/wpt/tests/websockets/handlers/echo_wsh.py create mode 100644 test/wpt/tests/websockets/handlers/empty-message_wsh.py create mode 100644 test/wpt/tests/websockets/handlers/handshake_no_extensions_wsh.py create mode 100644 test/wpt/tests/websockets/handlers/handshake_no_protocol_wsh.py create mode 100644 test/wpt/tests/websockets/handlers/handshake_protocol_wsh.py create mode 100644 test/wpt/tests/websockets/handlers/handshake_sleep_2_wsh.py create mode 100644 test/wpt/tests/websockets/handlers/invalid_wsh.py create mode 100644 test/wpt/tests/websockets/handlers/msg_channel_wsh.py create mode 100644 test/wpt/tests/websockets/handlers/origin_wsh.py create mode 100644 test/wpt/tests/websockets/handlers/protocol_array_wsh.py create mode 100644 test/wpt/tests/websockets/handlers/protocol_wsh.py create mode 100644 test/wpt/tests/websockets/handlers/receive-backpressure_wsh.py create mode 100644 test/wpt/tests/websockets/handlers/referrer_wsh.py create mode 100644 test/wpt/tests/websockets/handlers/send-backpressure_wsh.py create mode 100644 test/wpt/tests/websockets/handlers/set-cookie-secure_wsh.py create mode 100644 test/wpt/tests/websockets/handlers/set-cookie_http_wsh.py create mode 100644 test/wpt/tests/websockets/handlers/set-cookie_wsh.py create mode 100644 test/wpt/tests/websockets/handlers/set-cookies-samesite_wsh.py create mode 100644 test/wpt/tests/websockets/handlers/simple_handshake_wsh.py create mode 100644 test/wpt/tests/websockets/handlers/sleep_10_v13_wsh.py create mode 100644 test/wpt/tests/websockets/handlers/stash_responder_blocking_wsh.py create mode 100644 test/wpt/tests/websockets/handlers/stash_responder_wsh.py create mode 100644 test/wpt/tests/websockets/handlers/wrong_accept_key_wsh.py create mode 100644 test/wpt/tests/websockets/interfaces/CloseEvent/clean-close.html create mode 100644 test/wpt/tests/websockets/interfaces/CloseEvent/constructor.html create mode 100644 test/wpt/tests/websockets/interfaces/CloseEvent/historical.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-arraybuffer.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-blob.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-defineProperty-getter.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-defineProperty-setter.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-deleting.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-getting.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-initial.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-large.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-readonly.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/bufferedAmount/bufferedAmount-unicode.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/close/close-basic.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/close/close-connecting.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/close/close-multiple.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/close/close-nested.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/close/close-replace.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/close/close-return.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/constants/001.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/constants/002.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/constants/003.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/constants/004.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/constants/005.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/constants/006.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/events/001.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/events/002.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/events/003.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/events/004.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/events/006.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/events/007.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/events/008.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/events/009.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/events/010.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/events/011.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/events/012.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/events/013.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/events/014.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/events/015.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/events/016.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/events/017.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/events/018.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/events/019.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/events/020.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/extensions/001.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/protocol/protocol-initial.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/readyState/001.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/readyState/002.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/readyState/003.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/readyState/004.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/readyState/005.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/readyState/006.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/readyState/007.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/readyState/008.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/send/001.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/send/002.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/send/003.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/send/004.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/send/005.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/send/006.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/send/007.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/send/008.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/send/009.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/send/010.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/send/011.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/send/012.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/url/001.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/url/002.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/url/003.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/url/004.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/url/005.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/url/006.html create mode 100644 test/wpt/tests/websockets/interfaces/WebSocket/url/resolve.html create mode 100644 test/wpt/tests/websockets/keeping-connection-open/001.html create mode 100644 test/wpt/tests/websockets/multi-globals/message-received.html create mode 100644 test/wpt/tests/websockets/multi-globals/support/incumbent.sub.html create mode 100644 test/wpt/tests/websockets/multi-globals/support/relevant.html create mode 100644 test/wpt/tests/websockets/opening-handshake/001.html create mode 100644 test/wpt/tests/websockets/opening-handshake/002.html create mode 100644 test/wpt/tests/websockets/opening-handshake/003-sets-origin.worker.js create mode 100644 test/wpt/tests/websockets/opening-handshake/003.html create mode 100644 test/wpt/tests/websockets/opening-handshake/005.html create mode 100644 test/wpt/tests/websockets/remove-own-iframe-during-onerror.window.js create mode 100644 test/wpt/tests/websockets/security/001.html create mode 100644 test/wpt/tests/websockets/security/002.html create mode 100644 test/wpt/tests/websockets/security/check.py create mode 100644 test/wpt/tests/websockets/stream/tentative/README.md create mode 100644 test/wpt/tests/websockets/stream/tentative/abort.any.js create mode 100644 test/wpt/tests/websockets/stream/tentative/backpressure-receive.any.js create mode 100644 test/wpt/tests/websockets/stream/tentative/backpressure-send.any.js create mode 100644 test/wpt/tests/websockets/stream/tentative/close.any.js create mode 100644 test/wpt/tests/websockets/stream/tentative/constructor.any.js create mode 100644 test/wpt/tests/websockets/stream/tentative/resources/url-constants.js create mode 100644 test/wpt/tests/websockets/unload-a-document/001-1.html create mode 100644 test/wpt/tests/websockets/unload-a-document/001-2.html create mode 100644 test/wpt/tests/websockets/unload-a-document/001.html create mode 100644 test/wpt/tests/websockets/unload-a-document/002-1.html create mode 100644 test/wpt/tests/websockets/unload-a-document/002-2.html create mode 100644 test/wpt/tests/websockets/unload-a-document/002.html create mode 100644 test/wpt/tests/websockets/unload-a-document/003.html create mode 100644 test/wpt/tests/websockets/unload-a-document/004.html create mode 100644 test/wpt/tests/websockets/unload-a-document/005-1.html create mode 100644 test/wpt/tests/websockets/unload-a-document/005.html create mode 100644 test/wpt/tests/xhr/META.yml create mode 100644 test/wpt/tests/xhr/README.md create mode 100644 test/wpt/tests/xhr/XMLHttpRequest-withCredentials.any.js create mode 100644 test/wpt/tests/xhr/abort-after-receive.any.js create mode 100644 test/wpt/tests/xhr/abort-after-send.any.js create mode 100644 test/wpt/tests/xhr/abort-after-stop.window.js create mode 100644 test/wpt/tests/xhr/abort-after-timeout.any.js create mode 100644 test/wpt/tests/xhr/abort-during-done.window.js create mode 100644 test/wpt/tests/xhr/abort-during-headers-received.window.js create mode 100644 test/wpt/tests/xhr/abort-during-loading.window.js create mode 100644 test/wpt/tests/xhr/abort-during-open.any.js create mode 100644 test/wpt/tests/xhr/abort-during-readystatechange.any.js create mode 100644 test/wpt/tests/xhr/abort-during-unsent.any.js create mode 100644 test/wpt/tests/xhr/abort-during-upload.any.js create mode 100644 test/wpt/tests/xhr/abort-event-abort.any.js create mode 100644 test/wpt/tests/xhr/abort-event-listeners.any.js create mode 100644 test/wpt/tests/xhr/abort-event-loadend.any.js create mode 100644 test/wpt/tests/xhr/abort-event-order.htm create mode 100644 test/wpt/tests/xhr/abort-upload-event-abort.any.js create mode 100644 test/wpt/tests/xhr/abort-upload-event-loadend.any.js create mode 100644 test/wpt/tests/xhr/access-control-and-redirects-async-same-origin.any.js create mode 100644 test/wpt/tests/xhr/access-control-and-redirects-async.any.js create mode 100644 test/wpt/tests/xhr/access-control-and-redirects.any.js create mode 100644 test/wpt/tests/xhr/access-control-basic-allow-access-control-origin-header-data-url.htm create mode 100644 test/wpt/tests/xhr/access-control-basic-allow-access-control-origin-header.any.js create mode 100644 test/wpt/tests/xhr/access-control-basic-allow-async.any.js create mode 100644 test/wpt/tests/xhr/access-control-basic-allow-non-cors-safelisted-method-async.any.js create mode 100644 test/wpt/tests/xhr/access-control-basic-allow-non-cors-safelisted-method.any.js create mode 100644 test/wpt/tests/xhr/access-control-basic-allow-preflight-cache-invalidation-by-header.any.js create mode 100644 test/wpt/tests/xhr/access-control-basic-allow-preflight-cache-invalidation-by-method.any.js create mode 100644 test/wpt/tests/xhr/access-control-basic-allow-preflight-cache-timeout.any.js create mode 100644 test/wpt/tests/xhr/access-control-basic-allow-preflight-cache.any.js create mode 100644 test/wpt/tests/xhr/access-control-basic-allow-star.any.js create mode 100644 test/wpt/tests/xhr/access-control-basic-allow.any.js create mode 100644 test/wpt/tests/xhr/access-control-basic-cors-safelisted-request-headers.htm create mode 100644 test/wpt/tests/xhr/access-control-basic-cors-safelisted-response-headers.htm create mode 100644 test/wpt/tests/xhr/access-control-basic-denied.htm create mode 100644 test/wpt/tests/xhr/access-control-basic-get-fail-non-simple.htm create mode 100644 test/wpt/tests/xhr/access-control-basic-non-cors-safelisted-content-type.htm create mode 100644 test/wpt/tests/xhr/access-control-basic-post-success-no-content-type.htm create mode 100644 test/wpt/tests/xhr/access-control-basic-post-with-non-cors-safelisted-content-type.htm create mode 100644 test/wpt/tests/xhr/access-control-basic-preflight-denied.htm create mode 100644 test/wpt/tests/xhr/access-control-expose-headers-on-redirect.html create mode 100644 test/wpt/tests/xhr/access-control-preflight-async-header-denied.htm create mode 100644 test/wpt/tests/xhr/access-control-preflight-async-method-denied.htm create mode 100644 test/wpt/tests/xhr/access-control-preflight-async-not-supported.htm create mode 100644 test/wpt/tests/xhr/access-control-preflight-credential-async.htm create mode 100644 test/wpt/tests/xhr/access-control-preflight-credential-sync.htm create mode 100644 test/wpt/tests/xhr/access-control-preflight-headers-async.htm create mode 100644 test/wpt/tests/xhr/access-control-preflight-headers-sync.htm create mode 100644 test/wpt/tests/xhr/access-control-preflight-request-allow-headers-returns-star.any.js create mode 100644 test/wpt/tests/xhr/access-control-preflight-request-header-lowercase.htm create mode 100644 test/wpt/tests/xhr/access-control-preflight-request-header-returns-origin.any.js create mode 100644 test/wpt/tests/xhr/access-control-preflight-request-header-sorted.htm create mode 100644 test/wpt/tests/xhr/access-control-preflight-request-headers-origin.htm create mode 100644 test/wpt/tests/xhr/access-control-preflight-request-invalid-status-301.htm create mode 100644 test/wpt/tests/xhr/access-control-preflight-request-invalid-status-400.htm create mode 100644 test/wpt/tests/xhr/access-control-preflight-request-invalid-status-501.htm create mode 100644 test/wpt/tests/xhr/access-control-preflight-request-must-not-contain-cookie.htm create mode 100644 test/wpt/tests/xhr/access-control-preflight-sync-header-denied.htm create mode 100644 test/wpt/tests/xhr/access-control-preflight-sync-method-denied.htm create mode 100644 test/wpt/tests/xhr/access-control-preflight-sync-not-supported.htm create mode 100644 test/wpt/tests/xhr/access-control-recursive-failed-request.htm create mode 100644 test/wpt/tests/xhr/access-control-response-with-body-sync.htm create mode 100644 test/wpt/tests/xhr/access-control-response-with-body.htm create mode 100644 test/wpt/tests/xhr/access-control-response-with-exposed-headers.htm create mode 100644 test/wpt/tests/xhr/access-control-sandboxed-iframe-allow-origin-null.htm create mode 100644 test/wpt/tests/xhr/access-control-sandboxed-iframe-allow.htm create mode 100644 test/wpt/tests/xhr/access-control-sandboxed-iframe-denied-without-wildcard.htm create mode 100644 test/wpt/tests/xhr/access-control-sandboxed-iframe-denied.htm create mode 100644 test/wpt/tests/xhr/allow-lists-starting-with-comma.htm create mode 100644 test/wpt/tests/xhr/anonymous-mode-unsupported.htm create mode 100644 test/wpt/tests/xhr/close-worker-with-xhr-in-progress.html create mode 100644 test/wpt/tests/xhr/content-type-unmodified.any.js create mode 100644 test/wpt/tests/xhr/cookies.http.html create mode 100644 test/wpt/tests/xhr/cors-expose-star.sub.any.js create mode 100644 test/wpt/tests/xhr/cors-upload.any.js create mode 100644 test/wpt/tests/xhr/data-uri.htm create mode 100644 test/wpt/tests/xhr/event-abort.any.js create mode 100644 test/wpt/tests/xhr/event-error-order.sub.html create mode 100644 test/wpt/tests/xhr/event-error.sub.any.js create mode 100644 test/wpt/tests/xhr/event-load.any.js create mode 100644 test/wpt/tests/xhr/event-loadend.any.js create mode 100644 test/wpt/tests/xhr/event-loadstart-upload.any.js create mode 100644 test/wpt/tests/xhr/event-loadstart.any.js create mode 100644 test/wpt/tests/xhr/event-progress.any.js create mode 100644 test/wpt/tests/xhr/event-readystate-sync-open.any.js create mode 100644 test/wpt/tests/xhr/event-readystatechange-loaded.any.js create mode 100644 test/wpt/tests/xhr/event-timeout-order.any.js create mode 100644 test/wpt/tests/xhr/event-timeout.any.js create mode 100644 test/wpt/tests/xhr/event-upload-progress-crossorigin.any.js create mode 100644 test/wpt/tests/xhr/event-upload-progress.any.js create mode 100644 test/wpt/tests/xhr/firing-events-http-content-length.html create mode 100644 test/wpt/tests/xhr/firing-events-http-no-content-length.html create mode 100644 test/wpt/tests/xhr/folder.txt create mode 100644 test/wpt/tests/xhr/formdata.html create mode 100644 test/wpt/tests/xhr/formdata/append-formelement.html create mode 100644 test/wpt/tests/xhr/formdata/constructor-formelement.html create mode 100644 test/wpt/tests/xhr/formdata/constructor-submitter.html create mode 100644 test/wpt/tests/xhr/formdata/delete-formelement.html create mode 100644 test/wpt/tests/xhr/formdata/get-formelement.html create mode 100644 test/wpt/tests/xhr/formdata/has-formelement.html create mode 100644 test/wpt/tests/xhr/formdata/iteration.any.js create mode 100644 test/wpt/tests/xhr/formdata/set-formelement.html create mode 100644 test/wpt/tests/xhr/getallresponseheaders-cookies.htm create mode 100644 test/wpt/tests/xhr/getallresponseheaders-status.htm create mode 100644 test/wpt/tests/xhr/getallresponseheaders.htm create mode 100644 test/wpt/tests/xhr/getresponseheader-case-insensitive.htm create mode 100644 test/wpt/tests/xhr/getresponseheader-chunked-trailer.htm create mode 100644 test/wpt/tests/xhr/getresponseheader-cookies-and-more.htm create mode 100644 test/wpt/tests/xhr/getresponseheader-error-state.htm create mode 100644 test/wpt/tests/xhr/getresponseheader-server-date.htm create mode 100644 test/wpt/tests/xhr/getresponseheader-special-characters.htm create mode 100644 test/wpt/tests/xhr/getresponseheader-unsent-opened-state.htm create mode 100644 test/wpt/tests/xhr/getresponseheader.any.js create mode 100644 test/wpt/tests/xhr/header-user-agent-async.htm create mode 100644 test/wpt/tests/xhr/header-user-agent-sync.htm create mode 100644 test/wpt/tests/xhr/headers-normalize-response.htm create mode 100644 test/wpt/tests/xhr/historical.html create mode 100644 test/wpt/tests/xhr/idlharness.any.js create mode 100644 test/wpt/tests/xhr/json.any.js create mode 100644 test/wpt/tests/xhr/loadstart-and-state.html create mode 100644 test/wpt/tests/xhr/open-after-abort.htm create mode 100644 test/wpt/tests/xhr/open-after-setrequestheader.htm create mode 100644 test/wpt/tests/xhr/open-after-stop.window.js create mode 100644 test/wpt/tests/xhr/open-during-abort-event.htm create mode 100644 test/wpt/tests/xhr/open-during-abort-processing.htm create mode 100644 test/wpt/tests/xhr/open-during-abort.htm create mode 100644 test/wpt/tests/xhr/open-method-bogus.htm create mode 100644 test/wpt/tests/xhr/open-method-case-insensitive.htm create mode 100644 test/wpt/tests/xhr/open-method-case-sensitive.htm create mode 100644 test/wpt/tests/xhr/open-method-insecure.htm create mode 100644 test/wpt/tests/xhr/open-method-responsetype-set-sync.htm create mode 100644 test/wpt/tests/xhr/open-open-send.htm create mode 100644 test/wpt/tests/xhr/open-open-sync-send.htm create mode 100644 test/wpt/tests/xhr/open-parameters-toString.htm create mode 100644 test/wpt/tests/xhr/open-referer.htm create mode 100644 test/wpt/tests/xhr/open-send-during-abort.htm create mode 100644 test/wpt/tests/xhr/open-send-open.htm create mode 100644 test/wpt/tests/xhr/open-sync-open-send.htm create mode 100644 test/wpt/tests/xhr/open-url-about-blank-window.htm create mode 100644 test/wpt/tests/xhr/open-url-base-inserted-after-open.htm create mode 100644 test/wpt/tests/xhr/open-url-base-inserted.htm create mode 100644 test/wpt/tests/xhr/open-url-base.htm create mode 100644 test/wpt/tests/xhr/open-url-encoding.htm create mode 100644 test/wpt/tests/xhr/open-url-fragment.htm create mode 100644 test/wpt/tests/xhr/open-url-javascript-window-2.htm create mode 100644 test/wpt/tests/xhr/open-url-javascript-window.htm create mode 100644 test/wpt/tests/xhr/open-url-multi-window-2.htm create mode 100644 test/wpt/tests/xhr/open-url-multi-window-3.htm create mode 100644 test/wpt/tests/xhr/open-url-multi-window-4.htm create mode 100644 test/wpt/tests/xhr/open-url-multi-window-5.htm create mode 100644 test/wpt/tests/xhr/open-url-multi-window-6.htm create mode 100644 test/wpt/tests/xhr/open-url-multi-window.htm create mode 100644 test/wpt/tests/xhr/open-url-redirected-sharedworker-origin.htm create mode 100644 test/wpt/tests/xhr/open-url-redirected-worker-origin.htm create mode 100644 test/wpt/tests/xhr/open-url-worker-origin.htm create mode 100644 test/wpt/tests/xhr/open-url-worker-simple.htm create mode 100644 test/wpt/tests/xhr/open-user-password-non-same-origin.htm create mode 100644 test/wpt/tests/xhr/over-1-meg.any.js create mode 100644 test/wpt/tests/xhr/overridemimetype-blob.html create mode 100644 test/wpt/tests/xhr/overridemimetype-done-state.any.js create mode 100644 test/wpt/tests/xhr/overridemimetype-edge-cases.window.js create mode 100644 test/wpt/tests/xhr/overridemimetype-headers-received-state-force-shiftjis.htm create mode 100644 test/wpt/tests/xhr/overridemimetype-invalid-mime-type.htm create mode 100644 test/wpt/tests/xhr/overridemimetype-loading-state.htm create mode 100644 test/wpt/tests/xhr/overridemimetype-open-state-force-utf-8.htm create mode 100644 test/wpt/tests/xhr/overridemimetype-open-state-force-xml.htm create mode 100644 test/wpt/tests/xhr/overridemimetype-unsent-state-force-shiftjis.any.js create mode 100644 test/wpt/tests/xhr/preserve-ua-header-on-redirect.htm create mode 100644 test/wpt/tests/xhr/progress-events-response-data-gzip.htm create mode 100644 test/wpt/tests/xhr/progressevent-constructor.html create mode 100644 test/wpt/tests/xhr/progressevent-interface.html create mode 100644 test/wpt/tests/xhr/request-content-length.any.js create mode 100644 test/wpt/tests/xhr/resources/accept-language.py create mode 100644 test/wpt/tests/xhr/resources/accept.py create mode 100644 test/wpt/tests/xhr/resources/access-control-allow-lists.py create mode 100644 test/wpt/tests/xhr/resources/access-control-allow-with-body.py create mode 100644 test/wpt/tests/xhr/resources/access-control-auth-basic.py create mode 100644 test/wpt/tests/xhr/resources/access-control-basic-allow-no-credentials.py create mode 100644 test/wpt/tests/xhr/resources/access-control-basic-allow-star.py create mode 100644 test/wpt/tests/xhr/resources/access-control-basic-allow.py create mode 100644 test/wpt/tests/xhr/resources/access-control-basic-cors-safelisted-request-headers.py create mode 100644 test/wpt/tests/xhr/resources/access-control-basic-cors-safelisted-response-headers.py create mode 100644 test/wpt/tests/xhr/resources/access-control-basic-denied.py create mode 100644 test/wpt/tests/xhr/resources/access-control-basic-options-not-supported.py create mode 100644 test/wpt/tests/xhr/resources/access-control-basic-preflight-cache-invalidation.py create mode 100644 test/wpt/tests/xhr/resources/access-control-basic-preflight-cache-timeout.py create mode 100644 test/wpt/tests/xhr/resources/access-control-basic-preflight-cache.py create mode 100644 test/wpt/tests/xhr/resources/access-control-basic-put-allow.py create mode 100644 test/wpt/tests/xhr/resources/access-control-cookie.py create mode 100644 test/wpt/tests/xhr/resources/access-control-origin-header.py create mode 100644 test/wpt/tests/xhr/resources/access-control-preflight-denied.py create mode 100644 test/wpt/tests/xhr/resources/access-control-preflight-request-allow-headers-returns-star.py create mode 100644 test/wpt/tests/xhr/resources/access-control-preflight-request-header-lowercase.py create mode 100644 test/wpt/tests/xhr/resources/access-control-preflight-request-header-returns-origin.py create mode 100644 test/wpt/tests/xhr/resources/access-control-preflight-request-header-sorted.py create mode 100644 test/wpt/tests/xhr/resources/access-control-preflight-request-headers-origin.py create mode 100644 test/wpt/tests/xhr/resources/access-control-preflight-request-invalid-status.py create mode 100644 test/wpt/tests/xhr/resources/access-control-preflight-request-must-not-contain-cookie.py create mode 100644 test/wpt/tests/xhr/resources/access-control-sandboxed-iframe.html create mode 100644 test/wpt/tests/xhr/resources/auth1/auth.py create mode 100644 test/wpt/tests/xhr/resources/auth10/auth.py create mode 100644 test/wpt/tests/xhr/resources/auth11/auth.py create mode 100644 test/wpt/tests/xhr/resources/auth2/auth.py create mode 100644 test/wpt/tests/xhr/resources/auth2/corsenabled.py create mode 100644 test/wpt/tests/xhr/resources/auth3/auth.py create mode 100644 test/wpt/tests/xhr/resources/auth4/auth.py create mode 100644 test/wpt/tests/xhr/resources/auth5/auth.py create mode 100644 test/wpt/tests/xhr/resources/auth6/auth.py create mode 100644 test/wpt/tests/xhr/resources/auth7/corsenabled.py create mode 100644 test/wpt/tests/xhr/resources/auth8/corsenabled-no-authorize.py create mode 100644 test/wpt/tests/xhr/resources/auth9/auth.py create mode 100644 test/wpt/tests/xhr/resources/authentication.py create mode 100644 test/wpt/tests/xhr/resources/bad-chunk-encoding.py create mode 100644 test/wpt/tests/xhr/resources/base.xml create mode 100644 test/wpt/tests/xhr/resources/chunked.py create mode 100644 test/wpt/tests/xhr/resources/conditional.py create mode 100644 test/wpt/tests/xhr/resources/content.py create mode 100644 test/wpt/tests/xhr/resources/corsenabled.py create mode 100644 test/wpt/tests/xhr/resources/delay.py create mode 100644 test/wpt/tests/xhr/resources/echo-content-cors.py create mode 100644 test/wpt/tests/xhr/resources/echo-content-type.py create mode 100644 test/wpt/tests/xhr/resources/echo-headers.py create mode 100644 test/wpt/tests/xhr/resources/echo-method.py create mode 100644 test/wpt/tests/xhr/resources/empty-div-utf8-html.py create mode 100644 test/wpt/tests/xhr/resources/folder.txt create mode 100644 test/wpt/tests/xhr/resources/form.py create mode 100644 test/wpt/tests/xhr/resources/get-set-cookie.py create mode 100644 test/wpt/tests/xhr/resources/gzip.py create mode 100644 test/wpt/tests/xhr/resources/header-user-agent.py create mode 100644 test/wpt/tests/xhr/resources/headers.asis create mode 100644 test/wpt/tests/xhr/resources/headers.py create mode 100644 test/wpt/tests/xhr/resources/image.gif create mode 100644 test/wpt/tests/xhr/resources/img-utf8-html.py create mode 100644 test/wpt/tests/xhr/resources/img.jpg create mode 100644 test/wpt/tests/xhr/resources/infinite-redirects.py create mode 100644 test/wpt/tests/xhr/resources/init.htm create mode 100644 test/wpt/tests/xhr/resources/inspect-headers.py create mode 100644 test/wpt/tests/xhr/resources/invalid-utf8-html.py create mode 100644 test/wpt/tests/xhr/resources/last-modified.py create mode 100644 test/wpt/tests/xhr/resources/no-custom-header-on-preflight.py create mode 100644 test/wpt/tests/xhr/resources/nocors/folder.txt create mode 100644 test/wpt/tests/xhr/resources/over-1-meg.txt create mode 100644 test/wpt/tests/xhr/resources/parse-headers.py create mode 100644 test/wpt/tests/xhr/resources/pass.txt create mode 100644 test/wpt/tests/xhr/resources/redirect-cors.py create mode 100644 test/wpt/tests/xhr/resources/redirect.py create mode 100644 test/wpt/tests/xhr/resources/requri.py create mode 100644 test/wpt/tests/xhr/resources/reset-token.py create mode 100644 test/wpt/tests/xhr/resources/responseType-document-in-worker.js create mode 100644 test/wpt/tests/xhr/resources/responseXML-unavailable-in-worker.js create mode 100644 test/wpt/tests/xhr/resources/send-after-setting-document-domain-window-1.htm create mode 100644 test/wpt/tests/xhr/resources/send-after-setting-document-domain-window-2.htm create mode 100644 test/wpt/tests/xhr/resources/send-after-setting-document-domain-window-helper.js create mode 100644 test/wpt/tests/xhr/resources/shift-jis-html.py create mode 100644 test/wpt/tests/xhr/resources/status.py create mode 100644 test/wpt/tests/xhr/resources/top.txt create mode 100644 test/wpt/tests/xhr/resources/trickle.py create mode 100644 test/wpt/tests/xhr/resources/upload.py create mode 100644 test/wpt/tests/xhr/resources/utf16.txt create mode 100644 test/wpt/tests/xhr/resources/well-formed.xml create mode 100644 test/wpt/tests/xhr/resources/win-1252-html.py create mode 100644 test/wpt/tests/xhr/resources/win-1252-xml.py create mode 100644 test/wpt/tests/xhr/resources/workerxhr-origin-referrer.js create mode 100644 test/wpt/tests/xhr/resources/workerxhr-simple.js create mode 100644 test/wpt/tests/xhr/resources/xmlhttprequest-event-order.js create mode 100644 test/wpt/tests/xhr/resources/xmlhttprequest-timeout-aborted.js create mode 100644 test/wpt/tests/xhr/resources/xmlhttprequest-timeout-abortedonmain.js create mode 100644 test/wpt/tests/xhr/resources/xmlhttprequest-timeout-overrides.js create mode 100644 test/wpt/tests/xhr/resources/xmlhttprequest-timeout-overridesexpires.js create mode 100644 test/wpt/tests/xhr/resources/xmlhttprequest-timeout-runner.js create mode 100644 test/wpt/tests/xhr/resources/xmlhttprequest-timeout-simple.js create mode 100644 test/wpt/tests/xhr/resources/xmlhttprequest-timeout-synconmain.js create mode 100644 test/wpt/tests/xhr/resources/xmlhttprequest-timeout-synconworker.js create mode 100644 test/wpt/tests/xhr/resources/xmlhttprequest-timeout-twice.js create mode 100644 test/wpt/tests/xhr/resources/xmlhttprequest-timeout.js create mode 100644 test/wpt/tests/xhr/resources/zlib.py create mode 100644 test/wpt/tests/xhr/response-body-errors.any.js create mode 100644 test/wpt/tests/xhr/response-data-arraybuffer.htm create mode 100644 test/wpt/tests/xhr/response-data-blob.htm create mode 100644 test/wpt/tests/xhr/response-data-deflate.htm create mode 100644 test/wpt/tests/xhr/response-data-gzip.htm create mode 100644 test/wpt/tests/xhr/response-data-progress.htm create mode 100644 test/wpt/tests/xhr/response-invalid-responsetype.htm create mode 100644 test/wpt/tests/xhr/response-json.htm create mode 100644 test/wpt/tests/xhr/response-method.htm create mode 100644 test/wpt/tests/xhr/responseText-status.html create mode 100644 test/wpt/tests/xhr/responseType-document-in-worker.html create mode 100644 test/wpt/tests/xhr/responseXML-unavailable-in-worker.html create mode 100644 test/wpt/tests/xhr/responsedocument-decoding.htm create mode 100644 test/wpt/tests/xhr/responsetext-decoding.htm create mode 100644 test/wpt/tests/xhr/responsetype.any.js create mode 100644 test/wpt/tests/xhr/responseurl.html create mode 100644 test/wpt/tests/xhr/responsexml-basic.htm create mode 100644 test/wpt/tests/xhr/responsexml-document-properties.htm create mode 100644 test/wpt/tests/xhr/responsexml-get-twice.htm create mode 100644 test/wpt/tests/xhr/responsexml-media-type.htm create mode 100644 test/wpt/tests/xhr/responsexml-non-document-types.htm create mode 100644 test/wpt/tests/xhr/responsexml-non-well-formed.htm create mode 100644 test/wpt/tests/xhr/security-consideration.sub.html create mode 100644 test/wpt/tests/xhr/send-accept-language.htm create mode 100644 test/wpt/tests/xhr/send-accept.htm create mode 100644 test/wpt/tests/xhr/send-after-setting-document-domain.htm create mode 100644 test/wpt/tests/xhr/send-authentication-basic-cors-not-enabled.htm create mode 100644 test/wpt/tests/xhr/send-authentication-basic-cors.htm create mode 100644 test/wpt/tests/xhr/send-authentication-basic-repeat-no-args.htm create mode 100644 test/wpt/tests/xhr/send-authentication-basic-setrequestheader-and-arguments.htm create mode 100644 test/wpt/tests/xhr/send-authentication-basic-setrequestheader-existing-session.htm create mode 100644 test/wpt/tests/xhr/send-authentication-basic-setrequestheader.htm create mode 100644 test/wpt/tests/xhr/send-authentication-basic.htm create mode 100644 test/wpt/tests/xhr/send-authentication-competing-names-passwords.htm create mode 100644 test/wpt/tests/xhr/send-authentication-cors-basic-setrequestheader.htm create mode 100644 test/wpt/tests/xhr/send-authentication-cors-setrequestheader-no-cred.htm create mode 100644 test/wpt/tests/xhr/send-authentication-existing-session-manual.htm create mode 100644 test/wpt/tests/xhr/send-authentication-prompt-2-manual.htm create mode 100644 test/wpt/tests/xhr/send-authentication-prompt-manual.htm create mode 100644 test/wpt/tests/xhr/send-blob-with-no-mime-type.html create mode 100644 test/wpt/tests/xhr/send-conditional-cors.htm create mode 100644 test/wpt/tests/xhr/send-conditional.htm create mode 100644 test/wpt/tests/xhr/send-content-type-charset.htm create mode 100644 test/wpt/tests/xhr/send-content-type-string.htm create mode 100644 test/wpt/tests/xhr/send-data-arraybuffer.any.js create mode 100644 test/wpt/tests/xhr/send-data-arraybufferview.any.js create mode 100644 test/wpt/tests/xhr/send-data-blob.htm create mode 100644 test/wpt/tests/xhr/send-data-es-object.any.js create mode 100644 test/wpt/tests/xhr/send-data-formdata.any.js create mode 100644 test/wpt/tests/xhr/send-data-sharedarraybuffer.any.js create mode 100644 test/wpt/tests/xhr/send-data-string-invalid-unicode.any.js create mode 100644 test/wpt/tests/xhr/send-data-unexpected-tostring.htm create mode 100644 test/wpt/tests/xhr/send-entity-body-basic.htm create mode 100644 test/wpt/tests/xhr/send-entity-body-document-bogus.htm create mode 100644 test/wpt/tests/xhr/send-entity-body-document.htm create mode 100644 test/wpt/tests/xhr/send-entity-body-empty.htm create mode 100644 test/wpt/tests/xhr/send-entity-body-get-head-async.htm create mode 100644 test/wpt/tests/xhr/send-entity-body-get-head.htm create mode 100644 test/wpt/tests/xhr/send-entity-body-none.htm create mode 100644 test/wpt/tests/xhr/send-network-error-async-events.sub.htm create mode 100644 test/wpt/tests/xhr/send-network-error-sync-events.sub.htm create mode 100644 test/wpt/tests/xhr/send-no-response-event-loadend.htm create mode 100644 test/wpt/tests/xhr/send-no-response-event-loadstart.htm create mode 100644 test/wpt/tests/xhr/send-no-response-event-order.htm create mode 100644 test/wpt/tests/xhr/send-non-same-origin.htm create mode 100644 test/wpt/tests/xhr/send-receive-utf16.htm create mode 100644 test/wpt/tests/xhr/send-redirect-bogus-sync.htm create mode 100644 test/wpt/tests/xhr/send-redirect-bogus.htm create mode 100644 test/wpt/tests/xhr/send-redirect-infinite-sync.htm create mode 100644 test/wpt/tests/xhr/send-redirect-infinite.htm create mode 100644 test/wpt/tests/xhr/send-redirect-no-location.htm create mode 100644 test/wpt/tests/xhr/send-redirect-post-upload.htm create mode 100644 test/wpt/tests/xhr/send-redirect-to-cors.htm create mode 100644 test/wpt/tests/xhr/send-redirect-to-non-cors.htm create mode 100644 test/wpt/tests/xhr/send-redirect.htm create mode 100644 test/wpt/tests/xhr/send-response-event-order.htm create mode 100644 test/wpt/tests/xhr/send-response-upload-event-loadend.htm create mode 100644 test/wpt/tests/xhr/send-response-upload-event-loadstart.htm create mode 100644 test/wpt/tests/xhr/send-response-upload-event-progress.htm create mode 100644 test/wpt/tests/xhr/send-send.any.js create mode 100644 test/wpt/tests/xhr/send-sync-blocks-async.htm create mode 100644 test/wpt/tests/xhr/send-sync-no-response-event-load.htm create mode 100644 test/wpt/tests/xhr/send-sync-no-response-event-loadend.htm create mode 100644 test/wpt/tests/xhr/send-sync-no-response-event-order.htm create mode 100644 test/wpt/tests/xhr/send-sync-response-event-order.htm create mode 100644 test/wpt/tests/xhr/send-sync-timeout.htm create mode 100644 test/wpt/tests/xhr/send-timeout-events.htm create mode 100644 test/wpt/tests/xhr/send-usp.any.js create mode 100644 test/wpt/tests/xhr/setrequestheader-after-send.htm create mode 100644 test/wpt/tests/xhr/setrequestheader-allow-empty-value.htm create mode 100644 test/wpt/tests/xhr/setrequestheader-allow-whitespace-in-value.htm create mode 100644 test/wpt/tests/xhr/setrequestheader-before-open.htm create mode 100644 test/wpt/tests/xhr/setrequestheader-bogus-name.htm create mode 100644 test/wpt/tests/xhr/setrequestheader-bogus-value.htm create mode 100644 test/wpt/tests/xhr/setrequestheader-case-insensitive.htm create mode 100644 test/wpt/tests/xhr/setrequestheader-combining.window.js create mode 100644 test/wpt/tests/xhr/setrequestheader-content-type.htm create mode 100644 test/wpt/tests/xhr/setrequestheader-header-allowed.htm create mode 100644 test/wpt/tests/xhr/setrequestheader-header-forbidden.htm create mode 100644 test/wpt/tests/xhr/setrequestheader-open-setrequestheader.htm create mode 100644 test/wpt/tests/xhr/status-async.htm create mode 100644 test/wpt/tests/xhr/status-basic.htm create mode 100644 test/wpt/tests/xhr/status-error.htm create mode 100644 test/wpt/tests/xhr/status.h2.window.js create mode 100644 test/wpt/tests/xhr/sync-no-progress.any.js create mode 100644 test/wpt/tests/xhr/sync-no-timeout.any.js create mode 100644 test/wpt/tests/xhr/sync-xhr-and-window-onload.html create mode 100644 test/wpt/tests/xhr/sync-xhr-supported-by-feature-policy.html create mode 100644 test/wpt/tests/xhr/template-element.html create mode 100644 test/wpt/tests/xhr/thrown-error-in-events.html create mode 100644 test/wpt/tests/xhr/timeout-cors-async.htm create mode 100644 test/wpt/tests/xhr/timeout-multiple-fetches.html create mode 100644 test/wpt/tests/xhr/timeout-sync.htm create mode 100644 test/wpt/tests/xhr/xhr-authorization-redirect.any.js create mode 100644 test/wpt/tests/xhr/xhr-timeout-longtask.any.js create mode 100644 test/wpt/tests/xhr/xmlhttprequest-basic.htm create mode 100644 test/wpt/tests/xhr/xmlhttprequest-eventtarget.htm create mode 100644 test/wpt/tests/xhr/xmlhttprequest-network-error-sync.htm create mode 100644 test/wpt/tests/xhr/xmlhttprequest-network-error.htm create mode 100644 test/wpt/tests/xhr/xmlhttprequest-sync-block-defer-scripts-subframe.html create mode 100644 test/wpt/tests/xhr/xmlhttprequest-sync-block-defer-scripts.html create mode 100644 test/wpt/tests/xhr/xmlhttprequest-sync-block-scripts.html create mode 100644 test/wpt/tests/xhr/xmlhttprequest-sync-default-feature-policy.sub.html create mode 100644 test/wpt/tests/xhr/xmlhttprequest-sync-not-hang-scriptloader-subframe.html create mode 100644 test/wpt/tests/xhr/xmlhttprequest-sync-not-hang-scriptloader.html create mode 100644 test/wpt/tests/xhr/xmlhttprequest-timeout-aborted.html create mode 100644 test/wpt/tests/xhr/xmlhttprequest-timeout-abortedonmain.html create mode 100644 test/wpt/tests/xhr/xmlhttprequest-timeout-overrides.html create mode 100644 test/wpt/tests/xhr/xmlhttprequest-timeout-overridesexpires.html create mode 100644 test/wpt/tests/xhr/xmlhttprequest-timeout-reused.html create mode 100644 test/wpt/tests/xhr/xmlhttprequest-timeout-simple.html create mode 100644 test/wpt/tests/xhr/xmlhttprequest-timeout-synconmain.html create mode 100644 test/wpt/tests/xhr/xmlhttprequest-timeout-twice.html create mode 100644 test/wpt/tests/xhr/xmlhttprequest-timeout-worker-aborted.html create mode 100644 test/wpt/tests/xhr/xmlhttprequest-timeout-worker-overrides.html create mode 100644 test/wpt/tests/xhr/xmlhttprequest-timeout-worker-overridesexpires.html create mode 100644 test/wpt/tests/xhr/xmlhttprequest-timeout-worker-simple.html create mode 100644 test/wpt/tests/xhr/xmlhttprequest-timeout-worker-synconworker.html create mode 100644 test/wpt/tests/xhr/xmlhttprequest-timeout-worker-twice.html create mode 100644 test/wpt/tests/xhr/xmlhttprequest-unsent.htm diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7aec65f8164..a8041026934 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -77,6 +77,45 @@ cd npm run build:wasm ``` + +### Update `WPTs` + +`undici` runs a subset of the [`web-platform-tests`](https://github.com/web-platform-tests/wpt). + +Here are the steps to update them. + +#### Sparse-clone the [wpt](https://github.com/web-platform-tests/wpt) repo + +```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 + +``` + +#### Checkout the tests + +Only run the commands for the folder(s) you want to update. + +```bash +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 +``` + +#### Run the tests + +Run the tests to ensure that any new failures are marked as such. + +You can mark tests as failing in their corresponding [status](./test/wpt/status) file. + +```bash +npm run test:wpt +``` + ### Lint diff --git a/lib/fetch/request.js b/lib/fetch/request.js index eca7b060e0e..b8b6d17c5f9 100644 --- a/lib/fetch/request.js +++ b/lib/fetch/request.js @@ -54,7 +54,10 @@ class Request { // TODO this[kRealm] = { settingsObject: { - baseUrl: getGlobalOrigin() + baseUrl: getGlobalOrigin(), + get origin () { + return this.baseUrl?.origin + } } } diff --git a/lib/fetch/util.js b/lib/fetch/util.js index a0faed91354..9987e37ea74 100644 --- a/lib/fetch/util.js +++ b/lib/fetch/util.js @@ -1,6 +1,7 @@ 'use strict' const { redirectStatus, badPorts, referrerPolicy: referrerPolicyTokens } = require('./constants') +const { getGlobalOrigin } = require('./global') const { performance } = require('perf_hooks') const { isBlobLike, toUSVString, ReadableStreamFrom } = require('../core/util') const assert = require('assert') @@ -36,9 +37,11 @@ function responseLocationURL (response, requestFragment) { // `Location` and response’s header list. let location = response.headersList.get('location') - // 3. If location is a value, then set location to the result of parsing - // location with response’s URL. - location = location ? new URL(location, responseURL(response)) : null + // 3. If location is a header value, then set location to the result of + // parsing location with response’s URL. + if (location !== null && isValidHeaderValue(location)) { + location = new URL(location, responseURL(response)) + } // 4. If location is a URL whose fragment is null, then set location’s // fragment to requestFragment. @@ -267,7 +270,7 @@ function appendRequestOriginHeader (request) { // 2. If request’s response tainting is "cors" or request’s mode is "websocket", then append (`Origin`, serializedOrigin) to request’s header list. if (request.responseTainting === 'cors' || request.mode === 'websocket') { if (serializedOrigin) { - request.headersList.append('Origin', serializedOrigin) + request.headersList.append('origin', serializedOrigin) } // 3. Otherwise, if request’s method is neither `GET` nor `HEAD`, then: @@ -298,7 +301,7 @@ function appendRequestOriginHeader (request) { if (serializedOrigin) { // 2. Append (`Origin`, serializedOrigin) to request’s header list. - request.headersList.append('Origin', serializedOrigin) + request.headersList.append('origin', serializedOrigin) } } } @@ -340,106 +343,77 @@ function clonePolicyContainer () { // https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer function determineRequestsReferrer (request) { // 1. Let policy be request's referrer policy. - const policy = request.referrerPolicy + // TODO(@KhafraDev): referrerPolicy is supposed to be non-null & not an empty string. + // this is because we don't implement policyContainer. + const policy = request.referrerPolicy ?? 'strict-origin-when-cross-origin' - // Return no-referrer when empty or policy says so - if (policy == null || policy === '' || policy === 'no-referrer') { - return 'no-referrer' - } + // 2. Let environment be request’s client. - // 2. Let environment be the request client - const environment = request.client let referrerSource = null - /** - * 3, Switch on request’s referrer: - "client" - If environment’s global object is a Window object, then - Let document be the associated Document of environment’s global object. - If document’s origin is an opaque origin, return no referrer. - While document is an iframe srcdoc document, - let document be document’s browsing context’s browsing context container’s node document. - Let referrerSource be document’s URL. - - Otherwise, let referrerSource be environment’s creation URL. - - a URL - Let referrerSource be request’s referrer. - */ + // 3. Switch on request’s referrer: if (request.referrer === 'client') { - // Not defined in Node but part of the spec - if (request.client?.globalObject?.constructor?.name === 'Window' ) { // eslint-disable-line - const origin = environment.globalObject.self?.origin ?? environment.globalObject.location?.origin - - // If document’s origin is an opaque origin, return no referrer. - if (origin == null || origin === 'null') return 'no-referrer' - - // Let referrerSource be document’s URL. - referrerSource = new URL(environment.globalObject.location.href) - } else { - // 3(a)(II) If environment's global object is not Window, - // Let referrerSource be environments creationURL - if (environment?.globalObject?.location == null) { - return 'no-referrer' - } + // Note: node isn't a browser and doesn't implement document/iframes, + // so we bypass this step and replace it with our own. + + const globalOrigin = getGlobalOrigin() - referrerSource = new URL(environment.globalObject.location.href) + if (!globalOrigin || globalOrigin.origin === 'null') { + return 'no-referrer' } + + // note: we need to clone it as it's mutated + referrerSource = new URL(globalOrigin) } else if (request.referrer instanceof URL) { - // 3(b) If requests's referrer is a URL instance, then make - // referrerSource be requests's referrer. + // Let referrerSource be request’s referrer. referrerSource = request.referrer - } else { - // If referrerSource neither client nor instance of URL - // then return "no-referrer". - return 'no-referrer' } - const urlProtocol = referrerSource.protocol + // 4. Let request’s referrerURL be the result of stripping referrerSource for + // use as a referrer. + let referrerURL = stripURLForReferrer(referrerSource) - // If url's scheme is a local scheme (i.e. one of "about", "data", "javascript", "file") - // then return "no-referrer". - if ( - urlProtocol === 'about:' || urlProtocol === 'data:' || - urlProtocol === 'blob:' - ) { - return 'no-referrer' + // 5. Let referrerOrigin be the result of stripping referrerSource for use as + // a referrer, with the origin-only flag set to true. + const referrerOrigin = stripURLForReferrer(referrerSource, true) + + // 6. If the result of serializing referrerURL is a string whose length is + // greater than 4096, set referrerURL to referrerOrigin. + if (referrerURL.toString().length > 4096) { + referrerURL = referrerOrigin } - let temp - let referrerOrigin - // 4. Let requests's referrerURL be the result of stripping referrer - // source for use as referrer (using util function, without origin only) - const referrerUrl = (temp = stripURLForReferrer(referrerSource)).length > 4096 - // 5. Let referrerOrigin be the result of stripping referrer - // source for use as referrer (using util function, with originOnly true) - ? (referrerOrigin = stripURLForReferrer(referrerSource, true)) - // 6. If result of seralizing referrerUrl is a string whose length is greater than - // 4096, then set referrerURL to referrerOrigin - : temp - const areSameOrigin = sameOrigin(request, referrerUrl) - const isNonPotentiallyTrustWorthy = isURLPotentiallyTrustworthy(referrerUrl) && + const areSameOrigin = sameOrigin(request, referrerURL) + const isNonPotentiallyTrustWorthy = isURLPotentiallyTrustworthy(referrerURL) && !isURLPotentiallyTrustworthy(request.url) - // NOTE: How to treat step 7? // 8. Execute the switch statements corresponding to the value of policy: switch (policy) { case 'origin': return referrerOrigin != null ? referrerOrigin : stripURLForReferrer(referrerSource, true) - case 'unsafe-url': return referrerUrl + case 'unsafe-url': return referrerURL case 'same-origin': return areSameOrigin ? referrerOrigin : 'no-referrer' case 'origin-when-cross-origin': - return areSameOrigin ? referrerUrl : referrerOrigin - case 'strict-origin-when-cross-origin': - /** - * 1. If the origin of referrerURL and the origin of request’s current URL are the same, - * then return referrerURL. - * 2. If referrerURL is a potentially trustworthy URL and request’s current URL is not a - * potentially trustworthy URL, then return no referrer. - * 3. Return referrerOrigin - */ - if (areSameOrigin) return referrerOrigin - // else return isNonPotentiallyTrustWorthy ? 'no-referrer' : referrerOrigin + return areSameOrigin ? referrerURL : referrerOrigin + case 'strict-origin-when-cross-origin': { + const currentURL = requestCurrentURL(request) + + // 1. If the origin of referrerURL and the origin of request’s current + // URL are the same, then return referrerURL. + if (sameOrigin(referrerURL, currentURL)) { + return referrerURL + } + + // 2. If referrerURL is a potentially trustworthy URL and request’s + // current URL is not a potentially trustworthy URL, then return no + // referrer. + if (isURLPotentiallyTrustworthy(referrerURL) && !isURLPotentiallyTrustworthy(currentURL)) { + return 'no-referrer' + } + + // 3. Return referrerOrigin. + return referrerOrigin + } case 'strict-origin': // eslint-disable-line /** * 1. If referrerURL is a potentially trustworthy URL and @@ -458,15 +432,42 @@ function determineRequestsReferrer (request) { default: // eslint-disable-line return isNonPotentiallyTrustWorthy ? 'no-referrer' : referrerOrigin } +} - function stripURLForReferrer (url, originOnly = false) { - const urlObject = new URL(url.href) - urlObject.username = '' - urlObject.password = '' - urlObject.hash = '' +/** + * @see https://w3c.github.io/webappsec-referrer-policy/#strip-url + * @param {URL} url + * @param {boolean|undefined} originOnly + */ +function stripURLForReferrer (url, originOnly) { + // 1. Assert: url is a URL. + assert(url instanceof URL) - return originOnly ? urlObject.origin : urlObject.href + // 2. If url’s scheme is a local scheme, then return no referrer. + if (url.protocol === 'file:' || url.protocol === 'about:' || url.protocol === 'blank:') { + return 'no-referrer' } + + // 3. Set url’s username to the empty string. + url.username = '' + + // 4. Set url’s password to the empty string. + url.password = '' + + // 5. Set url’s fragment to null. + url.hash = '' + + // 6. If the origin-only flag is true, then: + if (originOnly) { + // 1. Set url’s path to « the empty string ». + url.pathname = '' + + // 2. Set url’s query to null. + url.search = '' + } + + // 7. Return url. + return url } function isURLPotentiallyTrustworthy (url) { diff --git a/test/wpt/runner/runner/runner.mjs b/test/wpt/runner/runner/runner.mjs index 1a17ca6f7a0..af0d959155f 100644 --- a/test/wpt/runner/runner/runner.mjs +++ b/test/wpt/runner/runner/runner.mjs @@ -1,10 +1,9 @@ -import { deepStrictEqual } from 'node:assert' import { EventEmitter, once } from 'node:events' import { readdirSync, readFileSync, statSync } from 'node:fs' -import { basename, isAbsolute, join, resolve } from 'node:path' +import { isAbsolute, join, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { Worker } from 'node:worker_threads' -import { parseMeta, handlePipes, normalizeName, colors } from './util.mjs' +import { colors, handlePipes, normalizeName, parseMeta, resolveStatusPath } from './util.mjs' const basePath = fileURLToPath(join(import.meta.url, '../../..')) const testPath = join(basePath, 'tests') @@ -45,10 +44,12 @@ export class WPTRunner extends EventEmitter { this.#folderName = folder this.#folderPath = join(testPath, folder) - this.#files.push(...WPTRunner.walk( - this.#folderPath, - (file) => file.endsWith('.js') - )) + this.#files.push( + ...WPTRunner.walk( + this.#folderPath, + (file) => file.endsWith('.any.js') + ) + ) this.#status = JSON.parse(readFileSync(join(statusPath, `${folder}.status.json`))) this.#url = url @@ -103,10 +104,14 @@ export class WPTRunner extends EventEmitter { }) for (const [test, code, meta] of files) { - if (this.#status[basename(test)]?.skip) { + console.log('='.repeat(96)) + console.log(`Started ${test}`) + + const status = resolveStatusPath(test, this.#status) + + if (status.file.skip || status.topLevel.skip) { this.#stats.skipped += 1 - console.log('='.repeat(96)) console.log(colors(`[${finishedFiles}/${total}] SKIPPED - ${test}`, 'yellow')) console.log('='.repeat(96)) @@ -140,29 +145,30 @@ export class WPTRunner extends EventEmitter { worker.on('message', (message) => { if (message.type === 'result') { - this.handleIndividualTestCompletion(message, basename(test)) + this.handleIndividualTestCompletion(message, status, test) } else if (message.type === 'completion') { this.handleTestCompletion(worker) } }) try { - activeWorkers.delete(worker) - await once(worker, 'exit', { signal: AbortSignal.timeout(timeout) }) - console.log('='.repeat(96)) console.log(colors(`[${finishedFiles}/${total}] PASSED - ${test}`, 'green')) if (variant) console.log('Variant:', variant) console.log(`Test took ${(performance.now() - start).toFixed(2)}ms`) console.log('='.repeat(96)) finishedFiles++ + activeWorkers.delete(worker) } catch (e) { console.log(`${test} timed out after ${timeout}ms`) - throw e + queueMicrotask(() => { + throw e + }) + return } } } @@ -173,8 +179,8 @@ export class WPTRunner extends EventEmitter { /** * Called after a test has succeeded or failed. */ - handleIndividualTestCompletion (message, fileName) { - const { fail, allowUnexpectedFailures, flaky } = this.#status[fileName] ?? this.#status + handleIndividualTestCompletion (message, status, path) { + const { file, topLevel } = status if (message.type === 'result') { this.#stats.completed += 1 @@ -184,13 +190,16 @@ export class WPTRunner extends EventEmitter { const name = normalizeName(message.result.name) - if (flaky?.includes(name)) { + if (file.flaky?.includes(name)) { this.#stats.expectedFailures += 1 - } else if (allowUnexpectedFailures || fail?.includes(name)) { - if (!allowUnexpectedFailures) { - this.#statusOutput[fileName] ??= [] - this.#statusOutput[fileName].push(name) + } else if (file.allowUnexpectedFailures || topLevel.allowUnexpectedFailures || file.fail?.includes(name)) { + if (!file.allowUnexpectedFailures && !topLevel.allowUnexpectedFailures) { + if (Array.isArray(file.fail)) { + this.#statusOutput[path] ??= [] + this.#statusOutput[path].push(file.fail) + } } + this.#stats.expectedFailures += 1 } else { process.exitCode = 1 @@ -214,17 +223,7 @@ export class WPTRunner extends EventEmitter { * Called after every test has completed. */ handleRunnerCompletion () { - const expectedFailuresObject = Object.keys(this.#status).reduce((a, b) => { - if (Array.isArray(this.#status[b].fail)) { - a[b] = [...this.#status[b].fail] - } - return a - }, {}) - - deepStrictEqual( - this.#statusOutput, - expectedFailuresObject - ) + console.log(this.#statusOutput) // tests that failed this.emit('completion') const { completed, failed, success, expectedFailures, skipped } = this.#stats diff --git a/test/wpt/runner/runner/util.mjs b/test/wpt/runner/runner/util.mjs index aa6ad53afd7..ec284df68b2 100644 --- a/test/wpt/runner/runner/util.mjs +++ b/test/wpt/runner/runner/util.mjs @@ -2,6 +2,7 @@ import assert from 'node:assert' import { exit } from 'node:process' import { inspect } from 'node:util' import tty from 'node:tty' +import { sep } from 'node:path' /** * Parse the `Meta:` tags sometimes included in tests. @@ -150,3 +151,22 @@ export function colors (str, color) { return `\u001b[${start}m${str}\u001b[${end}m` } + +/** @param {string} path */ +export function resolveStatusPath (path, status) { + const paths = path + .slice(process.cwd().length + sep.length) + .split(sep) + .slice(3) // [test, wpt, tests, fetch, b, c.js] -> [fetch, b, c.js] + + // skip the first folder name + for (let i = 1; i < paths.length - 1; i++) { + status = status[paths[i]] + + if (!status) { + break + } + } + + return { topLevel: status ?? {}, file: status?.[paths.at(-1)] ?? {} } +} diff --git a/test/wpt/server/server.mjs b/test/wpt/server/server.mjs index 9df64e75010..b06f86d13d5 100644 --- a/test/wpt/server/server.mjs +++ b/test/wpt/server/server.mjs @@ -335,6 +335,28 @@ const server = createServer(async (req, res) => { res.end(body) break } + case '/fetch/api/resources/authentication.py': { + const auth = Buffer.from(req.headers.authorization.slice('Basic '.length), 'base64') + const [user, password] = auth.toString().split(':') + + if (user === 'user' && password === 'password') { + res.end('Authentication done') + return + } + + const realm = fullUrl.searchParams.get('realm') ?? 'test' + + res.statusCode = 401 + res.setHeader('WWW-Authenticate', `Basic realm="${realm}"`) + res.end('Please login with credentials \'user\' and \'password\'') + return + } + case '/fetch/api/resources/redirect-empty-location.py': { + res.setHeader('location', '') + res.statusCode = 302 + res.end('') + return + } default: { res.statusCode = 200 res.end('body') diff --git a/test/wpt/start-xhr.mjs b/test/wpt/start-xhr.mjs index 09d7578823b..f67208a651f 100644 --- a/test/wpt/start-xhr.mjs +++ b/test/wpt/start-xhr.mjs @@ -1,7 +1,7 @@ import { WPTRunner } from './runner/runner/runner.mjs' import { once } from 'events' -const runner = new WPTRunner('xhr', 'http://localhost:3333') +const runner = new WPTRunner('xhr/formdata', 'http://localhost:3333') runner.run() await once(runner, 'completion') diff --git a/test/wpt/status/FileAPI.status.json b/test/wpt/status/FileAPI.status.json index 844608acd9a..c64d255a0d3 100644 --- a/test/wpt/status/FileAPI.status.json +++ b/test/wpt/status/FileAPI.status.json @@ -1,9 +1,54 @@ { - "File-constructor.any.js": { - "flaky": [ - "Using type in File constructor: nonparsable" - ] - }, + "file": { + "File-constructor.any.js": { + "flaky": [ + "Using type in File constructor: nonparsable" + ] + } + }, + "blob": { + "Blob-constructor.any.js": { + "skip": true + }, + "Blob-stream.any.js": { + "fail": [ + "Reading Blob.stream() with BYOB reader" + ] + } + }, + "url": { + "url-with-xhr.any.js": { + "skip": true + }, + "url-with-fetch.any.js": { + "note": "needs investigation", + "fail": [ + "Only exact matches should revoke URLs, using fetch", + "Revoke blob URL after creating Request, will fetch", + "Revoke blob URL after creating Request, then clone Request, will fetch" + ] + }, + "url-format.any.js": { + "fail": [ + "Origin of Blob URL matches our origin", + "Blob URL parses correctly", + "Origin of Blob URL matches our origin for Files" + ] + } + }, + "reading-data-section": { + "filereader_result.any.js": { + "note": "has to do with html microtask queue being different than queueMicrotask", + "skip": true + }, + "filereader_events.any.js": { + "note": "has to do with html microtask queue being different than queueMicrotask", + "fail": [ + "events are dispatched in the correct order for an empty blob", + "events are dispatched in the correct order for a non-empty blob" + ] + } + }, "idlharness.any.js": { "note": "These flaky tests only fail in < node v19; add in a way to mark them as such eventually", "flaky": [ @@ -26,11 +71,5 @@ "FileList interface: operation item(unsigned long)", "FileList interface: attribute length" ] - }, - "filereader_events.any.js": { - "fail": [ - "events are dispatched in the correct order for an empty blob", - "events are dispatched in the correct order for a non-empty blob" - ] } } diff --git a/test/wpt/status/fetch.status.json b/test/wpt/status/fetch.status.json index 380f0153f7c..46a2a6b3bfd 100644 --- a/test/wpt/status/fetch.status.json +++ b/test/wpt/status/fetch.status.json @@ -1,215 +1,395 @@ { - "general.any.js": { - "flaky": [ - "Already aborted signal rejects immediately", - "Underlying connection is closed when aborting after receiving response - no-cors", - "Stream errors once aborted. Underlying connection closed." - ] - }, - "request-disturbed.any.js": { - "fail": [ - "Input request used for creating new request became disturbed even if body is not used" - ] - }, - "response-error-from-stream.any.js": { - "fail": [ - "ReadableStream start() Error propagates to Response.formData() Promise", - "ReadableStream pull() Error propagates to Response.formData() Promise" - ] - }, - "response-consume-empty.any.js": { - "fail": [ - "Consume empty FormData response body as text" - ] - }, + "api": { + "abort": { + "general.any.js": { + "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" + ] + }, + "cache.https.any.js": { + "note": "undici doesn't implement http caching", + "skip": true + } + }, + "basic": { + "conditional-get.any.js": { + "fail": [ + "Testing conditional GET with ETags" + ] + }, + "header-value-combining.any.js": { + "fail": [ + "response.headers.get('content-length') expects 0, 0", + "response.headers.get('foo-test') expects 1, 2, 3", + "response.headers.get('heya') expects , \\x0B\f, 1, , , 2" + ], + "flaky": [ + "response.headers.get('content-length') expects 0", + "response.headers.get('double-trouble') expects , ", + "response.headers.get('www-authenticate') expects 1, 2, 3, 4" + ] + }, + "integrity.sub.any.js": { + "fail": [ + "Empty string integrity for opaque response" + ] + }, + "mode-no-cors.sub.any.js": { + "note": "undici doesn't implement CORs", + "skip": true + }, + "mode-same-origin.any.js": { + "note": "undici doesn't respect RequestInit.mode", + "skip": true + }, + "referrer.any.js": { + "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" + ] + }, + "request-forbidden-headers.any.js": { + "note": "undici doesn't filter headers", + "skip": true + }, + "request-headers.any.js": { + "fail": [ + "Fetch with Chicken", + "Fetch with Chicken with body", + "Fetch with TacO and mode \"same-origin\" needs an Origin header", + "Fetch with TacO and mode \"cors\" needs an Origin header" + ] + }, + "request-referrer.any.js": { + "note": "TODO(@KhafraDev): url referrer test could probably be fixed", + "fail": [ + "about:client referrer", + "url referrer" + ] + }, + "request-upload.any.js": { + "fail": [ + "Fetch with POST with text body on 421 response should be retried once on new connection." + ] + }, + "request-upload.h2.any.js": { + "note": "undici doesn't support http/2", + "skip": true + }, + "status.h2.any.js": { + "note": "undici doesn't support http/2", + "skip": true + }, + "stream-safe-creation.any.js": { + "note": "tests are very finnicky", + "fail": [ + "throwing Object.prototype.type accessor should not affect stream creation by 'fetch'", + "Object.prototype.type accessor returning invalid value should not affect stream creation by 'fetch'", + "throwing Object.prototype.highWaterMark accessor should not affect stream creation by 'fetch'", + "Object.prototype.highWaterMark accessor returning invalid value should not affect stream creation by 'fetch'" + ] + } + }, + "cors": { + "note": "undici doesn't implement CORs", + "skip": true + }, + "credentials": { + "authentication-redirection.any.js": { + "note": "connects to https server", + "fail": [ + "getAuthorizationHeaderValue - cross origin redirection" + ] + }, + "cookies.any.js": { + "fail": [ + "Include mode: 1 cookie", + "Include mode: 2 cookies", + "Same-origin mode: 1 cookie", + "Same-origin mode: 2 cookies" + ] + } + }, + "headers": { + "header-setcookie.any.js": { + "note": "undici doesn't filter headers", + "fail": [ + "Set-Cookie is a forbidden response header" + ] + }, + "header-values-normalize.any.js": { + "note": "TODO(@KhafraDev): https://github.com/nodejs/undici/issues/1680", + "fail": [ + "XMLHttpRequest with value %00", + "XMLHttpRequest with value %01", + "XMLHttpRequest with value %02", + "XMLHttpRequest with value %03", + "XMLHttpRequest with value %04", + "XMLHttpRequest with value %05", + "XMLHttpRequest with value %06", + "XMLHttpRequest with value %07", + "XMLHttpRequest with value %08", + "XMLHttpRequest with value %09", + "XMLHttpRequest with value %0A", + "XMLHttpRequest with value %0D", + "XMLHttpRequest with value %0E", + "XMLHttpRequest with value %0F", + "XMLHttpRequest with value %10", + "XMLHttpRequest with value %11", + "XMLHttpRequest with value %12", + "XMLHttpRequest with value %13", + "XMLHttpRequest with value %14", + "XMLHttpRequest with value %15", + "XMLHttpRequest with value %16", + "XMLHttpRequest with value %17", + "XMLHttpRequest with value %18", + "XMLHttpRequest with value %19", + "XMLHttpRequest with value %1A", + "XMLHttpRequest with value %1B", + "XMLHttpRequest with value %1C", + "XMLHttpRequest with value %1D", + "XMLHttpRequest with value %1E", + "XMLHttpRequest with value %1F", + "XMLHttpRequest with value %20", + "fetch() with value %01", + "fetch() with value %02", + "fetch() with value %03", + "fetch() with value %04", + "fetch() with value %05", + "fetch() with value %06", + "fetch() with value %07", + "fetch() with value %08", + "fetch() with value %0E", + "fetch() with value %0F", + "fetch() with value %10", + "fetch() with value %11", + "fetch() with value %12", + "fetch() with value %13", + "fetch() with value %14", + "fetch() with value %15", + "fetch() with value %16", + "fetch() with value %17", + "fetch() with value %18", + "fetch() with value %19", + "fetch() with value %1A", + "fetch() with value %1B", + "fetch() with value %1C", + "fetch() with value %1D", + "fetch() with value %1E", + "fetch() with value %1F" + ] + }, + "header-values.any.js": { + "fail": [ + "XMLHttpRequest with value x%00x needs to throw", + "XMLHttpRequest with value x%0Ax needs to throw", + "XMLHttpRequest with value x%0Dx needs to throw", + "XMLHttpRequest with all valid values", + "fetch() with all valid values" + ] + }, + "headers-no-cors.any.js": { + "note": "undici doesn't implement CORs", + "skip": true + } + }, + "redirect": { + "redirect-empty-location.any.js": { + "note": "undici handles redirect: manual differently than browsers", + "fail": [ + "redirect response with empty Location, manual mode" + ] + }, + "redirect-location-escape.tentative.any.js": { + "note": "TODO(@KhafraDev): crashes runner", + "skip": true + }, + "redirect-location.any.js": { + "note": "undici handles redirect: manual differently than browsers", + "fail": [ + "Redirect 301 in \"manual\" mode without location", + "Redirect 301 in \"manual\" mode with invalid location", + "Redirect 301 in \"manual\" mode with data location", + "Redirect 302 in \"manual\" mode without location", + "Redirect 302 in \"manual\" mode with invalid location", + "Redirect 302 in \"manual\" mode with data location", + "Redirect 303 in \"manual\" mode without location", + "Redirect 303 in \"manual\" mode with invalid location", + "Redirect 303 in \"manual\" mode with data location", + "Redirect 307 in \"manual\" mode without location", + "Redirect 307 in \"manual\" mode with invalid location", + "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-method.any.js": { + "fail": [ + "Redirect 303 with TESTING" + ] + }, + "redirect-mode.any.js": { + "note": "mode isn't respected", + "skip": true + }, + "redirect-origin.any.js": { + "note": "TODO(@KhafraDev): investigate", + "skip": true + }, + "redirect-referrer-override.any.js": { + "note": "TODO(@KhafraDev): investigate", + "skip": true + }, + "redirect-referrer.any.js": { + "note": "TODO(@KhafraDev): investigate", + "skip": true + }, + "redirect-upload.h2.any.js": { + "note": "undici doesn't support http/2", + "skip": true + } + }, + "request": { + "request-cache-default-conditional.any.js": { + "note": "undici doesn't implement an http cache", + "skip": true + }, + "request-cache-default.any.js": { + "note": "undici doesn't implement an http cache", + "skip": true + }, + "request-cache-force-cache.any.js": { + "note": "undici doesn't implement an http cache", + "skip": true + }, + "request-cache-no-cache.any.js": { + "note": "undici doesn't implement an http cache", + "skip": true + }, + "request-cache-no-store.any.js": { + "note": "undici doesn't implement an http cache", + "skip": true + }, + "request-cache-only-if-cached.any.js": { + "note": "undici doesn't implement an http cache", + "skip": true + }, + "request-cache-reload.any.js": { + "note": "undici doesn't implement an http cache", + "skip": true + }, + "request-consume-empty.any.js": { + "note": "the semantics about this test are being discussed - https://github.com/web-platform-tests/wpt/pull/3950", + "fail": [ + "Consume empty FormData request body as text" + ] + }, + "request-consume.any.js": { + "note": "TODO(@KhafraDev): investigate", + "fail": [ + "Consume blob response's body as blob" + ] + }, + "request-disturbed.any.js": { + "note": "TODO(@KhafraDev): investigate", + "fail": [ + "Input request used for creating new request became disturbed even if body is not used" + ] + }, + "request-headers.any.js": { + "note": "TODO(@KhafraDev): investigate", + "skip": true + }, + "request-init-priority.any.js": { + "note": "undici doesn't implement priority hints, yet(?)", + "skip": true + } + }, + "response": { + "response-clone.any.js": { + "fail": [ + "Check response clone use structureClone for teed ReadableStreams (ArrayBufferchunk)", + "Check response clone use structureClone for teed ReadableStreams (DataViewchunk)" + ] + }, + "response-consume-empty.any.js": { + "fail": [ + "Consume empty FormData response body as text" + ] + }, + "response-consume-stream.any.js": { + "fail": [ + "Read blob response's body as readableStream with mode=byob", + "Read text response's body as readableStream with mode=byob", + "Read URLSearchParams response's body as readableStream with mode=byob", + "Read array buffer response's body as readableStream with mode=byob", + "Read form data response's body as readableStream with mode=byob" + ] + }, + "response-error-from-stream.any.js": { + "fail": [ + "ReadableStream start() Error propagates to Response.formData() Promise", + "ReadableStream pull() Error propagates to Response.formData() Promise" + ] + }, + "response-stream-with-broken-then.any.js": { + "note": "TODO(@KhafraDev): either requires primordials or a bug in node core", + "skip": true + } + } + }, + "content-length": { + "api-and-duplicate-headers.any.js": { + "fail": [ + "XMLHttpRequest and duplicate Content-Length/Content-Type headers", + "fetch() and duplicate Content-Length/Content-Type headers" + ] + } + }, + "cross-origin-resource-policy": { + "note": "undici doesn't implement CORs", + "skip": true + }, + "http-cache": { + "note": "undici doesn't implement http caching", + "skip": true + }, + "metadata": { + "note": "undici doesn't respect RequestInit.mode", + "skip": true + }, + "orb": { + "tentative": { + "note": "undici doesn't implement orb", + "skip": true + } + }, + "range": { + "note": "undici doesn't respect range header", + "skip": true + }, + "security": { + "1xx-response.any.js": { + "fail": [ + "Status(100) should be ignored.", + "Status(101) should be accepted, with removing body.", + "Status(103) should be ignored.", + "Status(199) should be ignored." + ] + } + }, + "stale-while-revalidate": { + "note": "undici doesn't implement http caching", + "skip": true + }, "idlharness.any.js": { "flaky": [ "Window interface: operation fetch(RequestInfo, optional RequestInit)" ] - }, - "response-clone.any.js": { - "fail": [ - "Check response clone use structureClone for teed ReadableStreams (ArrayBufferchunk)", - "Check response clone use structureClone for teed ReadableStreams (DataViewchunk)" - ] - }, - "request-upload.any.js": { - "fail": [ - "Fetch with POST with text body on 421 response should be retried once on new connection." - ] - }, - "stream-safe-creation.any.js": { - "fail": [ - "throwing Object.prototype.type accessor should not affect stream creation by 'fetch'", - "Object.prototype.type accessor returning invalid value should not affect stream creation by 'fetch'", - "throwing Object.prototype.highWaterMark accessor should not affect stream creation by 'fetch'", - "Object.prototype.highWaterMark accessor returning invalid value should not affect stream creation by 'fetch'" - ] - }, - "header-value-combining.any.js": { - "fail": [ - "response.headers.get('content-length') expects 0, 0", - "response.headers.get('foo-test') expects 1, 2, 3", - "response.headers.get('heya') expects , \\x0B\f, 1, , , 2" - ], - "flaky": [ - "response.headers.get('content-length') expects 0", - "response.headers.get('double-trouble') expects , ", - "response.headers.get('www-authenticate') expects 1, 2, 3, 4" - ] - }, - "integrity.sub.any.js": { - "fail": [ - "Empty string integrity for opaque response" - ] - }, - "request-headers.any.js": { - "fail": [ - "Adding invalid request header \"Accept-Charset: KO\"", - "Adding invalid request header \"accept-charset: KO\"", - "Adding invalid request header \"ACCEPT-ENCODING: KO\"", - "Adding invalid request header \"Accept-Encoding: KO\"", - "Adding invalid request header \"Access-Control-Request-Headers: KO\"", - "Adding invalid request header \"Access-Control-Request-Method: KO\"", - "Adding invalid request header \"Connection: KO\"", - "Adding invalid request header \"Content-Length: KO\"", - "Adding invalid request header \"Cookie: KO\"", - "Adding invalid request header \"Cookie2: KO\"", - "Adding invalid request header \"Date: KO\"", - "Adding invalid request header \"DNT: KO\"", - "Adding invalid request header \"Expect: KO\"", - "Adding invalid request header \"Host: KO\"", - "Adding invalid request header \"Keep-Alive: KO\"", - "Adding invalid request header \"Origin: KO\"", - "Adding invalid request header \"Referer: KO\"", - "Adding invalid request header \"Set-Cookie: KO\"", - "Adding invalid request header \"TE: KO\"", - "Adding invalid request header \"Trailer: KO\"", - "Adding invalid request header \"Transfer-Encoding: KO\"", - "Adding invalid request header \"Upgrade: KO\"", - "Adding invalid request header \"Via: KO\"", - "Adding invalid request header \"Proxy-: KO\"", - "Adding invalid request header \"proxy-a: KO\"", - "Adding invalid request header \"Sec-: KO\"", - "Adding invalid request header \"sec-b: KO\"", - "Adding invalid no-cors request header \"Content-Type: KO\"", - "Adding invalid no-cors request header \"Potato: KO\"", - "Adding invalid no-cors request header \"proxy: KO\"", - "Adding invalid no-cors request header \"proxya: KO\"", - "Adding invalid no-cors request header \"sec: KO\"", - "Adding invalid no-cors request header \"secb: KO\"", - "Check that request constructor is filtering headers provided as init parameter", - "Check that no-cors request constructor is filtering headers provided as init parameter", - "Check that no-cors request constructor is filtering headers provided as part of request parameter" - ] - }, - "request-consume.any.js": { - "fail": [ - "Consume blob response's body as blob" - ] - }, - "request-consume-empty.any.js": { - "fail": [ - "Consume empty FormData request body as text" - ] - }, - "redirect-method.any.js": { - "fail": [ - "Redirect 303 with TESTING" - ] - }, - "redirect-location.any.js": { - "fail": [ - "Redirect 301 in \"manual\" mode without location", - "Redirect 301 in \"manual\" mode with invalid location", - "Redirect 301 in \"manual\" mode with data location", - "Redirect 302 in \"manual\" mode without location", - "Redirect 302 in \"manual\" mode with invalid location", - "Redirect 302 in \"manual\" mode with data location", - "Redirect 303 in \"manual\" mode without location", - "Redirect 303 in \"manual\" mode with invalid location", - "Redirect 303 in \"manual\" mode with data location", - "Redirect 307 in \"manual\" mode without location", - "Redirect 307 in \"manual\" mode with invalid location", - "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" - ] - }, - "header-values.any.js": { - "fail": [ - "XMLHttpRequest with value x%00x needs to throw", - "XMLHttpRequest with value x%0Ax needs to throw", - "XMLHttpRequest with value x%0Dx needs to throw", - "XMLHttpRequest with all valid values", - "fetch() with all valid values" - ] - }, - "header-values-normalize.any.js": { - "fail": [ - "XMLHttpRequest with value %00", - "XMLHttpRequest with value %01", - "XMLHttpRequest with value %02", - "XMLHttpRequest with value %03", - "XMLHttpRequest with value %04", - "XMLHttpRequest with value %05", - "XMLHttpRequest with value %06", - "XMLHttpRequest with value %07", - "XMLHttpRequest with value %08", - "XMLHttpRequest with value %09", - "XMLHttpRequest with value %0A", - "XMLHttpRequest with value %0D", - "XMLHttpRequest with value %0E", - "XMLHttpRequest with value %0F", - "XMLHttpRequest with value %10", - "XMLHttpRequest with value %11", - "XMLHttpRequest with value %12", - "XMLHttpRequest with value %13", - "XMLHttpRequest with value %14", - "XMLHttpRequest with value %15", - "XMLHttpRequest with value %16", - "XMLHttpRequest with value %17", - "XMLHttpRequest with value %18", - "XMLHttpRequest with value %19", - "XMLHttpRequest with value %1A", - "XMLHttpRequest with value %1B", - "XMLHttpRequest with value %1C", - "XMLHttpRequest with value %1D", - "XMLHttpRequest with value %1E", - "XMLHttpRequest with value %1F", - "XMLHttpRequest with value %20", - "fetch() with value %01", - "fetch() with value %02", - "fetch() with value %03", - "fetch() with value %04", - "fetch() with value %05", - "fetch() with value %06", - "fetch() with value %07", - "fetch() with value %08", - "fetch() with value %0E", - "fetch() with value %0F", - "fetch() with value %10", - "fetch() with value %11", - "fetch() with value %12", - "fetch() with value %13", - "fetch() with value %14", - "fetch() with value %15", - "fetch() with value %16", - "fetch() with value %17", - "fetch() with value %18", - "fetch() with value %19", - "fetch() with value %1A", - "fetch() with value %1B", - "fetch() with value %1C", - "fetch() with value %1D", - "fetch() with value %1E", - "fetch() with value %1F" - ] - }, - "header-setcookie.any.js": { - "fail": [ - "Set-Cookie is a forbidden response header" - ] } } diff --git a/test/wpt/status/mimesniff.status.json b/test/wpt/status/mimesniff.status.json index d1b1c33e8af..ab9a3d32640 100644 --- a/test/wpt/status/mimesniff.status.json +++ b/test/wpt/status/mimesniff.status.json @@ -1,5 +1,7 @@ { - "parsing.any.js": { - "allowUnexpectedFailures": true - } -} \ No newline at end of file + "mime-types": { + "parsing.any.js": { + "allowUnexpectedFailures": true + } + } +} diff --git a/test/wpt/status/websockets.status.json b/test/wpt/status/websockets.status.json index 4f0efbdded9..4a5bdf502c3 100644 --- a/test/wpt/status/websockets.status.json +++ b/test/wpt/status/websockets.status.json @@ -1,5 +1,57 @@ { - "allowUnexpectedFailures": true, + "stream": { + "tentative": { + "skip": true + } + }, + "Create-blocked-port.any.js": { + "note": "TODO(@KhafraDev): investigate failure", + "fail": [ + "Basic check" + ] + }, + "Send-binary-arraybufferview-float32.any.js": { + "note": "TODO(@KhafraDev): investigate failure", + "fail": [ + "Send binary data on a WebSocket - ArrayBufferView - Float32Array - Connection should be closed" + ] + }, + "Send-binary-arraybufferview-float64.any.js": { + "note": "TODO(@KhafraDev): investigate failure", + "fail": [ + "Send binary data on a WebSocket - ArrayBufferView - Float64Array - Connection should be closed" + ] + }, + "Send-binary-arraybufferview-int16-offset.any.js": { + "note": "TODO(@KhafraDev): investigate failure", + "fail": [ + "Send binary data on a WebSocket - ArrayBufferView - Int16Array with offset - Connection should be closed" + ] + }, + "Send-binary-arraybufferview-int32.any.js": { + "note": "TODO(@KhafraDev): investigate failure", + "fail": [ + "Send binary data on a WebSocket - ArrayBufferView - Int32Array - Connection should be closed" + ] + }, + "Send-binary-arraybufferview-uint16-offset-length.any.js": { + "note": "TODO(@KhafraDev): investigate failure", + "fail": [ + "Send binary data on a WebSocket - ArrayBufferView - Uint16Array with offset and length - Connection should be closed" + ] + }, + "Send-binary-arraybufferview-uint32-offset.any.js": { + "note": "TODO(@KhafraDev): investigate failure", + "fail": [ + "Send binary data on a WebSocket - ArrayBufferView - Uint32Array with offset - Connection should be closed" + ] + }, + "basic-auth.any.js": { + "note": "TODO(@KhafraDev): investigate failure", + "fail": [ + "HTTP basic authentication should work with WebSockets" + ] + }, "Create-on-worker-shutdown.any.js": { "skip": true, "//": "Node.js workers are different from web workers & don't work with blob: urls" diff --git a/test/wpt/status/xhr.status.json b/test/wpt/status/xhr.status.json deleted file mode 100644 index 9e26dfeeb6e..00000000000 --- a/test/wpt/status/xhr.status.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/test/wpt/status/xhr/formdata.status.json b/test/wpt/status/xhr/formdata.status.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/test/wpt/status/xhr/formdata.status.json @@ -0,0 +1 @@ +{} diff --git a/test/wpt/tests/FileAPI/Blob-methods-from-detached-frame.html b/test/wpt/tests/FileAPI/Blob-methods-from-detached-frame.html new file mode 100644 index 00000000000..37efd5ed201 --- /dev/null +++ b/test/wpt/tests/FileAPI/Blob-methods-from-detached-frame.html @@ -0,0 +1,59 @@ + + +Blob methods from detached frame work as expected + + + + + + diff --git a/test/wpt/tests/FileAPI/BlobURL/cross-partition.tentative.https.html b/test/wpt/tests/FileAPI/BlobURL/cross-partition.tentative.https.html new file mode 100644 index 00000000000..c75ce07d054 --- /dev/null +++ b/test/wpt/tests/FileAPI/BlobURL/cross-partition.tentative.https.html @@ -0,0 +1,276 @@ + + + + + + + + + + + + + + + diff --git a/test/wpt/tests/FileAPI/BlobURL/support/file_test2.txt b/test/wpt/tests/FileAPI/BlobURL/support/file_test2.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/wpt/tests/FileAPI/BlobURL/test2-manual.html b/test/wpt/tests/FileAPI/BlobURL/test2-manual.html new file mode 100644 index 00000000000..07fb27ef8af --- /dev/null +++ b/test/wpt/tests/FileAPI/BlobURL/test2-manual.html @@ -0,0 +1,62 @@ + + + + + Blob and File reference URL Test(2) + + + + + + +
+
+
+ +
+

Test steps:

+
    +
  1. Download the file.
  2. +
  3. Select the file in the file inputbox.
  4. +
  5. Delete the file.
  6. +
  7. Click the 'start' button.
  8. +
+
+ +
+ + + + diff --git a/test/wpt/tests/FileAPI/FileReader/progress_event_bubbles_cancelable.html b/test/wpt/tests/FileAPI/FileReader/progress_event_bubbles_cancelable.html new file mode 100644 index 00000000000..6a03243f934 --- /dev/null +++ b/test/wpt/tests/FileAPI/FileReader/progress_event_bubbles_cancelable.html @@ -0,0 +1,33 @@ + + +File API Test: Progress Event - bubbles, cancelable + + + + +
+ + diff --git a/test/wpt/tests/FileAPI/FileReader/support/file_test1.txt b/test/wpt/tests/FileAPI/FileReader/support/file_test1.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/wpt/tests/FileAPI/FileReader/test_errors-manual.html b/test/wpt/tests/FileAPI/FileReader/test_errors-manual.html new file mode 100644 index 00000000000..b8c3f84d2bf --- /dev/null +++ b/test/wpt/tests/FileAPI/FileReader/test_errors-manual.html @@ -0,0 +1,72 @@ + + + + + FileReader Errors Test + + + + + + +
+
+
+ +
+

Test steps:

+
    +
  1. Download the file.
  2. +
  3. Select the file in the file inputbox.
  4. +
  5. Delete the file.
  6. +
  7. Click the 'start' button.
  8. +
+
+ +
+ + + + diff --git a/test/wpt/tests/FileAPI/FileReader/test_notreadableerrors-manual.html b/test/wpt/tests/FileAPI/FileReader/test_notreadableerrors-manual.html new file mode 100644 index 00000000000..46d73598a0f --- /dev/null +++ b/test/wpt/tests/FileAPI/FileReader/test_notreadableerrors-manual.html @@ -0,0 +1,42 @@ + + +FileReader NotReadableError Test + + + + +
+
+
+ +
+

Test steps:

+
    +
  1. Download the file.
  2. +
  3. Select the file in the file inputbox.
  4. +
  5. Delete the file's readable permission.
  6. +
  7. Click the 'start' button.
  8. +
+
+ + + diff --git a/test/wpt/tests/FileAPI/FileReader/test_securityerrors-manual.html b/test/wpt/tests/FileAPI/FileReader/test_securityerrors-manual.html new file mode 100644 index 00000000000..add93ed69d1 --- /dev/null +++ b/test/wpt/tests/FileAPI/FileReader/test_securityerrors-manual.html @@ -0,0 +1,40 @@ + + +FileReader SecurityError Test + + + + +
+
+
+ +
+

Test steps:

+
    +
  1. Select a system sensitive file (e.g. files in /usr/bin, password files, + and other native operating system executables) in the file inputbox.
  2. +
  3. Click the 'start' button.
  4. +
+
+ + diff --git a/test/wpt/tests/FileAPI/FileReader/workers.html b/test/wpt/tests/FileAPI/FileReader/workers.html new file mode 100644 index 00000000000..8e114eeaf86 --- /dev/null +++ b/test/wpt/tests/FileAPI/FileReader/workers.html @@ -0,0 +1,27 @@ + + + + + diff --git a/test/wpt/tests/FileAPI/FileReaderSync.worker.js b/test/wpt/tests/FileAPI/FileReaderSync.worker.js new file mode 100644 index 00000000000..3d7a0222f31 --- /dev/null +++ b/test/wpt/tests/FileAPI/FileReaderSync.worker.js @@ -0,0 +1,56 @@ +importScripts("/resources/testharness.js"); + +var blob, empty_blob, readerSync; +setup(() => { + readerSync = new FileReaderSync(); + blob = new Blob(["test"]); + empty_blob = new Blob(); +}); + +test(() => { + assert_true(readerSync instanceof FileReaderSync); +}, "Interface"); + +test(() => { + var text = readerSync.readAsText(blob); + assert_equals(text, "test"); +}, "readAsText"); + +test(() => { + var text = readerSync.readAsText(empty_blob); + assert_equals(text, ""); +}, "readAsText with empty blob"); + +test(() => { + var data = readerSync.readAsDataURL(blob); + assert_equals(data.indexOf("data:"), 0); +}, "readAsDataURL"); + +test(() => { + var data = readerSync.readAsDataURL(empty_blob); + assert_equals(data.indexOf("data:"), 0); +}, "readAsDataURL with empty blob"); + +test(() => { + var data = readerSync.readAsBinaryString(blob); + assert_equals(data, "test"); +}, "readAsBinaryString"); + +test(() => { + var data = readerSync.readAsBinaryString(empty_blob); + assert_equals(data, ""); +}, "readAsBinaryString with empty blob"); + +test(() => { + var data = readerSync.readAsArrayBuffer(blob); + assert_true(data instanceof ArrayBuffer); + assert_equals(data.byteLength, "test".length); +}, "readAsArrayBuffer"); + +test(() => { + var data = readerSync.readAsArrayBuffer(empty_blob); + assert_true(data instanceof ArrayBuffer); + assert_equals(data.byteLength, 0); +}, "readAsArrayBuffer with empty blob"); + +done(); diff --git a/test/wpt/tests/FileAPI/META.yml b/test/wpt/tests/FileAPI/META.yml new file mode 100644 index 00000000000..506a59fec1e --- /dev/null +++ b/test/wpt/tests/FileAPI/META.yml @@ -0,0 +1,6 @@ +spec: https://w3c.github.io/FileAPI/ +suggested_reviewers: + - inexorabletash + - zqzhang + - jdm + - mkruisselbrink diff --git a/test/wpt/tests/FileAPI/blob/Blob-array-buffer.any.js b/test/wpt/tests/FileAPI/blob/Blob-array-buffer.any.js new file mode 100644 index 00000000000..2310646e5fd --- /dev/null +++ b/test/wpt/tests/FileAPI/blob/Blob-array-buffer.any.js @@ -0,0 +1,45 @@ +// META: title=Blob Array Buffer +// META: script=../support/Blob.js +'use strict'; + +promise_test(async () => { + const input_arr = new TextEncoder().encode("PASS"); + const blob = new Blob([input_arr]); + const array_buffer = await blob.arrayBuffer(); + assert_true(array_buffer instanceof ArrayBuffer); + assert_equals_typed_array(new Uint8Array(array_buffer), input_arr); +}, "Blob.arrayBuffer()") + +promise_test(async () => { + const input_arr = new TextEncoder().encode(""); + const blob = new Blob([input_arr]); + const array_buffer = await blob.arrayBuffer(); + assert_true(array_buffer instanceof ArrayBuffer); + assert_equals_typed_array(new Uint8Array(array_buffer), input_arr); +}, "Blob.arrayBuffer() empty Blob data") + +promise_test(async () => { + const input_arr = new TextEncoder().encode("\u08B8\u000a"); + const blob = new Blob([input_arr]); + const array_buffer = await blob.arrayBuffer(); + assert_equals_typed_array(new Uint8Array(array_buffer), input_arr); +}, "Blob.arrayBuffer() non-ascii input") + +promise_test(async () => { + const input_arr = [8, 241, 48, 123, 151]; + const typed_arr = new Uint8Array(input_arr); + const blob = new Blob([typed_arr]); + const array_buffer = await blob.arrayBuffer(); + assert_equals_typed_array(new Uint8Array(array_buffer), typed_arr); +}, "Blob.arrayBuffer() non-unicode input") + +promise_test(async () => { + const input_arr = new TextEncoder().encode("PASS"); + const blob = new Blob([input_arr]); + const array_buffer_results = await Promise.all([blob.arrayBuffer(), + blob.arrayBuffer(), blob.arrayBuffer()]); + for (let array_buffer of array_buffer_results) { + assert_true(array_buffer instanceof ArrayBuffer); + assert_equals_typed_array(new Uint8Array(array_buffer), input_arr); + } +}, "Blob.arrayBuffer() concurrent reads") diff --git a/test/wpt/tests/FileAPI/blob/Blob-constructor-dom.window.js b/test/wpt/tests/FileAPI/blob/Blob-constructor-dom.window.js new file mode 100644 index 00000000000..4fd4a43ec4b --- /dev/null +++ b/test/wpt/tests/FileAPI/blob/Blob-constructor-dom.window.js @@ -0,0 +1,53 @@ +// META: title=Blob constructor +// META: script=../support/Blob.js +'use strict'; + +var test_error = { + name: "test", + message: "test error", +}; + +test(function() { + var args = [ + document.createElement("div"), + window, + ]; + args.forEach(function(arg) { + assert_throws_js(TypeError, function() { + new Blob(arg); + }, "Should throw for argument " + format_value(arg) + "."); + }); +}, "Passing platform objects for blobParts should throw a TypeError."); + +test(function() { + var element = document.createElement("div"); + element.appendChild(document.createElement("div")); + element.appendChild(document.createElement("p")); + var list = element.children; + Object.defineProperty(list, "length", { + get: function() { throw test_error; } + }); + assert_throws_exactly(test_error, function() { + new Blob(list); + }); +}, "A platform object that supports indexed properties should be treated as a sequence for the blobParts argument (overwritten 'length'.)"); + +test_blob(function() { + var select = document.createElement("select"); + select.appendChild(document.createElement("option")); + return new Blob(select); +}, { + expected: "[object HTMLOptionElement]", + type: "", + desc: "Passing an platform object that supports indexed properties as the blobParts array should work (select)." +}); + +test_blob(function() { + var elm = document.createElement("div"); + elm.setAttribute("foo", "bar"); + return new Blob(elm.attributes); +}, { + expected: "[object Attr]", + type: "", + desc: "Passing an platform object that supports indexed properties as the blobParts array should work (attributes)." +}); \ No newline at end of file diff --git a/test/wpt/tests/FileAPI/blob/Blob-constructor-endings.html b/test/wpt/tests/FileAPI/blob/Blob-constructor-endings.html new file mode 100644 index 00000000000..04edd2a303b --- /dev/null +++ b/test/wpt/tests/FileAPI/blob/Blob-constructor-endings.html @@ -0,0 +1,104 @@ + + +Blob constructor: endings option + + + + diff --git a/test/wpt/tests/FileAPI/blob/Blob-constructor.any.js b/test/wpt/tests/FileAPI/blob/Blob-constructor.any.js new file mode 100644 index 00000000000..d16f760caee --- /dev/null +++ b/test/wpt/tests/FileAPI/blob/Blob-constructor.any.js @@ -0,0 +1,468 @@ +// META: title=Blob constructor +// META: script=../support/Blob.js +'use strict'; + +test(function() { + assert_true("Blob" in globalThis, "globalThis should have a Blob property."); + assert_equals(Blob.length, 0, "Blob.length should be 0."); + assert_true(Blob instanceof Function, "Blob should be a function."); +}, "Blob interface object"); + +// Step 1. +test(function() { + var blob = new Blob(); + assert_true(blob instanceof Blob); + assert_equals(String(blob), '[object Blob]'); + assert_equals(blob.size, 0); + assert_equals(blob.type, ""); +}, "Blob constructor with no arguments"); +test(function() { + assert_throws_js(TypeError, function() { var blob = Blob(); }); +}, "Blob constructor with no arguments, without 'new'"); +test(function() { + var blob = new Blob; + assert_true(blob instanceof Blob); + assert_equals(blob.size, 0); + assert_equals(blob.type, ""); +}, "Blob constructor without brackets"); +test(function() { + var blob = new Blob(undefined); + assert_true(blob instanceof Blob); + assert_equals(String(blob), '[object Blob]'); + assert_equals(blob.size, 0); + assert_equals(blob.type, ""); +}, "Blob constructor with undefined as first argument"); + +// blobParts argument (WebIDL). +test(function() { + var args = [ + null, + true, + false, + 0, + 1, + 1.5, + "FAIL", + new Date(), + new RegExp(), + {}, + { 0: "FAIL", length: 1 }, + ]; + args.forEach(function(arg) { + assert_throws_js(TypeError, function() { + new Blob(arg); + }, "Should throw for argument " + format_value(arg) + "."); + }); +}, "Passing non-objects, Dates and RegExps for blobParts should throw a TypeError."); + +test_blob(function() { + return new Blob({ + [Symbol.iterator]: Array.prototype[Symbol.iterator], + }); +}, { + expected: "", + type: "", + desc: "A plain object with @@iterator should be treated as a sequence for the blobParts argument." +}); +test(t => { + const blob = new Blob({ + [Symbol.iterator]() { + var i = 0; + return {next: () => [ + {done:false, value:'ab'}, + {done:false, value:'cde'}, + {done:true} + ][i++] + }; + } + }); + assert_equals(blob.size, 5, 'Custom @@iterator should be treated as a sequence'); +}, "A plain object with custom @@iterator should be treated as a sequence for the blobParts argument."); +test_blob(function() { + return new Blob({ + [Symbol.iterator]: Array.prototype[Symbol.iterator], + 0: "PASS", + length: 1 + }); +}, { + expected: "PASS", + type: "", + desc: "A plain object with @@iterator and a length property should be treated as a sequence for the blobParts argument." +}); +test_blob(function() { + return new Blob(new String("xyz")); +}, { + expected: "xyz", + type: "", + desc: "A String object should be treated as a sequence for the blobParts argument." +}); +test_blob(function() { + return new Blob(new Uint8Array([1, 2, 3])); +}, { + expected: "123", + type: "", + desc: "A Uint8Array object should be treated as a sequence for the blobParts argument." +}); + +var test_error = { + name: "test", + message: "test error", +}; + +test(function() { + var obj = { + [Symbol.iterator]: Array.prototype[Symbol.iterator], + get length() { throw test_error; } + }; + assert_throws_exactly(test_error, function() { + new Blob(obj); + }); +}, "The length getter should be invoked and any exceptions should be propagated."); + +test(function() { + assert_throws_exactly(test_error, function() { + var obj = { + [Symbol.iterator]: Array.prototype[Symbol.iterator], + length: { + valueOf: null, + toString: function() { throw test_error; } + } + }; + new Blob(obj); + }); + assert_throws_exactly(test_error, function() { + var obj = { + [Symbol.iterator]: Array.prototype[Symbol.iterator], + length: { valueOf: function() { throw test_error; } } + }; + new Blob(obj); + }); +}, "ToUint32 should be applied to the length and any exceptions should be propagated."); + +test(function() { + var received = []; + var obj = { + get [Symbol.iterator]() { + received.push("Symbol.iterator"); + return Array.prototype[Symbol.iterator]; + }, + get length() { + received.push("length getter"); + return { + valueOf: function() { + received.push("length valueOf"); + return 3; + } + }; + }, + get 0() { + received.push("0 getter"); + return { + toString: function() { + received.push("0 toString"); + return "a"; + } + }; + }, + get 1() { + received.push("1 getter"); + throw test_error; + }, + get 2() { + received.push("2 getter"); + assert_unreached("Should not call the getter for 2 if the getter for 1 threw."); + } + }; + assert_throws_exactly(test_error, function() { + new Blob(obj); + }); + assert_array_equals(received, [ + "Symbol.iterator", + "length getter", + "length valueOf", + "0 getter", + "0 toString", + "length getter", + "length valueOf", + "1 getter", + ]); +}, "Getters and value conversions should happen in order until an exception is thrown."); + +// XXX should add tests edge cases of ToLength(length) + +test(function() { + assert_throws_exactly(test_error, function() { + new Blob([{ toString: function() { throw test_error; } }]); + }, "Throwing toString"); + assert_throws_exactly(test_error, function() { + new Blob([{ toString: undefined, valueOf: function() { throw test_error; } }]); + }, "Throwing valueOf"); + assert_throws_exactly(test_error, function() { + new Blob([{ + toString: function() { throw test_error; }, + valueOf: function() { assert_unreached("Should not call valueOf if toString is present."); } + }]); + }, "Throwing toString and valueOf"); + assert_throws_js(TypeError, function() { + new Blob([{toString: null, valueOf: null}]); + }, "Null toString and valueOf"); +}, "ToString should be called on elements of the blobParts array and any exceptions should be propagated."); + +test_blob(function() { + var arr = [ + { toString: function() { arr.pop(); return "PASS"; } }, + { toString: function() { assert_unreached("Should have removed the second element of the array rather than called toString() on it."); } } + ]; + return new Blob(arr); +}, { + expected: "PASS", + type: "", + desc: "Changes to the blobParts array should be reflected in the returned Blob (pop)." +}); + +test_blob(function() { + var arr = [ + { + toString: function() { + if (arr.length === 3) { + return "A"; + } + arr.unshift({ + toString: function() { + assert_unreached("Should only access index 0 once."); + } + }); + return "P"; + } + }, + { + toString: function() { + return "SS"; + } + } + ]; + return new Blob(arr); +}, { + expected: "PASS", + type: "", + desc: "Changes to the blobParts array should be reflected in the returned Blob (unshift)." +}); + +test_blob(function() { + // https://www.w3.org/Bugs/Public/show_bug.cgi?id=17652 + return new Blob([ + null, + undefined, + true, + false, + 0, + 1, + new String("stringobject"), + [], + ['x', 'y'], + {}, + { 0: "FAIL", length: 1 }, + { toString: function() { return "stringA"; } }, + { toString: undefined, valueOf: function() { return "stringB"; } }, + { valueOf: function() { assert_unreached("Should not call valueOf if toString is present on the prototype."); } } + ]); +}, { + expected: "nullundefinedtruefalse01stringobjectx,y[object Object][object Object]stringAstringB[object Object]", + type: "", + desc: "ToString should be called on elements of the blobParts array." +}); + +test_blob(function() { + return new Blob([ + new ArrayBuffer(8) + ]); +}, { + expected: "\0\0\0\0\0\0\0\0", + type: "", + desc: "ArrayBuffer elements of the blobParts array should be supported." +}); + +test_blob(function() { + return new Blob([ + new Uint8Array([0x50, 0x41, 0x53, 0x53]), + new Int8Array([0x50, 0x41, 0x53, 0x53]), + new Uint16Array([0x4150, 0x5353]), + new Int16Array([0x4150, 0x5353]), + new Uint32Array([0x53534150]), + new Int32Array([0x53534150]), + new Float32Array([0xD341500000]) + ]); +}, { + expected: "PASSPASSPASSPASSPASSPASSPASS", + type: "", + desc: "Passing typed arrays as elements of the blobParts array should work." +}); +test_blob(function() { + return new Blob([ + // 0x535 3415053534150 + // 0x535 = 0b010100110101 -> Sign = +, Exponent = 1333 - 1023 = 310 + // 0x13415053534150 * 2**(-52) + // ==> 0x13415053534150 * 2**258 = 2510297372767036725005267563121821874921913208671273727396467555337665343087229079989707079680 + new Float64Array([2510297372767036725005267563121821874921913208671273727396467555337665343087229079989707079680]) + ]); +}, { + expected: "PASSPASS", + type: "", + desc: "Passing a Float64Array as element of the blobParts array should work." +}); + +test_blob(function() { + return new Blob([ + new BigInt64Array([BigInt("0x5353415053534150")]), + new BigUint64Array([BigInt("0x5353415053534150")]) + ]); +}, { + expected: "PASSPASSPASSPASS", + type: "", + desc: "Passing BigInt typed arrays as elements of the blobParts array should work." +}); + +var t_ports = async_test("Passing a FrozenArray as the blobParts array should work (FrozenArray)."); +t_ports.step(function() { + var channel = new MessageChannel(); + channel.port2.onmessage = this.step_func(function(e) { + var b_ports = new Blob(e.ports); + assert_equals(b_ports.size, "[object MessagePort]".length); + this.done(); + }); + var channel2 = new MessageChannel(); + channel.port1.postMessage('', [channel2.port1]); +}); + +test_blob(function() { + var blob = new Blob(['foo']); + return new Blob([blob, blob]); +}, { + expected: "foofoo", + type: "", + desc: "Array with two blobs" +}); + +test_blob_binary(function() { + var view = new Uint8Array([0, 255, 0]); + return new Blob([view.buffer, view.buffer]); +}, { + expected: [0, 255, 0, 0, 255, 0], + type: "", + desc: "Array with two buffers" +}); + +test_blob_binary(function() { + var view = new Uint8Array([0, 255, 0, 4]); + var blob = new Blob([view, view]); + assert_equals(blob.size, 8); + var view1 = new Uint16Array(view.buffer, 2); + return new Blob([view1, view.buffer, view1]); +}, { + expected: [0, 4, 0, 255, 0, 4, 0, 4], + type: "", + desc: "Array with two bufferviews" +}); + +test_blob(function() { + var view = new Uint8Array([0]); + var blob = new Blob(["fo"]); + return new Blob([view.buffer, blob, "foo"]); +}, { + expected: "\0fofoo", + type: "", + desc: "Array with mixed types" +}); + +test(function() { + const accessed = []; + const stringified = []; + + new Blob([], { + get type() { accessed.push('type'); }, + get endings() { accessed.push('endings'); } + }); + new Blob([], { + type: { toString: () => { stringified.push('type'); return ''; } }, + endings: { toString: () => { stringified.push('endings'); return 'transparent'; } } + }); + assert_array_equals(accessed, ['endings', 'type']); + assert_array_equals(stringified, ['endings', 'type']); +}, "options properties should be accessed in lexicographic order."); + +test(function() { + assert_throws_exactly(test_error, function() { + new Blob( + [{ toString: function() { throw test_error } }], + { + get type() { assert_unreached("type getter should not be called."); } + } + ); + }); +}, "Arguments should be evaluated from left to right."); + +[ + null, + undefined, + {}, + { unrecognized: true }, + /regex/, + function() {} +].forEach(function(arg, idx) { + test_blob(function() { + return new Blob([], arg); + }, { + expected: "", + type: "", + desc: "Passing " + format_value(arg) + " (index " + idx + ") for options should use the defaults." + }); + test_blob(function() { + return new Blob(["\na\r\nb\n\rc\r"], arg); + }, { + expected: "\na\r\nb\n\rc\r", + type: "", + desc: "Passing " + format_value(arg) + " (index " + idx + ") for options should use the defaults (with newlines)." + }); +}); + +[ + 123, + 123.4, + true, + 'abc' +].forEach(arg => { + test(t => { + assert_throws_js(TypeError, () => new Blob([], arg), + 'Blob constructor should throw with invalid property bag'); + }, `Passing ${JSON.stringify(arg)} for options should throw`); +}); + +var type_tests = [ + // blobParts, type, expected type + [[], '', ''], + [[], 'a', 'a'], + [[], 'A', 'a'], + [[], 'text/html', 'text/html'], + [[], 'TEXT/HTML', 'text/html'], + [[], 'text/plain;charset=utf-8', 'text/plain;charset=utf-8'], + [[], '\u00E5', ''], + [[], '\uD801\uDC7E', ''], // U+1047E + [[], ' image/gif ', ' image/gif '], + [[], '\timage/gif\t', ''], + [[], 'image/gif;\u007f', ''], + [[], '\u0130mage/gif', ''], // uppercase i with dot + [[], '\u0131mage/gif', ''], // lowercase dotless i + [[], 'image/gif\u0000', ''], + // check that type isn't changed based on sniffing + [[0x3C, 0x48, 0x54, 0x4D, 0x4C, 0x3E], 'unknown/unknown', 'unknown/unknown'], // "" + [[0x00, 0xFF], 'text/plain', 'text/plain'], + [[0x47, 0x49, 0x46, 0x38, 0x39, 0x61], 'image/png', 'image/png'], // "GIF89a" +]; + +type_tests.forEach(function(t) { + test(function() { + var arr = new Uint8Array([t[0]]).buffer; + var b = new Blob([arr], {type:t[1]}); + assert_equals(b.type, t[2]); + }, "Blob with type " + format_value(t[1])); +}); diff --git a/test/wpt/tests/FileAPI/blob/Blob-in-worker.worker.js b/test/wpt/tests/FileAPI/blob/Blob-in-worker.worker.js new file mode 100644 index 00000000000..a0ca84551dd --- /dev/null +++ b/test/wpt/tests/FileAPI/blob/Blob-in-worker.worker.js @@ -0,0 +1,9 @@ +importScripts("/resources/testharness.js"); + +promise_test(async () => { + const data = "TEST"; + const blob = new Blob([data], {type: "text/plain"}); + assert_equals(await blob.text(), data); +}, 'Create Blob in Worker'); + +done(); diff --git a/test/wpt/tests/FileAPI/blob/Blob-slice-overflow.any.js b/test/wpt/tests/FileAPI/blob/Blob-slice-overflow.any.js new file mode 100644 index 00000000000..388fd9282c9 --- /dev/null +++ b/test/wpt/tests/FileAPI/blob/Blob-slice-overflow.any.js @@ -0,0 +1,32 @@ +// META: title=Blob slice overflow +'use strict'; + +var text = ''; + +for (var i = 0; i < 2000; ++i) { + text += 'A'; +} + +test(function() { + var blob = new Blob([text]); + var sliceBlob = blob.slice(-1, blob.size); + assert_equals(sliceBlob.size, 1, "Blob slice size"); +}, "slice start is negative, relativeStart will be max((size + start), 0)"); + +test(function() { + var blob = new Blob([text]); + var sliceBlob = blob.slice(blob.size + 1, blob.size); + assert_equals(sliceBlob.size, 0, "Blob slice size"); +}, "slice start is greater than blob size, relativeStart will be min(start, size)"); + +test(function() { + var blob = new Blob([text]); + var sliceBlob = blob.slice(blob.size - 2, -1); + assert_equals(sliceBlob.size, 1, "Blob slice size"); +}, "slice end is negative, relativeEnd will be max((size + end), 0)"); + +test(function() { + var blob = new Blob([text]); + var sliceBlob = blob.slice(blob.size - 2, blob.size + 999); + assert_equals(sliceBlob.size, 2, "Blob slice size"); +}, "slice end is greater than blob size, relativeEnd will be min(end, size)"); diff --git a/test/wpt/tests/FileAPI/blob/Blob-slice.any.js b/test/wpt/tests/FileAPI/blob/Blob-slice.any.js new file mode 100644 index 00000000000..1f85d44d269 --- /dev/null +++ b/test/wpt/tests/FileAPI/blob/Blob-slice.any.js @@ -0,0 +1,231 @@ +// META: title=Blob slice +// META: script=../support/Blob.js +'use strict'; + +test_blob(function() { + var blobTemp = new Blob(["PASS"]); + return blobTemp.slice(); +}, { + expected: "PASS", + type: "", + desc: "no-argument Blob slice" +}); + +test(function() { + var blob1, blob2; + + test_blob(function() { + return blob1 = new Blob(["squiggle"]); + }, { + expected: "squiggle", + type: "", + desc: "blob1." + }); + + test_blob(function() { + return blob2 = new Blob(["steak"], {type: "content/type"}); + }, { + expected: "steak", + type: "content/type", + desc: "blob2." + }); + + test_blob(function() { + return new Blob().slice(0,0,null); + }, { + expected: "", + type: "null", + desc: "null type Blob slice" + }); + + test_blob(function() { + return new Blob().slice(0,0,undefined); + }, { + expected: "", + type: "", + desc: "undefined type Blob slice" + }); + + test_blob(function() { + return new Blob().slice(0,0); + }, { + expected: "", + type: "", + desc: "no type Blob slice" + }); + + var arrayBuffer = new ArrayBuffer(16); + var int8View = new Int8Array(arrayBuffer); + for (var i = 0; i < 16; i++) { + int8View[i] = i + 65; + } + + var testData = [ + [ + ["PASSSTRING"], + [{start: -6, contents: "STRING"}, + {start: -12, contents: "PASSSTRING"}, + {start: 4, contents: "STRING"}, + {start: 12, contents: ""}, + {start: 0, end: -6, contents: "PASS"}, + {start: 0, end: -12, contents: ""}, + {start: 0, end: 4, contents: "PASS"}, + {start: 0, end: 12, contents: "PASSSTRING"}, + {start: 7, end: 4, contents: ""}] + ], + + // Test 3 strings + [ + ["foo", "bar", "baz"], + [{start: 0, end: 9, contents: "foobarbaz"}, + {start: 0, end: 3, contents: "foo"}, + {start: 3, end: 9, contents: "barbaz"}, + {start: 6, end: 9, contents: "baz"}, + {start: 6, end: 12, contents: "baz"}, + {start: 0, end: 9, contents: "foobarbaz"}, + {start: 0, end: 11, contents: "foobarbaz"}, + {start: 10, end: 15, contents: ""}] + ], + + // Test string, Blob, string + [ + ["foo", blob1, "baz"], + [{start: 0, end: 3, contents: "foo"}, + {start: 3, end: 11, contents: "squiggle"}, + {start: 2, end: 4, contents: "os"}, + {start: 10, end: 12, contents: "eb"}] + ], + + // Test blob, string, blob + [ + [blob1, "foo", blob1], + [{start: 0, end: 8, contents: "squiggle"}, + {start: 7, end: 9, contents: "ef"}, + {start: 10, end: 12, contents: "os"}, + {start: 1, end: 4, contents: "qui"}, + {start: 12, end: 15, contents: "qui"}, + {start: 40, end: 60, contents: ""}] + ], + + // Test blobs all the way down + [ + [blob2, blob1, blob2], + [{start: 0, end: 5, contents: "steak"}, + {start: 5, end: 13, contents: "squiggle"}, + {start: 13, end: 18, contents: "steak"}, + {start: 1, end: 3, contents: "te"}, + {start: 6, end: 10, contents: "quig"}] + ], + + // Test an ArrayBufferView + [ + [int8View, blob1, "foo"], + [{start: 0, end: 8, contents: "ABCDEFGH"}, + {start: 8, end: 18, contents: "IJKLMNOPsq"}, + {start: 17, end: 20, contents: "qui"}, + {start: 4, end: 12, contents: "EFGHIJKL"}] + ], + + // Test a partial ArrayBufferView + [ + [new Uint8Array(arrayBuffer, 3, 5), blob1, "foo"], + [{start: 0, end: 8, contents: "DEFGHsqu"}, + {start: 8, end: 18, contents: "igglefoo"}, + {start: 4, end: 12, contents: "Hsquiggl"}] + ], + + // Test type coercion of a number + [ + [3, int8View, "foo"], + [{start: 0, end: 8, contents: "3ABCDEFG"}, + {start: 8, end: 18, contents: "HIJKLMNOPf"}, + {start: 17, end: 21, contents: "foo"}, + {start: 4, end: 12, contents: "DEFGHIJK"}] + ], + + [ + [(new Uint8Array([0, 255, 0])).buffer, + new Blob(['abcd']), + 'efgh', + 'ijklmnopqrstuvwxyz'], + [{start: 1, end: 4, contents: "\uFFFD\u0000a"}, + {start: 4, end: 8, contents: "bcde"}, + {start: 8, end: 12, contents: "fghi"}, + {start: 1, end: 12, contents: "\uFFFD\u0000abcdefghi"}] + ] + ]; + + testData.forEach(function(data, i) { + var blobs = data[0]; + var tests = data[1]; + tests.forEach(function(expectations, j) { + test(function() { + var blob = new Blob(blobs); + assert_true(blob instanceof Blob); + assert_false(blob instanceof File); + + test_blob(function() { + return expectations.end === undefined + ? blob.slice(expectations.start) + : blob.slice(expectations.start, expectations.end); + }, { + expected: expectations.contents, + type: "", + desc: "Slicing test: slice (" + i + "," + j + ")." + }); + }, "Slicing test (" + i + "," + j + ")."); + }); + }); +}, "Slices"); + +var invalidTypes = [ + "\xFF", + "te\x09xt/plain", + "te\x00xt/plain", + "te\x1Fxt/plain", + "te\x7Fxt/plain" +]; +invalidTypes.forEach(function(type) { + test_blob(function() { + var blob = new Blob(["PASS"]); + return blob.slice(0, 4, type); + }, { + expected: "PASS", + type: "", + desc: "Invalid contentType (" + format_value(type) + ")" + }); +}); + +var validTypes = [ + "te(xt/plain", + "te)xt/plain", + "text/plain", + "te@xt/plain", + "te,xt/plain", + "te;xt/plain", + "te:xt/plain", + "te\\xt/plain", + "te\"xt/plain", + "te/xt/plain", + "te[xt/plain", + "te]xt/plain", + "te?xt/plain", + "te=xt/plain", + "te{xt/plain", + "te}xt/plain", + "te\x20xt/plain", + "TEXT/PLAIN", + "text/plain;charset = UTF-8", + "text/plain;charset=UTF-8" +]; +validTypes.forEach(function(type) { + test_blob(function() { + var blob = new Blob(["PASS"]); + return blob.slice(0, 4, type); + }, { + expected: "PASS", + type: type.toLowerCase(), + desc: "Valid contentType (" + format_value(type) + ")" + }); +}); diff --git a/test/wpt/tests/FileAPI/blob/Blob-stream-byob-crash.html b/test/wpt/tests/FileAPI/blob/Blob-stream-byob-crash.html new file mode 100644 index 00000000000..5992ed1396c --- /dev/null +++ b/test/wpt/tests/FileAPI/blob/Blob-stream-byob-crash.html @@ -0,0 +1,11 @@ + + diff --git a/test/wpt/tests/FileAPI/blob/Blob-stream.any.js b/test/wpt/tests/FileAPI/blob/Blob-stream.any.js new file mode 100644 index 00000000000..87710a171a9 --- /dev/null +++ b/test/wpt/tests/FileAPI/blob/Blob-stream.any.js @@ -0,0 +1,83 @@ +// META: title=Blob Stream +// META: script=../support/Blob.js +// META: script=/common/gc.js +'use strict'; + +// Helper function that triggers garbage collection while reading a chunk +// if perform_gc is true. +async function read_and_gc(reader, perform_gc) { + // Passing Uint8Array for byte streams; non-byte streams will simply ignore it + const read_promise = reader.read(new Uint8Array(64)); + if (perform_gc) { + await garbageCollect(); + } + return read_promise; +} + +// Takes in a ReadableStream and reads from it until it is done, returning +// an array that contains the results of each read operation. If perform_gc +// is true, garbage collection is triggered while reading every chunk. +async function read_all_chunks(stream, { perform_gc = false, mode } = {}) { + assert_true(stream instanceof ReadableStream); + assert_true('getReader' in stream); + const reader = stream.getReader({ mode }); + + assert_true('read' in reader); + let read_value = await read_and_gc(reader, perform_gc); + + let out = []; + let i = 0; + while (!read_value.done) { + for (let val of read_value.value) { + out[i++] = val; + } + read_value = await read_and_gc(reader, perform_gc); + } + return out; +} + +promise_test(async () => { + const blob = new Blob(["PASS"]); + const stream = blob.stream(); + const chunks = await read_all_chunks(stream); + for (let [index, value] of chunks.entries()) { + assert_equals(value, "PASS".charCodeAt(index)); + } +}, "Blob.stream()") + +promise_test(async () => { + const blob = new Blob(); + const stream = blob.stream(); + const chunks = await read_all_chunks(stream); + assert_array_equals(chunks, []); +}, "Blob.stream() empty Blob") + +promise_test(async () => { + const input_arr = [8, 241, 48, 123, 151]; + const typed_arr = new Uint8Array(input_arr); + const blob = new Blob([typed_arr]); + const stream = blob.stream(); + const chunks = await read_all_chunks(stream); + assert_array_equals(chunks, input_arr); +}, "Blob.stream() non-unicode input") + +promise_test(async() => { + const input_arr = [8, 241, 48, 123, 151]; + const typed_arr = new Uint8Array(input_arr); + let blob = new Blob([typed_arr]); + const stream = blob.stream(); + blob = null; + await garbageCollect(); + const chunks = await read_all_chunks(stream, { perform_gc: true }); + assert_array_equals(chunks, input_arr); +}, "Blob.stream() garbage collection of blob shouldn't break stream" + + "consumption") + +promise_test(async () => { + const input_arr = [8, 241, 48, 123, 151]; + const typed_arr = new Uint8Array(input_arr); + let blob = new Blob([typed_arr]); + const stream = blob.stream(); + const chunks = await read_all_chunks(stream, { mode: "byob" }); + assert_array_equals(chunks, input_arr); +}, "Reading Blob.stream() with BYOB reader") diff --git a/test/wpt/tests/FileAPI/blob/Blob-text.any.js b/test/wpt/tests/FileAPI/blob/Blob-text.any.js new file mode 100644 index 00000000000..d04fa97cffe --- /dev/null +++ b/test/wpt/tests/FileAPI/blob/Blob-text.any.js @@ -0,0 +1,64 @@ +// META: title=Blob Text +// META: script=../support/Blob.js +'use strict'; + +promise_test(async () => { + const blob = new Blob(["PASS"]); + const text = await blob.text(); + assert_equals(text, "PASS"); +}, "Blob.text()") + +promise_test(async () => { + const blob = new Blob(); + const text = await blob.text(); + assert_equals(text, ""); +}, "Blob.text() empty blob data") + +promise_test(async () => { + const blob = new Blob(["P", "A", "SS"]); + const text = await blob.text(); + assert_equals(text, "PASS"); +}, "Blob.text() multi-element array in constructor") + +promise_test(async () => { + const non_unicode = "\u0061\u030A"; + const input_arr = new TextEncoder().encode(non_unicode); + const blob = new Blob([input_arr]); + const text = await blob.text(); + assert_equals(text, non_unicode); +}, "Blob.text() non-unicode") + +promise_test(async () => { + const blob = new Blob(["PASS"], { type: "text/plain;charset=utf-16le" }); + const text = await blob.text(); + assert_equals(text, "PASS"); +}, "Blob.text() different charset param in type option") + +promise_test(async () => { + const non_unicode = "\u0061\u030A"; + const input_arr = new TextEncoder().encode(non_unicode); + const blob = new Blob([input_arr], { type: "text/plain;charset=utf-16le" }); + const text = await blob.text(); + assert_equals(text, non_unicode); +}, "Blob.text() different charset param with non-ascii input") + +promise_test(async () => { + const input_arr = new Uint8Array([192, 193, 245, 246, 247, 248, 249, 250, 251, + 252, 253, 254, 255]); + const blob = new Blob([input_arr]); + const text = await blob.text(); + assert_equals(text, "\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd" + + "\ufffd\ufffd\ufffd\ufffd"); +}, "Blob.text() invalid utf-8 input") + +promise_test(async () => { + const input_arr = new Uint8Array([192, 193, 245, 246, 247, 248, 249, 250, 251, + 252, 253, 254, 255]); + const blob = new Blob([input_arr]); + const text_results = await Promise.all([blob.text(), blob.text(), + blob.text()]); + for (let text of text_results) { + assert_equals(text, "\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd" + + "\ufffd\ufffd\ufffd\ufffd"); + } +}, "Blob.text() concurrent reads") diff --git a/test/wpt/tests/FileAPI/file/File-constructor-endings.html b/test/wpt/tests/FileAPI/file/File-constructor-endings.html new file mode 100644 index 00000000000..1282b6c5ac2 --- /dev/null +++ b/test/wpt/tests/FileAPI/file/File-constructor-endings.html @@ -0,0 +1,104 @@ + + +File constructor: endings option + + + + diff --git a/test/wpt/tests/FileAPI/file/Worker-read-file-constructor.worker.js b/test/wpt/tests/FileAPI/file/Worker-read-file-constructor.worker.js new file mode 100644 index 00000000000..4e003b3c958 --- /dev/null +++ b/test/wpt/tests/FileAPI/file/Worker-read-file-constructor.worker.js @@ -0,0 +1,15 @@ +importScripts("/resources/testharness.js"); + +async_test(function() { + var file = new File(["bits"], "dummy", { 'type': 'text/plain', lastModified: 42 }); + var reader = new FileReader(); + reader.onload = this.step_func_done(function() { + assert_equals(file.name, "dummy", "file name"); + assert_equals(reader.result, "bits", "file content"); + assert_equals(file.lastModified, 42, "file lastModified"); + }); + reader.onerror = this.unreached_func("Unexpected error event"); + reader.readAsText(file); +}, "FileReader in Worker"); + +done(); diff --git a/test/wpt/tests/FileAPI/file/resources/echo-content-escaped.py b/test/wpt/tests/FileAPI/file/resources/echo-content-escaped.py new file mode 100644 index 00000000000..5370e1e46ac --- /dev/null +++ b/test/wpt/tests/FileAPI/file/resources/echo-content-escaped.py @@ -0,0 +1,26 @@ +from wptserve.utils import isomorphic_encode + +# Outputs the request body, with controls and non-ASCII bytes escaped +# (b"\n" becomes b"\\x0a"), and with backslashes doubled. +# As a convenience, CRLF newlines are left as is. + +def escape_byte(byte): + # Convert int byte into a single-char binary string. + byte = bytes([byte]) + if b"\0" <= byte <= b"\x1F" or byte >= b"\x7F": + return b"\\x%02x" % ord(byte) + if byte == b"\\": + return b"\\\\" + return byte + +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; charset=UTF-8")] + + content = b"".join(map(escape_byte, request.body)).replace(b"\\x0d\\x0a", b"\r\n") + + return headers, content diff --git a/test/wpt/tests/FileAPI/file/send-file-form-controls.html b/test/wpt/tests/FileAPI/file/send-file-form-controls.html new file mode 100644 index 00000000000..6347065bcae --- /dev/null +++ b/test/wpt/tests/FileAPI/file/send-file-form-controls.html @@ -0,0 +1,113 @@ + + +Upload files named using controls + + + + + + + + diff --git a/test/wpt/tests/FileAPI/file/send-file-form-iso-2022-jp.html b/test/wpt/tests/FileAPI/file/send-file-form-iso-2022-jp.html new file mode 100644 index 00000000000..c931c9be3ab --- /dev/null +++ b/test/wpt/tests/FileAPI/file/send-file-form-iso-2022-jp.html @@ -0,0 +1,65 @@ + + + +Upload files in ISO-2022-JP form + + + + + + + + diff --git a/test/wpt/tests/FileAPI/file/send-file-form-punctuation.html b/test/wpt/tests/FileAPI/file/send-file-form-punctuation.html new file mode 100644 index 00000000000..a6568e2e56e --- /dev/null +++ b/test/wpt/tests/FileAPI/file/send-file-form-punctuation.html @@ -0,0 +1,226 @@ + + +Upload files named using punctuation + + + + + + + + diff --git a/test/wpt/tests/FileAPI/file/send-file-form-utf-8.html b/test/wpt/tests/FileAPI/file/send-file-form-utf-8.html new file mode 100644 index 00000000000..1be44f4f4db --- /dev/null +++ b/test/wpt/tests/FileAPI/file/send-file-form-utf-8.html @@ -0,0 +1,62 @@ + + +Upload files in UTF-8 form + + + + + + + + diff --git a/test/wpt/tests/FileAPI/file/send-file-form-windows-1252.html b/test/wpt/tests/FileAPI/file/send-file-form-windows-1252.html new file mode 100644 index 00000000000..21b219ffd2d --- /dev/null +++ b/test/wpt/tests/FileAPI/file/send-file-form-windows-1252.html @@ -0,0 +1,62 @@ + + +Upload files in Windows-1252 form + + + + + + + + diff --git a/test/wpt/tests/FileAPI/file/send-file-form-x-user-defined.html b/test/wpt/tests/FileAPI/file/send-file-form-x-user-defined.html new file mode 100644 index 00000000000..8d6605d86de --- /dev/null +++ b/test/wpt/tests/FileAPI/file/send-file-form-x-user-defined.html @@ -0,0 +1,63 @@ + + +Upload files in x-user-defined form + + + + + + + + diff --git a/test/wpt/tests/FileAPI/file/send-file-form.html b/test/wpt/tests/FileAPI/file/send-file-form.html new file mode 100644 index 00000000000..baa8d4286c5 --- /dev/null +++ b/test/wpt/tests/FileAPI/file/send-file-form.html @@ -0,0 +1,25 @@ + + +Upload ASCII-named file in UTF-8 form + + + + + + + + diff --git a/test/wpt/tests/FileAPI/filelist-section/filelist.html b/test/wpt/tests/FileAPI/filelist-section/filelist.html new file mode 100644 index 00000000000..b97dcde19f6 --- /dev/null +++ b/test/wpt/tests/FileAPI/filelist-section/filelist.html @@ -0,0 +1,57 @@ + + + + + FileAPI Test: filelist + + + + + + + + + +
+ +
+
+ + + + + diff --git a/test/wpt/tests/FileAPI/filelist-section/filelist_multiple_selected_files-manual.html b/test/wpt/tests/FileAPI/filelist-section/filelist_multiple_selected_files-manual.html new file mode 100644 index 00000000000..2efaa059fa4 --- /dev/null +++ b/test/wpt/tests/FileAPI/filelist-section/filelist_multiple_selected_files-manual.html @@ -0,0 +1,64 @@ + + + + + FileAPI Test: filelist_multiple_selected_files + + + + + + + + + +
+ +
+
+

Test steps:

+
    +
  1. Download upload.txt, upload.zip to local.
  2. +
  3. Select the local two files (upload.txt, upload.zip) to run the test.
  4. +
+
+ +
+ + + + diff --git a/test/wpt/tests/FileAPI/filelist-section/filelist_selected_file-manual.html b/test/wpt/tests/FileAPI/filelist-section/filelist_selected_file-manual.html new file mode 100644 index 00000000000..966aadda615 --- /dev/null +++ b/test/wpt/tests/FileAPI/filelist-section/filelist_selected_file-manual.html @@ -0,0 +1,64 @@ + + + + + FileAPI Test: filelist_selected_file + + + + + + + + + +
+ +
+
+

Test steps:

+
    +
  1. Download upload.txt to local.
  2. +
  3. Select the local upload.txt file to run the test.
  4. +
+
+ +
+ + + + diff --git a/test/wpt/tests/FileAPI/filelist-section/support/upload.txt b/test/wpt/tests/FileAPI/filelist-section/support/upload.txt new file mode 100644 index 00000000000..f45965b711f --- /dev/null +++ b/test/wpt/tests/FileAPI/filelist-section/support/upload.txt @@ -0,0 +1 @@ +Hello, this is test file for file upload. diff --git a/test/wpt/tests/FileAPI/filelist-section/support/upload.zip b/test/wpt/tests/FileAPI/filelist-section/support/upload.zip new file mode 100644 index 0000000000000000000000000000000000000000..a933d6a949428cc52dc51687c126af4ba66cc084 GIT binary patch literal 220 zcmWIWW@Zs#U|`^2u&Md)bl|2>v<8r;4aEEmG7O~!Ir)hxX_+~xMtUU`C7~gl49t)B z{s?>m#HAJ742&!C$r#AlDQCr@4v)Hr!UCyZgyq$`hvTDP2;6QbO@<&Tnq_rfg- z>h^}N{oD)z-i%Cg%($$S09wqzzzD=!8bK@!2e3jMfM$7sH!B-RIU^8;0_j2!hXDZ7 C6g&_B literal 0 HcmV?d00001 diff --git a/test/wpt/tests/FileAPI/historical.https.html b/test/wpt/tests/FileAPI/historical.https.html new file mode 100644 index 00000000000..4f841f17639 --- /dev/null +++ b/test/wpt/tests/FileAPI/historical.https.html @@ -0,0 +1,65 @@ + + + + + Historical features + + + + + +
+ + + diff --git a/test/wpt/tests/FileAPI/idlharness-manual.html b/test/wpt/tests/FileAPI/idlharness-manual.html new file mode 100644 index 00000000000..c1d8b0c7149 --- /dev/null +++ b/test/wpt/tests/FileAPI/idlharness-manual.html @@ -0,0 +1,45 @@ + + + + + File API manual IDL tests + + + + + + + + +

File API manual IDL tests

+ +

Either download upload.txt and select it below or select an + arbitrary local file.

+ +
+ +
+ +
+ + + + diff --git a/test/wpt/tests/FileAPI/idlharness.html b/test/wpt/tests/FileAPI/idlharness.html new file mode 100644 index 00000000000..45e8684f002 --- /dev/null +++ b/test/wpt/tests/FileAPI/idlharness.html @@ -0,0 +1,37 @@ + + + + + File API automated IDL tests (requiring dom) + + + + + + + + +

File API automated IDL tests

+ +
+ +
+ +
+ + + + + diff --git a/test/wpt/tests/FileAPI/idlharness.worker.js b/test/wpt/tests/FileAPI/idlharness.worker.js new file mode 100644 index 00000000000..002aaed40a5 --- /dev/null +++ b/test/wpt/tests/FileAPI/idlharness.worker.js @@ -0,0 +1,17 @@ +importScripts("/resources/testharness.js"); +importScripts("/resources/WebIDLParser.js", "/resources/idlharness.js"); + +'use strict'; + +// https://w3c.github.io/FileAPI/ + +idl_test( + ['FileAPI'], + ['dom', 'html', 'url'], + idl_array => { + idl_array.add_objects({ + FileReaderSync: ['new FileReaderSync()'] + }); + } +); +done(); diff --git a/test/wpt/tests/FileAPI/progress-manual.html b/test/wpt/tests/FileAPI/progress-manual.html new file mode 100644 index 00000000000..b2e03b3eb27 --- /dev/null +++ b/test/wpt/tests/FileAPI/progress-manual.html @@ -0,0 +1,49 @@ + + +Process Events for FileReader + + + + +Please choose one file through this input below.
+ +
+ diff --git a/test/wpt/tests/FileAPI/reading-data-section/filereader_file-manual.html b/test/wpt/tests/FileAPI/reading-data-section/filereader_file-manual.html new file mode 100644 index 00000000000..702ca9afd7b --- /dev/null +++ b/test/wpt/tests/FileAPI/reading-data-section/filereader_file-manual.html @@ -0,0 +1,69 @@ + + + + + FileAPI Test: filereader_file + + + + + + + +
+

Test step:

+
    +
  1. Download blue-100x100.png to local.
  2. +
  3. Select the local file (blue-100x100.png) to run the test.
  4. +
+
+ +
+ +
+ +
+ + + diff --git a/test/wpt/tests/FileAPI/reading-data-section/filereader_file_img-manual.html b/test/wpt/tests/FileAPI/reading-data-section/filereader_file_img-manual.html new file mode 100644 index 00000000000..fca42c7fceb --- /dev/null +++ b/test/wpt/tests/FileAPI/reading-data-section/filereader_file_img-manual.html @@ -0,0 +1,47 @@ + + + + + FileAPI Test: filereader_file_img + + + + + + + +
+

Test step:

+
    +
  1. Download blue-100x100.png to local.
  2. +
  3. Select the local file (blue-100x100.png) to run the test.
  4. +
+
+ +
+ +
+ +
+ + + diff --git a/test/wpt/tests/FileAPI/reading-data-section/filereader_result.any.js b/test/wpt/tests/FileAPI/reading-data-section/filereader_result.any.js new file mode 100644 index 00000000000..28c068bb349 --- /dev/null +++ b/test/wpt/tests/FileAPI/reading-data-section/filereader_result.any.js @@ -0,0 +1,82 @@ +// META: title=FileAPI Test: filereader_result + + var blob, blob2; + setup(function() { + blob = new Blob(["This test the result attribute"]); + blob2 = new Blob(["This is a second blob"]); + }); + + async_test(function() { + var readText = new FileReader(); + assert_equals(readText.result, null); + + readText.onloadend = this.step_func(function(evt) { + assert_equals(typeof readText.result, "string", "The result type is string"); + assert_equals(readText.result, "This test the result attribute", "The result is correct"); + this.done(); + }); + + readText.readAsText(blob); + }, "readAsText"); + + async_test(function() { + var readDataURL = new FileReader(); + assert_equals(readDataURL.result, null); + + readDataURL.onloadend = this.step_func(function(evt) { + assert_equals(typeof readDataURL.result, "string", "The result type is string"); + assert_true(readDataURL.result.indexOf("VGhpcyB0ZXN0IHRoZSByZXN1bHQgYXR0cmlidXRl") != -1, "return the right base64 string"); + this.done(); + }); + + readDataURL.readAsDataURL(blob); + }, "readAsDataURL"); + + async_test(function() { + var readArrayBuffer = new FileReader(); + assert_equals(readArrayBuffer.result, null); + + readArrayBuffer.onloadend = this.step_func(function(evt) { + assert_true(readArrayBuffer.result instanceof ArrayBuffer, "The result is instanceof ArrayBuffer"); + this.done(); + }); + + readArrayBuffer.readAsArrayBuffer(blob); + }, "readAsArrayBuffer"); + + async_test(function() { + var readBinaryString = new FileReader(); + assert_equals(readBinaryString.result, null); + + readBinaryString.onloadend = this.step_func(function(evt) { + assert_equals(typeof readBinaryString.result, "string", "The result type is string"); + assert_equals(readBinaryString.result, "This test the result attribute", "The result is correct"); + this.done(); + }); + + readBinaryString.readAsBinaryString(blob); + }, "readAsBinaryString"); + + + for (let event of ['loadstart', 'progress']) { + for (let method of ['readAsText', 'readAsDataURL', 'readAsArrayBuffer', 'readAsBinaryString']) { + promise_test(async function(t) { + var reader = new FileReader(); + assert_equals(reader.result, null, 'result is null before read'); + + var eventWatcher = new EventWatcher(t, reader, + [event, 'loadend']); + + reader[method](blob); + assert_equals(reader.result, null, 'result is null after first read call'); + await eventWatcher.wait_for(event); + assert_equals(reader.result, null, 'result is null during event'); + await eventWatcher.wait_for('loadend'); + assert_not_equals(reader.result, null); + reader[method](blob); + assert_equals(reader.result, null, 'result is null after second read call'); + await eventWatcher.wait_for(event); + assert_equals(reader.result, null, 'result is null during second read event'); + }, 'result is null during "' + event + '" event for ' + method); + } + } diff --git a/test/wpt/tests/FileAPI/reading-data-section/support/blue-100x100.png b/test/wpt/tests/FileAPI/reading-data-section/support/blue-100x100.png new file mode 100644 index 0000000000000000000000000000000000000000..5748719ff22a34993eeb4de4d1fe7cfd27e92959 GIT binary patch literal 227 zcmeAS@N?(olHy`uVBq!ia0vp^DIm~)y|_`3!GMF=@cn+{ zM(Z>W$Ayn<8>TLantrR&N?1UFiOI#GfrCSUk?|1=5=V*VY==1v5S#zzF*vSX*L$9$ Q1mqnCPgg&ebxsLQ0LwxylmGw# literal 0 HcmV?d00001 diff --git a/test/wpt/tests/FileAPI/support/Blob.js b/test/wpt/tests/FileAPI/support/Blob.js new file mode 100644 index 00000000000..2c249746858 --- /dev/null +++ b/test/wpt/tests/FileAPI/support/Blob.js @@ -0,0 +1,70 @@ +'use strict' + +self.test_blob = (fn, expectations) => { + var expected = expectations.expected, + type = expectations.type, + desc = expectations.desc; + + var t = async_test(desc); + t.step(function() { + var blob = fn(); + assert_true(blob instanceof Blob); + assert_false(blob instanceof File); + assert_equals(blob.type, type); + assert_equals(blob.size, expected.length); + + var fr = new FileReader(); + fr.onload = t.step_func_done(function(event) { + assert_equals(this.result, expected); + }, fr); + fr.onerror = t.step_func(function(e) { + assert_unreached("got error event on FileReader"); + }); + fr.readAsText(blob, "UTF-8"); + }); +} + +self.test_blob_binary = (fn, expectations) => { + var expected = expectations.expected, + type = expectations.type, + desc = expectations.desc; + + var t = async_test(desc); + t.step(function() { + var blob = fn(); + assert_true(blob instanceof Blob); + assert_false(blob instanceof File); + assert_equals(blob.type, type); + assert_equals(blob.size, expected.length); + + var fr = new FileReader(); + fr.onload = t.step_func_done(function(event) { + assert_true(this.result instanceof ArrayBuffer, + "Result should be an ArrayBuffer"); + assert_array_equals(new Uint8Array(this.result), expected); + }, fr); + fr.onerror = t.step_func(function(e) { + assert_unreached("got error event on FileReader"); + }); + fr.readAsArrayBuffer(blob); + }); +} + +// Assert that two TypedArray objects have the same byte values +self.assert_equals_typed_array = (array1, array2) => { + const [view1, view2] = [array1, array2].map((array) => { + assert_true(array.buffer instanceof ArrayBuffer, + 'Expect input ArrayBuffers to contain field `buffer`'); + return new DataView(array.buffer, array.byteOffset, array.byteLength); + }); + + assert_equals(view1.byteLength, view2.byteLength, + 'Expect both arrays to be of the same byte length'); + + const byteLength = view1.byteLength; + + for (let i = 0; i < byteLength; ++i) { + assert_equals(view1.getUint8(i), view2.getUint8(i), + `Expect byte at buffer position ${i} to be equal`); + } +} diff --git a/test/wpt/tests/FileAPI/support/document-domain-setter.sub.html b/test/wpt/tests/FileAPI/support/document-domain-setter.sub.html new file mode 100644 index 00000000000..61aebdf3266 --- /dev/null +++ b/test/wpt/tests/FileAPI/support/document-domain-setter.sub.html @@ -0,0 +1,7 @@ + +Relevant/current/blob source page used as a test helper + + diff --git a/test/wpt/tests/FileAPI/support/empty-document.html b/test/wpt/tests/FileAPI/support/empty-document.html new file mode 100644 index 00000000000..b9cd130a07f --- /dev/null +++ b/test/wpt/tests/FileAPI/support/empty-document.html @@ -0,0 +1,3 @@ + + + diff --git a/test/wpt/tests/FileAPI/support/historical-serviceworker.js b/test/wpt/tests/FileAPI/support/historical-serviceworker.js new file mode 100644 index 00000000000..8bd89a23adb --- /dev/null +++ b/test/wpt/tests/FileAPI/support/historical-serviceworker.js @@ -0,0 +1,5 @@ +importScripts('/resources/testharness.js'); + +test(() => { + assert_false('FileReaderSync' in self); +}, '"FileReaderSync" should not be supported in service workers'); diff --git a/test/wpt/tests/FileAPI/support/incumbent.sub.html b/test/wpt/tests/FileAPI/support/incumbent.sub.html new file mode 100644 index 00000000000..63a81cd3281 --- /dev/null +++ b/test/wpt/tests/FileAPI/support/incumbent.sub.html @@ -0,0 +1,22 @@ + +Incumbent page used as a test helper + + + + + + diff --git a/test/wpt/tests/FileAPI/support/send-file-form-helper.js b/test/wpt/tests/FileAPI/support/send-file-form-helper.js new file mode 100644 index 00000000000..d6adf21ec33 --- /dev/null +++ b/test/wpt/tests/FileAPI/support/send-file-form-helper.js @@ -0,0 +1,282 @@ +'use strict'; + +// See /FileAPI/file/resources/echo-content-escaped.py +function escapeString(string) { + return string.replace(/\\/g, "\\\\").replace( + /[^\x20-\x7E]/g, + (x) => { + let hex = x.charCodeAt(0).toString(16); + if (hex.length < 2) hex = "0" + hex; + return `\\x${hex}`; + }, + ).replace(/\\x0d\\x0a/g, "\r\n"); +} + +// Rationale for this particular test character sequence, which is +// used in filenames and also in file contents: +// +// - ABC~ ensures the string starts with something we can read to +// ensure it is from the correct source; ~ is used because even +// some 1-byte otherwise-ASCII-like parts of ISO-2022-JP +// interpret it differently. +// - ‾¥ are inside a single-byte range of ISO-2022-JP and help +// diagnose problems due to filesystem encoding or locale +// - ≈ is inside IBM437 and helps diagnose problems due to filesystem +// encoding or locale +// - ¤ is inside Latin-1 and helps diagnose problems due to +// filesystem encoding or locale; it is also the "simplest" case +// needing substitution in ISO-2022-JP +// - ・ is inside a single-byte range of ISO-2022-JP in some variants +// and helps diagnose problems due to filesystem encoding or locale; +// on the web it is distinct when decoding but unified when encoding +// - ・ is inside a double-byte range of ISO-2022-JP and helps +// diagnose problems due to filesystem encoding or locale +// - • is inside Windows-1252 and helps diagnose problems due to +// filesystem encoding or locale and also ensures these aren't +// accidentally turned into e.g. control codes +// - ∙ is inside IBM437 and helps diagnose problems due to filesystem +// encoding or locale +// - · is inside Latin-1 and helps diagnose problems due to +// filesystem encoding or locale and also ensures HTML named +// character references (e.g. ·) are not used +// - ☼ is inside IBM437 shadowing C0 and helps diagnose problems due to +// filesystem encoding or locale and also ensures these aren't +// accidentally turned into e.g. control codes +// - ★ is inside ISO-2022-JP on a non-Kanji page and makes correct +// output easier to spot +// - 星 is inside ISO-2022-JP on a Kanji page and makes correct +// output easier to spot +// - 🌟 is outside the BMP and makes incorrect surrogate pair +// substitution detectable and ensures substitutions work +// correctly immediately after Kanji 2-byte ISO-2022-JP +// - 星 repeated here ensures the correct codec state is used +// after a non-BMP substitution +// - ★ repeated here also makes correct output easier to spot +// - ☼ is inside IBM437 shadowing C0 and helps diagnose problems due to +// filesystem encoding or locale and also ensures these aren't +// accidentally turned into e.g. control codes and also ensures +// substitutions work correctly immediately after non-Kanji +// 2-byte ISO-2022-JP +// - · is inside Latin-1 and helps diagnose problems due to +// filesystem encoding or locale and also ensures HTML named +// character references (e.g. ·) are not used +// - ∙ is inside IBM437 and helps diagnose problems due to filesystem +// encoding or locale +// - • is inside Windows-1252 and again helps diagnose problems +// due to filesystem encoding or locale +// - ・ is inside a double-byte range of ISO-2022-JP and helps +// diagnose problems due to filesystem encoding or locale +// - ・ is inside a single-byte range of ISO-2022-JP in some variants +// and helps diagnose problems due to filesystem encoding or locale; +// on the web it is distinct when decoding but unified when encoding +// - ¤ is inside Latin-1 and helps diagnose problems due to +// filesystem encoding or locale; again it is a "simple" +// substitution case +// - ≈ is inside IBM437 and helps diagnose problems due to filesystem +// encoding or locale +// - ¥‾ are inside a single-byte range of ISO-2022-JP and help +// diagnose problems due to filesystem encoding or locale +// - ~XYZ ensures earlier errors don't lead to misencoding of +// simple ASCII +// +// Overall the near-symmetry makes common I18N mistakes like +// off-by-1-after-non-BMP easier to spot. All the characters +// are also allowed in Windows Unicode filenames. +const kTestChars = 'ABC~‾¥≈¤・・•∙·☼★星🌟星★☼·∙•・・¤≈¥‾~XYZ'; + +// The kTestFallback* strings represent the expected byte sequence from +// encoding kTestChars with the given encoding with "html" replacement +// mode, isomorphic-decoded. That means, characters that can't be +// encoded in that encoding get HTML-escaped, but no further +// `escapeString`-like escapes are needed. +const kTestFallbackUtf8 = ( + "ABC~\xE2\x80\xBE\xC2\xA5\xE2\x89\x88\xC2\xA4\xEF\xBD\xA5\xE3\x83\xBB\xE2" + + "\x80\xA2\xE2\x88\x99\xC2\xB7\xE2\x98\xBC\xE2\x98\x85\xE6\x98\x9F\xF0\x9F" + + "\x8C\x9F\xE6\x98\x9F\xE2\x98\x85\xE2\x98\xBC\xC2\xB7\xE2\x88\x99\xE2\x80" + + "\xA2\xE3\x83\xBB\xEF\xBD\xA5\xC2\xA4\xE2\x89\x88\xC2\xA5\xE2\x80\xBE~XYZ" +); + +const kTestFallbackIso2022jp = ( + ("ABC~\x1B(J~\\≈¤\x1B$B!&!&\x1B(B•∙·☼\x1B$B!z@1\x1B(B🌟" + + "\x1B$B@1!z\x1B(B☼·∙•\x1B$B!&!&\x1B(B¤≈\x1B(J\\~\x1B(B~XYZ") + .replace(/[^\0-\x7F]/gu, (x) => `&#${x.codePointAt(0)};`) +); + +const kTestFallbackWindows1252 = ( + "ABC~‾\xA5≈\xA4・・\x95∙\xB7☼★星🌟星★☼\xB7∙\x95・・\xA4≈\xA5‾~XYZ".replace( + /[^\0-\xFF]/gu, + (x) => `&#${x.codePointAt(0)};`, + ) +); + +const kTestFallbackXUserDefined = kTestChars.replace( + /[^\0-\x7F]/gu, + (x) => `&#${x.codePointAt(0)};`, +); + +// formPostFileUploadTest - verifies multipart upload structure and +// numeric character reference replacement for filenames, field names, +// and field values using form submission. +// +// Uses /FileAPI/file/resources/echo-content-escaped.py to echo the +// upload POST with controls and non-ASCII bytes escaped. This is done +// because navigations whose response body contains [\0\b\v] may get +// treated as a download, which is not what we want. Use the +// `escapeString` function to replicate that kind of escape (note that +// it takes an isomorphic-decoded string, not a byte sequence). +// +// Fields in the parameter object: +// +// - fileNameSource: purely explanatory and gives a clue about which +// character encoding is the source for the non-7-bit-ASCII parts of +// the fileBaseName, or Unicode if no smaller-than-Unicode source +// contains all the characters. Used in the test name. +// - fileBaseName: the not-necessarily-just-7-bit-ASCII file basename +// used for the constructed test file. Used in the test name. +// - formEncoding: the acceptCharset of the form used to submit the +// test file. Used in the test name. +// - expectedEncodedBaseName: the expected formEncoding-encoded +// version of fileBaseName, isomorphic-decoded. That means, characters +// that can't be encoded in that encoding get HTML-escaped, but no +// further `escapeString`-like escapes are needed. +const formPostFileUploadTest = ({ + fileNameSource, + fileBaseName, + formEncoding, + expectedEncodedBaseName, +}) => { + promise_test(async testCase => { + + if (document.readyState !== 'complete') { + await new Promise(resolve => addEventListener('load', resolve)); + } + + const formTargetFrame = Object.assign(document.createElement('iframe'), { + name: 'formtargetframe', + }); + document.body.append(formTargetFrame); + testCase.add_cleanup(() => { + document.body.removeChild(formTargetFrame); + }); + + const form = Object.assign(document.createElement('form'), { + acceptCharset: formEncoding, + action: '/FileAPI/file/resources/echo-content-escaped.py', + method: 'POST', + enctype: 'multipart/form-data', + target: formTargetFrame.name, + }); + document.body.append(form); + testCase.add_cleanup(() => { + document.body.removeChild(form); + }); + + // Used to verify that the browser agrees with the test about + // which form charset is used. + form.append(Object.assign(document.createElement('input'), { + type: 'hidden', + name: '_charset_', + })); + + // Used to verify that the browser agrees with the test about + // field value replacement and encoding independently of file system + // idiosyncracies. + form.append(Object.assign(document.createElement('input'), { + type: 'hidden', + name: 'filename', + value: fileBaseName, + })); + + // Same, but with name and value reversed to ensure field names + // get the same treatment. + form.append(Object.assign(document.createElement('input'), { + type: 'hidden', + name: fileBaseName, + value: 'filename', + })); + + const fileInput = Object.assign(document.createElement('input'), { + type: 'file', + name: 'file', + }); + form.append(fileInput); + + // Removes c:\fakepath\ or other pseudofolder and returns just the + // final component of filePath; allows both / and \ as segment + // delimiters. + const baseNameOfFilePath = filePath => filePath.split(/[\/\\]/).pop(); + await new Promise(resolve => { + const dataTransfer = new DataTransfer; + dataTransfer.items.add( + new File([kTestChars], fileBaseName, {type: 'text/plain'})); + fileInput.files = dataTransfer.files; + // For historical reasons .value will be prefixed with + // c:\fakepath\, but the basename should match the file name + // exposed through the newer .files[0].name API. This check + // verifies that assumption. + assert_equals( + baseNameOfFilePath(fileInput.files[0].name), + baseNameOfFilePath(fileInput.value), + `The basename of the field's value should match its files[0].name`); + form.submit(); + formTargetFrame.onload = resolve; + }); + + const formDataText = formTargetFrame.contentDocument.body.textContent; + const formDataLines = formDataText.split('\n'); + if (formDataLines.length && !formDataLines[formDataLines.length - 1]) { + --formDataLines.length; + } + assert_greater_than( + formDataLines.length, + 2, + `${fileBaseName}: multipart form data must have at least 3 lines: ${ + JSON.stringify(formDataText) + }`); + const boundary = formDataLines[0]; + assert_equals( + formDataLines[formDataLines.length - 1], + boundary + '--', + `${fileBaseName}: multipart form data must end with ${boundary}--: ${ + JSON.stringify(formDataText) + }`); + + const asValue = expectedEncodedBaseName.replace(/\r\n?|\n/g, "\r\n"); + const asName = asValue.replace(/[\r\n"]/g, encodeURIComponent); + const asFilename = expectedEncodedBaseName.replace(/[\r\n"]/g, encodeURIComponent); + + // The response body from echo-content-escaped.py has controls and non-ASCII + // bytes escaped, so any caller-provided field that might contain such bytes + // must be passed to `escapeString`, after any other expected + // transformations. + const expectedText = [ + boundary, + 'Content-Disposition: form-data; name="_charset_"', + '', + formEncoding, + boundary, + 'Content-Disposition: form-data; name="filename"', + '', + // Unlike for names and filenames, multipart/form-data values don't escape + // \r\n linebreaks, and when they're read from an iframe they become \n. + escapeString(asValue).replace(/\r\n/g, "\n"), + boundary, + `Content-Disposition: form-data; name="${escapeString(asName)}"`, + '', + 'filename', + boundary, + `Content-Disposition: form-data; name="file"; ` + + `filename="${escapeString(asFilename)}"`, + 'Content-Type: text/plain', + '', + escapeString(kTestFallbackUtf8), + boundary + '--', + ].join('\n'); + + assert_true( + formDataText.startsWith(expectedText), + `Unexpected multipart-shaped form data received:\n${ + formDataText + }\nExpected:\n${expectedText}`); + }, `Upload ${fileBaseName} (${fileNameSource}) in ${formEncoding} form`); +}; diff --git a/test/wpt/tests/FileAPI/support/upload.txt b/test/wpt/tests/FileAPI/support/upload.txt new file mode 100644 index 00000000000..5ab2f8a4323 --- /dev/null +++ b/test/wpt/tests/FileAPI/support/upload.txt @@ -0,0 +1 @@ +Hello \ No newline at end of file diff --git a/test/wpt/tests/FileAPI/support/url-origin.html b/test/wpt/tests/FileAPI/support/url-origin.html new file mode 100644 index 00000000000..63755113915 --- /dev/null +++ b/test/wpt/tests/FileAPI/support/url-origin.html @@ -0,0 +1,6 @@ + + diff --git a/test/wpt/tests/FileAPI/unicode.html b/test/wpt/tests/FileAPI/unicode.html new file mode 100644 index 00000000000..ce3e3579d7c --- /dev/null +++ b/test/wpt/tests/FileAPI/unicode.html @@ -0,0 +1,46 @@ + + +Blob/Unicode interaction: normalization and encoding + + + diff --git a/test/wpt/tests/FileAPI/url/cross-global-revoke.sub.html b/test/wpt/tests/FileAPI/url/cross-global-revoke.sub.html new file mode 100644 index 00000000000..ce9d680709e --- /dev/null +++ b/test/wpt/tests/FileAPI/url/cross-global-revoke.sub.html @@ -0,0 +1,62 @@ + + + + + + + diff --git a/test/wpt/tests/FileAPI/url/multi-global-origin-serialization.sub.html b/test/wpt/tests/FileAPI/url/multi-global-origin-serialization.sub.html new file mode 100644 index 00000000000..0052b26fa62 --- /dev/null +++ b/test/wpt/tests/FileAPI/url/multi-global-origin-serialization.sub.html @@ -0,0 +1,26 @@ + + +Blob URL serialization (specifically the origin) in multi-global situations + + + + + + + + + + + diff --git a/test/wpt/tests/FileAPI/url/resources/create-helper.html b/test/wpt/tests/FileAPI/url/resources/create-helper.html new file mode 100644 index 00000000000..fa6cf4e671e --- /dev/null +++ b/test/wpt/tests/FileAPI/url/resources/create-helper.html @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/test/wpt/tests/FileAPI/url/resources/create-helper.js b/test/wpt/tests/FileAPI/url/resources/create-helper.js new file mode 100644 index 00000000000..e6344f700ce --- /dev/null +++ b/test/wpt/tests/FileAPI/url/resources/create-helper.js @@ -0,0 +1,4 @@ +self.addEventListener('message', e => { + let url = URL.createObjectURL(e.data.blob); + self.postMessage({url: url}); +}); diff --git a/test/wpt/tests/FileAPI/url/resources/fetch-tests.js b/test/wpt/tests/FileAPI/url/resources/fetch-tests.js new file mode 100644 index 00000000000..a81ea1e7b1d --- /dev/null +++ b/test/wpt/tests/FileAPI/url/resources/fetch-tests.js @@ -0,0 +1,71 @@ +// This method generates a number of tests verifying fetching of blob URLs, +// allowing the same tests to be used both with fetch() and XMLHttpRequest. +// +// |fetch_method| is only used in test names, and should describe the +// (javascript) method being used by the other two arguments (i.e. 'fetch' or 'XHR'). +// +// |fetch_should_succeed| is a callback that is called with the Test and a URL. +// Fetching the URL is expected to succeed. The callback should return a promise +// resolved with whatever contents were fetched. +// +// |fetch_should_fail| similarly is a callback that is called with the Test, a URL +// to fetch, and optionally a method to use to do the fetch. If no method is +// specified the callback should use the 'GET' method. Fetching of these URLs is +// expected to fail, and the callback should return a promise that resolves iff +// fetching did indeed fail. +function fetch_tests(fetch_method, fetch_should_succeed, fetch_should_fail) { + const blob_contents = 'test blob contents'; + const blob = new Blob([blob_contents]); + + promise_test(t => { + const url = URL.createObjectURL(blob); + + return fetch_should_succeed(t, url).then(text => { + assert_equals(text, blob_contents); + }); + }, 'Blob URLs can be used in ' + fetch_method); + + promise_test(t => { + const url = URL.createObjectURL(blob); + + return fetch_should_succeed(t, url + '#fragment').then(text => { + assert_equals(text, blob_contents); + }); + }, fetch_method + ' with a fragment should succeed'); + + promise_test(t => { + const url = URL.createObjectURL(blob); + URL.revokeObjectURL(url); + + return fetch_should_fail(t, url); + }, fetch_method + ' of a revoked URL should fail'); + + promise_test(t => { + const url = URL.createObjectURL(blob); + URL.revokeObjectURL(url + '#fragment'); + + return fetch_should_succeed(t, url).then(text => { + assert_equals(text, blob_contents); + }); + }, 'Only exact matches should revoke URLs, using ' + fetch_method); + + promise_test(t => { + const url = URL.createObjectURL(blob); + + return fetch_should_fail(t, url + '?querystring'); + }, 'Appending a query string should cause ' + fetch_method + ' to fail'); + + promise_test(t => { + const url = URL.createObjectURL(blob); + + return fetch_should_fail(t, url + '/path'); + }, 'Appending a path should cause ' + fetch_method + ' to fail'); + + for (const method of ['HEAD', 'POST', 'DELETE', 'OPTIONS', 'PUT', 'CUSTOM']) { + const url = URL.createObjectURL(blob); + + promise_test(t => { + return fetch_should_fail(t, url, method); + }, fetch_method + ' with method "' + method + '" should fail'); + } +} \ No newline at end of file diff --git a/test/wpt/tests/FileAPI/url/resources/revoke-helper.html b/test/wpt/tests/FileAPI/url/resources/revoke-helper.html new file mode 100644 index 00000000000..adf5a014a66 --- /dev/null +++ b/test/wpt/tests/FileAPI/url/resources/revoke-helper.html @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/test/wpt/tests/FileAPI/url/resources/revoke-helper.js b/test/wpt/tests/FileAPI/url/resources/revoke-helper.js new file mode 100644 index 00000000000..c3e05b64b1a --- /dev/null +++ b/test/wpt/tests/FileAPI/url/resources/revoke-helper.js @@ -0,0 +1,9 @@ +self.addEventListener('message', e => { + URL.revokeObjectURL(e.data.url); + // Registering a new object URL will make absolutely sure that the revocation + // has propagated. Without this at least in chrome it is possible for the + // below postMessage to arrive at its destination before the revocation has + // been fully processed. + URL.createObjectURL(new Blob([])); + self.postMessage('revoked'); +}); diff --git a/test/wpt/tests/FileAPI/url/sandboxed-iframe.html b/test/wpt/tests/FileAPI/url/sandboxed-iframe.html new file mode 100644 index 00000000000..a52939a3eb2 --- /dev/null +++ b/test/wpt/tests/FileAPI/url/sandboxed-iframe.html @@ -0,0 +1,32 @@ + + +FileAPI Test: Verify behavior of Blob URL in unique origins + + + + + + + diff --git a/test/wpt/tests/FileAPI/url/unicode-origin.sub.html b/test/wpt/tests/FileAPI/url/unicode-origin.sub.html new file mode 100644 index 00000000000..2c4921c0344 --- /dev/null +++ b/test/wpt/tests/FileAPI/url/unicode-origin.sub.html @@ -0,0 +1,23 @@ + + +FileAPI Test: Verify origin of Blob URL + + + + diff --git a/test/wpt/tests/FileAPI/url/url-charset.window.js b/test/wpt/tests/FileAPI/url/url-charset.window.js new file mode 100644 index 00000000000..777709b64a5 --- /dev/null +++ b/test/wpt/tests/FileAPI/url/url-charset.window.js @@ -0,0 +1,34 @@ +async_test(t => { + // This could be detected as ISO-2022-JP, in which case there would be no + // bbb` + ], + {type: 'text/html;charset=utf-8'}); + const url = URL.createObjectURL(blob); + const win = window.open(url); + t.add_cleanup(() => { + win.close(); + }); + + win.onload = t.step_func_done(() => { + assert_equals(win.document.charset, 'UTF-8'); + }); +}, 'Blob charset should override any auto-detected charset.'); + +async_test(t => { + const blob = new Blob( + [`\n`], + {type: 'text/html;charset=utf-8'}); + const url = URL.createObjectURL(blob); + const win = window.open(url); + t.add_cleanup(() => { + win.close(); + }); + + win.onload = t.step_func_done(() => { + assert_equals(win.document.charset, 'UTF-8'); + }); +}, 'Blob charset should override .'); diff --git a/test/wpt/tests/FileAPI/url/url-format.any.js b/test/wpt/tests/FileAPI/url/url-format.any.js new file mode 100644 index 00000000000..69c51113e6b --- /dev/null +++ b/test/wpt/tests/FileAPI/url/url-format.any.js @@ -0,0 +1,70 @@ +// META: timeout=long +const blob = new Blob(['test']); +const file = new File(['test'], 'name'); + +test(t => { + const url_count = 5000; + let list = []; + + t.add_cleanup(() => { + for (let url of list) { + URL.revokeObjectURL(url); + } + }); + + for (let i = 0; i < url_count; ++i) + list.push(URL.createObjectURL(blob)); + + list.sort(); + + for (let i = 1; i < list.length; ++i) + assert_not_equals(list[i], list[i-1], 'generated Blob URLs should be unique'); +}, 'Generated Blob URLs are unique'); + +test(() => { + const url = URL.createObjectURL(blob); + assert_equals(typeof url, 'string'); + assert_true(url.startsWith('blob:')); +}, 'Blob URL starts with "blob:"'); + +test(() => { + const url = URL.createObjectURL(file); + assert_equals(typeof url, 'string'); + assert_true(url.startsWith('blob:')); +}, 'Blob URL starts with "blob:" for Files'); + +test(() => { + const url = URL.createObjectURL(blob); + assert_equals(new URL(url).origin, location.origin); + if (location.origin !== 'null') { + assert_true(url.includes(location.origin)); + assert_true(url.startsWith('blob:' + location.protocol)); + } +}, 'Origin of Blob URL matches our origin'); + +test(() => { + const url = URL.createObjectURL(blob); + const url_record = new URL(url); + assert_equals(url_record.protocol, 'blob:'); + assert_equals(url_record.origin, location.origin); + assert_equals(url_record.host, '', 'host should be an empty string'); + assert_equals(url_record.port, '', 'port should be an empty string'); + const uuid_path_re = /\/[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + assert_true(uuid_path_re.test(url_record.pathname), 'Path must end with a valid UUID'); + if (location.origin !== 'null') { + const nested_url = new URL(url_record.pathname); + assert_equals(nested_url.origin, location.origin); + assert_equals(nested_url.pathname.search(uuid_path_re), 0, 'Path must be a valid UUID'); + assert_true(url.includes(location.origin)); + assert_true(url.startsWith('blob:' + location.protocol)); + } +}, 'Blob URL parses correctly'); + +test(() => { + const url = URL.createObjectURL(file); + assert_equals(new URL(url).origin, location.origin); + if (location.origin !== 'null') { + assert_true(url.includes(location.origin)); + assert_true(url.startsWith('blob:' + location.protocol)); + } +}, 'Origin of Blob URL matches our origin for Files'); diff --git a/test/wpt/tests/FileAPI/url/url-in-tags-revoke.window.js b/test/wpt/tests/FileAPI/url/url-in-tags-revoke.window.js new file mode 100644 index 00000000000..1cdad79f7e3 --- /dev/null +++ b/test/wpt/tests/FileAPI/url/url-in-tags-revoke.window.js @@ -0,0 +1,115 @@ +// META: timeout=long +async_test(t => { + const run_result = 'test_frame_OK'; + const blob_contents = '\n\n' + + ''; + const blob = new Blob([blob_contents], {type: 'text/html'}); + const url = URL.createObjectURL(blob); + + const frame = document.createElement('iframe'); + frame.setAttribute('src', url); + frame.setAttribute('style', 'display:none;'); + document.body.appendChild(frame); + URL.revokeObjectURL(url); + + frame.onload = t.step_func_done(() => { + assert_equals(frame.contentWindow.test_result, run_result); + }); +}, 'Fetching a blob URL immediately before revoking it works in an iframe.'); + +async_test(t => { + const run_result = 'test_frame_OK'; + const blob_contents = '\n\n' + + ''; + const blob = new Blob([blob_contents], {type: 'text/html'}); + const url = URL.createObjectURL(blob); + + const frame = document.createElement('iframe'); + frame.setAttribute('src', '/common/blank.html'); + frame.setAttribute('style', 'display:none;'); + document.body.appendChild(frame); + + frame.onload = t.step_func(() => { + frame.contentWindow.location = url; + URL.revokeObjectURL(url); + frame.onload = t.step_func_done(() => { + assert_equals(frame.contentWindow.test_result, run_result); + }); + }); +}, 'Fetching a blob URL immediately before revoking it works in an iframe navigation.'); + +async_test(t => { + const run_result = 'test_frame_OK'; + const blob_contents = '\n\n' + + ''; + const blob = new Blob([blob_contents], {type: 'text/html'}); + const url = URL.createObjectURL(blob); + const win = window.open(url); + URL.revokeObjectURL(url); + add_completion_callback(() => { win.close(); }); + + win.onload = t.step_func_done(() => { + assert_equals(win.test_result, run_result); + }); +}, 'Opening a blob URL in a new window immediately before revoking it works.'); + +function receive_message_on_channel(t, channel_name) { + const channel = new BroadcastChannel(channel_name); + return new Promise(resolve => { + channel.addEventListener('message', t.step_func(e => { + resolve(e.data); + })); + }); +} + +function window_contents_for_channel(channel_name) { + return '\n' + + ''; +} + +async_test(t => { + const channel_name = 'noopener-window-test'; + const blob = new Blob([window_contents_for_channel(channel_name)], {type: 'text/html'}); + receive_message_on_channel(t, channel_name).then(t.step_func_done(t => { + assert_equals(t, 'foobar'); + })); + const url = URL.createObjectURL(blob); + const win = window.open(); + win.opener = null; + win.location = url; + URL.revokeObjectURL(url); +}, 'Opening a blob URL in a noopener about:blank window immediately before revoking it works.'); + +async_test(t => { + const run_result = 'test_script_OK'; + const blob_contents = 'window.script_test_result = "' + run_result + '";'; + const blob = new Blob([blob_contents]); + const url = URL.createObjectURL(blob); + + const e = document.createElement('script'); + e.setAttribute('src', url); + e.onload = t.step_func_done(() => { + assert_equals(window.script_test_result, run_result); + }); + + document.body.appendChild(e); + URL.revokeObjectURL(url); +}, 'Fetching a blob URL immediately before revoking it works in '; + const blob = new Blob([blob_contents], {type: 'text/html'}); + const url = URL.createObjectURL(blob); + + const frame = document.createElement('iframe'); + frame.setAttribute('src', url); + frame.setAttribute('style', 'display:none;'); + document.body.appendChild(frame); + + frame.onload = t.step_func_done(() => { + assert_equals(frame.contentWindow.test_result, run_result); + }); +}, 'Blob URLs can be used in iframes, and are treated same origin'); + +async_test(t => { + const blob_contents = '\n\n' + + '\n' + + '\n' + + '
\n' + + '
'; + const blob = new Blob([blob_contents], {type: 'text/html'}); + const url = URL.createObjectURL(blob); + + const frame = document.createElement('iframe'); + frame.setAttribute('src', url + '#block2'); + document.body.appendChild(frame); + frame.contentWindow.onscroll = t.step_func_done(() => { + assert_equals(frame.contentWindow.scrollY, 5000); + }); +}, 'Blob URL fragment is implemented.'); diff --git a/test/wpt/tests/FileAPI/url/url-lifetime.html b/test/wpt/tests/FileAPI/url/url-lifetime.html new file mode 100644 index 00000000000..ad5d667193a --- /dev/null +++ b/test/wpt/tests/FileAPI/url/url-lifetime.html @@ -0,0 +1,56 @@ + + + + + + \ No newline at end of file diff --git a/test/wpt/tests/FileAPI/url/url-reload.window.js b/test/wpt/tests/FileAPI/url/url-reload.window.js new file mode 100644 index 00000000000..d333b3a74aa --- /dev/null +++ b/test/wpt/tests/FileAPI/url/url-reload.window.js @@ -0,0 +1,36 @@ +function blob_url_reload_test(t, revoke_before_reload) { + const run_result = 'test_frame_OK'; + const blob_contents = '\n\n' + + ''; + const blob = new Blob([blob_contents], {type: 'text/html'}); + const url = URL.createObjectURL(blob); + + const frame = document.createElement('iframe'); + frame.setAttribute('src', url); + frame.setAttribute('style', 'display:none;'); + document.body.appendChild(frame); + + frame.onload = t.step_func(() => { + if (revoke_before_reload) + URL.revokeObjectURL(url); + assert_equals(frame.contentWindow.test_result, run_result); + frame.contentWindow.test_result = null; + frame.onload = t.step_func_done(() => { + assert_equals(frame.contentWindow.test_result, run_result); + }); + // Slight delay before reloading to ensure revoke actually has had a chance + // to be processed. + t.step_timeout(() => { + frame.contentWindow.location.reload(); + }, 250); + }); +} + +async_test(t => { + blob_url_reload_test(t, false); +}, 'Reloading a blob URL succeeds.'); + + +async_test(t => { + blob_url_reload_test(t, true); +}, 'Reloading a blob URL succeeds even if the URL was revoked.'); diff --git a/test/wpt/tests/FileAPI/url/url-with-fetch.any.js b/test/wpt/tests/FileAPI/url/url-with-fetch.any.js new file mode 100644 index 00000000000..54e6a3da5af --- /dev/null +++ b/test/wpt/tests/FileAPI/url/url-with-fetch.any.js @@ -0,0 +1,72 @@ +// META: script=resources/fetch-tests.js +// META: script=/common/gc.js + +function fetch_should_succeed(test, request) { + return fetch(request).then(response => response.text()); +} + +function fetch_should_fail(test, url, method = 'GET') { + return promise_rejects_js(test, TypeError, fetch(url, {method: method})); +} + +fetch_tests('fetch', fetch_should_succeed, fetch_should_fail); + +promise_test(t => { + const blob_contents = 'test blob contents'; + const blob_type = 'image/png'; + const blob = new Blob([blob_contents], {type: blob_type}); + const url = URL.createObjectURL(blob); + + return fetch(url).then(response => { + assert_equals(response.headers.get('Content-Type'), blob_type); + }); +}, 'fetch should return Content-Type from Blob'); + +promise_test(t => { + const blob_contents = 'test blob contents'; + const blob = new Blob([blob_contents]); + const url = URL.createObjectURL(blob); + const request = new Request(url); + + // Revoke the object URL. Request should take a reference to the blob as + // soon as it receives it in open(), so the request succeeds even though we + // revoke the URL before calling fetch(). + URL.revokeObjectURL(url); + + return fetch_should_succeed(t, request).then(text => { + assert_equals(text, blob_contents); + }); +}, 'Revoke blob URL after creating Request, will fetch'); + +promise_test(async t => { + const blob_contents = 'test blob contents'; + const blob = new Blob([blob_contents]); + const url = URL.createObjectURL(blob); + let request = new Request(url); + + // Revoke the object URL. Request should take a reference to the blob as + // soon as it receives it in open(), so the request succeeds even though we + // revoke the URL before calling fetch(). + URL.revokeObjectURL(url); + + request = request.clone(); + await garbageCollect(); + + const text = await fetch_should_succeed(t, request); + assert_equals(text, blob_contents); +}, 'Revoke blob URL after creating Request, then clone Request, will fetch'); + +promise_test(function(t) { + const blob_contents = 'test blob contents'; + const blob = new Blob([blob_contents]); + const url = URL.createObjectURL(blob); + + const result = fetch_should_succeed(t, url).then(text => { + assert_equals(text, blob_contents); + }); + + // Revoke the object URL. fetch should have already resolved the blob URL. + URL.revokeObjectURL(url); + + return result; +}, 'Revoke blob URL after calling fetch, fetch should succeed'); diff --git a/test/wpt/tests/FileAPI/url/url-with-xhr.any.js b/test/wpt/tests/FileAPI/url/url-with-xhr.any.js new file mode 100644 index 00000000000..29d83080ab5 --- /dev/null +++ b/test/wpt/tests/FileAPI/url/url-with-xhr.any.js @@ -0,0 +1,68 @@ +// META: script=resources/fetch-tests.js + +function xhr_should_succeed(test, url) { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open('GET', url); + xhr.onload = test.step_func(() => { + assert_equals(xhr.status, 200); + assert_equals(xhr.statusText, 'OK'); + resolve(xhr.response); + }); + xhr.onerror = () => reject('Got unexpected error event'); + xhr.send(); + }); +} + +function xhr_should_fail(test, url, method = 'GET') { + const xhr = new XMLHttpRequest(); + xhr.open(method, url); + const result1 = new Promise((resolve, reject) => { + xhr.onload = () => reject('Got unexpected load event'); + xhr.onerror = resolve; + }); + const result2 = new Promise(resolve => { + xhr.onreadystatechange = test.step_func(() => { + if (xhr.readyState !== xhr.DONE) return; + assert_equals(xhr.status, 0); + resolve(); + }); + }); + xhr.send(); + return Promise.all([result1, result2]); +} + +fetch_tests('XHR', xhr_should_succeed, xhr_should_fail); + +async_test(t => { + const blob_contents = 'test blob contents'; + const blob_type = 'image/png'; + const blob = new Blob([blob_contents], {type: blob_type}); + const url = URL.createObjectURL(blob); + const xhr = new XMLHttpRequest(); + xhr.open('GET', url); + xhr.onloadend = t.step_func_done(() => { + assert_equals(xhr.getResponseHeader('Content-Type'), blob_type); + }); + xhr.send(); +}, 'XHR should return Content-Type from Blob'); + +async_test(t => { + const blob_contents = 'test blob contents'; + const blob = new Blob([blob_contents]); + const url = URL.createObjectURL(blob); + const xhr = new XMLHttpRequest(); + xhr.open('GET', url); + + // Revoke the object URL. XHR should take a reference to the blob as soon as + // it receives it in open(), so the request succeeds even though we revoke the + // URL before calling send(). + URL.revokeObjectURL(url); + + xhr.onload = t.step_func_done(() => { + assert_equals(xhr.response, blob_contents); + }); + xhr.onerror = t.unreached_func('Got unexpected error event'); + + xhr.send(); +}, 'Revoke blob URL after open(), will fetch'); diff --git a/test/wpt/tests/FileAPI/url/url_createobjecturl_file-manual.html b/test/wpt/tests/FileAPI/url/url_createobjecturl_file-manual.html new file mode 100644 index 00000000000..7ae32512e07 --- /dev/null +++ b/test/wpt/tests/FileAPI/url/url_createobjecturl_file-manual.html @@ -0,0 +1,45 @@ + + +FileAPI Test: Creating Blob URL with File + + + + + + +
+

Test steps:

+
    +
  1. Download blue96x96.png to local.
  2. +
  3. Select the local file (blue96x96.png) to run the test.
  4. +
+
+ +
+ +
+ +
+ + + diff --git a/test/wpt/tests/FileAPI/url/url_createobjecturl_file_img-manual.html b/test/wpt/tests/FileAPI/url/url_createobjecturl_file_img-manual.html new file mode 100644 index 00000000000..534c1de9968 --- /dev/null +++ b/test/wpt/tests/FileAPI/url/url_createobjecturl_file_img-manual.html @@ -0,0 +1,28 @@ + + +FileAPI Test: Creating Blob URL with File as image source + + + +
+

Test steps:

+
    +
  1. Download blue96x96.png to local.
  2. +
  3. Select the local file (blue96x96.png) to run the test.
  4. +
+

Pass/fail criteria:

+

Test passes if there is a filled blue square.

+ +

+

+
+ + + diff --git a/test/wpt/tests/FileAPI/url/url_xmlhttprequest_img-ref.html b/test/wpt/tests/FileAPI/url/url_xmlhttprequest_img-ref.html new file mode 100644 index 00000000000..7d7390442d3 --- /dev/null +++ b/test/wpt/tests/FileAPI/url/url_xmlhttprequest_img-ref.html @@ -0,0 +1,12 @@ + + +FileAPI Reference File + + + +

Test passes if there is a filled blue square.

+ +

+ +

+ diff --git a/test/wpt/tests/FileAPI/url/url_xmlhttprequest_img.html b/test/wpt/tests/FileAPI/url/url_xmlhttprequest_img.html new file mode 100644 index 00000000000..468dcb086d7 --- /dev/null +++ b/test/wpt/tests/FileAPI/url/url_xmlhttprequest_img.html @@ -0,0 +1,27 @@ + + + +FileAPI Test: Creating Blob URL via XMLHttpRequest as image source + + + + +

Test passes if there is a filled blue square.

+ +

+ +

+ + + + diff --git a/test/wpt/tests/LICENSE.md b/test/wpt/tests/LICENSE.md deleted file mode 100644 index 39c46d03ac2..00000000000 --- a/test/wpt/tests/LICENSE.md +++ /dev/null @@ -1,11 +0,0 @@ -# The 3-Clause BSD License - -Copyright © web-platform-tests contributors - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. -2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/test/wpt/tests/common/CustomCorsResponse.py b/test/wpt/tests/common/CustomCorsResponse.py new file mode 100644 index 00000000000..fc4d122f1b1 --- /dev/null +++ b/test/wpt/tests/common/CustomCorsResponse.py @@ -0,0 +1,30 @@ +import json + +def main(request, response): + '''Handler for getting an HTTP response customised by the given query + parameters. + + The returned response will have + - HTTP headers defined by the 'headers' query parameter + - Must be a serialized JSON dictionary mapping header names to header + values + - HTTP status code defined by the 'status' query parameter + - Must be a positive serialized JSON integer like the string '200' + - Response content defined by the 'content' query parameter + - Must be a serialized JSON string representing the desired response body + ''' + def query_parameter_or_default(param, default): + return request.GET.first(param) if param in request.GET else default + + headers = json.loads(query_parameter_or_default(b'headers', b'"{}"')) + for k, v in headers.items(): + response.headers.set(k, v) + + # Note that, in order to have out-of-the-box support for tests that don't call + # setup({'allow_uncaught_exception': true}) + # we return a no-op JS payload. This approach will avoid syntax errors in + # script resources that would otherwise cause the test harness to fail. + response.content = json.loads(query_parameter_or_default(b'content', + b'"/* CustomCorsResponse.py content */"')) + response.status_code = json.loads(query_parameter_or_default(b'status', + b'200')) diff --git a/test/wpt/tests/common/META.yml b/test/wpt/tests/common/META.yml new file mode 100644 index 00000000000..ca4d2e523c0 --- /dev/null +++ b/test/wpt/tests/common/META.yml @@ -0,0 +1,3 @@ +suggested_reviewers: + - zqzhang + - deniak diff --git a/test/wpt/tests/common/PrefixedLocalStorage.js b/test/wpt/tests/common/PrefixedLocalStorage.js new file mode 100644 index 00000000000..2f4e7b6a055 --- /dev/null +++ b/test/wpt/tests/common/PrefixedLocalStorage.js @@ -0,0 +1,116 @@ +/** + * Supports pseudo-"namespacing" localStorage for a given test + * by generating and using a unique prefix for keys. Why trounce on other + * tests' localStorage items when you can keep it "separated"? + * + * PrefixedLocalStorageTest: Instantiate in testharness.js tests to generate + * a new unique-ish prefix + * PrefixedLocalStorageResource: Instantiate in supporting test resource + * files to use/share a prefix generated by a test. + */ +var PrefixedLocalStorage = function () { + this.prefix = ''; // Prefix for localStorage keys + this.param = 'prefixedLocalStorage'; // Param to use in querystrings +}; + +PrefixedLocalStorage.prototype.clear = function () { + if (this.prefix === '') { return; } + Object.keys(localStorage).forEach(sKey => { + if (sKey.indexOf(this.prefix) === 0) { + localStorage.removeItem(sKey); + } + }); +}; + +/** + * Append/replace prefix parameter and value in URI querystring + * Use to generate URLs to resource files that will share the prefix. + */ +PrefixedLocalStorage.prototype.url = function (uri) { + function updateUrlParameter (uri, key, value) { + var i = uri.indexOf('#'); + var hash = (i === -1) ? '' : uri.substr(i); + uri = (i === -1) ? uri : uri.substr(0, i); + var re = new RegExp(`([?&])${key}=.*?(&|$)`, 'i'); + var separator = uri.indexOf('?') !== -1 ? '&' : '?'; + uri = (uri.match(re)) ? uri.replace(re, `$1${key}=${value}$2`) : + `${uri}${separator}${key}=${value}`; + return uri + hash; + } + return updateUrlParameter(uri, this.param, this.prefix); +}; + +PrefixedLocalStorage.prototype.prefixedKey = function (baseKey) { + return `${this.prefix}${baseKey}`; +}; + +PrefixedLocalStorage.prototype.setItem = function (baseKey, value) { + localStorage.setItem(this.prefixedKey(baseKey), value); +}; + +/** + * Listen for `storage` events pertaining to a particular key, + * prefixed with this object's prefix. Ignore when value is being set to null + * (i.e. removeItem). + */ +PrefixedLocalStorage.prototype.onSet = function (baseKey, fn) { + window.addEventListener('storage', e => { + var match = this.prefixedKey(baseKey); + if (e.newValue !== null && e.key.indexOf(match) === 0) { + fn.call(this, e); + } + }); +}; + +/***************************************************************************** + * Use in a testharnessjs test to generate a new key prefix. + * async_test(t => { + * var prefixedStorage = new PrefixedLocalStorageTest(); + * t.add_cleanup(() => prefixedStorage.cleanup()); + * /... + * }); + */ +var PrefixedLocalStorageTest = function () { + PrefixedLocalStorage.call(this); + this.prefix = `${document.location.pathname}-${Math.random()}-${Date.now()}-`; +}; +PrefixedLocalStorageTest.prototype = Object.create(PrefixedLocalStorage.prototype); +PrefixedLocalStorageTest.prototype.constructor = PrefixedLocalStorageTest; + +/** + * Use in a cleanup function to clear out prefixed entries in localStorage + */ +PrefixedLocalStorageTest.prototype.cleanup = function () { + this.setItem('closeAll', 'true'); + this.clear(); +}; + +/***************************************************************************** + * Use in test resource files to share a prefix generated by a + * PrefixedLocalStorageTest. Will look in URL querystring for prefix. + * Setting `close_on_cleanup` opt truthy will make this script's window listen + * for storage `closeAll` event from controlling test and close itself. + * + * var PrefixedLocalStorageResource({ close_on_cleanup: true }); + */ +var PrefixedLocalStorageResource = function (options) { + PrefixedLocalStorage.call(this); + this.options = Object.assign({}, { + close_on_cleanup: false + }, options || {}); + // Check URL querystring for prefix to use + var regex = new RegExp(`[?&]${this.param}(=([^&#]*)|&|#|$)`), + results = regex.exec(document.location.href); + if (results && results[2]) { + this.prefix = results[2]; + } + // Optionally have this window close itself when the PrefixedLocalStorageTest + // sets a `closeAll` item. + if (this.options.close_on_cleanup) { + this.onSet('closeAll', () => { + window.close(); + }); + } +}; +PrefixedLocalStorageResource.prototype = Object.create(PrefixedLocalStorage.prototype); +PrefixedLocalStorageResource.prototype.constructor = PrefixedLocalStorageResource; diff --git a/test/wpt/tests/common/PrefixedLocalStorage.js.headers b/test/wpt/tests/common/PrefixedLocalStorage.js.headers new file mode 100644 index 00000000000..6805c323df5 --- /dev/null +++ b/test/wpt/tests/common/PrefixedLocalStorage.js.headers @@ -0,0 +1 @@ +Content-Type: text/javascript; charset=utf-8 diff --git a/test/wpt/tests/common/PrefixedPostMessage.js b/test/wpt/tests/common/PrefixedPostMessage.js new file mode 100644 index 00000000000..674b52877c0 --- /dev/null +++ b/test/wpt/tests/common/PrefixedPostMessage.js @@ -0,0 +1,100 @@ +/** + * Supports pseudo-"namespacing" for window-posted messages for a given test + * by generating and using a unique prefix that gets wrapped into message + * objects. This makes it more feasible to have multiple tests that use + * `window.postMessage` in a single test file. Basically, make it possible + * for the each test to listen for only the messages that are pertinent to it. + * + * 'Prefix' not an elegant term to use here but this models itself after + * PrefixedLocalStorage. + * + * PrefixedMessageTest: Instantiate in testharness.js tests to generate + * a new unique-ish prefix that can be used by other test support files + * PrefixedMessageResource: Instantiate in supporting test resource + * files to use/share a prefix generated by a test. + */ +var PrefixedMessage = function () { + this.prefix = ''; + this.param = 'prefixedMessage'; // Param to use in querystrings +}; + +/** + * Generate a URL that adds/replaces param with this object's prefix + * Use to link to test support files that make use of + * PrefixedMessageResource. + */ +PrefixedMessage.prototype.url = function (uri) { + function updateUrlParameter (uri, key, value) { + var i = uri.indexOf('#'); + var hash = (i === -1) ? '' : uri.substr(i); + uri = (i === -1) ? uri : uri.substr(0, i); + var re = new RegExp(`([?&])${key}=.*?(&|$)`, 'i'); + var separator = uri.indexOf('?') !== -1 ? '&' : '?'; + uri = (uri.match(re)) ? uri.replace(re, `$1${key}=${value}$2`) : + `${uri}${separator}${key}=${value}`; + return uri + hash; + } + return updateUrlParameter(uri, this.param, this.prefix); +}; + +/** + * Add an eventListener on `message` but only invoke the given callback + * for messages whose object contains this object's prefix. Remove the + * event listener once the anticipated message has been received. + */ +PrefixedMessage.prototype.onMessage = function (fn) { + window.addEventListener('message', e => { + if (typeof e.data === 'object' && e.data.hasOwnProperty('prefix')) { + if (e.data.prefix === this.prefix) { + // Only invoke callback when `data` is an object containing + // a `prefix` key with this object's prefix value + // Note fn is invoked with "unwrapped" data first, then the event `e` + // (which contains the full, wrapped e.data should it be needed) + fn.call(this, e.data.data, e); + window.removeEventListener('message', fn); + } + } + }); +}; + +/** + * Instantiate in a test file (e.g. during `setup`) to create a unique-ish + * prefix that can be shared by support files + */ +var PrefixedMessageTest = function () { + PrefixedMessage.call(this); + this.prefix = `${document.location.pathname}-${Math.random()}-${Date.now()}-`; +}; +PrefixedMessageTest.prototype = Object.create(PrefixedMessage.prototype); +PrefixedMessageTest.prototype.constructor = PrefixedMessageTest; + +/** + * Instantiate in a test support script to use a "prefix" generated by a + * PrefixedMessageTest in a controlling test file. It will look for + * the prefix in a URL param (see also PrefixedMessage#url) + */ +var PrefixedMessageResource = function () { + PrefixedMessage.call(this); + // Check URL querystring for prefix to use + var regex = new RegExp(`[?&]${this.param}(=([^&#]*)|&|#|$)`), + results = regex.exec(document.location.href); + if (results && results[2]) { + this.prefix = results[2]; + } +}; +PrefixedMessageResource.prototype = Object.create(PrefixedMessage.prototype); +PrefixedMessageResource.prototype.constructor = PrefixedMessageResource; + +/** + * This is how a test resource document can "send info" to its + * opener context. It will whatever message is being sent (`data`) in + * an object that injects the prefix. + */ +PrefixedMessageResource.prototype.postToOpener = function (data) { + if (window.opener) { + window.opener.postMessage({ + prefix: this.prefix, + data: data + }, '*'); + } +}; diff --git a/test/wpt/tests/common/PrefixedPostMessage.js.headers b/test/wpt/tests/common/PrefixedPostMessage.js.headers new file mode 100644 index 00000000000..6805c323df5 --- /dev/null +++ b/test/wpt/tests/common/PrefixedPostMessage.js.headers @@ -0,0 +1 @@ +Content-Type: text/javascript; charset=utf-8 diff --git a/test/wpt/tests/common/README.md b/test/wpt/tests/common/README.md new file mode 100644 index 00000000000..9aef19cb73f --- /dev/null +++ b/test/wpt/tests/common/README.md @@ -0,0 +1,10 @@ +The files in this directory are non-infrastructure support files that can be used by tests. + +* `blank.html` - An empty HTML document. +* `domain-setter.sub.html` - An HTML document that sets `document.domain`. +* `dummy.xhtml` - An XHTML document. +* `dummy.xml` - An XML document. +* `text-plain.txt` - A text/plain document. +* `*.js` - Utility scripts. These are documented in the source. +* `*.py` - wptserve [Python Handlers](https://web-platform-tests.org/writing-tests/python-handlers/). These are documented in the source. +* `security-features` - Documented in `security-features/README.md`. diff --git a/test/wpt/tests/common/__init__.py b/test/wpt/tests/common/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/wpt/tests/common/arrays.js b/test/wpt/tests/common/arrays.js new file mode 100644 index 00000000000..2b31bb4179c --- /dev/null +++ b/test/wpt/tests/common/arrays.js @@ -0,0 +1,31 @@ +/** + * Callback for checking equality of c and d. + * + * @callback equalityCallback + * @param {*} c + * @param {*} d + * @returns {boolean} + */ + +/** + * Returns true if the given arrays are equal. Optionally can pass an equality function. + * @param {Array} a + * @param {Array} b + * @param {equalityCallback} callbackFunction - defaults to `c === d` + * @returns {boolean} + */ +export function areArraysEqual(a, b, equalityFunction = (c, d) => { return c === d; }) { + try { + if (a.length !== b.length) + return false; + + for (let i = 0; i < a.length; i++) { + if (!equalityFunction(a[i], b[i])) + return false; + } + } catch (ex) { + return false; + } + + return true; +} diff --git a/test/wpt/tests/common/blank-with-cors.html b/test/wpt/tests/common/blank-with-cors.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/wpt/tests/common/blank-with-cors.html.headers b/test/wpt/tests/common/blank-with-cors.html.headers new file mode 100644 index 00000000000..cb762eff806 --- /dev/null +++ b/test/wpt/tests/common/blank-with-cors.html.headers @@ -0,0 +1 @@ +Access-Control-Allow-Origin: * diff --git a/test/wpt/tests/common/blank.html b/test/wpt/tests/common/blank.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/wpt/tests/common/custom-cors-response.js b/test/wpt/tests/common/custom-cors-response.js new file mode 100644 index 00000000000..be9c7ce3bdc --- /dev/null +++ b/test/wpt/tests/common/custom-cors-response.js @@ -0,0 +1,32 @@ +const custom_cors_response = (payload, base_url) => { + base_url = base_url || new URL(location.href); + + // Clone the given `payload` so that, as we modify it, we won't be mutating + // the caller's value in unexpected ways. + payload = Object.assign({}, payload); + payload.headers = payload.headers || {}; + // Note that, in order to have out-of-the-box support for tests that don't + // call `setup({'allow_uncaught_exception': true})` we return a no-op JS + // payload. This approach will avoid hitting syntax errors if the resource is + // interpreted as script. Without this workaround, the SyntaxError would be + // caught by the test harness and trigger a test failure. + payload.content = payload.content || '/* custom-cors-response.js content */'; + payload.status_code = payload.status_code || 200; + + // Assume that we'll be doing a CORS-enabled fetch so we'll need to set ACAO. + const acao = "Access-Control-Allow-Origin"; + if (!(acao in payload.headers)) { + payload.headers[acao] = '*'; + } + + if (!("Content-Type" in payload.headers)) { + payload.headers["Content-Type"] = "text/javascript"; + } + + let ret = new URL("/common/CustomCorsResponse.py", base_url); + for (const key in payload) { + ret.searchParams.append(key, JSON.stringify(payload[key])); + } + + return ret; +}; diff --git a/test/wpt/tests/common/dispatcher/README.md b/test/wpt/tests/common/dispatcher/README.md new file mode 100644 index 00000000000..cfaafb6e5d6 --- /dev/null +++ b/test/wpt/tests/common/dispatcher/README.md @@ -0,0 +1,228 @@ +# `RemoteContext`: API for script execution in another context + +`RemoteContext` in `/common/dispatcher/dispatcher.js` provides an interface to +execute JavaScript in another global object (page or worker, the "executor"), +based on: + +- [WPT RFC 88: context IDs from uuid searchParams in URL](https://github.com/web-platform-tests/rfcs/pull/88), +- [WPT RFC 89: execute_script](https://github.com/web-platform-tests/rfcs/pull/89) and +- [WPT RFC 91: RemoteContext](https://github.com/web-platform-tests/rfcs/pull/91). + +Tests can send arbitrary javascript to executors to evaluate in its global +object, like: + +``` +// injector.html +const argOnLocalContext = ...; + +async function execute() { + window.open('executor.html?uuid=' + uuid); + const ctx = new RemoteContext(uuid); + await ctx.execute_script( + (arg) => functionOnRemoteContext(arg), + [argOnLocalContext]); +}; +``` + +and on executor: + +``` +// executor.html +function functionOnRemoteContext(arg) { ... } + +const uuid = new URLSearchParams(window.location.search).get('uuid'); +const executor = new Executor(uuid); +``` + +For concrete examples, see +[events.html](../../html/browsers/browsing-the-web/back-forward-cache/events.html) +and +[executor.html](../../html/browsers/browsing-the-web/back-forward-cache/resources/executor.html) +in back-forward cache tests. + +Note that `executor*` files under `/common/dispatcher/` are NOT for +`RemoteContext.execute_script()`. Use `remote-executor.html` instead. + +This is universal and avoids introducing many specific `XXX-helper.html` +resources. +Moreover, tests are easier to read, because the whole logic of the test can be +defined in a single file. + +## `new RemoteContext(uuid)` + +- `uuid` is a UUID string that identifies the remote context and should match + with the `uuid` parameter of the URL of the remote context. +- Callers should create the remote context outside this constructor (e.g. + `window.open('executor.html?uuid=' + uuid)`). + +## `RemoteContext.execute_script(fn, args)` + +- `fn` is a JavaScript function to execute on the remote context, which is + converted to a string using `toString()` and sent to the remote context. +- `args` is null or an array of arguments to pass to the function on the + remote context. Arguments are passed as JSON. +- If the return value of `fn` when executed in the remote context is a promise, + the promise returned by `execute_script` resolves to the resolved value of + that promise. Otherwise the `execute_script` promise resolves to the return + value of `fn`. + +Note that `fn` is evaluated on the remote context (`executor.html` in the +example above), while `args` are evaluated on the caller context +(`injector.html`) and then passed to the remote context. + +## Return value of injected functions and `execute_script()` + +If the return value of the injected function when executed in the remote +context is a promise, the promise returned by `execute_script` resolves to the +resolved value of that promise. Otherwise the `execute_script` promise resolves +to the return value of the function. + +When the return value of an injected script is a Promise, it should be resolved +before any navigation starts on the remote context. For example, it shouldn't +be resolved after navigating out and navigating back to the page again. +It's fine to create a Promise to be resolved after navigations, if it's not the +return value of the injected function. + +## Calling timing of `execute_script()` + +When `RemoteContext.execute_script()` is called when the remote context is not +active (for example before it is created, before navigation to the page, or +during the page is in back-forward cache), the injected script is evaluated +after the remote context becomes active. + +Multiple calls to `RemoteContext.execute_script()` will result in multiple scripts +being executed in remote context and ordering will be maintained. + +## Errors from `execute_script()` + +Errors from `execute_script()` will result in promise rejections, so it is +important to await the result. This can be `await ctx.execute_script(...)` for +every call but if there are multiple scripts to executed, it may be preferable +to wait on them in parallel to avoid incurring full round-trip time for each, +e.g. + +```js +await Promise.all( + ctx1.execute_script(...), + ctx1.execute_script(...), + ctx2.execute_script(...), + ctx2.execute_script(...), + ... +) +``` + +## Evaluation timing of injected functions + +The script injected by `RemoteContext.execute_script()` can be evaluated any +time during the remote context is active. +For example, even before DOMContentLoaded events or even during navigation. +It's the responsibility of test-specific code/helpers to ensure evaluation +timing constraints (which can be also test-specific), if any needed. + +### Ensuring evaluation timing around page load + +For example, to ensure that injected functions (`mainFunction` below) are +evaluated after the first `pageshow` event, we can use pure JavaScript code +like below: + +``` +// executor.html +window.pageShowPromise = new Promise(resolve => + window.addEventListener('pageshow', resolve, {once: true})); + + +// injector.html +const waitForPageShow = async () => { + while (!window.pageShowPromise) { + await new Promise(resolve => setTimeout(resolve, 100)); + } + await window.pageShowPromise; +}; + +await ctx.execute(waitForPageShow); +await ctx.execute(mainFunction); +``` + +### Ensuring evaluation timing around navigation out/unloading + +It can be important to ensure there are no injected functions nor code behind +`RemoteContext` (such as Fetch APIs accessing server-side stash) running after +navigation is initiated, for example in the case of back-forward cache testing. + +To ensure this, + +- Do not call the next `RemoteContext.execute()` for the remote context after + triggering the navigation, until we are sure that the remote context is not + active (e.g. after we confirm that the new page is loaded). +- Call `Executor.suspend(callback)` synchronously within the injected script. + This suspends executor-related code, and calls `callback` when it is ready + to start navigation. + +The code on the injector side would be like: + +``` +// injector.html +await ctx.execute_script(() => { + executor.suspend(() => { + location.href = 'new-url.html'; + }); +}); +``` + +## Future Work: Possible integration with `test_driver` + +Currently `RemoteContext` is implemented by JavaScript and WPT-server-side +stash, and not integrated with `test_driver` nor `testharness`. +There is a proposal of `test_driver`-integrated version (see the RFCs listed +above). + +The API semantics and guidelines in this document are designed to be applicable +to both the current stash-based `RemoteContext` and `test_driver`-based +version, and thus the tests using `RemoteContext` will be migrated with minimum +modifications (mostly in `/common/dispatcher/dispatcher.js` and executors), for +example in a +[draft CL](https://chromium-review.googlesource.com/c/chromium/src/+/3082215/). + + +# `send()`/`receive()` Message passing APIs + +`dispatcher.js` (and its server-side backend `dispatcher.py`) provides a +universal queue-based message passing API. +Each queue is identified by a UUID, and accessed via the following APIs: + +- `send(uuid, message)` pushes a string `message` to the queue `uuid`. +- `receive(uuid)` pops the first item from the queue `uuid`. +- `showRequestHeaders(origin, uuid)` and + `cacheableShowRequestHeaders(origin, uuid)` return URLs, that push request + headers to the queue `uuid` upon fetching. + +It works cross-origin, and even access different browser context groups. + +Messages are queued, this means one doesn't need to wait for the receiver to +listen, before sending the first message +(but still need to wait for the resolution of the promise returned by `send()` +to ensure the order between `send()`s). + +## Executors + +Similar to `RemoteContext.execute_script()`, `send()`/`receive()` can be used +for sending arbitrary javascript to be evaluated in another page or worker. + +- `executor.html` (as a Document), +- `executor-worker.js` (as a Web Worker), and +- `executor-service-worker.js` (as a Service Worker) + +are examples of executors. +Note that these executors are NOT compatible with +`RemoteContext.execute_script()`. + +## Future Work + +`send()`, `receive()` and the executors below are kept for COEP/COOP tests. + +For remote script execution, new tests should use +`RemoteContext.execute_script()` instead. + +For message passing, +[WPT RFC 90](https://github.com/web-platform-tests/rfcs/pull/90) is still under +discussion. diff --git a/test/wpt/tests/common/dispatcher/dispatcher.js b/test/wpt/tests/common/dispatcher/dispatcher.js new file mode 100644 index 00000000000..a0f9f43e622 --- /dev/null +++ b/test/wpt/tests/common/dispatcher/dispatcher.js @@ -0,0 +1,256 @@ +// Define a universal message passing API. It works cross-origin and across +// browsing context groups. +const dispatcher_path = "/common/dispatcher/dispatcher.py"; +const dispatcher_url = new URL(dispatcher_path, location.href).href; + +// Return a promise, limiting the number of concurrent accesses to a shared +// resources to |max_concurrent_access|. +const concurrencyLimiter = (max_concurrency) => { + let pending = 0; + let waiting = []; + return async (task) => { + pending++; + if (pending > max_concurrency) + await new Promise(resolve => waiting.push(resolve)); + let result = await task(); + pending--; + waiting.shift()?.(); + return result; + }; +} + +// Wait for a random amount of time in the range [10ms,100ms]. +const randomDelay = () => { + return new Promise(resolve => setTimeout(resolve, 10 + 90*Math.random())); +} + +// Sending too many requests in parallel causes congestion. Limiting it improves +// throughput. +// +// Note: The following table has been determined on the test: +// ../cache-storage.tentative.https.html +// using Chrome with a 64 core CPU / 64GB ram, in release mode: +// ┌───────────┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬────┐ +// │concurrency│ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 10│ 15│ 20│ 30│ 50│ 100│ +// ├───────────┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼────┤ +// │time (s) │ 54│ 38│ 31│ 29│ 26│ 24│ 22│ 22│ 22│ 22│ 34│ 36 │ +// └───────────┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴────┘ +const limiter = concurrencyLimiter(6); + +// While requests to different remote contexts can go in parallel, we need to +// ensure that requests to each remote context are done in order. This maps a +// uuid to a queue of requests to send. A queue is processed until it is empty +// and then is deleted from the map. +const sendQueues = new Map(); + +// Sends a single item (with rate-limiting) and calls the associated resolver +// when it is successfully sent. +const sendItem = async function (uuid, resolver, message) { + await limiter(async () => { + // Requests might be dropped. Retry until getting a confirmation it has been + // processed. + while(1) { + try { + let response = await fetch(dispatcher_url + `?uuid=${uuid}`, { + method: 'POST', + body: message + }) + if (await response.text() == "done") { + resolver(); + return; + } + } catch (fetch_error) {} + await randomDelay(); + }; + }); +} + +// While the queue is non-empty, send the next item. This is async and new items +// may be added to the queue while others are being sent. +const processQueue = async function (uuid, queue) { + while (queue.length) { + const [resolver, message] = queue.shift(); + await sendItem(uuid, resolver, message); + } + // The queue is empty, delete it. + sendQueues.delete(uuid); +} + +const send = async function (uuid, message) { + const itemSentPromise = new Promise((resolve) => { + const item = [resolve, message]; + if (sendQueues.has(uuid)) { + // There is already a queue for `uuid`, just add to it and it will be processed. + sendQueues.get(uuid).push(item); + } else { + // There is no queue for `uuid`, create it and start processing. + const queue = [item]; + sendQueues.set(uuid, queue); + processQueue(uuid, queue); + } + }); + // Wait until the item has been successfully sent. + await itemSentPromise; +} + +const receive = async function (uuid) { + while(1) { + let data = "not ready"; + try { + data = await limiter(async () => { + let response = await fetch(dispatcher_url + `?uuid=${uuid}`); + return await response.text(); + }); + } catch (fetch_error) {} + + if (data == "not ready") { + await randomDelay(); + continue; + } + + return data; + } +} + +// Returns an URL. When called, the server sends toward the `uuid` queue the +// request headers. Useful for determining if something was requested with +// Cookies. +const showRequestHeaders = function(origin, uuid) { + return origin + dispatcher_path + `?uuid=${uuid}&show-headers`; +} + +// Same as above, except for the response is cacheable. +const cacheableShowRequestHeaders = function(origin, uuid) { + return origin + dispatcher_path + `?uuid=${uuid}&cacheable&show-headers`; +} + +// This script requires +// - `/common/utils.js` for `token()`. + +// Returns the URL of a document that can be used as a `RemoteContext`. +// +// `uuid` should be a UUID uniquely identifying the given remote context. +// `options` has the following shape: +// +// { +// host: (optional) Sets the returned URL's `host` property. Useful for +// cross-origin executors. +// protocol: (optional) Sets the returned URL's `protocol` property. +// } +function remoteExecutorUrl(uuid, options) { + const url = new URL("/common/dispatcher/remote-executor.html", location); + url.searchParams.set("uuid", uuid); + + if (options?.host) { + url.host = options.host; + } + + if (options?.protocol) { + url.protocol = options.protocol; + } + + return url; +} + +// Represents a remote executor. For more detailed explanation see `README.md`. +class RemoteContext { + // `uuid` is a UUID string that identifies the remote context and should + // match with the `uuid` parameter of the URL of the remote context. + constructor(uuid) { + this.context_id = uuid; + } + + // Evaluates the script `expr` on the executor. + // - If `expr` is evaluated to a Promise that is resolved with a value: + // `execute_script()` returns a Promise resolved with the value. + // - If `expr` is evaluated to a non-Promise value: + // `execute_script()` returns a Promise resolved with the value. + // - If `expr` throws an error or is evaluated to a Promise that is rejected: + // `execute_script()` returns a rejected Promise with the error's + // `message`. + // Note that currently the type of error (e.g. DOMException) is not + // preserved, except for `TypeError`. + // The values should be able to be serialized by JSON.stringify(). + async execute_script(fn, args) { + const receiver = token(); + await this.send({receiver: receiver, fn: fn.toString(), args: args}); + const response = JSON.parse(await receive(receiver)); + if (response.status === 'success') { + return response.value; + } + + // exception + if (response.name === 'TypeError') { + throw new TypeError(response.value); + } + throw new Error(response.value); + } + + async send(msg) { + return await send(this.context_id, JSON.stringify(msg)); + } +}; + +class Executor { + constructor(uuid) { + this.uuid = uuid; + + // If `suspend_callback` is not `null`, the executor should be suspended + // when there are no ongoing tasks. + this.suspend_callback = null; + + this.execute(); + } + + // Wait until there are no ongoing tasks nor fetch requests for polling + // tasks, and then suspend the executor and call `callback()`. + // Navigation from the executor page should be triggered inside `callback()`, + // to avoid conflict with in-flight fetch requests. + suspend(callback) { + this.suspend_callback = callback; + } + + resume() { + } + + async execute() { + while(true) { + if (this.suspend_callback !== null) { + this.suspend_callback(); + this.suspend_callback = null; + // Wait for `resume()` to be called. + await new Promise(resolve => this.resume = resolve); + + // Workaround for https://crbug.com/1244230. + // Without this workaround, the executor is resumed and the fetch + // request to poll the next task is initiated synchronously from + // pageshow event after the page restored from BFCache, and the fetch + // request promise is never resolved (and thus the test results in + // timeout) due to https://crbug.com/1244230. The root cause is not yet + // known, but setTimeout() with 0ms causes the resume triggered on + // another task and seems to resolve the issue. + await new Promise(resolve => setTimeout(resolve, 0)); + + continue; + } + + const task = JSON.parse(await receive(this.uuid)); + + let response; + try { + const value = await eval(task.fn).apply(null, task.args); + response = JSON.stringify({ + status: 'success', + value: value + }); + } catch(e) { + response = JSON.stringify({ + status: 'exception', + name: e.name, + value: e.message + }); + } + await send(task.receiver, response); + } + } +} diff --git a/test/wpt/tests/common/dispatcher/dispatcher.py b/test/wpt/tests/common/dispatcher/dispatcher.py new file mode 100644 index 00000000000..9fe7a38ac87 --- /dev/null +++ b/test/wpt/tests/common/dispatcher/dispatcher.py @@ -0,0 +1,53 @@ +import json +from wptserve.utils import isomorphic_decode + +# A server used to store and retrieve arbitrary data. +# This is used by: ./dispatcher.js +def main(request, response): + # This server is configured so that is accept to receive any requests and + # any cookies the web browser is willing to send. + response.headers.set(b"Access-Control-Allow-Credentials", b"true") + response.headers.set(b'Access-Control-Allow-Methods', b'OPTIONS, GET, POST') + response.headers.set(b'Access-Control-Allow-Headers', b'Content-Type') + response.headers.set(b"Access-Control-Allow-Origin", request.headers.get(b"origin") or '*') + + if b"cacheable" in request.GET: + response.headers.set(b"Cache-Control", b"max-age=31536000") + else: + response.headers.set(b'Cache-Control', b'no-cache, no-store, must-revalidate') + + # CORS preflight + if request.method == u'OPTIONS': + return b'' + + uuid = request.GET[b'uuid'] + stash = request.server.stash; + + # The stash is accessed concurrently by many clients. A lock is used to + # avoid unterleaved read/write from different clients. + with stash.lock: + queue = stash.take(uuid, '/common/dispatcher') or []; + + # Push into the |uuid| queue, the requested headers. + if b"show-headers" in request.GET: + headers = {}; + for key, value in request.headers.items(): + headers[isomorphic_decode(key)] = isomorphic_decode(request.headers[key]) + headers = json.dumps(headers); + queue.append(headers); + ret = b''; + + # Push into the |uuid| queue, the posted data. + elif request.method == u'POST': + queue.append(request.body) + ret = b'done' + + # Pull from the |uuid| queue, the posted data. + else: + if len(queue) == 0: + ret = b'not ready' + else: + ret = queue.pop(0) + + stash.put(uuid, queue, '/common/dispatcher') + return ret; diff --git a/test/wpt/tests/common/dispatcher/executor-service-worker.js b/test/wpt/tests/common/dispatcher/executor-service-worker.js new file mode 100644 index 00000000000..0b47d66b65f --- /dev/null +++ b/test/wpt/tests/common/dispatcher/executor-service-worker.js @@ -0,0 +1,24 @@ +importScripts('./dispatcher.js'); + +const params = new URLSearchParams(location.search); +const uuid = params.get('uuid'); + +// The fetch handler must be registered before parsing the main script response. +// So do it here, for future use. +fetchHandler = () => {} +addEventListener('fetch', e => { + fetchHandler(e); +}); + +// Force ServiceWorker to immediately activate itself. +addEventListener('install', event => { + skipWaiting(); +}); + +let executeOrders = async function() { + while(true) { + let task = await receive(uuid); + eval(`(async () => {${task}})()`); + } +}; +executeOrders(); diff --git a/test/wpt/tests/common/dispatcher/executor-worker.js b/test/wpt/tests/common/dispatcher/executor-worker.js new file mode 100644 index 00000000000..ea065a6bf11 --- /dev/null +++ b/test/wpt/tests/common/dispatcher/executor-worker.js @@ -0,0 +1,12 @@ +importScripts('./dispatcher.js'); + +const params = new URLSearchParams(location.search); +const uuid = params.get('uuid'); + +let executeOrders = async function() { + while(true) { + let task = await receive(uuid); + eval(`(async () => {${task}})()`); + } +}; +executeOrders(); diff --git a/test/wpt/tests/common/dispatcher/executor.html b/test/wpt/tests/common/dispatcher/executor.html new file mode 100644 index 00000000000..5fe6a95efaf --- /dev/null +++ b/test/wpt/tests/common/dispatcher/executor.html @@ -0,0 +1,15 @@ + + diff --git a/test/wpt/tests/common/dispatcher/remote-executor.html b/test/wpt/tests/common/dispatcher/remote-executor.html new file mode 100644 index 00000000000..8b0030390d0 --- /dev/null +++ b/test/wpt/tests/common/dispatcher/remote-executor.html @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/test/wpt/tests/common/domain-setter.sub.html b/test/wpt/tests/common/domain-setter.sub.html new file mode 100644 index 00000000000..ad3b9f8b808 --- /dev/null +++ b/test/wpt/tests/common/domain-setter.sub.html @@ -0,0 +1,8 @@ + + +A page that will likely be same-origin-domain but not same-origin + + diff --git a/test/wpt/tests/common/dummy.xhtml b/test/wpt/tests/common/dummy.xhtml new file mode 100644 index 00000000000..dba6945f4ba --- /dev/null +++ b/test/wpt/tests/common/dummy.xhtml @@ -0,0 +1,2 @@ + +Dummy XHTML document diff --git a/test/wpt/tests/common/dummy.xml b/test/wpt/tests/common/dummy.xml new file mode 100644 index 00000000000..4a60c3035fc --- /dev/null +++ b/test/wpt/tests/common/dummy.xml @@ -0,0 +1 @@ +Dummy XML document diff --git a/test/wpt/tests/common/echo.py b/test/wpt/tests/common/echo.py new file mode 100644 index 00000000000..911b54a0b48 --- /dev/null +++ b/test/wpt/tests/common/echo.py @@ -0,0 +1,6 @@ +def main(request, response): + # Without X-XSS-Protection to disable non-standard XSS protection the functionality this + # resource offers is useless + response.headers.set(b"X-XSS-Protection", b"0") + response.headers.set(b"Content-Type", b"text/html") + response.content = request.GET.first(b"content") diff --git a/test/wpt/tests/common/gc.js b/test/wpt/tests/common/gc.js new file mode 100644 index 00000000000..ac43a4cfaf7 --- /dev/null +++ b/test/wpt/tests/common/gc.js @@ -0,0 +1,52 @@ +/** + * Does a best-effort attempt at invoking garbage collection. Attempts to use + * the standardized `TestUtils.gc()` function, but falls back to other + * environment-specific nonstandard functions, with a final result of just + * creating a lot of garbage (in which case you will get a console warning). + * + * This should generally only be used to attempt to trigger bugs and crashes + * inside tests, i.e. cases where if garbage collection happened, then this + * should not trigger some misbehavior. You cannot rely on garbage collection + * successfully trigger, or that any particular unreachable object will be + * collected. + * + * @returns {Promise} A promise you should await to ensure garbage + * collection has had a chance to complete. + */ +self.garbageCollect = async () => { + // https://testutils.spec.whatwg.org/#the-testutils-namespace + if (self.TestUtils?.gc) { + return TestUtils.gc(); + } + + // Use --expose_gc for V8 (and Node.js) + // to pass this flag at chrome launch use: --js-flags="--expose-gc" + // Exposed in SpiderMonkey shell as well + if (self.gc) { + return self.gc(); + } + + // Present in some WebKit development environments + if (self.GCController) { + return GCController.collect(); + } + + console.warn( + 'Tests are running without the ability to do manual garbage collection. ' + + 'They will still work, but coverage will be suboptimal.'); + + for (var i = 0; i < 1000; i++) { + gcRec(10); + } + + function gcRec(n) { + if (n < 1) { + return {}; + } + + let temp = { i: "ab" + i + i / 100000 }; + temp += "foo"; + + gcRec(n - 1); + } +}; diff --git a/test/wpt/tests/common/get-host-info.sub.js.headers b/test/wpt/tests/common/get-host-info.sub.js.headers new file mode 100644 index 00000000000..6805c323df5 --- /dev/null +++ b/test/wpt/tests/common/get-host-info.sub.js.headers @@ -0,0 +1 @@ +Content-Type: text/javascript; charset=utf-8 diff --git a/test/wpt/tests/common/media.js b/test/wpt/tests/common/media.js new file mode 100644 index 00000000000..f2dc8612660 --- /dev/null +++ b/test/wpt/tests/common/media.js @@ -0,0 +1,55 @@ +/** + * Returns the URL of a supported video source based on the user agent + * @param {string} base - media URL without file extension + * @returns {string} + */ +function getVideoURI(base) +{ + var extension = '.mp4'; + + var videotag = document.createElement("video"); + + if ( videotag.canPlayType && + videotag.canPlayType('video/ogg; codecs="theora, vorbis"') ) + { + extension = '.ogv'; + } + + return base + extension; +} + +/** + * Returns the URL of a supported audio source based on the user agent + * @param {string} base - media URL without file extension + * @returns {string} + */ +function getAudioURI(base) +{ + var extension = '.mp3'; + + var audiotag = document.createElement("audio"); + + if ( audiotag.canPlayType && + audiotag.canPlayType('audio/ogg') ) + { + extension = '.oga'; + } + + return base + extension; +} + +/** + * Returns the MIME type for a media URL based on the file extension. + * @param {string} url + * @returns {string} + */ +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", + }; + return map[extension]; +} diff --git a/test/wpt/tests/common/media.js.headers b/test/wpt/tests/common/media.js.headers new file mode 100644 index 00000000000..6805c323df5 --- /dev/null +++ b/test/wpt/tests/common/media.js.headers @@ -0,0 +1 @@ +Content-Type: text/javascript; charset=utf-8 diff --git a/test/wpt/tests/common/object-association.js b/test/wpt/tests/common/object-association.js new file mode 100644 index 00000000000..669c17c07b1 --- /dev/null +++ b/test/wpt/tests/common/object-association.js @@ -0,0 +1,74 @@ +"use strict"; + +// This is for testing whether an object (e.g., a global property) is associated with Window, or +// with Document. Recall that Window and Document are 1:1 except when doing a same-origin navigation +// away from the initial about:blank. In that case the Window object gets reused for the new +// Document. +// +// So: +// - If something is per-Window, then it should maintain its identity across an about:blank +// navigation. +// - If something is per-Document, then it should be recreated across an about:blank navigation. + +window.testIsPerWindow = propertyName => { + runTests(propertyName, assert_equals, "must not"); +}; + +window.testIsPerDocument = propertyName => { + runTests(propertyName, assert_not_equals, "must"); +}; + +function runTests(propertyName, equalityOrInequalityAsserter, mustOrMustNotReplace) { + async_test(t => { + const iframe = document.createElement("iframe"); + document.body.appendChild(iframe); + const frame = iframe.contentWindow; + + const before = frame[propertyName]; + assert_implements(before, `window.${propertyName} must be implemented`); + + iframe.onload = t.step_func_done(() => { + const after = frame[propertyName]; + equalityOrInequalityAsserter(after, before); + }); + + iframe.src = "/common/blank.html"; + }, `Navigating from the initial about:blank ${mustOrMustNotReplace} replace window.${propertyName}`); + + // Per spec, discarding a browsing context should not change any of the global objects. + test(() => { + const iframe = document.createElement("iframe"); + document.body.appendChild(iframe); + const frame = iframe.contentWindow; + + const before = frame[propertyName]; + assert_implements(before, `window.${propertyName} must be implemented`); + + iframe.remove(); + + const after = frame[propertyName]; + assert_equals(after, before, `window.${propertyName} should not change after iframe.remove()`); + }, `Discarding the browsing context must not change window.${propertyName}`); + + // Per spec, document.open() should not change any of the global objects. In historical versions + // of the spec, it did, so we test here. + async_test(t => { + const iframe = document.createElement("iframe"); + + iframe.onload = t.step_func_done(() => { + const frame = iframe.contentWindow; + const before = frame[propertyName]; + assert_implements(before, `window.${propertyName} must be implemented`); + + frame.document.open(); + + const after = frame[propertyName]; + assert_equals(after, before); + + frame.document.close(); + }); + + iframe.src = "/common/blank.html"; + document.body.appendChild(iframe); + }, `document.open() must not replace window.${propertyName}`); +} diff --git a/test/wpt/tests/common/object-association.js.headers b/test/wpt/tests/common/object-association.js.headers new file mode 100644 index 00000000000..6805c323df5 --- /dev/null +++ b/test/wpt/tests/common/object-association.js.headers @@ -0,0 +1 @@ +Content-Type: text/javascript; charset=utf-8 diff --git a/test/wpt/tests/common/performance-timeline-utils.js b/test/wpt/tests/common/performance-timeline-utils.js new file mode 100644 index 00000000000..b20241cc610 --- /dev/null +++ b/test/wpt/tests/common/performance-timeline-utils.js @@ -0,0 +1,56 @@ +/* +author: W3C http://www.w3.org/ +help: http://www.w3.org/TR/navigation-timing/#sec-window.performance-attribute +*/ +var performanceNamespace = window.performance; +var namespace_check = false; +function wp_test(func, msg, properties) +{ + // only run the namespace check once + if (!namespace_check) + { + namespace_check = true; + + if (performanceNamespace === undefined || performanceNamespace == null) + { + // show a single error that window.performance is undefined + // The window.performance attribute provides a hosting area for performance related attributes. + test(function() { assert_true(performanceNamespace !== undefined && performanceNamespace != null, "window.performance is defined and not null"); }, "window.performance is defined and not null."); + } + } + + test(func, msg, properties); +} + +function test_true(value, msg, properties) +{ + wp_test(function () { assert_true(value, msg); }, msg, properties); +} + +function test_equals(value, equals, msg, properties) +{ + wp_test(function () { assert_equals(value, equals, msg); }, msg, properties); +} + +// assert for every entry in `expectedEntries`, there is a matching entry _somewhere_ in `actualEntries` +function test_entries(actualEntries, expectedEntries) { + test_equals(actualEntries.length, expectedEntries.length) + expectedEntries.forEach(function (expectedEntry) { + var foundEntry = actualEntries.find(function (actualEntry) { + return typeof Object.keys(expectedEntry).find(function (key) { + return actualEntry[key] !== expectedEntry[key] + }) === 'undefined' + }) + test_true(!!foundEntry, `Entry ${JSON.stringify(expectedEntry)} could not be found.`) + if (foundEntry) { + assert_object_equals(foundEntry.toJSON(), expectedEntry) + } + }) +} + +function delayedLoadListener(callback) { + window.addEventListener('load', function() { + // TODO(cvazac) Remove this setTimeout when spec enforces sync entries. + step_timeout(callback, 0) + }) +} diff --git a/test/wpt/tests/common/performance-timeline-utils.js.headers b/test/wpt/tests/common/performance-timeline-utils.js.headers new file mode 100644 index 00000000000..6805c323df5 --- /dev/null +++ b/test/wpt/tests/common/performance-timeline-utils.js.headers @@ -0,0 +1 @@ +Content-Type: text/javascript; charset=utf-8 diff --git a/test/wpt/tests/common/proxy-all.sub.pac b/test/wpt/tests/common/proxy-all.sub.pac new file mode 100644 index 00000000000..de601e5d702 --- /dev/null +++ b/test/wpt/tests/common/proxy-all.sub.pac @@ -0,0 +1,3 @@ +function FindProxyForURL(url, host) { + return "PROXY {{host}}:{{ports[http][0]}}" +} diff --git a/test/wpt/tests/common/redirect-opt-in.py b/test/wpt/tests/common/redirect-opt-in.py new file mode 100644 index 00000000000..b5e674a27fb --- /dev/null +++ b/test/wpt/tests/common/redirect-opt-in.py @@ -0,0 +1,20 @@ +def main(request, response): + """Simple handler that causes redirection. + + The request should typically have two query parameters: + status - The status to use for the redirection. Defaults to 302. + location - The resource to redirect to. + """ + status = 302 + if b"status" in request.GET: + try: + status = int(request.GET.first(b"status")) + except ValueError: + pass + + response.status = status + + location = request.GET.first(b"location") + + response.headers.set(b"Location", location) + response.headers.set(b"Timing-Allow-Origin", b"*") diff --git a/test/wpt/tests/common/redirect.py b/test/wpt/tests/common/redirect.py new file mode 100644 index 00000000000..f2fd1ebd51d --- /dev/null +++ b/test/wpt/tests/common/redirect.py @@ -0,0 +1,19 @@ +def main(request, response): + """Simple handler that causes redirection. + + The request should typically have two query parameters: + status - The status to use for the redirection. Defaults to 302. + location - The resource to redirect to. + """ + status = 302 + if b"status" in request.GET: + try: + status = int(request.GET.first(b"status")) + except ValueError: + pass + + response.status = status + + location = request.GET.first(b"location") + + response.headers.set(b"Location", location) diff --git a/test/wpt/tests/common/refresh.py b/test/wpt/tests/common/refresh.py new file mode 100644 index 00000000000..0d309900d47 --- /dev/null +++ b/test/wpt/tests/common/refresh.py @@ -0,0 +1,11 @@ +def main(request, response): + """ + Respond with a blank HTML document and a `Refresh` header which describes + an immediate redirect to the URL specified by the requests `location` query + string parameter + """ + headers = [ + (b'Content-Type', b'text/html'), + (b'Refresh', b'0; URL=' + request.GET.first(b'location')) + ] + return (200, headers, b'') diff --git a/test/wpt/tests/common/reftest-wait.js b/test/wpt/tests/common/reftest-wait.js new file mode 100644 index 00000000000..64fe9bfd7f5 --- /dev/null +++ b/test/wpt/tests/common/reftest-wait.js @@ -0,0 +1,39 @@ +/** + * Remove the `reftest-wait` class on the document element. + * The reftest runner will wait with taking a screenshot while + * this class is present. + * + * See https://web-platform-tests.org/writing-tests/reftests.html#controlling-when-comparison-occurs + */ +function takeScreenshot() { + document.documentElement.classList.remove("reftest-wait"); +} + +/** + * Call `takeScreenshot()` after a delay of at least |timeout| milliseconds. + * @param {number} timeout - milliseconds + */ +function takeScreenshotDelayed(timeout) { + setTimeout(function() { + takeScreenshot(); + }, timeout); +} + +/** + * Ensure that a precondition is met before waiting for a screenshot. + * @param {bool} condition - Fail the test if this evaluates to false + * @param {string} msg - Error message to write to the screenshot + */ +function failIfNot(condition, msg) { + const fail = () => { + (document.body || document.documentElement).textContent = `Precondition Failed: ${msg}`; + takeScreenshot(); + }; + if (!condition) { + if (document.readyState == "interactive") { + fail(); + } else { + document.addEventListener("DOMContentLoaded", fail, false); + } + } +} diff --git a/test/wpt/tests/common/reftest-wait.js.headers b/test/wpt/tests/common/reftest-wait.js.headers new file mode 100644 index 00000000000..6805c323df5 --- /dev/null +++ b/test/wpt/tests/common/reftest-wait.js.headers @@ -0,0 +1 @@ +Content-Type: text/javascript; charset=utf-8 diff --git a/test/wpt/tests/common/rendering-utils.js b/test/wpt/tests/common/rendering-utils.js new file mode 100644 index 00000000000..46283bd5d07 --- /dev/null +++ b/test/wpt/tests/common/rendering-utils.js @@ -0,0 +1,19 @@ +"use strict"; + +/** + * Waits until we have at least one frame rendered, regardless of the engine. + * + * @returns {Promise} + */ +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 + // sufficient to ensure at least one frame has been generated anywhere. + window.requestAnimationFrame(() => { + window.requestAnimationFrame(() => { + resolve(); + }); + }); + }); +} diff --git a/test/wpt/tests/common/sab.js b/test/wpt/tests/common/sab.js new file mode 100644 index 00000000000..a3ea610e165 --- /dev/null +++ b/test/wpt/tests/common/sab.js @@ -0,0 +1,21 @@ +const createBuffer = (() => { + // See https://github.com/whatwg/html/issues/5380 for why not `new SharedArrayBuffer()` + let sabConstructor; + try { + sabConstructor = new WebAssembly.Memory({ shared:true, initial:0, maximum:0 }).buffer.constructor; + } catch(e) { + sabConstructor = null; + } + return (type, length, opts) => { + if (type === "ArrayBuffer") { + return new ArrayBuffer(length, opts); + } else if (type === "SharedArrayBuffer") { + if (sabConstructor && sabConstructor.name !== "SharedArrayBuffer") { + throw new Error("WebAssembly.Memory does not support shared:true"); + } + return new sabConstructor(length, opts); + } else { + throw new Error("type has to be ArrayBuffer or SharedArrayBuffer"); + } + } +})(); diff --git a/test/wpt/tests/common/security-features/README.md b/test/wpt/tests/common/security-features/README.md new file mode 100644 index 00000000000..f957541f75e --- /dev/null +++ b/test/wpt/tests/common/security-features/README.md @@ -0,0 +1,460 @@ +This directory contains the common infrastructure for the following tests (also referred below as projects). + +- referrer-policy/ +- mixed-content/ +- upgrade-insecure-requests/ + +Subdirectories: + +- `resources`: + Serves JavaScript test helpers. +- `subresource`: + Serves subresources, with support for redirects, stash, etc. + The subresource paths are managed by `subresourceMap` and + fetched in `requestVia*()` functions in `resources/common.js`. +- `scope`: + Serves nested contexts, such as iframe documents or workers. + Used from `invokeFrom*()` functions in `resources/common.js`. +- `tools`: + Scripts that generate test HTML files. Not used while running tests. +- `/referrer-policy/generic/subresource-test`: + Sanity checking tests for subresource invocation + (This is still placed outside common/) + +# Test generator + +The test generator ([common/security-features/tools/generate.py](tools/generate.py)) generates test HTML files from templates and a seed (`spec.src.json`) that defines all the test scenarios. + +The project (i.e. a WPT subdirectory, for example `referrer-policy/`) that uses the generator should define per-project data and invoke the common generator logic in `common/security-features/tools`. + +This is the overview of the project structure: + +``` +common/security-features/ +└── tools/ - the common test generator logic + ├── spec.src.json + └── template/ - the test files templates +project-directory/ (e.g. referrer-policy/) +├── spec.src.json +├── generic/ +│ ├── test-case.sub.js - Per-project test helper +│ ├── sanity-checker.js (Used by debug target only) +│ └── spec_json.js (Used by debug target only) +└── gen/ - generated tests +``` + +## Generating the tests + +Note: When the repository already contains generated tests, [remove all generated tests](#removing-all-generated-tests) first. + +```bash +# Install json5 module if needed. +pip install --user json5 + +# Generate the test files under gen/ (HTMLs and .headers files). +path/to/common/security-features/tools/generate.py --spec path/to/project-directory/ + +# Add all generated tests to the repo. +git add path/to/project-directory/gen/ && git commit -m "Add generated tests" +``` + +This will parse the spec JSON5 files and determine which tests to generate (or skip) while using templates. + +- The default spec JSON5: `common/security-features/tools/spec.src.json`. + - Describes common configurations, such as subresource types, source context types, etc. +- The per-project spec JSON5: `project-directory/spec.src.json`. + - Describes project-specific configurations, particularly those related to test generation patterns (`specification`), policy deliveries (e.g. `delivery_type`, `delivery_value`) and `expectation`. + +For how these two spec JSON5 files are merged, see [Sub projects](#sub-projects) section. + +Note: `spec.src.json` is transitioning to JSON5 [#21710](https://github.com/web-platform-tests/wpt/issues/21710). + +During the generation, the spec is validated by ```common/security-features/tools/spec_validator.py```. This is specially important when you're making changes to `spec.src.json`. Make sure it's a valid JSON (no comments or trailing commas). The validator reports specific errors (missing keys etc.), if any. + +### Removing all generated tests + +Simply remove all files under `project-directory/gen/`. + +```bash +rm -r path/to/project-directory/gen/ +``` + +### Options for generating tests + +Note: this section is currently obsolete. Only the release template is working. + +The generator script has two targets: ```release``` and ```debug```. + +* Using **release** for the target will produce tests using a template for optimizing size and performance. The release template is intended for the official web-platform-tests and possibly other test suites. No sanity checking is done in release mode. Use this option whenever you're checking into web-platform-tests. + +* When generating for ```debug```, the produced tests will contain more verbosity and sanity checks. Use this target to identify problems with the test suites when making changes locally. Make sure you don't check in tests generated with the debug target. + +Note that **release** is the default target when invoking ```generate.py```. + + +## Sub projects + +Projects can be nested, for example to reuse a single `spec.src.json` across similar but slightly different sets of generated tests. +The directory structure would look like: + +``` +project-directory/ (e.g. referrer-policy/) +├── spec.src.json - Parent project's spec JSON +├── generic/ +│ └── test-case.sub.js - Parent project's test helper +├── gen/ - parent project's generated tests +└── sub-project-directory/ (e.g. 4K) + ├── spec.src.json - Child project's spec JSON + ├── generic/ + │ └── test-case.sub.js - Child project's test helper + └── gen/ - child project's generated tests +``` + +`generate.py --spec project-directory/sub-project-directory` generates test files under `project-directory/sub-project-directory/gen`, based on `project-directory/spec.src.json` and `project-directory/sub-project-directory/spec.src.json`. + +- The child project's `spec.src.json` is merged into parent project's `spec.src.json`. + - Two spec JSON objects are merged recursively. + - If a same key exists in both objects, the child's value overwrites the parent's value. + - If both (child's and parent's) values are arrays, then the child's value is concatenated to the parent's value. + - For debugging, `generate.py` dumps the merged spec JSON object as `generic/debug-output.spec.src.json`. +- The child project's generated tests include both of the parent and child project's `test-case.sub.js`: + ```html + + + + ``` + + +## Updating the tests + +The main test logic lives in ```project-directory/generic/test-case.sub.js``` with helper functions defined in ```/common/security-features/resources/common.js``` so you should probably start there. + +For updating the test suites you will most likely do **a subset** of the following: + +* Add a new subresource type: + + * Add a new sub-resource python script to `/common/security-features/subresource/`. + * Add a sanity check test for a sub-resource to `referrer-policy/generic/subresource-test/`. + * Add a new entry to `subresourceMap` in `/common/security-features/resources/common.js`. + * Add a new entry to `valid_subresource_names` in `/common/security-features/tools/spec_validator.py`. + * Add a new entry to `subresource_schema` in `spec.src.json`. + * Update `source_context_schema` to specify in which source context the subresource can be used. + +* Add a new subresource redirection type + + * TODO: to be documented. Example: [https://github.com/web-platform-tests/wpt/pull/18939](https://github.com/web-platform-tests/wpt/pull/18939) + +* Add a new subresource origin type + + * TODO: to be documented. Example: [https://github.com/web-platform-tests/wpt/pull/18940](https://github.com/web-platform-tests/wpt/pull/18940) + +* Add a new source context (e.g. "module sharedworker global scope") + + * TODO: to be documented. Example: [https://github.com/web-platform-tests/wpt/pull/18904](https://github.com/web-platform-tests/wpt/pull/18904) + +* Add a new source context list (e.g. "subresource request from a dedicated worker in a ` + invoker: invokeFromIframe, + }, + "iframe": { // + invoker: invokeFromIframe, + }, + "iframe-blank": { // + invoker: invokeFromIframe, + }, + "worker-classic": { + // Classic dedicated worker loaded from same-origin. + invoker: invokeFromWorker.bind(undefined, "worker", false, {}), + }, + "worker-classic-data": { + // Classic dedicated worker loaded from data: URL. + invoker: invokeFromWorker.bind(undefined, "worker", true, {}), + }, + "worker-module": { + // Module dedicated worker loaded from same-origin. + invoker: invokeFromWorker.bind(undefined, "worker", false, {type: 'module'}), + }, + "worker-module-data": { + // Module dedicated worker loaded from data: URL. + invoker: invokeFromWorker.bind(undefined, "worker", true, {type: 'module'}), + }, + "sharedworker-classic": { + // Classic shared worker loaded from same-origin. + invoker: invokeFromWorker.bind(undefined, "sharedworker", false, {}), + }, + "sharedworker-classic-data": { + // Classic shared worker loaded from data: URL. + invoker: invokeFromWorker.bind(undefined, "sharedworker", true, {}), + }, + "sharedworker-module": { + // Module shared worker loaded from same-origin. + invoker: invokeFromWorker.bind(undefined, "sharedworker", false, {type: 'module'}), + }, + "sharedworker-module-data": { + // Module shared worker loaded from data: URL. + invoker: invokeFromWorker.bind(undefined, "sharedworker", true, {type: 'module'}), + }, + }; + + return sourceContextMap[sourceContextList[0].sourceContextType].invoker( + subresource, sourceContextList); +} + +// Quick hack to expose invokeRequest when common.sub.js is loaded either +// as a classic or module script. +self.invokeRequest = invokeRequest; + +/** + invokeFrom*() functions are helper functions with the same parameters + and return values as invokeRequest(), that are tied to specific types + of top-most environment settings objects. + For example, invokeFromIframe() is the helper function for the cases where + sourceContextList[0] is an iframe. +*/ + +/** + @param {string} workerType + "worker" (for dedicated worker) or "sharedworker". + @param {boolean} isDataUrl + true if the worker script is loaded from data: URL. + Otherwise, the script is loaded from same-origin. + @param {object} workerOptions + The `options` argument for Worker constructor. + + Other parameters and return values are the same as those of invokeRequest(). +*/ +function invokeFromWorker(workerType, isDataUrl, workerOptions, + subresource, sourceContextList) { + const currentSourceContext = sourceContextList[0]; + let workerUrl = + "/common/security-features/scope/worker.py?policyDeliveries=" + + encodeURIComponent(JSON.stringify( + currentSourceContext.policyDeliveries || [])); + if (workerOptions.type === 'module') { + workerUrl += "&type=module"; + } + + let promise; + if (isDataUrl) { + promise = fetch(workerUrl) + .then(r => r.text()) + .then(source => { + return 'data:text/javascript;base64,' + btoa(source); + }); + } else { + promise = Promise.resolve(workerUrl); + } + + return promise + .then(url => { + if (workerType === "worker") { + const worker = new Worker(url, workerOptions); + worker.postMessage({subresource: subresource, + sourceContextList: sourceContextList.slice(1)}); + return bindEvents2(worker, "message", worker, "error", window, "error"); + } else if (workerType === "sharedworker") { + const worker = new SharedWorker(url, workerOptions); + worker.port.start(); + worker.port.postMessage({subresource: subresource, + sourceContextList: sourceContextList.slice(1)}); + return bindEvents2(worker.port, "message", worker, "error", window, "error"); + } else { + throw new Error('Invalid worker type: ' + workerType); + } + }) + .then(event => { + if (event.data.error) + return Promise.reject(event.data.error); + return event.data; + }); +} + +function invokeFromIframe(subresource, sourceContextList) { + const currentSourceContext = sourceContextList[0]; + const frameUrl = + "/common/security-features/scope/document.py?policyDeliveries=" + + encodeURIComponent(JSON.stringify( + currentSourceContext.policyDeliveries || [])); + + let iframe; + let promise; + if (currentSourceContext.sourceContextType === 'srcdoc') { + promise = fetch(frameUrl) + .then(r => r.text()) + .then(srcdoc => { + iframe = createElement( + "iframe", {srcdoc: srcdoc}, document.body, true); + return iframe.eventPromise; + }); + } else if (currentSourceContext.sourceContextType === 'iframe') { + iframe = createElement("iframe", {src: frameUrl}, document.body, true); + promise = iframe.eventPromise; + } else if (currentSourceContext.sourceContextType === 'iframe-blank') { + let frameContent; + promise = fetch(frameUrl) + .then(r => r.text()) + .then(t => { + frameContent = t; + iframe = createElement("iframe", {}, document.body, true); + return iframe.eventPromise; + }) + .then(() => { + // Reinitialize `iframe.eventPromise` with a new promise + // that catches the load event for the document.write() below. + bindEvents(iframe); + + iframe.contentDocument.write(frameContent); + iframe.contentDocument.close(); + return iframe.eventPromise; + }); + } + + return promise + .then(() => { + const promise = bindEvents2( + window, "message", iframe, "error", window, "error"); + iframe.contentWindow.postMessage( + {subresource: subresource, + sourceContextList: sourceContextList.slice(1)}, + "*"); + return promise; + }) + .then(event => { + if (event.data.error) + return Promise.reject(event.data.error); + return event.data; + }); +} + +// SanityChecker does nothing in release mode. See sanity-checker.js for debug +// mode. +function SanityChecker() {} +SanityChecker.prototype.checkScenario = function() {}; +SanityChecker.prototype.setFailTimeout = function(test, timeout) {}; +SanityChecker.prototype.checkSubresourceResult = function() {}; diff --git a/test/wpt/tests/common/security-features/resources/common.sub.js.headers b/test/wpt/tests/common/security-features/resources/common.sub.js.headers new file mode 100644 index 00000000000..cb762eff806 --- /dev/null +++ b/test/wpt/tests/common/security-features/resources/common.sub.js.headers @@ -0,0 +1 @@ +Access-Control-Allow-Origin: * diff --git a/test/wpt/tests/common/security-features/scope/__init__.py b/test/wpt/tests/common/security-features/scope/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/wpt/tests/common/security-features/scope/document.py b/test/wpt/tests/common/security-features/scope/document.py new file mode 100644 index 00000000000..9a9f045e640 --- /dev/null +++ b/test/wpt/tests/common/security-features/scope/document.py @@ -0,0 +1,36 @@ +import os, sys, json + +from wptserve.utils import isomorphic_decode, isomorphic_encode + +import importlib +util = importlib.import_module("common.security-features.scope.util") + +def main(request, response): + policyDeliveries = json.loads(request.GET.first(b"policyDeliveries", b"[]")) + maybe_additional_headers = {} + meta = u'' + error = u'' + for delivery in policyDeliveries: + if delivery[u'deliveryType'] == u'meta': + if delivery[u'key'] == u'referrerPolicy': + meta += u'' % delivery[u'value'] + else: + error = u'invalid delivery key' + elif delivery[u'deliveryType'] == u'http-rp': + if delivery[u'key'] == u'referrerPolicy': + maybe_additional_headers[b'Referrer-Policy'] = isomorphic_encode(delivery[u'value']) + else: + error = u'invalid delivery key' + else: + error = u'invalid deliveryType' + + handler = lambda: util.get_template(u"document.html.template") % ({ + u"meta": meta, + u"error": error + }) + util.respond( + request, + response, + payload_generator=handler, + content_type=b"text/html", + maybe_additional_headers=maybe_additional_headers) diff --git a/test/wpt/tests/common/security-features/scope/template/document.html.template b/test/wpt/tests/common/security-features/scope/template/document.html.template new file mode 100644 index 00000000000..37e29f8e97f --- /dev/null +++ b/test/wpt/tests/common/security-features/scope/template/document.html.template @@ -0,0 +1,30 @@ + + + + %(meta)s + + + + diff --git a/test/wpt/tests/common/security-features/scope/template/worker.js.template b/test/wpt/tests/common/security-features/scope/template/worker.js.template new file mode 100644 index 00000000000..7a2a6e05c44 --- /dev/null +++ b/test/wpt/tests/common/security-features/scope/template/worker.js.template @@ -0,0 +1,29 @@ +%(import)s + +if ('DedicatedWorkerGlobalScope' in self && + self instanceof DedicatedWorkerGlobalScope) { + self.onmessage = event => onMessageFromParent(event, self); +} else if ('SharedWorkerGlobalScope' in self && + self instanceof SharedWorkerGlobalScope) { + onconnect = event => { + const port = event.ports[0]; + port.onmessage = event => onMessageFromParent(event, port); + }; +} + +// Receive a message from the parent and start the test. +function onMessageFromParent(event, port) { + const configurationError = "%(error)s"; + if (configurationError.length > 0) { + port.postMessage({error: configurationError}); + return; + } + + invokeRequest(event.data.subresource, + event.data.sourceContextList) + .then(result => port.postMessage(result)) + .catch(e => { + const message = (e.error && e.error.stack) || e.message || "Error"; + port.postMessage({error: message}); + }); +} diff --git a/test/wpt/tests/common/security-features/scope/util.py b/test/wpt/tests/common/security-features/scope/util.py new file mode 100644 index 00000000000..da5aacf35e9 --- /dev/null +++ b/test/wpt/tests/common/security-features/scope/util.py @@ -0,0 +1,43 @@ +import os + +from wptserve.utils import isomorphic_decode + +def get_template(template_basename): + script_directory = os.path.dirname(os.path.abspath(isomorphic_decode(__file__))) + template_directory = os.path.abspath( + os.path.join(script_directory, u"template")) + template_filename = os.path.join(template_directory, template_basename) + + with open(template_filename, "r") as f: + return f.read() + + +def __noop(request, response): + return u"" + + +def respond(request, + response, + status_code=200, + content_type=b"text/html", + payload_generator=__noop, + cache_control=b"no-cache; must-revalidate", + access_control_allow_origin=b"*", + maybe_additional_headers=None): + response.add_required_headers = False + response.writer.write_status(status_code) + + if access_control_allow_origin != None: + response.writer.write_header(b"access-control-allow-origin", + access_control_allow_origin) + response.writer.write_header(b"content-type", content_type) + response.writer.write_header(b"cache-control", cache_control) + + additional_headers = maybe_additional_headers or {} + for header, value in additional_headers.items(): + response.writer.write_header(header, value) + + response.writer.end_headers() + + payload = payload_generator() + response.writer.write(payload) diff --git a/test/wpt/tests/common/security-features/scope/worker.py b/test/wpt/tests/common/security-features/scope/worker.py new file mode 100644 index 00000000000..6b321e7de17 --- /dev/null +++ b/test/wpt/tests/common/security-features/scope/worker.py @@ -0,0 +1,44 @@ +import os, sys, json + +from wptserve.utils import isomorphic_decode, isomorphic_encode +import importlib +util = importlib.import_module("common.security-features.scope.util") + +def main(request, response): + policyDeliveries = json.loads(request.GET.first(b'policyDeliveries', b'[]')) + worker_type = request.GET.first(b'type', b'classic') + commonjs_url = u'%s://%s:%s/common/security-features/resources/common.sub.js' % ( + request.url_parts.scheme, request.url_parts.hostname, + request.url_parts.port) + if worker_type == b'classic': + import_line = u'importScripts("%s");' % commonjs_url + else: + import_line = u'import "%s";' % commonjs_url + + maybe_additional_headers = {} + error = u'' + for delivery in policyDeliveries: + if delivery[u'deliveryType'] == u'meta': + error = u' cannot be used in WorkerGlobalScope' + elif delivery[u'deliveryType'] == u'http-rp': + if delivery[u'key'] == u'referrerPolicy': + maybe_additional_headers[b'Referrer-Policy'] = isomorphic_encode(delivery[u'value']) + elif delivery[u'key'] == u'mixedContent' and delivery[u'value'] == u'opt-in': + maybe_additional_headers[b'Content-Security-Policy'] = b'block-all-mixed-content' + elif delivery[u'key'] == u'upgradeInsecureRequests' and delivery[u'value'] == u'upgrade': + maybe_additional_headers[b'Content-Security-Policy'] = b'upgrade-insecure-requests' + else: + error = u'invalid delivery key for http-rp: %s' % delivery[u'key'] + else: + error = u'invalid deliveryType: %s' % delivery[u'deliveryType'] + + handler = lambda: util.get_template(u'worker.js.template') % ({ + u'import': import_line, + u'error': error + }) + util.respond( + request, + response, + payload_generator=handler, + content_type=b'text/javascript', + maybe_additional_headers=maybe_additional_headers) diff --git a/test/wpt/tests/common/security-features/subresource/__init__.py b/test/wpt/tests/common/security-features/subresource/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/wpt/tests/common/security-features/subresource/audio.py b/test/wpt/tests/common/security-features/subresource/audio.py new file mode 100644 index 00000000000..f16a0f7fbbe --- /dev/null +++ b/test/wpt/tests/common/security-features/subresource/audio.py @@ -0,0 +1,18 @@ +import os, sys +from wptserve.utils import isomorphic_decode +import importlib +subresource = importlib.import_module("common.security-features.subresource.subresource") + +def generate_payload(request, server_data): + file = os.path.join(request.doc_root, u"webaudio", u"resources", + u"sin_440Hz_-6dBFS_1s.wav") + return open(file, "rb").read() + + +def main(request, response): + handler = lambda data: generate_payload(request, data) + subresource.respond(request, + response, + payload_generator = handler, + access_control_allow_origin = b"*", + content_type = b"audio/wav") diff --git a/test/wpt/tests/common/security-features/subresource/document.py b/test/wpt/tests/common/security-features/subresource/document.py new file mode 100644 index 00000000000..52b684a4d9f --- /dev/null +++ b/test/wpt/tests/common/security-features/subresource/document.py @@ -0,0 +1,12 @@ +import os, sys +from wptserve.utils import isomorphic_decode +import importlib +subresource = importlib.import_module("common.security-features.subresource.subresource") + +def generate_payload(server_data): + return subresource.get_template(u"document.html.template") % server_data + +def main(request, response): + subresource.respond(request, + response, + payload_generator = generate_payload) diff --git a/test/wpt/tests/common/security-features/subresource/empty.py b/test/wpt/tests/common/security-features/subresource/empty.py new file mode 100644 index 00000000000..312e12cbed9 --- /dev/null +++ b/test/wpt/tests/common/security-features/subresource/empty.py @@ -0,0 +1,14 @@ +import os, sys +from wptserve.utils import isomorphic_decode +import importlib +subresource = importlib.import_module("common.security-features.subresource.subresource") + +def generate_payload(server_data): + return u'' + +def main(request, response): + subresource.respond(request, + response, + payload_generator = generate_payload, + access_control_allow_origin = b"*", + content_type = b"text/plain") diff --git a/test/wpt/tests/common/security-features/subresource/font.py b/test/wpt/tests/common/security-features/subresource/font.py new file mode 100644 index 00000000000..7900079cdf3 --- /dev/null +++ b/test/wpt/tests/common/security-features/subresource/font.py @@ -0,0 +1,76 @@ +import os, sys +from base64 import decodebytes + +from wptserve.utils import isomorphic_decode +import importlib +subresource = importlib.import_module("common.security-features.subresource.subresource") + + +def generate_payload(request, server_data): + data = (u'{"headers": %(headers)s}') % server_data + if b"id" in request.GET: + request.server.stash.put(request.GET[b"id"], data) + # Simple base64 encoded .tff font + return decodebytes(b"AAEAAAANAIAAAwBQRkZUTU6u6MkAAAXcAAAAHE9TLzJWYW" + b"QKAAABWAAAAFZjbWFwAA8D7wAAAcAAAAFCY3Z0IAAhAnkA" + b"AAMEAAAABGdhc3D//wADAAAF1AAAAAhnbHlmCC6aTwAAAx" + b"QAAACMaGVhZO8ooBcAAADcAAAANmhoZWEIkAV9AAABFAAA" + b"ACRobXR4EZQAhQAAAbAAAAAQbG9jYQBwAFQAAAMIAAAACm" + b"1heHAASQA9AAABOAAAACBuYW1lehAVOgAAA6AAAAIHcG9z" + b"dP+uADUAAAWoAAAAKgABAAAAAQAAMhPyuV8PPPUACwPoAA" + b"AAAMU4Lm0AAAAAxTgubQAh/5wFeAK8AAAACAACAAAAAAAA" + b"AAEAAAK8/5wAWgXcAAAAAAV4AAEAAAAAAAAAAAAAAAAAAA" + b"AEAAEAAAAEAAwAAwAAAAAAAgAAAAEAAQAAAEAALgAAAAAA" + b"AQXcAfQABQAAAooCvAAAAIwCigK8AAAB4AAxAQIAAAIABg" + b"kAAAAAAAAAAAABAAAAAAAAAAAAAAAAUGZFZABAAEEAQQMg" + b"/zgAWgK8AGQAAAABAAAAAAAABdwAIQAAAAAF3AAABdwAZA" + b"AAAAMAAAADAAAAHAABAAAAAAA8AAMAAQAAABwABAAgAAAA" + b"BAAEAAEAAABB//8AAABB////wgABAAAAAAAAAQYAAAEAAA" + b"AAAAAAAQIAAAACAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAA" + b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAAAAA" + b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + b"AAAAAAAAAAAAAAAAAAAhAnkAAAAqACoAKgBGAAAAAgAhAA" + b"ABKgKaAAMABwAusQEALzyyBwQA7TKxBgXcPLIDAgDtMgCx" + b"AwAvPLIFBADtMrIHBgH8PLIBAgDtMjMRIREnMxEjIQEJ6M" + b"fHApr9ZiECWAAAAwBk/5wFeAK8AAMABwALAAABNSEVATUh" + b"FQE1IRUB9AH0/UQDhPu0BRQB9MjI/tTIyP7UyMgAAAAAAA" + b"4ArgABAAAAAAAAACYATgABAAAAAAABAAUAgQABAAAAAAAC" + b"AAYAlQABAAAAAAADACEA4AABAAAAAAAEAAUBDgABAAAAAA" + b"AFABABNgABAAAAAAAGAAUBUwADAAEECQAAAEwAAAADAAEE" + b"CQABAAoAdQADAAEECQACAAwAhwADAAEECQADAEIAnAADAA" + b"EECQAEAAoBAgADAAEECQAFACABFAADAAEECQAGAAoBRwBD" + b"AG8AcAB5AHIAaQBnAGgAdAAgACgAYwApACAAMgAwADAAOA" + b"AgAE0AbwB6AGkAbABsAGEAIABDAG8AcgBwAG8AcgBhAHQA" + b"aQBvAG4AAENvcHlyaWdodCAoYykgMjAwOCBNb3ppbGxhIE" + b"NvcnBvcmF0aW9uAABNAGEAcgBrAEEAAE1hcmtBAABNAGUA" + b"ZABpAHUAbQAATWVkaXVtAABGAG8AbgB0AEYAbwByAGcAZQ" + b"AgADIALgAwACAAOgAgAE0AYQByAGsAQQAgADoAIAA1AC0A" + b"MQAxAC0AMgAwADAAOAAARm9udEZvcmdlIDIuMCA6IE1hcm" + b"tBIDogNS0xMS0yMDA4AABNAGEAcgBrAEEAAE1hcmtBAABW" + b"AGUAcgBzAGkAbwBuACAAMAAwADEALgAwADAAMAAgAABWZX" + b"JzaW9uIDAwMS4wMDAgAABNAGEAcgBrAEEAAE1hcmtBAAAA" + b"AgAAAAAAAP+DADIAAAABAAAAAAAAAAAAAAAAAAAAAAAEAA" + b"AAAQACACQAAAAAAAH//wACAAAAAQAAAADEPovuAAAAAMU4" + b"Lm0AAAAAxTgubQ==") + +def generate_report_headers_payload(request, server_data): + stashed_data = request.server.stash.take(request.GET[b"id"]) + return stashed_data + +def main(request, response): + handler = lambda data: generate_payload(request, data) + content_type = b'application/x-font-truetype' + + if b"report-headers" in request.GET: + handler = lambda data: generate_report_headers_payload(request, data) + content_type = b'application/json' + + subresource.respond(request, + response, + payload_generator = handler, + content_type = content_type, + access_control_allow_origin = b"*") diff --git a/test/wpt/tests/common/security-features/subresource/image.py b/test/wpt/tests/common/security-features/subresource/image.py new file mode 100644 index 00000000000..5c9a0c063c3 --- /dev/null +++ b/test/wpt/tests/common/security-features/subresource/image.py @@ -0,0 +1,116 @@ +import os, sys, array, math + +from io import BytesIO + +from wptserve.utils import isomorphic_decode + +import importlib +subresource = importlib.import_module("common.security-features.subresource.subresource") + +class Image: + """This class partially implements the interface of the PIL.Image.Image. + One day in the future WPT might support the PIL module or another imaging + library, so this hacky BMP implementation will no longer be required. + """ + def __init__(self, width, height): + self.width = width + self.height = height + self.img = bytearray([0 for i in range(3 * width * height)]) + + @staticmethod + def new(mode, size, color=0): + return Image(size[0], size[1]) + + def _int_to_bytes(self, number): + packed_bytes = [0, 0, 0, 0] + for i in range(4): + packed_bytes[i] = number & 0xFF + number >>= 8 + + return packed_bytes + + def putdata(self, color_data): + for y in range(self.height): + for x in range(self.width): + i = x + y * self.width + if i > len(color_data) - 1: + return + + self.img[i * 3: i * 3 + 3] = color_data[i][::-1] + + def save(self, f, type): + assert type == "BMP" + # 54 bytes of preambule + image color data. + filesize = 54 + 3 * self.width * self.height + # 14 bytes of header. + bmpfileheader = bytearray([ord('B'), ord('M')] + self._int_to_bytes(filesize) + + [0, 0, 0, 0, 54, 0, 0, 0]) + # 40 bytes of info. + bmpinfoheader = bytearray([40, 0, 0, 0] + + self._int_to_bytes(self.width) + + self._int_to_bytes(self.height) + + [1, 0, 24] + (25 * [0])) + + padlength = (4 - (self.width * 3) % 4) % 4 + bmppad = bytearray([0, 0, 0]) + padding = bmppad[0 : padlength] + + f.write(bmpfileheader) + f.write(bmpinfoheader) + + for i in range(self.height): + offset = self.width * (self.height - i - 1) * 3 + f.write(self.img[offset : offset + 3 * self.width]) + f.write(padding) + +def encode_string_as_bmp_image(string_data): + data_bytes = array.array("B", string_data.encode("utf-8")) + + num_bytes = len(data_bytes) + + # Encode data bytes to color data (RGB), one bit per channel. + # This is to avoid errors due to different color spaces used in decoding. + color_data = [] + for byte in data_bytes: + p = [int(x) * 255 for x in '{0:08b}'.format(byte)] + color_data.append((p[0], p[1], p[2])) + color_data.append((p[3], p[4], p[5])) + color_data.append((p[6], p[7], 0)) + + # Render image. + num_pixels = len(color_data) + sqrt = int(math.ceil(math.sqrt(num_pixels))) + img = Image.new("RGB", (sqrt, sqrt), "black") + img.putdata(color_data) + + # Flush image to string. + f = BytesIO() + img.save(f, "BMP") + f.seek(0) + + return f.read() + +def generate_payload(request, server_data): + data = (u'{"headers": %(headers)s}') % server_data + if b"id" in request.GET: + request.server.stash.put(request.GET[b"id"], data) + data = encode_string_as_bmp_image(data) + return data + +def generate_report_headers_payload(request, server_data): + stashed_data = request.server.stash.take(request.GET[b"id"]) + return stashed_data + +def main(request, response): + handler = lambda data: generate_payload(request, data) + content_type = b'image/bmp' + + if b"report-headers" in request.GET: + handler = lambda data: generate_report_headers_payload(request, data) + content_type = b'application/json' + + subresource.respond(request, + response, + payload_generator = handler, + content_type = content_type, + access_control_allow_origin = b"*") diff --git a/test/wpt/tests/common/security-features/subresource/referrer.py b/test/wpt/tests/common/security-features/subresource/referrer.py new file mode 100644 index 00000000000..e36631479e6 --- /dev/null +++ b/test/wpt/tests/common/security-features/subresource/referrer.py @@ -0,0 +1,4 @@ +def main(request, response): + referrer = request.headers.get(b"referer", b"") + response_headers = [(b"Content-Type", b"text/javascript")] + return (200, response_headers, b"window.referrer = '" + referrer + b"'") diff --git a/test/wpt/tests/common/security-features/subresource/script.py b/test/wpt/tests/common/security-features/subresource/script.py new file mode 100644 index 00000000000..9701816b9fa --- /dev/null +++ b/test/wpt/tests/common/security-features/subresource/script.py @@ -0,0 +1,14 @@ +import os, sys +from wptserve.utils import isomorphic_decode + +import importlib +subresource = importlib.import_module("common.security-features.subresource.subresource") + +def generate_payload(server_data): + return subresource.get_template(u"script.js.template") % server_data + +def main(request, response): + subresource.respond(request, + response, + payload_generator = generate_payload, + content_type = b"application/javascript") diff --git a/test/wpt/tests/common/security-features/subresource/shared-worker.py b/test/wpt/tests/common/security-features/subresource/shared-worker.py new file mode 100644 index 00000000000..bdfb61bbb3f --- /dev/null +++ b/test/wpt/tests/common/security-features/subresource/shared-worker.py @@ -0,0 +1,13 @@ +import os, sys +from wptserve.utils import isomorphic_decode +import importlib +subresource = importlib.import_module("common.security-features.subresource.subresource") + +def generate_payload(server_data): + return subresource.get_template(u"shared-worker.js.template") % server_data + +def main(request, response): + subresource.respond(request, + response, + payload_generator = generate_payload, + content_type = b"application/javascript") diff --git a/test/wpt/tests/common/security-features/subresource/static-import.py b/test/wpt/tests/common/security-features/subresource/static-import.py new file mode 100644 index 00000000000..717d3de6b18 --- /dev/null +++ b/test/wpt/tests/common/security-features/subresource/static-import.py @@ -0,0 +1,19 @@ +import os, sys +from urllib.parse import unquote + +from wptserve.utils import isomorphic_decode +import importlib +subresource = importlib.import_module("common.security-features.subresource.subresource") + +def generate_payload(request): + import_url = unquote(isomorphic_decode(request.GET[b'import_url'])) + return subresource.get_template(u"static-import.js.template") % { + u"import_url": import_url + } + +def main(request, response): + payload_generator = lambda _: generate_payload(request) + subresource.respond(request, + response, + payload_generator = payload_generator, + content_type = b"application/javascript") diff --git a/test/wpt/tests/common/security-features/subresource/stylesheet.py b/test/wpt/tests/common/security-features/subresource/stylesheet.py new file mode 100644 index 00000000000..05db249250d --- /dev/null +++ b/test/wpt/tests/common/security-features/subresource/stylesheet.py @@ -0,0 +1,61 @@ +import os, sys +from wptserve.utils import isomorphic_decode +import importlib +subresource = importlib.import_module("common.security-features.subresource.subresource") + +def generate_payload(request, server_data): + data = (u'{"headers": %(headers)s}') % server_data + type = b'image' + if b"type" in request.GET: + type = request.GET[b"type"] + + if b"id" in request.GET: + request.server.stash.put(request.GET[b"id"], data) + + if type == b'image': + return subresource.get_template(u"image.css.template") % {u"id": isomorphic_decode(request.GET[b"id"])} + + elif type == b'font': + return subresource.get_template(u"font.css.template") % {u"id": isomorphic_decode(request.GET[b"id"])} + + elif type == b'svg': + return subresource.get_template(u"svg.css.template") % { + u"id": isomorphic_decode(request.GET[b"id"]), + u"property": isomorphic_decode(request.GET[b"property"])} + + # A `'stylesheet-only'`-type stylesheet has no nested resources; this is + # useful in tests that cover referrers for stylesheet fetches (e.g. fetches + # triggered by `@import` statements). + elif type == b'stylesheet-only': + return u'' + +def generate_import_rule(request, server_data): + return u"@import url('%(url)s');" % { + u"url": subresource.create_url(request, swap_origin=True, + query_parameter_to_remove=u"import-rule") + } + +def generate_report_headers_payload(request, server_data): + stashed_data = request.server.stash.take(request.GET[b"id"]) + return stashed_data + +def main(request, response): + payload_generator = lambda data: generate_payload(request, data) + content_type = b"text/css" + referrer_policy = b"unsafe-url" + if b"import-rule" in request.GET: + payload_generator = lambda data: generate_import_rule(request, data) + + if b"report-headers" in request.GET: + payload_generator = lambda data: generate_report_headers_payload(request, data) + content_type = b'application/json' + + if b"referrer-policy" in request.GET: + referrer_policy = request.GET[b"referrer-policy"] + + subresource.respond( + request, + response, + payload_generator = payload_generator, + content_type = content_type, + maybe_additional_headers = { b"Referrer-Policy": referrer_policy }) diff --git a/test/wpt/tests/common/security-features/subresource/subresource.py b/test/wpt/tests/common/security-features/subresource/subresource.py new file mode 100644 index 00000000000..b3c055a93a5 --- /dev/null +++ b/test/wpt/tests/common/security-features/subresource/subresource.py @@ -0,0 +1,199 @@ +import os, json +from urllib.parse import parse_qsl, SplitResult, urlencode, urlsplit, urlunsplit + +from wptserve.utils import isomorphic_decode, isomorphic_encode + +def get_template(template_basename): + script_directory = os.path.dirname(os.path.abspath(isomorphic_decode(__file__))) + template_directory = os.path.abspath(os.path.join(script_directory, + u"template")) + template_filename = os.path.join(template_directory, template_basename) + + with open(template_filename, "r") as f: + return f.read() + + +def redirect(url, response): + response.add_required_headers = False + response.writer.write_status(301) + response.writer.write_header(b"access-control-allow-origin", b"*") + response.writer.write_header(b"location", isomorphic_encode(url)) + response.writer.end_headers() + response.writer.write(u"") + + +# TODO(kristijanburnik): subdomain_prefix is a hardcoded value aligned with +# referrer-policy-test-case.js. The prefix should be configured in one place. +def __get_swapped_origin_netloc(netloc, subdomain_prefix = u"www1."): + if netloc.startswith(subdomain_prefix): + return netloc[len(subdomain_prefix):] + else: + return subdomain_prefix + netloc + + +# Creates a URL (typically a redirect target URL) that is the same as the +# current request URL `request.url`, except for: +# - When `swap_scheme` or `swap_origin` is True, its scheme/origin is changed +# to the other one. (http <-> https, ws <-> wss, etc.) +# - For `downgrade`, we redirect to a URL that would be successfully loaded +# if and only if upgrade-insecure-request is applied. +# - `query_parameter_to_remove` parameter is removed from query part. +# Its default is "redirection" to avoid redirect loops. +def create_url(request, + swap_scheme=False, + swap_origin=False, + downgrade=False, + query_parameter_to_remove=u"redirection"): + parsed = urlsplit(request.url) + destination_netloc = parsed.netloc + + scheme = parsed.scheme + if swap_scheme: + scheme = u"http" if parsed.scheme == u"https" else u"https" + hostname = parsed.netloc.split(u':')[0] + port = request.server.config[u"ports"][scheme][0] + destination_netloc = u":".join([hostname, str(port)]) + + if downgrade: + # These rely on some unintuitive cleverness due to WPT's test setup: + # 'Upgrade-Insecure-Requests' does not upgrade the port number, + # so we use URLs in the form `http://[domain]:[https-port]`, + # which will be upgraded to `https://[domain]:[https-port]`. + # If the upgrade fails, the load will fail, as we don't serve HTTP over + # the secure port. + if parsed.scheme == u"https": + scheme = u"http" + elif parsed.scheme == u"wss": + scheme = u"ws" + else: + raise ValueError(u"Downgrade redirection: Invalid scheme '%s'" % + parsed.scheme) + hostname = parsed.netloc.split(u':')[0] + port = request.server.config[u"ports"][parsed.scheme][0] + destination_netloc = u":".join([hostname, str(port)]) + + if swap_origin: + destination_netloc = __get_swapped_origin_netloc(destination_netloc) + + parsed_query = parse_qsl(parsed.query, keep_blank_values=True) + parsed_query = [x for x in parsed_query if x[0] != query_parameter_to_remove] + + destination_url = urlunsplit(SplitResult( + scheme = scheme, + netloc = destination_netloc, + path = parsed.path, + query = urlencode(parsed_query), + fragment = None)) + + return destination_url + + +def preprocess_redirection(request, response): + if b"redirection" not in request.GET: + return False + + redirection = request.GET[b"redirection"] + + if redirection == b"no-redirect": + return False + elif redirection == b"keep-scheme": + redirect_url = create_url(request, swap_scheme=False) + elif redirection == b"swap-scheme": + redirect_url = create_url(request, swap_scheme=True) + elif redirection == b"downgrade": + redirect_url = create_url(request, downgrade=True) + elif redirection == b"keep-origin": + redirect_url = create_url(request, swap_origin=False) + elif redirection == b"swap-origin": + redirect_url = create_url(request, swap_origin=True) + else: + raise ValueError(u"Invalid redirection type '%s'" % isomorphic_decode(redirection)) + + redirect(redirect_url, response) + return True + + +def preprocess_stash_action(request, response): + if b"action" not in request.GET: + return False + + action = request.GET[b"action"] + + key = request.GET[b"key"] + stash = request.server.stash + path = request.GET[b"path"] if b"path" in request.GET \ + else isomorphic_encode(request.url.split(u'?')[0]) + + if action == b"put": + value = isomorphic_decode(request.GET[b"value"]) + stash.take(key=key, path=path) + stash.put(key=key, value=value, path=path) + response_data = json.dumps({u"status": u"success", u"result": isomorphic_decode(key)}) + elif action == b"purge": + value = stash.take(key=key, path=path) + return False + elif action == b"take": + value = stash.take(key=key, path=path) + if value is None: + status = u"allowed" + else: + status = u"blocked" + response_data = json.dumps({u"status": status, u"result": value}) + else: + return False + + response.add_required_headers = False + response.writer.write_status(200) + response.writer.write_header(b"content-type", b"text/javascript") + response.writer.write_header(b"cache-control", b"no-cache; must-revalidate") + response.writer.end_headers() + response.writer.write(response_data) + return True + + +def __noop(request, response): + return u"" + + +def respond(request, + response, + status_code = 200, + content_type = b"text/html", + payload_generator = __noop, + cache_control = b"no-cache; must-revalidate", + access_control_allow_origin = b"*", + maybe_additional_headers = None): + if preprocess_redirection(request, response): + return + + if preprocess_stash_action(request, response): + return + + response.add_required_headers = False + response.writer.write_status(status_code) + + if access_control_allow_origin != None: + response.writer.write_header(b"access-control-allow-origin", + access_control_allow_origin) + response.writer.write_header(b"content-type", content_type) + response.writer.write_header(b"cache-control", cache_control) + + additional_headers = maybe_additional_headers or {} + for header, value in additional_headers.items(): + response.writer.write_header(header, value) + + response.writer.end_headers() + + new_headers = {} + new_val = [] + for key, val in request.headers.items(): + if len(val) == 1: + new_val = isomorphic_decode(val[0]) + else: + new_val = [isomorphic_decode(x) for x in val] + new_headers[isomorphic_decode(key)] = new_val + + server_data = {u"headers": json.dumps(new_headers, indent = 4)} + + payload = payload_generator(server_data) + response.writer.write(payload) diff --git a/test/wpt/tests/common/security-features/subresource/svg.py b/test/wpt/tests/common/security-features/subresource/svg.py new file mode 100644 index 00000000000..9c569e3bf52 --- /dev/null +++ b/test/wpt/tests/common/security-features/subresource/svg.py @@ -0,0 +1,37 @@ +import os, sys +from wptserve.utils import isomorphic_decode +import importlib +subresource = importlib.import_module("common.security-features.subresource.subresource") + +def generate_payload(request, server_data): + data = (u'{"headers": %(headers)s}') % server_data + if b"id" in request.GET: + with request.server.stash.lock: + request.server.stash.take(request.GET[b"id"]) + request.server.stash.put(request.GET[b"id"], data) + return u"" + +def generate_payload_embedded(request, server_data): + return subresource.get_template(u"svg.embedded.template") % { + u"id": isomorphic_decode(request.GET[b"id"]), + u"property": isomorphic_decode(request.GET[b"property"])} + +def generate_report_headers_payload(request, server_data): + stashed_data = request.server.stash.take(request.GET[b"id"]) + return stashed_data + +def main(request, response): + handler = lambda data: generate_payload(request, data) + content_type = b'image/svg+xml' + + if b"embedded-svg" in request.GET: + handler = lambda data: generate_payload_embedded(request, data) + + if b"report-headers" in request.GET: + handler = lambda data: generate_report_headers_payload(request, data) + content_type = b'application/json' + + subresource.respond(request, + response, + payload_generator = handler, + content_type = content_type) diff --git a/test/wpt/tests/common/security-features/subresource/template/document.html.template b/test/wpt/tests/common/security-features/subresource/template/document.html.template new file mode 100644 index 00000000000..141711c1483 --- /dev/null +++ b/test/wpt/tests/common/security-features/subresource/template/document.html.template @@ -0,0 +1,16 @@ + + + + This page reports back it's request details to the parent frame + + + + + diff --git a/test/wpt/tests/common/security-features/subresource/template/font.css.template b/test/wpt/tests/common/security-features/subresource/template/font.css.template new file mode 100644 index 00000000000..9d1e9c421cc --- /dev/null +++ b/test/wpt/tests/common/security-features/subresource/template/font.css.template @@ -0,0 +1,9 @@ +@font-face { + font-family: 'wpt'; + font-style: normal; + font-weight: normal; + src: url(/common/security-features/subresource/font.py?id=%(id)s) format('truetype'); +} +body { + font-family: 'wpt'; +} diff --git a/test/wpt/tests/common/security-features/subresource/template/image.css.template b/test/wpt/tests/common/security-features/subresource/template/image.css.template new file mode 100644 index 00000000000..dfe41f1bf16 --- /dev/null +++ b/test/wpt/tests/common/security-features/subresource/template/image.css.template @@ -0,0 +1,3 @@ +div.styled::before { + content:url(/common/security-features/subresource/image.py?id=%(id)s) +} diff --git a/test/wpt/tests/common/security-features/subresource/template/script.js.template b/test/wpt/tests/common/security-features/subresource/template/script.js.template new file mode 100644 index 00000000000..e2edf21819d --- /dev/null +++ b/test/wpt/tests/common/security-features/subresource/template/script.js.template @@ -0,0 +1,3 @@ +postMessage({ + "headers": %(headers)s +}, "*"); diff --git a/test/wpt/tests/common/security-features/subresource/template/shared-worker.js.template b/test/wpt/tests/common/security-features/subresource/template/shared-worker.js.template new file mode 100644 index 00000000000..c3f109e4a90 --- /dev/null +++ b/test/wpt/tests/common/security-features/subresource/template/shared-worker.js.template @@ -0,0 +1,5 @@ +onconnect = function(e) { + e.ports[0].postMessage({ + "headers": %(headers)s + }); +}; diff --git a/test/wpt/tests/common/security-features/subresource/template/static-import.js.template b/test/wpt/tests/common/security-features/subresource/template/static-import.js.template new file mode 100644 index 00000000000..095459b5475 --- /dev/null +++ b/test/wpt/tests/common/security-features/subresource/template/static-import.js.template @@ -0,0 +1 @@ +import '%(import_url)s'; diff --git a/test/wpt/tests/common/security-features/subresource/template/svg.css.template b/test/wpt/tests/common/security-features/subresource/template/svg.css.template new file mode 100644 index 00000000000..c2e509cc3b8 --- /dev/null +++ b/test/wpt/tests/common/security-features/subresource/template/svg.css.template @@ -0,0 +1,3 @@ +path { + %(property)s: url(/common/security-features/subresource/svg.py?id=%(id)s#invalidFragment); +} diff --git a/test/wpt/tests/common/security-features/subresource/template/svg.embedded.template b/test/wpt/tests/common/security-features/subresource/template/svg.embedded.template new file mode 100644 index 00000000000..5986c4800a7 --- /dev/null +++ b/test/wpt/tests/common/security-features/subresource/template/svg.embedded.template @@ -0,0 +1,5 @@ + + + + + diff --git a/test/wpt/tests/common/security-features/subresource/template/worker.js.template b/test/wpt/tests/common/security-features/subresource/template/worker.js.template new file mode 100644 index 00000000000..817dd8c87ac --- /dev/null +++ b/test/wpt/tests/common/security-features/subresource/template/worker.js.template @@ -0,0 +1,3 @@ +postMessage({ + "headers": %(headers)s +}); diff --git a/test/wpt/tests/common/security-features/subresource/video.py b/test/wpt/tests/common/security-features/subresource/video.py new file mode 100644 index 00000000000..7cfbbfa68c8 --- /dev/null +++ b/test/wpt/tests/common/security-features/subresource/video.py @@ -0,0 +1,17 @@ +import os, sys +from wptserve.utils import isomorphic_decode +import importlib +subresource = importlib.import_module("common.security-features.subresource.subresource") + +def generate_payload(request, server_data): + file = os.path.join(request.doc_root, u"media", u"movie_5.ogv") + return open(file, "rb").read() + + +def main(request, response): + handler = lambda data: generate_payload(request, data) + subresource.respond(request, + response, + payload_generator = handler, + access_control_allow_origin = b"*", + content_type = b"video/ogg") diff --git a/test/wpt/tests/common/security-features/subresource/worker.py b/test/wpt/tests/common/security-features/subresource/worker.py new file mode 100644 index 00000000000..f655633b5db --- /dev/null +++ b/test/wpt/tests/common/security-features/subresource/worker.py @@ -0,0 +1,13 @@ +import os, sys +from wptserve.utils import isomorphic_decode +import importlib +subresource = importlib.import_module("common.security-features.subresource.subresource") + +def generate_payload(server_data): + return subresource.get_template(u"worker.js.template") % server_data + +def main(request, response): + subresource.respond(request, + response, + payload_generator = generate_payload, + content_type = b"application/javascript") diff --git a/test/wpt/tests/common/security-features/subresource/xhr.py b/test/wpt/tests/common/security-features/subresource/xhr.py new file mode 100644 index 00000000000..75921e91569 --- /dev/null +++ b/test/wpt/tests/common/security-features/subresource/xhr.py @@ -0,0 +1,16 @@ +import os, sys +from wptserve.utils import isomorphic_decode +import importlib +subresource = importlib.import_module("common.security-features.subresource.subresource") + +def generate_payload(server_data): + data = (u'{"headers": %(headers)s}') % server_data + return data + +def main(request, response): + subresource.respond(request, + response, + payload_generator = generate_payload, + access_control_allow_origin = b"*", + content_type = b"application/json", + cache_control = b"no-store") diff --git a/test/wpt/tests/common/security-features/tools/format_spec_src_json.py b/test/wpt/tests/common/security-features/tools/format_spec_src_json.py new file mode 100644 index 00000000000..d1bf5817ad2 --- /dev/null +++ b/test/wpt/tests/common/security-features/tools/format_spec_src_json.py @@ -0,0 +1,24 @@ +import collections +import json +import os + + +def main(): + '''Formats spec.src.json.''' + script_directory = os.path.dirname(os.path.abspath(__file__)) + for dir in [ + 'mixed-content', 'referrer-policy', 'referrer-policy/4K-1', + 'referrer-policy/4K', 'referrer-policy/4K+1', + 'upgrade-insecure-requests' + ]: + filename = os.path.join(script_directory, '..', '..', '..', dir, + 'spec.src.json') + spec = json.load( + open(filename, 'r'), object_pairs_hook=collections.OrderedDict) + with open(filename, 'w') as f: + f.write(json.dumps(spec, indent=2, separators=(',', ': '))) + f.write('\n') + + +if __name__ == '__main__': + main() diff --git a/test/wpt/tests/common/security-features/tools/generate.py b/test/wpt/tests/common/security-features/tools/generate.py new file mode 100644 index 00000000000..176e0ebbebc --- /dev/null +++ b/test/wpt/tests/common/security-features/tools/generate.py @@ -0,0 +1,464 @@ +#!/usr/bin/env python3 + +from __future__ import print_function + +import argparse +import collections +import copy +import json +import os +import sys + +import spec_validator +import util + + +def expand_pattern(expansion_pattern, test_expansion_schema): + expansion = {} + for artifact_key in expansion_pattern: + artifact_value = expansion_pattern[artifact_key] + if artifact_value == '*': + expansion[artifact_key] = test_expansion_schema[artifact_key] + elif isinstance(artifact_value, list): + expansion[artifact_key] = artifact_value + elif isinstance(artifact_value, dict): + # Flattened expansion. + expansion[artifact_key] = [] + values_dict = expand_pattern(artifact_value, + test_expansion_schema[artifact_key]) + for sub_key in values_dict.keys(): + expansion[artifact_key] += values_dict[sub_key] + else: + expansion[artifact_key] = [artifact_value] + + return expansion + + +def permute_expansion(expansion, + artifact_order, + selection={}, + artifact_index=0): + assert isinstance(artifact_order, list), "artifact_order should be a list" + + if artifact_index >= len(artifact_order): + yield selection + return + + artifact_key = artifact_order[artifact_index] + + for artifact_value in expansion[artifact_key]: + selection[artifact_key] = artifact_value + for next_selection in permute_expansion(expansion, artifact_order, + selection, artifact_index + 1): + yield next_selection + + +# Dumps the test config `selection` into a serialized JSON string. +def dump_test_parameters(selection): + return json.dumps( + selection, + indent=2, + separators=(',', ': '), + sort_keys=True, + cls=util.CustomEncoder) + + +def get_test_filename(spec_directory, spec_json, selection): + '''Returns the filname for the main test HTML file''' + + selection_for_filename = copy.deepcopy(selection) + # Use 'unset' rather than 'None' in test filenames. + if selection_for_filename['delivery_value'] is None: + selection_for_filename['delivery_value'] = 'unset' + + return os.path.join( + spec_directory, + spec_json['test_file_path_pattern'] % selection_for_filename) + + +def get_csp_value(value): + ''' + Returns actual CSP header values (e.g. "worker-src 'self'") for the + given string used in PolicyDelivery's value (e.g. "worker-src-self"). + ''' + + # script-src + # Test-related scripts like testharness.js and inline scripts containing + # test bodies. + # 'unsafe-inline' is added as a workaround here. This is probably not so + # bad, as it shouldn't intefere non-inline-script requests that we want to + # test. + if value == 'script-src-wildcard': + return "script-src * 'unsafe-inline'" + if value == 'script-src-self': + return "script-src 'self' 'unsafe-inline'" + # Workaround for "script-src 'none'" would be more complicated, because + # - "script-src 'none' 'unsafe-inline'" is handled somehow differently from + # "script-src 'none'", i.e. + # https://w3c.github.io/webappsec-csp/#match-url-to-source-list Step 3 + # handles the latter but not the former. + # - We need nonce- or path-based additional values to allow same-origin + # test scripts like testharness.js. + # Therefore, we disable 'script-src-none' tests for now in + # `/content-security-policy/spec.src.json`. + if value == 'script-src-none': + return "script-src 'none'" + + # worker-src + if value == 'worker-src-wildcard': + return 'worker-src *' + if value == 'worker-src-self': + return "worker-src 'self'" + if value == 'worker-src-none': + return "worker-src 'none'" + raise Exception('Invalid delivery_value: %s' % value) + +def handle_deliveries(policy_deliveries): + ''' + Generate elements and HTTP headers for the given list of + PolicyDelivery. + TODO(hiroshige): Merge duplicated code here, scope/document.py, etc. + ''' + + meta = '' + headers = {} + + for delivery in policy_deliveries: + if delivery.value is None: + continue + if delivery.key == 'referrerPolicy': + if delivery.delivery_type == 'meta': + meta += \ + '' % delivery.value + elif delivery.delivery_type == 'http-rp': + headers['Referrer-Policy'] = delivery.value + # TODO(kristijanburnik): Limit to WPT origins. + headers['Access-Control-Allow-Origin'] = '*' + else: + raise Exception( + 'Invalid delivery_type: %s' % delivery.delivery_type) + elif delivery.key == 'mixedContent': + assert (delivery.value == 'opt-in') + if delivery.delivery_type == 'meta': + meta += '' + elif delivery.delivery_type == 'http-rp': + headers['Content-Security-Policy'] = 'block-all-mixed-content' + else: + raise Exception( + 'Invalid delivery_type: %s' % delivery.delivery_type) + elif delivery.key == 'contentSecurityPolicy': + csp_value = get_csp_value(delivery.value) + if delivery.delivery_type == 'meta': + meta += '' + elif delivery.delivery_type == 'http-rp': + headers['Content-Security-Policy'] = csp_value + else: + raise Exception( + 'Invalid delivery_type: %s' % delivery.delivery_type) + elif delivery.key == 'upgradeInsecureRequests': + # https://w3c.github.io/webappsec-upgrade-insecure-requests/#delivery + assert (delivery.value == 'upgrade') + if delivery.delivery_type == 'meta': + meta += '' + elif delivery.delivery_type == 'http-rp': + headers[ + 'Content-Security-Policy'] = 'upgrade-insecure-requests' + else: + raise Exception( + 'Invalid delivery_type: %s' % delivery.delivery_type) + else: + raise Exception('Invalid delivery_key: %s' % delivery.key) + return {"meta": meta, "headers": headers} + + +def generate_selection(spec_json, selection): + ''' + Returns a scenario object (with a top-level source_context_list entry, + which will be removed in generate_test_file() later). + ''' + + target_policy_delivery = util.PolicyDelivery(selection['delivery_type'], + selection['delivery_key'], + selection['delivery_value']) + del selection['delivery_type'] + del selection['delivery_key'] + del selection['delivery_value'] + + # Parse source context list and policy deliveries of source contexts. + # `util.ShouldSkip()` exceptions are raised if e.g. unsuppported + # combinations of source contexts and policy deliveries are used. + source_context_list_scheme = spec_json['source_context_list_schema'][ + selection['source_context_list']] + selection['source_context_list'] = [ + util.SourceContext.from_json(source_context, target_policy_delivery, + spec_json['source_context_schema']) + for source_context in source_context_list_scheme['sourceContextList'] + ] + + # Check if the subresource is supported by the innermost source context. + innermost_source_context = selection['source_context_list'][-1] + supported_subresource = spec_json['source_context_schema'][ + 'supported_subresource'][innermost_source_context.source_context_type] + if supported_subresource != '*': + if selection['subresource'] not in supported_subresource: + raise util.ShouldSkip() + + # Parse subresource policy deliveries. + selection[ + 'subresource_policy_deliveries'] = util.PolicyDelivery.list_from_json( + source_context_list_scheme['subresourcePolicyDeliveries'], + target_policy_delivery, spec_json['subresource_schema'] + ['supported_delivery_type'][selection['subresource']]) + + # Generate per-scenario test description. + selection['test_description'] = spec_json[ + 'test_description_template'] % selection + + return selection + + +def generate_test_file(spec_directory, test_helper_filenames, + test_html_template_basename, test_filename, scenarios): + ''' + Generates a test HTML file (and possibly its associated .headers file) + from `scenarios`. + ''' + + # Scenarios for the same file should have the same `source_context_list`, + # including the top-level one. + # Note: currently, non-top-level source contexts aren't necessarily required + # to be the same, but we set this requirement as it will be useful e.g. when + # we e.g. reuse a worker among multiple scenarios. + for scenario in scenarios: + assert (scenario['source_context_list'] == scenarios[0] + ['source_context_list']) + + # We process the top source context below, and do not include it in + # the JSON objects (i.e. `scenarios`) in generated HTML files. + top_source_context = scenarios[0]['source_context_list'].pop(0) + assert (top_source_context.source_context_type == 'top') + for scenario in scenarios[1:]: + assert (scenario['source_context_list'].pop(0) == top_source_context) + + parameters = {} + + # Sort scenarios, to avoid unnecessary diffs due to different orders in + # `scenarios`. + serialized_scenarios = sorted( + [dump_test_parameters(scenario) for scenario in scenarios]) + + parameters['scenarios'] = ",\n".join(serialized_scenarios).replace( + "\n", "\n" + " " * 10) + + test_directory = os.path.dirname(test_filename) + + parameters['helper_js'] = "" + for test_helper_filename in test_helper_filenames: + parameters['helper_js'] += ' \n' % ( + os.path.relpath(test_helper_filename, test_directory)) + parameters['sanity_checker_js'] = os.path.relpath( + os.path.join(spec_directory, 'generic', 'sanity-checker.js'), + test_directory) + parameters['spec_json_js'] = os.path.relpath( + os.path.join(spec_directory, 'generic', 'spec_json.js'), + test_directory) + + test_headers_filename = test_filename + ".headers" + + test_html_template = util.get_template(test_html_template_basename) + disclaimer_template = util.get_template('disclaimer.template') + + html_template_filename = os.path.join(util.template_directory, + test_html_template_basename) + generated_disclaimer = disclaimer_template \ + % {'generating_script_filename': os.path.relpath(sys.argv[0], + util.test_root_directory), + 'spec_directory': os.path.relpath(spec_directory, + util.test_root_directory)} + + # Adjust the template for the test invoking JS. Indent it to look nice. + parameters['generated_disclaimer'] = generated_disclaimer.rstrip() + + # Directory for the test files. + try: + os.makedirs(test_directory) + except: + pass + + delivery = handle_deliveries(top_source_context.policy_deliveries) + + if len(delivery['headers']) > 0: + with open(test_headers_filename, "w") as f: + for header in delivery['headers']: + f.write('%s: %s\n' % (header, delivery['headers'][header])) + + parameters['meta_delivery_method'] = delivery['meta'] + # Obey the lint and pretty format. + if len(parameters['meta_delivery_method']) > 0: + parameters['meta_delivery_method'] = "\n " + \ + parameters['meta_delivery_method'] + + # Write out the generated HTML file. + util.write_file(test_filename, test_html_template % parameters) + + +def generate_test_source_files(spec_directory, test_helper_filenames, + spec_json, target): + test_expansion_schema = spec_json['test_expansion_schema'] + specification = spec_json['specification'] + + if target == "debug": + spec_json_js_template = util.get_template('spec_json.js.template') + util.write_file( + os.path.join(spec_directory, "generic", "spec_json.js"), + spec_json_js_template % {'spec_json': json.dumps(spec_json)}) + util.write_file( + os.path.join(spec_directory, "generic", + "debug-output.spec.src.json"), + json.dumps(spec_json, indent=2, separators=(',', ': '))) + + # Choose a debug/release template depending on the target. + html_template = "test.%s.html.template" % target + + artifact_order = test_expansion_schema.keys() + artifact_order.remove('expansion') + + excluded_selection_pattern = '' + for key in artifact_order: + excluded_selection_pattern += '%(' + key + ')s/' + + # Create list of excluded tests. + exclusion_dict = set() + for excluded_pattern in spec_json['excluded_tests']: + excluded_expansion = \ + expand_pattern(excluded_pattern, test_expansion_schema) + for excluded_selection in permute_expansion(excluded_expansion, + artifact_order): + excluded_selection['delivery_key'] = spec_json['delivery_key'] + exclusion_dict.add(excluded_selection_pattern % excluded_selection) + + # `scenarios[filename]` represents the list of scenario objects to be + # generated into `filename`. + scenarios = {} + + for spec in specification: + # Used to make entries with expansion="override" override preceding + # entries with the same |selection_path|. + output_dict = {} + + for expansion_pattern in spec['test_expansion']: + expansion = expand_pattern(expansion_pattern, + test_expansion_schema) + for selection in permute_expansion(expansion, artifact_order): + selection['delivery_key'] = spec_json['delivery_key'] + selection_path = spec_json['selection_pattern'] % selection + if selection_path in output_dict: + if expansion_pattern['expansion'] != 'override': + print("Error: expansion is default in:") + print(dump_test_parameters(selection)) + print("but overrides:") + print(dump_test_parameters( + output_dict[selection_path])) + sys.exit(1) + output_dict[selection_path] = copy.deepcopy(selection) + + for selection_path in output_dict: + selection = output_dict[selection_path] + if (excluded_selection_pattern % selection) in exclusion_dict: + print('Excluding selection:', selection_path) + continue + try: + test_filename = get_test_filename(spec_directory, spec_json, + selection) + scenario = generate_selection(spec_json, selection) + scenarios[test_filename] = scenarios.get(test_filename, + []) + [scenario] + except util.ShouldSkip: + continue + + for filename in scenarios: + generate_test_file(spec_directory, test_helper_filenames, + html_template, filename, scenarios[filename]) + + +def merge_json(base, child): + for key in child: + if key not in base: + base[key] = child[key] + continue + # `base[key]` and `child[key]` both exists. + if isinstance(base[key], list) and isinstance(child[key], list): + base[key].extend(child[key]) + elif isinstance(base[key], dict) and isinstance(child[key], dict): + merge_json(base[key], child[key]) + else: + base[key] = child[key] + + +def main(): + parser = argparse.ArgumentParser( + description='Test suite generator utility') + parser.add_argument( + '-t', + '--target', + type=str, + choices=("release", "debug"), + default="release", + help='Sets the appropriate template for generating tests') + parser.add_argument( + '-s', + '--spec', + type=str, + default=os.getcwd(), + help='Specify a file used for describing and generating the tests') + # TODO(kristijanburnik): Add option for the spec_json file. + args = parser.parse_args() + + spec_directory = os.path.abspath(args.spec) + + # Read `spec.src.json` files, starting from `spec_directory`, and + # continuing to parent directories as long as `spec.src.json` exists. + spec_filenames = [] + test_helper_filenames = [] + spec_src_directory = spec_directory + while len(spec_src_directory) >= len(util.test_root_directory): + spec_filename = os.path.join(spec_src_directory, "spec.src.json") + if not os.path.exists(spec_filename): + break + spec_filenames.append(spec_filename) + test_filename = os.path.join(spec_src_directory, 'generic', + 'test-case.sub.js') + assert (os.path.exists(test_filename)) + test_helper_filenames.append(test_filename) + spec_src_directory = os.path.abspath( + os.path.join(spec_src_directory, "..")) + + spec_filenames = list(reversed(spec_filenames)) + test_helper_filenames = list(reversed(test_helper_filenames)) + + if len(spec_filenames) == 0: + print('Error: No spec.src.json is found at %s.' % spec_directory) + return + + # Load the default spec JSON file, ... + default_spec_filename = os.path.join(util.script_directory, + 'spec.src.json') + spec_json = collections.OrderedDict() + if os.path.exists(default_spec_filename): + spec_json = util.load_spec_json(default_spec_filename) + + # ... and then make spec JSON files in subdirectories override the default. + for spec_filename in spec_filenames: + child_spec_json = util.load_spec_json(spec_filename) + merge_json(spec_json, child_spec_json) + + spec_validator.assert_valid_spec_json(spec_json) + generate_test_source_files(spec_directory, test_helper_filenames, + spec_json, args.target) + + +if __name__ == '__main__': + main() diff --git a/test/wpt/tests/common/security-features/tools/spec.src.json b/test/wpt/tests/common/security-features/tools/spec.src.json new file mode 100644 index 00000000000..4a84493f475 --- /dev/null +++ b/test/wpt/tests/common/security-features/tools/spec.src.json @@ -0,0 +1,533 @@ +{ + "selection_pattern": "%(source_context_list)s.%(delivery_type)s/%(delivery_value)s/%(subresource)s/%(origin)s.%(redirection)s.%(source_scheme)s", + "test_file_path_pattern": "gen/%(source_context_list)s.%(delivery_type)s/%(delivery_value)s/%(subresource)s.%(source_scheme)s.html", + "excluded_tests": [ + { + // Workers are same-origin only + "expansion": "*", + "source_scheme": "*", + "source_context_list": "*", + "delivery_type": "*", + "delivery_value": "*", + "redirection": "*", + "subresource": [ + "worker-classic", + "worker-module", + "sharedworker-classic", + "sharedworker-module" + ], + "origin": [ + "cross-https", + "cross-http", + "cross-http-downgrade", + "cross-wss", + "cross-ws", + "cross-ws-downgrade" + ], + "expectation": "*" + }, + { + // Workers are same-origin only (redirects) + "expansion": "*", + "source_scheme": "*", + "source_context_list": "*", + "delivery_type": "*", + "delivery_value": "*", + "redirection": [ + "swap-origin", + "swap-scheme" + ], + "subresource": [ + "worker-classic", + "worker-module", + "sharedworker-classic", + "sharedworker-module" + ], + "origin": "*", + "expectation": "*" + }, + { + // Websockets are ws/wss-only + "expansion": "*", + "source_scheme": "*", + "source_context_list": "*", + "delivery_type": "*", + "delivery_value": "*", + "redirection": "*", + "subresource": "websocket", + "origin": [ + "same-https", + "same-http", + "same-http-downgrade", + "cross-https", + "cross-http", + "cross-http-downgrade" + ], + "expectation": "*" + }, + { + // Redirects are intentionally forbidden in browsers: + // https://fetch.spec.whatwg.org/#concept-websocket-establish + // Websockets are no-redirect only + "expansion": "*", + "source_scheme": "*", + "source_context_list": "*", + "delivery_type": "*", + "delivery_value": "*", + "redirection": [ + "keep-origin", + "swap-origin", + "keep-scheme", + "swap-scheme", + "downgrade" + ], + "subresource": "websocket", + "origin": "*", + "expectation": "*" + }, + { + // ws/wss are websocket-only + "expansion": "*", + "source_scheme": "*", + "source_context_list": "*", + "delivery_type": "*", + "delivery_value": "*", + "redirection": "*", + "subresource": [ + "a-tag", + "area-tag", + "audio-tag", + "beacon", + "fetch", + "iframe-tag", + "img-tag", + "link-css-tag", + "link-prefetch-tag", + "object-tag", + "picture-tag", + "script-tag", + "script-tag-dynamic-import", + "sharedworker-classic", + "sharedworker-import", + "sharedworker-import-data", + "sharedworker-module", + "video-tag", + "worker-classic", + "worker-import", + "worker-import-data", + "worker-module", + "worklet-animation", + "worklet-animation-import-data", + "worklet-audio", + "worklet-audio-import-data", + "worklet-layout", + "worklet-layout-import-data", + "worklet-paint", + "worklet-paint-import-data", + "xhr" + ], + "origin": [ + "same-wss", + "same-ws", + "same-ws-downgrade", + "cross-wss", + "cross-ws", + "cross-ws-downgrade" + ], + "expectation": "*" + }, + { + // Worklets are HTTPS contexts only + "expansion": "*", + "source_scheme": "http", + "source_context_list": "*", + "delivery_type": "*", + "delivery_value": "*", + "redirection": "*", + "subresource": [ + "worklet-animation", + "worklet-animation-import-data", + "worklet-audio", + "worklet-audio-import-data", + "worklet-layout", + "worklet-layout-import-data", + "worklet-paint", + "worklet-paint-import-data" + ], + "origin": "*", + "expectation": "*" + } + ], + "source_context_schema": { + "supported_subresource": { + "top": "*", + "iframe": "*", + "iframe-blank": "*", + "srcdoc": "*", + "worker-classic": [ + "xhr", + "fetch", + "websocket", + "worker-classic", + "worker-module" + ], + "worker-module": [ + "xhr", + "fetch", + "websocket", + "worker-classic", + "worker-module" + ], + "worker-classic-data": [ + "xhr", + "fetch", + "websocket" + ], + "worker-module-data": [ + "xhr", + "fetch", + "websocket" + ], + "sharedworker-classic": [ + "xhr", + "fetch", + "websocket" + ], + "sharedworker-module": [ + "xhr", + "fetch", + "websocket" + ], + "sharedworker-classic-data": [ + "xhr", + "fetch", + "websocket" + ], + "sharedworker-module-data": [ + "xhr", + "fetch", + "websocket" + ] + } + }, + "source_context_list_schema": { + // Warning: Currently, some nested patterns of contexts have different + // inheritance rules for different kinds of policies. + // The generated tests will be used to test/investigate the policy + // inheritance rules, and eventually the policy inheritance rules will + // be unified (https://github.com/w3ctag/design-principles/issues/111). + "top": { + "description": "Policy set by the top-level Document", + "sourceContextList": [ + { + "sourceContextType": "top", + "policyDeliveries": [ + "policy" + ] + } + ], + "subresourcePolicyDeliveries": [] + }, + "req": { + "description": "Subresource request's policy should override Document's policy", + "sourceContextList": [ + { + "sourceContextType": "top", + "policyDeliveries": [ + "anotherPolicy" + ] + } + ], + "subresourcePolicyDeliveries": [ + "nonNullPolicy" + ] + }, + "srcdoc-inherit": { + "description": "srcdoc iframe without its own policy should inherit parent Document's policy", + "sourceContextList": [ + { + "sourceContextType": "top", + "policyDeliveries": [ + "policy" + ] + }, + { + "sourceContextType": "srcdoc" + } + ], + "subresourcePolicyDeliveries": [] + }, + "srcdoc": { + "description": "srcdoc iframe's policy should override parent Document's policy", + "sourceContextList": [ + { + "sourceContextType": "top", + "policyDeliveries": [ + "anotherPolicy" + ] + }, + { + "sourceContextType": "srcdoc", + "policyDeliveries": [ + "nonNullPolicy" + ] + } + ], + "subresourcePolicyDeliveries": [] + }, + "iframe": { + "description": "external iframe's policy should override parent Document's policy", + "sourceContextList": [ + { + "sourceContextType": "top", + "policyDeliveries": [ + "anotherPolicy" + ] + }, + { + "sourceContextType": "iframe", + "policyDeliveries": [ + "policy" + ] + } + ], + "subresourcePolicyDeliveries": [] + }, + "iframe-blank-inherit": { + "description": "blank iframe should inherit parent Document's policy", + "sourceContextList": [ + { + "sourceContextType": "top", + "policyDeliveries": [ + "policy" + ] + }, + { + "sourceContextType": "iframe-blank" + } + ], + "subresourcePolicyDeliveries": [] + }, + "worker-classic": { + // This is applicable to referrer-policy tests. + // Use "worker-classic-inherit" for CSP (mixed-content, etc.). + "description": "dedicated workers shouldn't inherit its parent's policy.", + "sourceContextList": [ + { + "sourceContextType": "top", + "policyDeliveries": [ + "anotherPolicy" + ] + }, + { + "sourceContextType": "worker-classic", + "policyDeliveries": [ + "policy" + ] + } + ], + "subresourcePolicyDeliveries": [] + }, + "worker-classic-data": { + "description": "data: dedicated workers should inherit its parent's policy.", + "sourceContextList": [ + { + "sourceContextType": "top", + "policyDeliveries": [ + "policy" + ] + }, + { + "sourceContextType": "worker-classic-data", + "policyDeliveries": [] + } + ], + "subresourcePolicyDeliveries": [] + }, + "worker-module": { + // This is applicable to referrer-policy tests. + "description": "dedicated workers shouldn't inherit its parent's policy.", + "sourceContextList": [ + { + "sourceContextType": "top", + "policyDeliveries": [ + "anotherPolicy" + ] + }, + { + "sourceContextType": "worker-module", + "policyDeliveries": [ + "policy" + ] + } + ], + "subresourcePolicyDeliveries": [] + }, + "worker-module-data": { + "description": "data: dedicated workers should inherit its parent's policy.", + "sourceContextList": [ + { + "sourceContextType": "top", + "policyDeliveries": [ + "policy" + ] + }, + { + "sourceContextType": "worker-module-data", + "policyDeliveries": [] + } + ], + "subresourcePolicyDeliveries": [] + }, + "sharedworker-classic": { + "description": "shared workers shouldn't inherit its parent's policy.", + "sourceContextList": [ + { + "sourceContextType": "top", + "policyDeliveries": [ + "anotherPolicy" + ] + }, + { + "sourceContextType": "sharedworker-classic", + "policyDeliveries": [ + "policy" + ] + } + ], + "subresourcePolicyDeliveries": [] + }, + "sharedworker-classic-data": { + "description": "data: shared workers should inherit its parent's policy.", + "sourceContextList": [ + { + "sourceContextType": "top", + "policyDeliveries": [ + "policy" + ] + }, + { + "sourceContextType": "sharedworker-classic-data", + "policyDeliveries": [] + } + ], + "subresourcePolicyDeliveries": [] + }, + "sharedworker-module": { + "description": "shared workers shouldn't inherit its parent's policy.", + "sourceContextList": [ + { + "sourceContextType": "top", + "policyDeliveries": [ + "anotherPolicy" + ] + }, + { + "sourceContextType": "sharedworker-module", + "policyDeliveries": [ + "policy" + ] + } + ], + "subresourcePolicyDeliveries": [] + }, + "sharedworker-module-data": { + "description": "data: shared workers should inherit its parent's policy.", + "sourceContextList": [ + { + "sourceContextType": "top", + "policyDeliveries": [ + "policy" + ] + }, + { + "sourceContextType": "sharedworker-module-data", + "policyDeliveries": [] + } + ], + "subresourcePolicyDeliveries": [] + } + }, + "test_expansion_schema": { + "expansion": [ + "default", + "override" + ], + "source_scheme": [ + "http", + "https" + ], + "source_context_list": [ + "top", + "req", + "srcdoc-inherit", + "srcdoc", + "iframe", + "iframe-blank-inherit", + "worker-classic", + "worker-classic-data", + "worker-module", + "worker-module-data", + "sharedworker-classic", + "sharedworker-classic-data", + "sharedworker-module", + "sharedworker-module-data" + ], + "redirection": [ + "no-redirect", + "keep-origin", + "swap-origin", + "keep-scheme", + "swap-scheme", + "downgrade" + ], + "origin": [ + "same-https", + "same-http", + "same-http-downgrade", + "cross-https", + "cross-http", + "cross-http-downgrade", + "same-wss", + "same-ws", + "same-ws-downgrade", + "cross-wss", + "cross-ws", + "cross-ws-downgrade" + ], + "subresource": [ + "a-tag", + "area-tag", + "audio-tag", + "beacon", + "fetch", + "iframe-tag", + "img-tag", + "link-css-tag", + "link-prefetch-tag", + "object-tag", + "picture-tag", + "script-tag", + "script-tag-dynamic-import", + "sharedworker-classic", + "sharedworker-import", + "sharedworker-import-data", + "sharedworker-module", + "video-tag", + "websocket", + "worker-classic", + "worker-import", + "worker-import-data", + "worker-module", + "worklet-animation", + "worklet-animation-import-data", + "worklet-audio", + "worklet-audio-import-data", + "worklet-layout", + "worklet-layout-import-data", + "worklet-paint", + "worklet-paint-import-data", + "xhr" + ] + } +} diff --git a/test/wpt/tests/common/security-features/tools/spec_validator.py b/test/wpt/tests/common/security-features/tools/spec_validator.py new file mode 100644 index 00000000000..3ac3f530169 --- /dev/null +++ b/test/wpt/tests/common/security-features/tools/spec_validator.py @@ -0,0 +1,253 @@ +#!/usr/bin/env python3 + +from __future__ import print_function + +import json, sys + + +def assert_non_empty_string(obj, field): + assert field in obj, 'Missing field "%s"' % field + assert isinstance(obj[field], basestring), \ + 'Field "%s" must be a string' % field + assert len(obj[field]) > 0, 'Field "%s" must not be empty' % field + + +def assert_non_empty_list(obj, field): + assert isinstance(obj[field], list), \ + '%s must be a list' % field + assert len(obj[field]) > 0, \ + '%s list must not be empty' % field + + +def assert_non_empty_dict(obj, field): + assert isinstance(obj[field], dict), \ + '%s must be a dict' % field + assert len(obj[field]) > 0, \ + '%s dict must not be empty' % field + + +def assert_contains(obj, field): + assert field in obj, 'Must contain field "%s"' % field + + +def assert_value_from(obj, field, items): + assert obj[field] in items, \ + 'Field "%s" must be from: %s' % (field, str(items)) + + +def assert_atom_or_list_items_from(obj, field, items): + if isinstance(obj[field], basestring) or isinstance( + obj[field], int) or obj[field] is None: + assert_value_from(obj, field, items) + return + + assert isinstance(obj[field], list), '%s must be a list' % field + for allowed_value in obj[field]: + assert allowed_value != '*', "Wildcard is not supported for lists!" + assert allowed_value in items, \ + 'Field "%s" must be from: %s' % (field, str(items)) + + +def assert_contains_only_fields(obj, expected_fields): + for expected_field in expected_fields: + assert_contains(obj, expected_field) + + for actual_field in obj: + assert actual_field in expected_fields, \ + 'Unexpected field "%s".' % actual_field + + +def leaf_values(schema): + if isinstance(schema, list): + return schema + ret = [] + for _, sub_schema in schema.iteritems(): + ret += leaf_values(sub_schema) + return ret + + +def assert_value_unique_in(value, used_values): + assert value not in used_values, 'Duplicate value "%s"!' % str(value) + used_values[value] = True + + +def assert_valid_artifact(exp_pattern, artifact_key, schema): + if isinstance(schema, list): + assert_atom_or_list_items_from(exp_pattern, artifact_key, + ["*"] + schema) + return + + for sub_artifact_key, sub_schema in schema.iteritems(): + assert_valid_artifact(exp_pattern[artifact_key], sub_artifact_key, + sub_schema) + + +def validate(spec_json, details): + """ Validates the json specification for generating tests. """ + + details['object'] = spec_json + assert_contains_only_fields(spec_json, [ + "selection_pattern", "test_file_path_pattern", + "test_description_template", "test_page_title_template", + "specification", "delivery_key", "subresource_schema", + "source_context_schema", "source_context_list_schema", + "test_expansion_schema", "excluded_tests" + ]) + assert_non_empty_list(spec_json, "specification") + assert_non_empty_dict(spec_json, "test_expansion_schema") + assert_non_empty_list(spec_json, "excluded_tests") + + specification = spec_json['specification'] + test_expansion_schema = spec_json['test_expansion_schema'] + excluded_tests = spec_json['excluded_tests'] + + valid_test_expansion_fields = test_expansion_schema.keys() + + # Should be consistent with `sourceContextMap` in + # `/common/security-features/resources/common.sub.js`. + valid_source_context_names = [ + "top", "iframe", "iframe-blank", "srcdoc", "worker-classic", + "worker-module", "worker-classic-data", "worker-module-data", + "sharedworker-classic", "sharedworker-module", + "sharedworker-classic-data", "sharedworker-module-data" + ] + + valid_subresource_names = [ + "a-tag", "area-tag", "audio-tag", "form-tag", "iframe-tag", "img-tag", + "link-css-tag", "link-prefetch-tag", "object-tag", "picture-tag", + "script-tag", "script-tag-dynamic-import", "video-tag" + ] + ["beacon", "fetch", "xhr", "websocket"] + [ + "worker-classic", "worker-module", "worker-import", + "worker-import-data", "sharedworker-classic", "sharedworker-module", + "sharedworker-import", "sharedworker-import-data", + "serviceworker-classic", "serviceworker-module", + "serviceworker-import", "serviceworker-import-data" + ] + [ + "worklet-animation", "worklet-audio", "worklet-layout", + "worklet-paint", "worklet-animation-import", "worklet-audio-import", + "worklet-layout-import", "worklet-paint-import", + "worklet-animation-import-data", "worklet-audio-import-data", + "worklet-layout-import-data", "worklet-paint-import-data" + ] + + # Validate each single spec. + for spec in specification: + details['object'] = spec + + # Validate required fields for a single spec. + assert_contains_only_fields(spec, [ + 'title', 'description', 'specification_url', 'test_expansion' + ]) + assert_non_empty_string(spec, 'title') + assert_non_empty_string(spec, 'description') + assert_non_empty_string(spec, 'specification_url') + assert_non_empty_list(spec, 'test_expansion') + + for spec_exp in spec['test_expansion']: + details['object'] = spec_exp + assert_contains_only_fields(spec_exp, valid_test_expansion_fields) + + for artifact in test_expansion_schema: + details['test_expansion_field'] = artifact + assert_valid_artifact(spec_exp, artifact, + test_expansion_schema[artifact]) + del details['test_expansion_field'] + + # Validate source_context_schema. + details['object'] = spec_json['source_context_schema'] + assert_contains_only_fields( + spec_json['source_context_schema'], + ['supported_delivery_type', 'supported_subresource']) + assert_contains_only_fields( + spec_json['source_context_schema']['supported_delivery_type'], + valid_source_context_names) + for source_context in spec_json['source_context_schema'][ + 'supported_delivery_type']: + assert_valid_artifact( + spec_json['source_context_schema']['supported_delivery_type'], + source_context, test_expansion_schema['delivery_type']) + assert_contains_only_fields( + spec_json['source_context_schema']['supported_subresource'], + valid_source_context_names) + for source_context in spec_json['source_context_schema'][ + 'supported_subresource']: + assert_valid_artifact( + spec_json['source_context_schema']['supported_subresource'], + source_context, leaf_values(test_expansion_schema['subresource'])) + + # Validate subresource_schema. + details['object'] = spec_json['subresource_schema'] + assert_contains_only_fields(spec_json['subresource_schema'], + ['supported_delivery_type']) + assert_contains_only_fields( + spec_json['subresource_schema']['supported_delivery_type'], + leaf_values(test_expansion_schema['subresource'])) + for subresource in spec_json['subresource_schema'][ + 'supported_delivery_type']: + assert_valid_artifact( + spec_json['subresource_schema']['supported_delivery_type'], + subresource, test_expansion_schema['delivery_type']) + + # Validate the test_expansion schema members. + details['object'] = test_expansion_schema + assert_contains_only_fields(test_expansion_schema, [ + 'expansion', 'source_scheme', 'source_context_list', 'delivery_type', + 'delivery_value', 'redirection', 'subresource', 'origin', 'expectation' + ]) + assert_atom_or_list_items_from(test_expansion_schema, 'expansion', + ['default', 'override']) + assert_atom_or_list_items_from(test_expansion_schema, 'source_scheme', + ['http', 'https']) + assert_atom_or_list_items_from( + test_expansion_schema, 'source_context_list', + spec_json['source_context_list_schema'].keys()) + + # Should be consistent with `preprocess_redirection` in + # `/common/security-features/subresource/subresource.py`. + assert_atom_or_list_items_from(test_expansion_schema, 'redirection', [ + 'no-redirect', 'keep-origin', 'swap-origin', 'keep-scheme', + 'swap-scheme', 'downgrade' + ]) + for subresource in leaf_values(test_expansion_schema['subresource']): + assert subresource in valid_subresource_names, "Invalid subresource %s" % subresource + # Should be consistent with getSubresourceOrigin() in + # `/common/security-features/resources/common.sub.js`. + assert_atom_or_list_items_from(test_expansion_schema, 'origin', [ + 'same-http', 'same-https', 'same-ws', 'same-wss', 'cross-http', + 'cross-https', 'cross-ws', 'cross-wss', 'same-http-downgrade', + 'cross-http-downgrade', 'same-ws-downgrade', 'cross-ws-downgrade' + ]) + + # Validate excluded tests. + details['object'] = excluded_tests + for excluded_test_expansion in excluded_tests: + assert_contains_only_fields(excluded_test_expansion, + valid_test_expansion_fields) + details['object'] = excluded_test_expansion + for artifact in test_expansion_schema: + details['test_expansion_field'] = artifact + assert_valid_artifact(excluded_test_expansion, artifact, + test_expansion_schema[artifact]) + del details['test_expansion_field'] + + del details['object'] + + +def assert_valid_spec_json(spec_json): + error_details = {} + try: + validate(spec_json, error_details) + except AssertionError as err: + print('ERROR:', err.message) + print(json.dumps(error_details, indent=4)) + sys.exit(1) + + +def main(): + spec_json = load_spec_json() + assert_valid_spec_json(spec_json) + print("Spec JSON is valid.") + + +if __name__ == '__main__': + main() diff --git a/test/wpt/tests/common/security-features/tools/template/disclaimer.template b/test/wpt/tests/common/security-features/tools/template/disclaimer.template new file mode 100644 index 00000000000..ba9458cb312 --- /dev/null +++ b/test/wpt/tests/common/security-features/tools/template/disclaimer.template @@ -0,0 +1 @@ + diff --git a/test/wpt/tests/common/security-features/tools/template/spec_json.js.template b/test/wpt/tests/common/security-features/tools/template/spec_json.js.template new file mode 100644 index 00000000000..e4cbd034259 --- /dev/null +++ b/test/wpt/tests/common/security-features/tools/template/spec_json.js.template @@ -0,0 +1 @@ +var SPEC_JSON = %(spec_json)s; diff --git a/test/wpt/tests/common/security-features/tools/template/test.debug.html.template b/test/wpt/tests/common/security-features/tools/template/test.debug.html.template new file mode 100644 index 00000000000..b6be088f611 --- /dev/null +++ b/test/wpt/tests/common/security-features/tools/template/test.debug.html.template @@ -0,0 +1,26 @@ + +%(generated_disclaimer)s + + + + %(meta_delivery_method)s + + + + + + + +%(helper_js)s + + +
+ + diff --git a/test/wpt/tests/common/security-features/tools/template/test.release.html.template b/test/wpt/tests/common/security-features/tools/template/test.release.html.template new file mode 100644 index 00000000000..bac2d5b5a4d --- /dev/null +++ b/test/wpt/tests/common/security-features/tools/template/test.release.html.template @@ -0,0 +1,22 @@ + +%(generated_disclaimer)s + + + + %(meta_delivery_method)s + + + +%(helper_js)s + + +
+ + diff --git a/test/wpt/tests/common/security-features/tools/util.py b/test/wpt/tests/common/security-features/tools/util.py new file mode 100644 index 00000000000..72541c78142 --- /dev/null +++ b/test/wpt/tests/common/security-features/tools/util.py @@ -0,0 +1,230 @@ +from __future__ import print_function + +import os, sys, json, json5, re +import collections + +script_directory = os.path.dirname(os.path.abspath(__file__)) +template_directory = os.path.abspath( + os.path.join(script_directory, 'template')) +test_root_directory = os.path.abspath( + os.path.join(script_directory, '..', '..', '..')) + + +def get_template(basename): + with open(os.path.join(template_directory, basename), "r") as f: + return f.read() + + +def write_file(filename, contents): + with open(filename, "w") as f: + f.write(contents) + + +def read_nth_line(fp, line_number): + fp.seek(0) + for i, line in enumerate(fp): + if (i + 1) == line_number: + return line + + +def load_spec_json(path_to_spec): + re_error_location = re.compile('line ([0-9]+) column ([0-9]+)') + with open(path_to_spec, "r") as f: + try: + return json5.load(f, object_pairs_hook=collections.OrderedDict) + except ValueError as ex: + print(ex.message) + match = re_error_location.search(ex.message) + if match: + line_number, column = int(match.group(1)), int(match.group(2)) + print(read_nth_line(f, line_number).rstrip()) + print(" " * (column - 1) + "^") + sys.exit(1) + + +class ShouldSkip(Exception): + ''' + Raised when the given combination of subresource type, source context type, + delivery type etc. are not supported and we should skip that configuration. + ShouldSkip is expected in normal generator execution (and thus subsequent + generation continues), as we first enumerate a broad range of configurations + first, and later raise ShouldSkip to filter out unsupported combinations. + + ShouldSkip is distinguished from other general errors that cause immediate + termination of the generator and require fix. + ''' + def __init__(self): + pass + + +class PolicyDelivery(object): + ''' + See `@typedef PolicyDelivery` comments in + `common/security-features/resources/common.sub.js`. + ''' + + def __init__(self, delivery_type, key, value): + self.delivery_type = delivery_type + self.key = key + self.value = value + + def __eq__(self, other): + return type(self) is type(other) and self.__dict__ == other.__dict__ + + @classmethod + def list_from_json(cls, list, target_policy_delivery, + supported_delivery_types): + # type: (dict, PolicyDelivery, typing.List[str]) -> typing.List[PolicyDelivery] + ''' + Parses a JSON object `list` that represents a list of `PolicyDelivery` + and returns a list of `PolicyDelivery`, plus supporting placeholders + (see `from_json()` comments below or + `common/security-features/README.md`). + + Can raise `ShouldSkip`. + ''' + if list is None: + return [] + + out = [] + for obj in list: + policy_delivery = PolicyDelivery.from_json( + obj, target_policy_delivery, supported_delivery_types) + # Drop entries with null values. + if policy_delivery.value is None: + continue + out.append(policy_delivery) + return out + + @classmethod + def from_json(cls, obj, target_policy_delivery, supported_delivery_types): + # type: (dict, PolicyDelivery, typing.List[str]) -> PolicyDelivery + ''' + Parses a JSON object `obj` and returns a `PolicyDelivery` object. + In addition to dicts (in the same format as to_json() outputs), + this method accepts the following placeholders: + "policy": + `target_policy_delivery` + "policyIfNonNull": + `target_policy_delivery` if its value is not None. + "anotherPolicy": + A PolicyDelivery that has the same key as + `target_policy_delivery` but a different value. + The delivery type is selected from `supported_delivery_types`. + + Can raise `ShouldSkip`. + ''' + + if obj == "policy": + policy_delivery = target_policy_delivery + elif obj == "nonNullPolicy": + if target_policy_delivery.value is None: + raise ShouldSkip() + policy_delivery = target_policy_delivery + elif obj == "anotherPolicy": + if len(supported_delivery_types) == 0: + raise ShouldSkip() + policy_delivery = target_policy_delivery.get_another_policy( + supported_delivery_types[0]) + elif isinstance(obj, dict): + policy_delivery = PolicyDelivery(obj['deliveryType'], obj['key'], + obj['value']) + else: + raise Exception('policy delivery is invalid: ' + obj) + + # Omit unsupported combinations of source contexts and delivery type. + if policy_delivery.delivery_type not in supported_delivery_types: + raise ShouldSkip() + + return policy_delivery + + def to_json(self): + # type: () -> dict + return { + "deliveryType": self.delivery_type, + "key": self.key, + "value": self.value + } + + def get_another_policy(self, delivery_type): + # type: (str) -> PolicyDelivery + if self.key == 'referrerPolicy': + # Return 'unsafe-url' (i.e. more unsafe policy than `self.value`) + # as long as possible, to make sure the tests to fail if the + # returned policy is used unexpectedly instead of `self.value`. + # Using safer policy wouldn't be distinguishable from acceptable + # arbitrary policy enforcement by user agents, as specified at + # Step 7 of + # https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer: + # "The user agent MAY alter referrerURL or referrerOrigin at this + # point to enforce arbitrary policy considerations in the + # interests of minimizing data leakage." + # See also the comments at `referrerUrlResolver` in + # `wpt/referrer-policy/generic/test-case.sub.js`. + if self.value != 'unsafe-url': + return PolicyDelivery(delivery_type, self.key, 'unsafe-url') + else: + return PolicyDelivery(delivery_type, self.key, 'no-referrer') + elif self.key == 'mixedContent': + if self.value == 'opt-in': + return PolicyDelivery(delivery_type, self.key, None) + else: + return PolicyDelivery(delivery_type, self.key, 'opt-in') + elif self.key == 'contentSecurityPolicy': + if self.value is not None: + return PolicyDelivery(delivery_type, self.key, None) + else: + return PolicyDelivery(delivery_type, self.key, 'worker-src-none') + elif self.key == 'upgradeInsecureRequests': + if self.value == 'upgrade': + return PolicyDelivery(delivery_type, self.key, None) + else: + return PolicyDelivery(delivery_type, self.key, 'upgrade') + else: + raise Exception('delivery key is invalid: ' + self.key) + + +class SourceContext(object): + def __init__(self, source_context_type, policy_deliveries): + # type: (unicode, typing.List[PolicyDelivery]) -> None + self.source_context_type = source_context_type + self.policy_deliveries = policy_deliveries + + def __eq__(self, other): + return type(self) is type(other) and self.__dict__ == other.__dict__ + + @classmethod + def from_json(cls, obj, target_policy_delivery, source_context_schema): + ''' + Parses a JSON object `obj` and returns a `SourceContext` object. + + `target_policy_delivery` and `source_context_schema` are used for + policy delivery placeholders and filtering out unsupported + delivery types. + + Can raise `ShouldSkip`. + ''' + source_context_type = obj.get('sourceContextType') + policy_deliveries = PolicyDelivery.list_from_json( + obj.get('policyDeliveries'), target_policy_delivery, + source_context_schema['supported_delivery_type'] + [source_context_type]) + return SourceContext(source_context_type, policy_deliveries) + + def to_json(self): + return { + "sourceContextType": self.source_context_type, + "policyDeliveries": [x.to_json() for x in self.policy_deliveries] + } + + +class CustomEncoder(json.JSONEncoder): + ''' + Used to dump dicts containing `SourceContext`/`PolicyDelivery` into JSON. + ''' + def default(self, obj): + if isinstance(obj, SourceContext): + return obj.to_json() + if isinstance(obj, PolicyDelivery): + return obj.to_json() + return json.JSONEncoder.default(self, obj) diff --git a/test/wpt/tests/common/security-features/types.md b/test/wpt/tests/common/security-features/types.md new file mode 100644 index 00000000000..17079916c1e --- /dev/null +++ b/test/wpt/tests/common/security-features/types.md @@ -0,0 +1,62 @@ +# Types around the generator and generated tests + +This document describes types and concepts used across JavaScript and Python parts of this test framework. +Please refer to the JSDoc in `common.sub.js` or docstrings in Python scripts (if any). + +## Scenario + +### Properties + +- All keys of `test_expansion_schema` in `spec.src.json`, except for `expansion`, `delivery_type`, `delivery_value`, and `source_context_list`. Their values are **string**s specified in `test_expansion_schema`. +- `source_context_list` +- `subresource_policy_deliveries` + +### Types + +- Generator (`spec.src.json`): JSON object +- Generator (Python): `dict` +- Runtime (JS): JSON object +- Runtime (Python): N/A + +## `PolicyDelivery` + +### Types + +- Generator (`spec.src.json`): JSON object +- Generator (Python): `util.PolicyDelivery` +- Runtime (JS): JSON object (`@typedef PolicyDelivery` in `common.sub.js`) +- Runtime (Python): N/A + +## `SourceContext` + +Subresource requests can be possibly sent from various kinds of fetch client's environment settings objects. For example: + +- top-level windows, +- ` diff --git a/test/wpt/tests/fetch/api/abort/general.any.js b/test/wpt/tests/fetch/api/abort/general.any.js index 7bf98ba9b24..e9e8e93d30f 100644 --- a/test/wpt/tests/fetch/api/abort/general.any.js +++ b/test/wpt/tests/fetch/api/abort/general.any.js @@ -519,7 +519,6 @@ promise_test(async t => { const fetchPromise = fetch('../resources/empty.txt', { body, signal, method: 'POST', - duplex: 'half', headers: { 'Content-Type': 'text/plain' } diff --git a/test/wpt/tests/fetch/api/abort/keepalive.html b/test/wpt/tests/fetch/api/abort/keepalive.html new file mode 100644 index 00000000000..db12df0d289 --- /dev/null +++ b/test/wpt/tests/fetch/api/abort/keepalive.html @@ -0,0 +1,85 @@ + + + + + + + + diff --git a/test/wpt/tests/fetch/api/abort/serviceworker-intercepted.https.html b/test/wpt/tests/fetch/api/abort/serviceworker-intercepted.https.html new file mode 100644 index 00000000000..ed9bc973e80 --- /dev/null +++ b/test/wpt/tests/fetch/api/abort/serviceworker-intercepted.https.html @@ -0,0 +1,212 @@ + + + + + Aborting fetch when intercepted by a service worker + + + + + + + + diff --git a/test/wpt/tests/fetch/api/basic/block-mime-as-script.html b/test/wpt/tests/fetch/api/basic/block-mime-as-script.html new file mode 100644 index 00000000000..afc2bbbafb0 --- /dev/null +++ b/test/wpt/tests/fetch/api/basic/block-mime-as-script.html @@ -0,0 +1,43 @@ + + +Block mime type as script + + +
+ diff --git a/test/wpt/tests/fetch/api/basic/conditional-get.any.js b/test/wpt/tests/fetch/api/basic/conditional-get.any.js new file mode 100644 index 00000000000..2f9fa81c02b --- /dev/null +++ b/test/wpt/tests/fetch/api/basic/conditional-get.any.js @@ -0,0 +1,38 @@ +// META: title=Request ETag +// META: global=window,worker +// META: script=/common/utils.js + +promise_test(function() { + var cacheBuster = token(); // ensures first request is uncached + var url = "../resources/cache.py?v=" + cacheBuster; + var etag; + + // make the first request + return fetch(url).then(function(response) { + // ensure we're getting the regular, uncached response + assert_equals(response.status, 200); + assert_equals(response.headers.get("X-HTTP-STATUS"), null) + + return response.text(); // consuming the body, just to be safe + }).then(function(body) { + // make a second request + return fetch(url); + }).then(function(response) { + // while the server responds with 304 if our browser sent the correct + // If-None-Match request header, at the JavaScript level this surfaces + // as 200 + assert_equals(response.status, 200); + assert_equals(response.headers.get("X-HTTP-STATUS"), "304") + + etag = response.headers.get("ETag") + + return response.text(); // consuming the body, just to be safe + }).then(function(body) { + // make a third request, explicitly setting If-None-Match request header + var headers = { "If-None-Match": etag } + return fetch(url, { headers: headers }) + }).then(function(response) { + // 304 now surfaces thanks to the explicit If-None-Match request header + assert_equals(response.status, 304); + }); +}, "Testing conditional GET with ETags"); diff --git a/test/wpt/tests/fetch/api/basic/keepalive.html b/test/wpt/tests/fetch/api/basic/keepalive.html new file mode 100644 index 00000000000..36d156bba43 --- /dev/null +++ b/test/wpt/tests/fetch/api/basic/keepalive.html @@ -0,0 +1,106 @@ + + + +Fetch API: keepalive handling + + + + + + + diff --git a/test/wpt/tests/fetch/api/basic/mediasource.window.js b/test/wpt/tests/fetch/api/basic/mediasource.window.js new file mode 100644 index 00000000000..1f89595393d --- /dev/null +++ b/test/wpt/tests/fetch/api/basic/mediasource.window.js @@ -0,0 +1,5 @@ +promise_test(t => { + const mediaSource = new MediaSource(), + mediaSourceURL = URL.createObjectURL(mediaSource); + return promise_rejects_js(t, TypeError, fetch(mediaSourceURL)); +}, "Cannot fetch blob: URL from a MediaSource"); diff --git a/test/wpt/tests/fetch/api/basic/mode-no-cors.sub.any.js b/test/wpt/tests/fetch/api/basic/mode-no-cors.sub.any.js new file mode 100644 index 00000000000..a4abcac55f3 --- /dev/null +++ b/test/wpt/tests/fetch/api/basic/mode-no-cors.sub.any.js @@ -0,0 +1,29 @@ +// META: script=../resources/utils.js + +function fetchNoCors(url, isOpaqueFiltered) { + var urlQuery = "?pipe=header(x-is-filtered,value)" + promise_test(function(test) { + if (isOpaqueFiltered) + return fetch(url + urlQuery, {"mode": "no-cors"}).then(function(resp) { + assert_equals(resp.status, 0, "Opaque filter: status is 0"); + assert_equals(resp.statusText, "", "Opaque filter: statusText is \"\""); + assert_equals(resp.url, "", "Opaque filter: url is \"\""); + assert_equals(resp.type , "opaque", "Opaque filter: response's type is opaque"); + assert_equals(resp.headers.get("x-is-filtered"), null, "Header x-is-filtered is filtered"); + }); + else + return fetch(url + urlQuery, {"mode": "no-cors"}).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.type , "basic", "Response's type is basic"); + assert_equals(resp.headers.get("x-is-filtered"), "value", "Header x-is-filtered is not filtered"); + }); + }, "Fetch "+ url + " with no-cors mode"); +} + +fetchNoCors(RESOURCES_DIR + "top.txt", false); +fetchNoCors("http://{{host}}:{{ports[http][0]}}/fetch/api/resources/top.txt", false); +fetchNoCors("https://{{host}}:{{ports[https][0]}}/fetch/api/resources/top.txt", true); +fetchNoCors("http://{{host}}:{{ports[http][1]}}/fetch/api/resources/top.txt", true); + +done(); + diff --git a/test/wpt/tests/fetch/api/basic/mode-same-origin.any.js b/test/wpt/tests/fetch/api/basic/mode-same-origin.any.js new file mode 100644 index 00000000000..1457702f1b1 --- /dev/null +++ b/test/wpt/tests/fetch/api/basic/mode-same-origin.any.js @@ -0,0 +1,28 @@ +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +function fetchSameOrigin(url, shouldPass) { + promise_test(function(test) { + if (shouldPass) + return fetch(url , {"mode": "same-origin"}).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.type, "basic", "response type is basic"); + }); + else + return promise_rejects_js(test, TypeError, fetch(url, {mode: "same-origin"})); + }, "Fetch "+ url + " with same-origin mode"); +} + +var host_info = get_host_info(); + +fetchSameOrigin(RESOURCES_DIR + "top.txt", true); +fetchSameOrigin(host_info.HTTP_ORIGIN + "/fetch/api/resources/top.txt", true); +fetchSameOrigin(host_info.HTTPS_ORIGIN + "/fetch/api/resources/top.txt", false); +fetchSameOrigin(host_info.HTTP_REMOTE_ORIGIN + "/fetch/api/resources/top.txt", false); + +var redirPath = dirname(location.pathname) + RESOURCES_DIR + "redirect.py?location="; + +fetchSameOrigin(redirPath + RESOURCES_DIR + "top.txt", true); +fetchSameOrigin(redirPath + host_info.HTTP_ORIGIN + "/fetch/api/resources/top.txt", true); +fetchSameOrigin(redirPath + host_info.HTTPS_ORIGIN + "/fetch/api/resources/top.txt", false); +fetchSameOrigin(redirPath + host_info.HTTP_REMOTE_ORIGIN + "/fetch/api/resources/top.txt", false); diff --git a/test/wpt/tests/fetch/api/basic/referrer.any.js b/test/wpt/tests/fetch/api/basic/referrer.any.js new file mode 100644 index 00000000000..85745e692a2 --- /dev/null +++ b/test/wpt/tests/fetch/api/basic/referrer.any.js @@ -0,0 +1,29 @@ +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +function runTest(url, init, expectedReferrer, title) { + promise_test(function(test) { + url += (url.indexOf('?') !== -1 ? '&' : '?') + "headers=referer&cors"; + + return fetch(url , init).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.headers.get("x-request-referer"), expectedReferrer, "Request's referrer is correct"); + }); + }, title); +} + +var fetchedUrl = RESOURCES_DIR + "inspect-headers.py"; +var corsFetchedUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py"; +var redirectUrl = RESOURCES_DIR + "redirect.py?location=" ; +var corsRedirectUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "redirect.py?location="; + +runTest(fetchedUrl, { referrerPolicy: "origin-when-cross-origin"}, location.toString(), "origin-when-cross-origin policy on a same-origin URL"); +runTest(corsFetchedUrl, { referrerPolicy: "origin-when-cross-origin"}, get_host_info().HTTP_ORIGIN + "/", "origin-when-cross-origin policy on a cross-origin URL"); +runTest(redirectUrl + corsFetchedUrl, { referrerPolicy: "origin-when-cross-origin"}, get_host_info().HTTP_ORIGIN + "/", "origin-when-cross-origin policy on a cross-origin URL after same-origin redirection"); +runTest(corsRedirectUrl + fetchedUrl, { referrerPolicy: "origin-when-cross-origin"}, get_host_info().HTTP_ORIGIN + "/", "origin-when-cross-origin policy on a same-origin URL after cross-origin redirection"); + + +var referrerUrlWithCredentials = get_host_info().HTTP_ORIGIN.replace("http://", "http://username:password@"); +runTest(fetchedUrl, {referrer: referrerUrlWithCredentials}, get_host_info().HTTP_ORIGIN + "/", "Referrer with credentials should be stripped"); +var referrerUrlWithFragmentIdentifier = get_host_info().HTTP_ORIGIN + "#fragmentIdentifier"; +runTest(fetchedUrl, {referrer: referrerUrlWithFragmentIdentifier}, get_host_info().HTTP_ORIGIN + "/", "Referrer with fragment ID should be stripped"); diff --git a/test/wpt/tests/fetch/api/basic/request-forbidden-headers.any.js b/test/wpt/tests/fetch/api/basic/request-forbidden-headers.any.js new file mode 100644 index 00000000000..511ce601e7c --- /dev/null +++ b/test/wpt/tests/fetch/api/basic/request-forbidden-headers.any.js @@ -0,0 +1,100 @@ +// META: global=window,worker +// META: script=../resources/utils.js + +function requestForbiddenHeaders(desc, forbiddenHeaders) { + var url = RESOURCES_DIR + "inspect-headers.py"; + var requestInit = {"headers": forbiddenHeaders} + var urlParameters = "?headers=" + Object.keys(forbiddenHeaders).join("|"); + + promise_test(function(test){ + return fetch(url + urlParameters, requestInit).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.type , "basic", "Response's type is basic"); + for (var header in forbiddenHeaders) + assert_not_equals(resp.headers.get("x-request-" + header), forbiddenHeaders[header], header + " does not have the value we defined"); + }); + }, desc); +} + +function requestValidOverrideHeaders(desc, validHeaders) { + var url = RESOURCES_DIR + "inspect-headers.py"; + var requestInit = {"headers": validHeaders} + var urlParameters = "?headers=" + Object.keys(validHeaders).join("|"); + + promise_test(function(test){ + return fetch(url + urlParameters, requestInit).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.type , "basic", "Response's type is basic"); + for (var header in validHeaders) + assert_equals(resp.headers.get("x-request-" + header), validHeaders[header], header + "is not skipped for non-forbidden methods"); + }); + }, desc); +} + +requestForbiddenHeaders("Accept-Charset is a forbidden request header", {"Accept-Charset": "utf-8"}); +requestForbiddenHeaders("Accept-Encoding is a forbidden request header", {"Accept-Encoding": ""}); + +requestForbiddenHeaders("Access-Control-Request-Headers is a forbidden request header", {"Access-Control-Request-Headers": ""}); +requestForbiddenHeaders("Access-Control-Request-Method is a forbidden request header", {"Access-Control-Request-Method": ""}); +requestForbiddenHeaders( + 'Access-Control-Request-Private-Network is a forbidden request header', + {'Access-Control-Request-Private-Network': ''}); +requestForbiddenHeaders("Connection is a forbidden request header", {"Connection": "close"}); +requestForbiddenHeaders("Content-Length is a forbidden request header", {"Content-Length": "42"}); +requestForbiddenHeaders("Cookie is a forbidden request header", {"Cookie": "cookie=none"}); +requestForbiddenHeaders("Cookie2 is a forbidden request header", {"Cookie2": "cookie2=none"}); +requestForbiddenHeaders("Date is a forbidden request header", {"Date": "Wed, 04 May 1988 22:22:22 GMT"}); +requestForbiddenHeaders("DNT is a forbidden request header", {"DNT": "4"}); +requestForbiddenHeaders("Expect is a forbidden request header", {"Expect": "100-continue"}); +requestForbiddenHeaders("Host is a forbidden request header", {"Host": "http://wrong-host.com"}); +requestForbiddenHeaders("Keep-Alive is a forbidden request header", {"Keep-Alive": "timeout=15"}); +requestForbiddenHeaders("Origin is a forbidden request header", {"Origin": "http://wrong-origin.com"}); +requestForbiddenHeaders("Referer is a forbidden request header", {"Referer": "http://wrong-referer.com"}); +requestForbiddenHeaders("TE is a forbidden request header", {"TE": "trailers"}); +requestForbiddenHeaders("Trailer is a forbidden request header", {"Trailer": "Accept"}); +requestForbiddenHeaders("Transfer-Encoding is a forbidden request header", {"Transfer-Encoding": "chunked"}); +requestForbiddenHeaders("Upgrade is a forbidden request header", {"Upgrade": "HTTP/2.0"}); +requestForbiddenHeaders("Via is a forbidden request header", {"Via": "1.1 nowhere.com"}); +requestForbiddenHeaders("Proxy- is a forbidden request header", {"Proxy-": "value"}); +requestForbiddenHeaders("Proxy-Test is a forbidden request header", {"Proxy-Test": "value"}); +requestForbiddenHeaders("Sec- is a forbidden request header", {"Sec-": "value"}); +requestForbiddenHeaders("Sec-Test is a forbidden request header", {"Sec-Test": "value"}); + +let forbiddenMethods = [ + "TRACE", + "TRACK", + "CONNECT", + "trace", + "track", + "connect", + "trace,", + "GET,track ", + " connect", +]; + +let overrideHeaders = [ + "x-http-method-override", + "x-http-method", + "x-method-override", + "X-HTTP-METHOD-OVERRIDE", + "X-HTTP-METHOD", + "X-METHOD-OVERRIDE", +]; + +for (forbiddenMethod of forbiddenMethods) { + for (overrideHeader of overrideHeaders) { + requestForbiddenHeaders(`header ${overrideHeader} is forbidden to use value ${forbiddenMethod}`, {[overrideHeader]: forbiddenMethod}); + } +} + +let permittedValues = [ + "GETTRACE", + "GET", + "\",TRACE\",", +]; + +for (permittedValue of permittedValues) { + for (overrideHeader of overrideHeaders) { + requestValidOverrideHeaders(`header ${overrideHeader} is allowed to use value ${permittedValue}`, {[overrideHeader]: permittedValue}); + } +} diff --git a/test/wpt/tests/fetch/api/basic/request-headers.any.js b/test/wpt/tests/fetch/api/basic/request-headers.any.js new file mode 100644 index 00000000000..ac54256e4c6 --- /dev/null +++ b/test/wpt/tests/fetch/api/basic/request-headers.any.js @@ -0,0 +1,82 @@ +// META: global=window,worker +// META: script=../resources/utils.js + +function checkContentType(contentType, body) +{ + if (self.FormData && body instanceof self.FormData) { + assert_true(contentType.startsWith("multipart/form-data; boundary="), "Request should have header content-type starting with multipart/form-data; boundary=, but got " + contentType); + return; + } + + var expectedContentType = "text/plain;charset=UTF-8"; + if(body === null || body instanceof ArrayBuffer || body.buffer instanceof ArrayBuffer) + expectedContentType = null; + else if (body instanceof Blob) + expectedContentType = body.type ? body.type : null; + else if (body instanceof URLSearchParams) + expectedContentType = "application/x-www-form-urlencoded;charset=UTF-8"; + + assert_equals(contentType , expectedContentType, "Request should have header content-type: " + expectedContentType); +} + +function requestHeaders(desc, url, method, body, expectedOrigin, expectedContentLength) { + var urlParameters = "?headers=origin|user-agent|accept-charset|content-length|content-type"; + var requestInit = {"method": method} + promise_test(function(test){ + if (typeof body === "function") + body = body(); + if (body) + requestInit["body"] = body; + return fetch(url + urlParameters, requestInit).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.type , "basic", "Response's type is basic"); + assert_true(resp.headers.has("x-request-user-agent"), "Request has header user-agent"); + assert_false(resp.headers.has("accept-charset"), "Request has header accept-charset"); + assert_equals(resp.headers.get("x-request-origin") , expectedOrigin, "Request should have header origin: " + expectedOrigin); + if (expectedContentLength !== undefined) + assert_equals(resp.headers.get("x-request-content-length") , expectedContentLength, "Request should have header content-length: " + expectedContentLength); + checkContentType(resp.headers.get("x-request-content-type"), body); + }); + }, desc); +} + +var url = RESOURCES_DIR + "inspect-headers.py" + +requestHeaders("Fetch with GET", url, "GET", null, null, null); +requestHeaders("Fetch with HEAD", url, "HEAD", null, null, null); +requestHeaders("Fetch with PUT without body", url, "POST", null, location.origin, "0"); +requestHeaders("Fetch with PUT with body", url, "PUT", "Request's body", location.origin, "14"); +requestHeaders("Fetch with POST without body", url, "POST", null, location.origin, "0"); +requestHeaders("Fetch with POST with text body", url, "POST", "Request's body", location.origin, "14"); +requestHeaders("Fetch with POST with FormData body", url, "POST", function() { return new FormData(); }, location.origin); +requestHeaders("Fetch with POST with URLSearchParams body", url, "POST", function() { return new URLSearchParams("name=value"); }, location.origin, "10"); +requestHeaders("Fetch with POST with Blob body", url, "POST", new Blob(["Test"]), location.origin, "4"); +requestHeaders("Fetch with POST with ArrayBuffer body", url, "POST", new ArrayBuffer(4), location.origin, "4"); +requestHeaders("Fetch with POST with Uint8Array body", url, "POST", new Uint8Array(4), location.origin, "4"); +requestHeaders("Fetch with POST with Int8Array body", url, "POST", new Int8Array(4), location.origin, "4"); +requestHeaders("Fetch with POST with Float32Array body", url, "POST", new Float32Array(1), location.origin, "4"); +requestHeaders("Fetch with POST with Float64Array body", url, "POST", new Float64Array(1), location.origin, "8"); +requestHeaders("Fetch with POST with DataView body", url, "POST", new DataView(new ArrayBuffer(8), 0, 4), location.origin, "4"); +requestHeaders("Fetch with POST with Blob body with mime type", url, "POST", new Blob(["Test"], { type: "text/maybe" }), location.origin, "4"); +requestHeaders("Fetch with Chicken", url, "Chicken", null, location.origin, null); +requestHeaders("Fetch with Chicken with body", url, "Chicken", "Request's body", location.origin, "14"); + +function requestOriginHeader(method, mode, needsOrigin) { + promise_test(function(test){ + return fetch(url + "?headers=origin", {method:method, mode:mode}).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.type , "basic", "Response's type is basic"); + if(needsOrigin) + assert_equals(resp.headers.get("x-request-origin") , location.origin, "Request should have an Origin header with origin: " + location.origin); + else + assert_equals(resp.headers.get("x-request-origin"), null, "Request should not have an Origin header") + }); + }, "Fetch with " + method + " and mode \"" + mode + "\" " + (needsOrigin ? "needs" : "does not need") + " an Origin header"); +} + +requestOriginHeader("GET", "cors", false); +requestOriginHeader("POST", "same-origin", true); +requestOriginHeader("POST", "no-cors", true); +requestOriginHeader("PUT", "same-origin", true); +requestOriginHeader("TacO", "same-origin", true); +requestOriginHeader("TacO", "cors", true); diff --git a/test/wpt/tests/fetch/api/basic/request-referrer-redirected-worker.html b/test/wpt/tests/fetch/api/basic/request-referrer-redirected-worker.html new file mode 100644 index 00000000000..bdea1e18531 --- /dev/null +++ b/test/wpt/tests/fetch/api/basic/request-referrer-redirected-worker.html @@ -0,0 +1,17 @@ + + + + + Fetch in worker: referrer header + + + + + + + diff --git a/test/wpt/tests/fetch/api/basic/request-referrer.any.js b/test/wpt/tests/fetch/api/basic/request-referrer.any.js new file mode 100644 index 00000000000..0c3357642d6 --- /dev/null +++ b/test/wpt/tests/fetch/api/basic/request-referrer.any.js @@ -0,0 +1,24 @@ +// META: global=window,worker +// META: script=../resources/utils.js + +function testReferrer(referrer, expected, desc) { + promise_test(function(test) { + var url = RESOURCES_DIR + "inspect-headers.py?headers=referer" + var req = new Request(url, { referrer: referrer }); + return fetch(req).then(function(resp) { + var actual = resp.headers.get("x-request-referer"); + if (expected) { + assert_equals(actual, expected, "request's referer should be: " + expected); + return; + } + if (actual) { + assert_equals(actual, "", "request's referer should be empty"); + } + }); + }, desc); +} + +testReferrer("about:client", self.location.href, 'about:client referrer'); + +var fooURL = new URL("./foo", self.location).href; +testReferrer(fooURL, fooURL, 'url referrer'); diff --git a/test/wpt/tests/fetch/api/basic/request-upload.h2.any.js b/test/wpt/tests/fetch/api/basic/request-upload.h2.any.js new file mode 100644 index 00000000000..eedc2bf6a76 --- /dev/null +++ b/test/wpt/tests/fetch/api/basic/request-upload.h2.any.js @@ -0,0 +1,186 @@ +// META: global=window,worker +// META: script=../resources/utils.js +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js + +const duplex = "half"; + +async function assertUpload(url, method, createBody, expectedBody) { + const requestInit = {method}; + const body = createBody(); + if (body) { + requestInit["body"] = body; + requestInit.duplex = "half"; + } + const resp = await fetch(url, requestInit); + const text = await resp.text(); + assert_equals(text, expectedBody); +} + +function testUpload(desc, url, method, createBody, expectedBody) { + promise_test(async () => { + await assertUpload(url, method, createBody, expectedBody); + }, desc); +} + +function createStream(chunks) { + return new ReadableStream({ + start: (controller) => { + for (const chunk of chunks) { + controller.enqueue(chunk); + } + controller.close(); + } + }); +} + +const url = RESOURCES_DIR + "echo-content.h2.py" + +testUpload("Fetch with POST with empty ReadableStream", url, + "POST", + () => { + return new ReadableStream({start: controller => { + controller.close(); + }}) + }, + ""); + +testUpload("Fetch with POST with ReadableStream", url, + "POST", + () => { + return new ReadableStream({start: controller => { + const encoder = new TextEncoder(); + controller.enqueue(encoder.encode("Test")); + controller.close(); + }}) + }, + "Test"); + +promise_test(async (test) => { + const body = new ReadableStream({start: controller => { + const encoder = new TextEncoder(); + controller.enqueue(encoder.encode("Test")); + controller.close(); + }}); + const resp = await fetch( + "/fetch/connection-pool/resources/network-partition-key.py?" + + `status=421&uuid=${token()}&partition_id=${self.origin}` + + `&dispatch=check_partition&addcounter=true`, + {method: "POST", body: body, duplex}); + assert_equals(resp.status, 421); + const text = await resp.text(); + assert_equals(text, "ok. Request was sent 1 times. 1 connections were created."); +}, "Fetch with POST with ReadableStream on 421 response should return the response and not retry."); + +promise_test(async (test) => { + const request = new Request('', { + body: new ReadableStream(), + method: 'POST', + duplex, + }); + + assert_equals(request.headers.get('Content-Type'), null, `Request should not have a content-type set`); + + const response = await fetch('data:a/a;charset=utf-8,test', { + method: 'POST', + body: new ReadableStream(), + duplex, + }); + + assert_equals(await response.text(), 'test', `Response has correct body`); +}, "Feature detect for POST with ReadableStream"); + +promise_test(async (test) => { + const request = new Request('data:a/a;charset=utf-8,test', { + body: new ReadableStream(), + method: 'POST', + duplex, + }); + + assert_equals(request.headers.get('Content-Type'), null, `Request should not have a content-type set`); + const response = await fetch(request); + assert_equals(await response.text(), 'test', `Response has correct body`); +}, "Feature detect for POST with ReadableStream, using request object"); + +test(() => { + let duplexAccessed = false; + + const request = new Request("", { + body: new ReadableStream(), + method: "POST", + get duplex() { + duplexAccessed = true; + return "half"; + }, + }); + + assert_equals( + request.headers.get("Content-Type"), + null, + `Request should not have a content-type set` + ); + assert_true(duplexAccessed, `duplex dictionary property should be accessed`); +}, "Synchronous feature detect"); + +// The asserts the synchronousFeatureDetect isn't broken by a partial implementation. +// An earlier feature detect was broken by Safari implementing streaming bodies as part of Request, +// but it failed when passed to fetch(). +// This tests ensures that UAs must not implement RequestInit.duplex and streaming request bodies without also implementing the fetch() parts. +promise_test(async () => { + let duplexAccessed = false; + + const request = new Request("", { + body: new ReadableStream(), + method: "POST", + get duplex() { + duplexAccessed = true; + return "half"; + }, + }); + + const supported = + request.headers.get("Content-Type") === null && duplexAccessed; + + // If the feature detect fails, assume the browser is being truthful (other tests pick up broken cases here) + if (!supported) return false; + + await assertUpload( + url, + "POST", + () => + new ReadableStream({ + start: (controller) => { + const encoder = new TextEncoder(); + controller.enqueue(encoder.encode("Test")); + controller.close(); + }, + }), + "Test" + ); +}, "Synchronous feature detect fails if feature unsupported"); + +promise_test(async (t) => { + const body = createStream(["hello"]); + const method = "POST"; + await promise_rejects_js(t, TypeError, fetch(url, { method, body, duplex })); +}, "Streaming upload with body containing a String"); + +promise_test(async (t) => { + const body = createStream([null]); + const method = "POST"; + await promise_rejects_js(t, TypeError, fetch(url, { method, body, duplex })); +}, "Streaming upload with body containing null"); + +promise_test(async (t) => { + const body = createStream([33]); + const method = "POST"; + await promise_rejects_js(t, TypeError, fetch(url, { method, body, duplex })); +}, "Streaming upload with body containing a number"); + +promise_test(async (t) => { + const url = "/fetch/api/resources/authentication.py?realm=test"; + const body = createStream([]); + const method = "POST"; + await promise_rejects_js(t, TypeError, fetch(url, { method, body, duplex })); +}, "Streaming upload should fail on a 401 response"); + diff --git a/test/wpt/tests/fetch/api/basic/status.h2.any.js b/test/wpt/tests/fetch/api/basic/status.h2.any.js new file mode 100644 index 00000000000..99fec88f505 --- /dev/null +++ b/test/wpt/tests/fetch/api/basic/status.h2.any.js @@ -0,0 +1,17 @@ +// See also /xhr/status.h2.window.js + +[ + 200, + 210, + 400, + 404, + 410, + 500, + 502 +].forEach(status => { + promise_test(async t => { + const response = await fetch("/xhr/resources/status.py?code=" + status); + assert_equals(response.status, status, "status should be " + status); + assert_equals(response.statusText, "", "statusText should be the empty string"); + }, "statusText over H2 for status " + status + " should be the empty string"); +}); 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 92440f0b511..a0f90a0abdf 100644 --- a/test/wpt/tests/fetch/api/body/mime-type.any.js +++ b/test/wpt/tests/fetch/api/body/mime-type.any.js @@ -37,4 +37,4 @@ const blob = await bodyContainer.blob(); assert_equals(blob.type, newMIMEType); }, `${bodyContainer.constructor.name}: setting missing Content-Type`); -}); \ No newline at end of file +}); diff --git a/test/wpt/tests/fetch/api/cors/cors-basic.any.js b/test/wpt/tests/fetch/api/cors/cors-basic.any.js new file mode 100644 index 00000000000..23f5f91c87d --- /dev/null +++ b/test/wpt/tests/fetch/api/cors/cors-basic.any.js @@ -0,0 +1,37 @@ +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +function cors(desc, origin) { + var url = origin + dirname(location.pathname); + var urlParameters = "?pipe=header(Access-Control-Allow-Origin,*)"; + + promise_test(function(test) { + return fetch(url + RESOURCES_DIR + "top.txt" + urlParameters, {"mode": "no-cors"} ).then(function(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) { + assert_equals(value, "", "Opaque response should have an empty body"); + }); + }); + }, 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(function(test) { + return fetch(url + RESOURCES_DIR + "top.txt" + urlParameters, {"mode": "cors"} ).then(function(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]"); +} + +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); diff --git a/test/wpt/tests/fetch/api/cors/cors-cookies-redirect.any.js b/test/wpt/tests/fetch/api/cors/cors-cookies-redirect.any.js new file mode 100644 index 00000000000..f5217b42460 --- /dev/null +++ b/test/wpt/tests/fetch/api/cors/cors-cookies-redirect.any.js @@ -0,0 +1,49 @@ +// META: script=/common/utils.js +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +var redirectUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "redirect.py"; +var urlSetCookies1 = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "top.txt"; +var urlSetCookies2 = get_host_info().HTTP_ORIGIN_WITH_DIFFERENT_PORT + dirname(location.pathname) + RESOURCES_DIR + "top.txt"; +var urlCheckCookies = get_host_info().HTTP_ORIGIN_WITH_DIFFERENT_PORT + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?cors&headers=cookie"; + +var urlSetCookiesParameters = "?pipe=header(Access-Control-Allow-Origin," + location.origin + ")"; +urlSetCookiesParameters += "|header(Access-Control-Allow-Credentials,true)"; + +urlSetCookiesParameters1 = urlSetCookiesParameters + "|header(Set-Cookie,a=1)"; +urlSetCookiesParameters2 = urlSetCookiesParameters + "|header(Set-Cookie,a=2)"; + +urlClearCookiesParameters1 = urlSetCookiesParameters + "|header(Set-Cookie,a=1%3B%20max-age=0)"; +urlClearCookiesParameters2 = urlSetCookiesParameters + "|header(Set-Cookie,a=2%3B%20max-age=0)"; + +promise_test(async (test) => { + await fetch(urlSetCookies1 + urlSetCookiesParameters1, {"credentials": "include", "mode": "cors"}); + await fetch(urlSetCookies2 + urlSetCookiesParameters2, {"credentials": "include", "mode": "cors"}); +}, "Set cookies"); + +function doTest(usePreflight) { + promise_test(async (test) => { + var url = redirectUrl; + var uuid_token = token(); + var urlParameters = "?token=" + uuid_token + "&max_age=0"; + urlParameters += "&redirect_status=301"; + urlParameters += "&location=" + encodeURIComponent(urlCheckCookies); + urlParameters += "&allow_headers=a&headers=Cookie"; + headers = []; + if (usePreflight) + headers.push(["a", "b"]); + + var requestInit = {"credentials": "include", "mode": "cors", "headers": headers}; + var response = await fetch(url + urlParameters, requestInit); + + assert_equals(response.headers.get("x-request-cookie") , "a=2", "Request includes cookie(s)"); + }, "Testing credentials after cross-origin redirection with CORS and " + (usePreflight ? "" : "no ") + "preflight"); +} + +doTest(false); +doTest(true); + +promise_test(async (test) => { + await fetch(urlSetCookies1 + urlClearCookiesParameters1, {"credentials": "include", "mode": "cors"}); + await fetch(urlSetCookies2 + urlClearCookiesParameters2, {"credentials": "include", "mode": "cors"}); +}, "Clean cookies"); diff --git a/test/wpt/tests/fetch/api/cors/cors-cookies.any.js b/test/wpt/tests/fetch/api/cors/cors-cookies.any.js new file mode 100644 index 00000000000..8c666e4782f --- /dev/null +++ b/test/wpt/tests/fetch/api/cors/cors-cookies.any.js @@ -0,0 +1,56 @@ +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +function corsCookies(desc, baseURL1, baseURL2, credentialsMode, cookies) { + var urlSetCookie = baseURL1 + dirname(location.pathname) + RESOURCES_DIR + "top.txt"; + var urlCheckCookies = baseURL2 + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?cors&headers=cookie"; + //enable cors with credentials + var urlParameters = "?pipe=header(Access-Control-Allow-Origin," + location.origin + ")"; + urlParameters += "|header(Access-Control-Allow-Credentials,true)"; + + var urlCleanParameters = "?pipe=header(Access-Control-Allow-Origin," + location.origin + ")"; + urlCleanParameters += "|header(Access-Control-Allow-Credentials,true)"; + if (cookies) { + urlParameters += "|header(Set-Cookie,"; + urlParameters += cookies.join(",True)|header(Set-Cookie,") + ",True)"; + urlCleanParameters += "|header(Set-Cookie,"; + urlCleanParameters += cookies.join("%3B%20max-age=0,True)|header(Set-Cookie,") + "%3B%20max-age=0,True)"; + } + + var requestInit = {"credentials": credentialsMode, "mode": "cors"}; + + promise_test(function(test){ + return fetch(urlSetCookie + urlParameters, requestInit).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + //check cookies sent + return fetch(urlCheckCookies, requestInit); + }).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_false(resp.headers.has("Cookie") , "Cookie header is not exposed in response"); + if (credentialsMode === "include" && baseURL1 === baseURL2) { + assert_equals(resp.headers.get("x-request-cookie") , cookies.join("; "), "Request includes cookie(s)"); + } + else { + assert_false(resp.headers.has("x-request-cookie") , "Request should have no cookie"); + } + //clean cookies + return fetch(urlSetCookie + urlCleanParameters, {"credentials": "include"}); + }).catch(function(e) { + return fetch(urlSetCookie + urlCleanParameters, {"credentials": "include"}).then(function(resp) { + throw e; + }) + }); + }, desc); +} + +var local = get_host_info().HTTP_ORIGIN; +var remote = get_host_info().HTTP_REMOTE_ORIGIN; +// FIXME: otherRemote might not be accessible on some test environments. +var otherRemote = local.replace("http://", "http://www."); + +corsCookies("Omit mode: no cookie sent", local, local, "omit", ["g=7"]); +corsCookies("Include mode: 1 cookie", remote, remote, "include", ["a=1"]); +corsCookies("Include mode: local cookies are not sent with remote request", local, remote, "include", ["c=3"]); +corsCookies("Include mode: remote cookies are not sent with local request", remote, local, "include", ["d=4"]); +corsCookies("Same-origin mode: cookies are discarded in cors request", remote, remote, "same-origin", ["f=6"]); +corsCookies("Include mode: remote cookies are not sent with other remote request", remote, otherRemote, "include", ["e=5"]); diff --git a/test/wpt/tests/fetch/api/cors/cors-expose-star.sub.any.js b/test/wpt/tests/fetch/api/cors/cors-expose-star.sub.any.js new file mode 100644 index 00000000000..340e99ab5f9 --- /dev/null +++ b/test/wpt/tests/fetch/api/cors/cors-expose-star.sub.any.js @@ -0,0 +1,41 @@ +// META: script=../resources/utils.js + +const url = "http://{{host}}:{{ports[http][1]}}" + dirname(location.pathname) + RESOURCES_DIR + "top.txt", + sharedHeaders = "?pipe=header(Access-Control-Expose-Headers,*)|header(Test,X)|header(Set-Cookie,X)|header(*,whoa)|" + +promise_test(() => { + const headers = "header(Access-Control-Allow-Origin,*)" + return fetch(url + sharedHeaders + headers).then(resp => { + assert_equals(resp.status, 200) + assert_equals(resp.type , "cors") + assert_equals(resp.headers.get("test"), "X") + assert_equals(resp.headers.get("set-cookie"), null) + assert_equals(resp.headers.get("*"), "whoa") + }) +}, "Basic Access-Control-Expose-Headers: * support") + +promise_test(() => { + const origin = location.origin, // assuming an ASCII origin + headers = "header(Access-Control-Allow-Origin," + origin + ")|header(Access-Control-Allow-Credentials,true)" + return fetch(url + sharedHeaders + headers, { credentials:"include" }).then(resp => { + assert_equals(resp.status, 200) + assert_equals(resp.type , "cors") + assert_equals(resp.headers.get("content-type"), "text/plain") // safelisted + assert_equals(resp.headers.get("test"), null) + assert_equals(resp.headers.get("set-cookie"), null) + assert_equals(resp.headers.get("*"), "whoa") + }) +}, "* for credentialed fetches only matches literally") + +promise_test(() => { + const headers = "header(Access-Control-Allow-Origin,*)|header(Access-Control-Expose-Headers,set-cookie\\,*)" + return fetch(url + sharedHeaders + headers).then(resp => { + assert_equals(resp.status, 200) + assert_equals(resp.type , "cors") + assert_equals(resp.headers.get("test"), "X") + assert_equals(resp.headers.get("set-cookie"), null) + assert_equals(resp.headers.get("*"), "whoa") + }) +}, "* can be one of several values") + +done(); diff --git a/test/wpt/tests/fetch/api/cors/cors-filtering.sub.any.js b/test/wpt/tests/fetch/api/cors/cors-filtering.sub.any.js new file mode 100644 index 00000000000..a26eaccf2a5 --- /dev/null +++ b/test/wpt/tests/fetch/api/cors/cors-filtering.sub.any.js @@ -0,0 +1,69 @@ +// META: script=../resources/utils.js + +function corsFilter(corsUrl, headerName, headerValue, isFiltered) { + var url = corsUrl + "?pipe=header(" + headerName + "," + encodeURIComponent(headerValue) +")|header(Access-Control-Allow-Origin,*)"; + promise_test(function(test) { + return fetch(url).then(function(resp) { + assert_equals(resp.status, 200, "Fetch success with code 200"); + assert_equals(resp.type , "cors", "CORS fetch's response has cors type"); + if (!isFiltered) { + assert_equals(resp.headers.get(headerName), headerValue, + headerName + " header should be included in response with value: " + headerValue); + } else { + assert_false(resp.headers.has(headerName), "UA should exclude " + headerName + " header from response"); + } + test.done(); + }); + }, "CORS filter on " + headerName + " header"); +} + +function corsExposeFilter(corsUrl, headerName, headerValue, isForbidden, withCredentials) { + var url = corsUrl + "?pipe=header(" + headerName + "," + encodeURIComponent(headerValue) +")|" + + "header(Access-Control-Allow-Origin, http://{{host}}:{{ports[http][0]}})" + + "header(Access-Control-Allow-Credentials, true)" + + "header(Access-Control-Expose-Headers," + headerName + ")"; + + var title = "CORS filter on " + headerName + " header, header is " + (isForbidden ? "forbidden" : "exposed"); + if (withCredentials) + title+= "(credentials = include)"; + promise_test(function(test) { + return fetch(new Request(url, { credentials: withCredentials ? "include" : "omit" })).then(function(resp) { + assert_equals(resp.status, 200, "Fetch success with code 200"); + assert_equals(resp.type , "cors", "CORS fetch's response has cors type"); + if (!isForbidden) { + assert_equals(resp.headers.get(headerName), headerValue, + headerName + " header should be included in response with value: " + headerValue); + } else { + assert_false(resp.headers.has(headerName), "UA should exclude " + headerName + " header from response"); + } + test.done(); + }); + }, title); +} + +var url = "http://{{host}}:{{ports[http][1]}}" + dirname(location.pathname) + RESOURCES_DIR + "top.txt"; + +corsFilter(url, "Cache-Control", "no-cache", false); +corsFilter(url, "Content-Language", "fr", false); +corsFilter(url, "Content-Type", "text/html", false); +corsFilter(url, "Expires","04 May 1988 22:22:22 GMT" , false); +corsFilter(url, "Last-Modified", "04 May 1988 22:22:22 GMT", false); +corsFilter(url, "Pragma", "no-cache", false); +corsFilter(url, "Content-Length", "3" , false); // top.txt contains "top" + +corsFilter(url, "Age", "27", true); +corsFilter(url, "Server", "wptServe" , true); +corsFilter(url, "Warning", "Mind the gap" , true); +corsFilter(url, "Set-Cookie", "name=value" , true); +corsFilter(url, "Set-Cookie2", "name=value" , true); + +corsExposeFilter(url, "Age", "27", false); +corsExposeFilter(url, "Server", "wptServe" , false); +corsExposeFilter(url, "Warning", "Mind the gap" , false); + +corsExposeFilter(url, "Set-Cookie", "name=value" , true); +corsExposeFilter(url, "Set-Cookie2", "name=value" , true); +corsExposeFilter(url, "Set-Cookie", "name=value" , true, true); +corsExposeFilter(url, "Set-Cookie2", "name=value" , true, true); + +done(); diff --git a/test/wpt/tests/fetch/api/cors/cors-multiple-origins.sub.any.js b/test/wpt/tests/fetch/api/cors/cors-multiple-origins.sub.any.js new file mode 100644 index 00000000000..b3abb922841 --- /dev/null +++ b/test/wpt/tests/fetch/api/cors/cors-multiple-origins.sub.any.js @@ -0,0 +1,22 @@ +// META: global=window,worker +// META: script=../resources/utils.js + +function corsMultipleOrigins(originList) { + var urlParameters = "?origin=" + encodeURIComponent(originList.join(", ")); + var url = "http://{{host}}:{{ports[http][1]}}" + dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; + + promise_test(function(test) { + return promise_rejects_js(test, TypeError, fetch(url + urlParameters)); + }, "Listing multiple origins is illegal: " + originList); +} +/* Actual origin */ +var origin = "http://{{host}}:{{ports[http][0]}}"; + +corsMultipleOrigins(["\"\"", "http://example.com", origin]); +corsMultipleOrigins(["\"\"", "http://example.com", "*"]); +corsMultipleOrigins(["\"\"", origin, origin]); +corsMultipleOrigins(["*", "http://example.com", "*"]); +corsMultipleOrigins(["*", "http://example.com", origin]); +corsMultipleOrigins(["", "http://example.com", "https://example2.com"]); + +done(); diff --git a/test/wpt/tests/fetch/api/cors/cors-no-preflight.any.js b/test/wpt/tests/fetch/api/cors/cors-no-preflight.any.js new file mode 100644 index 00000000000..7a0269aae4e --- /dev/null +++ b/test/wpt/tests/fetch/api/cors/cors-no-preflight.any.js @@ -0,0 +1,41 @@ +// META: script=/common/utils.js +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +function corsNoPreflight(desc, baseURL, method, headerName, headerValue) { + + var uuid_token = token(); + var url = baseURL + dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; + var urlParameters = "?token=" + uuid_token + "&max_age=0"; + var requestInit = {"mode": "cors", "method": method, "headers":{}}; + if (headerName) + requestInit["headers"][headerName] = headerValue; + + 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(resp) { + assert_equals(resp.status, 200, "Response's status is 200"); + assert_equals(resp.headers.get("x-did-preflight"), "0", "No preflight request has been made"); + }); + }); + }, desc); +} + +var host_info = get_host_info(); + +corsNoPreflight("Cross domain basic usage [GET]", host_info.HTTP_REMOTE_ORIGIN, "GET"); +corsNoPreflight("Same domain different port [GET]", host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT, "GET"); +corsNoPreflight("Cross domain different port [GET]", host_info.HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT, "GET"); +corsNoPreflight("Cross domain different protocol [GET]", host_info.HTTPS_REMOTE_ORIGIN, "GET"); +corsNoPreflight("Same domain different protocol different port [GET]", host_info.HTTPS_ORIGIN, "GET"); +corsNoPreflight("Cross domain [POST]", host_info.HTTP_REMOTE_ORIGIN, "POST"); +corsNoPreflight("Cross domain [HEAD]", host_info.HTTP_REMOTE_ORIGIN, "HEAD"); +corsNoPreflight("Cross domain [GET] [Accept: */*]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Accept", "*/*"); +corsNoPreflight("Cross domain [GET] [Accept-Language: fr]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Accept-Language", "fr"); +corsNoPreflight("Cross domain [GET] [Content-Language: fr]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Content-Language", "fr"); +corsNoPreflight("Cross domain [GET] [Content-Type: application/x-www-form-urlencoded]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Content-Type", "application/x-www-form-urlencoded"); +corsNoPreflight("Cross domain [GET] [Content-Type: multipart/form-data]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Content-Type", "multipart/form-data"); +corsNoPreflight("Cross domain [GET] [Content-Type: text/plain]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Content-Type", "text/plain"); +corsNoPreflight("Cross domain [GET] [Content-Type: text/plain;charset=utf-8]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Content-Type", "text/plain;charset=utf-8"); +corsNoPreflight("Cross domain [GET] [Content-Type: Text/Plain;charset=utf-8]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Content-Type", "Text/Plain;charset=utf-8"); diff --git a/test/wpt/tests/fetch/api/cors/cors-origin.any.js b/test/wpt/tests/fetch/api/cors/cors-origin.any.js new file mode 100644 index 00000000000..30a02d910fd --- /dev/null +++ b/test/wpt/tests/fetch/api/cors/cors-origin.any.js @@ -0,0 +1,51 @@ +// META: script=/common/utils.js +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +/* If origin is undefined, it is set to fetched url's origin*/ +function corsOrigin(desc, baseURL, method, origin, shouldPass) { + if (!origin) + origin = baseURL; + + var uuid_token = token(); + var urlParameters = "?token=" + uuid_token + "&max_age=0&origin=" + encodeURIComponent(origin) + "&allow_methods=" + method; + var url = baseURL + dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; + var requestInit = {"mode": "cors", "method": method}; + + 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 fetch(url + urlParameters, requestInit).then(function(resp) { + assert_equals(resp.status, 200, "Response's status is 200"); + }); + } else { + return promise_rejects_js(test, TypeError, fetch(url + urlParameters, requestInit)); + } + }); + }, desc); + +} + +var host_info = get_host_info(); + +/* Actual origin */ +var origin = host_info.HTTP_ORIGIN; + +corsOrigin("Cross domain different subdomain [origin OK]", host_info.HTTP_REMOTE_ORIGIN, "GET", origin, true); +corsOrigin("Cross domain different subdomain [origin KO]", host_info.HTTP_REMOTE_ORIGIN, "GET", undefined, false); +corsOrigin("Same domain different port [origin OK]", host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT, "GET", origin, true); +corsOrigin("Same domain different port [origin KO]", host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT, "GET", undefined, false); +corsOrigin("Cross domain different port [origin OK]", host_info.HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT, "GET", origin, true); +corsOrigin("Cross domain different port [origin KO]", host_info.HTTP_REMOTE_ORIGIN_WITH_DIFFERENT_PORT, "GET", undefined, false); +corsOrigin("Cross domain different protocol [origin OK]", host_info.HTTPS_REMOTE_ORIGIN, "GET", origin, true); +corsOrigin("Cross domain different protocol [origin KO]", host_info.HTTPS_REMOTE_ORIGIN, "GET", undefined, false); +corsOrigin("Same domain different protocol different port [origin OK]", host_info.HTTPS_ORIGIN, "GET", origin, true); +corsOrigin("Same domain different protocol different port [origin KO]", host_info.HTTPS_ORIGIN, "GET", undefined, false); +corsOrigin("Cross domain [POST] [origin OK]", host_info.HTTP_REMOTE_ORIGIN, "POST", origin, true); +corsOrigin("Cross domain [POST] [origin KO]", host_info.HTTP_REMOTE_ORIGIN, "POST", undefined, false); +corsOrigin("Cross domain [HEAD] [origin OK]", host_info.HTTP_REMOTE_ORIGIN, "HEAD", origin, true); +corsOrigin("Cross domain [HEAD] [origin KO]", host_info.HTTP_REMOTE_ORIGIN, "HEAD", undefined, false); +corsOrigin("CORS preflight [PUT] [origin OK]", host_info.HTTP_REMOTE_ORIGIN, "PUT", origin, true); +corsOrigin("CORS preflight [PUT] [origin KO]", host_info.HTTP_REMOTE_ORIGIN, "PUT", undefined, false); +corsOrigin("Allowed origin: \"\" [origin KO]", host_info.HTTP_REMOTE_ORIGIN, "GET", "" , false); diff --git a/test/wpt/tests/fetch/api/cors/cors-preflight-cache.any.js b/test/wpt/tests/fetch/api/cors/cors-preflight-cache.any.js new file mode 100644 index 00000000000..ce6a169d814 --- /dev/null +++ b/test/wpt/tests/fetch/api/cors/cors-preflight-cache.any.js @@ -0,0 +1,46 @@ +// META: script=/common/utils.js +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +var cors_url = get_host_info().HTTP_REMOTE_ORIGIN + + dirname(location.pathname) + + RESOURCES_DIR + + "preflight.py"; + +promise_test((test) => { + var uuid_token = token(); + var request_url = + cors_url + "?token=" + uuid_token + "&max_age=12000&allow_methods=POST" + + "&allow_headers=x-test-header"; + return fetch(cors_url + "?token=" + uuid_token + "&clear-stash") + .then(() => { + return fetch( + new Request(request_url, + { + mode: "cors", + method: "POST", + headers: [["x-test-header", "test1"]] + })); + }) + .then((resp) => { + assert_equals(resp.status, 200, "Response's status is 200"); + assert_equals(resp.headers.get("x-did-preflight"), "1", "Preflight request has been made"); + return fetch(cors_url + "?token=" + uuid_token + "&clear-stash"); + }) + .then((res) => res.text()) + .then((txt) => { + assert_equals(txt, "1", "Server stash must be cleared."); + return fetch( + new Request(request_url, + { + mode: "cors", + method: "POST", + headers: [["x-test-header", "test2"]] + })); + }) + .then((resp) => { + assert_equals(resp.status, 200, "Response's status is 200"); + assert_equals(resp.headers.get("x-did-preflight"), "0", "Preflight request has not been made"); + return fetch(cors_url + "?token=" + uuid_token + "&clear-stash"); + }); +}); diff --git a/test/wpt/tests/fetch/api/cors/cors-preflight-not-cors-safelisted.any.js b/test/wpt/tests/fetch/api/cors/cors-preflight-not-cors-safelisted.any.js new file mode 100644 index 00000000000..b2747ccd5bc --- /dev/null +++ b/test/wpt/tests/fetch/api/cors/cors-preflight-not-cors-safelisted.any.js @@ -0,0 +1,19 @@ +// META: script=/common/utils.js +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=resources/corspreflight.js + +const corsURL = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; + +promise_test(() => fetch("resources/not-cors-safelisted.json").then(res => res.json().then(runTests)), "Loading data…"); + +function runTests(testArray) { + testArray.forEach(testItem => { + const [headerName, headerValue] = testItem; + corsPreflight("Need CORS-preflight for " + headerName + "/" + headerValue + " header", + corsURL, + "GET", + true, + [[headerName, headerValue]]); + }); +} diff --git a/test/wpt/tests/fetch/api/cors/cors-preflight-redirect.any.js b/test/wpt/tests/fetch/api/cors/cors-preflight-redirect.any.js new file mode 100644 index 00000000000..15f7659abd2 --- /dev/null +++ b/test/wpt/tests/fetch/api/cors/cors-preflight-redirect.any.js @@ -0,0 +1,37 @@ +// META: global=window,worker +// META: script=/common/utils.js +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +function corsPreflightRedirect(desc, redirectUrl, redirectLocation, redirectStatus, redirectPreflight) { + var uuid_token = token(); + var url = redirectUrl; + var urlParameters = "?token=" + uuid_token + "&max_age=0"; + urlParameters += "&redirect_status=" + redirectStatus; + urlParameters += "&location=" + encodeURIComponent(redirectLocation); + + if (redirectPreflight) + urlParameters += "&redirect_preflight"; + var requestInit = {"mode": "cors", "redirect": "follow"}; + + /* Force preflight */ + requestInit["headers"] = {"x-force-preflight": ""}; + urlParameters += "&allow_headers=x-force-preflight"; + + 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 promise_rejects_js(test, TypeError, fetch(url + urlParameters, requestInit)); + }); + }, desc); +} + +var redirectUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "redirect.py"; +var locationUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; + +for (var code of [301, 302, 303, 307, 308]) { + /* preflight should not follow the redirection */ + corsPreflightRedirect("Redirection " + code + " on preflight failed", redirectUrl, locationUrl, code, true); + /* preflight is done before redirection: preflight force redirect to error */ + corsPreflightRedirect("Redirection " + code + " after preflight failed", redirectUrl, locationUrl, code, false); +} diff --git a/test/wpt/tests/fetch/api/cors/cors-preflight-referrer.any.js b/test/wpt/tests/fetch/api/cors/cors-preflight-referrer.any.js new file mode 100644 index 00000000000..5df9fcf1429 --- /dev/null +++ b/test/wpt/tests/fetch/api/cors/cors-preflight-referrer.any.js @@ -0,0 +1,51 @@ +// META: script=/common/utils.js +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +function corsPreflightReferrer(desc, corsUrl, referrerPolicy, referrer, expectedReferrer) { + var uuid_token = token(); + var url = corsUrl; + var urlParameters = "?token=" + uuid_token + "&max_age=0"; + var requestInit = {"mode": "cors", "referrerPolicy": referrerPolicy}; + + if (referrer) + requestInit.referrer = referrer; + + /* Force preflight */ + requestInit["headers"] = {"x-force-preflight": ""}; + urlParameters += "&allow_headers=x-force-preflight"; + + 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(resp) { + assert_equals(resp.status, 200, "Response's status is 200"); + assert_equals(resp.headers.get("x-did-preflight"), "1", "Preflight request has been made"); + assert_equals(resp.headers.get("x-preflight-referrer"), expectedReferrer, "Preflight's referrer is correct"); + assert_equals(resp.headers.get("x-referrer"), expectedReferrer, "Request's referrer is correct"); + assert_equals(resp.headers.get("x-control-request-headers"), "", "Access-Control-Allow-Headers value"); + }); + }); + }, desc + " and referrer: " + (referrer ? "'" + referrer + "'" : "default")); +} + +var corsUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; +var origin = get_host_info().HTTP_ORIGIN + "/"; + +corsPreflightReferrer("Referrer policy: no-referrer", corsUrl, "no-referrer", undefined, ""); +corsPreflightReferrer("Referrer policy: no-referrer", corsUrl, "no-referrer", "myreferrer", ""); + +corsPreflightReferrer("Referrer policy: \"\"", corsUrl, "", undefined, origin); +corsPreflightReferrer("Referrer policy: \"\"", corsUrl, "", "myreferrer", origin); + +corsPreflightReferrer("Referrer policy: no-referrer-when-downgrade", corsUrl, "no-referrer-when-downgrade", undefined, location.toString()) +corsPreflightReferrer("Referrer policy: no-referrer-when-downgrade", corsUrl, "no-referrer-when-downgrade", "myreferrer", new URL("myreferrer", location).toString()); + +corsPreflightReferrer("Referrer policy: origin", corsUrl, "origin", undefined, origin); +corsPreflightReferrer("Referrer policy: origin", corsUrl, "origin", "myreferrer", origin); + +corsPreflightReferrer("Referrer policy: origin-when-cross-origin", corsUrl, "origin-when-cross-origin", undefined, origin); +corsPreflightReferrer("Referrer policy: origin-when-cross-origin", corsUrl, "origin-when-cross-origin", "myreferrer", origin); + +corsPreflightReferrer("Referrer policy: unsafe-url", corsUrl, "unsafe-url", undefined, location.toString()); +corsPreflightReferrer("Referrer policy: unsafe-url", corsUrl, "unsafe-url", "myreferrer", new URL("myreferrer", location).toString()); diff --git a/test/wpt/tests/fetch/api/cors/cors-preflight-response-validation.any.js b/test/wpt/tests/fetch/api/cors/cors-preflight-response-validation.any.js new file mode 100644 index 00000000000..718e351c1d3 --- /dev/null +++ b/test/wpt/tests/fetch/api/cors/cors-preflight-response-validation.any.js @@ -0,0 +1,33 @@ +// META: script=/common/utils.js +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +function corsPreflightResponseValidation(desc, corsUrl, allowHeaders, allowMethods) { + var uuid_token = token(); + var url = corsUrl; + var requestInit = {"mode": "cors"}; + /* Force preflight */ + requestInit["headers"] = {"x-force-preflight": ""}; + + var urlParameters = "?token=" + uuid_token + "&max_age=0"; + urlParameters += "&allow_headers=x-force-preflight"; + if (allowHeaders) + urlParameters += "," + allowHeaders; + if (allowMethods) + urlParameters += "&allow_methods="+ allowMethods; + + promise_test(function(test) { + return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(async function(resp) { + assert_equals(resp.status, 200, "Clean stash response's status is 200"); + await promise_rejects_js(test, TypeError, fetch(url + urlParameters, requestInit)); + + return fetch(url + urlParameters).then(function(resp) { + assert_equals(resp.headers.get("x-did-preflight"), "1", "Preflight request has been made"); + }); + }); + }, desc); +} + +var corsUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; +corsPreflightResponseValidation("Preflight response with a bad Access-Control-Allow-Headers", corsUrl, "Bad value", null); +corsPreflightResponseValidation("Preflight response with a bad Access-Control-Allow-Methods", corsUrl, null, "Bad value"); 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 new file mode 100644 index 00000000000..e8cbc80b808 --- /dev/null +++ b/test/wpt/tests/fetch/api/cors/cors-preflight-star.any.js @@ -0,0 +1,82 @@ +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +const url = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py", + origin = location.origin // assuming an ASCII origin + +function preflightTest(succeeds, withCredentials, allowMethod, allowHeader, useMethod, useHeader) { + return promise_test(t => { + let testURL = url + "?", + requestInit = {} + if (withCredentials) { + testURL += "origin=" + origin + "&" + testURL += "credentials&" + requestInit.credentials = "include" + } + if (useMethod) { + requestInit.method = useMethod + } + if (useHeader.length > 0) { + requestInit.headers = [useHeader] + } + testURL += "allow_methods=" + allowMethod + "&" + testURL += "allow_headers=" + allowHeader + "&" + + if (succeeds) { + return fetch(testURL, requestInit).then(resp => { + assert_equals(resp.headers.get("x-origin"), origin) + }) + } else { + return promise_rejects_js(t, TypeError, fetch(testURL, requestInit)) + } + }, "CORS that " + (succeeds ? "succeeds" : "fails") + " with credentials: " + withCredentials + "; method: " + useMethod + " (allowed: " + allowMethod + "); header: " + useHeader + " (allowed: " + allowHeader + ")") +} + +// "GET" does not pass the case-sensitive method check, but in the safe list. +preflightTest(true, false, "get", "x-test", "GET", ["X-Test", "1"]) +// Headers check is case-insensitive, and "*" works as any for method. +preflightTest(true, false, "*", "x-test", "SUPER", ["X-Test", "1"]) +// "*" works as any only without credentials. +preflightTest(true, false, "*", "*", "OK", ["X-Test", "1"]) +preflightTest(false, true, "*", "*", "OK", ["X-Test", "1"]) +preflightTest(false, true, "*", "", "PUT", []) +preflightTest(false, true, "get", "*", "GET", ["X-Test", "1"]) +preflightTest(false, true, "*", "*", "GET", ["X-Test", "1"]) +// Exact character match works even for "*" with credentials. +preflightTest(true, true, "*", "*", "*", ["*", "1"]) + +// The following methods are upper-cased for init["method"] by +// https://fetch.spec.whatwg.org/#concept-method-normalize +// but not in Access-Control-Allow-Methods response. +// But they are https://fetch.spec.whatwg.org/#cors-safelisted-method, +// CORS anyway passes regardless of the cases. +for (const METHOD of ['GET', 'HEAD', 'POST']) { + const method = METHOD.toLowerCase(); + preflightTest(true, true, METHOD, "*", METHOD, []) + preflightTest(true, true, METHOD, "*", method, []) + preflightTest(true, true, method, "*", METHOD, []) + preflightTest(true, true, method, "*", method, []) +} + +// The following methods are upper-cased for init["method"] by +// https://fetch.spec.whatwg.org/#concept-method-normalize +// but not in Access-Control-Allow-Methods response. +// As they are not https://fetch.spec.whatwg.org/#cors-safelisted-method, +// Access-Control-Allow-Methods should contain upper-cased methods, +// while init["method"] can be either in upper or lower case. +for (const METHOD of ['DELETE', 'PUT']) { + const method = METHOD.toLowerCase(); + preflightTest(true, true, METHOD, "*", METHOD, []) + preflightTest(true, true, METHOD, "*", method, []) + preflightTest(false, true, method, "*", METHOD, []) + preflightTest(false, true, method, "*", method, []) +} + +// "PATCH" is NOT upper-cased in both places because it is not listed in +// https://fetch.spec.whatwg.org/#concept-method-normalize. +// So Access-Control-Allow-Methods value and init["method"] should match +// case-sensitively. +preflightTest(true, true, "PATCH", "*", "PATCH", []) +preflightTest(false, true, "PATCH", "*", "patch", []) +preflightTest(false, true, "patch", "*", "PATCH", []) +preflightTest(true, true, "patch", "*", "patch", []) diff --git a/test/wpt/tests/fetch/api/cors/cors-preflight-status.any.js b/test/wpt/tests/fetch/api/cors/cors-preflight-status.any.js new file mode 100644 index 00000000000..a4467a6087b --- /dev/null +++ b/test/wpt/tests/fetch/api/cors/cors-preflight-status.any.js @@ -0,0 +1,37 @@ +// META: script=/common/utils.js +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +/* Check preflight is ok if status is ok status (200 to 299)*/ +function corsPreflightStatus(desc, corsUrl, preflightStatus) { + var uuid_token = token(); + var url = corsUrl; + var requestInit = {"mode": "cors"}; + /* Force preflight */ + requestInit["headers"] = {"x-force-preflight": ""}; + + var urlParameters = "?token=" + uuid_token + "&max_age=0"; + urlParameters += "&allow_headers=x-force-preflight"; + urlParameters += "&preflight_status=" + preflightStatus; + + 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 (200 <= preflightStatus && 299 >= preflightStatus) { + return fetch(url + urlParameters, requestInit).then(function(resp) { + assert_equals(resp.status, 200, "Response's status is 200"); + assert_equals(resp.headers.get("x-did-preflight"), "1", "Preflight request has been made"); + }); + } else { + return promise_rejects_js(test, TypeError, fetch(url + urlParameters, requestInit)); + } + }); + }, desc); +} + +var corsUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; +for (status of [200, 201, 202, 203, 204, 205, 206, + 300, 301, 302, 303, 304, 305, 306, 307, 308, + 400, 401, 402, 403, 404, 405, + 501, 502, 503, 504, 505]) + corsPreflightStatus("Preflight answered with status " + status, corsUrl, status); diff --git a/test/wpt/tests/fetch/api/cors/cors-preflight.any.js b/test/wpt/tests/fetch/api/cors/cors-preflight.any.js new file mode 100644 index 00000000000..045422f40b1 --- /dev/null +++ b/test/wpt/tests/fetch/api/cors/cors-preflight.any.js @@ -0,0 +1,62 @@ +// META: script=/common/utils.js +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=resources/corspreflight.js + +var corsUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; + +corsPreflight("CORS [DELETE], server allows", corsUrl, "DELETE", true); +corsPreflight("CORS [DELETE], server refuses", corsUrl, "DELETE", false); +corsPreflight("CORS [PUT], server allows", corsUrl, "PUT", true); +corsPreflight("CORS [PUT], server allows, check preflight has user agent", corsUrl + "?checkUserAgentHeaderInPreflight", "PUT", true); +corsPreflight("CORS [PUT], server refuses", corsUrl, "PUT", false); +corsPreflight("CORS [PATCH], server allows", corsUrl, "PATCH", true); +corsPreflight("CORS [PATCH], server refuses", corsUrl, "PATCH", false); +corsPreflight("CORS [patcH], server allows", corsUrl, "patcH", true); +corsPreflight("CORS [patcH], server refuses", corsUrl, "patcH", false); +corsPreflight("CORS [NEW], server allows", corsUrl, "NEW", true); +corsPreflight("CORS [NEW], server refuses", corsUrl, "NEW", false); +corsPreflight("CORS [chicken], server allows", corsUrl, "chicken", true); +corsPreflight("CORS [chicken], server refuses", corsUrl, "chicken", false); + +corsPreflight("CORS [GET] [x-test-header: allowed], server allows", corsUrl, "GET", true, [["x-test-header1", "allowed"]]); +corsPreflight("CORS [GET] [x-test-header: refused], server refuses", corsUrl, "GET", false, [["x-test-header1", "refused"]]); + +var headers = [ + ["x-test-header1", "allowedOrRefused"], + ["x-test-header2", "allowedOrRefused"], + ["X-test-header3", "allowedOrRefused"], + ["x-test-header-b", "allowedOrRefused"], + ["x-test-header-D", "allowedOrRefused"], + ["x-test-header-C", "allowedOrRefused"], + ["x-test-header-a", "allowedOrRefused"], + ["Content-Type", "allowedOrRefused"], +]; +var safeHeaders= [ + ["Accept", "*"], + ["Accept-Language", "bzh"], + ["Content-Language", "eu"], +]; + +corsPreflight("CORS [GET] [several headers], server allows", corsUrl, "GET", true, headers, safeHeaders); +corsPreflight("CORS [GET] [several headers], server refuses", corsUrl, "GET", false, headers, safeHeaders); +corsPreflight("CORS [PUT] [several headers], server allows", corsUrl, "PUT", true, headers, safeHeaders); +corsPreflight("CORS [PUT] [several headers], server refuses", corsUrl, "PUT", false, headers, safeHeaders); + +corsPreflight("CORS [PUT] [only safe headers], server allows", corsUrl, "PUT", true, null, safeHeaders); + +promise_test(async t => { + const url = `${corsUrl}?allow_headers=*`; + await promise_rejects_js(t, TypeError, fetch(url, { + headers: { + authorization: 'foobar' + } + })); +}, '"authorization" should not be covered by the wildcard symbol'); + +promise_test(async t => { + const url = `${corsUrl}?allow_headers=authorization`; + await fetch(url, { headers: { + authorization: 'foobar' + }}); +}, '"authorization" should be covered by "authorization"'); \ No newline at end of file diff --git a/test/wpt/tests/fetch/api/cors/cors-redirect-credentials.any.js b/test/wpt/tests/fetch/api/cors/cors-redirect-credentials.any.js new file mode 100644 index 00000000000..2aff3134063 --- /dev/null +++ b/test/wpt/tests/fetch/api/cors/cors-redirect-credentials.any.js @@ -0,0 +1,52 @@ +// META: timeout=long +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +function corsRedirectCredentials(desc, redirectUrl, redirectLocation, redirectStatus, locationCredentials) { + var url = redirectUrl + var urlParameters = "?redirect_status=" + redirectStatus; + urlParameters += "&location=" + redirectLocation.replace("://", "://" + locationCredentials + "@"); + + var requestInit = {"mode": "cors", "redirect": "follow"}; + + promise_test(t => { + const result = fetch(url + urlParameters, requestInit) + if(locationCredentials === "") { + return result; + } else { + return promise_rejects_js(t, TypeError, result); + } + }, desc); +} + +var redirPath = dirname(location.pathname) + RESOURCES_DIR + "redirect.py"; +var preflightPath = dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; + +var host_info = get_host_info(); + +var localRedirect = host_info.HTTP_ORIGIN + redirPath; +var remoteRedirect = host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT + redirPath; + +var localLocation = host_info.HTTP_ORIGIN + preflightPath; +var remoteLocation = host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT + preflightPath; +var remoteLocation2 = host_info.HTTP_REMOTE_ORIGIN + preflightPath; + +for (var code of [301, 302, 303, 307, 308]) { + corsRedirectCredentials("Redirect " + code + " from same origin to remote without user and password", localRedirect, remoteLocation, code, ""); + + corsRedirectCredentials("Redirect " + code + " from same origin to remote with user and password", localRedirect, remoteLocation, code, "user:password"); + corsRedirectCredentials("Redirect " + code + " from same origin to remote with user", localRedirect, remoteLocation, code, "user:"); + corsRedirectCredentials("Redirect " + code + " from same origin to remote with password", localRedirect, remoteLocation, code, ":password"); + + corsRedirectCredentials("Redirect " + code + " from remote to same origin with user and password", remoteRedirect, localLocation, code, "user:password"); + corsRedirectCredentials("Redirect " + code + " from remote to same origin with user", remoteRedirect, localLocation, code, "user:"); + corsRedirectCredentials("Redirect " + code + " from remote to same origin with password", remoteRedirect, localLocation, code, ":password"); + + corsRedirectCredentials("Redirect " + code + " from remote to same remote with user and password", remoteRedirect, remoteLocation, code, "user:password"); + corsRedirectCredentials("Redirect " + code + " from remote to same remote with user", remoteRedirect, remoteLocation, code, "user:"); + corsRedirectCredentials("Redirect " + code + " from remote to same remote with password", remoteRedirect, remoteLocation, code, ":password"); + + corsRedirectCredentials("Redirect " + code + " from remote to another remote with user and password", remoteRedirect, remoteLocation2, code, "user:password"); + corsRedirectCredentials("Redirect " + code + " from remote to another remote with user", remoteRedirect, remoteLocation2, code, "user:"); + corsRedirectCredentials("Redirect " + code + " from remote to another remote with password", remoteRedirect, remoteLocation2, code, ":password"); +} diff --git a/test/wpt/tests/fetch/api/cors/cors-redirect-preflight.any.js b/test/wpt/tests/fetch/api/cors/cors-redirect-preflight.any.js new file mode 100644 index 00000000000..50848170d0d --- /dev/null +++ b/test/wpt/tests/fetch/api/cors/cors-redirect-preflight.any.js @@ -0,0 +1,46 @@ +// META: timeout=long +// META: script=/common/utils.js +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +function corsRedirect(desc, redirectUrl, redirectLocation, redirectStatus, expectSuccess) { + var urlBaseParameters = "&redirect_status=" + redirectStatus; + var urlParametersSuccess = urlBaseParameters + "&allow_headers=x-w3c&location=" + encodeURIComponent(redirectLocation + "?allow_headers=x-w3c"); + var urlParametersFailure = urlBaseParameters + "&location=" + encodeURIComponent(redirectLocation); + + var requestInit = {"mode": "cors", "redirect": "follow", "headers" : [["x-w3c", "test"]]}; + + promise_test(function(test) { + var uuid_token = token(); + return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) { + return fetch(redirectUrl + "?token=" + uuid_token + "&max_age=0" + urlParametersSuccess, requestInit).then(function(resp) { + assert_equals(resp.status, 200, "Response's status is 200"); + assert_equals(resp.headers.get("x-did-preflight"), "1", "Preflight request has been made"); + }); + }); + }, desc + " (preflight after redirection success case)"); + promise_test(function(test) { + var uuid_token = token(); + return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) { + return promise_rejects_js(test, TypeError, fetch(redirectUrl + "?token=" + uuid_token + "&max_age=0" + urlParametersFailure, requestInit)); + }); + }, desc + " (preflight after redirection failure case)"); +} + +var redirPath = dirname(location.pathname) + RESOURCES_DIR + "redirect.py"; +var preflightPath = dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; + +var host_info = get_host_info(); + +var localRedirect = host_info.HTTP_ORIGIN + redirPath; +var remoteRedirect = host_info.HTTP_REMOTE_ORIGIN + redirPath; + +var localLocation = host_info.HTTP_ORIGIN + preflightPath; +var remoteLocation = host_info.HTTP_REMOTE_ORIGIN + preflightPath; +var remoteLocation2 = host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT + preflightPath; + +for (var code of [301, 302, 303, 307, 308]) { + corsRedirect("Redirect " + code + ": same origin to cors", localRedirect, remoteLocation, code); + corsRedirect("Redirect " + code + ": cors to same origin", remoteRedirect, localLocation, code); + corsRedirect("Redirect " + code + ": cors to another cors", remoteRedirect, remoteLocation2, code); +} diff --git a/test/wpt/tests/fetch/api/cors/cors-redirect.any.js b/test/wpt/tests/fetch/api/cors/cors-redirect.any.js new file mode 100644 index 00000000000..cdf4097d566 --- /dev/null +++ b/test/wpt/tests/fetch/api/cors/cors-redirect.any.js @@ -0,0 +1,42 @@ +// META: script=/common/utils.js +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +function corsRedirect(desc, 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); + + var requestInit = {"mode": "cors", "redirect": "follow"}; + + return promise_test(function(test) { + return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(resp) { + return fetch(url + urlParameters, requestInit).then(function(resp) { + assert_equals(resp.status, 200, "Response's status is 200"); + assert_equals(resp.headers.get("x-did-preflight"), "0", "No preflight request has been made"); + assert_equals(resp.headers.get("x-origin"), expectedOrigin, "Origin is correctly set after redirect"); + }); + }); + }, desc); +} + +var redirPath = dirname(location.pathname) + RESOURCES_DIR + "redirect.py"; +var preflightPath = dirname(location.pathname) + RESOURCES_DIR + "preflight.py"; + +var host_info = get_host_info(); + +var localRedirect = host_info.HTTP_ORIGIN + redirPath; +var remoteRedirect = host_info.HTTP_REMOTE_ORIGIN + redirPath; + +var localLocation = host_info.HTTP_ORIGIN + preflightPath; +var remoteLocation = host_info.HTTP_REMOTE_ORIGIN + preflightPath; +var remoteLocation2 = host_info.HTTP_ORIGIN_WITH_DIFFERENT_PORT + preflightPath; + +for (var code of [301, 302, 303, 307, 308]) { + corsRedirect("Redirect " + code + ": cors to same cors", remoteRedirect, remoteLocation, code, location.origin); + corsRedirect("Redirect " + code + ": cors to another cors", remoteRedirect, remoteLocation2, code, "null"); + corsRedirect("Redirect " + code + ": same origin to cors", localRedirect, remoteLocation, code, location.origin); + corsRedirect("Redirect " + code + ": cors to same origin", remoteRedirect, localLocation, code, "null"); +} diff --git a/test/wpt/tests/fetch/api/cors/data-url-iframe.html b/test/wpt/tests/fetch/api/cors/data-url-iframe.html new file mode 100644 index 00000000000..217baa3c46b --- /dev/null +++ b/test/wpt/tests/fetch/api/cors/data-url-iframe.html @@ -0,0 +1,58 @@ + + + + + + diff --git a/test/wpt/tests/fetch/api/cors/data-url-shared-worker.html b/test/wpt/tests/fetch/api/cors/data-url-shared-worker.html new file mode 100644 index 00000000000..d69748ab261 --- /dev/null +++ b/test/wpt/tests/fetch/api/cors/data-url-shared-worker.html @@ -0,0 +1,53 @@ + + + + + diff --git a/test/wpt/tests/fetch/api/cors/data-url-worker.html b/test/wpt/tests/fetch/api/cors/data-url-worker.html new file mode 100644 index 00000000000..13113e62621 --- /dev/null +++ b/test/wpt/tests/fetch/api/cors/data-url-worker.html @@ -0,0 +1,50 @@ + + + + + diff --git a/test/wpt/tests/fetch/api/cors/resources/corspreflight.js b/test/wpt/tests/fetch/api/cors/resources/corspreflight.js new file mode 100644 index 00000000000..18b8f6dfa28 --- /dev/null +++ b/test/wpt/tests/fetch/api/cors/resources/corspreflight.js @@ -0,0 +1,58 @@ +function headerNames(headers) { + let names = []; + for (let header of headers) { + names.push(header[0].toLowerCase()); + } + return names; +} + +/* + Check preflight is done + Control if server allows method and headers and check accordingly + Check control access headers added by UA (for method and headers) +*/ +function corsPreflight(desc, corsUrl, method, allowed, headers, safeHeaders) { + return promise_test(function(test) { + var uuid_token = token(); + return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token).then(function(response) { + var url = corsUrl + (corsUrl.indexOf("?") === -1 ? "?" : "&"); + var urlParameters = "token=" + uuid_token + "&max_age=0"; + var requestInit = {"mode": "cors", "method": method}; + var requestHeaders = []; + if (headers) + requestHeaders.push.apply(requestHeaders, headers); + if (safeHeaders) + requestHeaders.push.apply(requestHeaders, safeHeaders); + requestInit["headers"] = requestHeaders; + + if (allowed) { + urlParameters += "&allow_methods=" + method + "&control_request_headers"; + if (headers) { + //Make the server allow the headers + urlParameters += "&allow_headers=" + headerNames(headers).join("%20%2C"); + } + return fetch(url + urlParameters, requestInit).then(function(resp) { + assert_equals(resp.status, 200, "Response's status is 200"); + assert_equals(resp.headers.get("x-did-preflight"), "1", "Preflight request has been made"); + if (headers) { + var actualHeaders = resp.headers.get("x-control-request-headers").toLowerCase().split(","); + for (var i in actualHeaders) + actualHeaders[i] = actualHeaders[i].trim(); + for (var header of headers) + assert_in_array(header[0].toLowerCase(), actualHeaders, "Preflight asked permission for header: " + header); + + let accessControlAllowHeaders = headerNames(headers).sort().join(","); + assert_equals(resp.headers.get("x-control-request-headers"), accessControlAllowHeaders, "Access-Control-Allow-Headers value"); + return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token); + } else { + assert_equals(resp.headers.get("x-control-request-headers"), null, "Access-Control-Request-Headers should be omitted") + } + }); + } else { + return promise_rejects_js(test, TypeError, fetch(url + urlParameters, requestInit)).then(function(){ + return fetch(RESOURCES_DIR + "clean-stash.py?token=" + uuid_token); + }); + } + }); + }, desc); +} diff --git a/test/wpt/tests/fetch/api/cors/resources/not-cors-safelisted.json b/test/wpt/tests/fetch/api/cors/resources/not-cors-safelisted.json new file mode 100644 index 00000000000..945dc0f93ba --- /dev/null +++ b/test/wpt/tests/fetch/api/cors/resources/not-cors-safelisted.json @@ -0,0 +1,13 @@ +[ + ["accept", "\""], + ["accept", "012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678"], + ["accept-language", "\u0001"], + ["accept-language", "@"], + ["authorization", "basics"], + ["content-language", "\u0001"], + ["content-language", "@"], + ["content-type", "text/html"], + ["content-type", "text/plain; long=0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901"], + ["range", "bytes 0-"], + ["test", "hi"] +] diff --git a/test/wpt/tests/fetch/api/cors/sandboxed-iframe.html b/test/wpt/tests/fetch/api/cors/sandboxed-iframe.html new file mode 100644 index 00000000000..feb9f1f2e5b --- /dev/null +++ b/test/wpt/tests/fetch/api/cors/sandboxed-iframe.html @@ -0,0 +1,14 @@ + + + + + + + diff --git a/test/wpt/tests/fetch/api/crashtests/request.html b/test/wpt/tests/fetch/api/crashtests/request.html new file mode 100644 index 00000000000..2d21930c3bb --- /dev/null +++ b/test/wpt/tests/fetch/api/crashtests/request.html @@ -0,0 +1,8 @@ + + + + diff --git a/test/wpt/tests/fetch/api/credentials/authentication-basic.any.js b/test/wpt/tests/fetch/api/credentials/authentication-basic.any.js new file mode 100644 index 00000000000..31ccc386977 --- /dev/null +++ b/test/wpt/tests/fetch/api/credentials/authentication-basic.any.js @@ -0,0 +1,17 @@ +// META: global=window,worker + +function basicAuth(desc, user, pass, mode, status) { + promise_test(function(test) { + var headers = { "Authorization": "Basic " + btoa(user + ":" + pass)}; + var requestInit = {"credentials": mode, "headers": headers}; + return fetch("../resources/authentication.py?realm=test", requestInit).then(function(resp) { + assert_equals(resp.status, status, "HTTP status is " + status); + assert_equals(resp.type , "basic", "Response's type is basic"); + }); + }, desc); +} + +basicAuth("User-added Authorization header with include mode", "user", "password", "include", 200); +basicAuth("User-added Authorization header with same-origin mode", "user", "password", "same-origin", 200); +basicAuth("User-added Authorization header with omit mode", "user", "password", "omit", 200); +basicAuth("User-added bogus Authorization header with omit mode", "notuser", "notpassword", "omit", 401); diff --git a/test/wpt/tests/fetch/api/credentials/authentication-redirection.any.js b/test/wpt/tests/fetch/api/credentials/authentication-redirection.any.js index b6376368116..a0175e6d2a7 100644 --- a/test/wpt/tests/fetch/api/credentials/authentication-redirection.any.js +++ b/test/wpt/tests/fetch/api/credentials/authentication-redirection.any.js @@ -21,6 +21,6 @@ promise_test(async test => { }, "getAuthorizationHeaderValue - same origin redirection"); promise_test(async (test) => { - const result = await getAuthorizationHeaderValue(get_host_info().HTTP_REMOTE_ORIGIN + "/fetch/api/resources/redirect.py?allow_headers=Authorization&location=" + encodeURIComponent(get_host_info().HTTP_ORIGIN + "/fetch/api/resources/dump-authorization-header.py")); + const result = await getAuthorizationHeaderValue(get_host_info().HTTPS_REMOTE_ORIGIN + "/fetch/api/resources/redirect.py?allow_headers=Authorization&location=" + encodeURIComponent(get_host_info().HTTPS_ORIGIN + "/fetch/api/resources/dump-authorization-header.py")); assert_equals(result, "none"); }, "getAuthorizationHeaderValue - cross origin redirection"); diff --git a/test/wpt/tests/fetch/api/credentials/cookies.any.js b/test/wpt/tests/fetch/api/credentials/cookies.any.js new file mode 100644 index 00000000000..de30e477655 --- /dev/null +++ b/test/wpt/tests/fetch/api/credentials/cookies.any.js @@ -0,0 +1,49 @@ +// META: global=window,worker +// META: script=../resources/utils.js + +function cookies(desc, credentials1, credentials2 ,cookies) { + var url = RESOURCES_DIR + "top.txt" + var urlParameters = ""; + var urlCleanParameters = ""; + if (cookies) { + urlParameters +="?pipe=header(Set-Cookie,"; + urlParameters += cookies.join(",True)|header(Set-Cookie,") + ",True)"; + urlCleanParameters +="?pipe=header(Set-Cookie,"; + urlCleanParameters += cookies.join("%3B%20max-age=0,True)|header(Set-Cookie,") + "%3B%20max-age=0,True)"; + } + + var requestInit = {"credentials": credentials1} + promise_test(function(test){ + var requestInit = {"credentials": credentials1} + return fetch(url + urlParameters, requestInit).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.type , "basic", "Response's type is basic"); + //check cookies sent + return fetch(RESOURCES_DIR + "inspect-headers.py?headers=cookie" , {"credentials": credentials2}); + }).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.type , "basic", "Response's type is basic"); + assert_false(resp.headers.has("Cookie") , "Cookie header is not exposed in response"); + if (credentials1 != "omit" && credentials2 != "omit") { + assert_equals(resp.headers.get("x-request-cookie") , cookies.join("; "), "Request include cookie(s)"); + } + else { + assert_false(resp.headers.has("x-request-cookie") , "Request does not have cookie(s)"); + } + //clean cookies + return fetch(url + urlCleanParameters, {"credentials": "include"}); + }).catch(function(e) { + return fetch(url + urlCleanParameters, {"credentials": "include"}).then(function() { + return Promise.reject(e); + }); + }); + }, desc); +} + +cookies("Include mode: 1 cookie", "include", "include", ["a=1"]); +cookies("Include mode: 2 cookies", "include", "include", ["b=2", "c=3"]); +cookies("Omit mode: discard cookies", "omit", "omit", ["d=4"]); +cookies("Omit mode: no cookie is stored", "omit", "include", ["e=5"]); +cookies("Omit mode: no cookie is sent", "include", "omit", ["f=6"]); +cookies("Same-origin mode: 1 cookie", "same-origin", "same-origin", ["a=1"]); +cookies("Same-origin mode: 2 cookies", "same-origin", "same-origin", ["b=2", "c=3"]); diff --git a/test/wpt/tests/fetch/api/headers/header-setcookie.any.js b/test/wpt/tests/fetch/api/headers/header-setcookie.any.js index b8a3348f5f5..cafb780c2c7 100644 --- a/test/wpt/tests/fetch/api/headers/header-setcookie.any.js +++ b/test/wpt/tests/fetch/api/headers/header-setcookie.any.js @@ -129,6 +129,23 @@ test(function () { assert_true(iterator.next().done); }, "Headers iterator is correctly updated with set-cookie changes"); +test(function () { + const headers = new Headers([ + ["set-cookie", "a"], + ["set-cookie", "b"], + ["set-cookie", "c"] + ]); + const iterator = headers[Symbol.iterator](); + assert_array_equals(iterator.next().value, ["set-cookie", "a"]); + headers.delete("set-cookie"); + headers.append("set-cookie", "d"); + headers.append("set-cookie", "e"); + headers.append("set-cookie", "f"); + assert_array_equals(iterator.next().value, ["set-cookie", "e"]); + assert_array_equals(iterator.next().value, ["set-cookie", "f"]); + assert_true(iterator.next().done); +}, "Headers iterator is correctly updated with set-cookie changes #2"); + test(function () { const headers = new Headers(headerList); assert_true(headers.has("sEt-cOoKiE")); @@ -215,6 +232,31 @@ test(function () { assert_array_equals(headers.getSetCookie(), ["z=z", "a=a", "n=n"]); }, "Headers.prototype.getSetCookie preserves header ordering"); +test(function () { + const headers = new Headers({"Set-Cookie": " a=b\n"}); + headers.append("set-cookie", "\n\rc=d "); + assert_nested_array_equals([...headers], [ + ["set-cookie", "a=b"], + ["set-cookie", "c=d"] + ]); + headers.set("set-cookie", "\te=f "); + assert_nested_array_equals([...headers], [["set-cookie", "e=f"]]); +}, "Adding Set-Cookie headers normalizes their value"); + +test(function () { + assert_throws_js(TypeError, () => { + new Headers({"set-cookie": "\0"}); + }); + + const headers = new Headers(); + assert_throws_js(TypeError, () => { + headers.append("Set-Cookie", "a\nb"); + }); + assert_throws_js(TypeError, () => { + headers.set("Set-Cookie", "a\rb"); + }); +}, "Adding invalid Set-Cookie headers throws"); + test(function () { const response = new Response(); response.headers.append("Set-Cookie", "foo=bar"); diff --git a/test/wpt/tests/fetch/api/headers/headers-no-cors.any.js b/test/wpt/tests/fetch/api/headers/headers-no-cors.any.js new file mode 100644 index 00000000000..60dbb9ef67a --- /dev/null +++ b/test/wpt/tests/fetch/api/headers/headers-no-cors.any.js @@ -0,0 +1,59 @@ +// META: global=window,worker + +"use strict"; + +promise_test(() => fetch("../cors/resources/not-cors-safelisted.json").then(res => res.json().then(runTests)), "Loading data…"); + +const longValue = "s".repeat(127); + +[ + { + "headers": ["accept", "accept-language", "content-language"], + "values": [longValue, "", longValue] + }, + { + "headers": ["accept", "accept-language", "content-language"], + "values": ["", longValue] + }, + { + "headers": ["content-type"], + "values": ["text/plain;" + "s".repeat(116), "text/plain"] + } +].forEach(testItem => { + testItem.headers.forEach(header => { + test(() => { + const noCorsHeaders = new Request("about:blank", { mode: "no-cors" }).headers; + testItem.values.forEach((value) => { + noCorsHeaders.append(header, value); + assert_equals(noCorsHeaders.get(header), testItem.values[0], '1'); + }); + noCorsHeaders.set(header, testItem.values.join(", ")); + assert_equals(noCorsHeaders.get(header), testItem.values[0], '2'); + noCorsHeaders.delete(header); + assert_false(noCorsHeaders.has(header)); + }, "\"no-cors\" Headers object cannot have " + header + " set to " + testItem.values.join(", ")); + }); +}); + +function runTests(testArray) { + testArray = testArray.concat([ + ["dpr", "2"], + ["rtt", "1.0"], + ["downlink", "-1.0"], + ["ect", "6g"], + ["save-data", "on"], + ["viewport-width", "100"], + ["width", "100"], + ["unknown", "doesitmatter"] + ]); + testArray.forEach(testItem => { + const [headerName, headerValue] = testItem; + test(() => { + const noCorsHeaders = new Request("about:blank", { mode: "no-cors" }).headers; + noCorsHeaders.append(headerName, headerValue); + assert_false(noCorsHeaders.has(headerName)); + noCorsHeaders.set(headerName, headerValue); + assert_false(noCorsHeaders.has(headerName)); + }, "\"no-cors\" Headers object cannot have " + headerName + "/" + headerValue + " as header"); + }); +} diff --git a/test/wpt/tests/fetch/api/policies/csp-blocked-worker.html b/test/wpt/tests/fetch/api/policies/csp-blocked-worker.html new file mode 100644 index 00000000000..e8660dffa94 --- /dev/null +++ b/test/wpt/tests/fetch/api/policies/csp-blocked-worker.html @@ -0,0 +1,16 @@ + + + + + Fetch in worker: blocked by CSP + + + + + + + + + \ No newline at end of file diff --git a/test/wpt/tests/fetch/api/policies/csp-blocked.html b/test/wpt/tests/fetch/api/policies/csp-blocked.html new file mode 100644 index 00000000000..99e90dfcd8f --- /dev/null +++ b/test/wpt/tests/fetch/api/policies/csp-blocked.html @@ -0,0 +1,15 @@ + + + + + Fetch: blocked by CSP + + + + + + + + + + \ No newline at end of file diff --git a/test/wpt/tests/fetch/api/policies/csp-blocked.html.headers b/test/wpt/tests/fetch/api/policies/csp-blocked.html.headers new file mode 100644 index 00000000000..c8c1e9ffbd9 --- /dev/null +++ b/test/wpt/tests/fetch/api/policies/csp-blocked.html.headers @@ -0,0 +1 @@ +Content-Security-Policy: connect-src 'none'; \ No newline at end of file diff --git a/test/wpt/tests/fetch/api/policies/csp-blocked.js b/test/wpt/tests/fetch/api/policies/csp-blocked.js new file mode 100644 index 00000000000..28653fff85c --- /dev/null +++ b/test/wpt/tests/fetch/api/policies/csp-blocked.js @@ -0,0 +1,13 @@ +if (this.document === undefined) { + importScripts("/resources/testharness.js"); + importScripts("../resources/utils.js"); +} + +//Content-Security-Policy: connect-src 'none'; cf .headers file +cspViolationUrl = RESOURCES_DIR + "top.txt"; + +promise_test(function(test) { + return promise_rejects_js(test, TypeError, fetch(cspViolationUrl)); +}, "Fetch is blocked by CSP, got a TypeError"); + +done(); diff --git a/test/wpt/tests/fetch/api/policies/csp-blocked.js.headers b/test/wpt/tests/fetch/api/policies/csp-blocked.js.headers new file mode 100644 index 00000000000..c8c1e9ffbd9 --- /dev/null +++ b/test/wpt/tests/fetch/api/policies/csp-blocked.js.headers @@ -0,0 +1 @@ +Content-Security-Policy: connect-src 'none'; \ No newline at end of file diff --git a/test/wpt/tests/fetch/api/policies/nested-policy.js b/test/wpt/tests/fetch/api/policies/nested-policy.js new file mode 100644 index 00000000000..b0d17696c33 --- /dev/null +++ b/test/wpt/tests/fetch/api/policies/nested-policy.js @@ -0,0 +1 @@ +// empty, but referrer-policy set on this file diff --git a/test/wpt/tests/fetch/api/policies/nested-policy.js.headers b/test/wpt/tests/fetch/api/policies/nested-policy.js.headers new file mode 100644 index 00000000000..7ffbf17d6be --- /dev/null +++ b/test/wpt/tests/fetch/api/policies/nested-policy.js.headers @@ -0,0 +1 @@ +Referrer-Policy: no-referrer diff --git a/test/wpt/tests/fetch/api/policies/referrer-no-referrer-service-worker.https.html b/test/wpt/tests/fetch/api/policies/referrer-no-referrer-service-worker.https.html new file mode 100644 index 00000000000..af898aa29f5 --- /dev/null +++ b/test/wpt/tests/fetch/api/policies/referrer-no-referrer-service-worker.https.html @@ -0,0 +1,18 @@ + + + + + Fetch in service worker: referrer with no-referrer policy + + + + + + + + + + + diff --git a/test/wpt/tests/fetch/api/policies/referrer-no-referrer-worker.html b/test/wpt/tests/fetch/api/policies/referrer-no-referrer-worker.html new file mode 100644 index 00000000000..dbef9bb658f --- /dev/null +++ b/test/wpt/tests/fetch/api/policies/referrer-no-referrer-worker.html @@ -0,0 +1,17 @@ + + + + + Fetch in worker: referrer with no-referrer policy + + + + + + + + + + \ No newline at end of file diff --git a/test/wpt/tests/fetch/api/policies/referrer-no-referrer.html b/test/wpt/tests/fetch/api/policies/referrer-no-referrer.html new file mode 100644 index 00000000000..22a6f34c525 --- /dev/null +++ b/test/wpt/tests/fetch/api/policies/referrer-no-referrer.html @@ -0,0 +1,15 @@ + + + + + Fetch: referrer with no-referrer policy + + + + + + + + + + \ No newline at end of file diff --git a/test/wpt/tests/fetch/api/policies/referrer-no-referrer.html.headers b/test/wpt/tests/fetch/api/policies/referrer-no-referrer.html.headers new file mode 100644 index 00000000000..7ffbf17d6be --- /dev/null +++ b/test/wpt/tests/fetch/api/policies/referrer-no-referrer.html.headers @@ -0,0 +1 @@ +Referrer-Policy: no-referrer diff --git a/test/wpt/tests/fetch/api/policies/referrer-no-referrer.js b/test/wpt/tests/fetch/api/policies/referrer-no-referrer.js new file mode 100644 index 00000000000..60600bf081c --- /dev/null +++ b/test/wpt/tests/fetch/api/policies/referrer-no-referrer.js @@ -0,0 +1,19 @@ +if (this.document === undefined) { + importScripts("/resources/testharness.js"); + importScripts("../resources/utils.js"); +} + +var fetchedUrl = RESOURCES_DIR + "inspect-headers.py?headers=origin"; + +promise_test(function(test) { + return fetch(fetchedUrl).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.type , "basic", "Response's type is basic"); + var referrer = resp.headers.get("x-request-referer"); + //Either no referrer header is sent or it is empty + if (referrer) + assert_equals(referrer, "", "request's referrer is empty"); + }); +}, "Request's referrer is empty"); + +done(); diff --git a/test/wpt/tests/fetch/api/policies/referrer-no-referrer.js.headers b/test/wpt/tests/fetch/api/policies/referrer-no-referrer.js.headers new file mode 100644 index 00000000000..7ffbf17d6be --- /dev/null +++ b/test/wpt/tests/fetch/api/policies/referrer-no-referrer.js.headers @@ -0,0 +1 @@ +Referrer-Policy: no-referrer diff --git a/test/wpt/tests/fetch/api/policies/referrer-origin-service-worker.https.html b/test/wpt/tests/fetch/api/policies/referrer-origin-service-worker.https.html new file mode 100644 index 00000000000..4018b837816 --- /dev/null +++ b/test/wpt/tests/fetch/api/policies/referrer-origin-service-worker.https.html @@ -0,0 +1,18 @@ + + + + + Fetch in service worker: referrer with no-referrer policy + + + + + + + + + + + diff --git a/test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin-service-worker.https.html b/test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin-service-worker.https.html new file mode 100644 index 00000000000..d87192e2271 --- /dev/null +++ b/test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin-service-worker.https.html @@ -0,0 +1,17 @@ + + + + + Fetch in service worker: referrer with origin-when-cross-origin policy + + + + + + + + + + diff --git a/test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin-worker.html b/test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin-worker.html new file mode 100644 index 00000000000..f95ae8cf081 --- /dev/null +++ b/test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin-worker.html @@ -0,0 +1,16 @@ + + + + + Fetch in worker: referrer with origin-when-cross-origin policy + + + + + + + + + diff --git a/test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin.html b/test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin.html new file mode 100644 index 00000000000..5cd79e4b536 --- /dev/null +++ b/test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin.html @@ -0,0 +1,16 @@ + + + + + Fetch: referrer with origin-when-cross-origin policy + + + + + + + + + + + diff --git a/test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin.html.headers b/test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin.html.headers new file mode 100644 index 00000000000..ad768e63294 --- /dev/null +++ b/test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin.html.headers @@ -0,0 +1 @@ +Referrer-Policy: origin-when-cross-origin diff --git a/test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin.js b/test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin.js new file mode 100644 index 00000000000..0adadbc5508 --- /dev/null +++ b/test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin.js @@ -0,0 +1,21 @@ +if (this.document === undefined) { + importScripts("/resources/testharness.js"); + importScripts("../resources/utils.js"); + importScripts("/common/get-host-info.sub.js"); + + // A nested importScripts() with a referrer-policy should have no effect + // on overall worker policy. + importScripts("nested-policy.js"); +} + +var referrerOrigin = location.origin + '/'; +var fetchedUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?cors&headers=referer"; + +promise_test(function(test) { + return fetch(fetchedUrl).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.headers.get("x-request-referer"), referrerOrigin, "request's referrer is " + referrerOrigin); + }); +}, "Request's referrer is origin"); + +done(); diff --git a/test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin.js.headers b/test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin.js.headers new file mode 100644 index 00000000000..ad768e63294 --- /dev/null +++ b/test/wpt/tests/fetch/api/policies/referrer-origin-when-cross-origin.js.headers @@ -0,0 +1 @@ +Referrer-Policy: origin-when-cross-origin diff --git a/test/wpt/tests/fetch/api/policies/referrer-origin-worker.html b/test/wpt/tests/fetch/api/policies/referrer-origin-worker.html new file mode 100644 index 00000000000..bb80dd54fbf --- /dev/null +++ b/test/wpt/tests/fetch/api/policies/referrer-origin-worker.html @@ -0,0 +1,17 @@ + + + + + Fetch in worker: referrer with origin policy + + + + + + + + + + \ No newline at end of file diff --git a/test/wpt/tests/fetch/api/policies/referrer-origin.html b/test/wpt/tests/fetch/api/policies/referrer-origin.html new file mode 100644 index 00000000000..b164afe01de --- /dev/null +++ b/test/wpt/tests/fetch/api/policies/referrer-origin.html @@ -0,0 +1,16 @@ + + + + + Fetch: referrer with origin policy + + + + + + + + + + + diff --git a/test/wpt/tests/fetch/api/policies/referrer-origin.html.headers b/test/wpt/tests/fetch/api/policies/referrer-origin.html.headers new file mode 100644 index 00000000000..5b29739bbdd --- /dev/null +++ b/test/wpt/tests/fetch/api/policies/referrer-origin.html.headers @@ -0,0 +1 @@ +Referrer-Policy: origin diff --git a/test/wpt/tests/fetch/api/policies/referrer-origin.js b/test/wpt/tests/fetch/api/policies/referrer-origin.js new file mode 100644 index 00000000000..918f8f207c3 --- /dev/null +++ b/test/wpt/tests/fetch/api/policies/referrer-origin.js @@ -0,0 +1,30 @@ +if (this.document === undefined) { + importScripts("/resources/testharness.js"); + importScripts("../resources/utils.js"); + + // A nested importScripts() with a referrer-policy should have no effect + // on overall worker policy. + importScripts("nested-policy.js"); +} + +var referrerOrigin = (new URL("/", location.href)).href; +var fetchedUrl = RESOURCES_DIR + "inspect-headers.py?headers=referer"; + +promise_test(function(test) { + return fetch(fetchedUrl).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.type , "basic", "Response's type is basic"); + assert_equals(resp.headers.get("x-request-referer"), referrerOrigin, "request's referrer is " + referrerOrigin); + }); +}, "Request's referrer is origin"); + +promise_test(function(test) { + var referrerUrl = "https://{{domains[www]}}:{{ports[https][0]}}/"; + return fetch(fetchedUrl, { "referrer": referrerUrl }).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.type , "basic", "Response's type is basic"); + assert_equals(resp.headers.get("x-request-referer"), referrerOrigin, "request's referrer is " + referrerOrigin); + }); +}, "Cross-origin referrer is overridden by client origin"); + +done(); diff --git a/test/wpt/tests/fetch/api/policies/referrer-origin.js.headers b/test/wpt/tests/fetch/api/policies/referrer-origin.js.headers new file mode 100644 index 00000000000..5b29739bbdd --- /dev/null +++ b/test/wpt/tests/fetch/api/policies/referrer-origin.js.headers @@ -0,0 +1 @@ +Referrer-Policy: origin diff --git a/test/wpt/tests/fetch/api/policies/referrer-unsafe-url-service-worker.https.html b/test/wpt/tests/fetch/api/policies/referrer-unsafe-url-service-worker.https.html new file mode 100644 index 00000000000..634877edae8 --- /dev/null +++ b/test/wpt/tests/fetch/api/policies/referrer-unsafe-url-service-worker.https.html @@ -0,0 +1,18 @@ + + + + + Fetch in worker: referrer with unsafe-url policy + + + + + + + + + + + diff --git a/test/wpt/tests/fetch/api/policies/referrer-unsafe-url-worker.html b/test/wpt/tests/fetch/api/policies/referrer-unsafe-url-worker.html new file mode 100644 index 00000000000..42045776b12 --- /dev/null +++ b/test/wpt/tests/fetch/api/policies/referrer-unsafe-url-worker.html @@ -0,0 +1,17 @@ + + + + + Fetch in worker: referrer with unsafe-url policy + + + + + + + + + + \ No newline at end of file diff --git a/test/wpt/tests/fetch/api/policies/referrer-unsafe-url.html b/test/wpt/tests/fetch/api/policies/referrer-unsafe-url.html new file mode 100644 index 00000000000..10dd79e3d35 --- /dev/null +++ b/test/wpt/tests/fetch/api/policies/referrer-unsafe-url.html @@ -0,0 +1,16 @@ + + + + + Fetch: referrer with unsafe-url policy + + + + + + + + + + + \ No newline at end of file diff --git a/test/wpt/tests/fetch/api/policies/referrer-unsafe-url.html.headers b/test/wpt/tests/fetch/api/policies/referrer-unsafe-url.html.headers new file mode 100644 index 00000000000..8e23770bd60 --- /dev/null +++ b/test/wpt/tests/fetch/api/policies/referrer-unsafe-url.html.headers @@ -0,0 +1 @@ +Referrer-Policy: unsafe-url diff --git a/test/wpt/tests/fetch/api/policies/referrer-unsafe-url.js b/test/wpt/tests/fetch/api/policies/referrer-unsafe-url.js new file mode 100644 index 00000000000..4d61172613e --- /dev/null +++ b/test/wpt/tests/fetch/api/policies/referrer-unsafe-url.js @@ -0,0 +1,21 @@ +if (this.document === undefined) { + importScripts("/resources/testharness.js"); + importScripts("../resources/utils.js"); + + // A nested importScripts() with a referrer-policy should have no effect + // on overall worker policy. + importScripts("nested-policy.js"); +} + +var referrerUrl = location.href; +var fetchedUrl = RESOURCES_DIR + "inspect-headers.py?headers=referer"; + +promise_test(function(test) { + return fetch(fetchedUrl).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.type , "basic", "Response's type is basic"); + assert_equals(resp.headers.get("x-request-referer"), referrerUrl, "request's referrer is " + referrerUrl); + }); +}, "Request's referrer is the full url of current document/worker"); + +done(); diff --git a/test/wpt/tests/fetch/api/policies/referrer-unsafe-url.js.headers b/test/wpt/tests/fetch/api/policies/referrer-unsafe-url.js.headers new file mode 100644 index 00000000000..8e23770bd60 --- /dev/null +++ b/test/wpt/tests/fetch/api/policies/referrer-unsafe-url.js.headers @@ -0,0 +1 @@ +Referrer-Policy: unsafe-url diff --git a/test/wpt/tests/fetch/api/redirect/redirect-empty-location.any.js b/test/wpt/tests/fetch/api/redirect/redirect-empty-location.any.js new file mode 100644 index 00000000000..487f4d42e92 --- /dev/null +++ b/test/wpt/tests/fetch/api/redirect/redirect-empty-location.any.js @@ -0,0 +1,21 @@ +// META: global=window,worker +// META: script=../resources/utils.js + +// Tests receiving a redirect response with a Location header with an empty +// value. + +const url = RESOURCES_DIR + 'redirect-empty-location.py'; + +promise_test(t => { + return promise_rejects_js(t, TypeError, fetch(url, {redirect:'follow'})); +}, 'redirect response with empty Location, follow mode'); + +promise_test(t => { + return fetch(url, {redirect:'manual'}) + .then(resp => { + assert_equals(resp.type, 'opaqueredirect'); + assert_equals(resp.status, 0); + }); +}, 'redirect response with empty Location, manual mode'); + +done(); diff --git a/test/wpt/tests/fetch/api/redirect/redirect-location-escape.tentative.any.js b/test/wpt/tests/fetch/api/redirect/redirect-location-escape.tentative.any.js new file mode 100644 index 00000000000..779ad705793 --- /dev/null +++ b/test/wpt/tests/fetch/api/redirect/redirect-location-escape.tentative.any.js @@ -0,0 +1,46 @@ +// META: global=window,worker +// META: script=../resources/utils.js + +// See https://github.com/whatwg/fetch/issues/883 for the behavior covered by +// this test. As of writing, the Fetch spec has not been updated to cover these. + +// redirectLocation tests that a Location header of |locationHeader| is resolved +// to a URL which ends in |expectedUrlSuffix|. |locationHeader| is interpreted +// as a byte sequence via isomorphic encode, as described in [INFRA]. This +// allows the caller to specify byte sequences which are not valid UTF-8. +// However, this means, e.g., U+2603 must be passed in as "\xe2\x98\x83", its +// UTF-8 encoding, not "\u2603". +// +// [INFRA] https://infra.spec.whatwg.org/#isomorphic-encode +function redirectLocation( + desc, redirectUrl, locationHeader, expectedUrlSuffix) { + promise_test(function(test) { + // Note we use escape() instead of encodeURIComponent(), so that characters + // are escaped as bytes in the isomorphic encoding. + var url = redirectUrl + '?simple=1&location=' + escape(locationHeader); + + return fetch(url, {'redirect': 'follow'}).then(function(resp) { + assert_true( + resp.url.endsWith(expectedUrlSuffix), + resp.url + ' ends with ' + expectedUrlSuffix); + }); + }, desc); +} + +var redirUrl = RESOURCES_DIR + 'redirect.py'; +redirectLocation( + 'Redirect to escaped UTF-8', redirUrl, 'top.txt?%E2%98%83%e2%98%83', + 'top.txt?%E2%98%83%e2%98%83'); +redirectLocation( + 'Redirect to unescaped UTF-8', redirUrl, 'top.txt?\xe2\x98\x83', + 'top.txt?%E2%98%83'); +redirectLocation( + 'Redirect to escaped and unescaped UTF-8', redirUrl, + 'top.txt?\xe2\x98\x83%e2%98%83', 'top.txt?%E2%98%83%e2%98%83'); +redirectLocation( + 'Escaping produces double-percent', redirUrl, 'top.txt?%\xe2\x98\x83', + 'top.txt?%%E2%98%83'); +redirectLocation( + 'Redirect to invalid UTF-8', redirUrl, 'top.txt?\xff', 'top.txt?%FF'); + +done(); diff --git a/test/wpt/tests/fetch/api/redirect/redirect-mode.any.js b/test/wpt/tests/fetch/api/redirect/redirect-mode.any.js new file mode 100644 index 00000000000..9f1ff98c65a --- /dev/null +++ b/test/wpt/tests/fetch/api/redirect/redirect-mode.any.js @@ -0,0 +1,59 @@ +// META: script=/common/get-host-info.sub.js + +var redirectLocation = "cors-top.txt"; +const { ORIGIN, REMOTE_ORIGIN } = get_host_info(); + +function testRedirect(origin, redirectStatus, redirectMode, corsMode) { + var url = new URL("../resources/redirect.py", self.location); + if (origin === "cross-origin") { + url.host = get_host_info().REMOTE_HOST; + url.port = get_host_info().HTTP_PORT; + } + + var urlParameters = "?redirect_status=" + redirectStatus; + urlParameters += "&location=" + encodeURIComponent(redirectLocation); + + var requestInit = {redirect: redirectMode, mode: corsMode}; + + promise_test(function(test) { + if (redirectMode === "error" || + (corsMode === "no-cors" && redirectMode !== "follow" && origin !== "same-origin")) + return promise_rejects_js(test, TypeError, fetch(url + urlParameters, requestInit)); + if (redirectMode === "manual") + return fetch(url + urlParameters, requestInit).then(function(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.url, url + urlParameters, "Response URL should be the original one"); + }); + if (redirectMode === "follow") + return fetch(url + urlParameters, requestInit).then(function(resp) { + if (corsMode !== "no-cors" || origin === "same-origin") { + assert_true(new URL(resp.url).pathname.endsWith(redirectLocation), "Response's url should be the redirected one"); + assert_equals(resp.status, 200, "Response's status is 200"); + } else { + assert_equals(resp.type, "opaque", "Response is opaque"); + } + }); + assert_unreached(redirectMode + " is no a valid redirect mode"); + }, origin + " redirect " + redirectStatus + " in " + redirectMode + " redirect and " + corsMode + " mode"); +} + +for (var origin of ["same-origin", "cross-origin"]) { + for (var statusCode of [301, 302, 303, 307, 308]) { + for (var redirect of ["error", "manual", "follow"]) { + for (var mode of ["cors", "no-cors"]) + testRedirect(origin, statusCode, redirect, mode); + } + } +} + +promise_test(async (t) => { + const destination = `${ORIGIN}/common/blank.html`; + // We use /common/redirect.py intentionally, as we want a CORS error. + const url = + `${REMOTE_ORIGIN}/common/redirect.py?location=${destination}`; + await promise_rejects_js(t, TypeError, fetch(url, { redirect: "manual" })); +}, "manual redirect with a CORS error should be rejected"); + +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 new file mode 100644 index 00000000000..b81b91601a8 --- /dev/null +++ b/test/wpt/tests/fetch/api/redirect/redirect-origin.any.js @@ -0,0 +1,42 @@ +// META: script=/common/utils.js +// 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); + + var requestInit = {"mode": "cors", "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"); + 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"); + }); + }); + }, 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"); + + 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"); +} + +done(); diff --git a/test/wpt/tests/fetch/api/redirect/redirect-referrer-override.any.js b/test/wpt/tests/fetch/api/redirect/redirect-referrer-override.any.js new file mode 100644 index 00000000000..56e55d79e14 --- /dev/null +++ b/test/wpt/tests/fetch/api/redirect/redirect-referrer-override.any.js @@ -0,0 +1,104 @@ +// META: timeout=long +// META: script=/common/utils.js +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +function getExpectation(expectations, init, initScenario, redirectPolicy, redirectScenario) { + let policies = [ + expectations[initPolicy][initScenario], + expectations[redirectPolicy][redirectScenario] + ]; + + if (policies.includes("omitted")) { + return null; + } else if (policies.includes("origin")) { + return referrerOrigin; + } else { + // "stripped-referrer" + return referrerUrl; + } +} + +function testReferrerAfterRedirection(desc, redirectUrl, redirectLocation, referrerPolicy, redirectReferrerPolicy, expectedReferrer) { + var url = redirectUrl; + var urlParameters = "?location=" + encodeURIComponent(redirectLocation); + var description = desc + ", " + referrerPolicy + " init, " + redirectReferrerPolicy + " redirect header "; + + if (redirectReferrerPolicy) + urlParameters += "&redirect_referrerpolicy=" + redirectReferrerPolicy; + + var requestInit = {"redirect": "follow", "referrerPolicy": referrerPolicy}; + promise_test(function(test) { + 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-referer"), expectedReferrer ? expectedReferrer : null, "Check referrer header"); + }); + }, description); +} + +var referrerOrigin = get_host_info().HTTP_ORIGIN + "/"; +var referrerUrl = location.href; + +var redirectUrl = RESOURCES_DIR + "redirect.py"; +var locationUrl = get_host_info().HTTP_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?headers=referer"; +var crossLocationUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?cors&headers=referer"; + +var expectations = { + "no-referrer": { + "same-origin": "omitted", + "cross-origin": "omitted" + }, + "no-referrer-when-downgrade": { + "same-origin": "stripped-referrer", + "cross-origin": "stripped-referrer" + }, + "origin": { + "same-origin": "origin", + "cross-origin": "origin" + }, + "origin-when-cross-origin": { + "same-origin": "stripped-referrer", + "cross-origin": "origin", + }, + "same-origin": { + "same-origin": "stripped-referrer", + "cross-origin": "omitted" + }, + "strict-origin": { + "same-origin": "origin", + "cross-origin": "origin" + }, + "strict-origin-when-cross-origin": { + "same-origin": "stripped-referrer", + "cross-origin": "origin" + }, + "unsafe-url": { + "same-origin": "stripped-referrer", + "cross-origin": "stripped-referrer" + } +}; + +for (var initPolicy in expectations) { + for (var redirectPolicy in expectations) { + + // Redirect to same-origin URL + testReferrerAfterRedirection( + "Same origin redirection", + redirectUrl, + locationUrl, + initPolicy, + redirectPolicy, + getExpectation(expectations, initPolicy, "same-origin", redirectPolicy, "same-origin")); + + // Redirect to cross-origin URL + testReferrerAfterRedirection( + "Cross origin redirection", + redirectUrl, + crossLocationUrl, + initPolicy, + redirectPolicy, + getExpectation(expectations, initPolicy, "same-origin", redirectPolicy, "cross-origin")); + } +} + +done(); diff --git a/test/wpt/tests/fetch/api/redirect/redirect-referrer.any.js b/test/wpt/tests/fetch/api/redirect/redirect-referrer.any.js new file mode 100644 index 00000000000..99fda42e69b --- /dev/null +++ b/test/wpt/tests/fetch/api/redirect/redirect-referrer.any.js @@ -0,0 +1,66 @@ +// META: timeout=long +// META: script=/common/utils.js +// META: script=../resources/utils.js +// META: script=/common/get-host-info.sub.js + +function testReferrerAfterRedirection(desc, redirectUrl, redirectLocation, referrerPolicy, redirectReferrerPolicy, expectedReferrer) { + var url = redirectUrl; + var urlParameters = "?location=" + encodeURIComponent(redirectLocation); + + if (redirectReferrerPolicy) + urlParameters += "&redirect_referrerpolicy=" + redirectReferrerPolicy; + + var requestInit = {"redirect": "follow", "referrerPolicy": referrerPolicy}; + + promise_test(function(test) { + 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-referer"), expectedReferrer ? expectedReferrer : null, "Check referrer header"); + }); + }, desc); +} + +var referrerOrigin = get_host_info().HTTP_ORIGIN + "/"; +var referrerUrl = location.href; + +var redirectUrl = RESOURCES_DIR + "redirect.py"; +var locationUrl = get_host_info().HTTP_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?headers=referer"; +var crossLocationUrl = get_host_info().HTTP_REMOTE_ORIGIN + dirname(location.pathname) + RESOURCES_DIR + "inspect-headers.py?cors&headers=referer"; + +testReferrerAfterRedirection("Same origin redirection, empty init, unsafe-url redirect header ", redirectUrl, locationUrl, "", "unsafe-url", referrerUrl); +testReferrerAfterRedirection("Same origin redirection, empty init, no-referrer-when-downgrade redirect header ", redirectUrl, locationUrl, "", "no-referrer-when-downgrade", referrerUrl); +testReferrerAfterRedirection("Same origin redirection, empty init, same-origin redirect header ", redirectUrl, locationUrl, "", "same-origin", referrerUrl); +testReferrerAfterRedirection("Same origin redirection, empty init, origin redirect header ", redirectUrl, locationUrl, "", "origin", referrerOrigin); +testReferrerAfterRedirection("Same origin redirection, empty init, origin-when-cross-origin redirect header ", redirectUrl, locationUrl, "", "origin-when-cross-origin", referrerUrl); +testReferrerAfterRedirection("Same origin redirection, empty init, no-referrer redirect header ", redirectUrl, locationUrl, "", "no-referrer", null); +testReferrerAfterRedirection("Same origin redirection, empty init, strict-origin redirect header ", redirectUrl, locationUrl, "", "strict-origin", referrerOrigin); +testReferrerAfterRedirection("Same origin redirection, empty init, strict-origin-when-cross-origin redirect header ", redirectUrl, locationUrl, "", "strict-origin-when-cross-origin", referrerUrl); + +testReferrerAfterRedirection("Same origin redirection, empty redirect header, unsafe-url init ", redirectUrl, locationUrl, "unsafe-url", "", referrerUrl); +testReferrerAfterRedirection("Same origin redirection, empty redirect header, no-referrer-when-downgrade init ", redirectUrl, locationUrl, "no-referrer-when-downgrade", "", referrerUrl); +testReferrerAfterRedirection("Same origin redirection, empty redirect header, same-origin init ", redirectUrl, locationUrl, "same-origin", "", referrerUrl); +testReferrerAfterRedirection("Same origin redirection, empty redirect header, origin init ", redirectUrl, locationUrl, "origin", "", referrerOrigin); +testReferrerAfterRedirection("Same origin redirection, empty redirect header, origin-when-cross-origin init ", redirectUrl, locationUrl, "origin-when-cross-origin", "", referrerUrl); +testReferrerAfterRedirection("Same origin redirection, empty redirect header, no-referrer init ", redirectUrl, locationUrl, "no-referrer", "", null); +testReferrerAfterRedirection("Same origin redirection, empty redirect header, strict-origin init ", redirectUrl, locationUrl, "strict-origin", "", referrerOrigin); +testReferrerAfterRedirection("Same origin redirection, empty redirect header, strict-origin-when-cross-origin init ", redirectUrl, locationUrl, "strict-origin-when-cross-origin", "", referrerUrl); + +testReferrerAfterRedirection("Cross origin redirection, empty init, unsafe-url redirect header ", redirectUrl, crossLocationUrl, "", "unsafe-url", referrerUrl); +testReferrerAfterRedirection("Cross origin redirection, empty init, no-referrer-when-downgrade redirect header ", redirectUrl, crossLocationUrl, "", "no-referrer-when-downgrade", referrerUrl); +testReferrerAfterRedirection("Cross origin redirection, empty init, same-origin redirect header ", redirectUrl, crossLocationUrl, "", "same-origin", null); +testReferrerAfterRedirection("Cross origin redirection, empty init, origin redirect header ", redirectUrl, crossLocationUrl, "", "origin", referrerOrigin); +testReferrerAfterRedirection("Cross origin redirection, empty init, origin-when-cross-origin redirect header ", redirectUrl, crossLocationUrl, "", "origin-when-cross-origin", referrerOrigin); +testReferrerAfterRedirection("Cross origin redirection, empty init, no-referrer redirect header ", redirectUrl, crossLocationUrl, "", "no-referrer", null); +testReferrerAfterRedirection("Cross origin redirection, empty init, strict-origin redirect header ", redirectUrl, crossLocationUrl, "", "strict-origin", referrerOrigin); +testReferrerAfterRedirection("Cross origin redirection, empty init, strict-origin-when-cross-origin redirect header ", redirectUrl, crossLocationUrl, "", "strict-origin-when-cross-origin", referrerOrigin); + +testReferrerAfterRedirection("Cross origin redirection, empty redirect header, unsafe-url init ", redirectUrl, crossLocationUrl, "unsafe-url", "", referrerUrl); +testReferrerAfterRedirection("Cross origin redirection, empty redirect header, no-referrer-when-downgrade init ", redirectUrl, crossLocationUrl, "no-referrer-when-downgrade", "", referrerUrl); +testReferrerAfterRedirection("Cross origin redirection, empty redirect header, same-origin init ", redirectUrl, crossLocationUrl, "same-origin", "", null); +testReferrerAfterRedirection("Cross origin redirection, empty redirect header, origin init ", redirectUrl, crossLocationUrl, "origin", "", referrerOrigin); +testReferrerAfterRedirection("Cross origin redirection, empty redirect header, origin-when-cross-origin init ", redirectUrl, crossLocationUrl, "origin-when-cross-origin", "", referrerOrigin); +testReferrerAfterRedirection("Cross origin redirection, empty redirect header, no-referrer init ", redirectUrl, crossLocationUrl, "no-referrer", "", null); +testReferrerAfterRedirection("Cross origin redirection, empty redirect header, strict-origin init ", redirectUrl, crossLocationUrl, "strict-origin", "", referrerOrigin); +testReferrerAfterRedirection("Cross origin redirection, empty redirect header, strict-origin-when-cross-origin init ", redirectUrl, crossLocationUrl, "strict-origin-when-cross-origin", "", referrerOrigin); + +done(); diff --git a/test/wpt/tests/fetch/api/redirect/redirect-upload.h2.any.js b/test/wpt/tests/fetch/api/redirect/redirect-upload.h2.any.js new file mode 100644 index 00000000000..521bd3adc28 --- /dev/null +++ b/test/wpt/tests/fetch/api/redirect/redirect-upload.h2.any.js @@ -0,0 +1,33 @@ +// META: global=window,worker +// META: script=../resources/utils.js +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js + +const redirectUrl = RESOURCES_DIR + "redirect.h2.py"; +const redirectLocation = "top.txt"; + +async function fetchStreamRedirect(statusCode) { + const url = RESOURCES_DIR + "redirect.h2.py" + + `?redirect_status=${statusCode}&location=${redirectLocation}`; + const requestInit = {method: "POST"}; + requestInit["body"] = new ReadableStream({start: controller => { + const encoder = new TextEncoder(); + controller.enqueue(encoder.encode("Test")); + controller.close(); + }}); + requestInit.duplex = "half"; + return fetch(url, requestInit); +} + +promise_test(async () => { + const resp = await fetchStreamRedirect(303); + assert_equals(resp.status, 200); + assert_true(new URL(resp.url).pathname.endsWith(redirectLocation), + "Response's url should be the redirected one"); +}, "Fetch upload streaming should be accepted on 303"); + +for (const statusCode of [301, 302, 307, 308]) { + promise_test(t => { + return promise_rejects_js(t, TypeError, fetchStreamRedirect(statusCode)); + }, `Fetch upload streaming should fail on ${statusCode}`); +} diff --git a/test/wpt/tests/fetch/api/request/destination/fetch-destination-frame.https.html b/test/wpt/tests/fetch/api/request/destination/fetch-destination-frame.https.html new file mode 100644 index 00000000000..f3f9f7856d5 --- /dev/null +++ b/test/wpt/tests/fetch/api/request/destination/fetch-destination-frame.https.html @@ -0,0 +1,51 @@ + +Fetch destination tests for resources with no load event + + + + + diff --git a/test/wpt/tests/fetch/api/request/destination/fetch-destination-iframe.https.html b/test/wpt/tests/fetch/api/request/destination/fetch-destination-iframe.https.html new file mode 100644 index 00000000000..1aa5a5613b1 --- /dev/null +++ b/test/wpt/tests/fetch/api/request/destination/fetch-destination-iframe.https.html @@ -0,0 +1,51 @@ + +Fetch destination tests for resources with no load event + + + + + diff --git a/test/wpt/tests/fetch/api/request/destination/fetch-destination-no-load-event.https.html b/test/wpt/tests/fetch/api/request/destination/fetch-destination-no-load-event.https.html new file mode 100644 index 00000000000..1778bf2581a --- /dev/null +++ b/test/wpt/tests/fetch/api/request/destination/fetch-destination-no-load-event.https.html @@ -0,0 +1,124 @@ + +Fetch destination tests for resources with no load event + + + + + diff --git a/test/wpt/tests/fetch/api/request/destination/fetch-destination-prefetch.https.html b/test/wpt/tests/fetch/api/request/destination/fetch-destination-prefetch.https.html new file mode 100644 index 00000000000..db99202df87 --- /dev/null +++ b/test/wpt/tests/fetch/api/request/destination/fetch-destination-prefetch.https.html @@ -0,0 +1,46 @@ + +Fetch destination test for prefetching + + + + + + diff --git a/test/wpt/tests/fetch/api/request/destination/fetch-destination-worker.https.html b/test/wpt/tests/fetch/api/request/destination/fetch-destination-worker.https.html new file mode 100644 index 00000000000..5935c1ff31e --- /dev/null +++ b/test/wpt/tests/fetch/api/request/destination/fetch-destination-worker.https.html @@ -0,0 +1,60 @@ + +Fetch destination tests for resources with no load event + + + + + diff --git a/test/wpt/tests/fetch/api/request/destination/fetch-destination.https.html b/test/wpt/tests/fetch/api/request/destination/fetch-destination.https.html new file mode 100644 index 00000000000..0094b0b6fe8 --- /dev/null +++ b/test/wpt/tests/fetch/api/request/destination/fetch-destination.https.html @@ -0,0 +1,435 @@ + +Fetch destination tests + + + + + + diff --git a/test/wpt/tests/fetch/api/request/destination/resources/dummy b/test/wpt/tests/fetch/api/request/destination/resources/dummy new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/wpt/tests/fetch/api/request/destination/resources/dummy.es b/test/wpt/tests/fetch/api/request/destination/resources/dummy.es new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/wpt/tests/fetch/api/request/destination/resources/dummy.es.headers b/test/wpt/tests/fetch/api/request/destination/resources/dummy.es.headers new file mode 100644 index 00000000000..9bb8badcad4 --- /dev/null +++ b/test/wpt/tests/fetch/api/request/destination/resources/dummy.es.headers @@ -0,0 +1 @@ +Content-Type: text/event-stream diff --git a/test/wpt/tests/fetch/api/request/destination/resources/dummy.html b/test/wpt/tests/fetch/api/request/destination/resources/dummy.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/wpt/tests/fetch/api/request/destination/resources/dummy.png b/test/wpt/tests/fetch/api/request/destination/resources/dummy.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/fetch/api/request/destination/resources/dummy.ttf b/test/wpt/tests/fetch/api/request/destination/resources/dummy.ttf new file mode 100644 index 0000000000000000000000000000000000000000..9023592ef5aa83a03dd6957398897a585062ca57 GIT binary patch literal 2528 zcmds3+iM(E9RAMC>}--IZHkSlbcJb}Hmx+fn~4|=c}SaNsWzb@-3F9mI_^#~3%fJR z?rbg~7_kpEQi_7Nf1zrjK~Q`sQnW2nkwV|M-@%oK-E?>x_(MpM29?acOqAH}eAZX8>i{v90{QD@UK8 z?l;0S4h7BS^-meAn|!xZ@)uj54c;RECHbzRm$TF>nv6e6K2fq3%b3Ch^~cB?u2r(v zkH7ad5c`2P>9SY#cXvOjFn>GsdB|P~`n~De%#NWyu}z}@xPEE<^GzId1b6hq`YrNJ zpl7(G&#mANPHPA{)^6*E!$^@bL|Q0mLqc}TB|Swb8%8pe2%7wX7*!uCHz~QWfyJ-r z7tPW^=Wn!Ro%h$|>{uSdXvUa|I&fOQrS7LPvS9~Z(v&zMA=;65&=C=`DhY|mZ&Kj!3HhbNxDPn<$e@rAG|9>`>_U%Yaa|m@d0+T+9$}A07}*9%}w^Ondom zl3bI=hUcy>--uAx7wLGEX&Kzn_%3s9JFtg_4YL!pi){|FXP{H;ZW!kJ85v$FZVvZc z=7R1t%y+FTKz4K3D>LVrE9j_0Kg^W>kV`b=3OX8csX3V|_H9EhakU|rxWPtGG$xDs zeRLLqoPhkgUkcxKN!R$Lr8Sp8i&%_ketg8+5v}5Y_$i__v?%)`I)-*-GNN_L7dTC! z$#2##gbi9?mv|+j6|{;sB3i|`_#mP+>{8kyItD{YMzl`3g%NltV+j=$Fb4-d3>-ub zhlow2(MK>aNlgJoLYZ6^7Cnmetngdg!aW6>yiIwPzj@l!;1b)kFc{MzW$@m3p1uag z87D`H8(I%iBJ=u;J%|+dLb#J*WgAu=<5fZ*DXp;5R9MY}C{;>IjO(NK5lxbD9RfzY z@=~QR=lI6K+#$nE_oaaWNmxCd;m?tPvxY zJ8xC9c9rx5g?W}XCSRSBcZdsV~Z&>YL1E97mjWcg0TE4dJ(nei;|UEZ=>^4@JlJ9c3=csI~@nRO6C WZTNf5{_GpcUH|0vRERIFfAlvXi@|mP literal 0 HcmV?d00001 diff --git a/test/wpt/tests/fetch/api/request/destination/resources/dummy_audio.mp3 b/test/wpt/tests/fetch/api/request/destination/resources/dummy_audio.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..0091330f1ecd9ba5a70c6e5e6da284a061e2b99f GIT binary patch literal 20498 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 literal 0 HcmV?d00001 diff --git a/test/wpt/tests/fetch/api/request/destination/resources/dummy_video.mp4 b/test/wpt/tests/fetch/api/request/destination/resources/dummy_video.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..7022e75c15ee5263f64bf5c2d86b19b72d2ab5f9 GIT binary patch literal 67369 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%0A2m7yRqQ_nS@nN}nLUP!`1NpmtZgHEInVp#_ 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 literal 0 HcmV?d00001 diff --git a/test/wpt/tests/fetch/api/request/destination/resources/empty.https.html b/test/wpt/tests/fetch/api/request/destination/resources/empty.https.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/wpt/tests/fetch/api/request/destination/resources/fetch-destination-worker-frame.js b/test/wpt/tests/fetch/api/request/destination/resources/fetch-destination-worker-frame.js new file mode 100644 index 00000000000..b69de0b7df9 --- /dev/null +++ b/test/wpt/tests/fetch/api/request/destination/resources/fetch-destination-worker-frame.js @@ -0,0 +1,20 @@ +self.addEventListener('fetch', function(event) { + if (event.request.url.includes('dummy')) { + event.waitUntil(async function() { + let destination = new URL(event.request.url).searchParams.get("dest"); + let clients = await self.clients.matchAll({"includeUncontrolled": true}); + clients.forEach(function(client) { + if (client.url.includes("fetch-destination-frame")) { + if (event.request.destination == destination) { + client.postMessage("PASS"); + } else { + client.postMessage("FAIL"); + } + } + }) + }()); + } + event.respondWith(fetch(event.request)); +}); + + diff --git a/test/wpt/tests/fetch/api/request/destination/resources/fetch-destination-worker-iframe.js b/test/wpt/tests/fetch/api/request/destination/resources/fetch-destination-worker-iframe.js new file mode 100644 index 00000000000..76345839ead --- /dev/null +++ b/test/wpt/tests/fetch/api/request/destination/resources/fetch-destination-worker-iframe.js @@ -0,0 +1,20 @@ +self.addEventListener('fetch', function(event) { + if (event.request.url.includes('dummy')) { + event.waitUntil(async function() { + let destination = new URL(event.request.url).searchParams.get("dest"); + let clients = await self.clients.matchAll({"includeUncontrolled": true}); + clients.forEach(function(client) { + if (client.url.includes("fetch-destination-iframe")) { + if (event.request.destination == destination) { + client.postMessage("PASS"); + } else { + client.postMessage("FAIL"); + } + } + }) + }()); + } + event.respondWith(fetch(event.request)); +}); + + diff --git a/test/wpt/tests/fetch/api/request/destination/resources/fetch-destination-worker-no-load-event.js b/test/wpt/tests/fetch/api/request/destination/resources/fetch-destination-worker-no-load-event.js new file mode 100644 index 00000000000..a583b1272a1 --- /dev/null +++ b/test/wpt/tests/fetch/api/request/destination/resources/fetch-destination-worker-no-load-event.js @@ -0,0 +1,20 @@ +self.addEventListener('fetch', function(event) { + const url = event.request.url; + if (url.includes('dummy') && url.includes('?')) { + event.waitUntil(async function() { + let destination = new URL(url).searchParams.get("dest"); + var result = "FAIL"; + if (event.request.destination == destination || + (event.request.destination == "empty" && destination == "")) { + result = "PASS"; + } + let cl = await clients.matchAll({includeUncontrolled: true}); + for (i = 0; i < cl.length; i++) { + cl[i].postMessage(result); + } + }()) + } + event.respondWith(fetch(event.request)); +}); + + diff --git a/test/wpt/tests/fetch/api/request/destination/resources/fetch-destination-worker.js b/test/wpt/tests/fetch/api/request/destination/resources/fetch-destination-worker.js new file mode 100644 index 00000000000..904009c1721 --- /dev/null +++ b/test/wpt/tests/fetch/api/request/destination/resources/fetch-destination-worker.js @@ -0,0 +1,12 @@ +self.addEventListener('fetch', function(event) { + if (event.request.url.includes('dummy')) { + let destination = new URL(event.request.url).searchParams.get("dest"); + if (event.request.destination == destination || + (event.request.destination == "empty" && destination == "")) { + event.respondWith(fetch(event.request)); + } else { + event.respondWith(Response.error()); + } + } +}); + diff --git a/test/wpt/tests/fetch/api/request/destination/resources/importer.js b/test/wpt/tests/fetch/api/request/destination/resources/importer.js new file mode 100644 index 00000000000..9568474d505 --- /dev/null +++ b/test/wpt/tests/fetch/api/request/destination/resources/importer.js @@ -0,0 +1 @@ +importScripts("dummy?t=importScripts&dest=script"); diff --git a/test/wpt/tests/fetch/api/request/multi-globals/current/current.html b/test/wpt/tests/fetch/api/request/multi-globals/current/current.html new file mode 100644 index 00000000000..9bb6e0bbf3f --- /dev/null +++ b/test/wpt/tests/fetch/api/request/multi-globals/current/current.html @@ -0,0 +1,3 @@ + +Current page used as a test helper + diff --git a/test/wpt/tests/fetch/api/request/multi-globals/incumbent/incumbent.html b/test/wpt/tests/fetch/api/request/multi-globals/incumbent/incumbent.html new file mode 100644 index 00000000000..a885b8a0a73 --- /dev/null +++ b/test/wpt/tests/fetch/api/request/multi-globals/incumbent/incumbent.html @@ -0,0 +1,14 @@ + +Incumbent page used as a test helper + + + + diff --git a/test/wpt/tests/fetch/api/request/multi-globals/url-parsing.html b/test/wpt/tests/fetch/api/request/multi-globals/url-parsing.html new file mode 100644 index 00000000000..df60e72507f --- /dev/null +++ b/test/wpt/tests/fetch/api/request/multi-globals/url-parsing.html @@ -0,0 +1,27 @@ + +Request constructor URL parsing, with multiple globals in play + + + + + + + + + + diff --git a/test/wpt/tests/fetch/api/request/request-cache-default-conditional.any.js b/test/wpt/tests/fetch/api/request/request-cache-default-conditional.any.js new file mode 100644 index 00000000000..c5b2001cc8f --- /dev/null +++ b/test/wpt/tests/fetch/api/request/request-cache-default-conditional.any.js @@ -0,0 +1,170 @@ +// META: global=window,worker +// META: title=Request cache - default with conditional requests +// META: timeout=long +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=request-cache.js + +var tests = [ + { + name: 'RequestCache "default" mode with an If-Modified-Since header (following a request without additional headers) is treated similarly to "no-store"', + state: "stale", + request_cache: ["default", "default"], + request_headers: [{}, {"If-Modified-Since": now.toGMTString()}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, true], + }, + { + name: 'RequestCache "default" mode with an If-Modified-Since header (following a request without additional headers) is treated similarly to "no-store"', + state: "fresh", + request_cache: ["default", "default"], + request_headers: [{}, {"If-Modified-Since": now.toGMTString()}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, true], + }, + { + name: 'RequestCache "default" mode with an If-Modified-Since header is treated similarly to "no-store"', + state: "stale", + request_cache: ["default", "default"], + request_headers: [{"If-Modified-Since": now.toGMTString()}, {}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [true, false], + }, + { + name: 'RequestCache "default" mode with an If-Modified-Since header is treated similarly to "no-store"', + state: "fresh", + request_cache: ["default", "default"], + request_headers: [{"If-Modified-Since": now.toGMTString()}, {}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [true, false], + }, + { + name: 'RequestCache "default" mode with an If-None-Match header (following a request without additional headers) is treated similarly to "no-store"', + state: "stale", + request_cache: ["default", "default"], + request_headers: [{}, {"If-None-Match": '"foo"'}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, true], + }, + { + name: 'RequestCache "default" mode with an If-None-Match header (following a request without additional headers) is treated similarly to "no-store"', + state: "fresh", + request_cache: ["default", "default"], + request_headers: [{}, {"If-None-Match": '"foo"'}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, true], + }, + { + name: 'RequestCache "default" mode with an If-None-Match header is treated similarly to "no-store"', + state: "stale", + request_cache: ["default", "default"], + request_headers: [{"If-None-Match": '"foo"'}, {}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [true, false], + }, + { + name: 'RequestCache "default" mode with an If-None-Match header is treated similarly to "no-store"', + state: "fresh", + request_cache: ["default", "default"], + request_headers: [{"If-None-Match": '"foo"'}, {}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [true, false], + }, + { + name: 'RequestCache "default" mode with an If-Unmodified-Since header (following a request without additional headers) is treated similarly to "no-store"', + state: "stale", + request_cache: ["default", "default"], + request_headers: [{}, {"If-Unmodified-Since": now.toGMTString()}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, true], + }, + { + name: 'RequestCache "default" mode with an If-Unmodified-Since header (following a request without additional headers) is treated similarly to "no-store"', + state: "fresh", + request_cache: ["default", "default"], + request_headers: [{}, {"If-Unmodified-Since": now.toGMTString()}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, true], + }, + { + name: 'RequestCache "default" mode with an If-Unmodified-Since header is treated similarly to "no-store"', + state: "stale", + request_cache: ["default", "default"], + request_headers: [{"If-Unmodified-Since": now.toGMTString()}, {}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [true, false], + }, + { + name: 'RequestCache "default" mode with an If-Unmodified-Since header is treated similarly to "no-store"', + state: "fresh", + request_cache: ["default", "default"], + request_headers: [{"If-Unmodified-Since": now.toGMTString()}, {}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [true, false], + }, + { + name: 'RequestCache "default" mode with an If-Match header (following a request without additional headers) is treated similarly to "no-store"', + state: "stale", + request_cache: ["default", "default"], + request_headers: [{}, {"If-Match": '"foo"'}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, true], + }, + { + name: 'RequestCache "default" mode with an If-Match header (following a request without additional headers) is treated similarly to "no-store"', + state: "fresh", + request_cache: ["default", "default"], + request_headers: [{}, {"If-Match": '"foo"'}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, true], + }, + { + name: 'RequestCache "default" mode with an If-Match header is treated similarly to "no-store"', + state: "stale", + request_cache: ["default", "default"], + request_headers: [{"If-Match": '"foo"'}, {}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [true, false], + }, + { + name: 'RequestCache "default" mode with an If-Match header is treated similarly to "no-store"', + state: "fresh", + request_cache: ["default", "default"], + request_headers: [{"If-Match": '"foo"'}, {}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [true, false], + }, + { + name: 'RequestCache "default" mode with an If-Range header (following a request without additional headers) is treated similarly to "no-store"', + state: "stale", + request_cache: ["default", "default"], + request_headers: [{}, {"If-Range": '"foo"'}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, true], + }, + { + name: 'RequestCache "default" mode with an If-Range header (following a request without additional headers) is treated similarly to "no-store"', + state: "fresh", + request_cache: ["default", "default"], + request_headers: [{}, {"If-Range": '"foo"'}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, true], + }, + { + name: 'RequestCache "default" mode with an If-Range header is treated similarly to "no-store"', + state: "stale", + request_cache: ["default", "default"], + request_headers: [{"If-Range": '"foo"'}, {}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [true, false], + }, + { + name: 'RequestCache "default" mode with an If-Range header is treated similarly to "no-store"', + state: "fresh", + request_cache: ["default", "default"], + request_headers: [{"If-Range": '"foo"'}, {}], + expected_validation_headers: [false, false], + expected_no_cache_headers: [true, false], + }, +]; +run_tests(tests); diff --git a/test/wpt/tests/fetch/api/request/request-cache-default.any.js b/test/wpt/tests/fetch/api/request/request-cache-default.any.js new file mode 100644 index 00000000000..dfa8369c9a3 --- /dev/null +++ b/test/wpt/tests/fetch/api/request/request-cache-default.any.js @@ -0,0 +1,39 @@ +// META: global=window,worker +// META: title=Request cache - default +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=request-cache.js + +var tests = [ + { + name: 'RequestCache "default" mode checks the cache for previously cached content and goes to the network for stale responses', + state: "stale", + request_cache: ["default", "default"], + expected_validation_headers: [false, true], + expected_no_cache_headers: [false, false], + }, + { + name: 'RequestCache "default" mode checks the cache for previously cached content and avoids going to the network if a fresh response exists', + state: "fresh", + request_cache: ["default", "default"], + expected_validation_headers: [false], + expected_no_cache_headers: [false], + }, + { + name: 'Responses with the "Cache-Control: no-store" header are not stored in the cache', + state: "stale", + cache_control: "no-store", + request_cache: ["default", "default"], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, false], + }, + { + name: 'Responses with the "Cache-Control: no-store" header are not stored in the cache', + state: "fresh", + cache_control: "no-store", + request_cache: ["default", "default"], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, false], + }, +]; +run_tests(tests); diff --git a/test/wpt/tests/fetch/api/request/request-cache-force-cache.any.js b/test/wpt/tests/fetch/api/request/request-cache-force-cache.any.js new file mode 100644 index 00000000000..00dce096c72 --- /dev/null +++ b/test/wpt/tests/fetch/api/request/request-cache-force-cache.any.js @@ -0,0 +1,67 @@ +// META: global=window,worker +// META: title=Request cache - force-cache +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=request-cache.js + +var tests = [ + { + name: 'RequestCache "force-cache" mode checks the cache for previously cached content and avoid revalidation for stale responses', + state: "stale", + request_cache: ["default", "force-cache"], + expected_validation_headers: [false], + expected_no_cache_headers: [false], + }, + { + name: 'RequestCache "force-cache" mode checks the cache for previously cached content and avoid revalidation for fresh responses', + state: "fresh", + request_cache: ["default", "force-cache"], + expected_validation_headers: [false], + expected_no_cache_headers: [false], + }, + { + name: 'RequestCache "force-cache" mode checks the cache for previously cached content and goes to the network if a cached response is not found', + state: "stale", + request_cache: ["force-cache"], + expected_validation_headers: [false], + expected_no_cache_headers: [false], + }, + { + name: 'RequestCache "force-cache" mode checks the cache for previously cached content and goes to the network if a cached response is not found', + state: "fresh", + request_cache: ["force-cache"], + expected_validation_headers: [false], + expected_no_cache_headers: [false], + }, + { + name: 'RequestCache "force-cache" mode checks the cache for previously cached content and goes to the network if a cached response would vary', + state: "stale", + vary: "*", + request_cache: ["default", "force-cache"], + expected_validation_headers: [false, true], + expected_no_cache_headers: [false, false], + }, + { + name: 'RequestCache "force-cache" mode checks the cache for previously cached content and goes to the network if a cached response would vary', + state: "fresh", + vary: "*", + request_cache: ["default", "force-cache"], + expected_validation_headers: [false, true], + expected_no_cache_headers: [false, false], + }, + { + name: 'RequestCache "force-cache" stores the response in the cache if it goes to the network', + state: "stale", + request_cache: ["force-cache", "default"], + expected_validation_headers: [false, true], + expected_no_cache_headers: [false, false], + }, + { + name: 'RequestCache "force-cache" stores the response in the cache if it goes to the network', + state: "fresh", + request_cache: ["force-cache", "default"], + expected_validation_headers: [false], + expected_no_cache_headers: [false], + }, +]; +run_tests(tests); diff --git a/test/wpt/tests/fetch/api/request/request-cache-no-cache.any.js b/test/wpt/tests/fetch/api/request/request-cache-no-cache.any.js new file mode 100644 index 00000000000..41fc22baf23 --- /dev/null +++ b/test/wpt/tests/fetch/api/request/request-cache-no-cache.any.js @@ -0,0 +1,25 @@ +// META: global=window,worker +// META: title=Request cache : no-cache +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=request-cache.js + +var tests = [ + { + name: 'RequestCache "no-cache" mode revalidates stale responses found in the cache', + state: "stale", + request_cache: ["default", "no-cache"], + expected_validation_headers: [false, true], + expected_no_cache_headers: [false, false], + expected_max_age_headers: [false, true], + }, + { + name: 'RequestCache "no-cache" mode revalidates fresh responses found in the cache', + state: "fresh", + request_cache: ["default", "no-cache"], + expected_validation_headers: [false, true], + expected_no_cache_headers: [false, false], + expected_max_age_headers: [false, true], + }, +]; +run_tests(tests); diff --git a/test/wpt/tests/fetch/api/request/request-cache-no-store.any.js b/test/wpt/tests/fetch/api/request/request-cache-no-store.any.js new file mode 100644 index 00000000000..9a28718bf22 --- /dev/null +++ b/test/wpt/tests/fetch/api/request/request-cache-no-store.any.js @@ -0,0 +1,37 @@ +// META: global=window,worker +// META: title=Request cache - no store +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=request-cache.js + +var tests = [ + { + name: 'RequestCache "no-store" mode does not check the cache for previously cached content and goes to the network regardless', + state: "stale", + request_cache: ["default", "no-store"], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, true], + }, + { + name: 'RequestCache "no-store" mode does not check the cache for previously cached content and goes to the network regardless', + state: "fresh", + request_cache: ["default", "no-store"], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, true], + }, + { + name: 'RequestCache "no-store" mode does not store the response in the cache', + state: "stale", + request_cache: ["no-store", "default"], + expected_validation_headers: [false, false], + expected_no_cache_headers: [true, false], + }, + { + name: 'RequestCache "no-store" mode does not store the response in the cache', + state: "fresh", + request_cache: ["no-store", "default"], + expected_validation_headers: [false, false], + expected_no_cache_headers: [true, false], + }, +]; +run_tests(tests); diff --git a/test/wpt/tests/fetch/api/request/request-cache-only-if-cached.any.js b/test/wpt/tests/fetch/api/request/request-cache-only-if-cached.any.js new file mode 100644 index 00000000000..1305787c7c1 --- /dev/null +++ b/test/wpt/tests/fetch/api/request/request-cache-only-if-cached.any.js @@ -0,0 +1,66 @@ +// META: global=window,dedicatedworker,sharedworker +// META: title=Request cache - only-if-cached +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=request-cache.js + +// FIXME: avoid mixed content requests to enable service worker global +var tests = [ + { + name: 'RequestCache "only-if-cached" mode checks the cache for previously cached content and avoids revalidation for stale responses', + state: "stale", + request_cache: ["default", "only-if-cached"], + expected_validation_headers: [false], + expected_no_cache_headers: [false] + }, + { + name: 'RequestCache "only-if-cached" mode checks the cache for previously cached content and avoids revalidation for fresh responses', + state: "fresh", + request_cache: ["default", "only-if-cached"], + expected_validation_headers: [false], + expected_no_cache_headers: [false] + }, + { + name: 'RequestCache "only-if-cached" mode checks the cache for previously cached content and does not go to the network if a cached response is not found', + state: "fresh", + request_cache: ["only-if-cached"], + response: ["error"], + expected_validation_headers: [], + expected_no_cache_headers: [] + }, + { + name: 'RequestCache "only-if-cached" (with "same-origin") uses cached same-origin redirects to same-origin content', + state: "fresh", + request_cache: ["default", "only-if-cached"], + redirect: "same-origin", + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, false], + }, + { + name: 'RequestCache "only-if-cached" (with "same-origin") uses cached same-origin redirects to same-origin content', + state: "stale", + request_cache: ["default", "only-if-cached"], + redirect: "same-origin", + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, false], + }, + { + name: 'RequestCache "only-if-cached" (with "same-origin") does not follow redirects across origins and rejects', + state: "fresh", + request_cache: ["default", "only-if-cached"], + redirect: "cross-origin", + response: [null, "error"], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, false], + }, + { + name: 'RequestCache "only-if-cached" (with "same-origin") does not follow redirects across origins and rejects', + state: "stale", + request_cache: ["default", "only-if-cached"], + redirect: "cross-origin", + response: [null, "error"], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, false], + }, +]; +run_tests(tests); diff --git a/test/wpt/tests/fetch/api/request/request-cache-reload.any.js b/test/wpt/tests/fetch/api/request/request-cache-reload.any.js new file mode 100644 index 00000000000..c7bfffb3988 --- /dev/null +++ b/test/wpt/tests/fetch/api/request/request-cache-reload.any.js @@ -0,0 +1,51 @@ +// META: global=window,worker +// META: title=Request cache - reload +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=request-cache.js + +var tests = [ + { + name: 'RequestCache "reload" mode does not check the cache for previously cached content and goes to the network regardless', + state: "stale", + request_cache: ["default", "reload"], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, true], + }, + { + name: 'RequestCache "reload" mode does not check the cache for previously cached content and goes to the network regardless', + state: "fresh", + request_cache: ["default", "reload"], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, true], + }, + { + name: 'RequestCache "reload" mode does store the response in the cache', + state: "stale", + request_cache: ["reload", "default"], + expected_validation_headers: [false, true], + expected_no_cache_headers: [true, false], + }, + { + name: 'RequestCache "reload" mode does store the response in the cache', + state: "fresh", + request_cache: ["reload", "default"], + expected_validation_headers: [false], + expected_no_cache_headers: [true], + }, + { + name: 'RequestCache "reload" mode does store the response in the cache even if a previous response is already stored', + state: "stale", + request_cache: ["default", "reload", "default"], + expected_validation_headers: [false, false, true], + expected_no_cache_headers: [false, true, false], + }, + { + name: 'RequestCache "reload" mode does store the response in the cache even if a previous response is already stored', + state: "fresh", + request_cache: ["default", "reload", "default"], + expected_validation_headers: [false, false], + expected_no_cache_headers: [false, true], + }, +]; +run_tests(tests); diff --git a/test/wpt/tests/fetch/api/request/request-cache.js b/test/wpt/tests/fetch/api/request/request-cache.js new file mode 100644 index 00000000000..f2fbecf4969 --- /dev/null +++ b/test/wpt/tests/fetch/api/request/request-cache.js @@ -0,0 +1,223 @@ +/** + * Each test is run twice: once using etag/If-None-Match and once with + * date/If-Modified-Since. Each test run gets its own URL and randomized + * content and operates independently. + * + * The test steps are run with request_cache.length fetch requests issued + * and their immediate results sanity-checked. The cache.py server script + * stashes an entry containing any If-None-Match, If-Modified-Since, Pragma, + * and Cache-Control observed headers for each request it receives. When + * the test fetches have run, this state is retrieved from cache.py and the + * expected_* lists are checked, including their length. + * + * This means that if a request_* fetch is expected to hit the cache and not + * touch the network, then there will be no entry for it in the expect_* + * lists. AKA (request_cache.length - expected_validation_headers.length) + * should equal the number of cache hits that didn't touch the network. + * + * Test dictionary keys: + * - state: required string that determines whether the Expires response for + * the fetched document should be set in the future ("fresh") or past + * ("stale"). + * - vary: optional string to be passed to the server for it to quote back + * in a Vary header on the response to us. + * - cache_control: optional string to be passed to the server for it to + * quote back in a Cache-Control header on the response to us. + * - redirect: optional string "same-origin" or "cross-origin". If + * provided, the server will issue an absolute redirect to the script on + * the same or a different origin, as appropriate. The redirected + * location is the script with the redirect parameter removed, so the + * content/state/etc. will be as if you hadn't specified a redirect. + * - request_cache: required array of cache modes to use (via `cache`). + * - request_headers: optional array of explicit fetch `headers` arguments. + * If provided, the server will log an empty dictionary for each request + * instead of the request headers it would normally log. + * - response: optional array of specialized response handling. Right now, + * "error" array entries indicate a network error response is expected + * which will reject with a TypeError. + * - expected_validation_headers: required boolean array indicating whether + * the server should have seen an If-None-Match/If-Modified-Since header + * in the request. + * - expected_no_cache_headers: required boolean array indicating whether + * the server should have seen Pragma/Cache-control:no-cache headers in + * the request. + * - expected_max_age_headers: optional boolean array indicating whether + * the server should have seen a Cache-Control:max-age=0 header in the + * request. + */ + +var now = new Date(); + +function base_path() { + return location.pathname.replace(/\/[^\/]*$/, '/'); +} +function make_url(uuid, id, value, content, info) { + var dates = { + fresh: new Date(now.getFullYear() + 1, now.getMonth(), now.getDay()).toGMTString(), + stale: new Date(now.getFullYear() - 1, now.getMonth(), now.getDay()).toGMTString(), + }; + var vary = ""; + if ("vary" in info) { + vary = "&vary=" + info.vary; + } + var cache_control = ""; + if ("cache_control" in info) { + cache_control = "&cache_control=" + info.cache_control; + } + var redirect = ""; + + var ignore_request_headers = ""; + if ("request_headers" in info) { + // Ignore the request headers that we send since they may be synthesized by the test. + ignore_request_headers = "&ignore"; + } + var url_sans_redirect = "resources/cache.py?token=" + uuid + + "&content=" + content + + "&" + id + "=" + value + + "&expires=" + dates[info.state] + + vary + cache_control + ignore_request_headers; + // If there's a redirect, the target is the script without any redirect at + // either the same domain or a different domain. + if ("redirect" in info) { + var host_info = get_host_info(); + var origin; + switch (info.redirect) { + case "same-origin": + origin = host_info['HTTP_ORIGIN']; + break; + case "cross-origin": + origin = host_info['HTTP_REMOTE_ORIGIN']; + break; + } + var redirected_url = origin + base_path() + url_sans_redirect; + return url_sans_redirect + "&redirect=" + encodeURIComponent(redirected_url); + } else { + return url_sans_redirect; + } +} +function expected_status(type, identifier, init) { + if (type == "date" && + init.headers && + init.headers["If-Modified-Since"] == identifier) { + // The server will respond with a 304 in this case. + return [304, "Not Modified"]; + } + return [200, "OK"]; +} +function expected_response_text(type, identifier, init, content) { + if (type == "date" && + init.headers && + init.headers["If-Modified-Since"] == identifier) { + // The server will respond with a 304 in this case. + return ""; + } + return content; +} +function server_state(uuid) { + return fetch("resources/cache.py?querystate&token=" + uuid) + .then(function(response) { + return response.text(); + }).then(function(text) { + // null will be returned if the server never received any requests + // for the given uuid. Normalize that to an empty list consistent + // with our representation. + return JSON.parse(text) || []; + }); +} +function make_test(type, info) { + return function(test) { + var uuid = token(); + var identifier = (type == "tag" ? Math.random() : now.toGMTString()); + var content = Math.random().toString(); + var url = make_url(uuid, type, identifier, content, info); + var fetch_functions = []; + for (var i = 0; i < info.request_cache.length; ++i) { + fetch_functions.push(function(idx) { + var init = {cache: info.request_cache[idx]}; + if ("request_headers" in info) { + init.headers = info.request_headers[idx]; + } + if (init.cache === "only-if-cached") { + // only-if-cached requires we use same-origin mode. + init.mode = "same-origin"; + } + return fetch(url, init) + .then(function(response) { + if ("response" in info && info.response[idx] === "error") { + assert_true(false, "fetch should have been an error"); + return; + } + assert_array_equals([response.status, response.statusText], + expected_status(type, identifier, init)); + return response.text(); + }).then(function(text) { + assert_equals(text, expected_response_text(type, identifier, init, content)); + }, function(reason) { + if ("response" in info && info.response[idx] === "error") { + assert_throws_js(TypeError, function() { throw reason; }); + } else { + throw reason; + } + }); + }); + } + var i = 0; + function run_next_step() { + if (fetch_functions.length) { + return fetch_functions.shift()(i++) + .then(run_next_step); + } else { + return Promise.resolve(); + } + } + return run_next_step() + .then(function() { + // Now, query the server state + return server_state(uuid); + }).then(function(state) { + var expectedState = []; + info.expected_validation_headers.forEach(function (validate) { + if (validate) { + if (type == "tag") { + expectedState.push({"If-None-Match": '"' + identifier + '"'}); + } else { + expectedState.push({"If-Modified-Since": identifier}); + } + } else { + expectedState.push({}); + } + }); + for (var i = 0; i < info.expected_no_cache_headers.length; ++i) { + if (info.expected_no_cache_headers[i]) { + expectedState[i]["Pragma"] = "no-cache"; + expectedState[i]["Cache-Control"] = "no-cache"; + } + } + if ("expected_max_age_headers" in info) { + for (var i = 0; i < info.expected_max_age_headers.length; ++i) { + if (info.expected_max_age_headers[i]) { + expectedState[i]["Cache-Control"] = "max-age=0"; + } + } + } + assert_equals(state.length, expectedState.length); + for (var i = 0; i < state.length; ++i) { + for (var header in state[i]) { + assert_equals(state[i][header], expectedState[i][header]); + delete expectedState[i][header]; + } + for (var header in expectedState[i]) { + assert_false(header in state[i]); + } + } + }); + }; +} + +function run_tests(tests) +{ + tests.forEach(function(info) { + promise_test(make_test("tag", info), info.name + " with Etag and " + info.state + " response"); + promise_test(make_test("date", info), info.name + " with Last-Modified and " + info.state + " response"); + }); +} diff --git a/test/wpt/tests/fetch/api/request/request-clone.sub.html b/test/wpt/tests/fetch/api/request/request-clone.sub.html new file mode 100644 index 00000000000..c690bb3dc03 --- /dev/null +++ b/test/wpt/tests/fetch/api/request/request-clone.sub.html @@ -0,0 +1,63 @@ + + + + + Request clone + + + + + + + + + + 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 cb34e5a790a..6de5dc16e96 100644 --- a/test/wpt/tests/fetch/api/request/request-headers.any.js +++ b/test/wpt/tests/fetch/api/request/request-headers.any.js @@ -17,6 +17,7 @@ var invalidRequestHeaders = [ ["Accept-Encoding", "KO"], ["Access-Control-Request-Headers", "KO"], ["Access-Control-Request-Method", "KO"], + ["Access-Control-Request-Private-Network", "KO"], ["Connection", "KO"], ["Content-Length", "KO"], ["Cookie", "KO"], @@ -58,6 +59,7 @@ var invalidRequestNoCorsHeaders = [ ["proxya", "KO"], ["sec", "KO"], ["secb", "KO"], + ["Empty-Value", ""], ]; validRequestHeaders.forEach(function(header) { diff --git a/test/wpt/tests/fetch/api/request/request-init-001.sub.html b/test/wpt/tests/fetch/api/request/request-init-001.sub.html new file mode 100644 index 00000000000..cc495a66527 --- /dev/null +++ b/test/wpt/tests/fetch/api/request/request-init-001.sub.html @@ -0,0 +1,112 @@ + + + + + Request init: simple cases + + + + + + + + + diff --git a/test/wpt/tests/fetch/api/request/request-init-003.sub.html b/test/wpt/tests/fetch/api/request/request-init-003.sub.html new file mode 100644 index 00000000000..79c91cdfe82 --- /dev/null +++ b/test/wpt/tests/fetch/api/request/request-init-003.sub.html @@ -0,0 +1,84 @@ + + + + + Request: init with request or url + + + + + + + + + + + diff --git a/test/wpt/tests/fetch/api/request/request-init-priority.any.js b/test/wpt/tests/fetch/api/request/request-init-priority.any.js new file mode 100644 index 00000000000..eb5073c8578 --- /dev/null +++ b/test/wpt/tests/fetch/api/request/request-init-priority.any.js @@ -0,0 +1,26 @@ +var priorities = ["high", + "low", + "auto" + ]; + +for (idx in priorities) { + test(() => { + new Request("", {priority: priorities[idx]}); + }, "new Request() with a '" + priorities[idx] + "' priority does not throw an error"); +} + +test(() => { + assert_throws_js(TypeError, () => { + new Request("", {priority: 'invalid'}); + }, "a new Request() must throw a TypeError if RequestInit's priority is an invalid value"); +}, "new Request() throws a TypeError if any of RequestInit's members' values are invalid"); + +for (idx in priorities) { + promise_test(function(t) { + return fetch('hello.txt', { priority: priorities[idx] }); + }, "fetch() with a '" + priorities[idx] + "' priority completes successfully"); +} + +promise_test(function(t) { + return promise_rejects_js(t, TypeError, fetch('hello.txt', { priority: 'invalid' })); +}, "fetch() with an invalid priority returns a rejected promise with a TypeError"); diff --git a/test/wpt/tests/fetch/api/request/request-keepalive-quota.html b/test/wpt/tests/fetch/api/request/request-keepalive-quota.html new file mode 100644 index 00000000000..548ab38d7e1 --- /dev/null +++ b/test/wpt/tests/fetch/api/request/request-keepalive-quota.html @@ -0,0 +1,97 @@ + + + + + Request Keepalive Quota Tests + + + + + + + + + + + + + + + + diff --git a/test/wpt/tests/fetch/api/request/request-reset-attributes.https.html b/test/wpt/tests/fetch/api/request/request-reset-attributes.https.html new file mode 100644 index 00000000000..7be3608d737 --- /dev/null +++ b/test/wpt/tests/fetch/api/request/request-reset-attributes.https.html @@ -0,0 +1,96 @@ + + + + + + + diff --git a/test/wpt/tests/fetch/api/request/request-structure.any.js b/test/wpt/tests/fetch/api/request/request-structure.any.js index 3d55c70ac1e..5e785538555 100644 --- a/test/wpt/tests/fetch/api/request/request-structure.any.js +++ b/test/wpt/tests/fetch/api/request/request-structure.any.js @@ -26,7 +26,11 @@ var attributes = ["method", "duplex", //Request implements Body "bodyUsed" - ]; + ]; +var internalAttributes = ["priority", + "internalpriority", + "blocking" + ]; function isReadOnly(request, attributeToCheck) { var defaultValue = undefined; @@ -131,3 +135,9 @@ for (var idx in attributes) { isReadOnly(request, attributes[idx]); }, "Check " + attributes[idx] + " attribute"); } + +for (var idx in internalAttributes) { + test(function() { + assert_false(internalAttributes[idx] in request, "request does not expose " + internalAttributes[idx] + " attribute"); + }, "Request does not expose " + internalAttributes[idx] + " attribute"); +} \ No newline at end of file diff --git a/test/wpt/tests/fetch/api/request/resources/cache.py b/test/wpt/tests/fetch/api/request/resources/cache.py new file mode 100644 index 00000000000..ca0bd644b4f --- /dev/null +++ b/test/wpt/tests/fetch/api/request/resources/cache.py @@ -0,0 +1,67 @@ +from wptserve.utils import isomorphic_decode + +def main(request, response): + token = request.GET.first(b"token", None) + if b"querystate" in request.GET: + from json import JSONEncoder + response.headers.set(b"Content-Type", b"text/plain") + return JSONEncoder().encode(request.server.stash.take(token)) + content = request.GET.first(b"content", None) + tag = request.GET.first(b"tag", None) + date = request.GET.first(b"date", None) + expires = request.GET.first(b"expires", None) + vary = request.GET.first(b"vary", None) + cc = request.GET.first(b"cache_control", None) + redirect = request.GET.first(b"redirect", None) + inm = request.headers.get(b"If-None-Match", None) + ims = request.headers.get(b"If-Modified-Since", None) + pragma = request.headers.get(b"Pragma", None) + cache_control = request.headers.get(b"Cache-Control", None) + ignore = b"ignore" in request.GET + + if tag: + tag = b'"%s"' % tag + + server_state = request.server.stash.take(token) + if not server_state: + server_state = [] + state = dict() + if not ignore: + if inm: + state[u"If-None-Match"] = isomorphic_decode(inm) + if ims: + state[u"If-Modified-Since"] = isomorphic_decode(ims) + if pragma: + state[u"Pragma"] = isomorphic_decode(pragma) + if cache_control: + state[u"Cache-Control"] = isomorphic_decode(cache_control) + server_state.append(state) + request.server.stash.put(token, server_state) + + if tag: + response.headers.set(b"ETag", b'%s' % tag) + elif date: + response.headers.set(b"Last-Modified", date) + if expires: + response.headers.set(b"Expires", expires) + if vary: + response.headers.set(b"Vary", vary) + if cc: + response.headers.set(b"Cache-Control", cc) + + # The only-if-cached redirect tests wants CORS to be okay, the other tests + # are all same-origin anyways and don't care. + response.headers.set(b"Access-Control-Allow-Origin", b"*") + + if redirect: + response.headers.set(b"Location", redirect) + response.status = (302, b"Redirect") + return b"" + elif ((inm is not None and inm == tag) or + (ims is not None and ims == date)): + response.status = (304, b"Not Modified") + return b"" + else: + response.status = (200, b"OK") + response.headers.set(b"Content-Type", b"text/plain") + return content diff --git a/test/wpt/tests/fetch/api/request/resources/hello.txt b/test/wpt/tests/fetch/api/request/resources/hello.txt new file mode 100644 index 00000000000..ce013625030 --- /dev/null +++ b/test/wpt/tests/fetch/api/request/resources/hello.txt @@ -0,0 +1 @@ +hello diff --git a/test/wpt/tests/fetch/api/request/resources/request-reset-attributes-worker.js b/test/wpt/tests/fetch/api/request/resources/request-reset-attributes-worker.js new file mode 100644 index 00000000000..4b264ca2fec --- /dev/null +++ b/test/wpt/tests/fetch/api/request/resources/request-reset-attributes-worker.js @@ -0,0 +1,19 @@ +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; + } + + const name = params.get('name'); + const old_attribute = event.request[name]; + // If any of |init|'s member is present... + const init = {cache: 'no-store'} + const new_attribute = (new Request(event.request, init))[name]; + + event.respondWith( + new Response(`old: ${old_attribute}, new: ${new_attribute}`)); + }); diff --git a/test/wpt/tests/fetch/api/request/url-encoding.html b/test/wpt/tests/fetch/api/request/url-encoding.html new file mode 100644 index 00000000000..31c1ed3920b --- /dev/null +++ b/test/wpt/tests/fetch/api/request/url-encoding.html @@ -0,0 +1,25 @@ + + +Fetch: URL encoding + + + diff --git a/test/wpt/tests/fetch/api/resources/authentication.py b/test/wpt/tests/fetch/api/resources/authentication.py new file mode 100644 index 00000000000..8b6b00b0873 --- /dev/null +++ b/test/wpt/tests/fetch/api/resources/authentication.py @@ -0,0 +1,14 @@ +def main(request, response): + user = request.auth.username + password = request.auth.password + + if user == b"user" and password == b"password": + return b"Authentication done" + + realm = b"test" + if b"realm" in request.GET: + realm = request.GET.first(b"realm") + + return ((401, b"Unauthorized"), + [(b"WWW-Authenticate", b'Basic realm="' + realm + b'"')], + b"Please login with credentials 'user' and 'password'") diff --git a/test/wpt/tests/fetch/api/resources/bad-chunk-encoding.py b/test/wpt/tests/fetch/api/resources/bad-chunk-encoding.py new file mode 100644 index 00000000000..94a77adead8 --- /dev/null +++ b/test/wpt/tests/fetch/api/resources/bad-chunk-encoding.py @@ -0,0 +1,13 @@ +import time + +def main(request, response): + delay = float(request.GET.first(b"ms", 1000)) / 1E3 + count = int(request.GET.first(b"count", 50)) + time.sleep(delay) + response.headers.set(b"Transfer-Encoding", b"chunked") + response.write_status_headers() + time.sleep(delay) + for i in range(count): + response.writer.write_content(b"a\r\nTEST_CHUNK\r\n") + time.sleep(delay) + response.writer.write_content(b"garbage") diff --git a/test/wpt/tests/fetch/api/resources/basic.html b/test/wpt/tests/fetch/api/resources/basic.html new file mode 100644 index 00000000000..e23afd4bf6a --- /dev/null +++ b/test/wpt/tests/fetch/api/resources/basic.html @@ -0,0 +1,5 @@ + + diff --git a/test/wpt/tests/fetch/api/resources/cache.py b/test/wpt/tests/fetch/api/resources/cache.py new file mode 100644 index 00000000000..4de751e30bf --- /dev/null +++ b/test/wpt/tests/fetch/api/resources/cache.py @@ -0,0 +1,18 @@ +ETAG = b'"123abc"' +CONTENT_TYPE = b"text/plain" +CONTENT = b"lorem ipsum dolor sit amet" + + +def main(request, response): + # let caching kick in if possible (conditional GET) + etag = request.headers.get(b"If-None-Match", None) + if etag == ETAG: + response.headers.set(b"X-HTTP-STATUS", 304) + response.status = (304, b"Not Modified") + return b"" + + # cache miss, so respond with the actual content + response.status = (200, b"OK") + response.headers.set(b"ETag", ETAG) + response.headers.set(b"Content-Type", CONTENT_TYPE) + return CONTENT diff --git a/test/wpt/tests/fetch/api/resources/clean-stash.py b/test/wpt/tests/fetch/api/resources/clean-stash.py new file mode 100644 index 00000000000..ee8c69ac446 --- /dev/null +++ b/test/wpt/tests/fetch/api/resources/clean-stash.py @@ -0,0 +1,6 @@ +def main(request, response): + token = request.GET.first(b"token") + if request.server.stash.take(token) is not None: + return b"1" + else: + return b"0" diff --git a/test/wpt/tests/fetch/api/resources/cors-top.txt.headers b/test/wpt/tests/fetch/api/resources/cors-top.txt.headers new file mode 100644 index 00000000000..cb762eff806 --- /dev/null +++ b/test/wpt/tests/fetch/api/resources/cors-top.txt.headers @@ -0,0 +1 @@ +Access-Control-Allow-Origin: * diff --git a/test/wpt/tests/fetch/api/resources/dump-authorization-header.py b/test/wpt/tests/fetch/api/resources/dump-authorization-header.py new file mode 100644 index 00000000000..a651aeb4e8b --- /dev/null +++ b/test/wpt/tests/fetch/api/resources/dump-authorization-header.py @@ -0,0 +1,14 @@ +def main(request, response): + headers = [(b"Content-Type", "text/html"), + (b"Cache-Control", b"no-cache")] + + if b"Origin" in request.headers: + headers.append((b"Access-Control-Allow-Origin", request.headers.get(b"Origin", b""))) + headers.append((b"Access-Control-Allow-Credentials", b"true")) + else: + headers.append((b"Access-Control-Allow-Origin", b"*")) + headers.append((b"Access-Control-Allow-Headers", b'Authorization')) + + if b"authorization" in request.headers: + return 200, headers, request.headers.get(b"Authorization") + return 200, headers, "none" diff --git a/test/wpt/tests/fetch/api/resources/echo-content.h2.py b/test/wpt/tests/fetch/api/resources/echo-content.h2.py new file mode 100644 index 00000000000..0be3ece4a5f --- /dev/null +++ b/test/wpt/tests/fetch/api/resources/echo-content.h2.py @@ -0,0 +1,7 @@ +def handle_headers(frame, request, response): + response.status = 200 + response.headers.update([('Content-Type', 'text/plain')]) + response.write_status_headers() + +def handle_data(frame, request, response): + response.writer.write_data(frame.data) diff --git a/test/wpt/tests/fetch/api/resources/echo-content.py b/test/wpt/tests/fetch/api/resources/echo-content.py new file mode 100644 index 00000000000..5e137e15d7d --- /dev/null +++ b/test/wpt/tests/fetch/api/resources/echo-content.py @@ -0,0 +1,12 @@ +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/fetch/api/resources/infinite-slow-response.py b/test/wpt/tests/fetch/api/resources/infinite-slow-response.py new file mode 100644 index 00000000000..a26cd8064c8 --- /dev/null +++ b/test/wpt/tests/fetch/api/resources/infinite-slow-response.py @@ -0,0 +1,35 @@ +import time + + +def url_dir(request): + return u'/'.join(request.url_parts.path.split(u'/')[:-1]) + u'/' + + +def stash_write(request, key, value): + """Write to the stash, overwriting any previous value""" + request.server.stash.take(key, url_dir(request)) + request.server.stash.put(key, value, url_dir(request)) + + +def main(request, response): + stateKey = request.GET.first(b"stateKey", b"") + abortKey = request.GET.first(b"abortKey", b"") + + if stateKey: + stash_write(request, stateKey, 'open') + + response.headers.set(b"Content-type", b"text/plain") + response.write_status_headers() + + # Writing an initial 2k so browsers realise it's there. *shrug* + response.writer.write(b"." * 2048) + + while True: + if not response.writer.write(b"."): + break + if abortKey and request.server.stash.take(abortKey, url_dir(request)): + break + time.sleep(0.01) + + if stateKey: + stash_write(request, stateKey, 'closed') diff --git a/test/wpt/tests/fetch/api/resources/inspect-headers.py b/test/wpt/tests/fetch/api/resources/inspect-headers.py new file mode 100644 index 00000000000..9ed566e607b --- /dev/null +++ b/test/wpt/tests/fetch/api/resources/inspect-headers.py @@ -0,0 +1,24 @@ +def main(request, response): + headers = [] + if b"headers" in request.GET: + checked_headers = request.GET.first(b"headers").split(b"|") + for header in checked_headers: + if header in request.headers: + headers.append((b"x-request-" + header, request.headers.get(header, b""))) + + if b"cors" in request.GET: + if b"Origin" in request.headers: + headers.append((b"Access-Control-Allow-Origin", request.headers.get(b"Origin", b""))) + else: + headers.append((b"Access-Control-Allow-Origin", b"*")) + headers.append((b"Access-Control-Allow-Credentials", b"true")) + headers.append((b"Access-Control-Allow-Methods", b"GET, POST, HEAD")) + exposed_headers = [b"x-request-" + header for header in checked_headers] + headers.append((b"Access-Control-Expose-Headers", b", ".join(exposed_headers))) + if b"allow_headers" in request.GET: + headers.append((b"Access-Control-Allow-Headers", request.GET[b'allow_headers'])) + else: + headers.append((b"Access-Control-Allow-Headers", b", ".join(request.headers))) + + headers.append((b"content-type", b"text/plain")) + return headers, b"" diff --git a/test/wpt/tests/fetch/api/resources/keepalive-iframe.html b/test/wpt/tests/fetch/api/resources/keepalive-iframe.html new file mode 100644 index 00000000000..47de0da7790 --- /dev/null +++ b/test/wpt/tests/fetch/api/resources/keepalive-iframe.html @@ -0,0 +1,25 @@ + + + + + + + diff --git a/test/wpt/tests/fetch/api/resources/keepalive-window.html b/test/wpt/tests/fetch/api/resources/keepalive-window.html new file mode 100644 index 00000000000..6ccf484644c --- /dev/null +++ b/test/wpt/tests/fetch/api/resources/keepalive-window.html @@ -0,0 +1,34 @@ + + + + + + + diff --git a/test/wpt/tests/fetch/api/resources/method.py b/test/wpt/tests/fetch/api/resources/method.py new file mode 100644 index 00000000000..c1a111b4cdf --- /dev/null +++ b/test/wpt/tests/fetch/api/resources/method.py @@ -0,0 +1,18 @@ +from wptserve.utils import isomorphic_encode + +def main(request, response): + headers = [] + if b"cors" in request.GET: + headers.append((b"Access-Control-Allow-Origin", b"*")) + headers.append((b"Access-Control-Allow-Credentials", b"true")) + headers.append((b"Access-Control-Allow-Methods", b"GET, POST, PUT, FOO")) + headers.append((b"Access-Control-Allow-Headers", b"x-test, x-foo")) + headers.append((b"Access-Control-Expose-Headers", b"x-request-method")) + + headers.append((b"x-request-method", isomorphic_encode(request.method))) + headers.append((b"x-request-content-type", request.headers.get(b"Content-Type", b"NO"))) + headers.append((b"x-request-content-length", request.headers.get(b"Content-Length", b"NO"))) + headers.append((b"x-request-content-encoding", request.headers.get(b"Content-Encoding", b"NO"))) + headers.append((b"x-request-content-language", request.headers.get(b"Content-Language", b"NO"))) + headers.append((b"x-request-content-location", request.headers.get(b"Content-Location", b"NO"))) + return headers, request.body diff --git a/test/wpt/tests/fetch/api/resources/preflight.py b/test/wpt/tests/fetch/api/resources/preflight.py new file mode 100644 index 00000000000..f983ef95227 --- /dev/null +++ b/test/wpt/tests/fetch/api/resources/preflight.py @@ -0,0 +1,78 @@ +def main(request, response): + headers = [(b"Content-Type", b"text/plain")] + stashed_data = {b'control_request_headers': b"", b'preflight': b"0", b'preflight_referrer': b""} + + token = None + if b"token" in request.GET: + token = request.GET.first(b"token") + + if b"origin" in request.GET: + for origin in request.GET[b'origin'].split(b", "): + headers.append((b"Access-Control-Allow-Origin", origin)) + else: + headers.append((b"Access-Control-Allow-Origin", b"*")) + + if b"clear-stash" in request.GET: + if request.server.stash.take(token) is not None: + return headers, b"1" + else: + return headers, b"0" + + if b"credentials" in request.GET: + headers.append((b"Access-Control-Allow-Credentials", b"true")) + + if request.method == u"OPTIONS": + if not b"Access-Control-Request-Method" in request.headers: + response.set_error(400, u"No Access-Control-Request-Method header") + return b"ERROR: No access-control-request-method in preflight!" + + if request.headers.get(b"Accept", b"") != b"*/*": + response.set_error(400, u"Request does not have 'Accept: */*' header") + return b"ERROR: Invalid access in preflight!" + + if b"control_request_headers" in request.GET: + stashed_data[b'control_request_headers'] = request.headers.get(b"Access-Control-Request-Headers", None) + + if b"max_age" in request.GET: + headers.append((b"Access-Control-Max-Age", request.GET[b'max_age'])) + + if b"allow_headers" in request.GET: + headers.append((b"Access-Control-Allow-Headers", request.GET[b'allow_headers'])) + + if b"allow_methods" in request.GET: + headers.append((b"Access-Control-Allow-Methods", request.GET[b'allow_methods'])) + + preflight_status = 200 + if b"preflight_status" in request.GET: + preflight_status = int(request.GET.first(b"preflight_status")) + + stashed_data[b'preflight'] = b"1" + stashed_data[b'preflight_referrer'] = request.headers.get(b"Referer", b"") + stashed_data[b'preflight_user_agent'] = request.headers.get(b"User-Agent", b"") + if token: + request.server.stash.put(token, stashed_data) + + return preflight_status, headers, b"" + + + if token: + data = request.server.stash.take(token) + if data: + stashed_data = data + + if b"checkUserAgentHeaderInPreflight" in request.GET and request.headers.get(b"User-Agent") != stashed_data[b'preflight_user_agent']: + return 400, headers, b"ERROR: No user-agent header in preflight" + + #use x-* headers for returning value to bodyless responses + headers.append((b"Access-Control-Expose-Headers", b"x-did-preflight, x-control-request-headers, x-referrer, x-preflight-referrer, x-origin")) + headers.append((b"x-did-preflight", stashed_data[b'preflight'])) + if stashed_data[b'control_request_headers'] != None: + headers.append((b"x-control-request-headers", stashed_data[b'control_request_headers'])) + headers.append((b"x-preflight-referrer", stashed_data[b'preflight_referrer'])) + headers.append((b"x-referrer", request.headers.get(b"Referer", b""))) + headers.append((b"x-origin", request.headers.get(b"Origin", b""))) + + if token: + request.server.stash.put(token, stashed_data) + + return headers, b"" diff --git a/test/wpt/tests/fetch/api/resources/redirect-empty-location.py b/test/wpt/tests/fetch/api/resources/redirect-empty-location.py new file mode 100644 index 00000000000..1a5f7feb2a4 --- /dev/null +++ b/test/wpt/tests/fetch/api/resources/redirect-empty-location.py @@ -0,0 +1,3 @@ +def main(request, response): + headers = [(b"Location", b"")] + return 302, headers, b"" diff --git a/test/wpt/tests/fetch/api/resources/redirect.h2.py b/test/wpt/tests/fetch/api/resources/redirect.h2.py new file mode 100644 index 00000000000..69370145876 --- /dev/null +++ b/test/wpt/tests/fetch/api/resources/redirect.h2.py @@ -0,0 +1,14 @@ +from wptserve.utils import isomorphic_decode, isomorphic_encode + +def handle_headers(frame, request, response): + status = 302 + if b'redirect_status' in request.GET: + status = int(request.GET[b'redirect_status']) + response.status = status + + if b'location' in request.GET: + url = isomorphic_decode(request.GET[b'location']) + response.headers[b'Location'] = isomorphic_encode(url) + + response.headers.update([('Content-Type', 'text/plain')]) + response.write_status_headers() diff --git a/test/wpt/tests/fetch/api/resources/redirect.py b/test/wpt/tests/fetch/api/resources/redirect.py new file mode 100644 index 00000000000..d52ab5f3eee --- /dev/null +++ b/test/wpt/tests/fetch/api/resources/redirect.py @@ -0,0 +1,73 @@ +import time + +from urllib.parse import urlencode, urlparse + +from wptserve.utils import isomorphic_decode, isomorphic_encode + +def main(request, response): + stashed_data = {b'count': 0, b'preflight': b"0"} + status = 302 + headers = [(b"Content-Type", b"text/plain"), + (b"Cache-Control", b"no-cache"), + (b"Pragma", b"no-cache")] + if b"Origin" in request.headers: + headers.append((b"Access-Control-Allow-Origin", request.headers.get(b"Origin", b""))) + headers.append((b"Access-Control-Allow-Credentials", b"true")) + else: + headers.append((b"Access-Control-Allow-Origin", b"*")) + + token = None + if b"token" in request.GET: + token = request.GET.first(b"token") + data = request.server.stash.take(token) + if data: + stashed_data = data + + if request.method == u"OPTIONS": + if b"allow_headers" in request.GET: + headers.append((b"Access-Control-Allow-Headers", request.GET[b'allow_headers'])) + stashed_data[b'preflight'] = b"1" + #Preflight is not redirected: return 200 + if not b"redirect_preflight" in request.GET: + if token: + request.server.stash.put(request.GET.first(b"token"), stashed_data) + return 200, headers, u"" + + if b"redirect_status" in request.GET: + status = int(request.GET[b'redirect_status']) + elif b"redirect_status" in request.POST: + status = int(request.POST[b'redirect_status']) + + stashed_data[b'count'] += 1 + + if b"location" in request.GET: + url = isomorphic_decode(request.GET[b'location']) + if b"simple" not in request.GET: + scheme = urlparse(url).scheme + if scheme == u"" or scheme == u"http" or scheme == u"https": + url += u"&" if u'?' in url else u"?" + #keep url parameters in location + url_parameters = {} + for item in request.GET.items(): + url_parameters[isomorphic_decode(item[0])] = isomorphic_decode(item[1][0]) + url += urlencode(url_parameters) + #make sure location changes during redirection loop + url += u"&count=" + str(stashed_data[b'count']) + headers.append((b"Location", isomorphic_encode(url))) + + if b"redirect_referrerpolicy" in request.GET: + headers.append((b"Referrer-Policy", request.GET[b'redirect_referrerpolicy'])) + + if b"delay" in request.GET: + time.sleep(float(request.GET.first(b"delay", 0)) / 1E3) + + if token: + request.server.stash.put(request.GET.first(b"token"), stashed_data) + if b"max_count" in request.GET: + max_count = int(request.GET[b'max_count']) + #stop redirecting and return count + if stashed_data[b'count'] > max_count: + # -1 because the last is not a redirection + return str(stashed_data[b'count'] - 1) + + return status, headers, u"" diff --git a/test/wpt/tests/fetch/api/resources/sandboxed-iframe.html b/test/wpt/tests/fetch/api/resources/sandboxed-iframe.html new file mode 100644 index 00000000000..6e5d5065474 --- /dev/null +++ b/test/wpt/tests/fetch/api/resources/sandboxed-iframe.html @@ -0,0 +1,34 @@ + + + + diff --git a/test/wpt/tests/fetch/api/resources/script-with-header.py b/test/wpt/tests/fetch/api/resources/script-with-header.py new file mode 100644 index 00000000000..9a9c70ef5cf --- /dev/null +++ b/test/wpt/tests/fetch/api/resources/script-with-header.py @@ -0,0 +1,7 @@ +def main(request, response): + headers = [(b"Content-type", request.GET.first(b"mime"))] + if b"content" in request.GET and request.GET.first(b"content") == b"empty": + content = b'' + else: + content = b"console.log('Script loaded')" + return 200, headers, content diff --git a/test/wpt/tests/fetch/api/resources/stash-put.py b/test/wpt/tests/fetch/api/resources/stash-put.py new file mode 100644 index 00000000000..dbc7ceebb88 --- /dev/null +++ b/test/wpt/tests/fetch/api/resources/stash-put.py @@ -0,0 +1,17 @@ +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' + + 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) + response.headers.set(b'Access-Control-Allow-Origin', b'*') + return "done" diff --git a/test/wpt/tests/fetch/api/resources/stash-take.py b/test/wpt/tests/fetch/api/resources/stash-take.py new file mode 100644 index 00000000000..e6db80dd86d --- /dev/null +++ b/test/wpt/tests/fetch/api/resources/stash-take.py @@ -0,0 +1,9 @@ +from wptserve.handlers import json_handler + + +@json_handler +def main(request, response): + dir = u'/'.join(request.url_parts.path.split(u'/')[:-1]) + u'/' + key = request.GET.first(b"key") + response.headers.set(b'Access-Control-Allow-Origin', b'*') + return request.server.stash.take(key, dir) diff --git a/test/wpt/tests/fetch/api/resources/status.py b/test/wpt/tests/fetch/api/resources/status.py new file mode 100644 index 00000000000..05a59d5a637 --- /dev/null +++ b/test/wpt/tests/fetch/api/resources/status.py @@ -0,0 +1,11 @@ +from wptserve.utils import isomorphic_encode + +def main(request, response): + code = int(request.GET.first(b"code", 200)) + text = request.GET.first(b"text", b"OMG") + content = request.GET.first(b"content", b"") + type = request.GET.first(b"type", b"") + status = (code, text) + headers = [(b"Content-Type", type), + (b"X-Request-Method", isomorphic_encode(request.method))] + return status, headers, content diff --git a/test/wpt/tests/fetch/api/resources/sw-intercept-abort.js b/test/wpt/tests/fetch/api/resources/sw-intercept-abort.js new file mode 100644 index 00000000000..19d4b189d85 --- /dev/null +++ b/test/wpt/tests/fetch/api/resources/sw-intercept-abort.js @@ -0,0 +1,19 @@ +async function messageClient(clientId, message) { + const client = await clients.get(clientId); + client.postMessage(message); +} + +addEventListener('fetch', event => { + let resolve; + const promise = new Promise(r => resolve = r); + + function onAborted() { + messageClient(event.clientId, event.request.signal.reason); + resolve(); + } + + messageClient(event.clientId, 'fetch event has arrived'); + + event.respondWith(promise.then(() => new Response('hello'))); + event.request.signal.addEventListener('abort', onAborted); +}); diff --git a/test/wpt/tests/fetch/api/resources/sw-intercept.js b/test/wpt/tests/fetch/api/resources/sw-intercept.js new file mode 100644 index 00000000000..b8166b62a5c --- /dev/null +++ b/test/wpt/tests/fetch/api/resources/sw-intercept.js @@ -0,0 +1,10 @@ +async function broadcast(msg) { + for (const client of await clients.matchAll()) { + client.postMessage(msg); + } +} + +addEventListener('fetch', event => { + event.waitUntil(broadcast(event.request.url)); + event.respondWith(fetch(event.request)); +}); diff --git a/test/wpt/tests/fetch/api/resources/trickle.py b/test/wpt/tests/fetch/api/resources/trickle.py new file mode 100644 index 00000000000..99833f1b38d --- /dev/null +++ b/test/wpt/tests/fetch/api/resources/trickle.py @@ -0,0 +1,15 @@ +import time + +def main(request, response): + delay = float(request.GET.first(b"ms", 500)) / 1E3 + count = int(request.GET.first(b"count", 50)) + # Read request body + request.body + time.sleep(delay) + if not b"notype" in request.GET: + response.headers.set(b"Content-type", b"text/plain") + response.write_status_headers() + time.sleep(delay) + for i in range(count): + response.writer.write_content(b"TEST_TRICKLE\n") + time.sleep(delay) diff --git a/test/wpt/tests/fetch/api/resources/utils.js b/test/wpt/tests/fetch/api/resources/utils.js index 662de799181..3b20ecc8346 100644 --- a/test/wpt/tests/fetch/api/resources/utils.js +++ b/test/wpt/tests/fetch/api/resources/utils.js @@ -57,7 +57,8 @@ function validateBufferFromString(buffer, expectedValue, message) } function validateStreamFromString(reader, expectedValue, retrievedArrayBuffer) { - return reader.read().then(function(data) { + // Passing Uint8Array for byte streams; non-byte streams will simply ignore it + return reader.read(new Uint8Array(64)).then(function(data) { if (!data.done) { assert_true(data.value instanceof Uint8Array, "Fetch ReadableStream chunks should be Uint8Array"); var newBuffer; @@ -75,7 +76,8 @@ function validateStreamFromString(reader, expectedValue, retrievedArrayBuffer) { } function validateStreamFromPartialString(reader, expectedValue, retrievedArrayBuffer) { - return reader.read().then(function(data) { + // Passing Uint8Array for byte streams; non-byte streams will simply ignore it + return reader.read(new Uint8Array(64)).then(function(data) { if (!data.done) { assert_true(data.value instanceof Uint8Array, "Fetch ReadableStream chunks should be Uint8Array"); var newBuffer; diff --git a/test/wpt/tests/fetch/api/response/multi-globals/current/current.html b/test/wpt/tests/fetch/api/response/multi-globals/current/current.html new file mode 100644 index 00000000000..9bb6e0bbf3f --- /dev/null +++ b/test/wpt/tests/fetch/api/response/multi-globals/current/current.html @@ -0,0 +1,3 @@ + +Current page used as a test helper + diff --git a/test/wpt/tests/fetch/api/response/multi-globals/incumbent/incumbent.html b/test/wpt/tests/fetch/api/response/multi-globals/incumbent/incumbent.html new file mode 100644 index 00000000000..f63372e64c2 --- /dev/null +++ b/test/wpt/tests/fetch/api/response/multi-globals/incumbent/incumbent.html @@ -0,0 +1,16 @@ + +Incumbent page used as a test helper + + + + + diff --git a/test/wpt/tests/fetch/api/response/multi-globals/relevant/relevant.html b/test/wpt/tests/fetch/api/response/multi-globals/relevant/relevant.html new file mode 100644 index 00000000000..44f42eda493 --- /dev/null +++ b/test/wpt/tests/fetch/api/response/multi-globals/relevant/relevant.html @@ -0,0 +1,2 @@ + +Relevant page used as a test helper diff --git a/test/wpt/tests/fetch/api/response/multi-globals/url-parsing.html b/test/wpt/tests/fetch/api/response/multi-globals/url-parsing.html new file mode 100644 index 00000000000..5f2f42a1cea --- /dev/null +++ b/test/wpt/tests/fetch/api/response/multi-globals/url-parsing.html @@ -0,0 +1,27 @@ + +Response.redirect URL parsing, with multiple globals in play + + + + + + + + + + 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 new file mode 100644 index 00000000000..c2c90eaa8bd --- /dev/null +++ b/test/wpt/tests/fetch/api/response/response-body-read-task-handling.html @@ -0,0 +1,54 @@ + + + + + + + + + + + + diff --git a/test/wpt/tests/fetch/api/response/response-clone-iframe.window.js b/test/wpt/tests/fetch/api/response/response-clone-iframe.window.js new file mode 100644 index 00000000000..da54616c376 --- /dev/null +++ b/test/wpt/tests/fetch/api/response/response-clone-iframe.window.js @@ -0,0 +1,32 @@ +// Verify that calling Response clone() in a detached iframe doesn't crash. +// Regression test for https://crbug.com/1082688. + +'use strict'; + +promise_test(async () => { + // Wait for the document body to be available. + await new Promise(resolve => { + onload = resolve; + }); + + window.iframe = document.createElement('iframe'); + document.body.appendChild(iframe); + iframe.srcdoc = ` + +`; + + await new Promise(resolve => { + onmessage = evt => { + if (evt.data === 'okay') { + resolve(); + } + }; + }); + + // If it got here without crashing, the test passed. +}, 'clone within removed iframe should not crash'); diff --git a/test/wpt/tests/fetch/api/response/response-consume-stream.any.js b/test/wpt/tests/fetch/api/response/response-consume-stream.any.js index d5b0c388cc7..befce620a3f 100644 --- a/test/wpt/tests/fetch/api/response/response-consume-stream.any.js +++ b/test/wpt/tests/fetch/api/response/response-consume-stream.any.js @@ -20,35 +20,37 @@ var blob = new Blob([textData], { "type" : "text/plain" }); var urlSearchParamsData = "name=value"; var urlSearchParams = new URLSearchParams(urlSearchParamsData); -promise_test(function(test) { - var response = new Response(blob); - return validateStreamFromString(response.body.getReader(), textData); -}, "Read blob response's body as readableStream"); - -promise_test(function(test) { - var response = new Response(textData); - return validateStreamFromString(response.body.getReader(), textData); -}, "Read text response's body as readableStream"); - -promise_test(function(test) { - var response = new Response(urlSearchParams); - return validateStreamFromString(response.body.getReader(), urlSearchParamsData); -}, "Read URLSearchParams response's body as readableStream"); - -promise_test(function(test) { - var arrayBuffer = new ArrayBuffer(textData.length); - var int8Array = new Int8Array(arrayBuffer); - for (var cptr = 0; cptr < textData.length; cptr++) - int8Array[cptr] = textData.charCodeAt(cptr); - - return validateStreamFromString(new Response(arrayBuffer).body.getReader(), textData); -}, "Read array buffer response's body as readableStream"); - -promise_test(function(test) { - var response = new Response(formData); - return validateStreamFromPartialString(response.body.getReader(), - "Content-Disposition: form-data; name=\"name\"\r\n\r\nvalue"); -}, "Read form data response's body as readableStream"); +for (const mode of [undefined, "byob"]) { + promise_test(function(test) { + var response = new Response(blob); + return validateStreamFromString(response.body.getReader({ mode }), textData); + }, `Read blob response's body as readableStream with mode=${mode}`); + + promise_test(function(test) { + var response = new Response(textData); + return validateStreamFromString(response.body.getReader({ mode }), textData); + }, `Read text response's body as readableStream with mode=${mode}`); + + promise_test(function(test) { + var response = new Response(urlSearchParams); + return validateStreamFromString(response.body.getReader({ mode }), urlSearchParamsData); + }, `Read URLSearchParams response's body as readableStream with mode=${mode}`); + + promise_test(function(test) { + var arrayBuffer = new ArrayBuffer(textData.length); + var int8Array = new Int8Array(arrayBuffer); + for (var cptr = 0; cptr < textData.length; cptr++) + int8Array[cptr] = textData.charCodeAt(cptr); + + return validateStreamFromString(new Response(arrayBuffer).body.getReader({ mode }), textData); + }, `Read array buffer response's body as readableStream with mode=${mode}`); + + promise_test(function(test) { + var response = new Response(formData); + return validateStreamFromPartialString(response.body.getReader({ mode }), + "Content-Disposition: form-data; name=\"name\"\r\n\r\nvalue"); + }, `Read form data response's body as readableStream with mode=${mode}`); +} test(function() { assert_equals(Response.error().body, null); diff --git a/test/wpt/tests/fetch/api/response/response-consume.html b/test/wpt/tests/fetch/api/response/response-consume.html new file mode 100644 index 00000000000..89fc49fd3c2 --- /dev/null +++ b/test/wpt/tests/fetch/api/response/response-consume.html @@ -0,0 +1,317 @@ + + + + + Response consume + + + + + + + + + + + diff --git a/test/wpt/tests/fetch/api/response/response-stream-with-broken-then.any.js b/test/wpt/tests/fetch/api/response/response-stream-with-broken-then.any.js new file mode 100644 index 00000000000..8fef66c8a28 --- /dev/null +++ b/test/wpt/tests/fetch/api/response/response-stream-with-broken-then.any.js @@ -0,0 +1,117 @@ +// META: global=window,worker +// META: script=../resources/utils.js + +promise_test(async () => { + // t.add_cleanup doesn't work when Object.prototype.then is overwritten, so + // these tests use add_completion_callback for cleanup instead. + add_completion_callback(() => delete Object.prototype.then); + const hello = new TextEncoder().encode('hello'); + const bye = new TextEncoder().encode('bye'); + const rs = new ReadableStream({ + start(controller) { + controller.enqueue(hello); + controller.close(); + } + }); + const resp = new Response(rs); + Object.prototype.then = (onFulfilled) => { + delete Object.prototype.then; + onFulfilled({done: false, value: bye}); + }; + const text = await resp.text(); + delete Object.prototype.then; + assert_equals(text, 'hello', 'The value should be "hello".'); +}, 'Attempt to inject {done: false, value: bye} via Object.prototype.then.'); + +promise_test(async (t) => { + add_completion_callback(() => delete Object.prototype.then); + const hello = new TextEncoder().encode('hello'); + const rs = new ReadableStream({ + start(controller) { + controller.enqueue(hello); + controller.close(); + } + }); + const resp = new Response(rs); + Object.prototype.then = (onFulfilled) => { + delete Object.prototype.then; + onFulfilled({done: false, value: undefined}); + }; + const text = await resp.text(); + delete Object.prototype.then; + assert_equals(text, 'hello', 'The value should be "hello".'); +}, 'Attempt to inject value: undefined via Object.prototype.then.'); + +promise_test(async (t) => { + add_completion_callback(() => delete Object.prototype.then); + const hello = new TextEncoder().encode('hello'); + const rs = new ReadableStream({ + start(controller) { + controller.enqueue(hello); + controller.close(); + } + }); + const resp = new Response(rs); + Object.prototype.then = (onFulfilled) => { + delete Object.prototype.then; + onFulfilled(undefined); + }; + const text = await resp.text(); + delete Object.prototype.then; + assert_equals(text, 'hello', 'The value should be "hello".'); +}, 'Attempt to inject undefined via Object.prototype.then.'); + +promise_test(async (t) => { + add_completion_callback(() => delete Object.prototype.then); + const hello = new TextEncoder().encode('hello'); + const rs = new ReadableStream({ + start(controller) { + controller.enqueue(hello); + controller.close(); + } + }); + const resp = new Response(rs); + Object.prototype.then = (onFulfilled) => { + delete Object.prototype.then; + onFulfilled(8.2); + }; + const text = await resp.text(); + delete Object.prototype.then; + assert_equals(text, 'hello', 'The value should be "hello".'); +}, 'Attempt to inject 8.2 via Object.prototype.then.'); + +promise_test(async () => { + add_completion_callback(() => delete Object.prototype.then); + const hello = new TextEncoder().encode('hello'); + const bye = new TextEncoder().encode('bye'); + const resp = new Response(hello); + Object.prototype.then = (onFulfilled) => { + delete Object.prototype.then; + onFulfilled({done: false, value: bye}); + }; + const text = await resp.text(); + delete Object.prototype.then; + assert_equals(text, 'hello', 'The value should be "hello".'); +}, 'intercepting arraybuffer to text conversion via Object.prototype.then ' + + 'should not be possible'); + +promise_test(async () => { + add_completion_callback(() => delete Object.prototype.then); + const u8a123 = new Uint8Array([1, 2, 3]); + const u8a456 = new Uint8Array([4, 5, 6]); + const resp = new Response(u8a123); + const writtenBytes = []; + const ws = new WritableStream({ + write(chunk) { + writtenBytes.push(...Array.from(chunk)); + } + }); + Object.prototype.then = (onFulfilled) => { + delete Object.prototype.then; + onFulfilled({done: false, value: u8a456}); + }; + await resp.body.pipeTo(ws); + delete Object.prototype.then; + assert_array_equals(writtenBytes, u8a123, 'The value should be [1, 2, 3]'); +}, 'intercepting arraybuffer to body readable stream conversion via ' + + 'Object.prototype.then should not be possible'); diff --git a/test/wpt/tests/fetch/connection-pool/network-partition-key.html b/test/wpt/tests/fetch/connection-pool/network-partition-key.html new file mode 100644 index 00000000000..60a784cd84e --- /dev/null +++ b/test/wpt/tests/fetch/connection-pool/network-partition-key.html @@ -0,0 +1,264 @@ + + + + + Connection partitioning by site + + + + + + + + + + + + + + diff --git a/test/wpt/tests/fetch/connection-pool/resources/network-partition-about-blank-checker.html b/test/wpt/tests/fetch/connection-pool/resources/network-partition-about-blank-checker.html new file mode 100644 index 00000000000..7a8b6132375 --- /dev/null +++ b/test/wpt/tests/fetch/connection-pool/resources/network-partition-about-blank-checker.html @@ -0,0 +1,35 @@ + + + + + about:blank Network Partition Checker + + + + + + + diff --git a/test/wpt/tests/fetch/connection-pool/resources/network-partition-checker.html b/test/wpt/tests/fetch/connection-pool/resources/network-partition-checker.html new file mode 100644 index 00000000000..b058f611242 --- /dev/null +++ b/test/wpt/tests/fetch/connection-pool/resources/network-partition-checker.html @@ -0,0 +1,30 @@ + + + + + Network Partition Checker + + + + + + + + + + diff --git a/test/wpt/tests/fetch/connection-pool/resources/network-partition-iframe-checker.html b/test/wpt/tests/fetch/connection-pool/resources/network-partition-iframe-checker.html new file mode 100644 index 00000000000..f76ed184471 --- /dev/null +++ b/test/wpt/tests/fetch/connection-pool/resources/network-partition-iframe-checker.html @@ -0,0 +1,22 @@ + + + + + Iframe Network Partition Checker + + + + + + + + + + + diff --git a/test/wpt/tests/fetch/connection-pool/resources/network-partition-key.js b/test/wpt/tests/fetch/connection-pool/resources/network-partition-key.js new file mode 100644 index 00000000000..bd66109380f --- /dev/null +++ b/test/wpt/tests/fetch/connection-pool/resources/network-partition-key.js @@ -0,0 +1,47 @@ +// Runs multiple fetches that validate connections see only a single partition_id. +// Requests are run in parallel so that they use multiple connections to maximize the +// chance of exercising all matching connections in the connection pool. Only returns +// once all requests have completed to make cleaning up server state non-racy. +function check_partition_ids(location) { + const NUM_FETCHES = 20; + + var base_url = 'SUBRESOURCE_PREFIX:&dispatch=check_partition'; + + // Not a perfect parse of the query string, but good enough for this test. + var include_credentials = base_url.search('include_credentials=true') != -1; + var exclude_credentials = base_url.search('include_credentials=false') != -1; + if (include_credentials != !exclude_credentials) + throw new Exception('Credentials mode not specified'); + + + // Run NUM_FETCHES in parallel. + var fetches = []; + for (i = 0; i < NUM_FETCHES; ++i) { + var fetch_params = { + credentials: 'omit', + mode: 'cors', + headers: { + 'Header-To-Force-CORS': 'cors' + }, + }; + + // Use a unique URL for each request, in case the caching layer serializes multiple + // requests for the same URL. + var url = `${base_url}&${token()}`; + + fetches.push(fetch(url, fetch_params).then( + function (response) { + return response.text().then(function(text) { + assert_equals(text, 'ok', `Socket unexpectedly reused`); + }); + })); + } + + // Wait for all promises to complete. + return Promise.allSettled(fetches).then(function (results) { + results.forEach(function (result) { + if (result.status != 'fulfilled') + throw result.reason; + }); + }); +} diff --git a/test/wpt/tests/fetch/connection-pool/resources/network-partition-key.py b/test/wpt/tests/fetch/connection-pool/resources/network-partition-key.py new file mode 100644 index 00000000000..32fe4999b7d --- /dev/null +++ b/test/wpt/tests/fetch/connection-pool/resources/network-partition-key.py @@ -0,0 +1,130 @@ +import mimetypes +import os + +from wptserve.utils import isomorphic_decode, isomorphic_encode + +# Test server that tracks the last partition_id was used with each connection for each uuid, and +# lets consumers query if multiple different partition_ids have been been used for any socket. +# +# Server assumes that ports aren't reused, so a client address and a server port uniquely identify +# a connection. If that constraint is ever violated, the test will be flaky. No sockets being +# closed for the duration of the test is sufficient to ensure that, though even if sockets are +# closed, the OS should generally prefer to use new ports for new connections, if any are +# available. +def main(request, response): + response.headers.set(b"Cache-Control", b"no-store") + dispatch = request.GET.first(b"dispatch", None) + uuid = request.GET.first(b"uuid", None) + partition_id = request.GET.first(b"partition_id", None) + + if not uuid or not dispatch or not partition_id: + return simple_response(request, response, 404, b"Not found", b"Invalid query parameters") + + # Unless nocheck_partition is true, check partition_id against server_state, and update server_state. + stash = request.server.stash + test_failed = False + request_count = 0; + connection_count = 0; + if request.GET.first(b"nocheck_partition", None) != b"True": + # Need to grab the lock to access the Stash, since requests are made in parallel. + with stash.lock: + # Don't use server hostname here, since H2 allows multiple hosts to reuse a connection. + # Server IP is not currently available, unfortunately. + address_key = isomorphic_encode(str(request.client_address) + u"|" + str(request.url_parts.port)) + server_state = stash.take(uuid) or {b"test_failed": False, + b"request_count": 0, b"connection_count": 0} + request_count = server_state[b"request_count"] + request_count += 1 + server_state[b"request_count"] = request_count + if address_key in server_state: + if server_state[address_key] != partition_id: + server_state[b"test_failed"] = True + else: + connection_count = server_state[b"connection_count"] + connection_count += 1 + server_state[b"connection_count"] = connection_count + server_state[address_key] = partition_id + test_failed = server_state[b"test_failed"] + stash.put(uuid, server_state) + + origin = request.headers.get(b"Origin") + if origin: + response.headers.set(b"Access-Control-Allow-Origin", origin) + response.headers.set(b"Access-Control-Allow-Credentials", b"true") + + if request.method == u"OPTIONS": + return handle_preflight(request, response) + + if dispatch == b"fetch_file": + return handle_fetch_file(request, response, partition_id, uuid) + + if dispatch == b"check_partition": + status = request.GET.first(b"status", 200) + if test_failed: + return simple_response(request, response, status, b"OK", b"Multiple partition IDs used on a socket") + body = b"ok" + if request.GET.first(b"addcounter", False): + body += (". Request was sent " + str(request_count) + " times. " + + str(connection_count) + " connections were created.").encode('utf-8') + return simple_response(request, response, status, b"OK", body) + + if dispatch == b"clean_up": + stash.take(uuid) + if test_failed: + return simple_response(request, response, 200, b"OK", b"Test failed, but cleanup completed.") + return simple_response(request, response, 200, b"OK", b"cleanup complete") + + return simple_response(request, response, 404, b"Not Found", b"Unrecognized dispatch parameter: " + dispatch) + +def handle_preflight(request, response): + response.status = (200, b"OK") + response.headers.set(b"Access-Control-Allow-Methods", b"GET") + response.headers.set(b"Access-Control-Allow-Headers", b"header-to-force-cors") + response.headers.set(b"Access-Control-Max-Age", b"86400") + return b"Preflight request" + +def simple_response(request, response, status_code, status_message, body, content_type=b"text/plain"): + response.status = (status_code, status_message) + response.headers.set(b"Content-Type", content_type) + return body + +def handle_fetch_file(request, response, partition_id, uuid): + subresource_origin = request.GET.first(b"subresource_origin", None) + rel_path = request.GET.first(b"path", None) + + # This needs to be passed on to subresources so they all have access to it. + include_credentials = request.GET.first(b"include_credentials", None) + if not subresource_origin or not rel_path or not include_credentials: + return simple_response(request, response, 404, b"Not found", b"Invalid query parameters") + + cur_path = os.path.realpath(isomorphic_decode(__file__)) + base_path = os.path.abspath(os.path.join(os.path.dirname(cur_path), os.pardir, os.pardir, os.pardir)) + path = os.path.abspath(os.path.join(base_path, isomorphic_decode(rel_path))) + + # Basic security check. + if not path.startswith(base_path): + return simple_response(request, response, 404, b"Not found", b"Invalid path") + + sandbox = request.GET.first(b"sandbox", None) + if sandbox == b"true": + response.headers.set(b"Content-Security-Policy", b"sandbox allow-scripts") + + file = open(path, mode="rb") + body = file.read() + file.close() + + subresource_path = b"/" + isomorphic_encode(os.path.relpath(isomorphic_decode(__file__), base_path)).replace(b'\\', b'/') + subresource_params = b"?partition_id=" + partition_id + b"&uuid=" + uuid + b"&subresource_origin=" + subresource_origin + b"&include_credentials=" + include_credentials + body = body.replace(b"SUBRESOURCE_PREFIX:", subresource_origin + subresource_path + subresource_params) + + other_origin = request.GET.first(b"other_origin", None) + if other_origin: + body = body.replace(b"OTHER_PREFIX:", other_origin + subresource_path + subresource_params) + + mimetypes.init() + mimetype_pair = mimetypes.guess_type(path) + mimetype = mimetype_pair[0] + + if mimetype == None or mimetype_pair[1] != None: + return simple_response(request, response, 500, b"Server Error", b"Unknown MIME type") + return simple_response(request, response, 200, b"OK", body, mimetype) diff --git a/test/wpt/tests/fetch/connection-pool/resources/network-partition-worker-checker.html b/test/wpt/tests/fetch/connection-pool/resources/network-partition-worker-checker.html new file mode 100644 index 00000000000..e6b7ea7673f --- /dev/null +++ b/test/wpt/tests/fetch/connection-pool/resources/network-partition-worker-checker.html @@ -0,0 +1,24 @@ + + + + + Worker Network Partition Checker + + + + + + + + + + diff --git a/test/wpt/tests/fetch/connection-pool/resources/network-partition-worker.js b/test/wpt/tests/fetch/connection-pool/resources/network-partition-worker.js new file mode 100644 index 00000000000..1745edfacb1 --- /dev/null +++ b/test/wpt/tests/fetch/connection-pool/resources/network-partition-worker.js @@ -0,0 +1,15 @@ +// This tests the partition key of fetches to subresouce_origin made by the worker and +// imported scripts from subresource_origin. +importScripts('SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=common/utils.js'); +importScripts('SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=resources/testharness.js'); +importScripts('SUBRESOURCE_PREFIX:&dispatch=fetch_file&path=fetch/connection-pool/resources/network-partition-key.js'); + +async function fetch_and_reply() { + try { + await check_partition_ids(); + self.postMessage({result: 'success'}); + } catch (e) { + self.postMessage({result: 'error', details: e.message}); + } +} +fetch_and_reply(); diff --git a/test/wpt/tests/fetch/content-encoding/resources/bad-gzip-body.py b/test/wpt/tests/fetch/content-encoding/resources/bad-gzip-body.py new file mode 100644 index 00000000000..a79b94ed041 --- /dev/null +++ b/test/wpt/tests/fetch/content-encoding/resources/bad-gzip-body.py @@ -0,0 +1,3 @@ +def main(request, response): + headers = [(b"Content-Encoding", b"gzip")] + return headers, b"not actually gzip" diff --git a/test/wpt/tests/fetch/content-length/api-and-duplicate-headers.any.js b/test/wpt/tests/fetch/content-length/api-and-duplicate-headers.any.js new file mode 100644 index 00000000000..8015289f8d6 --- /dev/null +++ b/test/wpt/tests/fetch/content-length/api-and-duplicate-headers.any.js @@ -0,0 +1,23 @@ +promise_test(async t => { + const response = await fetch("resources/identical-duplicates.asis"); + assert_equals(response.statusText, "BLAH"); + assert_equals(response.headers.get("test"), "x, x"); + assert_equals(response.headers.get("content-type"), "text/plain, text/plain"); + assert_equals(response.headers.get("content-length"), "6, 6"); + const text = await response.text(); + assert_equals(text, "Test.\n"); +}, "fetch() and duplicate Content-Length/Content-Type headers"); + +async_test(t => { + const xhr = new XMLHttpRequest(); + xhr.open("GET", "resources/identical-duplicates.asis"); + xhr.send(); + xhr.onload = t.step_func_done(() => { + assert_equals(xhr.statusText, "BLAH"); + assert_equals(xhr.getResponseHeader("test"), "x, x"); + assert_equals(xhr.getResponseHeader("content-type"), "text/plain, text/plain"); + assert_equals(xhr.getResponseHeader("content-length"), "6, 6"); + assert_equals(xhr.getAllResponseHeaders(), "content-length: 6, 6\r\ncontent-type: text/plain, text/plain\r\ntest: x, x\r\n"); + assert_equals(xhr.responseText, "Test.\n"); + }); +}, "XMLHttpRequest and duplicate Content-Length/Content-Type headers"); diff --git a/test/wpt/tests/fetch/content-length/content-length.html b/test/wpt/tests/fetch/content-length/content-length.html new file mode 100644 index 00000000000..cda9b5b5237 --- /dev/null +++ b/test/wpt/tests/fetch/content-length/content-length.html @@ -0,0 +1,14 @@ + + +Content-Length Test + + + +PASS +but FAIL if this is in the body. \ No newline at end of file diff --git a/test/wpt/tests/fetch/content-length/content-length.html.headers b/test/wpt/tests/fetch/content-length/content-length.html.headers new file mode 100644 index 00000000000..25389b7c0fa --- /dev/null +++ b/test/wpt/tests/fetch/content-length/content-length.html.headers @@ -0,0 +1 @@ +Content-Length: 403 diff --git a/test/wpt/tests/fetch/content-length/parsing.window.js b/test/wpt/tests/fetch/content-length/parsing.window.js new file mode 100644 index 00000000000..5028ad943de --- /dev/null +++ b/test/wpt/tests/fetch/content-length/parsing.window.js @@ -0,0 +1,18 @@ +promise_test(() => { + return fetch("resources/content-lengths.json").then(res => res.json()).then(runTests); +}, "Loading JSON…"); + +function runTests(testUnits) { + testUnits.forEach(({ input, output }) => { + promise_test(t => { + const result = fetch(`resources/content-length.py?length=${encodeURIComponent(input)}`); + if (output === null) { + return promise_rejects_js(t, TypeError, result); + } else { + return result.then(res => res.text()).then(text => { + assert_equals(text.length, output); + }); + } + }, `Input: ${format_value(input)}. Expected: ${output === null ? "network error" : output}.`); + }); +} diff --git a/test/wpt/tests/fetch/content-length/resources/content-length.py b/test/wpt/tests/fetch/content-length/resources/content-length.py new file mode 100644 index 00000000000..92cfadeb061 --- /dev/null +++ b/test/wpt/tests/fetch/content-length/resources/content-length.py @@ -0,0 +1,10 @@ +def main(request, response): + response.add_required_headers = False + output = b"HTTP/1.1 200 OK\r\n" + output += b"Content-Type: text/plain;charset=UTF-8\r\n" + output += b"Connection: close\r\n" + output += request.GET.first(b"length") + b"\r\n" + output += b"\r\n" + output += b"Fact: this is really forty-two bytes long." + response.writer.write(output) + response.close_connection = True diff --git a/test/wpt/tests/fetch/content-length/resources/content-lengths.json b/test/wpt/tests/fetch/content-length/resources/content-lengths.json new file mode 100644 index 00000000000..dac9c82dc09 --- /dev/null +++ b/test/wpt/tests/fetch/content-length/resources/content-lengths.json @@ -0,0 +1,126 @@ +[ + { + "input": "Content-Length: 42", + "output": 42 + }, + { + "input": "Content-Length: 42,42", + "output": 42 + }, + { + "input": "Content-Length: 42\r\nContent-Length: 42", + "output": 42 + }, + { + "input": "Content-Length: 42\r\nContent-Length: 42,42", + "output": 42 + }, + { + "input": "Content-Length: 30", + "output": 30 + }, + { + "input": "Content-Length: 30,30", + "output": 30 + }, + { + "input": "Content-Length: 30\r\nContent-Length: 30", + "output": 30 + }, + { + "input": "Content-Length: 30\r\nContent-Length: 30,30", + "output": 30 + }, + { + "input": "Content-Length: 42,30", + "output": null + }, + { + "input": "Content-Length: 30,42", + "output": null + }, + { + "input": "Content-Length: 42\r\nContent-Length: 30", + "output": null + }, + { + "input": "Content-Length: 30\r\nContent-Length: 42", + "output": null + }, + { + "input": "Content-Length: 30,", + "output": null + }, + { + "input": "Content-Length: ,30", + "output": null + }, + { + "input": "Content-Length: 30\r\nContent-Length: \t", + "output": null + }, + { + "input": "Content-Length: \r\nContent-Length: 30", + "output": null + }, + { + "input": "Content-Length: aaaah\r\nContent-Length: nah", + "output": null + }, + { + "input": "Content-Length: aaaah, nah", + "output": null + }, + { + "input": "Content-Length: aaaah\r\nContent-Length: aaaah", + "output": 42 + }, + { + "input": "Content-Length: aaaah, aaaah", + "output": 42 + }, + { + "input": "Content-Length: aaaah", + "output": 42 + }, + { + "input": "Content-Length: 42s", + "output": 42 + }, + { + "input": "Content-Length: 30s", + "output": 42 + }, + { + "input": "Content-Length: -1", + "output": 42 + }, + { + "input": "Content-Length: 0x20", + "output": 42 + }, + { + "input": "Content-Length: 030", + "output": 30 + }, + { + "input": "Content-Length: 030\r\nContent-Length: 30", + "output": null + }, + { + "input": "Content-Length: 030, 30", + "output": null + }, + { + "input": "Content-Length: \"30\"", + "output": 42 + }, + { + "input": "Content-Length:30\r\nContent-Length:,\r\nContent-Length:30", + "output": null + }, + { + "input": "Content-Length: ", + "output": 42 + } +] diff --git a/test/wpt/tests/fetch/content-length/resources/identical-duplicates.asis b/test/wpt/tests/fetch/content-length/resources/identical-duplicates.asis new file mode 100644 index 00000000000..f38c9a4b8a0 --- /dev/null +++ b/test/wpt/tests/fetch/content-length/resources/identical-duplicates.asis @@ -0,0 +1,9 @@ +HTTP/1.1 200 BLAH +Test: x +Test: x +Content-Type: text/plain +Content-Type: text/plain +Content-Length: 6 +Content-Length: 6 + +Test. diff --git a/test/wpt/tests/fetch/content-length/too-long.window.js b/test/wpt/tests/fetch/content-length/too-long.window.js new file mode 100644 index 00000000000..f8cefaa9c23 --- /dev/null +++ b/test/wpt/tests/fetch/content-length/too-long.window.js @@ -0,0 +1,4 @@ +promise_test(async t => { + const result = await fetch(`resources/content-length.py?length=${encodeURIComponent("Content-Length: 50")}`); + await promise_rejects_js(t, TypeError, result.text()); +}, "Content-Length header value of network response exceeds response body"); diff --git a/test/wpt/tests/fetch/content-type/README.md b/test/wpt/tests/fetch/content-type/README.md new file mode 100644 index 00000000000..f553b7ee8e6 --- /dev/null +++ b/test/wpt/tests/fetch/content-type/README.md @@ -0,0 +1,20 @@ +# `resources/content-types.json` + +An array of tests. Each test has these fields: + +* `contentType`: an array of values for the `Content-Type` header. A harness needs to run the test twice if there are multiple values. One time with the values concatenated with `,` followed by a space and one time with multiple `Content-Type` declarations, each on their own line with one of the values, in order. +* `encoding`: the expected encoding, null for the default. +* `mimeType`: the result of extracting a MIME type and serializing it. +* `documentContentType`: the MIME type expected to be exposed in DOM documents. + +(These tests are currently somewhat geared towards browser use, but could be generalized easily enough if someone wanted to contribute tests for MIME types that would cause downloads in the browser or some such.) + +# `resources/script-content-types.json` + +An array of tests, surprise. Each test has these fields: + +* `contentType`: see above. +* `executes`: whether the script is expected to execute. +* `encoding`: how the script is expected to be decoded. + +These tests are expected to be loaded through ` + +
+ diff --git a/test/wpt/tests/fetch/corb/img-png-mislabeled-as-html-nosniff.tentative.sub-ref.html b/test/wpt/tests/fetch/corb/img-png-mislabeled-as-html-nosniff.tentative.sub-ref.html new file mode 100644 index 00000000000..a771ed6a653 --- /dev/null +++ b/test/wpt/tests/fetch/corb/img-png-mislabeled-as-html-nosniff.tentative.sub-ref.html @@ -0,0 +1,4 @@ + + + + diff --git a/test/wpt/tests/fetch/corb/img-png-mislabeled-as-html-nosniff.tentative.sub.html b/test/wpt/tests/fetch/corb/img-png-mislabeled-as-html-nosniff.tentative.sub.html new file mode 100644 index 00000000000..82adc47b0cf --- /dev/null +++ b/test/wpt/tests/fetch/corb/img-png-mislabeled-as-html-nosniff.tentative.sub.html @@ -0,0 +1,11 @@ + + + + + + + diff --git a/test/wpt/tests/fetch/corb/img-png-mislabeled-as-html.sub-ref.html b/test/wpt/tests/fetch/corb/img-png-mislabeled-as-html.sub-ref.html new file mode 100644 index 00000000000..ebb337dba80 --- /dev/null +++ b/test/wpt/tests/fetch/corb/img-png-mislabeled-as-html.sub-ref.html @@ -0,0 +1,4 @@ + + + + diff --git a/test/wpt/tests/fetch/corb/img-png-mislabeled-as-html.sub.html b/test/wpt/tests/fetch/corb/img-png-mislabeled-as-html.sub.html new file mode 100644 index 00000000000..1ae4cfcaa7c --- /dev/null +++ b/test/wpt/tests/fetch/corb/img-png-mislabeled-as-html.sub.html @@ -0,0 +1,10 @@ + + + + + + + diff --git a/test/wpt/tests/fetch/corb/img-svg-doctype-html-mimetype-empty.sub.html b/test/wpt/tests/fetch/corb/img-svg-doctype-html-mimetype-empty.sub.html new file mode 100644 index 00000000000..3219feda171 --- /dev/null +++ b/test/wpt/tests/fetch/corb/img-svg-doctype-html-mimetype-empty.sub.html @@ -0,0 +1,7 @@ + + + + + diff --git a/test/wpt/tests/fetch/corb/img-svg-doctype-html-mimetype-svg.sub.html b/test/wpt/tests/fetch/corb/img-svg-doctype-html-mimetype-svg.sub.html new file mode 100644 index 00000000000..efcfaa27374 --- /dev/null +++ b/test/wpt/tests/fetch/corb/img-svg-doctype-html-mimetype-svg.sub.html @@ -0,0 +1,11 @@ + + + + + diff --git a/test/wpt/tests/fetch/corb/img-svg-invalid.sub-ref.html b/test/wpt/tests/fetch/corb/img-svg-invalid.sub-ref.html new file mode 100644 index 00000000000..484cd0a4fde --- /dev/null +++ b/test/wpt/tests/fetch/corb/img-svg-invalid.sub-ref.html @@ -0,0 +1,5 @@ + + + + diff --git a/test/wpt/tests/fetch/corb/img-svg-labeled-as-dash.sub.html b/test/wpt/tests/fetch/corb/img-svg-labeled-as-dash.sub.html new file mode 100644 index 00000000000..0578b835fe5 --- /dev/null +++ b/test/wpt/tests/fetch/corb/img-svg-labeled-as-dash.sub.html @@ -0,0 +1,6 @@ + + + + + diff --git a/test/wpt/tests/fetch/corb/img-svg-labeled-as-svg-xml.sub.html b/test/wpt/tests/fetch/corb/img-svg-labeled-as-svg-xml.sub.html new file mode 100644 index 00000000000..30a2eb3246c --- /dev/null +++ b/test/wpt/tests/fetch/corb/img-svg-labeled-as-svg-xml.sub.html @@ -0,0 +1,6 @@ + + + + + diff --git a/test/wpt/tests/fetch/corb/img-svg-xml-decl.sub.html b/test/wpt/tests/fetch/corb/img-svg-xml-decl.sub.html new file mode 100644 index 00000000000..0d3aeafb258 --- /dev/null +++ b/test/wpt/tests/fetch/corb/img-svg-xml-decl.sub.html @@ -0,0 +1,6 @@ + + + + + diff --git a/test/wpt/tests/fetch/corb/img-svg.sub-ref.html b/test/wpt/tests/fetch/corb/img-svg.sub-ref.html new file mode 100644 index 00000000000..5462f685a0e --- /dev/null +++ b/test/wpt/tests/fetch/corb/img-svg.sub-ref.html @@ -0,0 +1,5 @@ + + + + diff --git a/test/wpt/tests/fetch/corb/preload-image-png-mislabeled-as-html-nosniff.tentative.sub.html b/test/wpt/tests/fetch/corb/preload-image-png-mislabeled-as-html-nosniff.tentative.sub.html new file mode 100644 index 00000000000..cea80f2f89f --- /dev/null +++ b/test/wpt/tests/fetch/corb/preload-image-png-mislabeled-as-html-nosniff.tentative.sub.html @@ -0,0 +1,24 @@ + + + + + +
+ + + + + diff --git a/test/wpt/tests/fetch/corb/resources/css-mislabeled-as-html-nosniff.css b/test/wpt/tests/fetch/corb/resources/css-mislabeled-as-html-nosniff.css new file mode 100644 index 00000000000..afd2b92975d --- /dev/null +++ b/test/wpt/tests/fetch/corb/resources/css-mislabeled-as-html-nosniff.css @@ -0,0 +1 @@ +#header { color: red; } diff --git a/test/wpt/tests/fetch/corb/resources/css-mislabeled-as-html-nosniff.css.headers b/test/wpt/tests/fetch/corb/resources/css-mislabeled-as-html-nosniff.css.headers new file mode 100644 index 00000000000..0f228f94ecb --- /dev/null +++ b/test/wpt/tests/fetch/corb/resources/css-mislabeled-as-html-nosniff.css.headers @@ -0,0 +1,2 @@ +Content-Type: text/html +X-Content-Type-Options: nosniff diff --git a/test/wpt/tests/fetch/corb/resources/css-mislabeled-as-html.css b/test/wpt/tests/fetch/corb/resources/css-mislabeled-as-html.css new file mode 100644 index 00000000000..afd2b92975d --- /dev/null +++ b/test/wpt/tests/fetch/corb/resources/css-mislabeled-as-html.css @@ -0,0 +1 @@ +#header { color: red; } diff --git a/test/wpt/tests/fetch/corb/resources/css-mislabeled-as-html.css.headers b/test/wpt/tests/fetch/corb/resources/css-mislabeled-as-html.css.headers new file mode 100644 index 00000000000..156209f9c81 --- /dev/null +++ b/test/wpt/tests/fetch/corb/resources/css-mislabeled-as-html.css.headers @@ -0,0 +1 @@ +Content-Type: text/html diff --git a/test/wpt/tests/fetch/corb/resources/css-with-json-parser-breaker.css b/test/wpt/tests/fetch/corb/resources/css-with-json-parser-breaker.css new file mode 100644 index 00000000000..7db6f5c6d36 --- /dev/null +++ b/test/wpt/tests/fetch/corb/resources/css-with-json-parser-breaker.css @@ -0,0 +1,3 @@ +)]}' +{} +#header { color: red; } diff --git a/test/wpt/tests/fetch/corb/resources/empty-labeled-as-png.png b/test/wpt/tests/fetch/corb/resources/empty-labeled-as-png.png new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/wpt/tests/fetch/corb/resources/empty-labeled-as-png.png.headers b/test/wpt/tests/fetch/corb/resources/empty-labeled-as-png.png.headers new file mode 100644 index 00000000000..e7be84a714b --- /dev/null +++ b/test/wpt/tests/fetch/corb/resources/empty-labeled-as-png.png.headers @@ -0,0 +1 @@ +Content-Type: image/png diff --git a/test/wpt/tests/fetch/corb/resources/html-correctly-labeled.html b/test/wpt/tests/fetch/corb/resources/html-correctly-labeled.html new file mode 100644 index 00000000000..7bad71bfbd8 --- /dev/null +++ b/test/wpt/tests/fetch/corb/resources/html-correctly-labeled.html @@ -0,0 +1,10 @@ + + + + + Page Title + + +

Page body

+ + diff --git a/test/wpt/tests/fetch/corb/resources/html-correctly-labeled.html.headers b/test/wpt/tests/fetch/corb/resources/html-correctly-labeled.html.headers new file mode 100644 index 00000000000..156209f9c81 --- /dev/null +++ b/test/wpt/tests/fetch/corb/resources/html-correctly-labeled.html.headers @@ -0,0 +1 @@ +Content-Type: text/html diff --git a/test/wpt/tests/fetch/corb/resources/html-js-polyglot.js b/test/wpt/tests/fetch/corb/resources/html-js-polyglot.js new file mode 100644 index 00000000000..db45bb4acc9 --- /dev/null +++ b/test/wpt/tests/fetch/corb/resources/html-js-polyglot.js @@ -0,0 +1,9 @@ + diff --git a/test/wpt/tests/fetch/corb/resources/html-js-polyglot.js.headers b/test/wpt/tests/fetch/corb/resources/html-js-polyglot.js.headers new file mode 100644 index 00000000000..156209f9c81 --- /dev/null +++ b/test/wpt/tests/fetch/corb/resources/html-js-polyglot.js.headers @@ -0,0 +1 @@ +Content-Type: text/html diff --git a/test/wpt/tests/fetch/corb/resources/html-js-polyglot2.js b/test/wpt/tests/fetch/corb/resources/html-js-polyglot2.js new file mode 100644 index 00000000000..faae1b7682b --- /dev/null +++ b/test/wpt/tests/fetch/corb/resources/html-js-polyglot2.js @@ -0,0 +1,10 @@ + diff --git a/test/wpt/tests/fetch/corb/resources/html-js-polyglot2.js.headers b/test/wpt/tests/fetch/corb/resources/html-js-polyglot2.js.headers new file mode 100644 index 00000000000..156209f9c81 --- /dev/null +++ b/test/wpt/tests/fetch/corb/resources/html-js-polyglot2.js.headers @@ -0,0 +1 @@ +Content-Type: text/html diff --git a/test/wpt/tests/fetch/corb/resources/js-mislabeled-as-html-nosniff.js b/test/wpt/tests/fetch/corb/resources/js-mislabeled-as-html-nosniff.js new file mode 100644 index 00000000000..a880a5bc724 --- /dev/null +++ b/test/wpt/tests/fetch/corb/resources/js-mislabeled-as-html-nosniff.js @@ -0,0 +1 @@ +window.has_executed_script = true; diff --git a/test/wpt/tests/fetch/corb/resources/js-mislabeled-as-html-nosniff.js.headers b/test/wpt/tests/fetch/corb/resources/js-mislabeled-as-html-nosniff.js.headers new file mode 100644 index 00000000000..0f228f94ecb --- /dev/null +++ b/test/wpt/tests/fetch/corb/resources/js-mislabeled-as-html-nosniff.js.headers @@ -0,0 +1,2 @@ +Content-Type: text/html +X-Content-Type-Options: nosniff diff --git a/test/wpt/tests/fetch/corb/resources/js-mislabeled-as-html.js b/test/wpt/tests/fetch/corb/resources/js-mislabeled-as-html.js new file mode 100644 index 00000000000..a880a5bc724 --- /dev/null +++ b/test/wpt/tests/fetch/corb/resources/js-mislabeled-as-html.js @@ -0,0 +1 @@ +window.has_executed_script = true; diff --git a/test/wpt/tests/fetch/corb/resources/js-mislabeled-as-html.js.headers b/test/wpt/tests/fetch/corb/resources/js-mislabeled-as-html.js.headers new file mode 100644 index 00000000000..156209f9c81 --- /dev/null +++ b/test/wpt/tests/fetch/corb/resources/js-mislabeled-as-html.js.headers @@ -0,0 +1 @@ +Content-Type: text/html diff --git a/test/wpt/tests/fetch/corb/resources/png-correctly-labeled.png b/test/wpt/tests/fetch/corb/resources/png-correctly-labeled.png new file mode 100644 index 0000000000000000000000000000000000000000..820f8cace2143bfc45c0c301e84b6c29b8630068 GIT binary patch literal 1010 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD3?#3*wSy!Wi-X*q7}lMWc?smOq&xaLGB9lH z=l+w(3gjy!dj$D1FjT2AFf_CT|`v++n6B;aK<+8d}OFfd9ReoF^w N_jL7hS?83{1OVd{KuZ7s literal 0 HcmV?d00001 diff --git a/test/wpt/tests/fetch/corb/resources/png-correctly-labeled.png.headers b/test/wpt/tests/fetch/corb/resources/png-correctly-labeled.png.headers new file mode 100644 index 00000000000..e7be84a714b --- /dev/null +++ b/test/wpt/tests/fetch/corb/resources/png-correctly-labeled.png.headers @@ -0,0 +1 @@ +Content-Type: image/png diff --git a/test/wpt/tests/fetch/corb/resources/png-mislabeled-as-html-nosniff.png b/test/wpt/tests/fetch/corb/resources/png-mislabeled-as-html-nosniff.png new file mode 100644 index 0000000000000000000000000000000000000000..820f8cace2143bfc45c0c301e84b6c29b8630068 GIT binary patch literal 1010 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD3?#3*wSy!Wi-X*q7}lMWc?smOq&xaLGB9lH z=l+w(3gjy!dj$D1FjT2AFf_CT|`v++n6B;aK<+8d}OFfd9ReoF^w N_jL7hS?83{1OVd{KuZ7s literal 0 HcmV?d00001 diff --git a/test/wpt/tests/fetch/corb/resources/png-mislabeled-as-html-nosniff.png.headers b/test/wpt/tests/fetch/corb/resources/png-mislabeled-as-html-nosniff.png.headers new file mode 100644 index 00000000000..0f228f94ecb --- /dev/null +++ b/test/wpt/tests/fetch/corb/resources/png-mislabeled-as-html-nosniff.png.headers @@ -0,0 +1,2 @@ +Content-Type: text/html +X-Content-Type-Options: nosniff diff --git a/test/wpt/tests/fetch/corb/resources/png-mislabeled-as-html.png b/test/wpt/tests/fetch/corb/resources/png-mislabeled-as-html.png new file mode 100644 index 0000000000000000000000000000000000000000..820f8cace2143bfc45c0c301e84b6c29b8630068 GIT binary patch literal 1010 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD3?#3*wSy!Wi-X*q7}lMWc?smOq&xaLGB9lH z=l+w(3gjy!dj$D1FjT2AFf_CT|`v++n6B;aK<+8d}OFfd9ReoF^w N_jL7hS?83{1OVd{KuZ7s literal 0 HcmV?d00001 diff --git a/test/wpt/tests/fetch/corb/resources/png-mislabeled-as-html.png.headers b/test/wpt/tests/fetch/corb/resources/png-mislabeled-as-html.png.headers new file mode 100644 index 00000000000..156209f9c81 --- /dev/null +++ b/test/wpt/tests/fetch/corb/resources/png-mislabeled-as-html.png.headers @@ -0,0 +1 @@ +Content-Type: text/html diff --git a/test/wpt/tests/fetch/corb/resources/response_block_probe.js b/test/wpt/tests/fetch/corb/resources/response_block_probe.js new file mode 100644 index 00000000000..d23ad488af2 --- /dev/null +++ b/test/wpt/tests/fetch/corb/resources/response_block_probe.js @@ -0,0 +1 @@ +window.script_callback(); diff --git a/test/wpt/tests/fetch/corb/resources/response_block_probe.js.headers b/test/wpt/tests/fetch/corb/resources/response_block_probe.js.headers new file mode 100644 index 00000000000..0d848b02c2e --- /dev/null +++ b/test/wpt/tests/fetch/corb/resources/response_block_probe.js.headers @@ -0,0 +1 @@ +Content-Type: text/csv diff --git a/test/wpt/tests/fetch/corb/resources/sniffable-resource.py b/test/wpt/tests/fetch/corb/resources/sniffable-resource.py new file mode 100644 index 00000000000..f8150936acb --- /dev/null +++ b/test/wpt/tests/fetch/corb/resources/sniffable-resource.py @@ -0,0 +1,11 @@ +def main(request, response): + body = request.GET.first(b"body", None) + type = request.GET.first(b"type", None) + + response.add_required_headers = False + response.writer.write_status(200) + response.writer.write_header(b"content-length", len(body)) + response.writer.write_header(b"content-type", type) + response.writer.end_headers() + + response.writer.write(body) diff --git a/test/wpt/tests/fetch/corb/resources/subframe-that-posts-html-containing-blob-url-to-parent.html b/test/wpt/tests/fetch/corb/resources/subframe-that-posts-html-containing-blob-url-to-parent.html new file mode 100644 index 00000000000..67b3ad5a600 --- /dev/null +++ b/test/wpt/tests/fetch/corb/resources/subframe-that-posts-html-containing-blob-url-to-parent.html @@ -0,0 +1,16 @@ + + + diff --git a/test/wpt/tests/fetch/corb/resources/svg-doctype-html-mimetype-empty.svg b/test/wpt/tests/fetch/corb/resources/svg-doctype-html-mimetype-empty.svg new file mode 100644 index 00000000000..fa2d29b3b0f --- /dev/null +++ b/test/wpt/tests/fetch/corb/resources/svg-doctype-html-mimetype-empty.svg @@ -0,0 +1,4 @@ + + + + diff --git a/test/wpt/tests/fetch/corb/resources/svg-doctype-html-mimetype-empty.svg.headers b/test/wpt/tests/fetch/corb/resources/svg-doctype-html-mimetype-empty.svg.headers new file mode 100644 index 00000000000..29515ee7d4a --- /dev/null +++ b/test/wpt/tests/fetch/corb/resources/svg-doctype-html-mimetype-empty.svg.headers @@ -0,0 +1 @@ +Content-Type: diff --git a/test/wpt/tests/fetch/corb/resources/svg-doctype-html-mimetype-svg.svg b/test/wpt/tests/fetch/corb/resources/svg-doctype-html-mimetype-svg.svg new file mode 100644 index 00000000000..fa2d29b3b0f --- /dev/null +++ b/test/wpt/tests/fetch/corb/resources/svg-doctype-html-mimetype-svg.svg @@ -0,0 +1,4 @@ + + + + diff --git a/test/wpt/tests/fetch/corb/resources/svg-doctype-html-mimetype-svg.svg.headers b/test/wpt/tests/fetch/corb/resources/svg-doctype-html-mimetype-svg.svg.headers new file mode 100644 index 00000000000..070de35fbe9 --- /dev/null +++ b/test/wpt/tests/fetch/corb/resources/svg-doctype-html-mimetype-svg.svg.headers @@ -0,0 +1 @@ +Content-Type: image/svg+xml diff --git a/test/wpt/tests/fetch/corb/resources/svg-labeled-as-dash.svg b/test/wpt/tests/fetch/corb/resources/svg-labeled-as-dash.svg new file mode 100644 index 00000000000..2b7d1016b1e --- /dev/null +++ b/test/wpt/tests/fetch/corb/resources/svg-labeled-as-dash.svg @@ -0,0 +1,3 @@ + + + diff --git a/test/wpt/tests/fetch/corb/resources/svg-labeled-as-dash.svg.headers b/test/wpt/tests/fetch/corb/resources/svg-labeled-as-dash.svg.headers new file mode 100644 index 00000000000..43ce612c9fd --- /dev/null +++ b/test/wpt/tests/fetch/corb/resources/svg-labeled-as-dash.svg.headers @@ -0,0 +1 @@ +Content-Type: application/dash+xml diff --git a/test/wpt/tests/fetch/corb/resources/svg-labeled-as-svg-xml.svg b/test/wpt/tests/fetch/corb/resources/svg-labeled-as-svg-xml.svg new file mode 100644 index 00000000000..2b7d1016b1e --- /dev/null +++ b/test/wpt/tests/fetch/corb/resources/svg-labeled-as-svg-xml.svg @@ -0,0 +1,3 @@ + + + diff --git a/test/wpt/tests/fetch/corb/resources/svg-labeled-as-svg-xml.svg.headers b/test/wpt/tests/fetch/corb/resources/svg-labeled-as-svg-xml.svg.headers new file mode 100644 index 00000000000..070de35fbe9 --- /dev/null +++ b/test/wpt/tests/fetch/corb/resources/svg-labeled-as-svg-xml.svg.headers @@ -0,0 +1 @@ +Content-Type: image/svg+xml diff --git a/test/wpt/tests/fetch/corb/resources/svg-xml-decl.svg b/test/wpt/tests/fetch/corb/resources/svg-xml-decl.svg new file mode 100644 index 00000000000..3b39aff8e5c --- /dev/null +++ b/test/wpt/tests/fetch/corb/resources/svg-xml-decl.svg @@ -0,0 +1,4 @@ + + + + diff --git a/test/wpt/tests/fetch/corb/resources/svg.svg b/test/wpt/tests/fetch/corb/resources/svg.svg new file mode 100644 index 00000000000..2b7d1016b1e --- /dev/null +++ b/test/wpt/tests/fetch/corb/resources/svg.svg @@ -0,0 +1,3 @@ + + + diff --git a/test/wpt/tests/fetch/corb/resources/svg.svg.headers b/test/wpt/tests/fetch/corb/resources/svg.svg.headers new file mode 100644 index 00000000000..070de35fbe9 --- /dev/null +++ b/test/wpt/tests/fetch/corb/resources/svg.svg.headers @@ -0,0 +1 @@ +Content-Type: image/svg+xml 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 new file mode 100644 index 00000000000..860e0d3b93c --- /dev/null +++ b/test/wpt/tests/fetch/corb/response_block.tentative.sub.https.html @@ -0,0 +1,44 @@ + + + + + diff --git a/test/wpt/tests/fetch/corb/script-html-correctly-labeled.tentative.sub.html b/test/wpt/tests/fetch/corb/script-html-correctly-labeled.tentative.sub.html new file mode 100644 index 00000000000..6d1947cea7d --- /dev/null +++ b/test/wpt/tests/fetch/corb/script-html-correctly-labeled.tentative.sub.html @@ -0,0 +1,32 @@ + + + + + +
+ diff --git a/test/wpt/tests/fetch/corb/script-html-js-polyglot.sub.html b/test/wpt/tests/fetch/corb/script-html-js-polyglot.sub.html new file mode 100644 index 00000000000..9a272d63ffc --- /dev/null +++ b/test/wpt/tests/fetch/corb/script-html-js-polyglot.sub.html @@ -0,0 +1,32 @@ + + + + + +
+ diff --git a/test/wpt/tests/fetch/corb/script-html-via-cross-origin-blob-url.sub.html b/test/wpt/tests/fetch/corb/script-html-via-cross-origin-blob-url.sub.html new file mode 100644 index 00000000000..c8a90c79b3f --- /dev/null +++ b/test/wpt/tests/fetch/corb/script-html-via-cross-origin-blob-url.sub.html @@ -0,0 +1,38 @@ + + + + + +
+ diff --git a/test/wpt/tests/fetch/corb/script-js-mislabeled-as-html-nosniff.sub.html b/test/wpt/tests/fetch/corb/script-js-mislabeled-as-html-nosniff.sub.html new file mode 100644 index 00000000000..b6bc90964de --- /dev/null +++ b/test/wpt/tests/fetch/corb/script-js-mislabeled-as-html-nosniff.sub.html @@ -0,0 +1,33 @@ + + + + + +
+ + + + + + + diff --git a/test/wpt/tests/fetch/corb/script-js-mislabeled-as-html.sub.html b/test/wpt/tests/fetch/corb/script-js-mislabeled-as-html.sub.html new file mode 100644 index 00000000000..44cb1f8659d --- /dev/null +++ b/test/wpt/tests/fetch/corb/script-js-mislabeled-as-html.sub.html @@ -0,0 +1,25 @@ + + + + + +
+ + + + + + + diff --git a/test/wpt/tests/fetch/corb/script-resource-with-json-parser-breaker.tentative.sub.html b/test/wpt/tests/fetch/corb/script-resource-with-json-parser-breaker.tentative.sub.html new file mode 100644 index 00000000000..f0eb1f0ab1f --- /dev/null +++ b/test/wpt/tests/fetch/corb/script-resource-with-json-parser-breaker.tentative.sub.html @@ -0,0 +1,85 @@ + + + + + +
+ diff --git a/test/wpt/tests/fetch/corb/script-resource-with-nonsniffable-types.tentative.sub.html b/test/wpt/tests/fetch/corb/script-resource-with-nonsniffable-types.tentative.sub.html new file mode 100644 index 00000000000..6d490d55bce --- /dev/null +++ b/test/wpt/tests/fetch/corb/script-resource-with-nonsniffable-types.tentative.sub.html @@ -0,0 +1,84 @@ + + + + + + +
+ diff --git a/test/wpt/tests/fetch/corb/style-css-mislabeled-as-html-nosniff.sub.html b/test/wpt/tests/fetch/corb/style-css-mislabeled-as-html-nosniff.sub.html new file mode 100644 index 00000000000..8fef0dc59e4 --- /dev/null +++ b/test/wpt/tests/fetch/corb/style-css-mislabeled-as-html-nosniff.sub.html @@ -0,0 +1,42 @@ + + + +CSS is not applied (because of nosniff + non-text/css headers) + + + + + + + + + + + +

Header example

+

Paragraph body

+ + + diff --git a/test/wpt/tests/fetch/corb/style-css-mislabeled-as-html.sub.html b/test/wpt/tests/fetch/corb/style-css-mislabeled-as-html.sub.html new file mode 100644 index 00000000000..4f0b4c22f56 --- /dev/null +++ b/test/wpt/tests/fetch/corb/style-css-mislabeled-as-html.sub.html @@ -0,0 +1,36 @@ + + + +CSS is not applied (because of strict content-type enforcement for cross-origin stylesheets) + + + + + + + + + + + +

Header example

+

Paragraph body

+ + + diff --git a/test/wpt/tests/fetch/corb/style-css-with-json-parser-breaker.sub.html b/test/wpt/tests/fetch/corb/style-css-with-json-parser-breaker.sub.html new file mode 100644 index 00000000000..29ed586a4f0 --- /dev/null +++ b/test/wpt/tests/fetch/corb/style-css-with-json-parser-breaker.sub.html @@ -0,0 +1,38 @@ + + + +CORB doesn't block a stylesheet that has a proper Content-Type and begins with a JSON parser breaker + + + + + + + + + + + +

Header example

+

Paragraph body

+ + + diff --git a/test/wpt/tests/fetch/corb/style-html-correctly-labeled.sub.html b/test/wpt/tests/fetch/corb/style-html-correctly-labeled.sub.html new file mode 100644 index 00000000000..cdefcd2d2c9 --- /dev/null +++ b/test/wpt/tests/fetch/corb/style-html-correctly-labeled.sub.html @@ -0,0 +1,41 @@ + + + +CSS is not applied (because of mismatched Content-Type header) + + + + + + + + + + + +

Header example

+

Paragraph body

+ + + diff --git a/test/wpt/tests/fetch/cross-origin-resource-policy/fetch-in-iframe.html b/test/wpt/tests/fetch/cross-origin-resource-policy/fetch-in-iframe.html new file mode 100644 index 00000000000..cc6a3a81bcf --- /dev/null +++ b/test/wpt/tests/fetch/cross-origin-resource-policy/fetch-in-iframe.html @@ -0,0 +1,67 @@ + + + + + + + + + + + diff --git a/test/wpt/tests/fetch/cross-origin-resource-policy/fetch.any.js b/test/wpt/tests/fetch/cross-origin-resource-policy/fetch.any.js new file mode 100644 index 00000000000..64a7bfeb864 --- /dev/null +++ b/test/wpt/tests/fetch/cross-origin-resource-policy/fetch.any.js @@ -0,0 +1,76 @@ +// META: timeout=long +// META: global=window,dedicatedworker,sharedworker +// META: script=/common/get-host-info.sub.js + +const host = get_host_info(); +const path = "/fetch/cross-origin-resource-policy/"; +const localBaseURL = host.HTTP_ORIGIN + path; +const sameSiteBaseURL = "http://" + host.ORIGINAL_HOST + ":" + host.HTTP_PORT2 + path; +const notSameSiteBaseURL = host.HTTP_NOTSAMESITE_ORIGIN + path; +const httpsBaseURL = host.HTTPS_ORIGIN + path; + +promise_test(async () => { + const response = await fetch("./resources/hello.py?corp=same-origin"); + assert_equals(await response.text(), "hello"); +}, "Same-origin fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header."); + +promise_test(async () => { + const response = await fetch("./resources/hello.py?corp=same-site"); + assert_equals(await response.text(), "hello"); +}, "Same-origin fetch with a 'Cross-Origin-Resource-Policy: same-site' response header."); + +promise_test(async (test) => { + const response = await fetch(notSameSiteBaseURL + "resources/hello.py?corp=same-origin"); + assert_equals(await response.text(), "hello"); +}, "Cross-origin cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header."); + +promise_test(async (test) => { + const response = await fetch(notSameSiteBaseURL + "resources/hello.py?corp=same-site"); + assert_equals(await response.text(), "hello"); +}, "Cross-origin cors fetch with a 'Cross-Origin-Resource-Policy: same-site' response header."); + +promise_test((test) => { + const remoteURL = notSameSiteBaseURL + "resources/hello.py?corp=same-origin"; + return promise_rejects_js(test, TypeError, fetch(remoteURL, { mode : "no-cors" })); +}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header."); + +promise_test((test) => { + const remoteURL = notSameSiteBaseURL + "resources/hello.py?corp=same-site"; + return promise_rejects_js(test, TypeError, fetch(remoteURL, { mode: "no-cors" })); +}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-site' response header."); + +promise_test((test) => { + const remoteURL = httpsBaseURL + "resources/hello.py?corp=same-site"; + return promise_rejects_js(test, TypeError, fetch(remoteURL, { mode: "no-cors" })); +}, "Cross-scheme (HTTP to HTTPS) no-cors fetch to a same-site URL with a 'Cross-Origin-Resource-Policy: same-site' response header."); + +promise_test((test) => { + const remoteURL = httpsBaseURL + "resources/hello.py?corp=same-origin"; + return promise_rejects_js(test, TypeError, fetch(remoteURL, { mode : "no-cors" })); +}, "Cross-origin no-cors fetch to a same-site URL with a 'Cross-Origin-Resource-Policy: same-origin' response header."); + +promise_test(async (test) => { + const remoteSameSiteURL = sameSiteBaseURL + "resources/hello.py?corp=same-site"; + + await fetch(remoteSameSiteURL, { mode: "no-cors" }); + + return promise_rejects_js(test, TypeError, fetch(sameSiteBaseURL + "resources/hello.py?corp=same-origin", { mode: "no-cors" })); +}, "Valid cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-site' response header."); + +promise_test((test) => { + const finalURL = notSameSiteBaseURL + "resources/hello.py?corp=same-origin"; + return promise_rejects_js(test, TypeError, fetch("resources/redirect.py?redirectTo=" + encodeURIComponent(finalURL), { mode: "no-cors" })); +}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header after a redirection."); + +promise_test((test) => { + const finalURL = localBaseURL + "resources/hello.py?corp=same-origin"; + return fetch(notSameSiteBaseURL + "resources/redirect.py?redirectTo=" + encodeURIComponent(finalURL), { mode: "no-cors" }); +}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header after a cross-origin redirection."); + +promise_test(async (test) => { + const finalURL = localBaseURL + "resources/hello.py?corp=same-origin"; + + await fetch(finalURL, { mode: "no-cors" }); + + return promise_rejects_js(test, TypeError, fetch(notSameSiteBaseURL + "resources/redirect.py?corp=same-origin&redirectTo=" + encodeURIComponent(finalURL), { mode: "no-cors" })); +}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' redirect response header."); diff --git a/test/wpt/tests/fetch/cross-origin-resource-policy/fetch.https.any.js b/test/wpt/tests/fetch/cross-origin-resource-policy/fetch.https.any.js new file mode 100644 index 00000000000..c9b5b7502f4 --- /dev/null +++ b/test/wpt/tests/fetch/cross-origin-resource-policy/fetch.https.any.js @@ -0,0 +1,56 @@ +// META: timeout=long +// META: global=window,worker +// META: script=/common/get-host-info.sub.js + +const host = get_host_info(); +const path = "/fetch/cross-origin-resource-policy/"; +const localBaseURL = host.HTTPS_ORIGIN + path; +const notSameSiteBaseURL = host.HTTPS_NOTSAMESITE_ORIGIN + path; + +promise_test(async () => { + const response = await fetch("./resources/hello.py?corp=same-origin"); + assert_equals(await response.text(), "hello"); +}, "Same-origin fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header."); + +promise_test(async () => { + const response = await fetch("./resources/hello.py?corp=same-site"); + assert_equals(await response.text(), "hello"); +}, "Same-origin fetch with a 'Cross-Origin-Resource-Policy: same-site' response header."); + +promise_test(async (test) => { + const response = await fetch(notSameSiteBaseURL + "resources/hello.py?corp=same-origin"); + assert_equals(await response.text(), "hello"); +}, "Cross-origin cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header."); + +promise_test(async (test) => { + const response = await fetch(notSameSiteBaseURL + "resources/hello.py?corp=same-site"); + assert_equals(await response.text(), "hello"); +}, "Cross-origin cors fetch with a 'Cross-Origin-Resource-Policy: same-site' response header."); + +promise_test((test) => { + const remoteURL = notSameSiteBaseURL + "resources/hello.py?corp=same-origin"; + return promise_rejects_js(test, TypeError, fetch(remoteURL, { mode : "no-cors" })); +}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header."); + +promise_test((test) => { + const remoteURL = notSameSiteBaseURL + "resources/hello.py?corp=same-site"; + return promise_rejects_js(test, TypeError, fetch(remoteURL, { mode: "no-cors" })); +}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-site' response header."); + +promise_test((test) => { + const finalURL = notSameSiteBaseURL + "resources/hello.py?corp=same-origin"; + return promise_rejects_js(test, TypeError, fetch("resources/redirect.py?redirectTo=" + encodeURIComponent(finalURL), { mode: "no-cors" })); +}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header after a redirection."); + +promise_test((test) => { + const finalURL = localBaseURL + "resources/hello.py?corp=same-origin"; + return fetch(notSameSiteBaseURL + "resources/redirect.py?redirectTo=" + encodeURIComponent(finalURL), { mode: "no-cors" }); +}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' response header after a cross-origin redirection."); + +promise_test(async (test) => { + const finalURL = localBaseURL + "resources/hello.py?corp=same-origin"; + + await fetch(finalURL, { mode: "no-cors" }); + + return promise_rejects_js(test, TypeError, fetch(notSameSiteBaseURL + "resources/redirect.py?corp=same-origin&redirectTo=" + encodeURIComponent(finalURL), { mode: "no-cors" })); +}, "Cross-origin no-cors fetch with a 'Cross-Origin-Resource-Policy: same-origin' redirect response header."); diff --git a/test/wpt/tests/fetch/cross-origin-resource-policy/iframe-loads.html b/test/wpt/tests/fetch/cross-origin-resource-policy/iframe-loads.html new file mode 100644 index 00000000000..63902c302b7 --- /dev/null +++ b/test/wpt/tests/fetch/cross-origin-resource-policy/iframe-loads.html @@ -0,0 +1,46 @@ + + + + + + + + + + + diff --git a/test/wpt/tests/fetch/cross-origin-resource-policy/image-loads.html b/test/wpt/tests/fetch/cross-origin-resource-policy/image-loads.html new file mode 100644 index 00000000000..060b7551ea5 --- /dev/null +++ b/test/wpt/tests/fetch/cross-origin-resource-policy/image-loads.html @@ -0,0 +1,54 @@ + + + + + + + + +
+ + + diff --git a/test/wpt/tests/fetch/cross-origin-resource-policy/resources/green.png b/test/wpt/tests/fetch/cross-origin-resource-policy/resources/green.png new file mode 100644 index 0000000000000000000000000000000000000000..28a1faab37797ef39454aa1deac1b470712f7be4 GIT binary patch literal 87 zcmeAS@N?(olHy`uVBq!ia0vp^DL`z*$P6SW{C@KnNHGWagt#*NXE2F7umZ^C_jGX# j(GX2ekYHV$kio>jw1

The iframe

" + diff --git a/test/wpt/tests/fetch/cross-origin-resource-policy/resources/iframeFetch.html b/test/wpt/tests/fetch/cross-origin-resource-policy/resources/iframeFetch.html new file mode 100644 index 00000000000..257185805d9 --- /dev/null +++ b/test/wpt/tests/fetch/cross-origin-resource-policy/resources/iframeFetch.html @@ -0,0 +1,19 @@ + + + + + + +

The iframe making a same origin fetch call.

+ + diff --git a/test/wpt/tests/fetch/cross-origin-resource-policy/resources/image.py b/test/wpt/tests/fetch/cross-origin-resource-policy/resources/image.py new file mode 100644 index 00000000000..2a779cf11bf --- /dev/null +++ b/test/wpt/tests/fetch/cross-origin-resource-policy/resources/image.py @@ -0,0 +1,22 @@ +import os.path + +from wptserve.utils import isomorphic_decode + +def main(request, response): + type = request.GET.first(b"type", None) + + body = open(os.path.join(os.path.dirname(isomorphic_decode(__file__)), u"green.png"), u"rb").read() + + response.add_required_headers = False + response.writer.write_status(200) + + if b'corp' in request.GET: + response.writer.write_header(b"cross-origin-resource-policy", request.GET[b'corp']) + if b'acao' in request.GET: + response.writer.write_header(b"access-control-allow-origin", request.GET[b'acao']) + response.writer.write_header(b"content-length", len(body)) + if(type != None): + response.writer.write_header(b"content-type", type) + response.writer.end_headers() + + response.writer.write(body) diff --git a/test/wpt/tests/fetch/cross-origin-resource-policy/resources/redirect.py b/test/wpt/tests/fetch/cross-origin-resource-policy/resources/redirect.py new file mode 100644 index 00000000000..0dad4dd923b --- /dev/null +++ b/test/wpt/tests/fetch/cross-origin-resource-policy/resources/redirect.py @@ -0,0 +1,6 @@ +def main(request, response): + headers = [(b"Location", request.GET[b'redirectTo'])] + if b'corp' in request.GET: + headers.append((b'Cross-Origin-Resource-Policy', request.GET[b'corp'])) + + return 302, headers, b"" diff --git a/test/wpt/tests/fetch/cross-origin-resource-policy/resources/script.py b/test/wpt/tests/fetch/cross-origin-resource-policy/resources/script.py new file mode 100644 index 00000000000..58f8d341547 --- /dev/null +++ b/test/wpt/tests/fetch/cross-origin-resource-policy/resources/script.py @@ -0,0 +1,6 @@ +def main(request, response): + headers = [(b"Cross-Origin-Resource-Policy", request.GET[b'corp'])] + if b'origin' in request.headers: + headers.append((b'Access-Control-Allow-Origin', request.headers[b'origin'])) + + return 200, headers, b"" diff --git a/test/wpt/tests/fetch/cross-origin-resource-policy/scheme-restriction.any.js b/test/wpt/tests/fetch/cross-origin-resource-policy/scheme-restriction.any.js new file mode 100644 index 00000000000..8f6338176a3 --- /dev/null +++ b/test/wpt/tests/fetch/cross-origin-resource-policy/scheme-restriction.any.js @@ -0,0 +1,7 @@ +// META: script=/common/get-host-info.sub.js + +promise_test(t => { + return promise_rejects_js(t, + TypeError, + fetch(get_host_info().HTTPS_REMOTE_ORIGIN + "/fetch/cross-origin-resource-policy/resources/hello.py?corp=same-site", { mode: "no-cors" })); +}, "Cross-Origin-Resource-Policy: same-site blocks retrieving HTTPS from HTTP"); diff --git a/test/wpt/tests/fetch/cross-origin-resource-policy/scheme-restriction.https.window.js b/test/wpt/tests/fetch/cross-origin-resource-policy/scheme-restriction.https.window.js new file mode 100644 index 00000000000..4c745718741 --- /dev/null +++ b/test/wpt/tests/fetch/cross-origin-resource-policy/scheme-restriction.https.window.js @@ -0,0 +1,13 @@ +// META: script=/common/get-host-info.sub.js + +promise_test(t => { + const img = new Image(); + img.src = get_host_info().HTTP_REMOTE_ORIGIN + "/fetch/cross-origin-resource-policy/resources/image.py?corp=same-site"; + return new Promise((resolve, reject) => { + img.onload = resolve; + img.onerror = reject; + document.body.appendChild(img); + }).finally(() => { + img.remove(); + }); +}, "Cross-Origin-Resource-Policy does not block Mixed Content "); diff --git a/test/wpt/tests/fetch/cross-origin-resource-policy/script-loads.html b/test/wpt/tests/fetch/cross-origin-resource-policy/script-loads.html new file mode 100644 index 00000000000..a9690fc70be --- /dev/null +++ b/test/wpt/tests/fetch/cross-origin-resource-policy/script-loads.html @@ -0,0 +1,52 @@ + + + + + + + + +
+ + + diff --git a/test/wpt/tests/fetch/cross-origin-resource-policy/syntax.any.js b/test/wpt/tests/fetch/cross-origin-resource-policy/syntax.any.js new file mode 100644 index 00000000000..dc874977a63 --- /dev/null +++ b/test/wpt/tests/fetch/cross-origin-resource-policy/syntax.any.js @@ -0,0 +1,19 @@ +// META: script=/common/get-host-info.sub.js + +const crossOriginURL = get_host_info().HTTP_REMOTE_ORIGIN + "/fetch/cross-origin-resource-policy/resources/hello.py?corp="; + +[ + "same", + "same, same-origin", + "SAME-ORIGIN", + "Same-Origin", + "same-origin, <>", + "same-origin, same-origin", + "https://www.example.com", // See https://github.com/whatwg/fetch/issues/760 +].forEach(incorrectHeaderValue => { + // Note: an incorrect value results in a successful load, so this test is only meaningful in + // implementations with support for the header. + promise_test(t => { + return fetch(crossOriginURL + encodeURIComponent(incorrectHeaderValue), { mode: "no-cors" }); + }, "Parsing Cross-Origin-Resource-Policy: " + incorrectHeaderValue); +}); diff --git a/test/wpt/tests/fetch/data-urls/README.md b/test/wpt/tests/fetch/data-urls/README.md new file mode 100644 index 00000000000..1ce5b18b538 --- /dev/null +++ b/test/wpt/tests/fetch/data-urls/README.md @@ -0,0 +1,11 @@ +## data: URLs + +`resources/data-urls.json` contains `data:` URL tests. The tests are encoded as a JSON array. Each value in the array is an array of two or three values. The first value describes the input, the second value describes the expected MIME type, null if the input is expected to fail somehow, or the empty string if the expected value is `text/plain;charset=US-ASCII`. The third value, if present, describes the expected body as an array of integers representing bytes. + +These tests are used for `data:` URLs in this directory (see `processing.any.js`). + +## Forgiving-base64 decode + +`resources/base64.json` contains [forgiving-base64 decode](https://infra.spec.whatwg.org/#forgiving-base64-decode) tests. The tests are encoded as a JSON array. Each value in the array is an array of two values. The first value describes the input, the second value describes the output as an array of integers representing bytes or null if the input cannot be decoded. + +These tests are used for `data:` URLs in this directory (see `base64.any.js`) and `window.atob()` in `../../html/webappapis/atob/base64.html`. diff --git a/test/wpt/tests/fetch/h1-parsing/README.md b/test/wpt/tests/fetch/h1-parsing/README.md new file mode 100644 index 00000000000..487a892dcff --- /dev/null +++ b/test/wpt/tests/fetch/h1-parsing/README.md @@ -0,0 +1,5 @@ +This directory tries to document "rough consensus" on where HTTP/1 parsing should end up between browsers. + +Any tests that browsers currently fail should have associated bug reports. + +[whatwg/fetch issue #1156](https://github.com/whatwg/fetch/issues/1156) provides context for this effort and pointers to the various issues, pull requests, and bug reports that are associated with it. diff --git a/test/wpt/tests/fetch/h1-parsing/lone-cr.window.js b/test/wpt/tests/fetch/h1-parsing/lone-cr.window.js new file mode 100644 index 00000000000..6b46ed632f4 --- /dev/null +++ b/test/wpt/tests/fetch/h1-parsing/lone-cr.window.js @@ -0,0 +1,23 @@ +// These tests expect that a network error is returned if there's a CR that is not immediately +// followed by LF before reaching message-body. +// +// No browser does this currently, but Firefox does treat it equivalently to a space which gives +// hope. + +[ + "HTTP/1.1\r200 OK\n\nBODY", + "HTTP/1.1 200\rOK\n\nBODY", + "HTTP/1.1 200 OK\n\rHeader: Value\n\nBODY", + "HTTP/1.1 200 OK\nHeader\r: Value\n\nBODY", + "HTTP/1.1 200 OK\nHeader:\r Value\n\nBODY", + "HTTP/1.1 200 OK\nHeader: Value\r\n\nBody", + "HTTP/1.1 200 OK\nHeader: Value\r\r\nBODY", + "HTTP/1.1 200 OK\nHeader: Value\rHeader2: Value2\n\nBODY", + "HTTP/1.1 200 OK\nHeader: Value\n\rBODY", + "HTTP/1.1 200 OK\nHeader: Value\n\r" +].forEach(input => { + promise_test(t => { + const message = encodeURIComponent(input); + return promise_rejects_js(t, TypeError, fetch(`resources/message.py?message=${message}`)); + }, `Parsing response with a lone CR before message-body (${input})`); +}); 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 new file mode 100644 index 00000000000..f1afeeb740b --- /dev/null +++ b/test/wpt/tests/fetch/h1-parsing/resources-with-0x00-in-header.window.js @@ -0,0 +1,31 @@ +async_test(t => { + const script = document.createElement("script"); + t.add_cleanup(() => script.remove()); + script.src = "resources/script-with-0x00-in-header.py"; + script.onerror = t.step_func_done(); + script.onload = t.unreached_func(); + document.body.append(script); +}, "Expect network error for script with 0x00 in a header"); + +async_test(t => { + const frame = document.createElement("iframe"); + t.add_cleanup(() => frame.remove()); + frame.src = "resources/document-with-0x00-in-header.py"; + // If network errors result in load events for frames per + // 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); + t.done(); + }, 1000); + document.body.append(frame); +}, "Expect network error for frame navigation to resource with 0x00 in a header"); + +async_test(t => { + const img = document.createElement("img"); + t.add_cleanup(() => img.remove()); + img.src = "resources/blue-with-0x00-in-a-header.asis"; + img.onerror = t.step_func_done(); + img.onload = t.unreached_func(); + document.body.append(img); +}, "Expect network error for image with 0x00 in a header"); diff --git a/test/wpt/tests/fetch/h1-parsing/resources/README.md b/test/wpt/tests/fetch/h1-parsing/resources/README.md new file mode 100644 index 00000000000..2175d274088 --- /dev/null +++ b/test/wpt/tests/fetch/h1-parsing/resources/README.md @@ -0,0 +1,6 @@ +`blue-with-0x00-in-a-header.asis` is a copy from `../../images/blue.png` with the following prepended using Control Pictures to signify actual newlines and 0x00: +``` +HTTP/1.1 200 AN IMAGE␍␊ +Content-Type: image/png␍␊ +Custom: ␀␍␊␍␊ +``` diff --git a/test/wpt/tests/fetch/h1-parsing/resources/blue-with-0x00-in-a-header.asis b/test/wpt/tests/fetch/h1-parsing/resources/blue-with-0x00-in-a-header.asis new file mode 100644 index 0000000000000000000000000000000000000000..102340a6313feb75c1cad7f15b4d5a31e9c67568 GIT binary patch literal 546 zcmeYW2?@|Q)H75tGB8kZ^i%Nkb#!;-<#Nu?D@n~O(G96ANVQVP%uP&B)i20P2TGI{ zm*nSKDKPMI@p5$r___0PNpUeSFz|YMxC8;|Rv^yeU;>h|t5|J-6k~CayA#8@b22Z1 z9F}xPUq=Rpjs4tz5?O(K&H|6fVg?4j!ywFfJby(BP(zici(^Pd+}q2JybKIH%?Ey& z@4fk;(34fbWBc5xJTER9U075YW*fy8W%6&K`)P;nWAThis is a document." diff --git a/test/wpt/tests/fetch/h1-parsing/resources/message.py b/test/wpt/tests/fetch/h1-parsing/resources/message.py new file mode 100644 index 00000000000..640080c18bc --- /dev/null +++ b/test/wpt/tests/fetch/h1-parsing/resources/message.py @@ -0,0 +1,3 @@ +def main(request, response): + response.writer.write(request.GET.first(b"message")) + response.close_connection = True diff --git a/test/wpt/tests/fetch/h1-parsing/resources/script-with-0x00-in-header.py b/test/wpt/tests/fetch/h1-parsing/resources/script-with-0x00-in-header.py new file mode 100644 index 00000000000..39f58d8270e --- /dev/null +++ b/test/wpt/tests/fetch/h1-parsing/resources/script-with-0x00-in-header.py @@ -0,0 +1,4 @@ +def main(request, response): + response.headers.set(b"Content-Type", b"text/javascript") + response.headers.set(b"Custom", b"\0") + return b"var thisIsJavaScript = 0" diff --git a/test/wpt/tests/fetch/h1-parsing/resources/status-code.py b/test/wpt/tests/fetch/h1-parsing/resources/status-code.py new file mode 100644 index 00000000000..5421893b267 --- /dev/null +++ b/test/wpt/tests/fetch/h1-parsing/resources/status-code.py @@ -0,0 +1,6 @@ +def main(request, response): + output = b"HTTP/1.1 " + output += request.GET.first(b"input") + output += b"\nheader-parsing: is sad\n" + response.writer.write(output) + response.close_connection = True diff --git a/test/wpt/tests/fetch/h1-parsing/status-code.window.js b/test/wpt/tests/fetch/h1-parsing/status-code.window.js new file mode 100644 index 00000000000..5776cf4050f --- /dev/null +++ b/test/wpt/tests/fetch/h1-parsing/status-code.window.js @@ -0,0 +1,98 @@ +[ + { + input: "", + expected: null + }, + { + input: "BLAH", + expected: null + }, + { + input: "0 OK", + expected: { + status: 0, + statusText: "OK" + } + }, + { + input: "1 OK", + expected: { + status: 1, + statusText: "OK" + } + }, + { + input: "99 NOT OK", + expected: { + status: 99, + statusText: "NOT OK" + } + }, + { + input: "077 77", + expected: { + status: 77, + statusText: "77" + } + }, + { + input: "099 HELLO", + expected: { + status: 99, + statusText: "HELLO" + } + }, + { + input: "200", + expected: { + status: 200, + statusText: "" + } + }, + { + input: "999 DOES IT MATTER", + expected: { + status: 999, + statusText: "DOES IT MATTER" + } + }, + { + input: "1000 BOO", + expected: null + }, + { + input: "0200 BOO", + expected: null + }, + { + input: "65736 NOT 200 OR SOME SUCH", + expected: null + }, + { + input: "131072 HI", + expected: null + }, + { + input: "-200 TEST", + expected: null + }, + { + input: "0xA", + expected: null + }, + { + input: "C8", + expected: null + } +].forEach(({ description, input, expected }) => { + promise_test(async t => { + if (expected !== null) { + const response = await fetch("resources/status-code.py?input=" + input); + assert_equals(response.status, expected.status); + assert_equals(response.statusText, expected.statusText); + assert_equals(response.headers.get("header-parsing"), "is sad"); + } else { + await promise_rejects_js(t, TypeError, fetch("resources/status-code.py?input=" + input)); + } + }, `HTTP/1.1 ${input} ${expected === null ? "(network error)" : ""}`); +}); diff --git a/test/wpt/tests/fetch/http-cache/304-update.any.js b/test/wpt/tests/fetch/http-cache/304-update.any.js new file mode 100644 index 00000000000..15484f01eb3 --- /dev/null +++ b/test/wpt/tests/fetch/http-cache/304-update.any.js @@ -0,0 +1,146 @@ +// META: global=window,worker +// META: title=HTTP Cache - 304 Updates +// META: timeout=long +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=http-cache.js + +var tests = [ + { + name: "HTTP cache updates returned headers from a Last-Modified 304", + requests: [ + { + response_headers: [ + ["Expires", -5000], + ["Last-Modified", -3000], + ["Test-Header", "A"] + ] + }, + { + response_headers: [ + ["Expires", -3000], + ["Last-Modified", -3000], + ["Test-Header", "B"] + ], + expected_type: "lm_validated", + expected_response_headers: [ + ["Test-Header", "B"] + ] + } + ] + }, + { + name: "HTTP cache updates stored headers from a Last-Modified 304", + requests: [ + { + response_headers: [ + ["Expires", -5000], + ["Last-Modified", -3000], + ["Test-Header", "A"] + ] + }, + { + response_headers: [ + ["Expires", 3000], + ["Last-Modified", -3000], + ["Test-Header", "B"] + ], + expected_type: "lm_validated", + expected_response_headers: [ + ["Test-Header", "B"] + ], + pause_after: true + }, + { + expected_type: "cached", + expected_response_headers: [ + ["Test-Header", "B"] + ] + } + ] + }, + { + name: "HTTP cache updates returned headers from a ETag 304", + requests: [ + { + response_headers: [ + ["Expires", -5000], + ["ETag", "ABC"], + ["Test-Header", "A"] + ] + }, + { + response_headers: [ + ["Expires", -3000], + ["ETag", "ABC"], + ["Test-Header", "B"] + ], + expected_type: "etag_validated", + expected_response_headers: [ + ["Test-Header", "B"] + ] + } + ] + }, + { + name: "HTTP cache updates stored headers from a ETag 304", + requests: [ + { + response_headers: [ + ["Expires", -5000], + ["ETag", "DEF"], + ["Test-Header", "A"] + ] + }, + { + response_headers: [ + ["Expires", 3000], + ["ETag", "DEF"], + ["Test-Header", "B"] + ], + expected_type: "etag_validated", + expected_response_headers: [ + ["Test-Header", "B"] + ], + pause_after: true + }, + { + expected_type: "cached", + expected_response_headers: [ + ["Test-Header", "B"] + ] + } + ] + }, + { + name: "Content-* header", + requests: [ + { + response_headers: [ + ["Expires", -5000], + ["ETag", "GHI"], + ["Content-Test-Header", "A"] + ] + }, + { + response_headers: [ + ["Expires", 3000], + ["ETag", "GHI"], + ["Content-Test-Header", "B"] + ], + expected_type: "etag_validated", + expected_response_headers: [ + ["Content-Test-Header", "B"] + ], + pause_after: true + }, + { + expected_type: "cached", + expected_response_headers: [ + ["Content-Test-Header", "B"] + ] + } + ] + }, +]; +run_tests(tests); diff --git a/test/wpt/tests/fetch/http-cache/README.md b/test/wpt/tests/fetch/http-cache/README.md new file mode 100644 index 00000000000..512c422e108 --- /dev/null +++ b/test/wpt/tests/fetch/http-cache/README.md @@ -0,0 +1,72 @@ +## HTTP Caching Tests + +These tests cover HTTP-specified behaviours for caches, primarily from +[RFC9111](https://www.rfc-editor.org/rfc/rfc9111.html), but as seen through the +lens of Fetch. + +A few notes: + +* By its nature, [caching is entirely optional]( + https://www.rfc-editor.org/rfc/rfc9111.html#section-2-2); + some tests expecting a response to be + cached might fail because the client chose not to cache it, or chose to + race the cache with a network request. + +* Likewise, some tests might fail because there is a separate document-level + cache that's not well defined; see [this + issue](https://github.com/whatwg/fetch/issues/354). + +* [Partial content tests](partial.any.js) (a.k.a. Range requests) are not specified + in Fetch; tests are included here for interest only. + +* Some browser caches will behave differently when reloading / + shift-reloading, despite the `cache mode` staying the same. + +* [cache-tests.fyi](https://cache-tests.fyi/) is another test suite of HTTP caching + which also caters to server/CDN implementations. + +## Test Format + +Each test run gets its own URL and randomized content and operates independently. + +Each test is an an array of objects, with the following members: + +- `name` - The name of the test. +- `requests` - a list of request objects (see below). + +Possible members of a request object: + +- template - A template object for the request, by name. +- request_method - A string containing the HTTP method to be used. +- request_headers - An array of `[header_name_string, header_value_string]` arrays to + emit in the request. +- request_body - A string to use as the request body. +- mode - The mode string to pass to `fetch()`. +- credentials - The credentials string to pass to `fetch()`. +- cache - The cache string to pass to `fetch()`. +- pause_after - Boolean controlling a 3-second pause after the request completes. +- response_status - A `[number, string]` array containing the HTTP status code + and phrase to return. +- response_headers - An array of `[header_name_string, header_value_string]` arrays to + emit in the response. These values will also be checked like + expected_response_headers, unless there is a third value that is + `false`. See below for special handling considerations. +- response_body - String to send as the response body. If not set, it will contain + the test identifier. +- expected_type - One of `["cached", "not_cached", "lm_validate", "etag_validate", "error"]` +- expected_status - A number representing a HTTP status code to check the response for. + If not set, the value of `response_status[0]` will be used; if that + is not set, 200 will be used. +- expected_request_headers - An array of `[header_name_string, header_value_string]` representing + headers to check the request for. +- expected_response_headers - An array of `[header_name_string, header_value_string]` representing + headers to check the response for. See also response_headers. +- expected_response_text - A string to check the response body against. If not present, `response_body` will be checked if present and non-null; otherwise the response body will be checked for the test uuid (unless the status code disallows a body). Set to `null` to disable all response body checking. + +Some headers in `response_headers` are treated specially: + +* For date-carrying headers, if the value is a number, it will be interpreted as a delta to the time of the first request at the server. +* For URL-carrying headers, the value will be appended as a query parameter for `target`. + +See the source for exact details. + diff --git a/test/wpt/tests/fetch/http-cache/basic-auth-cache-test-ref.html b/test/wpt/tests/fetch/http-cache/basic-auth-cache-test-ref.html new file mode 100644 index 00000000000..905facdc888 --- /dev/null +++ b/test/wpt/tests/fetch/http-cache/basic-auth-cache-test-ref.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/test/wpt/tests/fetch/http-cache/basic-auth-cache-test.html b/test/wpt/tests/fetch/http-cache/basic-auth-cache-test.html new file mode 100644 index 00000000000..a8979baf548 --- /dev/null +++ b/test/wpt/tests/fetch/http-cache/basic-auth-cache-test.html @@ -0,0 +1,27 @@ + + + + + + + + + + + diff --git a/test/wpt/tests/fetch/http-cache/cache-mode.any.js b/test/wpt/tests/fetch/http-cache/cache-mode.any.js new file mode 100644 index 00000000000..8f406d5a6a5 --- /dev/null +++ b/test/wpt/tests/fetch/http-cache/cache-mode.any.js @@ -0,0 +1,61 @@ +// META: global=window,worker +// META: title=Fetch - Cache Mode +// META: timeout=long +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=http-cache.js + +var tests = [ + { + name: "Fetch sends Cache-Control: max-age=0 when cache mode is no-cache", + requests: [ + { + cache: "no-cache", + expected_request_headers: [['cache-control', 'max-age=0']] + } + ] + }, + { + name: "Fetch doesn't touch Cache-Control when cache mode is no-cache and Cache-Control is already present", + requests: [ + { + cache: "no-cache", + request_headers: [['cache-control', 'foo']], + expected_request_headers: [['cache-control', 'foo']] + } + ] + }, + { + name: "Fetch sends Cache-Control: no-cache and Pragma: no-cache when cache mode is no-store", + requests: [ + { + cache: "no-store", + expected_request_headers: [ + ['cache-control', 'no-cache'], + ['pragma', 'no-cache'] + ] + } + ] + }, + { + name: "Fetch doesn't touch Cache-Control when cache mode is no-store and Cache-Control is already present", + requests: [ + { + cache: "no-store", + request_headers: [['cache-control', 'foo']], + expected_request_headers: [['cache-control', 'foo']] + } + ] + }, + { + name: "Fetch doesn't touch Pragma when cache mode is no-store and Pragma is already present", + requests: [ + { + cache: "no-store", + request_headers: [['pragma', 'foo']], + expected_request_headers: [['pragma', 'foo']] + } + ] + } +]; +run_tests(tests); diff --git a/test/wpt/tests/fetch/http-cache/cc-request.any.js b/test/wpt/tests/fetch/http-cache/cc-request.any.js new file mode 100644 index 00000000000..d5565668414 --- /dev/null +++ b/test/wpt/tests/fetch/http-cache/cc-request.any.js @@ -0,0 +1,202 @@ +// META: global=window,worker +// META: title=HTTP Cache - Cache-Control Request Directives +// META: timeout=long +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=http-cache.js + +var tests = [ + { + name: "HTTP cache doesn't use aged but fresh response when request contains Cache-Control: max-age=0", + requests: [ + { + template: "fresh", + pause_after: true + }, + { + request_headers: [ + ["Cache-Control", "max-age=0"] + ], + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache doesn't use aged but fresh response when request contains Cache-Control: max-age=1", + requests: [ + { + template: "fresh", + pause_after: true + }, + { + request_headers: [ + ["Cache-Control", "max-age=1"] + ], + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache doesn't use fresh response with Age header when request contains Cache-Control: max-age that is greater than remaining freshness", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=3600"], + ["Age", "1800"] + ] + }, + { + request_headers: [ + ["Cache-Control", "max-age=600"] + ], + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache does use aged stale response when request contains Cache-Control: max-stale that permits its use", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=1"] + ], + pause_after: true + }, + { + request_headers: [ + ["Cache-Control", "max-stale=1000"] + ], + expected_type: "cached" + } + ] + }, + { + name: "HTTP cache does reuse stale response with Age header when request contains Cache-Control: max-stale that permits its use", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=1500"], + ["Age", "2000"] + ] + }, + { + request_headers: [ + ["Cache-Control", "max-stale=1000"] + ], + expected_type: "cached" + } + ] + }, + { + name: "HTTP cache doesn't reuse fresh response when request contains Cache-Control: min-fresh that wants it fresher", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=1500"] + ] + }, + { + request_headers: [ + ["Cache-Control", "min-fresh=2000"] + ], + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache doesn't reuse fresh response with Age header when request contains Cache-Control: min-fresh that wants it fresher", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=1500"], + ["Age", "1000"] + ] + }, + { + request_headers: [ + ["Cache-Control", "min-fresh=1000"] + ], + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache doesn't reuse fresh response when request contains Cache-Control: no-cache", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=3600"] + ] + }, + { + request_headers: [ + ["Cache-Control", "no-cache"] + ], + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache validates fresh response with Last-Modified when request contains Cache-Control: no-cache", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=3600"], + ["Last-Modified", -10000] + ] + }, + { + request_headers: [ + ["Cache-Control", "no-cache"] + ], + expected_type: "lm_validate" + } + ] + }, + { + name: "HTTP cache validates fresh response with ETag when request contains Cache-Control: no-cache", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=3600"], + ["ETag", http_content("abc")] + ] + }, + { + request_headers: [ + ["Cache-Control", "no-cache"] + ], + expected_type: "etag_validate" + } + ] + }, + { + name: "HTTP cache doesn't reuse fresh response when request contains Cache-Control: no-store", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=3600"] + ] + }, + { + request_headers: [ + ["Cache-Control", "no-store"] + ], + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache generates 504 status code when nothing is in cache and request contains Cache-Control: only-if-cached", + requests: [ + { + request_headers: [ + ["Cache-Control", "only-if-cached"] + ], + expected_status: 504, + expected_response_text: null + } + ] + } +]; +run_tests(tests); diff --git a/test/wpt/tests/fetch/http-cache/credentials.tentative.any.js b/test/wpt/tests/fetch/http-cache/credentials.tentative.any.js new file mode 100644 index 00000000000..31770925cde --- /dev/null +++ b/test/wpt/tests/fetch/http-cache/credentials.tentative.any.js @@ -0,0 +1,62 @@ +// META: global=window,worker +// META: title=HTTP Cache - Content +// META: timeout=long +// META: script=/common/utils.js +// META: script=http-cache.js + +// This is a tentative test. +// Firefox behavior is used as expectations. +// +// whatwg/fetch issue: +// https://github.com/whatwg/fetch/issues/1253 +// +// Chrome design doc: +// https://docs.google.com/document/d/1lvbiy4n-GM5I56Ncw304sgvY5Td32R6KHitjRXvkZ6U/edit# + +const request_cacheable = { + request_headers: [], + response_headers: [ + ['Cache-Control', 'max-age=3600'], + ], + // TODO(arthursonzogni): The behavior is tested only for same-origin requests. + // It must behave similarly for cross-site and cross-origin requests. The + // problems is the http-cache.js infrastructure returns the + // "Server-Request-Count" as HTTP response headers, which aren't readable for + // CORS requests. + base_url: location.href.replace(/\/[^\/]*$/, '/'), +}; + +const request_credentialled = { ...request_cacheable, credentials: 'include', }; +const request_anonymous = { ...request_cacheable, credentials: 'omit', }; + +const responseIndex = count => { + return { + expected_response_headers: [ + ['Server-Request-Count', count.toString()], + ], + } +}; + +var tests = [ + { + name: 'same-origin: 2xAnonymous, 2xCredentialled, 1xAnonymous', + requests: [ + { ...request_anonymous , ...responseIndex(1)} , + { ...request_anonymous , ...responseIndex(1)} , + { ...request_credentialled , ...responseIndex(2)} , + { ...request_credentialled , ...responseIndex(2)} , + { ...request_anonymous , ...responseIndex(1)} , + ] + }, + { + name: 'same-origin: 2xCredentialled, 2xAnonymous, 1xCredentialled', + requests: [ + { ...request_credentialled , ...responseIndex(1)} , + { ...request_credentialled , ...responseIndex(1)} , + { ...request_anonymous , ...responseIndex(2)} , + { ...request_anonymous , ...responseIndex(2)} , + { ...request_credentialled , ...responseIndex(1)} , + ] + }, +]; +run_tests(tests); diff --git a/test/wpt/tests/fetch/http-cache/freshness.any.js b/test/wpt/tests/fetch/http-cache/freshness.any.js new file mode 100644 index 00000000000..6b97c8244f6 --- /dev/null +++ b/test/wpt/tests/fetch/http-cache/freshness.any.js @@ -0,0 +1,215 @@ +// META: global=window,worker +// META: title=HTTP Cache - Freshness +// META: timeout=long +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=http-cache.js + +var tests = [ + // response directives + { + name: "HTTP cache reuses a response with a future Expires", + requests: [ + { + response_headers: [ + ["Expires", (30 * 24 * 60 * 60)] + ] + }, + { + expected_type: "cached" + } + ] + }, + { + name: "HTTP cache does not reuse a response with a past Expires", + requests: [ + { + response_headers: [ + ["Expires", (-30 * 24 * 60 * 60)] + ] + }, + { + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache does not reuse a response with a present Expires", + requests: [ + { + response_headers: [ + ["Expires", 0] + ] + }, + { + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache does not reuse a response with an invalid Expires", + requests: [ + { + response_headers: [ + ["Expires", "0"] + ] + }, + { + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache reuses a response with positive Cache-Control: max-age", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=3600"] + ] + }, + { + expected_type: "cached" + } + ] + }, + { + name: "HTTP cache does not reuse a response with Cache-Control: max-age=0", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=0"] + ] + }, + { + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache reuses a response with positive Cache-Control: max-age and a past Expires", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=3600"], + ["Expires", -10000] + ] + }, + { + expected_type: "cached" + } + ] + }, + { + name: "HTTP cache reuses a response with positive Cache-Control: max-age and an invalid Expires", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=3600"], + ["Expires", "0"] + ] + }, + { + expected_type: "cached" + } + ] + }, + { + name: "HTTP cache does not reuse a response with Cache-Control: max-age=0 and a future Expires", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=0"], + ["Expires", 10000] + ] + }, + { + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache does not prefer Cache-Control: s-maxage over Cache-Control: max-age", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=1, s-maxage=3600"] + ], + pause_after: true, + }, + { + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache does not reuse a response when the Age header is greater than its freshness lifetime", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=3600"], + ["Age", "12000"] + ], + }, + { + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache does not store a response with Cache-Control: no-store", + requests: [ + { + response_headers: [ + ["Cache-Control", "no-store"] + ] + }, + { + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache does not store a response with Cache-Control: no-store, even with max-age and Expires", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=10000, no-store"], + ["Expires", 10000] + ] + }, + { + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache stores a response with Cache-Control: no-cache, but revalidates upon use", + requests: [ + { + response_headers: [ + ["Cache-Control", "no-cache"], + ["ETag", "abcd"] + ] + }, + { + expected_type: "etag_validated" + } + ] + }, + { + name: "HTTP cache stores a response with Cache-Control: no-cache, but revalidates upon use, even with max-age and Expires", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=10000, no-cache"], + ["Expires", 10000], + ["ETag", "abcd"] + ] + }, + { + expected_type: "etag_validated" + } + ] + }, +]; +run_tests(tests); diff --git a/test/wpt/tests/fetch/http-cache/heuristic.any.js b/test/wpt/tests/fetch/http-cache/heuristic.any.js new file mode 100644 index 00000000000..d8461318882 --- /dev/null +++ b/test/wpt/tests/fetch/http-cache/heuristic.any.js @@ -0,0 +1,93 @@ +// META: global=window,worker +// META: title=HTTP Cache - Heuristic Freshness +// META: timeout=long +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=http-cache.js + +var tests = [ + { + name: "HTTP cache reuses an unknown response with Last-Modified based upon heuristic freshness when Cache-Control: public is present", + requests: [ + { + response_status: [299, "Whatever"], + response_headers: [ + ["Last-Modified", (-3 * 100)], + ["Cache-Control", "public"] + ], + }, + { + expected_type: "cached", + response_status: [299, "Whatever"] + } + ] + }, + { + name: "HTTP cache does not reuse an unknown response with Last-Modified based upon heuristic freshness when Cache-Control: public is not present", + requests: [ + { + response_status: [299, "Whatever"], + response_headers: [ + ["Last-Modified", (-3 * 100)] + ], + }, + { + expected_type: "not_cached" + } + ] + } +]; + +function check_status(status) { + var succeed = status[0]; + var code = status[1]; + var phrase = status[2]; + var body = status[3]; + if (body === undefined) { + body = http_content(code); + } + var expected_type = "not_cached"; + var desired = "does not use" + if (succeed === true) { + expected_type = "cached"; + desired = "reuses"; + } + tests.push( + { + name: "HTTP cache " + desired + " a " + code + " " + phrase + " response with Last-Modified based upon heuristic freshness", + requests: [ + { + response_status: [code, phrase], + response_headers: [ + ["Last-Modified", (-3 * 100)] + ], + response_body: body + }, + { + expected_type: expected_type, + response_status: [code, phrase], + response_body: body + } + ] + } + ) +} +[ + [true, 200, "OK"], + [true, 203, "Non-Authoritative Information"], + [true, 204, "No Content", ""], + [true, 404, "Not Found"], + [true, 405, "Method Not Allowed"], + [true, 410, "Gone"], + [true, 414, "URI Too Long"], + [true, 501, "Not Implemented"] +].forEach(check_status); +[ + [false, 201, "Created"], + [false, 202, "Accepted"], + [false, 403, "Forbidden"], + [false, 502, "Bad Gateway"], + [false, 503, "Service Unavailable"], + [false, 504, "Gateway Timeout"], +].forEach(check_status); +run_tests(tests); diff --git a/test/wpt/tests/fetch/http-cache/http-cache.js b/test/wpt/tests/fetch/http-cache/http-cache.js new file mode 100644 index 00000000000..19f1ca9b2bc --- /dev/null +++ b/test/wpt/tests/fetch/http-cache/http-cache.js @@ -0,0 +1,274 @@ +/* global btoa fetch token promise_test step_timeout */ +/* global assert_equals assert_true assert_own_property assert_throws_js assert_less_than */ + +const templates = { + 'fresh': { + 'response_headers': [ + ['Expires', 100000], + ['Last-Modified', 0] + ] + }, + 'stale': { + 'response_headers': [ + ['Expires', -5000], + ['Last-Modified', -100000] + ] + }, + 'lcl_response': { + 'response_headers': [ + ['Location', 'location_target'], + ['Content-Location', 'content_location_target'] + ] + }, + 'location': { + 'query_arg': 'location_target', + 'response_headers': [ + ['Expires', 100000], + ['Last-Modified', 0] + ] + }, + 'content_location': { + 'query_arg': 'content_location_target', + 'response_headers': [ + ['Expires', 100000], + ['Last-Modified', 0] + ] + } +} + +const noBodyStatus = new Set([204, 304]) + +function makeTest (test) { + return function () { + var uuid = token() + var requests = expandTemplates(test) + var fetchFunctions = makeFetchFunctions(requests, uuid) + return runTest(fetchFunctions, requests, uuid) + } +} + +function makeFetchFunctions(requests, uuid) { + var fetchFunctions = [] + for (let i = 0; i < requests.length; ++i) { + fetchFunctions.push({ + code: function (idx) { + var config = requests[idx] + var url = makeTestUrl(uuid, config) + var init = fetchInit(requests, config) + return fetch(url, init) + .then(makeCheckResponse(idx, config)) + .then(makeCheckResponseBody(config, uuid), function (reason) { + if ('expected_type' in config && config.expected_type === 'error') { + assert_throws_js(TypeError, function () { throw reason }) + } else { + throw reason + } + }) + }, + pauseAfter: 'pause_after' in requests[i] + }) + } + return fetchFunctions +} + +function runTest(fetchFunctions, requests, uuid) { + var idx = 0 + function runNextStep () { + if (fetchFunctions.length) { + var nextFetchFunction = fetchFunctions.shift() + if (nextFetchFunction.pauseAfter === true) { + return nextFetchFunction.code(idx++) + .then(pause) + .then(runNextStep) + } else { + return nextFetchFunction.code(idx++) + .then(runNextStep) + } + } else { + return Promise.resolve() + } + } + + return runNextStep() + .then(function () { + return getServerState(uuid) + }).then(function (testState) { + checkRequests(requests, testState) + return Promise.resolve() + }) +} + +function expandTemplates (test) { + var rawRequests = test.requests + var requests = [] + for (let i = 0; i < rawRequests.length; i++) { + var request = rawRequests[i] + request.name = test.name + if ('template' in request) { + var template = templates[request['template']] + for (let member in template) { + if (!request.hasOwnProperty(member)) { + request[member] = template[member] + } + } + } + requests.push(request) + } + return requests +} + +function fetchInit (requests, config) { + var init = { + 'headers': [] + } + if ('request_method' in config) init.method = config['request_method'] + // Note: init.headers must be a copy of config['request_headers'] array, + // because new elements are added later. + if ('request_headers' in config) init.headers = [...config['request_headers']]; + if ('name' in config) init.headers.push(['Test-Name', config.name]) + if ('request_body' in config) init.body = config['request_body'] + if ('mode' in config) init.mode = config['mode'] + if ('credentials' in config) init.credentials = config['credentials'] + if ('cache' in config) init.cache = config['cache'] + init.headers.push(['Test-Requests', btoa(JSON.stringify(requests))]) + return init +} + +function makeCheckResponse (idx, config) { + return function checkResponse (response) { + var reqNum = idx + 1 + var resNum = parseInt(response.headers.get('Server-Request-Count')) + if ('expected_type' in config) { + if (config.expected_type === 'error') { + assert_true(false, `Request ${reqNum} doesn't throw an error`) + return response.text() + } + if (config.expected_type === 'cached') { + assert_less_than(resNum, reqNum, `Response ${reqNum} does not come from cache`) + } + if (config.expected_type === 'not_cached') { + assert_equals(resNum, reqNum, `Response ${reqNum} comes from cache`) + } + } + if ('expected_status' in config) { + assert_equals(response.status, config.expected_status, + `Response ${reqNum} status is ${response.status}, not ${config.expected_status}`) + } else if ('response_status' in config) { + assert_equals(response.status, config.response_status[0], + `Response ${reqNum} status is ${response.status}, not ${config.response_status[0]}`) + } else { + assert_equals(response.status, 200, `Response ${reqNum} status is ${response.status}, not 200`) + } + if ('response_headers' in config) { + config.response_headers.forEach(function (header) { + if (header.len < 3 || header[2] === true) { + assert_equals(response.headers.get(header[0]), header[1], + `Response ${reqNum} header ${header[0]} is "${response.headers.get(header[0])}", not "${header[1]}"`) + } + }) + } + if ('expected_response_headers' in config) { + config.expected_response_headers.forEach(function (header) { + assert_equals(response.headers.get(header[0]), header[1], + `Response ${reqNum} header ${header[0]} is "${response.headers.get(header[0])}", not "${header[1]}"`) + }) + } + return response.text() + } +} + +function makeCheckResponseBody (config, uuid) { + return function checkResponseBody (resBody) { + var statusCode = 200 + if ('response_status' in config) { + statusCode = config.response_status[0] + } + if ('expected_response_text' in config) { + if (config.expected_response_text !== null) { + assert_equals(resBody, config.expected_response_text, + `Response body is "${resBody}", not expected "${config.expected_response_text}"`) + } + } else if ('response_body' in config && config.response_body !== null) { + assert_equals(resBody, config.response_body, + `Response body is "${resBody}", not sent "${config.response_body}"`) + } else if (!noBodyStatus.has(statusCode)) { + assert_equals(resBody, uuid, `Response body is "${resBody}", not default "${uuid}"`) + } + } +} + +function checkRequests (requests, testState) { + var testIdx = 0 + for (let i = 0; i < requests.length; ++i) { + var expectedValidatingHeaders = [] + var config = requests[i] + var serverRequest = testState[testIdx] + var reqNum = i + 1 + if ('expected_type' in config) { + if (config.expected_type === 'cached') continue // the server will not see the request + if (config.expected_type === 'etag_validated') { + expectedValidatingHeaders.push('if-none-match') + } + if (config.expected_type === 'lm_validated') { + expectedValidatingHeaders.push('if-modified-since') + } + } + testIdx++ + expectedValidatingHeaders.forEach(vhdr => { + assert_own_property(serverRequest.request_headers, vhdr, + `request ${reqNum} doesn't have ${vhdr} header`) + }) + if ('expected_request_headers' in config) { + config.expected_request_headers.forEach(expectedHdr => { + assert_equals(serverRequest.request_headers[expectedHdr[0].toLowerCase()], expectedHdr[1], + `request ${reqNum} header ${expectedHdr[0]} value is "${serverRequest.request_headers[expectedHdr[0].toLowerCase()]}", not "${expectedHdr[1]}"`) + }) + } + } +} + +function pause () { + return new Promise(function (resolve, reject) { + step_timeout(function () { + return resolve() + }, 3000) + }) +} + +function makeTestUrl (uuid, config) { + var arg = '' + var base_url = '' + if ('base_url' in config) { + base_url = config.base_url + } + if ('query_arg' in config) { + arg = `&target=${config.query_arg}` + } + return `${base_url}resources/http-cache.py?dispatch=test&uuid=${uuid}${arg}` +} + +function getServerState (uuid) { + return fetch(`resources/http-cache.py?dispatch=state&uuid=${uuid}`) + .then(function (response) { + return response.text() + }).then(function (text) { + return JSON.parse(text) || [] + }) +} + +function run_tests (tests) { + tests.forEach(function (test) { + promise_test(makeTest(test), test.name) + }) +} + +var contentStore = {} +function http_content (csKey) { + if (csKey in contentStore) { + return contentStore[csKey] + } else { + var content = btoa(Math.random() * Date.now()) + contentStore[csKey] = content + return content + } +} diff --git a/test/wpt/tests/fetch/http-cache/invalidate.any.js b/test/wpt/tests/fetch/http-cache/invalidate.any.js new file mode 100644 index 00000000000..9f8090ace65 --- /dev/null +++ b/test/wpt/tests/fetch/http-cache/invalidate.any.js @@ -0,0 +1,235 @@ +// META: global=window,worker +// META: title=HTTP Cache - Invalidation +// META: timeout=long +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=http-cache.js + +var tests = [ + { + name: 'HTTP cache invalidates after a successful response from a POST', + requests: [ + { + template: "fresh" + }, { + request_method: "POST", + request_body: "abc" + }, { + expected_type: "not_cached" + } + ] + }, + { + name: 'HTTP cache does not invalidate after a failed response from an unsafe request', + requests: [ + { + template: "fresh" + }, { + request_method: "POST", + request_body: "abc", + response_status: [500, "Internal Server Error"] + }, { + expected_type: "cached" + } + ] + }, + { + name: 'HTTP cache invalidates after a successful response from a PUT', + requests: [ + { + template: "fresh" + }, { + template: "fresh", + request_method: "PUT", + request_body: "abc" + }, { + expected_type: "not_cached" + } + ] + }, + { + name: 'HTTP cache invalidates after a successful response from a DELETE', + requests: [ + { + template: "fresh" + }, { + request_method: "DELETE", + request_body: "abc" + }, { + expected_type: "not_cached" + } + ] + }, + { + name: 'HTTP cache invalidates after a successful response from an unknown method', + requests: [ + { + template: "fresh" + }, { + request_method: "FOO", + request_body: "abc" + }, { + expected_type: "not_cached" + } + ] + }, + + + { + name: 'HTTP cache invalidates Location URL after a successful response from a POST', + requests: [ + { + template: "location" + }, { + request_method: "POST", + request_body: "abc", + template: "lcl_response" + }, { + template: "location", + expected_type: "not_cached" + } + ] + }, + { + name: 'HTTP cache does not invalidate Location URL after a failed response from an unsafe request', + requests: [ + { + template: "location" + }, { + template: "lcl_response", + request_method: "POST", + request_body: "abc", + response_status: [500, "Internal Server Error"] + }, { + template: "location", + expected_type: "cached" + } + ] + }, + { + name: 'HTTP cache invalidates Location URL after a successful response from a PUT', + requests: [ + { + template: "location" + }, { + template: "lcl_response", + request_method: "PUT", + request_body: "abc" + }, { + template: "location", + expected_type: "not_cached" + } + ] + }, + { + name: 'HTTP cache invalidates Location URL after a successful response from a DELETE', + requests: [ + { + template: "location" + }, { + template: "lcl_response", + request_method: "DELETE", + request_body: "abc" + }, { + template: "location", + expected_type: "not_cached" + } + ] + }, + { + name: 'HTTP cache invalidates Location URL after a successful response from an unknown method', + requests: [ + { + template: "location" + }, { + template: "lcl_response", + request_method: "FOO", + request_body: "abc" + }, { + template: "location", + expected_type: "not_cached" + } + ] + }, + + + + { + name: 'HTTP cache invalidates Content-Location URL after a successful response from a POST', + requests: [ + { + template: "content_location" + }, { + request_method: "POST", + request_body: "abc", + template: "lcl_response" + }, { + template: "content_location", + expected_type: "not_cached" + } + ] + }, + { + name: 'HTTP cache does not invalidate Content-Location URL after a failed response from an unsafe request', + requests: [ + { + template: "content_location" + }, { + template: "lcl_response", + request_method: "POST", + request_body: "abc", + response_status: [500, "Internal Server Error"] + }, { + template: "content_location", + expected_type: "cached" + } + ] + }, + { + name: 'HTTP cache invalidates Content-Location URL after a successful response from a PUT', + requests: [ + { + template: "content_location" + }, { + template: "lcl_response", + request_method: "PUT", + request_body: "abc" + }, { + template: "content_location", + expected_type: "not_cached" + } + ] + }, + { + name: 'HTTP cache invalidates Content-Location URL after a successful response from a DELETE', + requests: [ + { + template: "content_location" + }, { + template: "lcl_response", + request_method: "DELETE", + request_body: "abc" + }, { + template: "content_location", + expected_type: "not_cached" + } + ] + }, + { + name: 'HTTP cache invalidates Content-Location URL after a successful response from an unknown method', + requests: [ + { + template: "content_location" + }, { + template: "lcl_response", + request_method: "FOO", + request_body: "abc" + }, { + template: "content_location", + expected_type: "not_cached" + } + ] + } + +]; +run_tests(tests); diff --git a/test/wpt/tests/fetch/http-cache/partial.any.js b/test/wpt/tests/fetch/http-cache/partial.any.js new file mode 100644 index 00000000000..3f23b5930f7 --- /dev/null +++ b/test/wpt/tests/fetch/http-cache/partial.any.js @@ -0,0 +1,208 @@ +// META: global=window,worker +// META: title=HTTP Cache - Partial Content +// META: timeout=long +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=http-cache.js + +var tests = [ + { + name: "HTTP cache stores partial content and reuses it", + requests: [ + { + request_headers: [ + ['Range', "bytes=-5"] + ], + response_status: [206, "Partial Content"], + response_headers: [ + ["Cache-Control", "max-age=3600"], + ["Content-Range", "bytes 4-9/10"] + ], + response_body: "01234", + expected_request_headers: [ + ["Range", "bytes=-5"] + ] + }, + { + request_headers: [ + ["Range", "bytes=-5"] + ], + expected_type: "cached", + expected_status: 206, + expected_response_text: "01234" + } + ] + }, + { + name: "HTTP cache stores complete response and serves smaller ranges from it (byte-range-spec)", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=3600"] + ], + response_body: "01234567890" + }, + { + request_headers: [ + ['Range', "bytes=0-1"] + ], + expected_type: "cached", + expected_status: 206, + expected_response_text: "01" + }, + ] + }, + { + name: "HTTP cache stores complete response and serves smaller ranges from it (absent last-byte-pos)", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=3600"], + ], + response_body: "01234567890" + }, + { + request_headers: [ + ['Range', "bytes=1-"] + ], + expected_type: "cached", + expected_status: 206, + expected_response_text: "1234567890" + } + ] + }, + { + name: "HTTP cache stores complete response and serves smaller ranges from it (suffix-byte-range-spec)", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=3600"], + ], + response_body: "0123456789A" + }, + { + request_headers: [ + ['Range', "bytes=-1"] + ], + expected_type: "cached", + expected_status: 206, + expected_response_text: "A" + } + ] + }, + { + name: "HTTP cache stores complete response and serves smaller ranges from it with only-if-cached", + requests: [ + { + response_headers: [ + ["Cache-Control", "max-age=3600"] + ], + response_body: "01234567890" + }, + { + request_headers: [ + ['Range', "bytes=0-1"] + ], + mode: "same-origin", + cache: "only-if-cached", + expected_type: "cached", + expected_status: 206, + expected_response_text: "01" + }, + ] + }, + { + name: "HTTP cache stores partial response and serves smaller ranges from it (byte-range-spec)", + requests: [ + { + request_headers: [ + ['Range', "bytes=-5"] + ], + response_status: [206, "Partial Content"], + response_headers: [ + ["Cache-Control", "max-age=3600"], + ["Content-Range", "bytes 4-9/10"] + ], + response_body: "01234" + }, + { + request_headers: [ + ['Range', "bytes=6-8"] + ], + expected_type: "cached", + expected_status: 206, + expected_response_text: "234" + } + ] + }, + { + name: "HTTP cache stores partial response and serves smaller ranges from it (absent last-byte-pos)", + requests: [ + { + request_headers: [ + ['Range', "bytes=-5"] + ], + response_status: [206, "Partial Content"], + response_headers: [ + ["Cache-Control", "max-age=3600"], + ["Content-Range", "bytes 4-9/10"] + ], + response_body: "01234" + }, + { + request_headers: [ + ["Range", "bytes=6-"] + ], + expected_type: "cached", + expected_status: 206, + expected_response_text: "234" + } + ] + }, + { + name: "HTTP cache stores partial response and serves smaller ranges from it (suffix-byte-range-spec)", + requests: [ + { + request_headers: [ + ['Range', "bytes=-5"] + ], + response_status: [206, "Partial Content"], + response_headers: [ + ["Cache-Control", "max-age=3600"], + ["Content-Range", "bytes 4-9/10"] + ], + response_body: "01234" + }, + { + request_headers: [ + ['Range', "bytes=-1"] + ], + expected_type: "cached", + expected_status: 206, + expected_response_text: "4" + } + ] + }, + { + name: "HTTP cache stores partial content and completes it", + requests: [ + { + request_headers: [ + ['Range', "bytes=-5"] + ], + response_status: [206, "Partial Content"], + response_headers: [ + ["Cache-Control", "max-age=3600"], + ["Content-Range", "bytes 0-4/10"] + ], + response_body: "01234" + }, + { + expected_request_headers: [ + ["range", "bytes=5-"] + ] + } + ] + }, +]; +run_tests(tests); diff --git a/test/wpt/tests/fetch/http-cache/post-patch.any.js b/test/wpt/tests/fetch/http-cache/post-patch.any.js new file mode 100644 index 00000000000..0a69baa5c66 --- /dev/null +++ b/test/wpt/tests/fetch/http-cache/post-patch.any.js @@ -0,0 +1,46 @@ +// META: global=window,worker +// META: title=HTTP Cache - Caching POST and PATCH responses +// META: timeout=long +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=http-cache.js + +var tests = [ + { + name: "HTTP cache uses content after PATCH request with response containing Content-Location and cache-allowing header", + requests: [ + { + request_method: "PATCH", + request_body: "abc", + response_status: [200, "OK"], + response_headers: [ + ['Cache-Control', "private, max-age=1000"], + ['Content-Location', ""] + ], + response_body: "abc" + }, + { + expected_type: "cached" + } + ] + }, + { + name: "HTTP cache uses content after POST request with response containing Content-Location and cache-allowing header", + requests: [ + { + request_method: "POST", + request_body: "abc", + response_status: [200, "OK"], + response_headers: [ + ['Cache-Control', "private, max-age=1000"], + ['Content-Location', ""] + ], + response_body: "abc" + }, + { + expected_type: "cached" + } + ] + } +]; +run_tests(tests); diff --git a/test/wpt/tests/fetch/http-cache/resources/http-cache.py b/test/wpt/tests/fetch/http-cache/resources/http-cache.py new file mode 100644 index 00000000000..3ab610dd142 --- /dev/null +++ b/test/wpt/tests/fetch/http-cache/resources/http-cache.py @@ -0,0 +1,124 @@ +import datetime +import json +import time +from base64 import b64decode + +from wptserve.utils import isomorphic_decode, isomorphic_encode + +NOTEHDRS = set([u'content-type', u'access-control-allow-origin', u'last-modified', u'etag']) +NOBODYSTATUS = set([204, 304]) +LOCATIONHDRS = set([u'location', u'content-location']) +DATEHDRS = set([u'date', u'expires', u'last-modified']) + +def main(request, response): + dispatch = request.GET.first(b"dispatch", None) + uuid = request.GET.first(b"uuid", None) + response.headers.set(b"Access-Control-Allow-Credentials", b"true") + + if request.method == u"OPTIONS": + return handle_preflight(uuid, request, response) + if not uuid: + response.status = (404, b"Not Found") + response.headers.set(b"Content-Type", b"text/plain") + return b"UUID not found" + if dispatch == b'test': + return handle_test(uuid, request, response) + elif dispatch == b'state': + return handle_state(uuid, request, response) + response.status = (404, b"Not Found") + response.headers.set(b"Content-Type", b"text/plain") + return b"Fallthrough" + +def handle_preflight(uuid, request, response): + response.status = (200, b"OK") + response.headers.set(b"Access-Control-Allow-Origin", request.headers.get(b"origin") or '*') + response.headers.set(b"Access-Control-Allow-Methods", b"GET") + response.headers.set(b"Access-Control-Allow-Headers", request.headers.get(b"Access-Control-Request-Headers") or "*") + response.headers.set(b"Access-Control-Max-Age", b"86400") + return b"Preflight request" + +def handle_state(uuid, request, response): + response.headers.set(b"Content-Type", b"text/plain") + return json.dumps(request.server.stash.take(uuid)) + +def handle_test(uuid, request, response): + server_state = request.server.stash.take(uuid) or [] + try: + requests = json.loads(b64decode(request.headers.get(b'Test-Requests', b""))) + except: + response.status = (400, b"Bad Request") + response.headers.set(b"Content-Type", b"text/plain") + return b"No or bad Test-Requests request header" + config = requests[len(server_state)] + if not config: + response.status = (404, b"Not Found") + response.headers.set(b"Content-Type", b"text/plain") + return b"Config not found" + noted_headers = {} + now = time.time() + for header in config.get(u'response_headers', []): + if header[0].lower() in LOCATIONHDRS: # magic locations + if (len(header[1]) > 0): + header[1] = u"%s&target=%s" % (request.url, header[1]) + else: + header[1] = request.url + if header[0].lower() in DATEHDRS and isinstance(header[1], int): # magic dates + header[1] = http_date(now, header[1]) + response.headers.set(isomorphic_encode(header[0]), isomorphic_encode(header[1])) + if header[0].lower() in NOTEHDRS: + noted_headers[header[0].lower()] = header[1] + state = { + u'now': now, + u'request_method': request.method, + u'request_headers': dict([[isomorphic_decode(h.lower()), isomorphic_decode(request.headers[h])] for h in request.headers]), + u'response_headers': noted_headers + } + server_state.append(state) + request.server.stash.put(uuid, server_state) + + if u"access-control-allow-origin" not in noted_headers: + response.headers.set(b"Access-Control-Allow-Origin", b"*") + if u"content-type" not in noted_headers: + response.headers.set(b"Content-Type", b"text/plain") + response.headers.set(b"Server-Request-Count", len(server_state)) + + code, phrase = config.get(u"response_status", [200, b"OK"]) + if config.get(u"expected_type", u"").endswith(u'validated'): + ref_hdrs = server_state[0][u'response_headers'] + previous_lm = ref_hdrs.get(u'last-modified', False) + if previous_lm and request.headers.get(b"If-Modified-Since", False) == isomorphic_encode(previous_lm): + code, phrase = [304, b"Not Modified"] + previous_etag = ref_hdrs.get(u'etag', False) + if previous_etag and request.headers.get(b"If-None-Match", False) == isomorphic_encode(previous_etag): + code, phrase = [304, b"Not Modified"] + if code != 304: + code, phrase = [999, b'304 Not Generated'] + response.status = (code, phrase) + + content = config.get(u"response_body", uuid) + if code in NOBODYSTATUS: + return b"" + return content + + +def get_header(headers, header_name): + result = None + for header in headers: + if header[0].lower() == header_name.lower(): + result = header[1] + return result + +WEEKDAYS = [u'Mon', u'Tue', u'Wed', u'Thu', u'Fri', u'Sat', u'Sun'] +MONTHS = [None, u'Jan', u'Feb', u'Mar', u'Apr', u'May', u'Jun', u'Jul', + u'Aug', u'Sep', u'Oct', u'Nov', u'Dec'] + +def http_date(now, delta_secs=0): + date = datetime.datetime.utcfromtimestamp(now + delta_secs) + return u"%s, %.2d %s %.4d %.2d:%.2d:%.2d GMT" % ( + WEEKDAYS[date.weekday()], + date.day, + MONTHS[date.month], + date.year, + date.hour, + date.minute, + date.second) diff --git a/test/wpt/tests/fetch/http-cache/resources/securedimage.py b/test/wpt/tests/fetch/http-cache/resources/securedimage.py new file mode 100644 index 00000000000..cac9cfedd27 --- /dev/null +++ b/test/wpt/tests/fetch/http-cache/resources/securedimage.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 - + +from wptserve.utils import isomorphic_decode, isomorphic_encode + +def main(request, response): + image_url = str.replace(request.url, u"fetch/http-cache/resources/securedimage.py", u"images/green.png") + + if b"authorization" not in request.headers: + response.status = 401 + response.headers.set(b"WWW-Authenticate", b"Basic") + return + else: + auth = request.headers.get(b"Authorization") + if auth != b"Basic dGVzdHVzZXI6dGVzdHBhc3M=": + response.set_error(403, u"Invalid username or password - " + isomorphic_decode(auth)) + return + + response.status = 301 + response.headers.set(b"Location", isomorphic_encode(image_url)) diff --git a/test/wpt/tests/fetch/http-cache/resources/split-cache-popup-with-iframe.html b/test/wpt/tests/fetch/http-cache/resources/split-cache-popup-with-iframe.html new file mode 100644 index 00000000000..48b16180cfd --- /dev/null +++ b/test/wpt/tests/fetch/http-cache/resources/split-cache-popup-with-iframe.html @@ -0,0 +1,34 @@ + + + + + HTTP Cache - helper + + + + + + + + + diff --git a/test/wpt/tests/fetch/http-cache/resources/split-cache-popup.html b/test/wpt/tests/fetch/http-cache/resources/split-cache-popup.html new file mode 100644 index 00000000000..edb57947941 --- /dev/null +++ b/test/wpt/tests/fetch/http-cache/resources/split-cache-popup.html @@ -0,0 +1,28 @@ + + + + + HTTP Cache - helper + + + + + + + + + diff --git a/test/wpt/tests/fetch/http-cache/split-cache.html b/test/wpt/tests/fetch/http-cache/split-cache.html new file mode 100644 index 00000000000..fe93d2e3400 --- /dev/null +++ b/test/wpt/tests/fetch/http-cache/split-cache.html @@ -0,0 +1,158 @@ + + + + + HTTP Cache - Partioning by site + + + + + + + + + + + diff --git a/test/wpt/tests/fetch/http-cache/status.any.js b/test/wpt/tests/fetch/http-cache/status.any.js new file mode 100644 index 00000000000..10c83a25a26 --- /dev/null +++ b/test/wpt/tests/fetch/http-cache/status.any.js @@ -0,0 +1,60 @@ +// META: global=window,worker +// META: title=HTTP Cache - Status Codes +// META: timeout=long +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=http-cache.js + +var tests = []; +function check_status(status) { + var code = status[0]; + var phrase = status[1]; + var body = status[2]; + if (body === undefined) { + body = http_content(code); + } + tests.push({ + name: "HTTP cache goes to the network if it has a stale " + code + " response", + requests: [ + { + template: "stale", + response_status: [code, phrase], + response_body: body + }, { + expected_type: "not_cached", + response_status: [code, phrase], + response_body: body + } + ] + }) + tests.push({ + name: "HTTP cache avoids going to the network if it has a fresh " + code + " response", + requests: [ + { + template: "fresh", + response_status: [code, phrase], + response_body: body + }, { + expected_type: "cached", + response_status: [code, phrase], + response_body: body + } + ] + }) +} +[ + [200, "OK"], + [203, "Non-Authoritative Information"], + [204, "No Content", null], + [299, "Whatever"], + [400, "Bad Request"], + [404, "Not Found"], + [410, "Gone"], + [499, "Whatever"], + [500, "Internal Server Error"], + [502, "Bad Gateway"], + [503, "Service Unavailable"], + [504, "Gateway Timeout"], + [599, "Whatever"] +].forEach(check_status); +run_tests(tests); diff --git a/test/wpt/tests/fetch/http-cache/vary.any.js b/test/wpt/tests/fetch/http-cache/vary.any.js new file mode 100644 index 00000000000..2cfd226af81 --- /dev/null +++ b/test/wpt/tests/fetch/http-cache/vary.any.js @@ -0,0 +1,313 @@ +// META: global=window,worker +// META: title=HTTP Cache - Vary +// META: timeout=long +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=http-cache.js + +var tests = [ + { + name: "HTTP cache reuses Vary response when request matches", + requests: [ + { + request_headers: [ + ["Foo", "1"] + ], + response_headers: [ + ["Expires", 5000], + ["Last-Modified", -3000], + ["Vary", "Foo"] + ] + }, + { + request_headers: [ + ["Foo", "1"] + ], + expected_type: "cached" + } + ] + }, + { + name: "HTTP cache doesn't use Vary response when request doesn't match", + requests: [ + { + request_headers: [ + ["Foo", "1"] + ], + response_headers: [ + ["Expires", 5000], + ["Last-Modified", -3000], + ["Vary", "Foo"] + ] + }, + { + request_headers: [ + ["Foo", "2"] + ], + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache doesn't use Vary response when request omits variant header", + requests: [ + { + request_headers: [ + ["Foo", "1"] + ], + response_headers: [ + ["Expires", 5000], + ["Last-Modified", -3000], + ["Vary", "Foo"] + ] + }, + { + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache doesn't invalidate existing Vary response", + requests: [ + { + request_headers: [ + ["Foo", "1"] + ], + response_headers: [ + ["Expires", 5000], + ["Last-Modified", -3000], + ["Vary", "Foo"] + ], + response_body: http_content('foo_1') + }, + { + request_headers: [ + ["Foo", "2"] + ], + response_headers: [ + ["Expires", 5000], + ["Last-Modified", -3000], + ["Vary", "Foo"] + ], + expected_type: "not_cached", + response_body: http_content('foo_2'), + }, + { + request_headers: [ + ["Foo", "1"] + ], + response_body: http_content('foo_1'), + expected_type: "cached" + } + ] + }, + { + name: "HTTP cache doesn't pay attention to headers not listed in Vary", + requests: [ + { + request_headers: [ + ["Foo", "1"], + ["Other", "2"] + ], + response_headers: [ + ["Expires", 5000], + ["Last-Modified", -3000], + ["Vary", "Foo"] + ], + }, + { + request_headers: [ + ["Foo", "1"], + ["Other", "3"] + ], + expected_type: "cached" + } + ] + }, + { + name: "HTTP cache reuses two-way Vary response when request matches", + requests: [ + { + request_headers: [ + ["Foo", "1"], + ["Bar", "abc"] + ], + response_headers: [ + ["Expires", 5000], + ["Last-Modified", -3000], + ["Vary", "Foo, Bar"] + ] + }, + { + request_headers: [ + ["Foo", "1"], + ["Bar", "abc"] + ], + expected_type: "cached" + } + ] + }, + { + name: "HTTP cache doesn't use two-way Vary response when request doesn't match", + requests: [ + { + request_headers: [ + ["Foo", "1"], + ["Bar", "abc"] + ], + response_headers: [ + ["Expires", 5000], + ["Last-Modified", -3000], + ["Vary", "Foo, Bar"] + ] + }, + { + request_headers: [ + ["Foo", "2"], + ["Bar", "abc"] + ], + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache doesn't use two-way Vary response when request omits variant header", + requests: [ + { + request_headers: [ + ["Foo", "1"] + ], + response_headers: [ + ["Expires", 5000], + ["Last-Modified", -3000], + ["Vary", "Foo, Bar"] + ] + }, + { + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache reuses three-way Vary response when request matches", + requests: [ + { + request_headers: [ + ["Foo", "1"], + ["Bar", "abc"], + ["Baz", "789"] + ], + response_headers: [ + ["Expires", 5000], + ["Last-Modified", -3000], + ["Vary", "Foo, Bar, Baz"] + ] + }, + { + request_headers: [ + ["Foo", "1"], + ["Bar", "abc"], + ["Baz", "789"] + ], + expected_type: "cached" + } + ] + }, + { + name: "HTTP cache doesn't use three-way Vary response when request doesn't match", + requests: [ + { + request_headers: [ + ["Foo", "1"], + ["Bar", "abc"], + ["Baz", "789"] + ], + response_headers: [ + ["Expires", 5000], + ["Last-Modified", -3000], + ["Vary", "Foo, Bar, Baz"] + ] + }, + { + request_headers: [ + ["Foo", "2"], + ["Bar", "abc"], + ["Baz", "789"] + ], + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache doesn't use three-way Vary response when request doesn't match, regardless of header order", + requests: [ + { + request_headers: [ + ["Foo", "1"], + ["Bar", "abc4"], + ["Baz", "789"] + ], + response_headers: [ + ["Expires", 5000], + ["Last-Modified", -3000], + ["Vary", "Foo, Bar, Baz"] + ] + }, + { + request_headers: [ + ["Foo", "1"], + ["Bar", "abc"], + ["Baz", "789"] + ], + expected_type: "not_cached" + } + ] + }, + { + name: "HTTP cache uses three-way Vary response when both request and the original request omited a variant header", + requests: [ + { + request_headers: [ + ["Foo", "1"], + ["Baz", "789"] + ], + response_headers: [ + ["Expires", 5000], + ["Last-Modified", -3000], + ["Vary", "Foo, Bar, Baz"] + ] + }, + { + request_headers: [ + ["Foo", "1"], + ["Baz", "789"] + ], + expected_type: "cached" + } + ] + }, + { + name: "HTTP cache doesn't use Vary response with a field value of '*'", + requests: [ + { + request_headers: [ + ["Foo", "1"], + ["Baz", "789"] + ], + response_headers: [ + ["Expires", 5000], + ["Last-Modified", -3000], + ["Vary", "*"] + ] + }, + { + request_headers: [ + ["*", "1"], + ["Baz", "789"] + ], + expected_type: "not_cached" + } + ] + } +]; +run_tests(tests); diff --git a/test/wpt/tests/fetch/images/canvas-remote-read-remote-image-redirect.html b/test/wpt/tests/fetch/images/canvas-remote-read-remote-image-redirect.html new file mode 100644 index 00000000000..4a887f3d331 --- /dev/null +++ b/test/wpt/tests/fetch/images/canvas-remote-read-remote-image-redirect.html @@ -0,0 +1,28 @@ + + +Load a no-cors image from a same-origin URL that redirects to a cross-origin URL that redirects to the initial origin + + + + diff --git a/test/wpt/tests/fetch/metadata/META.yml b/test/wpt/tests/fetch/metadata/META.yml new file mode 100644 index 00000000000..85f0a7d2ee1 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/META.yml @@ -0,0 +1,4 @@ +spec: https://w3c.github.io/webappsec-fetch-metadata/ +suggested_reviewers: + - mikewest + - iVanlIsh diff --git a/test/wpt/tests/fetch/metadata/README.md b/test/wpt/tests/fetch/metadata/README.md new file mode 100644 index 00000000000..34864d4a4b6 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/README.md @@ -0,0 +1,9 @@ +Fetch Metadata Tests +==================== + +This directory contains tests related to the Fetch Metadata proposal: + +: Explainer +:: +: "Spec" +:: diff --git a/test/wpt/tests/fetch/metadata/audio-worklet.https.html b/test/wpt/tests/fetch/metadata/audio-worklet.https.html new file mode 100644 index 00000000000..3b768ef0b5d --- /dev/null +++ b/test/wpt/tests/fetch/metadata/audio-worklet.https.html @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/embed.https.sub.tentative.html b/test/wpt/tests/fetch/metadata/embed.https.sub.tentative.html new file mode 100644 index 00000000000..1900dbdf081 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/embed.https.sub.tentative.html @@ -0,0 +1,63 @@ + + + + + + + + + +
+ + diff --git a/test/wpt/tests/fetch/metadata/fetch-preflight.https.sub.any.js b/test/wpt/tests/fetch/metadata/fetch-preflight.https.sub.any.js new file mode 100644 index 00000000000..d52474353ba --- /dev/null +++ b/test/wpt/tests/fetch/metadata/fetch-preflight.https.sub.any.js @@ -0,0 +1,29 @@ +// META: global=window,worker +// META: script=/fetch/metadata/resources/helper.js + +// Site +promise_test(t => { + return validate_expectations_custom_url("https://{{hosts[][www]}}:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", + { + mode: "cors", + headers: { 'x-test': 'testing' } + }, { + "site": "same-site", + "user": "", + "mode": "cors", + "dest": "empty" + }, "Same-site fetch with preflight"); +}, "Same-site fetch with preflight"); + +promise_test(t => { + return validate_expectations_custom_url("https://{{hosts[alt][www]}}:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", + { + mode: "cors", + headers: { 'x-test': 'testing' } + }, { + "site": "cross-site", + "user": "", + "mode": "cors", + "dest": "empty" + }, "Cross-site fetch with preflight"); +}, "Cross-site fetch with preflight"); diff --git a/test/wpt/tests/fetch/metadata/fetch.https.sub.any.js b/test/wpt/tests/fetch/metadata/fetch.https.sub.any.js new file mode 100644 index 00000000000..aeec5cdf2dc --- /dev/null +++ b/test/wpt/tests/fetch/metadata/fetch.https.sub.any.js @@ -0,0 +1,58 @@ +// META: global=window,worker +// META: script=/fetch/metadata/resources/helper.js + +// Site +promise_test(t => { + return validate_expectations_custom_url("https://{{host}}:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", {}, { + "site": "same-origin", + "user": "", + "mode": "cors", + "dest": "empty" + }, "Same-origin fetch"); +}, "Same-origin fetch"); + +promise_test(t => { + return validate_expectations_custom_url("https://{{hosts[][www]}}:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", {}, { + "site": "same-site", + "user": "", + "mode": "cors", + "dest": "empty" + }, "Same-site fetch"); +}, "Same-site fetch"); + +promise_test(t => { + return validate_expectations_custom_url("https://{{hosts[alt][www]}}:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", {}, { + "site": "cross-site", + "user": "", + "mode": "cors", + "dest": "empty" + }, "Cross-site fetch"); +}, "Cross-site fetch"); + +// Mode +promise_test(t => { + return validate_expectations_custom_url("https://{{host}}:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", {mode: "same-origin"}, { + "site": "same-origin", + "user": "", + "mode": "same-origin", + "dest": "empty" + }, "Same-origin mode"); +}, "Same-origin mode"); + +promise_test(t => { + return validate_expectations_custom_url("https://{{host}}:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", {mode: "cors"}, { + "site": "same-origin", + "user": "", + "mode": "cors", + "dest": "empty" + }, "CORS mode"); +}, "CORS mode"); + +promise_test(t => { + return validate_expectations_custom_url("https://{{host}}:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", {mode: "no-cors"}, { + "site": "same-origin", + "user": "", + "mode": "no-cors", + "dest": "empty" + }, "no-CORS mode"); +}, "no-CORS mode"); diff --git a/test/wpt/tests/fetch/metadata/generated/appcache-manifest.https.sub.html b/test/wpt/tests/fetch/metadata/generated/appcache-manifest.https.sub.html new file mode 100644 index 00000000000..cf322fd34bc --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/appcache-manifest.https.sub.html @@ -0,0 +1,341 @@ + + + + + HTTP headers on request for Appcache manifest + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/audioworklet.https.sub.html b/test/wpt/tests/fetch/metadata/generated/audioworklet.https.sub.html new file mode 100644 index 00000000000..64fb7607e26 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/audioworklet.https.sub.html @@ -0,0 +1,271 @@ + + + + + HTTP headers on request for AudioWorklet module + + + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/css-font-face.https.sub.tentative.html b/test/wpt/tests/fetch/metadata/generated/css-font-face.https.sub.tentative.html new file mode 100644 index 00000000000..332effeb1f8 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/css-font-face.https.sub.tentative.html @@ -0,0 +1,230 @@ + + + + + HTTP headers on request for CSS font-face + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/css-font-face.sub.tentative.html b/test/wpt/tests/fetch/metadata/generated/css-font-face.sub.tentative.html new file mode 100644 index 00000000000..8a0b90cee10 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/css-font-face.sub.tentative.html @@ -0,0 +1,196 @@ + + + + + HTTP headers on request for CSS font-face + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/css-images.https.sub.tentative.html b/test/wpt/tests/fetch/metadata/generated/css-images.https.sub.tentative.html new file mode 100644 index 00000000000..3fa24019289 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/css-images.https.sub.tentative.html @@ -0,0 +1,1384 @@ + + + + + + HTTP headers on request for CSS image-accepting properties + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/css-images.sub.tentative.html b/test/wpt/tests/fetch/metadata/generated/css-images.sub.tentative.html new file mode 100644 index 00000000000..f1ef27cf087 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/css-images.sub.tentative.html @@ -0,0 +1,1099 @@ + + + + + + HTTP headers on request for CSS image-accepting properties + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/element-a.https.sub.html b/test/wpt/tests/fetch/metadata/generated/element-a.https.sub.html new file mode 100644 index 00000000000..dffd36c73ee --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/element-a.https.sub.html @@ -0,0 +1,482 @@ + + + + + + HTTP headers on request for HTML "a" element navigation + + + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/element-a.sub.html b/test/wpt/tests/fetch/metadata/generated/element-a.sub.html new file mode 100644 index 00000000000..0661de3c871 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/element-a.sub.html @@ -0,0 +1,342 @@ + + + + + + HTTP headers on request for HTML "a" element navigation + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/element-area.https.sub.html b/test/wpt/tests/fetch/metadata/generated/element-area.https.sub.html new file mode 100644 index 00000000000..be3f5f9b621 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/element-area.https.sub.html @@ -0,0 +1,482 @@ + + + + + + HTTP headers on request for HTML "area" element navigation + + + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/element-area.sub.html b/test/wpt/tests/fetch/metadata/generated/element-area.sub.html new file mode 100644 index 00000000000..5f5c338324f --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/element-area.sub.html @@ -0,0 +1,342 @@ + + + + + + HTTP headers on request for HTML "area" element navigation + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/element-audio.https.sub.html b/test/wpt/tests/fetch/metadata/generated/element-audio.https.sub.html new file mode 100644 index 00000000000..a9d951233e2 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/element-audio.https.sub.html @@ -0,0 +1,325 @@ + + + + + HTTP headers on request for HTML "audio" element source + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/element-audio.sub.html b/test/wpt/tests/fetch/metadata/generated/element-audio.sub.html new file mode 100644 index 00000000000..2b62632ac2e --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/element-audio.sub.html @@ -0,0 +1,229 @@ + + + + + HTTP headers on request for HTML "audio" element source + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/element-embed.https.sub.html b/test/wpt/tests/fetch/metadata/generated/element-embed.https.sub.html new file mode 100644 index 00000000000..819bed888ee --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/element-embed.https.sub.html @@ -0,0 +1,224 @@ + + + + + HTTP headers on request for HTML "embed" element source + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/element-embed.sub.html b/test/wpt/tests/fetch/metadata/generated/element-embed.sub.html new file mode 100644 index 00000000000..b6e14a55e4a --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/element-embed.sub.html @@ -0,0 +1,190 @@ + + + + + HTTP headers on request for HTML "embed" element source + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/element-frame.https.sub.html b/test/wpt/tests/fetch/metadata/generated/element-frame.https.sub.html new file mode 100644 index 00000000000..17504ff5635 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/element-frame.https.sub.html @@ -0,0 +1,309 @@ + + + + + HTTP headers on request for HTML "frame" element source + + + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/element-frame.sub.html b/test/wpt/tests/fetch/metadata/generated/element-frame.sub.html new file mode 100644 index 00000000000..2d9a7ec97d7 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/element-frame.sub.html @@ -0,0 +1,250 @@ + + + + + HTTP headers on request for HTML "frame" element source + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/element-iframe.https.sub.html b/test/wpt/tests/fetch/metadata/generated/element-iframe.https.sub.html new file mode 100644 index 00000000000..fba1c8b9e02 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/element-iframe.https.sub.html @@ -0,0 +1,309 @@ + + + + + HTTP headers on request for HTML "frame" element source + + + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/element-iframe.sub.html b/test/wpt/tests/fetch/metadata/generated/element-iframe.sub.html new file mode 100644 index 00000000000..6f71cc0d254 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/element-iframe.sub.html @@ -0,0 +1,250 @@ + + + + + HTTP headers on request for HTML "frame" element source + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/element-img-environment-change.https.sub.html b/test/wpt/tests/fetch/metadata/generated/element-img-environment-change.https.sub.html new file mode 100644 index 00000000000..a19aa117c4f --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/element-img-environment-change.https.sub.html @@ -0,0 +1,357 @@ + + + + + HTTP headers on image request triggered by change to environment + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/element-img-environment-change.sub.html b/test/wpt/tests/fetch/metadata/generated/element-img-environment-change.sub.html new file mode 100644 index 00000000000..96658726ba0 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/element-img-environment-change.sub.html @@ -0,0 +1,270 @@ + + + + + HTTP headers on image request triggered by change to environment + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/element-img.https.sub.html b/test/wpt/tests/fetch/metadata/generated/element-img.https.sub.html new file mode 100644 index 00000000000..51d6e082b0e --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/element-img.https.sub.html @@ -0,0 +1,645 @@ + + + + + HTTP headers on request for HTML "img" element source + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/element-img.sub.html b/test/wpt/tests/fetch/metadata/generated/element-img.sub.html new file mode 100644 index 00000000000..5a4b152c552 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/element-img.sub.html @@ -0,0 +1,456 @@ + + + + + HTTP headers on request for HTML "img" element source + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/element-input-image.https.sub.html b/test/wpt/tests/fetch/metadata/generated/element-input-image.https.sub.html new file mode 100644 index 00000000000..7fa674043e2 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/element-input-image.https.sub.html @@ -0,0 +1,229 @@ + + + + + HTTP headers on request for HTML "input" element with type="button" + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/element-input-image.sub.html b/test/wpt/tests/fetch/metadata/generated/element-input-image.sub.html new file mode 100644 index 00000000000..fb2a146b199 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/element-input-image.sub.html @@ -0,0 +1,184 @@ + + + + + HTTP headers on request for HTML "input" element with type="button" + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/element-link-icon.https.sub.html b/test/wpt/tests/fetch/metadata/generated/element-link-icon.https.sub.html new file mode 100644 index 00000000000..b2449607553 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/element-link-icon.https.sub.html @@ -0,0 +1,371 @@ + + + + + + HTTP headers on request for HTML "link" element with rel="icon" + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/element-link-icon.sub.html b/test/wpt/tests/fetch/metadata/generated/element-link-icon.sub.html new file mode 100644 index 00000000000..e9226c190a5 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/element-link-icon.sub.html @@ -0,0 +1,279 @@ + + + + + + HTTP headers on request for HTML "link" element with rel="icon" + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/element-link-prefetch.https.optional.sub.html b/test/wpt/tests/fetch/metadata/generated/element-link-prefetch.https.optional.sub.html new file mode 100644 index 00000000000..bdd684a2679 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/element-link-prefetch.https.optional.sub.html @@ -0,0 +1,559 @@ + + + + + + HTTP headers on request for HTML "link" element with rel="prefetch" + + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/element-link-prefetch.optional.sub.html b/test/wpt/tests/fetch/metadata/generated/element-link-prefetch.optional.sub.html new file mode 100644 index 00000000000..c2244883cc9 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/element-link-prefetch.optional.sub.html @@ -0,0 +1,275 @@ + + + + + + HTTP headers on request for HTML "link" element with rel="prefetch" + + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/element-meta-refresh.https.optional.sub.html b/test/wpt/tests/fetch/metadata/generated/element-meta-refresh.https.optional.sub.html new file mode 100644 index 00000000000..3a1a8eb49af --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/element-meta-refresh.https.optional.sub.html @@ -0,0 +1,276 @@ + + + + + HTTP headers on request for HTML "meta" element with http-equiv="refresh" + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/element-meta-refresh.optional.sub.html b/test/wpt/tests/fetch/metadata/generated/element-meta-refresh.optional.sub.html new file mode 100644 index 00000000000..df3e92e2c8f --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/element-meta-refresh.optional.sub.html @@ -0,0 +1,225 @@ + + + + + HTTP headers on request for HTML "meta" element with http-equiv="refresh" + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/element-picture.https.sub.html b/test/wpt/tests/fetch/metadata/generated/element-picture.https.sub.html new file mode 100644 index 00000000000..ba6636a019a --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/element-picture.https.sub.html @@ -0,0 +1,997 @@ + + + + + HTTP headers on request for HTML "picture" element source + + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/element-picture.sub.html b/test/wpt/tests/fetch/metadata/generated/element-picture.sub.html new file mode 100644 index 00000000000..64f851c682b --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/element-picture.sub.html @@ -0,0 +1,721 @@ + + + + + HTTP headers on request for HTML "picture" element source + + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/element-script.https.sub.html b/test/wpt/tests/fetch/metadata/generated/element-script.https.sub.html new file mode 100644 index 00000000000..dcdcba2792d --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/element-script.https.sub.html @@ -0,0 +1,593 @@ + + + + + HTTP headers on request for HTML "script" element source + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/element-script.sub.html b/test/wpt/tests/fetch/metadata/generated/element-script.sub.html new file mode 100644 index 00000000000..a2526698fbd --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/element-script.sub.html @@ -0,0 +1,488 @@ + + + + + HTTP headers on request for HTML "script" element source + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/element-video-poster.https.sub.html b/test/wpt/tests/fetch/metadata/generated/element-video-poster.https.sub.html new file mode 100644 index 00000000000..5805b46bd0c --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/element-video-poster.https.sub.html @@ -0,0 +1,243 @@ + + + + + HTTP headers on request for HTML "video" element "poster" + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/element-video-poster.sub.html b/test/wpt/tests/fetch/metadata/generated/element-video-poster.sub.html new file mode 100644 index 00000000000..e6cc5ee7e06 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/element-video-poster.sub.html @@ -0,0 +1,198 @@ + + + + + HTTP headers on request for HTML "video" element "poster" + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/element-video.https.sub.html b/test/wpt/tests/fetch/metadata/generated/element-video.https.sub.html new file mode 100644 index 00000000000..971360dceea --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/element-video.https.sub.html @@ -0,0 +1,325 @@ + + + + + HTTP headers on request for HTML "video" element source + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/element-video.sub.html b/test/wpt/tests/fetch/metadata/generated/element-video.sub.html new file mode 100644 index 00000000000..9707413ab68 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/element-video.sub.html @@ -0,0 +1,229 @@ + + + + + HTTP headers on request for HTML "video" element source + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/fetch-via-serviceworker.https.sub.html b/test/wpt/tests/fetch/metadata/generated/fetch-via-serviceworker.https.sub.html new file mode 100644 index 00000000000..22f930960d6 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/fetch-via-serviceworker.https.sub.html @@ -0,0 +1,683 @@ + + + + + + HTTP headers on request using the "fetch" API and passing through a Serive Worker + + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/fetch.https.sub.html b/test/wpt/tests/fetch/metadata/generated/fetch.https.sub.html new file mode 100644 index 00000000000..dde1daede4e --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/fetch.https.sub.html @@ -0,0 +1,302 @@ + + + + + HTTP headers on request using the "fetch" API + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/fetch.sub.html b/test/wpt/tests/fetch/metadata/generated/fetch.sub.html new file mode 100644 index 00000000000..d28ea9bb900 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/fetch.sub.html @@ -0,0 +1,220 @@ + + + + + HTTP headers on request using the "fetch" API + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/form-submission.https.sub.html b/test/wpt/tests/fetch/metadata/generated/form-submission.https.sub.html new file mode 100644 index 00000000000..988b07c74a9 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/form-submission.https.sub.html @@ -0,0 +1,522 @@ + + + + + + HTTP headers on request for HTML form navigation + + + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/form-submission.sub.html b/test/wpt/tests/fetch/metadata/generated/form-submission.sub.html new file mode 100644 index 00000000000..f862062aebc --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/form-submission.sub.html @@ -0,0 +1,400 @@ + + + + + + HTTP headers on request for HTML form navigation + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/header-link.https.sub.html b/test/wpt/tests/fetch/metadata/generated/header-link.https.sub.html new file mode 100644 index 00000000000..09f01138955 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/header-link.https.sub.html @@ -0,0 +1,529 @@ + + + + + HTTP headers on request for HTTP "Link" header + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/header-link.https.sub.tentative.html b/test/wpt/tests/fetch/metadata/generated/header-link.https.sub.tentative.html new file mode 100644 index 00000000000..307c37fbf77 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/header-link.https.sub.tentative.html @@ -0,0 +1,51 @@ + + + + + HTTP headers on request for HTTP "Link" header + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/header-link.sub.html b/test/wpt/tests/fetch/metadata/generated/header-link.sub.html new file mode 100644 index 00000000000..8b6cdae0eda --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/header-link.sub.html @@ -0,0 +1,460 @@ + + + + + HTTP headers on request for HTTP "Link" header + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/header-refresh.https.optional.sub.html b/test/wpt/tests/fetch/metadata/generated/header-refresh.https.optional.sub.html new file mode 100644 index 00000000000..e63ee423cd7 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/header-refresh.https.optional.sub.html @@ -0,0 +1,273 @@ + + + + + + HTTP headers on request for HTTP "Refresh" header + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/header-refresh.optional.sub.html b/test/wpt/tests/fetch/metadata/generated/header-refresh.optional.sub.html new file mode 100644 index 00000000000..4674ada9c6d --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/header-refresh.optional.sub.html @@ -0,0 +1,222 @@ + + + + + + HTTP headers on request for HTTP "Refresh" header + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/script-module-import-dynamic.https.sub.html b/test/wpt/tests/fetch/metadata/generated/script-module-import-dynamic.https.sub.html new file mode 100644 index 00000000000..72d60fc30cb --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/script-module-import-dynamic.https.sub.html @@ -0,0 +1,254 @@ + + + + + HTTP headers on request for dynamic ECMAScript module import + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/script-module-import-dynamic.sub.html b/test/wpt/tests/fetch/metadata/generated/script-module-import-dynamic.sub.html new file mode 100644 index 00000000000..088720c23e0 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/script-module-import-dynamic.sub.html @@ -0,0 +1,214 @@ + + + + + HTTP headers on request for dynamic ECMAScript module import + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/script-module-import-static.https.sub.html b/test/wpt/tests/fetch/metadata/generated/script-module-import-static.https.sub.html new file mode 100644 index 00000000000..cea3464f807 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/script-module-import-static.https.sub.html @@ -0,0 +1,288 @@ + + + + + HTTP headers on request for static ECMAScript module import + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/script-module-import-static.sub.html b/test/wpt/tests/fetch/metadata/generated/script-module-import-static.sub.html new file mode 100644 index 00000000000..0f94f71cf6f --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/script-module-import-static.sub.html @@ -0,0 +1,246 @@ + + + + + HTTP headers on request for static ECMAScript module import + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/serviceworker.https.sub.html b/test/wpt/tests/fetch/metadata/generated/serviceworker.https.sub.html new file mode 100644 index 00000000000..12e37369a42 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/serviceworker.https.sub.html @@ -0,0 +1,170 @@ + + + + + + HTTP headers on request for Service Workers + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/svg-image.https.sub.html b/test/wpt/tests/fetch/metadata/generated/svg-image.https.sub.html new file mode 100644 index 00000000000..b059eb31456 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/svg-image.https.sub.html @@ -0,0 +1,367 @@ + + + + + + HTTP headers on request for SVG "image" element source + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/svg-image.sub.html b/test/wpt/tests/fetch/metadata/generated/svg-image.sub.html new file mode 100644 index 00000000000..a28bbb12eb7 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/svg-image.sub.html @@ -0,0 +1,265 @@ + + + + + + HTTP headers on request for SVG "image" element source + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/window-history.https.sub.html b/test/wpt/tests/fetch/metadata/generated/window-history.https.sub.html new file mode 100644 index 00000000000..c2b3079a6d3 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/window-history.https.sub.html @@ -0,0 +1,237 @@ + + + + + HTTP headers on request for navigation via the HTML History API + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/window-history.sub.html b/test/wpt/tests/fetch/metadata/generated/window-history.sub.html new file mode 100644 index 00000000000..333d90c2860 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/window-history.sub.html @@ -0,0 +1,360 @@ + + + + + + HTTP headers on request for navigation via the HTML History API + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/window-location.https.sub.html b/test/wpt/tests/fetch/metadata/generated/window-location.https.sub.html new file mode 100644 index 00000000000..4a0d2fdc068 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/window-location.https.sub.html @@ -0,0 +1,1184 @@ + + + + + + HTTP headers on request for navigation via the HTML Location API + + + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/window-location.sub.html b/test/wpt/tests/fetch/metadata/generated/window-location.sub.html new file mode 100644 index 00000000000..bb3e6805cb0 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/window-location.sub.html @@ -0,0 +1,894 @@ + + + + + + HTTP headers on request for navigation via the HTML Location API + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/worker-dedicated-constructor.https.sub.html b/test/wpt/tests/fetch/metadata/generated/worker-dedicated-constructor.https.sub.html new file mode 100644 index 00000000000..86f17607554 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/worker-dedicated-constructor.https.sub.html @@ -0,0 +1,118 @@ + + + + + HTTP headers on request for dedicated worker via the "Worker" constructor + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/worker-dedicated-constructor.sub.html b/test/wpt/tests/fetch/metadata/generated/worker-dedicated-constructor.sub.html new file mode 100644 index 00000000000..69ac7682a5c --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/worker-dedicated-constructor.sub.html @@ -0,0 +1,204 @@ + + + + + HTTP headers on request for dedicated worker via the "Worker" constructor + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/worker-dedicated-importscripts.https.sub.html b/test/wpt/tests/fetch/metadata/generated/worker-dedicated-importscripts.https.sub.html new file mode 100644 index 00000000000..0cd9f35d582 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/worker-dedicated-importscripts.https.sub.html @@ -0,0 +1,268 @@ + + + + + HTTP headers on request for dedicated worker via the "importScripts" API + + + + + diff --git a/test/wpt/tests/fetch/metadata/generated/worker-dedicated-importscripts.sub.html b/test/wpt/tests/fetch/metadata/generated/worker-dedicated-importscripts.sub.html new file mode 100644 index 00000000000..0555bbaf432 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/generated/worker-dedicated-importscripts.sub.html @@ -0,0 +1,228 @@ + + + + + HTTP headers on request for dedicated worker via the "importScripts" API + + + + + diff --git a/test/wpt/tests/fetch/metadata/navigation.https.sub.html b/test/wpt/tests/fetch/metadata/navigation.https.sub.html new file mode 100644 index 00000000000..32c9cf77f90 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/navigation.https.sub.html @@ -0,0 +1,23 @@ + + + + + + diff --git a/test/wpt/tests/fetch/metadata/object.https.sub.html b/test/wpt/tests/fetch/metadata/object.https.sub.html new file mode 100644 index 00000000000..fae5b37b592 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/object.https.sub.html @@ -0,0 +1,62 @@ + + + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/paint-worklet.https.html b/test/wpt/tests/fetch/metadata/paint-worklet.https.html new file mode 100644 index 00000000000..49fc7765f62 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/paint-worklet.https.html @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/portal.https.sub.html b/test/wpt/tests/fetch/metadata/portal.https.sub.html new file mode 100644 index 00000000000..55b555a1b8e --- /dev/null +++ b/test/wpt/tests/fetch/metadata/portal.https.sub.html @@ -0,0 +1,50 @@ + + + + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/preload.https.sub.html b/test/wpt/tests/fetch/metadata/preload.https.sub.html new file mode 100644 index 00000000000..29042a85474 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/preload.https.sub.html @@ -0,0 +1,50 @@ + + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/redirect/multiple-redirect-https-downgrade-upgrade.sub.html b/test/wpt/tests/fetch/metadata/redirect/multiple-redirect-https-downgrade-upgrade.sub.html new file mode 100644 index 00000000000..0f8f3200165 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/redirect/multiple-redirect-https-downgrade-upgrade.sub.html @@ -0,0 +1,18 @@ + + + + + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/redirect/redirect-http-upgrade.sub.html b/test/wpt/tests/fetch/metadata/redirect/redirect-http-upgrade.sub.html new file mode 100644 index 00000000000..fa765b66d03 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/redirect/redirect-http-upgrade.sub.html @@ -0,0 +1,17 @@ + + + + + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/redirect/redirect-https-downgrade.sub.html b/test/wpt/tests/fetch/metadata/redirect/redirect-https-downgrade.sub.html new file mode 100644 index 00000000000..4e5a48e6f6e --- /dev/null +++ b/test/wpt/tests/fetch/metadata/redirect/redirect-https-downgrade.sub.html @@ -0,0 +1,17 @@ + + + + + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/report.https.sub.html b/test/wpt/tests/fetch/metadata/report.https.sub.html new file mode 100644 index 00000000000..b65f7c0a244 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/report.https.sub.html @@ -0,0 +1,33 @@ + + + + + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/report.https.sub.html.sub.headers b/test/wpt/tests/fetch/metadata/report.https.sub.html.sub.headers new file mode 100644 index 00000000000..1ec5df78f30 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/report.https.sub.html.sub.headers @@ -0,0 +1,3 @@ +Content-Security-Policy: style-src 'self' 'unsafe-inline'; report-uri /fetch/metadata/resources/record-header.py?file=report-same-origin +Content-Security-Policy: style-src 'self' 'unsafe-inline'; report-uri https://{{hosts[][www]}}:{{ports[https][0]}}/fetch/metadata/resources/record-header.py?file=report-same-site +Content-Security-Policy: style-src 'self' 'unsafe-inline'; report-uri https://{{hosts[alt][www]}}:{{ports[https][0]}}/fetch/metadata/resources/record-header.py?file=report-cross-site diff --git a/test/wpt/tests/fetch/metadata/resources/appcache-iframe.sub.html b/test/wpt/tests/fetch/metadata/resources/appcache-iframe.sub.html new file mode 100644 index 00000000000..cea9a4feaec --- /dev/null +++ b/test/wpt/tests/fetch/metadata/resources/appcache-iframe.sub.html @@ -0,0 +1,15 @@ + + + + diff --git a/test/wpt/tests/fetch/metadata/resources/dedicatedWorker.js b/test/wpt/tests/fetch/metadata/resources/dedicatedWorker.js new file mode 100644 index 00000000000..18626d3d845 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/resources/dedicatedWorker.js @@ -0,0 +1 @@ +self.postMessage("Loaded"); diff --git a/test/wpt/tests/fetch/metadata/resources/echo-as-json.py b/test/wpt/tests/fetch/metadata/resources/echo-as-json.py new file mode 100644 index 00000000000..44f68e8fe9e --- /dev/null +++ b/test/wpt/tests/fetch/metadata/resources/echo-as-json.py @@ -0,0 +1,29 @@ +import json + +from wptserve.utils import isomorphic_decode + +def main(request, response): + headers = [(b"Content-Type", b"application/json"), + (b"Access-Control-Allow-Credentials", b"true")] + + if b"origin" in request.headers: + headers.append((b"Access-Control-Allow-Origin", request.headers[b"origin"])) + + body = u"" + + # If we're in a preflight, verify that `Sec-Fetch-Mode` is `cors`. + if request.method == u'OPTIONS': + if request.headers.get(b"sec-fetch-mode") != b"cors": + return (403, b"Failed"), [], body + + headers.append((b"Access-Control-Allow-Methods", b"*")) + headers.append((b"Access-Control-Allow-Headers", b"*")) + else: + body = json.dumps({ + u"dest": isomorphic_decode(request.headers.get(b"sec-fetch-dest", b"")), + u"mode": isomorphic_decode(request.headers.get(b"sec-fetch-mode", b"")), + u"site": isomorphic_decode(request.headers.get(b"sec-fetch-site", b"")), + u"user": isomorphic_decode(request.headers.get(b"sec-fetch-user", b"")), + }) + + return headers, body diff --git a/test/wpt/tests/fetch/metadata/resources/echo-as-script.py b/test/wpt/tests/fetch/metadata/resources/echo-as-script.py new file mode 100644 index 00000000000..1e7bc91184f --- /dev/null +++ b/test/wpt/tests/fetch/metadata/resources/echo-as-script.py @@ -0,0 +1,14 @@ +import json + +from wptserve.utils import isomorphic_decode + +def main(request, response): + headers = [(b"Content-Type", b"text/javascript")] + body = u"var header = %s;" % json.dumps({ + u"dest": isomorphic_decode(request.headers.get(b"sec-fetch-dest", b"")), + u"mode": isomorphic_decode(request.headers.get(b"sec-fetch-mode", b"")), + u"site": isomorphic_decode(request.headers.get(b"sec-fetch-site", b"")), + u"user": isomorphic_decode(request.headers.get(b"sec-fetch-user", b"")), + }) + + return headers, body diff --git a/test/wpt/tests/fetch/metadata/resources/es-module.sub.js b/test/wpt/tests/fetch/metadata/resources/es-module.sub.js new file mode 100644 index 00000000000..f9668a3dc67 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/resources/es-module.sub.js @@ -0,0 +1 @@ +import '{{GET[moduleId]}}'; diff --git a/test/wpt/tests/fetch/metadata/resources/fetch-via-serviceworker--fallback--sw.js b/test/wpt/tests/fetch/metadata/resources/fetch-via-serviceworker--fallback--sw.js new file mode 100644 index 00000000000..09858b2663f --- /dev/null +++ b/test/wpt/tests/fetch/metadata/resources/fetch-via-serviceworker--fallback--sw.js @@ -0,0 +1,3 @@ +self.addEventListener('fetch', function(event) { + // Empty event handler - will fallback to the network. +}); diff --git a/test/wpt/tests/fetch/metadata/resources/fetch-via-serviceworker--respondWith--sw.js b/test/wpt/tests/fetch/metadata/resources/fetch-via-serviceworker--respondWith--sw.js new file mode 100644 index 00000000000..8bf8d8f2217 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/resources/fetch-via-serviceworker--respondWith--sw.js @@ -0,0 +1,3 @@ +self.addEventListener('fetch', function(event) { + event.respondWith(fetch(event.request)); +}); diff --git a/test/wpt/tests/fetch/metadata/resources/fetch-via-serviceworker-frame.html b/test/wpt/tests/fetch/metadata/resources/fetch-via-serviceworker-frame.html new file mode 100644 index 00000000000..98798025005 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/resources/fetch-via-serviceworker-frame.html @@ -0,0 +1,3 @@ + + +Page Title diff --git a/test/wpt/tests/fetch/metadata/resources/header-link.py b/test/wpt/tests/fetch/metadata/resources/header-link.py new file mode 100644 index 00000000000..de891163a33 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/resources/header-link.py @@ -0,0 +1,15 @@ +def main(request, response): + """ + Respond with a blank HTML document and a `Link` header which describes + a link relation specified by the requests `location` and `rel` query string + parameters + """ + headers = [ + (b'Content-Type', b'text/html'), + ( + b'Link', + b'<' + request.GET.first(b'location') + b'>; rel=' + request.GET.first(b'rel') + ) + ] + return (200, headers, b'') + diff --git a/test/wpt/tests/fetch/metadata/resources/helper.js b/test/wpt/tests/fetch/metadata/resources/helper.js new file mode 100644 index 00000000000..725f9a7e43b --- /dev/null +++ b/test/wpt/tests/fetch/metadata/resources/helper.js @@ -0,0 +1,42 @@ +function validate_expectations(key, expected, tag) { + return fetch("/fetch/metadata/resources/record-header.py?retrieve=true&file=" + key) + .then(response => response.text()) + .then(text => { + assert_not_equals(text, "No header has been recorded"); + let value = JSON.parse(text); + test(t => assert_equals(value.dest, expected.dest), `${tag}: sec-fetch-dest`); + test(t => assert_equals(value.mode, expected.mode), `${tag}: sec-fetch-mode`); + test(t => assert_equals(value.site, expected.site), `${tag}: sec-fetch-site`); + test(t => assert_equals(value.user, expected.user), `${tag}: sec-fetch-user`); + }); +} + +function validate_expectations_custom_url(url, header, expected, tag) { + return fetch(url, header) + .then(response => response.text()) + .then(text => { + assert_not_equals(text, "No header has been recorded"); + let value = JSON.parse(text); + test(t => assert_equals(value.dest, expected.dest), `${tag}: sec-fetch-dest`); + test(t => assert_equals(value.mode, expected.mode), `${tag}: sec-fetch-mode`); + test(t => assert_equals(value.site, expected.site), `${tag}: sec-fetch-site`); + test(t => assert_equals(value.user, expected.user), `${tag}: sec-fetch-user`); + }); +} + +/** + * @param {object} value + * @param {object} expected + * @param {string} tag + **/ +function assert_header_equals(value, expected, tag) { + if (typeof(value) === "string"){ + assert_not_equals(value, "No header has been recorded"); + value = JSON.parse(value); + } + + test(t => assert_equals(value.dest, expected.dest), `${tag}: sec-fetch-dest`); + test(t => assert_equals(value.mode, expected.mode), `${tag}: sec-fetch-mode`); + test(t => assert_equals(value.site, expected.site), `${tag}: sec-fetch-site`); + test(t => assert_equals(value.user, expected.user), `${tag}: sec-fetch-user`); +} diff --git a/test/wpt/tests/fetch/metadata/resources/helper.sub.js b/test/wpt/tests/fetch/metadata/resources/helper.sub.js new file mode 100644 index 00000000000..fd179fe6f25 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/resources/helper.sub.js @@ -0,0 +1,67 @@ +'use strict'; + +/** + * Construct a URL which, when followed, will trigger redirection through zero + * or more specified origins and ultimately resolve in the Python handler + * `record-headers.py`. + * + * @param {string} key - the WPT server "stash" name where the request's + * headers should be stored + * @param {string[]} [origins] - zero or more origin names through which the + * request should pass; see the function + * implementation for a completel list of names + * and corresponding origins; If specified, the + * final origin will be used to access the + * `record-headers.py` hander. + * @param {object} [params] - a collection of key-value pairs to include as + * URL "search" parameters in the final request to + * `record-headers.py` + * + * @returns {string} an absolute URL + */ +function makeRequestURL(key, origins, params) { + const byName = { + httpOrigin: 'http://{{host}}:{{ports[http][0]}}', + httpSameSite: 'http://{{hosts[][www]}}:{{ports[http][0]}}', + httpCrossSite: 'http://{{hosts[alt][]}}:{{ports[http][0]}}', + httpsOrigin: 'https://{{host}}:{{ports[https][0]}}', + httpsSameSite: 'https://{{hosts[][www]}}:{{ports[https][0]}}', + httpsCrossSite: 'https://{{hosts[alt][]}}:{{ports[https][0]}}' + }; + const redirectPath = '/fetch/api/resources/redirect.py?location='; + const path = '/fetch/metadata/resources/record-headers.py?key=' + key; + + let requestUrl = path; + if (params) { + requestUrl += '&' + new URLSearchParams(params).toString(); + } + + if (origins && origins.length) { + requestUrl = byName[origins.pop()] + requestUrl; + + while (origins.length) { + requestUrl = byName[origins.pop()] + redirectPath + + encodeURIComponent(requestUrl); + } + } else { + requestUrl = byName.httpsOrigin + requestUrl; + } + + return requestUrl; +} + +function retrieve(key, options) { + return fetch('/fetch/metadata/resources/record-headers.py?retrieve&key=' + key) + .then((response) => { + if (response.status === 204 && options && options.poll) { + return new Promise((resolve) => setTimeout(resolve, 300)) + .then(() => retrieve(key, options)); + } + + if (response.status !== 200) { + throw new Error('Failed to query for recorded headers.'); + } + + return response.text().then((text) => JSON.parse(text)); + }); +} diff --git a/test/wpt/tests/fetch/metadata/resources/message-opener.html b/test/wpt/tests/fetch/metadata/resources/message-opener.html new file mode 100644 index 00000000000..eb2af7b250b --- /dev/null +++ b/test/wpt/tests/fetch/metadata/resources/message-opener.html @@ -0,0 +1,17 @@ + diff --git a/test/wpt/tests/fetch/metadata/resources/post-to-owner.py b/test/wpt/tests/fetch/metadata/resources/post-to-owner.py new file mode 100644 index 00000000000..256dd6e49dc --- /dev/null +++ b/test/wpt/tests/fetch/metadata/resources/post-to-owner.py @@ -0,0 +1,36 @@ +import json + +from wptserve.utils import isomorphic_decode + +def main(request, response): + headers = [ + (b"Content-Type", b"text/html"), + (b"Cache-Control", b"no-cache, no-store, must-revalidate") + ] + key = request.GET.first(b"key", None) + + # We serialize the key into JSON, so have to decode it first. + if key is not None: + key = key.decode('utf-8') + + body = u""" + + + + """ % (json.dumps({ + u"dest": isomorphic_decode(request.headers.get(b"sec-fetch-dest", b"")), + u"mode": isomorphic_decode(request.headers.get(b"sec-fetch-mode", b"")), + u"site": isomorphic_decode(request.headers.get(b"sec-fetch-site", b"")), + u"user": isomorphic_decode(request.headers.get(b"sec-fetch-user", b"")), + }), json.dumps(key)) + return headers, body diff --git a/test/wpt/tests/fetch/metadata/resources/record-header.py b/test/wpt/tests/fetch/metadata/resources/record-header.py new file mode 100644 index 00000000000..29ff2ed7985 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/resources/record-header.py @@ -0,0 +1,145 @@ +import os +import hashlib +import json + +from wptserve.utils import isomorphic_decode + +def main(request, response): + ## Get the query parameter (key) from URL ## + ## Tests will record POST requests (CSP Report) and GET (rest) ## + if request.GET: + key = request.GET[b'file'] + elif request.POST: + key = request.POST[b'file'] + + ## Convert the key from String to UUID valid String ## + testId = hashlib.md5(key).hexdigest() + + ## Handle the header retrieval request ## + if b'retrieve' in request.GET: + response.writer.write_status(200) + response.writer.write_header(b"Connection", b"close") + response.writer.end_headers() + try: + header_value = request.server.stash.take(testId) + response.writer.write(header_value) + except (KeyError, ValueError) as e: + response.writer.write(u"No header has been recorded") + pass + + response.close_connection = True + + ## Record incoming fetch metadata header value + else: + try: + ## Return a serialized JSON object with one member per header. If the ## + ## header isn't present, the member will contain an empty string. ## + header = json.dumps({ + u"dest": isomorphic_decode(request.headers.get(b"sec-fetch-dest", b"")), + u"mode": isomorphic_decode(request.headers.get(b"sec-fetch-mode", b"")), + u"site": isomorphic_decode(request.headers.get(b"sec-fetch-site", b"")), + u"user": isomorphic_decode(request.headers.get(b"sec-fetch-user", b"")), + }) + request.server.stash.put(testId, header) + except KeyError: + ## The header is already recorded or it doesn't exist + pass + + ## Prevent the browser from caching returned responses and allow CORS ## + response.headers.set(b"Access-Control-Allow-Origin", b"*") + response.headers.set(b"Cache-Control", b"no-cache, no-store, must-revalidate") + response.headers.set(b"Pragma", b"no-cache") + response.headers.set(b"Expires", b"0") + + ## Add a valid ServiceWorker Content-Type ## + if key.startswith(b"serviceworker"): + response.headers.set(b"Content-Type", b"application/javascript") + + ## Add a valid image Content-Type ## + if key.startswith(b"image"): + response.headers.set(b"Content-Type", b"image/png") + file = open(os.path.join(request.doc_root, u"media", u"1x1-green.png"), u"rb") + image = file.read() + file.close() + return image + + ## Return a valid .vtt content for the tag ## + if key.startswith(b"track"): + return b"WEBVTT" + + ## Return a valid SharedWorker ## + if key.startswith(b"sharedworker"): + response.headers.set(b"Content-Type", b"application/javascript") + file = open(os.path.join(request.doc_root, u"fetch", u"metadata", + u"resources", u"sharedWorker.js"), u"rb") + shared_worker = file.read() + file.close() + return shared_worker + + ## Return a valid font content and Content-Type ## + if key.startswith(b"font"): + response.headers.set(b"Content-Type", b"application/x-font-ttf") + file = open(os.path.join(request.doc_root, u"fonts", u"Ahem.ttf"), u"rb") + font = file.read() + file.close() + return font + + ## Return a valid audio content and Content-Type ## + if key.startswith(b"audio"): + response.headers.set(b"Content-Type", b"audio/mpeg") + file = open(os.path.join(request.doc_root, u"media", u"sound_5.mp3"), u"rb") + audio = file.read() + file.close() + return audio + + ## Return a valid video content and Content-Type ## + if key.startswith(b"video"): + response.headers.set(b"Content-Type", b"video/mp4") + file = open(os.path.join(request.doc_root, u"media", u"A4.mp4"), u"rb") + video = file.read() + file.close() + return video + + ## Return valid style content and Content-Type ## + if key.startswith(b"style"): + response.headers.set(b"Content-Type", b"text/css") + return b"div { }" + + ## Return a valid embed/object content and Content-Type ## + if key.startswith(b"embed") or key.startswith(b"object"): + response.headers.set(b"Content-Type", b"text/html") + return b"EMBED!" + + ## Return a valid image content and Content-Type for redirect requests ## + if key.startswith(b"redirect"): + response.headers.set(b"Content-Type", b"image/jpeg") + file = open(os.path.join(request.doc_root, u"media", u"1x1-green.png"), u"rb") + image = file.read() + file.close() + return image + + ## Return a valid dedicated worker + if key.startswith(b"worker"): + response.headers.set(b"Content-Type", b"application/javascript") + return b"self.postMessage('loaded');" + + ## Return a valid worklet + if key.startswith(b"worklet"): + response.headers.set(b"Content-Type", b"application/javascript") + return b"" + + ## Return a valid XSLT + if key.startswith(b"xslt"): + response.headers.set(b"Content-Type", b"text/xsl") + return b""" + + + + + + +""" + + if key.startswith(b"script"): + response.headers.set(b"Content-Type", b"application/javascript") + return b"void 0;" diff --git a/test/wpt/tests/fetch/metadata/resources/record-headers.py b/test/wpt/tests/fetch/metadata/resources/record-headers.py new file mode 100644 index 00000000000..0362fe228c2 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/resources/record-headers.py @@ -0,0 +1,73 @@ +import os +import uuid +import hashlib +import time +import json + + +def bytes_to_strings(d): + # Recursively convert bytes to strings in `d`. + if not isinstance(d, dict): + if isinstance(d, (tuple,list,set)): + v = [bytes_to_strings(x) for x in d] + return v + else: + if isinstance(d, bytes): + d = d.decode() + return d + + result = {} + for k,v in d.items(): + if isinstance(k, bytes): + k = k.decode() + if isinstance(v, dict): + v = bytes_to_strings(v) + elif isinstance(v, (tuple,list,set)): + v = [bytes_to_strings(x) for x in v] + elif isinstance(v, bytes): + v = v.decode() + result[k] = v + return result + + +def main(request, response): + # This condition avoids false positives from CORS preflight checks, where the + # request under test may be followed immediately by a request to the same URL + # using a different HTTP method. + if b'requireOPTIONS' in request.GET and request.method != b'OPTIONS': + return + + if b'key' in request.GET: + key = request.GET[b'key'] + elif b'key' in request.POST: + key = request.POST[b'key'] + + ## Convert the key from String to UUID valid String ## + testId = hashlib.md5(key).hexdigest() + + ## Handle the header retrieval request ## + if b'retrieve' in request.GET: + recorded_headers = request.server.stash.take(testId) + + if recorded_headers is None: + return (204, [], b'') + + return (200, [], recorded_headers) + + ## Record incoming fetch metadata header value + else: + try: + request.server.stash.put(testId, json.dumps(bytes_to_strings(request.headers))) + except KeyError: + ## The header is already recorded or it doesn't exist + pass + + ## Prevent the browser from caching returned responses and allow CORS ## + response.headers.set(b"Access-Control-Allow-Origin", b"*") + response.headers.set(b"Cache-Control", b"no-cache, no-store, must-revalidate") + response.headers.set(b"Pragma", b"no-cache") + response.headers.set(b"Expires", b"0") + if b"mime" in request.GET: + response.headers.set(b"Content-Type", request.GET.first(b"mime")) + + return request.GET.first(b"body", request.POST.first(b"body", b"")) diff --git a/test/wpt/tests/fetch/metadata/resources/redirectTestHelper.sub.js b/test/wpt/tests/fetch/metadata/resources/redirectTestHelper.sub.js new file mode 100644 index 00000000000..1bfbbae70c2 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/resources/redirectTestHelper.sub.js @@ -0,0 +1,167 @@ +function createVideoElement() { + let el = document.createElement('video'); + el.src = '/media/movie_5.mp4'; + el.setAttribute('controls', ''); + el.setAttribute('crossorigin', ''); + return el; +} + +function createTrack() { + let el = document.createElement('track'); + el.setAttribute('default', ''); + el.setAttribute('kind', 'captions'); + el.setAttribute('srclang', 'en'); + return el; +} + +let secureRedirectURL = 'https://{{host}}:{{ports[https][0]}}/fetch/api/resources/redirect.py?location='; +let insecureRedirectURL = 'http://{{host}}:{{ports[http][0]}}/fetch/api/resources/redirect.py?location='; +let secureTestURL = 'https://{{host}}:{{ports[https][0]}}/fetch/metadata/'; +let insecureTestURL = 'http://{{host}}:{{ports[http][0]}}/fetch/metadata/'; + +// Helper to craft an URL that will go from HTTPS => HTTP => HTTPS to +// simulate us downgrading then upgrading again during the same redirect chain. +function MultipleRedirectTo(partialPath) { + let finalURL = insecureRedirectURL + encodeURIComponent(secureTestURL + partialPath); + return secureRedirectURL + encodeURIComponent(finalURL); +} + +// Helper to craft an URL that will go from HTTP => HTTPS to simulate upgrading a +// given request. +function upgradeRedirectTo(partialPath) { + return insecureRedirectURL + encodeURIComponent(secureTestURL + partialPath); +} + +// Helper to craft an URL that will go from HTTPS => HTTP to simulate downgrading a +// given request. +function downgradeRedirectTo(partialPath) { + return secureRedirectURL + encodeURIComponent(insecureTestURL + partialPath); +} + +// Helper to run common redirect test cases that don't require special setup on +// the test page itself. +function RunCommonRedirectTests(testNamePrefix, urlHelperMethod, expectedResults) { + async_test(t => { + let testWindow = window.open(urlHelperMethod('resources/post-to-owner.py?top-level-navigation' + nonce)); + t.add_cleanup(_ => testWindow.close()); + window.addEventListener('message', t.step_func(e => { + if (e.source != testWindow) { + return; + } + + let expectation = { ...expectedResults }; + if (expectation['mode'] != '') + expectation['mode'] = 'navigate'; + if (expectation['dest'] == 'font') + expectation['dest'] = 'document'; + assert_header_equals(e.data, expectation, testNamePrefix + ' top level navigation'); + t.done(); + })); + }, testNamePrefix + ' top level navigation'); + + promise_test(t => { + return new Promise((resolve, reject) => { + let key = 'embed-https-redirect' + nonce; + let e = document.createElement('embed'); + e.src = urlHelperMethod('resources/record-header.py?file=' + key); + e.onload = e => { + let expectation = { ...expectedResults }; + if (expectation['mode'] != '') + expectation['mode'] = 'navigate'; + if (expectation['dest'] == 'font') + expectation['dest'] = 'embed'; + fetch('/fetch/metadata/resources/record-header.py?retrieve=true&file=' + key) + .then(response => response.text()) + .then(t.step_func(text => assert_header_equals(text, expectation, testNamePrefix + ' embed'))) + .then(resolve) + .catch(e => reject(e)); + }; + document.body.appendChild(e); + }); + }, testNamePrefix + ' embed'); + + promise_test(t => { + return new Promise((resolve, reject) => { + let key = 'object-https-redirect' + nonce; + let e = document.createElement('object'); + e.data = urlHelperMethod('resources/record-header.py?file=' + key); + e.onload = e => { + let expectation = { ...expectedResults }; + if (expectation['mode'] != '') + expectation['mode'] = 'navigate'; + if (expectation['dest'] == 'font') + expectation['dest'] = 'object'; + fetch('/fetch/metadata/resources/record-header.py?retrieve=true&file=' + key) + .then(response => response.text()) + .then(t.step_func(text => assert_header_equals(text, expectation, testNamePrefix + ' object'))) + .then(resolve) + .catch(e => reject(e)); + }; + document.body.appendChild(e); + }); + }, testNamePrefix + ' object'); + + if (document.createElement('link').relList.supports('preload')) { + async_test(t => { + let key = 'preload' + nonce; + let e = document.createElement('link'); + e.rel = 'preload'; + e.href = urlHelperMethod('resources/record-header.py?file=' + key); + e.setAttribute('as', 'track'); + e.onload = e.onerror = t.step_func_done(e => { + let expectation = { ...expectedResults }; + if (expectation['mode'] != '') + expectation['mode'] = 'cors'; + fetch('/fetch/metadata/resources/record-header.py?retrieve=true&file=' + key) + .then(t.step_func(response => response.text())) + .then(t.step_func_done(text => assert_header_equals(text, expectation, testNamePrefix + ' preload'))) + .catch(t.unreached_func()); + }); + document.head.appendChild(e); + }, testNamePrefix + ' preload'); + } + + promise_test(t => { + return new Promise((resolve, reject) => { + let key = 'style-https-redirect' + nonce; + let e = document.createElement('link'); + e.rel = 'stylesheet'; + e.href = urlHelperMethod('resources/record-header.py?file=' + key); + e.onload = e => { + let expectation = { ...expectedResults }; + if (expectation['mode'] != '') + expectation['mode'] = 'no-cors'; + if (expectation['dest'] == 'font') + expectation['dest'] = 'style'; + fetch('/fetch/metadata/resources/record-header.py?retrieve=true&file=' + key) + .then(response => response.text()) + .then(t.step_func(text => assert_header_equals(text, expectation, testNamePrefix + ' stylesheet'))) + .then(resolve) + .catch(e => reject(e)); + }; + document.body.appendChild(e); + }); + }, testNamePrefix + ' stylesheet'); + + promise_test(t => { + return new Promise((resolve, reject) => { + let key = 'track-https-redirect' + nonce; + let video = createVideoElement(); + let el = createTrack(); + el.src = urlHelperMethod('resources/record-header.py?file=' + key); + el.onload = t.step_func(_ => { + let expectation = { ...expectedResults }; + if (expectation['mode'] != '') + expectation['mode'] = 'cors'; + if (expectation['dest'] == 'font') + expectation['dest'] = 'track'; + fetch('/fetch/metadata/resources/record-header.py?retrieve=true&file=' + key) + .then(response => response.text()) + .then(t.step_func(text => assert_header_equals(text, expectation, testNamePrefix + ' track'))) + .then(resolve); + }); + video.appendChild(el); + document.body.appendChild(video); + }); + }, testNamePrefix + ' track'); +} diff --git a/test/wpt/tests/fetch/metadata/resources/serviceworker-accessors-frame.html b/test/wpt/tests/fetch/metadata/resources/serviceworker-accessors-frame.html new file mode 100644 index 00000000000..98798025005 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/resources/serviceworker-accessors-frame.html @@ -0,0 +1,3 @@ + + +Page Title diff --git a/test/wpt/tests/fetch/metadata/resources/serviceworker-accessors.sw.js b/test/wpt/tests/fetch/metadata/resources/serviceworker-accessors.sw.js new file mode 100644 index 00000000000..36c55a77860 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/resources/serviceworker-accessors.sw.js @@ -0,0 +1,14 @@ +addEventListener("fetch", event => { + event.waitUntil(async function () { + if (!event.clientId) return; + const client = await clients.get(event.clientId); + if (!client) return; + + client.postMessage({ + "dest": event.request.headers.get("sec-fetch-dest"), + "mode": event.request.headers.get("sec-fetch-mode"), + "site": event.request.headers.get("sec-fetch-site"), + "user": event.request.headers.get("sec-fetch-user") + }); + }()); +}); diff --git a/test/wpt/tests/fetch/metadata/resources/sharedWorker.js b/test/wpt/tests/fetch/metadata/resources/sharedWorker.js new file mode 100644 index 00000000000..5eb89cb4f68 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/resources/sharedWorker.js @@ -0,0 +1,9 @@ +onconnect = function(e) { + var port = e.ports[0]; + + port.addEventListener('message', function(e) { + port.postMessage("Ready"); + }); + + port.start(); +} diff --git a/test/wpt/tests/fetch/metadata/resources/unload-with-beacon.html b/test/wpt/tests/fetch/metadata/resources/unload-with-beacon.html new file mode 100644 index 00000000000..b00c9a5776a --- /dev/null +++ b/test/wpt/tests/fetch/metadata/resources/unload-with-beacon.html @@ -0,0 +1,12 @@ + + diff --git a/test/wpt/tests/fetch/metadata/resources/xslt-test.sub.xml b/test/wpt/tests/fetch/metadata/resources/xslt-test.sub.xml new file mode 100644 index 00000000000..acb478ab641 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/resources/xslt-test.sub.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/test/wpt/tests/fetch/metadata/serviceworker-accessors.https.sub.html b/test/wpt/tests/fetch/metadata/serviceworker-accessors.https.sub.html new file mode 100644 index 00000000000..03a8321d4ca --- /dev/null +++ b/test/wpt/tests/fetch/metadata/serviceworker-accessors.https.sub.html @@ -0,0 +1,51 @@ + + + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/sharedworker.https.sub.html b/test/wpt/tests/fetch/metadata/sharedworker.https.sub.html new file mode 100644 index 00000000000..4df858208a7 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/sharedworker.https.sub.html @@ -0,0 +1,40 @@ + + + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/style.https.sub.html b/test/wpt/tests/fetch/metadata/style.https.sub.html new file mode 100644 index 00000000000..a30d81d70dd --- /dev/null +++ b/test/wpt/tests/fetch/metadata/style.https.sub.html @@ -0,0 +1,86 @@ + + + + + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/tools/README.md b/test/wpt/tests/fetch/metadata/tools/README.md new file mode 100644 index 00000000000..1c3bac2be5b --- /dev/null +++ b/test/wpt/tests/fetch/metadata/tools/README.md @@ -0,0 +1,126 @@ +# Fetch Metadata test generation framework + +This directory defines a command-line tool for procedurally generating WPT +tests. + +## Motivation + +Many features of the web platform involve the browser making one or more HTTP +requests to remote servers. Only some aspects of these requests are specified +within the standard that defines the relevant feature. Other aspects are +specified by external standards which span the entire platform (e.g. [Fetch +Metadata Request Headers](https://w3c.github.io/webappsec-fetch-metadata/)). + +This state of affairs makes it difficult to maintain test coverage for two +reasons: + +- When a new feature introduces a new kind of web request, it must be verified + to integrate with every cross-cutting standard. +- When a new cross-cutting standard is introduced, it must be verified to + integrate with every kind of web request. + +The tool in this directory attempts to reduce this tension. It allows +maintainers to express instructions for making web requests in an abstract +sense. These generic instructions can be reused by to produce a different suite +of tests for each cross-cutting feature. + +When a new kind of request is proposed, a single generic template can be +defined here. This will provide the maintainers of all cross-cutting features +with clear instruction on how to extend their test suite with the new feature. + +Similarly, when a new cross-cutting feature is proposed, the authors can use +this tool to build a test suite which spans the entire platform. + +## Build script + +To generate the Fetch Metadata tests, run `./wpt update-built --include fetch` +in the root of the repository. + +## Configuration + +The test generation tool requires a YAML-formatted configuration file as its +input. The file should define a dictionary with the following keys: + +- `templates` - a string describing the filesystem path from which template + files should be loaded +- `output_directory` - a string describing the filesystem path where the + generated test files should be written +- `cases` - a list of dictionaries describing how the test templates should be + expanded with individual subtests; each dictionary should have the following + keys: + - `all_subtests` - properties which should be defined for every expansion + - `common_axis` - a list of dictionaries + - `template_axes` - a dictionary relating template names to properties that + should be used when expanding that particular template + +Internally, the tool creates a set of "subtests" for each template. This set is +the Cartesian product of the `common_axis` and the given template's entry in +the `template_axes` dictionary. It uses this set of subtests to expand the +template, creating an output file. Refer to the next section for a concrete +example of how the expansion is performed. + +In general, the tool will output a single file for each template. However, the +`filename_flags` attribute has special semantics. It is used to separate +subtests for the same template file. This is intended to accommodate [the +web-platform-test's filename-based +conventions](https://web-platform-tests.org/writing-tests/file-names.html). + +For instance, when `.https` is present in a test file's name, the WPT test +harness will load that test using the HTTPS protocol. Subtests which include +the value `https` in the `filename_flags` property will be expanded using the +appropriate template but written to a distinct file whose name includes +`.https`. + +The generation tool requires that the configuration file references every +template in the `templates` directory. Because templates and configuration +files may be contributed by different people, this requirement ensures that +configuration authors are aware of all available templates. Some templates may +not be relevant for some features; in those cases, the configuration file can +include an empty array for the template's entry in the `template_axes` +dictionary (as in `template3.html` in the example which follows). + +## Expansion example + +In the following example configuration file, `a`, `b`, `s`, `w`, `x`, `y`, and +`z` all represent associative arrays. + +```yaml +templates: path/to/templates +output_directory: path/to/output +cases: + - every_subtest: s + common_axis: [a, b] + template_axes: + template1.html: [w] + template2.html: [x, y, z] + template3.html: [] +``` + +When run with such a configuration file, the tool would generate two files, +expanded with data as described below (where `(a, b)` represents the union of +`a` and `b`): + + template1.html: [(a, w), (b, w)] + template2.html: [(a, x), (b, x), (a, y), (b, y), (a, z), (b, z)] + template3.html: (zero tests; not expanded) + +## Design Considerations + +**Efficiency of generated output** The tool is capable of generating a large +number of tests given a small amount of input. Naively structured, this could +result in test suites which take large amount of time and computational +resources to complete. The tool has been designed to help authors structure the +generated output to reduce these resource requirements. + +**Literalness of generated output** Because the generated output is how most +people will interact with the tests, it is important that it be approachable. +This tool avoids outputting abstractions which would frustrate attempts to read +the source code or step through its execution environment. + +**Simplicity** The test generation logic itself was written to be approachable. +This makes it easier to anticipate how the tool will behave with new input, and +it lowers the bar for others to contribute improvements. + +Non-goals include conciseness of template files (verbosity makes the potential +expansions more predictable) and conciseness of generated output (verbosity +aids in the interpretation of results). diff --git a/test/wpt/tests/fetch/metadata/tools/fetch-metadata.conf.yml b/test/wpt/tests/fetch/metadata/tools/fetch-metadata.conf.yml new file mode 100644 index 00000000000..b277bcb7b53 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/tools/fetch-metadata.conf.yml @@ -0,0 +1,806 @@ +--- +templates: templates +output_directory: ../generated +cases: + - all_subtests: + expected: NULL + filename_flags: [] + common_axis: + - headerName: sec-fetch-site + origins: [httpOrigin] + description: Not sent to non-trustworthy same-origin destination + - headerName: sec-fetch-site + origins: [httpSameSite] + description: Not sent to non-trustworthy same-site destination + - headerName: sec-fetch-site + origins: [httpCrossSite] + description: Not sent to non-trustworthy cross-site destination + - headerName: sec-fetch-mode + origins: [httpOrigin] + description: Not sent to non-trustworthy same-origin destination + - headerName: sec-fetch-mode + origins: [httpSameSite] + description: Not sent to non-trustworthy same-site destination + - headerName: sec-fetch-mode + origins: [httpCrossSite] + description: Not sent to non-trustworthy cross-site destination + - headerName: sec-fetch-dest + origins: [httpOrigin] + description: Not sent to non-trustworthy same-origin destination + - headerName: sec-fetch-dest + origins: [httpSameSite] + description: Not sent to non-trustworthy same-site destination + - headerName: sec-fetch-dest + origins: [httpCrossSite] + description: Not sent to non-trustworthy cross-site destination + - headerName: sec-fetch-user + origins: [httpOrigin] + description: Not sent to non-trustworthy same-origin destination + - headerName: sec-fetch-user + origins: [httpSameSite] + description: Not sent to non-trustworthy same-site destination + - headerName: sec-fetch-user + origins: [httpCrossSite] + description: Not sent to non-trustworthy cross-site destination + template_axes: + # Unused + appcache-manifest.sub.https.html: [] + # The `audioWorklet` interface is only available in secure contexts + # https://webaudio.github.io/web-audio-api/#BaseAudioContext + audioworklet.https.sub.html: [] + # Service workers are only available in secure context + fetch-via-serviceworker.https.sub.html: [] + # Service workers are only available in secure context + serviceworker.https.sub.html: [] + + css-images.sub.html: + - filename_flags: [tentative] + css-font-face.sub.html: + - filename_flags: [tentative] + element-a.sub.html: [{}] + element-area.sub.html: [{}] + element-audio.sub.html: [{}] + element-embed.sub.html: [{}] + element-frame.sub.html: [{}] + element-iframe.sub.html: [{}] + element-img.sub.html: + - sourceAttr: src + - sourceAttr: srcset + element-img-environment-change.sub.html: [{}] + element-input-image.sub.html: [{}] + element-link-icon.sub.html: [{}] + element-link-prefetch.optional.sub.html: [{}] + element-meta-refresh.optional.sub.html: [{}] + element-picture.sub.html: [{}] + element-script.sub.html: + - {} + - elementAttrs: { type: module } + element-video.sub.html: [{}] + element-video-poster.sub.html: [{}] + fetch.sub.html: [{}] + form-submission.sub.html: + - method: GET + - method: POST + header-link.sub.html: + - rel: icon + - rel: stylesheet + header-refresh.optional.sub.html: [{}] + window-location.sub.html: [{}] + script-module-import-dynamic.sub.html: [{}] + script-module-import-static.sub.html: [{}] + svg-image.sub.html: [{}] + window-history.sub.html: [{}] + worker-dedicated-importscripts.sub.html: [{}] + worker-dedicated-constructor.sub.html: [{}] + + # Sec-Fetch-Site - direct requests + - all_subtests: + headerName: sec-fetch-site + filename_flags: [https] + common_axis: + - description: Same origin + origins: [httpsOrigin] + expected: same-origin + - description: Cross-site + origins: [httpsCrossSite] + expected: cross-site + - description: Same site + origins: [httpsSameSite] + expected: same-site + template_axes: + # Unused + # - the request mode of all "classic" worker scripts is set to + # "same-origin" + # https://html.spec.whatwg.org/#fetch-a-classic-worker-script + # - the request mode of all "top-level "module" worker scripts is set to + # "same-origin": + # https://html.spec.whatwg.org/#fetch-a-single-module-script + worker-dedicated-constructor.sub.html: [] + + appcache-manifest.sub.https.html: [{}] + audioworklet.https.sub.html: [{}] + css-images.sub.html: + - filename_flags: [tentative] + css-font-face.sub.html: + - filename_flags: [tentative] + element-a.sub.html: [{}] + element-area.sub.html: [{}] + element-audio.sub.html: [{}] + element-embed.sub.html: [{}] + element-frame.sub.html: [{}] + element-iframe.sub.html: [{}] + element-img.sub.html: + - sourceAttr: src + - sourceAttr: srcset + element-img-environment-change.sub.html: [{}] + element-input-image.sub.html: [{}] + element-link-icon.sub.html: [{}] + element-link-prefetch.optional.sub.html: [{}] + element-meta-refresh.optional.sub.html: [{}] + element-picture.sub.html: [{}] + element-script.sub.html: + - {} + - elementAttrs: { type: module } + element-video.sub.html: [{}] + element-video-poster.sub.html: [{}] + fetch.sub.html: [{ init: { mode: no-cors } }] + fetch-via-serviceworker.https.sub.html: [{ init: { mode: no-cors } }] + form-submission.sub.html: + - method: GET + - method: POST + header-link.sub.html: + - rel: icon + - rel: stylesheet + header-refresh.optional.sub.html: [{}] + window-location.sub.html: [{}] + script-module-import-dynamic.sub.html: [{}] + script-module-import-static.sub.html: [{}] + serviceworker.https.sub.html: [{}] + svg-image.sub.html: [{}] + window-history.sub.html: [{}] + worker-dedicated-importscripts.sub.html: [{}] + + # Sec-Fetch-Site - redirection from HTTP + - all_subtests: + headerName: sec-fetch-site + filename_flags: [] + common_axis: + - description: HTTPS downgrade (header not sent) + origins: [httpsOrigin, httpOrigin] + expected: NULL + - description: HTTPS upgrade + origins: [httpOrigin, httpsOrigin] + expected: cross-site + - description: HTTPS downgrade-upgrade + origins: [httpsOrigin, httpOrigin, httpsOrigin] + expected: cross-site + template_axes: + # Unused + # The `audioWorklet` interface is only available in secure contexts + # https://webaudio.github.io/web-audio-api/#BaseAudioContext + audioworklet.https.sub.html: [] + # Service workers are only available in secure context + fetch-via-serviceworker.https.sub.html: [] + # Service workers' redirect mode is "error" + serviceworker.https.sub.html: [] + # Interstitial locations in an HTTP redirect chain are not added to the + # session history, so these requests cannot be initiated using the + # History API. + window-history.sub.html: [] + # Unused + # - the request mode of all "classic" worker scripts is set to + # "same-origin" + # https://html.spec.whatwg.org/#fetch-a-classic-worker-script + # - the request mode of all "top-level "module" worker scripts is set to + # "same-origin": + # https://html.spec.whatwg.org/#fetch-a-single-module-script + worker-dedicated-constructor.sub.html: [] + + appcache-manifest.sub.https.html: [{}] + css-images.sub.html: + - filename_flags: [tentative] + css-font-face.sub.html: + - filename_flags: [tentative] + element-a.sub.html: [{}] + element-area.sub.html: [{}] + element-audio.sub.html: [{}] + element-embed.sub.html: [{}] + element-frame.sub.html: [{}] + element-iframe.sub.html: [{}] + element-img.sub.html: + - sourceAttr: src + - sourceAttr: srcset + element-img-environment-change.sub.html: [{}] + element-input-image.sub.html: [{}] + element-link-icon.sub.html: [{}] + element-link-prefetch.optional.sub.html: [{}] + element-meta-refresh.optional.sub.html: [{}] + element-picture.sub.html: [{}] + element-script.sub.html: + - {} + - elementAttrs: { type: module } + element-video.sub.html: [{}] + element-video-poster.sub.html: [{}] + fetch.sub.html: [{}] + form-submission.sub.html: + - method: GET + - method: POST + header-link.sub.html: + - rel: icon + - rel: stylesheet + header-refresh.optional.sub.html: [{}] + window-location.sub.html: [{}] + script-module-import-dynamic.sub.html: [{}] + script-module-import-static.sub.html: [{}] + svg-image.sub.html: [{}] + worker-dedicated-importscripts.sub.html: [{}] + + # Sec-Fetch-Site - redirection from HTTPS + - all_subtests: + headerName: sec-fetch-site + filename_flags: [https] + common_axis: + - description: Same-Origin -> Cross-Site -> Same-Origin redirect + origins: [httpsOrigin, httpsCrossSite, httpsOrigin] + expected: cross-site + - description: Same-Origin -> Same-Site -> Same-Origin redirect + origins: [httpsOrigin, httpsSameSite, httpsOrigin] + expected: same-site + - description: Cross-Site -> Same Origin + origins: [httpsCrossSite, httpsOrigin] + expected: cross-site + - description: Cross-Site -> Same-Site + origins: [httpsCrossSite, httpsSameSite] + expected: cross-site + - description: Cross-Site -> Cross-Site + origins: [httpsCrossSite, httpsCrossSite] + expected: cross-site + - description: Same-Origin -> Same Origin + origins: [httpsOrigin, httpsOrigin] + expected: same-origin + - description: Same-Origin -> Same-Site + origins: [httpsOrigin, httpsSameSite] + expected: same-site + - description: Same-Origin -> Cross-Site + origins: [httpsOrigin, httpsCrossSite] + expected: cross-site + - description: Same-Site -> Same Origin + origins: [httpsSameSite, httpsOrigin] + expected: same-site + - description: Same-Site -> Same-Site + origins: [httpsSameSite, httpsSameSite] + expected: same-site + - description: Same-Site -> Cross-Site + origins: [httpsSameSite, httpsCrossSite] + expected: cross-site + template_axes: + # Service Workers' redirect mode is "error" + serviceworker.https.sub.html: [] + # Interstitial locations in an HTTP redirect chain are not added to the + # session history, so these requests cannot be initiated using the + # History API. + window-history.sub.html: [] + # Unused + # - the request mode of all "classic" worker scripts is set to + # "same-origin" + # https://html.spec.whatwg.org/#fetch-a-classic-worker-script + # - the request mode of all "top-level "module" worker scripts is set to + # "same-origin": + # https://html.spec.whatwg.org/#fetch-a-single-module-script + worker-dedicated-constructor.sub.html: [] + + appcache-manifest.sub.https.html: [{}] + audioworklet.https.sub.html: [{}] + css-images.sub.html: + - filename_flags: [tentative] + css-font-face.sub.html: + - filename_flags: [tentative] + element-a.sub.html: [{}] + element-area.sub.html: [{}] + element-audio.sub.html: [{}] + element-embed.sub.html: [{}] + element-frame.sub.html: [{}] + element-iframe.sub.html: [{}] + element-img.sub.html: + - sourceAttr: src + - sourceAttr: srcset + element-img-environment-change.sub.html: [{}] + element-input-image.sub.html: [{}] + element-link-icon.sub.html: [{}] + element-link-prefetch.optional.sub.html: [{}] + element-meta-refresh.optional.sub.html: [{}] + element-picture.sub.html: [{}] + element-script.sub.html: + - {} + - elementAttrs: { type: module } + element-video.sub.html: [{}] + element-video-poster.sub.html: [{}] + fetch.sub.html: [{ init: { mode: no-cors } }] + fetch-via-serviceworker.https.sub.html: [{ init: { mode: no-cors } }] + form-submission.sub.html: + - method: GET + - method: POST + header-link.sub.html: + - rel: icon + - rel: stylesheet + header-refresh.optional.sub.html: [{}] + window-location.sub.html: [{}] + script-module-import-dynamic.sub.html: [{}] + script-module-import-static.sub.html: [{}] + svg-image.sub.html: [{}] + worker-dedicated-importscripts.sub.html: [{}] + + # Sec-Fetch-Site - redirection with mixed content + # These tests verify the effect that redirection has on the request's "site". + # The initial request must be made to a resource that is "same-site" with its + # origin. This avoids false positives because if the request were made to a + # cross-site resource, the value of "cross-site" would be assigned regardless + # of the subseqent redirection. + # + # Because these conditions necessarily warrant mixed content, only templates + # which can be configured to allow mixed content [1] can be used. + # + # [1] https://w3c.github.io/webappsec-mixed-content/#should-block-fetch + + - common_axis: + - description: HTTPS downgrade-upgrade + headerName: sec-fetch-site + origins: [httpsOrigin, httpOrigin, httpsOrigin] + expected: cross-site + filename_flags: [https] + template_axes: + # Mixed Content considers only a small subset of requests as + # "optionally-blockable." These are the only requests that can be tested + # for the "downgrade-upgrade" scenario, so all other templates must be + # explicitly ignored. + audioworklet.https.sub.html: [] + css-font-face.sub.html: [] + element-embed.sub.html: [] + element-frame.sub.html: [] + element-iframe.sub.html: [] + element-img-environment-change.sub.html: [] + element-link-icon.sub.html: [] + element-link-prefetch.optional.sub.html: [] + element-picture.sub.html: [] + element-script.sub.html: [] + fetch.sub.html: [] + fetch-via-serviceworker.https.sub.html: [] + header-link.sub.html: [] + script-module-import-static.sub.html: [] + script-module-import-dynamic.sub.html: [] + # Service Workers' redirect mode is "error" + serviceworker.https.sub.html: [] + # Interstitial locations in an HTTP redirect chain are not added to the + # session history, so these requests cannot be initiated using the + # History API. + window-history.sub.html: [] + worker-dedicated-constructor.sub.html: [] + worker-dedicated-importscripts.sub.html: [] + # Avoid duplicate subtest for 'sec-fetch-site - HTTPS downgrade-upgrade' + appcache-manifest.sub.https.html: [] + css-images.sub.html: + - filename_flags: [tentative] + element-a.sub.html: [{}] + element-area.sub.html: [{}] + element-audio.sub.html: [{}] + element-img.sub.html: + # srcset omitted because it is not "optionally-blockable" + # https://w3c.github.io/webappsec-mixed-content/#category-optionally-blockable + - sourceAttr: src + element-input-image.sub.html: [{}] + element-meta-refresh.optional.sub.html: [{}] + element-video.sub.html: [{}] + element-video-poster.sub.html: [{}] + form-submission.sub.html: + - method: GET + - method: POST + header-refresh.optional.sub.html: [{}] + svg-image.sub.html: [{}] + window-location.sub.html: [{}] + + # Sec-Fetch-Mode + # These tests are served over HTTPS so the induced requests will be both + # same-origin with the document [1] and a potentially-trustworthy URL [2]. + # + # [1] https://html.spec.whatwg.org/multipage/origin.html#same-origin + # [2] https://w3c.github.io/webappsec-secure-contexts/#potentially-trustworthy-url + - common_axis: + - headerName: sec-fetch-mode + filename_flags: [https] + origins: [] + template_axes: + appcache-manifest.sub.https.html: + - expected: no-cors + audioworklet.https.sub.html: + # https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-single-module-script + - expected: cors + css-images.sub.html: + - expected: no-cors + filename_flags: [tentative] + css-font-face.sub.html: + - expected: cors + filename_flags: [tentative] + element-a.sub.html: + - expected: navigate + # https://html.spec.whatwg.org/multipage/links.html#downloading-hyperlinks + - elementAttrs: {download: ''} + expected: no-cors + element-area.sub.html: + - expected: navigate + # https://html.spec.whatwg.org/multipage/links.html#downloading-hyperlinks + - elementAttrs: {download: ''} + expected: no-cors + element-audio.sub.html: + - expected: no-cors + - expected: cors + elementAttrs: { crossorigin: '' } + - expected: cors + elementAttrs: { crossorigin: anonymous } + - expected: cors + elementAttrs: { crossorigin: use-credentials } + element-embed.sub.html: + - expected: no-cors + element-frame.sub.html: + - expected: navigate + element-iframe.sub.html: + - expected: navigate + element-img.sub.html: + - sourceAttr: src + expected: no-cors + - sourceAttr: src + expected: cors + elementAttrs: { crossorigin: '' } + - sourceAttr: src + expected: cors + elementAttrs: { crossorigin: anonymous } + - sourceAttr: src + expected: cors + elementAttrs: { crossorigin: use-credentials } + - sourceAttr: srcset + expected: no-cors + - sourceAttr: srcset + expected: cors + elementAttrs: { crossorigin: '' } + - sourceAttr: srcset + expected: cors + elementAttrs: { crossorigin: anonymous } + - sourceAttr: srcset + expected: cors + elementAttrs: { crossorigin: use-credentials } + element-img-environment-change.sub.html: + - expected: no-cors + - expected: cors + elementAttrs: { crossorigin: '' } + - expected: cors + elementAttrs: { crossorigin: anonymous } + - expected: cors + elementAttrs: { crossorigin: use-credentials } + element-input-image.sub.html: + - expected: no-cors + element-link-icon.sub.html: + - expected: no-cors + - expected: cors + elementAttrs: { crossorigin: '' } + - expected: cors + elementAttrs: { crossorigin: anonymous } + - expected: cors + elementAttrs: { crossorigin: use-credentials } + element-link-prefetch.optional.sub.html: + - expected: no-cors + - expected: cors + elementAttrs: { crossorigin: '' } + - expected: cors + elementAttrs: { crossorigin: anonymous } + - expected: cors + elementAttrs: { crossorigin: use-credentials } + element-meta-refresh.optional.sub.html: + - expected: navigate + element-picture.sub.html: + - expected: no-cors + - expected: cors + elementAttrs: { crossorigin: '' } + - expected: cors + elementAttrs: { crossorigin: anonymous } + - expected: cors + elementAttrs: { crossorigin: use-credentials } + element-script.sub.html: + - expected: no-cors + - expected: cors + elementAttrs: { type: module } + - expected: cors + elementAttrs: { crossorigin: '' } + - expected: cors + elementAttrs: { crossorigin: anonymous } + - expected: cors + elementAttrs: { crossorigin: use-credentials } + element-video.sub.html: + - expected: no-cors + - expected: cors + elementAttrs: { crossorigin: '' } + - expected: cors + elementAttrs: { crossorigin: anonymous } + - expected: cors + elementAttrs: { crossorigin: use-credentials } + element-video-poster.sub.html: + - expected: no-cors + fetch.sub.html: + - expected: cors + - expected: cors + init: { mode: cors } + - expected: no-cors + init: { mode: no-cors } + - expected: same-origin + init: { mode: same-origin } + fetch-via-serviceworker.https.sub.html: + - expected: cors + - expected: cors + init: { mode: cors } + - expected: no-cors + init: { mode: no-cors } + - expected: same-origin + init: { mode: same-origin } + form-submission.sub.html: + - method: GET + expected: navigate + - method: POST + expected: navigate + header-link.sub.html: + - rel: icon + expected: no-cors + - rel: stylesheet + expected: no-cors + header-refresh.optional.sub.html: + - expected: navigate + window-history.sub.html: + - expected: navigate + window-location.sub.html: + - expected: navigate + script-module-import-dynamic.sub.html: + - expected: cors + script-module-import-static.sub.html: + - expected: cors + # https://svgwg.org/svg2-draft/linking.html#processingURL-fetch + svg-image.sub.html: + - expected: no-cors + - expected: cors + elementAttrs: { crossorigin: '' } + - expected: cors + elementAttrs: { crossorigin: anonymous } + - expected: cors + elementAttrs: { crossorigin: use-credentials } + serviceworker.https.sub.html: + - expected: same-origin + options: { type: 'classic' } + # https://github.com/whatwg/html/pull/5875 + - expected: same-origin + worker-dedicated-constructor.sub.html: + - expected: same-origin + - options: { type: module } + expected: same-origin + worker-dedicated-importscripts.sub.html: + - expected: no-cors + + # Sec-Fetch-Dest + - common_axis: + - headerName: sec-fetch-dest + filename_flags: [https] + origins: [] + template_axes: + appcache-manifest.sub.https.html: + - expected: empty + audioworklet.https.sub.html: + # https://github.com/WebAudio/web-audio-api/issues/2203 + - expected: audioworklet + css-images.sub.html: + - expected: image + filename_flags: [tentative] + css-font-face.sub.html: + - expected: font + filename_flags: [tentative] + element-a.sub.html: + - expected: document + # https://html.spec.whatwg.org/multipage/links.html#downloading-hyperlinks + - elementAttrs: {download: ''} + expected: empty + element-area.sub.html: + - expected: document + # https://html.spec.whatwg.org/multipage/links.html#downloading-hyperlinks + - elementAttrs: {download: ''} + expected: empty + element-audio.sub.html: + - expected: audio + element-embed.sub.html: + - expected: embed + element-frame.sub.html: + # https://github.com/whatwg/html/pull/4976 + - expected: frame + element-iframe.sub.html: + # https://github.com/whatwg/html/pull/4976 + - expected: iframe + element-img.sub.html: + - sourceAttr: src + expected: image + - sourceAttr: srcset + expected: image + element-img-environment-change.sub.html: + - expected: image + element-input-image.sub.html: + - expected: image + element-link-icon.sub.html: + - expected: empty + element-link-prefetch.optional.sub.html: + - expected: empty + - elementAttrs: { as: audio } + expected: audio + - elementAttrs: { as: document } + expected: document + - elementAttrs: { as: embed } + expected: embed + - elementAttrs: { as: fetch } + expected: fetch + - elementAttrs: { as: font } + expected: font + - elementAttrs: { as: image } + expected: image + - elementAttrs: { as: object } + expected: object + - elementAttrs: { as: script } + expected: script + - elementAttrs: { as: style } + expected: style + - elementAttrs: { as: track } + expected: track + - elementAttrs: { as: video } + expected: video + - elementAttrs: { as: worker } + expected: worker + element-meta-refresh.optional.sub.html: + - expected: document + element-picture.sub.html: + - expected: image + element-script.sub.html: + - expected: script + element-video.sub.html: + - expected: video + element-video-poster.sub.html: + - expected: image + fetch.sub.html: + - expected: empty + fetch-via-serviceworker.https.sub.html: + - expected: empty + form-submission.sub.html: + - method: GET + expected: document + - method: POST + expected: document + header-link.sub.html: + - rel: icon + expected: empty + - rel: stylesheet + filename_flags: [tentative] + expected: style + header-refresh.optional.sub.html: + - expected: document + window-history.sub.html: + - expected: document + window-location.sub.html: + - expected: document + script-module-import-dynamic.sub.html: + - expected: script + script-module-import-static.sub.html: + - expected: script + serviceworker.https.sub.html: + - expected: serviceworker + # Implemented as "image" in Chromium and Firefox, but specified as + # "empty" + # https://github.com/w3c/svgwg/issues/782 + svg-image.sub.html: + - expected: empty + worker-dedicated-constructor.sub.html: + - expected: worker + - options: { type: module } + expected: worker + worker-dedicated-importscripts.sub.html: + - expected: script + + # Sec-Fetch-User + - common_axis: + - headerName: sec-fetch-user + filename_flags: [https] + origins: [] + template_axes: + appcache-manifest.sub.https.html: + - expected: NULL + audioworklet.https.sub.html: + - expected: NULL + css-images.sub.html: + - expected: NULL + filename_flags: [tentative] + css-font-face.sub.html: + - expected: NULL + filename_flags: [tentative] + element-a.sub.html: + - expected: NULL + - userActivated: TRUE + expected: ?1 + element-area.sub.html: + - expected: NULL + - userActivated: TRUE + expected: ?1 + element-audio.sub.html: + - expected: NULL + element-embed.sub.html: + - expected: NULL + element-frame.sub.html: + - expected: NULL + - userActivated: TRUE + expected: ?1 + element-iframe.sub.html: + - expected: NULL + - userActivated: TRUE + expected: ?1 + element-img.sub.html: + - sourceAttr: src + expected: NULL + - sourceAttr: srcset + expected: NULL + element-img-environment-change.sub.html: + - expected: NULL + element-input-image.sub.html: + - expected: NULL + element-link-icon.sub.html: + - expected: NULL + element-link-prefetch.optional.sub.html: + - expected: NULL + element-meta-refresh.optional.sub.html: + - expected: NULL + element-picture.sub.html: + - expected: NULL + element-script.sub.html: + - expected: NULL + element-video.sub.html: + - expected: NULL + element-video-poster.sub.html: + - expected: NULL + fetch.sub.html: + - expected: NULL + fetch-via-serviceworker.https.sub.html: + - expected: NULL + form-submission.sub.html: + - method: GET + expected: NULL + - method: GET + userActivated: TRUE + expected: ?1 + - method: POST + expected: NULL + - method: POST + userActivated: TRUE + expected: ?1 + header-link.sub.html: + - rel: icon + expected: NULL + - rel: stylesheet + expected: NULL + header-refresh.optional.sub.html: + - expected: NULL + window-history.sub.html: + - expected: NULL + window-location.sub.html: + - expected: NULL + - userActivated: TRUE + expected: ?1 + script-module-import-dynamic.sub.html: + - expected: NULL + script-module-import-static.sub.html: + - expected: NULL + serviceworker.https.sub.html: + - expected: NULL + svg-image.sub.html: + - expected: NULL + worker-dedicated-constructor.sub.html: + - expected: NULL + - options: { type: module } + expected: NULL + worker-dedicated-importscripts.sub.html: + - expected: NULL diff --git a/test/wpt/tests/fetch/metadata/tools/generate.py b/test/wpt/tests/fetch/metadata/tools/generate.py new file mode 100644 index 00000000000..fa850c8c8a0 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/tools/generate.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python3 + +import itertools +import os + +import jinja2 +import yaml + +HERE = os.path.abspath(os.path.dirname(__file__)) +PROJECT_ROOT = os.path.join(HERE, '..', '..', '..') + +def find_templates(starting_directory): + for directory, subdirectories, file_names in os.walk(starting_directory): + for file_name in file_names: + if file_name.startswith('.'): + continue + yield file_name, os.path.join(directory, file_name) + +def test_name(directory, template_name, subtest_flags): + ''' + Create a test name based on a template and the WPT file name flags [1] + required for a given subtest. This name is used to determine how subtests + may be grouped together. In order to promote grouping, the combination uses + a few aspects of how file name flags are interpreted: + + - repeated flags have no effect, so duplicates are removed + - flag sequence does not matter, so flags are consistently sorted + + directory | template_name | subtest_flags | result + ----------|------------------|-----------------|------- + cors | image.html | [] | cors/image.html + cors | image.https.html | [] | cors/image.https.html + cors | image.html | [https] | cors/image.https.html + cors | image.https.html | [https] | cors/image.https.html + cors | image.https.html | [https] | cors/image.https.html + cors | image.sub.html | [https] | cors/image.https.sub.html + cors | image.https.html | [sub] | cors/image.https.sub.html + + [1] docs/writing-tests/file-names.md + ''' + template_name_parts = template_name.split('.') + flags = set(subtest_flags) | set(template_name_parts[1:-1]) + test_name_parts = ( + [template_name_parts[0]] + + sorted(flags) + + [template_name_parts[-1]] + ) + return os.path.join(directory, '.'.join(test_name_parts)) + +def merge(a, b): + if type(a) != type(b): + raise Exception('Cannot merge disparate types') + if type(a) == list: + return a + b + if type(a) == dict: + merged = {} + + for key in a: + if key in b: + merged[key] = merge(a[key], b[key]) + else: + merged[key] = a[key] + + for key in b: + if not key in a: + merged[key] = b[key] + + return merged + + raise Exception('Cannot merge {} type'.format(type(a).__name__)) + +def product(a, b): + ''' + Given two lists of objects, compute their Cartesian product by merging the + elements together. For example, + + product( + [{'a': 1}, {'b': 2}], + [{'c': 3}, {'d': 4}, {'e': 5}] + ) + + returns the following list: + + [ + {'a': 1, 'c': 3}, + {'a': 1, 'd': 4}, + {'a': 1, 'e': 5}, + {'b': 2, 'c': 3}, + {'b': 2, 'd': 4}, + {'b': 2, 'e': 5} + ] + ''' + result = [] + + for a_object in a: + for b_object in b: + result.append(merge(a_object, b_object)) + + return result + +def make_provenance(project_root, cases, template): + return '\n'.join([ + 'This test was procedurally generated. Please do not modify it directly.', + 'Sources:', + '- {}'.format(os.path.relpath(cases, project_root)), + '- {}'.format(os.path.relpath(template, project_root)) + ]) + +def collection_filter(obj, title): + if not obj: + return 'no {}'.format(title) + + members = [] + for name, value in obj.items(): + if value == '': + members.append(name) + else: + members.append('{}={}'.format(name, value)) + + return '{}: {}'.format(title, ', '.join(members)) + +def pad_filter(value, side, padding): + if not value: + return '' + if side == 'start': + return padding + value + + return value + padding + +def main(config_file): + with open(config_file, 'r') as handle: + config = yaml.safe_load(handle.read()) + + templates_directory = os.path.normpath( + os.path.join(os.path.dirname(config_file), config['templates']) + ) + + environment = jinja2.Environment( + variable_start_string='[%', + variable_end_string='%]' + ) + environment.filters['collection'] = collection_filter + environment.filters['pad'] = pad_filter + templates = {} + subtests = {} + + for template_name, path in find_templates(templates_directory): + subtests[template_name] = [] + with open(path, 'r') as handle: + templates[template_name] = environment.from_string(handle.read()) + + for case in config['cases']: + unused_templates = set(templates) - set(case['template_axes']) + + # This warning is intended to help authors avoid mistakenly omitting + # templates. It can be silenced by extending the`template_axes` + # dictionary with an empty list for templates which are intentionally + # unused. + if unused_templates: + print( + 'Warning: case does not reference the following templates:' + ) + print('\n'.join('- {}'.format(name) for name in unused_templates)) + + common_axis = product( + case['common_axis'], [case.get('all_subtests', {})] + ) + + for template_name, template_axis in case['template_axes'].items(): + subtests[template_name].extend(product(common_axis, template_axis)) + + for template_name, template in templates.items(): + provenance = make_provenance( + PROJECT_ROOT, + config_file, + os.path.join(templates_directory, template_name) + ) + get_filename = lambda subtest: test_name( + config['output_directory'], + template_name, + subtest['filename_flags'] + ) + subtests_by_filename = itertools.groupby( + sorted(subtests[template_name], key=get_filename), + key=get_filename + ) + for filename, some_subtests in subtests_by_filename: + with open(filename, 'w') as handle: + handle.write(templates[template_name].render( + subtests=list(some_subtests), + provenance=provenance + ) + '\n') + +if __name__ == '__main__': + main('fetch-metadata.conf.yml') diff --git a/test/wpt/tests/fetch/metadata/tools/templates/appcache-manifest.sub.https.html b/test/wpt/tests/fetch/metadata/tools/templates/appcache-manifest.sub.https.html new file mode 100644 index 00000000000..0dfc084f2e3 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/tools/templates/appcache-manifest.sub.https.html @@ -0,0 +1,63 @@ + + + + + HTTP headers on request for Appcache manifest + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/tools/templates/audioworklet.https.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/audioworklet.https.sub.html new file mode 100644 index 00000000000..7be309c5068 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/tools/templates/audioworklet.https.sub.html @@ -0,0 +1,53 @@ + + + + + HTTP headers on request for AudioWorklet module + + + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/tools/templates/css-font-face.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/css-font-face.sub.html new file mode 100644 index 00000000000..94b33f4e6b0 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/tools/templates/css-font-face.sub.html @@ -0,0 +1,60 @@ + + + + + HTTP headers on request for CSS font-face + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/tools/templates/css-images.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/css-images.sub.html new file mode 100644 index 00000000000..e394f9f5b06 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/tools/templates/css-images.sub.html @@ -0,0 +1,137 @@ + + + + + {%- if subtests|length > 10 %} + + {%- endif %} + HTTP headers on request for CSS image-accepting properties + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/tools/templates/element-a.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/element-a.sub.html new file mode 100644 index 00000000000..2bd8e8a40e0 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/tools/templates/element-a.sub.html @@ -0,0 +1,72 @@ + + + + + {%- if subtests|length > 10 %} + + {%- endif %} + HTTP headers on request for HTML "a" element navigation + + + {%- if subtests|selectattr('userActivated')|list %} + + + {%- endif %} + + + + + diff --git a/test/wpt/tests/fetch/metadata/tools/templates/element-area.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/element-area.sub.html new file mode 100644 index 00000000000..0cef5b22941 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/tools/templates/element-area.sub.html @@ -0,0 +1,72 @@ + + + + + {%- if subtests|length > 10 %} + + {%- endif %} + HTTP headers on request for HTML "area" element navigation + + + {%- if subtests|selectattr('userActivated')|list %} + + + {%- endif %} + + + + + diff --git a/test/wpt/tests/fetch/metadata/tools/templates/element-audio.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/element-audio.sub.html new file mode 100644 index 00000000000..92bc22198e7 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/tools/templates/element-audio.sub.html @@ -0,0 +1,51 @@ + + + + + HTTP headers on request for HTML "audio" element source + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/tools/templates/element-embed.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/element-embed.sub.html new file mode 100644 index 00000000000..18ce09e5fdc --- /dev/null +++ b/test/wpt/tests/fetch/metadata/tools/templates/element-embed.sub.html @@ -0,0 +1,54 @@ + + + + + HTTP headers on request for HTML "embed" element source + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/tools/templates/element-frame.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/element-frame.sub.html new file mode 100644 index 00000000000..ce90171779a --- /dev/null +++ b/test/wpt/tests/fetch/metadata/tools/templates/element-frame.sub.html @@ -0,0 +1,62 @@ + + + + + HTTP headers on request for HTML "frame" element source + + + {%- if subtests|selectattr('userActivated')|list %} + + + {%- endif %} + + + + + diff --git a/test/wpt/tests/fetch/metadata/tools/templates/element-iframe.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/element-iframe.sub.html new file mode 100644 index 00000000000..43a632a15cd --- /dev/null +++ b/test/wpt/tests/fetch/metadata/tools/templates/element-iframe.sub.html @@ -0,0 +1,62 @@ + + + + + HTTP headers on request for HTML "frame" element source + + + {%- if subtests|selectattr('userActivated')|list %} + + + {%- endif %} + + + + + diff --git a/test/wpt/tests/fetch/metadata/tools/templates/element-img-environment-change.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/element-img-environment-change.sub.html new file mode 100644 index 00000000000..5a65114f184 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/tools/templates/element-img-environment-change.sub.html @@ -0,0 +1,78 @@ + + + + + HTTP headers on image request triggered by change to environment + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/tools/templates/element-img.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/element-img.sub.html new file mode 100644 index 00000000000..1dac5843ecd --- /dev/null +++ b/test/wpt/tests/fetch/metadata/tools/templates/element-img.sub.html @@ -0,0 +1,52 @@ + + + + + HTTP headers on request for HTML "img" element source + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/tools/templates/element-input-image.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/element-input-image.sub.html new file mode 100644 index 00000000000..3c50008433c --- /dev/null +++ b/test/wpt/tests/fetch/metadata/tools/templates/element-input-image.sub.html @@ -0,0 +1,48 @@ + + + + + HTTP headers on request for HTML "input" element with type="button" + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/tools/templates/element-link-icon.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/element-link-icon.sub.html new file mode 100644 index 00000000000..18ce12a6898 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/tools/templates/element-link-icon.sub.html @@ -0,0 +1,75 @@ + + + + + {%- if subtests|length > 10 %} + + {%- endif %} + HTTP headers on request for HTML "link" element with rel="icon" + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/tools/templates/element-link-prefetch.optional.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/element-link-prefetch.optional.sub.html new file mode 100644 index 00000000000..59d677d8d67 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/tools/templates/element-link-prefetch.optional.sub.html @@ -0,0 +1,71 @@ + + + + + {%- if subtests|length > 10 %} + + {%- endif %} + HTTP headers on request for HTML "link" element with rel="prefetch" + + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/tools/templates/element-meta-refresh.optional.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/element-meta-refresh.optional.sub.html new file mode 100644 index 00000000000..5a8d8f8ecd2 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/tools/templates/element-meta-refresh.optional.sub.html @@ -0,0 +1,60 @@ + + + + + HTTP headers on request for HTML "meta" element with http-equiv="refresh" + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/tools/templates/element-picture.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/element-picture.sub.html new file mode 100644 index 00000000000..903aeed1f37 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/tools/templates/element-picture.sub.html @@ -0,0 +1,101 @@ + + + + + HTTP headers on request for HTML "picture" element source + + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/tools/templates/element-script.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/element-script.sub.html new file mode 100644 index 00000000000..4a281ae5192 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/tools/templates/element-script.sub.html @@ -0,0 +1,54 @@ + + + + + HTTP headers on request for HTML "script" element source + + + + + + diff --git a/test/wpt/tests/fetch/metadata/tools/templates/element-video-poster.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/element-video-poster.sub.html new file mode 100644 index 00000000000..9cdaf063ace --- /dev/null +++ b/test/wpt/tests/fetch/metadata/tools/templates/element-video-poster.sub.html @@ -0,0 +1,62 @@ + + + + + HTTP headers on request for HTML "video" element "poster" + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/tools/templates/element-video.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/element-video.sub.html new file mode 100644 index 00000000000..1b7b976d7c4 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/tools/templates/element-video.sub.html @@ -0,0 +1,51 @@ + + + + + HTTP headers on request for HTML "video" element source + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/tools/templates/fetch-via-serviceworker.https.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/fetch-via-serviceworker.https.sub.html new file mode 100644 index 00000000000..eead7102008 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/tools/templates/fetch-via-serviceworker.https.sub.html @@ -0,0 +1,88 @@ + + + + + {%- if subtests|length > 10 %} + + {%- endif %} + HTTP headers on request using the "fetch" API and passing through a Serive Worker + + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/tools/templates/fetch.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/fetch.sub.html new file mode 100644 index 00000000000..a8dc5368f8b --- /dev/null +++ b/test/wpt/tests/fetch/metadata/tools/templates/fetch.sub.html @@ -0,0 +1,42 @@ + + + + + HTTP headers on request using the "fetch" API + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/tools/templates/form-submission.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/form-submission.sub.html new file mode 100644 index 00000000000..4c9c8c50f82 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/tools/templates/form-submission.sub.html @@ -0,0 +1,87 @@ + + + + + + HTTP headers on request for HTML form navigation + + + {%- if subtests|selectattr('userActivated')|list %} + + + {%- endif %} + + + + + diff --git a/test/wpt/tests/fetch/metadata/tools/templates/header-link.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/header-link.sub.html new file mode 100644 index 00000000000..2831f221d5c --- /dev/null +++ b/test/wpt/tests/fetch/metadata/tools/templates/header-link.sub.html @@ -0,0 +1,56 @@ + + + + + HTTP headers on request for HTTP "Link" header + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/tools/templates/header-refresh.optional.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/header-refresh.optional.sub.html new file mode 100644 index 00000000000..ec963d5cc00 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/tools/templates/header-refresh.optional.sub.html @@ -0,0 +1,59 @@ + + + + + {%- if subtests|length > 10 %} + + {%- endif %} + HTTP headers on request for HTTP "Refresh" header + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/tools/templates/script-module-import-dynamic.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/script-module-import-dynamic.sub.html new file mode 100644 index 00000000000..653d3cdec44 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/tools/templates/script-module-import-dynamic.sub.html @@ -0,0 +1,35 @@ + + + + + HTTP headers on request for dynamic ECMAScript module import + + + + + diff --git a/test/wpt/tests/fetch/metadata/tools/templates/script-module-import-static.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/script-module-import-static.sub.html new file mode 100644 index 00000000000..c8d5f9532a9 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/tools/templates/script-module-import-static.sub.html @@ -0,0 +1,53 @@ + + + + + HTTP headers on request for static ECMAScript module import + + + + + + diff --git a/test/wpt/tests/fetch/metadata/tools/templates/serviceworker.https.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/serviceworker.https.sub.html new file mode 100644 index 00000000000..82843255469 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/tools/templates/serviceworker.https.sub.html @@ -0,0 +1,72 @@ + + + + + + HTTP headers on request for Service Workers + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/tools/templates/svg-image.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/svg-image.sub.html new file mode 100644 index 00000000000..52f7806b33c --- /dev/null +++ b/test/wpt/tests/fetch/metadata/tools/templates/svg-image.sub.html @@ -0,0 +1,75 @@ + + + + + {%- if subtests|length > 10 %} + + {%- endif %} + HTTP headers on request for SVG "image" element source + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/tools/templates/window-history.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/window-history.sub.html new file mode 100644 index 00000000000..286d019887d --- /dev/null +++ b/test/wpt/tests/fetch/metadata/tools/templates/window-history.sub.html @@ -0,0 +1,134 @@ + + + + + {%- if subtests|length > 10 %} + + {%- endif %} + HTTP headers on request for navigation via the HTML History API + + + {%- if subtests|selectattr('userActivated')|list %} + + + {%- endif %} + + + + + diff --git a/test/wpt/tests/fetch/metadata/tools/templates/window-location.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/window-location.sub.html new file mode 100644 index 00000000000..96f39123616 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/tools/templates/window-location.sub.html @@ -0,0 +1,128 @@ + + + + + {%- if subtests|length > 10 %} + + {%- endif %} + HTTP headers on request for navigation via the HTML Location API + + + {%- if subtests|selectattr('userActivated')|list %} + + + {%- endif %} + + + + + diff --git a/test/wpt/tests/fetch/metadata/tools/templates/worker-dedicated-constructor.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/worker-dedicated-constructor.sub.html new file mode 100644 index 00000000000..fede5965d3a --- /dev/null +++ b/test/wpt/tests/fetch/metadata/tools/templates/worker-dedicated-constructor.sub.html @@ -0,0 +1,49 @@ + + + + + HTTP headers on request for dedicated worker via the "Worker" constructor + + + + + diff --git a/test/wpt/tests/fetch/metadata/tools/templates/worker-dedicated-importscripts.sub.html b/test/wpt/tests/fetch/metadata/tools/templates/worker-dedicated-importscripts.sub.html new file mode 100644 index 00000000000..93e6374d54b --- /dev/null +++ b/test/wpt/tests/fetch/metadata/tools/templates/worker-dedicated-importscripts.sub.html @@ -0,0 +1,54 @@ + + + + + HTTP headers on request for dedicated worker via the "importScripts" API + + + + + diff --git a/test/wpt/tests/fetch/metadata/track.https.sub.html b/test/wpt/tests/fetch/metadata/track.https.sub.html new file mode 100644 index 00000000000..346798fdc0e --- /dev/null +++ b/test/wpt/tests/fetch/metadata/track.https.sub.html @@ -0,0 +1,119 @@ + + + + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/trailing-dot.https.sub.any.js b/test/wpt/tests/fetch/metadata/trailing-dot.https.sub.any.js new file mode 100644 index 00000000000..5e32fc4e7f6 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/trailing-dot.https.sub.any.js @@ -0,0 +1,30 @@ +// META: global=window,worker +// META: script=/fetch/metadata/resources/helper.js + +// Site +promise_test(t => { + return validate_expectations_custom_url("https://{{host}}.:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", {}, { + "site": "cross-site", + "user": "", + "mode": "cors", + "dest": "empty" + }, "Fetching a resource from the same origin, but spelled with a trailing dot."); +}, "Fetching a resource from the same origin, but spelled with a trailing dot."); + +promise_test(t => { + return validate_expectations_custom_url("https://{{hosts[][www]}}.:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", {}, { + "site": "cross-site", + "user": "", + "mode": "cors", + "dest": "empty" + }, "Fetching a resource from the same site, but spelled with a trailing dot."); +}, "Fetching a resource from the same site, but spelled with a trailing dot."); + +promise_test(t => { + return validate_expectations_custom_url("https://{{hosts[alt][www]}}.:{{ports[https][0]}}/fetch/metadata/resources/echo-as-json.py", {}, { + "site": "cross-site", + "user": "", + "mode": "cors", + "dest": "empty" + }, "Fetching a resource from a cross-site host, spelled with a trailing dot."); +}, "Fetching a resource from a cross-site host, spelled with a trailing dot."); diff --git a/test/wpt/tests/fetch/metadata/unload.https.sub.html b/test/wpt/tests/fetch/metadata/unload.https.sub.html new file mode 100644 index 00000000000..bc26048c810 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/unload.https.sub.html @@ -0,0 +1,64 @@ + + + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/window-open.https.sub.html b/test/wpt/tests/fetch/metadata/window-open.https.sub.html new file mode 100644 index 00000000000..94ba76a19ff --- /dev/null +++ b/test/wpt/tests/fetch/metadata/window-open.https.sub.html @@ -0,0 +1,199 @@ + + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/worker.https.sub.html b/test/wpt/tests/fetch/metadata/worker.https.sub.html new file mode 100644 index 00000000000..20a4fe54166 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/worker.https.sub.html @@ -0,0 +1,24 @@ + + + + + + + + + diff --git a/test/wpt/tests/fetch/metadata/xslt.https.sub.html b/test/wpt/tests/fetch/metadata/xslt.https.sub.html new file mode 100644 index 00000000000..dc72d7b8a67 --- /dev/null +++ b/test/wpt/tests/fetch/metadata/xslt.https.sub.html @@ -0,0 +1,25 @@ + + + + + + + + diff --git a/test/wpt/tests/fetch/nosniff/image.html b/test/wpt/tests/fetch/nosniff/image.html new file mode 100644 index 00000000000..9dfdb94cf62 --- /dev/null +++ b/test/wpt/tests/fetch/nosniff/image.html @@ -0,0 +1,39 @@ + + +
+ diff --git a/test/wpt/tests/fetch/nosniff/importscripts.html b/test/wpt/tests/fetch/nosniff/importscripts.html new file mode 100644 index 00000000000..920b6bdd409 --- /dev/null +++ b/test/wpt/tests/fetch/nosniff/importscripts.html @@ -0,0 +1,14 @@ + + +
+ diff --git a/test/wpt/tests/fetch/nosniff/importscripts.js b/test/wpt/tests/fetch/nosniff/importscripts.js new file mode 100644 index 00000000000..18952805bb7 --- /dev/null +++ b/test/wpt/tests/fetch/nosniff/importscripts.js @@ -0,0 +1,28 @@ +// Testing importScripts() +function log(w) { this.postMessage(w) } +function f() { log("FAIL") } +function p() { log("PASS") } + +const get_url = (mime, outcome) => { + let url = "resources/js.py" + if (mime != null) { + url += "?type=" + encodeURIComponent(mime) + } + if (outcome) { + url += "&outcome=p" + } + return url +} + +[null, "", "x", "x/x", "text/html", "text/json"].forEach(function(mime) { + try { + importScripts(get_url(mime)) + } catch(e) { + (e.name == "NetworkError") ? p() : log("FAIL (no NetworkError exception): " + mime) + } + +}) +importScripts(get_url("text/javascript", true)) +importScripts(get_url("text/ecmascript", true)) +importScripts(get_url("text/ecmascript;blah", true)) +log("END") diff --git a/test/wpt/tests/fetch/nosniff/parsing-nosniff.window.js b/test/wpt/tests/fetch/nosniff/parsing-nosniff.window.js new file mode 100644 index 00000000000..2a2648653ca --- /dev/null +++ b/test/wpt/tests/fetch/nosniff/parsing-nosniff.window.js @@ -0,0 +1,27 @@ +promise_test(() => fetch("resources/x-content-type-options.json").then(res => res.json()).then(runTests), "Loading JSON…"); + +function runTests(allTestData) { + for (let i = 0; i < allTestData.length; i++) { + const testData = allTestData[i], + input = encodeURIComponent(testData.input); + promise_test(t => { + let resolve; + const promise = new Promise(r => resolve = r); + const script = document.createElement("script"); + t.add_cleanup(() => script.remove()); + // A + +
+ diff --git a/test/wpt/tests/fetch/nosniff/stylesheet.html b/test/wpt/tests/fetch/nosniff/stylesheet.html new file mode 100644 index 00000000000..8f2b5476e90 --- /dev/null +++ b/test/wpt/tests/fetch/nosniff/stylesheet.html @@ -0,0 +1,60 @@ + + + +
+ diff --git a/test/wpt/tests/fetch/nosniff/worker.html b/test/wpt/tests/fetch/nosniff/worker.html new file mode 100644 index 00000000000..c8c1076df5c --- /dev/null +++ b/test/wpt/tests/fetch/nosniff/worker.html @@ -0,0 +1,28 @@ + + +
+ diff --git a/test/wpt/tests/fetch/orb/resources/data.json b/test/wpt/tests/fetch/orb/resources/data.json new file mode 100644 index 00000000000..f2a886f39de --- /dev/null +++ b/test/wpt/tests/fetch/orb/resources/data.json @@ -0,0 +1,3 @@ +{ + "hello": "world" +} diff --git a/test/wpt/tests/fetch/orb/resources/font.ttf b/test/wpt/tests/fetch/orb/resources/font.ttf new file mode 100644 index 0000000000000000000000000000000000000000..9023592ef5aa83a03dd6957398897a585062ca57 GIT binary patch literal 2528 zcmds3+iM(E9RAMC>}--IZHkSlbcJb}Hmx+fn~4|=c}SaNsWzb@-3F9mI_^#~3%fJR z?rbg~7_kpEQi_7Nf1zrjK~Q`sQnW2nkwV|M-@%oK-E?>x_(MpM29?acOqAH}eAZX8>i{v90{QD@UK8 z?l;0S4h7BS^-meAn|!xZ@)uj54c;RECHbzRm$TF>nv6e6K2fq3%b3Ch^~cB?u2r(v zkH7ad5c`2P>9SY#cXvOjFn>GsdB|P~`n~De%#NWyu}z}@xPEE<^GzId1b6hq`YrNJ zpl7(G&#mANPHPA{)^6*E!$^@bL|Q0mLqc}TB|Swb8%8pe2%7wX7*!uCHz~QWfyJ-r z7tPW^=Wn!Ro%h$|>{uSdXvUa|I&fOQrS7LPvS9~Z(v&zMA=;65&=C=`DhY|mZ&Kj!3HhbNxDPn<$e@rAG|9>`>_U%Yaa|m@d0+T+9$}A07}*9%}w^Ondom zl3bI=hUcy>--uAx7wLGEX&Kzn_%3s9JFtg_4YL!pi){|FXP{H;ZW!kJ85v$FZVvZc z=7R1t%y+FTKz4K3D>LVrE9j_0Kg^W>kV`b=3OX8csX3V|_H9EhakU|rxWPtGG$xDs zeRLLqoPhkgUkcxKN!R$Lr8Sp8i&%_ketg8+5v}5Y_$i__v?%)`I)-*-GNN_L7dTC! z$#2##gbi9?mv|+j6|{;sB3i|`_#mP+>{8kyItD{YMzl`3g%NltV+j=$Fb4-d3>-ub zhlow2(MK>aNlgJoLYZ6^7Cnmetngdg!aW6>yiIwPzj@l!;1b)kFc{MzW$@m3p1uag z87D`H8(I%iBJ=u;J%|+dLb#J*WgAu=<5fZ*DXp;5R9MY}C{;>IjO(NK5lxbD9RfzY z@=~QR=lI6K+#$nE_oaaWNmxCd;m?tPvxY zJ8xC9c9rx5g?W}XCSRSBcZdsV~Z&>YL1E97mjWcg0TE4dJ(nei;|UEZ=>^4@JlJ9c3=csI~@nRO6C WZTNf5{_GpcUH|0vRERIFfAlvXi@|mP literal 0 HcmV?d00001 diff --git a/test/wpt/tests/fetch/orb/resources/image.png b/test/wpt/tests/fetch/orb/resources/image.png new file mode 100644 index 0000000000000000000000000000000000000000..820f8cace2143bfc45c0c301e84b6c29b8630068 GIT binary patch literal 1010 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD3?#3*wSy!Wi-X*q7}lMWc?smOq&xaLGB9lH z=l+w(3gjy!dj$D1FjT2AFf_CT|`v++n6B;aK<+8d}OFfd9ReoF^w N_jL7hS?83{1OVd{KuZ7s literal 0 HcmV?d00001 diff --git a/test/wpt/tests/fetch/orb/resources/js-unlabeled.js b/test/wpt/tests/fetch/orb/resources/js-unlabeled.js new file mode 100644 index 00000000000..a880a5bc724 --- /dev/null +++ b/test/wpt/tests/fetch/orb/resources/js-unlabeled.js @@ -0,0 +1 @@ +window.has_executed_script = true; diff --git a/test/wpt/tests/fetch/orb/resources/png-mislabeled-as-html.png b/test/wpt/tests/fetch/orb/resources/png-mislabeled-as-html.png new file mode 100644 index 0000000000000000000000000000000000000000..820f8cace2143bfc45c0c301e84b6c29b8630068 GIT binary patch literal 1010 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD3?#3*wSy!Wi-X*q7}lMWc?smOq&xaLGB9lH z=l+w(3gjy!dj$D1FjT2AFf_CT|`v++n6B;aK<+8d}OFfd9ReoF^w N_jL7hS?83{1OVd{KuZ7s literal 0 HcmV?d00001 diff --git a/test/wpt/tests/fetch/orb/resources/png-mislabeled-as-html.png.headers b/test/wpt/tests/fetch/orb/resources/png-mislabeled-as-html.png.headers new file mode 100644 index 00000000000..156209f9c81 --- /dev/null +++ b/test/wpt/tests/fetch/orb/resources/png-mislabeled-as-html.png.headers @@ -0,0 +1 @@ +Content-Type: text/html diff --git a/test/wpt/tests/fetch/orb/resources/png-unlabeled.png b/test/wpt/tests/fetch/orb/resources/png-unlabeled.png new file mode 100644 index 0000000000000000000000000000000000000000..820f8cace2143bfc45c0c301e84b6c29b8630068 GIT binary patch literal 1010 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD3?#3*wSy!Wi-X*q7}lMWc?smOq&xaLGB9lH z=l+w(3gjy!dj$D1FjT2AFf_CT|`v++n6B;aK<+8d}OFfd9ReoF^w N_jL7hS?83{1OVd{KuZ7s literal 0 HcmV?d00001 diff --git a/test/wpt/tests/fetch/orb/resources/script.js b/test/wpt/tests/fetch/orb/resources/script.js new file mode 100644 index 00000000000..19675d25d83 --- /dev/null +++ b/test/wpt/tests/fetch/orb/resources/script.js @@ -0,0 +1,4 @@ +"use strict"; +function fn() { + return 42; +} diff --git a/test/wpt/tests/fetch/orb/resources/sound.mp3 b/test/wpt/tests/fetch/orb/resources/sound.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..a15d1de328f3f3325cb9589d423abf60d538d838 GIT binary patch literal 539 zcmeZtF=k-^0p*b3U{@f`&%nU!lUSB!YNlsmpl4`c2$qEq|9?9iK;lA}o_T5cKo(FR z12Y2y<2!~agup36;6EV%vKi>eWS}F>3_u(hP=PTR-<`~R#tG*A|1EHYf%yOf;}RfO ufq}uKfq{X=$I;i-SkKZ@&oq=;0A!D5GnzfrG91YqkUhcZ{y~zb783yc + + +
+ + diff --git a/test/wpt/tests/fetch/orb/tentative/content-range.sub.any.js b/test/wpt/tests/fetch/orb/tentative/content-range.sub.any.js new file mode 100644 index 00000000000..ee97521a55c --- /dev/null +++ b/test/wpt/tests/fetch/orb/tentative/content-range.sub.any.js @@ -0,0 +1,31 @@ +// META: script=/fetch/orb/resources/utils.js + +const url = + "http://{{domains[www1]}}:{{ports[http][0]}}/fetch/orb/resources/image.png"; + +promise_test(async () => { + let headers = new Headers([["Range", "bytes=0-99"]]); + await fetchORB( + url, + { headers }, + header("Content-Range", "bytes 0-99/1010"), + "slice(null,100)", + "status(206)" + ); +}, "ORB shouldn't block opaque range of image/png starting at zero"); + +promise_test( + t => + promise_rejects_js( + t, + TypeError, + fetchORB( + url, + { headers: new Headers([["Range", "bytes 10-99"]]) }, + header("Content-Range", "bytes 10-99/1010"), + "slice(10,100)", + "status(206)" + ) + ), + "ORB should block opaque range of image/png not starting at zero, that isn't subsequent" +); diff --git a/test/wpt/tests/fetch/orb/tentative/img-mime-types-coverage.tentative.sub.html b/test/wpt/tests/fetch/orb/tentative/img-mime-types-coverage.tentative.sub.html new file mode 100644 index 00000000000..5dc6c5d63af --- /dev/null +++ b/test/wpt/tests/fetch/orb/tentative/img-mime-types-coverage.tentative.sub.html @@ -0,0 +1,126 @@ + + + +
+ + diff --git a/test/wpt/tests/fetch/orb/tentative/img-png-mislabeled-as-html.sub-ref.html b/test/wpt/tests/fetch/orb/tentative/img-png-mislabeled-as-html.sub-ref.html new file mode 100644 index 00000000000..66462fb5e3c --- /dev/null +++ b/test/wpt/tests/fetch/orb/tentative/img-png-mislabeled-as-html.sub-ref.html @@ -0,0 +1,5 @@ + + + + + diff --git a/test/wpt/tests/fetch/orb/tentative/img-png-mislabeled-as-html.sub.html b/test/wpt/tests/fetch/orb/tentative/img-png-mislabeled-as-html.sub.html new file mode 100644 index 00000000000..aa03f4db636 --- /dev/null +++ b/test/wpt/tests/fetch/orb/tentative/img-png-mislabeled-as-html.sub.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/test/wpt/tests/fetch/orb/tentative/img-png-unlabeled.sub-ref.html b/test/wpt/tests/fetch/orb/tentative/img-png-unlabeled.sub-ref.html new file mode 100644 index 00000000000..2d5e3bb8b58 --- /dev/null +++ b/test/wpt/tests/fetch/orb/tentative/img-png-unlabeled.sub-ref.html @@ -0,0 +1,5 @@ + + + + + diff --git a/test/wpt/tests/fetch/orb/tentative/img-png-unlabeled.sub.html b/test/wpt/tests/fetch/orb/tentative/img-png-unlabeled.sub.html new file mode 100644 index 00000000000..77415f6af1d --- /dev/null +++ b/test/wpt/tests/fetch/orb/tentative/img-png-unlabeled.sub.html @@ -0,0 +1,7 @@ + + + + + + + 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 new file mode 100644 index 00000000000..a7bb6630583 --- /dev/null +++ b/test/wpt/tests/fetch/orb/tentative/known-mime-type.sub.any.js @@ -0,0 +1,41 @@ +// META: script=/fetch/orb/resources/utils.js + +const path = "http://{{domains[www1]}}:{{ports[http][0]}}/fetch/orb/resources"; + +promise_test( + t => + promise_rejects_js( + t, + TypeError, + fetchORB(`${path}/font.ttf`, null, contentType("font/ttf")) + ), + "ORB should block opaque font/ttf" +); + +promise_test( + t => + promise_rejects_js( + t, + TypeError, + fetchORB(`${path}/text.txt`, null, contentType("text/plain")) + ), + "ORB should block opaque text/plain" +); + +promise_test( + t => + promise_rejects_js( + t, + TypeError, + fetchORB(`${path}/data.json`, null, contentType("application/json")) + ), + "ORB should block opaque application/json" +); + +promise_test(async () => { + fetchORB(`${path}/image.png`, null, contentType("image/png")); +}, "ORB shouldn't block opaque image/png"); + +promise_test(async () => { + await fetchORB(`${path}/script.js`, null, contentType("text/javascript")); +}, "ORB shouldn't block opaque text/javascript"); diff --git a/test/wpt/tests/fetch/orb/tentative/nosniff.sub.any.js b/test/wpt/tests/fetch/orb/tentative/nosniff.sub.any.js new file mode 100644 index 00000000000..3df9d22e0b7 --- /dev/null +++ b/test/wpt/tests/fetch/orb/tentative/nosniff.sub.any.js @@ -0,0 +1,59 @@ +// META: script=/fetch/orb/resources/utils.js + +const path = "http://{{domains[www1]}}:{{ports[http][0]}}/fetch/orb/resources"; + +promise_test( + t => + promise_rejects_js( + t, + TypeError, + fetchORB( + `${path}/text.txt`, + null, + contentType("text/plain"), + contentTypeOptions("nosniff") + ) + ), + "ORB should block opaque text/plain with nosniff" +); + +promise_test( + t => + promise_rejects_js( + t, + TypeError, + fetchORB( + `${path}/data.json`, + null, + contentType("application/json"), + contentTypeOptions("nosniff") + ) + ), + "ORB should block opaque-response-blocklisted MIME type with nosniff" +); + +promise_test( + t => + promise_rejects_js( + t, + TypeError, + fetchORB( + `${path}/data.json`, + null, + contentType(""), + contentTypeOptions("nosniff") + ) + ), + "ORB should block opaque response with empty Content-Type and nosniff" +); + +promise_test( + () => + fetchORB( + `${path}/image.png`, + null, + contentType(""), + contentTypeOptions("nosniff") + ), + "ORB shouldn't block opaque image with empty Content-Type and nosniff" +); diff --git a/test/wpt/tests/fetch/orb/tentative/script-js-unlabeled-gziped.sub.html b/test/wpt/tests/fetch/orb/tentative/script-js-unlabeled-gziped.sub.html new file mode 100644 index 00000000000..fe854407988 --- /dev/null +++ b/test/wpt/tests/fetch/orb/tentative/script-js-unlabeled-gziped.sub.html @@ -0,0 +1,24 @@ + + + + + +
+ + + + + + + + diff --git a/test/wpt/tests/fetch/orb/tentative/script-unlabeled.sub.html b/test/wpt/tests/fetch/orb/tentative/script-unlabeled.sub.html new file mode 100644 index 00000000000..4987f1307e7 --- /dev/null +++ b/test/wpt/tests/fetch/orb/tentative/script-unlabeled.sub.html @@ -0,0 +1,24 @@ + + + + + +
+ + + + + + + + diff --git a/test/wpt/tests/fetch/orb/tentative/status.sub.any.js b/test/wpt/tests/fetch/orb/tentative/status.sub.any.js new file mode 100644 index 00000000000..b94d8b7f635 --- /dev/null +++ b/test/wpt/tests/fetch/orb/tentative/status.sub.any.js @@ -0,0 +1,33 @@ +// META: script=/fetch/orb/resources/utils.js + +const path = "http://{{domains[www1]}}:{{ports[http][0]}}/fetch/orb/resources"; + +promise_test( + t => + promise_rejects_js( + t, + TypeError, + fetchORB( + `${path}/data.json`, + null, + contentType("application/json"), + "status(206)" + ) + ), + "ORB should block opaque-response-blocklisted MIME type with status 206" +); + +promise_test( + t => + promise_rejects_js( + t, + TypeError, + fetchORB( + `${path}/data.json`, + null, + contentType("application/json"), + "status(302)" + ) + ), + "ORB should block opaque response with non-ok status" +); diff --git a/test/wpt/tests/fetch/orb/tentative/status.sub.html b/test/wpt/tests/fetch/orb/tentative/status.sub.html new file mode 100644 index 00000000000..a62bdeb35e4 --- /dev/null +++ b/test/wpt/tests/fetch/orb/tentative/status.sub.html @@ -0,0 +1,17 @@ +'use strict'; + + + +
+ diff --git a/test/wpt/tests/fetch/orb/tentative/unknown-mime-type.sub.any.js b/test/wpt/tests/fetch/orb/tentative/unknown-mime-type.sub.any.js new file mode 100644 index 00000000000..f72ff928adb --- /dev/null +++ b/test/wpt/tests/fetch/orb/tentative/unknown-mime-type.sub.any.js @@ -0,0 +1,28 @@ +// META: script=/fetch/orb/resources/utils.js + +const path = "http://{{domains[www1]}}:{{ports[http][0]}}/fetch/orb/resources"; + +promise_test( + () => fetchORB(`${path}/font.ttf`, null, contentType("")), + "ORB shouldn't block opaque failed missing MIME type (font/ttf)" +); + +promise_test( + () => fetchORB(`${path}/text.txt`, null, contentType("")), + "ORB shouldn't block opaque failed missing MIME type (text/plain)" +); + +promise_test( + t => fetchORB(`${path}/data.json`, null, contentType("")), + "ORB shouldn't block opaque failed missing MIME type (application/json)" +); + +promise_test( + () => fetchORB(`${path}/image.png`, null, contentType("")), + "ORB shouldn't block opaque failed missing MIME type (image/png)" +); + +promise_test( + () => fetchORB(`${path}/script.js`, null, contentType("")), + "ORB shouldn't block opaque failed missing MIME type (text/javascript)" +); diff --git a/test/wpt/tests/fetch/origin/assorted.window.js b/test/wpt/tests/fetch/origin/assorted.window.js new file mode 100644 index 00000000000..423790dfb1e --- /dev/null +++ b/test/wpt/tests/fetch/origin/assorted.window.js @@ -0,0 +1,211 @@ +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js + +const origins = get_host_info(); + +promise_test(async function () { + const stash = token(), + redirectPath = "/fetch/origin/resources/redirect-and-stash.py"; + + // Cross-origin -> same-origin will result in setting the tainted origin flag for the second + // request. + let url = origins.HTTP_ORIGIN + redirectPath + "?stash=" + stash; + url = origins.HTTP_REMOTE_ORIGIN + redirectPath + "?stash=" + stash + "&location=" + encodeURIComponent(url); + + await fetch(url, { mode: "no-cors", method: "POST" }); + + const json = await (await fetch(redirectPath + "?dump&stash=" + stash)).json(); + + assert_equals(json[0], origins.HTTP_ORIGIN); + assert_equals(json[1], "null"); +}, "Origin header and 308 redirect"); + +promise_test(async function () { + const stash = token(), + redirectPath = "/fetch/origin/resources/redirect-and-stash.py"; + + let url = origins.HTTP_ORIGIN + redirectPath + "?stash=" + stash; + url = origins.HTTP_REMOTE_ORIGIN + redirectPath + "?stash=" + stash + "&location=" + encodeURIComponent(url); + + await new Promise(resolve => { + const frame = document.createElement("iframe"); + frame.src = url; + frame.onload = () => { + resolve(); + frame.remove(); + } + document.body.appendChild(frame); + }); + + const json = await (await fetch(redirectPath + "?dump&stash=" + stash)).json(); + + assert_equals(json[0], "no Origin header"); + assert_equals(json[1], "no Origin header"); +}, "Origin header and GET navigation"); + +promise_test(async function () { + const stash = token(), + redirectPath = "/fetch/origin/resources/redirect-and-stash.py"; + + let url = origins.HTTP_ORIGIN + redirectPath + "?stash=" + stash; + url = origins.HTTP_REMOTE_ORIGIN + redirectPath + "?stash=" + stash + "&location=" + encodeURIComponent(url); + + await new Promise(resolve => { + const frame = document.createElement("iframe"); + self.addEventListener("message", e => { + if (e.data === "loaded") { + resolve(); + frame.remove(); + } + }, { once: true }); + frame.onload = () => { + const doc = frame.contentDocument, + form = doc.body.appendChild(doc.createElement("form")), + submit = form.appendChild(doc.createElement("input")); + form.action = url; + form.method = "POST"; + submit.type = "submit"; + submit.click(); + } + document.body.appendChild(frame); + }); + + const json = await (await fetch(redirectPath + "?dump&stash=" + stash)).json(); + + assert_equals(json[0], origins.HTTP_ORIGIN); + assert_equals(json[1], "null"); +}, "Origin header and POST navigation"); + +function navigationReferrerPolicy(referrerPolicy, destination, expectedOrigin) { + return async function () { + const stash = token(); + const referrerPolicyPath = "/fetch/origin/resources/referrer-policy.py"; + const redirectPath = "/fetch/origin/resources/redirect-and-stash.py"; + + let postUrl = + (destination === "same-origin" ? origins.HTTP_ORIGIN + : origins.HTTP_REMOTE_ORIGIN) + + redirectPath + "?stash=" + stash; + + await new Promise(resolve => { + const frame = document.createElement("iframe"); + document.body.appendChild(frame); + frame.src = origins.HTTP_ORIGIN + referrerPolicyPath + + "?referrerPolicy=" + referrerPolicy; + self.addEventListener("message", function listener(e) { + if (e.data === "loaded") { + resolve(); + frame.remove(); + self.removeEventListener("message", listener); + } else if (e.data === "action") { + const doc = frame.contentDocument, + form = doc.body.appendChild(doc.createElement("form")), + submit = form.appendChild(doc.createElement("input")); + form.action = postUrl; + form.method = "POST"; + submit.type = "submit"; + submit.click(); + } + }); + }); + + const json = await (await fetch(redirectPath + "?dump&stash=" + stash)).json(); + + assert_equals(json[0], expectedOrigin); + }; +} + +function fetchReferrerPolicy(referrerPolicy, destination, fetchMode, expectedOrigin, httpMethod) { + return async function () { + const stash = token(); + const redirectPath = "/fetch/origin/resources/redirect-and-stash.py"; + + let fetchUrl = + (destination === "same-origin" ? origins.HTTP_ORIGIN + : origins.HTTP_REMOTE_ORIGIN) + + redirectPath + "?stash=" + stash; + + await fetch(fetchUrl, { mode: fetchMode, method: httpMethod , "referrerPolicy": referrerPolicy}); + + const json = await (await fetch(redirectPath + "?dump&stash=" + stash)).json(); + + assert_equals(json[0], expectedOrigin); + }; +} + +function referrerPolicyTestString(referrerPolicy, method, destination) { + return "Origin header and " + method + " " + destination + " with Referrer-Policy " + + referrerPolicy; +} + +[ + { + "policy": "no-referrer", + "expectedOriginForSameOrigin": "null", + "expectedOriginForCrossOrigin": "null" + }, + { + "policy": "same-origin", + "expectedOriginForSameOrigin": origins.HTTP_ORIGIN, + "expectedOriginForCrossOrigin": "null" + }, + { + "policy": "origin-when-cross-origin", + "expectedOriginForSameOrigin": origins.HTTP_ORIGIN, + "expectedOriginForCrossOrigin": origins.HTTP_ORIGIN + }, + { + "policy": "no-referrer-when-downgrade", + "expectedOriginForSameOrigin": origins.HTTP_ORIGIN, + "expectedOriginForCrossOrigin": origins.HTTP_ORIGIN + }, + { + "policy": "unsafe-url", + "expectedOriginForSameOrigin": origins.HTTP_ORIGIN, + "expectedOriginForCrossOrigin": origins.HTTP_ORIGIN + }, +].forEach(testObj => { + [ + { + "name": "same-origin", + "expectedOrigin": testObj.expectedOriginForSameOrigin + }, + { + "name": "cross-origin", + "expectedOrigin": testObj.expectedOriginForCrossOrigin + } + ].forEach(destination => { + // Test form POST navigation + promise_test(navigationReferrerPolicy(testObj.policy, + destination.name, + destination.expectedOrigin), + referrerPolicyTestString(testObj.policy, "POST", + destination.name + " navigation")); + // Test fetch + promise_test(fetchReferrerPolicy(testObj.policy, + destination.name, + "no-cors", + destination.expectedOrigin, + "POST"), + referrerPolicyTestString(testObj.policy, "POST", + destination.name + " fetch no-cors mode")); + + // Test cors mode POST + promise_test(fetchReferrerPolicy(testObj.policy, + destination.name, + "cors", + origins.HTTP_ORIGIN, + "POST"), + referrerPolicyTestString(testObj.policy, "POST", + destination.name + " fetch cors mode")); + + // Test cors mode GET + promise_test(fetchReferrerPolicy(testObj.policy, + destination.name, + "cors", + (destination.name == "same-origin") ? "no Origin header" : origins.HTTP_ORIGIN, + "GET"), + referrerPolicyTestString(testObj.policy, "GET", + destination.name + " fetch cors mode")); + }); +}); diff --git a/test/wpt/tests/fetch/origin/resources/redirect-and-stash.py b/test/wpt/tests/fetch/origin/resources/redirect-and-stash.py new file mode 100644 index 00000000000..9bf9e1f18c2 --- /dev/null +++ b/test/wpt/tests/fetch/origin/resources/redirect-and-stash.py @@ -0,0 +1,32 @@ +import json + +from wptserve.utils import isomorphic_decode + +def main(request, response): + key = request.GET.first(b"stash") + origin = request.headers.get(b"origin") + if origin is None: + origin = b"no Origin header" + + origin_list = request.server.stash.take(key) + + if b"dump" in request.GET: + response.headers.set(b"Content-Type", b"application/json") + response.content = json.dumps(origin_list) + return + + if origin_list is None: + origin_list = [isomorphic_decode(origin)] + else: + origin_list.append(isomorphic_decode(origin)) + + request.server.stash.put(key, origin_list) + + if b"location" in request.GET: + response.status = 308 + response.headers.set(b"Location", request.GET.first(b"location")) + return + + response.headers.set(b"Content-Type", b"text/html") + response.headers.set(b"Access-Control-Allow-Origin", b"*") + response.content = b"\n" diff --git a/test/wpt/tests/fetch/origin/resources/referrer-policy.py b/test/wpt/tests/fetch/origin/resources/referrer-policy.py new file mode 100644 index 00000000000..15716e068b9 --- /dev/null +++ b/test/wpt/tests/fetch/origin/resources/referrer-policy.py @@ -0,0 +1,7 @@ +def main(request, response): + if b"referrerPolicy" in request.GET: + response.headers.set(b"Referrer-Policy", + request.GET.first(b"referrerPolicy")) + response.status = 200 + response.headers.set(b"Content-Type", b"text/html") + response.content = b"\n" diff --git a/test/wpt/tests/fetch/private-network-access/META.yml b/test/wpt/tests/fetch/private-network-access/META.yml new file mode 100644 index 00000000000..944ce6f14a1 --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/META.yml @@ -0,0 +1,7 @@ +spec: https://wicg.github.io/private-network-access/ +suggested_reviewers: + - letitz + - lyf + - hemeryar + - camillelamy + - mikewest 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/fetch-from-treat-as-public.https.window.js b/test/wpt/tests/fetch/private-network-access/fetch-from-treat-as-public.https.window.js new file mode 100644 index 00000000000..90f7feb6341 --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/fetch-from-treat-as-public.https.window.js @@ -0,0 +1,68 @@ +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests verify that documents fetched from the `local` or `private` +// 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(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { server: Server.HTTPS_LOCAL }, + expected: FetchTestResult.FAILURE, +}), "treat-as-public-address to local: failed preflight."); + +promise_test(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + // Interesting: no need for CORS headers on same-origin final response. + }, + }, + expected: FetchTestResult.SUCCESS, +}), "treat-as-public-address to local: success."); + +promise_test(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { server: Server.HTTPS_PRIVATE }, + expected: FetchTestResult.FAILURE, +}), "treat-as-public-address to private: failed preflight."); + +promise_test(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: FetchTestResult.SUCCESS, +}), "treat-as-public-address to private: success."); + +promise_test(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PUBLIC, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: FetchTestResult.SUCCESS, +}), "treat-as-public-address to public: no preflight required."); diff --git a/test/wpt/tests/fetch/private-network-access/fetch.https.window.js b/test/wpt/tests/fetch/private-network-access/fetch.https.window.js new file mode 100644 index 00000000000..dbc4f23f677 --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/fetch.https.window.js @@ -0,0 +1,271 @@ +// META: script=/common/subset-tests-by-key.js +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// META: variant=?include=baseline +// META: variant=?include=from-local +// META: variant=?include=from-private +// META: variant=?include=from-public +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests verify that secure contexts can fetch subresources from all +// address spaces, provided that the target server, if more private than the +// initiator, respond affirmatively to preflight requests. +// +// This file covers only those tests that must execute in a secure context. +// Other tests are defined in: fetch.window.js + +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 => fetchTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { server: Server.HTTPS_LOCAL }, + expected: FetchTestResult.SUCCESS, +}), "local to local: no preflight required."); + +subsetTestByKey("from-local", promise_test, t => fetchTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: FetchTestResult.SUCCESS, +}), "local to private: no preflight required."); + + +subsetTestByKey("from-local", promise_test, t => fetchTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { + server: Server.HTTPS_PUBLIC, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + 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 => fetchTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { + server: Server.HTTPS_PUBLIC, + behavior: { + preflight: PreflightBehavior.failure(), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + fetchOptions: { method: "PUT" }, + expected: FetchTestResult.FAILURE, +}), "local to public: PUT preflight failure."); + +subsetTestByKey("baseline", promise_test, t => fetchTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { + server: Server.HTTPS_PUBLIC, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + } + }, + fetchOptions: { method: "PUT" }, + 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 => fetchTest(t, { + source, + target: { + server: targetServer, + behavior: { + preflight: PreflightBehavior.failure(), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: FetchTestResult.FAILURE, + }), prefix + "failed preflight."); + + subsetTestByKey(subsetKey, promise_test, t => fetchTest(t, { + source, + target: { + server: targetServer, + behavior: { + preflight: PreflightBehavior.noCorsHeader(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: FetchTestResult.FAILURE, + }), prefix + "missing CORS headers on preflight response."); + + subsetTestByKey(subsetKey, promise_test, t => fetchTest(t, { + source, + target: { + server: targetServer, + behavior: { + preflight: PreflightBehavior.noPnaHeader(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: FetchTestResult.FAILURE, + }), prefix + "missing PNA header on preflight response."); + + subsetTestByKey(subsetKey, promise_test, t => fetchTest(t, { + source, + target: { + server: targetServer, + behavior: { preflight: PreflightBehavior.success(token()) }, + }, + expected: FetchTestResult.FAILURE, + }), prefix + "missing CORS headers on final response."); + + subsetTestByKey(subsetKey, promise_test, t => fetchTest(t, { + source, + target: { + server: targetServer, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: FetchTestResult.SUCCESS, + }), prefix + "success."); + + subsetTestByKey(subsetKey, promise_test, t => fetchTest(t, { + source, + target: { + server: targetServer, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + fetchOptions: { method: "PUT" }, + expected: FetchTestResult.SUCCESS, + }), prefix + "PUT success."); + + subsetTestByKey(subsetKey, promise_test, t => fetchTest(t, { + source, + target: { server: targetServer }, + fetchOptions: { mode: "no-cors" }, + expected: FetchTestResult.FAILURE, + }), prefix + "no-CORS mode failed preflight."); + + subsetTestByKey(subsetKey, promise_test, t => fetchTest(t, { + source, + target: { + server: targetServer, + behavior: { preflight: PreflightBehavior.noCorsHeader(token()) }, + }, + fetchOptions: { mode: "no-cors" }, + expected: FetchTestResult.FAILURE, + }), prefix + "no-CORS mode missing CORS headers on preflight response."); + + subsetTestByKey(subsetKey, promise_test, t => fetchTest(t, { + source, + target: { + server: targetServer, + behavior: { preflight: PreflightBehavior.noPnaHeader(token()) }, + }, + fetchOptions: { mode: "no-cors" }, + expected: FetchTestResult.FAILURE, + }), prefix + "no-CORS mode missing PNA header on preflight response."); + + subsetTestByKey(subsetKey, promise_test, t => fetchTest(t, { + source, + target: { + server: targetServer, + behavior: { preflight: PreflightBehavior.success(token()) }, + }, + fetchOptions: { 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 => fetchTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { server: Server.HTTPS_PRIVATE }, + expected: FetchTestResult.SUCCESS, +}), "private to private: no preflight required."); + +subsetTestByKey("from-private", promise_test, t => fetchTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + 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 => fetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { server: Server.HTTPS_PUBLIC }, + expected: FetchTestResult.SUCCESS, +}), "public to public: no preflight required."); + diff --git a/test/wpt/tests/fetch/private-network-access/fetch.window.js b/test/wpt/tests/fetch/private-network-access/fetch.window.js new file mode 100644 index 00000000000..8ee54c90562 --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/fetch.window.js @@ -0,0 +1,183 @@ +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests verify that non-secure contexts cannot fetch subresources from +// less-public address spaces, and can fetch them otherwise. +// +// This file covers only those tests that must execute in a non secure context. +// Other tests are defined in: fetch.https.window.js + +setup(() => { + // Making sure we are in a non secure context, as expected. + assert_false(window.isSecureContext); +}); + +promise_test(t => fetchTest(t, { + source: { server: Server.HTTP_LOCAL }, + target: { server: Server.HTTP_LOCAL }, + expected: FetchTestResult.SUCCESS, +}), "local to local: no preflight required."); + +promise_test(t => fetchTest(t, { + source: { server: Server.HTTP_LOCAL }, + target: { + server: Server.HTTP_PRIVATE, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: FetchTestResult.SUCCESS, +}), "local to private: no preflight required."); + +promise_test(t => fetchTest(t, { + source: { server: Server.HTTP_LOCAL }, + target: { + server: Server.HTTP_PUBLIC, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: FetchTestResult.SUCCESS, +}), "local to public: no preflight required."); + +promise_test(t => fetchTest(t, { + source: { server: Server.HTTP_PRIVATE }, + target: { + server: Server.HTTP_LOCAL, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: FetchTestResult.FAILURE, +}), "private to local: failure."); + +promise_test(t => fetchTest(t, { + source: { server: Server.HTTP_PRIVATE }, + target: { server: Server.HTTP_PRIVATE }, + expected: FetchTestResult.SUCCESS, +}), "private to private: no preflight required."); + +promise_test(t => fetchTest(t, { + source: { server: Server.HTTP_PRIVATE }, + target: { + server: Server.HTTP_PUBLIC, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: FetchTestResult.SUCCESS, +}), "private to public: no preflight required."); + +promise_test(t => fetchTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { + server: Server.HTTP_LOCAL, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: FetchTestResult.FAILURE, +}), "public to local: failure."); + +promise_test(t => fetchTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { + server: Server.HTTP_PRIVATE, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: FetchTestResult.FAILURE, +}), "public to private: failure."); + +promise_test(t => fetchTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { server: Server.HTTP_PUBLIC }, + expected: FetchTestResult.SUCCESS, +}), "public to public: no preflight required."); + +// 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(t => fetchTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTP_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: FetchTestResult.FAILURE, +}), "treat-as-public-address to local: failure."); + +promise_test(t => fetchTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTP_PRIVATE, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: FetchTestResult.FAILURE, +}), "treat-as-public-address to private: failure."); + +promise_test(t => fetchTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTP_PUBLIC, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: FetchTestResult.SUCCESS, +}), "treat-as-public-address to public: no preflight required."); + +// These tests verify that HTTPS iframes embedded in an HTTP top-level document +// cannot fetch subresources from less-public address spaces. Indeed, even +// though the iframes have HTTPS origins, they are non-secure contexts because +// their parent is a non-secure context. + +promise_test(t => fetchTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: FetchTestResult.FAILURE, +}), "private https to local: failure."); + +promise_test(t => fetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: FetchTestResult.FAILURE, +}), "public https to local: failure."); + +promise_test(t => fetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: FetchTestResult.FAILURE, +}), "public https to private: failure."); diff --git a/test/wpt/tests/fetch/private-network-access/iframe.tentative.https.window.js b/test/wpt/tests/fetch/private-network-access/iframe.tentative.https.window.js new file mode 100644 index 00000000000..48bd6420fd9 --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/iframe.tentative.https.window.js @@ -0,0 +1,229 @@ +// META: script=/common/dispatcher/dispatcher.js +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests verify that contexts can navigate iframes to less-public address +// spaces iff the target server responds affirmatively to preflight requests. +// +// This file covers only those tests that must execute in a secure context. +// Other tests are defined in: iframe.tentative.window.js + +setup(() => { + assert_true(window.isSecureContext); +}); + +// Source: secure local context. +// +// All fetches unaffected by Private Network Access. + +promise_test_parallel(t => iframeTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { server: Server.HTTPS_LOCAL }, + expected: IframeTestResult.SUCCESS, +}), "local to local: no preflight required."); + +promise_test_parallel(t => iframeTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { server: Server.HTTPS_PRIVATE }, + expected: IframeTestResult.SUCCESS, +}), "local to private: no preflight required."); + +promise_test_parallel(t => iframeTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { server: Server.HTTPS_PUBLIC }, + expected: IframeTestResult.SUCCESS, +}), "local to public: no preflight required."); + +// 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 +// - success +// +function makePreflightTests({ + sourceName, + sourceServer, + sourceTreatAsPublic, + targetName, + targetServer, +}) { + const prefix = + `${sourceName} to ${targetName}: `; + + const source = { + server: sourceServer, + treatAsPublic: sourceTreatAsPublic, + }; + + promise_test_parallel(t => iframeTest(t, { + source, + target: { + server: targetServer, + behavior: { preflight: PreflightBehavior.failure() }, + }, + expected: IframeTestResult.FAILURE, + }), prefix + "failed preflight."); + + promise_test_parallel(t => iframeTest(t, { + source, + target: { + server: targetServer, + behavior: { preflight: PreflightBehavior.noCorsHeader(token()) }, + }, + expected: IframeTestResult.FAILURE, + }), prefix + "missing CORS headers."); + + promise_test_parallel(t => iframeTest(t, { + source, + target: { + server: targetServer, + behavior: { preflight: PreflightBehavior.noPnaHeader(token()) }, + }, + expected: IframeTestResult.FAILURE, + }), prefix + "missing PNA header."); + + promise_test_parallel(t => iframeTest(t, { + source, + target: { + server: targetServer, + behavior: { preflight: PreflightBehavior.success(token()) }, + }, + expected: IframeTestResult.SUCCESS, + }), prefix + "success."); +} + +// 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", +}); + +promise_test_parallel(t => iframeTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { server: Server.HTTPS_PRIVATE }, + expected: IframeTestResult.SUCCESS, +}), "private to private: no preflight required."); + +promise_test_parallel(t => iframeTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { server: Server.HTTPS_PUBLIC }, + expected: IframeTestResult.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({ + 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", +}); + +promise_test_parallel(t => iframeTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { server: Server.HTTPS_PUBLIC }, + expected: IframeTestResult.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({ + sourceServer: Server.HTTPS_LOCAL, + sourceTreatAsPublic: true, + sourceName: "treat-as-public-address", + targetServer: Server.HTTPS_LOCAL, + targetName: "local", +}); + +makePreflightTests({ + sourceServer: Server.HTTPS_LOCAL, + sourceTreatAsPublic: true, + sourceName: "treat-as-public-address", + targetServer: Server.HTTPS_PRIVATE, + 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."); + +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"); + +// 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.", + grandparentServer: Server.HTTPS_LOCAL, + child: { server: Server.HTTPS_PUBLIC }, + grandchild: { server: Server.HTTPS_LOCAL }, + expected: IframeTestResult.SUCCESS, +}); + +iframeGrandparentTest({ + name: "public to local, grandparent navigates: failure.", + grandparentServer: Server.HTTPS_PUBLIC, + child: { + server: Server.HTTPS_LOCAL, + behavior: { preflight: PreflightBehavior.success(token()) }, + }, + grandchild: { + server: Server.HTTPS_LOCAL, + behavior: { preflight: PreflightBehavior.failure() }, + }, + expected: IframeTestResult.FAILURE, +}); + +iframeGrandparentTest({ + name: "public to local, grandparent navigates: success.", + grandparentServer: Server.HTTPS_PUBLIC, + child: { + server: Server.HTTPS_LOCAL, + behavior: { preflight: PreflightBehavior.success(token()) }, + }, + grandchild: { + server: Server.HTTPS_LOCAL, + behavior: { preflight: PreflightBehavior.success(token()) }, + }, + expected: IframeTestResult.SUCCESS, +}); diff --git a/test/wpt/tests/fetch/private-network-access/iframe.tentative.window.js b/test/wpt/tests/fetch/private-network-access/iframe.tentative.window.js new file mode 100644 index 00000000000..e00cb202bec --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/iframe.tentative.window.js @@ -0,0 +1,110 @@ +// META: script=/common/dispatcher/dispatcher.js +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests verify that non-secure contexts cannot navigate iframes to +// less-public address spaces, and can navigate them otherwise. +// +// This file covers only those tests that must execute in a non secure context. +// Other tests are defined in: iframe.https.window.js + +setup(() => { + // Making sure we are in a non secure context, as expected. + assert_false(window.isSecureContext); +}); + +promise_test_parallel(t => iframeTest(t, { + source: { server: Server.HTTP_LOCAL }, + target: { server: Server.HTTP_LOCAL }, + expected: IframeTestResult.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, +}), "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, +}), "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, +}), "private to local: failure."); + +promise_test_parallel(t => iframeTest(t, { + source: { server: Server.HTTP_PRIVATE }, + target: { server: Server.HTTP_PRIVATE }, + expected: IframeTestResult.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, +}), "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, +}), "public to local: failure."); + +promise_test_parallel(t => iframeTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { server: Server.HTTP_PRIVATE }, + expected: IframeTestResult.FAILURE, +}), "public to private: failure."); + +promise_test_parallel(t => iframeTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { server: Server.HTTP_PUBLIC }, + expected: IframeTestResult.SUCCESS, +}), "public to public: no preflight required."); + +promise_test_parallel(t => iframeTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { server: Server.HTTP_LOCAL }, + expected: IframeTestResult.FAILURE, +}), "treat-as-public-address to local: failure."); + +promise_test_parallel(t => iframeTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { server: Server.HTTP_PRIVATE }, + expected: IframeTestResult.FAILURE, +}), "treat-as-public-address to private: failure."); + +promise_test_parallel(t => iframeTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { server: Server.HTTP_PUBLIC }, + expected: IframeTestResult.SUCCESS, +}), "treat-as-public-address to public: no preflight required."); + +// The following test verifies 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: success.", + grandparentServer: Server.HTTP_LOCAL, + child: { server: Server.HTTP_PUBLIC }, + grandchild: { server: Server.HTTP_LOCAL }, + expected: IframeTestResult.SUCCESS, +}); diff --git a/test/wpt/tests/fetch/private-network-access/mixed-content-fetch.tentative.https.window.js b/test/wpt/tests/fetch/private-network-access/mixed-content-fetch.tentative.https.window.js new file mode 100644 index 00000000000..54485dc7047 --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/mixed-content-fetch.tentative.https.window.js @@ -0,0 +1,277 @@ +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-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 +// they specify a valid `targetAddressSpace` fetch option that matches the +// target server's address space. + +setup(() => { + // Making sure we are in a secure context, as expected. + assert_true(window.isSecureContext); +}); + +// 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"]; + } +} + +// Generates tests of `targetAddressSpace` for the given (source, target) +// address space pair, expecting fetches to succeed iff `targetAddressSpace` is +// correct. +// +// Scenarios exercised: +// +// - cors mode: +// - missing targetAddressSpace option +// - incorrect targetAddressSpace option (x3, see `otherAddressSpaces()`) +// - failed preflight +// - success +// - success with PUT method (non-"simple" request) +// - no-cors mode: +// - success +// +function makeTests({ source, target }) { + const sourceServer = Server.get("https", source); + const targetServer = Server.get("http", target); + + const makeTest = ({ + fetchOptions, + targetBehavior, + name, + expected + }) => { + promise_test_parallel(t => fetchTest(t, { + source: { server: sourceServer }, + target: { + server: targetServer, + behavior: targetBehavior, + }, + fetchOptions, + expected, + }), `${sourceServer.name} to ${targetServer.name}: ${name}.`); + }; + + makeTest({ + name: "missing targetAddressSpace", + targetBehavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + expected: FetchTestResult.FAILURE, + }); + + const correctAddressSpace = targetServer.addressSpace; + + for (const targetAddressSpace of otherAddressSpaces(correctAddressSpace)) { + makeTest({ + name: `wrong targetAddressSpace "${targetAddressSpace}"`, + targetBehavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + fetchOptions: { targetAddressSpace }, + expected: FetchTestResult.FAILURE, + }); + } + + makeTest({ + name: "failed preflight", + targetBehavior: { + preflight: PreflightBehavior.failure(), + response: ResponseBehavior.allowCrossOrigin(), + }, + fetchOptions: { targetAddressSpace: correctAddressSpace }, + expected: FetchTestResult.FAILURE, + }); + + makeTest({ + name: "success", + targetBehavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + fetchOptions: { targetAddressSpace: correctAddressSpace }, + expected: FetchTestResult.SUCCESS, + }); + + makeTest({ + name: "PUT success", + targetBehavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + fetchOptions: { + targetAddressSpace: correctAddressSpace, + method: "PUT", + }, + expected: FetchTestResult.SUCCESS, + }); + + makeTest({ + name: "no-cors success", + targetBehavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + fetchOptions: { + targetAddressSpace: correctAddressSpace, + mode: "no-cors", + }, + expected: FetchTestResult.OPAQUE, + }); +} + +// Generates tests for the given (source, target) address space pair expecting +// that `targetAddressSpace` cannot be used to bypass mixed content. +// +// Scenarios exercised: +// +// - wrong `targetAddressSpace` (x3, see `otherAddressSpaces()`) +// - correct `targetAddressSpace` +// +function makeNoBypassTests({ source, target }) { + const sourceServer = Server.get("https", source); + const targetServer = Server.get("http", target); + + const prefix = `${sourceServer.name} to ${targetServer.name}: `; + + const correctAddressSpace = targetServer.addressSpace; + for (const targetAddressSpace of otherAddressSpaces(correctAddressSpace)) { + promise_test_parallel(t => fetchTest(t, { + source: { server: sourceServer }, + target: { + server: targetServer, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + fetchOptions: { targetAddressSpace }, + expected: FetchTestResult.FAILURE, + }), prefix + `wrong targetAddressSpace "${targetAddressSpace}".`); + } + + promise_test_parallel(t => fetchTest(t, { + source: { server: sourceServer }, + target: { + server: targetServer, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + fetchOptions: { targetAddressSpace: correctAddressSpace }, + expected: FetchTestResult.FAILURE, + }), prefix + 'not a private network request.'); +} + +// Source: local secure context. +// +// Fetches to the local and private address spaces cannot use +// `targetAddressSpace` to bypass mixed content, as they are not otherwise +// blocked by Private Network Access. + +makeNoBypassTests({ source: "local", target: "local" }); +makeNoBypassTests({ source: "local", target: "private" }); +makeNoBypassTests({ source: "local", target: "public" }); + +// Source: private secure context. +// +// 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 private address space cannot use `targetAddressSpace` to +// bypass mixed content, as they are not otherwise blocked by Private Network +// Access. + +makeTests({ source: "private", target: "local" }); + +makeNoBypassTests({ source: "private", target: "private" }); +makeNoBypassTests({ source: "private", target: "public" }); + +// Source: public secure context. +// +// 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: "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. + +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.SUCCESS, +}), "https-treat-as-public to http-local: success."); + +promise_test_parallel(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTP_PRIVATE, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + fetchOptions: { targetAddressSpace: "local" }, + expected: FetchTestResult.FAILURE, +}), 'https-treat-as-public to http-private: wrong targetAddressSpace "local".'); + +promise_test_parallel(t => fetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTP_PRIVATE, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + fetchOptions: { targetAddressSpace: "private" }, + expected: FetchTestResult.SUCCESS, +}), "https-treat-as-public to http-private: success."); diff --git a/test/wpt/tests/fetch/private-network-access/nested-worker.https.window.js b/test/wpt/tests/fetch/private-network-access/nested-worker.https.window.js new file mode 100644 index 00000000000..3eeb435badb --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/nested-worker.https.window.js @@ -0,0 +1,36 @@ +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests check that initial `Worker` script fetches from within worker +// scopes are subject to Private Network Access checks, just like a worker +// script fetches from within document scopes (for non-nested workers). The +// latter are tested in: worker.https.window.js +// +// This file covers only those tests that must execute in a secure context. +// Other tests are defined in: nested-worker.window.js + +promise_test(t => nestedWorkerScriptTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { server: Server.HTTPS_LOCAL }, + expected: WorkerScriptTestResult.FAILURE, +}), "treat-as-public to local: failure."); + +promise_test(t => nestedWorkerScriptTest(t, { + source: { + server: Server.HTTPS_PRIVATE, + treatAsPublic: true, + }, + target: { server: Server.HTTPS_PRIVATE }, + expected: WorkerScriptTestResult.FAILURE, +}), "treat-as-public to private: failure."); + +promise_test(t => nestedWorkerScriptTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { server: Server.HTTPS_PUBLIC }, + expected: WorkerScriptTestResult.SUCCESS, +}), "public to public: success."); diff --git a/test/wpt/tests/fetch/private-network-access/nested-worker.window.js b/test/wpt/tests/fetch/private-network-access/nested-worker.window.js new file mode 100644 index 00000000000..6d246e1c76d --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/nested-worker.window.js @@ -0,0 +1,36 @@ +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests check that initial `Worker` script fetches from within worker +// scopes are subject to Private Network Access checks, just like a worker +// script fetches from within document scopes (for non-nested workers). The +// latter are tested in: worker.window.js +// +// This file covers only those tests that must execute in a non secure context. +// Other tests are defined in: nested-worker.https.window.js + +promise_test(t => nestedWorkerScriptTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { server: Server.HTTP_LOCAL }, + expected: WorkerScriptTestResult.FAILURE, +}), "treat-as-public to local: failure."); + +promise_test(t => nestedWorkerScriptTest(t, { + source: { + server: Server.HTTP_PRIVATE, + treatAsPublic: true, + }, + target: { server: Server.HTTP_PRIVATE }, + expected: WorkerScriptTestResult.FAILURE, +}), "treat-as-public to private: failure."); + +promise_test(t => nestedWorkerScriptTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { server: Server.HTTP_PUBLIC }, + expected: WorkerScriptTestResult.SUCCESS, +}), "public to public: success."); diff --git a/test/wpt/tests/fetch/private-network-access/preflight-cache.https.window.js b/test/wpt/tests/fetch/private-network-access/preflight-cache.https.window.js new file mode 100644 index 00000000000..87dbf501f62 --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/preflight-cache.https.window.js @@ -0,0 +1,88 @@ +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#cors-preflight +// +// These tests verify that PNA preflight responses are cached. +// +// TODO(https://crbug.com/1268312): We cannot currently test that cache +// entries are keyed by target IP address space because that requires +// loading the same URL from different IP address spaces, and the WPT +// framework does not allow that. +promise_test(async t => { + let uuid = token(); + await fetchTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.singlePreflight(uuid), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: FetchTestResult.SUCCESS, + }); + await fetchTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.singlePreflight(uuid), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: FetchTestResult.SUCCESS, + }); +}, "private to local: success."); + +promise_test(async t => { + let uuid = token(); + await fetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.singlePreflight(uuid), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: FetchTestResult.SUCCESS, + }); + await fetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.singlePreflight(uuid), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: FetchTestResult.SUCCESS, + }); +}, "public to local: success."); + +promise_test(async t => { + let uuid = token(); + await fetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.singlePreflight(uuid), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: FetchTestResult.SUCCESS, + }); + await fetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.singlePreflight(uuid), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: FetchTestResult.SUCCESS, + }); +}, "public to private: success."); \ No newline at end of file diff --git a/test/wpt/tests/fetch/private-network-access/redirect.https.window.js b/test/wpt/tests/fetch/private-network-access/redirect.https.window.js new file mode 100644 index 00000000000..fe004d929de --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/redirect.https.window.js @@ -0,0 +1,232 @@ +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// This test verifies that Private Network Access checks are applied to all +// the endpoints in a redirect chain, relative to the same client context. + +// local -> private -> public +// +// Request 1 (local -> private): no preflight. +// Request 2 (local -> public): no preflight. + +promise_test(t => fetchTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + response: ResponseBehavior.allowCrossOrigin(), + redirect: preflightUrl({ + server: Server.HTTPS_PUBLIC, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }), + } + }, + expected: FetchTestResult.SUCCESS, +}), "local to private to public: success."); + +// local -> private -> local +// +// Request 1 (local -> private): no preflight. +// Request 2 (local -> local): no preflight. +// +// This checks that the client for the second request is still the initial +// context, not the redirector. + +promise_test(t => fetchTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + response: ResponseBehavior.allowCrossOrigin(), + redirect: preflightUrl({ + server: Server.HTTPS_LOCAL, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }), + } + }, + expected: FetchTestResult.SUCCESS, +}), "local to private to local: success."); + +// private -> private -> local +// +// Request 1 (private -> private): no preflight. +// Request 2 (private -> local): preflight required. +// +// This verifies that PNA checks are applied after redirects. + +promise_test(t => fetchTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + redirect: preflightUrl({ + server: Server.HTTPS_LOCAL, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }), + } + }, + expected: FetchTestResult.FAILURE, +}), "private to private to local: failed preflight."); + +promise_test(t => fetchTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + redirect: preflightUrl({ + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }), + } + }, + expected: FetchTestResult.SUCCESS, +}), "private to private to local: success."); + +promise_test(t => fetchTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + redirect: preflightUrl({ + server: Server.HTTPS_LOCAL, + behavior: { preflight: PreflightBehavior.success(token()) }, + }), + } + }, + fetchOptions: { mode: "no-cors" }, + expected: FetchTestResult.OPAQUE, +}), "private to private to local: no-cors success."); + +// private -> local -> private +// +// Request 1 (private -> local): preflight required. +// Request 2 (private -> private): no preflight. +// +// This verifies that PNA checks are applied independently to every step in a +// redirect chain. + +promise_test(t => fetchTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + response: ResponseBehavior.allowCrossOrigin(), + redirect: preflightUrl({ + server: Server.HTTPS_PRIVATE, + }), + } + }, + expected: FetchTestResult.FAILURE, +}), "private to local to private: failed preflight."); + +promise_test(t => fetchTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + redirect: preflightUrl({ + server: Server.HTTPS_PRIVATE, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }), + } + }, + expected: FetchTestResult.SUCCESS, +}), "private to local to private: success."); + +promise_test(t => fetchTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + redirect: preflightUrl({ server: Server.HTTPS_PRIVATE }), + } + }, + fetchOptions: { mode: "no-cors" }, + expected: FetchTestResult.OPAQUE, +}), "private to local to private: no-cors success."); + +// public -> private -> local +// +// Request 1 (public -> private): preflight required. +// Request 2 (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_PUBLIC }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + response: ResponseBehavior.allowCrossOrigin(), + redirect: preflightUrl({ + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }), + } + }, + expected: FetchTestResult.FAILURE, +}), "public to private to local: failed first preflight."); + +promise_test(t => fetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + 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, +}), "public to private to local: failed second preflight."); + +promise_test(t => fetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + 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, +}), "public to private to local: success."); + +promise_test(t => fetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + 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, +}), "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/private-network-access/resources/executor.html new file mode 100644 index 00000000000..d71212951cb --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/resources/executor.html @@ -0,0 +1,9 @@ + + +Executor + + + diff --git a/test/wpt/tests/fetch/private-network-access/resources/fetcher.html b/test/wpt/tests/fetch/private-network-access/resources/fetcher.html new file mode 100644 index 00000000000..000a5cc25bb --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/resources/fetcher.html @@ -0,0 +1,21 @@ + + +Fetcher + diff --git a/test/wpt/tests/fetch/private-network-access/resources/fetcher.js b/test/wpt/tests/fetch/private-network-access/resources/fetcher.js new file mode 100644 index 00000000000..3a1859876d4 --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/resources/fetcher.js @@ -0,0 +1,20 @@ +async function doFetch(url) { + const response = await fetch(url); + const body = await response.text(); + return { + status: response.status, + body, + }; +} + +async function fetchAndPost(url) { + try { + const message = await doFetch(url); + self.postMessage(message); + } catch(e) { + self.postMessage({ error: e.name }); + } +} + +const url = new URL(self.location.href).searchParams.get("url"); +fetchAndPost(url); diff --git a/test/wpt/tests/fetch/private-network-access/resources/iframed.html b/test/wpt/tests/fetch/private-network-access/resources/iframed.html new file mode 100644 index 00000000000..c889c2882a4 --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/resources/iframed.html @@ -0,0 +1,7 @@ + + +Iframed + diff --git a/test/wpt/tests/fetch/private-network-access/resources/iframer.html b/test/wpt/tests/fetch/private-network-access/resources/iframer.html new file mode 100644 index 00000000000..304cc54ae44 --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/resources/iframer.html @@ -0,0 +1,9 @@ + + +Iframer + + diff --git a/test/wpt/tests/fetch/private-network-access/resources/preflight.py b/test/wpt/tests/fetch/private-network-access/resources/preflight.py new file mode 100644 index 00000000000..4aefee6f40d --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/resources/preflight.py @@ -0,0 +1,169 @@ +# This endpoint responds to both preflight requests and the subsequent requests. +# +# Its behavior can be configured with various search/GET parameters, all of +# which are optional: +# +# - treat-as-public-once: Must be a valid UUID if set. +# If set, then this endpoint expects to receive a non-preflight request first, +# for which it sets the `Content-Security-Policy: treat-as-public-address` +# response header. This allows testing "DNS rebinding", where a URL first +# resolves to the public IP address space, then a non-public IP address space. +# - preflight-uuid: Must be a valid UUID if set, distinct from the value of the +# `treat-as-public-once` parameter if both are set. +# If set, then this endpoint expects to receive a preflight request first +# followed by a regular request, as in the regular CORS protocol. If the +# `treat-as-public-once` header is also set, it takes precedence: this +# endpoint expects to receive a non-preflight request first, then a preflight +# request, then finally a regular request. +# If unset, then this endpoint expects to receive no preflight request, only +# a regular (non-OPTIONS) request. +# - preflight-headers: Valid values are: +# - cors: this endpoint responds with valid CORS headers to preflights. These +# should be sufficient for non-PNA preflight requests to succeed, but not +# for PNA-specific preflight requests. +# - cors+pna: this endpoint responds with valid CORS and PNA headers to +# preflights. These should be sufficient for both non-PNA preflight +# requests and PNA-specific preflight requests to succeed. +# - cors+pna+sw: this endpoint responds with valid CORS and PNA headers and +# "Access-Control-Allow-Headers: Service-Worker" to preflights. These should +# be sufficient for both non-PNA preflight requests and PNA-specific +# preflight requests to succeed. This allows the main request to fetch a +# service worker script. +# - unspecified, or any other value: this endpoint responds with no CORS or +# PNA headers. Preflight requests should fail. +# - final-headers: Valid values are: +# - cors: this endpoint responds with valid CORS headers to CORS-enabled +# non-preflight requests. These should be sufficient for non-preflighted +# CORS-enabled requests to succeed. +# - unspecified: this endpoint responds with no CORS headers to non-preflight +# requests. This should fail CORS-enabled requests, but be sufficient for +# no-CORS requests. +# +# The following parameters only affect non-preflight responses: +# +# - redirect: If set, the response code is set to 301 and the `Location` +# response header is set to this value. +# - mime-type: If set, the `Content-Type` response header is set to this value. +# - file: Specifies a path (relative to this file's directory) to a file. If +# set, the response body is copied from this file. +# - random-js-prefix: If set to any value, the response body is prefixed with +# a Javascript comment line containing a random value. This is useful in +# service worker tests, since service workers are only updated if the new +# script is not byte-for-byte identical with the old script. +# - body: If set and `file` is not, the response body is set to this value. +# + +import os +import random + +from wptserve.utils import isomorphic_encode + +_ACAO = ("Access-Control-Allow-Origin", "*") +_ACAPN = ("Access-Control-Allow-Private-Network", "true") +_ACAH = ("Access-Control-Allow-Headers", "Service-Worker") + +def _get_response_headers(method, mode): + acam = ("Access-Control-Allow-Methods", method) + + if mode == b"cors": + return [acam, _ACAO] + + if mode == b"cors+pna": + return [acam, _ACAO, _ACAPN] + + if mode == b"cors+pna+sw": + return [acam, _ACAO, _ACAPN, _ACAH] + + return [] + +def _get_expect_single_preflight(request): + return request.GET.get(b"expect-single-preflight") + +def _is_preflight_optional(request): + return request.GET.get(b"is-preflight-optional") + +def _get_preflight_uuid(request): + return request.GET.get(b"preflight-uuid") + +def _should_treat_as_public_once(request): + uuid = request.GET.get(b"treat-as-public-once") + if uuid is None: + # If the search parameter is not given, never treat as public. + return False + + # If the parameter is given, we treat the request as public only if the UUID + # has never been seen and stashed. + result = request.server.stash.take(uuid) is None + request.server.stash.put(uuid, "") + return result + +def _handle_preflight_request(request, response): + if _should_treat_as_public_once(request): + return (400, [], "received preflight for first treat-as-public request") + + uuid = _get_preflight_uuid(request) + if uuid is None: + return (400, [], "missing `preflight-uuid` param from preflight URL") + + value = request.server.stash.take(uuid) + request.server.stash.put(uuid, "preflight") + if _get_expect_single_preflight(request) and value is not None: + return (400, [], "received duplicated preflight") + + method = request.headers.get("Access-Control-Request-Method") + mode = request.GET.get(b"preflight-headers") + headers = _get_response_headers(method, mode) + + return (headers, "preflight") + +def _final_response_body(request): + file_name = request.GET.get(b"file") + if file_name is None: + return request.GET.get(b"body") or "success" + + prefix = b"" + if request.GET.get(b"random-js-prefix"): + value = random.randint(0, 1000000000) + prefix = isomorphic_encode("// Random value: {}\n\n".format(value)) + + path = os.path.join(os.path.dirname(isomorphic_encode(__file__)), file_name) + with open(path, 'rb') as f: + contents = f.read() + + return prefix + contents + +def _handle_final_request(request, response): + if _should_treat_as_public_once(request): + headers = [("Content-Security-Policy", "treat-as-public-address"),] + else: + uuid = _get_preflight_uuid(request) + 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)) + request.server.stash.put(uuid, "final") + + mode = request.GET.get(b"final-headers") + headers = _get_response_headers(request.method, mode) + + redirect = request.GET.get(b"redirect") + if redirect is not None: + headers.append(("Location", redirect)) + return (301, headers, b"") + + mime_type = request.GET.get(b"mime-type") + if mime_type is not None: + headers.append(("Content-Type", mime_type),) + + body = _final_response_body(request) + return (headers, body) + +def main(request, response): + try: + if request.method == "OPTIONS": + return _handle_preflight_request(request, response) + else: + return _handle_final_request(request, response) + except BaseException as e: + # Surface exceptions to the client, where they show up as assertion errors. + return (500, [("X-exception", str(e))], "exception: {}".format(e)) diff --git a/test/wpt/tests/fetch/private-network-access/resources/service-worker-bridge.html b/test/wpt/tests/fetch/private-network-access/resources/service-worker-bridge.html new file mode 100644 index 00000000000..816de535fea --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/resources/service-worker-bridge.html @@ -0,0 +1,155 @@ + + +ServiceWorker Bridge + + + diff --git a/test/wpt/tests/fetch/private-network-access/resources/service-worker.js b/test/wpt/tests/fetch/private-network-access/resources/service-worker.js new file mode 100644 index 00000000000..bca71ad910c --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/resources/service-worker.js @@ -0,0 +1,18 @@ +self.addEventListener("install", () => { + // Skip waiting before replacing the previously-active service worker, if any. + // This allows the bridge script to notice the controller change and query + // the install time via fetch. + self.skipWaiting(); +}); + +self.addEventListener("activate", (event) => { + // Claim all clients so that the bridge script notices the activation. + event.waitUntil(self.clients.claim()); +}); + +self.addEventListener("fetch", (event) => { + const url = new URL(event.request.url).searchParams.get("proxied-url"); + if (url) { + event.respondWith(fetch(url)); + } +}); diff --git a/test/wpt/tests/fetch/private-network-access/resources/shared-fetcher.js b/test/wpt/tests/fetch/private-network-access/resources/shared-fetcher.js new file mode 100644 index 00000000000..30bde1e0542 --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/resources/shared-fetcher.js @@ -0,0 +1,23 @@ +async function doFetch(url) { + const response = await fetch(url); + const body = await response.text(); + return { + status: response.status, + body, + }; +} + +async function fetchAndPost(url, port) { + try { + const message = await doFetch(url); + port.postMessage(message); + } catch(e) { + port.postMessage({ error: e.name }); + } +} + +const url = new URL(self.location.href).searchParams.get("url"); + +self.addEventListener("connect", async (evt) => { + await fetchAndPost(url, evt.ports[0]); +}); diff --git a/test/wpt/tests/fetch/private-network-access/resources/shared-worker-fetcher.html b/test/wpt/tests/fetch/private-network-access/resources/shared-worker-fetcher.html new file mode 100644 index 00000000000..4af4b1f2395 --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/resources/shared-worker-fetcher.html @@ -0,0 +1,19 @@ + + +SharedWorker Fetcher + diff --git a/test/wpt/tests/fetch/private-network-access/resources/socket-opener.html b/test/wpt/tests/fetch/private-network-access/resources/socket-opener.html new file mode 100644 index 00000000000..48d27216bed --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/resources/socket-opener.html @@ -0,0 +1,15 @@ + + +WebSocket Opener + diff --git a/test/wpt/tests/fetch/private-network-access/resources/support.sub.js b/test/wpt/tests/fetch/private-network-access/resources/support.sub.js new file mode 100644 index 00000000000..a104021d158 --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/resources/support.sub.js @@ -0,0 +1,673 @@ +// Creates a new iframe in `doc`, calls `func` on it and appends it as a child +// of `doc`. +// Returns a promise that resolves to the iframe once loaded (successfully or +// not). +// The iframe is removed from `doc` once test `t` is done running. +// +// NOTE: There exists no interoperable way to check whether an iframe failed to +// load, so this should only be used when the iframe is expected to load. It +// also means we cannot wire the iframe's `error` event to a promise +// rejection. See: https://github.com/whatwg/html/issues/125 +function appendIframeWith(t, doc, func) { + return new Promise(resolve => { + const child = doc.createElement("iframe"); + t.add_cleanup(() => child.remove()); + + child.addEventListener("load", () => resolve(child), { once: true }); + func(child); + doc.body.appendChild(child); + }); +} + +// Appends a child iframe to `doc` sourced from `src`. +// +// See `appendIframeWith()` for more details. +function appendIframe(t, doc, src) { + return appendIframeWith(t, doc, child => { child.src = src; }); +} + +// Registers an event listener that will resolve this promise when this +// window receives a message posted to it. +// +// `options` has the following shape: +// +// { +// source: If specified, this function waits for the first message from the +// given source only, ignoring other messages. +// +// filter: If specified, this function calls `filter` on each incoming +// message, and resolves iff it returns true. +// } +// +function futureMessage(options) { + return new Promise(resolve => { + window.addEventListener("message", (e) => { + if (options?.source && options.source !== e.source) { + return; + } + + if (options?.filter && !options.filter(e.data)) { + return; + } + + resolve(e.data); + }); + }); +}; + +// Like `promise_test()`, but executes tests in parallel like `async_test()`. +// +// Cribbed from COEP tests. +function promise_test_parallel(promise, description) { + async_test(test => { + promise(test) + .then(() => test.done()) + .catch(test.step_func(error => { throw error; })); + }, description); +}; + +async function postMessageAndAwaitReply(target, message) { + const reply = futureMessage({ source: target }); + target.postMessage(message, "*"); + return await reply; +} + +// Maps protocol (without the trailing colon) and address space to port. +const SERVER_PORTS = { + "http": { + "local": {{ports[http][0]}}, + "private": {{ports[http-private][0]}}, + "public": {{ports[http-public][0]}}, + }, + "https": { + "local": {{ports[https][0]}}, + "private": {{ports[https-private][0]}}, + "public": {{ports[https-public][0]}}, + }, + "ws": { + "local": {{ports[ws][0]}}, + }, + "wss": { + "local": {{ports[wss][0]}}, + }, +}; + +// A `Server` is a web server accessible by tests. It has the following shape: +// +// { +// addressSpace: the IP address space of the server ("local", "private" or +// "public"), +// name: a human-readable name for the server, +// port: the port on which the server listens for connections, +// protocol: the protocol (including trailing colon) spoken by the server, +// } +// +// Constants below define the available servers, which can also be accessed +// programmatically with `get()`. +class Server { + // Maps the given `protocol` (without a trailing colon) and `addressSpace` to + // a server. Returns null if no such server exists. + static get(protocol, addressSpace) { + const ports = SERVER_PORTS[protocol]; + if (ports === undefined) { + return null; + } + + const port = ports[addressSpace]; + if (port === undefined) { + return null; + } + + return { + addressSpace, + name: `${protocol}-${addressSpace}`, + port, + protocol: protocol + ':', + }; + } + + 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", "local"); + static HTTPS_PRIVATE = Server.get("https", "private"); + static HTTPS_PUBLIC = Server.get("https", "public"); + 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. +// +// `url` specifies the relative URL, e.g. "foo.html" or "http://foo.example". +// `options`, if defined, should have the following shape: +// +// { +// // Optional. Overrides the protocol of the returned URL. +// protocol, +// +// // Optional. Overrides the port of the returned URL. +// port, +// +// // Extra headers. +// headers, +// +// // Extra search params. +// searchParams, +// } +// +function resolveUrl(url, options) { + const result = new URL(url, window.location); + if (options === undefined) { + return result; + } + + const { port, protocol, headers, searchParams } = options; + if (port !== undefined) { + result.port = port; + } + if (protocol !== undefined) { + result.protocol = protocol; + } + if (headers !== undefined) { + const pipes = []; + for (key in headers) { + pipes.push(`header(${key},${headers[key]})`); + } + result.searchParams.append("pipe", pipes.join("|")); + } + if (searchParams !== undefined) { + for (key in searchParams) { + result.searchParams.append(key, searchParams[key]); + } + } + + return result; +} + +// Computes options to pass to `resolveUrl()` for a source document's URL. +// +// `server` identifies the server from which to load the document. +// `treatAsPublic`, if set to true, specifies that the source document should +// be artificially placed in the `public` address space using CSP. +function sourceResolveOptions({ server, treatAsPublic }) { + const options = {...server}; + if (treatAsPublic) { + options.headers = { "Content-Security-Policy": "treat-as-public-address" }; + } + return options; +} + +// Computes the URL of a preflight handler configured with the given options. +// +// `server` identifies the server from which to load the resource. +// `behavior` specifies the behavior of the target server. It may contain: +// - `preflight`: The result of calling one of `PreflightBehavior`'s methods. +// - `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 }) { + const options = {...server}; + if (behavior) { + const { preflight, response, redirect } = behavior; + options.searchParams = { + ...preflight, + ...response, + }; + if (redirect !== undefined) { + options.searchParams.redirect = redirect; + } + } + + return resolveUrl("resources/preflight.py", options); +} + +// Methods generate behavior specifications for how `resources/preflight.py` +// should behave upon receiving a preflight request. +const PreflightBehavior = { + // The preflight response should fail with a non-2xx code. + failure: () => ({}), + + // The preflight response should be missing CORS headers. + // `uuid` should be a UUID that uniquely identifies the preflight request. + noCorsHeader: (uuid) => ({ + "preflight-uuid": uuid, + }), + + // The preflight response should be missing PNA headers. + // `uuid` should be a UUID that uniquely identifies the preflight request. + noPnaHeader: (uuid) => ({ + "preflight-uuid": uuid, + "preflight-headers": "cors", + }), + + // The preflight response should succeed. + // `uuid` should be a UUID that uniquely identifies the preflight request. + success: (uuid) => ({ + "preflight-uuid": uuid, + "preflight-headers": "cors+pna", + }), + + optionalSuccess: (uuid) => ({ + "preflight-uuid": uuid, + "preflight-headers": "cors+pna", + "is-preflight-optional": true, + }), + + // The preflight response should succeed and allow service-worker header. + // `uuid` should be a UUID that uniquely identifies the preflight request. + serviceWorkerSuccess: (uuid) => ({ + "preflight-uuid": uuid, + "preflight-headers": "cors+pna+sw", + }), + + // The preflight response should succeed only if it is the first preflight. + // `uuid` should be a UUID that uniquely identifies the preflight request. + singlePreflight: (uuid) => ({ + "preflight-uuid": uuid, + "preflight-headers": "cors+pna", + "expect-single-preflight": true, + }), +}; + +// Methods generate behavior specifications for how `resources/preflight.py` +// should behave upon receiving a regular (non-preflight) request. +const ResponseBehavior = { + // The response should succeed without CORS headers. + default: () => ({}), + + // The response should succeed with CORS headers. + allowCrossOrigin: () => ({ "final-headers": "cors" }), +}; + +const FetchTestResult = { + SUCCESS: { + ok: true, + body: "success", + }, + OPAQUE: { + ok: false, + type: "opaque", + body: "", + }, + FAILURE: { + error: "TypeError: Failed to fetch", + }, +}; + +// Runs a fetch test. Tries to fetch a given subresource from a given document. +// +// Main argument shape: +// +// { +// // Optional. Passed to `sourceResolveOptions()`. +// source, +// +// // Optional. Passed to `preflightUrl()`. +// target, +// +// // Optional. Passed to `fetch()`. +// fetchOptions, +// +// // Required. One of the values in `FetchTestResult`. +// expected, +// } +// +async function fetchTest(t, { source, target, fetchOptions, expected }) { + const sourceUrl = + resolveUrl("resources/fetcher.html", sourceResolveOptions(source)); + + const targetUrl = preflightUrl(target); + + const iframe = await appendIframe(t, document, sourceUrl); + const reply = futureMessage({ source: iframe.contentWindow }); + + const message = { + url: targetUrl.href, + options: fetchOptions, + }; + iframe.contentWindow.postMessage(message, "*"); + + const { error, ok, type, body } = await reply; + + assert_equals(error, expected.error, "error"); + + assert_equals(ok, expected.ok, "response ok"); + assert_equals(body, expected.body, "response body"); + + if (expected.type !== undefined) { + assert_equals(type, expected.type, "response type"); + } +} + +const XhrTestResult = { + SUCCESS: { + loaded: true, + status: 200, + body: "success", + }, + FAILURE: { + loaded: false, + status: 0, + }, +}; + +// Runs an XHR test. Tries to fetch a given subresource from a given document. +// +// Main argument shape: +// +// { +// // Optional. Passed to `sourceResolveOptions()`. +// source, +// +// // Optional. Passed to `preflightUrl()`. +// target, +// +// // Optional. Method to use when sending the request. Defaults to "GET". +// method, +// +// // Required. One of the values in `XhrTestResult`. +// expected, +// } +// +async function xhrTest(t, { source, target, method, expected }) { + const sourceUrl = + resolveUrl("resources/xhr-sender.html", sourceResolveOptions(source)); + + const targetUrl = preflightUrl(target); + + const iframe = await appendIframe(t, document, sourceUrl); + const reply = futureMessage(); + + const message = { + url: targetUrl.href, + method: method, + }; + iframe.contentWindow.postMessage(message, "*"); + + const { loaded, status, body } = await reply; + + assert_equals(loaded, expected.loaded, "response loaded"); + assert_equals(status, expected.status, "response status"); + assert_equals(body, expected.body, "response body"); +} + +const IframeTestResult = { + SUCCESS: "loaded", + FAILURE: "timeout", +}; + +async function iframeTest(t, { source, target, expected }) { + // Allows running tests in parallel. + const uuid = token(); + + const targetUrl = preflightUrl(target); + targetUrl.searchParams.set("file", "iframed.html"); + targetUrl.searchParams.set("iframe-uuid", uuid); + + const sourceUrl = + resolveUrl("resources/iframer.html", sourceResolveOptions(source)); + sourceUrl.searchParams.set("url", targetUrl); + + const messagePromise = futureMessage({ + filter: (data) => data.uuid === uuid, + }); + const iframe = await appendIframe(t, document, sourceUrl); + + // The grandchild frame posts a message iff it loads successfully. + // There exists no interoperable way to check whether an iframe failed to + // load, so we use a timeout. + // See: https://github.com/whatwg/html/issues/125 + const result = await Promise.race([ + messagePromise.then((data) => data.message), + new Promise((resolve) => { + t.step_timeout(() => resolve("timeout"), 500 /* ms */); + }), + ]); + + assert_equals(result, expected); +} + +const iframeGrandparentTest = ({ + name, + grandparentServer, + child, + grandchild, + expected, +}) => promise_test_parallel(async (t) => { + // Allows running tests in parallel. + const grandparentUuid = token(); + const childUuid = token(); + const grandchildUuid = token(); + + const grandparentUrl = + resolveUrl("resources/executor.html", grandparentServer); + grandparentUrl.searchParams.set("executor-uuid", grandparentUuid); + + const childUrl = preflightUrl(child); + childUrl.searchParams.set("file", "executor.html"); + childUrl.searchParams.set("executor-uuid", childUuid); + + const grandchildUrl = preflightUrl(grandchild); + grandchildUrl.searchParams.set("file", "iframed.html"); + grandchildUrl.searchParams.set("iframe-uuid", grandchildUuid); + + const iframe = await appendIframe(t, document, grandparentUrl); + + const addChild = (url) => new Promise((resolve) => { + const child = document.createElement("iframe"); + child.src = url; + child.addEventListener("load", () => resolve(), { once: true }); + document.body.appendChild(child); + }); + + const grandparentCtx = new RemoteContext(grandparentUuid); + await grandparentCtx.execute_script(addChild, [childUrl]); + + // Add a blank grandchild frame inside the child. + // Apply a timeout to this step so that failures at this step do not block the + // execution of other tests. + const childCtx = new RemoteContext(childUuid); + await Promise.race([ + childCtx.execute_script(addChild, ["about:blank"]), + new Promise((resolve, reject) => t.step_timeout( + () => reject("timeout adding grandchild"), + 2000 /* ms */ + )), + ]); + + const messagePromise = futureMessage({ + filter: (data) => data.uuid === grandchildUuid, + }); + await grandparentCtx.execute_script((url) => { + const child = window.frames[0]; + const grandchild = child.frames[0]; + grandchild.location = url; + }, [grandchildUrl]); + + // The great-grandchild frame posts a message iff it loads successfully. + // There exists no interoperable way to check whether an iframe failed to + // load, so we use a timeout. + // See: https://github.com/whatwg/html/issues/125 + const result = await Promise.race([ + messagePromise.then((data) => data.message), + new Promise((resolve) => { + t.step_timeout(() => resolve("timeout"), 2000 /* ms */); + }), + ]); + + assert_equals(result, expected); +}, name); + +const WebsocketTestResult = { + SUCCESS: "open", + + // The code is a best guess. It is not yet entirely specified, so it may need + // to be changed in the future based on implementation experience. + FAILURE: "close: code 1006", +}; + +// Runs a websocket test. Attempts to open a websocket from `source` (in an +// iframe) to `target`, then checks that the result is as `expected`. +// +// Argument shape: +// +// { +// // Required. Passed to `sourceResolveOptions()`. +// source, +// +// // Required. +// target: { +// // Required. Target server. +// server, +// } +// +// // Required. Should be one of the values in `WebsocketTestResult`. +// expected, +// } +// +async function websocketTest(t, { source, target, expected }) { + const sourceUrl = + resolveUrl("resources/socket-opener.html", sourceResolveOptions(source)); + + const targetUrl = resolveUrl("/echo", target.server); + + const iframe = await appendIframe(t, document, sourceUrl); + + const reply = futureMessage(); + iframe.contentWindow.postMessage(targetUrl.href, "*"); + + assert_equals(await reply, expected); +} + +const WorkerScriptTestResult = { + SUCCESS: { loaded: true }, + FAILURE: { error: "unknown error" }, +}; + +function workerScriptUrl(target) { + const url = preflightUrl(target); + + url.searchParams.append("body", "postMessage({ loaded: true })") + url.searchParams.append("mime-type", "application/javascript") + + return url; +} + +async function workerScriptTest(t, { source, target, expected }) { + const sourceUrl = + resolveUrl("resources/worker-fetcher.html", sourceResolveOptions(source)); + + const targetUrl = workerScriptUrl(target); + + const iframe = await appendIframe(t, document, sourceUrl); + const reply = futureMessage(); + + iframe.contentWindow.postMessage({ url: targetUrl.href }, "*"); + + const { error, loaded } = await reply; + + assert_equals(error, expected.error, "worker error"); + assert_equals(loaded, expected.loaded, "response loaded"); +} + +async function nestedWorkerScriptTest(t, { source, target, expected }) { + const targetUrl = workerScriptUrl(target); + + const sourceUrl = resolveUrl( + "resources/worker-fetcher.js", sourceResolveOptions(source)); + sourceUrl.searchParams.append("url", targetUrl); + + // Iframe must be same-origin with the parent worker. + const iframeUrl = new URL("worker-fetcher.html", sourceUrl); + + const iframe = await appendIframe(t, document, iframeUrl); + const reply = futureMessage(); + + iframe.contentWindow.postMessage({ url: sourceUrl.href }, "*"); + + const { error, loaded } = await reply; + + assert_equals(error, expected.error, "worker error"); + assert_equals(loaded, expected.loaded, "response loaded"); +} + +async function sharedWorkerScriptTest(t, { source, target, expected }) { + const sourceUrl = resolveUrl("resources/shared-worker-fetcher.html", + sourceResolveOptions(source)); + const targetUrl = preflightUrl(target); + targetUrl.searchParams.append( + "body", "onconnect = (e) => e.ports[0].postMessage({ loaded: true })") + targetUrl.searchParams.append("mime-type", "application/javascript") + + const iframe = await appendIframe(t, document, sourceUrl); + const reply = futureMessage(); + + iframe.contentWindow.postMessage({ url: targetUrl.href }, "*"); + + const { error, loaded } = await reply; + + assert_equals(error, expected.error, "worker error"); + assert_equals(loaded, expected.loaded, "response loaded"); +} + +// Results that may be expected in tests. +const WorkerFetchTestResult = { + SUCCESS: { status: 200, body: "success" }, + FAILURE: { error: "TypeError" }, +}; + +async function workerFetchTest(t, { source, target, expected }) { + const targetUrl = preflightUrl(target); + + const sourceUrl = + resolveUrl("resources/fetcher.js", sourceResolveOptions(source)); + sourceUrl.searchParams.append("url", targetUrl.href); + + const fetcherUrl = new URL("worker-fetcher.html", sourceUrl); + + const reply = futureMessage(); + const iframe = await appendIframe(t, document, fetcherUrl); + + iframe.contentWindow.postMessage({ url: sourceUrl.href }, "*"); + + const { error, status, message } = await reply; + assert_equals(error, expected.error, "fetch error"); + assert_equals(status, expected.status, "response status"); + assert_equals(message, expected.message, "response body"); +} + +async function workerBlobFetchTest(t, { source, target, expected }) { + const targetUrl = preflightUrl(target); + + const fetcherUrl = resolveUrl( + 'resources/worker-blob-fetcher.html', sourceResolveOptions(source)); + + const reply = futureMessage(); + const iframe = await appendIframe(t, document, fetcherUrl); + + iframe.contentWindow.postMessage({ url: targetUrl.href }, "*"); + + const { error, status, message } = await reply; + assert_equals(error, expected.error, "fetch error"); + assert_equals(status, expected.status, "response status"); + assert_equals(message, expected.message, "response body"); +} + +async function sharedWorkerFetchTest(t, { source, target, expected }) { + const targetUrl = preflightUrl(target); + + const sourceUrl = + resolveUrl("resources/shared-fetcher.js", sourceResolveOptions(source)); + sourceUrl.searchParams.append("url", targetUrl.href); + + const fetcherUrl = new URL("shared-worker-fetcher.html", sourceUrl); + + const reply = futureMessage(); + const iframe = await appendIframe(t, document, fetcherUrl); + + iframe.contentWindow.postMessage({ url: sourceUrl.href }, "*"); + + const { error, status, message } = await reply; + assert_equals(error, expected.error, "fetch error"); + assert_equals(status, expected.status, "response status"); + assert_equals(message, expected.message, "response body"); +} diff --git a/test/wpt/tests/fetch/private-network-access/resources/worker-blob-fetcher.html b/test/wpt/tests/fetch/private-network-access/resources/worker-blob-fetcher.html new file mode 100644 index 00000000000..18a454b7fa3 --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/resources/worker-blob-fetcher.html @@ -0,0 +1,45 @@ + + +Worker Fetcher + diff --git a/test/wpt/tests/fetch/private-network-access/resources/worker-fetcher.html b/test/wpt/tests/fetch/private-network-access/resources/worker-fetcher.html new file mode 100644 index 00000000000..bd155a532bd --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/resources/worker-fetcher.html @@ -0,0 +1,18 @@ + + +Worker Fetcher + diff --git a/test/wpt/tests/fetch/private-network-access/resources/worker-fetcher.js b/test/wpt/tests/fetch/private-network-access/resources/worker-fetcher.js new file mode 100644 index 00000000000..aab49afe6f4 --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/resources/worker-fetcher.js @@ -0,0 +1,11 @@ +const url = new URL(self.location).searchParams.get("url"); +const worker = new Worker(url); + +// Relay messages from the worker to the parent frame. +worker.addEventListener("message", (evt) => { + self.postMessage(evt.data); +}); + +worker.addEventListener("error", (evt) => { + self.postMessage({ error: evt.message || "unknown error" }); +}); diff --git a/test/wpt/tests/fetch/private-network-access/resources/xhr-sender.html b/test/wpt/tests/fetch/private-network-access/resources/xhr-sender.html new file mode 100644 index 00000000000..b131fa41f9a --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/resources/xhr-sender.html @@ -0,0 +1,33 @@ + + +XHR Sender + diff --git a/test/wpt/tests/fetch/private-network-access/service-worker-background-fetch.https.window.js b/test/wpt/tests/fetch/private-network-access/service-worker-background-fetch.https.window.js new file mode 100644 index 00000000000..6369b166e21 --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/service-worker-background-fetch.https.window.js @@ -0,0 +1,142 @@ +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// Spec: https://wicg.github.io/background-fetch/ +// +// These tests check that background fetches from within `ServiceWorker` scripts +// are not subject to Private Network Access checks. + +// Results that may be expected in tests. +const TestResult = { + SUCCESS: { ok: true, body: "success", result: "success", failureReason: "" }, +}; + +async function makeTest(t, { source, target, expected }) { + const scriptUrl = + resolveUrl("resources/service-worker.js", sourceResolveOptions(source)); + + const bridgeUrl = new URL("service-worker-bridge.html", scriptUrl); + + const targetUrl = preflightUrl(target); + + const iframe = await appendIframe(t, document, bridgeUrl); + + const request = (message) => { + const reply = futureMessage(); + iframe.contentWindow.postMessage(message, "*"); + return reply; + }; + + { + const { error, loaded } = await request({ + action: "register", + url: scriptUrl.href, + }); + + assert_equals(error, undefined, "register error"); + assert_true(loaded, "response loaded"); + } + + { + const { error, state } = await request({ + action: "set-permission", + name: "background-fetch", + state: "granted", + }); + + assert_equals(error, undefined, "set permission error"); + assert_equals(state, "granted", "permission state"); + } + + { + const { error, result, failureReason, ok, body } = await request({ + action: "background-fetch", + url: targetUrl.href, + }); + + assert_equals(error, expected.error, "error"); + assert_equals(failureReason, expected.failureReason, "fetch failure reason"); + assert_equals(result, expected.result, "fetch result"); + assert_equals(ok, expected.ok, "response ok"); + assert_equals(body, expected.body, "response body"); + } +} + +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, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: TestResult.SUCCESS, +}), "private to local: success."); + +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, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: TestResult.SUCCESS, +}), "public to local: success."); + +promise_test(t => makeTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: TestResult.SUCCESS, +}), "public to private: success."); + +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, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { server: Server.HTTPS_LOCAL }, + expected: TestResult.SUCCESS, +}), "treat-as-public to local: success."); + +promise_test(t => makeTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: TestResult.SUCCESS, +}), "treat-as-public to private: success."); + +promise_test(t => makeTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PUBLIC, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: TestResult.SUCCESS, +}), "treat-as-public to public: success."); diff --git a/test/wpt/tests/fetch/private-network-access/service-worker-fetch.https.window.js b/test/wpt/tests/fetch/private-network-access/service-worker-fetch.https.window.js new file mode 100644 index 00000000000..5645d784561 --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/service-worker-fetch.https.window.js @@ -0,0 +1,217 @@ +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests check that fetches from within `ServiceWorker` scripts are +// subject to Private Network Access checks, just like fetches from within +// documents. + +// Results that may be expected in tests. +const TestResult = { + SUCCESS: { ok: true, body: "success" }, + FAILURE: { error: "TypeError" }, +}; + +async function makeTest(t, { source, target, expected }) { + const bridgeUrl = resolveUrl( + "resources/service-worker-bridge.html", + sourceResolveOptions({ server: source.server })); + + const scriptUrl = + resolveUrl("resources/service-worker.js", sourceResolveOptions(source)); + + const realTargetUrl = preflightUrl(target); + + // Fetch a URL within the service worker's scope, but tell it which URL to + // really fetch. + const targetUrl = new URL("service-worker-proxy", scriptUrl); + targetUrl.searchParams.append("proxied-url", realTargetUrl.href); + + const iframe = await appendIframe(t, document, bridgeUrl); + + const request = (message) => { + const reply = futureMessage(); + iframe.contentWindow.postMessage(message, "*"); + return reply; + }; + + { + const { error, loaded } = await request({ + action: "register", + url: scriptUrl.href, + }); + + assert_equals(error, undefined, "register error"); + assert_true(loaded, "response loaded"); + } + + try { + const { controlled, numControllerChanges } = await request({ + action: "wait", + numControllerChanges: 1, + }); + + assert_equals(numControllerChanges, 1, "controller change"); + assert_true(controlled, "bridge script is controlled"); + + const { error, ok, body } = await request({ + action: "fetch", + url: targetUrl.href, + }); + + assert_equals(error, expected.error, "fetch error"); + assert_equals(ok, expected.ok, "response ok"); + assert_equals(body, expected.body, "response body"); + } finally { + // Always unregister the service worker. + const { error, unregistered } = await request({ + action: "unregister", + scope: new URL("./", scriptUrl).href, + }); + + assert_equals(error, undefined, "unregister error"); + assert_true(unregistered, "unregistered"); + } +} + +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, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: TestResult.FAILURE, +}), "private to local: failed preflight."); + +promise_test(t => makeTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: TestResult.SUCCESS, +}), "private to local: success."); + +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, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: TestResult.FAILURE, +}), "public to local: failed preflight."); + +promise_test(t => makeTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: TestResult.SUCCESS, +}), "public to local: success."); + +promise_test(t => makeTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: TestResult.FAILURE, +}), "public to private: failed preflight."); + +promise_test(t => makeTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: TestResult.SUCCESS, +}), "public to private: success."); + +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, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { server: Server.HTTPS_LOCAL }, + expected: TestResult.FAILURE, +}), "treat-as-public to local: failed preflight."); + +promise_test(t => makeTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { preflight: PreflightBehavior.success(token()) }, + }, + expected: TestResult.SUCCESS, +}), "treat-as-public to local: success."); + +promise_test(t => makeTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: TestResult.FAILURE, +}), "treat-as-public to private: failed preflight."); + +promise_test(t => makeTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: TestResult.SUCCESS, +}), "treat-as-public to private: success."); + +promise_test(t => makeTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PUBLIC, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: TestResult.SUCCESS, +}), "treat-as-public to public: success."); diff --git a/test/wpt/tests/fetch/private-network-access/service-worker-update.https.window.js b/test/wpt/tests/fetch/private-network-access/service-worker-update.https.window.js new file mode 100644 index 00000000000..fe2bf58f660 --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/service-worker-update.https.window.js @@ -0,0 +1,121 @@ +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests check that `ServiceWorker` script update fetches are subject to +// Private Network Access checks, just like regular `fetch()` calls. The client +// of the fetch, for PNA purposes, is taken to be the previous script. +// +// The tests is carried out by instantiating a service worker from a resource +// that carries the `Content-Security-Policy: treat-as-public-address` header, +// such that the registration is placed in the public IP address space. When +// the script is fetched for an update, the client is thus considered public, +// yet the same-origin fetch observes that the server's IP endpoint is not +// necessarily in the public IP address space. +// +// See also: worker.https.window.js + +// Results that may be expected in tests. +const TestResult = { + SUCCESS: { updated: true }, + FAILURE: { error: "TypeError" }, +}; + +async function makeTest(t, { target, expected }) { + // The bridge must be same-origin with the service worker script. + const bridgeUrl = resolveUrl( + "resources/service-worker-bridge.html", + sourceResolveOptions({ server: target.server })); + + const scriptUrl = preflightUrl(target); + scriptUrl.searchParams.append("treat-as-public-once", token()); + scriptUrl.searchParams.append("mime-type", "application/javascript"); + scriptUrl.searchParams.append("file", "service-worker.js"); + scriptUrl.searchParams.append("random-js-prefix", true); + + const iframe = await appendIframe(t, document, bridgeUrl); + + const request = (message) => { + const reply = futureMessage(); + iframe.contentWindow.postMessage(message, "*"); + return reply; + }; + + { + const { error, loaded } = await request({ + action: "register", + url: scriptUrl.href, + }); + + assert_equals(error, undefined, "register error"); + assert_true(loaded, "response loaded"); + } + + try { + let { controlled, numControllerChanges } = await request({ + action: "wait", + numControllerChanges: 1, + }); + + assert_equals(numControllerChanges, 1, "controller change"); + assert_true(controlled, "bridge script is controlled"); + + const { error, updated } = await request({ action: "update" }); + + assert_equals(error, expected.error, "update error"); + assert_equals(updated, expected.updated, "registration updated"); + + // Stop here if we do not expect the update to succeed. + if (!expected.updated) { + return; + } + + ({ controlled, numControllerChanges } = await request({ + action: "wait", + numControllerChanges: 2, + })); + + assert_equals(numControllerChanges, 2, "controller change"); + assert_true(controlled, "bridge script still controlled"); + } finally { + const { error, unregistered } = await request({ + action: "unregister", + scope: new URL("./", scriptUrl).href, + }); + + assert_equals(error, undefined, "unregister error"); + assert_true(unregistered, "unregistered"); + } +} + +promise_test(t => makeTest(t, { + target: { server: Server.HTTPS_LOCAL }, + expected: TestResult.FAILURE, +}), "update public to local: failed preflight."); + +promise_test(t => makeTest(t, { + target: { + server: Server.HTTPS_LOCAL, + behavior: { preflight: PreflightBehavior.serviceWorkerSuccess(token()) }, + }, + expected: TestResult.SUCCESS, +}), "update public to local: success."); + +promise_test(t => makeTest(t, { + target: { server: Server.HTTPS_PRIVATE }, + expected: TestResult.FAILURE, +}), "update public to private: failed preflight."); + +promise_test(t => makeTest(t, { + target: { + server: Server.HTTPS_PRIVATE, + behavior: { preflight: PreflightBehavior.serviceWorkerSuccess(token()) }, + }, + expected: TestResult.SUCCESS, +}), "update public to private: success."); + +promise_test(t => makeTest(t, { + target: { server: Server.HTTPS_PUBLIC }, + expected: TestResult.SUCCESS, +}), "update public to public: success."); diff --git a/test/wpt/tests/fetch/private-network-access/service-worker.https.window.js b/test/wpt/tests/fetch/private-network-access/service-worker.https.window.js new file mode 100644 index 00000000000..8dc631f65a8 --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/service-worker.https.window.js @@ -0,0 +1,107 @@ +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests check that initial `ServiceWorker` script fetches are subject to +// Private Network Access checks, just like a regular `fetch()`. +// +// See also: worker.https.window.js + +// Results that may be expected in tests. +const TestResult = { + SUCCESS: { + register: { loaded: true }, + unregister: { unregistered: true }, + }, + FAILURE: { + register: { error: "TypeError" }, + unregister: { unregistered: false, error: "no registration" }, + }, +}; + +async function makeTest(t, { source, target, expected }) { + const sourceUrl = resolveUrl("resources/service-worker-bridge.html", + sourceResolveOptions(source)); + + const targetUrl = preflightUrl(target); + targetUrl.searchParams.append("body", "undefined"); + targetUrl.searchParams.append("mime-type", "application/javascript"); + + const scope = resolveUrl(`resources/${token()}`, {...target.server}).href; + + const iframe = await appendIframe(t, document, sourceUrl); + + { + const reply = futureMessage(); + const message = { + action: "register", + url: targetUrl.href, + options: { scope }, + }; + iframe.contentWindow.postMessage(message, "*"); + + const { error, loaded } = await reply; + + assert_equals(error, expected.register.error, "register error"); + assert_equals(loaded, expected.register.loaded, "response loaded"); + } + + { + const reply = futureMessage(); + iframe.contentWindow.postMessage({ action: "unregister", scope }, "*"); + + const { error, unregistered } = await reply; + assert_equals(error, expected.unregister.error, "unregister error"); + assert_equals( + unregistered, expected.unregister.unregistered, "worker unregistered"); + } +} + +promise_test(t => makeTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { server: Server.HTTPS_LOCAL }, + expected: TestResult.FAILURE, +}), "treat-as-public to local: failed preflight."); + +promise_test(t => makeTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { preflight: PreflightBehavior.serviceWorkerSuccess(token()) }, + }, + expected: TestResult.SUCCESS, +}), "treat-as-public to local: success."); + +promise_test(t => makeTest(t, { + source: { + server: Server.HTTPS_PRIVATE, + treatAsPublic: true, + }, + target: { server: Server.HTTPS_PRIVATE }, + expected: TestResult.FAILURE, +}), "treat-as-public to private: failed preflight."); + +promise_test(t => makeTest(t, { + source: { + server: Server.HTTPS_PRIVATE, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { preflight: PreflightBehavior.serviceWorkerSuccess(token()) }, + }, + expected: TestResult.SUCCESS, +}), "treat-as-public to private: success."); + +promise_test(t => makeTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { server: Server.HTTPS_PUBLIC }, + expected: TestResult.SUCCESS, +}), "public to public: success."); diff --git a/test/wpt/tests/fetch/private-network-access/shared-worker-fetch.https.window.js b/test/wpt/tests/fetch/private-network-access/shared-worker-fetch.https.window.js new file mode 100644 index 00000000000..7066b359caa --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/shared-worker-fetch.https.window.js @@ -0,0 +1,152 @@ +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests check that fetches from within `SharedWorker` scripts are subject +// to Private Network Access checks, just like fetches from within documents. +// +// This file covers only those tests that must execute in a secure context. +// Other tests are defined in: shared-worker-fetch.window.js + +promise_test(t => sharedWorkerFetchTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { server: Server.HTTPS_LOCAL }, + expected: WorkerFetchTestResult.SUCCESS, +}), "local to local: success."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "private to local: failure."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.SUCCESS, +}), "private to local: success."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { server: Server.HTTPS_PRIVATE }, + expected: WorkerFetchTestResult.SUCCESS, +}), "private to private: success."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "public to local: failed preflight."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.SUCCESS, +}), "public to local: success."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "public to private: failed preflight."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.SUCCESS, +}), "public to private: success."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { server: Server.HTTPS_PUBLIC }, + expected: WorkerFetchTestResult.SUCCESS, +}), "public to public: success."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { server: Server.HTTPS_LOCAL }, + expected: WorkerFetchTestResult.FAILURE, +}), "treat-as-public to local: failed preflight."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { preflight: PreflightBehavior.optionalSuccess(token()) }, + }, + expected: WorkerFetchTestResult.SUCCESS, +}), "treat-as-public to local: success."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "treat-as-public to private: failed preflight."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.SUCCESS, +}), "treat-as-public to private: success."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PUBLIC, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: WorkerFetchTestResult.SUCCESS, +}), "treat-as-public to public: success."); + diff --git a/test/wpt/tests/fetch/private-network-access/shared-worker-fetch.window.js b/test/wpt/tests/fetch/private-network-access/shared-worker-fetch.window.js new file mode 100644 index 00000000000..1c772003a16 --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/shared-worker-fetch.window.js @@ -0,0 +1,142 @@ +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests check that fetches from within `SharedWorker` scripts are subject +// to Private Network Access checks, just like fetches from within documents. +// +// This file covers only those tests that must execute in a non-secure context. +// Other tests are defined in: shared-worker-fetch.https.window.js + +promise_test(t => sharedWorkerFetchTest(t, { + source: { server: Server.HTTP_LOCAL }, + target: { server: Server.HTTP_LOCAL }, + expected: WorkerFetchTestResult.SUCCESS, +}), "local to local: success."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { server: Server.HTTP_PRIVATE }, + target: { + server: Server.HTTP_LOCAL, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "private to local: failure."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { server: Server.HTTP_PRIVATE }, + target: { server: Server.HTTP_PRIVATE }, + expected: WorkerFetchTestResult.SUCCESS, +}), "private to private: success."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { + server: Server.HTTP_LOCAL, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "public to local: failure."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { + server: Server.HTTP_PRIVATE, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "public to private: failure."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { server: Server.HTTP_PUBLIC }, + expected: WorkerFetchTestResult.SUCCESS, +}), "public to public: success."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTP_LOCAL, + behavior: { preflight: PreflightBehavior.optionalSuccess(token()) }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "treat-as-public to local: failure."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTP_PRIVATE, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "treat-as-public to private: failure."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTP_PUBLIC, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: WorkerFetchTestResult.SUCCESS, +}), "treat-as-public to public: success."); + +// The following tests verify that workers served over HTTPS are not allowed to +// make private network requests because they are not secure contexts. + +promise_test(t => sharedWorkerFetchTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { + server: Server.HTTP_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "local https to local: failure."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTP_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "private https to local: failure."); + +promise_test(t => sharedWorkerFetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTP_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "public https to local: failure."); diff --git a/test/wpt/tests/fetch/private-network-access/shared-worker.https.window.js b/test/wpt/tests/fetch/private-network-access/shared-worker.https.window.js new file mode 100644 index 00000000000..ecb70c41245 --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/shared-worker.https.window.js @@ -0,0 +1,58 @@ +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests mirror `Worker` tests, except using `SharedWorker`. +// See also: worker.https.window.js +// +// This file covers only those tests that must execute in a secure context. +// Other tests are defined in: shared-worker.window.js + +promise_test(t => sharedWorkerScriptTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { server: Server.HTTPS_LOCAL }, + expected: WorkerScriptTestResult.FAILURE, +}), "treat-as-public to local: failed preflight."); + +promise_test(t => sharedWorkerScriptTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { preflight: PreflightBehavior.optionalSuccess(token()) }, + }, + expected: WorkerScriptTestResult.SUCCESS, +}), "treat-as-public to local: success."); + +promise_test(t => sharedWorkerScriptTest(t, { + source: { + server: Server.HTTPS_PRIVATE, + treatAsPublic: true, + }, + target: { server: Server.HTTPS_PRIVATE }, + expected: WorkerScriptTestResult.FAILURE, +}), "treat-as-public to private: failed preflight."); + +promise_test(t => sharedWorkerScriptTest(t, { + source: { + server: Server.HTTPS_PRIVATE, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { preflight: PreflightBehavior.optionalSuccess(token()) }, + }, + expected: WorkerScriptTestResult.SUCCESS, +}), "treat-as-public to private: success."); + +promise_test(t => sharedWorkerScriptTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { server: Server.HTTPS_PUBLIC }, + expected: WorkerScriptTestResult.SUCCESS, +}), "public to public: success."); diff --git a/test/wpt/tests/fetch/private-network-access/shared-worker.window.js b/test/wpt/tests/fetch/private-network-access/shared-worker.window.js new file mode 100644 index 00000000000..ffa8a360c70 --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/shared-worker.window.js @@ -0,0 +1,34 @@ +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests mirror `Worker` tests, except using `SharedWorker`. +// See also: shared-worker.window.js +// +// This file covers only those tests that must execute in a non secure context. +// Other tests are defined in: shared-worker.https.window.js + +promise_test(t => sharedWorkerScriptTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { server: Server.HTTP_LOCAL }, + expected: WorkerScriptTestResult.FAILURE, +}), "treat-as-public to local: failure."); + +promise_test(t => sharedWorkerScriptTest(t, { + source: { + server: Server.HTTP_PRIVATE, + treatAsPublic: true, + }, + target: { server: Server.HTTP_PRIVATE }, + expected: WorkerScriptTestResult.FAILURE, +}), "treat-as-public to private: failure."); + +promise_test(t => sharedWorkerScriptTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { server: Server.HTTP_PUBLIC }, + expected: WorkerScriptTestResult.SUCCESS, +}), "public to public: success."); diff --git a/test/wpt/tests/fetch/private-network-access/websocket.https.window.js b/test/wpt/tests/fetch/private-network-access/websocket.https.window.js new file mode 100644 index 00000000000..0731896098b --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/websocket.https.window.js @@ -0,0 +1,40 @@ +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests verify that websocket connections behave similarly to fetches. +// +// This file covers only those tests that must execute in a secure context. +// Other tests are defined in: websocket.https.window.js + +setup(() => { + // Making sure we are in a secure context, as expected. + assert_true(window.isSecureContext); +}); + +promise_test(t => websocketTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { server: Server.WSS_LOCAL }, + expected: WebsocketTestResult.SUCCESS, +}), "local to local: websocket success."); + +promise_test(t => websocketTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { server: Server.WSS_LOCAL }, + expected: WebsocketTestResult.SUCCESS, +}), "private to local: websocket success."); + +promise_test(t => websocketTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { server: Server.WSS_LOCAL }, + expected: WebsocketTestResult.SUCCESS, +}), "public to local: websocket success."); + +promise_test(t => websocketTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { server: Server.WSS_LOCAL }, + expected: WebsocketTestResult.SUCCESS, +}), "treat-as-public to local: websocket success."); diff --git a/test/wpt/tests/fetch/private-network-access/websocket.window.js b/test/wpt/tests/fetch/private-network-access/websocket.window.js new file mode 100644 index 00000000000..a44cfaedec7 --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/websocket.window.js @@ -0,0 +1,40 @@ +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch + +// These tests verify that websocket connections behave similarly to fetches. +// +// This file covers only those tests that must execute in a non secure context. +// Other tests are defined in: websocket.https.window.js + +setup(() => { + // Making sure we are in a non secure context, as expected. + assert_false(window.isSecureContext); +}); + +promise_test(t => websocketTest(t, { + source: { server: Server.HTTP_LOCAL }, + target: { server: Server.WS_LOCAL }, + expected: WebsocketTestResult.SUCCESS, +}), "local to local: websocket success."); + +promise_test(t => websocketTest(t, { + source: { server: Server.HTTP_PRIVATE }, + target: { server: Server.WS_LOCAL }, + expected: WebsocketTestResult.FAILURE, +}), "private to local: websocket failure."); + +promise_test(t => websocketTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { server: Server.WS_LOCAL }, + expected: WebsocketTestResult.FAILURE, +}), "public to local: websocket failure."); + +promise_test(t => websocketTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { server: Server.WS_LOCAL }, + expected: WebsocketTestResult.FAILURE, +}), "treat-as-public to local: websocket failure."); diff --git a/test/wpt/tests/fetch/private-network-access/worker-blob-fetch.window.js b/test/wpt/tests/fetch/private-network-access/worker-blob-fetch.window.js new file mode 100644 index 00000000000..80374143a28 --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/worker-blob-fetch.window.js @@ -0,0 +1,143 @@ +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests check that fetches from within `Worker` scripts loaded from blob +// URLs are subject to Private Network Access checks, just like fetches from +// within documents. +// +// This file covers only those tests that must execute in a non-secure context. +// Other tests are defined in: worker-fetch.https.window.js + +promise_test(t => workerBlobFetchTest(t, { + source: { server: Server.HTTP_LOCAL }, + target: { server: Server.HTTP_LOCAL }, + expected: WorkerFetchTestResult.SUCCESS, +}), "local to local: success."); + +promise_test(t => workerBlobFetchTest(t, { + source: { server: Server.HTTP_PRIVATE }, + target: { + server: Server.HTTP_LOCAL, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "private to local: failure."); + +promise_test(t => workerBlobFetchTest(t, { + source: { server: Server.HTTP_PRIVATE }, + target: { server: Server.HTTP_PRIVATE }, + expected: WorkerFetchTestResult.SUCCESS, +}), "private to private: success."); + +promise_test(t => workerBlobFetchTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { + server: Server.HTTP_LOCAL, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "public to local: failure."); + +promise_test(t => workerBlobFetchTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { + server: Server.HTTP_PRIVATE, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "public to private: failure."); + +promise_test(t => workerBlobFetchTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { server: Server.HTTP_PUBLIC }, + expected: WorkerFetchTestResult.SUCCESS, +}), "public to public: success."); + +promise_test(t => workerBlobFetchTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTP_LOCAL, + behavior: { preflight: PreflightBehavior.optionalSuccess(token()) }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "treat-as-public to local: failure."); + +promise_test(t => workerBlobFetchTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTP_PRIVATE, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "treat-as-public to private: failure."); + +promise_test(t => workerBlobFetchTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTP_PUBLIC, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: WorkerFetchTestResult.SUCCESS, +}), "treat-as-public to public: success."); + +// The following tests verify that workers served over HTTPS are not allowed to +// make private network requests because they are not secure contexts. + +promise_test(t => workerBlobFetchTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTP_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "private https to local: failure."); + +promise_test(t => workerBlobFetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTP_PRIVATE, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "public https to private: failure."); + +promise_test(t => workerBlobFetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTP_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "public https to local: failure."); diff --git a/test/wpt/tests/fetch/private-network-access/worker-fetch.https.window.js b/test/wpt/tests/fetch/private-network-access/worker-fetch.https.window.js new file mode 100644 index 00000000000..89e0c3cf1f3 --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/worker-fetch.https.window.js @@ -0,0 +1,151 @@ +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests check that fetches from within `Worker` scripts are subject to +// Private Network Access checks, just like fetches from within documents. +// +// This file covers only those tests that must execute in a secure context. +// Other tests are defined in: worker-fetch.window.js + +promise_test(t => workerFetchTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { server: Server.HTTPS_LOCAL }, + expected: WorkerFetchTestResult.SUCCESS, +}), "local to local: success."); + +promise_test(t => workerFetchTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "private to local: failed preflight."); + +promise_test(t => workerFetchTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.SUCCESS, +}), "private to local: success."); + +promise_test(t => workerFetchTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { server: Server.HTTPS_PRIVATE }, + expected: WorkerFetchTestResult.SUCCESS, +}), "private to private: success."); + +promise_test(t => workerFetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "public to local: failed preflight."); + +promise_test(t => workerFetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.SUCCESS, +}), "public to local: success."); + +promise_test(t => workerFetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "public to private: failed preflight."); + +promise_test(t => workerFetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.SUCCESS, +}), "public to private: success."); + +promise_test(t => workerFetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { server: Server.HTTPS_PUBLIC }, + expected: WorkerFetchTestResult.SUCCESS, +}), "public to public: success."); + +promise_test(t => workerFetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { server: Server.HTTPS_LOCAL }, + expected: WorkerFetchTestResult.FAILURE, +}), "treat-as-public to local: failed preflight."); + +promise_test(t => workerFetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { preflight: PreflightBehavior.optionalSuccess(token()) }, + }, + expected: WorkerFetchTestResult.SUCCESS, +}), "treat-as-public to local: success."); + +promise_test(t => workerFetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "treat-as-public to private: failed preflight."); + +promise_test(t => workerFetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.SUCCESS, +}), "treat-as-public to private: success."); + +promise_test(t => workerFetchTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PUBLIC, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: WorkerFetchTestResult.SUCCESS, +}), "treat-as-public to public: success."); diff --git a/test/wpt/tests/fetch/private-network-access/worker-fetch.window.js b/test/wpt/tests/fetch/private-network-access/worker-fetch.window.js new file mode 100644 index 00000000000..1a57a106a65 --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/worker-fetch.window.js @@ -0,0 +1,142 @@ +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests check that fetches from within `Worker` scripts are subject to +// Private Network Access checks, just like fetches from within documents. +// +// This file covers only those tests that must execute in a non-secure context. +// Other tests are defined in: worker-fetch.https.window.js + +promise_test(t => workerFetchTest(t, { + source: { server: Server.HTTP_LOCAL }, + target: { server: Server.HTTP_LOCAL }, + expected: WorkerFetchTestResult.SUCCESS, +}), "local to local: success."); + +promise_test(t => workerFetchTest(t, { + source: { server: Server.HTTP_PRIVATE }, + target: { + server: Server.HTTP_LOCAL, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "private to local: failure."); + +promise_test(t => workerFetchTest(t, { + source: { server: Server.HTTP_PRIVATE }, + target: { server: Server.HTTP_PRIVATE }, + expected: WorkerFetchTestResult.SUCCESS, +}), "private to private: success."); + +promise_test(t => workerFetchTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { + server: Server.HTTP_LOCAL, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "public to local: failure."); + +promise_test(t => workerFetchTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { + server: Server.HTTP_PRIVATE, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "public to private: failure."); + +promise_test(t => workerFetchTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { server: Server.HTTP_PUBLIC }, + expected: WorkerFetchTestResult.SUCCESS, +}), "public to public: success."); + +promise_test(t => workerFetchTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTP_LOCAL, + behavior: { preflight: PreflightBehavior.optionalSuccess(token()) }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "treat-as-public to local: failure."); + +promise_test(t => workerFetchTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTP_PRIVATE, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "treat-as-public to private: failure."); + +promise_test(t => workerFetchTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTP_PUBLIC, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: WorkerFetchTestResult.SUCCESS, +}), "treat-as-public to public: success."); + +// The following tests verify that workers served over HTTPS are not allowed to +// make private network requests because they are not secure contexts. + +promise_test(t => workerFetchTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTP_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "private https to local: failure."); + +promise_test(t => workerFetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTP_PRIVATE, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "public https to private: failure."); + +promise_test(t => workerFetchTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTP_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: WorkerFetchTestResult.FAILURE, +}), "public https to local: failure."); diff --git a/test/wpt/tests/fetch/private-network-access/worker.https.window.js b/test/wpt/tests/fetch/private-network-access/worker.https.window.js new file mode 100644 index 00000000000..2d0dddd685c --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/worker.https.window.js @@ -0,0 +1,61 @@ +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests check that initial `Worker` script fetches are subject to Private +// Network Access checks, just like a regular `fetch()`. The main difference is +// that workers can only be fetched same-origin, so the only way to test this +// is using the `treat-as-public` CSP directive to artificially place the parent +// document in the `public` IP address space. +// +// This file covers only those tests that must execute in a secure context. +// Other tests are defined in: worker.window.js + +promise_test(t => workerScriptTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { server: Server.HTTPS_LOCAL }, + expected: WorkerScriptTestResult.FAILURE, +}), "treat-as-public to local: failed preflight."); + +promise_test(t => workerScriptTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { preflight: PreflightBehavior.optionalSuccess(token()) }, + }, + expected: WorkerScriptTestResult.SUCCESS, +}), "treat-as-public to local: success."); + +promise_test(t => workerScriptTest(t, { + source: { + server: Server.HTTPS_PRIVATE, + treatAsPublic: true, + }, + target: { server: Server.HTTPS_PRIVATE }, + expected: WorkerScriptTestResult.FAILURE, +}), "treat-as-public to private: failed preflight."); + +promise_test(t => workerScriptTest(t, { + source: { + server: Server.HTTPS_PRIVATE, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { preflight: PreflightBehavior.optionalSuccess(token()) }, + }, + expected: WorkerScriptTestResult.SUCCESS, +}), "treat-as-public to private: success."); + +promise_test(t => workerScriptTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { server: Server.HTTPS_PUBLIC }, + expected: WorkerScriptTestResult.SUCCESS, +}), "public to public: success."); diff --git a/test/wpt/tests/fetch/private-network-access/worker.window.js b/test/wpt/tests/fetch/private-network-access/worker.window.js new file mode 100644 index 00000000000..118c0992544 --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/worker.window.js @@ -0,0 +1,37 @@ +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests check that initial `Worker` script fetches are subject to Private +// Network Access checks, just like a regular `fetch()`. The main difference is +// that workers can only be fetched same-origin, so the only way to test this +// is using the `treat-as-public` CSP directive to artificially place the parent +// document in the `public` IP address space. +// +// This file covers only those tests that must execute in a non secure context. +// Other tests are defined in: worker.https.window.js + +promise_test(t => workerScriptTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { server: Server.HTTP_LOCAL }, + expected: WorkerScriptTestResult.FAILURE, +}), "treat-as-public to local: failure."); + +promise_test(t => workerScriptTest(t, { + source: { + server: Server.HTTP_PRIVATE, + treatAsPublic: true, + }, + target: { server: Server.HTTP_PRIVATE }, + expected: WorkerScriptTestResult.FAILURE, +}), "treat-as-public to private: failure."); + +promise_test(t => workerScriptTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { server: Server.HTTP_PUBLIC }, + expected: WorkerScriptTestResult.SUCCESS, +}), "public to public: success."); diff --git a/test/wpt/tests/fetch/private-network-access/xhr-from-treat-as-public.https.window.js b/test/wpt/tests/fetch/private-network-access/xhr-from-treat-as-public.https.window.js new file mode 100644 index 00000000000..04fe5449d19 --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/xhr-from-treat-as-public.https.window.js @@ -0,0 +1,74 @@ +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// 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(t => xhrTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: XhrTestResult.FAILURE, +}), "treat-as-public to local: failed preflight."); + +promise_test(t => xhrTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: XhrTestResult.SUCCESS, +}), "treat-as-public to local: success."); + +promise_test(t => xhrTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: XhrTestResult.FAILURE, +}), "treat-as-public to private: failed preflight."); + +promise_test(t => xhrTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: XhrTestResult.SUCCESS, +}), "treat-as-public to private: success."); + +promise_test(t => xhrTest(t, { + source: { + server: Server.HTTPS_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTPS_PUBLIC, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: XhrTestResult.SUCCESS, +}), "treat-as-public to public: no preflight required."); diff --git a/test/wpt/tests/fetch/private-network-access/xhr.https.window.js b/test/wpt/tests/fetch/private-network-access/xhr.https.window.js new file mode 100644 index 00000000000..4dc5da9912f --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/xhr.https.window.js @@ -0,0 +1,142 @@ +// META: script=/common/subset-tests-by-key.js +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// META: variant=?include=from-local +// META: variant=?include=from-private +// META: variant=?include=from-public +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests mirror fetch.https.window.js, but use `XmlHttpRequest` instead of +// `fetch()` to perform subresource fetches. Preflights are tested less +// extensively due to coverage being already provided by `fetch()`. +// +// This file covers only those tests that must execute in a secure context. +// Other tests are defined in: xhr.window.js + +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 => xhrTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { server: Server.HTTPS_LOCAL }, + expected: XhrTestResult.SUCCESS, +}), "local to local: no preflight required."); + +subsetTestByKey("from-local", promise_test, t => xhrTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: XhrTestResult.SUCCESS, +}), "local to private: no preflight required."); + +subsetTestByKey("from-local", promise_test, t => xhrTest(t, { + source: { server: Server.HTTPS_LOCAL }, + target: { + server: Server.HTTPS_PUBLIC, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: XhrTestResult.SUCCESS, +}), "local to public: no preflight required."); + +// Source: private secure context. +// +// Fetches to the local address space require a successful preflight response +// carrying a PNA-specific header. + +subsetTestByKey("from-private", promise_test, t => xhrTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: XhrTestResult.FAILURE, +}), "private to local: failed preflight."); + +subsetTestByKey("from-private", promise_test, t => xhrTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: XhrTestResult.SUCCESS, +}), "private to local: success."); + +subsetTestByKey("from-private", promise_test, t => xhrTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { server: Server.HTTPS_PRIVATE }, + expected: XhrTestResult.SUCCESS, +}), "private to private: no preflight required."); + +subsetTestByKey("from-private", promise_test, t => xhrTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTPS_PUBLIC, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: XhrTestResult.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. + +subsetTestByKey("from-public", promise_test, t => xhrTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: XhrTestResult.FAILURE, +}), "public to local: failed preflight."); + +subsetTestByKey("from-public", promise_test, t => xhrTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: XhrTestResult.SUCCESS, +}), "public to local: success."); + +subsetTestByKey("from-public", promise_test, t => xhrTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: XhrTestResult.FAILURE, +}), "public to private: failed preflight."); + +subsetTestByKey("from-public", promise_test, t => xhrTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: XhrTestResult.SUCCESS, +}), "public to private: success."); + +subsetTestByKey("from-public", promise_test, t => xhrTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { server: Server.HTTPS_PUBLIC }, + expected: XhrTestResult.SUCCESS, +}), "public to public: no preflight required."); diff --git a/test/wpt/tests/fetch/private-network-access/xhr.window.js b/test/wpt/tests/fetch/private-network-access/xhr.window.js new file mode 100644 index 00000000000..b45f8283e93 --- /dev/null +++ b/test/wpt/tests/fetch/private-network-access/xhr.window.js @@ -0,0 +1,183 @@ +// META: script=/common/utils.js +// META: script=resources/support.sub.js +// +// Spec: https://wicg.github.io/private-network-access/#integration-fetch +// +// These tests mirror fetch.window.js, but use `XmlHttpRequest` instead of +// `fetch()` to perform subresource fetches. +// +// This file covers only those tests that must execute in a non secure context. +// Other tests are defined in: xhr.https.window.js + +setup(() => { + // Making sure we are in a non secure context, as expected. + assert_false(window.isSecureContext); +}); + +promise_test(t => xhrTest(t, { + source: { server: Server.HTTP_LOCAL }, + target: { server: Server.HTTP_LOCAL }, + expected: XhrTestResult.SUCCESS, +}), "local to local: no preflight required."); + +promise_test(t => xhrTest(t, { + source: { server: Server.HTTP_LOCAL }, + target: { + server: Server.HTTP_PRIVATE, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: XhrTestResult.SUCCESS, +}), "local to private: no preflight required."); + +promise_test(t => xhrTest(t, { + source: { server: Server.HTTP_LOCAL }, + target: { + server: Server.HTTP_PUBLIC, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: XhrTestResult.SUCCESS, +}), "local to public: no preflight required."); + +promise_test(t => xhrTest(t, { + source: { server: Server.HTTP_PRIVATE }, + target: { + server: Server.HTTP_LOCAL, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: XhrTestResult.FAILURE, +}), "private to local: failure."); + +promise_test(t => xhrTest(t, { + source: { server: Server.HTTP_PRIVATE }, + target: { server: Server.HTTP_PRIVATE }, + expected: XhrTestResult.SUCCESS, +}), "private to private: no preflight required."); + +promise_test(t => xhrTest(t, { + source: { server: Server.HTTP_PRIVATE }, + target: { + server: Server.HTTP_PUBLIC, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: XhrTestResult.SUCCESS, +}), "private to public: no preflight required."); + +promise_test(t => xhrTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { + server: Server.HTTP_LOCAL, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: XhrTestResult.FAILURE, +}), "public to local: failure."); + +promise_test(t => xhrTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { + server: Server.HTTP_PRIVATE, + behavior: { + preflight: PreflightBehavior.optionalSuccess(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: XhrTestResult.FAILURE, +}), "public to private: failure."); + +promise_test(t => xhrTest(t, { + source: { server: Server.HTTP_PUBLIC }, + target: { server: Server.HTTP_PUBLIC }, + expected: XhrTestResult.SUCCESS, +}), "public to public: no preflight required."); + +// 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(t => xhrTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTP_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: XhrTestResult.FAILURE, +}), "treat-as-public-address to local: failure."); + +promise_test(t => xhrTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTP_PRIVATE, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: XhrTestResult.FAILURE, +}), "treat-as-public-address to private: failure."); + +promise_test(t => xhrTest(t, { + source: { + server: Server.HTTP_LOCAL, + treatAsPublic: true, + }, + target: { + server: Server.HTTP_PUBLIC, + behavior: { response: ResponseBehavior.allowCrossOrigin() }, + }, + expected: XhrTestResult.SUCCESS, +}), "treat-as-public-address to public: no preflight required."); + +// These tests verify that HTTPS iframes embedded in an HTTP top-level document +// cannot fetch subresources from less-public address spaces. Indeed, even +// though the iframes have HTTPS origins, they are non-secure contexts because +// their parent is a non-secure context. + +promise_test(t => xhrTest(t, { + source: { server: Server.HTTPS_PRIVATE }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: XhrTestResult.FAILURE, +}), "private https to local: failure."); + +promise_test(t => xhrTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_LOCAL, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: XhrTestResult.FAILURE, +}), "public https to local: failure."); + +promise_test(t => xhrTest(t, { + source: { server: Server.HTTPS_PUBLIC }, + target: { + server: Server.HTTPS_PRIVATE, + behavior: { + preflight: PreflightBehavior.success(token()), + response: ResponseBehavior.allowCrossOrigin(), + }, + }, + expected: XhrTestResult.FAILURE, +}), "public https to private: failure."); diff --git a/test/wpt/tests/fetch/range/blob.any.js b/test/wpt/tests/fetch/range/blob.any.js new file mode 100644 index 00000000000..f3eb313b34b --- /dev/null +++ b/test/wpt/tests/fetch/range/blob.any.js @@ -0,0 +1,197 @@ +// META: script=/common/utils.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 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", + }, +]; + +const unsupportedBlobRange = [ + { + name: "Blob range request with multiple range values", + data: ["Multiple ranges are not currently supported"], + type: "text/plain", + headers: { + "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-" + } + }, + { + name: "Blob range request with trailing comma", + data: ["Range with invalid trailing comma"], + type: "text/plain", + headers: { + "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" + } + }, + { + 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" + } + }, + { + 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" + } + }, + { + name: "Blob range should have a dash", + data: ["Blob range should have a dash"], + type: "text/plain" , + headers: { + "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" + } + }, + { + name: "Blob range should include '-'", + data: ["Range end must include '-'"], + type: "text/plain" , + headers: { + "Range": "bytes=x" + } + }, + { + name: "Blob range should include '='", + data: ["Range end must include '='"], + type: "text/plain" , + headers: { + "Range": "bytes 5-" + } + }, + { + name: "Blob range should include 'bytes='", + data: ["Range end must include 'bytes='"], + type: "text/plain" , + headers: { + "Range": "5-" + } + }, + { + name: "Blob content with short content and a large range start", + data: ["Not much here"], + type: "text/plain", + headers: { + "Range": "bytes=100000-", + } + }, +]; + + +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", + "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"); + }); + }, 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, + }); + return promise_rejects_js(test, TypeError, promise); + }, name); +}); diff --git a/test/wpt/tests/fetch/range/data.any.js b/test/wpt/tests/fetch/range/data.any.js new file mode 100644 index 00000000000..22ef11e9317 --- /dev/null +++ b/test/wpt/tests/fetch/range/data.any.js @@ -0,0 +1,29 @@ +// META: script=/common/utils.js + +promise_test(async () => { + return fetch("data:text/plain;charset=US-ASCII,paddingHello%2C%20World%21padding", { + "method": "GET", + "Range": "bytes=13-26" + }).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.type, "basic", "response type is basic"); + assert_equals(resp.headers.get("Content-Type"), "text/plain;charset=US-ASCII", "Content-Type is " + resp.headers.get("Content-Type")); + return resp.text(); + }).then(function(text) { + assert_equals(text, 'paddingHello, World!padding', "Response's body ignores range"); + }); +}, "data: URL and Range header"); + +promise_test(async () => { + return fetch("data:text/plain;charset=US-ASCII,paddingHello%2C%20paddingWorld%21padding", { + "method": "GET", + "Range": "bytes=7-14,21-27" + }).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.type, "basic", "response type is basic"); + assert_equals(resp.headers.get("Content-Type"), "text/plain;charset=US-ASCII", "Content-Type is " + resp.headers.get("Content-Type")); + return resp.text(); + }).then(function(text) { + assert_equals(text, 'paddingHello, paddingWorld!padding', "Response's body ignores range"); + }); +}, "data: URL and Range header with multiple ranges"); diff --git a/test/wpt/tests/fetch/range/general.any.js b/test/wpt/tests/fetch/range/general.any.js new file mode 100644 index 00000000000..64b225a60bd --- /dev/null +++ b/test/wpt/tests/fetch/range/general.any.js @@ -0,0 +1,140 @@ +// META: timeout=long +// META: global=window,worker +// META: script=/common/get-host-info.sub.js +// META: script=/common/utils.js + +// Helpers that return headers objects with a particular guard +function headersGuardNone(fill) { + if (fill) return new Headers(fill); + return new Headers(); +} + +function headersGuardResponse(fill) { + const opts = {}; + if (fill) opts.headers = fill; + return new Response('', opts).headers; +} + +function headersGuardRequest(fill) { + const opts = {}; + if (fill) opts.headers = fill; + return new Request('./', opts).headers; +} + +function headersGuardRequestNoCors(fill) { + const opts = { mode: 'no-cors' }; + if (fill) opts.headers = fill; + return new Request('./', opts).headers; +} + +const headerGuardTypes = [ + ['none', headersGuardNone], + ['response', headersGuardResponse], + ['request', headersGuardRequest] +]; + +for (const [guardType, createHeaders] of headerGuardTypes) { + test(() => { + // There are three ways to set headers. + // Filling, appending, and setting. Test each: + let headers = createHeaders({ Range: 'foo' }); + assert_equals(headers.get('Range'), 'foo'); + + headers = createHeaders(); + headers.append('Range', 'foo'); + assert_equals(headers.get('Range'), 'foo'); + + headers = createHeaders(); + headers.set('Range', 'foo'); + assert_equals(headers.get('Range'), 'foo'); + }, `Range header setting allowed for guard type: ${guardType}`); +} + +test(() => { + let headers = headersGuardRequestNoCors({ Range: 'foo' }); + assert_false(headers.has('Range')); + + headers = headersGuardRequestNoCors(); + headers.append('Range', 'foo'); + assert_false(headers.has('Range')); + + headers = headersGuardRequestNoCors(); + headers.set('Range', 'foo'); + assert_false(headers.has('Range')); +}, `Privileged header not allowed for guard type: request-no-cors`); + +promise_test(async () => { + const wavURL = new URL('resources/long-wav.py', location); + const stashTakeURL = new URL('resources/stash-take.py', location); + + function changeToken() { + const stashToken = token(); + wavURL.searchParams.set('accept-encoding-key', stashToken); + stashTakeURL.searchParams.set('key', stashToken); + } + + const rangeHeaders = [ + 'bytes=0-10', + 'foo=0-10', + 'foo', + '' + ]; + + for (const rangeHeader of rangeHeaders) { + changeToken(); + + await fetch(wavURL, { + headers: { Range: rangeHeader } + }); + + const response = await fetch(stashTakeURL); + + assert_regexp_match(await response.json(), + /.*\bidentity\b.*/, + `Expect identity accept-encoding if range header is ${JSON.stringify(rangeHeader)}`); + } +}, `Fetch with range header will be sent with Accept-Encoding: identity`); + +promise_test(async () => { + const wavURL = new URL(get_host_info().HTTP_REMOTE_ORIGIN + '/fetch/range/resources/long-wav.py'); + const stashTakeURL = new URL('resources/stash-take.py', location); + + function changeToken() { + const stashToken = token(); + wavURL.searchParams.set('accept-encoding-key', stashToken); + stashTakeURL.searchParams.set('key', stashToken); + } + + const rangeHeaders = [ + 'bytes=10-9', + 'bytes=-0', + 'bytes=0000000000000000000000000000000000000000000000000000000000011-0000000000000000000000000000000000000000000000000000000000111', + ]; + + for (const rangeHeader of rangeHeaders) { + changeToken(); + await fetch(wavURL, { headers: { Range : rangeHeader} }).then(() => { throw "loaded with range header " + rangeHeader }, () => { }); + } +}, `Cross Origin Fetch with non safe range header`); + +promise_test(async () => { + const wavURL = new URL(get_host_info().HTTP_REMOTE_ORIGIN + '/fetch/range/resources/long-wav.py'); + const stashTakeURL = new URL('resources/stash-take.py', location); + + function changeToken() { + const stashToken = token(); + wavURL.searchParams.set('accept-encoding-key', stashToken); + stashTakeURL.searchParams.set('key', stashToken); + } + + const rangeHeaders = [ + 'bytes=0-10', + 'bytes=0-', + 'bytes=00000000000000000000000000000000000000000000000000000000011-00000000000000000000000000000000000000000000000000000000000111', + ]; + + for (const rangeHeader of rangeHeaders) { + changeToken(); + await fetch(wavURL, { headers: { Range: rangeHeader } }).then(() => { }, () => { throw "failed load with range header " + rangeHeader }); + } +}, `Cross Origin Fetch with safe range header`); diff --git a/test/wpt/tests/fetch/range/general.window.js b/test/wpt/tests/fetch/range/general.window.js new file mode 100644 index 00000000000..afe80d63a6b --- /dev/null +++ b/test/wpt/tests/fetch/range/general.window.js @@ -0,0 +1,29 @@ +// META: script=resources/utils.js +// META: script=/common/utils.js + +const onload = new Promise(r => window.addEventListener('load', r)); + +// It's weird that browsers do this, but it should continue to work. +promise_test(async t => { + await loadScript('resources/partial-script.py?pretend-offset=90000'); + assert_true(self.scriptExecuted); +}, `Script executed from partial response`); + +promise_test(async () => { + const wavURL = new URL('resources/long-wav.py', location); + const stashTakeURL = new URL('resources/stash-take.py', location); + const stashToken = token(); + wavURL.searchParams.set('accept-encoding-key', stashToken); + stashTakeURL.searchParams.set('key', stashToken); + + // The testing framework waits for window onload. If the audio element + // is appended before onload, it extends it, and the test times out. + await onload; + + const audio = appendAudio(document, wavURL); + await new Promise(r => audio.addEventListener('progress', r)); + audio.remove(); + + const response = await fetch(stashTakeURL); + assert_equals(await response.json(), 'identity', `Expect identity accept-encoding on media request`); +}, `Fetch with range header will be sent with Accept-Encoding: identity`); diff --git a/test/wpt/tests/fetch/range/non-matching-range-response.html b/test/wpt/tests/fetch/range/non-matching-range-response.html new file mode 100644 index 00000000000..ba76c367661 --- /dev/null +++ b/test/wpt/tests/fetch/range/non-matching-range-response.html @@ -0,0 +1,34 @@ + + + + + + + diff --git a/test/wpt/tests/fetch/range/resources/basic.html b/test/wpt/tests/fetch/range/resources/basic.html new file mode 100644 index 00000000000..0e76edd65b7 --- /dev/null +++ b/test/wpt/tests/fetch/range/resources/basic.html @@ -0,0 +1 @@ + diff --git a/test/wpt/tests/fetch/range/resources/long-wav.py b/test/wpt/tests/fetch/range/resources/long-wav.py new file mode 100644 index 00000000000..acfc81a7180 --- /dev/null +++ b/test/wpt/tests/fetch/range/resources/long-wav.py @@ -0,0 +1,134 @@ +""" +This generates a 30 minute silent wav, and is capable of +responding to Range requests. +""" +import time +import re +import struct + +from wptserve.utils import isomorphic_decode + +def create_wav_header(sample_rate, bit_depth, channels, duration): + bytes_per_sample = int(bit_depth / 8) + block_align = bytes_per_sample * channels + byte_rate = sample_rate * block_align + sub_chunk_2_size = duration * byte_rate + + data = b'' + # ChunkID + data += b'RIFF' + # ChunkSize + data += struct.pack(' 0: + to_send = b'\x00' * min(bytes_remaining_to_send, sample_rate) + bytes_remaining_to_send -= len(to_send) + + if not response.writer.write(to_send): + break + + # Throttle the stream + time.sleep(0.5) diff --git a/test/wpt/tests/fetch/range/resources/partial-script.py b/test/wpt/tests/fetch/range/resources/partial-script.py new file mode 100644 index 00000000000..a9570ec355c --- /dev/null +++ b/test/wpt/tests/fetch/range/resources/partial-script.py @@ -0,0 +1,29 @@ +""" +This generates a partial response containing valid JavaScript. +""" + +def main(request, response): + require_range = request.GET.first(b'require-range', b'') + pretend_offset = int(request.GET.first(b'pretend-offset', b'0')) + range_header = request.headers.get(b'Range', b'') + + if require_range and not range_header: + response.set_error(412, u"Range header required") + response.write() + return + + response.headers.set(b"Content-Type", b"text/plain") + response.headers.set(b"Accept-Ranges", b"bytes") + response.headers.set(b"Cache-Control", b"no-cache") + response.status = 206 + + to_send = b'self.scriptExecuted = true;' + length = len(to_send) + + content_range = b"bytes %d-%d/%d" % ( + pretend_offset, pretend_offset + length - 1, pretend_offset + length) + + response.headers.set(b"Content-Range", content_range) + response.headers.set(b"Content-Length", length) + + response.content = to_send diff --git a/test/wpt/tests/fetch/range/resources/partial-text.py b/test/wpt/tests/fetch/range/resources/partial-text.py new file mode 100644 index 00000000000..fa3d1171b68 --- /dev/null +++ b/test/wpt/tests/fetch/range/resources/partial-text.py @@ -0,0 +1,53 @@ +""" +This generates a partial response for a 100-byte text file. +""" +import re + +from wptserve.utils import isomorphic_decode + +def main(request, response): + total_length = int(request.GET.first(b'length', b'100')) + partial_code = int(request.GET.first(b'partial', b'206')) + content_type = request.GET.first(b'type', b'text/plain') + range_header = request.headers.get(b'Range', b'') + + # Send a 200 if there is no range request + if not range_header: + to_send = ''.zfill(total_length) + response.headers.set(b"Content-Type", content_type) + response.headers.set(b"Cache-Control", b"no-cache") + response.headers.set(b"Content-Length", total_length) + response.content = to_send + return + + # Simple range parsing, requires specifically "bytes=xxx-xxxx" + range_header_match = re.search(r'^bytes=(\d*)-(\d*)$', isomorphic_decode(range_header)) + start, end = range_header_match.groups() + start = int(start) + end = int(end) if end else total_length + length = end - start + + # Error the request if the range goes beyond the length + if length <= 0 or end > total_length: + response.set_error(416, u"Range Not Satisfiable") + # set_error sets the MIME type to application/json, which - for a + # no-cors media request - will be blocked by ORB. We'll just force + # the expected MIME type here, whichfixes the test, but doesn't make + # sense in general. + response.headers = [(b"Content-Type", content_type)] + response.write() + return + + # Generate a partial response of the requested length + to_send = ''.zfill(length) + response.headers.set(b"Content-Type", content_type) + response.headers.set(b"Accept-Ranges", b"bytes") + response.headers.set(b"Cache-Control", b"no-cache") + response.status = partial_code + + content_range = b"bytes %d-%d/%d" % (start, end, total_length) + + response.headers.set(b"Content-Range", content_range) + response.headers.set(b"Content-Length", length) + + response.content = to_send diff --git a/test/wpt/tests/fetch/range/resources/range-sw.js b/test/wpt/tests/fetch/range/resources/range-sw.js new file mode 100644 index 00000000000..b47823f03b4 --- /dev/null +++ b/test/wpt/tests/fetch/range/resources/range-sw.js @@ -0,0 +1,218 @@ +importScripts('/resources/testharness.js'); + +setup({ explicit_done: true }); + +function assert_range_request(request, expectedRangeHeader, name) { + assert_equals(request.headers.get('Range'), expectedRangeHeader, name); +} + +async function broadcast(msg) { + for (const client of await clients.matchAll()) { + client.postMessage(msg); + } +} + +addEventListener('fetch', async event => { + /** @type Request */ + const request = event.request; + const url = new URL(request.url); + const action = url.searchParams.get('action'); + + switch (action) { + case 'range-header-filter-test': + rangeHeaderFilterTest(request); + return; + case 'range-header-passthrough-test': + rangeHeaderPassthroughTest(event); + return; + case 'store-ranged-response': + storeRangedResponse(event); + return; + case 'use-stored-ranged-response': + useStoredRangeResponse(event); + return; + case 'broadcast-accept-encoding': + broadcastAcceptEncoding(event); + return; + case 'record-media-range-request': + return recordMediaRangeRequest(event); + case 'use-media-range-request': + useMediaRangeRequest(event); + return; + } +}); + +/** + * @param {Request} request + */ +function rangeHeaderFilterTest(request) { + const rangeValue = request.headers.get('Range'); + + test(() => { + assert_range_request(new Request(request), rangeValue, `Untampered`); + assert_range_request(new Request(request, {}), rangeValue, `Untampered (no init props set)`); + assert_range_request(new Request(request, { __foo: 'bar' }), rangeValue, `Untampered (only invalid props set)`); + assert_range_request(new Request(request, { mode: 'cors' }), rangeValue, `More permissive mode`); + assert_range_request(request.clone(), rangeValue, `Clone`); + }, "Range headers correctly preserved"); + + test(() => { + assert_range_request(new Request(request, { headers: { Range: 'foo' } }), null, `Tampered - range header set`); + assert_range_request(new Request(request, { headers: {} }), null, `Tampered - empty headers set`); + assert_range_request(new Request(request, { mode: 'no-cors' }), null, `Tampered – mode set`); + assert_range_request(new Request(request, { cache: 'no-cache' }), null, `Tampered – cache mode set`); + }, "Range headers correctly removed"); + + test(() => { + let headers; + + headers = new Request(request).headers; + headers.delete('does-not-exist'); + assert_equals(headers.get('Range'), rangeValue, `Preserved if no header actually removed`); + + headers = new Request(request).headers; + headers.append('foo', 'bar'); + assert_equals(headers.get('Range'), rangeValue, `Preserved if silent-failure on append (due to request-no-cors guard)`); + + headers = new Request(request).headers; + headers.set('foo', 'bar'); + assert_equals(headers.get('Range'), rangeValue, `Preserved if silent-failure on set (due to request-no-cors guard)`); + + headers = new Request(request).headers; + headers.append('Range', 'foo'); + assert_equals(headers.get('Range'), rangeValue, `Preserved if silent-failure on append (due to request-no-cors guard)`); + + headers = new Request(request).headers; + headers.set('Range', 'foo'); + assert_equals(headers.get('Range'), rangeValue, `Preserved if silent-failure on set (due to request-no-cors guard)`); + + headers = new Request(request).headers; + headers.append('Accept', 'whatever'); + assert_equals(headers.get('Range'), null, `Stripped if header successfully appended`); + + headers = new Request(request).headers; + headers.set('Accept', 'whatever'); + assert_equals(headers.get('Range'), null, `Stripped if header successfully set`); + + headers = new Request(request).headers; + headers.delete('Accept'); + assert_equals(headers.get('Range'), null, `Stripped if header successfully deleted`); + + headers = new Request(request).headers; + headers.delete('Range'); + assert_equals(headers.get('Range'), null, `Stripped if range header successfully deleted`); + }, "Headers correctly filtered"); + + done(); +} + +function rangeHeaderPassthroughTest(event) { + /** @type Request */ + const request = event.request; + const url = new URL(request.url); + const key = url.searchParams.get('range-received-key'); + + event.waitUntil(new Promise(resolve => { + promise_test(async () => { + await fetch(event.request); + const response = await fetch('stash-take.py?key=' + key); + assert_equals(await response.json(), 'range-header-received'); + resolve(); + }, `Include range header in network request`); + + done(); + })); + + // Just send back any response, it isn't important for the test. + event.respondWith(new Response('')); +} + +let storedRangeResponseP; + +function storeRangedResponse(event) { + /** @type Request */ + const request = event.request; + const id = new URL(request.url).searchParams.get('id'); + + storedRangeResponseP = fetch(event.request); + broadcast({ id }); + + // Just send back any response, it isn't important for the test. + event.respondWith(new Response('')); +} + +function useStoredRangeResponse(event) { + event.respondWith(async function() { + const response = await storedRangeResponseP; + if (!response) throw Error("Expected stored range response"); + return response.clone(); + }()); +} + +function broadcastAcceptEncoding(event) { + /** @type Request */ + const request = event.request; + const id = new URL(request.url).searchParams.get('id'); + + broadcast({ + id, + acceptEncoding: request.headers.get('Accept-Encoding') + }); + + // Just send back any response, it isn't important for the test. + event.respondWith(new Response('')); +} + +let rangeResponse = {}; + +async function recordMediaRangeRequest(event) { + /** @type Request */ + const request = event.request; + const url = new URL(request.url); + const urlParams = new URLSearchParams(url.search); + const size = urlParams.get("size"); + const id = urlParams.get('id'); + const key = 'size' + size; + + if (key in rangeResponse) { + // Don't re-fetch ranges we already have. + const clonedResponse = rangeResponse[key].clone(); + event.respondWith(clonedResponse); + } else if (event.request.headers.get("range") === "bytes=0-") { + // Generate a bogus 206 response to trigger subsequent range requests + // of the desired size. + const length = urlParams.get("length") + 100; + const body = "A".repeat(Number(size)); + event.respondWith(new Response(body, {status: 206, headers: { + "Content-Type": "audio/mp4", + "Content-Range": `bytes 0-1/${length}` + }})); + } else if (event.request.headers.get("range") === `bytes=${Number(size)}-`) { + // Pass through actual range requests which will attempt to fetch up to the + // length in the original response which is bigger than the actual resource + // to make sure 206 and 416 responses are treated the same. + rangeResponse[key] = await fetch(event.request); + + // Let the client know we have the range response for the given ID + broadcast({id}); + } else { + event.respondWith(Promise.reject(Error("Invalid Request"))); + } +} + +function useMediaRangeRequest(event) { + /** @type Request */ + const request = event.request; + const url = new URL(request.url); + const urlParams = new URLSearchParams(url.search); + const size = urlParams.get("size"); + const key = 'size' + size; + + // Send a clone of the range response to preload. + if (key in rangeResponse) { + const clonedResponse = rangeResponse[key].clone(); + event.respondWith(clonedResponse); + } else { + event.respondWith(Promise.reject(Error("Invalid Request"))); + } +} diff --git a/test/wpt/tests/fetch/range/resources/stash-take.py b/test/wpt/tests/fetch/range/resources/stash-take.py new file mode 100644 index 00000000000..6cf6ff585bf --- /dev/null +++ b/test/wpt/tests/fetch/range/resources/stash-take.py @@ -0,0 +1,7 @@ +from wptserve.handlers import json_handler + + +@json_handler +def main(request, response): + key = request.GET.first(b"key") + return request.server.stash.take(key, b'/fetch/range/') diff --git a/test/wpt/tests/fetch/range/resources/utils.js b/test/wpt/tests/fetch/range/resources/utils.js new file mode 100644 index 00000000000..ad2853b33dc --- /dev/null +++ b/test/wpt/tests/fetch/range/resources/utils.js @@ -0,0 +1,36 @@ +function loadScript(url, { doc = document }={}) { + return new Promise((resolve, reject) => { + const script = doc.createElement('script'); + script.onload = () => resolve(); + script.onerror = () => reject(Error("Script load failed")); + script.src = url; + doc.body.appendChild(script); + }) +} + +function preloadImage(url, { doc = document }={}) { + return new Promise((resolve, reject) => { + const preload = doc.createElement('link'); + preload.rel = 'preload'; + preload.as = 'image'; + preload.onload = () => resolve(); + preload.onerror = () => resolve(); + preload.href = url; + doc.body.appendChild(preload); + }) +} + +/** + * + * @param {Document} document + * @param {string|URL} url + * @returns {HTMLAudioElement} + */ +function appendAudio(document, url) { + const audio = document.createElement('audio'); + audio.muted = true; + audio.src = url; + audio.preload = true; + document.body.appendChild(audio); + return audio; +} diff --git a/test/wpt/tests/fetch/range/resources/video-with-range.py b/test/wpt/tests/fetch/range/resources/video-with-range.py new file mode 100644 index 00000000000..2d15ccf3c43 --- /dev/null +++ b/test/wpt/tests/fetch/range/resources/video-with-range.py @@ -0,0 +1,43 @@ +import re +import os +import json +from wptserve.utils import isomorphic_decode + +def main(request, response): + path = os.path.join(request.doc_root, u"media", "sine440.mp3") + total_size = os.path.getsize(path) + rewrites = json.loads(request.GET.first(b'rewrites', '[]')) + range_header = request.headers.get(b'Range') + range_header_match = range_header and re.search(r'^bytes=(\d*)-(\d*)$', isomorphic_decode(range_header)) + start = None + end = None + if range_header_match: + response.status = 206 + start, end = range_header_match.groups() + if range_header: + status = 206 + else: + status = 200 + for rewrite in rewrites: + req_start, req_end = rewrite['request'] + if start == req_start or req_start == '*': + if end == req_end or req_end == '*': + if 'response' in rewrite: + start, end = rewrite['response'] + if 'status' in rewrite: + status = rewrite['status'] + + start = int(start or 0) + end = int(end or total_size) + headers = [] + if status == 206: + headers.append((b"Content-Range", b"bytes %d-%d/%d" % (start, end - 1, total_size))) + headers.append((b"Accept-Ranges", b"bytes")) + + headers.append((b"Content-Type", b"audio/mp3")) + headers.append((b"Content-Length", str(end - start))) + headers.append((b"Cache-Control", b"no-cache")) + video_file = open(path, "rb") + video_file.seek(start) + content = video_file.read(end) + return status, headers, content diff --git a/test/wpt/tests/fetch/range/sw.https.window.js b/test/wpt/tests/fetch/range/sw.https.window.js new file mode 100644 index 00000000000..62ad894da3d --- /dev/null +++ b/test/wpt/tests/fetch/range/sw.https.window.js @@ -0,0 +1,228 @@ +// META: script=../../../service-workers/service-worker/resources/test-helpers.sub.js +// META: script=/common/utils.js +// META: script=/common/get-host-info.sub.js +// META: script=resources/utils.js + +const { REMOTE_HOST } = get_host_info(); +const BASE_SCOPE = 'resources/basic.html?'; + +async function cleanup() { + for (const iframe of document.querySelectorAll('.test-iframe')) { + iframe.parentNode.removeChild(iframe); + } + + for (const reg of await navigator.serviceWorker.getRegistrations()) { + await reg.unregister(); + } +} + +async function setupRegistration(t, scope) { + await cleanup(); + const reg = await navigator.serviceWorker.register('resources/range-sw.js', { scope }); + await wait_for_state(t, reg.installing, 'activated'); + return reg; +} + +function awaitMessage(obj, id) { + return new Promise(resolve => { + obj.addEventListener('message', function listener(event) { + if (event.data.id !== id) return; + obj.removeEventListener('message', listener); + resolve(event.data); + }); + }); +} + +promise_test(async t => { + const scope = BASE_SCOPE + Math.random(); + const reg = await setupRegistration(t, scope); + const iframe = await with_iframe(scope); + const w = iframe.contentWindow; + + // Trigger a cross-origin range request using media + const url = new URL('long-wav.py?action=range-header-filter-test', w.location); + url.hostname = REMOTE_HOST; + appendAudio(w.document, url); + + // See rangeHeaderFilterTest in resources/range-sw.js + await fetch_tests_from_worker(reg.active); +}, `Defer range header filter tests to service worker`); + +promise_test(async t => { + const scope = BASE_SCOPE + Math.random(); + const reg = await setupRegistration(t, scope); + const iframe = await with_iframe(scope); + const w = iframe.contentWindow; + + // Trigger a cross-origin range request using media + const url = new URL('long-wav.py', w.location); + url.searchParams.set('action', 'range-header-passthrough-test'); + url.searchParams.set('range-received-key', token()); + url.hostname = REMOTE_HOST; + appendAudio(w.document, url); + + // See rangeHeaderPassthroughTest in resources/range-sw.js + await fetch_tests_from_worker(reg.active); +}, `Defer range header passthrough tests to service worker`); + +promise_test(async t => { + const scope = BASE_SCOPE + Math.random(); + await setupRegistration(t, scope); + const iframe = await with_iframe(scope); + const w = iframe.contentWindow; + const id = Math.random() + ''; + const storedRangeResponse = awaitMessage(w.navigator.serviceWorker, id); + + // Trigger a cross-origin range request using media + const url = new URL('partial-script.py', w.location); + url.searchParams.set('require-range', '1'); + url.searchParams.set('action', 'store-ranged-response'); + url.searchParams.set('id', id); + url.hostname = REMOTE_HOST; + + appendAudio(w.document, url); + + await storedRangeResponse; + + // Fetching should reject + const fetchPromise = w.fetch('?action=use-stored-ranged-response', { mode: 'no-cors' }); + await promise_rejects_js(t, w.TypeError, fetchPromise); + + // Script loading should error too + const loadScriptPromise = loadScript('?action=use-stored-ranged-response', { doc: w.document }); + await promise_rejects_js(t, Error, loadScriptPromise); + + await loadScriptPromise.catch(() => {}); + + assert_false(!!w.scriptExecuted, `Partial response shouldn't be executed`); +}, `Ranged response not allowed following no-cors ranged request`); + +promise_test(async t => { + const scope = BASE_SCOPE + Math.random(); + await setupRegistration(t, scope); + const iframe = await with_iframe(scope); + const w = iframe.contentWindow; + const id = Math.random() + ''; + const storedRangeResponse = awaitMessage(w.navigator.serviceWorker, id); + + // Trigger a range request using media + const url = new URL('partial-script.py', w.location); + url.searchParams.set('require-range', '1'); + url.searchParams.set('action', 'store-ranged-response'); + url.searchParams.set('id', id); + + appendAudio(w.document, url); + + await storedRangeResponse; + + // This should not throw + await w.fetch('?action=use-stored-ranged-response'); + + // This shouldn't throw either + await loadScript('?action=use-stored-ranged-response', { doc: w.document }); + + assert_true(w.scriptExecuted, `Partial response should be executed`); +}, `Non-opaque ranged response executed`); + +promise_test(async t => { + const scope = BASE_SCOPE + Math.random(); + await setupRegistration(t, scope); + const iframe = await with_iframe(scope); + const w = iframe.contentWindow; + const fetchId = Math.random() + ''; + const fetchBroadcast = awaitMessage(w.navigator.serviceWorker, fetchId); + const audioId = Math.random() + ''; + const audioBroadcast = awaitMessage(w.navigator.serviceWorker, audioId); + + const url = new URL('long-wav.py', w.location); + url.searchParams.set('action', 'broadcast-accept-encoding'); + url.searchParams.set('id', fetchId); + + await w.fetch(url, { + headers: { Range: 'bytes=0-10' } + }); + + assert_equals((await fetchBroadcast).acceptEncoding, null, "Accept-Encoding should not be set for fetch"); + + url.searchParams.set('id', audioId); + appendAudio(w.document, url); + + assert_equals((await audioBroadcast).acceptEncoding, null, "Accept-Encoding should not be set for media"); +}, `Accept-Encoding should not appear in a service worker`); + +promise_test(async t => { + const scope = BASE_SCOPE + Math.random(); + await setupRegistration(t, scope); + const iframe = await with_iframe(scope); + const w = iframe.contentWindow; + const length = 100; + const count = 3; + const counts = {}; + + // test a single range request size + async function testSizedRange(size, partialResponseCode) { + const rangeId = Math.random() + ''; + const rangeBroadcast = awaitMessage(w.navigator.serviceWorker, rangeId); + + // Create a bogus audio element to trick the browser into sending + // cross-origin range requests that can be manipulated by the service worker. + const sound_url = new URL('partial-text.py', w.location); + sound_url.hostname = REMOTE_HOST; + sound_url.searchParams.set('action', 'record-media-range-request'); + sound_url.searchParams.set('length', length); + sound_url.searchParams.set('size', size); + sound_url.searchParams.set('partial', partialResponseCode); + sound_url.searchParams.set('id', rangeId); + sound_url.searchParams.set('type', 'audio/mp4'); + appendAudio(w.document, sound_url); + + // wait for the range requests to happen + await rangeBroadcast; + + // Create multiple preload requests and count the number of resource timing + // entries that get created to make sure 206 and 416 range responses are treated + // the same. + const url = new URL('partial-text.py', w.location); + url.searchParams.set('action', 'use-media-range-request'); + url.searchParams.set('size', size); + url.searchParams.set('type', 'audio/mp4'); + counts['size' + size] = 0; + for (let i = 0; i < count; i++) { + await preloadImage(url, { doc: w.document }); + } + } + + // Test range requests from 1 smaller than the correct size to 1 larger than + // the correct size to exercise the various permutations using the default 206 + // response code for successful range requests. + for (let size = length - 1; size <= length + 1; size++) { + await testSizedRange(size, '206'); + } + + // Test a successful range request using a 200 response. + await testSizedRange(length - 2, '200'); + + // Check the resource timing entries and count the reported number of fetches of each type + const resources = w.performance.getEntriesByType("resource"); + for (const entry of resources) { + const url = new URL(entry.name); + if (url.searchParams.has('action') && + url.searchParams.get('action') == 'use-media-range-request' && + url.searchParams.has('size')) { + counts['size' + url.searchParams.get('size')]++; + } + } + + // Make sure there are a non-zero number of preload requests and they are all the same + let counts_valid = true; + const first = 'size' + (length - 2); + for (let size = length - 2; size <= length + 1; size++) { + let key = 'size' + size; + if (!(key in counts) || counts[key] <= 0 || counts[key] != counts[first]) { + counts_valid = false; + break; + } + } + + assert_true(counts_valid, `Opaque range request preloads were different for error and success`); +}, `Opaque range preload successes and failures should be indistinguishable`); diff --git a/test/wpt/tests/fetch/redirect-navigate/302-found-post-handler.py b/test/wpt/tests/fetch/redirect-navigate/302-found-post-handler.py new file mode 100644 index 00000000000..40a224f6565 --- /dev/null +++ b/test/wpt/tests/fetch/redirect-navigate/302-found-post-handler.py @@ -0,0 +1,15 @@ +from wptserve.utils import isomorphic_encode + +def main(request, response): + if request.method == u"POST": + response.add_required_headers = False + response.writer.write_status(302) + response.writer.write_header(b"Location", isomorphic_encode(request.url)) + response.writer.end_headers() + response.writer.write(b"") + elif request.method == u"GET": + return ([(b"Content-Type", b"text/plain")], + b"OK") + else: + return ([(b"Content-Type", b"text/plain")], + b"FAIL") \ No newline at end of file diff --git a/test/wpt/tests/fetch/redirect-navigate/302-found-post.html b/test/wpt/tests/fetch/redirect-navigate/302-found-post.html new file mode 100644 index 00000000000..854cd329a8f --- /dev/null +++ b/test/wpt/tests/fetch/redirect-navigate/302-found-post.html @@ -0,0 +1,20 @@ + + +HTTP 302 Found POST Navigation Test + + + + + diff --git a/test/wpt/tests/fetch/redirect-navigate/preserve-fragment.html b/test/wpt/tests/fetch/redirect-navigate/preserve-fragment.html new file mode 100644 index 00000000000..682539a7445 --- /dev/null +++ b/test/wpt/tests/fetch/redirect-navigate/preserve-fragment.html @@ -0,0 +1,202 @@ + + + + + Ensure fragment is kept across redirects + + + + + + + + + + + + + + diff --git a/test/wpt/tests/fetch/redirect-navigate/resources/destination.html b/test/wpt/tests/fetch/redirect-navigate/resources/destination.html new file mode 100644 index 00000000000..f98c5a8cd77 --- /dev/null +++ b/test/wpt/tests/fetch/redirect-navigate/resources/destination.html @@ -0,0 +1,28 @@ + + + + + + + + +

Target

+

Target

+ + diff --git a/test/wpt/tests/fetch/redirects/data.window.js b/test/wpt/tests/fetch/redirects/data.window.js new file mode 100644 index 00000000000..eeb41966b44 --- /dev/null +++ b/test/wpt/tests/fetch/redirects/data.window.js @@ -0,0 +1,25 @@ +// See ../api/redirect/redirect-to-dataurl.any.js for fetch() tests + +async_test(t => { + const img = document.createElement("img"); + img.onload = t.unreached_func(); + img.onerror = t.step_func_done(); + img.src = "../api/resources/redirect.py?location=data:image/png%3Bbase64,iVBORw0KGgoAAAANSUhEUgAAAIUAAABqCAIAAAAdqgU8AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAF6SURBVHhe7dNBDQAADIPA%2Bje92eBxSQUQSLedlQzo0TLQonFWPVoGWjT%2BoUfLQIvGP/RoGWjR%2BIceLQMtGv/Qo2WgReMferQMtGj8Q4%2BWgRaNf%2BjRMtCi8Q89WgZaNP6hR8tAi8Y/9GgZaNH4hx4tAy0a/9CjZaBF4x96tAy0aPxDj5aBFo1/6NEy0KLxDz1aBlo0/qFHy0CLxj/0aBlo0fiHHi0DLRr/0KNloEXjH3q0DLRo/EOPloEWjX/o0TLQovEPPVoGWjT%2BoUfLQIvGP/RoGWjR%2BIceLQMtGv/Qo2WgReMferQMtGj8Q4%2BWgRaNf%2BjRMtCi8Q89WgZaNP6hR8tAi8Y/9GgZaNH4hx4tAy0a/9CjZaBF4x96tAy0aPxDj5aBFo1/6NEy0KLxDz1aBlo0/qFHy0CLxj/0aBlo0fiHHi0DLRr/0KNloEXjH3q0DLRo/EOPloEWjX/o0TLQovEPPVoGWjT%2BoUfLQIvGP/RoGWjR%2BIceJQMPIOzeGc0PIDEAAAAASUVORK5CYII"; +}, " fetch that redirects to data: URL"); + +globalThis.globalTest = null; +async_test(t => { + globalThis.globalTest = t; + const script = document.createElement("script"); + script.src = "../api/resources/redirect.py?location=data:text/javascript,(globalThis.globalTest.unreached_func())()"; + script.onerror = t.step_func_done(); + document.body.append(script); +}, " + + +
+ + + + + + + diff --git a/test/wpt/tests/fetch/security/1xx-response.any.js b/test/wpt/tests/fetch/security/1xx-response.any.js new file mode 100644 index 00000000000..df4dafcd80b --- /dev/null +++ b/test/wpt/tests/fetch/security/1xx-response.any.js @@ -0,0 +1,28 @@ +promise_test(async (t) => { + // The 100 response should be ignored, then the transaction ends, which + // should lead to an error. + await promise_rejects_js( + t, TypeError, fetch('/common/text-plain.txt?pipe=status(100)')); +}, 'Status(100) should be ignored.'); + +// This behavior is being discussed at https://github.com/whatwg/fetch/issues/1397. +promise_test(async (t) => { + const res = await fetch('/common/text-plain.txt?pipe=status(101)'); + assert_equals(res.status, 101); + const body = await res.text(); + assert_equals(body, ''); +}, 'Status(101) should be accepted, with removing body.'); + +promise_test(async (t) => { + // The 103 response should be ignored, then the transaction ends, which + // should lead to an error. + await promise_rejects_js( + t, TypeError, fetch('/common/text-plain.txt?pipe=status(103)')); +}, 'Status(103) should be ignored.'); + +promise_test(async (t) => { + // The 199 response should be ignored, then the transaction ends, which + // should lead to an error. + await promise_rejects_js( + t, TypeError, fetch('/common/text-plain.txt?pipe=status(199)')); +}, 'Status(199) should be ignored.'); diff --git a/test/wpt/tests/fetch/security/dangling-markup-mitigation-data-url.tentative.sub.html b/test/wpt/tests/fetch/security/dangling-markup-mitigation-data-url.tentative.sub.html new file mode 100644 index 00000000000..f27735daa1d --- /dev/null +++ b/test/wpt/tests/fetch/security/dangling-markup-mitigation-data-url.tentative.sub.html @@ -0,0 +1,229 @@ + + + + + diff --git a/test/wpt/tests/fetch/security/dangling-markup-mitigation.tentative.html b/test/wpt/tests/fetch/security/dangling-markup-mitigation.tentative.html new file mode 100644 index 00000000000..61a931608ba --- /dev/null +++ b/test/wpt/tests/fetch/security/dangling-markup-mitigation.tentative.html @@ -0,0 +1,147 @@ + + + + + diff --git a/test/wpt/tests/fetch/security/embedded-credentials.tentative.sub.html b/test/wpt/tests/fetch/security/embedded-credentials.tentative.sub.html new file mode 100644 index 00000000000..ca5ee1c87bd --- /dev/null +++ b/test/wpt/tests/fetch/security/embedded-credentials.tentative.sub.html @@ -0,0 +1,89 @@ + + + + + diff --git a/test/wpt/tests/fetch/security/redirect-to-url-with-credentials.https.html b/test/wpt/tests/fetch/security/redirect-to-url-with-credentials.https.html new file mode 100644 index 00000000000..b06464805c2 --- /dev/null +++ b/test/wpt/tests/fetch/security/redirect-to-url-with-credentials.https.html @@ -0,0 +1,68 @@ + +
+ + + +
+ + + + diff --git a/test/wpt/tests/fetch/security/support/embedded-credential-window.sub.html b/test/wpt/tests/fetch/security/support/embedded-credential-window.sub.html new file mode 100644 index 00000000000..20d307e9188 --- /dev/null +++ b/test/wpt/tests/fetch/security/support/embedded-credential-window.sub.html @@ -0,0 +1,19 @@ + + diff --git a/test/wpt/tests/fetch/stale-while-revalidate/fetch-sw.https.html b/test/wpt/tests/fetch/stale-while-revalidate/fetch-sw.https.html new file mode 100644 index 00000000000..efcebc24a63 --- /dev/null +++ b/test/wpt/tests/fetch/stale-while-revalidate/fetch-sw.https.html @@ -0,0 +1,65 @@ + + + + + Stale Revalidation Requests don't get sent to service worker + + + + + + + + + diff --git a/test/wpt/tests/fetch/stale-while-revalidate/fetch.any.js b/test/wpt/tests/fetch/stale-while-revalidate/fetch.any.js new file mode 100644 index 00000000000..3682b9d2c31 --- /dev/null +++ b/test/wpt/tests/fetch/stale-while-revalidate/fetch.any.js @@ -0,0 +1,32 @@ +// META: global=window,worker +// META: title=Tests Stale While Revalidate is executed for fetch API +// META: script=/common/utils.js + +function wait25ms(test) { + return new Promise(resolve => { + test.step_timeout(() => { + resolve(); + }, 25); + }); +} + +promise_test(async (test) => { + var request_token = token(); + + const response = await fetch(`resources/stale-script.py?token=` + request_token); + // Wait until resource is completely fetched to allow caching before next fetch. + const body = await response.text(); + const response2 = await fetch(`resources/stale-script.py?token=` + request_token); + + assert_equals(response.headers.get('Unique-Id'), response2.headers.get('Unique-Id')); + const body2 = await response2.text(); + assert_equals(body, body2); + + while(true) { + const revalidation_check = await fetch(`resources/stale-script.py?query&token=` + request_token); + if (revalidation_check.headers.get('Count') == '2') { + break; + } + await wait25ms(test); + } +}, 'Second fetch returns same response'); diff --git a/test/wpt/tests/fetch/stale-while-revalidate/resources/stale-css.py b/test/wpt/tests/fetch/stale-while-revalidate/resources/stale-css.py new file mode 100644 index 00000000000..b87668373ac --- /dev/null +++ b/test/wpt/tests/fetch/stale-while-revalidate/resources/stale-css.py @@ -0,0 +1,28 @@ +def main(request, response): + + token = request.GET.first(b"token", None) + is_query = request.GET.first(b"query", None) != None + with request.server.stash.lock: + value = request.server.stash.take(token) + count = 0 + if value != None: + count = int(value) + if is_query: + if count < 2: + request.server.stash.put(token, count) + else: + count = count + 1 + request.server.stash.put(token, count) + if is_query: + headers = [(b"Count", count)] + content = b"" + return 200, headers, content + else: + content = b"body { background: rgb(0, 128, 0); }" + if count > 1: + content = b"body { background: rgb(255, 0, 0); }" + + headers = [(b"Content-Type", b"text/css"), + (b"Cache-Control", b"private, max-age=0, stale-while-revalidate=60")] + + return 200, headers, content diff --git a/test/wpt/tests/fetch/stale-while-revalidate/resources/stale-image.py b/test/wpt/tests/fetch/stale-while-revalidate/resources/stale-image.py new file mode 100644 index 00000000000..36e6fc0c9bb --- /dev/null +++ b/test/wpt/tests/fetch/stale-while-revalidate/resources/stale-image.py @@ -0,0 +1,40 @@ +import os.path + +from wptserve.utils import isomorphic_decode + +def main(request, response): + + token = request.GET.first(b"token", None) + is_query = request.GET.first(b"query", None) != None + with request.server.stash.lock: + value = request.server.stash.take(token) + count = 0 + if value != None: + count = int(value) + if is_query: + if count < 2: + request.server.stash.put(token, count) + else: + count = count + 1 + request.server.stash.put(token, count) + + if is_query: + headers = [(b"Count", count)] + content = b"" + return 200, headers, content + else: + filename = u"green-16x16.png" + if count > 1: + filename = u"green-256x256.png" + + path = os.path.join(os.path.dirname(isomorphic_decode(__file__)), u"../../../images", filename) + body = open(path, "rb").read() + + response.add_required_headers = False + response.writer.write_status(200) + response.writer.write_header(b"content-length", len(body)) + response.writer.write_header(b"Cache-Control", b"private, max-age=0, stale-while-revalidate=60") + response.writer.write_header(b"content-type", b"image/png") + response.writer.end_headers() + + response.writer.write(body) diff --git a/test/wpt/tests/fetch/stale-while-revalidate/resources/stale-script.py b/test/wpt/tests/fetch/stale-while-revalidate/resources/stale-script.py new file mode 100644 index 00000000000..731cd805654 --- /dev/null +++ b/test/wpt/tests/fetch/stale-while-revalidate/resources/stale-script.py @@ -0,0 +1,32 @@ +import random, string + +def id_token(): + letters = string.ascii_lowercase + return b''.join(random.choice(letters).encode("utf-8") for i in range(20)) + +def main(request, response): + token = request.GET.first(b"token", None) + is_query = request.GET.first(b"query", None) != None + with request.server.stash.lock: + value = request.server.stash.take(token) + count = 0 + if value != None: + count = int(value) + if is_query: + if count < 2: + request.server.stash.put(token, count) + else: + count = count + 1 + request.server.stash.put(token, count) + + if is_query: + headers = [(b"Count", count)] + content = u"" + return 200, headers, content + else: + unique_id = id_token() + headers = [(b"Content-Type", b"text/javascript"), + (b"Cache-Control", b"private, max-age=0, stale-while-revalidate=60"), + (b"Unique-Id", unique_id)] + content = b"report('%s')" % unique_id + return 200, headers, content diff --git a/test/wpt/tests/fetch/stale-while-revalidate/revalidate-not-blocked-by-csp.html b/test/wpt/tests/fetch/stale-while-revalidate/revalidate-not-blocked-by-csp.html new file mode 100644 index 00000000000..ea70b9a9c76 --- /dev/null +++ b/test/wpt/tests/fetch/stale-while-revalidate/revalidate-not-blocked-by-csp.html @@ -0,0 +1,69 @@ + + +Test revalidations requests aren't blocked by CSP. + + + + + + diff --git a/test/wpt/tests/fetch/stale-while-revalidate/stale-css.html b/test/wpt/tests/fetch/stale-while-revalidate/stale-css.html new file mode 100644 index 00000000000..603a60c8bba --- /dev/null +++ b/test/wpt/tests/fetch/stale-while-revalidate/stale-css.html @@ -0,0 +1,51 @@ + + +Tests Stale While Revalidate works for css + + + + + + diff --git a/test/wpt/tests/fetch/stale-while-revalidate/stale-image.html b/test/wpt/tests/fetch/stale-while-revalidate/stale-image.html new file mode 100644 index 00000000000..d86bdfbde2c --- /dev/null +++ b/test/wpt/tests/fetch/stale-while-revalidate/stale-image.html @@ -0,0 +1,55 @@ + + +Tests Stale While Revalidate works for images + + + + + + + + + diff --git a/test/wpt/tests/fetch/stale-while-revalidate/stale-script.html b/test/wpt/tests/fetch/stale-while-revalidate/stale-script.html new file mode 100644 index 00000000000..f5317482c48 --- /dev/null +++ b/test/wpt/tests/fetch/stale-while-revalidate/stale-script.html @@ -0,0 +1,59 @@ + + +Tests Stale While Revalidate works for scripts + + + + + + diff --git a/test/wpt/tests/fetch/stale-while-revalidate/sw-intercept.js b/test/wpt/tests/fetch/stale-while-revalidate/sw-intercept.js new file mode 100644 index 00000000000..dca7de51b0b --- /dev/null +++ b/test/wpt/tests/fetch/stale-while-revalidate/sw-intercept.js @@ -0,0 +1,14 @@ +async function broadcast(msg) { + for (const client of await clients.matchAll()) { + client.postMessage(msg); + } +} + +self.addEventListener('fetch', event => { + event.waitUntil(broadcast(event.request.url)); + event.respondWith(fetch(event.request)); +}); + +self.addEventListener('activate', event => { + self.clients.claim(); +}); diff --git a/test/wpt/tests/interfaces/ANGLE_instanced_arrays.idl b/test/wpt/tests/interfaces/ANGLE_instanced_arrays.idl new file mode 100644 index 00000000000..557a4163331 --- /dev/null +++ b/test/wpt/tests/interfaces/ANGLE_instanced_arrays.idl @@ -0,0 +1,12 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: WebGL ANGLE_instanced_arrays Khronos Ratified Extension Specification (https://registry.khronos.org/webgl/extensions/ANGLE_instanced_arrays/) + +[Exposed=(Window,Worker), LegacyNoInterfaceObject] +interface ANGLE_instanced_arrays { + const GLenum VERTEX_ATTRIB_ARRAY_DIVISOR_ANGLE = 0x88FE; + undefined drawArraysInstancedANGLE(GLenum mode, GLint first, GLsizei count, GLsizei primcount); + undefined drawElementsInstancedANGLE(GLenum mode, GLsizei count, GLenum type, GLintptr offset, GLsizei primcount); + undefined vertexAttribDivisorANGLE(GLuint index, GLuint divisor); +}; diff --git a/test/wpt/tests/interfaces/CSP.idl b/test/wpt/tests/interfaces/CSP.idl new file mode 100644 index 00000000000..ac0a6ff5638 --- /dev/null +++ b/test/wpt/tests/interfaces/CSP.idl @@ -0,0 +1,56 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: Content Security Policy Level 3 (https://w3c.github.io/webappsec-csp/) + +[Exposed=Window] +interface CSPViolationReportBody : ReportBody { + [Default] object toJSON(); + readonly attribute USVString documentURL; + readonly attribute USVString? referrer; + readonly attribute USVString? blockedURL; + readonly attribute DOMString effectiveDirective; + readonly attribute DOMString originalPolicy; + readonly attribute USVString? sourceFile; + readonly attribute DOMString? sample; + readonly attribute SecurityPolicyViolationEventDisposition disposition; + readonly attribute unsigned short statusCode; + readonly attribute unsigned long? lineNumber; + readonly attribute unsigned long? columnNumber; +}; + +enum SecurityPolicyViolationEventDisposition { + "enforce", "report" +}; + +[Exposed=(Window,Worker)] +interface SecurityPolicyViolationEvent : Event { + constructor(DOMString type, optional SecurityPolicyViolationEventInit eventInitDict = {}); + readonly attribute USVString documentURI; + readonly attribute USVString referrer; + readonly attribute USVString blockedURI; + readonly attribute DOMString effectiveDirective; + readonly attribute DOMString violatedDirective; // historical alias of effectiveDirective + readonly attribute DOMString originalPolicy; + readonly attribute USVString sourceFile; + readonly attribute DOMString sample; + readonly attribute SecurityPolicyViolationEventDisposition disposition; + readonly attribute unsigned short statusCode; + readonly attribute unsigned long lineNumber; + readonly attribute unsigned long columnNumber; +}; + +dictionary SecurityPolicyViolationEventInit : EventInit { + required USVString documentURI; + USVString referrer = ""; + USVString blockedURI = ""; + required DOMString violatedDirective; + required DOMString effectiveDirective; + required DOMString originalPolicy; + USVString sourceFile = ""; + DOMString sample = ""; + required SecurityPolicyViolationEventDisposition disposition; + required unsigned short statusCode; + unsigned long lineNumber = 0; + unsigned long columnNumber = 0; +}; diff --git a/test/wpt/tests/interfaces/DOM-Parsing.idl b/test/wpt/tests/interfaces/DOM-Parsing.idl new file mode 100644 index 00000000000..d0d84ab6972 --- /dev/null +++ b/test/wpt/tests/interfaces/DOM-Parsing.idl @@ -0,0 +1,26 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: DOM Parsing and Serialization (https://w3c.github.io/DOM-Parsing/) + +[Exposed=Window] +interface XMLSerializer { + constructor(); + DOMString serializeToString(Node root); +}; + +interface mixin InnerHTML { + [CEReactions] attribute [LegacyNullToEmptyString] DOMString innerHTML; +}; + +Element includes InnerHTML; +ShadowRoot includes InnerHTML; + +partial interface Element { + [CEReactions] attribute [LegacyNullToEmptyString] DOMString outerHTML; + [CEReactions] undefined insertAdjacentHTML(DOMString position, DOMString text); +}; + +partial interface Range { + [CEReactions, NewObject] DocumentFragment createContextualFragment(DOMString fragment); +}; diff --git a/test/wpt/tests/interfaces/EXT_blend_minmax.idl b/test/wpt/tests/interfaces/EXT_blend_minmax.idl new file mode 100644 index 00000000000..fd7d26e7fb7 --- /dev/null +++ b/test/wpt/tests/interfaces/EXT_blend_minmax.idl @@ -0,0 +1,10 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: WebGL EXT_blend_minmax Khronos Ratified Extension Specification (https://registry.khronos.org/webgl/extensions/EXT_blend_minmax/) + +[Exposed=(Window,Worker), LegacyNoInterfaceObject] +interface EXT_blend_minmax { + const GLenum MIN_EXT = 0x8007; + const GLenum MAX_EXT = 0x8008; +}; diff --git a/test/wpt/tests/interfaces/EXT_clip_cull_distance.idl b/test/wpt/tests/interfaces/EXT_clip_cull_distance.idl new file mode 100644 index 00000000000..18d1c02a11a --- /dev/null +++ b/test/wpt/tests/interfaces/EXT_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 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/EXT_color_buffer_float.idl b/test/wpt/tests/interfaces/EXT_color_buffer_float.idl new file mode 100644 index 00000000000..09bd397d01f --- /dev/null +++ b/test/wpt/tests/interfaces/EXT_color_buffer_float.idl @@ -0,0 +1,8 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: WebGL EXT_color_buffer_float Extension Specification (https://registry.khronos.org/webgl/extensions/EXT_color_buffer_float/) + +[Exposed=(Window,Worker), LegacyNoInterfaceObject] +interface EXT_color_buffer_float { +}; // interface EXT_color_buffer_float diff --git a/test/wpt/tests/interfaces/EXT_color_buffer_half_float.idl b/test/wpt/tests/interfaces/EXT_color_buffer_half_float.idl new file mode 100644 index 00000000000..7197e44f270 --- /dev/null +++ b/test/wpt/tests/interfaces/EXT_color_buffer_half_float.idl @@ -0,0 +1,12 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: WebGL EXT_color_buffer_half_float Extension Specification (https://registry.khronos.org/webgl/extensions/EXT_color_buffer_half_float/) + +[Exposed=(Window,Worker), LegacyNoInterfaceObject] +interface EXT_color_buffer_half_float { + const GLenum RGBA16F_EXT = 0x881A; + const GLenum RGB16F_EXT = 0x881B; + const GLenum FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE_EXT = 0x8211; + const GLenum UNSIGNED_NORMALIZED_EXT = 0x8C17; +}; // interface EXT_color_buffer_half_float diff --git a/test/wpt/tests/interfaces/EXT_disjoint_timer_query.idl b/test/wpt/tests/interfaces/EXT_disjoint_timer_query.idl new file mode 100644 index 00000000000..cf0c8d9a286 --- /dev/null +++ b/test/wpt/tests/interfaces/EXT_disjoint_timer_query.idl @@ -0,0 +1,30 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: WebGL EXT_disjoint_timer_query Extension Specification (https://registry.khronos.org/webgl/extensions/EXT_disjoint_timer_query/) + +typedef unsigned long long GLuint64EXT; + +[Exposed=(Window,Worker), LegacyNoInterfaceObject] +interface WebGLTimerQueryEXT : WebGLObject { +}; + +[Exposed=(Window,Worker), LegacyNoInterfaceObject] +interface EXT_disjoint_timer_query { + const GLenum QUERY_COUNTER_BITS_EXT = 0x8864; + const GLenum CURRENT_QUERY_EXT = 0x8865; + const GLenum QUERY_RESULT_EXT = 0x8866; + const GLenum QUERY_RESULT_AVAILABLE_EXT = 0x8867; + const GLenum TIME_ELAPSED_EXT = 0x88BF; + const GLenum TIMESTAMP_EXT = 0x8E28; + const GLenum GPU_DISJOINT_EXT = 0x8FBB; + + WebGLTimerQueryEXT? createQueryEXT(); + undefined deleteQueryEXT(WebGLTimerQueryEXT? query); + [WebGLHandlesContextLoss] boolean isQueryEXT(WebGLTimerQueryEXT? query); + undefined beginQueryEXT(GLenum target, WebGLTimerQueryEXT query); + undefined endQueryEXT(GLenum target); + undefined queryCounterEXT(WebGLTimerQueryEXT query, GLenum target); + any getQueryEXT(GLenum target, GLenum pname); + any getQueryObjectEXT(WebGLTimerQueryEXT query, GLenum pname); +}; diff --git a/test/wpt/tests/interfaces/EXT_disjoint_timer_query_webgl2.idl b/test/wpt/tests/interfaces/EXT_disjoint_timer_query_webgl2.idl new file mode 100644 index 00000000000..689203cb477 --- /dev/null +++ b/test/wpt/tests/interfaces/EXT_disjoint_timer_query_webgl2.idl @@ -0,0 +1,14 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: WebGL EXT_disjoint_timer_query_webgl2 Extension Specification (https://registry.khronos.org/webgl/extensions/EXT_disjoint_timer_query_webgl2/) + +[Exposed=(Window,Worker), LegacyNoInterfaceObject] +interface EXT_disjoint_timer_query_webgl2 { + const GLenum QUERY_COUNTER_BITS_EXT = 0x8864; + const GLenum TIME_ELAPSED_EXT = 0x88BF; + const GLenum TIMESTAMP_EXT = 0x8E28; + const GLenum GPU_DISJOINT_EXT = 0x8FBB; + + undefined queryCounterEXT(WebGLQuery query, GLenum target); +}; diff --git a/test/wpt/tests/interfaces/EXT_float_blend.idl b/test/wpt/tests/interfaces/EXT_float_blend.idl new file mode 100644 index 00000000000..58ec47e17e2 --- /dev/null +++ b/test/wpt/tests/interfaces/EXT_float_blend.idl @@ -0,0 +1,8 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: WebGL EXT_float_blend Extension Specification (https://registry.khronos.org/webgl/extensions/EXT_float_blend/) + +[Exposed=(Window,Worker), LegacyNoInterfaceObject] +interface EXT_float_blend { +}; // interface EXT_float_blend diff --git a/test/wpt/tests/interfaces/EXT_frag_depth.idl b/test/wpt/tests/interfaces/EXT_frag_depth.idl new file mode 100644 index 00000000000..1ae6896effd --- /dev/null +++ b/test/wpt/tests/interfaces/EXT_frag_depth.idl @@ -0,0 +1,8 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: WebGL EXT_frag_depth Khronos Ratified Extension Specification (https://registry.khronos.org/webgl/extensions/EXT_frag_depth/) + +[Exposed=(Window,Worker), LegacyNoInterfaceObject] +interface EXT_frag_depth { +}; diff --git a/test/wpt/tests/interfaces/EXT_sRGB.idl b/test/wpt/tests/interfaces/EXT_sRGB.idl new file mode 100644 index 00000000000..3c03c33ff81 --- /dev/null +++ b/test/wpt/tests/interfaces/EXT_sRGB.idl @@ -0,0 +1,12 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: WebGL EXT_sRGB Extension Specification (https://registry.khronos.org/webgl/extensions/EXT_sRGB/) + +[Exposed=(Window,Worker), LegacyNoInterfaceObject] +interface EXT_sRGB { + const GLenum SRGB_EXT = 0x8C40; + const GLenum SRGB_ALPHA_EXT = 0x8C42; + const GLenum SRGB8_ALPHA8_EXT = 0x8C43; + const GLenum FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING_EXT = 0x8210; +}; diff --git a/test/wpt/tests/interfaces/EXT_shader_texture_lod.idl b/test/wpt/tests/interfaces/EXT_shader_texture_lod.idl new file mode 100644 index 00000000000..13df26c3ce0 --- /dev/null +++ b/test/wpt/tests/interfaces/EXT_shader_texture_lod.idl @@ -0,0 +1,8 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: WebGL EXT_shader_texture_lod Khronos Ratified Extension Specification (https://registry.khronos.org/webgl/extensions/EXT_shader_texture_lod/) + +[Exposed=(Window,Worker), LegacyNoInterfaceObject] +interface EXT_shader_texture_lod { +}; diff --git a/test/wpt/tests/interfaces/EXT_texture_compression_bptc.idl b/test/wpt/tests/interfaces/EXT_texture_compression_bptc.idl new file mode 100644 index 00000000000..2772980bdc9 --- /dev/null +++ b/test/wpt/tests/interfaces/EXT_texture_compression_bptc.idl @@ -0,0 +1,12 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: WebGL EXT_texture_compression_bptc Extension Specification (https://registry.khronos.org/webgl/extensions/EXT_texture_compression_bptc/) + +[Exposed=(Window,Worker), LegacyNoInterfaceObject] +interface EXT_texture_compression_bptc { + const GLenum COMPRESSED_RGBA_BPTC_UNORM_EXT = 0x8E8C; + const GLenum COMPRESSED_SRGB_ALPHA_BPTC_UNORM_EXT = 0x8E8D; + const GLenum COMPRESSED_RGB_BPTC_SIGNED_FLOAT_EXT = 0x8E8E; + const GLenum COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT_EXT = 0x8E8F; +}; diff --git a/test/wpt/tests/interfaces/EXT_texture_compression_rgtc.idl b/test/wpt/tests/interfaces/EXT_texture_compression_rgtc.idl new file mode 100644 index 00000000000..f12b962ea0a --- /dev/null +++ b/test/wpt/tests/interfaces/EXT_texture_compression_rgtc.idl @@ -0,0 +1,12 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: WebGL EXT_texture_compression_rgtc Extension Specification (https://registry.khronos.org/webgl/extensions/EXT_texture_compression_rgtc/) + +[Exposed=(Window,Worker), LegacyNoInterfaceObject] +interface EXT_texture_compression_rgtc { + const GLenum COMPRESSED_RED_RGTC1_EXT = 0x8DBB; + const GLenum COMPRESSED_SIGNED_RED_RGTC1_EXT = 0x8DBC; + const GLenum COMPRESSED_RED_GREEN_RGTC2_EXT = 0x8DBD; + const GLenum COMPRESSED_SIGNED_RED_GREEN_RGTC2_EXT = 0x8DBE; +}; diff --git a/test/wpt/tests/interfaces/EXT_texture_filter_anisotropic.idl b/test/wpt/tests/interfaces/EXT_texture_filter_anisotropic.idl new file mode 100644 index 00000000000..5c78bfaf4fa --- /dev/null +++ b/test/wpt/tests/interfaces/EXT_texture_filter_anisotropic.idl @@ -0,0 +1,10 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: WebGL EXT_texture_filter_anisotropic Khronos Ratified Extension Specification (https://registry.khronos.org/webgl/extensions/EXT_texture_filter_anisotropic/) + +[Exposed=(Window,Worker), LegacyNoInterfaceObject] +interface EXT_texture_filter_anisotropic { + const GLenum TEXTURE_MAX_ANISOTROPY_EXT = 0x84FE; + const GLenum MAX_TEXTURE_MAX_ANISOTROPY_EXT = 0x84FF; +}; diff --git a/test/wpt/tests/interfaces/EXT_texture_norm16.idl b/test/wpt/tests/interfaces/EXT_texture_norm16.idl new file mode 100644 index 00000000000..1fe5ed86e76 --- /dev/null +++ b/test/wpt/tests/interfaces/EXT_texture_norm16.idl @@ -0,0 +1,16 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: WebGL EXT_texture_norm16 Extension Specification (https://registry.khronos.org/webgl/extensions/EXT_texture_norm16/) + +[Exposed=(Window,Worker), LegacyNoInterfaceObject] +interface EXT_texture_norm16 { + const GLenum R16_EXT = 0x822A; + const GLenum RG16_EXT = 0x822C; + const GLenum RGB16_EXT = 0x8054; + const GLenum RGBA16_EXT = 0x805B; + const GLenum R16_SNORM_EXT = 0x8F98; + const GLenum RG16_SNORM_EXT = 0x8F99; + const GLenum RGB16_SNORM_EXT = 0x8F9A; + const GLenum RGBA16_SNORM_EXT = 0x8F9B; +}; diff --git a/test/wpt/tests/interfaces/FedCM.idl b/test/wpt/tests/interfaces/FedCM.idl new file mode 100644 index 00000000000..b3ddb54e0c6 --- /dev/null +++ b/test/wpt/tests/interfaces/FedCM.idl @@ -0,0 +1,81 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: Federated Credential Management API (https://fedidcg.github.io/FedCM/) + +dictionary IdentityProviderWellKnown { + required sequence provider_urls; +}; + +dictionary IdentityProviderIcon { + required USVString url; + unsigned long size; +}; + +dictionary IdentityProviderBranding { + USVString background_color; + USVString color; + sequence icons; +}; + +dictionary IdentityProviderAPIConfig { + required USVString accounts_endpoint; + required USVString client_metadata_endpoint; + required USVString id_assertion_endpoint; + IdentityProviderBranding branding; +}; + +dictionary IdentityProviderAccount { + required USVString id; + required USVString name; + required USVString email; + USVString given_name; + 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(); +}; diff --git a/test/wpt/tests/interfaces/IndexedDB.idl b/test/wpt/tests/interfaces/IndexedDB.idl new file mode 100644 index 00000000000..d82391da7e6 --- /dev/null +++ b/test/wpt/tests/interfaces/IndexedDB.idl @@ -0,0 +1,226 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: Indexed Database API 3.0 (https://w3c.github.io/IndexedDB/) + +[Exposed=(Window,Worker)] +interface IDBRequest : EventTarget { + readonly attribute any result; + readonly attribute DOMException? error; + readonly attribute (IDBObjectStore or IDBIndex or IDBCursor)? source; + readonly attribute IDBTransaction? transaction; + readonly attribute IDBRequestReadyState readyState; + + // Event handlers: + attribute EventHandler onsuccess; + attribute EventHandler onerror; +}; + +enum IDBRequestReadyState { + "pending", + "done" +}; + +[Exposed=(Window,Worker)] +interface IDBOpenDBRequest : IDBRequest { + // Event handlers: + attribute EventHandler onblocked; + attribute EventHandler onupgradeneeded; +}; + +[Exposed=(Window,Worker)] +interface IDBVersionChangeEvent : Event { + constructor(DOMString type, optional IDBVersionChangeEventInit eventInitDict = {}); + readonly attribute unsigned long long oldVersion; + readonly attribute unsigned long long? newVersion; +}; + +dictionary IDBVersionChangeEventInit : EventInit { + unsigned long long oldVersion = 0; + unsigned long long? newVersion = null; +}; + +partial interface mixin WindowOrWorkerGlobalScope { + [SameObject] readonly attribute IDBFactory indexedDB; +}; + +[Exposed=(Window,Worker)] +interface IDBFactory { + [NewObject] IDBOpenDBRequest open(DOMString name, + optional [EnforceRange] unsigned long long version); + [NewObject] IDBOpenDBRequest deleteDatabase(DOMString name); + + Promise> databases(); + + short cmp(any first, any second); +}; + +dictionary IDBDatabaseInfo { + DOMString name; + unsigned long long version; +}; + +[Exposed=(Window,Worker)] +interface IDBDatabase : EventTarget { + readonly attribute DOMString name; + readonly attribute unsigned long long version; + readonly attribute DOMStringList objectStoreNames; + + [NewObject] IDBTransaction transaction((DOMString or sequence) storeNames, + optional IDBTransactionMode mode = "readonly", + optional IDBTransactionOptions options = {}); + undefined close(); + + [NewObject] IDBObjectStore createObjectStore( + DOMString name, + optional IDBObjectStoreParameters options = {}); + undefined deleteObjectStore(DOMString name); + + // Event handlers: + attribute EventHandler onabort; + attribute EventHandler onclose; + attribute EventHandler onerror; + attribute EventHandler onversionchange; +}; + +enum IDBTransactionDurability { "default", "strict", "relaxed" }; + +dictionary IDBTransactionOptions { + IDBTransactionDurability durability = "default"; +}; + +dictionary IDBObjectStoreParameters { + (DOMString or sequence)? keyPath = null; + boolean autoIncrement = false; +}; + +[Exposed=(Window,Worker)] +interface IDBObjectStore { + attribute DOMString name; + readonly attribute any keyPath; + readonly attribute DOMStringList indexNames; + [SameObject] readonly attribute IDBTransaction transaction; + readonly attribute boolean autoIncrement; + + [NewObject] IDBRequest put(any value, optional any key); + [NewObject] IDBRequest add(any value, optional any key); + [NewObject] IDBRequest delete(any query); + [NewObject] IDBRequest clear(); + [NewObject] IDBRequest get(any query); + [NewObject] IDBRequest getKey(any query); + [NewObject] IDBRequest getAll(optional any query, + optional [EnforceRange] unsigned long count); + [NewObject] IDBRequest getAllKeys(optional any query, + optional [EnforceRange] unsigned long count); + [NewObject] IDBRequest count(optional any query); + + [NewObject] IDBRequest openCursor(optional any query, + optional IDBCursorDirection direction = "next"); + [NewObject] IDBRequest openKeyCursor(optional any query, + optional IDBCursorDirection direction = "next"); + + IDBIndex index(DOMString name); + + [NewObject] IDBIndex createIndex(DOMString name, + (DOMString or sequence) keyPath, + optional IDBIndexParameters options = {}); + undefined deleteIndex(DOMString name); +}; + +dictionary IDBIndexParameters { + boolean unique = false; + boolean multiEntry = false; +}; + +[Exposed=(Window,Worker)] +interface IDBIndex { + attribute DOMString name; + [SameObject] readonly attribute IDBObjectStore objectStore; + readonly attribute any keyPath; + readonly attribute boolean multiEntry; + readonly attribute boolean unique; + + [NewObject] IDBRequest get(any query); + [NewObject] IDBRequest getKey(any query); + [NewObject] IDBRequest getAll(optional any query, + optional [EnforceRange] unsigned long count); + [NewObject] IDBRequest getAllKeys(optional any query, + optional [EnforceRange] unsigned long count); + [NewObject] IDBRequest count(optional any query); + + [NewObject] IDBRequest openCursor(optional any query, + optional IDBCursorDirection direction = "next"); + [NewObject] IDBRequest openKeyCursor(optional any query, + optional IDBCursorDirection direction = "next"); +}; + +[Exposed=(Window,Worker)] +interface IDBKeyRange { + readonly attribute any lower; + readonly attribute any upper; + readonly attribute boolean lowerOpen; + readonly attribute boolean upperOpen; + + // Static construction methods: + [NewObject] static IDBKeyRange only(any value); + [NewObject] static IDBKeyRange lowerBound(any lower, optional boolean open = false); + [NewObject] static IDBKeyRange upperBound(any upper, optional boolean open = false); + [NewObject] static IDBKeyRange bound(any lower, + any upper, + optional boolean lowerOpen = false, + optional boolean upperOpen = false); + + boolean includes(any key); +}; + +[Exposed=(Window,Worker)] +interface IDBCursor { + readonly attribute (IDBObjectStore or IDBIndex) source; + readonly attribute IDBCursorDirection direction; + readonly attribute any key; + readonly attribute any primaryKey; + [SameObject] readonly attribute IDBRequest request; + + undefined advance([EnforceRange] unsigned long count); + undefined continue(optional any key); + undefined continuePrimaryKey(any key, any primaryKey); + + [NewObject] IDBRequest update(any value); + [NewObject] IDBRequest delete(); +}; + +enum IDBCursorDirection { + "next", + "nextunique", + "prev", + "prevunique" +}; + +[Exposed=(Window,Worker)] +interface IDBCursorWithValue : IDBCursor { + readonly attribute any value; +}; + +[Exposed=(Window,Worker)] +interface IDBTransaction : EventTarget { + readonly attribute DOMStringList objectStoreNames; + readonly attribute IDBTransactionMode mode; + readonly attribute IDBTransactionDurability durability; + [SameObject] readonly attribute IDBDatabase db; + readonly attribute DOMException? error; + + IDBObjectStore objectStore(DOMString name); + undefined commit(); + undefined abort(); + + // Event handlers: + attribute EventHandler onabort; + attribute EventHandler oncomplete; + attribute EventHandler onerror; +}; + +enum IDBTransactionMode { + "readonly", + "readwrite", + "versionchange" +}; diff --git a/test/wpt/tests/interfaces/KHR_parallel_shader_compile.idl b/test/wpt/tests/interfaces/KHR_parallel_shader_compile.idl new file mode 100644 index 00000000000..14709658e34 --- /dev/null +++ b/test/wpt/tests/interfaces/KHR_parallel_shader_compile.idl @@ -0,0 +1,9 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: WebGL KHR_parallel_shader_compile Extension Specification (https://registry.khronos.org/webgl/extensions/KHR_parallel_shader_compile/) + +[Exposed=(Window,Worker), LegacyNoInterfaceObject] +interface KHR_parallel_shader_compile { + const GLenum COMPLETION_STATUS_KHR = 0x91B1; +}; diff --git a/test/wpt/tests/interfaces/META.yml b/test/wpt/tests/interfaces/META.yml new file mode 100644 index 00000000000..c1dd8dddf9e --- /dev/null +++ b/test/wpt/tests/interfaces/META.yml @@ -0,0 +1,2 @@ +suggested_reviewers: + - foolip diff --git a/test/wpt/tests/interfaces/OES_draw_buffers_indexed.idl b/test/wpt/tests/interfaces/OES_draw_buffers_indexed.idl new file mode 100644 index 00000000000..ea1e217a116 --- /dev/null +++ b/test/wpt/tests/interfaces/OES_draw_buffers_indexed.idl @@ -0,0 +1,26 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: WebGL OES_draw_buffers_indexed Extension Specification (https://registry.khronos.org/webgl/extensions/OES_draw_buffers_indexed/) + +[Exposed=(Window,Worker), LegacyNoInterfaceObject] +interface OES_draw_buffers_indexed { + undefined enableiOES(GLenum target, GLuint index); + + undefined disableiOES(GLenum target, GLuint index); + + undefined blendEquationiOES(GLuint buf, GLenum mode); + + undefined blendEquationSeparateiOES(GLuint buf, + GLenum modeRGB, GLenum modeAlpha); + + undefined blendFunciOES(GLuint buf, + GLenum src, GLenum dst); + + undefined blendFuncSeparateiOES(GLuint buf, + GLenum srcRGB, GLenum dstRGB, + GLenum srcAlpha, GLenum dstAlpha); + + undefined colorMaskiOES(GLuint buf, + GLboolean r, GLboolean g, GLboolean b, GLboolean a); +}; diff --git a/test/wpt/tests/interfaces/OES_element_index_uint.idl b/test/wpt/tests/interfaces/OES_element_index_uint.idl new file mode 100644 index 00000000000..df43a57da80 --- /dev/null +++ b/test/wpt/tests/interfaces/OES_element_index_uint.idl @@ -0,0 +1,8 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: WebGL OES_element_index_uint Khronos Ratified Extension Specification (https://registry.khronos.org/webgl/extensions/OES_element_index_uint/) + +[Exposed=(Window,Worker), LegacyNoInterfaceObject] +interface OES_element_index_uint { +}; diff --git a/test/wpt/tests/interfaces/OES_fbo_render_mipmap.idl b/test/wpt/tests/interfaces/OES_fbo_render_mipmap.idl new file mode 100644 index 00000000000..608c39291af --- /dev/null +++ b/test/wpt/tests/interfaces/OES_fbo_render_mipmap.idl @@ -0,0 +1,8 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: WebGL OES_fbo_render_mipmap Extension Specification (https://registry.khronos.org/webgl/extensions/OES_fbo_render_mipmap/) + +[Exposed=(Window,Worker), LegacyNoInterfaceObject] +interface OES_fbo_render_mipmap { +}; diff --git a/test/wpt/tests/interfaces/OES_standard_derivatives.idl b/test/wpt/tests/interfaces/OES_standard_derivatives.idl new file mode 100644 index 00000000000..7bf073a3f15 --- /dev/null +++ b/test/wpt/tests/interfaces/OES_standard_derivatives.idl @@ -0,0 +1,9 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: WebGL OES_standard_derivatives Khronos Ratified Extension Specification (https://registry.khronos.org/webgl/extensions/OES_standard_derivatives/) + +[Exposed=(Window,Worker), LegacyNoInterfaceObject] +interface OES_standard_derivatives { + const GLenum FRAGMENT_SHADER_DERIVATIVE_HINT_OES = 0x8B8B; +}; diff --git a/test/wpt/tests/interfaces/OES_texture_float.idl b/test/wpt/tests/interfaces/OES_texture_float.idl new file mode 100644 index 00000000000..a1bb79cd5c5 --- /dev/null +++ b/test/wpt/tests/interfaces/OES_texture_float.idl @@ -0,0 +1,7 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: WebGL OES_texture_float Khronos Ratified Extension Specification (https://registry.khronos.org/webgl/extensions/OES_texture_float/) + +[Exposed=(Window,Worker), LegacyNoInterfaceObject] +interface OES_texture_float { }; diff --git a/test/wpt/tests/interfaces/OES_texture_float_linear.idl b/test/wpt/tests/interfaces/OES_texture_float_linear.idl new file mode 100644 index 00000000000..462629736de --- /dev/null +++ b/test/wpt/tests/interfaces/OES_texture_float_linear.idl @@ -0,0 +1,7 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: WebGL OES_texture_float_linear Khronos Ratified Extension Specification (https://registry.khronos.org/webgl/extensions/OES_texture_float_linear/) + +[Exposed=(Window,Worker), LegacyNoInterfaceObject] +interface OES_texture_float_linear { }; diff --git a/test/wpt/tests/interfaces/OES_texture_half_float.idl b/test/wpt/tests/interfaces/OES_texture_half_float.idl new file mode 100644 index 00000000000..be414543628 --- /dev/null +++ b/test/wpt/tests/interfaces/OES_texture_half_float.idl @@ -0,0 +1,9 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: WebGL OES_texture_half_float Khronos Ratified Extension Specification (https://registry.khronos.org/webgl/extensions/OES_texture_half_float/) + +[Exposed=(Window,Worker), LegacyNoInterfaceObject] +interface OES_texture_half_float { + const GLenum HALF_FLOAT_OES = 0x8D61; +}; diff --git a/test/wpt/tests/interfaces/OES_texture_half_float_linear.idl b/test/wpt/tests/interfaces/OES_texture_half_float_linear.idl new file mode 100644 index 00000000000..2f1a999b900 --- /dev/null +++ b/test/wpt/tests/interfaces/OES_texture_half_float_linear.idl @@ -0,0 +1,7 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: WebGL OES_texture_half_float_linear Khronos Ratified Extension Specification (https://registry.khronos.org/webgl/extensions/OES_texture_half_float_linear/) + +[Exposed=(Window,Worker), LegacyNoInterfaceObject] +interface OES_texture_half_float_linear { }; diff --git a/test/wpt/tests/interfaces/OES_vertex_array_object.idl b/test/wpt/tests/interfaces/OES_vertex_array_object.idl new file mode 100644 index 00000000000..8aeb7459f3b --- /dev/null +++ b/test/wpt/tests/interfaces/OES_vertex_array_object.idl @@ -0,0 +1,18 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: WebGL OES_vertex_array_object Khronos Ratified Extension Specification (https://registry.khronos.org/webgl/extensions/OES_vertex_array_object/) + +[Exposed=(Window,Worker), LegacyNoInterfaceObject] +interface WebGLVertexArrayObjectOES : WebGLObject { +}; + +[Exposed=(Window,Worker), LegacyNoInterfaceObject] +interface OES_vertex_array_object { + const GLenum VERTEX_ARRAY_BINDING_OES = 0x85B5; + + WebGLVertexArrayObjectOES? createVertexArrayOES(); + undefined deleteVertexArrayOES(WebGLVertexArrayObjectOES? arrayObject); + [WebGLHandlesContextLoss] GLboolean isVertexArrayOES(WebGLVertexArrayObjectOES? arrayObject); + undefined bindVertexArrayOES(WebGLVertexArrayObjectOES? arrayObject); +}; diff --git a/test/wpt/tests/interfaces/OVR_multiview2.idl b/test/wpt/tests/interfaces/OVR_multiview2.idl new file mode 100644 index 00000000000..9c1ecc4246c --- /dev/null +++ b/test/wpt/tests/interfaces/OVR_multiview2.idl @@ -0,0 +1,14 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: WebGL OVR_multiview2 Extension Specification (https://registry.khronos.org/webgl/extensions/OVR_multiview2/) + +[Exposed=(Window,Worker), LegacyNoInterfaceObject] +interface OVR_multiview2 { + const GLenum FRAMEBUFFER_ATTACHMENT_TEXTURE_NUM_VIEWS_OVR = 0x9630; + const GLenum FRAMEBUFFER_ATTACHMENT_TEXTURE_BASE_VIEW_INDEX_OVR = 0x9632; + const GLenum MAX_VIEWS_OVR = 0x9631; + const GLenum FRAMEBUFFER_INCOMPLETE_VIEW_TARGETS_OVR = 0x9633; + + undefined framebufferTextureMultiviewOVR(GLenum target, GLenum attachment, WebGLTexture? texture, GLint level, GLint baseViewIndex, GLsizei numViews); +}; diff --git a/test/wpt/tests/interfaces/README.md b/test/wpt/tests/interfaces/README.md new file mode 100644 index 00000000000..5e948ad955f --- /dev/null +++ b/test/wpt/tests/interfaces/README.md @@ -0,0 +1,3 @@ +This directory contains [Web IDL](https://webidl.spec.whatwg.org/) interface definitions for use in idlharness.js tests. + +The `.idl` files (except `*.tentative.idl`) are copied from [@webref/idl](https://www.npmjs.com/package/@webref/idl) by a [workflow](https://github.com/web-platform-tests/wpt/blob/master/.github/workflows/interfaces.yml) that tries to sync the files daily. The resulting pull requests require manual review but can be approved/merged by anyone with write access. diff --git a/test/wpt/tests/interfaces/SVG.idl b/test/wpt/tests/interfaces/SVG.idl new file mode 100644 index 00000000000..3a0b86126b5 --- /dev/null +++ b/test/wpt/tests/interfaces/SVG.idl @@ -0,0 +1,693 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: Scalable Vector Graphics (SVG) 2 (https://svgwg.org/svg2-draft/) + +[Exposed=Window] +interface SVGElement : Element { + + [SameObject] readonly attribute SVGAnimatedString className; + + readonly attribute SVGSVGElement? ownerSVGElement; + readonly attribute SVGElement? viewportElement; +}; + +SVGElement includes GlobalEventHandlers; +SVGElement includes SVGElementInstance; +SVGElement includes HTMLOrSVGElement; + +dictionary SVGBoundingBoxOptions { + boolean fill = true; + boolean stroke = false; + boolean markers = false; + boolean clipped = false; +}; + +[Exposed=Window] +interface SVGGraphicsElement : SVGElement { + [SameObject] readonly attribute SVGAnimatedTransformList transform; + + DOMRect getBBox(optional SVGBoundingBoxOptions options = {}); + DOMMatrix? getCTM(); + DOMMatrix? getScreenCTM(); +}; + +SVGGraphicsElement includes SVGTests; + +[Exposed=Window] +interface SVGGeometryElement : SVGGraphicsElement { + [SameObject] readonly attribute SVGAnimatedNumber pathLength; + + boolean isPointInFill(optional DOMPointInit point = {}); + boolean isPointInStroke(optional DOMPointInit point = {}); + float getTotalLength(); + DOMPoint getPointAtLength(float distance); +}; + +[Exposed=Window] +interface SVGNumber { + attribute float value; +}; + +[Exposed=Window] +interface SVGLength { + + // Length Unit Types + const unsigned short SVG_LENGTHTYPE_UNKNOWN = 0; + const unsigned short SVG_LENGTHTYPE_NUMBER = 1; + const unsigned short SVG_LENGTHTYPE_PERCENTAGE = 2; + const unsigned short SVG_LENGTHTYPE_EMS = 3; + const unsigned short SVG_LENGTHTYPE_EXS = 4; + const unsigned short SVG_LENGTHTYPE_PX = 5; + const unsigned short SVG_LENGTHTYPE_CM = 6; + const unsigned short SVG_LENGTHTYPE_MM = 7; + const unsigned short SVG_LENGTHTYPE_IN = 8; + const unsigned short SVG_LENGTHTYPE_PT = 9; + const unsigned short SVG_LENGTHTYPE_PC = 10; + + readonly attribute unsigned short unitType; + attribute float value; + attribute float valueInSpecifiedUnits; + attribute DOMString valueAsString; + + undefined newValueSpecifiedUnits(unsigned short unitType, float valueInSpecifiedUnits); + undefined convertToSpecifiedUnits(unsigned short unitType); +}; + +[Exposed=Window] +interface SVGAngle { + + // Angle Unit Types + const unsigned short SVG_ANGLETYPE_UNKNOWN = 0; + const unsigned short SVG_ANGLETYPE_UNSPECIFIED = 1; + const unsigned short SVG_ANGLETYPE_DEG = 2; + const unsigned short SVG_ANGLETYPE_RAD = 3; + const unsigned short SVG_ANGLETYPE_GRAD = 4; + + readonly attribute unsigned short unitType; + attribute float value; + attribute float valueInSpecifiedUnits; + attribute DOMString valueAsString; + + undefined newValueSpecifiedUnits(unsigned short unitType, float valueInSpecifiedUnits); + undefined convertToSpecifiedUnits(unsigned short unitType); +}; + +[Exposed=Window] +interface SVGNumberList { + + readonly attribute unsigned long length; + readonly attribute unsigned long numberOfItems; + + undefined clear(); + SVGNumber initialize(SVGNumber newItem); + getter SVGNumber getItem(unsigned long index); + SVGNumber insertItemBefore(SVGNumber newItem, unsigned long index); + SVGNumber replaceItem(SVGNumber newItem, unsigned long index); + SVGNumber removeItem(unsigned long index); + SVGNumber appendItem(SVGNumber newItem); + setter undefined (unsigned long index, SVGNumber newItem); +}; + +[Exposed=Window] +interface SVGLengthList { + + readonly attribute unsigned long length; + readonly attribute unsigned long numberOfItems; + + undefined clear(); + SVGLength initialize(SVGLength newItem); + getter SVGLength getItem(unsigned long index); + SVGLength insertItemBefore(SVGLength newItem, unsigned long index); + SVGLength replaceItem(SVGLength newItem, unsigned long index); + SVGLength removeItem(unsigned long index); + SVGLength appendItem(SVGLength newItem); + setter undefined (unsigned long index, SVGLength newItem); +}; + +[Exposed=Window] +interface SVGStringList { + + readonly attribute unsigned long length; + readonly attribute unsigned long numberOfItems; + + undefined clear(); + DOMString initialize(DOMString newItem); + getter DOMString getItem(unsigned long index); + DOMString insertItemBefore(DOMString newItem, unsigned long index); + DOMString replaceItem(DOMString newItem, unsigned long index); + DOMString removeItem(unsigned long index); + DOMString appendItem(DOMString newItem); + setter undefined (unsigned long index, DOMString newItem); +}; + +[Exposed=Window] +interface SVGAnimatedBoolean { + attribute boolean baseVal; + readonly attribute boolean animVal; +}; + +[Exposed=Window] +interface SVGAnimatedEnumeration { + attribute unsigned short baseVal; + readonly attribute unsigned short animVal; +}; + +[Exposed=Window] +interface SVGAnimatedInteger { + attribute long baseVal; + readonly attribute long animVal; +}; + +[Exposed=Window] +interface SVGAnimatedNumber { + attribute float baseVal; + readonly attribute float animVal; +}; + +[Exposed=Window] +interface SVGAnimatedLength { + [SameObject] readonly attribute SVGLength baseVal; + [SameObject] readonly attribute SVGLength animVal; +}; + +[Exposed=Window] +interface SVGAnimatedAngle { + [SameObject] readonly attribute SVGAngle baseVal; + [SameObject] readonly attribute SVGAngle animVal; +}; + +[Exposed=Window] +interface SVGAnimatedString { + attribute DOMString baseVal; + readonly attribute DOMString animVal; +}; + +[Exposed=Window] +interface SVGAnimatedRect { + [SameObject] readonly attribute DOMRect baseVal; + [SameObject] readonly attribute DOMRectReadOnly animVal; +}; + +[Exposed=Window] +interface SVGAnimatedNumberList { + [SameObject] readonly attribute SVGNumberList baseVal; + [SameObject] readonly attribute SVGNumberList animVal; +}; + +[Exposed=Window] +interface SVGAnimatedLengthList { + [SameObject] readonly attribute SVGLengthList baseVal; + [SameObject] readonly attribute SVGLengthList animVal; +}; + +[Exposed=Window] +interface SVGUnitTypes { + // Unit Types + const unsigned short SVG_UNIT_TYPE_UNKNOWN = 0; + const unsigned short SVG_UNIT_TYPE_USERSPACEONUSE = 1; + const unsigned short SVG_UNIT_TYPE_OBJECTBOUNDINGBOX = 2; +}; + +interface mixin SVGTests { + [SameObject] readonly attribute SVGStringList requiredExtensions; + [SameObject] readonly attribute SVGStringList systemLanguage; +}; + +interface mixin SVGFitToViewBox { + [SameObject] readonly attribute SVGAnimatedRect viewBox; + [SameObject] readonly attribute SVGAnimatedPreserveAspectRatio preserveAspectRatio; +}; + +interface mixin SVGURIReference { + [SameObject] readonly attribute SVGAnimatedString href; +}; + +partial interface Document { + readonly attribute SVGSVGElement? rootElement; +}; + +[Exposed=Window] +interface SVGSVGElement : SVGGraphicsElement { + + [SameObject] readonly attribute SVGAnimatedLength x; + [SameObject] readonly attribute SVGAnimatedLength y; + [SameObject] readonly attribute SVGAnimatedLength width; + [SameObject] readonly attribute SVGAnimatedLength height; + + attribute float currentScale; + [SameObject] readonly attribute DOMPointReadOnly currentTranslate; + + NodeList getIntersectionList(DOMRectReadOnly rect, SVGElement? referenceElement); + NodeList getEnclosureList(DOMRectReadOnly rect, SVGElement? referenceElement); + boolean checkIntersection(SVGElement element, DOMRectReadOnly rect); + boolean checkEnclosure(SVGElement element, DOMRectReadOnly rect); + + undefined deselectAll(); + + SVGNumber createSVGNumber(); + SVGLength createSVGLength(); + SVGAngle createSVGAngle(); + DOMPoint createSVGPoint(); + DOMMatrix createSVGMatrix(); + DOMRect createSVGRect(); + SVGTransform createSVGTransform(); + SVGTransform createSVGTransformFromMatrix(optional DOMMatrix2DInit matrix = {}); + + Element getElementById(DOMString elementId); + + // Deprecated methods that have no effect when called, + // but which are kept for compatibility reasons. + unsigned long suspendRedraw(unsigned long maxWaitMilliseconds); + undefined unsuspendRedraw(unsigned long suspendHandleID); + undefined unsuspendRedrawAll(); + undefined forceRedraw(); +}; + +SVGSVGElement includes SVGFitToViewBox; +SVGSVGElement includes WindowEventHandlers; + +[Exposed=Window] +interface SVGGElement : SVGGraphicsElement { +}; + +[Exposed=Window] +interface SVGDefsElement : SVGGraphicsElement { +}; + +[Exposed=Window] +interface SVGDescElement : SVGElement { +}; + +[Exposed=Window] +interface SVGMetadataElement : SVGElement { +}; + +[Exposed=Window] +interface SVGTitleElement : SVGElement { +}; + +[Exposed=Window] +interface SVGSymbolElement : SVGGraphicsElement { +}; + +SVGSymbolElement includes SVGFitToViewBox; + +[Exposed=Window] +interface SVGUseElement : SVGGraphicsElement { + [SameObject] readonly attribute SVGAnimatedLength x; + [SameObject] readonly attribute SVGAnimatedLength y; + [SameObject] readonly attribute SVGAnimatedLength width; + [SameObject] readonly attribute SVGAnimatedLength height; + [SameObject] readonly attribute SVGElement? instanceRoot; + [SameObject] readonly attribute SVGElement? animatedInstanceRoot; +}; + +SVGUseElement includes SVGURIReference; + +[Exposed=Window] +interface SVGUseElementShadowRoot : ShadowRoot { +}; + +interface mixin SVGElementInstance { + [SameObject] readonly attribute SVGElement? correspondingElement; + [SameObject] readonly attribute SVGUseElement? correspondingUseElement; +}; + +[Exposed=Window] +interface ShadowAnimation : Animation { + constructor(Animation source, (Element or CSSPseudoElement) newTarget); + [SameObject] readonly attribute Animation sourceAnimation; +}; + +[Exposed=Window] +interface SVGSwitchElement : SVGGraphicsElement { +}; + +interface mixin GetSVGDocument { + Document getSVGDocument(); +}; + +[Exposed=Window] +interface SVGStyleElement : SVGElement { + attribute DOMString type; + attribute DOMString media; + attribute DOMString title; +}; + +SVGStyleElement includes LinkStyle; + +[Exposed=Window] +interface SVGTransform { + + // Transform Types + const unsigned short SVG_TRANSFORM_UNKNOWN = 0; + const unsigned short SVG_TRANSFORM_MATRIX = 1; + const unsigned short SVG_TRANSFORM_TRANSLATE = 2; + const unsigned short SVG_TRANSFORM_SCALE = 3; + const unsigned short SVG_TRANSFORM_ROTATE = 4; + const unsigned short SVG_TRANSFORM_SKEWX = 5; + const unsigned short SVG_TRANSFORM_SKEWY = 6; + + readonly attribute unsigned short type; + [SameObject] readonly attribute DOMMatrix matrix; + readonly attribute float angle; + + undefined setMatrix(optional DOMMatrix2DInit matrix = {}); + undefined setTranslate(float tx, float ty); + undefined setScale(float sx, float sy); + undefined setRotate(float angle, float cx, float cy); + undefined setSkewX(float angle); + undefined setSkewY(float angle); +}; + +[Exposed=Window] +interface SVGTransformList { + + readonly attribute unsigned long length; + readonly attribute unsigned long numberOfItems; + + undefined clear(); + SVGTransform initialize(SVGTransform newItem); + getter SVGTransform getItem(unsigned long index); + SVGTransform insertItemBefore(SVGTransform newItem, unsigned long index); + SVGTransform replaceItem(SVGTransform newItem, unsigned long index); + SVGTransform removeItem(unsigned long index); + SVGTransform appendItem(SVGTransform newItem); + setter undefined (unsigned long index, SVGTransform newItem); + + // Additional methods not common to other list interfaces. + SVGTransform createSVGTransformFromMatrix(optional DOMMatrix2DInit matrix = {}); + SVGTransform? consolidate(); +}; + +[Exposed=Window] +interface SVGAnimatedTransformList { + [SameObject] readonly attribute SVGTransformList baseVal; + [SameObject] readonly attribute SVGTransformList animVal; +}; + +[Exposed=Window] +interface SVGPreserveAspectRatio { + + // Alignment Types + const unsigned short SVG_PRESERVEASPECTRATIO_UNKNOWN = 0; + const unsigned short SVG_PRESERVEASPECTRATIO_NONE = 1; + const unsigned short SVG_PRESERVEASPECTRATIO_XMINYMIN = 2; + const unsigned short SVG_PRESERVEASPECTRATIO_XMIDYMIN = 3; + const unsigned short SVG_PRESERVEASPECTRATIO_XMAXYMIN = 4; + const unsigned short SVG_PRESERVEASPECTRATIO_XMINYMID = 5; + const unsigned short SVG_PRESERVEASPECTRATIO_XMIDYMID = 6; + const unsigned short SVG_PRESERVEASPECTRATIO_XMAXYMID = 7; + const unsigned short SVG_PRESERVEASPECTRATIO_XMINYMAX = 8; + const unsigned short SVG_PRESERVEASPECTRATIO_XMIDYMAX = 9; + const unsigned short SVG_PRESERVEASPECTRATIO_XMAXYMAX = 10; + + // Meet-or-slice Types + const unsigned short SVG_MEETORSLICE_UNKNOWN = 0; + const unsigned short SVG_MEETORSLICE_MEET = 1; + const unsigned short SVG_MEETORSLICE_SLICE = 2; + + attribute unsigned short align; + attribute unsigned short meetOrSlice; +}; + +[Exposed=Window] +interface SVGAnimatedPreserveAspectRatio { + [SameObject] readonly attribute SVGPreserveAspectRatio baseVal; + [SameObject] readonly attribute SVGPreserveAspectRatio animVal; +}; + +[Exposed=Window] +interface SVGPathElement : SVGGeometryElement { +}; + +[Exposed=Window] +interface SVGRectElement : SVGGeometryElement { + [SameObject] readonly attribute SVGAnimatedLength x; + [SameObject] readonly attribute SVGAnimatedLength y; + [SameObject] readonly attribute SVGAnimatedLength width; + [SameObject] readonly attribute SVGAnimatedLength height; + [SameObject] readonly attribute SVGAnimatedLength rx; + [SameObject] readonly attribute SVGAnimatedLength ry; +}; + +[Exposed=Window] +interface SVGCircleElement : SVGGeometryElement { + [SameObject] readonly attribute SVGAnimatedLength cx; + [SameObject] readonly attribute SVGAnimatedLength cy; + [SameObject] readonly attribute SVGAnimatedLength r; +}; + +[Exposed=Window] +interface SVGEllipseElement : SVGGeometryElement { + [SameObject] readonly attribute SVGAnimatedLength cx; + [SameObject] readonly attribute SVGAnimatedLength cy; + [SameObject] readonly attribute SVGAnimatedLength rx; + [SameObject] readonly attribute SVGAnimatedLength ry; +}; + +[Exposed=Window] +interface SVGLineElement : SVGGeometryElement { + [SameObject] readonly attribute SVGAnimatedLength x1; + [SameObject] readonly attribute SVGAnimatedLength y1; + [SameObject] readonly attribute SVGAnimatedLength x2; + [SameObject] readonly attribute SVGAnimatedLength y2; +}; + +interface mixin SVGAnimatedPoints { + [SameObject] readonly attribute SVGPointList points; + [SameObject] readonly attribute SVGPointList animatedPoints; +}; + +[Exposed=Window] +interface SVGPointList { + + readonly attribute unsigned long length; + readonly attribute unsigned long numberOfItems; + + undefined clear(); + DOMPoint initialize(DOMPoint newItem); + getter DOMPoint getItem(unsigned long index); + DOMPoint insertItemBefore(DOMPoint newItem, unsigned long index); + DOMPoint replaceItem(DOMPoint newItem, unsigned long index); + DOMPoint removeItem(unsigned long index); + DOMPoint appendItem(DOMPoint newItem); + setter undefined (unsigned long index, DOMPoint newItem); +}; + +[Exposed=Window] +interface SVGPolylineElement : SVGGeometryElement { +}; + +SVGPolylineElement includes SVGAnimatedPoints; + +[Exposed=Window] +interface SVGPolygonElement : SVGGeometryElement { +}; + +SVGPolygonElement includes SVGAnimatedPoints; + +[Exposed=Window] +interface SVGTextContentElement : SVGGraphicsElement { + + // lengthAdjust Types + const unsigned short LENGTHADJUST_UNKNOWN = 0; + const unsigned short LENGTHADJUST_SPACING = 1; + const unsigned short LENGTHADJUST_SPACINGANDGLYPHS = 2; + + [SameObject] readonly attribute SVGAnimatedLength textLength; + [SameObject] readonly attribute SVGAnimatedEnumeration lengthAdjust; + + long getNumberOfChars(); + float getComputedTextLength(); + float getSubStringLength(unsigned long charnum, unsigned long nchars); + DOMPoint getStartPositionOfChar(unsigned long charnum); + DOMPoint getEndPositionOfChar(unsigned long charnum); + DOMRect getExtentOfChar(unsigned long charnum); + float getRotationOfChar(unsigned long charnum); + long getCharNumAtPosition(optional DOMPointInit point = {}); + undefined selectSubString(unsigned long charnum, unsigned long nchars); +}; + +[Exposed=Window] +interface SVGTextPositioningElement : SVGTextContentElement { + [SameObject] readonly attribute SVGAnimatedLengthList x; + [SameObject] readonly attribute SVGAnimatedLengthList y; + [SameObject] readonly attribute SVGAnimatedLengthList dx; + [SameObject] readonly attribute SVGAnimatedLengthList dy; + [SameObject] readonly attribute SVGAnimatedNumberList rotate; +}; + +[Exposed=Window] +interface SVGTextElement : SVGTextPositioningElement { +}; + +[Exposed=Window] +interface SVGTSpanElement : SVGTextPositioningElement { +}; + +[Exposed=Window] +interface SVGTextPathElement : SVGTextContentElement { + + // textPath Method Types + const unsigned short TEXTPATH_METHODTYPE_UNKNOWN = 0; + const unsigned short TEXTPATH_METHODTYPE_ALIGN = 1; + const unsigned short TEXTPATH_METHODTYPE_STRETCH = 2; + + // textPath Spacing Types + const unsigned short TEXTPATH_SPACINGTYPE_UNKNOWN = 0; + const unsigned short TEXTPATH_SPACINGTYPE_AUTO = 1; + const unsigned short TEXTPATH_SPACINGTYPE_EXACT = 2; + + [SameObject] readonly attribute SVGAnimatedLength startOffset; + [SameObject] readonly attribute SVGAnimatedEnumeration method; + [SameObject] readonly attribute SVGAnimatedEnumeration spacing; +}; + +SVGTextPathElement includes SVGURIReference; + +[Exposed=Window] +interface SVGImageElement : SVGGraphicsElement { + [SameObject] readonly attribute SVGAnimatedLength x; + [SameObject] readonly attribute SVGAnimatedLength y; + [SameObject] readonly attribute SVGAnimatedLength width; + [SameObject] readonly attribute SVGAnimatedLength height; + [SameObject] readonly attribute SVGAnimatedPreserveAspectRatio preserveAspectRatio; + attribute DOMString? crossOrigin; +}; + +SVGImageElement includes SVGURIReference; + +[Exposed=Window] +interface SVGForeignObjectElement : SVGGraphicsElement { + [SameObject] readonly attribute SVGAnimatedLength x; + [SameObject] readonly attribute SVGAnimatedLength y; + [SameObject] readonly attribute SVGAnimatedLength width; + [SameObject] readonly attribute SVGAnimatedLength height; +}; + +[Exposed=Window] +interface SVGMarkerElement : SVGElement { + + // Marker Unit Types + const unsigned short SVG_MARKERUNITS_UNKNOWN = 0; + const unsigned short SVG_MARKERUNITS_USERSPACEONUSE = 1; + const unsigned short SVG_MARKERUNITS_STROKEWIDTH = 2; + + // Marker Orientation Types + const unsigned short SVG_MARKER_ORIENT_UNKNOWN = 0; + const unsigned short SVG_MARKER_ORIENT_AUTO = 1; + const unsigned short SVG_MARKER_ORIENT_ANGLE = 2; + + [SameObject] readonly attribute SVGAnimatedLength refX; + [SameObject] readonly attribute SVGAnimatedLength refY; + [SameObject] readonly attribute SVGAnimatedEnumeration markerUnits; + [SameObject] readonly attribute SVGAnimatedLength markerWidth; + [SameObject] readonly attribute SVGAnimatedLength markerHeight; + [SameObject] readonly attribute SVGAnimatedEnumeration orientType; + [SameObject] readonly attribute SVGAnimatedAngle orientAngle; + attribute DOMString orient; + + undefined setOrientToAuto(); + undefined setOrientToAngle(SVGAngle angle); +}; + +SVGMarkerElement includes SVGFitToViewBox; + +[Exposed=Window] +interface SVGGradientElement : SVGElement { + + // Spread Method Types + const unsigned short SVG_SPREADMETHOD_UNKNOWN = 0; + const unsigned short SVG_SPREADMETHOD_PAD = 1; + const unsigned short SVG_SPREADMETHOD_REFLECT = 2; + const unsigned short SVG_SPREADMETHOD_REPEAT = 3; + + [SameObject] readonly attribute SVGAnimatedEnumeration gradientUnits; + [SameObject] readonly attribute SVGAnimatedTransformList gradientTransform; + [SameObject] readonly attribute SVGAnimatedEnumeration spreadMethod; +}; + +SVGGradientElement includes SVGURIReference; + +[Exposed=Window] +interface SVGLinearGradientElement : SVGGradientElement { + [SameObject] readonly attribute SVGAnimatedLength x1; + [SameObject] readonly attribute SVGAnimatedLength y1; + [SameObject] readonly attribute SVGAnimatedLength x2; + [SameObject] readonly attribute SVGAnimatedLength y2; +}; + +[Exposed=Window] +interface SVGRadialGradientElement : SVGGradientElement { + [SameObject] readonly attribute SVGAnimatedLength cx; + [SameObject] readonly attribute SVGAnimatedLength cy; + [SameObject] readonly attribute SVGAnimatedLength r; + [SameObject] readonly attribute SVGAnimatedLength fx; + [SameObject] readonly attribute SVGAnimatedLength fy; + [SameObject] readonly attribute SVGAnimatedLength fr; +}; + +[Exposed=Window] +interface SVGStopElement : SVGElement { + [SameObject] readonly attribute SVGAnimatedNumber offset; +}; + +[Exposed=Window] +interface SVGPatternElement : SVGElement { + [SameObject] readonly attribute SVGAnimatedEnumeration patternUnits; + [SameObject] readonly attribute SVGAnimatedEnumeration patternContentUnits; + [SameObject] readonly attribute SVGAnimatedTransformList patternTransform; + [SameObject] readonly attribute SVGAnimatedLength x; + [SameObject] readonly attribute SVGAnimatedLength y; + [SameObject] readonly attribute SVGAnimatedLength width; + [SameObject] readonly attribute SVGAnimatedLength height; +}; + +SVGPatternElement includes SVGFitToViewBox; +SVGPatternElement includes SVGURIReference; + +[Exposed=Window] +interface SVGScriptElement : SVGElement { + attribute DOMString type; + attribute DOMString? crossOrigin; +}; + +SVGScriptElement includes SVGURIReference; + +[Exposed=Window] +interface SVGAElement : SVGGraphicsElement { + [SameObject] readonly attribute SVGAnimatedString target; + attribute DOMString download; + attribute USVString ping; + attribute DOMString rel; + [SameObject, PutForwards=value] readonly attribute DOMTokenList relList; + attribute DOMString hreflang; + attribute DOMString type; + + attribute DOMString text; + + attribute DOMString referrerPolicy; +}; + +SVGAElement includes SVGURIReference; + +// Inline HTMLHyperlinkElementUtils except href, which conflicts. +partial interface SVGAElement { + readonly attribute USVString origin; + [CEReactions] attribute USVString protocol; + [CEReactions] attribute USVString username; + [CEReactions] attribute USVString password; + [CEReactions] attribute USVString host; + [CEReactions] attribute USVString hostname; + [CEReactions] attribute USVString port; + [CEReactions] attribute USVString pathname; + [CEReactions] attribute USVString search; + [CEReactions] attribute USVString hash; +}; + +[Exposed=Window] +interface SVGViewElement : SVGElement {}; + +SVGViewElement includes SVGFitToViewBox; diff --git a/test/wpt/tests/interfaces/WEBGL_blend_equation_advanced_coherent.idl b/test/wpt/tests/interfaces/WEBGL_blend_equation_advanced_coherent.idl new file mode 100644 index 00000000000..2208329ded1 --- /dev/null +++ b/test/wpt/tests/interfaces/WEBGL_blend_equation_advanced_coherent.idl @@ -0,0 +1,23 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: WebGL WEBGL_blend_equation_advanced_coherent Extension Draft Specification (https://registry.khronos.org/webgl/extensions/WEBGL_blend_equation_advanced_coherent/) + +[Exposed=(Window,Worker), LegacyNoInterfaceObject] +interface WEBGL_blend_equation_advanced_coherent { + const GLenum MULTIPLY = 0x9294; + const GLenum SCREEN = 0x9295; + const GLenum OVERLAY = 0x9296; + const GLenum DARKEN = 0x9297; + const GLenum LIGHTEN = 0x9298; + const GLenum COLORDODGE = 0x9299; + const GLenum COLORBURN = 0x929A; + const GLenum HARDLIGHT = 0x929B; + const GLenum SOFTLIGHT = 0x929C; + const GLenum DIFFERENCE = 0x929E; + const GLenum EXCLUSION = 0x92A0; + const GLenum HSL_HUE = 0x92AD; + const GLenum HSL_SATURATION = 0x92AE; + const GLenum HSL_COLOR = 0x92AF; + const GLenum HSL_LUMINOSITY = 0x92B0; +}; diff --git a/test/wpt/tests/interfaces/WEBGL_color_buffer_float.idl b/test/wpt/tests/interfaces/WEBGL_color_buffer_float.idl new file mode 100644 index 00000000000..b73f63153da --- /dev/null +++ b/test/wpt/tests/interfaces/WEBGL_color_buffer_float.idl @@ -0,0 +1,11 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: WebGL WEBGL_color_buffer_float Extension Specification (https://registry.khronos.org/webgl/extensions/WEBGL_color_buffer_float/) + +[Exposed=(Window,Worker), LegacyNoInterfaceObject] +interface WEBGL_color_buffer_float { + const GLenum RGBA32F_EXT = 0x8814; + const GLenum FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE_EXT = 0x8211; + const GLenum UNSIGNED_NORMALIZED_EXT = 0x8C17; +}; // interface WEBGL_color_buffer_float diff --git a/test/wpt/tests/interfaces/WEBGL_compressed_texture_astc.idl b/test/wpt/tests/interfaces/WEBGL_compressed_texture_astc.idl new file mode 100644 index 00000000000..9e4632ff7e7 --- /dev/null +++ b/test/wpt/tests/interfaces/WEBGL_compressed_texture_astc.idl @@ -0,0 +1,41 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: WebGL WEBGL_compressed_texture_astc Extension Specification (https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_astc/) + +[Exposed=(Window,Worker), LegacyNoInterfaceObject] +interface WEBGL_compressed_texture_astc { + /* Compressed Texture Format */ + const GLenum COMPRESSED_RGBA_ASTC_4x4_KHR = 0x93B0; + const GLenum COMPRESSED_RGBA_ASTC_5x4_KHR = 0x93B1; + const GLenum COMPRESSED_RGBA_ASTC_5x5_KHR = 0x93B2; + const GLenum COMPRESSED_RGBA_ASTC_6x5_KHR = 0x93B3; + const GLenum COMPRESSED_RGBA_ASTC_6x6_KHR = 0x93B4; + const GLenum COMPRESSED_RGBA_ASTC_8x5_KHR = 0x93B5; + const GLenum COMPRESSED_RGBA_ASTC_8x6_KHR = 0x93B6; + const GLenum COMPRESSED_RGBA_ASTC_8x8_KHR = 0x93B7; + const GLenum COMPRESSED_RGBA_ASTC_10x5_KHR = 0x93B8; + const GLenum COMPRESSED_RGBA_ASTC_10x6_KHR = 0x93B9; + const GLenum COMPRESSED_RGBA_ASTC_10x8_KHR = 0x93BA; + const GLenum COMPRESSED_RGBA_ASTC_10x10_KHR = 0x93BB; + const GLenum COMPRESSED_RGBA_ASTC_12x10_KHR = 0x93BC; + const GLenum COMPRESSED_RGBA_ASTC_12x12_KHR = 0x93BD; + + const GLenum COMPRESSED_SRGB8_ALPHA8_ASTC_4x4_KHR = 0x93D0; + const GLenum COMPRESSED_SRGB8_ALPHA8_ASTC_5x4_KHR = 0x93D1; + const GLenum COMPRESSED_SRGB8_ALPHA8_ASTC_5x5_KHR = 0x93D2; + const GLenum COMPRESSED_SRGB8_ALPHA8_ASTC_6x5_KHR = 0x93D3; + const GLenum COMPRESSED_SRGB8_ALPHA8_ASTC_6x6_KHR = 0x93D4; + const GLenum COMPRESSED_SRGB8_ALPHA8_ASTC_8x5_KHR = 0x93D5; + const GLenum COMPRESSED_SRGB8_ALPHA8_ASTC_8x6_KHR = 0x93D6; + const GLenum COMPRESSED_SRGB8_ALPHA8_ASTC_8x8_KHR = 0x93D7; + const GLenum COMPRESSED_SRGB8_ALPHA8_ASTC_10x5_KHR = 0x93D8; + const GLenum COMPRESSED_SRGB8_ALPHA8_ASTC_10x6_KHR = 0x93D9; + const GLenum COMPRESSED_SRGB8_ALPHA8_ASTC_10x8_KHR = 0x93DA; + const GLenum COMPRESSED_SRGB8_ALPHA8_ASTC_10x10_KHR = 0x93DB; + const GLenum COMPRESSED_SRGB8_ALPHA8_ASTC_12x10_KHR = 0x93DC; + const GLenum COMPRESSED_SRGB8_ALPHA8_ASTC_12x12_KHR = 0x93DD; + + // Profile query support. + sequence getSupportedProfiles(); +}; diff --git a/test/wpt/tests/interfaces/WEBGL_compressed_texture_etc.idl b/test/wpt/tests/interfaces/WEBGL_compressed_texture_etc.idl new file mode 100644 index 00000000000..5174a085461 --- /dev/null +++ b/test/wpt/tests/interfaces/WEBGL_compressed_texture_etc.idl @@ -0,0 +1,19 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: WebGL WEBGL_compressed_texture_etc Extension Specification (https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_etc/) + +[Exposed=(Window,Worker), LegacyNoInterfaceObject] +interface WEBGL_compressed_texture_etc { + /* Compressed Texture Formats */ + const GLenum COMPRESSED_R11_EAC = 0x9270; + const GLenum COMPRESSED_SIGNED_R11_EAC = 0x9271; + const GLenum COMPRESSED_RG11_EAC = 0x9272; + const GLenum COMPRESSED_SIGNED_RG11_EAC = 0x9273; + const GLenum COMPRESSED_RGB8_ETC2 = 0x9274; + const GLenum COMPRESSED_SRGB8_ETC2 = 0x9275; + const GLenum COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2 = 0x9276; + const GLenum COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2 = 0x9277; + const GLenum COMPRESSED_RGBA8_ETC2_EAC = 0x9278; + const GLenum COMPRESSED_SRGB8_ALPHA8_ETC2_EAC = 0x9279; +}; diff --git a/test/wpt/tests/interfaces/WEBGL_compressed_texture_etc1.idl b/test/wpt/tests/interfaces/WEBGL_compressed_texture_etc1.idl new file mode 100644 index 00000000000..773697e4504 --- /dev/null +++ b/test/wpt/tests/interfaces/WEBGL_compressed_texture_etc1.idl @@ -0,0 +1,10 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: WebGL WEBGL_compressed_texture_etc1 Extension Specification (https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_etc1/) + +[Exposed=(Window,Worker), LegacyNoInterfaceObject] +interface WEBGL_compressed_texture_etc1 { + /* Compressed Texture Format */ + const GLenum COMPRESSED_RGB_ETC1_WEBGL = 0x8D64; +}; diff --git a/test/wpt/tests/interfaces/WEBGL_compressed_texture_pvrtc.idl b/test/wpt/tests/interfaces/WEBGL_compressed_texture_pvrtc.idl new file mode 100644 index 00000000000..5aa004af80e --- /dev/null +++ b/test/wpt/tests/interfaces/WEBGL_compressed_texture_pvrtc.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_compressed_texture_pvrtc Extension Specification (https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_pvrtc/) + +[Exposed=(Window,Worker), LegacyNoInterfaceObject] +interface WEBGL_compressed_texture_pvrtc { + /* Compressed Texture Formats */ + const GLenum COMPRESSED_RGB_PVRTC_4BPPV1_IMG = 0x8C00; + const GLenum COMPRESSED_RGB_PVRTC_2BPPV1_IMG = 0x8C01; + const GLenum COMPRESSED_RGBA_PVRTC_4BPPV1_IMG = 0x8C02; + const GLenum COMPRESSED_RGBA_PVRTC_2BPPV1_IMG = 0x8C03; +}; diff --git a/test/wpt/tests/interfaces/WEBGL_compressed_texture_s3tc.idl b/test/wpt/tests/interfaces/WEBGL_compressed_texture_s3tc.idl new file mode 100644 index 00000000000..6e7c4bd89ef --- /dev/null +++ b/test/wpt/tests/interfaces/WEBGL_compressed_texture_s3tc.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_compressed_texture_s3tc Khronos Ratified Extension Specification (https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_s3tc/) + +[Exposed=(Window,Worker), LegacyNoInterfaceObject] +interface WEBGL_compressed_texture_s3tc { + /* Compressed Texture Formats */ + const GLenum COMPRESSED_RGB_S3TC_DXT1_EXT = 0x83F0; + const GLenum COMPRESSED_RGBA_S3TC_DXT1_EXT = 0x83F1; + const GLenum COMPRESSED_RGBA_S3TC_DXT3_EXT = 0x83F2; + const GLenum COMPRESSED_RGBA_S3TC_DXT5_EXT = 0x83F3; +}; diff --git a/test/wpt/tests/interfaces/WEBGL_compressed_texture_s3tc_srgb.idl b/test/wpt/tests/interfaces/WEBGL_compressed_texture_s3tc_srgb.idl new file mode 100644 index 00000000000..809265e68da --- /dev/null +++ b/test/wpt/tests/interfaces/WEBGL_compressed_texture_s3tc_srgb.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_compressed_texture_s3tc_srgb Extension Specification (https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_s3tc_srgb/) + +[Exposed=(Window,Worker), LegacyNoInterfaceObject] +interface WEBGL_compressed_texture_s3tc_srgb { + /* Compressed Texture Formats */ + const GLenum COMPRESSED_SRGB_S3TC_DXT1_EXT = 0x8C4C; + const GLenum COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT = 0x8C4D; + const GLenum COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT = 0x8C4E; + const GLenum COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT = 0x8C4F; +}; diff --git a/test/wpt/tests/interfaces/WEBGL_debug_renderer_info.idl b/test/wpt/tests/interfaces/WEBGL_debug_renderer_info.idl new file mode 100644 index 00000000000..769406151dc --- /dev/null +++ b/test/wpt/tests/interfaces/WEBGL_debug_renderer_info.idl @@ -0,0 +1,12 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: WebGL WEBGL_debug_renderer_info Khronos Ratified Extension Specification (https://registry.khronos.org/webgl/extensions/WEBGL_debug_renderer_info/) + +[Exposed=(Window,Worker), LegacyNoInterfaceObject] +interface WEBGL_debug_renderer_info { + + const GLenum UNMASKED_VENDOR_WEBGL = 0x9245; + const GLenum UNMASKED_RENDERER_WEBGL = 0x9246; + +}; diff --git a/test/wpt/tests/interfaces/WEBGL_debug_shaders.idl b/test/wpt/tests/interfaces/WEBGL_debug_shaders.idl new file mode 100644 index 00000000000..ecb48d0b1df --- /dev/null +++ b/test/wpt/tests/interfaces/WEBGL_debug_shaders.idl @@ -0,0 +1,11 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: WebGL WEBGL_debug_shaders Khronos Ratified Extension Specification (https://registry.khronos.org/webgl/extensions/WEBGL_debug_shaders/) + +[Exposed=(Window,Worker), LegacyNoInterfaceObject] +interface WEBGL_debug_shaders { + + DOMString getTranslatedShaderSource(WebGLShader shader); + +}; diff --git a/test/wpt/tests/interfaces/WEBGL_depth_texture.idl b/test/wpt/tests/interfaces/WEBGL_depth_texture.idl new file mode 100644 index 00000000000..a9ec791ad08 --- /dev/null +++ b/test/wpt/tests/interfaces/WEBGL_depth_texture.idl @@ -0,0 +1,9 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: WebGL WEBGL_depth_texture Khronos Ratified Extension Specification (https://registry.khronos.org/webgl/extensions/WEBGL_depth_texture/) + +[Exposed=(Window,Worker), LegacyNoInterfaceObject] +interface WEBGL_depth_texture { + const GLenum UNSIGNED_INT_24_8_WEBGL = 0x84FA; +}; diff --git a/test/wpt/tests/interfaces/WEBGL_draw_buffers.idl b/test/wpt/tests/interfaces/WEBGL_draw_buffers.idl new file mode 100644 index 00000000000..3310388f6ec --- /dev/null +++ b/test/wpt/tests/interfaces/WEBGL_draw_buffers.idl @@ -0,0 +1,46 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: WebGL WEBGL_draw_buffers Khronos Ratified Extension Specification (https://registry.khronos.org/webgl/extensions/WEBGL_draw_buffers/) + +[Exposed=(Window,Worker), LegacyNoInterfaceObject] +interface WEBGL_draw_buffers { + const GLenum COLOR_ATTACHMENT0_WEBGL = 0x8CE0; + const GLenum COLOR_ATTACHMENT1_WEBGL = 0x8CE1; + const GLenum COLOR_ATTACHMENT2_WEBGL = 0x8CE2; + const GLenum COLOR_ATTACHMENT3_WEBGL = 0x8CE3; + const GLenum COLOR_ATTACHMENT4_WEBGL = 0x8CE4; + const GLenum COLOR_ATTACHMENT5_WEBGL = 0x8CE5; + const GLenum COLOR_ATTACHMENT6_WEBGL = 0x8CE6; + const GLenum COLOR_ATTACHMENT7_WEBGL = 0x8CE7; + const GLenum COLOR_ATTACHMENT8_WEBGL = 0x8CE8; + const GLenum COLOR_ATTACHMENT9_WEBGL = 0x8CE9; + const GLenum COLOR_ATTACHMENT10_WEBGL = 0x8CEA; + const GLenum COLOR_ATTACHMENT11_WEBGL = 0x8CEB; + const GLenum COLOR_ATTACHMENT12_WEBGL = 0x8CEC; + const GLenum COLOR_ATTACHMENT13_WEBGL = 0x8CED; + const GLenum COLOR_ATTACHMENT14_WEBGL = 0x8CEE; + const GLenum COLOR_ATTACHMENT15_WEBGL = 0x8CEF; + + const GLenum DRAW_BUFFER0_WEBGL = 0x8825; + const GLenum DRAW_BUFFER1_WEBGL = 0x8826; + const GLenum DRAW_BUFFER2_WEBGL = 0x8827; + const GLenum DRAW_BUFFER3_WEBGL = 0x8828; + const GLenum DRAW_BUFFER4_WEBGL = 0x8829; + const GLenum DRAW_BUFFER5_WEBGL = 0x882A; + const GLenum DRAW_BUFFER6_WEBGL = 0x882B; + const GLenum DRAW_BUFFER7_WEBGL = 0x882C; + const GLenum DRAW_BUFFER8_WEBGL = 0x882D; + const GLenum DRAW_BUFFER9_WEBGL = 0x882E; + const GLenum DRAW_BUFFER10_WEBGL = 0x882F; + const GLenum DRAW_BUFFER11_WEBGL = 0x8830; + const GLenum DRAW_BUFFER12_WEBGL = 0x8831; + const GLenum DRAW_BUFFER13_WEBGL = 0x8832; + const GLenum DRAW_BUFFER14_WEBGL = 0x8833; + const GLenum DRAW_BUFFER15_WEBGL = 0x8834; + + const GLenum MAX_COLOR_ATTACHMENTS_WEBGL = 0x8CDF; + const GLenum MAX_DRAW_BUFFERS_WEBGL = 0x8824; + + undefined drawBuffersWEBGL(sequence buffers); +}; diff --git a/test/wpt/tests/interfaces/WEBGL_draw_instanced_base_vertex_base_instance.idl b/test/wpt/tests/interfaces/WEBGL_draw_instanced_base_vertex_base_instance.idl new file mode 100644 index 00000000000..38f7a42a11f --- /dev/null +++ b/test/wpt/tests/interfaces/WEBGL_draw_instanced_base_vertex_base_instance.idl @@ -0,0 +1,14 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: WebGL WEBGL_draw_instanced_base_vertex_base_instance Extension Draft Specification (https://registry.khronos.org/webgl/extensions/WEBGL_draw_instanced_base_vertex_base_instance/) + +[Exposed=(Window,Worker), LegacyNoInterfaceObject] +interface WEBGL_draw_instanced_base_vertex_base_instance { + undefined drawArraysInstancedBaseInstanceWEBGL( + GLenum mode, GLint first, GLsizei count, + GLsizei instanceCount, GLuint baseInstance); + undefined drawElementsInstancedBaseVertexBaseInstanceWEBGL( + GLenum mode, GLsizei count, GLenum type, GLintptr offset, + GLsizei instanceCount, GLint baseVertex, GLuint baseInstance); +}; diff --git a/test/wpt/tests/interfaces/WEBGL_lose_context.idl b/test/wpt/tests/interfaces/WEBGL_lose_context.idl new file mode 100644 index 00000000000..ee68fb5c09f --- /dev/null +++ b/test/wpt/tests/interfaces/WEBGL_lose_context.idl @@ -0,0 +1,10 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: WebGL WEBGL_lose_context Khronos Ratified Extension Specification (https://registry.khronos.org/webgl/extensions/WEBGL_lose_context/) + +[Exposed=(Window,Worker), LegacyNoInterfaceObject] +interface WEBGL_lose_context { + undefined loseContext(); + undefined restoreContext(); +}; diff --git a/test/wpt/tests/interfaces/WEBGL_multi_draw.idl b/test/wpt/tests/interfaces/WEBGL_multi_draw.idl new file mode 100644 index 00000000000..ee8c044b529 --- /dev/null +++ b/test/wpt/tests/interfaces/WEBGL_multi_draw.idl @@ -0,0 +1,32 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: WebGL WEBGL_multi_draw Extension Specification (https://registry.khronos.org/webgl/extensions/WEBGL_multi_draw/) + +[Exposed=(Window,Worker), LegacyNoInterfaceObject] +interface WEBGL_multi_draw { + undefined multiDrawArraysWEBGL( + GLenum mode, + ([AllowShared] Int32Array or sequence) firstsList, GLuint firstsOffset, + ([AllowShared] Int32Array or sequence) countsList, GLuint countsOffset, + GLsizei drawcount); + undefined multiDrawElementsWEBGL( + GLenum mode, + ([AllowShared] Int32Array or sequence) countsList, GLuint countsOffset, + GLenum type, + ([AllowShared] Int32Array or sequence) offsetsList, GLuint offsetsOffset, + GLsizei drawcount); + undefined multiDrawArraysInstancedWEBGL( + GLenum mode, + ([AllowShared] Int32Array or sequence) firstsList, GLuint firstsOffset, + ([AllowShared] Int32Array or sequence) countsList, GLuint countsOffset, + ([AllowShared] Int32Array or sequence) instanceCountsList, GLuint instanceCountsOffset, + GLsizei drawcount); + undefined multiDrawElementsInstancedWEBGL( + GLenum mode, + ([AllowShared] Int32Array or sequence) countsList, GLuint countsOffset, + GLenum type, + ([AllowShared] Int32Array or sequence) offsetsList, GLuint offsetsOffset, + ([AllowShared] Int32Array or sequence) instanceCountsList, GLuint instanceCountsOffset, + GLsizei drawcount); +}; diff --git a/test/wpt/tests/interfaces/WEBGL_multi_draw_instanced_base_vertex_base_instance.idl b/test/wpt/tests/interfaces/WEBGL_multi_draw_instanced_base_vertex_base_instance.idl new file mode 100644 index 00000000000..2258fa9a10c --- /dev/null +++ b/test/wpt/tests/interfaces/WEBGL_multi_draw_instanced_base_vertex_base_instance.idl @@ -0,0 +1,26 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: WebGL WEBGL_multi_draw_instanced_base_vertex_base_instance Extension Draft Specification (https://registry.khronos.org/webgl/extensions/WEBGL_multi_draw_instanced_base_vertex_base_instance/) + +[Exposed=(Window,Worker), LegacyNoInterfaceObject] +interface WEBGL_multi_draw_instanced_base_vertex_base_instance { + undefined multiDrawArraysInstancedBaseInstanceWEBGL( + GLenum mode, + ([AllowShared] Int32Array or sequence) firstsList, GLuint firstsOffset, + ([AllowShared] Int32Array or sequence) countsList, GLuint countsOffset, + ([AllowShared] Int32Array or sequence) instanceCountsList, GLuint instanceCountsOffset, + ([AllowShared] Uint32Array or sequence) baseInstancesList, GLuint baseInstancesOffset, + GLsizei drawcount + ); + undefined multiDrawElementsInstancedBaseVertexBaseInstanceWEBGL( + GLenum mode, + ([AllowShared] Int32Array or sequence) countsList, GLuint countsOffset, + GLenum type, + ([AllowShared] Int32Array or sequence) offsetsList, GLuint offsetsOffset, + ([AllowShared] Int32Array or sequence) instanceCountsList, GLuint instanceCountsOffset, + ([AllowShared] Int32Array or sequence) baseVerticesList, GLuint baseVerticesOffset, + ([AllowShared] Uint32Array or sequence) baseInstancesList, GLuint baseInstancesOffset, + GLsizei drawcount + ); +}; diff --git a/test/wpt/tests/interfaces/WebCryptoAPI.idl b/test/wpt/tests/interfaces/WebCryptoAPI.idl new file mode 100644 index 00000000000..0e68ea82f59 --- /dev/null +++ b/test/wpt/tests/interfaces/WebCryptoAPI.idl @@ -0,0 +1,237 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: Web Cryptography API (https://w3c.github.io/webcrypto/) + +partial interface mixin WindowOrWorkerGlobalScope { + [SameObject] readonly attribute Crypto crypto; +}; + +[Exposed=(Window,Worker)] +interface Crypto { + [SecureContext] readonly attribute SubtleCrypto subtle; + ArrayBufferView getRandomValues(ArrayBufferView array); + [SecureContext] DOMString randomUUID(); +}; + +typedef (object or DOMString) AlgorithmIdentifier; + +typedef AlgorithmIdentifier HashAlgorithmIdentifier; + +dictionary Algorithm { + required DOMString name; +}; + +dictionary KeyAlgorithm { + required DOMString name; +}; + +enum KeyType { "public", "private", "secret" }; + +enum KeyUsage { "encrypt", "decrypt", "sign", "verify", "deriveKey", "deriveBits", "wrapKey", "unwrapKey" }; + +[SecureContext,Exposed=(Window,Worker),Serializable] +interface CryptoKey { + readonly attribute KeyType type; + readonly attribute boolean extractable; + readonly attribute object algorithm; + readonly attribute object usages; +}; + +enum KeyFormat { "raw", "spki", "pkcs8", "jwk" }; + +[SecureContext,Exposed=(Window,Worker)] +interface SubtleCrypto { + Promise encrypt(AlgorithmIdentifier algorithm, + CryptoKey key, + BufferSource data); + Promise decrypt(AlgorithmIdentifier algorithm, + CryptoKey key, + BufferSource data); + Promise sign(AlgorithmIdentifier algorithm, + CryptoKey key, + BufferSource data); + Promise verify(AlgorithmIdentifier algorithm, + CryptoKey key, + BufferSource signature, + BufferSource data); + Promise digest(AlgorithmIdentifier algorithm, + BufferSource data); + + Promise generateKey(AlgorithmIdentifier algorithm, + boolean extractable, + sequence keyUsages ); + Promise deriveKey(AlgorithmIdentifier algorithm, + CryptoKey baseKey, + AlgorithmIdentifier derivedKeyType, + boolean extractable, + sequence keyUsages ); + Promise deriveBits(AlgorithmIdentifier algorithm, + CryptoKey baseKey, + unsigned long length); + + Promise importKey(KeyFormat format, + (BufferSource or JsonWebKey) keyData, + AlgorithmIdentifier algorithm, + boolean extractable, + sequence keyUsages ); + Promise exportKey(KeyFormat format, CryptoKey key); + + Promise wrapKey(KeyFormat format, + CryptoKey key, + CryptoKey wrappingKey, + AlgorithmIdentifier wrapAlgorithm); + Promise unwrapKey(KeyFormat format, + BufferSource wrappedKey, + CryptoKey unwrappingKey, + AlgorithmIdentifier unwrapAlgorithm, + AlgorithmIdentifier unwrappedKeyAlgorithm, + boolean extractable, + sequence keyUsages ); +}; + +dictionary RsaOtherPrimesInfo { + // The following fields are defined in Section 6.3.2.7 of JSON Web Algorithms + DOMString r; + DOMString d; + DOMString t; +}; + +dictionary JsonWebKey { + // The following fields are defined in Section 3.1 of JSON Web Key + DOMString kty; + DOMString use; + sequence key_ops; + DOMString alg; + + // The following fields are defined in JSON Web Key Parameters Registration + boolean ext; + + // The following fields are defined in Section 6 of JSON Web Algorithms + DOMString crv; + DOMString x; + DOMString y; + DOMString d; + DOMString n; + DOMString e; + DOMString p; + DOMString q; + DOMString dp; + DOMString dq; + DOMString qi; + sequence oth; + DOMString k; +}; + +typedef Uint8Array BigInteger; + +dictionary CryptoKeyPair { + CryptoKey publicKey; + CryptoKey privateKey; +}; + +dictionary RsaKeyGenParams : Algorithm { + required [EnforceRange] unsigned long modulusLength; + required BigInteger publicExponent; +}; + +dictionary RsaHashedKeyGenParams : RsaKeyGenParams { + required HashAlgorithmIdentifier hash; +}; + +dictionary RsaKeyAlgorithm : KeyAlgorithm { + required unsigned long modulusLength; + required BigInteger publicExponent; +}; + +dictionary RsaHashedKeyAlgorithm : RsaKeyAlgorithm { + required KeyAlgorithm hash; +}; + +dictionary RsaHashedImportParams : Algorithm { + required HashAlgorithmIdentifier hash; +}; + +dictionary RsaPssParams : Algorithm { + required [EnforceRange] unsigned long saltLength; +}; + +dictionary RsaOaepParams : Algorithm { + BufferSource label; +}; + +dictionary EcdsaParams : Algorithm { + required HashAlgorithmIdentifier hash; +}; + +typedef DOMString NamedCurve; + +dictionary EcKeyGenParams : Algorithm { + required NamedCurve namedCurve; +}; + +dictionary EcKeyAlgorithm : KeyAlgorithm { + required NamedCurve namedCurve; +}; + +dictionary EcKeyImportParams : Algorithm { + required NamedCurve namedCurve; +}; + +dictionary EcdhKeyDeriveParams : Algorithm { + required CryptoKey public; +}; + +dictionary AesCtrParams : Algorithm { + required BufferSource counter; + required [EnforceRange] octet length; +}; + +dictionary AesKeyAlgorithm : KeyAlgorithm { + required unsigned short length; +}; + +dictionary AesKeyGenParams : Algorithm { + required [EnforceRange] unsigned short length; +}; + +dictionary AesDerivedKeyParams : Algorithm { + required [EnforceRange] unsigned short length; +}; + +dictionary AesCbcParams : Algorithm { + required BufferSource iv; +}; + +dictionary AesGcmParams : Algorithm { + required BufferSource iv; + BufferSource additionalData; + [EnforceRange] octet tagLength; +}; + +dictionary HmacImportParams : Algorithm { + required HashAlgorithmIdentifier hash; + [EnforceRange] unsigned long length; +}; + +dictionary HmacKeyAlgorithm : KeyAlgorithm { + required KeyAlgorithm hash; + required unsigned long length; +}; + +dictionary HmacKeyGenParams : Algorithm { + required HashAlgorithmIdentifier hash; + [EnforceRange] unsigned long length; +}; + +dictionary HkdfParams : Algorithm { + required HashAlgorithmIdentifier hash; + required BufferSource salt; + required BufferSource info; +}; + +dictionary Pbkdf2Params : Algorithm { + required BufferSource salt; + required [EnforceRange] unsigned long iterations; + required HashAlgorithmIdentifier hash; +}; diff --git a/test/wpt/tests/interfaces/accelerometer.idl b/test/wpt/tests/interfaces/accelerometer.idl new file mode 100644 index 00000000000..fc8fc07ff77 --- /dev/null +++ b/test/wpt/tests/interfaces/accelerometer.idl @@ -0,0 +1,40 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: Accelerometer (https://w3c.github.io/accelerometer/) + +[SecureContext, Exposed=Window] +interface Accelerometer : Sensor { + constructor(optional AccelerometerSensorOptions options = {}); + readonly attribute double? x; + readonly attribute double? y; + readonly attribute double? z; +}; + +enum AccelerometerLocalCoordinateSystem { "device", "screen" }; + +dictionary AccelerometerSensorOptions : SensorOptions { + AccelerometerLocalCoordinateSystem referenceFrame = "device"; +}; + +[SecureContext, Exposed=Window] +interface LinearAccelerationSensor : Accelerometer { + constructor(optional AccelerometerSensorOptions options = {}); +}; + +[SecureContext, Exposed=Window] +interface GravitySensor : Accelerometer { + constructor(optional AccelerometerSensorOptions options = {}); +}; + +dictionary AccelerometerReadingValues { + required double? x; + required double? y; + required double? z; +}; + +dictionary LinearAccelerationReadingValues : AccelerometerReadingValues { +}; + +dictionary GravityReadingValues : AccelerometerReadingValues { +}; diff --git a/test/wpt/tests/interfaces/ambient-light.idl b/test/wpt/tests/interfaces/ambient-light.idl new file mode 100644 index 00000000000..6d9c8e03eaa --- /dev/null +++ b/test/wpt/tests/interfaces/ambient-light.idl @@ -0,0 +1,14 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: Ambient Light Sensor (https://w3c.github.io/ambient-light/) + +[SecureContext, Exposed=Window] +interface AmbientLightSensor : Sensor { + constructor(optional SensorOptions sensorOptions = {}); + readonly attribute double? illuminance; +}; + +dictionary AmbientLightReadingValues { + required double? illuminance; +}; diff --git a/test/wpt/tests/interfaces/anchors.idl b/test/wpt/tests/interfaces/anchors.idl new file mode 100644 index 00000000000..5aa9395f09f --- /dev/null +++ b/test/wpt/tests/interfaces/anchors.idl @@ -0,0 +1,35 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: WebXR Anchors Module (https://immersive-web.github.io/anchors/) + +[SecureContext, Exposed=Window] +interface XRAnchor { + readonly attribute XRSpace anchorSpace; + + Promise requestPersistentHandle(); + + undefined delete(); +}; + +partial interface XRFrame { + Promise createAnchor(XRRigidTransform pose, XRSpace space); +}; + +partial interface XRSession { + Promise restorePersistentAnchor(DOMString uuid); + Promise deletePersistentAnchor(DOMString uuid); +}; + +partial interface XRHitTestResult { + Promise createAnchor(); +}; + +[Exposed=Window] +interface XRAnchorSet { + readonly setlike; +}; + +partial interface XRFrame { + [SameObject] readonly attribute XRAnchorSet trackedAnchors; +}; diff --git a/test/wpt/tests/interfaces/attribution-reporting-api.idl b/test/wpt/tests/interfaces/attribution-reporting-api.idl new file mode 100644 index 00000000000..76640f54c8d --- /dev/null +++ b/test/wpt/tests/interfaces/attribution-reporting-api.idl @@ -0,0 +1,12 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: Attribution Reporting (https://wicg.github.io/attribution-reporting-api/) + +interface mixin HTMLAttributionSrcElementUtils { + [CEReactions] attribute USVString attributionSrc; +}; + +HTMLAnchorElement includes HTMLAttributionSrcElementUtils; +HTMLImageElement includes HTMLAttributionSrcElementUtils; +HTMLScriptElement includes HTMLAttributionSrcElementUtils; diff --git a/test/wpt/tests/interfaces/audio-output.idl b/test/wpt/tests/interfaces/audio-output.idl new file mode 100644 index 00000000000..80ceb225308 --- /dev/null +++ b/test/wpt/tests/interfaces/audio-output.idl @@ -0,0 +1,17 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: Audio Output Devices API (https://w3c.github.io/mediacapture-output/) + +partial interface HTMLMediaElement { + [SecureContext] readonly attribute DOMString sinkId; + [SecureContext] Promise setSinkId (DOMString sinkId); +}; + +partial interface MediaDevices { + Promise selectAudioOutput(optional AudioOutputOptions options = {}); +}; + +dictionary AudioOutputOptions { + DOMString deviceId = ""; +}; diff --git a/test/wpt/tests/interfaces/autoplay-detection.idl b/test/wpt/tests/interfaces/autoplay-detection.idl new file mode 100644 index 00000000000..cd0884fe521 --- /dev/null +++ b/test/wpt/tests/interfaces/autoplay-detection.idl @@ -0,0 +1,19 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: Autoplay Policy Detection (https://w3c.github.io/autoplay/) + +enum AutoplayPolicy { + "allowed", + "allowed-muted", + "disallowed" +}; + +enum AutoplayPolicyMediaType { "mediaelement", "audiocontext" }; + +[Exposed=Window] +partial interface Navigator { + AutoplayPolicy getAutoplayPolicy(AutoplayPolicyMediaType type); + AutoplayPolicy getAutoplayPolicy(HTMLMediaElement element); + AutoplayPolicy getAutoplayPolicy(AudioContext context); +}; diff --git a/test/wpt/tests/interfaces/background-fetch.idl b/test/wpt/tests/interfaces/background-fetch.idl new file mode 100644 index 00000000000..993bd8bc2fd --- /dev/null +++ b/test/wpt/tests/interfaces/background-fetch.idl @@ -0,0 +1,89 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: Background Fetch (https://wicg.github.io/background-fetch/) + +partial interface ServiceWorkerGlobalScope { + attribute EventHandler onbackgroundfetchsuccess; + attribute EventHandler onbackgroundfetchfail; + attribute EventHandler onbackgroundfetchabort; + attribute EventHandler onbackgroundfetchclick; +}; + +partial interface ServiceWorkerRegistration { + readonly attribute BackgroundFetchManager backgroundFetch; +}; + +[Exposed=(Window,Worker)] +interface BackgroundFetchManager { + Promise fetch(DOMString id, (RequestInfo or sequence) requests, optional BackgroundFetchOptions options = {}); + Promise get(DOMString id); + Promise> getIds(); +}; + +dictionary BackgroundFetchUIOptions { + sequence icons; + DOMString title; +}; + +dictionary BackgroundFetchOptions : BackgroundFetchUIOptions { + unsigned long long downloadTotal = 0; +}; + +[Exposed=(Window,Worker)] +interface BackgroundFetchRegistration : EventTarget { + readonly attribute DOMString id; + readonly attribute unsigned long long uploadTotal; + readonly attribute unsigned long long uploaded; + readonly attribute unsigned long long downloadTotal; + readonly attribute unsigned long long downloaded; + readonly attribute BackgroundFetchResult result; + readonly attribute BackgroundFetchFailureReason failureReason; + readonly attribute boolean recordsAvailable; + + attribute EventHandler onprogress; + + Promise abort(); + Promise match(RequestInfo request, optional CacheQueryOptions options = {}); + Promise> matchAll(optional RequestInfo request, optional CacheQueryOptions options = {}); +}; + +enum BackgroundFetchResult { "", "success", "failure" }; + +enum BackgroundFetchFailureReason { + // The background fetch has not completed yet, or was successful. + "", + // The operation was aborted by the user, or abort() was called. + "aborted", + // A response had a not-ok-status. + "bad-status", + // A fetch failed for other reasons, e.g. CORS, MIX, an invalid partial response, + // or a general network failure for a fetch that cannot be retried. + "fetch-error", + // Storage quota was reached during the operation. + "quota-exceeded", + // The provided downloadTotal was exceeded. + "download-total-exceeded" +}; + +[Exposed=(Window,Worker)] +interface BackgroundFetchRecord { + readonly attribute Request request; + readonly attribute Promise responseReady; +}; + +[Exposed=ServiceWorker] +interface BackgroundFetchEvent : ExtendableEvent { + constructor(DOMString type, BackgroundFetchEventInit init); + readonly attribute BackgroundFetchRegistration registration; +}; + +dictionary BackgroundFetchEventInit : ExtendableEventInit { + required BackgroundFetchRegistration registration; +}; + +[Exposed=ServiceWorker] +interface BackgroundFetchUpdateUIEvent : BackgroundFetchEvent { + constructor(DOMString type, BackgroundFetchEventInit init); + Promise updateUI(optional BackgroundFetchUIOptions options = {}); +}; diff --git a/test/wpt/tests/interfaces/background-sync.idl b/test/wpt/tests/interfaces/background-sync.idl new file mode 100644 index 00000000000..79a13a617af --- /dev/null +++ b/test/wpt/tests/interfaces/background-sync.idl @@ -0,0 +1,30 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: Web Background Synchronization (https://wicg.github.io/background-sync/spec/) + +partial interface ServiceWorkerRegistration { + readonly attribute SyncManager sync; +}; + +[Exposed=(Window,Worker)] +interface SyncManager { + Promise register(DOMString tag); + Promise> getTags(); +}; + +partial interface ServiceWorkerGlobalScope { + attribute EventHandler onsync; +}; + +[Exposed=ServiceWorker] +interface SyncEvent : ExtendableEvent { + constructor(DOMString type, SyncEventInit init); + readonly attribute DOMString tag; + readonly attribute boolean lastChance; +}; + +dictionary SyncEventInit : ExtendableEventInit { + required DOMString tag; + boolean lastChance = false; +}; diff --git a/test/wpt/tests/interfaces/badging.idl b/test/wpt/tests/interfaces/badging.idl new file mode 100644 index 00000000000..f34dfa7e04a --- /dev/null +++ b/test/wpt/tests/interfaces/badging.idl @@ -0,0 +1,19 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (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 clearAppBadge(); +}; + +Navigator includes NavigatorBadge; +WorkerNavigator includes NavigatorBadge; diff --git a/test/wpt/tests/interfaces/battery-status.idl b/test/wpt/tests/interfaces/battery-status.idl new file mode 100644 index 00000000000..2d042db28a6 --- /dev/null +++ b/test/wpt/tests/interfaces/battery-status.idl @@ -0,0 +1,21 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: Battery Status API (https://w3c.github.io/battery/) + +[SecureContext] +partial interface Navigator { + Promise getBattery(); +}; + +[SecureContext, Exposed=Window] +interface BatteryManager : EventTarget { + readonly attribute boolean charging; + readonly attribute unrestricted double chargingTime; + readonly attribute unrestricted double dischargingTime; + readonly attribute double level; + attribute EventHandler onchargingchange; + attribute EventHandler onchargingtimechange; + attribute EventHandler ondischargingtimechange; + attribute EventHandler onlevelchange; +}; diff --git a/test/wpt/tests/interfaces/beacon.idl b/test/wpt/tests/interfaces/beacon.idl new file mode 100644 index 00000000000..103a999fd43 --- /dev/null +++ b/test/wpt/tests/interfaces/beacon.idl @@ -0,0 +1,8 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: Beacon (https://w3c.github.io/beacon/) + +partial interface Navigator { + boolean sendBeacon(USVString url, optional BodyInit? data = null); +}; diff --git a/test/wpt/tests/interfaces/capture-handle-identity.idl b/test/wpt/tests/interfaces/capture-handle-identity.idl new file mode 100644 index 00000000000..37b2c61dac9 --- /dev/null +++ b/test/wpt/tests/interfaces/capture-handle-identity.idl @@ -0,0 +1,27 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: Capture Handle - Bootstrapping Collaboration when Screensharing (https://w3c.github.io/mediacapture-handle/identity/) + +dictionary CaptureHandleConfig { + boolean exposeOrigin = false; + DOMString handle = ""; + sequence permittedOrigins = []; +}; + +partial interface MediaDevices { + undefined setCaptureHandleConfig(optional CaptureHandleConfig config = {}); +}; + +dictionary CaptureHandle { + DOMString origin; + DOMString handle; +}; + +partial interface MediaStreamTrack { + CaptureHandle? getCaptureHandle(); +}; + +partial interface MediaStreamTrack { + attribute EventHandler oncapturehandlechange; +}; diff --git a/test/wpt/tests/interfaces/clipboard-apis.idl b/test/wpt/tests/interfaces/clipboard-apis.idl new file mode 100644 index 00000000000..3f2c9ba6f28 --- /dev/null +++ b/test/wpt/tests/interfaces/clipboard-apis.idl @@ -0,0 +1,51 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: Clipboard API and events (https://w3c.github.io/clipboard-apis/) + +dictionary ClipboardEventInit : EventInit { + DataTransfer? clipboardData = null; +}; + +[Exposed=Window] +interface ClipboardEvent : Event { + constructor(DOMString type, optional ClipboardEventInit eventInitDict = {}); + readonly attribute DataTransfer? clipboardData; +}; + +partial interface Navigator { + [SecureContext, SameObject] readonly attribute Clipboard clipboard; +}; + +typedef Promise<(DOMString or Blob)> ClipboardItemData; + +[SecureContext, Exposed=Window] +interface ClipboardItem { + constructor(record items, + optional ClipboardItemOptions options = {}); + + readonly attribute PresentationStyle presentationStyle; + readonly attribute FrozenArray types; + + Promise getType(DOMString type); +}; + +enum PresentationStyle { "unspecified", "inline", "attachment" }; + +dictionary ClipboardItemOptions { + PresentationStyle presentationStyle = "unspecified"; +}; + +typedef sequence ClipboardItems; + +[SecureContext, Exposed=Window] +interface Clipboard : EventTarget { + Promise read(); + Promise readText(); + Promise write(ClipboardItems data); + Promise writeText(DOMString data); +}; + +dictionary ClipboardPermissionDescriptor : PermissionDescriptor { + boolean allowWithoutGesture = false; +}; diff --git a/test/wpt/tests/interfaces/close-watcher.idl b/test/wpt/tests/interfaces/close-watcher.idl new file mode 100644 index 00000000000..de7940c07e7 --- /dev/null +++ b/test/wpt/tests/interfaces/close-watcher.idl @@ -0,0 +1,19 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: Close Watcher API (https://wicg.github.io/close-watcher/) + +[Exposed=Window] +interface CloseWatcher : EventTarget { + constructor(optional CloseWatcherOptions options = {}); + + undefined destroy(); + undefined close(); + + attribute EventHandler oncancel; + attribute EventHandler onclose; +}; + +dictionary CloseWatcherOptions { + AbortSignal signal; +}; diff --git a/test/wpt/tests/interfaces/compat.idl b/test/wpt/tests/interfaces/compat.idl new file mode 100644 index 00000000000..8106c2d4e05 --- /dev/null +++ b/test/wpt/tests/interfaces/compat.idl @@ -0,0 +1,13 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: Compatibility Standard (https://compat.spec.whatwg.org/) + +partial interface Window { + readonly attribute short orientation; + attribute EventHandler onorientationchange; +}; + +partial interface HTMLBodyElement { + attribute EventHandler onorientationchange; +}; diff --git a/test/wpt/tests/interfaces/compression.idl b/test/wpt/tests/interfaces/compression.idl new file mode 100644 index 00000000000..88be302a4a6 --- /dev/null +++ b/test/wpt/tests/interfaces/compression.idl @@ -0,0 +1,16 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: Compression Streams (https://wicg.github.io/compression/) + +[Exposed=*] +interface CompressionStream { + constructor(DOMString format); +}; +CompressionStream includes GenericTransformStream; + +[Exposed=*] +interface DecompressionStream { + constructor(DOMString format); +}; +DecompressionStream includes GenericTransformStream; diff --git a/test/wpt/tests/interfaces/compute-pressure.idl b/test/wpt/tests/interfaces/compute-pressure.idl new file mode 100644 index 00000000000..42ff4f207ab --- /dev/null +++ b/test/wpt/tests/interfaces/compute-pressure.idl @@ -0,0 +1,40 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: Compute Pressure Level 1 (https://w3c.github.io/compute-pressure/) + +enum PressureState { "nominal", "fair", "serious", "critical" }; + +enum PressureFactor { "thermal", "power-supply" }; + +callback PressureUpdateCallback = undefined ( + sequence changes, + PressureObserver observer +); + +enum PressureSource { "cpu" }; + +[Exposed=(DedicatedWorker,SharedWorker,Window), SecureContext] +interface PressureObserver { + constructor(PressureUpdateCallback callback, optional PressureObserverOptions options = {}); + + Promise observe(PressureSource source); + undefined unobserve(PressureSource source); + undefined disconnect(); + sequence takeRecords(); + + [SameObject] static readonly attribute FrozenArray supportedSources; +}; + +[Exposed=(DedicatedWorker,SharedWorker,Window), SecureContext] +interface PressureRecord { + readonly attribute PressureSource source; + readonly attribute PressureState state; + readonly attribute FrozenArray factors; + readonly attribute DOMHighResTimeStamp time; + [Default] object toJSON(); +}; + +dictionary PressureObserverOptions { + double sampleRate = 1.0; +}; diff --git a/test/wpt/tests/interfaces/console.idl b/test/wpt/tests/interfaces/console.idl new file mode 100644 index 00000000000..fdf1d0df978 --- /dev/null +++ b/test/wpt/tests/interfaces/console.idl @@ -0,0 +1,34 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: Console Standard (https://console.spec.whatwg.org/) + +[Exposed=*] +namespace console { // but see namespace object requirements below + // Logging + undefined assert(optional boolean condition = false, any... data); + undefined clear(); + undefined debug(any... data); + undefined error(any... data); + undefined info(any... data); + undefined log(any... data); + undefined table(optional any tabularData, optional sequence properties); + undefined trace(any... data); + undefined warn(any... data); + undefined dir(optional any item, optional object? options); + undefined dirxml(any... data); + + // Counting + undefined count(optional DOMString label = "default"); + undefined countReset(optional DOMString label = "default"); + + // Grouping + undefined group(any... data); + undefined groupCollapsed(any... data); + undefined groupEnd(); + + // Timing + undefined time(optional DOMString label = "default"); + undefined timeLog(optional DOMString label = "default", any... data); + undefined timeEnd(optional DOMString label = "default"); +}; diff --git a/test/wpt/tests/interfaces/contact-picker.idl b/test/wpt/tests/interfaces/contact-picker.idl new file mode 100644 index 00000000000..aece81664e4 --- /dev/null +++ b/test/wpt/tests/interfaces/contact-picker.idl @@ -0,0 +1,44 @@ +// 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/) + +[Exposed=Window] +partial interface Navigator { + [SecureContext, SameObject] readonly attribute ContactsManager contacts; +}; + +enum ContactProperty { "address", "email", "icon", "name", "tel" }; + +[Exposed=Window] +interface ContactAddress { + [Default] object toJSON(); + readonly attribute DOMString city; + readonly attribute DOMString country; + readonly attribute DOMString dependentLocality; + readonly attribute DOMString organization; + readonly attribute DOMString phone; + readonly attribute DOMString postalCode; + readonly attribute DOMString recipient; + readonly attribute DOMString region; + readonly attribute DOMString sortingCode; + readonly attribute FrozenArray addressLine; +}; + +dictionary ContactInfo { + sequence address; + sequence email; + sequence icon; + sequence name; + sequence tel; +}; + +dictionary ContactsSelectOptions { + boolean multiple = false; +}; + +[Exposed=Window,SecureContext] +interface ContactsManager { + Promise> getProperties(); + Promise> select(sequence properties, optional ContactsSelectOptions options = {}); +}; diff --git a/test/wpt/tests/interfaces/content-index.idl b/test/wpt/tests/interfaces/content-index.idl new file mode 100644 index 00000000000..177c5b9f11d --- /dev/null +++ b/test/wpt/tests/interfaces/content-index.idl @@ -0,0 +1,46 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: Content Index (https://wicg.github.io/content-index/spec/) + +partial interface ServiceWorkerGlobalScope { + attribute EventHandler oncontentdelete; +}; + +partial interface ServiceWorkerRegistration { + [SameObject] readonly attribute ContentIndex index; +}; + +enum ContentCategory { + "", + "homepage", + "article", + "video", + "audio", +}; + +dictionary ContentDescription { + required DOMString id; + required DOMString title; + required DOMString description; + ContentCategory category = ""; + sequence icons = []; + required USVString url; +}; + +[Exposed=(Window,Worker)] +interface ContentIndex { + Promise add(ContentDescription description); + Promise delete(DOMString id); + Promise> getAll(); +}; + +dictionary ContentIndexEventInit : ExtendableEventInit { + required DOMString id; +}; + +[Exposed=ServiceWorker] +interface ContentIndexEvent : ExtendableEvent { + constructor(DOMString type, ContentIndexEventInit init); + readonly attribute DOMString id; +}; diff --git a/test/wpt/tests/interfaces/cookie-store.idl b/test/wpt/tests/interfaces/cookie-store.idl new file mode 100644 index 00000000000..72ef3f8c824 --- /dev/null +++ b/test/wpt/tests/interfaces/cookie-store.idl @@ -0,0 +1,110 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: Cookie Store API (https://wicg.github.io/cookie-store/) + +[Exposed=(ServiceWorker,Window), + SecureContext] +interface CookieStore : EventTarget { + Promise get(USVString name); + Promise get(optional CookieStoreGetOptions options = {}); + + Promise getAll(USVString name); + Promise getAll(optional CookieStoreGetOptions options = {}); + + Promise set(USVString name, USVString value); + Promise set(CookieInit options); + + Promise delete(USVString name); + Promise delete(CookieStoreDeleteOptions options); + + [Exposed=Window] + attribute EventHandler onchange; +}; + +dictionary CookieStoreGetOptions { + USVString name; + USVString url; +}; + +enum CookieSameSite { + "strict", + "lax", + "none" +}; + +dictionary CookieInit { + required USVString name; + required USVString value; + EpochTimeStamp? expires = null; + USVString? domain = null; + USVString path = "/"; + CookieSameSite sameSite = "strict"; +}; + +dictionary CookieStoreDeleteOptions { + required USVString name; + USVString? domain = null; + USVString path = "/"; +}; + +dictionary CookieListItem { + USVString name; + USVString value; + USVString? domain; + USVString path; + EpochTimeStamp? expires; + boolean secure; + CookieSameSite sameSite; +}; + +typedef sequence CookieList; + +[Exposed=(ServiceWorker,Window), + SecureContext] +interface CookieStoreManager { + Promise subscribe(sequence subscriptions); + Promise> getSubscriptions(); + Promise unsubscribe(sequence subscriptions); +}; + +[Exposed=(ServiceWorker,Window)] +partial interface ServiceWorkerRegistration { + [SameObject] readonly attribute CookieStoreManager cookies; +}; + +[Exposed=Window, + SecureContext] +interface CookieChangeEvent : Event { + constructor(DOMString type, optional CookieChangeEventInit eventInitDict = {}); + [SameObject] readonly attribute FrozenArray changed; + [SameObject] readonly attribute FrozenArray deleted; +}; + +dictionary CookieChangeEventInit : EventInit { + CookieList changed; + CookieList deleted; +}; + +[Exposed=ServiceWorker] +interface ExtendableCookieChangeEvent : ExtendableEvent { + constructor(DOMString type, optional ExtendableCookieChangeEventInit eventInitDict = {}); + [SameObject] readonly attribute FrozenArray changed; + [SameObject] readonly attribute FrozenArray deleted; +}; + +dictionary ExtendableCookieChangeEventInit : ExtendableEventInit { + CookieList changed; + CookieList deleted; +}; + +[SecureContext] +partial interface Window { + [SameObject] readonly attribute CookieStore cookieStore; +}; + +partial interface ServiceWorkerGlobalScope { + [SameObject] readonly attribute CookieStore cookieStore; + + attribute EventHandler oncookiechange; +}; diff --git a/test/wpt/tests/interfaces/credential-management.idl b/test/wpt/tests/interfaces/credential-management.idl new file mode 100644 index 00000000000..e9fab13f35f --- /dev/null +++ b/test/wpt/tests/interfaces/credential-management.idl @@ -0,0 +1,105 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: Credential Management Level 1 (https://w3c.github.io/webappsec-credential-management/) + +[Exposed=Window, SecureContext] +interface Credential { + readonly attribute USVString id; + readonly attribute DOMString type; + static Promise isConditionalMediationAvailable(); +}; + +[SecureContext] +interface mixin CredentialUserData { + readonly attribute USVString name; + readonly attribute USVString iconURL; +}; + +partial interface Navigator { + [SecureContext, SameObject] readonly attribute CredentialsContainer credentials; +}; + +[Exposed=Window, SecureContext] +interface CredentialsContainer { + Promise get(optional CredentialRequestOptions options = {}); + Promise store(Credential credential); + Promise create(optional CredentialCreationOptions options = {}); + Promise preventSilentAccess(); +}; + +dictionary CredentialData { + required USVString id; +}; + +dictionary CredentialRequestOptions { + CredentialMediationRequirement mediation = "optional"; + AbortSignal signal; +}; + +enum CredentialMediationRequirement { + "silent", + "optional", + "conditional", + "required" +}; + +dictionary CredentialCreationOptions { + AbortSignal signal; +}; + +[Exposed=Window, + SecureContext] +interface PasswordCredential : Credential { + constructor(HTMLFormElement form); + constructor(PasswordCredentialData data); + readonly attribute USVString password; +}; +PasswordCredential includes CredentialUserData; + +partial dictionary CredentialRequestOptions { + boolean password = false; +}; + +dictionary PasswordCredentialData : CredentialData { + USVString name; + USVString iconURL; + required USVString origin; + required USVString password; +}; + +typedef (PasswordCredentialData or HTMLFormElement) PasswordCredentialInit; + +partial dictionary CredentialCreationOptions { + PasswordCredentialInit password; +}; + +[Exposed=Window, + SecureContext] +interface FederatedCredential : Credential { + constructor(FederatedCredentialInit data); + readonly attribute USVString provider; + readonly attribute DOMString? protocol; +}; +FederatedCredential includes CredentialUserData; + +dictionary FederatedCredentialRequestOptions { + sequence providers; + sequence protocols; +}; + +partial dictionary CredentialRequestOptions { + FederatedCredentialRequestOptions federated; +}; + +dictionary FederatedCredentialInit : CredentialData { + USVString name; + USVString iconURL; + required USVString origin; + required USVString provider; + DOMString protocol; +}; + +partial dictionary CredentialCreationOptions { + FederatedCredentialInit federated; +}; diff --git a/test/wpt/tests/interfaces/csp-embedded-enforcement.idl b/test/wpt/tests/interfaces/csp-embedded-enforcement.idl new file mode 100644 index 00000000000..a980630bc18 --- /dev/null +++ b/test/wpt/tests/interfaces/csp-embedded-enforcement.idl @@ -0,0 +1,8 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: Content Security Policy: Embedded Enforcement (https://w3c.github.io/webappsec-cspee/) + +partial interface HTMLIFrameElement { + [CEReactions] attribute DOMString csp; +}; diff --git a/test/wpt/tests/interfaces/csp-next.idl b/test/wpt/tests/interfaces/csp-next.idl new file mode 100644 index 00000000000..d94b36cf885 --- /dev/null +++ b/test/wpt/tests/interfaces/csp-next.idl @@ -0,0 +1,21 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: Scripting Policy (https://wicg.github.io/csp-next/scripting-policy.html) + +enum ScriptingPolicyViolationType { + "externalScript", + "inlineScript", + "inlineEventHandler", + "eval" +}; + +[Exposed=(Window,Worker), SecureContext] +interface ScriptingPolicyReportBody : ReportBody { + [Default] object toJSON(); + readonly attribute DOMString violationType; + readonly attribute USVString? violationURL; + readonly attribute USVString? violationSample; + readonly attribute unsigned long lineno; + readonly attribute unsigned long colno; +}; diff --git a/test/wpt/tests/interfaces/css-animation-worklet.idl b/test/wpt/tests/interfaces/css-animation-worklet.idl new file mode 100644 index 00000000000..82d34a3ea2a --- /dev/null +++ b/test/wpt/tests/interfaces/css-animation-worklet.idl @@ -0,0 +1,37 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: CSS Animation Worklet API (https://drafts.css-houdini.org/css-animationworklet-1/) + +[Exposed=Window] +partial namespace CSS { + [SameObject] readonly attribute Worklet animationWorklet; +}; + +[ Global=(Worklet,AnimationWorklet), Exposed=AnimationWorklet ] +interface AnimationWorkletGlobalScope : WorkletGlobalScope { + undefined registerAnimator(DOMString name, AnimatorInstanceConstructor animatorCtor); +}; + +callback AnimatorInstanceConstructor = any (any options, optional any state); + +[ Exposed=AnimationWorklet ] +interface WorkletAnimationEffect { + EffectTiming getTiming(); + ComputedEffectTiming getComputedTiming(); + attribute double? localTime; +}; + +[Exposed=Window] +interface WorkletAnimation : Animation { + constructor(DOMString animatorName, + optional (AnimationEffect or sequence)? effects = null, + optional AnimationTimeline? timeline, + optional any options); + readonly attribute DOMString animatorName; +}; + +[Exposed=AnimationWorklet] +interface WorkletGroupEffect { + sequence getChildren(); +}; diff --git a/test/wpt/tests/interfaces/css-animations-2.idl b/test/wpt/tests/interfaces/css-animations-2.idl new file mode 100644 index 00000000000..84f138e8abb --- /dev/null +++ b/test/wpt/tests/interfaces/css-animations-2.idl @@ -0,0 +1,9 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: CSS Animations Level 2 (https://drafts.csswg.org/css-animations-2/) + +[Exposed=Window] +interface CSSAnimation : Animation { + readonly attribute CSSOMString animationName; +}; diff --git a/test/wpt/tests/interfaces/css-animations.idl b/test/wpt/tests/interfaces/css-animations.idl new file mode 100644 index 00000000000..6620e0156dc --- /dev/null +++ b/test/wpt/tests/interfaces/css-animations.idl @@ -0,0 +1,47 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: CSS Animations Level 1 (https://drafts.csswg.org/css-animations-1/) + +[Exposed=Window] +interface AnimationEvent : Event { + constructor(CSSOMString type, optional AnimationEventInit animationEventInitDict = {}); + readonly attribute CSSOMString animationName; + readonly attribute double elapsedTime; + readonly attribute CSSOMString pseudoElement; +}; +dictionary AnimationEventInit : EventInit { + CSSOMString animationName = ""; + double elapsedTime = 0.0; + CSSOMString pseudoElement = ""; +}; + +partial interface CSSRule { + const unsigned short KEYFRAMES_RULE = 7; + const unsigned short KEYFRAME_RULE = 8; +}; + +[Exposed=Window] +interface CSSKeyframeRule : CSSRule { + attribute CSSOMString keyText; + [SameObject, PutForwards=cssText] readonly attribute CSSStyleDeclaration style; +}; + +[Exposed=Window] +interface CSSKeyframesRule : CSSRule { + attribute CSSOMString name; + readonly attribute CSSRuleList cssRules; + readonly attribute unsigned long length; + + getter CSSKeyframeRule (unsigned long index); + undefined appendRule(CSSOMString rule); + undefined deleteRule(CSSOMString select); + CSSKeyframeRule? findRule(CSSOMString select); +}; + +partial interface mixin GlobalEventHandlers { + attribute EventHandler onanimationstart; + attribute EventHandler onanimationiteration; + attribute EventHandler onanimationend; + attribute EventHandler onanimationcancel; +}; diff --git a/test/wpt/tests/interfaces/css-cascade.idl b/test/wpt/tests/interfaces/css-cascade.idl new file mode 100644 index 00000000000..9011dc7fd9e --- /dev/null +++ b/test/wpt/tests/interfaces/css-cascade.idl @@ -0,0 +1,18 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (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; +}; + +[Exposed=Window] +interface CSSLayerStatementRule : CSSRule { + readonly attribute FrozenArray nameList; +}; diff --git a/test/wpt/tests/interfaces/css-color-5.idl b/test/wpt/tests/interfaces/css-color-5.idl new file mode 100644 index 00000000000..6f5c6dfc096 --- /dev/null +++ b/test/wpt/tests/interfaces/css-color-5.idl @@ -0,0 +1,12 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: CSS Color Module Level 5 (https://drafts.csswg.org/css-color-5/) + +[Exposed=Window] +interface CSSColorProfileRule : CSSRule { + readonly attribute CSSOMString name ; + readonly attribute CSSOMString src ; + readonly attribute CSSOMString renderingIntent ; + readonly attribute CSSOMString components ; +}; diff --git a/test/wpt/tests/interfaces/css-conditional.idl b/test/wpt/tests/interfaces/css-conditional.idl new file mode 100644 index 00000000000..d87f305fddf --- /dev/null +++ b/test/wpt/tests/interfaces/css-conditional.idl @@ -0,0 +1,27 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: CSS Conditional Rules Module Level 3 (https://drafts.csswg.org/css-conditional-3/) + +partial interface CSSRule { + const unsigned short SUPPORTS_RULE = 12; +}; + +[Exposed=Window] +interface CSSConditionRule : CSSGroupingRule { + readonly attribute CSSOMString conditionText; +}; + +[Exposed=Window] +interface CSSMediaRule : CSSConditionRule { + [SameObject, PutForwards=mediaText] readonly attribute MediaList media; +}; + +[Exposed=Window] +interface CSSSupportsRule : CSSConditionRule { +}; + +partial namespace CSS { + boolean supports(CSSOMString property, CSSOMString value); + boolean supports(CSSOMString conditionText); +}; diff --git a/test/wpt/tests/interfaces/css-contain-3.idl b/test/wpt/tests/interfaces/css-contain-3.idl new file mode 100644 index 00000000000..0ecf3804954 --- /dev/null +++ b/test/wpt/tests/interfaces/css-contain-3.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 Containment Module Level 3 (https://drafts.csswg.org/css-contain-3/) + +[Exposed=Window] +interface CSSContainerRule : CSSConditionRule { + readonly attribute CSSOMString containerName; + readonly attribute CSSOMString containerQuery; +}; diff --git a/test/wpt/tests/interfaces/css-contain.idl b/test/wpt/tests/interfaces/css-contain.idl new file mode 100644 index 00000000000..6b29119617a --- /dev/null +++ b/test/wpt/tests/interfaces/css-contain.idl @@ -0,0 +1,13 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: CSS Containment Module Level 2 (https://drafts.csswg.org/css-contain-2/) + +[Exposed=Window] +interface ContentVisibilityAutoStateChangedEvent : Event { + constructor(DOMString type, optional ContentVisibilityAutoStateChangedEventInit eventInitDict = {}); + readonly attribute boolean skipped; +}; +dictionary ContentVisibilityAutoStateChangedEventInit : EventInit { + boolean skipped = false; +}; diff --git a/test/wpt/tests/interfaces/css-counter-styles.idl b/test/wpt/tests/interfaces/css-counter-styles.idl new file mode 100644 index 00000000000..f679e0fe558 --- /dev/null +++ b/test/wpt/tests/interfaces/css-counter-styles.idl @@ -0,0 +1,23 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: CSS Counter Styles Level 3 (https://drafts.csswg.org/css-counter-styles-3/) + +partial interface CSSRule { + const unsigned short COUNTER_STYLE_RULE = 11; +}; + +[Exposed=Window] +interface CSSCounterStyleRule : CSSRule { + attribute CSSOMString name; + attribute CSSOMString system; + attribute CSSOMString symbols; + attribute CSSOMString additiveSymbols; + attribute CSSOMString negative; + attribute CSSOMString prefix; + attribute CSSOMString suffix; + attribute CSSOMString range; + attribute CSSOMString pad; + attribute CSSOMString speakAs; + attribute CSSOMString fallback; +}; diff --git a/test/wpt/tests/interfaces/css-font-loading.idl b/test/wpt/tests/interfaces/css-font-loading.idl new file mode 100644 index 00000000000..6f2e16dd641 --- /dev/null +++ b/test/wpt/tests/interfaces/css-font-loading.idl @@ -0,0 +1,134 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: CSS Font Loading Module Level 3 (https://drafts.csswg.org/css-font-loading-3/) + +typedef (ArrayBuffer or ArrayBufferView) BinaryData; + +dictionary FontFaceDescriptors { + CSSOMString style = "normal"; + CSSOMString weight = "normal"; + CSSOMString stretch = "normal"; + CSSOMString unicodeRange = "U+0-10FFFF"; + CSSOMString variant = "normal"; + CSSOMString featureSettings = "normal"; + CSSOMString variationSettings = "normal"; + CSSOMString display = "auto"; + CSSOMString ascentOverride = "normal"; + CSSOMString descentOverride = "normal"; + CSSOMString lineGapOverride = "normal"; +}; + +enum FontFaceLoadStatus { "unloaded", "loading", "loaded", "error" }; + +[Exposed=(Window,Worker)] +interface FontFace { + constructor(CSSOMString family, (CSSOMString or BinaryData) source, + optional FontFaceDescriptors descriptors = {}); + attribute CSSOMString family; + attribute CSSOMString style; + attribute CSSOMString weight; + attribute CSSOMString stretch; + attribute CSSOMString unicodeRange; + attribute CSSOMString variant; + attribute CSSOMString featureSettings; + attribute CSSOMString variationSettings; + attribute CSSOMString display; + attribute CSSOMString ascentOverride; + attribute CSSOMString descentOverride; + attribute CSSOMString lineGapOverride; + + readonly attribute FontFaceLoadStatus status; + + Promise load(); + readonly attribute Promise loaded; +}; + +[Exposed=(Window,Worker)] +interface FontFaceFeatures { + /* The CSSWG is still discussing what goes in here */ +}; + +[Exposed=(Window,Worker)] +interface FontFaceVariationAxis { + readonly attribute DOMString name; + readonly attribute DOMString axisTag; + readonly attribute double minimumValue; + readonly attribute double maximumValue; + readonly attribute double defaultValue; +}; + +[Exposed=(Window,Worker)] +interface FontFaceVariations { + readonly setlike; +}; + +[Exposed=(Window,Worker)] +interface FontFacePalette { + iterable; + readonly attribute unsigned long length; + getter DOMString (unsigned long index); + readonly attribute boolean usableWithLightBackground; + readonly attribute boolean usableWithDarkBackground; +}; + +[Exposed=(Window,Worker)] +interface FontFacePalettes { + iterable; + readonly attribute unsigned long length; + getter FontFacePalette (unsigned long index); +}; + +partial interface FontFace { + readonly attribute FontFaceFeatures features; + readonly attribute FontFaceVariations variations; + readonly attribute FontFacePalettes palettes; +}; + +dictionary FontFaceSetLoadEventInit : EventInit { + sequence fontfaces = []; +}; + +[Exposed=(Window,Worker)] +interface FontFaceSetLoadEvent : Event { + constructor(CSSOMString type, optional FontFaceSetLoadEventInit eventInitDict = {}); + [SameObject] readonly attribute FrozenArray fontfaces; +}; + +enum FontFaceSetLoadStatus { "loading", "loaded" }; + +[Exposed=(Window,Worker)] +interface FontFaceSet : EventTarget { + constructor(sequence initialFaces); + + setlike; + FontFaceSet add(FontFace font); + boolean delete(FontFace font); + undefined clear(); + + // events for when loading state changes + attribute EventHandler onloading; + attribute EventHandler onloadingdone; + attribute EventHandler onloadingerror; + + // check and start loads if appropriate + // and fulfill promise when all loads complete + Promise> load(CSSOMString font, optional CSSOMString text = " "); + + // return whether all fonts in the fontlist are loaded + // (does not initiate load if not available) + boolean check(CSSOMString font, optional CSSOMString text = " "); + + // async notification that font loading and layout operations are done + readonly attribute Promise ready; + + // loading state, "loading" while one or more fonts loading, "loaded" otherwise + readonly attribute FontFaceSetLoadStatus status; +}; + +interface mixin FontFaceSource { + readonly attribute FontFaceSet fonts; +}; + +Document includes FontFaceSource; +WorkerGlobalScope includes FontFaceSource; diff --git a/test/wpt/tests/interfaces/css-fonts.idl b/test/wpt/tests/interfaces/css-fonts.idl new file mode 100644 index 00000000000..eddfc025215 --- /dev/null +++ b/test/wpt/tests/interfaces/css-fonts.idl @@ -0,0 +1,36 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: CSS Fonts Module Level 4 (https://drafts.csswg.org/css-fonts-4/) + +[Exposed=Window] +interface CSSFontFaceRule : CSSRule { + readonly attribute CSSStyleDeclaration style; +}; + +partial interface CSSRule { const unsigned short FONT_FEATURE_VALUES_RULE = 14; +}; +[Exposed=Window] +interface CSSFontFeatureValuesRule : CSSRule { + attribute CSSOMString fontFamily; + readonly attribute CSSFontFeatureValuesMap annotation; + readonly attribute CSSFontFeatureValuesMap ornaments; + readonly attribute CSSFontFeatureValuesMap stylistic; + readonly attribute CSSFontFeatureValuesMap swash; + readonly attribute CSSFontFeatureValuesMap characterVariant; + readonly attribute CSSFontFeatureValuesMap styleset; +}; + +[Exposed=Window] +interface CSSFontFeatureValuesMap { + maplike>; + undefined set(CSSOMString featureValueName, + (unsigned long or sequence) values); +}; + +[Exposed=Window]interface CSSFontPaletteValuesRule : CSSRule { + readonly attribute CSSOMString name; + readonly attribute CSSOMString fontFamily; + readonly attribute CSSOMString basePalette; + readonly attribute CSSOMString overrideColors; +}; diff --git a/test/wpt/tests/interfaces/css-highlight-api.idl b/test/wpt/tests/interfaces/css-highlight-api.idl new file mode 100644 index 00000000000..f3c6b2e9d21 --- /dev/null +++ b/test/wpt/tests/interfaces/css-highlight-api.idl @@ -0,0 +1,27 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: CSS Custom Highlight API Module Level 1 (https://drafts.csswg.org/css-highlight-api-1/) + +enum HighlightType { + "highlight", + "spelling-error", + "grammar-error" +}; + +[Exposed=Window] +interface Highlight { + constructor(AbstractRange... initialRanges); + setlike; + attribute long priority; + attribute HighlightType type; +}; + +partial namespace CSS { + readonly attribute HighlightRegistry highlights; +}; + +[Exposed=Window] +interface HighlightRegistry { + maplike; +}; diff --git a/test/wpt/tests/interfaces/css-images-4.idl b/test/wpt/tests/interfaces/css-images-4.idl new file mode 100644 index 00000000000..8866b008ccb --- /dev/null +++ b/test/wpt/tests/interfaces/css-images-4.idl @@ -0,0 +1,8 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: CSS Images Module Level 4 (https://drafts.csswg.org/css-images-4/) + +partial namespace CSS { + [SameObject] readonly attribute any elementSources; +}; diff --git a/test/wpt/tests/interfaces/css-layout-api.idl b/test/wpt/tests/interfaces/css-layout-api.idl new file mode 100644 index 00000000000..2b772d5b84a --- /dev/null +++ b/test/wpt/tests/interfaces/css-layout-api.idl @@ -0,0 +1,144 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: CSS Layout API Level 1 (https://drafts.css-houdini.org/css-layout-api-1/) + +partial namespace CSS { + [SameObject] readonly attribute Worklet layoutWorklet; +}; + +[Global=(Worklet,LayoutWorklet),Exposed=LayoutWorklet] +interface LayoutWorkletGlobalScope : WorkletGlobalScope { + undefined registerLayout(DOMString name, VoidFunction layoutCtor); +}; + +dictionary LayoutOptions { + ChildDisplayType childDisplay = "block"; + LayoutSizingMode sizing = "block-like"; +}; + +enum ChildDisplayType { + "block", // default - "blockifies" the child boxes. + "normal", +}; + +enum LayoutSizingMode { + "block-like", // default - Sizing behaves like block containers. + "manual", // Sizing is specified by the web developer. +}; + +[Exposed=LayoutWorklet] +interface LayoutChild { + readonly attribute StylePropertyMapReadOnly styleMap; + + Promise intrinsicSizes(); + Promise layoutNextFragment(LayoutConstraintsOptions constraints, ChildBreakToken breakToken); +}; + +[Exposed=LayoutWorklet] +interface LayoutFragment { + readonly attribute double inlineSize; + readonly attribute double blockSize; + + attribute double inlineOffset; + attribute double blockOffset; + + readonly attribute any data; + + readonly attribute ChildBreakToken? breakToken; +}; + +[Exposed=LayoutWorklet] +interface IntrinsicSizes { + readonly attribute double minContentSize; + readonly attribute double maxContentSize; +}; + +[Exposed=LayoutWorklet] +interface LayoutConstraints { + readonly attribute double availableInlineSize; + readonly attribute double availableBlockSize; + + readonly attribute double? fixedInlineSize; + readonly attribute double? fixedBlockSize; + + readonly attribute double percentageInlineSize; + readonly attribute double percentageBlockSize; + + readonly attribute double? blockFragmentationOffset; + readonly attribute BlockFragmentationType blockFragmentationType; + + readonly attribute any data; +}; + +enum BlockFragmentationType { "none", "page", "column", "region" }; + +dictionary LayoutConstraintsOptions { + double availableInlineSize; + double availableBlockSize; + + double fixedInlineSize; + double fixedBlockSize; + + double percentageInlineSize; + double percentageBlockSize; + + double blockFragmentationOffset; + BlockFragmentationType blockFragmentationType = "none"; + + any data; +}; + +[Exposed=LayoutWorklet] +interface ChildBreakToken { + readonly attribute BreakType breakType; + readonly attribute LayoutChild child; +}; + +[Exposed=LayoutWorklet] +interface BreakToken { + readonly attribute FrozenArray childBreakTokens; + readonly attribute any data; +}; + +dictionary BreakTokenOptions { + sequence childBreakTokens; + any data = null; +}; + +enum BreakType { "none", "line", "column", "page", "region" }; + +[Exposed=LayoutWorklet] +interface LayoutEdges { + readonly attribute double inlineStart; + readonly attribute double inlineEnd; + + readonly attribute double blockStart; + readonly attribute double blockEnd; + + // Convenience attributes for the sum in one direction. + readonly attribute double inline; + readonly attribute double block; +}; + +// This is the final return value from the author defined layout() method. +dictionary FragmentResultOptions { + double inlineSize = 0; + double blockSize = 0; + double autoBlockSize = 0; + sequence childFragments = []; + any data = null; + BreakTokenOptions breakToken = null; +}; + +[Exposed=LayoutWorklet] +interface FragmentResult { + constructor(optional FragmentResultOptions options = {}); + readonly attribute double inlineSize; + readonly attribute double blockSize; +}; + +dictionary IntrinsicSizesResultOptions { + double maxContentSize; + double minContentSize; +}; diff --git a/test/wpt/tests/interfaces/css-masking.idl b/test/wpt/tests/interfaces/css-masking.idl new file mode 100644 index 00000000000..72fbd9aa1ff --- /dev/null +++ b/test/wpt/tests/interfaces/css-masking.idl @@ -0,0 +1,20 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: CSS Masking Module Level 1 (https://drafts.fxtf.org/css-masking-1/) + +[Exposed=Window] +interface SVGClipPathElement : SVGElement { + readonly attribute SVGAnimatedEnumeration clipPathUnits; + readonly attribute SVGAnimatedTransformList transform; +}; + +[Exposed=Window] +interface SVGMaskElement : SVGElement { + readonly attribute SVGAnimatedEnumeration maskUnits; + readonly attribute SVGAnimatedEnumeration maskContentUnits; + readonly attribute SVGAnimatedLength x; + readonly attribute SVGAnimatedLength y; + readonly attribute SVGAnimatedLength width; + readonly attribute SVGAnimatedLength height; +}; diff --git a/test/wpt/tests/interfaces/css-nav.idl b/test/wpt/tests/interfaces/css-nav.idl new file mode 100644 index 00000000000..03f039e4cb5 --- /dev/null +++ b/test/wpt/tests/interfaces/css-nav.idl @@ -0,0 +1,48 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: CSS Spatial Navigation Level 1 (https://drafts.csswg.org/css-nav-1/) + +enum SpatialNavigationDirection { + "up", + "down", + "left", + "right", +}; + +partial interface Window { + undefined navigate(SpatialNavigationDirection dir); +}; + +enum FocusableAreaSearchMode { + "visible", + "all" +}; + +dictionary FocusableAreasOption { + FocusableAreaSearchMode mode; +}; + +dictionary SpatialNavigationSearchOptions { + sequence? candidates; + Node? container; +}; + +partial interface Element { + Node getSpatialNavigationContainer(); + sequence focusableAreas(optional FocusableAreasOption option = {}); + Node? spatialNavigationSearch(SpatialNavigationDirection dir, optional SpatialNavigationSearchOptions options = {}); +}; + +[Exposed=Window] +interface NavigationEvent : UIEvent { + constructor(DOMString type, + optional NavigationEventInit eventInitDict = {}); + readonly attribute SpatialNavigationDirection dir; + readonly attribute EventTarget? relatedTarget; +}; + +dictionary NavigationEventInit : UIEventInit { + SpatialNavigationDirection dir; + EventTarget? relatedTarget = null; +}; diff --git a/test/wpt/tests/interfaces/css-nesting.idl b/test/wpt/tests/interfaces/css-nesting.idl new file mode 100644 index 00000000000..01f27ab9d65 --- /dev/null +++ b/test/wpt/tests/interfaces/css-nesting.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 Nesting Module (https://drafts.csswg.org/css-nesting-1/) + +partial interface CSSStyleRule { + [SameObject] readonly attribute CSSRuleList cssRules; + unsigned long insertRule(CSSOMString rule, optional unsigned long index = 0); + undefined deleteRule(unsigned long index); +}; diff --git a/test/wpt/tests/interfaces/css-paint-api.idl b/test/wpt/tests/interfaces/css-paint-api.idl new file mode 100644 index 00000000000..0924c535566 --- /dev/null +++ b/test/wpt/tests/interfaces/css-paint-api.idl @@ -0,0 +1,39 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: CSS Painting API Level 1 (https://drafts.css-houdini.org/css-paint-api-1/) + +partial namespace CSS { + [SameObject] readonly attribute Worklet paintWorklet; +}; + +[Global=(Worklet,PaintWorklet),Exposed=PaintWorklet] +interface PaintWorkletGlobalScope : WorkletGlobalScope { + undefined registerPaint(DOMString name, VoidFunction paintCtor); + readonly attribute unrestricted double devicePixelRatio; +}; + +dictionary PaintRenderingContext2DSettings { + boolean alpha = true; +}; + +[Exposed=PaintWorklet] +interface PaintRenderingContext2D { +}; +PaintRenderingContext2D includes CanvasState; +PaintRenderingContext2D includes CanvasTransform; +PaintRenderingContext2D includes CanvasCompositing; +PaintRenderingContext2D includes CanvasImageSmoothing; +PaintRenderingContext2D includes CanvasFillStrokeStyles; +PaintRenderingContext2D includes CanvasShadowStyles; +PaintRenderingContext2D includes CanvasRect; +PaintRenderingContext2D includes CanvasDrawPath; +PaintRenderingContext2D includes CanvasDrawImage; +PaintRenderingContext2D includes CanvasPathDrawingStyles; +PaintRenderingContext2D includes CanvasPath; + +[Exposed=PaintWorklet] +interface PaintSize { + readonly attribute double width; + readonly attribute double height; +}; diff --git a/test/wpt/tests/interfaces/css-parser-api.idl b/test/wpt/tests/interfaces/css-parser-api.idl new file mode 100644 index 00000000000..4e34a3f25d7 --- /dev/null +++ b/test/wpt/tests/interfaces/css-parser-api.idl @@ -0,0 +1,76 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: CSS Parser API (https://wicg.github.io/css-parser-api/) + +typedef (DOMString or ReadableStream) CSSStringSource; +typedef (DOMString or CSSStyleValue or CSSParserValue) CSSToken; + +partial namespace CSS { + Promise> parseStylesheet(CSSStringSource css, optional CSSParserOptions options = {}); + Promise> parseRuleList(CSSStringSource css, optional CSSParserOptions options = {}); + Promise parseRule(CSSStringSource css, optional CSSParserOptions options = {}); + Promise> parseDeclarationList(CSSStringSource css, optional CSSParserOptions options = {}); + CSSParserDeclaration parseDeclaration(DOMString css, optional CSSParserOptions options = {}); + CSSToken parseValue(DOMString css); + sequence parseValueList(DOMString css); + sequence> parseCommaValueList(DOMString css); +}; + +dictionary CSSParserOptions { + object atRules; + /* dict of at-rule name => at-rule type + (contains decls or contains qualified rules) */ +}; + +[Exposed=Window] +interface CSSParserRule { + /* Just a superclass. */ +}; + +[Exposed=Window] +interface CSSParserAtRule : CSSParserRule { + constructor(DOMString name, sequence prelude, optional sequence? body); + readonly attribute DOMString name; + readonly attribute FrozenArray prelude; + readonly attribute FrozenArray? body; + /* nullable to handle at-statements */ + stringifier; +}; + +[Exposed=Window] +interface CSSParserQualifiedRule : CSSParserRule { + constructor(sequence prelude, optional sequence? body); + readonly attribute FrozenArray prelude; + readonly attribute FrozenArray body; + stringifier; +}; + +[Exposed=Window] +interface CSSParserDeclaration : CSSParserRule { + constructor(DOMString name, optional sequence body); + readonly attribute DOMString name; + readonly attribute FrozenArray body; + stringifier; +}; + +[Exposed=Window] +interface CSSParserValue { + /* Just a superclass. */ +}; + +[Exposed=Window] +interface CSSParserBlock : CSSParserValue { + constructor(DOMString name, sequence body); + readonly attribute DOMString name; /* "[]", "{}", or "()" */ + readonly attribute FrozenArray body; + stringifier; +}; + +[Exposed=Window] +interface CSSParserFunction : CSSParserValue { + constructor(DOMString name, sequence> args); + readonly attribute DOMString name; + readonly attribute FrozenArray> args; + stringifier; +}; diff --git a/test/wpt/tests/interfaces/css-properties-values-api.idl b/test/wpt/tests/interfaces/css-properties-values-api.idl new file mode 100644 index 00000000000..eb7d7b027e7 --- /dev/null +++ b/test/wpt/tests/interfaces/css-properties-values-api.idl @@ -0,0 +1,23 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: CSS Properties and Values API Level 1 (https://drafts.css-houdini.org/css-properties-values-api-1/) + +dictionary PropertyDefinition { + required DOMString name; + DOMString syntax = "*"; + required boolean inherits; + DOMString initialValue; +}; + +partial namespace CSS { + undefined registerProperty(PropertyDefinition definition); +}; + +[Exposed=Window] +interface CSSPropertyRule : CSSRule { + readonly attribute CSSOMString name; + readonly attribute CSSOMString syntax; + readonly attribute boolean inherits; + readonly attribute CSSOMString? initialValue; +}; diff --git a/test/wpt/tests/interfaces/css-pseudo.idl b/test/wpt/tests/interfaces/css-pseudo.idl new file mode 100644 index 00000000000..dbe4c5461f6 --- /dev/null +++ b/test/wpt/tests/interfaces/css-pseudo.idl @@ -0,0 +1,16 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: CSS Pseudo-Elements Module Level 4 (https://drafts.csswg.org/css-pseudo-4/) + +[Exposed=Window] +interface CSSPseudoElement : EventTarget { + readonly attribute CSSOMString type; + readonly attribute Element element; + readonly attribute (Element or CSSPseudoElement) parent; + CSSPseudoElement? pseudo(CSSOMString type); +}; + +partial interface Element { + CSSPseudoElement? pseudo(CSSOMString type); +}; diff --git a/test/wpt/tests/interfaces/css-regions.idl b/test/wpt/tests/interfaces/css-regions.idl new file mode 100644 index 00000000000..113438f790e --- /dev/null +++ b/test/wpt/tests/interfaces/css-regions.idl @@ -0,0 +1,29 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: CSS Regions Module Level 1 (https://drafts.csswg.org/css-regions-1/) + +partial interface Document { + readonly attribute NamedFlowMap namedFlows; +}; + +[Exposed=Window] interface NamedFlowMap { + maplike; +}; + +[Exposed=Window] +interface NamedFlow : EventTarget { + readonly attribute CSSOMString name; + readonly attribute boolean overset; + sequence getRegions(); + readonly attribute short firstEmptyRegionIndex; + sequence getContent(); + sequence getRegionsByContent(Node node); +}; + +interface mixin Region { + readonly attribute CSSOMString regionOverset; + sequence? getRegionFlowRanges(); +}; + +Element includes Region; diff --git a/test/wpt/tests/interfaces/css-shadow-parts.idl b/test/wpt/tests/interfaces/css-shadow-parts.idl new file mode 100644 index 00000000000..3759199fc14 --- /dev/null +++ b/test/wpt/tests/interfaces/css-shadow-parts.idl @@ -0,0 +1,8 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: CSS Shadow Parts (https://drafts.csswg.org/css-shadow-parts-1/) + +partial interface Element { + [SameObject, PutForwards=value] readonly attribute DOMTokenList part; +}; diff --git a/test/wpt/tests/interfaces/css-toggle.tentative.idl b/test/wpt/tests/interfaces/css-toggle.tentative.idl new file mode 100644 index 00000000000..5587019a290 --- /dev/null +++ b/test/wpt/tests/interfaces/css-toggle.tentative.idl @@ -0,0 +1,51 @@ +partial interface Element { + [SameObject] readonly attribute CSSToggleMap toggles; +}; + +interface CSSToggleMap { + maplike; + CSSToggleMap set(DOMString key, CSSToggle value); +}; + +interface CSSToggle { + attribute (unsigned long or DOMString) value; + attribute unsigned long? valueAsNumber; + attribute DOMString? valueAsString; + + attribute (unsigned long or FrozenArray) states; + attribute boolean group; + attribute CSSToggleScope scope; + attribute CSSToggleCycle cycle; + + constructor(optional CSSToggleData options); +}; + +dictionary CSSToggleData { + (unsigned long or DOMString) value = 0; + (unsigned long or sequence) states = 1; + boolean group = false; + CSSToggleScope scope = "wide"; + CSSToggleCycle cycle = "cycle"; +}; + +enum CSSToggleScope { + "narrow", + "wide", +}; + +enum CSSToggleCycle { + "cycle", + "cycle-on", + "sticky", +}; + +interface CSSToggleEvent : Event { + constructor(DOMString type, optional CSSToggleEventInit eventInitDict = {}); + readonly attribute DOMString toggleName; + readonly attribute CSSToggle? toggle; +}; + +dictionary CSSToggleEventInit : EventInit { + DOMString toggleName = ""; + CSSToggle? toggle = null; +}; diff --git a/test/wpt/tests/interfaces/css-transitions-2.idl b/test/wpt/tests/interfaces/css-transitions-2.idl new file mode 100644 index 00000000000..9d06f3cf260 --- /dev/null +++ b/test/wpt/tests/interfaces/css-transitions-2.idl @@ -0,0 +1,9 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: CSS Transitions Level 2 (https://drafts.csswg.org/css-transitions-2/) + +[Exposed=Window] +interface CSSTransition : Animation { + readonly attribute CSSOMString transitionProperty; +}; diff --git a/test/wpt/tests/interfaces/css-transitions.idl b/test/wpt/tests/interfaces/css-transitions.idl new file mode 100644 index 00000000000..0f00b2c014c --- /dev/null +++ b/test/wpt/tests/interfaces/css-transitions.idl @@ -0,0 +1,25 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: CSS Transitions (https://drafts.csswg.org/css-transitions-1/) + +[Exposed=Window] +interface TransitionEvent : Event { + constructor(CSSOMString type, optional TransitionEventInit transitionEventInitDict = {}); + readonly attribute CSSOMString propertyName; + readonly attribute double elapsedTime; + readonly attribute CSSOMString pseudoElement; +}; + +dictionary TransitionEventInit : EventInit { + CSSOMString propertyName = ""; + double elapsedTime = 0.0; + CSSOMString pseudoElement = ""; +}; + +partial interface mixin GlobalEventHandlers { + attribute EventHandler ontransitionrun; + attribute EventHandler ontransitionstart; + attribute EventHandler ontransitionend; + attribute EventHandler ontransitioncancel; +}; diff --git a/test/wpt/tests/interfaces/css-typed-om.idl b/test/wpt/tests/interfaces/css-typed-om.idl new file mode 100644 index 00000000000..595a424e014 --- /dev/null +++ b/test/wpt/tests/interfaces/css-typed-om.idl @@ -0,0 +1,425 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: CSS Typed OM Level 1 (https://drafts.css-houdini.org/css-typed-om-1/) + +[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)] +interface CSSStyleValue { + stringifier; + [Exposed=Window] static CSSStyleValue parse(USVString property, USVString cssText); + [Exposed=Window] static sequence parseAll(USVString property, USVString cssText); +}; + +[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 */ + sequence getAll(USVString property); + boolean has(USVString property); + readonly attribute unsigned long size; +}; + +[Exposed=Window] +interface StylePropertyMap : StylePropertyMapReadOnly { + undefined set(USVString property, (CSSStyleValue or USVString)... values); + undefined append(USVString property, (CSSStyleValue or USVString)... values); + undefined delete(USVString property); + undefined clear(); +}; + +partial interface Element { + [SameObject] StylePropertyMapReadOnly computedStyleMap(); +}; + +partial interface CSSStyleRule { + [SameObject] readonly attribute StylePropertyMap styleMap; +}; + +partial interface mixin ElementCSSInlineStyle { + [SameObject] readonly attribute StylePropertyMap attributeStyleMap; +}; + +[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)] +interface CSSUnparsedValue : CSSStyleValue { + constructor(sequence members); + iterable; + readonly attribute unsigned long length; + getter CSSUnparsedSegment (unsigned long index); + setter CSSUnparsedSegment (unsigned long index, CSSUnparsedSegment val); +}; + +typedef (USVString or CSSVariableReferenceValue) CSSUnparsedSegment; + +[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)] +interface CSSVariableReferenceValue { + constructor(USVString variable, optional CSSUnparsedValue? fallback = null); + attribute USVString variable; + readonly attribute CSSUnparsedValue? fallback; +}; + +[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)] +interface CSSKeywordValue : CSSStyleValue { + constructor(USVString value); + attribute USVString value; +}; + +typedef (DOMString or CSSKeywordValue) CSSKeywordish; + +typedef (double or CSSNumericValue) CSSNumberish; + +enum CSSNumericBaseType { + "length", + "angle", + "time", + "frequency", + "resolution", + "flex", + "percent", +}; + +dictionary CSSNumericType { + long length; + long angle; + long time; + long frequency; + long resolution; + long flex; + long percent; + CSSNumericBaseType percentHint; +}; + +[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)] +interface CSSNumericValue : CSSStyleValue { + CSSNumericValue add(CSSNumberish... values); + CSSNumericValue sub(CSSNumberish... values); + CSSNumericValue mul(CSSNumberish... values); + CSSNumericValue div(CSSNumberish... values); + CSSNumericValue min(CSSNumberish... values); + CSSNumericValue max(CSSNumberish... values); + + boolean equals(CSSNumberish... value); + + CSSUnitValue to(USVString unit); + CSSMathSum toSum(USVString... units); + CSSNumericType type(); + + [Exposed=Window] static CSSNumericValue parse(USVString cssText); +}; + +[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)] +interface CSSUnitValue : CSSNumericValue { + constructor(double value, USVString unit); + attribute double value; + readonly attribute USVString unit; +}; + +[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)] +interface CSSMathValue : CSSNumericValue { + readonly attribute CSSMathOperator operator; +}; + +[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)] +interface CSSMathSum : CSSMathValue { + constructor(CSSNumberish... args); + readonly attribute CSSNumericArray values; +}; + +[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)] +interface CSSMathProduct : CSSMathValue { + constructor(CSSNumberish... args); + readonly attribute CSSNumericArray values; +}; + +[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)] +interface CSSMathNegate : CSSMathValue { + constructor(CSSNumberish arg); + readonly attribute CSSNumericValue value; +}; + +[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)] +interface CSSMathInvert : CSSMathValue { + constructor(CSSNumberish arg); + readonly attribute CSSNumericValue value; +}; + +[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)] +interface CSSMathMin : CSSMathValue { + constructor(CSSNumberish... args); + readonly attribute CSSNumericArray values; +}; + +[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)] +interface CSSMathMax : CSSMathValue { + constructor(CSSNumberish... args); + readonly attribute CSSNumericArray values; +}; + +[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)] +interface CSSMathClamp : CSSMathValue { + constructor(CSSNumberish lower, CSSNumberish value, CSSNumberish upper); + readonly attribute CSSNumericValue lower; + readonly attribute CSSNumericValue value; + readonly attribute CSSNumericValue upper; +}; + +[Exposed=(Window, Worker, PaintWorklet, LayoutWorklet)] +interface CSSNumericArray { + iterable; + readonly attribute unsigned long length; + getter CSSNumericValue (unsigned long index); +}; + +enum CSSMathOperator { + "sum", + "product", + "negate", + "invert", + "min", + "max", + "clamp", +}; + +partial namespace CSS { + CSSUnitValue number(double value); + CSSUnitValue percent(double value); + + // + CSSUnitValue em(double value); + CSSUnitValue ex(double value); + CSSUnitValue ch(double value); + CSSUnitValue ic(double value); + CSSUnitValue rem(double value); + CSSUnitValue lh(double value); + CSSUnitValue rlh(double value); + CSSUnitValue vw(double value); + CSSUnitValue vh(double value); + CSSUnitValue vi(double value); + CSSUnitValue vb(double value); + CSSUnitValue vmin(double value); + CSSUnitValue vmax(double value); + CSSUnitValue svw(double value); + CSSUnitValue svh(double value); + CSSUnitValue svi(double value); + CSSUnitValue svb(double value); + CSSUnitValue svmin(double value); + CSSUnitValue svmax(double value); + CSSUnitValue lvw(double value); + CSSUnitValue lvh(double value); + CSSUnitValue lvi(double value); + CSSUnitValue lvb(double value); + CSSUnitValue lvmin(double value); + CSSUnitValue lvmax(double value); + CSSUnitValue dvw(double value); + CSSUnitValue dvh(double value); + CSSUnitValue dvi(double value); + CSSUnitValue dvb(double value); + CSSUnitValue dvmin(double value); + CSSUnitValue dvmax(double value); + CSSUnitValue cqw(double value); + CSSUnitValue cqh(double value); + CSSUnitValue cqi(double value); + CSSUnitValue cqb(double value); + CSSUnitValue cqmin(double value); + CSSUnitValue cqmax(double value); + CSSUnitValue cm(double value); + CSSUnitValue mm(double value); + CSSUnitValue Q(double value); + CSSUnitValue in(double value); + CSSUnitValue pt(double value); + CSSUnitValue pc(double value); + CSSUnitValue px(double value); + + // + CSSUnitValue deg(double value); + CSSUnitValue grad(double value); + CSSUnitValue rad(double value); + CSSUnitValue turn(double value); + + //

Relevant issue: +<embed> should support loading random HTML documents, like <object> +