common.sub.js (48204B)
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 102 function 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 */ 113 function 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 */ 124 function 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 */ 139 function 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 */ 165 function setAttributes(el, attrs) { 166 attrs = attrs || {} 167 for (var attr in attrs) { 168 if (attr !== 'src') 169 el.setAttribute(attr.toLowerCase(), 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 */ 190 function 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. 207 function 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 */ 253 function 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 288 function createRequestViaElement(tagName, attrs, parentNode) { 289 return createElement(tagName, attrs, parentNode, true).eventPromise; 290 } 291 292 function 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 */ 382 function 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 */ 402 function 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(). 412 function 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 */ 465 function 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 */ 474 function requestViaFetch(url) { 475 return fetch(url) 476 .then(res => res.json()) 477 .then(j => wrapResult(j)); 478 } 479 480 function 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 488 function workerUrlThatImports(url, additionalAttributes) { 489 let csp = ""; 490 if (additionalAttributes && additionalAttributes.contentSecurityPolicy) { 491 csp=`&contentSecurityPolicy=${additionalAttributes.contentSecurityPolicy}`; 492 } 493 return `/common/security-features/subresource/static-import.py` + 494 `?import_url=${encodeURIComponent(url)}${csp}`; 495 } 496 497 function workerDataUrlThatImports(url) { 498 return `data:text/javascript,import '${url}';`; 499 } 500 501 /** 502 * Creates a new Worker, binds message and error events wrapping them into. 503 * {@code worker.eventPromise} and posts an empty string message to start 504 * the worker. 505 * @param {string} url The endpoint URL for the worker script. 506 * @param {object} options The options for Worker constructor. 507 * @return {Promise} The promise for success/error events. 508 */ 509 function requestViaDedicatedWorker(url, options) { 510 var worker; 511 try { 512 worker = new Worker(url, options); 513 } catch (e) { 514 return Promise.reject(e); 515 } 516 worker.postMessage(''); 517 return bindEvents2(worker, "message", worker, "error") 518 .then(event => wrapResult(event.data)); 519 } 520 521 function requestViaSharedWorker(url, options) { 522 var worker; 523 try { 524 worker = new SharedWorker(url, options); 525 } catch(e) { 526 return Promise.reject(e); 527 } 528 const promise = bindEvents2(worker.port, "message", worker, "error") 529 .then(event => wrapResult(event.data)); 530 worker.port.start(); 531 return promise; 532 } 533 534 // Returns a reference to a worklet object corresponding to a given type. 535 function get_worklet(type) { 536 if (type == 'animation') 537 return CSS.animationWorklet; 538 if (type == 'layout') 539 return CSS.layoutWorklet; 540 if (type == 'paint') 541 return CSS.paintWorklet; 542 if (type == 'audio') 543 return new OfflineAudioContext(2,44100*40,44100).audioWorklet; 544 545 throw new Error('unknown worklet type is passed.'); 546 } 547 548 function requestViaWorklet(type, url) { 549 try { 550 return get_worklet(type).addModule(url); 551 } catch (e) { 552 return Promise.reject(e); 553 } 554 } 555 556 /** 557 * Creates a navigable element with the name `navigableElementName` 558 * (<a>, <area>, or <form>) under `parentNode`, and 559 * performs a navigation by `trigger()` (e.g. clicking <a>). 560 * To avoid navigating away from the current execution context, 561 * a target attribute is set to point to a new helper iframe. 562 * @param {string} navigableElementName 563 * @param {object} additionalAttributes The attributes of the navigable element. 564 * @param {DOMElement} parentNode 565 * @param {function(DOMElement} trigger A callback called after the navigable 566 * element is inserted and should trigger navigation using the element. 567 * @return {Promise} The promise for success/error events. 568 */ 569 function requestViaNavigable(navigableElementName, additionalAttributes, 570 parentNode, trigger) { 571 const name = guid(); 572 573 const iframe = 574 createElement("iframe", {"name": name, "id": name}, parentNode, false); 575 576 const navigable = createElement( 577 navigableElementName, 578 Object.assign({"target": name}, additionalAttributes), 579 parentNode, false); 580 581 const promise = 582 bindEvents2(window, "message", iframe, "error", window, "error") 583 .then(event => { 584 if (event.source !== iframe.contentWindow) 585 return Promise.reject(new Error('Unexpected event.source')); 586 return event.data; 587 }); 588 trigger(navigable); 589 return promise; 590 } 591 592 /** 593 * Creates a new anchor element, appends it to {@code document.body} and 594 * performs the navigation. 595 * @param {string} url The URL to navigate to. 596 * @return {Promise} The promise for success/error events. 597 */ 598 function requestViaAnchor(url, additionalAttributes) { 599 return requestViaNavigable( 600 "a", 601 Object.assign({"href": url, "innerHTML": "Link to resource"}, 602 additionalAttributes), 603 document.body, a => a.click()); 604 } 605 606 /** 607 * Creates a new area element, appends it to {@code document.body} and performs 608 * the navigation. 609 * @param {string} url The URL to navigate to. 610 * @return {Promise} The promise for success/error events. 611 */ 612 function requestViaArea(url, additionalAttributes) { 613 // TODO(kristijanburnik): Append to map and add image. 614 return requestViaNavigable( 615 "area", 616 Object.assign({"href": url}, additionalAttributes), 617 document.body, area => area.click()); 618 } 619 620 /** 621 * Creates a new script element, sets the src to url, and appends it to 622 * {@code document.body}. 623 * @param {string} url The src URL. 624 * @return {Promise} The promise for success/error events. 625 */ 626 function requestViaScript(url, additionalAttributes) { 627 const script = createElement( 628 "script", 629 Object.assign({"src": url}, additionalAttributes), 630 document.body, 631 false); 632 633 return bindEvents2(window, "message", script, "error", window, "error") 634 .then(event => wrapResult(event.data)); 635 } 636 637 /** 638 * Creates a new script element that performs a dynamic import to `url`, and 639 * appends the script element to {@code document.body}. 640 * @param {string} url The src URL. 641 * @return {Promise} The promise for success/error events. 642 */ 643 function requestViaDynamicImport(url, additionalAttributes) { 644 const scriptUrl = `data:text/javascript,import("${url}");`; 645 const script = createElement( 646 "script", 647 Object.assign({"src": scriptUrl}, additionalAttributes), 648 document.body, 649 false); 650 651 return bindEvents2(window, "message", script, "error", window, "error") 652 .then(event => wrapResult(event.data)); 653 } 654 655 /** 656 * Creates a new form element, sets attributes, appends it to 657 * {@code document.body} and submits the form. 658 * @param {string} url The URL to submit to. 659 * @return {Promise} The promise for success/error events. 660 */ 661 function requestViaForm(url, additionalAttributes) { 662 return requestViaNavigable( 663 "form", 664 Object.assign({"action": url, "method": "POST"}, additionalAttributes), 665 document.body, form => form.submit()); 666 } 667 668 /** 669 * Creates a new link element for a stylesheet, binds load and error events, 670 * sets the href to url and appends it to {@code document.head}. 671 * @param {string} url The URL for a stylesheet. 672 * @return {Promise} The promise for success/error events. 673 */ 674 function requestViaLinkStylesheet(url) { 675 return createRequestViaElement("link", 676 {"rel": "stylesheet", "href": url}, 677 document.head); 678 } 679 680 /** 681 * Creates a new link element for a prefetch, binds load and error events, sets 682 * the href to url and appends it to {@code document.head}. 683 * @param {string} url The URL of a resource to prefetch. 684 * @return {Promise} The promise for success/error events. 685 */ 686 function requestViaLinkPrefetch(url) { 687 var link = document.createElement('link'); 688 if (link.relList && link.relList.supports && link.relList.supports("prefetch")) { 689 return createRequestViaElement("link", 690 {"rel": "prefetch", "href": url}, 691 document.head); 692 } else { 693 return Promise.reject("This browser does not support 'prefetch'."); 694 } 695 } 696 697 /** 698 * Initiates a new beacon request. 699 * @param {string} url The URL of a resource to prefetch. 700 * @return {Promise} The promise for success/error events. 701 */ 702 async function requestViaSendBeacon(url) { 703 function wait(ms) { 704 return new Promise(resolve => step_timeout(resolve, ms)); 705 } 706 if (!navigator.sendBeacon(url)) { 707 // If mixed-content check fails, it should return false. 708 throw new Error('sendBeacon() fails.'); 709 } 710 // We don't have a means to see the result of sendBeacon() request 711 // for sure. Let's wait for a while and let the generic test function 712 // ask the server for the result. 713 await wait(500); 714 return 'allowed'; 715 } 716 717 /** 718 * Creates a new media element with a child source element, binds loadeddata and 719 * error events, sets attributes and appends to document.body. 720 * @param {string} type The type of the media element (audio/video/picture). 721 * @param {object} media_attrs The attributes for the media element. 722 * @param {object} source_attrs The attributes for the child source element. 723 * @return {DOMElement} The newly created media element. 724 */ 725 function createMediaElement(type, media_attrs, source_attrs) { 726 var mediaElement = createElement(type, {}); 727 728 var sourceElement = createElement("source", {}); 729 730 mediaElement.eventPromise = new Promise(function(resolve, reject) { 731 mediaElement.addEventListener("loadeddata", function (e) { 732 resolve(e); 733 }); 734 735 // Safari doesn't fire an `error` event when blocking mixed content. 736 mediaElement.addEventListener("stalled", function(e) { 737 reject(e); 738 }); 739 740 sourceElement.addEventListener("error", function(e) { 741 reject(e); 742 }); 743 }); 744 745 setAttributes(mediaElement, media_attrs); 746 setAttributes(sourceElement, source_attrs); 747 748 mediaElement.appendChild(sourceElement); 749 document.body.appendChild(mediaElement); 750 751 return mediaElement; 752 } 753 754 /** 755 * Creates a new video element, binds loadeddata and error events, sets 756 * attributes and source URL and appends to {@code document.body}. 757 * @param {string} url The URL of the video. 758 * @return {Promise} The promise for success/error events. 759 */ 760 function requestViaVideo(url) { 761 return createMediaElement("video", 762 {}, 763 {"src": url}).eventPromise; 764 } 765 766 /** 767 * Creates a new audio element, binds loadeddata and error events, sets 768 * attributes and source URL and appends to {@code document.body}. 769 * @param {string} url The URL of the audio. 770 * @return {Promise} The promise for success/error events. 771 */ 772 function requestViaAudio(url) { 773 return createMediaElement("audio", 774 {}, 775 {"type": "audio/wav", "src": url}).eventPromise; 776 } 777 778 /** 779 * Creates a new picture element, binds loadeddata and error events, sets 780 * attributes and source URL and appends to {@code document.body}. Also 781 * creates new image element appending it to the picture 782 * @param {string} url The URL of the image for the source and image elements. 783 * @return {Promise} The promise for success/error events. 784 */ 785 function requestViaPicture(url) { 786 var picture = createMediaElement("picture", {}, {"srcset": url, 787 "type": "image/png"}); 788 return createRequestViaElement("img", {"src": url}, picture); 789 } 790 791 /** 792 * Creates a new object element, binds load and error events, sets the data to 793 * url, and appends it to {@code document.body}. 794 * @param {string} url The data URL. 795 * @return {Promise} The promise for success/error events. 796 */ 797 function requestViaObject(url) { 798 return createRequestViaElement("object", {"data": url, "type": "text/html"}, document.body); 799 } 800 801 /** 802 * Creates a new WebSocket pointing to {@code url} and sends a message string 803 * "echo". The {@code message} and {@code error} events are triggering the 804 * returned promise resolve/reject events. 805 * @param {string} url The URL for WebSocket to connect to. 806 * @return {Promise} The promise for success/error events. 807 */ 808 function requestViaWebSocket(url) { 809 return new Promise(function(resolve, reject) { 810 var websocket = new WebSocket(url); 811 812 websocket.addEventListener("message", function(e) { 813 resolve(e.data); 814 }); 815 816 websocket.addEventListener("open", function(e) { 817 websocket.send("echo"); 818 }); 819 820 websocket.addEventListener("error", function(e) { 821 reject(e) 822 }); 823 }) 824 .then(data => { 825 return JSON.parse(data); 826 }); 827 } 828 829 /** 830 * Creates a svg anchor element and the corresponding svg setup, appends the 831 * setup to {@code document.body} and performs the navigation. 832 * @param {string} url The URL to navigate to. 833 * @return {Promise} The promise for success/error events. 834 */ 835 function requestViaSVGAnchor(url, additionalAttributes) { 836 const name = guid(); 837 838 const iframe = 839 createElement("iframe", { "name": name, "id": name }, document.body, false); 840 841 // Create SVG container 842 const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); 843 844 // Create SVG anchor element 845 const svgAnchor = document.createElementNS("http://www.w3.org/2000/svg", "a"); 846 const link_attributes = Object.assign({ "href": url, "target": name }, additionalAttributes); 847 setAttributes(svgAnchor, link_attributes); 848 849 // Add some text content for the anchor 850 const text = document.createElementNS("http://www.w3.org/2000/svg", "text"); 851 text.setAttribute("y", "50"); 852 text.textContent = "SVG Link to resource"; 853 854 svgAnchor.appendChild(text); 855 svg.appendChild(svgAnchor); 856 document.body.appendChild(svg); 857 858 const promise = 859 bindEvents2(window, "message", iframe, "error", window, "error") 860 .then(event => { 861 if (event.source !== iframe.contentWindow) 862 return Promise.reject(new Error('Unexpected event.source')); 863 return event.data; 864 }); 865 866 // Simulate a click event on the SVG anchor 867 const event = new MouseEvent('click', { 868 view: window, 869 bubbles: true, 870 cancelable: true 871 }); 872 svgAnchor.dispatchEvent(event); 873 874 return promise; 875 } 876 877 /** 878 @typedef SubresourceType 879 @type {string} 880 881 Represents how a subresource is sent. 882 The keys of `subresourceMap` below are the valid values. 883 */ 884 885 // Subresource paths and invokers. 886 const subresourceMap = { 887 "a-tag": { 888 path: "/common/security-features/subresource/document.py", 889 invoker: requestViaAnchor, 890 }, 891 "area-tag": { 892 path: "/common/security-features/subresource/document.py", 893 invoker: requestViaArea, 894 }, 895 "audio-tag": { 896 path: "/common/security-features/subresource/audio.py", 897 invoker: requestViaAudio, 898 }, 899 "beacon": { 900 path: "/common/security-features/subresource/empty.py", 901 invoker: requestViaSendBeacon, 902 }, 903 "fetch": { 904 path: "/common/security-features/subresource/xhr.py", 905 invoker: requestViaFetch, 906 }, 907 "form-tag": { 908 path: "/common/security-features/subresource/document.py", 909 invoker: requestViaForm, 910 }, 911 "iframe-tag": { 912 path: "/common/security-features/subresource/document.py", 913 invoker: requestViaIframe, 914 }, 915 "img-tag": { 916 path: "/common/security-features/subresource/image.py", 917 invoker: requestViaImage, 918 }, 919 "link-css-tag": { 920 path: "/common/security-features/subresource/empty.py", 921 invoker: requestViaLinkStylesheet, 922 }, 923 "link-prefetch-tag": { 924 path: "/common/security-features/subresource/empty.py", 925 invoker: requestViaLinkPrefetch, 926 }, 927 "object-tag": { 928 path: "/common/security-features/subresource/empty.py", 929 invoker: requestViaObject, 930 }, 931 "picture-tag": { 932 path: "/common/security-features/subresource/image.py", 933 invoker: requestViaPicture, 934 }, 935 "script-tag": { 936 path: "/common/security-features/subresource/script.py", 937 invoker: requestViaScript, 938 }, 939 "script-tag-dynamic-import": { 940 path: "/common/security-features/subresource/script.py", 941 invoker: requestViaDynamicImport, 942 }, 943 "svg-a-tag": { 944 path: "/common/security-features/subresource/document.py", 945 invoker: requestViaSVGAnchor, 946 }, 947 "video-tag": { 948 path: "/common/security-features/subresource/video.py", 949 invoker: requestViaVideo, 950 }, 951 "xhr": { 952 path: "/common/security-features/subresource/xhr.py", 953 invoker: requestViaXhr, 954 }, 955 956 "worker-classic": { 957 path: "/common/security-features/subresource/worker.py", 958 invoker: url => requestViaDedicatedWorker(url), 959 }, 960 "worker-module": { 961 path: "/common/security-features/subresource/worker.py", 962 invoker: url => requestViaDedicatedWorker(url, {type: "module"}), 963 }, 964 "worker-import": { 965 path: "/common/security-features/subresource/worker.py", 966 invoker: (url, additionalAttributes) => 967 requestViaDedicatedWorker(workerUrlThatImports(url, additionalAttributes), {type: "module"}), 968 }, 969 "worker-import-data": { 970 path: "/common/security-features/subresource/worker.py", 971 invoker: url => 972 requestViaDedicatedWorker(workerDataUrlThatImports(url), {type: "module"}), 973 }, 974 "sharedworker-classic": { 975 path: "/common/security-features/subresource/shared-worker.py", 976 invoker: url => requestViaSharedWorker(url), 977 }, 978 "sharedworker-module": { 979 path: "/common/security-features/subresource/shared-worker.py", 980 invoker: url => requestViaSharedWorker(url, {type: "module"}), 981 }, 982 "sharedworker-import": { 983 path: "/common/security-features/subresource/shared-worker.py", 984 invoker: (url, additionalAttributes) => 985 requestViaSharedWorker(workerUrlThatImports(url, additionalAttributes), {type: "module"}), 986 }, 987 "sharedworker-import-data": { 988 path: "/common/security-features/subresource/shared-worker.py", 989 invoker: url => 990 requestViaSharedWorker(workerDataUrlThatImports(url), {type: "module"}), 991 }, 992 993 "websocket": { 994 path: "/stash_responder", 995 invoker: requestViaWebSocket, 996 }, 997 }; 998 for (const workletType of ['animation', 'audio', 'layout', 'paint']) { 999 subresourceMap[`worklet-${workletType}`] = { 1000 path: "/common/security-features/subresource/worker.py", 1001 invoker: url => requestViaWorklet(workletType, url) 1002 }; 1003 subresourceMap[`worklet-${workletType}-import-data`] = { 1004 path: "/common/security-features/subresource/worker.py", 1005 invoker: url => 1006 requestViaWorklet(workletType, workerDataUrlThatImports(url)) 1007 }; 1008 } 1009 1010 /** 1011 @typedef RedirectionType 1012 @type {string} 1013 1014 Represents what redirects should occur to the subresource request 1015 after initial request. 1016 See preprocess_redirection() in 1017 /common/security-features/subresource/subresource.py for valid values. 1018 */ 1019 1020 /** 1021 Construct subresource (and related) origin. 1022 1023 @param {string} originType 1024 @returns {object} the origin of the subresource. 1025 */ 1026 function getSubresourceOrigin(originType) { 1027 const httpProtocol = "http"; 1028 const httpsProtocol = "https"; 1029 const wsProtocol = "ws"; 1030 const wssProtocol = "wss"; 1031 1032 const sameOriginHost = "{{host}}"; 1033 const crossOriginHost = "{{domains[www1]}}"; 1034 1035 // These values can evaluate to either empty strings or a ":port" string. 1036 const httpPort = getNormalizedPort(parseInt("{{ports[http][0]}}", 10)); 1037 const httpsRawPort = parseInt("{{ports[https][0]}}", 10); 1038 const httpsPort = getNormalizedPort(httpsRawPort); 1039 const wsPort = getNormalizedPort(parseInt("{{ports[ws][0]}}", 10)); 1040 const wssRawPort = parseInt("{{ports[wss][0]}}", 10); 1041 const wssPort = getNormalizedPort(wssRawPort); 1042 1043 /** 1044 @typedef OriginType 1045 @type {string} 1046 1047 Represents the origin of the subresource request URL. 1048 The keys of `originMap` below are the valid values. 1049 1050 Note that there can be redirects from the specified origin 1051 (see RedirectionType), and thus the origin of the subresource 1052 response URL might be different from what is specified by OriginType. 1053 */ 1054 const originMap = { 1055 "same-https": httpsProtocol + "://" + sameOriginHost + httpsPort, 1056 "same-http": httpProtocol + "://" + sameOriginHost + httpPort, 1057 "cross-https": httpsProtocol + "://" + crossOriginHost + httpsPort, 1058 "cross-http": httpProtocol + "://" + crossOriginHost + httpPort, 1059 "same-wss": wssProtocol + "://" + sameOriginHost + wssPort, 1060 "same-ws": wsProtocol + "://" + sameOriginHost + wsPort, 1061 "cross-wss": wssProtocol + "://" + crossOriginHost + wssPort, 1062 "cross-ws": wsProtocol + "://" + crossOriginHost + wsPort, 1063 1064 // The following origin types are used for upgrade-insecure-requests tests: 1065 // These rely on some unintuitive cleverness due to WPT's test setup: 1066 // 'Upgrade-Insecure-Requests' does not upgrade the port number, 1067 // so we use URLs in the form `http://[domain]:[https-port]`, 1068 // which will be upgraded to `https://[domain]:[https-port]`. 1069 // If the upgrade fails, the load will fail, as we don't serve HTTP over 1070 // the secure port. 1071 "same-http-downgrade": 1072 httpProtocol + "://" + sameOriginHost + ":" + httpsRawPort, 1073 "cross-http-downgrade": 1074 httpProtocol + "://" + crossOriginHost + ":" + httpsRawPort, 1075 "same-ws-downgrade": 1076 wsProtocol + "://" + sameOriginHost + ":" + wssRawPort, 1077 "cross-ws-downgrade": 1078 wsProtocol + "://" + crossOriginHost + ":" + wssRawPort, 1079 }; 1080 1081 return originMap[originType]; 1082 } 1083 1084 /** 1085 Construct subresource (and related) URLs. 1086 1087 @param {SubresourceType} subresourceType 1088 @param {OriginType} originType 1089 @param {RedirectionType} redirectionType 1090 @returns {object} with following properties: 1091 {string} testUrl 1092 The subresource request URL. 1093 {string} announceUrl 1094 {string} assertUrl 1095 The URLs to be used for detecting whether `testUrl` is actually sent 1096 to the server. 1097 1. Fetch `announceUrl` first, 1098 2. then possibly fetch `testUrl`, and 1099 3. finally fetch `assertUrl`. 1100 The fetch result of `assertUrl` should indicate whether 1101 `testUrl` is actually sent to the server or not. 1102 */ 1103 function getRequestURLs(subresourceType, originType, redirectionType) { 1104 const key = guid(); 1105 const value = guid(); 1106 1107 // We use the same stash path for both HTTP/S and WS/S stash requests. 1108 const stashPath = encodeURIComponent("/mixed-content"); 1109 1110 const stashEndpoint = "/common/security-features/subresource/xhr.py?key=" + 1111 key + "&path=" + stashPath; 1112 return { 1113 testUrl: 1114 getSubresourceOrigin(originType) + 1115 subresourceMap[subresourceType].path + 1116 "?redirection=" + encodeURIComponent(redirectionType) + 1117 "&action=purge&key=" + key + 1118 "&path=" + stashPath, 1119 announceUrl: stashEndpoint + "&action=put&value=" + value, 1120 assertUrl: stashEndpoint + "&action=take", 1121 }; 1122 } 1123 1124 // =============================================================== 1125 // Source Context 1126 // =============================================================== 1127 // Requests can be sent from several source contexts, 1128 // such as the main documents, iframes, workers, or so, 1129 // possibly nested, and possibly with <meta>/http headers added. 1130 // invokeRequest() and invokeFrom*() functions handles 1131 // SourceContext-related setup in client-side. 1132 1133 /** 1134 invokeRequest() invokes a subresource request 1135 (specified as `subresource`) 1136 from a (possibly nested) environment settings object 1137 (specified as `sourceContextList`). 1138 1139 For nested contexts, invokeRequest() calls an invokeFrom*() function 1140 that creates a nested environment settings object using 1141 /common/security-features/scope/, which calls invokeRequest() 1142 again inside the nested environment settings object. 1143 This cycle continues until all specified 1144 nested environment settings object are created, and 1145 finally invokeRequest() calls a requestVia*() function to start the 1146 subresource request from the inner-most environment settings object. 1147 1148 @param {Subresource} subresource 1149 @param {Array<SourceContext>} sourceContextList 1150 1151 @returns {Promise} A promise that is resolved with an RequestResult object. 1152 `sourceContextUrl` is always set. For whether other properties are set, 1153 see the comments for requestVia*() above. 1154 */ 1155 function invokeRequest(subresource, sourceContextList) { 1156 if (sourceContextList.length === 0) { 1157 // No further nested global objects. Send the subresource request here. 1158 1159 const additionalAttributes = {}; 1160 /** @type {PolicyDelivery} policyDelivery */ 1161 for (const policyDelivery of (subresource.policyDeliveries || [])) { 1162 // Depending on the delivery method, extend the subresource element with 1163 // these attributes. 1164 if (policyDelivery.deliveryType === "attr") { 1165 additionalAttributes[policyDelivery.key] = policyDelivery.value; 1166 } else if (policyDelivery.deliveryType === "rel-noref") { 1167 additionalAttributes["rel"] = "noreferrer"; 1168 } else if (policyDelivery.deliveryType === "http-rp") { 1169 additionalAttributes[policyDelivery.key] = policyDelivery.value; 1170 } else if (policyDelivery.deliveryType === "meta") { 1171 additionalAttributes[policyDelivery.key] = policyDelivery.value; 1172 } 1173 } 1174 1175 return subresourceMap[subresource.subresourceType].invoker( 1176 subresource.url, 1177 additionalAttributes) 1178 .then(result => Object.assign( 1179 {sourceContextUrl: location.toString()}, 1180 result)); 1181 } 1182 1183 // Defines invokers for each valid SourceContext.sourceContextType. 1184 const sourceContextMap = { 1185 "srcdoc": { // <iframe srcdoc></iframe> 1186 invoker: invokeFromIframe, 1187 }, 1188 "iframe": { // <iframe src="same-origin-URL"></iframe> 1189 invoker: invokeFromIframe, 1190 }, 1191 "iframe-blank": { // <iframe></iframe> 1192 invoker: invokeFromIframe, 1193 }, 1194 "worker-classic": { 1195 // Classic dedicated worker loaded from same-origin. 1196 invoker: invokeFromWorker.bind(undefined, "worker", false, {}), 1197 }, 1198 "worker-classic-data": { 1199 // Classic dedicated worker loaded from data: URL. 1200 invoker: invokeFromWorker.bind(undefined, "worker", true, {}), 1201 }, 1202 "worker-module": { 1203 // Module dedicated worker loaded from same-origin. 1204 invoker: invokeFromWorker.bind(undefined, "worker", false, {type: 'module'}), 1205 }, 1206 "worker-module-data": { 1207 // Module dedicated worker loaded from data: URL. 1208 invoker: invokeFromWorker.bind(undefined, "worker", true, {type: 'module'}), 1209 }, 1210 "sharedworker-classic": { 1211 // Classic shared worker loaded from same-origin. 1212 invoker: invokeFromWorker.bind(undefined, "sharedworker", false, {}), 1213 }, 1214 "sharedworker-classic-data": { 1215 // Classic shared worker loaded from data: URL. 1216 invoker: invokeFromWorker.bind(undefined, "sharedworker", true, {}), 1217 }, 1218 "sharedworker-module": { 1219 // Module shared worker loaded from same-origin. 1220 invoker: invokeFromWorker.bind(undefined, "sharedworker", false, {type: 'module'}), 1221 }, 1222 "sharedworker-module-data": { 1223 // Module shared worker loaded from data: URL. 1224 invoker: invokeFromWorker.bind(undefined, "sharedworker", true, {type: 'module'}), 1225 }, 1226 }; 1227 1228 return sourceContextMap[sourceContextList[0].sourceContextType].invoker( 1229 subresource, sourceContextList); 1230 } 1231 1232 // Quick hack to expose invokeRequest when common.sub.js is loaded either 1233 // as a classic or module script. 1234 self.invokeRequest = invokeRequest; 1235 1236 /** 1237 invokeFrom*() functions are helper functions with the same parameters 1238 and return values as invokeRequest(), that are tied to specific types 1239 of top-most environment settings objects. 1240 For example, invokeFromIframe() is the helper function for the cases where 1241 sourceContextList[0] is an iframe. 1242 */ 1243 1244 /** 1245 @param {string} workerType 1246 "worker" (for dedicated worker) or "sharedworker". 1247 @param {boolean} isDataUrl 1248 true if the worker script is loaded from data: URL. 1249 Otherwise, the script is loaded from same-origin. 1250 @param {object} workerOptions 1251 The `options` argument for Worker constructor. 1252 1253 Other parameters and return values are the same as those of invokeRequest(). 1254 */ 1255 function invokeFromWorker(workerType, isDataUrl, workerOptions, 1256 subresource, sourceContextList) { 1257 const currentSourceContext = sourceContextList[0]; 1258 let workerUrl = 1259 "/common/security-features/scope/worker.py?policyDeliveries=" + 1260 encodeURIComponent(JSON.stringify( 1261 currentSourceContext.policyDeliveries || [])); 1262 if (workerOptions.type === 'module') { 1263 workerUrl += "&type=module"; 1264 } 1265 1266 let promise; 1267 if (isDataUrl) { 1268 promise = fetch(workerUrl) 1269 .then(r => r.text()) 1270 .then(source => { 1271 return 'data:text/javascript;base64,' + btoa(source); 1272 }); 1273 } else { 1274 promise = Promise.resolve(workerUrl); 1275 } 1276 1277 return promise 1278 .then(url => { 1279 if (workerType === "worker") { 1280 const worker = new Worker(url, workerOptions); 1281 worker.postMessage({subresource: subresource, 1282 sourceContextList: sourceContextList.slice(1)}); 1283 return bindEvents2(worker, "message", worker, "error", window, "error"); 1284 } else if (workerType === "sharedworker") { 1285 const worker = new SharedWorker(url, workerOptions); 1286 worker.port.start(); 1287 worker.port.postMessage({subresource: subresource, 1288 sourceContextList: sourceContextList.slice(1)}); 1289 return bindEvents2(worker.port, "message", worker, "error", window, "error"); 1290 } else { 1291 throw new Error('Invalid worker type: ' + workerType); 1292 } 1293 }) 1294 .then(event => { 1295 if (event.data.error) 1296 return Promise.reject(event.data.error); 1297 return event.data; 1298 }); 1299 } 1300 1301 function invokeFromIframe(subresource, sourceContextList) { 1302 const currentSourceContext = sourceContextList[0]; 1303 const frameUrl = 1304 "/common/security-features/scope/document.py?policyDeliveries=" + 1305 encodeURIComponent(JSON.stringify( 1306 currentSourceContext.policyDeliveries || [])); 1307 1308 let iframe; 1309 let promise; 1310 if (currentSourceContext.sourceContextType === 'srcdoc') { 1311 promise = fetch(frameUrl) 1312 .then(r => r.text()) 1313 .then(srcdoc => { 1314 iframe = createElement( 1315 "iframe", {srcdoc: srcdoc}, document.body, true); 1316 return iframe.eventPromise; 1317 }); 1318 } else if (currentSourceContext.sourceContextType === 'iframe') { 1319 iframe = createElement("iframe", {src: frameUrl}, document.body, true); 1320 promise = iframe.eventPromise; 1321 } else if (currentSourceContext.sourceContextType === 'iframe-blank') { 1322 let frameContent; 1323 promise = fetch(frameUrl) 1324 .then(r => r.text()) 1325 .then(t => { 1326 frameContent = t; 1327 iframe = createElement("iframe", {}, document.body, true); 1328 return iframe.eventPromise; 1329 }) 1330 .then(() => { 1331 // Reinitialize `iframe.eventPromise` with a new promise 1332 // that catches the load event for the document.write() below. 1333 bindEvents(iframe); 1334 1335 iframe.contentDocument.write(frameContent); 1336 iframe.contentDocument.close(); 1337 return iframe.eventPromise; 1338 }); 1339 } 1340 1341 return promise 1342 .then(() => { 1343 const promise = bindEvents2( 1344 window, "message", iframe, "error", window, "error"); 1345 iframe.contentWindow.postMessage( 1346 {subresource: subresource, 1347 sourceContextList: sourceContextList.slice(1)}, 1348 "*"); 1349 return promise; 1350 }) 1351 .then(event => { 1352 if (event.data.error) 1353 return Promise.reject(event.data.error); 1354 return event.data; 1355 }); 1356 } 1357 1358 // SanityChecker does nothing in release mode. See sanity-checker.js for debug 1359 // mode. 1360 function SanityChecker() {} 1361 SanityChecker.prototype.checkScenario = function() {}; 1362 SanityChecker.prototype.setFailTimeout = function(test, timeout) {}; 1363 SanityChecker.prototype.checkSubresourceResult = function() {};