"use strict"; add_setup(() => { Services.prefs.setBoolPref("extensions.manifestV3.enabled", true); Services.prefs.setBoolPref("extensions.dnr.enabled", true); Services.prefs.setBoolPref("extensions.dnr.feedback", true); // Don't turn warnings in errors, to make sure that the parameter validation // tests verify real-world behavior, instead of the stricter test-only mode. ExtensionTestUtils.failOnSchemaWarnings(false); }); // This function is serialized and called in the context of the test extension's // background page. dnrTestUtils is passed to the background function. function makeDnrTestUtils() { const dnrTestUtils = {}; const dnr = browser.declarativeNetRequest; function makeDummyAction(type) { switch (type) { case "redirect": return { type, redirect: { url: "https://example.com/dummy" } }; case "modifyHeaders": return { type, responseHeaders: [{ operation: "append", header: "x", value: "y" }], }; default: return { type }; } } function makeDummyRequest() { // A value that matches the condition from makeDummyRule(). return { url: "https://example.com/some-dummy-url", type: "main_frame" }; } function makeDummyRule(id, actionType) { return { id, // condition matches makeDummyRequest(). condition: { resourceTypes: ["main_frame"] }, action: makeDummyAction(actionType), }; } async function testMatchesRequest(request, ruleIds, description) { browser.test.assertDeepEq( ruleIds, (await dnr.testMatchOutcome(request)).matchedRules.map(mr => mr.ruleId), description ); } async function testCanMatchAnyBlock({ matchedRequests, nonMatchedRequests }) { await dnr.updateSessionRules({ addRules: [ { // A rule that is supposed to match everything. id: 1, condition: { excludedResourceTypes: [] }, action: { type: "block" }, }, ], }); for (let request of matchedRequests) { await testMatchesRequest( request, [1], `${JSON.stringify(request)} - should match wildcard DNR block rule` ); } for (let request of nonMatchedRequests) { await testMatchesRequest( request, [], `${JSON.stringify(request)} - should not match any DNR rule` ); } await dnr.updateSessionRules({ removeRuleIds: [1] }); } async function testCanUseAction(type, canUse) { await dnr.updateSessionRules({ addRules: [makeDummyRule(1, type)] }); await testMatchesRequest( makeDummyRequest(), canUse ? [1] : [], `${type} - should${canUse ? "" : " not"} match` ); await dnr.updateSessionRules({ removeRuleIds: [1] }); } Object.assign(dnrTestUtils, { makeDummyAction, makeDummyRequest, makeDummyRule, testMatchesRequest, testCanMatchAnyBlock, testCanUseAction, }); return dnrTestUtils; } async function runAsDNRExtension({ background, manifest, unloadTestAtEnd = true, }) { let extension = ExtensionTestUtils.loadExtension({ background: `(${background})((${makeDnrTestUtils})())`, manifest: { manifest_version: 3, permissions: ["declarativeNetRequest", "declarativeNetRequestFeedback"], host_permissions: [""], granted_host_permissions: true, ...manifest, }, temporarilyInstalled: true, // <-- for granted_host_permissions }); await extension.startup(); await extension.awaitFinish(); if (unloadTestAtEnd) { await extension.unload(); } return extension; } add_task(async function validate_required_params() { await runAsDNRExtension({ background: async () => { const testMatchOutcome = browser.declarativeNetRequest.testMatchOutcome; browser.test.assertThrows( () => testMatchOutcome({ type: "image" }), /Type error for parameter request \(Property "url" is required\)/, "url is required" ); browser.test.assertThrows( () => testMatchOutcome({ url: "https://example.com/" }), /Type error for parameter request \(Property "type" is required\)/, "resource type is required" ); browser.test.assertDeepEq( { matchedRules: [] }, await testMatchOutcome({ url: "https://example.com/", type: "image" }), "testMatchOutcome with url and type succeeds" ); browser.test.notifyPass(); }, }); }); add_task(async function resource_type_validation() { await runAsDNRExtension({ background: async () => { const testMatchOutcome = browser.declarativeNetRequest.testMatchOutcome; const url = "https://example.com/some-dummy-url"; browser.test.assertThrows( () => testMatchOutcome({ url, type: "MAIN_FRAME" }), /Error processing type: Invalid enumeration value "MAIN_FRAME"/, "testMatchOutcome should expects a lowercase type" ); // Check that at least one ResourceType exists. browser.test.assertEq( "main_frame", browser.declarativeNetRequest.ResourceType.MAIN_FRAME, "ResourceType.MAIN_FRAME exists" ); for (let type of Object.values( browser.declarativeNetRequest.ResourceType )) { browser.test.assertDeepEq( { matchedRules: [] }, await testMatchOutcome({ url, type }), `testMatchOutcome for type=${type} is allowed` ); } browser.test.notifyPass(); }, }); }); add_task(async function url_validation() { await runAsDNRExtension({ background: async dnrTestUtils => { const dnr = browser.declarativeNetRequest; const { testMatchesRequest } = dnrTestUtils; const type = "other"; // Dummy resource type. await dnr.updateSessionRules({ addRules: [{ id: 1, condition: {}, action: { type: "block" } }], }); const supportedUrls = [ // All schemes that are potentially hooked up to the network are here. "http://example.com/", "https://example.com/", // While host permissions permits more (e.g. file:, moz-extension:), // we don't list them here since they are not hooked up to the network. // Trying to match such URLs is undefined behavior for now. ]; const supportedInitiators = [ // Supported URLs are also supported initiators. ...supportedUrls, // Note: moz-extension: has more tests in match_initiator_moz_extension. `moz-extension://${location.host}`, "file:///tmp/", // data:-URIs have a null principal. "data:text/plain,", ]; const disallowedUrlsOrInitiators = [ // about:-URI with system principal: "about:config", // Unprivileged about:-URL: "about:logo", "chrome://extensions/content/dummy.xhtml", "resource://pdf.js/web/viewer.html", // Extensions cannot see "view-source", only the result: bug 1683646. "view-source:http://example.com/", "view-source:about:config", // blob:-URLs do not go through the network. An actual network request // will never have a blob-URI as initiator, always the actual principal // URI. We don't try to extract the actual principal from the blob:-URI // because that is expensive and also performs a validation that the // blob:-URI is still valid, so testMatchOutcome could then return // inconsistent results. URL.createObjectURL(new Blob([])), ]; const disallowedUrls = [ ...disallowedUrlsOrInitiators, // data:-URIs are not hooked up to the network (bug 1631933), so we do // not support it in the testMatchOutcome API, even though the URL // matches "". "data:text/plain,", ]; const disallowedInitiator = [ ...disallowedUrlsOrInitiators, // "about:blank" inherits the principal or is null. testMatchOutcome // does not offer a way to specify it more precisely. "about:blank", // This is bogus: A principal URL can never be about:srcdoc. It is // always inherit from something. "about:srcdoc", "moz-extension://someone-elses-extension-here", ]; for (let url of supportedUrls) { await testMatchesRequest({ url, type }, [1], `Supported url: ${url}`); } for (let initiator of supportedInitiators) { await testMatchesRequest( { url: "http://example.com/", type, initiator }, [1], `Supported initiator: ${initiator}` ); } for (let url of disallowedUrls) { await testMatchesRequest({ type, url }, [], `Disallowed url: ${url}`); } for (let initiator of disallowedInitiator) { await testMatchesRequest( { url: "http://example.com/", type, initiator }, [], `Disallowed initiator: ${initiator}` ); } browser.test.notifyPass(); }, }); }); add_task(async function rule_priority_and_action_type_precedence() { await runAsDNRExtension({ background: async dnrTestUtils => { const dnr = browser.declarativeNetRequest; const { makeDummyRule, makeDummyRequest } = dnrTestUtils; await dnr.updateSessionRules({ addRules: [ makeDummyRule(1, "allow"), makeDummyRule(2, "allowAllRequests"), makeDummyRule(3, "block"), makeDummyRule(4, "upgradeScheme"), makeDummyRule(5, "redirect"), makeDummyRule(6, "modifyHeaders"), { ...makeDummyRule(7, "modifyHeaders"), priority: 2 }, { ...makeDummyRule(8, "allow"), priority: 2 }, { ...makeDummyRule(9, "block"), priority: 2 }, // Repeat rules so that we can verify that the outcome is due to the // rule action, instead of the rule ID / input order. makeDummyRule(11, "allow"), makeDummyRule(12, "allowAllRequests"), makeDummyRule(13, "block"), makeDummyRule(14, "upgradeScheme"), makeDummyRule(15, "redirect"), makeDummyRule(16, "modifyHeaders"), { ...makeDummyRule(17, "modifyHeaders"), priority: 2 }, ], }); async function testAndRemove(ruleId, expectedRuleIds, description) { browser.test.assertDeepEq( expectedRuleIds.map(ruleId => ({ ruleId, rulesetId: "_session" })), (await dnr.testMatchOutcome(makeDummyRequest())).matchedRules, description ); await dnr.updateSessionRules({ removeRuleIds: [ruleId] }); } await testAndRemove(8, [8], "highest-prio allow wins"); await testAndRemove(9, [9], "highest-prio block wins"); // after this point, we only have same-prio rules and two higher-prio // modifyHeaders rules (7 & 17). await testAndRemove( 1, [1, 7, 17], "1st allow ignores other rules, except for higher-prio modifyHeaders" ); await testAndRemove( 11, [11, 7, 17], "2nd allow ignores other rules, except for higher-prio modifyHeaders" ); await testAndRemove( 2, [2, 7, 17], "1st allowAllRequests ignores other rules, except for higher-prio modifyHeaders" ); await testAndRemove( 12, [12, 7, 17], "2nd allowAllRequests ignores other rules, except for higher-prio modifyHeaders" ); await testAndRemove(3, [3], "1st block > all other actions"); await testAndRemove(13, [13], "2nd block > all other actions"); await testAndRemove(4, [4], "1st upgradeScheme > redirect"); await testAndRemove(14, [14], "2nd upgradeScheme > redirect"); await testAndRemove(5, [5], "1st redirect > modifyHeaders"); await testAndRemove(15, [15], "2nd redirect > modifyHeaders"); await testAndRemove( 6, [7, 17, 6, 16], "All modifyHeaders match if there is no other action" ); // Verify that a new rule takes precedence again. await dnr.updateSessionRules({ addRules: [makeDummyRule(11, "allow")], }); await testAndRemove( 11, [11, 7, 17], "After adding an allow rule, only higher-prio modifyHeaders are shown" ); browser.test.assertDeepEq( [7, 16, 17], (await dnr.getSessionRules()).map(r => r.id), "Remaining rules at end of test" ); browser.test.notifyPass(); }, }); }); add_task(async function declarativeNetRequest_and_host_permissions() { await runAsDNRExtension({ background: async dnrTestUtils => { const { testCanUseAction, testCanMatchAnyBlock } = dnrTestUtils; // Unlocked by declarativeNetRequest permission: await testCanUseAction("allow", true); await testCanUseAction("allowAllRequests", true); await testCanUseAction("block", true); await testCanUseAction("upgradeScheme", true); // Unlocked by host permissions: await testCanUseAction("redirect", true); await testCanUseAction("modifyHeaders", true); const url = "https://example.com/"; await testCanMatchAnyBlock({ matchedRequests: [ { url, type: "other" }, { url, type: "main_frame" }, { url, type: "sub_frame" }, { url, initiator: url, type: "other" }, { url, initiator: url, type: "main_frame" }, { url, initiator: url, type: "sub_frame" }, ], nonMatchedRequests: [], }); browser.test.notifyPass(); }, }); }); add_task(async function declarativeNetRequest_permission_only() { await runAsDNRExtension({ manifest: { host_permissions: [], }, background: async dnrTestUtils => { const { testCanUseAction, testCanMatchAnyBlock } = dnrTestUtils; // Unlocked by declarativeNetRequest permission: await testCanUseAction("allow", true); await testCanUseAction("allowAllRequests", true); await testCanUseAction("block", true); await testCanUseAction("upgradeScheme", true); // These require host permissions, which we don't have: await testCanUseAction("redirect", false); await testCanUseAction("modifyHeaders", false); const url = "https://example.com/"; await testCanMatchAnyBlock({ matchedRequests: [ { url, type: "other" }, { url, type: "main_frame" }, { url, type: "sub_frame" }, { url, initiator: url, type: "other" }, { url, initiator: url, type: "main_frame" }, { url, initiator: url, type: "sub_frame" }, ], nonMatchedRequests: [], }); browser.test.notifyPass(); }, }); }); add_task(async function declarativeNetRequestWithHostAccess_only() { await runAsDNRExtension({ manifest: { permissions: [ "declarativeNetRequestWithHostAccess", "declarativeNetRequestFeedback", ], host_permissions: [], }, background: async dnrTestUtils => { const { testCanUseAction } = dnrTestUtils; // declarativeNetRequestWithHostAccess requires host permissions, // which we don't have. So none of the rules should match: await testCanUseAction("allow", false); await testCanUseAction("allowAllRequests", false); await testCanUseAction("block", false); await testCanUseAction("upgradeScheme", false); await testCanUseAction("redirect", false); await testCanUseAction("modifyHeaders", false); browser.test.notifyPass(); }, }); }); add_task(async function declarativeNetRequestWithHostAccess_and_host_perm() { await runAsDNRExtension({ manifest: { permissions: [ "declarativeNetRequestWithHostAccess", "declarativeNetRequestFeedback", ], // Origin used by makeDummyRequest() & makeDummyRule(): host_permissions: ["https://example.com/"], }, background: async dnrTestUtils => { const { testCanUseAction, testCanMatchAnyBlock } = dnrTestUtils; // declarativeNetRequestWithHostAccess + host permissions allows all: await testCanUseAction("allow", true); await testCanUseAction("allowAllRequests", true); await testCanUseAction("block", true); await testCanUseAction("upgradeScheme", true); await testCanUseAction("redirect", true); await testCanUseAction("modifyHeaders", true); const url = "https://example.com/"; const urlNoPerm = "https://example.net/?not_in:host_permissions"; await testCanMatchAnyBlock({ matchedRequests: [ { url, type: "other" }, { url, type: "main_frame" }, { url, type: "sub_frame" }, // Navigations do no require host permissions for initiator. { url, initiator: urlNoPerm, type: "main_frame" }, { url, initiator: urlNoPerm, type: "sub_frame" }, ], nonMatchedRequests: [ // url always requires declarativeNetRequest or host permissions. { url: urlNoPerm, type: "other" }, // Non-navigations require host permissions for initiator. { url, initiator: urlNoPerm, type: "other" }, ], }); browser.test.notifyPass(); }, }); }); // Tests: resourceTypes, excludedResourceTypes // Tests: requestMethods, excludedRequestMethods add_task(async function match_condition_types_and_methods() { await runAsDNRExtension({ background: async dnrTestUtils => { const dnr = browser.declarativeNetRequest; const { makeDummyAction, testMatchesRequest } = dnrTestUtils; // "modifyHeaders" is the only action that allows multiple rule matches. const action = makeDummyAction("modifyHeaders"); await dnr.updateSessionRules({ addRules: [ { id: 1, condition: { resourceTypes: ["xmlhttprequest"], requestMethods: ["put"], }, action, }, { id: 2, condition: { excludedResourceTypes: ["sub_frame"], excludedRequestMethods: ["post"], }, action, }, { id: 3, condition: { // resourceTypes not specified should imply all-minus-main_frame. requestMethods: ["get", "post"], }, action, }, { id: 4, condition: { resourceTypes: ["main_frame", "xmlhttprequest"], excludedRequestMethods: ["get"], }, action, }, ], }); const url = "https://example.com/some-dummy-url"; await testMatchesRequest( { url, type: "main_frame" }, [2], "main_frame + GET" ); await testMatchesRequest( { url, type: "xmlhttprequest" }, [2, 3], "xmlhttprequest + GET" ); await testMatchesRequest( { url, type: "xmlhttprequest", method: "put" }, [1, 2, 4], "xmlhttprequest + PUT" ); await testMatchesRequest( { url, type: "sub_frame", method: "post" }, [3], "sub_frame + POST" ); await testMatchesRequest( { url, type: "sub_frame", method: "post" }, [3], "sub_frame + POST" ); browser.test.notifyPass(); }, }); }); // Tests: requestDomains, excludedRequestDomains add_task(async function match_request_domains() { await runAsDNRExtension({ background: async dnrTestUtils => { const dnr = browser.declarativeNetRequest; const { makeDummyAction, testMatchesRequest } = dnrTestUtils; // "modifyHeaders" is the only action that allows multiple rule matches. const action = makeDummyAction("modifyHeaders"); await dnr.updateSessionRules({ addRules: [ { id: 1, condition: { requestDomains: ["a.com", "www.b.com"], }, action, }, { id: 2, condition: { excludedRequestDomains: ["a.com", "www.b.com", "127.0.0.1"], }, action, }, { id: 3, condition: { requestDomains: ["one.net"], excludedRequestDomains: ["sub.one.net"], }, action, }, { id: 4, condition: { // This can never match. requestDomains: ["sub.one.net"], excludedRequestDomains: ["one.net"], }, action, }, { id: 5, condition: { requestDomains: ["127.0.0.1", "[::1]"], }, action, }, { id: 6, condition: { requestDomains: [ "~b.com", // "~" should not be interpreted as pattern negation. ], }, action, }, { id: 7, condition: { // A canonical domain does not start with a ".". Domains filters // starting with a "." are therefore not matching anything. requestDomains: [".a.com"], }, action, }, ], }); const type = "sub_frame"; // Tests related to a.com: await testMatchesRequest( { url: "https://a.com:1234/path", type }, [1], "a.com: url's domain is equal to a.com" ); await testMatchesRequest( { url: "http://sub.a.com/", type }, [1], "sub.a.com: url is subdomain of a.com" ); await testMatchesRequest( { url: "http://nota.com/a.com?a.com#a.com", type }, [2], "nota.com: url's domain does not match a.com" ); await testMatchesRequest( { url: "http://a.com.not/a.com?a.com#a.com", type }, [2], "a.com.not: url's domain does not match a.com" ); await testMatchesRequest( { url: "http://a.com./a.com?a.com#a.com", type }, [2], "a.com.: url's domain (ending with dot) does not match a.com" ); // Tests related to www.b.com: await testMatchesRequest( { url: "http://www.b.com/", type }, [1], "www.b.com: url's domain is equal to www.b.com" ); await testMatchesRequest( { url: "http://sub.www.b.com", type }, [1], "sub.www.b.com: url's domain is a subdomain of www.b.com" ); await testMatchesRequest( { url: "http://b.com/", type }, [2], "b.com: url's domain is a superdomain, NOT a subdomain of www.b.com" ); // Tests related to sub.one.net / one.net await testMatchesRequest( { url: "http://one.net/", type }, [2, 3], "one.net: url's domain matches one.net, but not sub.one.net" ); await testMatchesRequest( { url: "http://sub.one.net/", type }, [2], // Rule 4 was a candidate, but excluded anyway. "sub.one.net: url's domain matches sub.one.net, but excluded by one.net" ); // Tests related to IP addresses await testMatchesRequest( { url: "http://127.0.0.1:8080/", type }, [5], "127.0.0.1: IP address is exact match for 127.0.0.1" ); await testMatchesRequest( { url: "http://8.8.8.8/", type }, [2], "8.8.8.8: not matched by any of the domains" ); await testMatchesRequest( { url: "http://[::1]/", type }, [2, 5], "[::1]: IPv6 matches with bracket" ); // For completeness, verify that the non-resolving domain "~b.com" // matches the input, so that we know that "~" was not given special // treatment. In filter list syntax, "~" before the domain negates the // meaning, but that should not be supported in DNR. await testMatchesRequest( { url: "http://~b.com/", type }, [2, 6], "~b.com: Although a non-resolving domain, it matches the pattern" ); // match_initiator_domains has more tests; here we just confirm that // requestDomains rules don't match initiator. await testMatchesRequest( { url: "http://url.does.not.match/", type, initiator: "http://a.com/" }, [2], "requestDomains should not match initiator URL" ); browser.test.notifyPass(); }, }); }); add_task(async function match_request_domains_punycode() { await runAsDNRExtension({ background: async dnrTestUtils => { const dnr = browser.declarativeNetRequest; const { makeDummyAction, testMatchesRequest } = dnrTestUtils; // "modifyHeaders" is the only action that allows multiple rule matches. const action = makeDummyAction("modifyHeaders"); // Note that the non-punycode domains are rejected by schema validation, // and checked by test validate_domains in test_ext_dnr_session_rules.js. await dnr.updateSessionRules({ addRules: [ { id: 1, condition: { // straß.de requestDomains: ["xn--stra-yna.de"], }, action, }, { id: 2, condition: { // IDNA2003 converted ß to ss. But IDNA2008 requires punycode. requestDomains: ["strass.de", "stras.de"], }, action, }, ], }); const type = "sub_frame"; await testMatchesRequest( { url: "https://straß.de/", type }, [1], "straß.de matches" ); await testMatchesRequest( { url: "https://xn--stra-yna.de/", type }, [1], "xn--stra-yna.de matches" ); await testMatchesRequest( { url: "https://strass.de/", type }, [2], "strass.de does not match the punycode pattern of straß" ); await testMatchesRequest( { url: "https://stras.de/", type }, [2], "stras.de does not match the punycode pattern of straß" ); browser.test.notifyPass(); }, }); }); // Tests: initiatorDomains, excludedInitiatorDomains // More tests in: match_initiator_moz_extension. add_task(async function match_initiator_domains() { await runAsDNRExtension({ background: async dnrTestUtils => { const dnr = browser.declarativeNetRequest; const { makeDummyAction, testMatchesRequest } = dnrTestUtils; // "modifyHeaders" is the only action that allows multiple rule matches. const action = makeDummyAction("modifyHeaders"); // The validation of initiatorDomains and requestDomains are shared. // The match_request_domains and match_request_domains_punycode tests // already verify semantics; this test just tests that the conditional // logic works as expected, plus coverage for initiator being void. await dnr.updateSessionRules({ addRules: [ { id: 1, condition: { initiatorDomains: ["a.com"], }, action, }, { id: 2, condition: { excludedInitiatorDomains: ["a.com"], }, action, }, { id: 3, condition: { initiatorDomains: ["c.com"], excludedInitiatorDomains: ["c.com"], }, action, }, { id: 4, // To verify that it does not match a void initiator. condition: { initiatorDomains: ["null"], }, action, }, { id: 5, condition: { excludedInitiatorDomains: ["null", "undefined"], }, action, }, { id: 6, // To verify that it does not match a void initiator. condition: { initiatorDomains: ["undefined"], }, action, }, ], }); const url = "https://do.not.look.here/look_at_initator_instead"; const type = "image"; await testMatchesRequest( { url, type, initiator: "http://a.com/" }, [1, 5], "initiatorDomains matches" ); await testMatchesRequest( { url, type, initiator: "http://b.com/" }, [2, 5], "excludedInitiatorDomains does not match, so request matched" ); await testMatchesRequest( { url, type, initiator: "http://c.com/" }, [2, 5], // 3 is not here, despite containing "c.com". "excludedInitiatorDomains takes precedence over initiatorDomains" ); // When initiator is not specified, rules with initiatorDomains should not // match, and rules with excludedInitiatorDomains may match. await testMatchesRequest( { url, type }, [2, 5], "request without initiator matches every excludedInitiatorDomains" ); // http://null is unlikely to exist in practice. Regardless, verify that // it won't match a void initiators. await testMatchesRequest( { url, type, initiator: "http://null/" }, [2, 4], "http://null is matched by the 'null' domain" ); await testMatchesRequest( { url, type, initiator: "http://undefined/" }, [2, 6], "http://null is matched by the 'undefined' domain" ); await testMatchesRequest( { url: "http://a.com/", type }, [2, 5], "initiatorDomains should not match the request URL (initiator=null)" ); browser.test.notifyPass(); }, }); }); // Tests: initiatorDomains, excludedInitiatorDomains with moz-extension:-URLs. add_task(async function match_initiator_moz_extension() { let extension = await runAsDNRExtension({ manifest: { browser_specific_settings: { gecko: { id: "other@ext" } } }, background: async dnrTestUtils => { const dnr = browser.declarativeNetRequest; const { makeDummyAction, testMatchesRequest } = dnrTestUtils; // "modifyHeaders" is the only action that allows multiple rule matches. // But we cannot use "modifyHeaders" because that feature depends on // access to "triggering principal". Fortunately, the two test rules in // this test case are mutually exclusive, so the block action works. // TODO bug 1825824: change to makeDummyAction("modifyHeaders"). const action = makeDummyAction("block"); const thisExtensionUUID = location.hostname; await dnr.updateSessionRules({ addRules: [ { id: 1, condition: { initiatorDomains: [thisExtensionUUID], }, action, }, { id: 2, condition: { excludedInitiatorDomains: [thisExtensionUUID], }, action, }, ], }); const url = "https://do.not.look.here/look_at_initator_instead"; const type = "other"; // Sanity check with non-moz-extension:-schemes as initiator. await testMatchesRequest( { url, type, initiator: `https://${thisExtensionUUID}/` }, [1], "https:+UUID matches initiatorDomains" ); await testMatchesRequest( { url, type, initiator: "https://random-uuid-here/" }, [2], "https:+UUID matches excludedInitiatorDomains" ); // Now test with moz-extension: as initiator. await testMatchesRequest( { url, type, initiator: location.origin }, [1], "moz-extension: initiator matches when it should" ); await testMatchesRequest( { url, type, initiator: `moz-extension://random-uuid-here/` }, [], "moz-extension: from unrelated extension cannot match by default" ); browser.test.onMessage.addListener(async msg => { browser.test.assertEq("test_with_pref", msg, "expected msg"); await testMatchesRequest( { url, type, initiator: `moz-extension://random-uuid-here/` }, [2], "With pref, moz-extension: from unrelated extension can match" ); browser.test.sendMessage("test_with_pref:done"); }); // Notify to continue. We don't exit yet due to unloadTestAtEnd:false browser.test.notifyPass(); }, // Continue running the DNR extension because we want to test the current // DNR rules with other extensions. unloadTestAtEnd: false, }); info("Testing foreign moz-extension request within same ext, with pref on"); await runWithPrefs( [["extensions.dnr.match_requests_from_other_extensions", true]], async () => { extension.sendMessage("test_with_pref"); await extension.awaitMessage("test_with_pref:done"); } ); const otherExtensionUUID = extension.uuid; await runAsDNRExtension({ manifest: { // Pass the DNR extension UUID to this extension. description: otherExtensionUUID, }, background: async () => { const otherExtensionUUID = browser.runtime.getManifest().description; const dnr = browser.declarativeNetRequest; const url = "https://do.not.look.here/look_at_initator_instead"; const type = "other"; browser.test.assertDeepEq( { matchedRules: [] }, await dnr.testMatchOutcome({ url, type, initiator: location.origin }), "testMatchOutcome excludes other extensions by default" ); browser.test.assertDeepEq( { matchedRules: [] }, await dnr.testMatchOutcome( { url, type, initiator: location.origin }, { includeOtherExtensions: true } ), "No matches when initiator is moz-extension:, different from DNR ext" ); browser.test.assertDeepEq( { matchedRules: [ { ruleId: 1, rulesetId: "_session", extensionId: "other@ext" }, ], }, await dnr.testMatchOutcome( { url, type, initiator: `moz-extension://${otherExtensionUUID}` }, { includeOtherExtensions: true } ), "Simulated moz-extension: for original extension finds a match" ); browser.test.notifyPass(); }, }); info("Testing foreign moz-extension request in other ext, with pref on"); await runWithPrefs( [["extensions.dnr.match_requests_from_other_extensions", true]], async () => { await runAsDNRExtension({ manifest: { // Pass the DNR extension UUID to this extension. description: otherExtensionUUID, }, background: async () => { const otherExtensionUUID = browser.runtime.getManifest().description; const dnr = browser.declarativeNetRequest; const url = "https://do.not.look.here/look_at_initator_instead"; const type = "other"; // Sanity check: testMatchOutcome for moz-extension:-URL different // from the DNR extension and the current test extension. browser.test.assertDeepEq( { matchedRules: [ { ruleId: 2, rulesetId: "_session", extensionId: "other@ext" }, ], }, await dnr.testMatchOutcome( { url, type, initiator: "moz-extension://random-uuid-here/" }, { includeOtherExtensions: true } ), "With pref, moz-extension: from unrelated extensions can match" ); // Usually, DNR does not affect requests from other extensions. That // was checked in the previous test extension (without pref override). // Here, we check that with the pref override, testMatchOutcome can // return matches from other extensions for the given extension UUID. browser.test.assertDeepEq( { matchedRules: [ { ruleId: 2, rulesetId: "_session", extensionId: "other@ext" }, ], }, await dnr.testMatchOutcome( { url, type, initiator: location.origin }, { includeOtherExtensions: true } ), "With pref, moz-extension:-initiator different from DNR ext matches" ); // Identical test as in the previous test extension (that ran without // the pref override). This verifies that the pref does not affect the // behavior of request matching for requests within that extension. browser.test.assertDeepEq( { matchedRules: [ { ruleId: 1, rulesetId: "_session", extensionId: "other@ext" }, ], }, await dnr.testMatchOutcome( { url, type, initiator: `moz-extension://${otherExtensionUUID}` }, { includeOtherExtensions: true } ), "With pref, moz-extension: for DNR ext still matches" ); browser.test.notifyPass(); }, }); } ); await extension.unload(); }); // Tests: urlFilter. For more comprehensive tests, see // toolkit/components/extensions/test/xpcshell/test_ext_dnr_urlFilter.js add_task(async function match_urlFilter() { await runAsDNRExtension({ background: async dnrTestUtils => { const dnr = browser.declarativeNetRequest; const { makeDummyAction, testMatchesRequest } = dnrTestUtils; // "modifyHeaders" is the only action that allows multiple rule matches. const action = makeDummyAction("modifyHeaders"); await dnr.updateSessionRules({ addRules: [ // Some patterns that match literally everything: { id: 1, condition: { urlFilter: "." }, action }, { id: 2, condition: { urlFilter: "^" }, action }, { id: 3, condition: { urlFilter: "|" }, action }, // Patterns that match the test URLs { id: 4, condition: { urlFilter: "https://example.com" }, action }, { // urlFilter matches, requestDomains matches. id: 5, condition: { urlFilter: "*", requestDomains: ["example.com"] }, action, }, { // urlFilter matches, requestDomains does not match. id: 6, condition: { urlFilter: "*", requestDomains: ["notexample.com"] }, action, }, { // urlFilter does not match, requestDomains matches. id: 7, condition: { urlFilter: "notm", requestDomains: ["example.com"] }, action, }, ], }); await testMatchesRequest( { url: "https://example.com/file.txt", type: "font" }, [1, 2, 3, 4, 5], "urlFilter should match when needed, and correctly with requestDomains" ); browser.test.notifyPass(); }, }); }); // Tests: regexFilter. For more comprehensive tests, see // toolkit/components/extensions/test/xpcshell/test_ext_dnr_regexFilter.js add_task(async function match_regexFilter() { await runAsDNRExtension({ background: async dnrTestUtils => { const dnr = browser.declarativeNetRequest; const { makeDummyAction, testMatchesRequest } = dnrTestUtils; // "modifyHeaders" is the only action that allows multiple rule matches. const action = makeDummyAction("modifyHeaders"); await dnr.updateSessionRules({ addRules: [ // Some patterns that match literally everything: { id: 1, condition: { regexFilter: ".*" }, action }, { id: 2, condition: { regexFilter: "^" }, action }, // Patterns that match the test URLs { id: 3, condition: { regexFilter: "https://.xample\\." }, action }, { id: 4, condition: { regexFilter: "https://example.com" }, action }, { // regexFilter matches, requestDomains matches. id: 5, condition: { regexFilter: "$", requestDomains: ["example.com"] }, action, }, { // regexFilter matches, requestDomains does not match. id: 6, condition: { regexFilter: "$", requestDomains: ["notexample.com"] }, action, }, { // regexFilter does not match, requestDomains matches. id: 7, condition: { regexFilter: "notm", requestDomains: ["example.com"] }, action, }, ], }); await testMatchesRequest( { url: "https://example.com/file.txt", type: "font" }, [1, 2, 3, 4, 5], "regexFilter should match when needed, and correctly with requestDomains" ); browser.test.notifyPass(); }, }); }); // Tests: tabIds, excludedTabIds add_task(async function match_tabIds() { await runAsDNRExtension({ background: async dnrTestUtils => { const dnr = browser.declarativeNetRequest; const { makeDummyAction, testMatchesRequest } = dnrTestUtils; // "modifyHeaders" is the only action that allows multiple rule matches. const action = makeDummyAction("modifyHeaders"); await dnr.updateSessionRules({ addRules: [ { id: 1, condition: { excludedTabIds: [-1, Number.MAX_SAFE_INTEGER], }, action, }, { id: 2, condition: { tabIds: [1, Number.MAX_SAFE_INTEGER], }, action, }, { id: 3, condition: { tabIds: [-1], }, action, }, ], }); const url = "https://example.com/some-dummy-url"; const type = "font"; await testMatchesRequest({ url, type }, [3], "tabId defaults to -1"); await testMatchesRequest({ url, type, tabId: -1 }, [3], "tabId -1"); await testMatchesRequest({ url, type, tabId: 1 }, [1, 2], "tabId 1"); await testMatchesRequest( { url, type, tabId: Number.MAX_SAFE_INTEGER, }, [2], `tabId high number (MAX_SAFE_INTEGER=${Number.MAX_SAFE_INTEGER})` ); // tabId -2 is invalid and not encountered in practice, but technically // it matches the first rule. await testMatchesRequest({ url, type, tabId: -2 }, [1], "bad tabId -2"); browser.test.notifyPass(); }, }); }); add_task(async function action_precedence_between_extensions() { // This test is structured as follows: // - otherExtension registers rules for several numeric conditions (tabId). // - otherExtensionNonBlockAndModifyHeaders adds allowAllRequests and // modifyHeaders to all requests. // - otherExtensionModifyHeaders adds modifyHeaders rules to all requests. // - the main test extension also registers rules, and then simulates requests // with testMatchOutcome for each tabId, and checks the result. let otherExtension = await runAsDNRExtension({ manifest: { browser_specific_settings: { gecko: { id: "other@ext" } } }, background: async dnrTestUtils => { const { makeDummyAction } = dnrTestUtils; // Dummy condition for testing requests in this test. const c = tabId => ({ resourceTypes: ["main_frame"], tabIds: [tabId] }); await browser.declarativeNetRequest.updateSessionRules({ addRules: [ { id: 11, condition: c(1), action: makeDummyAction("allow") }, { id: 12, condition: c(2), action: makeDummyAction("block") }, { id: 13, condition: c(3), action: makeDummyAction("redirect") }, { id: 14, condition: c(4), action: makeDummyAction("upgradeScheme") }, { id: 15, condition: c(5), action: makeDummyAction("allowAllRequests"), }, { id: 16, condition: c(6), action: makeDummyAction("allowAllRequests"), }, ], }); // Notify to continue. We don't exit yet due to unloadTestAtEnd:false browser.test.notifyPass(); }, unloadTestAtEnd: false, }); let otherExtensionNonBlockAndModifyHeaders = await runAsDNRExtension({ manifest: { browser_specific_settings: { gecko: { id: "other@ext2" } } }, background: async dnrTestUtils => { const { makeDummyAction } = dnrTestUtils; // Matches all requests from this test. const condition = { resourceTypes: ["main_frame"] }; await browser.declarativeNetRequest.updateSessionRules({ addRules: [ { id: 1000, condition, action: makeDummyAction("modifyHeaders"), // Same-or-lower priority "modifyHeaders" actions are ignored when // an "allowAllRequests" action exists within the same extension. // Since we have such a rule (ID 1001), this modifyHeaders rule must // have "priority: 2" to avoid being ignored. priority: 2, }, { id: 1001, condition, action: makeDummyAction("allowAllRequests") }, { id: 1002, condition, action: makeDummyAction("modifyHeaders"), priority: 2, // necessary as explained above at rule ID 1000. }, // should never appear because the first allowAllRequests rule should // take precedence: { id: 1003, condition, action: makeDummyAction("allowAllRequests") }, ], }); // Notify to continue. We don't exit yet due to unloadTestAtEnd:false browser.test.notifyPass(); }, unloadTestAtEnd: false, }); // |otherExtensionModifyHeaders| and |otherExtensionNonBlockAndModifyHeaders| // both have "modifyHeaders" rules. The documented order of rules is for // the most recently installed extension to take precedence when applying // modifyHeaders actions. The "priority" key is extension-specific, so even // though |otherExtensionNonBlockAndModifyHeaders| defines "priority: 2" for // modifyHeaders action (ID 1001), the modifyHeaders below (ID 1337) takes // precedence because the extension was installed later. let otherExtensionModifyHeaders = await runAsDNRExtension({ manifest: { browser_specific_settings: { gecko: { id: "other@ext3" } } }, background: async dnrTestUtils => { const { makeDummyAction } = dnrTestUtils; await browser.declarativeNetRequest.updateSessionRules({ addRules: [ { id: 1337, // Matches all requests from this test. condition: { resourceTypes: ["main_frame"] }, action: makeDummyAction("modifyHeaders"), // Note: no "priority" key set, so defaults to 1. }, ], }); // Notify to continue. We don't exit yet due to unloadTestAtEnd:false browser.test.notifyPass(); }, unloadTestAtEnd: false, }); await runAsDNRExtension({ background: async dnrTestUtils => { const dnr = browser.declarativeNetRequest; const { makeDummyAction } = dnrTestUtils; // Dummy condition for testing requests in this test. const c = tabId => ({ resourceTypes: ["main_frame"], tabIds: [tabId] }); await dnr.updateSessionRules({ addRules: [ { id: 91, condition: c(1), action: makeDummyAction("block") }, { id: 92, condition: c(2), action: makeDummyAction("allow") }, { id: 93, condition: c(3), action: makeDummyAction("block") }, { id: 94, condition: c(4), action: makeDummyAction("block") }, { id: 95, condition: c(5), action: makeDummyAction("allow") }, { id: 96, condition: c(6), action: makeDummyAction("allowAllRequests"), }, ], }); const url = "https://example.com/dummy-url"; const type = "main_frame"; const options = { includeOtherExtensions: true }; browser.test.assertDeepEq( [{ ruleId: 91, rulesetId: "_session" }], (await dnr.testMatchOutcome({ url, type, tabId: 1 }, options)) .matchedRules, "block takes precedence over allow (from other extension)" ); browser.test.assertDeepEq( [{ ruleId: 12, rulesetId: "_session", extensionId: "other@ext" }], (await dnr.testMatchOutcome({ url, type, tabId: 2 }, options)) .matchedRules, "block (from other extension) takes precedence over allow" ); browser.test.assertDeepEq( [{ ruleId: 93, rulesetId: "_session" }], (await dnr.testMatchOutcome({ url, type, tabId: 3 }, options)) .matchedRules, "block takes precedence over redirect (from other extension)" ); browser.test.assertDeepEq( [{ ruleId: 94, rulesetId: "_session" }], (await dnr.testMatchOutcome({ url, type, tabId: 4 }, options)) .matchedRules, "block takes precedence over upgradeScheme (from other extension)" ); browser.test.assertDeepEq( [ // allow: { ruleId: 95, rulesetId: "_session" }, // allowAllRequests (newest install first): { ruleId: 1001, rulesetId: "_session", extensionId: "other@ext2" }, { ruleId: 15, rulesetId: "_session", extensionId: "other@ext" }, // modifyHeaders (see comment at otherExtensionModifyHeaders): { ruleId: 1337, rulesetId: "_session", extensionId: "other@ext3" }, { ruleId: 1000, rulesetId: "_session", extensionId: "other@ext2" }, { ruleId: 1002, rulesetId: "_session", extensionId: "other@ext2" }, ], (await dnr.testMatchOutcome({ url, type, tabId: 5 }, options)) .matchedRules, "When allow matches, allowAllRequests from other extension matches too" ); browser.test.assertDeepEq( [ // allowAllRequests (newest install first): { ruleId: 96, rulesetId: "_session" }, { ruleId: 1001, rulesetId: "_session", extensionId: "other@ext2" }, { ruleId: 16, rulesetId: "_session", extensionId: "other@ext" }, // modifyHeaders (see comment at otherExtensionModifyHeaders): { ruleId: 1337, rulesetId: "_session", extensionId: "other@ext3" }, { ruleId: 1000, rulesetId: "_session", extensionId: "other@ext2" }, { ruleId: 1002, rulesetId: "_session", extensionId: "other@ext2" }, ], (await dnr.testMatchOutcome({ url, type, tabId: 6 }, options)) .matchedRules, "allowAllRequests from all other extensions are matched" ); browser.test.notifyPass(); }, }); await otherExtension.unload(); await otherExtensionNonBlockAndModifyHeaders.unload(); await otherExtensionModifyHeaders.unload(); });