tor-browser

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

browser_handle_command_retry.js (12820B)


      1 /* Any copyright is dedicated to the Public Domain.
      2 * http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 "use strict";
      5 
      6 const { isInitialDocument } = ChromeUtils.importESModule(
      7  "chrome://remote/content/shared/messagehandler/transports/BrowsingContextUtils.sys.mjs"
      8 );
      9 const { RootMessageHandler } = ChromeUtils.importESModule(
     10  "chrome://remote/content/shared/messagehandler/RootMessageHandler.sys.mjs"
     11 );
     12 
     13 // We are forcing the actors to shutdown while queries are unresolved.
     14 const { PromiseTestUtils } = ChromeUtils.importESModule(
     15  "resource://testing-common/PromiseTestUtils.sys.mjs"
     16 );
     17 PromiseTestUtils.allowMatchingRejectionsGlobally(
     18  /Actor 'MessageHandlerFrame' destroyed before query 'MessageHandlerFrameParent:sendCommand' was resolved/
     19 );
     20 
     21 // The tests in this file assert the retry behavior for MessageHandler commands.
     22 // We call "blocked" commands from resources/modules/windowglobal/retry.sys.mjs
     23 // and then trigger reload and navigations to simulate AbortErrors and force the
     24 // MessageHandler to retry the commands, when possible.
     25 
     26 // If no retryOnAbort argument is provided, the framework will retry automatically.
     27 add_task(async function test_default_retry() {
     28  let tab = BrowserTestUtils.addTab(
     29    gBrowser,
     30    "https://example.com/document-builder.sjs?html=tab"
     31  );
     32 
     33  let rootMessageHandler = createRootMessageHandler("session-id-retry");
     34 
     35  try {
     36    const initialBrowsingContext = tab.linkedBrowser.browsingContext;
     37    ok(
     38      isInitialDocument(initialBrowsingContext),
     39      "Module method needs to run in the initial document"
     40    );
     41 
     42    info("Call a module method which will throw");
     43    const onBlockedOneTime = rootMessageHandler.handleCommand({
     44      moduleName: "retry",
     45      commandName: "blockedOneTime",
     46      destination: {
     47        type: WindowGlobalMessageHandler.type,
     48        id: initialBrowsingContext.id,
     49      },
     50    });
     51 
     52    await onBlockedOneTime;
     53 
     54    ok(
     55      !isInitialDocument(tab.linkedBrowser.browsingContext),
     56      "module method to be successful"
     57    );
     58  } finally {
     59    await cleanup(rootMessageHandler, tab, false);
     60  }
     61 
     62  // Now try again with a normal navigation which has to retry as well.
     63 
     64  tab = BrowserTestUtils.addTab(
     65    gBrowser,
     66    "https://example.com/document-builder.sjs?html=tab"
     67  );
     68  await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
     69 
     70  try {
     71    rootMessageHandler = createRootMessageHandler("session-id-no-retry");
     72    const browsingContext = tab.linkedBrowser.browsingContext;
     73 
     74    info("Call a module method which will throw");
     75    const onBlockedOneTime = rootMessageHandler.handleCommand({
     76      moduleName: "retry",
     77      commandName: "blockedOneTime",
     78      destination: {
     79        type: WindowGlobalMessageHandler.type,
     80        id: browsingContext.id,
     81      },
     82    });
     83 
     84    // Reloading the tab will reject the pending query with an AbortError.
     85    await BrowserTestUtils.reloadTab(tab);
     86 
     87    await onBlockedOneTime;
     88  } finally {
     89    await cleanup(rootMessageHandler, tab);
     90  }
     91 });
     92 
     93 // Test that without retry behavior, a pending command rejects when the
     94 // underlying JSWindowActor pair is destroyed.
     95 add_task(async function test_forced_no_retry() {
     96  const tab = BrowserTestUtils.addTab(
     97    gBrowser,
     98    "https://example.com/document-builder.sjs?html=tab"
     99  );
    100  await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
    101  const browsingContext = tab.linkedBrowser.browsingContext;
    102 
    103  const rootMessageHandler = createRootMessageHandler("session-id-no-retry");
    104 
    105  try {
    106    info("Call a module method which will throw");
    107    const onBlockedOneTime = rootMessageHandler.handleCommand({
    108      moduleName: "retry",
    109      commandName: "blockedOneTime",
    110      destination: {
    111        type: WindowGlobalMessageHandler.type,
    112        id: browsingContext.id,
    113      },
    114      retryOnAbort: false,
    115    });
    116 
    117    // Reloading the tab will reject the pending query with an AbortError.
    118    await BrowserTestUtils.reloadTab(tab);
    119 
    120    await Assert.rejects(
    121      onBlockedOneTime,
    122      e => e.name == "DiscardedBrowsingContextError",
    123      "Caught the expected error when reloading"
    124    );
    125  } finally {
    126    await cleanup(rootMessageHandler, tab);
    127  }
    128 });
    129 
    130 // Test that without retry behavior, a pending command rejects when the
    131 // underlying browsing context is discarded.
    132 add_task(async function test_forced_no_retry_cross_group() {
    133  const tab = BrowserTestUtils.addTab(
    134    gBrowser,
    135    "https://example.com/document-builder.sjs?html=COM" +
    136      // Attach an unload listener to prevent the page from going into bfcache,
    137      // so that pending queries will be rejected with an AbortError.
    138      "<script type='text/javascript'>window.onunload = function() {};</script>"
    139  );
    140  await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
    141  const browsingContext = tab.linkedBrowser.browsingContext;
    142 
    143  const rootMessageHandler = createRootMessageHandler("session-id-no-retry");
    144 
    145  try {
    146    const onBlockedOneTime = rootMessageHandler.handleCommand({
    147      moduleName: "retry",
    148      commandName: "blockedOneTime",
    149      destination: {
    150        type: WindowGlobalMessageHandler.type,
    151        id: browsingContext.id,
    152      },
    153      retryOnAbort: false,
    154    });
    155 
    156    // This command will return when the old browsing context was discarded.
    157    const onDiscarded = rootMessageHandler.handleCommand({
    158      moduleName: "retry",
    159      commandName: "waitForDiscardedBrowsingContext",
    160      destination: {
    161        type: RootMessageHandler.type,
    162      },
    163      params: {
    164        browsingContext,
    165        retryOnAbort: false,
    166      },
    167    });
    168 
    169    ok(
    170      !(await hasPromiseResolved(onBlockedOneTime)),
    171      "blockedOneTime should not have resolved yet"
    172    );
    173    ok(
    174      !(await hasPromiseResolved(onDiscarded)),
    175      "waitForDiscardedBrowsingContext should not have resolved yet"
    176    );
    177 
    178    info(
    179      "Navigate to example.net with COOP headers to destroy browsing context"
    180    );
    181    await loadURL(
    182      tab.linkedBrowser,
    183      "https://example.net/document-builder.sjs?headers=Cross-Origin-Opener-Policy:same-origin&html=NET"
    184    );
    185 
    186    await Assert.rejects(
    187      onBlockedOneTime,
    188      e => e.name == "DiscardedBrowsingContextError",
    189      "Caught the expected error when navigating"
    190    );
    191 
    192    await Assert.rejects(
    193      onDiscarded,
    194      e => e.name == "DiscardedBrowsingContextError",
    195      "Caught the expected error when navigating"
    196    );
    197  } finally {
    198    await cleanup(rootMessageHandler, tab);
    199  }
    200 });
    201 
    202 // Test various commands, which all need a different number of "retries" to
    203 // succeed. Check that they only resolve when the expected number of "retries"
    204 // was reached. For commands which require more "retries" than we allow, check
    205 // that we still fail with an AbortError once all the attempts are consumed.
    206 add_task(async function test_forced_retry() {
    207  const tab = BrowserTestUtils.addTab(
    208    gBrowser,
    209    "https://example.com/document-builder.sjs?html=tab"
    210  );
    211  await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
    212  const browsingContext = tab.linkedBrowser.browsingContext;
    213 
    214  const rootMessageHandler = createRootMessageHandler("session-id-retry");
    215 
    216  try {
    217    // This command will return if called twice.
    218    const onBlockedOneTime = rootMessageHandler.handleCommand({
    219      moduleName: "retry",
    220      commandName: "blockedOneTime",
    221      destination: {
    222        type: WindowGlobalMessageHandler.type,
    223        id: browsingContext.id,
    224      },
    225      params: {
    226        foo: "bar",
    227      },
    228      retryOnAbort: true,
    229    });
    230 
    231    // This command will return if called three times.
    232    const onBlockedTenTimes = rootMessageHandler.handleCommand({
    233      moduleName: "retry",
    234      commandName: "blockedTenTimes",
    235      destination: {
    236        type: WindowGlobalMessageHandler.type,
    237        id: browsingContext.id,
    238      },
    239      params: {
    240        foo: "baz",
    241      },
    242      retryOnAbort: true,
    243    });
    244 
    245    // This command will return if called twelve times, which is greater than the
    246    // maximum amount of retries allowed.
    247    const onBlockedElevenTimes = rootMessageHandler.handleCommand({
    248      moduleName: "retry",
    249      commandName: "blockedElevenTimes",
    250      destination: {
    251        type: WindowGlobalMessageHandler.type,
    252        id: browsingContext.id,
    253      },
    254      retryOnAbort: true,
    255    });
    256 
    257    info("Reload one time");
    258    await BrowserTestUtils.reloadTab(tab);
    259 
    260    info("blockedOneTime should resolve on the first retry");
    261    let { callsToCommand, foo } = await onBlockedOneTime;
    262    is(
    263      callsToCommand,
    264      2,
    265      "The command was called twice (initial call + 1 retry)"
    266    );
    267    is(foo, "bar", "The parameter was sent when the command was retried");
    268 
    269    // We already reloaded 1 time. Reload 9 more times to unblock blockedTenTimes.
    270    for (let i = 2; i < 11; i++) {
    271      info("blockedTenTimes/blockedElevenTimes should not have resolved yet");
    272      ok(!(await hasPromiseResolved(onBlockedTenTimes)));
    273      ok(!(await hasPromiseResolved(onBlockedElevenTimes)));
    274 
    275      info(`Reload the tab (time: ${i})`);
    276      await BrowserTestUtils.reloadTab(tab);
    277    }
    278 
    279    info("blockedTenTimes should resolve on the 10th reload");
    280    ({ callsToCommand, foo } = await onBlockedTenTimes);
    281    is(
    282      callsToCommand,
    283      11,
    284      "The command was called 11 times (initial call + 10 retry)"
    285    );
    286    is(foo, "baz", "The parameter was sent when the command was retried");
    287 
    288    info("Reload one more time");
    289    await BrowserTestUtils.reloadTab(tab);
    290 
    291    info(
    292      "The call to blockedElevenTimes now exceeds the maximum attempts allowed"
    293    );
    294    await Assert.rejects(
    295      onBlockedElevenTimes,
    296      e => e.name == "DiscardedBrowsingContextError",
    297      "Caught the expected error when reloading"
    298    );
    299  } finally {
    300    await cleanup(rootMessageHandler, tab);
    301  }
    302 });
    303 
    304 // Test cross-group navigations to check that the retry mechanism will
    305 // transparently switch to the new Browsing Context created by the cross-group
    306 // navigation.
    307 add_task(async function test_retry_cross_group() {
    308  const tab = BrowserTestUtils.addTab(
    309    gBrowser,
    310    "https://example.com/document-builder.sjs?html=COM" +
    311      // Attach an unload listener to prevent the page from going into bfcache,
    312      // so that pending queries will be rejected with an AbortError.
    313      "<script type='text/javascript'>window.onunload = function() {};</script>"
    314  );
    315  await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
    316  const browsingContext = tab.linkedBrowser.browsingContext;
    317 
    318  const rootMessageHandler = createRootMessageHandler(
    319    "session-id-retry-cross-group"
    320  );
    321 
    322  try {
    323    // This command hangs and only returns if the current domain is example.net.
    324    // We send the command while on example.com, perform a series of reload and
    325    // navigations, and the retry mechanism should allow onBlockedOnNetDomain to
    326    // resolve.
    327    const onBlockedOnNetDomain = rootMessageHandler.handleCommand({
    328      moduleName: "retry",
    329      commandName: "blockedOnNetDomain",
    330      destination: {
    331        type: WindowGlobalMessageHandler.type,
    332        id: browsingContext.id,
    333      },
    334      params: {
    335        foo: "bar",
    336      },
    337      retryOnAbort: true,
    338    });
    339 
    340    // This command will return when the old browsing context was discarded.
    341    const onDiscarded = rootMessageHandler.handleCommand({
    342      moduleName: "retry",
    343      commandName: "waitForDiscardedBrowsingContext",
    344      destination: {
    345        type: RootMessageHandler.type,
    346      },
    347      params: {
    348        browsingContext,
    349        retryOnAbort: true,
    350      },
    351    });
    352 
    353    info("Reload one time");
    354    await BrowserTestUtils.reloadTab(tab);
    355 
    356    info("blockedOnNetDomain should not have resolved yet");
    357    ok(!(await hasPromiseResolved(onBlockedOnNetDomain)));
    358 
    359    info("waitForDiscardedBrowsingContext should not have resolved yet");
    360    ok(!(await hasPromiseResolved(onDiscarded)));
    361 
    362    info(
    363      "Navigate to example.net with COOP headers to destroy browsing context"
    364    );
    365    await loadURL(
    366      tab.linkedBrowser,
    367      "https://example.net/document-builder.sjs?headers=Cross-Origin-Opener-Policy:same-origin&html=NET"
    368    );
    369 
    370    info("blockedOnNetDomain should resolve now");
    371    let { foo } = await onBlockedOnNetDomain;
    372    is(foo, "bar", "The parameter was sent when the command was retried");
    373 
    374    info("waitForDiscardedBrowsingContext should resolve now");
    375    await onDiscarded;
    376  } finally {
    377    await cleanup(rootMessageHandler, tab);
    378  }
    379 });
    380 
    381 async function cleanup(rootMessageHandler, tab) {
    382  const browsingContext = tab.linkedBrowser.browsingContext;
    383  // Cleanup global JSM state in the test module.
    384  await rootMessageHandler.handleCommand({
    385    moduleName: "retry",
    386    commandName: "cleanup",
    387    destination: {
    388      type: WindowGlobalMessageHandler.type,
    389      id: browsingContext.id,
    390    },
    391  });
    392 
    393  rootMessageHandler.destroy();
    394  gBrowser.removeTab(tab);
    395 }