• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/**
2 * @fileoverview Utilities for mixed-content in web-platform-tests.
3 * @author burnik@google.com (Kristijan Burnik)
4 * Disclaimer: Some methods of other authors are annotated in the corresponding
5 *     method's JSDoc.
6 */
7
8// ===============================================================
9// Types
10// ===============================================================
11// Objects of the following types are used to represent what kind of
12// subresource requests should be sent with what kind of policies,
13// from what kind of possibly nested source contexts.
14// The objects are represented as JSON objects (not JavaScript/Python classes
15// in a strict sense) to be passed between JavaScript/Python code.
16//
17// See also common/security-features/Types.md for high-level description.
18
19/**
20  @typedef PolicyDelivery
21  @type {object}
22  Referrer policy etc. can be applied/delivered in several ways.
23  A PolicyDelivery object specifies what policy is delivered and how.
24
25  @property {string} deliveryType
26    Specifies how the policy is delivered.
27    The valid deliveryType are:
28
29     "attr"
30        [A] DOM attributes e.g. referrerPolicy.
31
32      "rel-noref"
33        [A] <link rel="noreferrer"> (referrer-policy only).
34
35      "http-rp"
36        [B] HTTP response headers.
37
38      "meta"
39        [B] <meta> elements.
40
41  @property {string} key
42  @property {string} value
43    Specifies what policy to be delivered. The valid keys are:
44
45      "referrerPolicy"
46        Referrer Policy
47        https://w3c.github.io/webappsec-referrer-policy/
48        Valid values are those listed in
49        https://w3c.github.io/webappsec-referrer-policy/#referrer-policy
50        (except that "" is represented as null/None)
51
52  A PolicyDelivery can be specified in several ways:
53
54  - (for [A]) Associated with an individual subresource request and
55    specified in `Subresource.policies`,
56    e.g. referrerPolicy attributes of DOM elements.
57    This is handled in invokeRequest().
58
59  - (for [B]) Associated with an nested environmental settings object and
60    specified in `SourceContext.policies`,
61    e.g. HTTP referrer-policy response headers of HTML/worker scripts.
62    This is handled in server-side under /common/security-features/scope/.
63
64  - (for [B]) Associated with the top-level HTML document.
65    This is handled by the generators.d
66*/
67
68/**
69  @typedef Subresource
70  @type {object}
71  A Subresource represents how a subresource request is sent.
72
73  @property{SubresourceType} subresourceType
74    How the subresource request is sent,
75    e.g. "img-tag" for sending a request via <img src>.
76    See the keys of `subresourceMap` for valid values.
77
78  @property{string} url
79    subresource's URL.
80    Typically this is constructed by getRequestURLs() below.
81
82  @property{PolicyDelivery} policyDeliveries
83    Policies delivered specific to the subresource request.
84*/
85
86/**
87  @typedef SourceContext
88  @type {object}
89
90  @property {string} sourceContextType
91    Kind of the source context to be used.
92    Valid values are the keys of `sourceContextMap` below.
93
94  @property {Array<PolicyDelivery>} policyDeliveries
95    A list of PolicyDelivery applied to the source context.
96*/
97
98// ===============================================================
99// General utility functions
100// ===============================================================
101
102function timeoutPromise(t, ms) {
103  return new Promise(resolve => { t.step_timeout(resolve, ms); });
104}
105
106/**
107 * Normalizes the target port for use in a URL. For default ports, this is the
108 *     empty string (omitted port), otherwise it's a colon followed by the port
109 *     number. Ports 80, 443 and an empty string are regarded as default ports.
110 * @param {number} targetPort The port to use
111 * @return {string} The port portion for using as part of a URL.
112 */
113function getNormalizedPort(targetPort) {
114  return ([80, 443, ""].indexOf(targetPort) >= 0) ? "" : ":" + targetPort;
115}
116
117/**
118 * Creates a GUID.
119 *     See: https://en.wikipedia.org/wiki/Globally_unique_identifier
120 *     Original author: broofa (http://www.broofa.com/)
121 *     Sourced from: http://stackoverflow.com/a/2117523/4949715
122 * @return {string} A pseudo-random GUID.
123 */
124function guid() {
125  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
126    var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
127    return v.toString(16);
128  });
129}
130
131/**
132 * Initiates a new XHR via GET.
133 * @param {string} url The endpoint URL for the XHR.
134 * @param {string} responseType Optional - how should the response be parsed.
135 *     Default is "json".
136 *     See: https://xhr.spec.whatwg.org/#dom-xmlhttprequest-responsetype
137 * @return {Promise} A promise wrapping the success and error events.
138 */
139function xhrRequest(url, responseType) {
140  return new Promise(function(resolve, reject) {
141    var xhr = new XMLHttpRequest();
142    xhr.open('GET', url, true);
143    xhr.responseType = responseType || "json";
144
145    xhr.addEventListener("error", function() {
146      reject(Error("Network Error"));
147    });
148
149    xhr.addEventListener("load", function() {
150      if (xhr.status != 200)
151        reject(Error(xhr.statusText));
152      else
153        resolve(xhr.response);
154    });
155
156    xhr.send();
157  });
158}
159
160/**
161 * Sets attributes on a given DOM element.
162 * @param {DOMElement} element The element on which to set the attributes.
163 * @param {object} An object with keys (serving as attribute names) and values.
164 */
165function setAttributes(el, attrs) {
166  attrs = attrs || {}
167  for (var attr in attrs) {
168    if (attr !== 'src')
169      el.setAttribute(attr, attrs[attr]);
170  }
171  // Workaround for Chromium: set <img>'s src attribute after all other
172  // attributes to ensure the policy is applied.
173  for (var attr in attrs) {
174    if (attr === 'src')
175      el.setAttribute(attr, attrs[attr]);
176  }
177}
178
179/**
180 * Binds to success and error events of an object wrapping them into a promise
181 *     available through {@code element.eventPromise}. The success event
182 *     resolves and error event rejects.
183 * This method adds event listeners, and then removes all the added listeners
184 * when one of listened event is fired.
185 * @param {object} element An object supporting events on which to bind the
186 *     promise.
187 * @param {string} resolveEventName [="load"] The event name to bind resolve to.
188 * @param {string} rejectEventName [="error"] The event name to bind reject to.
189 */
190function bindEvents(element, resolveEventName, rejectEventName) {
191  element.eventPromise =
192      bindEvents2(element, resolveEventName, element, rejectEventName);
193}
194
195// Returns a promise wrapping success and error events of objects.
196// This is a variant of bindEvents that can accept separate objects for each
197// events and two events to reject, and doesn't set `eventPromise`.
198//
199// When `resolveObject`'s `resolveEventName` event (default: "load") is
200// fired, the promise is resolved with the event.
201//
202// When `rejectObject`'s `rejectEventName` event (default: "error") or
203// `rejectObject2`'s `rejectEventName2` event (default: "error") is
204// fired, the promise is rejected.
205//
206// `rejectObject2` is optional.
207function bindEvents2(resolveObject, resolveEventName, rejectObject, rejectEventName, rejectObject2, rejectEventName2) {
208  return new Promise(function(resolve, reject) {
209    const actualResolveEventName = resolveEventName || "load";
210    const actualRejectEventName = rejectEventName || "error";
211    const actualRejectEventName2 = rejectEventName2 || "error";
212
213    const resolveHandler = function(event) {
214      cleanup();
215      resolve(event);
216    };
217
218    const rejectHandler = function(event) {
219      // Chromium starts propagating errors from worker.onerror to
220      // window.onerror. This handles the uncaught exceptions in tests.
221      event.preventDefault();
222      cleanup();
223      reject(event);
224    };
225
226    const cleanup = function() {
227      resolveObject.removeEventListener(actualResolveEventName, resolveHandler);
228      rejectObject.removeEventListener(actualRejectEventName, rejectHandler);
229      if (rejectObject2) {
230        rejectObject2.removeEventListener(actualRejectEventName2, rejectHandler);
231      }
232    };
233
234    resolveObject.addEventListener(actualResolveEventName, resolveHandler);
235    rejectObject.addEventListener(actualRejectEventName, rejectHandler);
236    if (rejectObject2) {
237      rejectObject2.addEventListener(actualRejectEventName2, rejectHandler);
238    }
239  });
240}
241
242/**
243 * Creates a new DOM element.
244 * @param {string} tagName The type of the DOM element.
245 * @param {object} attrs A JSON with attributes to apply to the element.
246 * @param {DOMElement} parent Optional - an existing DOM element to append to
247 *     If not provided, the returned element will remain orphaned.
248 * @param {boolean} doBindEvents Optional - Whether to bind to load and error
249 *     events and provide the promise wrapping the events via the element's
250 *     {@code eventPromise} property. Default value evaluates to false.
251 * @return {DOMElement} The newly created DOM element.
252 */
253function createElement(tagName, attrs, parentNode, doBindEvents) {
254  var element = document.createElement(tagName);
255
256  if (doBindEvents) {
257    bindEvents(element);
258    if (element.tagName == "IFRAME" && !('srcdoc' in attrs || 'src' in attrs)) {
259      // If we're loading a frame, ensure we spin the event loop after load to
260      // paper over the different event timing in Gecko vs Blink/WebKit
261      // see https://github.com/whatwg/html/issues/4965
262      element.eventPromise = element.eventPromise.then(() => {
263        return new Promise(resolve => setTimeout(resolve, 0))
264      });
265    }
266  }
267  // We set the attributes after binding to events to catch any
268  // event-triggering attribute changes. E.g. form submission.
269  //
270  // But be careful with images: unlike other elements they will start the load
271  // as soon as the attr is set, even if not in the document yet, and sometimes
272  // complete it synchronously, so the append doesn't have the effect we want.
273  // So for images, we want to set the attrs after appending, whereas for other
274  // elements we want to do it before appending.
275  var isImg = (tagName == "img");
276  if (!isImg)
277    setAttributes(element, attrs);
278
279  if (parentNode)
280    parentNode.appendChild(element);
281
282  if (isImg)
283    setAttributes(element, attrs);
284
285  return element;
286}
287
288function createRequestViaElement(tagName, attrs, parentNode) {
289  return createElement(tagName, attrs, parentNode, true).eventPromise;
290}
291
292function wrapResult(server_data) {
293  if (typeof(server_data) === "string") {
294    throw server_data;
295  }
296  return {
297    referrer: server_data.headers.referer,
298    headers: server_data.headers
299  }
300}
301
302// ===============================================================
303// Subresources
304// ===============================================================
305
306/**
307  @typedef RequestResult
308  @type {object}
309  Represents the result of sending an request.
310  All properties are optional. See the comments for
311  requestVia*() and invokeRequest() below to see which properties are set.
312
313  @property {Array<Object<string, string>>} headers
314    HTTP request headers sent to server.
315  @property {string} referrer - Referrer.
316  @property {string} location - The URL of the subresource.
317  @property {string} sourceContextUrl
318    the URL of the global object where the actual request is sent.
319*/
320
321/**
322  requestVia*(url, additionalAttributes) functions send a subresource
323  request from the current environment settings object.
324
325  @param {string} url
326    The URL of the subresource.
327  @param {Object<string, string>} additionalAttributes
328    Additional attributes set to DOM elements
329    (element-initiated requests only).
330
331  @returns {Promise} that are resolved with a RequestResult object
332  on successful requests.
333
334  - Category 1:
335      `headers`: set.
336      `referrer`: set via `document.referrer`.
337      `location`: set via `document.location`.
338      See `template/document.html.template`.
339  - Category 2:
340      `headers`: set.
341      `referrer`: set to `headers.referer` by `wrapResult()`.
342      `location`: not set.
343  - Category 3:
344      All the keys listed above are NOT set.
345  `sourceContextUrl` is not set here.
346
347  -------------------------------- -------- --------------------------
348  Function name                    Category Used in
349                                            -------- ------- ---------
350                                            referrer mixed-  upgrade-
351                                            policy   content insecure-
352                                            policy   content request
353  -------------------------------- -------- -------- ------- ---------
354  requestViaAnchor                 1        Y        Y       -
355  requestViaArea                   1        Y        Y       -
356  requestViaAudio                  3        -        Y       -
357  requestViaDedicatedWorker        2        Y        Y       Y
358  requestViaFetch                  2        Y        Y       -
359  requestViaForm                   2        -        Y       -
360  requestViaIframe                 1        Y        Y       -
361  requestViaImage                  2        Y        Y       -
362  requestViaLinkPrefetch           3        -        Y       -
363  requestViaLinkStylesheet         3        -        Y       -
364  requestViaObject                 3        -        Y       -
365  requestViaPicture                3        -        Y       -
366  requestViaScript                 2        Y        Y       -
367  requestViaSendBeacon             3        -        Y       -
368  requestViaSharedWorker           2        Y        Y       Y
369  requestViaVideo                  3        -        Y       -
370  requestViaWebSocket              3        -        Y       -
371  requestViaWorklet                3        -        Y       Y
372  requestViaXhr                    2        Y        Y       -
373  -------------------------------- -------- -------- ------- ---------
374*/
375
376/**
377 * Creates a new iframe, binds load and error events, sets the src attribute and
378 *     appends it to {@code document.body} .
379 * @param {string} url The src for the iframe.
380 * @return {Promise} The promise for success/error events.
381 */
382function requestViaIframe(url, additionalAttributes) {
383  const iframe = createElement(
384      "iframe",
385      Object.assign({"src": url}, additionalAttributes),
386      document.body,
387      false);
388  return bindEvents2(window, "message", iframe, "error", window, "error")
389      .then(event => {
390          if (event.source !== iframe.contentWindow)
391            return Promise.reject(new Error('Unexpected event.source'));
392          return event.data;
393        });
394}
395
396/**
397 * Creates a new image, binds load and error events, sets the src attribute and
398 *     appends it to {@code document.body} .
399 * @param {string} url The src for the image.
400 * @return {Promise} The promise for success/error events.
401 */
402function requestViaImage(url, additionalAttributes) {
403  const img = createElement(
404      "img",
405      // crossOrigin attribute is added to read the pixel data of the response.
406      Object.assign({"src": url, "crossOrigin": "Anonymous"}, additionalAttributes),
407      document.body, true);
408  return img.eventPromise.then(() => wrapResult(decodeImageData(img)));
409}
410
411// Helper for requestViaImage().
412function decodeImageData(img) {
413  var canvas = document.createElement("canvas");
414  var context = canvas.getContext('2d');
415  context.drawImage(img, 0, 0);
416  var imgData = context.getImageData(0, 0, img.clientWidth, img.clientHeight);
417  const rgba = imgData.data;
418
419  let decodedBytes = new Uint8ClampedArray(rgba.length);
420  let decodedLength = 0;
421
422  for (var i = 0; i + 12 <= rgba.length; i += 12) {
423    // A single byte is encoded in three pixels. 8 pixel octets (among
424    // 9 octets = 3 pixels * 3 channels) are used to encode 8 bits,
425    // the most significant bit first, where `0` and `255` in pixel values
426    // represent `0` and `1` in bits, respectively.
427    // This encoding is used to avoid errors due to different color spaces.
428    const bits = [];
429    for (let j = 0; j < 3; ++j) {
430      bits.push(rgba[i + j * 4 + 0]);
431      bits.push(rgba[i + j * 4 + 1]);
432      bits.push(rgba[i + j * 4 + 2]);
433      // rgba[i + j * 4 + 3]: Skip alpha channel.
434    }
435    // The last one element is not used.
436    bits.pop();
437
438    // Decode a single byte.
439    let byte = 0;
440    for (let j = 0; j < 8; ++j) {
441      byte <<= 1;
442      if (bits[j] >= 128)
443        byte |= 1;
444    }
445
446    // Zero is the string terminator.
447    if (byte == 0)
448      break;
449
450    decodedBytes[decodedLength++] = byte;
451  }
452
453  // Remove trailing nulls from data.
454  decodedBytes = decodedBytes.subarray(0, decodedLength);
455  var string_data = (new TextDecoder("ascii")).decode(decodedBytes);
456
457  return JSON.parse(string_data);
458}
459
460/**
461 * Initiates a new XHR GET request to provided URL.
462 * @param {string} url The endpoint URL for the XHR.
463 * @return {Promise} The promise for success/error events.
464 */
465function requestViaXhr(url) {
466  return xhrRequest(url).then(result => wrapResult(result));
467}
468
469/**
470 * Initiates a new GET request to provided URL via the Fetch API.
471 * @param {string} url The endpoint URL for the Fetch.
472 * @return {Promise} The promise for success/error events.
473 */
474function requestViaFetch(url) {
475  return fetch(url)
476    .then(res => res.json())
477    .then(j => wrapResult(j));
478}
479
480function dedicatedWorkerUrlThatFetches(url) {
481  return `data:text/javascript,
482    fetch('${url}')
483      .then(r => r.json())
484      .then(j => postMessage(j))
485      .catch((e) => postMessage(e.message));`;
486}
487
488function workerUrlThatImports(url) {
489  return `/common/security-features/subresource/static-import.py` +
490      `?import_url=${encodeURIComponent(url)}`;
491}
492
493function workerDataUrlThatImports(url) {
494  return `data:text/javascript,import '${url}';`;
495}
496
497/**
498 * Creates a new Worker, binds message and error events wrapping them into.
499 *     {@code worker.eventPromise} and posts an empty string message to start
500 *     the worker.
501 * @param {string} url The endpoint URL for the worker script.
502 * @param {object} options The options for Worker constructor.
503 * @return {Promise} The promise for success/error events.
504 */
505function requestViaDedicatedWorker(url, options) {
506  var worker;
507  try {
508    worker = new Worker(url, options);
509  } catch (e) {
510    return Promise.reject(e);
511  }
512  worker.postMessage('');
513  return bindEvents2(worker, "message", worker, "error")
514    .then(event => wrapResult(event.data));
515}
516
517function requestViaSharedWorker(url, options) {
518  var worker;
519  try {
520    worker = new SharedWorker(url, options);
521  } catch(e) {
522    return Promise.reject(e);
523  }
524  const promise = bindEvents2(worker.port, "message", worker, "error")
525    .then(event => wrapResult(event.data));
526  worker.port.start();
527  return promise;
528}
529
530// Returns a reference to a worklet object corresponding to a given type.
531function get_worklet(type) {
532  if (type == 'animation')
533    return CSS.animationWorklet;
534  if (type == 'layout')
535    return CSS.layoutWorklet;
536  if (type == 'paint')
537    return CSS.paintWorklet;
538  if (type == 'audio')
539    return new OfflineAudioContext(2,44100*40,44100).audioWorklet;
540
541  throw new Error('unknown worklet type is passed.');
542}
543
544function requestViaWorklet(type, url) {
545  try {
546    return get_worklet(type).addModule(url);
547  } catch (e) {
548    return Promise.reject(e);
549  }
550}
551
552/**
553 * Creates a navigable element with the name `navigableElementName`
554 * (<a>, <area>, or <form>) under `parentNode`, and
555 * performs a navigation by `trigger()` (e.g. clicking <a>).
556 * To avoid navigating away from the current execution context,
557 * a target attribute is set to point to a new helper iframe.
558 * @param {string} navigableElementName
559 * @param {object} additionalAttributes The attributes of the navigable element.
560 * @param {DOMElement} parentNode
561 * @param {function(DOMElement} trigger A callback called after the navigable
562 * element is inserted and should trigger navigation using the element.
563 * @return {Promise} The promise for success/error events.
564 */
565function requestViaNavigable(navigableElementName, additionalAttributes,
566                             parentNode, trigger) {
567  const name = guid();
568
569  const iframe =
570    createElement("iframe", {"name": name, "id": name}, parentNode, false);
571
572  const navigable = createElement(
573      navigableElementName,
574      Object.assign({"target": name}, additionalAttributes),
575      parentNode, false);
576
577  const promise =
578    bindEvents2(window, "message", iframe, "error", window, "error")
579      .then(event => {
580          if (event.source !== iframe.contentWindow)
581            return Promise.reject(new Error('Unexpected event.source'));
582          return event.data;
583        });
584  trigger(navigable);
585  return promise;
586}
587
588/**
589 * Creates a new anchor element, appends it to {@code document.body} and
590 *     performs the navigation.
591 * @param {string} url The URL to navigate to.
592 * @return {Promise} The promise for success/error events.
593 */
594function requestViaAnchor(url, additionalAttributes) {
595  return requestViaNavigable(
596      "a",
597      Object.assign({"href": url, "innerHTML": "Link to resource"},
598                    additionalAttributes),
599      document.body, a => a.click());
600}
601
602/**
603 * Creates a new area element, appends it to {@code document.body} and performs
604 *     the navigation.
605 * @param {string} url The URL to navigate to.
606 * @return {Promise} The promise for success/error events.
607 */
608function requestViaArea(url, additionalAttributes) {
609  // TODO(kristijanburnik): Append to map and add image.
610  return requestViaNavigable(
611      "area",
612      Object.assign({"href": url}, additionalAttributes),
613      document.body, area => area.click());
614}
615
616/**
617 * Creates a new script element, sets the src to url, and appends it to
618 *     {@code document.body}.
619 * @param {string} url The src URL.
620 * @return {Promise} The promise for success/error events.
621 */
622function requestViaScript(url, additionalAttributes) {
623  const script = createElement(
624      "script",
625      Object.assign({"src": url}, additionalAttributes),
626      document.body,
627      false);
628
629  return bindEvents2(window, "message", script, "error", window, "error")
630    .then(event => wrapResult(event.data));
631}
632
633/**
634 * Creates a new form element, sets attributes, appends it to
635 *     {@code document.body} and submits the form.
636 * @param {string} url The URL to submit to.
637 * @return {Promise} The promise for success/error events.
638 */
639function requestViaForm(url, additionalAttributes) {
640  return requestViaNavigable(
641      "form",
642      Object.assign({"action": url, "method": "POST"}, additionalAttributes),
643      document.body, form => form.submit());
644}
645
646/**
647 * Creates a new link element for a stylesheet, binds load and error events,
648 *     sets the href to url and appends it to {@code document.head}.
649 * @param {string} url The URL for a stylesheet.
650 * @return {Promise} The promise for success/error events.
651 */
652function requestViaLinkStylesheet(url) {
653  return createRequestViaElement("link",
654                                 {"rel": "stylesheet", "href": url},
655                                 document.head);
656}
657
658/**
659 * Creates a new link element for a prefetch, binds load and error events, sets
660 *     the href to url and appends it to {@code document.head}.
661 * @param {string} url The URL of a resource to prefetch.
662 * @return {Promise} The promise for success/error events.
663 */
664function requestViaLinkPrefetch(url) {
665  var link = document.createElement('link');
666  if (link.relList && link.relList.supports && link.relList.supports("prefetch")) {
667    return createRequestViaElement("link",
668                                   {"rel": "prefetch", "href": url},
669                                   document.head);
670  } else {
671    return Promise.reject("This browser does not support 'prefetch'.");
672  }
673}
674
675/**
676 * Initiates a new beacon request.
677 * @param {string} url The URL of a resource to prefetch.
678 * @return {Promise} The promise for success/error events.
679 */
680async function requestViaSendBeacon(url) {
681  function wait(ms) {
682    return new Promise(resolve => step_timeout(resolve, ms));
683  }
684  if (!navigator.sendBeacon(url)) {
685    // If mixed-content check fails, it should return false.
686    throw new Error('sendBeacon() fails.');
687  }
688  // We don't have a means to see the result of sendBeacon() request
689  // for sure. Let's wait for a while and let the generic test function
690  // ask the server for the result.
691  await wait(500);
692  return 'allowed';
693}
694
695/**
696 * Creates a new media element with a child source element, binds loadeddata and
697 *     error events, sets attributes and appends to document.body.
698 * @param {string} type The type of the media element (audio/video/picture).
699 * @param {object} media_attrs The attributes for the media element.
700 * @param {object} source_attrs The attributes for the child source element.
701 * @return {DOMElement} The newly created media element.
702 */
703function createMediaElement(type, media_attrs, source_attrs) {
704  var mediaElement = createElement(type, {});
705
706  var sourceElement = createElement("source", {});
707
708  mediaElement.eventPromise = new Promise(function(resolve, reject) {
709    mediaElement.addEventListener("loadeddata", function (e) {
710      resolve(e);
711    });
712
713    // Safari doesn't fire an `error` event when blocking mixed content.
714    mediaElement.addEventListener("stalled", function(e) {
715      reject(e);
716    });
717
718    sourceElement.addEventListener("error", function(e) {
719      reject(e);
720    });
721  });
722
723  setAttributes(mediaElement, media_attrs);
724  setAttributes(sourceElement, source_attrs);
725
726  mediaElement.appendChild(sourceElement);
727  document.body.appendChild(mediaElement);
728
729  return mediaElement;
730}
731
732/**
733 * Creates a new video element, binds loadeddata and error events, sets
734 *     attributes and source URL and appends to {@code document.body}.
735 * @param {string} url The URL of the video.
736 * @return {Promise} The promise for success/error events.
737 */
738function requestViaVideo(url) {
739  return createMediaElement("video",
740                            {},
741                            {"src": url}).eventPromise;
742}
743
744/**
745 * Creates a new audio element, binds loadeddata and error events, sets
746 *     attributes and source URL and appends to {@code document.body}.
747 * @param {string} url The URL of the audio.
748 * @return {Promise} The promise for success/error events.
749 */
750function requestViaAudio(url) {
751  return createMediaElement("audio",
752                            {},
753                            {"type": "audio/wav", "src": url}).eventPromise;
754}
755
756/**
757 * Creates a new picture element, binds loadeddata and error events, sets
758 *     attributes and source URL and appends to {@code document.body}. Also
759 *     creates new image element appending it to the picture
760 * @param {string} url The URL of the image for the source and image elements.
761 * @return {Promise} The promise for success/error events.
762 */
763function requestViaPicture(url) {
764  var picture = createMediaElement("picture", {}, {"srcset": url,
765                                                "type": "image/png"});
766  return createRequestViaElement("img", {"src": url}, picture);
767}
768
769/**
770 * Creates a new object element, binds load and error events, sets the data to
771 *     url, and appends it to {@code document.body}.
772 * @param {string} url The data URL.
773 * @return {Promise} The promise for success/error events.
774 */
775function requestViaObject(url) {
776  return createRequestViaElement("object", {"data": url, "type": "text/html"}, document.body);
777}
778
779/**
780 * Creates a new WebSocket pointing to {@code url} and sends a message string
781 * "echo". The {@code message} and {@code error} events are triggering the
782 * returned promise resolve/reject events.
783 * @param {string} url The URL for WebSocket to connect to.
784 * @return {Promise} The promise for success/error events.
785 */
786function requestViaWebSocket(url) {
787  return new Promise(function(resolve, reject) {
788    var websocket = new WebSocket(url);
789
790    websocket.addEventListener("message", function(e) {
791      resolve(e.data);
792    });
793
794    websocket.addEventListener("open", function(e) {
795      websocket.send("echo");
796    });
797
798    websocket.addEventListener("error", function(e) {
799      reject(e)
800    });
801  })
802  .then(data => {
803      return JSON.parse(data);
804    });
805}
806
807/**
808  @typedef SubresourceType
809  @type {string}
810
811  Represents how a subresource is sent.
812  The keys of `subresourceMap` below are the valid values.
813*/
814
815// Subresource paths and invokers.
816const subresourceMap = {
817  "a-tag": {
818    path: "/common/security-features/subresource/document.py",
819    invoker: requestViaAnchor,
820  },
821  "area-tag": {
822    path: "/common/security-features/subresource/document.py",
823    invoker: requestViaArea,
824  },
825  "audio-tag": {
826    path: "/common/security-features/subresource/audio.py",
827    invoker: requestViaAudio,
828  },
829  "beacon": {
830    path: "/common/security-features/subresource/empty.py",
831    invoker: requestViaSendBeacon,
832  },
833  "fetch": {
834    path: "/common/security-features/subresource/xhr.py",
835    invoker: requestViaFetch,
836  },
837  "form-tag": {
838    path: "/common/security-features/subresource/document.py",
839    invoker: requestViaForm,
840  },
841  "iframe-tag": {
842    path: "/common/security-features/subresource/document.py",
843    invoker: requestViaIframe,
844  },
845  "img-tag": {
846    path: "/common/security-features/subresource/image.py",
847    invoker: requestViaImage,
848  },
849  "link-css-tag": {
850    path: "/common/security-features/subresource/empty.py",
851    invoker: requestViaLinkStylesheet,
852  },
853  "link-prefetch-tag": {
854    path: "/common/security-features/subresource/empty.py",
855    invoker: requestViaLinkPrefetch,
856  },
857  "object-tag": {
858    path: "/common/security-features/subresource/empty.py",
859    invoker: requestViaObject,
860  },
861  "picture-tag": {
862    path: "/common/security-features/subresource/image.py",
863    invoker: requestViaPicture,
864  },
865  "script-tag": {
866    path: "/common/security-features/subresource/script.py",
867    invoker: requestViaScript,
868  },
869  "video-tag": {
870    path: "/common/security-features/subresource/video.py",
871    invoker: requestViaVideo,
872  },
873  "xhr": {
874    path: "/common/security-features/subresource/xhr.py",
875    invoker: requestViaXhr,
876  },
877
878  "worker-classic": {
879    path: "/common/security-features/subresource/worker.py",
880    invoker: url => requestViaDedicatedWorker(url),
881  },
882  "worker-module": {
883    path: "/common/security-features/subresource/worker.py",
884    invoker: url => requestViaDedicatedWorker(url, {type: "module"}),
885  },
886  "worker-import": {
887    path: "/common/security-features/subresource/worker.py",
888    invoker: url =>
889        requestViaDedicatedWorker(workerUrlThatImports(url), {type: "module"}),
890  },
891  "worker-import-data": {
892    path: "/common/security-features/subresource/worker.py",
893    invoker: url =>
894        requestViaDedicatedWorker(workerDataUrlThatImports(url), {type: "module"}),
895  },
896  "sharedworker-classic": {
897    path: "/common/security-features/subresource/shared-worker.py",
898    invoker: url => requestViaSharedWorker(url),
899  },
900  "sharedworker-module": {
901    path: "/common/security-features/subresource/shared-worker.py",
902    invoker: url => requestViaSharedWorker(url, {type: "module"}),
903  },
904  "sharedworker-import": {
905    path: "/common/security-features/subresource/shared-worker.py",
906    invoker: url =>
907        requestViaSharedWorker(workerUrlThatImports(url), {type: "module"}),
908  },
909  "sharedworker-import-data": {
910    path: "/common/security-features/subresource/shared-worker.py",
911    invoker: url =>
912        requestViaSharedWorker(workerDataUrlThatImports(url), {type: "module"}),
913  },
914
915  "websocket": {
916    path: "/stash_responder",
917    invoker: requestViaWebSocket,
918  },
919};
920for (const workletType of ['animation', 'audio', 'layout', 'paint']) {
921  subresourceMap[`worklet-${workletType}`] = {
922      path: "/common/security-features/subresource/worker.py",
923      invoker: url => requestViaWorklet(workletType, url)
924    };
925  subresourceMap[`worklet-${workletType}-import-data`] = {
926      path: "/common/security-features/subresource/worker.py",
927      invoker: url =>
928          requestViaWorklet(workletType, workerDataUrlThatImports(url))
929    };
930}
931
932/**
933  @typedef RedirectionType
934  @type {string}
935
936  Represents what redirects should occur to the subresource request
937  after initial request.
938  See preprocess_redirection() in
939  /common/security-features/subresource/subresource.py for valid values.
940*/
941
942/**
943  Construct subresource (and related) origin.
944
945  @param {string} originType
946  @returns {object} the origin of the subresource.
947*/
948function getSubresourceOrigin(originType) {
949  const httpProtocol = "http";
950  const httpsProtocol = "https";
951  const wsProtocol = "ws";
952  const wssProtocol = "wss";
953
954  const sameOriginHost = "{{host}}";
955  const crossOriginHost = "{{domains[www1]}}";
956
957  // These values can evaluate to either empty strings or a ":port" string.
958  const httpPort = getNormalizedPort(parseInt("{{ports[http][0]}}", 10));
959  const httpsRawPort = parseInt("{{ports[https][0]}}", 10);
960  const httpsPort = getNormalizedPort(httpsRawPort);
961  const wsPort = getNormalizedPort(parseInt("{{ports[ws][0]}}", 10));
962  const wssRawPort = parseInt("{{ports[wss][0]}}", 10);
963  const wssPort = getNormalizedPort(wssRawPort);
964
965  /**
966    @typedef OriginType
967    @type {string}
968
969    Represents the origin of the subresource request URL.
970    The keys of `originMap` below are the valid values.
971
972    Note that there can be redirects from the specified origin
973    (see RedirectionType), and thus the origin of the subresource
974    response URL might be different from what is specified by OriginType.
975  */
976  const originMap = {
977    "same-https": httpsProtocol + "://" + sameOriginHost + httpsPort,
978    "same-http": httpProtocol + "://" + sameOriginHost + httpPort,
979    "cross-https": httpsProtocol + "://" + crossOriginHost + httpsPort,
980    "cross-http": httpProtocol + "://" + crossOriginHost + httpPort,
981    "same-wss": wssProtocol + "://" + sameOriginHost + wssPort,
982    "same-ws": wsProtocol + "://" + sameOriginHost + wsPort,
983    "cross-wss": wssProtocol + "://" + crossOriginHost + wssPort,
984    "cross-ws": wsProtocol + "://" + crossOriginHost + wsPort,
985
986    // The following origin types are used for upgrade-insecure-requests tests:
987    // These rely on some unintuitive cleverness due to WPT's test setup:
988    // 'Upgrade-Insecure-Requests' does not upgrade the port number,
989    // so we use URLs in the form `http://[domain]:[https-port]`,
990    // which will be upgraded to `https://[domain]:[https-port]`.
991    // If the upgrade fails, the load will fail, as we don't serve HTTP over
992    // the secure port.
993    "same-http-downgrade":
994        httpProtocol + "://" + sameOriginHost + ":" + httpsRawPort,
995    "cross-http-downgrade":
996        httpProtocol + "://" + crossOriginHost + ":" + httpsRawPort,
997    "same-ws-downgrade":
998        wsProtocol + "://" + sameOriginHost + ":" + wssRawPort,
999    "cross-ws-downgrade":
1000        wsProtocol + "://" + crossOriginHost + ":" + wssRawPort,
1001  };
1002
1003  return originMap[originType];
1004}
1005
1006/**
1007  Construct subresource (and related) URLs.
1008
1009  @param {SubresourceType} subresourceType
1010  @param {OriginType} originType
1011  @param {RedirectionType} redirectionType
1012  @returns {object} with following properties:
1013    {string} testUrl
1014      The subresource request URL.
1015    {string} announceUrl
1016    {string} assertUrl
1017      The URLs to be used for detecting whether `testUrl` is actually sent
1018      to the server.
1019      1. Fetch `announceUrl` first,
1020      2. then possibly fetch `testUrl`, and
1021      3. finally fetch `assertUrl`.
1022         The fetch result of `assertUrl` should indicate whether
1023         `testUrl` is actually sent to the server or not.
1024*/
1025function getRequestURLs(subresourceType, originType, redirectionType) {
1026  const key = guid();
1027  const value = guid();
1028
1029  // We use the same stash path for both HTTP/S and WS/S stash requests.
1030  const stashPath = encodeURIComponent("/mixed-content");
1031
1032  const stashEndpoint = "/common/security-features/subresource/xhr.py?key=" +
1033                        key + "&path=" + stashPath;
1034  return {
1035    testUrl:
1036      getSubresourceOrigin(originType) +
1037        subresourceMap[subresourceType].path +
1038        "?redirection=" + encodeURIComponent(redirectionType) +
1039        "&action=purge&key=" + key +
1040        "&path=" + stashPath,
1041    announceUrl: stashEndpoint + "&action=put&value=" + value,
1042    assertUrl: stashEndpoint + "&action=take",
1043  };
1044}
1045
1046// ===============================================================
1047// Source Context
1048// ===============================================================
1049// Requests can be sent from several source contexts,
1050// such as the main documents, iframes, workers, or so,
1051// possibly nested, and possibly with <meta>/http headers added.
1052// invokeRequest() and invokeFrom*() functions handles
1053// SourceContext-related setup in client-side.
1054
1055/**
1056  invokeRequest() invokes a subresource request
1057  (specified as `subresource`)
1058  from a (possibly nested) environment settings object
1059  (specified as `sourceContextList`).
1060
1061  For nested contexts, invokeRequest() calls an invokeFrom*() function
1062  that creates a nested environment settings object using
1063  /common/security-features/scope/, which calls invokeRequest()
1064  again inside the nested environment settings object.
1065  This cycle continues until all specified
1066  nested environment settings object are created, and
1067  finally invokeRequest() calls a requestVia*() function to start the
1068  subresource request from the inner-most environment settings object.
1069
1070  @param {Subresource} subresource
1071  @param {Array<SourceContext>} sourceContextList
1072
1073  @returns {Promise} A promise that is resolved with an RequestResult object.
1074  `sourceContextUrl` is always set. For whether other properties are set,
1075  see the comments for requestVia*() above.
1076*/
1077function invokeRequest(subresource, sourceContextList) {
1078  if (sourceContextList.length === 0) {
1079    // No further nested global objects. Send the subresource request here.
1080
1081    const additionalAttributes = {};
1082    /** @type {PolicyDelivery} policyDelivery */
1083    for (const policyDelivery of (subresource.policyDeliveries || [])) {
1084      // Depending on the delivery method, extend the subresource element with
1085      // these attributes.
1086      if (policyDelivery.deliveryType === "attr") {
1087        additionalAttributes[policyDelivery.key] = policyDelivery.value;
1088      } else if (policyDelivery.deliveryType === "rel-noref") {
1089        additionalAttributes["rel"] = "noreferrer";
1090      }
1091    }
1092
1093    return subresourceMap[subresource.subresourceType].invoker(
1094        subresource.url,
1095        additionalAttributes)
1096      .then(result => Object.assign(
1097          {sourceContextUrl: location.toString()},
1098          result));
1099  }
1100
1101  // Defines invokers for each valid SourceContext.sourceContextType.
1102  const sourceContextMap = {
1103    "srcdoc": { // <iframe srcdoc></iframe>
1104      invoker: invokeFromIframe,
1105    },
1106    "iframe": { // <iframe src="same-origin-URL"></iframe>
1107      invoker: invokeFromIframe,
1108    },
1109    "iframe-blank": { // <iframe></iframe>
1110      invoker: invokeFromIframe,
1111    },
1112    "worker-classic": {
1113      // Classic dedicated worker loaded from same-origin.
1114      invoker: invokeFromWorker.bind(undefined, "worker", false, {}),
1115    },
1116    "worker-classic-data": {
1117      // Classic dedicated worker loaded from data: URL.
1118      invoker: invokeFromWorker.bind(undefined, "worker", true, {}),
1119    },
1120    "worker-module": {
1121      // Module dedicated worker loaded from same-origin.
1122      invoker: invokeFromWorker.bind(undefined, "worker", false, {type: 'module'}),
1123    },
1124    "worker-module-data": {
1125      // Module dedicated worker loaded from data: URL.
1126      invoker: invokeFromWorker.bind(undefined, "worker", true, {type: 'module'}),
1127    },
1128    "sharedworker-classic": {
1129      // Classic shared worker loaded from same-origin.
1130      invoker: invokeFromWorker.bind(undefined, "sharedworker", false, {}),
1131    },
1132    "sharedworker-classic-data": {
1133      // Classic shared worker loaded from data: URL.
1134      invoker: invokeFromWorker.bind(undefined, "sharedworker", true, {}),
1135    },
1136    "sharedworker-module": {
1137      // Module shared worker loaded from same-origin.
1138      invoker: invokeFromWorker.bind(undefined, "sharedworker", false, {type: 'module'}),
1139    },
1140    "sharedworker-module-data": {
1141      // Module shared worker loaded from data: URL.
1142      invoker: invokeFromWorker.bind(undefined, "sharedworker", true, {type: 'module'}),
1143    },
1144  };
1145
1146  return sourceContextMap[sourceContextList[0].sourceContextType].invoker(
1147      subresource, sourceContextList);
1148}
1149
1150// Quick hack to expose invokeRequest when common.sub.js is loaded either
1151// as a classic or module script.
1152self.invokeRequest = invokeRequest;
1153
1154/**
1155  invokeFrom*() functions are helper functions with the same parameters
1156  and return values as invokeRequest(), that are tied to specific types
1157  of top-most environment settings objects.
1158  For example, invokeFromIframe() is the helper function for the cases where
1159  sourceContextList[0] is an iframe.
1160*/
1161
1162/**
1163  @param {string} workerType
1164    "worker" (for dedicated worker) or "sharedworker".
1165  @param {boolean} isDataUrl
1166    true if the worker script is loaded from data: URL.
1167    Otherwise, the script is loaded from same-origin.
1168  @param {object} workerOptions
1169    The `options` argument for Worker constructor.
1170
1171  Other parameters and return values are the same as those of invokeRequest().
1172*/
1173function invokeFromWorker(workerType, isDataUrl, workerOptions,
1174                          subresource, sourceContextList) {
1175  const currentSourceContext = sourceContextList[0];
1176  let workerUrl =
1177    "/common/security-features/scope/worker.py?policyDeliveries=" +
1178    encodeURIComponent(JSON.stringify(
1179        currentSourceContext.policyDeliveries || []));
1180  if (workerOptions.type === 'module') {
1181    workerUrl += "&type=module";
1182  }
1183
1184  let promise;
1185  if (isDataUrl) {
1186    promise = fetch(workerUrl)
1187      .then(r => r.text())
1188      .then(source => {
1189          return 'data:text/javascript;base64,' + btoa(source);
1190        });
1191  } else {
1192    promise = Promise.resolve(workerUrl);
1193  }
1194
1195  return promise
1196    .then(url => {
1197      if (workerType === "worker") {
1198        const worker = new Worker(url, workerOptions);
1199        worker.postMessage({subresource: subresource,
1200                            sourceContextList: sourceContextList.slice(1)});
1201        return bindEvents2(worker, "message", worker, "error", window, "error");
1202      } else if (workerType === "sharedworker") {
1203        const worker = new SharedWorker(url, workerOptions);
1204        worker.port.start();
1205        worker.port.postMessage({subresource: subresource,
1206                                 sourceContextList: sourceContextList.slice(1)});
1207        return bindEvents2(worker.port, "message", worker, "error", window, "error");
1208      } else {
1209        throw new Error('Invalid worker type: ' + workerType);
1210      }
1211    })
1212    .then(event => {
1213        if (event.data.error)
1214          return Promise.reject(event.data.error);
1215        return event.data;
1216      });
1217}
1218
1219function invokeFromIframe(subresource, sourceContextList) {
1220  const currentSourceContext = sourceContextList[0];
1221  const frameUrl =
1222    "/common/security-features/scope/document.py?policyDeliveries=" +
1223    encodeURIComponent(JSON.stringify(
1224        currentSourceContext.policyDeliveries || []));
1225
1226  let iframe;
1227  let promise;
1228  if (currentSourceContext.sourceContextType === 'srcdoc') {
1229    promise = fetch(frameUrl)
1230      .then(r => r.text())
1231      .then(srcdoc => {
1232          iframe = createElement(
1233              "iframe", {srcdoc: srcdoc}, document.body, true);
1234          return iframe.eventPromise;
1235        });
1236  } else if (currentSourceContext.sourceContextType === 'iframe') {
1237    iframe = createElement("iframe", {src: frameUrl}, document.body, true);
1238    promise = iframe.eventPromise;
1239  } else if (currentSourceContext.sourceContextType === 'iframe-blank') {
1240    let frameContent;
1241    promise = fetch(frameUrl)
1242      .then(r => r.text())
1243      .then(t => {
1244          frameContent = t;
1245          iframe = createElement("iframe", {}, document.body, true);
1246          return iframe.eventPromise;
1247        })
1248      .then(() => {
1249          // Reinitialize `iframe.eventPromise` with a new promise
1250          // that catches the load event for the document.write() below.
1251          bindEvents(iframe);
1252
1253          iframe.contentDocument.write(frameContent);
1254          iframe.contentDocument.close();
1255          return iframe.eventPromise;
1256        });
1257  }
1258
1259  return promise
1260    .then(() => {
1261        const promise = bindEvents2(
1262            window, "message", iframe, "error", window, "error");
1263        iframe.contentWindow.postMessage(
1264            {subresource: subresource,
1265             sourceContextList: sourceContextList.slice(1)},
1266            "*");
1267        return promise;
1268      })
1269    .then(event => {
1270        if (event.data.error)
1271          return Promise.reject(event.data.error);
1272        return event.data;
1273      });
1274}
1275
1276// SanityChecker does nothing in release mode. See sanity-checker.js for debug
1277// mode.
1278function SanityChecker() {}
1279SanityChecker.prototype.checkScenario = function() {};
1280SanityChecker.prototype.setFailTimeout = function(test, timeout) {};
1281SanityChecker.prototype.checkSubresourceResult = function() {};
1282