Source code
Revision control
Copy as Markdown
Other Tools
Test Info: Warnings
- This test gets skipped with pattern: os == 'win' && socketprocess_networking && fission OR os == 'mac' && socketprocess_networking && fission OR os == 'mac' && debug OR os == 'linux' && socketprocess_networking
- Manifest: toolkit/components/extensions/test/xpcshell/xpcshell-remote.toml includes toolkit/components/extensions/test/xpcshell/xpcshell-common.toml
- Manifest: toolkit/components/extensions/test/xpcshell/xpcshell.toml includes toolkit/components/extensions/test/xpcshell/xpcshell-common.toml
"use strict";
// This file provides test coverage for regexFilter and regexSubstitution.
//
// The validate_actions task of test_ext_dnr_session_rules.js checks that the
// basic requirements of regexFilter + regexSubstitution are met.
//
// The match_regexFilter task of test_ext_dnr_testMatchOutcome.js verifies that
// regexFilter is evaluated correctly in testMatchOutcome.
//
// The quota on regexFilter is verified in test_ext_dnr_regexFilter_limits.js.
add_setup(() => {
Services.prefs.setBoolPref("extensions.manifestV3.enabled", true);
Services.prefs.setBoolPref("extensions.dnr.enabled", true);
Services.prefs.setBoolPref("extensions.dnr.feedback", true);
});
const server = createHttpServer({
hosts: ["example.com", "example-com", "from", "dest"],
});
server.registerPrefixHandler("/", (req, res) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.write("GOOD_RESPONSE");
});
// 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;
async function testFetch(from, to, description) {
let res = await fetch(from);
browser.test.assertEq(to, res.url, description);
browser.test.assertEq("GOOD_RESPONSE", await res.text(), "expected body");
}
async function _testRegexFilterOrRedirect({
description,
regexFilter,
isUrlFilterCaseSensitive,
regexSubstitution = expectedRedirectUrl,
urlsMatching,
urlsNonMatching,
}) {
browser.test.log(`Test description: ${description}`);
await dnr.updateSessionRules({
addRules: [
{
id: 12345,
condition: { regexFilter, isUrlFilterCaseSensitive },
action: { type: "redirect", redirect: { regexSubstitution } },
},
],
});
for (let url of urlsMatching) {
const description = `regexFilter ${regexFilter} should match: ${url}`;
await testFetch(url, expectedRedirectUrl, description);
}
for (let url of urlsNonMatching) {
const description = `regexFilter ${regexFilter} should not match: ${url}`;
let expectedUrl = new URL(url);
expectedUrl.hash = "";
await testFetch(url, expectedUrl.href, description);
}
await dnr.updateSessionRules({ removeRuleIds: [12345] });
}
async function testValidRegexFilter({
description,
regexFilter,
isUrlFilterCaseSensitive,
urlsMatching,
urlsNonMatching,
}) {
browser.test.assertDeepEq(
{ isSupported: true },
await dnr.isRegexSupported({
regex: regexFilter,
isCaseSensitive: isUrlFilterCaseSensitive,
}),
`isRegexSupported should detect support for: ${regexFilter}`
);
await _testRegexFilterOrRedirect({
description,
regexFilter,
isUrlFilterCaseSensitive,
urlsMatching,
urlsNonMatching,
});
}
async function testValidRegexSubstitution({
description,
regexFilter,
regexSubstitution,
inputUrl,
expectedRedirectUrl,
}) {
browser.test.assertDeepEq(
{ isSupported: true },
await dnr.isRegexSupported({
regex: regexFilter,
// requireCapturing option not strictly needed, but included to verify
// that the method can take the option without issues.
requireCapturing: true,
}),
`isRegexSupported should accept regexFilter: ${regexFilter}`
);
await _testRegexFilterOrRedirect({
description,
regexFilter,
regexSubstitution,
urlsMatching: [inputUrl],
urlsNonMatching: [],
expectedRedirectUrl,
});
}
async function testInvalidRegexFilter(regexFilter, expectedError, msg) {
browser.test.assertDeepEq(
{ isSupported: false, reason: "syntaxError" },
await dnr.isRegexSupported({ regex: regexFilter }),
`isRegexSupported should detect unsupported regex: ${regexFilter}`
);
await browser.test.assertRejects(
dnr.updateSessionRules({
addRules: [
{ id: 123, condition: { regexFilter }, action: { type: "block" } },
],
}),
expectedError,
`Should reject invalid regexFilter (${regexFilter}) - ${msg}`
);
}
async function testInvalidRegexSubstitution(
regexSubstitution,
expectedError,
msg
) {
await browser.test.assertRejects(
_testRegexFilterOrRedirect({
description: `testInvalidRegexSubstitution: "${regexSubstitution}"`,
regexFilter: ".",
regexSubstitution,
urlsMatching: [],
urlsNonMatching: [],
}),
expectedError,
msg
);
}
async function testRejectedRedirectAtRuntime({ regexSubstitution, url }) {
// Some regexSubstitution rules pass validation but the generated redirect
// URL is rejected at runtime. That is validated here.
await _testRegexFilterOrRedirect({
description: `testRejectedRedirectAtRuntime for URL: ${url}`,
regexSubstitution,
// When regexSubstitution is invalid, it should not be redirected:
expectedRedirectUrl: url,
urlsMatching: [url],
urlsNonMatching: [],
});
}
Object.assign(dnrTestUtils, {
testValidRegexFilter,
testValidRegexSubstitution,
testInvalidRegexFilter,
testInvalidRegexSubstitution,
testRejectedRedirectAtRuntime,
});
return dnrTestUtils;
}
async function runAsDNRExtension({ background, manifest }) {
let extension = ExtensionTestUtils.loadExtension({
background: `(${background})((${makeDnrTestUtils})())`,
allowInsecureRequests: true,
manifest: {
manifest_version: 3,
permissions: ["declarativeNetRequest"],
// host_permissions are needed for the redirect action.
host_permissions: ["<all_urls>"],
granted_host_permissions: true,
...manifest,
},
temporarilyInstalled: true, // <-- for granted_host_permissions
});
await extension.startup();
await extension.awaitFinish();
await extension.unload();
}
// The least common denominator across Chrome, Safari and Firefox is Safari, at
// the time of writing, the supported syntax in Safari's regexFilter is
// section "The Regular expression format":
//
// - Matching any character with “.”.
// - Matching ranges with the range syntax [a-b].
// - Quantifying expressions with “?”, “+” and “*”.
// - Groups with parenthesis.
// - ... beginning of line (“^”) and end of line (“$”) marker ...
//
// The above syntax is very limited, as expressed at
//
// The tests continue in regexFilter_more_than_basic.
add_task(async function regexFilter_basic() {
await runAsDNRExtension({
background: async dnrTestUtils => {
const { testValidRegexFilter } = dnrTestUtils;
await testValidRegexFilter({
description: "URL as regexFilter is sometimes a valid regexp",
urlsMatching: [
// dot is wildcard.
// Without ^ anchor, matches substring elsewhere.
],
urlsNonMatching: [
// Does not match reference fragment.
],
});
await testValidRegexFilter({
description: "\\. is literal dot",
});
await testValidRegexFilter({
description: "[a-b] range is supported",
});
await testValidRegexFilter({
description: "groups with parenthesis are supported",
});
await testValidRegexFilter({
description: "+, * and ? are quantifiers",
regexFilter: "a+b*c?d",
urlsMatching: [
],
urlsNonMatching: [
],
});
await testValidRegexFilter({
description: ".* matches anything",
regexFilter: "a.*b",
});
await testValidRegexFilter({
description: "^ is start-of-string anchor",
});
await testValidRegexFilter({
description: "$ is end-of-string anchor",
});
browser.test.notifyPass();
},
});
});
// regexFilter_basic lists the bare minimum, this tests more useful features.
add_task(async function regexFilter_more_than_basic() {
await runAsDNRExtension({
background: async dnrTestUtils => {
const { testValidRegexFilter } = dnrTestUtils;
// Use cases listed at
await testValidRegexFilter({
description: "{n,m} quantifier",
});
await testValidRegexFilter({
description: "{n,} quantifier",
});
await testValidRegexFilter({
description: "| disjunction and within groups",
regexFilter: "from/a|from/b$|c$",
});
await testValidRegexFilter({
description: "(?!) negative look-ahead",
});
// Features based on
await testValidRegexFilter({
description: "Negated character class",
});
await testValidRegexFilter({
description: "Word character class (\\w)",
});
// Rule that leads to "memoryLimitExceeded" in Chrome:
await testValidRegexFilter({
description: "regexFilter that triggers memoryLimitExceeded in Chrome",
regexFilter: "(https?://)104\\.154\\..{100,}",
});
browser.test.notifyPass();
},
});
});
// Adds more coverage in addition to what was tested by validate_regexFilter in
// test_ext_dnr_session_rules.js.
add_task(async function regexFilter_invalid() {
await runAsDNRExtension({
background: async dnrTestUtils => {
const { testInvalidRegexFilter } = dnrTestUtils;
await testInvalidRegexFilter(
"(",
"regexFilter is not a valid regular expression",
"( opens a group and should be closed"
);
await testInvalidRegexFilter(
"straß.d",
"regexFilter should not contain non-ASCII characters",
"regexFilter matches the canonical URL which does not contain non-ASCII"
);
browser.test.notifyPass();
},
});
});
add_task(async function regexFilter_isUrlFilterCaseSensitive() {
await runAsDNRExtension({
background: async dnrTestUtils => {
const { testValidRegexFilter } = dnrTestUtils;
await testValidRegexFilter({
description: "isUrlFilterCaseSensitive omitted (= false by default)",
// isUrlFilterCaseSensitive = false by default.
regexFilter: "from/Pa",
urlsNonMatching: [],
});
await testValidRegexFilter({
description: "isUrlFilterCaseSensitive: false",
isUrlFilterCaseSensitive: false,
regexFilter: "from/Pa",
urlsNonMatching: [],
});
await testValidRegexFilter({
description: "isUrlFilterCaseSensitive: true",
isUrlFilterCaseSensitive: true,
regexFilter: "from/Pa",
});
await testValidRegexFilter({
description: "Case-sensitive uppercase regexFilter cannot match HOST",
isUrlFilterCaseSensitive: true,
regexFilter: "FROM",
urlsMatching: [],
});
browser.test.notifyPass();
},
});
});
add_task(async function regexSubstitution_invalid() {
let { messages } = await promiseConsoleOutput(async () => {
await runAsDNRExtension({
manifest: { browser_specific_settings: { gecko: { id: "@dnr" } } },
background: async dnrTestUtils => {
const { testRejectedRedirectAtRuntime, testInvalidRegexSubstitution } =
dnrTestUtils;
await testInvalidRegexSubstitution(
"redirect.regexSubstitution only allows digit or \\ after \\.",
"\\x should not be allowed in regexSubstitution"
);
await testInvalidRegexSubstitution(
"redirect.regexSubstitution only allows digit or \\ after \\.",
"\\<end> should not be allowed in regexSubstitution"
);
await testRejectedRedirectAtRuntime({
regexSubstitution: "not-URL",
});
await testRejectedRedirectAtRuntime({
});
await testRejectedRedirectAtRuntime({
regexSubstitution: "data:,redirect-from-dnr",
});
await testRejectedRedirectAtRuntime({
});
browser.test.notifyPass();
},
});
});
AddonTestUtils.checkMessages(messages, {
expected: [
{
message: /Extension @dnr tried to redirect to an invalid URL: not-URL/,
},
{
message: /Extension @dnr may not redirect to: javascript:\/\/-URL/,
},
{
message: /Extension @dnr may not redirect to: data:,redirect-from-dnr/,
},
{
message: /Extension @dnr may not redirect to: resource:\/\/gre\//,
},
],
});
});
add_task(async function regexSubstitution_valid() {
await runAsDNRExtension({
background: async dnrTestUtils => {
const { testValidRegexSubstitution } = dnrTestUtils;
await testValidRegexSubstitution({
description: "All captured groups can be accessed by \\1 - \\9",
regexFilter: "from/(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)",
// ^ captured groups: 123456789
});
await testValidRegexSubstitution({
description: "\\0 captures the full match",
regexFilter: "from/$",
});
await testValidRegexSubstitution({
description: "\\10 means: captured group 1 + literal 0",
regexFilter: "/(captured)$",
});
await testValidRegexSubstitution({
description: "\\\\ is an escaped backslash",
regexFilter: "/(XXX)",
});
await testValidRegexSubstitution({
description: "Captured groups can be repeated",
regexFilter: "/(captured)$",
});
await testValidRegexSubstitution({
description: "Non-matching optional group is an empty string",
regexFilter: "(doesnotmatch)?suffix",
});
await testValidRegexSubstitution({
description: "Non-existing capturing group is an empty string",
regexFilter: "(captured)",
});
await testValidRegexSubstitution({
description: "Non-capturing group is not captured",
regexFilter: "(?:non-)(captured)",
});
browser.test.notifyPass();
},
});
});
add_task(async function regexSubstitution_redirect_chain() {
await runAsDNRExtension({
background: async dnrTestUtils => {
const { testValidRegexSubstitution } = dnrTestUtils;
await testValidRegexSubstitution({
description: "regexFilter matches intermediate redirect URLs",
regexSubstitution: "\\1\\3",
// After redirecting three times, we end up here:
});
browser.test.notifyPass();
},
});
});