/** * @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. // Note: So far this document covers: // - resources/common.js : client-side test infra code // - scope/ - server-side scripts that serves nested source contexts // but doesn't cover: // - tools/ - generator scripts that generates top-level HTML documents. // There are some policies only handled by generators (e.g. mixed-content // opt-ins) and not yet covered by the docs here. /** @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} Requests can be possibly sent from various kinds of source contexts, i.e. fetch client's environment settings objects: top-level windows, iframes, or workers. A SourceContext object specifies one environment settings object, and an Array specifies a possibly nested context, from the outer-most to inner-most environment settings objects. For example: [{sourceContextType: "srcdoc"}, {sourceContextType: "classic-worker"}] means that a subresource request is to be sent from a classic dedicated worker created from invoker: invokeFromIframe, }, "iframe": { // invoker: invokeFromIframe, }, "classic-worker": { // Classic dedicated worker loaded from same-origin. invoker: invokeFromWorker.bind(undefined, false, {}), }, "classic-data-worker": { // Classic dedicated worker loaded from data: URL. invoker: invokeFromWorker.bind(undefined, true, {}), }, "module-worker": { // Module dedicated worker loaded from same-origin. invoker: invokeFromWorker.bind(undefined, false, {type: 'module'}), }, "module-data-worker": { // Module dedicated worker loaded from data: URL. invoker: invokeFromWorker.bind(undefined, true, {type: 'module'}), }, }; return sourceContextMap[sourceContextList[0].sourceContextType].invoker( subresource, sourceContextList); } // Quick hack to expose invokeRequest when common.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 {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(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 => { const worker = new Worker(url, workerOptions); worker.postMessage({subresource: subresource, sourceContextList: sourceContextList.slice(1)}); return bindEvents2(worker, "message", worker, "error", window, "error"); }) .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 promise; if (currentSourceContext.sourceContextType === 'srcdoc') { promise = fetch(frameUrl) .then(r => r.text()) .then(srcdoc => { return createElement("iframe", {srcdoc: srcdoc}, document.body, true); }); } else if (currentSourceContext.sourceContextType === 'iframe') { promise = Promise.resolve( createElement("iframe", {src: frameUrl}, document.body, true)); } return promise .then(iframe => { return iframe.eventPromise .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() {};