tor-browser

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

browser_serviceworker_fetch_new_process.js (13419B)


      1 const DIRPATH = getRootDirectory(gTestPath).replace(
      2  "chrome://mochitests/content/",
      3  ""
      4 );
      5 
      6 /**
      7 * We choose blob contents that will roundtrip cleanly through the `textContent`
      8 * of our returned HTML page.
      9 */
     10 const TEST_BLOB_CONTENTS = `I'm a disk-backed test blob! Hooray!`;
     11 
     12 add_setup(async function () {
     13  await SpecialPowers.pushPrefEnv({
     14    set: [
     15      // Set preferences so that opening a page with the origin "example.org"
     16      // will result in a remoteType of "privilegedmozilla" for both the
     17      // page and the ServiceWorker.
     18      ["browser.tabs.remote.separatePrivilegedMozillaWebContentProcess", true],
     19      ["browser.tabs.remote.separatedMozillaDomains", "example.org"],
     20      ["dom.ipc.processCount.privilegedmozilla", 1],
     21      ["dom.ipc.processPrelaunch.enabled", false],
     22      ["dom.serviceWorkers.enabled", true],
     23      ["dom.serviceWorkers.testing.enabled", true],
     24      // ServiceWorker worker instances should stay alive until explicitly
     25      // caused to terminate by dropping these timeouts to 0 in
     26      // `waitForWorkerAndProcessShutdown`.
     27      ["dom.serviceWorkers.idle_timeout", 299999],
     28      ["dom.serviceWorkers.idle_extended_timeout", 299999],
     29    ],
     30  });
     31 });
     32 
     33 function countRemoteType(remoteType) {
     34  return ChromeUtils.getAllDOMProcesses().filter(
     35    p => p.remoteType == remoteType
     36  ).length;
     37 }
     38 
     39 /**
     40 * Helper function to get a list of all current processes and their remote
     41 * types.  Note that when in used in a templated literal that it is
     42 * synchronously invoked when the string is evaluated and captures system state
     43 * at that instant.
     44 */
     45 function debugRemotes() {
     46  return ChromeUtils.getAllDOMProcesses()
     47    .map(p => p.remoteType || "parent")
     48    .join(",");
     49 }
     50 
     51 /**
     52 * Wait for there to be zero processes of the given remoteType.  This check is
     53 * considered successful if there are already no processes of the given type
     54 * at this very moment.
     55 */
     56 async function waitForNoProcessesOfType(remoteType) {
     57  info(`waiting for there to be no ${remoteType} procs`);
     58  await TestUtils.waitForCondition(
     59    () => countRemoteType(remoteType) == 0,
     60    "wait for the worker's process to shutdown"
     61  );
     62 }
     63 
     64 /**
     65 * Given a ServiceWorkerRegistrationInfo with an active ServiceWorker that
     66 * has no active ExtendableEvents but would otherwise continue running thanks
     67 * to the idle keepalive:
     68 * - Assert that there is a ServiceWorker instance in the given registration's
     69 *   active slot.  (General invariant check.)
     70 * - Assert that a single process with the given remoteType currently exists.
     71 *   (This doesn't mean the SW is alive in that process, though this test
     72 *   verifies that via other checks when appropriate.)
     73 * - Induce the worker to shutdown by temporarily dropping the idle timeout to 0
     74 *   and causing the idle timer to be reset due to rapid debugger attach/detach.
     75 * - Wait for the the single process with the given remoteType to go away.
     76 * - Reset the idle timeouts back to their previous high values.
     77 */
     78 async function waitForWorkerAndProcessShutdown(swRegInfo, remoteType) {
     79  info(`terminating worker and waiting for ${remoteType} procs to shut down`);
     80  ok(swRegInfo.activeWorker, "worker should be in the active slot");
     81  is(
     82    countRemoteType(remoteType),
     83    1,
     84    `should have a single ${remoteType} process but have: ${debugRemotes()}`
     85  );
     86 
     87  // Let's not wait too long for the process to shutdown.
     88  await SpecialPowers.pushPrefEnv({
     89    set: [
     90      ["dom.serviceWorkers.idle_timeout", 0],
     91      ["dom.serviceWorkers.idle_extended_timeout", 0],
     92    ],
     93  });
     94 
     95  // We need to cause the worker to re-evaluate its idle timeout. The easiest
     96  // way to do this I could think of is to attach and then detach the debugger
     97  // from the active worker.
     98  swRegInfo.activeWorker.attachDebugger();
     99  await new Promise(resolve => Cu.dispatch(resolve));
    100  swRegInfo.activeWorker.detachDebugger();
    101 
    102  // Eventually the length will reach 0, meaning we're done!
    103  await waitForNoProcessesOfType(remoteType);
    104 
    105  is(
    106    countRemoteType(remoteType),
    107    0,
    108    `processes with remoteType=${remoteType} type should have shut down`
    109  );
    110 
    111  // Make sure we never kill workers on idle except when this is called.
    112  await SpecialPowers.popPrefEnv();
    113 }
    114 
    115 async function do_test_sw(host, remoteType, swMode, fileBlob) {
    116  info(
    117    `### entering test: host=${host}, remoteType=${remoteType}, mode=${swMode}`
    118  );
    119 
    120  const prin = Services.scriptSecurityManager.createContentPrincipal(
    121    Services.io.newURI(`https://${host}`),
    122    {}
    123  );
    124  const sw = `https://${host}/${DIRPATH}file_service_worker_fetch_synthetic.js`;
    125  const scope = `https://${host}/${DIRPATH}server_fetch_synthetic.sjs`;
    126 
    127  const swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService(
    128    Ci.nsIServiceWorkerManager
    129  );
    130  const swRegInfo = await swm.registerForTest(prin, scope, sw);
    131  swRegInfo.QueryInterface(Ci.nsIServiceWorkerRegistrationInfo);
    132 
    133  info(
    134    `service worker registered: ${JSON.stringify({
    135      principal: swRegInfo.principal.spec,
    136      scope: swRegInfo.scope,
    137    })}`
    138  );
    139 
    140  // Wait for the worker to install & shut down.
    141  await TestUtils.waitForCondition(
    142    () => swRegInfo.activeWorker,
    143    "wait for the worker to become active"
    144  );
    145  await waitForWorkerAndProcessShutdown(swRegInfo, remoteType);
    146 
    147  info(
    148    `test navigation interception with mode=${swMode} starting from about:blank`
    149  );
    150  await BrowserTestUtils.withNewTab(
    151    {
    152      gBrowser,
    153      url: "about:blank",
    154    },
    155    async browser => {
    156      // NOTE: We intentionally trigger the navigation from content in order to
    157      // make sure frontend doesn't eagerly process-switch for us.
    158      SpecialPowers.spawn(
    159        browser,
    160        [scope, swMode, fileBlob],
    161        // eslint-disable-next-line no-shadow
    162        async (scope, swMode, fileBlob) => {
    163          const pageUrl = `${scope}?mode=${swMode}`;
    164          if (!fileBlob) {
    165            content.location.href = pageUrl;
    166          } else {
    167            const doc = content.document;
    168            const formElem = doc.createElement("form");
    169            doc.body.appendChild(formElem);
    170 
    171            formElem.action = pageUrl;
    172            formElem.method = "POST";
    173            formElem.enctype = "multipart/form-data";
    174 
    175            const fileElem = doc.createElement("input");
    176            formElem.appendChild(fileElem);
    177 
    178            fileElem.type = "file";
    179            fileElem.name = "foo";
    180 
    181            fileElem.mozSetFileArray([fileBlob]);
    182 
    183            formElem.submit();
    184          }
    185        }
    186      );
    187 
    188      await BrowserTestUtils.browserLoaded(browser);
    189 
    190      is(
    191        countRemoteType(remoteType),
    192        1,
    193        `should have spawned a content process with remoteType=${remoteType}`
    194      );
    195 
    196      const { source, blobContents } = await SpecialPowers.spawn(
    197        browser,
    198        [],
    199        () => {
    200          return {
    201            source: content.document.getElementById("source").textContent,
    202            blobContents: content.document.getElementById("blob").textContent,
    203          };
    204        }
    205      );
    206 
    207      is(
    208        source,
    209        swMode === "synthetic" ? "ServiceWorker" : "ServerJS",
    210        "The page contents should come from the right place."
    211      );
    212 
    213      is(
    214        blobContents,
    215        fileBlob ? TEST_BLOB_CONTENTS : "",
    216        "The request blob contents should be the blob/empty as appropriate."
    217      );
    218 
    219      // Ensure the worker was loaded in this process.
    220      const workerDebuggerURLs = await SpecialPowers.spawn(
    221        browser,
    222        [sw],
    223        async url => {
    224          if (!content.navigator.serviceWorker.controller) {
    225            throw new Error("document not controlled!");
    226          }
    227          const wdm = Cc[
    228            "@mozilla.org/dom/workers/workerdebuggermanager;1"
    229          ].getService(Ci.nsIWorkerDebuggerManager);
    230 
    231          return Array.from(wdm.getWorkerDebuggerEnumerator())
    232            .map(wd => {
    233              return wd.url;
    234            })
    235            .filter(swURL => swURL == url);
    236        }
    237      );
    238      if (remoteType.startsWith("webServiceWorker=")) {
    239        Assert.notDeepEqual(
    240          workerDebuggerURLs,
    241          [sw],
    242          "Isolated workers should not be running in the content child process"
    243        );
    244      } else {
    245        Assert.deepEqual(
    246          workerDebuggerURLs,
    247          [sw],
    248          "The worker should be running in the correct child process"
    249        );
    250      }
    251 
    252      // Unregister the ServiceWorker.  The registration will continue to control
    253      // `browser` and therefore continue to exist and its worker to continue
    254      // running until the tab is closed.
    255      await SpecialPowers.spawn(browser, [], async () => {
    256        let registration = await content.navigator.serviceWorker.ready;
    257        await registration.unregister();
    258      });
    259    }
    260  );
    261 
    262  // Now that the controlled tab is closed and the registration has been
    263  // removed, the ServiceWorker will be made redundant which will forcibly
    264  // terminate it, which will result in the shutdown of the given content
    265  // process.  Wait for that to happen both as a verification and so the next
    266  // test has a sufficiently clean slate.
    267  await waitForNoProcessesOfType(remoteType);
    268 }
    269 
    270 /**
    271 * Create a File-backed blob.  This will happen synchronously from the main
    272 * thread, which isn't optimal, but the test blocks on this progress anyways.
    273 * Bug 1669578 has been filed on improving this idiom and avoiding the sync
    274 * writes.
    275 */
    276 async function makeFileBlob(blobContents) {
    277  const tmpFile = Cc["@mozilla.org/file/directory_service;1"]
    278    .getService(Ci.nsIDirectoryService)
    279    .QueryInterface(Ci.nsIProperties)
    280    .get("TmpD", Ci.nsIFile);
    281  tmpFile.append("test-file-backed-blob.txt");
    282  tmpFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
    283 
    284  var outStream = Cc[
    285    "@mozilla.org/network/file-output-stream;1"
    286  ].createInstance(Ci.nsIFileOutputStream);
    287  outStream.init(
    288    tmpFile,
    289    0x02 | 0x08 | 0x20, // write, create, truncate
    290    0o666,
    291    0
    292  );
    293  outStream.write(blobContents, blobContents.length);
    294  outStream.close();
    295 
    296  const fileBlob = await File.createFromNsIFile(tmpFile);
    297  return fileBlob;
    298 }
    299 
    300 function getSWTelemetrySums() {
    301  let telemetry = Cc["@mozilla.org/base/telemetry;1"].getService(
    302    Ci.nsITelemetry
    303  );
    304  let keyedhistograms = telemetry.getSnapshotForKeyedHistograms(
    305    "main",
    306    false
    307  ).parent;
    308  let keyedscalars = telemetry.getSnapshotForKeyedScalars("main", false).parent;
    309  // We're not looking at the distribution of the histograms, just that they changed
    310  return {
    311    SERVICE_WORKER_RUNNING_All: keyedhistograms.SERVICE_WORKER_RUNNING
    312      ? keyedhistograms.SERVICE_WORKER_RUNNING.All.sum
    313      : 0,
    314    SERVICE_WORKER_RUNNING_Fetch: keyedhistograms.SERVICE_WORKER_RUNNING
    315      ? keyedhistograms.SERVICE_WORKER_RUNNING.Fetch.sum
    316      : 0,
    317  };
    318 }
    319 
    320 add_task(async function test() {
    321  // Can't test telemetry without this since we may not be on the nightly channel
    322  let oldCanRecord = Services.telemetry.canRecordExtended;
    323  Services.telemetry.canRecordExtended = true;
    324  registerCleanupFunction(() => {
    325    Services.telemetry.canRecordExtended = oldCanRecord;
    326  });
    327 
    328  let initialSums = getSWTelemetrySums();
    329 
    330  // ## Isolated Privileged Process
    331  // Trigger a straightforward intercepted navigation with no request body that
    332  // returns a synthetic response.
    333  await do_test_sw("example.org", "privilegedmozilla", "synthetic", null);
    334 
    335  // Trigger an intercepted navigation with FormData containing an
    336  // <input type="file"> which will result in the request body containing a
    337  // RemoteLazyInputStream which will be consumed in the content process by the
    338  // ServiceWorker while generating the synthetic response.
    339  const fileBlob = await makeFileBlob(TEST_BLOB_CONTENTS);
    340  await do_test_sw("example.org", "privilegedmozilla", "synthetic", fileBlob);
    341 
    342  // Trigger an intercepted navigation with FormData containing an
    343  // <input type="file"> which will result in the request body containing a
    344  // RemoteLazyInputStream which will be relayed back to the parent process
    345  // via direct invocation of fetch() on the event.request but without any
    346  // cloning.
    347  await do_test_sw("example.org", "privilegedmozilla", "fetch", fileBlob);
    348 
    349  // Same as the above but cloning the request before fetching it.
    350  await do_test_sw("example.org", "privilegedmozilla", "clone", fileBlob);
    351 
    352  // ## Fission Isolation
    353  if (Services.appinfo.fissionAutostart) {
    354    // ## ServiceWorker isolation
    355    const isolateUrl = "example.com";
    356    const isolateRemoteType = `webServiceWorker=https://` + isolateUrl;
    357    await do_test_sw(isolateUrl, isolateRemoteType, "synthetic", null);
    358    await do_test_sw(isolateUrl, isolateRemoteType, "synthetic", fileBlob);
    359  }
    360  let telemetrySums = getSWTelemetrySums();
    361  info(JSON.stringify(telemetrySums));
    362  info(
    363    "Initial Running All: " +
    364      initialSums.SERVICE_WORKER_RUNNING_All +
    365      ", Fetch: " +
    366      initialSums.SERVICE_WORKER_RUNNING_Fetch
    367  );
    368  info(
    369    "Running All: " +
    370      telemetrySums.SERVICE_WORKER_RUNNING_All +
    371      ", Fetch: " +
    372      telemetrySums.SERVICE_WORKER_RUNNING_Fetch
    373  );
    374  Assert.greater(
    375    telemetrySums.SERVICE_WORKER_RUNNING_All,
    376    initialSums.SERVICE_WORKER_RUNNING_All,
    377    "ServiceWorker running count changed"
    378  );
    379  Assert.greater(
    380    telemetrySums.SERVICE_WORKER_RUNNING_Fetch,
    381    initialSums.SERVICE_WORKER_RUNNING_Fetch,
    382    "ServiceWorker running count changed"
    383  );
    384 });