/**
* @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] (referrer-policy only).
"http-rp"
[B] HTTP response headers.
"meta"
[B] 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 .
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} 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 '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