tor-browser

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

browser_resources_sources.js (14698B)


      1 /* Any copyright is dedicated to the Public Domain.
      2   http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 "use strict";
      5 
      6 // Test the ResourceCommand API around SOURCE.
      7 //
      8 // We cover each Spidermonkey Debugger Source's `introductionType`:
      9 // https://searchfox.org/mozilla-central/rev/4c184ca81b28f1ccffbfd08f465709b95bcb4aa1/js/src/doc/Debugger/Debugger.Source.md#172-213
     10 //
     11 // And especially cover sources being GC-ed before DevTools are opened
     12 // which are later recreated by `ThreadActor.resurrectSource`.
     13 
     14 const ResourceCommand = require("resource://devtools/shared/commands/resource/resource-command.js");
     15 
     16 const TEST_URL = URL_ROOT_SSL + "sources.html";
     17 
     18 const TEST_JS_URL = URL_ROOT_SSL + "sources.js";
     19 const TEST_WORKER_URL = URL_ROOT_SSL + "worker-sources.js";
     20 const TEST_SW_URL = URL_ROOT_SSL + "service-worker-sources.js";
     21 
     22 async function getExpectedResources(ignoreUnresurrectedSources = false) {
     23  const htmlRequest = await fetch(TEST_URL);
     24  const htmlContent = await htmlRequest.text();
     25 
     26  // First list sources that aren't GC-ed, or that the thread actor is able to resurrect
     27  const expectedSources = [
     28    {
     29      description: "eval",
     30      sourceForm: {
     31        introductionType: "eval",
     32        sourceMapBaseURL: TEST_URL,
     33        url: null,
     34        isBlackBoxed: false,
     35        sourceMapURL: null,
     36        extensionName: null,
     37        isInlineSource: false,
     38      },
     39      sourceContent: {
     40        contentType: "text/javascript",
     41        source: "this.global = function evalFunction() {}",
     42      },
     43    },
     44    {
     45      description: "new Function()",
     46      sourceForm: {
     47        introductionType: "Function",
     48        sourceMapBaseURL: TEST_URL,
     49        url: null,
     50        isBlackBoxed: false,
     51        sourceMapURL: null,
     52        extensionName: null,
     53        isInlineSource: false,
     54      },
     55      sourceContent: {
     56        contentType: "text/javascript",
     57        source: "function anonymous(\n) {\nreturn 42;\n}",
     58      },
     59    },
     60    {
     61      description: "Event Handler",
     62      sourceForm: {
     63        introductionType: "eventHandler",
     64        sourceMapBaseURL: TEST_URL,
     65        url: null,
     66        isBlackBoxed: false,
     67        sourceMapURL: null,
     68        extensionName: null,
     69        isInlineSource: false,
     70      },
     71      sourceContent: {
     72        contentType: "text/javascript",
     73        source: "console.log('link')",
     74      },
     75    },
     76    {
     77      description: "inline JS inserted at runtime",
     78      sourceForm: {
     79        introductionType: "scriptElement", // This is an injectedScript at SpiderMonkey level, but is translated into scriptElement by SourceActor.form()
     80        sourceMapBaseURL: TEST_URL,
     81        url: null,
     82        isBlackBoxed: false,
     83        sourceMapURL: null,
     84        extensionName: null,
     85        isInlineSource: false,
     86      },
     87      sourceContent: {
     88        contentType: "text/javascript",
     89        source: "console.log('inline-script')",
     90      },
     91    },
     92    {
     93      description: "inline JS",
     94      sourceForm: {
     95        introductionType: "scriptElement", // This is an inlineScript at SpiderMonkey level, but is translated into scriptElement by SourceActor.form()
     96        sourceMapBaseURL: TEST_URL,
     97        url: TEST_URL,
     98        isBlackBoxed: false,
     99        sourceMapURL: null,
    100        extensionName: null,
    101        isInlineSource: true,
    102      },
    103      sourceContent: {
    104        contentType: "text/html",
    105        source: htmlContent,
    106      },
    107    },
    108    {
    109      description: "worker script",
    110      sourceForm: {
    111        introductionType: undefined,
    112        sourceMapBaseURL: TEST_WORKER_URL,
    113        url: TEST_WORKER_URL,
    114        isBlackBoxed: false,
    115        sourceMapURL: null,
    116        extensionName: null,
    117        isInlineSource: false,
    118      },
    119      sourceContent: {
    120        contentType: "text/javascript",
    121        source: "/* eslint-disable */\nfunction workerSource() {}\n",
    122      },
    123    },
    124    {
    125      description: "service worker script",
    126      sourceForm: {
    127        introductionType: undefined,
    128        sourceMapBaseURL: TEST_SW_URL,
    129        url: TEST_SW_URL,
    130        isBlackBoxed: false,
    131        sourceMapURL: null,
    132        extensionName: null,
    133        isInlineSource: false,
    134      },
    135      sourceContent: {
    136        contentType: "text/javascript",
    137        source: "/* eslint-disable */\nfunction serviceWorkerSource() {}\n",
    138      },
    139    },
    140    {
    141      description: "independent js file",
    142      sourceForm: {
    143        introductionType: "scriptElement", // This is an srcScript at SpiderMonkey level, but is translated into scriptElement by SourceActor.form()
    144        sourceMapBaseURL: TEST_JS_URL,
    145        url: TEST_JS_URL,
    146        isBlackBoxed: false,
    147        sourceMapURL: null,
    148        extensionName: null,
    149        isInlineSource: false,
    150      },
    151      sourceContent: {
    152        contentType: "text/javascript",
    153        source: "/* eslint-disable */\nfunction scriptSource() {}\n",
    154      },
    155    },
    156    {
    157      description: "DOM Timer",
    158      sourceForm: {
    159        introductionType: "domTimer",
    160        sourceMapBaseURL: TEST_URL,
    161        url: null,
    162        isBlackBoxed: false,
    163        sourceMapURL: null,
    164        extensionName: null,
    165        isInlineSource: false,
    166      },
    167      sourceContent: {
    168        contentType: "text/javascript",
    169        /* the domTimer is prefixed by many empty lines in order to be positioned at the same line
    170           as in the HTML file where setTimeout is called.
    171           This is probably done by SourceActor.actualText().
    172           So the array size here, should be updated to match the line number of setTimeout call */
    173        source: new Array(39).join("\n") + `console.log("timeout")`,
    174      },
    175    },
    176  ];
    177 
    178  // Now list the sources that could be GC-ed for which the thread actor isn't able to resurrect.
    179  // This is the sources that we can't assert when we fetch sources after the page is already loaded.
    180  const unresurrectedSources = [
    181    {
    182      description: "javascript URL",
    183      sourceForm: {
    184        introductionType: "javascriptURL",
    185        sourceMapBaseURL: "about:blank",
    186        url: null,
    187        isBlackBoxed: false,
    188        sourceMapURL: null,
    189        extensionName: null,
    190        isInlineSource: false,
    191      },
    192      sourceContent: {
    193        contentType: "text/javascript",
    194        source: "'666'",
    195      },
    196    },
    197    {
    198      description: "srcdoc attribute on iframes #1",
    199      sourceForm: {
    200        introductionType: "scriptElement",
    201        // We do not assert url/sourceMapBaseURL as it includes the Debugger.Source.id
    202        // which is random
    203        isBlackBoxed: false,
    204        sourceMapURL: null,
    205        extensionName: null,
    206        isInlineSource: false,
    207      },
    208      sourceContent: {
    209        contentType: "text/javascript",
    210        source: "console.log('srcdoc')",
    211      },
    212    },
    213    {
    214      description: "srcdoc attribute on iframes #2",
    215      sourceForm: {
    216        introductionType: "scriptElement",
    217        // We do not assert url/sourceMapBaseURL as it includes the Debugger.Source.id
    218        // which is random
    219        isBlackBoxed: false,
    220        sourceMapURL: null,
    221        extensionName: null,
    222        isInlineSource: false,
    223      },
    224      sourceContent: {
    225        contentType: "text/javascript",
    226        source: "console.log('srcdoc 2')",
    227      },
    228    },
    229  ];
    230 
    231  if (ignoreUnresurrectedSources) {
    232    return expectedSources;
    233  }
    234  return expectedSources.concat(unresurrectedSources);
    235 }
    236 
    237 add_task(async function testSourcesOnload() {
    238  // Load an blank document first, in order to load the test page only once we already
    239  // started watching for sources
    240  const tab = await addTab("about:blank");
    241 
    242  const commands = await CommandsFactory.forTab(tab);
    243  const { targetCommand, resourceCommand } = commands;
    244 
    245  // Force the target list to cover workers and debug all the targets
    246  targetCommand.listenForWorkers = true;
    247  targetCommand.listenForServiceWorkers = true;
    248  await targetCommand.startListening();
    249 
    250  info("Check already available resources");
    251  const availableResources = [];
    252  await resourceCommand.watchResources([resourceCommand.TYPES.SOURCE], {
    253    onAvailable: resources => availableResources.push(...resources),
    254  });
    255 
    256  const promiseLoad = BrowserTestUtils.browserLoaded(
    257    gBrowser.selectedBrowser,
    258    false,
    259    TEST_URL
    260  );
    261  BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, TEST_URL);
    262  await promiseLoad;
    263 
    264  // Some sources may be created after the document is done loading (like eventHandler usecase)
    265  // so we may be received *after* watchResource resolved
    266  const expectedResources = await getExpectedResources();
    267  await waitFor(
    268    () => availableResources.length >= expectedResources.length,
    269    "Got all the sources"
    270  );
    271 
    272  await assertResources(availableResources, expectedResources);
    273 
    274  await commands.destroy();
    275 
    276  await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
    277    // registrationPromise is set by the test page.
    278    const registration = await content.wrappedJSObject.registrationPromise;
    279    registration.unregister();
    280  });
    281 });
    282 
    283 // Bug 1767772: Skipped via add_task(...).skip() for very frequent intermittent
    284 // failures.
    285 add_task(async function testGarbagedCollectedSources() {
    286  info(
    287    "Assert SOURCES on an already loaded page with some sources that have been GC-ed"
    288  );
    289  const tab = await addTab(TEST_URL);
    290 
    291  info("Force some GC to free some sources");
    292  // GC are not always guaranteed to be effective in one call,
    293  // so increase our chances of effectively free objects by doing two with a pause between them.
    294  await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
    295    Cu.forceGC();
    296    Cu.forceCC();
    297  });
    298  await wait(500);
    299  await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
    300    Cu.forceGC();
    301    Cu.forceCC();
    302  });
    303 
    304  const commands = await CommandsFactory.forTab(tab);
    305  const { targetCommand, resourceCommand } = commands;
    306 
    307  // Force the target list to cover workers and debug all the targets
    308  targetCommand.listenForWorkers = true;
    309  targetCommand.listenForServiceWorkers = true;
    310  await targetCommand.startListening();
    311 
    312  info("Check already available resources");
    313  const availableResources = [];
    314  await resourceCommand.watchResources([resourceCommand.TYPES.SOURCE], {
    315    onAvailable: resources => availableResources.push(...resources),
    316  });
    317 
    318  // Some sources may be created after the document is done loading (like eventHandler usecase)
    319  // so we may be received *after* watchResource resolved
    320  const expectedResources = await getExpectedResources(true);
    321  await waitFor(
    322    () => availableResources.length >= expectedResources.length,
    323    "Got all the sources"
    324  );
    325 
    326  await assertResources(availableResources, expectedResources);
    327 
    328  await commands.destroy();
    329 
    330  await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
    331    // registrationPromise is set by the test page.
    332    const registration = await content.wrappedJSObject.registrationPromise;
    333    registration.unregister();
    334  });
    335 }).skip();
    336 
    337 /**
    338 * Assert that evaluating sources for a new global, in the parent process
    339 * using the shared system principal will spawn SOURCE resources.
    340 *
    341 * For this we use a special `commands` which replicate what browser console
    342 * and toolbox use.
    343 */
    344 add_task(async function testParentProcessPrivilegedSources() {
    345  // Use a custom loader + server + client in order to spawn the server
    346  // in a distinct system compartment, so that it can see the system compartment
    347  // sandbox we are about to create in this test
    348  const client = await CommandsFactory.spawnClientToDebugSystemPrincipal();
    349 
    350  const commands = await CommandsFactory.forMainProcess({ client });
    351  await commands.targetCommand.startListening();
    352  const { resourceCommand } = commands;
    353 
    354  info("Check already available resources");
    355  const availableResources = [];
    356  await resourceCommand.watchResources([resourceCommand.TYPES.SOURCE], {
    357    onAvailable: resources => availableResources.push(...resources),
    358  });
    359  ok(
    360    !!availableResources.length,
    361    "We get many sources reported from a multiprocess command"
    362  );
    363 
    364  // Clear the list of sources
    365  availableResources.length = 0;
    366 
    367  // Force the creation of a new privileged source
    368  const systemPrincipal = Cc["@mozilla.org/systemprincipal;1"].createInstance(
    369    Ci.nsIPrincipal
    370  );
    371  const sandbox = Cu.Sandbox(systemPrincipal);
    372  Cu.evalInSandbox("function foo() {}", sandbox, null, "http://foo.com");
    373 
    374  info("Wait for the sandbox source");
    375  await waitFor(() => {
    376    return availableResources.some(
    377      resource => resource.url == "http://foo.com/"
    378    );
    379  });
    380 
    381  const expectedResources = [
    382    {
    383      description: "privileged sandbox script",
    384      sourceForm: {
    385        introductionType: undefined,
    386        sourceMapBaseURL: "http://foo.com/",
    387        url: "http://foo.com/",
    388        isBlackBoxed: false,
    389        sourceMapURL: null,
    390        extensionName: null,
    391        isInlineSource: false,
    392      },
    393      sourceContent: {
    394        contentType: "text/javascript",
    395        source: "function foo() {}",
    396      },
    397    },
    398  ];
    399  const matchingResource = availableResources.filter(resource =>
    400    resource.url.includes("http://foo.com")
    401  );
    402  await assertResources(matchingResource, expectedResources);
    403 
    404  await commands.destroy();
    405 });
    406 
    407 async function assertResources(resources, expected) {
    408  is(
    409    resources.length,
    410    expected.length,
    411    "Length of existing resources is correct at initial"
    412  );
    413  for (let i = 0; i < resources.length; i++) {
    414    await assertResource(resources[i], expected);
    415  }
    416 }
    417 
    418 async function assertResource(source, expected) {
    419  is(
    420    source.resourceType,
    421    ResourceCommand.TYPES.SOURCE,
    422    "Resource type is correct"
    423  );
    424 
    425  const threadFront = await source.targetFront.getFront("thread");
    426  // `source` is SourceActor's form()
    427  // so try to instantiate the related SourceFront:
    428  const sourceFront = threadFront.source(source);
    429  // then fetch source content
    430  const sourceContent = await sourceFront.source();
    431 
    432  // Order of sources is random, so we have to find the best expected resource.
    433  // The only unique attribute is the JS Source text content.
    434  const matchingExpected = expected.find(res => {
    435    return res.sourceContent.source == sourceContent.source;
    436  });
    437  ok(
    438    matchingExpected,
    439    `This source was expected with source content being "${sourceContent.source}"`
    440  );
    441  info(`Found "#${matchingExpected.description}"`);
    442  assertObject(
    443    sourceContent,
    444    matchingExpected.sourceContent,
    445    matchingExpected.description
    446  );
    447 
    448  assertObject(
    449    source,
    450    matchingExpected.sourceForm,
    451    matchingExpected.description
    452  );
    453 }
    454 
    455 function assertObject(object, expected, description) {
    456  for (const field in expected) {
    457    is(
    458      object[field],
    459      expected[field],
    460      `The value of ${field} is correct for "#${description}"`
    461    );
    462  }
    463 }