| // Copyright 2019 The Chromium Authors. All rights reserved. |
| // 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/composite_matcher.h" |
| |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/strings/stringprintf.h" |
| #include "components/version_info/version_info.h" |
| #include "extensions/browser/api/declarative_net_request/constants.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/ruleset_matcher.h" |
| #include "extensions/browser/api/declarative_net_request/ruleset_source.h" |
| #include "extensions/browser/api/declarative_net_request/test_utils.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/features/feature_channel.h" |
| #include "extensions/common/permissions/permissions_data.h" |
| #include "net/http/http_request_headers.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "url/gurl.h" |
| |
| namespace extensions { |
| namespace declarative_net_request { |
| |
| using PageAccess = PermissionsData::PageAccess; |
| using ActionInfo = CompositeMatcher::ActionInfo; |
| |
| namespace dnr_api = api::declarative_net_request; |
| |
| class CompositeMatcherTest : public ::testing::Test { |
| public: |
| CompositeMatcherTest() : channel_(::version_info::Channel::UNKNOWN) {} |
| |
| private: |
| // Run this on the trunk channel to ensure the API is available. |
| ScopedCurrentChannel channel_; |
| |
| DISALLOW_COPY_AND_ASSIGN(CompositeMatcherTest); |
| }; |
| |
| // Ensure that the rules in a CompositeMatcher are in the same priority space. |
| TEST_F(CompositeMatcherTest, SamePrioritySpace) { |
| // Create the first ruleset matcher. It allows requests to google.com. |
| TestRule allow_rule = CreateGenericRule(); |
| allow_rule.id = kMinValidID; |
| allow_rule.condition->url_filter = std::string("google.com"); |
| allow_rule.action->type = std::string("allow"); |
| allow_rule.priority = 1; |
| std::unique_ptr<RulesetMatcher> allow_matcher; |
| ASSERT_TRUE(CreateVerifiedMatcher( |
| {allow_rule}, CreateTemporarySource(/*id*/ 1), &allow_matcher)); |
| |
| // Now create the second matcher. It blocks requests to google.com, with |
| // higher priority than the allow rule. |
| TestRule block_rule = allow_rule; |
| block_rule.action->type = std::string("block"); |
| block_rule.priority = 2; |
| std::unique_ptr<RulesetMatcher> block_matcher; |
| ASSERT_TRUE(CreateVerifiedMatcher( |
| {block_rule}, CreateTemporarySource(/*id*/ 2), &block_matcher)); |
| |
| // Create a composite matcher with both rulesets. |
| std::vector<std::unique_ptr<RulesetMatcher>> matchers; |
| matchers.push_back(std::move(allow_matcher)); |
| matchers.push_back(std::move(block_matcher)); |
| auto composite_matcher = |
| std::make_unique<CompositeMatcher>(std::move(matchers)); |
| |
| GURL google_url("http://google.com"); |
| RequestParams params; |
| params.url = &google_url; |
| |
| // The block rule should be higher priority. |
| ActionInfo action_info = |
| composite_matcher->GetBeforeRequestAction(params, PageAccess::kAllowed); |
| ASSERT_TRUE(action_info.action); |
| EXPECT_EQ(action_info.action->type, RequestAction::Type::BLOCK); |
| |
| // Now swap the priority of the rules, which requires re-creating the ruleset |
| // matchers and composite matcher. |
| allow_rule.priority = 2; |
| block_rule.priority = 1; |
| ASSERT_TRUE(CreateVerifiedMatcher( |
| {allow_rule}, CreateTemporarySource(/*id*/ 1), &allow_matcher)); |
| ASSERT_TRUE(CreateVerifiedMatcher( |
| {block_rule}, CreateTemporarySource(/*id*/ 2), &block_matcher)); |
| matchers.clear(); |
| matchers.push_back(std::move(allow_matcher)); |
| matchers.push_back(std::move(block_matcher)); |
| composite_matcher = std::make_unique<CompositeMatcher>(std::move(matchers)); |
| |
| // The allow rule should now have higher priority. |
| action_info = |
| composite_matcher->GetBeforeRequestAction(params, PageAccess::kAllowed); |
| ASSERT_TRUE(action_info.action); |
| EXPECT_EQ(action_info.action->type, RequestAction::Type::ALLOW); |
| } |
| |
| // Tests that header masks are correctly attributed to rules for multiple |
| // matchers in a CompositeMatcher. |
| TEST_F(CompositeMatcherTest, HeadersMaskForRules) { |
| auto create_remove_headers_rule = |
| [](int id, const std::string& url_filter, |
| const std::vector<std::string>& remove_headers_list) { |
| TestRule rule = CreateGenericRule(); |
| rule.id = id; |
| rule.condition->url_filter = url_filter; |
| rule.action->type = std::string("removeHeaders"); |
| rule.action->remove_headers_list = remove_headers_list; |
| |
| return rule; |
| }; |
| |
| TestRule static_rule_1 = create_remove_headers_rule( |
| kMinValidID, "google.com", std::vector<std::string>({"cookie"})); |
| |
| TestRule dynamic_rule_1 = create_remove_headers_rule( |
| kMinValidID, "/path", std::vector<std::string>({"referer"})); |
| |
| TestRule dynamic_rule_2 = create_remove_headers_rule( |
| kMinValidID + 1, "/path", std::vector<std::string>({"setCookie"})); |
| |
| // Create the first ruleset matcher, which matches all requests with "g" in |
| // their URL. |
| const size_t kSource1ID = 1; |
| std::unique_ptr<RulesetMatcher> matcher_1; |
| ASSERT_TRUE(CreateVerifiedMatcher( |
| {static_rule_1}, |
| CreateTemporarySource(kSource1ID, dnr_api::SOURCE_TYPE_MANIFEST), |
| &matcher_1)); |
| |
| // Create a second ruleset matcher, which matches all requests from |
| // |google.com|. |
| const size_t kSource2ID = 2; |
| std::unique_ptr<RulesetMatcher> matcher_2; |
| ASSERT_TRUE(CreateVerifiedMatcher( |
| {dynamic_rule_1, dynamic_rule_2}, |
| CreateTemporarySource(kSource2ID, dnr_api::SOURCE_TYPE_DYNAMIC), |
| &matcher_2)); |
| |
| // Create a composite matcher with the two rulesets. |
| std::vector<std::unique_ptr<RulesetMatcher>> matchers; |
| matchers.push_back(std::move(matcher_1)); |
| matchers.push_back(std::move(matcher_2)); |
| auto composite_matcher = |
| std::make_unique<CompositeMatcher>(std::move(matchers)); |
| |
| GURL google_url = GURL("http://google.com/path"); |
| RequestParams google_params; |
| google_params.url = &google_url; |
| google_params.element_type = url_pattern_index::flat::ElementType_SUBDOCUMENT; |
| google_params.is_third_party = false; |
| |
| const uint8_t expected_mask = flat::RemoveHeaderType_referer | |
| flat::RemoveHeaderType_cookie | |
| flat::RemoveHeaderType_set_cookie; |
| |
| std::vector<RequestAction> actions; |
| EXPECT_EQ(expected_mask, composite_matcher->GetRemoveHeadersMask( |
| google_params, 0u, &actions)); |
| |
| // Construct expected request actions to be taken for a request to google.com. |
| // Static actions are attributed to |matcher_1| and dynamic actions are |
| // attributed to |matcher_2|. |
| RequestAction static_action_1 = CreateRequestActionForTesting( |
| RequestAction::Type::REMOVE_HEADERS, *static_rule_1.id, kDefaultPriority, |
| dnr_api::SOURCE_TYPE_MANIFEST); |
| static_action_1.request_headers_to_remove.push_back( |
| net::HttpRequestHeaders::kCookie); |
| |
| RequestAction dynamic_action_1 = CreateRequestActionForTesting( |
| RequestAction::Type::REMOVE_HEADERS, *dynamic_rule_1.id, kDefaultPriority, |
| dnr_api::SOURCE_TYPE_DYNAMIC); |
| dynamic_action_1.request_headers_to_remove.push_back( |
| net::HttpRequestHeaders::kReferer); |
| |
| RequestAction dynamic_action_2 = CreateRequestActionForTesting( |
| RequestAction::Type::REMOVE_HEADERS, *dynamic_rule_2.id, kDefaultPriority, |
| dnr_api::SOURCE_TYPE_DYNAMIC); |
| dynamic_action_2.response_headers_to_remove.push_back("set-cookie"); |
| |
| EXPECT_THAT(actions, ::testing::UnorderedElementsAre( |
| ::testing::Eq(::testing::ByRef(static_action_1)), |
| ::testing::Eq(::testing::ByRef(dynamic_action_1)), |
| ::testing::Eq(::testing::ByRef(dynamic_action_2)))); |
| } |
| |
| // Ensure CompositeMatcher detects requests to be notified based on the rule |
| // matched and whether the extenion has access to the request. |
| TEST_F(CompositeMatcherTest, NotifyWithholdFromPageAccess) { |
| TestRule redirect_rule = CreateGenericRule(); |
| redirect_rule.condition->url_filter = std::string("google.com"); |
| redirect_rule.priority = kMinValidPriority; |
| redirect_rule.action->type = std::string("redirect"); |
| redirect_rule.action->redirect.emplace(); |
| redirect_rule.action->redirect->url = std::string("http://ruleset1.com"); |
| redirect_rule.id = kMinValidID; |
| |
| TestRule upgrade_rule = CreateGenericRule(); |
| upgrade_rule.condition->url_filter = std::string("example.com"); |
| upgrade_rule.priority = kMinValidPriority + 1; |
| upgrade_rule.action->type = std::string("upgradeScheme"); |
| upgrade_rule.id = kMinValidID + 1; |
| |
| std::unique_ptr<RulesetMatcher> matcher_1; |
| ASSERT_TRUE(CreateVerifiedMatcher({redirect_rule, upgrade_rule}, |
| CreateTemporarySource(), &matcher_1)); |
| |
| // Create a composite matcher. |
| std::vector<std::unique_ptr<RulesetMatcher>> matchers; |
| matchers.push_back(std::move(matcher_1)); |
| auto composite_matcher = |
| std::make_unique<CompositeMatcher>(std::move(matchers)); |
| |
| GURL google_url = GURL("http://google.com"); |
| GURL example_url = GURL("http://example.com"); |
| GURL yahoo_url = GURL("http://yahoo.com"); |
| |
| GURL ruleset1_url = GURL("http://ruleset1.com"); |
| GURL https_example_url = GURL("https://example.com"); |
| |
| struct { |
| GURL& request_url; |
| PageAccess access; |
| base::Optional<GURL> expected_final_url; |
| bool should_notify_withheld; |
| } test_cases[] = { |
| // If access to the request is allowed, we should not notify that |
| // the request is withheld. |
| {google_url, PageAccess::kAllowed, ruleset1_url, false}, |
| {example_url, PageAccess::kAllowed, https_example_url, false}, |
| {yahoo_url, PageAccess::kAllowed, base::nullopt, false}, |
| |
| // Notify the request is withheld if it matches with a redirect rule. |
| {google_url, PageAccess::kWithheld, base::nullopt, true}, |
| // If the page access to the request is withheld but it matches with |
| // an upgrade rule, or no rule, then we should not notify. |
| {example_url, PageAccess::kWithheld, https_example_url, false}, |
| {yahoo_url, PageAccess::kWithheld, base::nullopt, false}, |
| |
| // If access to the request is denied instead of withheld, the extension |
| // should not be notified. |
| {google_url, PageAccess::kDenied, base::nullopt, false}, |
| // If the page access to the request is denied but it matches with |
| // an upgrade rule, or no rule, then we should not notify. |
| {example_url, PageAccess::kDenied, https_example_url, false}, |
| {yahoo_url, PageAccess::kDenied, base::nullopt, false}, |
| }; |
| |
| for (const auto& test_case : test_cases) { |
| SCOPED_TRACE(base::StringPrintf( |
| "request_url=%s, access=%d, expected_final_url=%s, " |
| "should_notify_withheld=%d", |
| test_case.request_url.spec().c_str(), test_case.access, |
| test_case.expected_final_url.value_or(GURL()).spec().c_str(), |
| test_case.should_notify_withheld)); |
| |
| RequestParams params; |
| params.url = &test_case.request_url; |
| params.element_type = url_pattern_index::flat::ElementType_SUBDOCUMENT; |
| params.is_third_party = false; |
| |
| ActionInfo redirect_action_info = |
| composite_matcher->GetBeforeRequestAction(params, test_case.access); |
| |
| EXPECT_EQ(test_case.should_notify_withheld, |
| redirect_action_info.notify_request_withheld); |
| } |
| } |
| |
| // Tests that the redirect url within an extension's ruleset is chosen based on |
| // the highest priority matching rule. |
| TEST_F(CompositeMatcherTest, GetRedirectUrlFromPriority) { |
| TestRule abc_redirect = CreateGenericRule(); |
| abc_redirect.condition->url_filter = std::string("*abc*"); |
| abc_redirect.priority = kMinValidPriority; |
| abc_redirect.action->type = std::string("redirect"); |
| abc_redirect.action->redirect.emplace(); |
| abc_redirect.action->redirect->url = std::string("http://google.com"); |
| abc_redirect.id = kMinValidID; |
| |
| TestRule def_upgrade = CreateGenericRule(); |
| def_upgrade.condition->url_filter = std::string("*def*"); |
| def_upgrade.priority = kMinValidPriority + 1; |
| def_upgrade.action->type = std::string("upgradeScheme"); |
| def_upgrade.id = kMinValidID + 1; |
| |
| TestRule ghi_redirect = CreateGenericRule(); |
| ghi_redirect.condition->url_filter = std::string("*ghi*"); |
| ghi_redirect.priority = kMinValidPriority + 2; |
| ghi_redirect.action->type = std::string("redirect"); |
| ghi_redirect.action->redirect.emplace(); |
| ghi_redirect.action->redirect->url = std::string("http://example.com"); |
| ghi_redirect.id = kMinValidID + 2; |
| |
| // In terms of priority: ghi > def > abc. |
| |
| std::unique_ptr<RulesetMatcher> matcher_1; |
| ASSERT_TRUE(CreateVerifiedMatcher({abc_redirect, def_upgrade, ghi_redirect}, |
| CreateTemporarySource(), &matcher_1)); |
| |
| // Create a composite matcher. |
| std::vector<std::unique_ptr<RulesetMatcher>> matchers; |
| matchers.push_back(std::move(matcher_1)); |
| auto composite_matcher = |
| std::make_unique<CompositeMatcher>(std::move(matchers)); |
| |
| struct { |
| GURL request_url; |
| base::Optional<GURL> expected_final_url; |
| } test_cases[] = { |
| // Test requests which match exactly one rule. |
| {GURL("http://abc.com"), GURL("http://google.com")}, |
| {GURL("http://def.com"), GURL("https://def.com")}, |
| {GURL("http://ghi.com"), GURL("http://example.com")}, |
| |
| // The upgrade rule has a higher priority than the redirect rule matched |
| // so the request should be upgraded. |
| {GURL("http://abcdef.com"), GURL("https://abcdef.com")}, |
| |
| // The upgrade rule has a lower priority than the redirect rule matched so |
| // the request should be redirected. |
| {GURL("http://defghi.com"), GURL("http://example.com")}, |
| |
| // The request will not be redirected as it matches the upgrade rule but |
| // is already https. |
| {GURL("https://abcdef.com"), base::nullopt}, |
| |
| {GURL("http://xyz.com"), base::nullopt}, |
| }; |
| |
| for (const auto& test_case : test_cases) { |
| SCOPED_TRACE(base::StringPrintf( |
| "Test redirect from %s to %s", test_case.request_url.spec().c_str(), |
| test_case.expected_final_url.value_or(GURL()).spec().c_str())); |
| |
| RequestParams params; |
| params.url = &test_case.request_url; |
| params.element_type = url_pattern_index::flat::ElementType_SUBDOCUMENT; |
| params.is_third_party = false; |
| |
| ActionInfo redirect_action_info = |
| composite_matcher->GetBeforeRequestAction(params, PageAccess::kAllowed); |
| |
| if (test_case.expected_final_url) { |
| ASSERT_TRUE(redirect_action_info.action); |
| EXPECT_TRUE(redirect_action_info.action->IsRedirectOrUpgrade()); |
| EXPECT_EQ(test_case.expected_final_url, |
| redirect_action_info.action->redirect_url); |
| } else { |
| EXPECT_FALSE(redirect_action_info.action.has_value()); |
| } |
| |
| EXPECT_FALSE(redirect_action_info.notify_request_withheld); |
| } |
| } |
| |
| } // namespace declarative_net_request |
| } // namespace extensions |