tor-browser

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

browser_head.js (12639B)


      1 /**
      2 * This file contains common functionality for ServiceWorker browser tests.
      3 *
      4 * Note that the normal auto-import mechanics for browser mochitests only
      5 * handles "head.js", but we currently store all of our different varieties of
      6 * mochitest in a single directory, which potentially results in a collision
      7 * for similar heuristics for xpcshell.
      8 *
      9 * Many of the storage-related helpers in this file come from:
     10 * https://searchfox.org/mozilla-central/source/dom/localstorage/test/unit/head.js
     11 */
     12 
     13 // To use this file, explicitly import it via:
     14 //
     15 // Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/dom/serviceworkers/test/browser_head.js", this);
     16 
     17 // Find the current parent directory of the test context we're being loaded into
     18 // such that one can do `${originNoTrailingSlash}/${DIR_PATH}/file_in_dir.foo`.
     19 const DIR_PATH = getRootDirectory(gTestPath)
     20  .replace("chrome://mochitests/content/", "")
     21  .slice(0, -1);
     22 
     23 const SWM = Cc["@mozilla.org/serviceworkers/manager;1"].getService(
     24  Ci.nsIServiceWorkerManager
     25 );
     26 
     27 // The expected minimum usage for an origin that has any Cache API storage in
     28 // use. Currently, the DB uses a page size of 4k and a minimum growth size of
     29 // 32k and has enough tables/indices for this to use 15 pages (61440) which
     30 // rounds up to 64k.  However, we also have to allow for the incremental
     31 // vacuum heuristic only firing if we have more than the growth increment's
     32 // worth of free pages, so we need to set the threshold at 32k * 3.
     33 const kMinimumOriginUsageBytes = 98304;
     34 
     35 function getPrincipal(url, attrs) {
     36  const uri = Services.io.newURI(url);
     37  if (!attrs) {
     38    attrs = {};
     39  }
     40  return Services.scriptSecurityManager.createContentPrincipal(uri, attrs);
     41 }
     42 
     43 async function _qm_requestFinished(request) {
     44  await new Promise(function (resolve) {
     45    request.callback = function () {
     46      resolve();
     47    };
     48  });
     49 
     50  if (request.resultCode !== Cr.NS_OK) {
     51    throw new RequestError(request.resultCode, request.resultName);
     52  }
     53 
     54  return request.result;
     55 }
     56 
     57 async function qm_reset_storage() {
     58  return new Promise(resolve => {
     59    let request = Services.qms.reset();
     60    request.callback = resolve;
     61  });
     62 }
     63 
     64 async function get_qm_origin_usage(origin) {
     65  return new Promise(resolve => {
     66    const principal =
     67      Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin);
     68    Services.qms.getUsageForPrincipal(principal, request => {
     69      info(`QM says usage of ${origin} is ${request.result.usage}`);
     70      resolve(request.result.usage);
     71    });
     72  });
     73 }
     74 
     75 /**
     76 * Clear the group associated with the given origin via nsIClearDataService.  We
     77 * are using nsIClearDataService here because nsIQuotaManagerService doesn't
     78 * (directly) provide a means of clearing a group.
     79 */
     80 async function clear_qm_origin_group_via_clearData(origin) {
     81  const uri = Services.io.newURI(origin);
     82  const baseDomain = Services.eTLD.getBaseDomain(uri);
     83  info(`Clearing storage on domain ${baseDomain} (from origin ${origin})`);
     84 
     85  // Initiate group clearing and wait for it.
     86  await new Promise((resolve, reject) => {
     87    Services.clearData.deleteDataFromSite(
     88      baseDomain,
     89      {},
     90      false,
     91      Services.clearData.CLEAR_DOM_QUOTA,
     92      failedFlags => {
     93        if (failedFlags) {
     94          reject(failedFlags);
     95        } else {
     96          resolve();
     97        }
     98      }
     99    );
    100  });
    101 }
    102 
    103 /**
    104 * Look up the nsIServiceWorkerRegistrationInfo for a given SW descriptor.
    105 */
    106 function swm_lookup_reg(swDesc) {
    107  // Scopes always include the full origin.
    108  const fullScope = `${swDesc.origin}/${DIR_PATH}/${swDesc.scope}`;
    109  const principal = getPrincipal(fullScope);
    110 
    111  const reg = SWM.getRegistrationByPrincipal(principal, fullScope);
    112 
    113  return reg;
    114 }
    115 
    116 /**
    117 * Install a ServiceWorker according to the provided descriptor by opening a
    118 * fresh tab that waits for the installed worker to be active and then closes
    119 * the tab.  Returns the`nsIServiceWorkerRegistrationInfo` corresponding to the
    120 * registration.
    121 *
    122 * The descriptor may have the following properties:
    123 * - scope: Optional.  This is usually a relative path for tests and because
    124 *   there are (security) checks if the scope is more generic than the page
    125 *   URL, you almost never would want to specify an absolute scope here.
    126 * - script: The script, which usually just wants to be a relative path.
    127 * - origin: Requred, the origin (which should not include a trailing slash).
    128 */
    129 async function install_sw(swDesc) {
    130  info(
    131    `Installing ServiceWorker ${swDesc.script} at ${swDesc.scope} on origin ${swDesc.origin}`
    132  );
    133  const pageUrlStr = `${swDesc.origin}/${DIR_PATH}/empty_with_utils.html`;
    134 
    135  await BrowserTestUtils.withNewTab(
    136    {
    137      gBrowser,
    138      url: pageUrlStr,
    139    },
    140    async browser => {
    141      await SpecialPowers.spawn(
    142        browser,
    143        [{ swScript: swDesc.script, swScope: swDesc.scope }],
    144        async function ({ swScript, swScope }) {
    145          await content.wrappedJSObject.registerAndWaitForActive(
    146            swScript,
    147            swScope
    148          );
    149        }
    150      );
    151    }
    152  );
    153  info(`ServiceWorker installed`);
    154 
    155  return swm_lookup_reg(swDesc);
    156 }
    157 
    158 async function createMessagingHelperTab(origin, channelName) {
    159  const pageUrlStr = `${origin}/${DIR_PATH}/empty_with_utils.html`;
    160 
    161  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrlStr);
    162 
    163  // For hygiene reasons we make sure the helper establishes its message channel
    164  // in a task distinct from any of the tasks that will initiate events.
    165  await SpecialPowers.spawn(tab.linkedBrowser, [channelName], channelName => {
    166    content.wrappedJSObject.setupMessagingChannel(channelName);
    167  });
    168 
    169  return {
    170    async postMessageScopeAndWaitFor(scope, messageToSend, messageToWaitFor) {
    171      info(
    172        `Sending message to SW scope ${scope} via helper page: ${messageToSend}`
    173      );
    174      info(`Waiting for message via helper page: ${messageToWaitFor}`);
    175      await SpecialPowers.spawn(
    176        tab.linkedBrowser,
    177        [channelName, scope, messageToSend, messageToWaitFor],
    178        async (channelName, scope, messageToSend, messageToWaitFor) => {
    179          await content.wrappedJSObject.postMessageScopeAndWaitFor(
    180            channelName,
    181            scope,
    182            messageToSend,
    183            messageToWaitFor
    184          );
    185        }
    186      );
    187      ok(true, "Expected message received");
    188    },
    189 
    190    async broadcastAndWaitFor(messageToBroadcast, messageToWaitFor) {
    191      info(`Sending messageToBroadcast via helper page: ${messageToBroadcast}`);
    192      info(`Waiting for message via helper page: ${messageToWaitFor}`);
    193      await SpecialPowers.spawn(
    194        tab.linkedBrowser,
    195        [channelName, messageToBroadcast, messageToWaitFor],
    196        async (channelName, messageToBroadcast, messageToWaitFor) => {
    197          await content.wrappedJSObject.broadcastAndWaitFor(
    198            channelName,
    199            messageToBroadcast,
    200            messageToWaitFor
    201          );
    202        }
    203      );
    204      ok(true, "Expected message received");
    205    },
    206 
    207    async updateScopeAndWaitFor(scope, messageToWaitFor) {
    208      info(`Updating scope ${scope} via helper page`);
    209      info(`Waiting for message via helper page: ${messageToWaitFor}`);
    210      await SpecialPowers.spawn(
    211        tab.linkedBrowser,
    212        [channelName, scope, messageToWaitFor],
    213        async (channelName, scope, messageToWaitFor) => {
    214          await content.wrappedJSObject.updateScopeAndWaitFor(
    215            channelName,
    216            scope,
    217            messageToWaitFor
    218          );
    219        }
    220      );
    221      ok(true, "Expected message received");
    222    },
    223 
    224    async closeHelperTab() {
    225      await BrowserTestUtils.removeTab(tab);
    226      tab = null;
    227    },
    228  };
    229 }
    230 
    231 /**
    232 * Consume storage in the given origin by storing randomly generated Blobs into
    233 * Cache API storage and IndexedDB storage.  We use both APIs in order to
    234 * ensure that data clearing wipes both QM clients.
    235 *
    236 * Randomly generated Blobs means Blobs with literally random content.  This is
    237 * done to compensate for the Cache API using snappy for compression.
    238 */
    239 async function consume_storage(origin, storageDesc) {
    240  info(`Consuming storage on origin ${origin}`);
    241  const pageUrlStr = `${origin}/${DIR_PATH}/empty_with_utils.html`;
    242 
    243  await BrowserTestUtils.withNewTab(
    244    {
    245      gBrowser,
    246      url: pageUrlStr,
    247    },
    248    async browser => {
    249      await SpecialPowers.spawn(
    250        browser,
    251        [storageDesc],
    252        async function ({ cacheBytes, idbBytes }) {
    253          await content.wrappedJSObject.fillStorage(cacheBytes, idbBytes);
    254        }
    255      );
    256    }
    257  );
    258 }
    259 
    260 // Check if the origin is effectively empty, but allowing for the minimum size
    261 // Cache API database to be present.
    262 function is_minimum_origin_usage(originUsageBytes) {
    263  return originUsageBytes <= kMinimumOriginUsageBytes;
    264 }
    265 
    266 /**
    267 * Perform a navigation, waiting until the navigation stops, then returning
    268 * the `textContent` of the body node.  The expectation is this will be used
    269 * with ServiceWorkers that return a body that indicates the ServiceWorker
    270 * provided the result (possibly derived from the request) versus if
    271 * interception didn't happen.
    272 */
    273 async function navigate_and_get_body(swDesc, debugTag) {
    274  let pageUrlStr = `${swDesc.origin}/${DIR_PATH}/${swDesc.scope}`;
    275  if (debugTag) {
    276    pageUrlStr += "?" + debugTag;
    277  }
    278  info(`Navigating to ${pageUrlStr}`);
    279 
    280  const tabResult = await BrowserTestUtils.withNewTab(
    281    {
    282      gBrowser,
    283      url: pageUrlStr,
    284      // In the event of an aborted navigation, the load event will never
    285      // happen...
    286      waitForLoad: false,
    287      // ...but the stop will.
    288      waitForStateStop: true,
    289    },
    290    async browser => {
    291      info(` Tab opened, querying body content.`);
    292      const spawnResult = await SpecialPowers.spawn(browser, [], function () {
    293        const controlled = !!content.navigator.serviceWorker.controller;
    294        // Special-case about: URL's.
    295        let loc = content.document.documentURI;
    296        if (loc.startsWith("about:")) {
    297          // about:neterror is parameterized by query string, so truncate that
    298          // off because our tests just care if we're seeing the neterror page.
    299          const idxQuestion = loc.indexOf("?");
    300          if (idxQuestion !== -1) {
    301            loc = loc.substring(0, idxQuestion);
    302          }
    303          return { controlled, body: loc };
    304        }
    305        return {
    306          controlled,
    307          body: content.document?.body?.textContent?.trim(),
    308        };
    309      });
    310 
    311      return spawnResult;
    312    }
    313  );
    314 
    315  return tabResult;
    316 }
    317 
    318 function waitForIframeLoad(iframe) {
    319  return new Promise(function (resolve) {
    320    iframe.onload = resolve;
    321  });
    322 }
    323 
    324 function waitForRegister(scope, callback) {
    325  return new Promise(function (resolve) {
    326    let listener = {
    327      onRegister(registration) {
    328        if (registration.scope !== scope) {
    329          return;
    330        }
    331        SWM.removeListener(listener);
    332        resolve(callback ? callback(registration) : registration);
    333      },
    334    };
    335    SWM.addListener(listener);
    336  });
    337 }
    338 
    339 function waitForUnregister(scope) {
    340  return new Promise(function (resolve) {
    341    let listener = {
    342      onUnregister(registration) {
    343        if (registration.scope !== scope) {
    344          return;
    345        }
    346        SWM.removeListener(listener);
    347        resolve(registration);
    348      },
    349    };
    350    SWM.addListener(listener);
    351  });
    352 }
    353 
    354 // Be careful using this helper function, please make sure QuotaUsageCheck must
    355 // happen, otherwise test would be stucked in this function.
    356 function waitForQuotaUsageCheckFinish(scope) {
    357  return new Promise(function (resolve) {
    358    let listener = {
    359      onQuotaUsageCheckFinish(registration) {
    360        if (registration.scope !== scope) {
    361          return;
    362        }
    363        SWM.removeListener(listener);
    364        resolve(registration);
    365      },
    366    };
    367    SWM.addListener(listener);
    368  });
    369 }
    370 
    371 function waitForServiceWorkerRegistrationChange(registration, callback) {
    372  return new Promise(function (resolve) {
    373    let listener = {
    374      onChange() {
    375        registration.removeListener(listener);
    376        if (callback) {
    377          callback();
    378        }
    379        resolve(callback ? callback() : undefined);
    380      },
    381    };
    382    registration.addListener(listener);
    383  });
    384 }
    385 
    386 function waitForServiceWorkerShutdown() {
    387  return new Promise(function (resolve) {
    388    let observer = {
    389      observe(subject, topic, data) {
    390        if (topic !== "service-worker-shutdown") {
    391          return;
    392        }
    393        SpecialPowers.removeObserver(observer, "service-worker-shutdown");
    394        resolve();
    395      },
    396    };
    397    SpecialPowers.addObserver(observer, "service-worker-shutdown");
    398  });
    399 }