blob: edde84b04521e828561c8ae45da100c9a6cdedce [file] [log] [blame]
// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "extensions/browser/api/declarative_net_request/ruleset_matcher.h"
#include <array>
#include <limits>
#include <optional>
#include <utility>
#include <vector>
#include "base/check.h"
#include "base/files/file_util.h"
#include "base/format_macros.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/test/scoped_feature_list.h"
#include "components/url_pattern_index/flat/url_pattern_index_generated.h"
#include "components/version_info/channel.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/mock_navigation_handle.h"
#include "content/public/test/navigation_simulator.h"
#include "content/public/test/test_renderer_host.h"
#include "content/public/test/web_contents_tester.h"
#include "extensions/browser/api/declarative_net_request/constants.h"
#include "extensions/browser/api/declarative_net_request/file_backed_ruleset_source.h"
#include "extensions/browser/api/declarative_net_request/request_action.h"
#include "extensions/browser/api/declarative_net_request/request_params.h"
#include "extensions/browser/api/declarative_net_request/test_utils.h"
#include "extensions/browser/api/declarative_net_request/utils.h"
#include "extensions/browser/extensions_test.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/api/declarative_net_request.h"
#include "extensions/common/api/declarative_net_request/constants.h"
#include "extensions/common/api/declarative_net_request/test_utils.h"
#include "extensions/common/extension_features.h"
#include "extensions/common/features/feature_channel.h"
#include "net/http/http_request_headers.h"
#include "net/http/http_response_headers.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
#include "url/origin.h"
static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));
namespace extensions::declarative_net_request {
namespace {
namespace dnr_api = api::declarative_net_request;
using RulesetMatcherTest = ExtensionsTest;
// Tests a simple blocking rule.
TEST_F(RulesetMatcherTest, BlockingRule) {
TestRule rule = CreateGenericRule();
rule.condition->url_filter = std::string("google.com");
std::unique_ptr<RulesetMatcher> matcher;
ASSERT_TRUE(CreateVerifiedMatcher({rule}, CreateTemporarySource(), &matcher));
auto should_block_request = [&matcher](const RequestParams& params) {
auto action =
matcher->GetAction(params, RulesetMatchingStage::kOnBeforeRequest);
return action.has_value() && action->IsBlockOrCollapse();
};
GURL google_url("http://google.com");
GURL yahoo_url("http://yahoo.com");
RequestParams params;
params.url = &google_url;
params.element_type = url_pattern_index::flat::ElementType_SUBDOCUMENT;
params.is_third_party = true;
EXPECT_TRUE(should_block_request(params));
params.url = &yahoo_url;
EXPECT_FALSE(should_block_request(params));
}
// Tests a simple redirect rule.
TEST_F(RulesetMatcherTest, RedirectRule) {
TestRule rule = CreateGenericRule();
rule.condition->url_filter = std::string("google.com");
rule.priority = kMinValidPriority;
rule.action->type = std::string("redirect");
rule.action->redirect.emplace();
rule.action->redirect->url = std::string("http://yahoo.com");
std::unique_ptr<RulesetMatcher> matcher;
ASSERT_TRUE(CreateVerifiedMatcher({rule}, CreateTemporarySource(), &matcher));
GURL google_url("http://google.com");
GURL yahoo_url("http://yahoo.com");
RequestParams params;
params.url = &google_url;
params.element_type = url_pattern_index::flat::ElementType_SUBDOCUMENT;
params.is_third_party = true;
std::optional<RequestAction> redirect_action =
matcher->GetAction(params, RulesetMatchingStage::kOnBeforeRequest);
ASSERT_TRUE(redirect_action);
ASSERT_EQ(redirect_action->type, RequestAction::Type::REDIRECT);
EXPECT_EQ(yahoo_url, redirect_action->redirect_url);
params.url = &yahoo_url;
EXPECT_FALSE(
matcher->GetAction(params, RulesetMatchingStage::kOnBeforeRequest));
}
// Test that a URL cannot redirect to itself, as filed in crbug.com/954646.
TEST_F(RulesetMatcherTest, PreventSelfRedirect) {
TestRule rule = CreateGenericRule();
rule.condition->url_filter = std::string("go*");
rule.priority = kMinValidPriority;
rule.action->type = std::string("redirect");
rule.action->redirect.emplace();
rule.action->redirect->url = std::string("http://google.com");
std::unique_ptr<RulesetMatcher> matcher;
ASSERT_TRUE(CreateVerifiedMatcher({rule}, CreateTemporarySource(), &matcher));
GURL url("http://google.com");
RequestParams params;
params.url = &url;
params.element_type = url_pattern_index::flat::ElementType_SUBDOCUMENT;
params.is_third_party = true;
EXPECT_FALSE(
matcher->GetAction(params, RulesetMatchingStage::kOnBeforeRequest));
}
// Tests a simple upgrade scheme rule.
TEST_F(RulesetMatcherTest, UpgradeRule) {
TestRule rule = CreateGenericRule();
rule.condition->url_filter = std::string("google.com");
rule.priority = kMinValidPriority;
rule.action->type = std::string("upgradeScheme");
std::unique_ptr<RulesetMatcher> matcher;
ASSERT_TRUE(CreateVerifiedMatcher({rule}, CreateTemporarySource(), &matcher));
auto should_upgrade_request = [&matcher](const RequestParams& params) {
auto action =
matcher->GetAction(params, RulesetMatchingStage::kOnBeforeRequest);
return action.has_value() && action->type == RequestAction::Type::UPGRADE;
};
GURL google_url("http://google.com");
GURL yahoo_url("http://yahoo.com");
GURL non_upgradeable_url("https://google.com");
RequestParams params;
params.url = &google_url;
params.element_type = url_pattern_index::flat::ElementType_SUBDOCUMENT;
params.is_third_party = true;
EXPECT_TRUE(should_upgrade_request(params));
params.url = &yahoo_url;
EXPECT_FALSE(should_upgrade_request(params));
params.url = &non_upgradeable_url;
EXPECT_FALSE(should_upgrade_request(params));
}
// Tests that a modified ruleset file fails verification.
TEST_F(RulesetMatcherTest, FailedVerification) {
FileBackedRulesetSource source = CreateTemporarySource();
std::unique_ptr<RulesetMatcher> matcher;
int expected_checksum;
ASSERT_TRUE(CreateVerifiedMatcher({}, source, &matcher, &expected_checksum));
// Persist invalid data to the ruleset file and ensure that a version mismatch
// occurs.
std::string data = "invalid data";
ASSERT_TRUE(base::WriteFile(source.indexed_path(), data));
EXPECT_EQ(LoadRulesetResult::kErrorVersionMismatch,
source.CreateVerifiedMatcher(expected_checksum, &matcher));
// Now, persist invalid data to the ruleset file, while maintaining the
// correct version header. Ensure that it fails verification due to checksum
// mismatch.
data = GetVersionHeaderForTesting() + "invalid data";
ASSERT_TRUE(base::WriteFile(source.indexed_path(), data));
EXPECT_EQ(LoadRulesetResult::kErrorChecksumMismatch,
source.CreateVerifiedMatcher(expected_checksum, &matcher));
}
TEST_F(RulesetMatcherTest, ModifyHeaders_IsExtraHeaderMatcher) {
TestRule rule = CreateGenericRule();
rule.condition->url_filter = std::string("example.com");
std::unique_ptr<RulesetMatcher> matcher;
ASSERT_TRUE(CreateVerifiedMatcher({rule}, CreateTemporarySource(), &matcher));
EXPECT_FALSE(matcher->IsExtraHeadersMatcher());
rule.action->type = std::string("modifyHeaders");
rule.action->response_headers = std::vector<TestHeaderInfo>(
{TestHeaderInfo("HEADER3", "append", "VALUE3")});
ASSERT_TRUE(CreateVerifiedMatcher({rule}, CreateTemporarySource(), &matcher));
EXPECT_TRUE(matcher->IsExtraHeadersMatcher());
}
TEST_F(RulesetMatcherTest, ModifyHeaders) {
TestRule rule_1 = CreateGenericRule();
rule_1.id = kMinValidID;
rule_1.priority = kMinValidPriority + 1;
rule_1.condition->url_filter = std::string("example.com");
rule_1.action->type = std::string("modifyHeaders");
rule_1.action->request_headers = std::vector<TestHeaderInfo>(
{TestHeaderInfo("header1", "remove", std::nullopt)});
TestRule rule_2 = CreateGenericRule();
rule_2.id = kMinValidID + 1;
rule_2.priority = kMinValidPriority;
rule_2.condition->url_filter = std::string("example.com");
rule_2.action->type = std::string("modifyHeaders");
rule_2.action->request_headers = std::vector<TestHeaderInfo>(
{TestHeaderInfo("header1", "set", "value1"),
TestHeaderInfo("header2", "remove", std::nullopt)});
std::unique_ptr<RulesetMatcher> matcher;
ASSERT_TRUE(CreateVerifiedMatcher({rule_1, rule_2}, CreateTemporarySource(),
&matcher));
EXPECT_TRUE(matcher->IsExtraHeadersMatcher());
GURL example_url("http://example.com");
RequestParams params;
params.url = &example_url;
params.element_type = url_pattern_index::flat::ElementType_SUBDOCUMENT;
params.is_third_party = true;
std::vector<RequestAction> modify_header_actions =
matcher->GetModifyHeadersActions(
params, RulesetMatchingStage::kOnBeforeRequest, /*min_priority=*/0u);
RequestAction expected_rule_1_action = CreateRequestActionForTesting(
RequestAction::Type::MODIFY_HEADERS, *rule_1.id, *rule_1.priority);
expected_rule_1_action.request_headers_to_modify = {RequestAction::HeaderInfo(
"header1", dnr_api::HeaderOperation::kRemove, std::nullopt)};
RequestAction expected_rule_2_action = CreateRequestActionForTesting(
RequestAction::Type::MODIFY_HEADERS, *rule_2.id, *rule_2.priority);
expected_rule_2_action.request_headers_to_modify = {
RequestAction::HeaderInfo("header1", dnr_api::HeaderOperation::kSet,
"value1"),
RequestAction::HeaderInfo("header2", dnr_api::HeaderOperation::kRemove,
std::nullopt)};
EXPECT_THAT(modify_header_actions,
testing::UnorderedElementsAre(
testing::Eq(testing::ByRef(expected_rule_1_action)),
testing::Eq(testing::ByRef(expected_rule_2_action))));
}
// Tests a rule to redirect to an extension path.
TEST_F(RulesetMatcherTest, RedirectToExtensionPath) {
TestRule rule = CreateGenericRule();
rule.condition->url_filter = std::string("example.com");
rule.action->type = std::string("redirect");
rule.priority = kMinValidPriority;
rule.action->redirect.emplace();
rule.action->redirect->extension_path = "/path/newfile.js?query#fragment";
std::unique_ptr<RulesetMatcher> matcher;
const RulesetID kRulesetId(5);
const size_t kRuleCountLimit = 10;
ASSERT_TRUE(CreateVerifiedMatcher(
{rule}, CreateTemporarySource(kRulesetId, kRuleCountLimit), &matcher));
GURL example_url("http://example.com");
RequestParams params;
params.url = &example_url;
std::optional<RequestAction> redirect_action =
matcher->GetAction(params, RulesetMatchingStage::kOnBeforeRequest);
RequestAction expected_action = CreateRequestActionForTesting(
RequestAction::Type::REDIRECT, *rule.id, *rule.priority, kRulesetId);
expected_action.redirect_url =
GURL("chrome-extension://extensionid/path/newfile.js?query#fragment");
EXPECT_EQ(expected_action, redirect_action);
}
// Tests a rule to redirect to a static url.
TEST_F(RulesetMatcherTest, RedirectToStaticUrl) {
TestRule rule = CreateGenericRule();
rule.condition->url_filter = std::string("example.com");
rule.action->type = std::string("redirect");
rule.priority = kMinValidPriority;
rule.action->redirect.emplace();
rule.action->redirect->url = "https://google.com";
std::unique_ptr<RulesetMatcher> matcher;
ASSERT_TRUE(CreateVerifiedMatcher({rule}, CreateTemporarySource(), &matcher));
GURL example_url("http://example.com");
RequestParams params;
params.url = &example_url;
std::optional<RequestAction> redirect_action =
matcher->GetAction(params, RulesetMatchingStage::kOnBeforeRequest);
ASSERT_TRUE(redirect_action.has_value());
EXPECT_EQ(redirect_action->type, RequestAction::Type::REDIRECT);
GURL expected_redirect_url("https://google.com");
EXPECT_EQ(expected_redirect_url, redirect_action->redirect_url);
}
// Tests url transformation rules.
TEST_F(RulesetMatcherTest, UrlTransform) {
struct TestCase {
std::string url;
// Valid if a redirect is expected.
std::optional<std::string> expected_redirect_url;
};
std::vector<TestCase> cases;
std::vector<TestRule> rules;
auto create_transform_rule = [](size_t id, const std::string& filter) {
TestRule rule = CreateGenericRule();
rule.id = id;
rule.condition->url_filter = filter;
rule.priority = kMinValidPriority;
rule.action->type = std::string("redirect");
rule.action->redirect.emplace();
rule.action->redirect->transform.emplace();
return rule;
};
TestRule rule = create_transform_rule(1, "||1.com");
rule.action->redirect->transform->scheme = "https";
rules.push_back(rule);
cases.push_back({"http://1.com/path?query", "https://1.com/path?query"});
rule = create_transform_rule(2, "||2.com");
rule.action->redirect->transform->scheme = "ftp";
rule.action->redirect->transform->host = "ftp.host.com";
rule.action->redirect->transform->port = "70";
rules.push_back(rule);
cases.push_back(
{"http://2.com:100/path?query", "ftp://ftp.host.com:70/path?query"});
rule = create_transform_rule(3, "||3.com");
rule.action->redirect->transform->port = "";
rule.action->redirect->transform->path = "";
rule.action->redirect->transform->query = "?new_query";
rule.action->redirect->transform->fragment = "#fragment";
rules.push_back(rule);
// The path separator '/' is output even when cleared, except when there is no
// authority, query and fragment.
cases.push_back(
{"http://3.com:100/path?query", "http://3.com/?new_query#fragment"});
rule = create_transform_rule(4, "||4.com");
rule.action->redirect->transform->scheme = "http";
rule.action->redirect->transform->path = " ";
rules.push_back(rule);
cases.push_back({"http://4.com/xyz", "http://4.com/%20"});
rule = create_transform_rule(5, "||5.com");
rule.action->redirect->transform->path = "/";
rule.action->redirect->transform->query = "";
rule.action->redirect->transform->fragment = "#";
rules.push_back(rule);
cases.push_back(
{"http://5.com:100/path?query#fragment", "http://5.com:100/#"});
rule = create_transform_rule(6, "||6.com");
rule.action->redirect->transform->path = "/path?query";
rules.push_back(rule);
// The "?" in path is url encoded since it's not part of the query.
cases.push_back({"http://6.com/xyz?q1", "http://6.com/path%3Fquery?q1"});
rule = create_transform_rule(7, "||7.com");
rule.action->redirect->transform->username = "new_user";
rule.action->redirect->transform->password = "new_pass";
rules.push_back(rule);
cases.push_back(
{"http://user@7.com/xyz", "http://new_user:new_pass@7.com/xyz"});
auto make_query = [](const std::string& key, const std::string& value,
const std::optional<bool>& replace_only = std::nullopt) {
TestRuleQueryKeyValue query;
query.key = key;
query.value = value;
query.replace_only = replace_only;
return query;
};
rule = create_transform_rule(8, "||8.com");
rule.action->redirect->transform->query_transform.emplace();
rule.action->redirect->transform->query_transform->remove_params =
std::vector<std::string>({"r1", "r2"});
rule.action->redirect->transform->query_transform->add_or_replace_params =
std::vector<TestRuleQueryKeyValue>(
{make_query("a1", "#"), make_query("a2", ""),
make_query("a1", "new2"), make_query("a1", "new3")});
rules.push_back(rule);
cases.push_back(
{"http://8.com/"
"path?r1&r1=val1&a1=val1&r2=val&x3=val&a1=val2&a2=val&r1=val2",
"http://8.com/path?a1=%23&x3=val&a1=new2&a2=&a1=new3"});
cases.push_back({"http://8.com/path?query",
"http://8.com/path?query=&a1=%23&a2=&a1=new2&a1=new3"});
rule = create_transform_rule(9, "||9.com");
rule.action->redirect->transform->query_transform.emplace();
rule.action->redirect->transform->query_transform->remove_params =
std::vector<std::string>({"r1", "r2"});
rules.push_back(rule);
// No redirect is performed since the url won't change.
cases.push_back({"http://9.com/path?query#fragment", std::nullopt});
rule = create_transform_rule(10, "||10.com");
rule.action->redirect->transform->query_transform.emplace();
rule.action->redirect->transform->query_transform->remove_params =
std::vector<std::string>({"q1"});
rule.action->redirect->transform->query_transform->add_or_replace_params =
std::vector<TestRuleQueryKeyValue>({make_query("q1", "new")});
rules.push_back(rule);
cases.push_back(
{"https://10.com/path?q1=1&q1=2&q1=3", "https://10.com/path?q1=new"});
rule = create_transform_rule(11, "||11.com");
rule.action->redirect->transform->query_transform.emplace();
rule.action->redirect->transform->query_transform->add_or_replace_params =
std::vector<TestRuleQueryKeyValue>(
{make_query("foo", "bar"), make_query("hello", "world", false),
make_query("abc", "123", true), make_query("abc", "456", true)});
rules.push_back(rule);
cases.push_back(
{"https://11.com/path", "https://11.com/path?foo=bar&hello=world"});
cases.push_back({"https://11.com/path?abc=def",
"https://11.com/path?abc=123&foo=bar&hello=world"});
cases.push_back({"https://11.com/path?hello=goodbye&abc=def",
"https://11.com/path?hello=world&abc=123&foo=bar"});
cases.push_back({"https://11.com/path?foo=1&foo=2",
"https://11.com/path?foo=bar&foo=2&hello=world"});
cases.push_back(
{"https://11.com/path?abc=1&abc=2&abc=3",
"https://11.com/path?abc=123&abc=456&abc=3&foo=bar&hello=world"});
std::unique_ptr<RulesetMatcher> matcher;
ASSERT_TRUE(CreateVerifiedMatcher(rules, CreateTemporarySource(), &matcher));
for (const auto& test_case : cases) {
SCOPED_TRACE(base::StringPrintf("Testing url %s", test_case.url.c_str()));
GURL url(test_case.url);
ASSERT_TRUE(url.is_valid()) << test_case.url;
RequestParams params;
params.url = &url;
std::optional<RequestAction> redirect_action =
matcher->GetAction(params, RulesetMatchingStage::kOnBeforeRequest);
if (!test_case.expected_redirect_url) {
EXPECT_FALSE(redirect_action) << redirect_action->redirect_url->spec();
continue;
}
ASSERT_TRUE(redirect_action.has_value());
EXPECT_EQ(redirect_action->type, RequestAction::Type::REDIRECT);
ASSERT_TRUE(GURL(*test_case.expected_redirect_url).is_valid())
<< *test_case.expected_redirect_url;
ASSERT_TRUE(redirect_action.has_value());
EXPECT_EQ(GURL(*test_case.expected_redirect_url),
redirect_action->redirect_url);
}
}
// Tests regex rules are evaluated correctly for different action types.
TEST_F(RulesetMatcherTest, RegexRules) {
auto create_regex_rule = [](size_t id, const std::string& regex_filter) {
TestRule rule = CreateGenericRule();
rule.id = id;
rule.condition->url_filter.reset();
rule.condition->regex_filter = regex_filter;
return rule;
};
std::vector<TestRule> rules;
// Add a blocking rule.
TestRule block_rule = create_regex_rule(1, R"((?:block|collapse)\.com/path)");
rules.push_back(block_rule);
// Add an allowlist rule.
TestRule allow_rule = create_regex_rule(2, R"(http://(\w+\.)+allow\.com)");
allow_rule.action->type = "allow";
rules.push_back(allow_rule);
// Add a redirect rule.
TestRule redirect_rule = create_regex_rule(3, R"(redirect\.com)");
redirect_rule.action->type = "redirect";
redirect_rule.action->redirect.emplace();
redirect_rule.priority = kMinValidPriority;
redirect_rule.action->redirect->url = "https://google.com";
rules.push_back(redirect_rule);
// Add a upgrade rule.
TestRule upgrade_rule = create_regex_rule(4, "upgrade");
upgrade_rule.action->type = "upgradeScheme";
upgrade_rule.priority = kMinValidPriority;
rules.push_back(upgrade_rule);
// Add a modify headers rule.
TestRule modify_headers_rule =
create_regex_rule(6, R"(^(?:http|https)://[a-z\.]+\.org)");
modify_headers_rule.action->type = "modifyHeaders";
modify_headers_rule.action->request_headers =
std::vector<TestHeaderInfo>({TestHeaderInfo("header1", "set", "value1")});
rules.push_back(modify_headers_rule);
std::unique_ptr<RulesetMatcher> matcher;
ASSERT_TRUE(CreateVerifiedMatcher(rules, CreateTemporarySource(), &matcher));
struct TestCase {
const char* url = nullptr;
std::optional<RequestAction> expected_action;
std::optional<RequestAction> expected_modify_header_action;
};
std::vector<TestCase> test_cases;
{
TestCase test_case = {"http://www.block.com/path"};
test_case.expected_action = CreateRequestActionForTesting(
RequestAction::Type::BLOCK, *block_rule.id);
test_cases.push_back(std::move(test_case));
}
{
TestCase test_case = {"http://www.collapse.com/PATH"};
// Filters are case insensitive by default, hence the request will match.
test_case.expected_action = CreateRequestActionForTesting(
RequestAction::Type::BLOCK, *block_rule.id);
test_cases.push_back(std::move(test_case));
}
{
TestCase test_case = {"http://abc.xyz.allow.com/path"};
test_case.expected_action = CreateRequestActionForTesting(
RequestAction::Type::ALLOW, *allow_rule.id);
test_cases.push_back(std::move(test_case));
}
{
TestCase test_case = {"http://allow.com/path"};
test_cases.push_back(std::move(test_case));
}
{
TestCase test_case = {"http://redirect.com?path=abc"};
test_case.expected_action = CreateRequestActionForTesting(
RequestAction::Type::REDIRECT, *redirect_rule.id);
test_case.expected_action->redirect_url =
GURL(*redirect_rule.action->redirect->url);
test_cases.push_back(std::move(test_case));
}
{
TestCase test_case = {"http://redirect.eu#query"};
test_cases.push_back(std::move(test_case));
}
{
TestCase test_case = {"http://example.com/upgrade"};
test_case.expected_action = CreateRequestActionForTesting(
RequestAction::Type::UPGRADE, *upgrade_rule.id);
test_case.expected_action->redirect_url.emplace(
"https://example.com/upgrade");
test_cases.push_back(std::move(test_case));
}
{
TestCase test_case = {"http://abc.org/path"};
test_case.expected_modify_header_action = CreateRequestActionForTesting(
RequestAction::Type::MODIFY_HEADERS, *modify_headers_rule.id);
test_case.expected_modify_header_action->request_headers_to_modify = {
RequestAction::HeaderInfo("header1", dnr_api::HeaderOperation::kSet,
"value1")};
test_cases.push_back(std::move(test_case));
}
{
TestCase test_case = {"http://example.com"};
test_cases.push_back(std::move(test_case));
}
for (const auto& test_case : test_cases) {
SCOPED_TRACE(test_case.url);
GURL url(test_case.url);
RequestParams params;
params.url = &url;
EXPECT_EQ(
test_case.expected_action,
matcher->GetAction(params, RulesetMatchingStage::kOnBeforeRequest));
std::vector<RequestAction> modify_header_actions =
matcher->GetModifyHeadersActions(params,
RulesetMatchingStage::kOnBeforeRequest,
/*min_priority=*/0u);
if (test_case.expected_modify_header_action) {
EXPECT_THAT(modify_header_actions,
testing::ElementsAre(testing::Eq(testing::ByRef(
test_case.expected_modify_header_action))));
} else {
EXPECT_TRUE(modify_header_actions.empty());
}
}
EXPECT_TRUE(matcher->IsExtraHeadersMatcher());
}
// Ensure that the rule metadata is checked correctly for regex rules.
TEST_F(RulesetMatcherTest, RegexRules_Metadata) {
auto create_regex_rule = [](size_t id, const std::string& regex_filter) {
TestRule rule = CreateGenericRule();
rule.id = id;
rule.condition->url_filter.reset();
rule.condition->regex_filter = regex_filter;
return rule;
};
std::vector<TestRule> rules;
// Add a case sensitive rule.
TestRule path_rule = create_regex_rule(1, "/PATH");
path_rule.condition->is_url_filter_case_sensitive = true;
rules.push_back(path_rule);
// Add a case insensitive rule.
TestRule xyz_rule = create_regex_rule(2, "/XYZ");
rules.push_back(xyz_rule);
// Test `domains`, `excludedDomains`.
TestRule domains_rule = create_regex_rule(3, "deprecated_domains");
domains_rule.condition->domains = std::vector<std::string>({"example.com"});
domains_rule.condition->excluded_domains =
std::vector<std::string>({"b.example.com"});
rules.push_back(domains_rule);
// Test `initiatorDomains`, `excludedInitiatorDomains`.
TestRule initiator_domains_rule = create_regex_rule(4, "initiator_domains");
initiator_domains_rule.condition->initiator_domains =
std::vector<std::string>({"example.com"});
initiator_domains_rule.condition->excluded_initiator_domains =
std::vector<std::string>({"b.example.com"});
rules.push_back(initiator_domains_rule);
// Test `requestDomains`, `excludedRequestDomains`.
TestRule request_domains_rule = create_regex_rule(5, "request_domains");
request_domains_rule.condition->request_domains =
std::vector<std::string>({"example.com"});
request_domains_rule.condition->excluded_request_domains =
std::vector<std::string>({"b.example.com"});
rules.push_back(request_domains_rule);
// Test `resourceTypes`.
TestRule sub_frame_rule = create_regex_rule(6, R"((abc|def)\.com)");
sub_frame_rule.condition->resource_types =
std::vector<std::string>({"sub_frame"});
rules.push_back(sub_frame_rule);
// Test `domainType`.
TestRule third_party_rule = create_regex_rule(7, R"(http://(\d+)\.com)");
third_party_rule.condition->domain_type = "thirdParty";
rules.push_back(third_party_rule);
std::unique_ptr<RulesetMatcher> matcher;
ASSERT_TRUE(CreateVerifiedMatcher(rules, CreateTemporarySource(), &matcher));
struct TestCase {
const char* url = nullptr;
url::Origin first_party_origin;
url_pattern_index::flat::ElementType element_type =
url_pattern_index::flat::ElementType_OTHER;
bool is_third_party = false;
std::optional<RequestAction> expected_action;
};
std::vector<TestCase> test_cases;
{
TestCase test_case = {"http://example.com/path/abc"};
test_cases.push_back(std::move(test_case));
}
{
TestCase test_case = {"http://example.com/PATH/abc"};
test_case.expected_action = CreateRequestActionForTesting(
RequestAction::Type::BLOCK, *path_rule.id);
test_cases.push_back(std::move(test_case));
}
{
TestCase test_case = {"http://example.com/xyz/abc"};
test_case.expected_action =
CreateRequestActionForTesting(RequestAction::Type::BLOCK, *xyz_rule.id);
test_cases.push_back(std::move(test_case));
}
{
TestCase test_case = {"http://example.com/XYZ/abc"};
test_case.expected_action =
CreateRequestActionForTesting(RequestAction::Type::BLOCK, *xyz_rule.id);
test_cases.push_back(std::move(test_case));
}
{
TestCase test_case = {"http://example.com/deprecated_domains"};
test_case.first_party_origin =
url::Origin::Create(GURL("http://a.example.com"));
test_case.is_third_party = true;
test_case.expected_action = CreateRequestActionForTesting(
RequestAction::Type::BLOCK, *domains_rule.id);
test_cases.push_back(std::move(test_case));
}
{
TestCase test_case = {"http://example.com/deprecated_domains"};
test_case.first_party_origin =
url::Origin::Create(GURL("http://b.example.com"));
test_case.is_third_party = true;
test_cases.push_back(std::move(test_case));
}
{
TestCase test_case = {"http://example.com/initiator_domains"};
test_case.first_party_origin =
url::Origin::Create(GURL("http://a.example.com"));
test_case.is_third_party = true;
test_case.expected_action = CreateRequestActionForTesting(
RequestAction::Type::BLOCK, *initiator_domains_rule.id);
test_cases.push_back(std::move(test_case));
}
{
TestCase test_case = {"http://example.com/initiator_domains"};
test_case.first_party_origin =
url::Origin::Create(GURL("http://b.example.com"));
test_case.is_third_party = true;
test_cases.push_back(std::move(test_case));
}
{
TestCase test_case = {"http://example.com/request_domains"};
test_case.first_party_origin =
url::Origin::Create(GURL("http://foobar.com"));
test_case.is_third_party = true;
test_case.expected_action = CreateRequestActionForTesting(
RequestAction::Type::BLOCK, *request_domains_rule.id);
test_cases.push_back(std::move(test_case));
}
{
TestCase test_case = {"http://example.com/request_domains"};
test_case.expected_action = CreateRequestActionForTesting(
RequestAction::Type::BLOCK, *request_domains_rule.id);
test_cases.push_back(std::move(test_case));
}
{
TestCase test_case = {"http://b.example.com/request_domains"};
test_cases.push_back(std::move(test_case));
}
{
TestCase test_case = {"http://foobar.com/request_domains"};
test_cases.push_back(std::move(test_case));
}
{
TestCase test_case = {"http://abc.com"};
test_cases.push_back(std::move(test_case));
}
{
TestCase test_case = {"http://abc.com"};
test_case.element_type = url_pattern_index::flat::ElementType_SUBDOCUMENT;
test_case.expected_action = CreateRequestActionForTesting(
RequestAction::Type::COLLAPSE, *sub_frame_rule.id);
test_cases.push_back(std::move(test_case));
}
{
TestCase test_case = {"http://243.com"};
test_case.is_third_party = true;
test_case.expected_action = CreateRequestActionForTesting(
RequestAction::Type::BLOCK, *third_party_rule.id);
test_cases.push_back(std::move(test_case));
}
{
TestCase test_case = {"http://243.com"};
test_case.first_party_origin = url::Origin::Create(GURL(test_case.url));
test_case.is_third_party = false;
test_cases.push_back(std::move(test_case));
}
for (size_t i = 0; i < std::size(test_cases); ++i) {
SCOPED_TRACE(base::StringPrintf("Case number-%" PRIuS " url-%s", i,
test_cases[i].url));
GURL url(test_cases[i].url);
RequestParams params;
params.url = &url;
params.first_party_origin = test_cases[i].first_party_origin;
params.element_type = test_cases[i].element_type;
params.is_third_party = test_cases[i].is_third_party;
EXPECT_EQ(
test_cases[i].expected_action,
matcher->GetAction(params, RulesetMatchingStage::kOnBeforeRequest));
}
}
// Ensures that RulesetMatcher combines the results of regex and filter-list
// style redirect rules correctly.
TEST_F(RulesetMatcherTest, RegexAndFilterListRules_RedirectPriority) {
struct RuleInfo {
size_t id;
size_t priority;
const char* action_type;
const char* filter;
bool is_regex_rule;
std::optional<std::string> redirect_url;
};
auto rule_info = std::to_array<RuleInfo>({
{1, 1, "redirect", "filter.com", false, "http://redirect_filter.com"},
{2, 1, "upgradeScheme", "regex\\.com", true, std::nullopt},
{3, 9, "redirect", "common1.com", false, "http://common1_filter.com"},
{4, 10, "redirect", "common1\\.com", true, "http://common1_regex.com"},
{5, 10, "upgradeScheme", "common2.com", false, std::nullopt},
{6, 9, "upgradeScheme", "common2\\.com", true, std::nullopt},
{7, 10, "redirect", "abc\\.com", true, "http://example1.com"},
{8, 9, "redirect", "abc", true, "http://example2.com"},
});
std::vector<TestRule> rules;
for (const auto& info : rule_info) {
TestRule rule = CreateGenericRule();
rule.id = info.id;
rule.priority = info.priority;
rule.action->type = info.action_type;
rule.condition->url_filter.reset();
if (info.is_regex_rule) {
rule.condition->regex_filter = info.filter;
} else {
rule.condition->url_filter = info.filter;
}
if (info.redirect_url) {
rule.action->redirect.emplace();
rule.action->redirect->url = info.redirect_url;
}
rules.push_back(rule);
}
std::unique_ptr<RulesetMatcher> matcher;
ASSERT_TRUE(CreateVerifiedMatcher(rules, CreateTemporarySource(), &matcher));
struct TestCase {
const char* url = nullptr;
std::optional<RequestAction> expected_action;
};
std::vector<TestCase> test_cases;
{
TestCase test_case = {"http://filter.com"};
test_case.expected_action = CreateRequestActionForTesting(
RequestAction::Type::REDIRECT, rule_info[0].id, rule_info[0].priority);
test_case.expected_action->redirect_url.emplace(
"http://redirect_filter.com");
test_cases.push_back(std::move(test_case));
}
{
TestCase test_case = {"http://regex.com"};
test_case.expected_action = CreateRequestActionForTesting(
RequestAction::Type::UPGRADE, rule_info[1].id, rule_info[1].priority);
test_case.expected_action->redirect_url.emplace("https://regex.com");
test_cases.push_back(std::move(test_case));
}
{
TestCase test_case = {"http://common1.com"};
test_case.expected_action = CreateRequestActionForTesting(
RequestAction::Type::REDIRECT, rule_info[3].id, rule_info[3].priority);
test_case.expected_action->redirect_url.emplace("http://common1_regex.com");
test_cases.push_back(std::move(test_case));
}
{
TestCase test_case = {"http://common2.com"};
test_case.expected_action = CreateRequestActionForTesting(
RequestAction::Type::UPGRADE, rule_info[4].id, rule_info[4].priority);
test_case.expected_action->redirect_url.emplace("https://common2.com");
test_cases.push_back(std::move(test_case));
}
{
TestCase test_case = {"https://common2.com"};
// No action since request is not upgradeable.
test_cases.push_back(std::move(test_case));
}
{
TestCase test_case = {"http://example.com"};
test_cases.push_back(std::move(test_case));
}
{
TestCase test_case = {"http://abc.com"};
test_case.expected_action = CreateRequestActionForTesting(
RequestAction::Type::REDIRECT, rule_info[6].id, rule_info[6].priority);
test_case.expected_action->redirect_url.emplace("http://example1.com");
test_cases.push_back(std::move(test_case));
}
{
TestCase test_case = {"http://xyz.com/abc"};
test_case.expected_action = CreateRequestActionForTesting(
RequestAction::Type::REDIRECT, rule_info[7].id, rule_info[7].priority);
test_case.expected_action->redirect_url.emplace("http://example2.com");
test_cases.push_back(std::move(test_case));
}
for (const auto& test_case : test_cases) {
SCOPED_TRACE(test_case.url);
GURL url(test_case.url);
RequestParams params;
params.url = &url;
EXPECT_EQ(
test_case.expected_action,
matcher->GetAction(params, RulesetMatchingStage::kOnBeforeRequest));
}
}
TEST_F(RulesetMatcherTest, RegexAndFilterListRules_ModifyHeaders) {
std::vector<TestRule> rules;
TestRule rule = CreateGenericRule();
rule.id = 1;
rule.priority = kMinValidPriority + 1;
rule.action->type = "modifyHeaders";
rule.condition->url_filter = "abc";
rule.action->request_headers = std::vector<TestHeaderInfo>(
{TestHeaderInfo("header1", "remove", std::nullopt),
TestHeaderInfo("header2", "remove", std::nullopt)});
rules.push_back(rule);
RequestAction action_1 = CreateRequestActionForTesting(
RequestAction::Type::MODIFY_HEADERS, 1, *rule.priority);
action_1.request_headers_to_modify = {
RequestAction::HeaderInfo("header1", dnr_api::HeaderOperation::kRemove,
std::nullopt),
RequestAction::HeaderInfo("header2", dnr_api::HeaderOperation::kRemove,
std::nullopt)};
rule = CreateGenericRule();
rule.id = 2;
rule.priority = kMinValidPriority;
rule.condition->url_filter.reset();
rule.condition->regex_filter = "example";
rule.action->type = "modifyHeaders";
rule.action->request_headers = std::vector<TestHeaderInfo>(
{TestHeaderInfo("header1", "remove", std::nullopt),
TestHeaderInfo("header3", "remove", std::nullopt)});
rules.push_back(rule);
RequestAction action_2 = CreateRequestActionForTesting(
RequestAction::Type::MODIFY_HEADERS, 2, *rule.priority);
action_2.request_headers_to_modify = {
RequestAction::HeaderInfo("header1", dnr_api::HeaderOperation::kRemove,
std::nullopt),
RequestAction::HeaderInfo("header3", dnr_api::HeaderOperation::kRemove,
std::nullopt)};
std::unique_ptr<RulesetMatcher> matcher;
ASSERT_TRUE(CreateVerifiedMatcher(rules, CreateTemporarySource(), &matcher));
{
GURL url("http://nomatch.com");
SCOPED_TRACE(url.spec());
RequestParams params;
params.url = &url;
EXPECT_TRUE(matcher
->GetModifyHeadersActions(
params, RulesetMatchingStage::kOnBeforeRequest,
/*min_priority=*/0u)
.empty());
}
{
GURL url("http://abc.com");
SCOPED_TRACE(url.spec());
RequestParams params;
params.url = &url;
std::vector<RequestAction> actions = matcher->GetModifyHeadersActions(
params, RulesetMatchingStage::kOnBeforeRequest, /*min_priority=*/0u);
EXPECT_THAT(actions, testing::UnorderedElementsAre(
testing::Eq(testing::ByRef(action_1))));
}
{
GURL url("http://example.com");
SCOPED_TRACE(url.spec());
RequestParams params;
params.url = &url;
std::vector<RequestAction> actions = matcher->GetModifyHeadersActions(
params, RulesetMatchingStage::kOnBeforeRequest, /*min_priority=*/0u);
EXPECT_THAT(actions, testing::UnorderedElementsAre(
testing::Eq(testing::ByRef(action_2))));
}
{
// Send a request that matches both the filter list and regex rules.
GURL url("http://abc.com/example");
SCOPED_TRACE(url.spec());
RequestParams params;
params.url = &url;
std::vector<RequestAction> actions = matcher->GetModifyHeadersActions(
params, RulesetMatchingStage::kOnBeforeRequest, /*min_priority=*/0u);
EXPECT_THAT(actions, testing::UnorderedElementsAre(
testing::Eq(testing::ByRef(action_1)),
testing::Eq(testing::ByRef(action_2))));
// GetModifyHeadersActions specifies a minimum priority greater than the
// rules' priority, so no actions should be returned.
EXPECT_TRUE(matcher
->GetModifyHeadersActions(
params, RulesetMatchingStage::kOnBeforeRequest,
/*min_priority=*/std::numeric_limits<uint64_t>::max())
.empty());
}
}
// Tests that regex substitution works correctly.
TEST_F(RulesetMatcherTest, RegexSubstitution) {
struct {
int id;
std::string regex_filter;
std::string regex_substitution;
} rule_info[] = {
// "\0" captures the complete matched string.
{1, R"(^.*google\.com.*$)", R"(https://redirect.com?original=\0)"},
{2, R"(http://((?:abc|def)\.xyz.com.*)$)", R"(https://www.\1)"},
{3, R"(^(http|https)://example\.com.*(\?|&)redirect=(.*?)(?:&|$).*$)",
R"(\1://\3)"},
{4, R"(reddit\.com)", "abc.com"},
{5, R"(^http://www\.(pqr|rst)\.xyz\.com)", R"(https://\1.xyz.com)"},
{6, R"(\.in)", ".co.in"},
};
std::vector<TestRule> rules;
for (const auto& info : rule_info) {
TestRule rule = CreateGenericRule();
rule.id = info.id;
rule.priority = kMinValidPriority;
rule.condition->url_filter.reset();
rule.condition->regex_filter = info.regex_filter;
rule.action->type = std::string("redirect");
rule.action->redirect.emplace();
rule.action->redirect->regex_substitution = info.regex_substitution;
rules.push_back(rule);
}
std::unique_ptr<RulesetMatcher> matcher;
ASSERT_TRUE(CreateVerifiedMatcher(rules, CreateTemporarySource(), &matcher));
auto create_redirect_action = [](int rule_id, std::string redirect_url) {
RequestAction action =
CreateRequestActionForTesting(RequestAction::Type::REDIRECT, rule_id);
action.redirect_url.emplace(redirect_url);
return action;
};
struct {
std::string url;
std::optional<RequestAction> expected_action;
} test_cases[] = {
{"http://google.com/path?query",
create_redirect_action(
1, "https://redirect.com?original=http://google.com/path?query")},
{"http://def.xyz.com/path?query",
create_redirect_action(2, "https://www.def.xyz.com/path?query")},
{"http://example.com/path?q1=1&redirect=facebook.com&q2=2",
create_redirect_action(3, "http://facebook.com")},
// The redirect url here would have been "http://" which is invalid.
{"http://example.com/path?q1=1&redirect=&q2=2", std::nullopt},
{"https://reddit.com", create_redirect_action(4, "https://abc.com")},
{"http://www.rst.xyz.com/path",
create_redirect_action(5, "https://rst.xyz.com/path")},
{"http://yahoo.in/path",
create_redirect_action(6, "http://yahoo.co.in/path")},
// No match.
{"http://example.com", std::nullopt}};
for (const auto& test_case : test_cases) {
SCOPED_TRACE(test_case.url);
GURL url(test_case.url);
CHECK(url.is_valid());
RequestParams params;
params.url = &url;
ASSERT_EQ(
test_case.expected_action,
matcher->GetAction(params, RulesetMatchingStage::kOnBeforeRequest));
}
}
TEST_F(RulesetMatcherTest, RulesCount) {
size_t kNumNonRegexRules = 20;
size_t kNumRegexRules = 10;
// Rules that are not block or allow rules are considered unsafe for dynamic
// rulesets. Make some of the rules these types as well.
size_t kNumUnsafeNonRegexRules = 5;
size_t kNumUnsafeRegexRules = 3;
std::vector<TestRule> rules;
int id = kMinValidID;
for (size_t i = 0; i < kNumNonRegexRules; ++i, ++id) {
TestRule rule = CreateGenericRule();
rule.id = id;
rule.condition->url_filter = base::NumberToString(id);
if (i < kNumUnsafeNonRegexRules) {
rule.action->type = "redirect";
rule.action->redirect.emplace();
rule.action->redirect->url = "http://google.com";
}
rules.push_back(rule);
}
for (size_t i = 0; i < kNumRegexRules; ++i, ++id) {
TestRule rule = CreateGenericRule();
rule.id = id;
rule.condition->url_filter.reset();
rule.condition->regex_filter = base::NumberToString(id);
if (i < kNumUnsafeRegexRules) {
rule.action->type = std::string("modifyHeaders");
rule.action->response_headers = std::vector<TestHeaderInfo>(
{TestHeaderInfo("HEADER3", "append", "VALUE3")});
}
rules.push_back(rule);
}
std::unique_ptr<RulesetMatcher> matcher;
ASSERT_TRUE(CreateVerifiedMatcher(rules, CreateTemporarySource(), &matcher));
ASSERT_TRUE(matcher);
EXPECT_EQ(kNumRegexRules + kNumNonRegexRules, matcher->GetRulesCount());
// For static rulesets, no rules are considered unsafe.
EXPECT_EQ(std::nullopt, matcher->GetUnsafeRulesCount());
EXPECT_EQ(kNumRegexRules, matcher->GetRegexRulesCount());
// Recreate `matcher` for a dynamic ruleset to test that unsafe rule count is
// calculated correctly.
ASSERT_TRUE(CreateVerifiedMatcher(
rules, CreateTemporarySource(kDynamicRulesetID), &matcher));
ASSERT_TRUE(matcher);
EXPECT_EQ(kNumRegexRules + kNumNonRegexRules, matcher->GetRulesCount());
EXPECT_EQ(kNumUnsafeNonRegexRules + kNumUnsafeRegexRules,
matcher->GetUnsafeRulesCount());
EXPECT_EQ(kNumRegexRules, matcher->GetRegexRulesCount());
// Also verify the rules count for an empty matcher.
ASSERT_TRUE(
CreateVerifiedMatcher({} /* rules */, CreateTemporarySource(), &matcher));
ASSERT_TRUE(matcher);
EXPECT_EQ(0u, matcher->GetRulesCount());
EXPECT_EQ(std::nullopt, matcher->GetUnsafeRulesCount());
EXPECT_EQ(0u, matcher->GetRegexRulesCount());
}
// Test that rules with the same priority will override each other correctly
// based on action.
TEST_F(RulesetMatcherTest, BreakTiesByActionPriority) {
struct {
int rule_id;
std::string rule_action;
// The expected action, assuming this rule and all previous rules match and
// have the same priority.
RequestAction::Type expected_action;
// The ID of the rule expected to match.
int expected_rule_id = 0;
} test_cases[] = {
{1, "redirect", RequestAction::Type::REDIRECT, 1},
{2, "upgradeScheme", RequestAction::Type::UPGRADE, 2},
{3, "block", RequestAction::Type::BLOCK, 3},
{4, "allowAllRequests", RequestAction::Type::ALLOW_ALL_REQUESTS, 4},
{5, "allow", RequestAction::Type::ALLOW, 5},
{6, "block", RequestAction::Type::ALLOW, 5},
};
std::vector<TestRule> rules;
for (const auto& test_case : test_cases) {
SCOPED_TRACE(test_case.rule_action);
TestRule rule = CreateGenericRule();
rule.id = test_case.rule_id;
rule.priority = kMinValidPriority;
rule.condition->url_filter = "http://example.com";
rule.action->type = test_case.rule_action;
rule.condition->resource_types = std::vector<std::string>{"main_frame"};
if (test_case.rule_action == "redirect") {
rule.action->redirect.emplace();
rule.action->redirect->url = "http://google.com";
}
rules.push_back(rule);
std::unique_ptr<RulesetMatcher> matcher;
ASSERT_TRUE(
CreateVerifiedMatcher(rules, CreateTemporarySource(), &matcher));
GURL url("http://example.com");
RequestParams params;
params.url = &url;
params.element_type = url_pattern_index::flat::ElementType_MAIN_FRAME;
int expected_rule_id = test_case.expected_rule_id;
if (expected_rule_id == 0) {
expected_rule_id = *rule.id;
}
RequestAction expected_action = CreateRequestActionForTesting(
test_case.expected_action, expected_rule_id);
if (test_case.expected_action == RequestAction::Type::REDIRECT) {
expected_action.redirect_url = GURL("http://google.com");
} else if (test_case.expected_action == RequestAction::Type::UPGRADE) {
expected_action.redirect_url = GURL("https://example.com");
}
EXPECT_EQ(
expected_action,
matcher->GetAction(params, RulesetMatchingStage::kOnBeforeRequest));
}
}
// Test fixture to test allowAllRequests rules. We inherit from ExtensionsTest
// to ensure we can work with WebContentsTester and associated classes.
using AllowAllRequestsTest = ExtensionsTest;
// Tests that we track allowlisted frames (frames matching allowAllRequests
// rules) correctly.
TEST_F(AllowAllRequestsTest, AllowlistedFrameTracking) {
TestRule google_rule_1 = CreateGenericRule();
google_rule_1.id = kMinValidID;
google_rule_1.condition->url_filter = "google.com/xyz";
google_rule_1.condition->resource_types =
std::vector<std::string>({"main_frame"});
google_rule_1.action->type = std::string("allowAllRequests");
google_rule_1.priority = 2;
TestRule google_rule_2 = CreateGenericRule();
google_rule_2.id = kMinValidID + 1;
google_rule_2.condition->url_filter.reset();
google_rule_2.condition->regex_filter = "xyz";
google_rule_2.condition->resource_types =
std::vector<std::string>({"main_frame"});
google_rule_2.action->type = std::string("allowAllRequests");
google_rule_2.priority = 3;
TestRule example_rule = CreateGenericRule();
example_rule.id = kMinValidID + 2;
example_rule.condition->url_filter.reset();
example_rule.condition->regex_filter = std::string("example");
example_rule.condition->resource_types =
std::vector<std::string>({"sub_frame"});
example_rule.action->type = std::string("allowAllRequests");
example_rule.priority = 4;
std::unique_ptr<RulesetMatcher> matcher;
ASSERT_TRUE(
CreateVerifiedMatcher({google_rule_1, google_rule_2, example_rule},
CreateTemporarySource(), &matcher));
auto simulate_navigation = [&matcher](content::RenderFrameHost* host,
const GURL& url) {
content::RenderFrameHost* new_host =
content::NavigationSimulator::NavigateAndCommitFromDocument(url, host);
EXPECT_TRUE(new_host);
// Note |host| might have been freed by now.
testing::NiceMock<content::MockNavigationHandle> navigation_handle(
url, new_host);
navigation_handle.set_has_committed(true);
matcher->OnDidFinishNavigation(&navigation_handle);
return new_host;
};
auto simulate_frame_destroyed = [&matcher](content::RenderFrameHost* host) {
matcher->OnRenderFrameDeleted(host);
};
std::unique_ptr<content::WebContents> web_contents =
content::WebContentsTester::CreateTestWebContents(
browser_context(), content::SiteInstance::Create(browser_context()));
ASSERT_TRUE(web_contents);
GURL example_url("http://example.com");
simulate_navigation(web_contents->GetPrimaryMainFrame(), example_url);
std::optional<RequestAction> action =
matcher->GetAllowlistedFrameActionForTesting(
web_contents->GetPrimaryMainFrame());
EXPECT_FALSE(action);
GURL google_url_1("http://google.com/xyz");
simulate_navigation(web_contents->GetPrimaryMainFrame(), google_url_1);
action = matcher->GetAllowlistedFrameActionForTesting(
web_contents->GetPrimaryMainFrame());
RequestAction google_rule_2_action =
CreateRequestActionForTesting(RequestAction::Type::ALLOW_ALL_REQUESTS,
*google_rule_2.id, *google_rule_2.priority);
EXPECT_EQ(google_rule_2_action, action);
auto* render_frame_host_tester =
content::RenderFrameHostTester::For(web_contents->GetPrimaryMainFrame());
content::RenderFrameHost* child =
render_frame_host_tester->AppendChild("sub_frame");
ASSERT_TRUE(child);
child = simulate_navigation(child, example_url);
action = matcher->GetAllowlistedFrameActionForTesting(child);
RequestAction example_rule_action =
CreateRequestActionForTesting(RequestAction::Type::ALLOW_ALL_REQUESTS,
*example_rule.id, *example_rule.priority);
EXPECT_EQ(example_rule_action, action);
GURL yahoo_url("http://yahoo.com");
child = simulate_navigation(child, yahoo_url);
action = matcher->GetAllowlistedFrameActionForTesting(child);
EXPECT_EQ(google_rule_2_action, action);
simulate_frame_destroyed(child);
action = matcher->GetAllowlistedFrameActionForTesting(child);
EXPECT_FALSE(action);
simulate_frame_destroyed(web_contents->GetPrimaryMainFrame());
action = matcher->GetAllowlistedFrameActionForTesting(
web_contents->GetPrimaryMainFrame());
EXPECT_FALSE(action);
}
// Ensures that GetBeforeRequestAction correctly incorporates allowAllRequests
// rules.
TEST_F(AllowAllRequestsTest, GetBeforeRequestAction) {
struct RuleData {
int id;
int priority;
std::string action_type;
std::string url_filter;
bool is_regex_rule;
};
auto rule_data = std::to_array<RuleData>({
{1, 1, "allowAllRequests", "google", true},
{2, 3, "block", "||match", false},
{3, 2, "allowAllRequests", "match1", true},
{4, 4, "allowAllRequests", "match2", false},
});
std::vector<TestRule> test_rules;
for (const auto& rule : rule_data) {
TestRule test_rule = CreateGenericRule();
test_rule.id = rule.id;
test_rule.priority = rule.priority;
test_rule.action->type = rule.action_type;
test_rule.condition->url_filter.reset();
if (rule.is_regex_rule) {
test_rule.condition->regex_filter = rule.url_filter;
} else {
test_rule.condition->url_filter = rule.url_filter;
}
if (rule.action_type == "allowAllRequests") {
test_rule.condition->resource_types =
std::vector<std::string>({"main_frame", "sub_frame"});
}
test_rules.push_back(test_rule);
}
std::unique_ptr<RulesetMatcher> matcher;
ASSERT_TRUE(
CreateVerifiedMatcher(test_rules, CreateTemporarySource(), &matcher));
std::unique_ptr<content::WebContents> web_contents =
content::WebContentsTester::CreateTestWebContents(
browser_context(), content::SiteInstance::Create(browser_context()));
ASSERT_TRUE(web_contents);
GURL google_url("http://google.com");
content::WebContentsTester::For(web_contents.get())
->NavigateAndCommit(google_url);
testing::NiceMock<content::MockNavigationHandle> navigation_handle(
google_url, web_contents->GetPrimaryMainFrame());
navigation_handle.set_has_committed(true);
matcher->OnDidFinishNavigation(&navigation_handle);
struct {
std::string url;
RequestAction expected_action;
} cases[] = {
{"http://nomatch.com",
CreateRequestActionForTesting(RequestAction::Type::ALLOW_ALL_REQUESTS,
rule_data[0].id, rule_data[0].priority)},
{"http://match1.com",
CreateRequestActionForTesting(RequestAction::Type::COLLAPSE,
rule_data[1].id, rule_data[1].priority)},
{"http://match2.com",
CreateRequestActionForTesting(RequestAction::Type::ALLOW_ALL_REQUESTS,
rule_data[3].id, rule_data[3].priority)},
};
// Simulate sub-frame requests.
for (const auto& test_case : cases) {
SCOPED_TRACE(test_case.url);
RequestParams params;
GURL url(test_case.url);
ASSERT_TRUE(url.is_valid());
params.url = &url;
params.first_party_origin = url::Origin::Create(google_url);
params.is_third_party = true;
params.element_type = url_pattern_index::flat::ElementType_SUBDOCUMENT;
params.parent_routing_id =
web_contents->GetPrimaryMainFrame()->GetGlobalId();
EXPECT_EQ(
test_case.expected_action,
matcher->GetAction(params, RulesetMatchingStage::kOnBeforeRequest));
}
}
// Tests disable rules with simple blocking rules.
TEST_F(RulesetMatcherTest, SetDisabledRuleIds) {
TestRule rule_1 = CreateGenericRule(kMinValidID);
rule_1.condition->url_filter = std::string("google.com");
GURL google_url("http://google.com");
TestRule rule_2 = CreateGenericRule(kMinValidID + 1);
rule_2.condition->url_filter = std::string("yahoo.com");
GURL yahoo_url("http://yahoo.com");
GURL example_url("http://example.com");
auto should_block_request = [](const RulesetMatcher& matcher,
const RequestParams& params) {
auto action =
matcher.GetAction(params, RulesetMatchingStage::kOnBeforeRequest);
return action.has_value() && action->IsBlockOrCollapse();
};
RequestParams params;
params.element_type = url_pattern_index::flat::ElementType_SUBDOCUMENT;
params.is_third_party = true;
std::unique_ptr<RulesetMatcher> matcher;
ASSERT_TRUE(CreateVerifiedMatcher({rule_1, rule_2}, CreateTemporarySource(),
&matcher));
ASSERT_TRUE(matcher);
params.url = &google_url;
EXPECT_TRUE(should_block_request(*matcher, params));
params.url = &yahoo_url;
EXPECT_TRUE(should_block_request(*matcher, params));
params.url = &example_url;
EXPECT_FALSE(should_block_request(*matcher, params));
EXPECT_THAT(matcher->GetDisabledRuleIdsForTesting(), testing::IsEmpty());
matcher->SetDisabledRuleIds({*rule_1.id});
EXPECT_THAT(matcher->GetDisabledRuleIdsForTesting(),
testing::ElementsAreArray({*rule_1.id}));
params.url = &google_url;
EXPECT_FALSE(should_block_request(*matcher, params));
params.url = &yahoo_url;
EXPECT_TRUE(should_block_request(*matcher, params));
params.url = &example_url;
EXPECT_FALSE(should_block_request(*matcher, params));
}
class RulesetMatcherResponseHeadersTest : public RulesetMatcherTest {
public:
RulesetMatcherResponseHeadersTest() {
scoped_feature_list_.InitAndEnableFeature(
extensions_features::kDeclarativeNetRequestResponseHeaderMatching);
}
private:
// TODO(crbug.com/40727004): Once feature is launched to stable and feature
// flag can be removed, replace usages of this test class with just
// DeclarativeNetRequestBrowserTest.
base::test::ScopedFeatureList scoped_feature_list_;
ScopedCurrentChannel current_channel_override_{version_info::Channel::DEV};
};
// Test that GetOnHeadersReceivedAction only matches rules with response header
// conditions.
TEST_F(RulesetMatcherResponseHeadersTest, OnHeadersReceivedAction) {
// Create 2 rules: a block rule that is matched in onBeforeRequest and a
// redirect rule that is matched in onHeadersReceived.
TestRule before_request_rule = CreateGenericRule(kMinValidID);
before_request_rule.condition->url_filter = std::string("google.com");
std::vector<TestHeaderCondition> header_condition(
{TestHeaderCondition("key1", {"value1"}, {})});
TestRule response_headers_rule = CreateGenericRule(kMinValidID + 1);
response_headers_rule.condition->url_filter = std::string("google.com");
response_headers_rule.condition->response_headers =
std::move(header_condition);
response_headers_rule.action->type = std::string("redirect");
response_headers_rule.action->redirect.emplace();
response_headers_rule.action->redirect->url = std::string("http://yahoo.com");
char base_headers_string[] =
"HTTP/1.0 200 OK\r\n"
"Key1: Value1\r\n";
auto base_headers = base::MakeRefCounted<net::HttpResponseHeaders>(
net::HttpUtil::AssembleRawHeaders(base_headers_string));
GURL google_url("http://google.com");
RequestParams params =
CreateRequestWithResponseHeaders(google_url, base_headers.get());
std::unique_ptr<RulesetMatcher> matcher;
ASSERT_TRUE(
CreateVerifiedMatcher({before_request_rule, response_headers_rule},
CreateTemporarySource(), &matcher));
ASSERT_TRUE(matcher);
EXPECT_EQ(2u, matcher->GetRulesCount());
EXPECT_EQ(1u, matcher->GetRulesCount(RulesetMatchingStage::kOnBeforeRequest));
EXPECT_EQ(1u,
matcher->GetRulesCount(RulesetMatchingStage::kOnHeadersReceived));
// The request should be blocked if matched with `before_request_rule`.
RequestAction expected_before_request_action =
CreateRequestActionForTesting(RequestAction::Type::COLLAPSE, kMinValidID);
EXPECT_EQ(expected_before_request_action,
matcher->GetAction(params, RulesetMatchingStage::kOnBeforeRequest));
// The request should be redirected if matched with `response_headers_rule`.
RequestAction expected_headers_received_action =
CreateRequestActionForTesting(RequestAction::Type::REDIRECT,
kMinValidID + 1);
expected_headers_received_action.redirect_url.emplace("http://yahoo.com");
EXPECT_EQ(
expected_headers_received_action,
matcher->GetAction(params, RulesetMatchingStage::kOnHeadersReceived));
// Sanity check that disabling rules works for response header rules as well.
EXPECT_THAT(matcher->GetDisabledRuleIdsForTesting(), testing::IsEmpty());
matcher->SetDisabledRuleIds({*response_headers_rule.id});
EXPECT_THAT(matcher->GetDisabledRuleIdsForTesting(),
testing::ElementsAreArray({*response_headers_rule.id}));
EXPECT_EQ(
std::nullopt,
matcher->GetAction(params, RulesetMatchingStage::kOnHeadersReceived));
}
// Test matching response header conditions with regex rules.
TEST_F(RulesetMatcherResponseHeadersTest, OnHeadersReceivedAction_Regex) {
auto create_regex_rule = [](size_t id, const std::string& regex_filter) {
TestRule rule = CreateGenericRule();
rule.id = id;
rule.condition->url_filter.reset();
rule.condition->regex_filter = regex_filter;
return rule;
};
// Create 3 rules:
// - A regex allow rule which is matched in the onBeforeRequest stage.
// - A url rule with a higher priority that redirects the request to
// urlmatch.com when matched in the onHeadersReceived stage.
// - A regex rule that redirects the request to regexmatch.com when matched in
// the onHeadersReceived stage.
TestRule before_request_rule =
create_regex_rule(kMinValidID, R"(^(?:http|https)://[a-z\.]+\.com)");
before_request_rule.action->type = std::string("allow");
TestRule url_response_headers_rule = CreateGenericRule(kMinValidID + 1);
url_response_headers_rule.priority = kMinValidPriority + 2;
url_response_headers_rule.condition->url_filter = std::string("google.com");
url_response_headers_rule.condition->response_headers = {
TestHeaderCondition("key1", {}, {})};
url_response_headers_rule.action->type = std::string("redirect");
url_response_headers_rule.action->redirect.emplace();
url_response_headers_rule.action->redirect->url =
std::string("http://urlmatch.com");
TestRule regex_response_headers_rule =
create_regex_rule(kMinValidID + 2, R"(^(?:http|https)://[a-z\.]+\.com)");
regex_response_headers_rule.priority = kMinValidPriority + 1;
regex_response_headers_rule.condition->response_headers = {
TestHeaderCondition("key1", {}, {})};
regex_response_headers_rule.action->type = std::string("redirect");
regex_response_headers_rule.action->redirect.emplace();
regex_response_headers_rule.action->redirect->url =
std::string("http://regexmatch.com");
char base_headers_string[] =
"HTTP/1.0 200 OK\r\n"
"Key1: Value1\r\n";
auto base_headers = base::MakeRefCounted<net::HttpResponseHeaders>(
net::HttpUtil::AssembleRawHeaders(base_headers_string));
std::unique_ptr<RulesetMatcher> matcher;
ASSERT_TRUE(
CreateVerifiedMatcher({before_request_rule, url_response_headers_rule,
regex_response_headers_rule},
CreateTemporarySource(), &matcher));
ASSERT_TRUE(matcher);
struct Cases {
std::string url;
std::optional<RequestAction> expected_before_request_action;
std::optional<RequestAction> expected_headers_received_action;
std::optional<std::string> expected_redirect_url;
};
auto cases = std::to_array<Cases>({
// The request to google.com will match `before_request_rule` for
// GetBeforeRequestAction and `url_response_headers_rule` because of its
// higher priority for GetOnHeadersReceivedAction.
{"http://google.com",
CreateRequestActionForTesting(RequestAction::Type::ALLOW, kMinValidID),
CreateRequestActionForTesting(RequestAction::Type::REDIRECT,
kMinValidID + 1, kMinValidPriority + 2),
std::string("http://urlmatch.com")},
// The request to someotherurl.com will match `before_request_rule` for
// GetBeforeRequestAction and `regex_response_headers_rule` for
// GetOnHeadersReceivedAction.
{"http://someotherurl.com",
CreateRequestActionForTesting(RequestAction::Type::ALLOW, kMinValidID),
CreateRequestActionForTesting(RequestAction::Type::REDIRECT,
kMinValidID + 2, kMinValidPriority + 1),
std::string("http://regexmatch.com")},
});
for (size_t i = 0; i < std::size(cases); ++i) {
SCOPED_TRACE(base::StringPrintf("Testing case[%" PRIuS "]", i));
auto& test_case = cases[i];
GURL url(test_case.url);
ASSERT_TRUE(url.is_valid());
RequestParams params =
CreateRequestWithResponseHeaders(url, base_headers.get());
EXPECT_EQ(
test_case.expected_before_request_action,
matcher->GetAction(params, RulesetMatchingStage::kOnBeforeRequest));
// We assign the value of redirect_url from the test case so that we can use
// the operator== on the RequestAction below.
if (test_case.expected_headers_received_action) {
test_case.expected_headers_received_action->redirect_url =
GURL(*test_case.expected_redirect_url);
}
EXPECT_EQ(
test_case.expected_headers_received_action,
matcher->GetAction(params, RulesetMatchingStage::kOnHeadersReceived));
}
}
// Test matching rules based on response header conditions.
TEST_F(RulesetMatcherResponseHeadersTest, MatchOnResponseHeaders) {
std::vector<TestHeaderCondition> header_condition(
{TestHeaderCondition("key1", {}, {}),
TestHeaderCondition("key2", {"Value1", "value2"}, {"excludedValue"})});
// `rule_1` will match if:
// - the key1 header is present, or:
// - the key2 header is present and has either value1 or value2, but not
// excludedValue
// `rule_1` will fail to match if:
// - the excludedKey header is present
TestRule rule_1 = CreateGenericRule(kMinValidID);
rule_1.action->type = std::string("block");
rule_1.condition->url_filter = std::string("google.com");
rule_1.condition->response_headers = std::move(header_condition);
rule_1.condition->excluded_response_headers =
std::vector<TestHeaderCondition>(
{TestHeaderCondition("excludedKey", {}, {})});
// `rule_2` will fail to match if:
// - the key3 header is present and has excludedValue, or:
// - the key4 header is present and does NOT have allowlistedValue
TestRule rule_2 = CreateGenericRule(kMinValidID + 1);
rule_2.action->type = std::string("block");
rule_2.condition->url_filter = std::string("example.com");
rule_2.condition->excluded_response_headers =
std::vector<TestHeaderCondition>(
{TestHeaderCondition("key3", {"excludedValue"}, {}),
TestHeaderCondition("key4", {}, {"allowlistedValue"})});
// `rule_3` will match if
// - the content-type header specifies a PDF
TestRule rule_3 = CreateGenericRule(kMinValidID + 2);
rule_3.action->type = std::string("block");
rule_3.condition->url_filter = std::string("nopdf.com");
rule_3.condition->response_headers = std::vector<TestHeaderCondition>(
{TestHeaderCondition("content-type", {"*application/pdf*"}, {})});
std::unique_ptr<RulesetMatcher> matcher;
ASSERT_TRUE(CreateVerifiedMatcher({rule_1, rule_2, rule_3},
CreateTemporarySource(), &matcher));
ASSERT_TRUE(matcher);
struct Cases {
std::string url;
std::string response_headers;
std::optional<RequestAction> expected_action;
};
auto cases = std::to_array<Cases>({
// No match for a non-matching URL.
{"http://nomatch.com", "HTTP/1.0 200 OK\r\nKey1: Value1\r\n",
std::nullopt},
// No match if no header conditions match.
{"http://google.com", "HTTP/1.0 200 OK\r\nNonmatching: Value\r\n",
std::nullopt},
// Test matching the key1 header by name only.
{"http://google.com", "HTTP/1.0 200 OK\r\nkey1: any\r\n",
CreateRequestActionForTesting(RequestAction::Type::COLLAPSE,
kMinValidID)},
// Test matching the key2 header by its value (case-insensitive).
{"http://google.com", "HTTP/1.0 200 OK\r\nkey2: VALUE1\r\n",
CreateRequestActionForTesting(RequestAction::Type::COLLAPSE,
kMinValidID)},
// No match since key2's value does not match what's specified in
// `rule_1`.
{"http://google.com", "HTTP/1.0 200 OK\r\nkey2: wrongvalue\r\n",
std::nullopt},
// No match since key2's value is excluded by `rule_1`. Note that the
// excluded value takes precedence over the included `value1`.
{"http://google.com",
"HTTP/1.0 200 OK\r\nkey2: value1\r\nkey2: excludedValue\r\n",
std::nullopt},
// Test that only one included header condition needs to match (key1) for
// the rule to match, even though another header condition does not match
// (key2).
{"http://google.com",
"HTTP/1.0 200 OK\r\nkey1: any\r\nkey2: excludedValue\r\n",
CreateRequestActionForTesting(RequestAction::Type::COLLAPSE,
kMinValidID)},
// No match since a header that is excluded by `rule_1` exists.
{"http://google.com",
"HTTP/1.0 200 OK\r\nkey1: any\r\nexcludedKey: value\r\n", std::nullopt},
// For the next 3 test cases, the request matches if it does NOT contain
// a "key3: value3" header-value pair.
{"http://example.com", "HTTP/1.0 200 OK\r\nkey3: othervalue\r\n",
CreateRequestActionForTesting(RequestAction::Type::COLLAPSE,
kMinValidID + 1)},
{"http://example.com", "HTTP/1.0 200 OK\r\nkey3: excludedValue\r\n",
std::nullopt},
{"http://example.com", "HTTP/1.0 200 OK\r\notherkey: key3doesntexist\r\n",
CreateRequestActionForTesting(RequestAction::Type::COLLAPSE,
kMinValidID + 1)},
// For the next 2 test cases, the request with key4 matches iff key4
// contains the allowlisted value.
{"http://example.com", "HTTP/1.0 200 OK\r\nkey4: randomValue\r\n",
std::nullopt},
{"http://example.com", "HTTP/1.0 200 OK\r\nkey4: allowlistedValue\r\n",
CreateRequestActionForTesting(RequestAction::Type::COLLAPSE,
kMinValidID + 1)},
// Test wildcard support for header value matching.
{"http://nopdf.com",
"HTTP/1.0 200 OK\r\ncontent-type: application/pdf; charset=utf-8\r\n",
CreateRequestActionForTesting(RequestAction::Type::COLLAPSE,
kMinValidID + 2)},
});
for (size_t i = 0; i < std::size(cases); ++i) {
SCOPED_TRACE(base::StringPrintf("Testing case[%" PRIuS "]", i));
GURL url(cases[i].url);
ASSERT_TRUE(url.is_valid());
auto base_headers = base::MakeRefCounted<net::HttpResponseHeaders>(
net::HttpUtil::AssembleRawHeaders(cases[i].response_headers.c_str()));
RequestParams params =
CreateRequestWithResponseHeaders(url, base_headers.get());
EXPECT_EQ(
cases[i].expected_action,
matcher->GetAction(params, RulesetMatchingStage::kOnHeadersReceived));
}
}
} // namespace
} // namespace extensions::declarative_net_request