forked from mirrors/gecko-dev
1231 lines
44 KiB
JavaScript
1231 lines
44 KiB
JavaScript
"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, {
|
|
TestUtils: "resource://testing-common/TestUtils.sys.mjs",
|
|
});
|
|
|
|
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("&", "&")
|
|
.replaceAll('"', """)
|
|
.replaceAll("'", "'")
|
|
.replaceAll("<", "<")
|
|
.replaceAll(">", ">");
|
|
}
|
|
|
|
// 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(
|
|
"http://example.com/"
|
|
);
|
|
Assert.equal(
|
|
await contentPage.spawn([], () => content.document.URL),
|
|
"http://example.com/",
|
|
"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:
|
|
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#allowed-to-navigate
|
|
// 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,
|
|
// https://source.chromium.org/chromium/chromium/src/+/main:extensions/browser/api/declarative_net_request/request_params.cc;l=123-130;drc=8a27797c643fb0f2d9ae835f8d8b509e027c97e9
|
|
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"],
|
|
matches: ["http://*/?test_contentscript_load_frame"],
|
|
},
|
|
{
|
|
all_frames: true,
|
|
run_at: "document_end",
|
|
js: ["contentscript_in_iframe.js"],
|
|
matches: ["http://example.net/?test_contentscript_triggered_frame"],
|
|
},
|
|
],
|
|
},
|
|
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");
|
|
f.src = "http://example.com/?test_contentscript_triggered_frame";
|
|
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:
|
|
// https://searchfox.org/mozilla-central/rev/d85572c1963f72e8bef2787d900e0a8ffd8e6728/toolkit/components/extensions/webrequest/ChannelWrapper.cpp#551
|
|
// 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(
|
|
"http://example.com/?test_contentscript_load_frame"
|
|
);
|
|
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(
|
|
"http://example.com/?dummy_see_iframe_for_interesting_stuff"
|
|
);
|
|
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:
|
|
// https://searchfox.org/mozilla-central/rev/00ea1649b59d5f427979e2d6ba42be96f62d6e82/docshell/shistory/nsSHistory.cpp#360-363
|
|
// ... 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(
|
|
"http://example.com/bfcache_test?1_aar_no"
|
|
);
|
|
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(
|
|
"http://example.com/?do_get"
|
|
);
|
|
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();
|
|
}
|
|
);
|