blob: 6aee30c28330e19cf7489f537386a92287ea5638 [file] [log] [blame]
// Copyright 2019 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/composite_matcher.h"
#include <array>
#include <string>
#include <utility>
#include <vector>
#include "base/memory/raw_ref.h"
#include "base/strings/stringprintf.h"
#include "base/test/scoped_feature_list.h"
#include "components/version_info/channel.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/ruleset_matcher.h"
#include "extensions/browser/api/declarative_net_request/test_utils.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 "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"
static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));
namespace extensions::declarative_net_request {
namespace {
using PageAccess = PermissionsData::PageAccess;
using ActionInfo = CompositeMatcher::ActionInfo;
namespace dnr_api = api::declarative_net_request;
using CompositeMatcherTest = ::testing::Test;
TestRule CreateModifyHeadersRule(
int id,
int priority,
std::optional<std::string> url_filter,
std::optional<std::string> regex_filter,
std::optional<std::vector<TestHeaderInfo>> request_headers_list,
std::optional<std::vector<TestHeaderInfo>> response_headers_list) {
TestRule rule = CreateGenericRule();
rule.id = id;
rule.priority = priority;
if (url_filter) {
rule.condition->url_filter = url_filter;
} else if (regex_filter) {
rule.condition->url_filter.reset();
rule.condition->regex_filter = regex_filter;
}
rule.action->type = std::string("modifyHeaders");
if (request_headers_list) {
rule.action->request_headers = std::move(request_headers_list);
}
if (response_headers_list) {
rule.action->response_headers = std::move(response_headers_list);
}
return rule;
}
// 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;
RulesetID ruleset_id_one(1);
ASSERT_TRUE(CreateVerifiedMatcher(
{allow_rule}, CreateTemporarySource(ruleset_id_one), &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;
RulesetID ruleset_id_two(2);
ASSERT_TRUE(CreateVerifiedMatcher(
{block_rule}, CreateTemporarySource(ruleset_id_two), &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), /*extension_id=*/"",
HostPermissionsAlwaysRequired::kFalse);
GURL google_url("http://google.com");
RequestParams params;
params.url = &google_url;
// The block rule should be higher priority.
ActionInfo action_info = composite_matcher->GetAction(
params, RulesetMatchingStage::kOnBeforeRequest, 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(ruleset_id_one), &allow_matcher));
ASSERT_TRUE(CreateVerifiedMatcher(
{block_rule}, CreateTemporarySource(ruleset_id_two), &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), /*extension_id=*/"",
HostPermissionsAlwaysRequired::kFalse);
// The allow rule should now have higher priority.
action_info = composite_matcher->GetAction(
params, RulesetMatchingStage::kOnBeforeRequest, PageAccess::kAllowed);
ASSERT_TRUE(action_info.action);
EXPECT_EQ(action_info.action->type, RequestAction::Type::ALLOW);
}
// Tests the GetModifyHeadersActions method.
TEST_F(CompositeMatcherTest, GetModifyHeadersActions) {
TestRule rule_1 = CreateModifyHeadersRule(
kMinValidID, kMinValidPriority, "google.com", std::nullopt,
std::vector<TestHeaderInfo>(
{TestHeaderInfo("header1", "remove", std::nullopt),
TestHeaderInfo("header2", "set", "value2")}),
std::nullopt);
TestRule rule_2 = CreateModifyHeadersRule(
kMinValidID, kMinValidPriority + 1, "/path", std::nullopt, std::nullopt,
std::vector<TestHeaderInfo>(
{TestHeaderInfo("header1", "remove", std::nullopt),
TestHeaderInfo("header2", "append", "VALUE2"),
TestHeaderInfo("header3", "set", "VALUE3")}));
// Create the first ruleset matcher, which matches all requests from
// |google.com|.
const RulesetID kSource1ID(1);
std::unique_ptr<RulesetMatcher> matcher_1;
ASSERT_TRUE(CreateVerifiedMatcher({rule_1}, CreateTemporarySource(kSource1ID),
&matcher_1));
// Create a second ruleset matcher, which matches all requests with |/path| in
// their URL.
const RulesetID kSource2ID(2);
std::unique_ptr<RulesetMatcher> matcher_2;
ASSERT_TRUE(CreateVerifiedMatcher({rule_2}, CreateTemporarySource(kSource2ID),
&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), /*extension_id=*/"",
HostPermissionsAlwaysRequired::kFalse);
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;
// Call GetBeforeRequestAction first to ensure that test and production code
// paths are consistent.
composite_matcher->GetAction(google_params,
RulesetMatchingStage::kOnBeforeRequest,
PageAccess::kAllowed);
std::vector<RequestAction> actions =
composite_matcher->GetModifyHeadersActions(
google_params, RulesetMatchingStage::kOnBeforeRequest);
// Construct expected request actions to be taken for a request to google.com.
RequestAction action_1 =
CreateRequestActionForTesting(RequestAction::Type::MODIFY_HEADERS,
*rule_1.id, *rule_1.priority, kSource1ID);
action_1.request_headers_to_modify = {
RequestAction::HeaderInfo("header1", dnr_api::HeaderOperation::kRemove,
std::nullopt),
RequestAction::HeaderInfo("header2", dnr_api::HeaderOperation::kSet,
"value2")};
RequestAction action_2 =
CreateRequestActionForTesting(RequestAction::Type::MODIFY_HEADERS,
*rule_2.id, *rule_2.priority, kSource2ID);
action_2.response_headers_to_modify = {
RequestAction::HeaderInfo("header1", dnr_api::HeaderOperation::kRemove,
std::nullopt),
RequestAction::HeaderInfo("header2", dnr_api::HeaderOperation::kAppend,
"VALUE2"),
RequestAction::HeaderInfo("header3", dnr_api::HeaderOperation::kSet,
"VALUE3")};
// |action_2| should be before |action_1| because |rule_2|
// has a higher priority.
EXPECT_THAT(actions, ::testing::ElementsAre(
::testing::Eq(::testing::ByRef(action_2)),
::testing::Eq(::testing::ByRef(action_1))));
// Now swap the priority of the rules, which requires re-creating the ruleset
// matchers and composite matcher.
rule_1.priority = kMinValidPriority + 1;
rule_2.priority = kMinValidPriority;
ASSERT_TRUE(CreateVerifiedMatcher({rule_1}, CreateTemporarySource(kSource1ID),
&matcher_1));
ASSERT_TRUE(CreateVerifiedMatcher({rule_2}, CreateTemporarySource(kSource2ID),
&matcher_2));
matchers.clear();
matchers.push_back(std::move(matcher_1));
matchers.push_back(std::move(matcher_2));
composite_matcher = std::make_unique<CompositeMatcher>(
std::move(matchers), /*extension_id=*/"",
HostPermissionsAlwaysRequired::kFalse);
// Call GetBeforeRequestAction first to ensure that test and production code
// paths are consistent.
composite_matcher->GetAction(google_params,
RulesetMatchingStage::kOnBeforeRequest,
PageAccess::kAllowed);
// Re-create |action_1| and |action_2| with the updated rule
// priorities. The headers modified by each action should not change.
actions = composite_matcher->GetModifyHeadersActions(
google_params, RulesetMatchingStage::kOnBeforeRequest);
action_1 =
CreateRequestActionForTesting(RequestAction::Type::MODIFY_HEADERS,
*rule_1.id, *rule_1.priority, kSource1ID);
action_1.request_headers_to_modify = {
RequestAction::HeaderInfo("header1", dnr_api::HeaderOperation::kRemove,
std::nullopt),
RequestAction::HeaderInfo("header2", dnr_api::HeaderOperation::kSet,
"value2")};
action_2 =
CreateRequestActionForTesting(RequestAction::Type::MODIFY_HEADERS,
*rule_2.id, *rule_2.priority, kSource2ID);
action_2.response_headers_to_modify = {
RequestAction::HeaderInfo("header1", dnr_api::HeaderOperation::kRemove,
std::nullopt),
RequestAction::HeaderInfo("header2", dnr_api::HeaderOperation::kAppend,
"VALUE2"),
RequestAction::HeaderInfo("header3", dnr_api::HeaderOperation::kSet,
"VALUE3")};
// |action_1| should now be before |action_2| after their
// priorities have been reversed.
EXPECT_THAT(actions, ::testing::ElementsAre(
::testing::Eq(::testing::ByRef(action_1)),
::testing::Eq(::testing::ByRef(action_2))));
}
// Tests that GetModifyHeadersActions method omits rules with an equal or lower
// priority than a matched allow or allowAllRequests rule.
TEST_F(CompositeMatcherTest, GetModifyHeadersActions_Priority) {
using HeaderInfo = RequestAction::HeaderInfo;
int allow_rule_priority = kMinValidPriority + 1;
TestRule allow_rule = CreateGenericRule();
allow_rule.id = kMinValidID;
allow_rule.condition->url_filter = std::string("google.com/1");
allow_rule.action->type = std::string("allow");
allow_rule.priority = allow_rule_priority;
TestRule url_rule_1 = CreateModifyHeadersRule(
kMinValidID + 1, allow_rule_priority - 1, "google.com", std::nullopt,
std::vector<TestHeaderInfo>(
{TestHeaderInfo("header1", "remove", std::nullopt)}),
std::nullopt);
TestRule url_rule_2 = CreateModifyHeadersRule(
kMinValidID + 2, allow_rule_priority, "google.com", std::nullopt,
std::vector<TestHeaderInfo>(
{TestHeaderInfo("header2", "remove", std::nullopt)}),
std::nullopt);
TestRule url_rule_3 = CreateModifyHeadersRule(
kMinValidID + 3, allow_rule_priority + 1, "google.com", std::nullopt,
std::vector<TestHeaderInfo>(
{TestHeaderInfo("header3", "remove", std::nullopt)}),
std::nullopt);
TestRule regex_rule_1 = CreateModifyHeadersRule(
kMinValidID + 4, allow_rule_priority - 1, std::nullopt, R"(google\.com)",
std::vector<TestHeaderInfo>(
{TestHeaderInfo("header4", "remove", std::nullopt)}),
std::nullopt);
TestRule regex_rule_2 = CreateModifyHeadersRule(
kMinValidID + 5, allow_rule_priority, std::nullopt, R"(google\.com)",
std::vector<TestHeaderInfo>(
{TestHeaderInfo("header5", "remove", std::nullopt)}),
std::nullopt);
TestRule regex_rule_3 = CreateModifyHeadersRule(
kMinValidID + 6, allow_rule_priority + 1, std::nullopt, R"(google\.com)",
std::vector<TestHeaderInfo>(
{TestHeaderInfo("header6", "remove", std::nullopt)}),
std::nullopt);
const RulesetID kSource1ID(1);
std::unique_ptr<RulesetMatcher> matcher_1;
ASSERT_TRUE(
CreateVerifiedMatcher({allow_rule, url_rule_1, url_rule_2, url_rule_3},
CreateTemporarySource(kSource1ID), &matcher_1));
const RulesetID kSource2ID(2);
std::unique_ptr<RulesetMatcher> matcher_2;
ASSERT_TRUE(CreateVerifiedMatcher({regex_rule_1, regex_rule_2, regex_rule_3},
CreateTemporarySource(kSource2ID),
&matcher_2));
// Create a CompositeMatcher with the 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), /*extension_id=*/"",
HostPermissionsAlwaysRequired::kFalse);
// Make a request to "http://google.com/1" which matches with all
// modifyHeaders rules and |allow_rule|.
GURL google_url = GURL("http://google.com/1");
RequestParams google_params;
google_params.url = &google_url;
google_params.element_type = url_pattern_index::flat::ElementType_SUBDOCUMENT;
google_params.is_third_party = false;
// Call GetBeforeRequestAction first to ensure that test and production code
// paths are consistent.
composite_matcher->GetAction(google_params,
RulesetMatchingStage::kOnBeforeRequest,
PageAccess::kAllowed);
std::vector<RequestAction> actions =
composite_matcher->GetModifyHeadersActions(
google_params, RulesetMatchingStage::kOnBeforeRequest);
auto create_action_for_rule =
[](const TestRule& rule, const RulesetID& ruleset_id,
const std::vector<HeaderInfo>& request_headers) {
RequestAction action =
CreateRequestActionForTesting(RequestAction::Type::MODIFY_HEADERS,
*rule.id, *rule.priority, ruleset_id);
action.request_headers_to_modify = request_headers;
return action;
};
RequestAction header_3_action = create_action_for_rule(
url_rule_3, kSource1ID,
{HeaderInfo("header3", dnr_api::HeaderOperation::kRemove, std::nullopt)});
RequestAction header_6_action = create_action_for_rule(
regex_rule_3, kSource2ID,
{HeaderInfo("header6", dnr_api::HeaderOperation::kRemove, std::nullopt)});
// For the request to "http://google.com/1", since |url_rule_3| and
// |regex_rule_3| are the only rules with a greater priority than
// |allow_rule|, "header3" and "header4" should be removed.
EXPECT_THAT(actions, ::testing::UnorderedElementsAre(
::testing::Eq(::testing::ByRef(header_3_action)),
::testing::Eq(::testing::ByRef(header_6_action))));
// Make a request to "http://google.com/2" which should match with all
// modifyHeaders rules but not |allow_rule|.
google_url = GURL("http://google.com/2");
google_params.url = &google_url;
// Reset the max allow rule priority cache since a new request is being made.
google_params.max_priority_allow_action.clear();
// Call GetBeforeRequestAction first to ensure that test and production code
// paths are consistent.
composite_matcher->GetAction(google_params,
RulesetMatchingStage::kOnBeforeRequest,
PageAccess::kAllowed);
actions = composite_matcher->GetModifyHeadersActions(
google_params, RulesetMatchingStage::kOnBeforeRequest);
RequestAction header_1_action = create_action_for_rule(
url_rule_1, kSource1ID,
{HeaderInfo("header1", dnr_api::HeaderOperation::kRemove, std::nullopt)});
RequestAction header_2_action = create_action_for_rule(
url_rule_2, kSource1ID,
{HeaderInfo("header2", dnr_api::HeaderOperation::kRemove, std::nullopt)});
RequestAction header_4_action = create_action_for_rule(
regex_rule_1, kSource2ID,
{HeaderInfo("header4", dnr_api::HeaderOperation::kRemove, std::nullopt)});
RequestAction header_5_action = create_action_for_rule(
regex_rule_2, kSource2ID,
{HeaderInfo("header5", dnr_api::HeaderOperation::kRemove, std::nullopt)});
// For the request to "http://google.com/2", "header1" to "header6" should be
// removed since all modifyHeaders rules are matched and there is no matching
// allow/allowAllRequests rule.
EXPECT_THAT(actions, ::testing::UnorderedElementsAre(
::testing::Eq(::testing::ByRef(header_1_action)),
::testing::Eq(::testing::ByRef(header_2_action)),
::testing::Eq(::testing::ByRef(header_3_action)),
::testing::Eq(::testing::ByRef(header_4_action)),
::testing::Eq(::testing::ByRef(header_5_action)),
::testing::Eq(::testing::ByRef(header_6_action))));
}
// 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), /*extension_id=*/"",
HostPermissionsAlwaysRequired::kFalse);
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 {
const raw_ref<GURL> request_url;
PageAccess access;
std::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.
{ToRawRef(google_url), PageAccess::kAllowed, ruleset1_url, false},
{ToRawRef(example_url), PageAccess::kAllowed, https_example_url, false},
{ToRawRef(yahoo_url), PageAccess::kAllowed, std::nullopt, false},
// Notify the request is withheld if it matches with a redirect rule.
{ToRawRef(google_url), PageAccess::kWithheld, std::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.
{ToRawRef(example_url), PageAccess::kWithheld, https_example_url, false},
{ToRawRef(yahoo_url), PageAccess::kWithheld, std::nullopt, false},
// If access to the request is denied instead of withheld, the extension
// should not be notified.
{ToRawRef(google_url), PageAccess::kDenied, std::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.
{ToRawRef(example_url), PageAccess::kDenied, https_example_url, false},
{ToRawRef(yahoo_url), PageAccess::kDenied, std::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(),
static_cast<int>(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->GetAction(
params, RulesetMatchingStage::kOnBeforeRequest, test_case.access);
EXPECT_EQ(test_case.should_notify_withheld,
redirect_action_info.notify_request_withheld);
}
}
// Tests CompositeMatcher with the HostPermissionsAlwaysRequired::kTrue mode.
TEST_F(CompositeMatcherTest, HostPermissionsAlwaysRequired) {
int rule_id = kMinValidID;
TestRule block_rule = CreateGenericRule(rule_id++);
block_rule.condition->url_filter = "example.com";
TestRule block_rule_2 = CreateGenericRule(rule_id++);
block_rule_2.condition->url_filter = "foo.com";
block_rule.priority = 3;
TestRule allow_rule = CreateGenericRule(rule_id++);
allow_rule.action->type = "allow";
allow_rule.condition->url_filter = "foo.com";
allow_rule.priority = 5;
TestRule upgrade_rule = CreateGenericRule(rule_id++);
upgrade_rule.action->type = "upgradeScheme";
upgrade_rule.condition->url_filter = "upgrade.com";
std::unique_ptr<RulesetMatcher> matcher;
ASSERT_TRUE(CreateVerifiedMatcher(
{block_rule, block_rule_2, allow_rule, upgrade_rule},
CreateTemporarySource(), &matcher));
CompositeMatcher::MatcherList matchers;
matchers.push_back(std::move(matcher));
auto composite_matcher = std::make_unique<CompositeMatcher>(
std::move(matchers), /*extension_id=*/"",
HostPermissionsAlwaysRequired::kTrue);
struct TestCase {
const char* url;
const PageAccess access;
const bool expected_notify_withheld;
std::optional<int> expected_matched_rule_id;
};
const auto cases = std::to_array<TestCase>({
{"https://example.com", PageAccess::kAllowed, false, block_rule.id},
{"https://example.com", PageAccess::kWithheld, true, std::nullopt},
{"https://foo.com", PageAccess::kAllowed, false, allow_rule.id},
// We don't expect to be notified about this (even though there's a rule
// that would have matched) because the rule would just allow the request.
{"https://foo.com", PageAccess::kWithheld, false, std::nullopt},
{"http://upgrade.com", PageAccess::kAllowed, false, upgrade_rule.id},
{"http://upgrade.com", PageAccess::kWithheld, true, std::nullopt},
{"http://nomatch.com", PageAccess::kAllowed, false, std::nullopt},
{"http://nomatch.com", PageAccess::kWithheld, false, std::nullopt},
});
for (size_t i = 0; i < std::size(cases); i++) {
SCOPED_TRACE(base::StringPrintf("Testing case %zu", i));
GURL url(cases[i].url);
RequestParams params;
params.url = &url;
ActionInfo info = composite_matcher->GetAction(
params, RulesetMatchingStage::kOnBeforeRequest, cases[i].access);
EXPECT_EQ(cases[i].expected_notify_withheld, info.notify_request_withheld);
std::optional<int> rule_matched_id;
if (info.action) {
rule_matched_id = info.action->rule_id;
}
EXPECT_EQ(cases[i].expected_matched_rule_id, rule_matched_id);
}
}
// 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), /*extension_id=*/"",
HostPermissionsAlwaysRequired::kFalse);
struct {
GURL request_url;
std::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"), std::nullopt},
{GURL("http://xyz.com"), std::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->GetAction(
params, RulesetMatchingStage::kOnBeforeRequest, 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);
}
}
// Ensure rule placement doesn't have side effects on matching priority.
TEST_F(CompositeMatcherTest, RulePlacement) {
TestRule block_rule = CreateGenericRule(kMinValidID);
block_rule.priority = 2;
block_rule.condition->url_filter = "example.com";
TestRule redirect_rule = CreateGenericRule(kMinValidID + 1);
redirect_rule.priority = 3;
redirect_rule.condition->url_filter = "example.com";
redirect_rule.action->type = "redirect";
redirect_rule.action->redirect.emplace();
redirect_rule.action->redirect->url = "http://newurl.com";
auto test_matchers = [](CompositeMatcher::MatcherList matchers) {
auto composite_matcher = std::make_unique<CompositeMatcher>(
std::move(matchers), /*extension_id=*/"",
HostPermissionsAlwaysRequired::kFalse);
GURL url("http://example.com");
RequestParams params;
params.url = &url;
ActionInfo info = composite_matcher->GetAction(
params, RulesetMatchingStage::kOnBeforeRequest, PageAccess::kAllowed);
ASSERT_TRUE(info.action);
EXPECT_EQ(kMinValidID + 1u, info.action->rule_id);
EXPECT_FALSE(info.notify_request_withheld);
// The highest priority matching rule (`redirect_rule`) needs host
// permissions to match.
info = composite_matcher->GetAction(
params, RulesetMatchingStage::kOnBeforeRequest, PageAccess::kWithheld);
EXPECT_FALSE(info.action);
EXPECT_TRUE(info.notify_request_withheld);
info = composite_matcher->GetAction(
params, RulesetMatchingStage::kOnBeforeRequest, PageAccess::kDenied);
EXPECT_FALSE(info.action);
EXPECT_FALSE(info.notify_request_withheld);
};
// Case 1: Both rules are part of the same ruleset.
{
SCOPED_TRACE("Same ruleset");
std::unique_ptr<RulesetMatcher> matcher;
ASSERT_TRUE(CreateVerifiedMatcher({block_rule, redirect_rule},
CreateTemporarySource(), &matcher));
std::vector<std::unique_ptr<RulesetMatcher>> matchers;
matchers.push_back(std::move(matcher));
test_matchers(std::move(matchers));
}
// Case 2: Both rules are part of different rulesets.
{
SCOPED_TRACE("Different ruleset");
std::unique_ptr<RulesetMatcher> block_matcher;
ASSERT_TRUE(CreateVerifiedMatcher(
{block_rule}, CreateTemporarySource(RulesetID(1)), &block_matcher));
std::unique_ptr<RulesetMatcher> redirect_matcher;
ASSERT_TRUE(CreateVerifiedMatcher({redirect_rule},
CreateTemporarySource(RulesetID(2)),
&redirect_matcher));
std::vector<std::unique_ptr<RulesetMatcher>> matchers;
matchers.push_back(std::move(block_matcher));
matchers.push_back(std::move(redirect_matcher));
test_matchers(std::move(matchers));
}
}
class CompositeMatcherResponseHeadersTest : public CompositeMatcherTest {
public:
CompositeMatcherResponseHeadersTest() {
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 an allow rule matched in OnBeforeRequest can be returned when
// matching rules in OnHeadersReceived if said rule outprioritizes all rules
// with response header conditions.
TEST_F(CompositeMatcherResponseHeadersTest, AllowRuleMatchedAcrossStages) {
// TODO(kelvinjiang): A lot of e2e DNR tests for response header rules test
// the matching logic for the onHeadersReceived stage, so a header condition
// that functionally matches on almost any header is used. Put this into
// test_utils.
std::vector<TestHeaderCondition> blank_header_condition =
std::vector<TestHeaderCondition>(
{TestHeaderCondition("nonsense-header", {}, {})});
int rule_id = kMinValidID;
// Add 3 rules:
// - OnBeforeRequest allow (pri = 3)
// - OnHeadersReceived block (pri = 2)
// - OnHeadersReceived allow (pri = 1)
TestRule before_request_allow = CreateGenericRule(rule_id++);
before_request_allow.action->type = "allow";
before_request_allow.condition->url_filter = "example.test2";
before_request_allow.priority = 3;
TestRule headers_received_block = CreateGenericRule(rule_id++);
headers_received_block.condition->url_filter = "example.test";
headers_received_block.condition->excluded_response_headers =
blank_header_condition;
headers_received_block.priority = 2;
TestRule headers_received_allow = CreateGenericRule(rule_id++);
headers_received_allow.action->type = "allow";
headers_received_allow.condition->url_filter = "example.test";
headers_received_allow.condition->excluded_response_headers =
blank_header_condition;
std::unique_ptr<RulesetMatcher> matcher;
ASSERT_TRUE(CreateVerifiedMatcher(
{before_request_allow, headers_received_block, headers_received_allow},
CreateTemporarySource(), &matcher));
CompositeMatcher::MatcherList matchers;
matchers.push_back(std::move(matcher));
auto composite_matcher = std::make_unique<CompositeMatcher>(
std::move(matchers), /*extension_id=*/"",
HostPermissionsAlwaysRequired::kTrue);
struct {
std::string url;
std::optional<int> expected_matched_before_request_id;
std::optional<int> expected_matched_headers_received_id;
} test_cases[] = {
// No rules are matched in OnBeforeRequest, but `headers_received_block`
// is matched in OnHeadersReceived.
{"https://example.test", std::nullopt, headers_received_block.id},
// `before_request_allow` is matched in OnBeforeRequest, and that match is
// carried over in `OnHeadersReceived` where it outprioritizes rules with
// header conditions, so it should be matched again.
{"https://example.test2", before_request_allow.id,
before_request_allow.id},
};
for (const auto& test_case : test_cases) {
SCOPED_TRACE(base::StringPrintf("Testing %s", test_case.url.c_str()));
// Navigate to the given URL.
GURL url(test_case.url);
auto base_headers = base::MakeRefCounted<net::HttpResponseHeaders>(
net::HttpUtil::AssembleRawHeaders("HTTP/1.0 200 OK\r\n"));
RequestParams params =
CreateRequestWithResponseHeaders(url, base_headers.get());
// Match rules in the OnBeforeRequest phase and verify the matched rule ID
// if any.
ActionInfo before_request_info = composite_matcher->GetAction(
params, RulesetMatchingStage::kOnBeforeRequest, PageAccess::kAllowed);
std::optional<int> before_request_rule_id;
if (before_request_info.action) {
before_request_rule_id = before_request_info.action->rule_id;
}
EXPECT_EQ(test_case.expected_matched_before_request_id,
before_request_rule_id);
// Reusing `params`, simulate a request matching flow by continuing to match
// rules in the OnHeadersReceived phase, and verify the matched rule ID if
// any.
ActionInfo headers_received_info = composite_matcher->GetAction(
params, RulesetMatchingStage::kOnHeadersReceived, PageAccess::kAllowed);
std::optional<int> headers_received_rule_id;
if (headers_received_info.action) {
headers_received_rule_id = headers_received_info.action->rule_id;
}
EXPECT_EQ(test_case.expected_matched_headers_received_id,
headers_received_rule_id);
}
}
} // namespace
} // namespace extensions::declarative_net_request