Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

"use strict";
// This file tests whether the "allowAllRequests" action is correctly applied
// to subresource requests. The relative precedence to other actions/extensions
// is tested in test_ext_dnr_testMatchOutcome.js, specifically by test tasks
// rule_priority_and_action_type_precedence and
// action_precedence_between_extensions.
ChromeUtils.defineESModuleGetters(this, {
});
add_setup(() => {
Services.prefs.setBoolPref("extensions.manifestV3.enabled", true);
Services.prefs.setBoolPref("extensions.dnr.enabled", true);
});
const server = createHttpServer({
hosts: ["example.com", "example.net", "example.org"],
});
server.registerPathHandler("/never_reached", () => {
Assert.ok(false, "Server should never have been reached");
});
server.registerPathHandler("/allowed", (req, res) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Max-Age", "0");
// Any test that is able to check the response body will be able to assert
// the response body's value. Let's use "fetchAllowed" so that the compared
// values are obvious when assertEq/assertDeepEq are used.
res.write("fetchAllowed");
});
server.registerPathHandler("/", (req, res) => {
res.write("Dummy page");
});
server.registerPathHandler("/echo_html", (req, res) => {
let code = decodeURIComponent(req.queryString);
res.setHeader("Content-Type", "text/html; charset=utf-8");
if (req.hasHeader("prependhtml")) {
code = req.getHeader("prependhtml") + code;
}
res.write(`<!DOCTYPE html>${code}`);
});
server.registerPathHandler("/bfcache_test", (req, res) => {
res.setHeader("Content-Type", "text/html; charset=utf-8");
res.write(`<body><script>
// false at initial load, true when loaded from bfcache.
onpageshow = e => document.body.textContent = e.persisted;
</script>`);
});
async function waitForRequestAtServer(path) {
return new Promise(resolve => {
let callCount = 0;
server.registerPathHandler(path, (req, res) => {
Assert.equal(++callCount, 1, `Got one request for: ${path}`);
res.processAsync();
resolve({ req, res });
});
});
}
// Several tests expect fetch() to fail due to the request being blocked.
// They can use testLoadInFrame({ ..., expectedError: FETCH_BLOCKED }).
const FETCH_BLOCKED =
"TypeError: NetworkError when attempting to fetch resource.";
function urlEchoHtml(domain, html) {
return `http://${domain}/echo_html?${encodeURIComponent(html)}`;
}
function htmlEscape(html) {
return html
.replaceAll("&", "&amp;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;");
}
// Values for domains in testLoadInFrame.
const ABOUT_SRCDOC_SAME_ORIGIN = "about:srcdoc (same-origin)";
const ABOUT_SRCDOC_CROSS_ORIGIN = "about:srcdoc (cross-origin)";
async function testLoadInFrame({
description,
// domains[0] = main frame, every extra item is a child frame.
domains = ["example.com"],
htmlPrependedToEachFrame = "",
// jsForFrame will be serialized and run in the deepest frame.
jsForFrame,
// The expected (potentially async) return value of jsForFrame.
expectedResult,
// The expected (potentially async) error thrown from jsForFrame.
expectedError,
}) {
const frameJs = async jsForFrame => {
let result = {};
try {
result.returnValue = await jsForFrame();
} catch (e) {
result.error = String(e);
}
// jsForFrame may return "delay_postMessage" to postpone the resolution of
// the promise. When the test is ready to resume, `top.postMessage()` can
// be called with the result, from any frame. This would also happen if the
// URL generated by this testLoadInFrame helper are re-used, e.g. by a new
// navigation to the URL that triggers a return value from jsForFrame that
// differs from "delay_postMessage".
if (result.returnValue !== "delay_postMessage") {
top.postMessage(result, "*");
}
};
const frameHtml = `<body><script>(${frameJs})(${jsForFrame})</script>`;
// Construct the frame tree so that domains[0] is the main frame, and
// domains[domains.length - 1] is the deepest level frame (if any).
const [mainFrameDomain, ...subFramesDomains] = domains;
// The loop below generates the HTML for the deepest frame first, so we have
// to reverse the list of domains.
subFramesDomains.reverse();
let html = frameHtml;
for (let domain of subFramesDomains) {
html = htmlPrependedToEachFrame + html;
if (domain === ABOUT_SRCDOC_SAME_ORIGIN) {
html = `<iframe srcdoc="${htmlEscape(html)}"></iframe>`;
} else if (domain === ABOUT_SRCDOC_CROSS_ORIGIN) {
html = `<iframe srcdoc="${htmlEscape(
html
)}" sandbox="allow-scripts"></iframe>`;
} else {
html = `<iframe src="${urlEchoHtml(domain, html)}"></iframe>`;
}
}
const mainFrameJs = () => {
window.resultPromise = new Promise(resolve => {
window.onmessage = e => resolve(e.data);
});
};
const mainFrameHtml = `<script>(${mainFrameJs})()</script>${html}`;
const mainFrameUrl = urlEchoHtml(mainFrameDomain, mainFrameHtml);
let contentPage = await ExtensionTestUtils.loadContentPage(mainFrameUrl);
let result = await contentPage.spawn([], () => {
return content.wrappedJSObject.resultPromise;
});
await contentPage.close();
if (expectedError) {
Assert.deepEqual(result, { error: expectedError }, description);
} else {
Assert.deepEqual(result, { returnValue: expectedResult }, description);
}
}
async function loadExtensionWithDNRRules(
rules,
{
// host_permissions is only required for modifyHeaders/redirect, or when
// "declarativeNetRequestWithHostAccess" is used.
host_permissions = [],
permissions = ["declarativeNetRequest"],
} = {}
) {
async function background(rules) {
try {
await browser.declarativeNetRequest.updateSessionRules({
addRules: rules,
});
} catch (e) {
browser.test.fail(`Failed to register DNR rules: ${e} :: ${e.stack}`);
}
browser.test.sendMessage("dnr_registered");
}
let extension = ExtensionTestUtils.loadExtension({
background: `(${background})(${JSON.stringify(rules)})`,
temporarilyInstalled: true, // Needed for granted_host_permissions
manifest: {
manifest_version: 3,
granted_host_permissions: true,
host_permissions,
permissions,
},
});
await extension.startup();
await extension.awaitMessage("dnr_registered");
return extension;
}
add_task(async function allowAllRequests_allows_request() {
let extension = await loadExtensionWithDNRRules([
// allowAllRequests should take precedence over block.
{
id: 1,
condition: { resourceTypes: ["main_frame", "xmlhttprequest"] },
action: { type: "block" },
},
{
id: 2,
condition: { resourceTypes: ["main_frame"] },
action: { type: "allowAllRequests" },
},
{
id: 3,
priority: 2,
// Note: when not specified, main_frame is excluded by default. So
// when a main_frame request is triggered, only rules 1 and 2 match.
condition: { requestDomains: ["example.com"] },
action: { type: "block" },
},
]);
let contentPage = await ExtensionTestUtils.loadContentPage(
);
Assert.equal(
await contentPage.spawn([], () => content.document.URL),
"main_frame request should have been allowed by allowAllRequests"
);
async function checkCanFetch(url) {
return contentPage.spawn([url], async url => {
try {
return await (await content.fetch(url)).text();
} catch (e) {
return e.toString();
}
});
}
Assert.equal(
await checkCanFetch("http://example.com/never_reached"),
FETCH_BLOCKED,
"should be blocked by DNR rule 3"
);
Assert.equal(
await checkCanFetch("http://example.net/allowed"),
"fetchAllowed",
"should not be blocked by block rule due to allowAllRequests rule"
);
await contentPage.close();
await extension.unload();
});
add_task(async function allowAllRequests_in_sub_frame() {
const extension = await loadExtensionWithDNRRules([
{
id: 1,
condition: { resourceTypes: ["xmlhttprequest"] },
action: { type: "block" },
},
{
id: 2,
condition: {
requestDomains: ["example.com"],
resourceTypes: ["main_frame", "sub_frame"],
},
action: { type: "allowAllRequests" },
},
]);
const testFetch = async () => {
// Should be able to read, unless blocked by DNR rule 1 above.
return (await fetch("http://example.com/allowed")).text();
};
// Sanity check: verify that the test result is different (i.e. FETCH_BLOCKED)
// when the "allowAllRequests" rule (rule ID 2) is not matched.
await testLoadInFrame({
description: "allowAllRequests was not matched anywhere, req in subframe",
domains: ["example.net", "example.org"],
jsForFrame: testFetch,
expectedError: FETCH_BLOCKED,
});
// allowAllRequests applied to domains[0], i.e. "main_frame".
await testLoadInFrame({
description: "allowAllRequests for main frame, req in main frame",
domains: ["example.com"],
jsForFrame: testFetch,
expectedResult: "fetchAllowed",
});
await testLoadInFrame({
description: "allowAllRequests for main frame, req in same-origin frame",
domains: ["example.com", "example.com"],
jsForFrame: testFetch,
expectedResult: "fetchAllowed",
});
await testLoadInFrame({
description: "allowAllRequests for main frame, req in cross-origin frame",
domains: ["example.com", "example.net"],
jsForFrame: testFetch,
expectedResult: "fetchAllowed",
});
// allowAllRequests applied to domains[1], i.e. "sub_frame".
await testLoadInFrame({
description: "allowAllRequests for subframe, req in same subframe",
domains: ["example.net", "example.com"],
jsForFrame: testFetch,
expectedResult: "fetchAllowed",
});
await testLoadInFrame({
description: "allowAllRequests for subframe, req in same-origin subframe",
domains: ["example.net", "example.com", "example.com"],
jsForFrame: testFetch,
expectedResult: "fetchAllowed",
});
await testLoadInFrame({
description: "allowAllRequests for subframe, req in cross-origin subframe",
domains: ["example.net", "example.com", "example.org"],
jsForFrame: testFetch,
expectedResult: "fetchAllowed",
});
await extension.unload();
});
add_task(async function allowAllRequests_does_not_affect_other_extension() {
const extension = await loadExtensionWithDNRRules([
{
id: 1,
condition: { resourceTypes: ["xmlhttprequest"] },
action: { type: "block" },
},
]);
const otherExtension = await loadExtensionWithDNRRules([
{
id: 2,
condition: { resourceTypes: ["main_frame", "sub_frame"] },
action: { type: "allowAllRequests" },
},
]);
const testFetch = async () => {
return (await fetch("http://example.com/allowed")).text();
};
// Sanity check: verify that the test result is different (i.e. FETCH_BLOCKED)
// when the "allowAllRequests" rule (rule ID 2) is not matched.
await testLoadInFrame({
description: "block rule from extension not superseded by otherExtension",
domains: ["example.net", "example.org"],
jsForFrame: testFetch,
expectedError: FETCH_BLOCKED,
});
await extension.unload();
await otherExtension.unload();
});
// When there are multiple frames and matching allowAllRequests, we need to
// use the highest-priority allowAllRequests rule. The selected rule can be
// observed through interleaved modifyHeaders rules.
add_task(async function allowAllRequests_multiple_frames_and_modifyHeaders() {
const domains = ["example.com", "example.com", "example.net", "example.org"];
const rules = [
{
id: 1,
priority: 3,
condition: { requestDomains: [domains[1]], resourceTypes: ["sub_frame"] },
action: { type: "allowAllRequests" },
},
{
id: 2,
priority: 7,
condition: { requestDomains: [domains[2]], resourceTypes: ["sub_frame"] },
action: { type: "allowAllRequests" },
},
{
id: 3,
priority: 5,
condition: { requestDomains: [domains[3]], resourceTypes: ["sub_frame"] },
action: { type: "allowAllRequests" },
},
// The loop below will add modifyHeaders rules with priorities 1 - 9.
];
for (let i = 1; i <= 9; ++i) {
rules.push({
id: 10 + i, // not overlapping with any rule in |rules|.
priority: i,
condition: { resourceTypes: ["xmlhttprequest"] },
action: {
type: "modifyHeaders",
responseHeaders: [
{
// Expose the header via CORS to allow fetch() to read the header.
operation: "set",
header: "Access-Control-Expose-Headers",
value: "addedByDnr",
},
{ operation: "append", header: "addedByDnr", value: `${i}` },
],
},
});
}
const extension = await loadExtensionWithDNRRules(rules, {
// host_permissions required for "modifyHeaders" action.
host_permissions: ["<all_urls>"],
});
await testLoadInFrame({
description: "Should select highest-prio allowAllRequests among ancestors",
domains,
jsForFrame: async () => {
let res = await fetch("http://example.com/allowed");
return res.headers.get("addedByDnr");
},
// The fetch request matches all xmlhttprequest rules, which would append
// the numbers 1...9 to the results via "modifyHeaders".
//
// But every frame also has one matching "allowAllRequests" rule. Among
// these, we should not select an arbitrary rule, but the one with the
// highest priority, i.e. priority 7 (matches domains[2]).
//
// Given the "allowAllRequests" of priority 7, all rules of lower-or-equal
// priority are ignored, so only "modifyHeaders" remain with priority 8 & 9.
//
// modifyHeaders are applied in the order of priority: "9, 8", not "8, 9".
expectedResult: "9, 8",
});
await extension.unload();
});
add_task(async function allowAllRequests_initiatorDomains() {
const rules = [
{
id: 1,
condition: {
initiatorDomains: ["example.com"], // Note: in host_permissions below.
resourceTypes: ["main_frame", "sub_frame"],
},
action: { type: "allowAllRequests" },
},
{
id: 2,
condition: {
initiatorDomains: ["example.net"], // Note: NOT in host_permissions.
resourceTypes: ["sub_frame"],
},
action: { type: "allowAllRequests" },
},
{
id: 3,
condition: { resourceTypes: ["xmlhttprequest"] },
action: { type: "block" },
},
];
const extension = await loadExtensionWithDNRRules(rules, {
// host_permissions matches initiatorDomains from rule 1 (allowAllRequests)
// and the origin of the frame that calls testCanFetch.
host_permissions: ["*://example.com/*", "*://example.org/*"],
});
const testCanFetch = async () => {
return (await fetch("http://example.com/allowed")).text();
};
await testLoadInFrame({
description: "main_frame request does not have an initiator",
domains: ["example.com"],
jsForFrame: testCanFetch,
// Rule 1 (initiatorDomains: ["example.com"]) should not match.
expectedError: FETCH_BLOCKED,
});
await testLoadInFrame({
description: "sub_frame loaded by initiator in host_permissions",
domains: ["example.com", "example.org"],
jsForFrame: testCanFetch,
// Matched by rule 1 (initiatorDomains: ["example.com"])
expectedResult: "fetchAllowed",
});
await testLoadInFrame({
description: "sub_frame loaded by initiator not in host_permissions",
domains: ["example.net", "example.org"],
jsForFrame: testCanFetch,
// Matched by rule 2 (initiatorDomains: ["example.net"]). While example.net
// is not in host_permissions, the "allowAllRequests" rule can apply because
// the extension does have the "declarativeNetRequest" permission (opposed
// to just "declarativeNetRequestWithHostAccess", which is covered by the
// allowAllRequests_initiatorDomains_dnrWithHostAccess test task below).
expectedResult: "fetchAllowed",
});
// about:srcdoc inherits parent origin.
await testLoadInFrame({
description: "about:srcdoc with matching initiator",
domains: ["example.com", ABOUT_SRCDOC_SAME_ORIGIN],
jsForFrame: testCanFetch,
// While the "about:srcdoc" frame's initiator is matched by rule 1
// (initiatorDomains: ["example.com"]), the frame's URL itself is
// "about:srcdoc" and consequently ignored in the matcher.
expectedError: FETCH_BLOCKED,
});
await testLoadInFrame({
description: "subframe in about:srcdoc with matching initiator",
domains: ["example.com", ABOUT_SRCDOC_SAME_ORIGIN, "example.org"],
jsForFrame: testCanFetch,
// The parent URL is "about:srcdoc", but its principal is inherit from its
// parent, i.e. "example.com". Therefore it matches rule 1.
expectedResult: "fetchAllowed",
});
await testLoadInFrame({
description: "subframe in opaque about:srcdoc despite matching initiator",
domains: ["example.com", ABOUT_SRCDOC_CROSS_ORIGIN, "example.org"],
jsForFrame: testCanFetch,
// The parent URL is "about:srcdoc". Because it is sandboxed, it has an
// opaque origin and therefore none of the allowAllRequests rules match,
// even not rule 1 even though the "about:srcdoc" frame was created by
// "example.com".
expectedError: FETCH_BLOCKED,
});
await extension.unload();
});
add_task(async function allowAllRequests_initiatorDomains_dnrWithHostAccess() {
const rules = [
{
id: 1,
condition: {
// This test shows that it does not matter whether initiatorDomains is
// in host_permissions; it only matters if the frame's URL is matched
// by host_permissions.
initiatorDomains: ["example.net"], // Not in host_permissions.
resourceTypes: ["sub_frame"],
},
action: { type: "allowAllRequests" },
},
{
id: 2,
condition: { resourceTypes: ["xmlhttprequest"] },
action: { type: "block" },
},
];
const extension = await loadExtensionWithDNRRules(rules, {
host_permissions: ["*://example.org/*"],
permissions: ["declarativeNetRequestWithHostAccess"],
});
const testCanFetch = async () => {
// example.org is in host_permissions above so "xmlhttprequest" rule is
// always expected to match this, unless "allowAllRequests" applied.
// If "allowAllRequests" applies, then expectedResult: "fetchAllowed".
// If "allowAllRequests" did not apply, then expectedError: FETCH_BLOCKED.
return (await fetch("http://example.org/allowed")).text();
};
await testLoadInFrame({
description:
"frame URL in host_permissions despite initiator not in host_permissions",
domains: ["example.com", "example.net", "example.org"],
jsForFrame: testCanFetch,
// The "xmlhttprequest" block rule applies because the request URL
// (example.org) and initiator (example.org) are part of host_permissions.
//
// The "allowAllRequests" rule applies and overrides the block because the
// "example.org" frame has "example.net" as initiator (as specified in the
// initiatorDomains DNR rule). Despite the lack of host_permissions for
// "example.net", the DNR rule is matched because navigation requests do
// not require host permissions.
expectedResult: "fetchAllowed",
});
await testLoadInFrame({
description: "frame URL and initiator not in host_permissions",
domains: ["example.net", "example.com", "example.org"],
jsForFrame: testCanFetch,
// The "xmlhttprequest" block rule applies because the request URL
// (example.org) and initiator (example.org) are part of host_permissions.
//
// The "allowAllRequests" rule does not apply because it would only apply
// to the "example.com" frame (that frame has "example.net" as initiator),
// but the DNR extension does not have host permissions for example.com.
expectedError: FETCH_BLOCKED,
});
await extension.unload();
});
add_task(async function allowAllRequests_initiator_is_parent() {
// The actual initiator of a request is the principal (origin) that triggered
// the request. Navigations of subframes are usually triggered by the parent,
// except in case of cross-frame/window navigations.
//
// There are some limits on cross-frame navigations, specified by:
// An ancestor can always navigate a descendant, so we do that here.
//
// - example.com (main frame)
// - example.net (sub frame 1)
// - example.org (sub frame 2)
// - example.com (sub frame 3) - will be navigated by sub frame 1.
//
// "initiatorDomains" is usually matched against the actual initiator of a
// request. Since the actual initiator (triggering principal) is not always
// known nor obvious, the parent principal (origin) is used instead, when the
// conditions for "allowAllRequests" are retroactively checked for a document.
const domains = ["example.com", "example.net", "example.org", "example.com"];
const rules = [
{
id: 1,
condition: {
// Note: restrict to example.org, so that we can verify that the
// "allowAllRequests" rule applies to subresource requests within any
// child frame of "example.org" (i.e. that rule 3 is ignored).
//
// Side note: the ultimate navigation request for the child frame
// itself has actual initiator "example.net" and does not match this
// rule, which we verify by confirming that rule 2 matches.
initiatorDomains: ["example.org"],
requestDomains: ["example.com"],
resourceTypes: ["sub_frame"],
},
action: { type: "allowAllRequests" },
},
{
id: 2,
condition: { resourceTypes: ["xmlhttprequest"] },
action: { type: "block" },
},
// The modifyHeaders rules below are not affected by the "allowAllRequests"
// rule above, but are part of the test to serve as a sanity check that the
// "initiatorDomains" field of sub_frame navigations are compared against
// the actual initiator.
{
id: 3,
priority: 2, // To not be ignored by allowAllRequests (rule 1).
condition: {
// The initial sub_frame navigation request is initiated by its parent,
// i.e. example.org.
initiatorDomains: ["example.org"],
requestDomains: ["example.com"],
resourceTypes: ["sub_frame"],
},
action: {
type: "modifyHeaders",
requestHeaders: [
{
operation: "append",
header: "prependhtml",
value: "<title>DNR rule 3 for initiator example.org</title>",
},
],
},
},
{
id: 4,
condition: {
// The final sub_frame navigation request is initiated by a frame other
// than the parent (i.e. example.net).
initiatorDomains: ["example.net"],
requestDomains: ["example.com"],
resourceTypes: ["sub_frame"],
},
action: {
type: "modifyHeaders",
requestHeaders: [
{
operation: "append",
header: "prependhtml",
value: "<title>DNR rule 4 for initiator example.net</title>",
},
],
},
},
];
const extension = await loadExtensionWithDNRRules(rules, {
// host_permissions needed for allowAllRequests of ancestors
// (initiatorDomains & requestDomains) and modifyHeaders.
host_permissions: ["<all_urls>"],
});
const jsNavigateOnMessage = () => {
window.onmessage = e => {
dump(`\nReceived message at ${origin} from ${e.origin}: ${e.data}\n`);
e.source.location = e.data;
};
};
const htmlNavigateOnMessage = `<script>(${jsNavigateOnMessage})()</script>`;
// First: sanity check that the actual initiators are as expected, which we
// verify through the modifyHeaders+initiatorDomains rules, observed through
// document.title (/echo_html prepends the "prependhtml" header's value).
await testLoadInFrame({
description: "Sanity check: navigation matches actual initiator (parent)",
domains,
jsForFrame: () => document.title,
expectedResult: "DNR rule 3 for initiator example.org",
});
await testLoadInFrame({
description: "Sanity check: navigation matches actual initiator (ancestor)",
domains,
htmlPrependedToEachFrame: htmlNavigateOnMessage,
jsForFrame: () => {
if (location.hash !== "#End") {
dump("Sanity: Trying to navigate with initiator set to example.net\n");
parent.parent.postMessage(document.URL + ".#End", "http://example.net");
return "delay_postMessage";
}
return document.title;
},
expectedResult: "DNR rule 4 for initiator example.net",
});
// Now the actual test: when fetch() is called, "allowAllRequests" should use
// the parent origin for each frame in the frame tree.
await testLoadInFrame({
description: "allowAllRequests matches parent (which is the initiator)",
domains,
jsForFrame: async () => {
return (await fetch("http://example.com/allowed")).text();
},
expectedResult: "fetchAllowed", // fetch() not blocked, rule 1 > rule 2.
});
// This is where the result differs from what one may expect from
// "initiatorDomains". This is consistent with Chrome's behavior,
await testLoadInFrame({
description: "allowAllRequests matches parent (not actual initiator)",
domains,
htmlPrependedToEachFrame: htmlNavigateOnMessage,
jsForFrame: async () => {
if (location.hash !== "#End") {
dump("Final: Trying to navigate with initiator set to example.net\n");
parent.parent.postMessage(document.URL + ".#End", "http://example.net");
return "delay_postMessage";
}
return (await fetch("http://example.com/allowed")).text();
},
expectedResult: "fetchAllowed", // fetch() not blocked, rule 1 > rule 2.
});
await extension.unload();
});
// Tests how initiatorDomains applies to document and non-document (fetch)
// requests triggered from content scripts.
add_task(async function allowAllRequests_initiatorDomains_content_script() {
const rules = [
{
id: 1,
condition: {
initiatorDomains: ["example.com"],
resourceTypes: ["sub_frame"],
},
action: { type: "allowAllRequests" },
},
{
id: 2,
condition: { resourceTypes: ["xmlhttprequest"] },
action: { type: "block" },
},
{
id: 3,
condition: {
resourceTypes: ["sub_frame"],
requestDomains: ["example.com"],
},
action: {
type: "redirect",
redirect: { transform: { host: "example.net" } },
},
},
];
const extension = await loadExtensionWithDNRRules(rules, {
host_permissions: ["*://example.com/*", "*://example.net/*"],
});
let contentScriptExtension = ExtensionTestUtils.loadExtension({
manifest: {
// Intentionally MV2 because its fetch() is tied to the content script
// sandbox, and thus potentially more likely to trigger bugs than the MV3
// fetch (fetch in MV3 is the same as the web page due to bug 1578405).
manifest_version: 2,
content_scripts: [
{
run_at: "document_end",
js: ["contentscript_load_frame.js"],
},
{
all_frames: true,
run_at: "document_end",
js: ["contentscript_in_iframe.js"],
},
],
},
files: {
"contentscript_load_frame.js": () => {
browser.test.log("Waiting for frame, then contentscript_in_iframe.js");
// Created by content script; initiatorDomains should match the page's
// domain (and not somehow be confused by the content script principal).
// let document = window.document.wrappedJSObject;
let f = document.createElement("iframe");
document.body.append(f);
},
"contentscript_in_iframe.js": async () => {
// When the iframe request was generated by the content script, its
// initiator is void because the content script has an ExpandedPrincipal
// that is treated as void when the request initiator is computed:
// Therefore the initiatorDomains condition of rule 1 (allowAllRequests)
// does not match, so rule 3 (redirect to example.net) applies.
browser.test.assertEq(
"example.net", // instead of the pre-redirect URL (example.com).
location.host,
"redirect rule matched because initiator is void for content-script-triggered navigation"
);
async function isFetchOk(fetchPromise) {
try {
await fetchPromise;
return true; // allowAllRequests matched.
} catch (e) {
await browser.test.assertRejects(fetchPromise, /NetworkError/);
return false; // block rule matched because allowAllRequests didn't.
}
}
browser.test.assertTrue(
await isFetchOk(content.fetch("http://example.net/allowed")),
"frame's parent origin matches initiatorDomains (content script fetch)"
);
// fetch() in MV2 content script is associated with the content script
// sandbox, not the frame, so there are no allowAllRequests rules to
// apply. For equivalent request details, see bug 1444729.
browser.test.assertFalse(
await isFetchOk(fetch("http://example.net/allowed")),
"MV2 content script fetch() is not associated with the document"
);
browser.test.sendMessage("contentscript_initiator");
},
},
});
await contentScriptExtension.startup();
let contentPage = await ExtensionTestUtils.loadContentPage(
);
info("Waiting for page load, will continue at contentscript_load_frame.js");
await contentScriptExtension.awaitMessage("contentscript_initiator");
await contentScriptExtension.unload();
await contentPage.close();
await extension.unload();
});
// Verifies that allowAllRequests is evaluated against the currently committed
// document, even if another document load has been initiated.
add_task(async function allowAllRequests_during_and_after_navigation() {
let extension = await loadExtensionWithDNRRules([
{
id: 1,
condition: { resourceTypes: ["xmlhttprequest"] },
action: { type: "block" },
},
{
id: 2,
condition: { urlFilter: "WITH_AAR", resourceTypes: ["sub_frame"] },
action: { type: "allowAllRequests" },
},
]);
const contentPage = await ExtensionTestUtils.loadContentPage(
);
await contentPage.spawn([], async () => {
let f = content.document.createElement("iframe");
f.id = "frame_to_navigate";
f.src = "/?init_WITH_AAR"; // allowAllRequests initially applies.
await new Promise(resolve => {
f.onload = resolve;
content.document.body.append(f);
});
});
async function navigateIframe(url) {
await contentPage.spawn([url], url => {
let f = content.document.getElementById("frame_to_navigate");
content.frameLoadedPromise = new Promise(resolve => {
f.addEventListener("load", resolve, { once: true });
});
f.contentWindow.location.href = url;
});
}
async function waitForNavigationCompleted(expectLoad = true) {
await contentPage.spawn([expectLoad], async expectLoad => {
if (expectLoad) {
info("Waiting for frame load - if stuck the load never happened\n");
return content.frameLoadedPromise.then(() => {});
}
// When HTTP 204 No Content is used, onload is not fired.
// Here we load another frame, and assume that once this completes, that
// any previous load of navigateIframe() would have completed by now.
let f = content.document.createElement("iframe");
f.src = "/?dummy_no_dnr_matched_" + Math.random();
await new Promise(resolve => {
f.onload = resolve;
content.document.body.append(f);
});
f.remove();
});
}
async function assertIframePath(expectedPath, description) {
let actualPath = await contentPage.spawn([], () => {
return content.frames[0].location.pathname;
});
Assert.equal(actualPath, expectedPath, description);
}
async function assertHasAAR(expected, description) {
let actual = await contentPage.spawn([], async () => {
try {
await (await content.frames[0].fetch("/allowed")).text();
return true; // allowAllRequests overrides block rule.
} catch (e) {
// Sanity check: NetworkError from fetch(), not a random other error.
Assert.equal(
e.toString(),
"TypeError: NetworkError when attempting to fetch resource.",
"Got error for failed fetch"
);
return false; // blocked by xmlhttprequest block rule.
}
});
Assert.equal(actual, expected, description);
}
await assertHasAAR(true, "Initial allowAllRequests overrides block rule");
const PATH_1_NO_AAR = "/delayed/PATH_1_NO_AAR";
const PATH_2_WITH_AAR = "/delayed/PATH_2_WITH_AAR";
const PATH_3_NO_AAR = "/delayed/PATH_3_NO_AAR";
info("First: transition from /?init_WITH_AAR to PATH_NOT_MATCHED_BY_DNR.");
{
let promisedServerReq = waitForRequestAtServer(PATH_1_NO_AAR);
await navigateIframe(PATH_1_NO_AAR);
let serverReq = await promisedServerReq;
await assertHasAAR(
true,
"Initial allowAllRequests still applies despite pending navigation"
);
await assertIframePath("/", "Frame has not navigated yet");
serverReq.res.finish();
await waitForNavigationCompleted();
await assertIframePath(PATH_1_NO_AAR, "Navigated to PATH_1_NO_AAR");
await assertHasAAR(
false,
"Old allowAllRequests should no longer apply after navigation to PATH_1_NO_AAR"
);
}
info("Second: transition from PATH_1_NO_AAR to PATH_2_WITH_AAR.");
{
let promisedServerReq = waitForRequestAtServer(PATH_2_WITH_AAR);
await navigateIframe(PATH_2_WITH_AAR);
let serverReq = await promisedServerReq;
await assertHasAAR(
false,
"No allowAllRequests yet despite pending navigation to PATH_2_WITH_AAR"
);
await assertIframePath(PATH_1_NO_AAR, "Frame has not navigated yet");
serverReq.res.finish();
await waitForNavigationCompleted();
await assertIframePath(PATH_2_WITH_AAR, "Navigated to PATH_2_WITH_AAR");
await assertHasAAR(
true,
"allowAllRequests should apply after navigation to PATH_2_WITH_AAR"
);
}
info("Third: AAR still applies after canceling navigation to PATH_3_NO_AAR.");
{
let promisedServerReq = waitForRequestAtServer(PATH_3_NO_AAR);
await navigateIframe(PATH_3_NO_AAR);
let serverReq = await promisedServerReq;
serverReq.res.setStatusLine(serverReq.req.httpVersion, 204, "No Content");
serverReq.res.finish();
await waitForNavigationCompleted(/* expectLoad */ false);
await assertIframePath(PATH_2_WITH_AAR, "HTTP 204 does not navigate away");
await assertHasAAR(
true,
"allowAllRequests still applied after aborted navigation to PATH_3_NO_AAR"
);
}
await contentPage.close();
await extension.unload();
});
add_task(
{
// Ensure that there is room for at least 2 non-evicted bfcache entries.
// Note: this pref is ignored (i.e forced 0) when configured (non-default)
// with bfcacheInParent=false while SHIP is enabled:
// ... we mainly care about the bfcache here because it triggers interesting
// behavior. DNR evaluation is correct regardless of bfcache.
pref_set: [["browser.sessionhistory.max_total_viewers", 3]],
},
async function allowAllRequests_and_bfcache_navigation() {
let extension = await loadExtensionWithDNRRules([
{
id: 1,
condition: { resourceTypes: ["xmlhttprequest"] },
action: { type: "block" },
},
{
id: 2,
condition: { urlFilter: "aar_yes", resourceTypes: ["main_frame"] },
action: { type: "allowAllRequests" },
},
]);
info("Navigating to initial URL: 1_aar_no");
const contentPage = await ExtensionTestUtils.loadContentPage(
);
async function navigateBackInHistory(expectedUrl) {
await contentPage.spawn([], () => {
content.history.back();
});
await TestUtils.waitForCondition(
() => contentPage.browsingContext.currentURI.spec === expectedUrl,
`Waiting for history.back() to trigger navigation to ${expectedUrl}`
);
await contentPage.spawn([expectedUrl], async expectedUrl => {
Assert.equal(content.location.href, expectedUrl, "URL after back");
Assert.equal(content.document.body.textContent, "true", "from bfcache");
});
}
async function checkCanFetch(url) {
return contentPage.spawn([url], async url => {
try {
return await (await content.fetch(url)).text();
} catch (e) {
return e.toString();
}
});
}
info("Navigating from initial URL to: 2_aar_yes");
await contentPage.loadURL("http://example.com/bfcache_test?2_aar_yes");
info("Navigating from 2_aar_yes to: 3_aar_no");
await contentPage.loadURL("http://example.com/bfcache_test?3_aar_no");
info("Going back in history (from 3_aar_no to 2_aar_yes)");
await navigateBackInHistory("http://example.com/bfcache_test?2_aar_yes");
Assert.equal(
await checkCanFetch("http://example.com/allowed"),
"fetchAllowed",
"after history.back(), allowAllRequests should apply from 2_aar_yes"
);
info("Going back in history (from 2_aar_yes to 1_aar_no)");
await navigateBackInHistory("http://example.com/bfcache_test?1_aar_no");
Assert.equal(
await checkCanFetch("http://example.net/never_reached"),
FETCH_BLOCKED,
"after history.back(), no allowAllRequests action applied at 1_aar_no"
);
await contentPage.close();
await extension.unload();
}
);
add_task(
{
// Usually, back/forward navigation to a POST form requires the user to
// confirm the form resubmission. Set pref to approve without prompting.
pref_set: [["dom.confirm_repost.testing.always_accept", true]],
},
async function allowAllRequests_navigate_with_http_method_POST() {
const rules = [
{
id: 1,
condition: {
requestMethods: ["post"],
resourceTypes: ["main_frame", "sub_frame"],
},
action: { type: "allowAllRequests" },
},
{
id: 2,
condition: { resourceTypes: ["xmlhttprequest"] },
action: { type: "block" },
},
];
if (!Services.appinfo.sessionHistoryInParent) {
// POST detection relies on SHIP being enabled. This is true by default,
// but there are some test configurations with SHIP disabled. When SHIP
// is disabled, all methods are interpreted as GET instead of POST.
// Rewrite the rule to specifically match the POST requests that are
// misinterpreted as GET, to verify that the request evaluation by DNR is
// functional (opposed to throwing errors).
rules[0].condition.requestMethods = ["get"];
rules[0].condition.urlFilter = "do_post|";
info(`WARNING: SHIP is disabled. POST will be misinterpreted as GET`);
}
const extension = await loadExtensionWithDNRRules(rules);
const contentPage = await ExtensionTestUtils.loadContentPage(
);
async function checkCanFetch(url) {
return contentPage.spawn([url], async url => {
try {
return await (await content.fetch(url)).text();
} catch (e) {
return e.toString();
}
});
}
// Check fetch() with regular GET navigation in main_frame.
Assert.equal(
await checkCanFetch("http://example.net/never_reached"),
FETCH_BLOCKED,
"main_frame: non-POST not matched by requestMethods:['post']"
);
// Check fetch() after POST navigation in main_frame.
await contentPage.spawn([], () => {
let form = content.document.createElement("form");
form.action = "/?do_post";
form.method = "POST";
content.document.body.append(form);
form.submit();
});
await TestUtils.waitForCondition(
() => contentPage.browsingContext.currentURI.pathQueryRef === "/?do_post",
"Waiting for navigation with POST to complete"
);
Assert.equal(
await checkCanFetch("http://example.net/allowed"),
"fetchAllowed",
"main_frame: requestMethods:['post'] applies to POST"
);
// Navigate back to the beginning and verify that allowAllRequests does not
// match any more.
await contentPage.spawn([], () => {
content.history.back();
});
await TestUtils.waitForCondition(
() => contentPage.browsingContext.currentURI.pathQueryRef === "/?do_get",
"Waiting for (back) navigation to initial GET page to complete"
);
Assert.equal(
await checkCanFetch("http://example.net/never_reached"),
FETCH_BLOCKED,
"main_frame: back to non-POST not matched by requestMethods:['post']"
);
// Now navigate forwards to verify that the POST method is still seen.
await contentPage.spawn([], () => {
content.history.forward();
});
await TestUtils.waitForCondition(
() => contentPage.browsingContext.currentURI.pathQueryRef === "/?do_post",
"Waiting for (forward) navigation to POST page to complete"
);
Assert.equal(
await checkCanFetch("http://example.net/allowed"),
"fetchAllowed",
"main_frame: requestMethods:['post'] detects POST after history.forward()"
);
// Now check that adding a new history entry drops the POST method.
await contentPage.spawn([], () => {
content.history.pushState(null, null, "/?hist_p");
});
await TestUtils.waitForCondition(
() => contentPage.browsingContext.currentURI.pathQueryRef === "/?hist_p",
"Waiting for history.pushState to have changed the URL"
);
Assert.equal(
await checkCanFetch("http://example.net/never_reached"),
FETCH_BLOCKED,
"history.pushState drops POST, not matched by requestMethods:['post']"
);
await contentPage.close();
// Finally, check that POST detection also works for child frames.
await testLoadInFrame({
description: "sub_frame: non-POST not matched by requestMethods:['post']",
domains: ["example.com", "example.com"],
jsForFrame: async () => {
return (await fetch("http://example.com/allowed")).text();
},
expectedError: FETCH_BLOCKED,
});
await testLoadInFrame({
description: "sub_frame: requestMethods:['post'] applies to POST",
domains: ["example.com", "example.com"],
jsForFrame: async () => {
if (!location.href.endsWith("?do_post")) {
dump("Triggering navigation with POST\n");
let form = document.createElement("form");
form.action = location.href + "?do_post";
form.method = "POST";
document.body.append(form);
form.submit();
return "delay_postMessage";
}
dump("Navigation with POST completed; testing fetch()...\n");
return (await fetch("http://example.com/allowed")).text();
},
expectedResult: "fetchAllowed",
});
await extension.unload();
}
);