| // Copyright 2017 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 <stddef.h> |
| #include <string> |
| #include <vector> |
| |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/json/json_reader.h" |
| #include "base/memory/ref_counted.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "chrome/browser/extensions/api/declarative_net_request/dnr_test_base.h" |
| #include "chrome/browser/extensions/chrome_test_extension_loader.h" |
| #include "chrome/browser/extensions/extension_service.h" |
| #include "chrome/browser/extensions/load_error_reporter.h" |
| #include "extensions/browser/api/declarative_net_request/constants.h" |
| #include "extensions/browser/api/declarative_net_request/parse_info.h" |
| #include "extensions/browser/api/declarative_net_request/test_utils.h" |
| #include "extensions/browser/test_extension_registry_observer.h" |
| #include "extensions/common/api/declarative_net_request.h" |
| #include "extensions/common/api/declarative_net_request/test_utils.h" |
| #include "extensions/common/error_utils.h" |
| #include "extensions/common/file_util.h" |
| #include "extensions/common/install_warning.h" |
| #include "extensions/common/manifest_constants.h" |
| #include "extensions/common/url_pattern.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace extensions { |
| namespace declarative_net_request { |
| namespace { |
| |
| constexpr char kJSONRulesFilename[] = "rules_file.json"; |
| const base::FilePath::CharType kJSONRulesetFilepath[] = |
| FILE_PATH_LITERAL("rules_file.json"); |
| |
| // Fixure testing that declarative rules corresponding to the Declarative Net |
| // Request API are correctly indexed, for both packed and unpacked |
| // extensions. |
| class RuleIndexingTest : public DNRTestBase { |
| public: |
| RuleIndexingTest() {} |
| |
| // DNRTestBase override. |
| void SetUp() override { |
| DNRTestBase::SetUp(); |
| loader_ = CreateExtensionLoader(); |
| } |
| |
| protected: |
| void AddRule(const TestRule& rule) { rules_list_.push_back(rule); } |
| |
| // This takes precedence over the AddRule method. |
| void SetRules(std::unique_ptr<base::Value> rules) { |
| rules_value_ = std::move(rules); |
| } |
| |
| // Loads the extension and verifies the indexed ruleset location and histogram |
| // counts. |
| void LoadAndExpectSuccess(size_t expected_indexed_rules_count) { |
| base::HistogramTester tester; |
| WriteExtensionData(); |
| |
| loader_->set_should_fail(false); |
| |
| // Clear all load errors before loading the extension. |
| error_reporter()->ClearErrors(); |
| |
| extension_ = loader_->LoadExtension(extension_dir_); |
| ASSERT_TRUE(extension_.get()); |
| |
| EXPECT_TRUE(HasValidIndexedRuleset(*extension_, browser_context())); |
| |
| // Ensure no load errors were reported. |
| EXPECT_TRUE(error_reporter()->GetErrors()->empty()); |
| |
| // The histograms below are not logged for unpacked extensions. |
| if (GetParam() == ExtensionLoadType::PACKED) { |
| tester.ExpectTotalCount(kIndexAndPersistRulesTimeHistogram, |
| 1 /* count */); |
| tester.ExpectBucketCount(kManifestRulesCountHistogram, |
| expected_indexed_rules_count, 1 /* count */); |
| } |
| } |
| |
| void LoadAndExpectError(const std::string& expected_error) { |
| // The error should be prepended with the JSON filename. |
| std::string error_with_filename = base::StringPrintf( |
| "%s: %s", kJSONRulesFilename, expected_error.c_str()); |
| |
| base::HistogramTester tester; |
| WriteExtensionData(); |
| |
| loader_->set_should_fail(true); |
| |
| // Clear all load errors before loading the extension. |
| error_reporter()->ClearErrors(); |
| |
| extension_ = loader_->LoadExtension(extension_dir_); |
| EXPECT_FALSE(extension_.get()); |
| |
| // Verify the error. Only verify if the |expected_error| is a substring of |
| // the actual error, since some string may be prepended/appended while |
| // creating the actual error. |
| const std::vector<base::string16>* errors = error_reporter()->GetErrors(); |
| ASSERT_EQ(1u, errors->size()); |
| EXPECT_NE(base::string16::npos, |
| errors->at(0).find(base::UTF8ToUTF16(error_with_filename))) |
| << "expected: " << error_with_filename << " actual: " << errors->at(0); |
| |
| tester.ExpectTotalCount(kIndexAndPersistRulesTimeHistogram, 0u); |
| tester.ExpectTotalCount(kManifestRulesCountHistogram, 0u); |
| } |
| |
| void set_persist_invalid_json_file() { persist_invalid_json_file_ = true; } |
| |
| void set_persist_initial_indexed_ruleset() { |
| persist_initial_indexed_ruleset_ = true; |
| } |
| |
| ChromeTestExtensionLoader* extension_loader() { return loader_.get(); } |
| |
| const Extension* extension() const { return extension_.get(); } |
| void set_extension(scoped_refptr<const Extension> extension) { |
| extension_ = extension; |
| } |
| |
| private: |
| void WriteExtensionData() { |
| extension_dir_ = |
| temp_dir().GetPath().Append(FILE_PATH_LITERAL("test_extension")); |
| |
| // Create extension directory. |
| EXPECT_TRUE(base::CreateDirectory(extension_dir_)); |
| |
| if (rules_value_) { |
| WriteManifestAndRuleset(extension_dir_, kJSONRulesetFilepath, |
| kJSONRulesFilename, *rules_value_, |
| {} /* hosts */); |
| } else { |
| WriteManifestAndRuleset(extension_dir_, kJSONRulesetFilepath, |
| kJSONRulesFilename, rules_list_, {} /* hosts */); |
| } |
| |
| // Overwrite the JSON rules file with some invalid json. |
| if (persist_invalid_json_file_) { |
| std::string data = "invalid json"; |
| base::WriteFile(extension_dir_.Append(kJSONRulesetFilepath), data.c_str(), |
| data.size()); |
| } |
| |
| if (persist_initial_indexed_ruleset_) { |
| std::string data = "user ruleset"; |
| base::WriteFile(file_util::GetIndexedRulesetPath(extension_dir_), |
| data.c_str(), data.size()); |
| } |
| } |
| |
| LoadErrorReporter* error_reporter() { |
| return LoadErrorReporter::GetInstance(); |
| } |
| |
| std::vector<TestRule> rules_list_; |
| std::unique_ptr<base::Value> rules_value_; |
| base::FilePath extension_dir_; |
| std::unique_ptr<ChromeTestExtensionLoader> loader_; |
| scoped_refptr<const Extension> extension_; |
| bool persist_invalid_json_file_ = false; |
| bool persist_initial_indexed_ruleset_ = false; |
| |
| DISALLOW_COPY_AND_ASSIGN(RuleIndexingTest); |
| }; |
| |
| TEST_P(RuleIndexingTest, DuplicateResourceTypes) { |
| TestRule rule = CreateGenericRule(); |
| rule.condition->resource_types = |
| std::vector<std::string>({"image", "stylesheet"}); |
| rule.condition->excluded_resource_types = std::vector<std::string>({"image"}); |
| AddRule(rule); |
| LoadAndExpectError( |
| ParseInfo(ParseResult::ERROR_RESOURCE_TYPE_DUPLICATED, *rule.id) |
| .GetErrorDescription()); |
| } |
| |
| TEST_P(RuleIndexingTest, EmptyRedirectRulePriority) { |
| TestRule rule = CreateGenericRule(); |
| rule.action->type = std::string("redirect"); |
| rule.action->redirect.emplace(); |
| rule.action->redirect->url = std::string("https://google.com"); |
| AddRule(rule); |
| LoadAndExpectError( |
| ParseInfo(ParseResult::ERROR_EMPTY_REDIRECT_RULE_PRIORITY, *rule.id) |
| .GetErrorDescription()); |
| } |
| |
| TEST_P(RuleIndexingTest, EmptyRedirectRuleUrl) { |
| TestRule rule = CreateGenericRule(); |
| rule.id = kMinValidID; |
| AddRule(rule); |
| |
| rule.id = kMinValidID + 1; |
| rule.action->type = std::string("redirect"); |
| rule.priority = kMinValidPriority; |
| AddRule(rule); |
| |
| LoadAndExpectError(ParseInfo(ParseResult::ERROR_INVALID_REDIRECT, *rule.id) |
| .GetErrorDescription()); |
| } |
| |
| TEST_P(RuleIndexingTest, InvalidRuleID) { |
| TestRule rule = CreateGenericRule(); |
| rule.id = kMinValidID - 1; |
| AddRule(rule); |
| LoadAndExpectError(ParseInfo(ParseResult::ERROR_INVALID_RULE_ID, *rule.id) |
| .GetErrorDescription()); |
| } |
| |
| TEST_P(RuleIndexingTest, InvalidRedirectRulePriority) { |
| TestRule rule = CreateGenericRule(); |
| rule.action->type = std::string("redirect"); |
| rule.action->redirect.emplace(); |
| rule.action->redirect->url = std::string("https://google.com"); |
| rule.priority = kMinValidPriority - 1; |
| AddRule(rule); |
| LoadAndExpectError( |
| ParseInfo(ParseResult::ERROR_INVALID_REDIRECT_RULE_PRIORITY, *rule.id) |
| .GetErrorDescription()); |
| } |
| |
| TEST_P(RuleIndexingTest, NoApplicableResourceTypes) { |
| TestRule rule = CreateGenericRule(); |
| rule.condition->excluded_resource_types = std::vector<std::string>( |
| {"main_frame", "sub_frame", "stylesheet", "script", "image", "font", |
| "object", "xmlhttprequest", "ping", "csp_report", "media", "websocket", |
| "other"}); |
| AddRule(rule); |
| LoadAndExpectError( |
| ParseInfo(ParseResult::ERROR_NO_APPLICABLE_RESOURCE_TYPES, *rule.id) |
| .GetErrorDescription()); |
| } |
| |
| TEST_P(RuleIndexingTest, EmptyDomainsList) { |
| TestRule rule = CreateGenericRule(); |
| rule.condition->domains = std::vector<std::string>(); |
| AddRule(rule); |
| LoadAndExpectError(ParseInfo(ParseResult::ERROR_EMPTY_DOMAINS_LIST, *rule.id) |
| .GetErrorDescription()); |
| } |
| |
| TEST_P(RuleIndexingTest, EmptyResourceTypeList) { |
| TestRule rule = CreateGenericRule(); |
| rule.condition->resource_types = std::vector<std::string>(); |
| AddRule(rule); |
| LoadAndExpectError( |
| ParseInfo(ParseResult::ERROR_EMPTY_RESOURCE_TYPES_LIST, *rule.id) |
| .GetErrorDescription()); |
| } |
| |
| TEST_P(RuleIndexingTest, EmptyURLFilter) { |
| TestRule rule = CreateGenericRule(); |
| rule.condition->url_filter = std::string(); |
| AddRule(rule); |
| LoadAndExpectError(ParseInfo(ParseResult::ERROR_EMPTY_URL_FILTER, *rule.id) |
| .GetErrorDescription()); |
| } |
| |
| TEST_P(RuleIndexingTest, InvalidRedirectURL) { |
| TestRule rule = CreateGenericRule(); |
| rule.action->type = std::string("redirect"); |
| rule.action->redirect.emplace(); |
| rule.action->redirect->url = std::string("google"); |
| rule.priority = kMinValidPriority; |
| AddRule(rule); |
| LoadAndExpectError( |
| ParseInfo(ParseResult::ERROR_INVALID_REDIRECT_URL, *rule.id) |
| .GetErrorDescription()); |
| } |
| |
| TEST_P(RuleIndexingTest, ListNotPassed) { |
| SetRules(std::make_unique<base::DictionaryValue>()); |
| LoadAndExpectError(kErrorListNotPassed); |
| } |
| |
| TEST_P(RuleIndexingTest, DuplicateIDS) { |
| TestRule rule = CreateGenericRule(); |
| AddRule(rule); |
| AddRule(rule); |
| LoadAndExpectError(ParseInfo(ParseResult::ERROR_DUPLICATE_IDS, *rule.id) |
| .GetErrorDescription()); |
| } |
| |
| // Ensure that we limit the number of parse failure warnings shown. |
| TEST_P(RuleIndexingTest, TooManyParseFailures) { |
| const size_t kNumInvalidRules = 10; |
| const size_t kNumValidRules = 6; |
| const size_t kMaxUnparsedRulesWarnings = 5; |
| |
| size_t rule_id = kMinValidID; |
| for (size_t i = 0; i < kNumInvalidRules; i++) { |
| TestRule rule = CreateGenericRule(); |
| rule.id = rule_id++; |
| rule.action->type = std::string("invalid_action_type"); |
| AddRule(rule); |
| } |
| |
| for (size_t i = 0; i < kNumValidRules; i++) { |
| TestRule rule = CreateGenericRule(); |
| rule.id = rule_id++; |
| AddRule(rule); |
| } |
| |
| extension_loader()->set_ignore_manifest_warnings(true); |
| LoadAndExpectSuccess(kNumValidRules); |
| |
| // TODO(crbug.com/879355): CrxInstaller reloads the extension after moving it, |
| // which causes it to lose the install warning. This should be fixed. |
| if (GetParam() != ExtensionLoadType::PACKED) { |
| const std::vector<InstallWarning>& expected_warnings = |
| extension()->install_warnings(); |
| ASSERT_EQ(1u + kMaxUnparsedRulesWarnings, expected_warnings.size()); |
| |
| InstallWarning warning(""); |
| warning.key = manifest_keys::kDeclarativeNetRequestKey; |
| warning.specific = manifest_keys::kDeclarativeRuleResourcesKey; |
| |
| // The initial warnings should correspond to the first |
| // |kMaxUnparsedRulesWarnings| rules, which couldn't be parsed. |
| for (size_t i = 0; i < kMaxUnparsedRulesWarnings; i++) { |
| EXPECT_EQ(expected_warnings[i].key, warning.key); |
| EXPECT_EQ(expected_warnings[i].specific, warning.specific); |
| EXPECT_THAT(expected_warnings[i].message, |
| ::testing::HasSubstr("Parse error")); |
| } |
| |
| warning.message = ErrorUtils::FormatErrorMessage( |
| kTooManyParseFailuresWarning, |
| std::to_string(kMaxUnparsedRulesWarnings)); |
| EXPECT_EQ(warning, expected_warnings[kMaxUnparsedRulesWarnings]); |
| } |
| } |
| |
| // Ensures that rules which can't be parsed are ignored and cause an install |
| // warning. |
| TEST_P(RuleIndexingTest, InvalidJSONRules_StrongTypes) { |
| { |
| TestRule rule = CreateGenericRule(); |
| rule.id = 1; |
| AddRule(rule); |
| } |
| |
| { |
| TestRule rule = CreateGenericRule(); |
| rule.id = 2; |
| rule.action->type = std::string("invalid action"); |
| AddRule(rule); |
| } |
| |
| { |
| TestRule rule = CreateGenericRule(); |
| rule.id = 3; |
| AddRule(rule); |
| } |
| |
| { |
| TestRule rule = CreateGenericRule(); |
| rule.id = 4; |
| rule.condition->domain_type = std::string("invalid_domain_type"); |
| AddRule(rule); |
| } |
| |
| extension_loader()->set_ignore_manifest_warnings(true); |
| LoadAndExpectSuccess(2 /* rules count */); |
| |
| // TODO(crbug.com/879355): CrxInstaller reloads the extension after moving it, |
| // which causes it to lose the install warning. This should be fixed. |
| if (GetParam() != ExtensionLoadType::PACKED) { |
| ASSERT_EQ(2u, extension()->install_warnings().size()); |
| std::vector<InstallWarning> expected_warnings; |
| |
| for (const auto& warning : extension()->install_warnings()) { |
| EXPECT_EQ(extensions::manifest_keys::kDeclarativeNetRequestKey, |
| warning.key); |
| EXPECT_EQ(extensions::manifest_keys::kDeclarativeRuleResourcesKey, |
| warning.specific); |
| EXPECT_THAT(warning.message, ::testing::HasSubstr("Parse error")); |
| } |
| } |
| } |
| |
| // Ensures that rules which can't be parsed are ignored and cause an install |
| // warning. |
| TEST_P(RuleIndexingTest, InvalidJSONRules_Parsed) { |
| const char* kRules = R"( |
| [ |
| { |
| "id" : 1, |
| "condition" : [], |
| "action" : {"type" : "block" } |
| }, |
| { |
| "id" : 2, |
| "condition" : {"urlFilter" : "abc"}, |
| "action" : {"type" : "block" } |
| }, |
| { |
| "id" : 3, |
| "invalidKey" : "invalidKeyValue", |
| "condition" : {"urlFilter" : "example"}, |
| "action" : {"type" : "block" } |
| }, |
| { |
| "id" : "6", |
| "condition" : {"urlFilter" : "google"}, |
| "action" : {"type" : "block" } |
| } |
| ] |
| )"; |
| SetRules(base::JSONReader::ReadDeprecated(kRules)); |
| |
| extension_loader()->set_ignore_manifest_warnings(true); |
| LoadAndExpectSuccess(1 /* rules count */); |
| |
| // TODO(crbug.com/879355): CrxInstaller reloads the extension after moving it, |
| // which causes it to lose the install warning. This should be fixed. |
| if (GetParam() != ExtensionLoadType::PACKED) { |
| ASSERT_EQ(3u, extension()->install_warnings().size()); |
| std::vector<InstallWarning> expected_warnings; |
| |
| expected_warnings.emplace_back( |
| ErrorUtils::FormatErrorMessage( |
| kRuleNotParsedWarning, "id 1", |
| "'condition': expected dictionary, got list"), |
| manifest_keys::kDeclarativeNetRequestKey, |
| manifest_keys::kDeclarativeRuleResourcesKey); |
| expected_warnings.emplace_back( |
| ErrorUtils::FormatErrorMessage(kRuleNotParsedWarning, "id 3", |
| "found unexpected key 'invalidKey'"), |
| manifest_keys::kDeclarativeNetRequestKey, |
| manifest_keys::kDeclarativeRuleResourcesKey); |
| expected_warnings.emplace_back( |
| ErrorUtils::FormatErrorMessage(kRuleNotParsedWarning, "index 4", |
| "'id': expected id, got string"), |
| manifest_keys::kDeclarativeNetRequestKey, |
| manifest_keys::kDeclarativeRuleResourcesKey); |
| EXPECT_EQ(expected_warnings, extension()->install_warnings()); |
| } |
| } |
| |
| // Ensure that we can add up to MAX_NUMBER_OF_RULES. |
| TEST_P(RuleIndexingTest, RuleCountLimitMatched) { |
| namespace dnr_api = extensions::api::declarative_net_request; |
| TestRule rule = CreateGenericRule(); |
| for (int i = 0; i < dnr_api::MAX_NUMBER_OF_RULES; ++i) { |
| rule.id = kMinValidID + i; |
| rule.condition->url_filter = std::to_string(i); |
| AddRule(rule); |
| } |
| LoadAndExpectSuccess(dnr_api::MAX_NUMBER_OF_RULES); |
| } |
| |
| // Ensure that we get an install warning on exceeding the rule count limit. |
| TEST_P(RuleIndexingTest, RuleCountLimitExceeded) { |
| namespace dnr_api = extensions::api::declarative_net_request; |
| TestRule rule = CreateGenericRule(); |
| for (int i = 1; i <= dnr_api::MAX_NUMBER_OF_RULES + 1; ++i) { |
| rule.id = kMinValidID + i; |
| rule.condition->url_filter = std::to_string(i); |
| AddRule(rule); |
| } |
| |
| extension_loader()->set_ignore_manifest_warnings(true); |
| LoadAndExpectSuccess(dnr_api::MAX_NUMBER_OF_RULES); |
| |
| // TODO(crbug.com/879355): CrxInstaller reloads the extension after moving it, |
| // which causes it to lose the install warning. This should be fixed. |
| if (GetParam() != ExtensionLoadType::PACKED) { |
| ASSERT_EQ(1u, extension()->install_warnings().size()); |
| EXPECT_EQ(InstallWarning(kRuleCountExceeded, |
| manifest_keys::kDeclarativeNetRequestKey, |
| manifest_keys::kDeclarativeRuleResourcesKey), |
| extension()->install_warnings()[0]); |
| } |
| } |
| |
| TEST_P(RuleIndexingTest, InvalidJSONFile) { |
| set_persist_invalid_json_file(); |
| // The error is returned by the JSON parser we use. Hence just test an error |
| // is raised. |
| LoadAndExpectError(""); |
| } |
| |
| TEST_P(RuleIndexingTest, EmptyRuleset) { |
| LoadAndExpectSuccess(0 /* rules count */); |
| } |
| |
| TEST_P(RuleIndexingTest, AddSingleRule) { |
| AddRule(CreateGenericRule()); |
| LoadAndExpectSuccess(1 /* rules count */); |
| } |
| |
| TEST_P(RuleIndexingTest, AddTwoRules) { |
| TestRule rule = CreateGenericRule(); |
| AddRule(rule); |
| |
| rule.id = kMinValidID + 1; |
| AddRule(rule); |
| LoadAndExpectSuccess(2 /* rules count */); |
| } |
| |
| // Test that we do not use an extension provided indexed ruleset. |
| TEST_P(RuleIndexingTest, ExtensionWithIndexedRuleset) { |
| set_persist_initial_indexed_ruleset(); |
| AddRule(CreateGenericRule()); |
| LoadAndExpectSuccess(1 /* rules count */); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(, |
| RuleIndexingTest, |
| ::testing::Values(ExtensionLoadType::PACKED, |
| ExtensionLoadType::UNPACKED)); |
| |
| } // namespace |
| } // namespace declarative_net_request |
| } // namespace extensions |