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