tor-browser

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

utils.sub.js (10067B)


      1 /**
      2 * Utilities for initiating prefetch via speculation rules.
      3 */
      4 
      5 // Resolved URL to find this script.
      6 const SR_PREFETCH_UTILS_URL = new URL(document.currentScript.src, document.baseURI);
      7 
      8 // If (and only if) you are writing a test that depends on
      9 // `requires: ["anonymous-client-ip-when-cross-origin"]`, then you must use this
     10 // host as the cross-origin host. (If you need a generic cross-origin host, use
     11 // `get_host_info().NOTSAMESITE_HOST` or similar instead.)
     12 //
     13 // TODO(domenic): document in the web platform tests server infrastructure that
     14 // such a host must exist, and possibly separate it from `{{hosts[alt][]}}`.
     15 const CROSS_ORIGIN_HOST_THAT_WORKS_WITH_ACIWCO = "{{hosts[alt][]}}";
     16 
     17 class PrefetchAgent extends RemoteContext {
     18  constructor(uuid, t) {
     19    super(uuid);
     20    this.t = t;
     21  }
     22 
     23  getExecutorURL(options = {}) {
     24    let {hostname, username, password, protocol, executor, ...extra} = options;
     25    let params = new URLSearchParams({uuid: this.context_id, ...extra});
     26    if(executor === undefined) {
     27      executor = "executor.sub.html";
     28    }
     29    let url = new URL(`${executor}?${params}`, SR_PREFETCH_UTILS_URL);
     30    if(hostname !== undefined) {
     31      url.hostname = hostname;
     32    }
     33    if(username !== undefined) {
     34      url.username = username;
     35    }
     36    if(password !== undefined) {
     37      url.password = password;
     38    }
     39    if(protocol !== undefined) {
     40      url.protocol = protocol;
     41      url.port = protocol === "https" ? "{{ports[https][0]}}" : "{{ports[http][0]}}";
     42    }
     43    return url;
     44  }
     45 
     46  // Requests prefetch via speculation rules.
     47  //
     48  // In the future, this should also use browser hooks to force the prefetch to
     49  // occur despite heuristic matching, etc., and await the completion of the
     50  // prefetch.
     51  async forceSinglePrefetch(url, extra = {}, wait_for_completion = true) {
     52    return this.forceSpeculationRules(
     53      {
     54        prefetch: [{source: 'list', urls: [url], ...extra}]
     55      }, wait_for_completion);
     56  }
     57 
     58  async forceSpeculationRules(rules, wait_for_completion = true) {
     59    await this.execute_script((rules) => {
     60      insertSpeculationRules(rules);
     61    }, [rules]);
     62    if (!wait_for_completion) {
     63      return Promise.resolve();
     64    }
     65    return new Promise(resolve => this.t.step_timeout(resolve, 2000));
     66  }
     67 
     68  // `url` is the URL to navigate.
     69  //
     70  // `expectedDestinationUrl` is the expected URL after navigation.
     71  // When omitted, `url` is used. When explicitly null, the destination URL is
     72  // not validated.
     73  async navigate(url, {expectedDestinationUrl} = {}) {
     74    await this.execute_script((url) => {
     75      window.executor.suspend(() => {
     76        location.href = url;
     77      });
     78    }, [url]);
     79    if (expectedDestinationUrl === undefined) {
     80      expectedDestinationUrl = url;
     81    }
     82    if (expectedDestinationUrl) {
     83      expectedDestinationUrl.username = '';
     84      expectedDestinationUrl.password = '';
     85      assert_equals(
     86          await this.execute_script(() => location.href),
     87          expectedDestinationUrl.toString(),
     88          "expected navigation to reach destination URL");
     89    }
     90    await this.execute_script(() => {});
     91  }
     92 
     93  async getRequestHeaders() {
     94    return this.execute_script(() => requestHeaders);
     95  }
     96 
     97  async getResponseCookies() {
     98    return this.execute_script(() => {
     99      let cookie = {};
    100      document.cookie.split(/\s*;\s*/).forEach((kv)=>{
    101        let [key, value] = kv.split(/\s*=\s*/);
    102        cookie[key] = value;
    103      });
    104      return cookie;
    105    });
    106  }
    107 
    108  async getRequestCookies() {
    109    return this.execute_script(() => window.requestCookies);
    110  }
    111 
    112  async getRequestCredentials() {
    113    return this.execute_script(() => window.requestCredentials);
    114  }
    115 
    116  async setReferrerPolicy(referrerPolicy) {
    117    return this.execute_script(referrerPolicy => {
    118      const meta = document.createElement("meta");
    119      meta.name = "referrer";
    120      meta.content = referrerPolicy;
    121      document.head.append(meta);
    122    }, [referrerPolicy]);
    123  }
    124 
    125  async getDeliveryType(){
    126    return this.execute_script(() => {
    127      return performance.getEntriesByType("navigation")[0].deliveryType;
    128    });
    129  }
    130 }
    131 
    132 // Produces a URL with a UUID which will record when it's prefetched.
    133 // |extra_params| can be specified to add extra search params to the generated
    134 // URL.
    135 function getPrefetchUrl(extra_params={}) {
    136  let params = new URLSearchParams({ uuid: token(), ...extra_params });
    137  return new URL(`prefetch.py?${params}`, SR_PREFETCH_UTILS_URL);
    138 }
    139 
    140 // Produces n URLs with unique UUIDs which will record when they are prefetched.
    141 function getPrefetchUrlList(n) {
    142  return Array.from({ length: n }, () => getPrefetchUrl());
    143 }
    144 
    145 async function isUrlPrefetched(url) {
    146  let response = await fetch(url, {redirect: 'follow'});
    147  assert_true(response.ok);
    148  return response.json();
    149 }
    150 
    151 // Must also include /common/utils.js and /common/dispatcher/dispatcher.js to use this.
    152 async function spawnWindowWithReference(t, options = {}, uuid = token()) {
    153  let agent = new PrefetchAgent(uuid, t);
    154  let w = window.open(agent.getExecutorURL(options), '_blank', options);
    155  t.add_cleanup(() => w.close());
    156  return {"agent":agent, "window":w};
    157 }
    158 
    159 // Must also include /common/utils.js and /common/dispatcher/dispatcher.js to use this.
    160 async function spawnWindow(t, options = {}, uuid = token()) {
    161  let agent_window_pair = await spawnWindowWithReference(t, options, uuid);
    162  return agent_window_pair.agent;
    163 }
    164 
    165 function insertSpeculationRules(body) {
    166  let script = document.createElement('script');
    167  script.type = 'speculationrules';
    168  script.textContent = JSON.stringify(body);
    169  document.head.appendChild(script);
    170 }
    171 
    172 // Creates and appends <a href=|href|> to |insertion point|. If
    173 // |insertion_point| is not specified, document.body is used.
    174 function addLink(href, insertion_point=document.body) {
    175  const a = document.createElement('a');
    176  a.href = href;
    177  insertion_point.appendChild(a);
    178  return a;
    179 }
    180 
    181 // Inserts a prefetch document rule with |predicate|. |predicate| can be
    182 // undefined, in which case the default predicate will be used (i.e. all links
    183 // in document will match).
    184 function insertDocumentRule(predicate, extra_options={}) {
    185  insertSpeculationRules({
    186    prefetch: [{
    187      source: 'document',
    188      eagerness: 'immediate',
    189      where: predicate,
    190      ...extra_options
    191    }]
    192  });
    193 }
    194 
    195 function assert_prefetched (requestHeaders, description) {
    196  assert_in_array(requestHeaders.purpose, [undefined, "prefetch"], "The vendor-specific header Purpose, if present, must be 'prefetch'.");
    197  assert_in_array(requestHeaders['sec-purpose'],
    198                  ["prefetch", "prefetch;anonymous-client-ip"], description);
    199 }
    200 
    201 function assert_prefetched_anonymous_client_ip(requestHeaders, description) {
    202  assert_in_array(requestHeaders.purpose, [undefined, "prefetch"], "The vendor-specific header Purpose, if present, must be 'prefetch'.");
    203  assert_equals(requestHeaders['sec-purpose'],
    204                "prefetch;anonymous-client-ip",
    205                description);
    206 }
    207 
    208 function assert_not_prefetched (requestHeaders, description){
    209  assert_equals(requestHeaders.purpose, undefined, description);
    210  assert_equals(requestHeaders['sec-purpose'], undefined, description);
    211 }
    212 
    213 // If the prefetch request is intercepted and modified by ServiceWorker,
    214 // - "Sec-Purpose: prefetch" header is dropped in Step 33 of
    215 //   https://fetch.spec.whatwg.org/#dom-request
    216 //   because it's a https://fetch.spec.whatwg.org/#forbidden-request-header.
    217 // - "Purpose: prefetch" can still be sent.
    218 // Note that this check passes also for non-prefetch requests, so additional
    219 // checks are needed to distinguish from non-prefetch requests.
    220 function assert_prefetched_without_sec_purpose(requestHeaders, description) {
    221  assert_in_array(requestHeaders.purpose, [undefined, "prefetch"],
    222      "The vendor-specific header Purpose, if present, must be 'prefetch'.");
    223  assert_equals(requestHeaders['sec-purpose'], undefined, description);
    224 }
    225 
    226 // For ServiceWorker tests.
    227 // `interceptedRequest` is an element of `interceptedRequests` in
    228 // `resources/basic-service-worker.js`.
    229 
    230 // The ServiceWorker fetch handler intercepted a prefetching request.
    231 function assert_intercept_prefetch(interceptedRequest, expectedUrl) {
    232  assert_equals(interceptedRequest.request.url, expectedUrl.toString(),
    233      "intercepted request URL.");
    234 
    235  assert_prefetched(interceptedRequest.request.headers,
    236      "Prefetch request should be intercepted.");
    237 
    238  if (new URL(location.href).searchParams.has('clientId')) {
    239    // https://github.com/WICG/nav-speculation/issues/346
    240    // https://crbug.com/404294123
    241    assert_equals(interceptedRequest.resultingClientId, "",
    242        "resultingClientId shouldn't be exposed.");
    243 
    244    // https://crbug.com/404286918
    245    // `assert_not_equals()` isn't used for now to create stable failure diffs.
    246    assert_false(interceptedRequest.clientId === "",
    247        "clientId should be initiator.");
    248  }
    249 }
    250 
    251 // The ServiceWorker fetch handler intercepted a non-prefetching request.
    252 function assert_intercept_non_prefetch(interceptedRequest, expectedUrl) {
    253  assert_equals(interceptedRequest.request.url, expectedUrl.toString(),
    254      "intercepted request URL.");
    255 
    256  assert_not_prefetched(interceptedRequest.request.headers,
    257      "Non-prefetch request should be intercepted.");
    258 
    259  if (new URL(location.href).searchParams.has('clientId')) {
    260    // Because this is an ordinal non-prefetch request, `resultingClientId`
    261    // can be set as normal.
    262    assert_not_equals(interceptedRequest.resultingClientId, "",
    263        "resultingClientId can be exposed.");
    264 
    265    assert_not_equals(interceptedRequest.clientId, "",
    266        "clientId should be initiator.");
    267  }
    268 }
    269 
    270 function assert_served_by_navigation_preload(requestHeaders) {
    271  assert_equals(
    272    requestHeaders['service-worker-navigation-preload'],
    273    'true',
    274    'Service-Worker-Navigation-Preload');
    275 }
    276 
    277 // Use nvs_header query parameter to ask the wpt server
    278 // to populate No-Vary-Search response header.
    279 function addNoVarySearchHeaderUsingQueryParam(url, value){
    280  if(value){
    281    url.searchParams.append("nvs_header", value);
    282  }
    283 }