| // 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 |