tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

utils.js (26582B)


      1 const STORE_URL = '/fenced-frame/resources/key-value-store.py';
      2 const BEACON_URL = '/fenced-frame/resources/beacon-store.py';
      3 const REMOTE_EXECUTOR_URL = '/fenced-frame/resources/remote-context-executor.https.html';
      4 
      5 // If your test needs to modify FLEDGE bidding or decision logic, you should
      6 // update the generated JS in the corresponding handler below.
      7 const FLEDGE_BIDDING_URL = '/fenced-frame/resources/fledge-bidding-logic.py';
      8 const FLEDGE_DECISION_URL = '/fenced-frame/resources/fledge-decision-logic.py';
      9 
     10 // Creates a URL that includes a list of stash key UUIDs that are being used
     11 // in the test. This allows us to generate UUIDs on the fly and let anything
     12 // (iframes, fenced frames, pop-ups, etc...) that wouldn't have access to the
     13 // original UUID variable know what the UUIDs are.
     14 // @param {string} href - The base url of the page being navigated to
     15 // @param {string list} keylist - The list of key UUIDs to be used. Note that
     16 //                                order matters when extracting the keys
     17 function generateURL(href, keylist) {
     18  const ret_url = new URL(href, location.href);
     19  ret_url.searchParams.append("keylist", keylist.join(','));
     20  return ret_url;
     21 }
     22 
     23 function getRemoteContextURL(origin) {
     24  return new URL(REMOTE_EXECUTOR_URL, origin);
     25 }
     26 
     27 async function runSelectRawURL(
     28    href, resolve_to_config = false, register_beacon = false) {
     29  try {
     30    await sharedStorage.worklet.addModule(
     31      "/shared-storage/resources/simple-module.js");
     32  } catch (e) {
     33    // Shared Storage needs to have a module added before we can operate on it.
     34    // It is generated on the fly with this call, and since there's no way to
     35    // tell through the API if a module already exists, wrap the addModule call
     36    // in a try/catch so that if it runs a second time in a test, it will
     37    // gracefully fail rather than bring the whole test down.
     38  }
     39  let operation = {url: href};
     40  if (register_beacon) {
     41    operation.reportingMetadata = {
     42      'reserved.top_navigation_start':
     43          BEACON_URL + '?type=reserved.top_navigation_start',
     44      'reserved.top_navigation_commit':
     45          BEACON_URL + '?type=reserved.top_navigation_commit',
     46    };
     47  }
     48  return await sharedStorage.selectURL(
     49      'test-url-selection-operation', [operation], {
     50        data: {'mockResult': 0},
     51        resolveToConfig: resolve_to_config,
     52        keepAlive: true,
     53      });
     54 }
     55 
     56 // Similar to generateURL, but creates
     57 // 1. An urn:uuid if `resolve_to_config` is false.
     58 // 2. A fenced frame config object if `resolve_to_config` is true.
     59 // This relies on a mock Shared Storage auction, since it is the simplest
     60 // WP-exposed way to turn a url into an urn:uuid or a fenced frame config.
     61 // Note: this function, unlike generateURL, is asynchronous and needs to be
     62 // called with an await operator.
     63 // @param {string} href - The base url of the page being navigated to
     64 // @param {string list} keylist - The list of key UUIDs to be used. Note that
     65 //                                order matters when extracting the keys
     66 // @param {boolean} [resolve_to_config = false] - Determines whether the result
     67 //                                                of `sharedStorage.selectURL()`
     68 //                                                is an urn:uuid or a fenced
     69 //                                                frame config.
     70 // Note:
     71 // 1. There is a limit of 3 calls per origin per pageload for
     72 // `sharedStorage.selectURL()`, so `runSelectURL()` must also respect this
     73 // limit.
     74 // 2. If `resolve_to_config` is true, blink feature `FencedFramesAPIChanges`
     75 // needs to be enabled for `selectURL()` to return a fenced frame config.
     76 // Otherwise `selectURL()` will fall back to the old behavior that returns an
     77 // urn:uuid.
     78 async function runSelectURL(
     79    href, keylist = [], resolve_to_config = false, register_beacon = false) {
     80  const full_url = generateURL(href, keylist);
     81  return await runSelectRawURL(full_url, resolve_to_config, register_beacon);
     82 }
     83 
     84 async function generateURNFromFledgeRawURL(
     85    href, nested_urls, resolve_to_config = false, ad_with_size = false,
     86    requested_size = null, register_beacon = false) {
     87  const bidding_token = token();
     88  const seller_token = token();
     89 
     90  const ad_components_list = nested_urls.map((url) => {
     91    return ad_with_size ?
     92      { renderURL: url, sizeGroup: "group1" } :
     93      { renderURL: url }
     94  });
     95 
     96  let interestGroup = {
     97    name: 'testAd1',
     98    owner: location.origin,
     99    biddingLogicURL: new URL(FLEDGE_BIDDING_URL, location.origin),
    100    ads: [{
    101      renderURL: href,
    102      bid: 1,
    103      allowedReportingOrigins: [location.origin],
    104    }],
    105    userBiddingSignals: {biddingToken: bidding_token},
    106    trustedBiddingSignalsKeys: ['key1'],
    107    adComponents: ad_components_list,
    108  };
    109 
    110  let biddingURLParams =
    111      new URLSearchParams(interestGroup.biddingLogicURL.search);
    112  if (requested_size)
    113    biddingURLParams.set(
    114        'requested-size', requested_size[0] + '-' + requested_size[1]);
    115  if (ad_with_size)
    116    biddingURLParams.set('ad-with-size', 1);
    117  if (register_beacon)
    118    biddingURLParams.set('beacon', 1);
    119  interestGroup.biddingLogicURL.search = biddingURLParams;
    120 
    121  if (ad_with_size) {
    122    interestGroup.ads[0].sizeGroup = 'group1';
    123    interestGroup.adSizes = {'size1': {width: '100px', height: '50px'}};
    124    interestGroup.sizeGroups = {'group1': ['size1']};
    125  }
    126 
    127  // Pick an arbitrarily high duration to guarantee that we never leave the
    128  // ad interest group while the test runs.
    129  navigator.joinAdInterestGroup(interestGroup, /*durationSeconds=*/3000000);
    130 
    131  let auctionConfig = {
    132    seller: location.origin,
    133    interestGroupBuyers: [location.origin],
    134    decisionLogicURL: new URL(FLEDGE_DECISION_URL, location.origin),
    135    auctionSignals: {biddingToken: bidding_token, sellerToken: seller_token},
    136    resolveToConfig: resolve_to_config
    137  };
    138 
    139  if (requested_size) {
    140    let decisionURLParams =
    141      new URLSearchParams(auctionConfig.decisionLogicURL.search);
    142    decisionURLParams.set(
    143        'requested-size', requested_size[0] + '-' + requested_size[1]);
    144    auctionConfig.decisionLogicURL.search = decisionURLParams;
    145 
    146    auctionConfig['requestedSize'] = {width: requested_size[0], height: requested_size[1]};
    147  }
    148 
    149  return navigator.runAdAuction(auctionConfig);
    150 }
    151 
    152 // Similar to runSelectURL, but uses FLEDGE instead of Shared Storage as the
    153 // auctioning tool.
    154 // Note: this function, unlike generateURL, is asynchronous and needs to be
    155 // called with an await operator. @param {string} href - The base url of the
    156 // page being navigated to @param {string list} keylist - The list of key UUIDs
    157 // to be used. Note that order matters when extracting the keys
    158 // @param {string} href - The base url of the page being navigated to
    159 // @param {string list} keylist - The list of key UUIDs to be used. Note that
    160 //                                order matters when extracting the keys
    161 // @param {string list} nested_urls - A list of urls that will eventually become
    162 //                                    the nested configs/ad components
    163 // @param {boolean} [resolve_to_config = false] - Determines whether the result
    164 //                                                of `navigator.runAdAuction()`
    165 //                                                is an urn:uuid or a fenced
    166 //                                                frame config.
    167 // @param {boolean} [ad_with_size = false] - Determines whether the auction is
    168 //                                           run with ad sizes specified.
    169 // @param {boolean} [register_beacon = false] - If true, FLEDGE logic will
    170 //                                              register reporting beacons after
    171 //                                              completion.
    172 async function generateURNFromFledge(
    173    href, keylist, nested_urls = [], resolve_to_config = false,
    174    ad_with_size = false, requested_size = null, register_beacon = false) {
    175  const full_url = generateURL(href, keylist);
    176  return generateURNFromFledgeRawURL(
    177      full_url, nested_urls, resolve_to_config, ad_with_size, requested_size,
    178      register_beacon);
    179 }
    180 
    181 // Extracts a list of UUIDs from the from the current page's URL.
    182 // @returns {string list} - The list of UUIDs extracted from the page. This can
    183 //                          be read into multiple variables using the
    184 //                          [key1, key2, etc...] = parseKeyList(); pattern.
    185 function parseKeylist() {
    186  const url = new URL(location.href);
    187  const keylist = url.searchParams.get("keylist");
    188  return keylist.split(',');
    189 }
    190 
    191 // Converts a same-origin URL to a cross-origin URL
    192 // @param {URL} url - The URL object whose origin is being converted
    193 // @param {boolean} [https=true] - Whether or not to use the HTTPS origin
    194 //
    195 // @returns {URL} The new cross-origin URL
    196 function getRemoteOriginURL(url, https=true) {
    197  const same_origin = location.origin;
    198  const cross_origin = https ? get_host_info().HTTPS_REMOTE_ORIGIN
    199      : get_host_info().HTTP_REMOTE_ORIGIN;
    200  return new URL(url.toString().replace(same_origin, cross_origin));
    201 }
    202 
    203 // Builds a URL to be used as a remote context executor.
    204 function generateRemoteContextURL(headers, origin) {
    205  // Generate the unique id for the parent/child channel.
    206  const uuid = token();
    207 
    208  // Use the absolute path of the remote context executor source file, so that
    209  // nested contexts will work.
    210  const url = getRemoteContextURL(origin ? origin : location.origin);
    211  url.searchParams.append('uuid', uuid);
    212 
    213  // Add the header to allow loading in a fenced frame.
    214  headers.push(["Supports-Loading-Mode", "fenced-frame"]);
    215 
    216  // Transform the headers into the expected format.
    217  // https://web-platform-tests.org/writing-tests/server-pipes.html#headers
    218  function escape(s) {
    219    return s.replace('(', '\\(').replace(')', '\\)').replace(',', '\\,');
    220  }
    221  const formatted_headers = headers.map((header) => {
    222    return `header(${escape(header[0])}, ${escape(header[1])})`;
    223  });
    224  url.searchParams.append('pipe', formatted_headers.join('|'));
    225 
    226  return [uuid, url];
    227 }
    228 
    229 function buildRemoteContextForObject(object, uuid, html) {
    230  // https://github.com/web-platform-tests/wpt/blob/master/common/dispatcher/README.md
    231  const context = new RemoteContext(uuid);
    232  if (html) {
    233    context.execute_script(
    234      (html_source) => {
    235        document.body.insertAdjacentHTML('beforebegin', html_source);
    236      },
    237    [html]);
    238  }
    239 
    240  // We need a little bit of boilerplate in the handlers because Proxy doesn't
    241  // work so nicely with HTML elements.
    242  const handler = {
    243    get: (target, key) => {
    244      if (key == "execute") {
    245        return context.execute_script;
    246      }
    247      if (key == "element") {
    248        return object;
    249      }
    250      if (key in target) {
    251        return target[key];
    252      }
    253      return context[key];
    254    },
    255    set: (target, key, value) => {
    256      target[key] = value;
    257      return value;
    258    }
    259  };
    260 
    261  // If `object` is null (e.g. a window created with noopener), set it to a
    262  // dummy value so that the Proxy constructor won't fail.
    263  if (object == null) {
    264    object = {};
    265  }
    266  const proxy = new Proxy(object, handler);
    267  return proxy;
    268 }
    269 
    270 // Attaches an object that waits for scripts to execute from RemoteContext.
    271 // (In practice, this is either a frame or a window.)
    272 // Returns a proxy for the object that first resolves to the object itself,
    273 // then resolves to the RemoteContext if the property isn't found.
    274 // The proxy also has an extra attribute `execute`, which is an alias for the
    275 // remote context's `execute_script(fn, args=[])`.
    276 function attachContext(object_constructor, html, headers, origin) {
    277  const [uuid, url] = generateRemoteContextURL(headers, origin);
    278  const object = object_constructor(url);
    279  return buildRemoteContextForObject(object, uuid, html);
    280 }
    281 
    282 // TODO(crbug.com/1347953): Update this function to also test
    283 // `sharedStorage.selectURL()` that returns a fenced frame config object.
    284 // This should be done after fixing the following flaky tests that use this
    285 // function.
    286 // 1. crbug.com/1372536: resize-lock-input.https.html
    287 // 2. crbug.com/1394559: unfenced-top.https.html
    288 async function attachOpaqueContext(
    289    generator_api, resolve_to_config, ad_with_size, requested_size,
    290    register_beacon, object_constructor, html, headers, origin,
    291    component_origin, num_components) {
    292  const [uuid, url] = generateRemoteContextURL(headers, origin);
    293 
    294  let components_list = [];
    295  for (let i = 0; i < num_components; i++) {
    296    let [component_uuid, component_url] =
    297        generateRemoteContextURL(headers, component_origin);
    298    // This field will be read by attachComponentFrameContext() in order to
    299    // know what uuid to point to when building the remote context.
    300    html += '<input type=\'hidden\' id=\'component_uuid_' + i + '\' value=\'' +
    301        component_uuid + '\'>';
    302    components_list.push(component_url);
    303  }
    304 
    305  const id = await (
    306      generator_api == 'fledge' ?
    307          generateURNFromFledge(
    308              url, [], components_list, resolve_to_config, ad_with_size,
    309              requested_size, register_beacon) :
    310          runSelectURL(url, [], resolve_to_config, register_beacon));
    311  const object = object_constructor(id);
    312  return buildRemoteContextForObject(object, uuid, html);
    313 }
    314 
    315 function attachPotentiallyOpaqueContext(
    316    generator_api, resolve_to_config, ad_with_size, requested_size,
    317    register_beacon, frame_constructor, html, headers, origin,
    318    component_origin, num_components) {
    319  generator_api = generator_api.toLowerCase();
    320  if (generator_api == 'fledge' || generator_api == 'sharedstorage') {
    321    return attachOpaqueContext(
    322        generator_api, resolve_to_config, ad_with_size, requested_size,
    323        register_beacon, frame_constructor, html, headers, origin,
    324        component_origin, num_components);
    325  } else {
    326    return attachContext(frame_constructor, html, headers, origin);
    327  }
    328 }
    329 
    330 function attachFrameContext(
    331    element_name, generator_api, resolve_to_config, ad_with_size,
    332    requested_size, register_beacon, html, headers, attributes, origin,
    333    component_origin, num_components) {
    334  frame_constructor = (id) => {
    335    frame = document.createElement(element_name);
    336    attributes.forEach(attribute => {
    337      frame.setAttribute(attribute[0], attribute[1]);
    338    });
    339    if (element_name == "iframe") {
    340      frame.src = id;
    341    } else if (id instanceof FencedFrameConfig) {
    342      frame.config = id;
    343    } else {
    344      const config = new FencedFrameConfig(id);
    345      frame.config = config;
    346    }
    347    document.body.append(frame);
    348    return frame;
    349  };
    350  return attachPotentiallyOpaqueContext(
    351      generator_api, resolve_to_config, ad_with_size, requested_size,
    352      register_beacon, frame_constructor, html, headers, origin,
    353      component_origin, num_components);
    354 }
    355 
    356 // Performs a content-initiated navigation of a frame proxy. This navigated page
    357 // uses a new urn:uuid as its communication channel to prevent potential clashes
    358 // with the currently loaded document.
    359 async function navigateFrameContext(frame_proxy, {headers = [], origin = ''}) {
    360  const [uuid, url] = generateRemoteContextURL(headers, origin);
    361  frame_proxy.execute((url) => {
    362    window.executor.suspend(() => {
    363      window.location = url;
    364    });
    365  }, [url])
    366  frame_proxy.context_id = uuid;
    367 }
    368 
    369 function replaceFrameContext(frame_proxy, {
    370  generator_api = '',
    371  resolve_to_config = false,
    372  ad_with_size = false,
    373  requested_size = null,
    374  register_beacon = false,
    375  html = '',
    376  headers = [],
    377  origin = ''
    378 } = {}) {
    379  frame_constructor = (id) => {
    380    if (frame_proxy.element.nodeName == "IFRAME") {
    381      frame_proxy.element.src = id;
    382    } else if (id instanceof FencedFrameConfig) {
    383      frame_proxy.element.config = id;
    384    } else {
    385      const config = new FencedFrameConfig(id);
    386      frame_proxy.element.config = config;
    387    }
    388    return frame_proxy.element;
    389  };
    390  return attachPotentiallyOpaqueContext(
    391      generator_api, resolve_to_config, ad_with_size, requested_size,
    392      register_beacon, frame_constructor, html, headers, origin);
    393 }
    394 
    395 // Attach a fenced frame that waits for scripts to execute. Takes as input a(n
    396 // optional) dictionary of configs:
    397 // - generator_api: the name of the API that should generate the urn/config.
    398 //    Supports (case-insensitive) "fledge" and "sharedstorage", or any other
    399 //    value as a default. If you generate a urn, then you need to await the
    400 //    result of this function.
    401 // - resolve_to_config: whether a config should be used. (currently only works
    402 //    for FLEDGE and sharedStorage generator_api)
    403 // - ad_with_size: whether an ad auction is run with size specified for the ads
    404 //    and ad components. (currently only works for FLEDGE)
    405 // - requested_size: A 2-element list with the width and height for
    406 //    requestedSize in the FLEDGE auction config. This is different from
    407 //    ad_with_size, which refers to size information provided alongside the ads
    408 //    themselves.
    409 // - register_beacon: If true and generator_api = "fledge", an automatic beacon
    410 //    and a destination URL reportEvent() beacon will be registered after the
    411 //    FLEDGE auction completes.
    412 // - html: extra HTML source code to inject into the loaded frame
    413 // - headers: an array of header pairs [[key, value], ...]
    414 // - attributes: an array of attribute pairs to set on the frame [[key, value],
    415 //    ...]
    416 // - origin: origin of the url, default to location.origin if not set. Returns a
    417 //   proxy that acts like the frame HTML element, but with an extra function
    418 //   `execute`. See `attachFrameContext` or the README for more details.
    419 function attachFencedFrameContext({
    420  generator_api = '',
    421  resolve_to_config = false,
    422  ad_with_size = false,
    423  requested_size = null,
    424  register_beacon = false,
    425  html = '',
    426  headers = [],
    427  attributes = [],
    428  origin = '',
    429  component_origin = '',
    430  num_components = 0
    431 } = {}) {
    432  return attachFrameContext(
    433      'fencedframe', generator_api, resolve_to_config, ad_with_size,
    434      requested_size, register_beacon, html, headers, attributes, origin,
    435      component_origin, num_components);
    436 }
    437 
    438 // Attach an iframe that waits for scripts to execute.
    439 // See `attachFencedFrameContext` for more details.
    440 function attachIFrameContext({
    441  generator_api = '',
    442  register_beacon = false,
    443  html = '',
    444  headers = [],
    445  attributes = [],
    446  origin = '',
    447  component_origin = '',
    448  num_components = 0
    449 } = {}) {
    450  return attachFrameContext(
    451      'iframe', generator_api, resolve_to_config = false, ad_with_size = false,
    452      requested_size = null, register_beacon, html, headers, attributes, origin,
    453      component_origin, num_components);
    454 }
    455 
    456 // Open a window that waits for scripts to execute.
    457 // Returns a proxy that acts like the window object, but with an extra
    458 // function `execute`. See `attachContext` for more details.
    459 function attachWindowContext({target="_blank", html="", headers=[], origin=""}={}) {
    460  window_constructor = (url) => {
    461    return window.open(url, target);
    462  }
    463 
    464  return attachContext(window_constructor, html, headers, origin);
    465 }
    466 
    467 // Attaches an ad component in a fenced frame. For this to work, this must be
    468 // called in a frame that was generated with attachFrameContext() using the
    469 // Protected Audience API (generator_api: 'fledge').
    470 function attachComponentFencedFrameContext(
    471    index = 0, {attributes = [], html = ''} = {}) {
    472  const urn = window.fence.getNestedConfigs()[index];
    473  return attachComponentFrameContext(
    474      index, 'fencedframe', urn, attributes, html);
    475 }
    476 
    477 // Same as attachComponentFencedFrameContext, but in a urn iframe.
    478 function attachComponentIFrameContext(
    479    index = 0, {attributes = [], html = ''} = {}) {
    480  const urn = navigator.adAuctionComponents(index + 1)[index];
    481  return attachComponentFrameContext(index, 'iframe', urn, attributes, html);
    482 }
    483 
    484 function attachComponentFrameContext(
    485    index, element_name, urn, attributes, html) {
    486  assert_not_equals(
    487      document.getElementById('component_uuid_' + index), null,
    488      'Component frames can only be attached to frames loaded with ' +
    489          'attach*FrameContext() with `num_components` set to at least ' +
    490          (index + 1) + '.');
    491 
    492  let frame = document.createElement(element_name);
    493  attributes.forEach(attribute => {
    494    frame.setAttribute(attribute[0], attribute[1]);
    495  });
    496  if (element_name == 'iframe') {
    497    frame.src = urn;
    498  } else {
    499    frame.config = urn;
    500  }
    501  document.body.append(frame);
    502  const context_uuid = document.getElementById('component_uuid_' + index).value;
    503  return buildRemoteContextForObject(frame, context_uuid, html);
    504 }
    505 
    506 // Converts a key string into a key uuid using a cryptographic hash function.
    507 // This function only works in secure contexts (HTTPS).
    508 async function stringToStashKey(string) {
    509  // Compute a SHA-256 hash of the input string, and convert it to hex.
    510  const data = new TextEncoder().encode(string);
    511  const digest = await crypto.subtle.digest('SHA-256', data);
    512  const digest_array = Array.from(new Uint8Array(digest));
    513  const digest_as_hex = digest_array.map(b => b.toString(16).padStart(2, '0')).join('');
    514 
    515  // UUIDs are structured as 8X-4X-4X-4X-12X.
    516  // Use the first 32 hex digits and ignore the rest.
    517  const digest_slices = [digest_as_hex.slice(0,8),
    518                         digest_as_hex.slice(8,12),
    519                         digest_as_hex.slice(12,16),
    520                         digest_as_hex.slice(16,20),
    521                         digest_as_hex.slice(20,32)];
    522  return digest_slices.join('-');
    523 }
    524 
    525 // Create a fenced frame. Then navigate it using the given `target`, which can
    526 // be either an urn:uuid or a fenced frame config object.
    527 function attachFencedFrame(target) {
    528  if (window.test_driver) {
    529    assert_implements(
    530        window.HTMLFencedFrameElement,
    531        'The HTMLFencedFrameElement should be exposed on the window object');
    532  }
    533 
    534  const fenced_frame = document.createElement('fencedframe');
    535 
    536  if (target instanceof FencedFrameConfig) {
    537    fenced_frame.config = target;
    538  } else {
    539    const config = new FencedFrameConfig(target);
    540    fenced_frame.config = config;
    541  }
    542 
    543  document.body.append(fenced_frame);
    544  return fenced_frame;
    545 }
    546 
    547 function attachIFrame(url) {
    548  const iframe = document.createElement('iframe');
    549  iframe.src = url;
    550  document.body.append(iframe);
    551  return iframe;
    552 }
    553 
    554 // Reads the value specified by `key` from the key-value store on the server.
    555 async function readValueFromServer(key) {
    556  // Resolve the key if it is a Promise.
    557  key = await key;
    558 
    559  const serverURL = `${STORE_URL}?key=${key}`;
    560  const response = await fetch(serverURL);
    561  if (!response.ok)
    562    throw new Error('An error happened in the server');
    563  const value = await response.text();
    564 
    565  // The value is not stored in the server.
    566  if (value === "<Not set>")
    567    return { status: false };
    568 
    569  return { status: true, value: value };
    570 }
    571 
    572 // Convenience wrapper around the above getter that will wait until a value is
    573 // available on the server.
    574 async function nextValueFromServer(key) {
    575  // Resolve the key if it is a Promise.
    576  key = await key;
    577 
    578  while (true) {
    579    // Fetches the test result from the server.
    580    const { status, value } = await readValueFromServer(key);
    581    if (!status) {
    582      // The test result has not been stored yet. Retry after a while.
    583      await new Promise(resolve => setTimeout(resolve, 20));
    584      continue;
    585    }
    586 
    587    return value;
    588  }
    589 }
    590 
    591 // Checks the beacon data server to see if it has received a beacon with a given
    592 // event type and body.
    593 async function readBeaconDataFromServer(event_type, expected_body) {
    594  let serverURL = `${BEACON_URL}`;
    595  const response = await fetch(serverURL + "?" + new URLSearchParams({
    596    type: event_type,
    597    expected_body: expected_body,
    598  }));
    599  if (!response.ok)
    600    throw new Error('An error happened in the server ' + response.status);
    601  const value = await response.text();
    602 
    603  // The value is not stored in the server.
    604  if (value === "<Not set>")
    605    return { status: false };
    606 
    607  return { status: true, value: value };
    608 }
    609 
    610 // Convenience wrapper around the above getter that will wait until a value is
    611 // available on the server. The server uses a hash of the concatenated event
    612 // type and beacon data as the key when storing the beacon in the database. To
    613 // retrieve it, we need to supply the endpoint with both pieces of information.
    614 async function nextBeacon(event_type, expected_body) {
    615  while (true) {
    616    // Fetches the test result from the server.
    617    const {status, value} =
    618        await readBeaconDataFromServer(event_type, expected_body);
    619    if (!status) {
    620      // The test result has not been stored yet. Retry after a while.
    621      await new Promise(resolve => setTimeout(resolve, 20));
    622      continue;
    623    }
    624 
    625    return value;
    626  }
    627 }
    628 
    629 // Writes `value` for `key` in the key-value store on the server.
    630 async function writeValueToServer(key, value, origin = '') {
    631  // Resolve the key if it is a Promise.
    632  key = await key;
    633 
    634  const serverURL = `${origin}${STORE_URL}?key=${key}&value=${value}`;
    635  await fetch(serverURL, {"mode": "no-cors"});
    636 }
    637 
    638 // Fenced frames are always put in the public IP address space which is the
    639 // least privileged. In case a navigation to a local data: URL or blob: URL
    640 // resource is allowed, they would only be able to fetch things that are *also*
    641 // in the public IP address space. So for the document described by these local
    642 // URLs, we'll set them up to only communicate back to the outer page via
    643 // resources obtained in the public address space.
    644 function createLocalSource(key, url) {
    645  return `
    646    <head>
    647      <script src="${url}"><\/script>
    648    </head>
    649    <body>
    650      <script>
    651        writeValueToServer("${key}", "LOADED", /*origin=*/"${url.origin}");
    652      <\/script>
    653    </body>
    654  `;
    655 }
    656 
    657 function setupCSP(csp, second_csp=null) {
    658  let headers = [];
    659 
    660  headers.push(["Content-Security-Policy", "fenced-frame-src " + csp]);
    661  if (second_csp != null) {
    662    headers.push(["Content-Security-Policy", "frame-src " + second_csp]);
    663  }
    664 
    665  const iframe = attachIFrameContext({headers: headers});
    666 
    667  return iframe;
    668 }
    669 
    670 // Clicking in WPT tends to be flaky (https://crbug.com/1066891), so you may
    671 // need to click multiple times to have an effect. This function clicks at
    672 // coordinates `{x, y}` relative to `click_origin`, by default 3 times. Should
    673 // not be used for tests where multiple clicks have distinct impact on the state
    674 // of the page, but rather to bruteforce through flakes that rely on only one
    675 // click.
    676 async function multiClick(x, y, click_origin, times = 3) {
    677  for (let i = 0; i < times; i++) {
    678    let actions = new test_driver.Actions();
    679    await actions.pointerMove(x, y, {origin: click_origin})
    680        .pointerDown()
    681        .pointerUp()
    682        .send();
    683  }
    684 }