tor-browser

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

browser_devtools_serviceworker_interception.js (7955B)


      1 "use strict";
      2 
      3 const BASE_URI = "http://mochi.test:8888/browser/dom/serviceworkers/test/";
      4 const emptyDoc = BASE_URI + "empty.html";
      5 const fakeDoc = BASE_URI + "fake.html";
      6 const helloDoc = BASE_URI + "hello.html";
      7 
      8 const CROSS_URI = "http://example.com/browser/dom/serviceworkers/test/";
      9 const crossRedirect = CROSS_URI + "redirect";
     10 const crossHelloDoc = CROSS_URI + "hello.html";
     11 
     12 const sw = BASE_URI + "fetch.js";
     13 
     14 async function checkObserver(aInput) {
     15  let interceptedChannel = null;
     16 
     17  // We always get two channels which receive the "http-on-stop-request"
     18  // notification if the service worker hijacks the request and respondWith an
     19  // another fetch. One is for the "outer" window request when the other one is
     20  // for the "inner" service worker request. Therefore, distinguish them by the
     21  // order.
     22  let waitForSecondOnStopRequest = aInput.intercepted;
     23 
     24  let promiseResolve;
     25 
     26  function observer(aSubject) {
     27    let channel = aSubject.QueryInterface(Ci.nsIChannel);
     28    // Since we cannot make sure that the network event triggered by the fetch()
     29    // in this testcase is the very next event processed by ObserverService, we
     30    // have to wait until we catch the one we want.
     31    if (!channel.URI.spec.includes(aInput.expectedURL)) {
     32      return;
     33    }
     34 
     35    if (waitForSecondOnStopRequest) {
     36      waitForSecondOnStopRequest = false;
     37      return;
     38    }
     39 
     40    // Wait for the service worker to intercept the request if it's expected to
     41    // be intercepted
     42    if (aInput.intercepted && interceptedChannel === null) {
     43      return;
     44    } else if (interceptedChannel) {
     45      ok(
     46        aInput.intercepted,
     47        "Service worker intercepted the channel as expected"
     48      );
     49    } else {
     50      ok(!aInput.intercepted, "The channel doesn't be intercepted");
     51    }
     52 
     53    var tc = interceptedChannel
     54      ? interceptedChannel.QueryInterface(Ci.nsITimedChannel)
     55      : aSubject.QueryInterface(Ci.nsITimedChannel);
     56 
     57    // Check service worker related timings.
     58    var serviceWorkerTimings = [
     59      {
     60        start: tc.launchServiceWorkerStartTime,
     61        end: tc.launchServiceWorkerEndTime,
     62      },
     63      {
     64        start: tc.dispatchFetchEventStartTime,
     65        end: tc.dispatchFetchEventEndTime,
     66      },
     67      { start: tc.handleFetchEventStartTime, end: tc.handleFetchEventEndTime },
     68    ];
     69    if (!aInput.swPresent) {
     70      serviceWorkerTimings.forEach(aTimings => {
     71        is(aTimings.start, 0, "SW timings should be 0.");
     72        is(aTimings.end, 0, "SW timings should be 0.");
     73      });
     74    }
     75 
     76    // Check network related timings.
     77    var networkTimings = [
     78      tc.domainLookupStartTime,
     79      tc.domainLookupEndTime,
     80      tc.connectStartTime,
     81      tc.connectEndTime,
     82      tc.requestStartTime,
     83      tc.responseStartTime,
     84      tc.responseEndTime,
     85    ];
     86    if (aInput.fetch) {
     87      networkTimings.reduce((aPreviousTiming, aCurrentTiming) => {
     88        Assert.lessOrEqual(
     89          aPreviousTiming,
     90          aCurrentTiming,
     91          "Checking network timings"
     92        );
     93        return aCurrentTiming;
     94      });
     95    } else {
     96      networkTimings.forEach(aTiming =>
     97        is(aTiming, 0, "Network timings should be 0.")
     98      );
     99    }
    100 
    101    interceptedChannel = null;
    102    Services.obs.removeObserver(observer, topic);
    103    promiseResolve();
    104  }
    105 
    106  function addInterceptedChannel(aSubject) {
    107    let channel = aSubject.QueryInterface(Ci.nsIChannel);
    108    if (!channel.URI.spec.includes(aInput.url)) {
    109      return;
    110    }
    111 
    112    // Hold the interceptedChannel until checking timing information.
    113    // Note: It's a interceptedChannel in the type of httpChannel
    114    interceptedChannel = channel;
    115    Services.obs.removeObserver(addInterceptedChannel, topic_SW);
    116  }
    117 
    118  const topic = "http-on-stop-request";
    119  const topic_SW = "service-worker-synthesized-response";
    120 
    121  Services.obs.addObserver(observer, topic);
    122  if (aInput.intercepted) {
    123    Services.obs.addObserver(addInterceptedChannel, topic_SW);
    124  }
    125 
    126  await new Promise(resolve => {
    127    promiseResolve = resolve;
    128  });
    129 }
    130 
    131 async function contentFetch(aURL) {
    132  if (aURL.includes("redirect")) {
    133    await content.window.fetch(aURL, { mode: "no-cors" });
    134    return;
    135  }
    136  await content.window.fetch(aURL);
    137 }
    138 
    139 // The observer topics are fired in the parent process in parent-intercept
    140 // and the content process in child-intercept. This function will handle running
    141 // the check in the correct process. Note that it will block until the observers
    142 // are notified.
    143 async function fetchAndCheckObservers(
    144  aFetchBrowser,
    145  aObserverBrowser,
    146  aTestCase
    147 ) {
    148  let promise = null;
    149 
    150  promise = checkObserver(aTestCase);
    151 
    152  await SpecialPowers.spawn(aFetchBrowser, [aTestCase.url], contentFetch);
    153  await promise;
    154 }
    155 
    156 async function registerSWAndWaitForActive(aServiceWorker) {
    157  let swr = await content.navigator.serviceWorker.register(aServiceWorker, {
    158    scope: "empty.html",
    159  });
    160  await new Promise(resolve => {
    161    let worker = swr.installing || swr.waiting || swr.active;
    162    if (worker.state === "activated") {
    163      resolve();
    164      return;
    165    }
    166 
    167    worker.addEventListener("statechange", () => {
    168      if (worker.state === "activated") {
    169        resolve();
    170      }
    171    });
    172  });
    173 
    174  await new Promise(resolve => {
    175    if (content.navigator.serviceWorker.controller) {
    176      resolve();
    177      return;
    178    }
    179 
    180    content.navigator.serviceWorker.addEventListener(
    181      "controllerchange",
    182      resolve,
    183      { once: true }
    184    );
    185  });
    186 }
    187 
    188 async function unregisterSW() {
    189  let swr = await content.navigator.serviceWorker.getRegistration();
    190  swr.unregister();
    191 }
    192 
    193 add_task(async function test_serivce_worker_interception() {
    194  info("Setting the prefs to having e10s enabled");
    195  await SpecialPowers.pushPrefEnv({
    196    set: [
    197      // Make sure observer and testing function run in the same process
    198      ["dom.ipc.processCount", 1],
    199      ["dom.serviceWorkers.enabled", true],
    200      ["dom.serviceWorkers.testing.enabled", true],
    201    ],
    202  });
    203 
    204  waitForExplicitFinish();
    205 
    206  info("Open the tab");
    207  let tab = BrowserTestUtils.addTab(gBrowser, emptyDoc);
    208  let tabBrowser = gBrowser.getBrowserForTab(tab);
    209  await BrowserTestUtils.browserLoaded(tabBrowser);
    210 
    211  info("Open the tab for observing");
    212  let tab_observer = BrowserTestUtils.addTab(gBrowser, emptyDoc);
    213  let tabBrowser_observer = gBrowser.getBrowserForTab(tab_observer);
    214  await BrowserTestUtils.browserLoaded(tabBrowser_observer);
    215 
    216  let testcases = [
    217    {
    218      url: helloDoc,
    219      expectedURL: helloDoc,
    220      swPresent: false,
    221      intercepted: false,
    222      fetch: true,
    223    },
    224    {
    225      url: fakeDoc,
    226      expectedURL: helloDoc,
    227      swPresent: true,
    228      intercepted: true,
    229      fetch: false, // should use HTTP cache
    230    },
    231    {
    232      // Bypass http cache
    233      url: helloDoc + "?ForBypassingHttpCache=" + Date.now(),
    234      expectedURL: helloDoc,
    235      swPresent: true,
    236      intercepted: false,
    237      fetch: true,
    238    },
    239    {
    240      // no-cors mode redirect to no-cors mode (trigger internal redirect)
    241      url: crossRedirect + "?url=" + crossHelloDoc + "&mode=no-cors",
    242      expectedURL: crossHelloDoc,
    243      swPresent: true,
    244      redirect: "hello.html",
    245      intercepted: true,
    246      fetch: true,
    247    },
    248  ];
    249 
    250  info("Test 1: Verify simple fetch");
    251  await fetchAndCheckObservers(tabBrowser, tabBrowser_observer, testcases[0]);
    252 
    253  info("Register a service worker");
    254  await SpecialPowers.spawn(tabBrowser, [sw], registerSWAndWaitForActive);
    255 
    256  info("Test 2: Verify simple hijack");
    257  await fetchAndCheckObservers(tabBrowser, tabBrowser_observer, testcases[1]);
    258 
    259  info("Test 3: Verify fetch without using http cache");
    260  await fetchAndCheckObservers(tabBrowser, tabBrowser_observer, testcases[2]);
    261 
    262  info("Test 4: make a internal redirect");
    263  await fetchAndCheckObservers(tabBrowser, tabBrowser_observer, testcases[3]);
    264 
    265  info("Clean up");
    266  await SpecialPowers.spawn(tabBrowser, [undefined], unregisterSW);
    267 
    268  gBrowser.removeTab(tab);
    269  gBrowser.removeTab(tab_observer);
    270 });