blob: 56aded2181a28d407566df2b7c98c35d664d8d2c [file] [log] [blame]
// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "extensions/browser/api/declarative_net_request/indexed_rule.h"
#include <algorithm>
#include <array>
#include <memory>
#include <optional>
#include <utility>
#include "base/containers/flat_set.h"
#include "base/format_macros.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/values_test_util.h"
#include "components/version_info/channel.h"
#include "extensions/browser/api/declarative_net_request/constants.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/extension.h"
#include "extensions/common/extension_features.h"
#include "extensions/common/features/feature_channel.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));
namespace extensions::declarative_net_request {
namespace {
namespace flat_rule = url_pattern_index::flat;
namespace dnr_api = extensions::api::declarative_net_request;
constexpr const char* kTestExtensionId = "extensionid";
GURL GetBaseURL() {
return Extension::GetBaseURLFromExtensionId(kTestExtensionId);
}
dnr_api::Redirect MakeRedirectUrl(const char* redirect_url) {
dnr_api::Redirect redirect;
redirect.url = redirect_url;
return redirect;
}
dnr_api::Rule CreateGenericParsedRule() {
dnr_api::Rule rule;
rule.priority = kMinValidPriority;
rule.id = kMinValidID;
rule.condition.url_filter = "filter";
rule.action.type = dnr_api::RuleActionType::kBlock;
return rule;
}
dnr_api::HeaderInfo CreateHeaderInfo(
std::string header,
std::optional<std::vector<std::string>> values,
std::optional<std::vector<std::string>> excluded_values) {
dnr_api::HeaderInfo info;
info.header = std::move(header);
info.values = std::move(values);
info.excluded_values = std::move(excluded_values);
return info;
}
using IndexedRuleTest = ::testing::Test;
TEST_F(IndexedRuleTest, IDParsing) {
struct TestCase {
const int id;
const ParseResult expected_result;
};
static constexpr auto cases = std::to_array<TestCase>({
{kMinValidID - 1, ParseResult::ERROR_INVALID_RULE_ID},
{kMinValidID, ParseResult::SUCCESS},
{kMinValidID + 1, ParseResult::SUCCESS},
});
for (size_t i = 0; i < std::size(cases); ++i) {
SCOPED_TRACE(base::StringPrintf("Testing case[%zu]", i));
dnr_api::Rule rule = CreateGenericParsedRule();
rule.id = cases[i].id;
IndexedRule indexed_rule;
ParseResult result = IndexedRule::CreateIndexedRule(
std::move(rule), GetBaseURL(), kMinValidStaticRulesetID, &indexed_rule);
EXPECT_EQ(cases[i].expected_result, result);
if (result == ParseResult::SUCCESS) {
EXPECT_EQ(base::checked_cast<uint32_t>(cases[i].id), indexed_rule.id);
}
}
}
TEST_F(IndexedRuleTest, PriorityParsing) {
struct TestCase {
dnr_api::RuleActionType action_type;
std::optional<int> priority;
const ParseResult expected_result;
// Only valid if |expected_result| is SUCCESS.
const uint32_t expected_priority;
};
static constexpr auto cases = std::to_array<TestCase>({
{dnr_api::RuleActionType::kRedirect, kMinValidPriority - 1,
ParseResult::ERROR_INVALID_RULE_PRIORITY, kDefaultPriority},
{dnr_api::RuleActionType::kRedirect, kMinValidPriority,
ParseResult::SUCCESS, kMinValidPriority},
{dnr_api::RuleActionType::kRedirect, std::nullopt, ParseResult::SUCCESS,
kDefaultPriority},
{dnr_api::RuleActionType::kRedirect, kMinValidPriority + 1,
ParseResult::SUCCESS, kMinValidPriority + 1},
{dnr_api::RuleActionType::kUpgradeScheme, kMinValidPriority - 1,
ParseResult::ERROR_INVALID_RULE_PRIORITY, kDefaultPriority},
{dnr_api::RuleActionType::kUpgradeScheme, kMinValidPriority,
ParseResult::SUCCESS, kMinValidPriority},
{dnr_api::RuleActionType::kBlock, kMinValidPriority - 1,
ParseResult::ERROR_INVALID_RULE_PRIORITY, kDefaultPriority},
{dnr_api::RuleActionType::kBlock, kMinValidPriority, ParseResult::SUCCESS,
kMinValidPriority},
});
for (size_t i = 0; i < std::size(cases); ++i) {
SCOPED_TRACE(base::StringPrintf("Testing case[%zu]", i));
dnr_api::Rule rule = CreateGenericParsedRule();
rule.priority = std::move(cases[i].priority);
rule.action.type = cases[i].action_type;
if (cases[i].action_type == dnr_api::RuleActionType::kRedirect) {
rule.action.redirect = MakeRedirectUrl("http://google.com");
}
IndexedRule indexed_rule;
ParseResult result = IndexedRule::CreateIndexedRule(
std::move(rule), GetBaseURL(), kMinValidStaticRulesetID, &indexed_rule);
EXPECT_EQ(cases[i].expected_result, result);
if (result == ParseResult::SUCCESS) {
EXPECT_EQ(ComputeIndexedRulePriority(cases[i].expected_priority,
cases[i].action_type),
indexed_rule.priority);
}
}
}
TEST_F(IndexedRuleTest, OptionsParsing) {
struct TestCase {
const dnr_api::DomainType domain_type;
const dnr_api::RuleActionType action_type;
std::optional<bool> is_url_filter_case_sensitive;
const uint8_t expected_options;
};
static constexpr auto cases = std::to_array<TestCase>({
{dnr_api::DomainType::kNone, dnr_api::RuleActionType::kBlock,
std::nullopt,
flat_rule::OptionFlag_APPLIES_TO_THIRD_PARTY |
flat_rule::OptionFlag_APPLIES_TO_FIRST_PARTY},
{dnr_api::DomainType::kFirstParty, dnr_api::RuleActionType::kAllow, true,
flat_rule::OptionFlag_IS_ALLOWLIST |
flat_rule::OptionFlag_APPLIES_TO_FIRST_PARTY |
flat_rule::OptionFlag_IS_MATCH_CASE},
{dnr_api::DomainType::kFirstParty, dnr_api::RuleActionType::kAllow, false,
flat_rule::OptionFlag_IS_ALLOWLIST |
flat_rule::OptionFlag_APPLIES_TO_FIRST_PARTY},
});
for (size_t i = 0; i < std::size(cases); ++i) {
SCOPED_TRACE(base::StringPrintf("Testing case[%zu]", i));
dnr_api::Rule rule = CreateGenericParsedRule();
rule.condition.domain_type = cases[i].domain_type;
rule.action.type = cases[i].action_type;
rule.condition.is_url_filter_case_sensitive =
std::move(cases[i].is_url_filter_case_sensitive);
IndexedRule indexed_rule;
ParseResult result = IndexedRule::CreateIndexedRule(
std::move(rule), GetBaseURL(), kMinValidStaticRulesetID, &indexed_rule);
EXPECT_EQ(ParseResult::SUCCESS, result);
EXPECT_EQ(cases[i].expected_options, indexed_rule.options);
}
}
TEST_F(IndexedRuleTest, ResourceTypesParsing) {
using ResourceTypeVec = std::vector<dnr_api::ResourceType>;
struct TestCase {
std::optional<ResourceTypeVec> resource_types;
std::optional<ResourceTypeVec> excluded_resource_types;
const ParseResult expected_result;
// Only valid if |expected_result| is SUCCESS.
const uint16_t expected_element_types;
};
const auto cases = std::to_array<TestCase>({
{std::nullopt, std::nullopt, ParseResult::SUCCESS,
flat_rule::ElementType_ANY & ~flat_rule::ElementType_MAIN_FRAME},
{std::nullopt, ResourceTypeVec({dnr_api::ResourceType::kScript}),
ParseResult::SUCCESS,
flat_rule::ElementType_ANY & ~flat_rule::ElementType_SCRIPT},
{ResourceTypeVec(
{dnr_api::ResourceType::kScript, dnr_api::ResourceType::kImage}),
std::nullopt, ParseResult::SUCCESS,
flat_rule::ElementType_SCRIPT | flat_rule::ElementType_IMAGE},
{ResourceTypeVec(
{dnr_api::ResourceType::kScript, dnr_api::ResourceType::kImage}),
ResourceTypeVec({dnr_api::ResourceType::kScript}),
ParseResult::ERROR_RESOURCE_TYPE_DUPLICATED,
flat_rule::ElementType_NONE},
{std::nullopt,
ResourceTypeVec(
{dnr_api::ResourceType::kMainFrame, dnr_api::ResourceType::kSubFrame,
dnr_api::ResourceType::kStylesheet, dnr_api::ResourceType::kScript,
dnr_api::ResourceType::kImage, dnr_api::ResourceType::kFont,
dnr_api::ResourceType::kObject,
dnr_api::ResourceType::kXmlhttprequest,
dnr_api::ResourceType::kPing, dnr_api::ResourceType::kCspReport,
dnr_api::ResourceType::kMedia, dnr_api::ResourceType::kWebsocket,
dnr_api::ResourceType::kWebtransport,
dnr_api::ResourceType::kWebbundle, dnr_api::ResourceType::kOther}),
ParseResult::ERROR_NO_APPLICABLE_RESOURCE_TYPES,
flat_rule::ElementType_NONE},
{{{}},
{{}},
ParseResult::ERROR_EMPTY_RESOURCE_TYPES_LIST,
flat_rule::ElementType_NONE},
{ResourceTypeVec({dnr_api::ResourceType::kScript}), ResourceTypeVec(),
ParseResult::SUCCESS, flat_rule::ElementType_SCRIPT},
});
for (size_t i = 0; i < std::size(cases); ++i) {
SCOPED_TRACE(base::StringPrintf("Testing case[%zu]", i));
dnr_api::Rule rule = CreateGenericParsedRule();
rule.condition.resource_types = std::move(cases[i].resource_types);
rule.condition.excluded_resource_types =
std::move(cases[i].excluded_resource_types);
IndexedRule indexed_rule;
ParseResult result = IndexedRule::CreateIndexedRule(
std::move(rule), GetBaseURL(), kMinValidStaticRulesetID, &indexed_rule);
EXPECT_EQ(cases[i].expected_result, result);
if (result == ParseResult::SUCCESS) {
EXPECT_EQ(cases[i].expected_element_types, indexed_rule.element_types);
}
}
}
TEST_F(IndexedRuleTest, UrlFilterParsing) {
struct TestCase {
std::optional<std::string> input_url_filter;
// Only valid if |expected_result| is SUCCESS.
const flat_rule::UrlPatternType expected_url_pattern_type;
const flat_rule::AnchorType expected_anchor_left;
const flat_rule::AnchorType expected_anchor_right;
const std::string expected_url_pattern;
const ParseResult expected_result;
};
const auto cases = std::to_array<TestCase>(
{{std::nullopt, flat_rule::UrlPatternType_SUBSTRING,
flat_rule::AnchorType_NONE, flat_rule::AnchorType_NONE, "",
ParseResult::SUCCESS},
{"", flat_rule::UrlPatternType_SUBSTRING, flat_rule::AnchorType_NONE,
flat_rule::AnchorType_NONE, "", ParseResult::ERROR_EMPTY_URL_FILTER},
{"|", flat_rule::UrlPatternType_SUBSTRING,
flat_rule::AnchorType_BOUNDARY, flat_rule::AnchorType_NONE, "",
ParseResult::SUCCESS},
{"||", flat_rule::UrlPatternType_SUBSTRING,
flat_rule::AnchorType_SUBDOMAIN, flat_rule::AnchorType_NONE, "",
ParseResult::SUCCESS},
{"|||", flat_rule::UrlPatternType_SUBSTRING,
flat_rule::AnchorType_SUBDOMAIN, flat_rule::AnchorType_BOUNDARY, "",
ParseResult::SUCCESS},
{"|*|||", flat_rule::UrlPatternType_WILDCARDED,
flat_rule::AnchorType_BOUNDARY, flat_rule::AnchorType_BOUNDARY, "*||",
ParseResult::SUCCESS},
{"|xyz|", flat_rule::UrlPatternType_SUBSTRING,
flat_rule::AnchorType_BOUNDARY, flat_rule::AnchorType_BOUNDARY, "xyz",
ParseResult::SUCCESS},
{"||x^yz", flat_rule::UrlPatternType_WILDCARDED,
flat_rule::AnchorType_SUBDOMAIN, flat_rule::AnchorType_NONE, "x^yz",
ParseResult::SUCCESS},
{"||xyz|", flat_rule::UrlPatternType_SUBSTRING,
flat_rule::AnchorType_SUBDOMAIN, flat_rule::AnchorType_BOUNDARY, "xyz",
ParseResult::SUCCESS},
{"x*y|z", flat_rule::UrlPatternType_WILDCARDED,
flat_rule::AnchorType_NONE, flat_rule::AnchorType_NONE, "x*y|z",
ParseResult::SUCCESS},
{"**^", flat_rule::UrlPatternType_WILDCARDED, flat_rule::AnchorType_NONE,
flat_rule::AnchorType_NONE, "**^", ParseResult::SUCCESS},
{"||google.com", flat_rule::UrlPatternType_SUBSTRING,
flat_rule::AnchorType_SUBDOMAIN, flat_rule::AnchorType_NONE,
"google.com", ParseResult::SUCCESS},
// Url pattern with non-ascii characters -ⱴase.com.
{base::WideToUTF8(L"\x2c74"
L"ase.com"),
flat_rule::UrlPatternType_SUBSTRING, flat_rule::AnchorType_NONE,
flat_rule::AnchorType_NONE, "",
ParseResult::ERROR_NON_ASCII_URL_FILTER},
// Url pattern starting with the domain anchor followed by a wildcard.
{"||*xyz", flat_rule::UrlPatternType_WILDCARDED,
flat_rule::AnchorType_SUBDOMAIN, flat_rule::AnchorType_NONE, "",
ParseResult::ERROR_INVALID_URL_FILTER}});
for (size_t i = 0; i < std::size(cases); ++i) {
SCOPED_TRACE(base::StringPrintf("Testing case[%zu]", i));
dnr_api::Rule rule = CreateGenericParsedRule();
rule.condition.url_filter = std::move(cases[i].input_url_filter);
IndexedRule indexed_rule;
ParseResult result = IndexedRule::CreateIndexedRule(
std::move(rule), GetBaseURL(), kMinValidStaticRulesetID, &indexed_rule);
if (result != ParseResult::SUCCESS) {
continue;
}
EXPECT_EQ(cases[i].expected_result, result);
EXPECT_EQ(cases[i].expected_url_pattern_type,
indexed_rule.url_pattern_type);
EXPECT_EQ(cases[i].expected_anchor_left, indexed_rule.anchor_left);
EXPECT_EQ(cases[i].expected_anchor_right, indexed_rule.anchor_right);
EXPECT_EQ(cases[i].expected_url_pattern, indexed_rule.url_pattern);
}
}
// Ensure case-insensitive patterns are lower-cased as required by
// url_pattern_index.
TEST_F(IndexedRuleTest, CaseInsensitiveLowerCased) {
const std::string kPattern = "/QUERY";
static constexpr struct TestCase {
std::optional<bool> is_url_filter_case_sensitive;
std::string_view expected_pattern;
} test_cases[] = {
{false, "/query"},
{true, "/QUERY"},
{std::nullopt, "/query"} // By default patterns are case insensitive.
};
for (const auto& test_case : test_cases) {
dnr_api::Rule rule = CreateGenericParsedRule();
rule.condition.url_filter = kPattern;
rule.condition.is_url_filter_case_sensitive =
test_case.is_url_filter_case_sensitive;
IndexedRule indexed_rule;
ASSERT_EQ(ParseResult::SUCCESS,
IndexedRule::CreateIndexedRule(std::move(rule), GetBaseURL(),
kMinValidStaticRulesetID,
&indexed_rule));
EXPECT_EQ(test_case.expected_pattern, indexed_rule.url_pattern);
}
}
TEST_F(IndexedRuleTest, DomainsParsing) {
using DomainVec = std::vector<std::string>;
struct TestCase {
std::optional<DomainVec> domains;
std::optional<DomainVec> excluded_domains;
const ParseResult expected_result;
// Only valid if |expected_result| is SUCCESS.
const DomainVec expected_domains;
const DomainVec expected_excluded_domains;
};
const auto cases = std::to_array<TestCase>({
{std::nullopt, std::nullopt, ParseResult::SUCCESS, {}, {}},
{{{}}, std::nullopt, ParseResult::ERROR_EMPTY_DOMAINS_LIST, {}, {}},
{std::nullopt, {{}}, ParseResult::SUCCESS, {}, {}},
{DomainVec({"a.com", "b.com", "a.com"}),
DomainVec({"g.com", "XY.COM", "zzz.com", "a.com", "google.com"}),
ParseResult::SUCCESS,
{"a.com", "a.com", "b.com"},
{"google.com", "zzz.com", "xy.com", "a.com", "g.com"}},
// Domain with non-ascii characters.
{DomainVec({base::WideToUTF8(L"abc\x2010" /*hyphen*/ L"def.com")}),
std::nullopt,
ParseResult::ERROR_NON_ASCII_DOMAIN,
{},
{}},
// Excluded domain with non-ascii characters.
{std::nullopt,
DomainVec({base::WideToUTF8(L"36\x00b0c.com" /*36°c.com*/)}),
ParseResult::ERROR_NON_ASCII_EXCLUDED_DOMAIN,
{},
{}},
// Internationalized domain in punycode.
{DomainVec({"xn--36c-tfa.com" /* punycode for 36°c.com*/}),
std::nullopt,
ParseResult::SUCCESS,
{"xn--36c-tfa.com"},
{}},
});
for (size_t i = 0; i < std::size(cases); ++i) {
SCOPED_TRACE(base::StringPrintf("Testing case[%zu]", i));
dnr_api::Rule domains_rule = CreateGenericParsedRule();
dnr_api::Rule initiator_domains_rule = CreateGenericParsedRule();
dnr_api::Rule request_domains_rule = CreateGenericParsedRule();
dnr_api::Rule top_domains_rule = CreateGenericParsedRule();
if (cases[i].domains) {
domains_rule.condition.domains = *cases[i].domains;
initiator_domains_rule.condition.initiator_domains = *cases[i].domains;
request_domains_rule.condition.request_domains = *cases[i].domains;
top_domains_rule.condition.top_domains = *cases[i].domains;
}
if (cases[i].excluded_domains) {
domains_rule.condition.excluded_domains = *cases[i].excluded_domains;
initiator_domains_rule.condition.excluded_initiator_domains =
*cases[i].excluded_domains;
request_domains_rule.condition.excluded_request_domains =
*cases[i].excluded_domains;
top_domains_rule.condition.excluded_top_domains =
*cases[i].excluded_domains;
}
IndexedRule indexed_domains_rule;
ParseResult domains_result = IndexedRule::CreateIndexedRule(
std::move(domains_rule), GetBaseURL(), kMinValidStaticRulesetID,
&indexed_domains_rule);
IndexedRule indexed_initiator_domains_rule;
ParseResult initiator_domains_result = IndexedRule::CreateIndexedRule(
std::move(initiator_domains_rule), GetBaseURL(),
kMinValidStaticRulesetID, &indexed_initiator_domains_rule);
IndexedRule indexed_request_domains_rule;
ParseResult request_domains_result = IndexedRule::CreateIndexedRule(
std::move(request_domains_rule), GetBaseURL(), kMinValidStaticRulesetID,
&indexed_request_domains_rule);
IndexedRule indexed_top_domains_rule;
ParseResult top_domains_result = IndexedRule::CreateIndexedRule(
std::move(top_domains_rule), GetBaseURL(), kMinValidStaticRulesetID,
&indexed_top_domains_rule);
EXPECT_EQ(cases[i].expected_result, domains_result);
switch (cases[i].expected_result) {
case ParseResult::ERROR_EMPTY_DOMAINS_LIST:
EXPECT_EQ(ParseResult::ERROR_EMPTY_INITIATOR_DOMAINS_LIST,
initiator_domains_result);
EXPECT_EQ(ParseResult::ERROR_EMPTY_REQUEST_DOMAINS_LIST,
request_domains_result);
EXPECT_EQ(ParseResult::ERROR_EMPTY_TOP_DOMAINS_LIST,
top_domains_result);
break;
case ParseResult::ERROR_NON_ASCII_DOMAIN:
EXPECT_EQ(ParseResult::ERROR_NON_ASCII_INITIATOR_DOMAIN,
initiator_domains_result);
EXPECT_EQ(ParseResult::ERROR_NON_ASCII_REQUEST_DOMAIN,
request_domains_result);
EXPECT_EQ(ParseResult::ERROR_NON_ASCII_TOP_DOMAIN, top_domains_result);
break;
case ParseResult::ERROR_NON_ASCII_EXCLUDED_DOMAIN:
EXPECT_EQ(ParseResult::ERROR_NON_ASCII_EXCLUDED_INITIATOR_DOMAIN,
initiator_domains_result);
EXPECT_EQ(ParseResult::ERROR_NON_ASCII_EXCLUDED_REQUEST_DOMAIN,
request_domains_result);
EXPECT_EQ(ParseResult::ERROR_NON_ASCII_EXCLUDED_TOP_DOMAIN,
top_domains_result);
break;
default:
EXPECT_EQ(cases[i].expected_result, initiator_domains_result);
EXPECT_EQ(cases[i].expected_result, request_domains_result);
EXPECT_EQ(cases[i].expected_result, top_domains_result);
}
if (cases[i].expected_result == ParseResult::SUCCESS) {
// The `domains` and `excluded_domains` rule conditions are deprecated and
// mapped to `initiator_domains` and `excluded_initiator_domains`.
EXPECT_EQ(cases[i].expected_domains,
indexed_domains_rule.initiator_domains);
EXPECT_EQ(cases[i].expected_excluded_domains,
indexed_domains_rule.excluded_initiator_domains);
EXPECT_EQ(cases[i].expected_domains,
indexed_initiator_domains_rule.initiator_domains);
EXPECT_EQ(cases[i].expected_excluded_domains,
indexed_initiator_domains_rule.excluded_initiator_domains);
EXPECT_EQ(cases[i].expected_domains,
indexed_request_domains_rule.request_domains);
EXPECT_EQ(cases[i].expected_excluded_domains,
indexed_request_domains_rule.excluded_request_domains);
EXPECT_EQ(cases[i].expected_domains,
indexed_top_domains_rule.top_domains);
EXPECT_EQ(cases[i].expected_excluded_domains,
indexed_top_domains_rule.excluded_top_domains);
}
}
// Test that attempting to include both domains + initiatorDomains, or
// excludedDomains + excludedInitiatorDomains results in an parsing error.
dnr_api::Rule both_domains_rule = CreateGenericParsedRule();
dnr_api::Rule both_excluded_domains_rule = CreateGenericParsedRule();
both_domains_rule.condition.domains = DomainVec({"foo"});
both_domains_rule.condition.initiator_domains = DomainVec({"bar"});
both_excluded_domains_rule.condition.excluded_domains = DomainVec({"flib"});
both_excluded_domains_rule.condition.excluded_initiator_domains =
DomainVec({"flob"});
IndexedRule indexed_both_domains_rule;
IndexedRule indexed_both_excluded_domains_rule;
EXPECT_EQ(ParseResult::ERROR_DOMAINS_AND_INITIATOR_DOMAINS_BOTH_SPECIFIED,
IndexedRule::CreateIndexedRule(
std::move(both_domains_rule), GetBaseURL(),
kMinValidStaticRulesetID, &indexed_both_domains_rule));
EXPECT_EQ(
ParseResult::
ERROR_EXCLUDED_DOMAINS_AND_EXCLUDED_INITIATOR_DOMAINS_BOTH_SPECIFIED,
IndexedRule::CreateIndexedRule(std::move(both_excluded_domains_rule),
GetBaseURL(), kMinValidStaticRulesetID,
&indexed_both_excluded_domains_rule));
}
TEST_F(IndexedRuleTest, RedirectUrlParsing) {
static constexpr struct {
const char* redirect_url;
const ParseResult expected_result;
// Only valid if |expected_result| is SUCCESS.
const std::string_view expected_redirect_url;
} cases[] = {
{"", ParseResult::ERROR_INVALID_REDIRECT_URL, ""},
{"http://google.com", ParseResult::SUCCESS, "http://google.com"},
{"/relative/url?q=1", ParseResult::ERROR_INVALID_REDIRECT_URL, ""},
{"abc", ParseResult::ERROR_INVALID_REDIRECT_URL, ""},
};
for (const auto& test_case : cases) {
SCOPED_TRACE(test_case.redirect_url);
dnr_api::Rule rule = CreateGenericParsedRule();
rule.action.redirect = MakeRedirectUrl(test_case.redirect_url);
rule.action.type = dnr_api::RuleActionType::kRedirect;
IndexedRule indexed_rule;
ParseResult result = IndexedRule::CreateIndexedRule(
std::move(rule), GetBaseURL(), kMinValidStaticRulesetID, &indexed_rule);
EXPECT_EQ(test_case.expected_result, result) << static_cast<int>(result);
if (result == ParseResult::SUCCESS) {
EXPECT_EQ(test_case.expected_redirect_url, indexed_rule.redirect_url);
}
}
}
TEST_F(IndexedRuleTest, RedirectParsing) {
struct Cases {
std::string redirect_dictionary_json;
ParseResult expected_result;
std::optional<std::string> expected_redirect_url;
};
auto cases = std::to_array<Cases>({
// clang-format off
{
"{}",
ParseResult::ERROR_INVALID_REDIRECT,
std::nullopt
},
{
R"({"url": "xyz"})",
ParseResult::ERROR_INVALID_REDIRECT_URL,
std::nullopt
},
{
R"({"url": "javascript:window.alert(\"hello,world\");"})",
ParseResult::ERROR_JAVASCRIPT_REDIRECT,
std::nullopt
},
{
R"({"url": "http://google.com"})",
ParseResult::SUCCESS,
std::string("http://google.com")
},
{
R"({"extensionPath": "foo/xyz/"})",
ParseResult::ERROR_INVALID_EXTENSION_PATH,
std::nullopt
},
{
R"({"extensionPath": "/foo/xyz?q=1"})",
ParseResult::SUCCESS,
GetBaseURL().Resolve("/foo/xyz?q=1").spec()
},
{
R"(
{
"transform": {
"scheme": "",
"host": "foo.com"
}
})", ParseResult::ERROR_INVALID_TRANSFORM_SCHEME, std::nullopt
},
{
R"(
{
"transform": {
"scheme": "javascript",
"host": "foo.com"
}
})", ParseResult::ERROR_INVALID_TRANSFORM_SCHEME, std::nullopt
},
{
R"(
{
"transform": {
"scheme": "http",
"port": "-1"
}
})", ParseResult::ERROR_INVALID_TRANSFORM_PORT, std::nullopt
},
{
R"(
{
"transform": {
"scheme": "http",
"query": "abc"
}
})", ParseResult::ERROR_INVALID_TRANSFORM_QUERY, std::nullopt
},
{
R"({"transform": {"path": "abc"}})",
ParseResult::SUCCESS,
std::nullopt
},
{
R"({"transform": {"fragment": "abc"}})",
ParseResult::ERROR_INVALID_TRANSFORM_FRAGMENT,
std::nullopt
},
{
R"({"transform": {"path": ""}})",
ParseResult::SUCCESS,
std::nullopt
},
{
R"(
{
"transform": {
"scheme": "http",
"query": "?abc",
"queryTransform": {
"removeParams": ["abc"]
}
}
})", ParseResult::ERROR_QUERY_AND_TRANSFORM_BOTH_SPECIFIED, std::nullopt
},
{
R"(
{
"transform": {
"scheme": "https",
"host": "foo.com",
"port": "80",
"path": "/foo",
"queryTransform": {
"removeParams": ["x1", "x2"],
"addOrReplaceParams": [
{"key": "y1", "value": "foo"}
]
},
"fragment": "",
"username": "user"
}
})", ParseResult::SUCCESS, std::nullopt
},
});
// clang-format on
for (const auto& test_case : cases) {
SCOPED_TRACE(test_case.redirect_dictionary_json);
dnr_api::Rule rule = CreateGenericParsedRule();
rule.action.type = dnr_api::RuleActionType::kRedirect;
auto redirect = dnr_api::Redirect::FromValue(
base::test::ParseJsonDict(test_case.redirect_dictionary_json));
ASSERT_TRUE(redirect.has_value());
rule.action.redirect = std::move(redirect).value();
IndexedRule indexed_rule;
ParseResult result = IndexedRule::CreateIndexedRule(
std::move(rule), GetBaseURL(), kMinValidStaticRulesetID, &indexed_rule);
EXPECT_EQ(test_case.expected_result, result) << static_cast<int>(result);
if (result != ParseResult::SUCCESS) {
continue;
}
EXPECT_TRUE(indexed_rule.url_transform || indexed_rule.redirect_url);
EXPECT_FALSE(indexed_rule.url_transform && indexed_rule.redirect_url);
EXPECT_EQ(test_case.expected_redirect_url, indexed_rule.redirect_url);
}
}
TEST_F(IndexedRuleTest, RegexFilterParsing) {
static constexpr struct {
std::string_view regex_filter;
ParseResult result;
} cases[] = {{"", ParseResult::ERROR_EMPTY_REGEX_FILTER},
// Filter with non-ascii characters.
{"αcd", ParseResult::ERROR_NON_ASCII_REGEX_FILTER},
// Invalid regex: Unterminated character class.
{"x[ab", ParseResult::ERROR_INVALID_REGEX_FILTER},
// Invalid regex: Incomplete capturing group.
{"x(", ParseResult::ERROR_INVALID_REGEX_FILTER},
// Invalid regex: Invalid escape sequence \x.
{R"(ij\x1)", ParseResult::ERROR_INVALID_REGEX_FILTER},
{R"(ij\\x1)", ParseResult::SUCCESS},
{R"(^http://www\.(abc|def)\.xyz\.com/)", ParseResult::SUCCESS}};
for (const auto& test_case : cases) {
SCOPED_TRACE(test_case.regex_filter);
dnr_api::Rule rule = CreateGenericParsedRule();
rule.condition.url_filter.reset();
rule.condition.regex_filter = test_case.regex_filter;
IndexedRule indexed_rule;
ParseResult result = IndexedRule::CreateIndexedRule(
std::move(rule), GetBaseURL(), kMinValidStaticRulesetID, &indexed_rule);
EXPECT_EQ(result, test_case.result);
if (result == ParseResult::SUCCESS) {
EXPECT_EQ(indexed_rule.url_pattern, test_case.regex_filter);
EXPECT_EQ(flat_rule::UrlPatternType_REGEXP,
indexed_rule.url_pattern_type);
}
}
}
TEST_F(IndexedRuleTest, MultipleFiltersSpecified) {
dnr_api::Rule rule = CreateGenericParsedRule();
rule.condition.url_filter = "google";
rule.condition.regex_filter = "example";
IndexedRule indexed_rule;
ParseResult result = IndexedRule::CreateIndexedRule(
std::move(rule), GetBaseURL(), kMinValidStaticRulesetID, &indexed_rule);
EXPECT_EQ(ParseResult::ERROR_MULTIPLE_FILTERS_SPECIFIED, result);
}
TEST_F(IndexedRuleTest, RegexSubstitutionParsing) {
static constexpr struct {
// |regex_filter| may be null.
const char* regex_filter;
std::string_view regex_substitution;
ParseResult result;
} cases[] = {
{nullptr, "http://google.com",
ParseResult::ERROR_REGEX_SUBSTITUTION_WITHOUT_FILTER},
// \0 in |regex_substitution| refers to the entire matching text.
{R"(^http://(.*)\.com/)", R"(https://redirect.com?referrer=\0)",
ParseResult::SUCCESS},
{R"(^http://google\.com?q1=(.*)&q2=(.*))",
R"(https://redirect.com?&q1=\0&q2=\2)", ParseResult::SUCCESS},
// Referencing invalid capture group.
{R"(^http://google\.com?q1=(.*)&q2=(.*))",
R"(https://redirect.com?&q1=\1&q2=\3)",
ParseResult::ERROR_INVALID_REGEX_SUBSTITUTION},
// Empty substitution.
{R"(^http://(.*)\.com/)", "",
ParseResult::ERROR_INVALID_REGEX_SUBSTITUTION},
// '\' must always be followed by a '\' or a digit.
{R"(^http://(.*)\.com/)", R"(https://example.com?q=\a)",
ParseResult::ERROR_INVALID_REGEX_SUBSTITUTION},
};
for (const auto& test_case : cases) {
SCOPED_TRACE(test_case.regex_substitution);
dnr_api::Rule rule = CreateGenericParsedRule();
rule.condition.url_filter.reset();
if (test_case.regex_filter) {
rule.condition.regex_filter = test_case.regex_filter;
}
rule.priority = kMinValidPriority;
rule.action.type = dnr_api::RuleActionType::kRedirect;
rule.action.redirect.emplace();
rule.action.redirect->regex_substitution = test_case.regex_substitution;
IndexedRule indexed_rule;
ParseResult result = IndexedRule::CreateIndexedRule(
std::move(rule), GetBaseURL(), kMinValidStaticRulesetID, &indexed_rule);
EXPECT_EQ(test_case.result, result);
if (result == ParseResult::SUCCESS) {
EXPECT_EQ(flat_rule::UrlPatternType_REGEXP,
indexed_rule.url_pattern_type);
ASSERT_TRUE(indexed_rule.regex_substitution);
EXPECT_EQ(test_case.regex_substitution, *indexed_rule.regex_substitution);
}
}
}
// Tests the parsing behavior when multiple keys in "Redirect" dictionary are
// specified.
TEST_F(IndexedRuleTest, MultipleRedirectKeys) {
dnr_api::Rule rule = CreateGenericParsedRule();
rule.priority = kMinValidPriority;
rule.condition.url_filter.reset();
rule.condition.regex_filter = "\\.*";
rule.action.type = dnr_api::RuleActionType::kRedirect;
rule.action.redirect.emplace();
dnr_api::Redirect& redirect = *rule.action.redirect;
redirect.url = "http://google.com";
redirect.regex_substitution = "http://example.com";
redirect.transform.emplace();
redirect.transform->scheme = "https";
IndexedRule indexed_rule;
ParseResult result = IndexedRule::CreateIndexedRule(
std::move(rule), GetBaseURL(), kMinValidStaticRulesetID, &indexed_rule);
EXPECT_EQ(ParseResult::SUCCESS, result);
// The redirect "url" is given preference in this case.
EXPECT_FALSE(indexed_rule.url_transform);
EXPECT_FALSE(indexed_rule.regex_substitution);
EXPECT_EQ("http://google.com", indexed_rule.redirect_url);
}
TEST_F(IndexedRuleTest, InvalidAllowAllRequestsResourceType) {
using ResourceTypeVec = std::vector<dnr_api::ResourceType>;
struct TestCase {
ResourceTypeVec resource_types;
ResourceTypeVec excluded_resource_types;
const ParseResult expected_result;
// Only valid if |expected_result| is SUCCESS.
const uint16_t expected_element_types;
};
const auto cases = std::to_array<TestCase>({
{{}, {}, ParseResult::ERROR_INVALID_ALLOW_ALL_REQUESTS_RESOURCE_TYPE, 0},
{{dnr_api::ResourceType::kSubFrame},
{dnr_api::ResourceType::kScript},
ParseResult::SUCCESS,
flat_rule::ElementType_SUBDOCUMENT},
{{dnr_api::ResourceType::kScript, dnr_api::ResourceType::kMainFrame},
{},
ParseResult::ERROR_INVALID_ALLOW_ALL_REQUESTS_RESOURCE_TYPE,
0},
{{dnr_api::ResourceType::kMainFrame, dnr_api::ResourceType::kSubFrame},
{},
ParseResult::SUCCESS,
flat_rule::ElementType_MAIN_FRAME | flat_rule::ElementType_SUBDOCUMENT},
{{dnr_api::ResourceType::kMainFrame},
{},
ParseResult::SUCCESS,
flat_rule::ElementType_MAIN_FRAME},
});
for (size_t i = 0; i < std::size(cases); ++i) {
SCOPED_TRACE(base::StringPrintf("Testing case[%zu]", i));
dnr_api::Rule rule = CreateGenericParsedRule();
if (cases[i].resource_types.empty()) {
rule.condition.resource_types = std::nullopt;
} else {
rule.condition.resource_types = cases[i].resource_types;
}
rule.condition.excluded_resource_types = cases[i].excluded_resource_types;
rule.action.type = dnr_api::RuleActionType::kAllowAllRequests;
IndexedRule indexed_rule;
ParseResult result = IndexedRule::CreateIndexedRule(
std::move(rule), GetBaseURL(), kMinValidStaticRulesetID, &indexed_rule);
EXPECT_EQ(cases[i].expected_result, result);
if (result == ParseResult::SUCCESS) {
EXPECT_EQ(cases[i].expected_element_types, indexed_rule.element_types);
}
}
}
TEST_F(IndexedRuleTest, ModifyHeadersParsing) {
struct RawHeaderInfo {
dnr_api::HeaderOperation operation;
std::string header;
std::optional<std::string> value;
std::optional<std::string> regex_filter;
std::optional<std::string> regex_substitution;
};
using RawHeaderInfoList = std::vector<RawHeaderInfo>;
using ModifyHeaderInfoList = std::vector<dnr_api::ModifyHeaderInfo>;
// A copy-able version of dnr_api::ModifyHeaderInfo is used for ease of
// specifying test cases because elements are copied when initializing a
// vector from an array.
struct TestCase {
std::optional<RawHeaderInfoList> request_headers;
std::optional<RawHeaderInfoList> response_headers;
ParseResult expected_result;
};
const auto cases = std::to_array<TestCase>({
// Raise an error if no headers are specified.
{std::nullopt, std::nullopt,
ParseResult::ERROR_NO_HEADERS_TO_MODIFY_SPECIFIED},
// Raise an error if the request or response headers list is specified,
// but empty.
{RawHeaderInfoList(),
RawHeaderInfoList(
{{dnr_api::HeaderOperation::kRemove, "set-cookie", std::nullopt}}),
ParseResult::ERROR_EMPTY_MODIFY_REQUEST_HEADERS_LIST},
{std::nullopt, RawHeaderInfoList(),
ParseResult::ERROR_EMPTY_MODIFY_RESPONSE_HEADERS_LIST},
// Raise an error if a header list contains an empty or invalid header
// name.
{std::nullopt,
RawHeaderInfoList(
{{dnr_api::HeaderOperation::kRemove, "", std::nullopt}}),
ParseResult::ERROR_INVALID_HEADER_TO_MODIFY_NAME},
{std::nullopt,
RawHeaderInfoList(
{{dnr_api::HeaderOperation::kRemove, "<<invalid>>", std::nullopt}}),
ParseResult::ERROR_INVALID_HEADER_TO_MODIFY_NAME},
// Raise an error if a header list contains an invalid header value.
{std::nullopt,
RawHeaderInfoList({{dnr_api::HeaderOperation::kAppend, "set-cookie",
"invalid\nvalue"}}),
ParseResult::ERROR_INVALID_HEADER_TO_MODIFY_VALUE},
// Raise an error if a header value is specified for a remove rule.
{RawHeaderInfoList(
{{dnr_api::HeaderOperation::kRemove, "cookie", "remove"}}),
std::nullopt, ParseResult::ERROR_HEADER_VALUE_PRESENT},
// Raise an error if no header value is specified for an append or set
// rule.
{RawHeaderInfoList(
{{dnr_api::HeaderOperation::kSet, "cookie", std::nullopt}}),
std::nullopt, ParseResult::ERROR_HEADER_VALUE_NOT_SPECIFIED},
{std::nullopt,
RawHeaderInfoList(
{{dnr_api::HeaderOperation::kAppend, "set-cookie", std::nullopt}}),
ParseResult::ERROR_HEADER_VALUE_NOT_SPECIFIED},
// Raise an error if a rule specifies an invalid request header to be
// appended.
{RawHeaderInfoList(
{{dnr_api::HeaderOperation::kAppend, "invalid-header", "value"}}),
std::nullopt, ParseResult::ERROR_APPEND_INVALID_REQUEST_HEADER},
{RawHeaderInfoList(
{{dnr_api::HeaderOperation::kRemove, "cookie", std::nullopt},
{dnr_api::HeaderOperation::kSet, "referer", ""},
{dnr_api::HeaderOperation::kAppend, "accept-language", "en-US"}}),
std::nullopt, ParseResult::SUCCESS},
{RawHeaderInfoList(
{{dnr_api::HeaderOperation::kRemove, "referer", std::nullopt}}),
RawHeaderInfoList(
{{dnr_api::HeaderOperation::kAppend, "set-cookie", "abcd"}}),
ParseResult::SUCCESS},
});
for (size_t i = 0; i < std::size(cases); ++i) {
SCOPED_TRACE(base::StringPrintf("Testing case[%zu]", i));
dnr_api::Rule rule = CreateGenericParsedRule();
rule.action.type = dnr_api::RuleActionType::kModifyHeaders;
ModifyHeaderInfoList expected_request_headers;
if (cases[i].request_headers) {
rule.action.request_headers.emplace();
for (auto header : *cases[i].request_headers) {
rule.action.request_headers->push_back(CreateModifyHeaderInfo(
header.operation, header.header, header.value, header.regex_filter,
header.regex_substitution));
expected_request_headers.push_back(CreateModifyHeaderInfo(
header.operation, header.header, header.value, header.regex_filter,
header.regex_substitution));
}
}
ModifyHeaderInfoList expected_response_headers;
if (cases[i].response_headers) {
rule.action.response_headers.emplace();
for (auto header : *cases[i].response_headers) {
rule.action.response_headers->push_back(CreateModifyHeaderInfo(
header.operation, header.header, header.value, header.regex_filter,
header.regex_substitution));
expected_response_headers.push_back(CreateModifyHeaderInfo(
header.operation, header.header, header.value, header.regex_filter,
header.regex_substitution));
}
}
IndexedRule indexed_rule;
ParseResult result = IndexedRule::CreateIndexedRule(
std::move(rule), GetBaseURL(), kMinValidStaticRulesetID, &indexed_rule);
EXPECT_EQ(cases[i].expected_result, result);
if (result != ParseResult::SUCCESS) {
continue;
}
EXPECT_EQ(dnr_api::RuleActionType::kModifyHeaders,
indexed_rule.action_type);
EXPECT_TRUE(std::ranges::equal(expected_request_headers,
indexed_rule.request_headers_to_modify,
EqualsForTesting));
EXPECT_TRUE(std::ranges::equal(expected_response_headers,
indexed_rule.response_headers_to_modify,
EqualsForTesting));
}
}
TEST_F(IndexedRuleTest, RequestMethodsParsing) {
using RequestMethodVec = std::vector<dnr_api::RequestMethod>;
struct TestCase {
std::optional<RequestMethodVec> request_methods;
std::optional<RequestMethodVec> excluded_request_methods;
const ParseResult expected_result;
// Only valid if `expected_result` is SUCCESS.
const uint16_t expected_request_methods_mask;
};
const auto cases = std::to_array<TestCase>(
{{std::nullopt, std::nullopt, ParseResult::SUCCESS,
flat_rule::RequestMethod_ANY},
{std::nullopt, RequestMethodVec({dnr_api::RequestMethod::kPut}),
ParseResult::SUCCESS,
flat_rule::RequestMethod_ANY & ~flat_rule::RequestMethod_PUT},
{RequestMethodVec(
{dnr_api::RequestMethod::kDelete, dnr_api::RequestMethod::kGet}),
std::nullopt, ParseResult::SUCCESS,
flat_rule::RequestMethod_DELETE | flat_rule::RequestMethod_GET},
{RequestMethodVec({dnr_api::RequestMethod::kHead,
dnr_api::RequestMethod::kOptions,
dnr_api::RequestMethod::kPatch}),
std::nullopt, ParseResult::SUCCESS,
flat_rule::RequestMethod_HEAD | flat_rule::RequestMethod_OPTIONS |
flat_rule::RequestMethod_PATCH},
{RequestMethodVec({dnr_api::RequestMethod::kPost}),
RequestMethodVec({dnr_api::RequestMethod::kPost}),
ParseResult::ERROR_REQUEST_METHOD_DUPLICATED,
flat_rule::RequestMethod_NONE},
{{{}},
std::nullopt,
ParseResult::ERROR_EMPTY_REQUEST_METHODS_LIST,
flat_rule::RequestMethod_NONE}});
for (size_t i = 0; i < std::size(cases); ++i) {
SCOPED_TRACE(base::StringPrintf("Testing case[%zu]", i));
dnr_api::Rule rule = CreateGenericParsedRule();
rule.condition.request_methods = std::move(cases[i].request_methods);
rule.condition.excluded_request_methods =
std::move(cases[i].excluded_request_methods);
IndexedRule indexed_rule;
ParseResult result = IndexedRule::CreateIndexedRule(
std::move(rule), GetBaseURL(), kMinValidStaticRulesetID, &indexed_rule);
EXPECT_EQ(cases[i].expected_result, result);
if (result == ParseResult::SUCCESS) {
EXPECT_EQ(cases[i].expected_request_methods_mask,
indexed_rule.request_methods);
}
}
}
TEST_F(IndexedRuleTest, TabID) {
using IntVec = std::vector<int>;
struct TestCase {
std::optional<IntVec> tab_ids;
std::optional<IntVec> excluded_tab_ids;
RulesetID ruleset_id;
ParseResult expected_result;
// Only relevant if `expected_result` is ParseResult::SUCCESS.
base::flat_set<int> expected_tab_ids;
base::flat_set<int> expected_excluded_tab_ids;
};
const auto cases = std::to_array<TestCase>({
{std::nullopt, std::nullopt, kSessionRulesetID, ParseResult::SUCCESS},
{IntVec(), IntVec({3, 4, 4}), kSessionRulesetID,
ParseResult::ERROR_EMPTY_TAB_IDS_LIST},
{IntVec({1, 2}),
IntVec({3, 4, 3}),
kSessionRulesetID,
ParseResult::SUCCESS,
{1, 2},
{}},
{std::nullopt,
IntVec({3, 4, 3}),
kSessionRulesetID,
ParseResult::SUCCESS,
{},
{3, 4}},
{IntVec({1, 2, 3}), IntVec({5, 2}), kSessionRulesetID,
ParseResult::ERROR_TAB_ID_DUPLICATED},
{IntVec({1, 2}), std::nullopt, kDynamicRulesetID,
ParseResult::ERROR_TAB_IDS_ON_NON_SESSION_RULE},
{IntVec({1, 2}), IntVec({3}), kMinValidStaticRulesetID,
ParseResult::ERROR_TAB_IDS_ON_NON_SESSION_RULE},
});
for (size_t i = 0; i < std::size(cases); ++i) {
SCOPED_TRACE(base::StringPrintf("Testing case[%zu]", i));
dnr_api::Rule rule = CreateGenericParsedRule();
if (cases[i].tab_ids) {
rule.condition.tab_ids = *cases[i].tab_ids;
}
if (cases[i].excluded_tab_ids) {
rule.condition.excluded_tab_ids = *cases[i].excluded_tab_ids;
}
IndexedRule indexed_rule;
ParseResult result = IndexedRule::CreateIndexedRule(
std::move(rule), GetBaseURL(), cases[i].ruleset_id, &indexed_rule);
EXPECT_EQ(cases[i].expected_result, result);
if (result == ParseResult::SUCCESS) {
EXPECT_EQ(cases[i].expected_tab_ids, indexed_rule.tab_ids);
EXPECT_EQ(cases[i].expected_excluded_tab_ids,
indexed_rule.excluded_tab_ids);
}
}
}
class IndexedResponseHeaderRuleTest : public IndexedRuleTest {
public:
IndexedResponseHeaderRuleTest() {
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 the validation of rules that specify response header matching
// conditions.
TEST_F(IndexedResponseHeaderRuleTest, MatchingResponseHeaders) {
struct RawHeaderInfo {
explicit RawHeaderInfo(std::string header) : header(std::move(header)) {}
RawHeaderInfo(std::string header,
std::optional<std::vector<std::string>> values,
std::optional<std::vector<std::string>> excluded_values)
: header(std::move(header)),
values(std::move(values)),
excluded_values(std::move(excluded_values)) {}
std::string header;
std::optional<std::vector<std::string>> values;
std::optional<std::vector<std::string>> excluded_values;
};
using HeaderValues = std::vector<std::string>;
using HeaderInfoList = std::vector<RawHeaderInfo>;
struct TestCase {
std::optional<HeaderInfoList> response_headers;
std::optional<HeaderInfoList> excluded_response_headers;
ParseResult expected_result;
};
const auto cases = std::to_array<TestCase>({
// No response headers included or excluded; should parse successfully.
{std::nullopt, std::nullopt, ParseResult::SUCCESS},
// only header1 specified, matching on name only should parse
// successfully.
{HeaderInfoList({RawHeaderInfo("header1")}), std::nullopt,
ParseResult::SUCCESS},
// Valid included and excluded response headers with values and excluded
// values should parse successfully.
{HeaderInfoList(
{{"header1", HeaderValues({"value-1", "value-2"}), std::nullopt},
{"header1", std::nullopt, HeaderValues({"excluded-value"})}}),
HeaderInfoList({RawHeaderInfo("excluded-header")}),
ParseResult::SUCCESS},
// An empty response header value should parse successfully.
{HeaderInfoList({{"header", HeaderValues({""}), std::nullopt}}),
std::nullopt, ParseResult::SUCCESS},
// An empty matching response header list should trigger an error.
{HeaderInfoList(), std::nullopt,
ParseResult::ERROR_EMPTY_RESPONSE_HEADER_MATCHING_LIST},
// An empty matching excluded response header list should trigger an
// error.
{std::nullopt, HeaderInfoList(),
ParseResult::ERROR_EMPTY_EXCLUDED_RESPONSE_HEADER_MATCHING_LIST},
// Test that a rule with an empty or invalid response header name will
// return an error.
{HeaderInfoList({RawHeaderInfo("")}), std::nullopt,
ParseResult::ERROR_INVALID_MATCHING_RESPONSE_HEADER_NAME},
{std::nullopt, HeaderInfoList({RawHeaderInfo("<<invalid_header>>")}),
ParseResult::ERROR_INVALID_MATCHING_EXCLUDED_RESPONSE_HEADER_NAME},
// Test that a rule with an invalid response header value will return an
// error.
{std::nullopt,
HeaderInfoList({{"invalid-header-value",
HeaderValues({"value\nwith\nnewline"}), std::nullopt}}),
ParseResult::ERROR_INVALID_MATCHING_RESPONSE_HEADER_VALUE},
// Test that a rule cannot specify the same header in `response_headers`
// and `excluded_response_headers` if that header is to be matched based
// on name only in `excluded_response_headers`.
{HeaderInfoList({{"repeated-header", HeaderValues({"specific-value"}),
std::nullopt}}),
HeaderInfoList({RawHeaderInfo("repeated-header")}),
ParseResult::ERROR_MATCHING_RESPONSE_HEADER_DUPLICATED},
// Test that a rule CAN specify the same header in `response_headers` and
// `excluded_response_headers` if that header is matched on name AND value
// in `excluded_response_headers`. In practice, the below rule will match
// a request if its `excluded_response_headers` condition does not match
// and its `response_headers` condition matches.
{HeaderInfoList({{"repeated-header", HeaderValues({"specific-value"}),
std::nullopt}}),
HeaderInfoList({{"repeated-header", HeaderValues({"excluded-value"}),
std::nullopt}}),
ParseResult::SUCCESS},
});
auto get_header_info_matcher = [](const RawHeaderInfo& info) {
return testing::AllOf(
testing::Field(&dnr_api::HeaderInfo::header, info.header),
testing::Field(&dnr_api::HeaderInfo::values, info.values),
testing::Field(&dnr_api::HeaderInfo::excluded_values,
info.excluded_values));
};
for (size_t i = 0; i < std::size(cases); ++i) {
SCOPED_TRACE(base::StringPrintf("Testing case[%zu]", i));
dnr_api::Rule rule = CreateGenericParsedRule();
std::vector<testing::Matcher<dnr_api::HeaderInfo>> response_header_matchers;
if (cases[i].response_headers) {
rule.condition.response_headers.emplace();
for (const auto& header : *cases[i].response_headers) {
rule.condition.response_headers->push_back(CreateHeaderInfo(
header.header, header.values, header.excluded_values));
response_header_matchers.push_back(get_header_info_matcher(header));
}
}
std::vector<testing::Matcher<dnr_api::HeaderInfo>>
expected_excluded_response_headers;
if (cases[i].excluded_response_headers) {
rule.condition.excluded_response_headers.emplace();
for (const auto& header : *cases[i].excluded_response_headers) {
rule.condition.excluded_response_headers->push_back(CreateHeaderInfo(
header.header, header.values, header.excluded_values));
expected_excluded_response_headers.push_back(
get_header_info_matcher(header));
}
}
IndexedRule indexed_rule;
ParseResult result = IndexedRule::CreateIndexedRule(
std::move(rule), GetBaseURL(), kMinValidStaticRulesetID, &indexed_rule);
EXPECT_EQ(cases[i].expected_result, result);
if (result != ParseResult::SUCCESS) {
continue;
}
// If parsing is successful, test that the `response_headers` and
// `excluded_response_headers` from `indexed_rule` are the same as what's
// specified in the test case.
EXPECT_THAT(indexed_rule.response_headers,
testing::UnorderedElementsAreArray(response_header_matchers));
EXPECT_THAT(
indexed_rule.excluded_response_headers,
testing::UnorderedElementsAreArray(expected_excluded_response_headers));
}
}
// Test that response header matching rules may only modify response headers.
TEST_F(IndexedResponseHeaderRuleTest, MatchingResponseHeaders_ModifyHeaders) {
struct RawModifyHeaderInfo {
dnr_api::HeaderOperation operation;
std::string header;
std::optional<std::string> value;
};
using ModifyHeaderInfoList = std::vector<RawModifyHeaderInfo>;
struct TestCase {
std::optional<ModifyHeaderInfoList> request_headers_to_modify;
std::optional<ModifyHeaderInfoList> response_headers_to_modify;
ParseResult expected_result;
};
const auto cases = std::to_array<TestCase>({
// Two test cases here: one for a rule that tries to modify request
// headers, one for response headers. The first rule is disallowed since
// request headers cannot be further modified when it comes time to match
// on response headers.
{ModifyHeaderInfoList({{dnr_api::HeaderOperation::kRemove,
"request-header", std::nullopt}}),
std::nullopt,
ParseResult::ERROR_RESPONSE_HEADER_RULE_CANNOT_MODIFY_REQUEST_HEADERS},
{std::nullopt,
ModifyHeaderInfoList(
{{dnr_api::HeaderOperation::kSet, "response-header", "new-value"}}),
ParseResult::SUCCESS},
});
for (size_t i = 0; i < std::size(cases); ++i) {
SCOPED_TRACE(base::StringPrintf("Testing case[%zu]", i));
dnr_api::Rule rule = CreateGenericParsedRule();
rule.condition.response_headers.emplace();
rule.condition.response_headers->push_back(
CreateHeaderInfo("header", std::nullopt, std::nullopt));
rule.action.type = dnr_api::RuleActionType::kModifyHeaders;
if (cases[i].request_headers_to_modify) {
rule.action.request_headers.emplace();
for (const auto& header : *cases[i].request_headers_to_modify) {
rule.action.request_headers->push_back(CreateModifyHeaderInfo(
header.operation, header.header, header.value));
}
}
if (cases[i].response_headers_to_modify) {
rule.action.response_headers.emplace();
for (const auto& header : *cases[i].response_headers_to_modify) {
rule.action.response_headers->push_back(CreateModifyHeaderInfo(
header.operation, header.header, header.value));
}
}
IndexedRule indexed_rule;
ParseResult result = IndexedRule::CreateIndexedRule(
std::move(rule), GetBaseURL(), kMinValidStaticRulesetID, &indexed_rule);
EXPECT_EQ(cases[i].expected_result, result);
}
}
class IndexedHeaderSubstitutionRuleTest : public IndexedRuleTest {
public:
IndexedHeaderSubstitutionRuleTest() {
scoped_feature_list_.InitAndEnableFeature(
extensions_features::kDeclarativeNetRequestHeaderSubstitution);
}
private:
// TODO(crbug.com/352093575): Once feature is launched and feature flag can be
// removed, replace usages of this test class with just
// DeclarativeNetRequestBrowserTest.
base::test::ScopedFeatureList scoped_feature_list_;
};
// Test parsing for regex filters and substitutions inside ModifyHeaderInfo.
TEST_F(IndexedHeaderSubstitutionRuleTest,
ModifyHeaderInfoRegexFilterAndSubstitutionParsing) {
struct TestCase {
std::optional<std::string> regex_filter;
std::optional<std::string> regex_substitution;
ParseResult expected_result;
};
const auto cases = std::to_array<TestCase>({
// Test valid cases:
{"bad-cookie", std::nullopt, ParseResult::SUCCESS},
{"bad-cookie", "good-cookie=phew", ParseResult::SUCCESS},
// TODO(crbug.com/352093575): When the feature is about to launch, add a
// case which requires a regex substitution if the operation is not
// specified.
// Test some invalid regex filter cases.
{"", "new-cookie=coconut", ParseResult::ERROR_EMPTY_REGEX_FILTER},
{"αcd", "new-cookie=coconut", ParseResult::ERROR_NON_ASCII_REGEX_FILTER},
// Invalid regex: Incomplete capturing group.
{"x(", "new-cookie=coconut", ParseResult::ERROR_INVALID_REGEX_FILTER},
// Test an invalid regex substitution case, where the substitution
// references more capture groups than what the filter captures.
{R"(^http://google\.com?q1=(.*)&q2=(.*))",
R"(https://redirect.com?&q1=\1&q2=\3)",
ParseResult::ERROR_INVALID_REGEX_SUBSTITUTION},
});
for (size_t i = 0; i < std::size(cases); ++i) {
SCOPED_TRACE(base::StringPrintf("Testing case[%zu]", i));
dnr_api::Rule rule = CreateGenericParsedRule();
rule.action.type = dnr_api::RuleActionType::kModifyHeaders;
rule.action.request_headers.emplace();
rule.action.request_headers->push_back(CreateModifyHeaderInfo(
dnr_api::HeaderOperation::kRemove, "cookie", std::nullopt,
cases[i].regex_filter, cases[i].regex_substitution));
dnr_api::ModifyHeaderInfo expected_request_header;
expected_request_header = CreateModifyHeaderInfo(
dnr_api::HeaderOperation::kRemove, "cookie", std::nullopt,
cases[i].regex_filter, cases[i].regex_substitution);
IndexedRule indexed_rule;
ParseResult result = IndexedRule::CreateIndexedRule(
std::move(rule), GetBaseURL(), kMinValidStaticRulesetID, &indexed_rule);
EXPECT_EQ(cases[i].expected_result, result);
}
}
} // namespace
} // namespace extensions::declarative_net_request