summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/common/security-features/resources/common.sub.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--testing/web-platform/tests/common/security-features/resources/common.sub.js1311
-rw-r--r--testing/web-platform/tests/common/security-features/resources/common.sub.js.headers1
2 files changed, 1312 insertions, 0 deletions
diff --git a/testing/web-platform/tests/common/security-features/resources/common.sub.js b/testing/web-platform/tests/common/security-features/resources/common.sub.js
new file mode 100644
index 0000000000..96ca280597
--- /dev/null
+++ b/testing/web-platform/tests/common/security-features/resources/common.sub.js
@@ -0,0 +1,1311 @@
+/**
+ * @fileoverview Utilities for mixed-content in web-platform-tests.
+ * @author burnik@google.com (Kristijan Burnik)
+ * Disclaimer: Some methods of other authors are annotated in the corresponding
+ * method's JSDoc.
+ */
+
+// ===============================================================
+// Types
+// ===============================================================
+// Objects of the following types are used to represent what kind of
+// subresource requests should be sent with what kind of policies,
+// from what kind of possibly nested source contexts.
+// The objects are represented as JSON objects (not JavaScript/Python classes
+// in a strict sense) to be passed between JavaScript/Python code.
+//
+// See also common/security-features/Types.md for high-level description.
+
+/**
+ @typedef PolicyDelivery
+ @type {object}
+ Referrer policy etc. can be applied/delivered in several ways.
+ A PolicyDelivery object specifies what policy is delivered and how.
+
+ @property {string} deliveryType
+ Specifies how the policy is delivered.
+ The valid deliveryType are:
+
+ "attr"
+ [A] DOM attributes e.g. referrerPolicy.
+
+ "rel-noref"
+ [A] <link rel="noreferrer"> (referrer-policy only).
+
+ "http-rp"
+ [B] HTTP response headers.
+
+ "meta"
+ [B] <meta> elements.
+
+ @property {string} key
+ @property {string} value
+ Specifies what policy to be delivered. The valid keys are:
+
+ "referrerPolicy"
+ Referrer Policy
+ https://w3c.github.io/webappsec-referrer-policy/
+ Valid values are those listed in
+ https://w3c.github.io/webappsec-referrer-policy/#referrer-policy
+ (except that "" is represented as null/None)
+
+ A PolicyDelivery can be specified in several ways:
+
+ - (for [A]) Associated with an individual subresource request and
+ specified in `Subresource.policies`,
+ e.g. referrerPolicy attributes of DOM elements.
+ This is handled in invokeRequest().
+
+ - (for [B]) Associated with an nested environmental settings object and
+ specified in `SourceContext.policies`,
+ e.g. HTTP referrer-policy response headers of HTML/worker scripts.
+ This is handled in server-side under /common/security-features/scope/.
+
+ - (for [B]) Associated with the top-level HTML document.
+ This is handled by the generators.d
+*/
+
+/**
+ @typedef Subresource
+ @type {object}
+ A Subresource represents how a subresource request is sent.
+
+ @property{SubresourceType} subresourceType
+ How the subresource request is sent,
+ e.g. "img-tag" for sending a request via <img src>.
+ See the keys of `subresourceMap` for valid values.
+
+ @property{string} url
+ subresource's URL.
+ Typically this is constructed by getRequestURLs() below.
+
+ @property{PolicyDelivery} policyDeliveries
+ Policies delivered specific to the subresource request.
+*/
+
+/**
+ @typedef SourceContext
+ @type {object}
+
+ @property {string} sourceContextType
+ Kind of the source context to be used.
+ Valid values are the keys of `sourceContextMap` below.
+
+ @property {Array<PolicyDelivery>} policyDeliveries
+ A list of PolicyDelivery applied to the source context.
+*/
+
+// ===============================================================
+// General utility functions
+// ===============================================================
+
+function timeoutPromise(t, ms) {
+ return new Promise(resolve => { t.step_timeout(resolve, ms); });
+}
+
+/**
+ * Normalizes the target port for use in a URL. For default ports, this is the
+ * empty string (omitted port), otherwise it's a colon followed by the port
+ * number. Ports 80, 443 and an empty string are regarded as default ports.
+ * @param {number} targetPort The port to use
+ * @return {string} The port portion for using as part of a URL.
+ */
+function getNormalizedPort(targetPort) {
+ return ([80, 443, ""].indexOf(targetPort) >= 0) ? "" : ":" + targetPort;
+}
+
+/**
+ * Creates a GUID.
+ * See: https://en.wikipedia.org/wiki/Globally_unique_identifier
+ * Original author: broofa (http://www.broofa.com/)
+ * Sourced from: http://stackoverflow.com/a/2117523/4949715
+ * @return {string} A pseudo-random GUID.
+ */
+function guid() {
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
+ var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
+ return v.toString(16);
+ });
+}
+
+/**
+ * Initiates a new XHR via GET.
+ * @param {string} url The endpoint URL for the XHR.
+ * @param {string} responseType Optional - how should the response be parsed.
+ * Default is "json".
+ * See: https://xhr.spec.whatwg.org/#dom-xmlhttprequest-responsetype
+ * @return {Promise} A promise wrapping the success and error events.
+ */
+function xhrRequest(url, responseType) {
+ return new Promise(function(resolve, reject) {
+ var xhr = new XMLHttpRequest();
+ xhr.open('GET', url, true);
+ xhr.responseType = responseType || "json";
+
+ xhr.addEventListener("error", function() {
+ reject(Error("Network Error"));
+ });
+
+ xhr.addEventListener("load", function() {
+ if (xhr.status != 200)
+ reject(Error(xhr.statusText));
+ else
+ resolve(xhr.response);
+ });
+
+ xhr.send();
+ });
+}
+
+/**
+ * Sets attributes on a given DOM element.
+ * @param {DOMElement} element The element on which to set the attributes.
+ * @param {object} An object with keys (serving as attribute names) and values.
+ */
+function setAttributes(el, attrs) {
+ attrs = attrs || {}
+ for (var attr in attrs) {
+ if (attr !== 'src')
+ el.setAttribute(attr, attrs[attr]);
+ }
+ // Workaround for Chromium: set <img>'s src attribute after all other
+ // attributes to ensure the policy is applied.
+ for (var attr in attrs) {
+ if (attr === 'src')
+ el.setAttribute(attr, attrs[attr]);
+ }
+}
+
+/**
+ * Binds to success and error events of an object wrapping them into a promise
+ * available through {@code element.eventPromise}. The success event
+ * resolves and error event rejects.
+ * This method adds event listeners, and then removes all the added listeners
+ * when one of listened event is fired.
+ * @param {object} element An object supporting events on which to bind the
+ * promise.
+ * @param {string} resolveEventName [="load"] The event name to bind resolve to.
+ * @param {string} rejectEventName [="error"] The event name to bind reject to.
+ */
+function bindEvents(element, resolveEventName, rejectEventName) {
+ element.eventPromise =
+ bindEvents2(element, resolveEventName, element, rejectEventName);
+}
+
+// Returns a promise wrapping success and error events of objects.
+// This is a variant of bindEvents that can accept separate objects for each
+// events and two events to reject, and doesn't set `eventPromise`.
+//
+// When `resolveObject`'s `resolveEventName` event (default: "load") is
+// fired, the promise is resolved with the event.
+//
+// When `rejectObject`'s `rejectEventName` event (default: "error") or
+// `rejectObject2`'s `rejectEventName2` event (default: "error") is
+// fired, the promise is rejected.
+//
+// `rejectObject2` is optional.
+function bindEvents2(resolveObject, resolveEventName, rejectObject, rejectEventName, rejectObject2, rejectEventName2) {
+ return new Promise(function(resolve, reject) {
+ const actualResolveEventName = resolveEventName || "load";
+ const actualRejectEventName = rejectEventName || "error";
+ const actualRejectEventName2 = rejectEventName2 || "error";
+
+ const resolveHandler = function(event) {
+ cleanup();
+ resolve(event);
+ };
+
+ const rejectHandler = function(event) {
+ // Chromium starts propagating errors from worker.onerror to
+ // window.onerror. This handles the uncaught exceptions in tests.
+ event.preventDefault();
+ cleanup();
+ reject(event);
+ };
+
+ const cleanup = function() {
+ resolveObject.removeEventListener(actualResolveEventName, resolveHandler);
+ rejectObject.removeEventListener(actualRejectEventName, rejectHandler);
+ if (rejectObject2) {
+ rejectObject2.removeEventListener(actualRejectEventName2, rejectHandler);
+ }
+ };
+
+ resolveObject.addEventListener(actualResolveEventName, resolveHandler);
+ rejectObject.addEventListener(actualRejectEventName, rejectHandler);
+ if (rejectObject2) {
+ rejectObject2.addEventListener(actualRejectEventName2, rejectHandler);
+ }
+ });
+}
+
+/**
+ * Creates a new DOM element.
+ * @param {string} tagName The type of the DOM element.
+ * @param {object} attrs A JSON with attributes to apply to the element.
+ * @param {DOMElement} parent Optional - an existing DOM element to append to
+ * If not provided, the returned element will remain orphaned.
+ * @param {boolean} doBindEvents Optional - Whether to bind to load and error
+ * events and provide the promise wrapping the events via the element's
+ * {@code eventPromise} property. Default value evaluates to false.
+ * @return {DOMElement} The newly created DOM element.
+ */
+function createElement(tagName, attrs, parentNode, doBindEvents) {
+ var element = document.createElement(tagName);
+
+ if (doBindEvents) {
+ bindEvents(element);
+ if (element.tagName == "IFRAME" && !('srcdoc' in attrs || 'src' in attrs)) {
+ // If we're loading a frame, ensure we spin the event loop after load to
+ // paper over the different event timing in Gecko vs Blink/WebKit
+ // see https://github.com/whatwg/html/issues/4965
+ element.eventPromise = element.eventPromise.then(() => {
+ return new Promise(resolve => setTimeout(resolve, 0))
+ });
+ }
+ }
+ // We set the attributes after binding to events to catch any
+ // event-triggering attribute changes. E.g. form submission.
+ //
+ // But be careful with images: unlike other elements they will start the load
+ // as soon as the attr is set, even if not in the document yet, and sometimes
+ // complete it synchronously, so the append doesn't have the effect we want.
+ // So for images, we want to set the attrs after appending, whereas for other
+ // elements we want to do it before appending.
+ var isImg = (tagName == "img");
+ if (!isImg)
+ setAttributes(element, attrs);
+
+ if (parentNode)
+ parentNode.appendChild(element);
+
+ if (isImg)
+ setAttributes(element, attrs);
+
+ return element;
+}
+
+function createRequestViaElement(tagName, attrs, parentNode) {
+ return createElement(tagName, attrs, parentNode, true).eventPromise;
+}
+
+function wrapResult(server_data) {
+ if (typeof(server_data) === "string") {
+ throw server_data;
+ }
+ return {
+ referrer: server_data.headers.referer,
+ headers: server_data.headers
+ }
+}
+
+// ===============================================================
+// Subresources
+// ===============================================================
+
+/**
+ @typedef RequestResult
+ @type {object}
+ Represents the result of sending an request.
+ All properties are optional. See the comments for
+ requestVia*() and invokeRequest() below to see which properties are set.
+
+ @property {Array<Object<string, string>>} headers
+ HTTP request headers sent to server.
+ @property {string} referrer - Referrer.
+ @property {string} location - The URL of the subresource.
+ @property {string} sourceContextUrl
+ the URL of the global object where the actual request is sent.
+*/
+
+/**
+ requestVia*(url, additionalAttributes) functions send a subresource
+ request from the current environment settings object.
+
+ @param {string} url
+ The URL of the subresource.
+ @param {Object<string, string>} additionalAttributes
+ Additional attributes set to DOM elements
+ (element-initiated requests only).
+
+ @returns {Promise} that are resolved with a RequestResult object
+ on successful requests.
+
+ - Category 1:
+ `headers`: set.
+ `referrer`: set via `document.referrer`.
+ `location`: set via `document.location`.
+ See `template/document.html.template`.
+ - Category 2:
+ `headers`: set.
+ `referrer`: set to `headers.referer` by `wrapResult()`.
+ `location`: not set.
+ - Category 3:
+ All the keys listed above are NOT set.
+ `sourceContextUrl` is not set here.
+
+ -------------------------------- -------- --------------------------
+ Function name Category Used in
+ -------- ------- ---------
+ referrer mixed- upgrade-
+ policy content insecure-
+ policy content request
+ -------------------------------- -------- -------- ------- ---------
+ requestViaAnchor 1 Y Y -
+ requestViaArea 1 Y Y -
+ requestViaAudio 3 - Y -
+ requestViaDedicatedWorker 2 Y Y Y
+ requestViaFetch 2 Y Y -
+ requestViaForm 2 - Y -
+ requestViaIframe 1 Y Y -
+ requestViaImage 2 Y Y -
+ requestViaLinkPrefetch 3 - Y -
+ requestViaLinkStylesheet 3 - Y -
+ requestViaObject 3 - Y -
+ requestViaPicture 3 - Y -
+ requestViaScript 2 Y Y -
+ requestViaSendBeacon 3 - Y -
+ requestViaSharedWorker 2 Y Y Y
+ requestViaVideo 3 - Y -
+ requestViaWebSocket 3 - Y -
+ requestViaWorklet 3 - Y Y
+ requestViaXhr 2 Y Y -
+ -------------------------------- -------- -------- ------- ---------
+*/
+
+/**
+ * Creates a new iframe, binds load and error events, sets the src attribute and
+ * appends it to {@code document.body} .
+ * @param {string} url The src for the iframe.
+ * @return {Promise} The promise for success/error events.
+ */
+function requestViaIframe(url, additionalAttributes) {
+ const iframe = createElement(
+ "iframe",
+ Object.assign({"src": url}, additionalAttributes),
+ document.body,
+ false);
+ return bindEvents2(window, "message", iframe, "error", window, "error")
+ .then(event => {
+ if (event.source !== iframe.contentWindow)
+ return Promise.reject(new Error('Unexpected event.source'));
+ return event.data;
+ });
+}
+
+/**
+ * Creates a new image, binds load and error events, sets the src attribute and
+ * appends it to {@code document.body} .
+ * @param {string} url The src for the image.
+ * @return {Promise} The promise for success/error events.
+ */
+function requestViaImage(url, additionalAttributes) {
+ const img = createElement(
+ "img",
+ // crossOrigin attribute is added to read the pixel data of the response.
+ Object.assign({"src": url, "crossOrigin": "Anonymous"}, additionalAttributes),
+ document.body, true);
+ return img.eventPromise.then(() => wrapResult(decodeImageData(img)));
+}
+
+// Helper for requestViaImage().
+function decodeImageData(img) {
+ var canvas = document.createElement("canvas");
+ var context = canvas.getContext('2d');
+ context.drawImage(img, 0, 0);
+ var imgData = context.getImageData(0, 0, img.clientWidth, img.clientHeight);
+ const rgba = imgData.data;
+
+ let decodedBytes = new Uint8ClampedArray(rgba.length);
+ let decodedLength = 0;
+
+ for (var i = 0; i + 12 <= rgba.length; i += 12) {
+ // A single byte is encoded in three pixels. 8 pixel octets (among
+ // 9 octets = 3 pixels * 3 channels) are used to encode 8 bits,
+ // the most significant bit first, where `0` and `255` in pixel values
+ // represent `0` and `1` in bits, respectively.
+ // This encoding is used to avoid errors due to different color spaces.
+ const bits = [];
+ for (let j = 0; j < 3; ++j) {
+ bits.push(rgba[i + j * 4 + 0]);
+ bits.push(rgba[i + j * 4 + 1]);
+ bits.push(rgba[i + j * 4 + 2]);
+ // rgba[i + j * 4 + 3]: Skip alpha channel.
+ }
+ // The last one element is not used.
+ bits.pop();
+
+ // Decode a single byte.
+ let byte = 0;
+ for (let j = 0; j < 8; ++j) {
+ byte <<= 1;
+ if (bits[j] >= 128)
+ byte |= 1;
+ }
+
+ // Zero is the string terminator.
+ if (byte == 0)
+ break;
+
+ decodedBytes[decodedLength++] = byte;
+ }
+
+ // Remove trailing nulls from data.
+ decodedBytes = decodedBytes.subarray(0, decodedLength);
+ var string_data = (new TextDecoder("ascii")).decode(decodedBytes);
+
+ return JSON.parse(string_data);
+}
+
+/**
+ * Initiates a new XHR GET request to provided URL.
+ * @param {string} url The endpoint URL for the XHR.
+ * @return {Promise} The promise for success/error events.
+ */
+function requestViaXhr(url) {
+ return xhrRequest(url).then(result => wrapResult(result));
+}
+
+/**
+ * Initiates a new GET request to provided URL via the Fetch API.
+ * @param {string} url The endpoint URL for the Fetch.
+ * @return {Promise} The promise for success/error events.
+ */
+function requestViaFetch(url) {
+ return fetch(url)
+ .then(res => res.json())
+ .then(j => wrapResult(j));
+}
+
+function dedicatedWorkerUrlThatFetches(url) {
+ return `data:text/javascript,
+ fetch('${url}')
+ .then(r => r.json())
+ .then(j => postMessage(j))
+ .catch((e) => postMessage(e.message));`;
+}
+
+function workerUrlThatImports(url, additionalAttributes) {
+ let csp = "";
+ if (additionalAttributes && additionalAttributes.contentSecurityPolicy) {
+ csp=`&contentSecurityPolicy=${additionalAttributes.contentSecurityPolicy}`;
+ }
+ return `/common/security-features/subresource/static-import.py` +
+ `?import_url=${encodeURIComponent(url)}${csp}`;
+}
+
+function workerDataUrlThatImports(url) {
+ return `data:text/javascript,import '${url}';`;
+}
+
+/**
+ * Creates a new Worker, binds message and error events wrapping them into.
+ * {@code worker.eventPromise} and posts an empty string message to start
+ * the worker.
+ * @param {string} url The endpoint URL for the worker script.
+ * @param {object} options The options for Worker constructor.
+ * @return {Promise} The promise for success/error events.
+ */
+function requestViaDedicatedWorker(url, options) {
+ var worker;
+ try {
+ worker = new Worker(url, options);
+ } catch (e) {
+ return Promise.reject(e);
+ }
+ worker.postMessage('');
+ return bindEvents2(worker, "message", worker, "error")
+ .then(event => wrapResult(event.data));
+}
+
+function requestViaSharedWorker(url, options) {
+ var worker;
+ try {
+ worker = new SharedWorker(url, options);
+ } catch(e) {
+ return Promise.reject(e);
+ }
+ const promise = bindEvents2(worker.port, "message", worker, "error")
+ .then(event => wrapResult(event.data));
+ worker.port.start();
+ return promise;
+}
+
+// Returns a reference to a worklet object corresponding to a given type.
+function get_worklet(type) {
+ if (type == 'animation')
+ return CSS.animationWorklet;
+ if (type == 'layout')
+ return CSS.layoutWorklet;
+ if (type == 'paint')
+ return CSS.paintWorklet;
+ if (type == 'audio')
+ return new OfflineAudioContext(2,44100*40,44100).audioWorklet;
+
+ throw new Error('unknown worklet type is passed.');
+}
+
+function requestViaWorklet(type, url) {
+ try {
+ return get_worklet(type).addModule(url);
+ } catch (e) {
+ return Promise.reject(e);
+ }
+}
+
+/**
+ * Creates a navigable element with the name `navigableElementName`
+ * (<a>, <area>, or <form>) under `parentNode`, and
+ * performs a navigation by `trigger()` (e.g. clicking <a>).
+ * To avoid navigating away from the current execution context,
+ * a target attribute is set to point to a new helper iframe.
+ * @param {string} navigableElementName
+ * @param {object} additionalAttributes The attributes of the navigable element.
+ * @param {DOMElement} parentNode
+ * @param {function(DOMElement} trigger A callback called after the navigable
+ * element is inserted and should trigger navigation using the element.
+ * @return {Promise} The promise for success/error events.
+ */
+function requestViaNavigable(navigableElementName, additionalAttributes,
+ parentNode, trigger) {
+ const name = guid();
+
+ const iframe =
+ createElement("iframe", {"name": name, "id": name}, parentNode, false);
+
+ const navigable = createElement(
+ navigableElementName,
+ Object.assign({"target": name}, additionalAttributes),
+ parentNode, false);
+
+ const promise =
+ bindEvents2(window, "message", iframe, "error", window, "error")
+ .then(event => {
+ if (event.source !== iframe.contentWindow)
+ return Promise.reject(new Error('Unexpected event.source'));
+ return event.data;
+ });
+ trigger(navigable);
+ return promise;
+}
+
+/**
+ * Creates a new anchor element, appends it to {@code document.body} and
+ * performs the navigation.
+ * @param {string} url The URL to navigate to.
+ * @return {Promise} The promise for success/error events.
+ */
+function requestViaAnchor(url, additionalAttributes) {
+ return requestViaNavigable(
+ "a",
+ Object.assign({"href": url, "innerHTML": "Link to resource"},
+ additionalAttributes),
+ document.body, a => a.click());
+}
+
+/**
+ * Creates a new area element, appends it to {@code document.body} and performs
+ * the navigation.
+ * @param {string} url The URL to navigate to.
+ * @return {Promise} The promise for success/error events.
+ */
+function requestViaArea(url, additionalAttributes) {
+ // TODO(kristijanburnik): Append to map and add image.
+ return requestViaNavigable(
+ "area",
+ Object.assign({"href": url}, additionalAttributes),
+ document.body, area => area.click());
+}
+
+/**
+ * Creates a new script element, sets the src to url, and appends it to
+ * {@code document.body}.
+ * @param {string} url The src URL.
+ * @return {Promise} The promise for success/error events.
+ */
+function requestViaScript(url, additionalAttributes) {
+ const script = createElement(
+ "script",
+ Object.assign({"src": url}, additionalAttributes),
+ document.body,
+ false);
+
+ return bindEvents2(window, "message", script, "error", window, "error")
+ .then(event => wrapResult(event.data));
+}
+
+/**
+ * Creates a new script element that performs a dynamic import to `url`, and
+ * appends the script element to {@code document.body}.
+ * @param {string} url The src URL.
+ * @return {Promise} The promise for success/error events.
+ */
+function requestViaDynamicImport(url, additionalAttributes) {
+ const scriptUrl = `data:text/javascript,import("${url}");`;
+ const script = createElement(
+ "script",
+ Object.assign({"src": scriptUrl}, additionalAttributes),
+ document.body,
+ false);
+
+ return bindEvents2(window, "message", script, "error", window, "error")
+ .then(event => wrapResult(event.data));
+}
+
+/**
+ * Creates a new form element, sets attributes, appends it to
+ * {@code document.body} and submits the form.
+ * @param {string} url The URL to submit to.
+ * @return {Promise} The promise for success/error events.
+ */
+function requestViaForm(url, additionalAttributes) {
+ return requestViaNavigable(
+ "form",
+ Object.assign({"action": url, "method": "POST"}, additionalAttributes),
+ document.body, form => form.submit());
+}
+
+/**
+ * Creates a new link element for a stylesheet, binds load and error events,
+ * sets the href to url and appends it to {@code document.head}.
+ * @param {string} url The URL for a stylesheet.
+ * @return {Promise} The promise for success/error events.
+ */
+function requestViaLinkStylesheet(url) {
+ return createRequestViaElement("link",
+ {"rel": "stylesheet", "href": url},
+ document.head);
+}
+
+/**
+ * Creates a new link element for a prefetch, binds load and error events, sets
+ * the href to url and appends it to {@code document.head}.
+ * @param {string} url The URL of a resource to prefetch.
+ * @return {Promise} The promise for success/error events.
+ */
+function requestViaLinkPrefetch(url) {
+ var link = document.createElement('link');
+ if (link.relList && link.relList.supports && link.relList.supports("prefetch")) {
+ return createRequestViaElement("link",
+ {"rel": "prefetch", "href": url},
+ document.head);
+ } else {
+ return Promise.reject("This browser does not support 'prefetch'.");
+ }
+}
+
+/**
+ * Initiates a new beacon request.
+ * @param {string} url The URL of a resource to prefetch.
+ * @return {Promise} The promise for success/error events.
+ */
+async function requestViaSendBeacon(url) {
+ function wait(ms) {
+ return new Promise(resolve => step_timeout(resolve, ms));
+ }
+ if (!navigator.sendBeacon(url)) {
+ // If mixed-content check fails, it should return false.
+ throw new Error('sendBeacon() fails.');
+ }
+ // We don't have a means to see the result of sendBeacon() request
+ // for sure. Let's wait for a while and let the generic test function
+ // ask the server for the result.
+ await wait(500);
+ return 'allowed';
+}
+
+/**
+ * Creates a new media element with a child source element, binds loadeddata and
+ * error events, sets attributes and appends to document.body.
+ * @param {string} type The type of the media element (audio/video/picture).
+ * @param {object} media_attrs The attributes for the media element.
+ * @param {object} source_attrs The attributes for the child source element.
+ * @return {DOMElement} The newly created media element.
+ */
+function createMediaElement(type, media_attrs, source_attrs) {
+ var mediaElement = createElement(type, {});
+
+ var sourceElement = createElement("source", {});
+
+ mediaElement.eventPromise = new Promise(function(resolve, reject) {
+ mediaElement.addEventListener("loadeddata", function (e) {
+ resolve(e);
+ });
+
+ // Safari doesn't fire an `error` event when blocking mixed content.
+ mediaElement.addEventListener("stalled", function(e) {
+ reject(e);
+ });
+
+ sourceElement.addEventListener("error", function(e) {
+ reject(e);
+ });
+ });
+
+ setAttributes(mediaElement, media_attrs);
+ setAttributes(sourceElement, source_attrs);
+
+ mediaElement.appendChild(sourceElement);
+ document.body.appendChild(mediaElement);
+
+ return mediaElement;
+}
+
+/**
+ * Creates a new video element, binds loadeddata and error events, sets
+ * attributes and source URL and appends to {@code document.body}.
+ * @param {string} url The URL of the video.
+ * @return {Promise} The promise for success/error events.
+ */
+function requestViaVideo(url) {
+ return createMediaElement("video",
+ {},
+ {"src": url}).eventPromise;
+}
+
+/**
+ * Creates a new audio element, binds loadeddata and error events, sets
+ * attributes and source URL and appends to {@code document.body}.
+ * @param {string} url The URL of the audio.
+ * @return {Promise} The promise for success/error events.
+ */
+function requestViaAudio(url) {
+ return createMediaElement("audio",
+ {},
+ {"type": "audio/wav", "src": url}).eventPromise;
+}
+
+/**
+ * Creates a new picture element, binds loadeddata and error events, sets
+ * attributes and source URL and appends to {@code document.body}. Also
+ * creates new image element appending it to the picture
+ * @param {string} url The URL of the image for the source and image elements.
+ * @return {Promise} The promise for success/error events.
+ */
+function requestViaPicture(url) {
+ var picture = createMediaElement("picture", {}, {"srcset": url,
+ "type": "image/png"});
+ return createRequestViaElement("img", {"src": url}, picture);
+}
+
+/**
+ * Creates a new object element, binds load and error events, sets the data to
+ * url, and appends it to {@code document.body}.
+ * @param {string} url The data URL.
+ * @return {Promise} The promise for success/error events.
+ */
+function requestViaObject(url) {
+ return createRequestViaElement("object", {"data": url, "type": "text/html"}, document.body);
+}
+
+/**
+ * Creates a new WebSocket pointing to {@code url} and sends a message string
+ * "echo". The {@code message} and {@code error} events are triggering the
+ * returned promise resolve/reject events.
+ * @param {string} url The URL for WebSocket to connect to.
+ * @return {Promise} The promise for success/error events.
+ */
+function requestViaWebSocket(url) {
+ return new Promise(function(resolve, reject) {
+ var websocket = new WebSocket(url);
+
+ websocket.addEventListener("message", function(e) {
+ resolve(e.data);
+ });
+
+ websocket.addEventListener("open", function(e) {
+ websocket.send("echo");
+ });
+
+ websocket.addEventListener("error", function(e) {
+ reject(e)
+ });
+ })
+ .then(data => {
+ return JSON.parse(data);
+ });
+}
+
+/**
+ @typedef SubresourceType
+ @type {string}
+
+ Represents how a subresource is sent.
+ The keys of `subresourceMap` below are the valid values.
+*/
+
+// Subresource paths and invokers.
+const subresourceMap = {
+ "a-tag": {
+ path: "/common/security-features/subresource/document.py",
+ invoker: requestViaAnchor,
+ },
+ "area-tag": {
+ path: "/common/security-features/subresource/document.py",
+ invoker: requestViaArea,
+ },
+ "audio-tag": {
+ path: "/common/security-features/subresource/audio.py",
+ invoker: requestViaAudio,
+ },
+ "beacon": {
+ path: "/common/security-features/subresource/empty.py",
+ invoker: requestViaSendBeacon,
+ },
+ "fetch": {
+ path: "/common/security-features/subresource/xhr.py",
+ invoker: requestViaFetch,
+ },
+ "form-tag": {
+ path: "/common/security-features/subresource/document.py",
+ invoker: requestViaForm,
+ },
+ "iframe-tag": {
+ path: "/common/security-features/subresource/document.py",
+ invoker: requestViaIframe,
+ },
+ "img-tag": {
+ path: "/common/security-features/subresource/image.py",
+ invoker: requestViaImage,
+ },
+ "link-css-tag": {
+ path: "/common/security-features/subresource/empty.py",
+ invoker: requestViaLinkStylesheet,
+ },
+ "link-prefetch-tag": {
+ path: "/common/security-features/subresource/empty.py",
+ invoker: requestViaLinkPrefetch,
+ },
+ "object-tag": {
+ path: "/common/security-features/subresource/empty.py",
+ invoker: requestViaObject,
+ },
+ "picture-tag": {
+ path: "/common/security-features/subresource/image.py",
+ invoker: requestViaPicture,
+ },
+ "script-tag": {
+ path: "/common/security-features/subresource/script.py",
+ invoker: requestViaScript,
+ },
+ "script-tag-dynamic-import": {
+ path: "/common/security-features/subresource/script.py",
+ invoker: requestViaDynamicImport,
+ },
+ "video-tag": {
+ path: "/common/security-features/subresource/video.py",
+ invoker: requestViaVideo,
+ },
+ "xhr": {
+ path: "/common/security-features/subresource/xhr.py",
+ invoker: requestViaXhr,
+ },
+
+ "worker-classic": {
+ path: "/common/security-features/subresource/worker.py",
+ invoker: url => requestViaDedicatedWorker(url),
+ },
+ "worker-module": {
+ path: "/common/security-features/subresource/worker.py",
+ invoker: url => requestViaDedicatedWorker(url, {type: "module"}),
+ },
+ "worker-import": {
+ path: "/common/security-features/subresource/worker.py",
+ invoker: (url, additionalAttributes) =>
+ requestViaDedicatedWorker(workerUrlThatImports(url, additionalAttributes), {type: "module"}),
+ },
+ "worker-import-data": {
+ path: "/common/security-features/subresource/worker.py",
+ invoker: url =>
+ requestViaDedicatedWorker(workerDataUrlThatImports(url), {type: "module"}),
+ },
+ "sharedworker-classic": {
+ path: "/common/security-features/subresource/shared-worker.py",
+ invoker: url => requestViaSharedWorker(url),
+ },
+ "sharedworker-module": {
+ path: "/common/security-features/subresource/shared-worker.py",
+ invoker: url => requestViaSharedWorker(url, {type: "module"}),
+ },
+ "sharedworker-import": {
+ path: "/common/security-features/subresource/shared-worker.py",
+ invoker: (url, additionalAttributes) =>
+ requestViaSharedWorker(workerUrlThatImports(url, additionalAttributes), {type: "module"}),
+ },
+ "sharedworker-import-data": {
+ path: "/common/security-features/subresource/shared-worker.py",
+ invoker: url =>
+ requestViaSharedWorker(workerDataUrlThatImports(url), {type: "module"}),
+ },
+
+ "websocket": {
+ path: "/stash_responder",
+ invoker: requestViaWebSocket,
+ },
+};
+for (const workletType of ['animation', 'audio', 'layout', 'paint']) {
+ subresourceMap[`worklet-${workletType}`] = {
+ path: "/common/security-features/subresource/worker.py",
+ invoker: url => requestViaWorklet(workletType, url)
+ };
+ subresourceMap[`worklet-${workletType}-import-data`] = {
+ path: "/common/security-features/subresource/worker.py",
+ invoker: url =>
+ requestViaWorklet(workletType, workerDataUrlThatImports(url))
+ };
+}
+
+/**
+ @typedef RedirectionType
+ @type {string}
+
+ Represents what redirects should occur to the subresource request
+ after initial request.
+ See preprocess_redirection() in
+ /common/security-features/subresource/subresource.py for valid values.
+*/
+
+/**
+ Construct subresource (and related) origin.
+
+ @param {string} originType
+ @returns {object} the origin of the subresource.
+*/
+function getSubresourceOrigin(originType) {
+ const httpProtocol = "http";
+ const httpsProtocol = "https";
+ const wsProtocol = "ws";
+ const wssProtocol = "wss";
+
+ const sameOriginHost = "{{host}}";
+ const crossOriginHost = "{{domains[www1]}}";
+
+ // These values can evaluate to either empty strings or a ":port" string.
+ const httpPort = getNormalizedPort(parseInt("{{ports[http][0]}}", 10));
+ const httpsRawPort = parseInt("{{ports[https][0]}}", 10);
+ const httpsPort = getNormalizedPort(httpsRawPort);
+ const wsPort = getNormalizedPort(parseInt("{{ports[ws][0]}}", 10));
+ const wssRawPort = parseInt("{{ports[wss][0]}}", 10);
+ const wssPort = getNormalizedPort(wssRawPort);
+
+ /**
+ @typedef OriginType
+ @type {string}
+
+ Represents the origin of the subresource request URL.
+ The keys of `originMap` below are the valid values.
+
+ Note that there can be redirects from the specified origin
+ (see RedirectionType), and thus the origin of the subresource
+ response URL might be different from what is specified by OriginType.
+ */
+ const originMap = {
+ "same-https": httpsProtocol + "://" + sameOriginHost + httpsPort,
+ "same-http": httpProtocol + "://" + sameOriginHost + httpPort,
+ "cross-https": httpsProtocol + "://" + crossOriginHost + httpsPort,
+ "cross-http": httpProtocol + "://" + crossOriginHost + httpPort,
+ "same-wss": wssProtocol + "://" + sameOriginHost + wssPort,
+ "same-ws": wsProtocol + "://" + sameOriginHost + wsPort,
+ "cross-wss": wssProtocol + "://" + crossOriginHost + wssPort,
+ "cross-ws": wsProtocol + "://" + crossOriginHost + wsPort,
+
+ // The following origin types are used for upgrade-insecure-requests tests:
+ // 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.
+ "same-http-downgrade":
+ httpProtocol + "://" + sameOriginHost + ":" + httpsRawPort,
+ "cross-http-downgrade":
+ httpProtocol + "://" + crossOriginHost + ":" + httpsRawPort,
+ "same-ws-downgrade":
+ wsProtocol + "://" + sameOriginHost + ":" + wssRawPort,
+ "cross-ws-downgrade":
+ wsProtocol + "://" + crossOriginHost + ":" + wssRawPort,
+ };
+
+ return originMap[originType];
+}
+
+/**
+ Construct subresource (and related) URLs.
+
+ @param {SubresourceType} subresourceType
+ @param {OriginType} originType
+ @param {RedirectionType} redirectionType
+ @returns {object} with following properties:
+ {string} testUrl
+ The subresource request URL.
+ {string} announceUrl
+ {string} assertUrl
+ The URLs to be used for detecting whether `testUrl` is actually sent
+ to the server.
+ 1. Fetch `announceUrl` first,
+ 2. then possibly fetch `testUrl`, and
+ 3. finally fetch `assertUrl`.
+ The fetch result of `assertUrl` should indicate whether
+ `testUrl` is actually sent to the server or not.
+*/
+function getRequestURLs(subresourceType, originType, redirectionType) {
+ const key = guid();
+ const value = guid();
+
+ // We use the same stash path for both HTTP/S and WS/S stash requests.
+ const stashPath = encodeURIComponent("/mixed-content");
+
+ const stashEndpoint = "/common/security-features/subresource/xhr.py?key=" +
+ key + "&path=" + stashPath;
+ return {
+ testUrl:
+ getSubresourceOrigin(originType) +
+ subresourceMap[subresourceType].path +
+ "?redirection=" + encodeURIComponent(redirectionType) +
+ "&action=purge&key=" + key +
+ "&path=" + stashPath,
+ announceUrl: stashEndpoint + "&action=put&value=" + value,
+ assertUrl: stashEndpoint + "&action=take",
+ };
+}
+
+// ===============================================================
+// Source Context
+// ===============================================================
+// Requests can be sent from several source contexts,
+// such as the main documents, iframes, workers, or so,
+// possibly nested, and possibly with <meta>/http headers added.
+// invokeRequest() and invokeFrom*() functions handles
+// SourceContext-related setup in client-side.
+
+/**
+ invokeRequest() invokes a subresource request
+ (specified as `subresource`)
+ from a (possibly nested) environment settings object
+ (specified as `sourceContextList`).
+
+ For nested contexts, invokeRequest() calls an invokeFrom*() function
+ that creates a nested environment settings object using
+ /common/security-features/scope/, which calls invokeRequest()
+ again inside the nested environment settings object.
+ This cycle continues until all specified
+ nested environment settings object are created, and
+ finally invokeRequest() calls a requestVia*() function to start the
+ subresource request from the inner-most environment settings object.
+
+ @param {Subresource} subresource
+ @param {Array<SourceContext>} sourceContextList
+
+ @returns {Promise} A promise that is resolved with an RequestResult object.
+ `sourceContextUrl` is always set. For whether other properties are set,
+ see the comments for requestVia*() above.
+*/
+function invokeRequest(subresource, sourceContextList) {
+ if (sourceContextList.length === 0) {
+ // No further nested global objects. Send the subresource request here.
+
+ const additionalAttributes = {};
+ /** @type {PolicyDelivery} policyDelivery */
+ for (const policyDelivery of (subresource.policyDeliveries || [])) {
+ // Depending on the delivery method, extend the subresource element with
+ // these attributes.
+ if (policyDelivery.deliveryType === "attr") {
+ additionalAttributes[policyDelivery.key] = policyDelivery.value;
+ } else if (policyDelivery.deliveryType === "rel-noref") {
+ additionalAttributes["rel"] = "noreferrer";
+ } else if (policyDelivery.deliveryType === "http-rp") {
+ additionalAttributes[policyDelivery.key] = policyDelivery.value;
+ } else if (policyDelivery.deliveryType === "meta") {
+ additionalAttributes[policyDelivery.key] = policyDelivery.value;
+ }
+ }
+
+ return subresourceMap[subresource.subresourceType].invoker(
+ subresource.url,
+ additionalAttributes)
+ .then(result => Object.assign(
+ {sourceContextUrl: location.toString()},
+ result));
+ }
+
+ // Defines invokers for each valid SourceContext.sourceContextType.
+ const sourceContextMap = {
+ "srcdoc": { // <iframe srcdoc></iframe>
+ invoker: invokeFromIframe,
+ },
+ "iframe": { // <iframe src="same-origin-URL"></iframe>
+ invoker: invokeFromIframe,
+ },
+ "iframe-blank": { // <iframe></iframe>
+ 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/testing/web-platform/tests/common/security-features/resources/common.sub.js.headers b/testing/web-platform/tests/common/security-features/resources/common.sub.js.headers
new file mode 100644
index 0000000000..cb762eff80
--- /dev/null
+++ b/testing/web-platform/tests/common/security-features/resources/common.sub.js.headers
@@ -0,0 +1 @@
+Access-Control-Allow-Origin: *