| // Copyright 2016 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 "components/safe_browsing_db/v4_get_hash_protocol_manager.h" |
| |
| #include <memory> |
| #include <vector> |
| |
| #include "base/base64.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/run_loop.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/test/simple_test_clock.h" |
| #include "base/time/time.h" |
| #include "components/safe_browsing_db/safebrowsing.pb.h" |
| #include "components/safe_browsing_db/util.h" |
| #include "components/safe_browsing_db/v4_test_util.h" |
| #include "content/public/test/test_browser_thread_bundle.h" |
| #include "net/base/escape.h" |
| #include "net/base/load_flags.h" |
| #include "net/base/net_errors.h" |
| #include "net/url_request/test_url_fetcher_factory.h" |
| #include "testing/platform_test.h" |
| |
| using base::Time; |
| using base::TimeDelta; |
| |
| namespace safe_browsing { |
| |
| namespace { |
| |
| struct KeyValue { |
| std::string key; |
| std::string value; |
| |
| explicit KeyValue(const std::string key, const std::string value) |
| : key(key), value(value){}; |
| explicit KeyValue(const KeyValue& other) = default; |
| |
| private: |
| KeyValue(); |
| }; |
| |
| struct ResponseInfo { |
| FullHash full_hash; |
| ListIdentifier list_id; |
| std::vector<KeyValue> key_values; |
| |
| explicit ResponseInfo(FullHash full_hash, ListIdentifier list_id) |
| : full_hash(full_hash), list_id(list_id){}; |
| explicit ResponseInfo(const ResponseInfo& other) |
| : full_hash(other.full_hash), |
| list_id(other.list_id), |
| key_values(other.key_values){}; |
| |
| private: |
| ResponseInfo(); |
| }; |
| |
| } // namespace |
| |
| class V4GetHashProtocolManagerTest : public PlatformTest { |
| public: |
| void SetUp() override { |
| PlatformTest::SetUp(); |
| callback_called_ = false; |
| } |
| |
| std::unique_ptr<V4GetHashProtocolManager> CreateProtocolManager() { |
| StoresToCheck stores_to_check( |
| {GetUrlMalwareId(), GetChromeUrlApiId(), |
| ListIdentifier(CHROME_PLATFORM, URL, SOCIAL_ENGINEERING_PUBLIC), |
| ListIdentifier(CHROME_PLATFORM, URL, |
| POTENTIALLY_HARMFUL_APPLICATION)}); |
| return V4GetHashProtocolManager::Create(NULL, stores_to_check, |
| GetTestV4ProtocolConfig()); |
| } |
| |
| static void SetupFetcherToReturnOKResponse( |
| const net::TestURLFetcherFactory& factory, |
| const std::vector<ResponseInfo>& infos) { |
| net::TestURLFetcher* fetcher = factory.GetFetcherByID(0); |
| DCHECK(fetcher); |
| fetcher->set_status(net::URLRequestStatus()); |
| fetcher->set_response_code(200); |
| fetcher->SetResponseString(GetV4HashResponse(infos)); |
| fetcher->delegate()->OnURLFetchComplete(fetcher); |
| } |
| |
| static std::vector<ResponseInfo> GetStockV4HashResponseInfos() { |
| ResponseInfo info(FullHash("Everything's shiny, Cap'n."), |
| GetChromeUrlApiId()); |
| info.key_values.emplace_back("permission", "NOTIFICATIONS"); |
| std::vector<ResponseInfo> infos; |
| infos.push_back(info); |
| return infos; |
| } |
| |
| static std::string GetStockV4HashResponse() { |
| return GetV4HashResponse(GetStockV4HashResponseInfos()); |
| } |
| |
| void SetTestClock(base::Time now, V4GetHashProtocolManager* pm) { |
| base::SimpleTestClock* clock = new base::SimpleTestClock(); |
| clock->SetNow(now); |
| pm->SetClockForTests(base::WrapUnique(clock)); |
| } |
| |
| void ValidateGetV4ApiResults(const ThreatMetadata& expected_md, |
| const ThreatMetadata& actual_md) { |
| EXPECT_EQ(expected_md, actual_md); |
| callback_called_ = true; |
| } |
| |
| void ValidateGetV4HashResults( |
| const std::vector<FullHashInfo>& expected_results, |
| const std::vector<FullHashInfo>& actual_results) { |
| EXPECT_EQ(expected_results.size(), actual_results.size()); |
| for (size_t i = 0; i < actual_results.size(); i++) { |
| EXPECT_TRUE(expected_results[i] == actual_results[i]); |
| } |
| callback_called_ = true; |
| } |
| |
| bool callback_called() { return callback_called_; } |
| |
| private: |
| static std::string GetV4HashResponse( |
| std::vector<ResponseInfo> response_infos) { |
| FindFullHashesResponse res; |
| res.mutable_negative_cache_duration()->set_seconds(600); |
| for (const ResponseInfo& info : response_infos) { |
| ThreatMatch* m = res.add_matches(); |
| m->set_platform_type(info.list_id.platform_type()); |
| m->set_threat_entry_type(info.list_id.threat_entry_type()); |
| m->set_threat_type(info.list_id.threat_type()); |
| m->mutable_cache_duration()->set_seconds(300); |
| m->mutable_threat()->set_hash(info.full_hash); |
| |
| for (const KeyValue& key_value : info.key_values) { |
| ThreatEntryMetadata::MetadataEntry* e = |
| m->mutable_threat_entry_metadata()->add_entries(); |
| e->set_key(key_value.key); |
| e->set_value(key_value.value); |
| } |
| } |
| |
| // Serialize. |
| std::string res_data; |
| res.SerializeToString(&res_data); |
| |
| return res_data; |
| } |
| |
| bool callback_called_; |
| content::TestBrowserThreadBundle thread_bundle_; |
| }; |
| |
| TEST_F(V4GetHashProtocolManagerTest, TestGetHashErrorHandlingNetwork) { |
| net::TestURLFetcherFactory factory; |
| std::unique_ptr<V4GetHashProtocolManager> pm(CreateProtocolManager()); |
| |
| FullHashToStoreAndHashPrefixesMap matched_locally; |
| matched_locally[FullHash("AHashFull")].emplace_back(GetUrlSocEngId(), |
| HashPrefix("AHash")); |
| std::vector<FullHashInfo> expected_results; |
| pm->GetFullHashes( |
| matched_locally, |
| base::Bind(&V4GetHashProtocolManagerTest::ValidateGetV4HashResults, |
| base::Unretained(this), expected_results)); |
| |
| net::TestURLFetcher* fetcher = factory.GetFetcherByID(0); |
| DCHECK(fetcher); |
| // Failed request status should result in error. |
| fetcher->set_status(net::URLRequestStatus(net::URLRequestStatus::FAILED, |
| net::ERR_CONNECTION_RESET)); |
| fetcher->set_response_code(200); |
| fetcher->SetResponseString(GetStockV4HashResponse()); |
| fetcher->delegate()->OnURLFetchComplete(fetcher); |
| |
| // Should have recorded one error, but back off multiplier is unchanged. |
| EXPECT_EQ(1ul, pm->gethash_error_count_); |
| EXPECT_EQ(1ul, pm->gethash_back_off_mult_); |
| EXPECT_TRUE(callback_called()); |
| } |
| |
| TEST_F(V4GetHashProtocolManagerTest, TestGetHashErrorHandlingResponseCode) { |
| net::TestURLFetcherFactory factory; |
| std::unique_ptr<V4GetHashProtocolManager> pm(CreateProtocolManager()); |
| |
| FullHashToStoreAndHashPrefixesMap matched_locally; |
| matched_locally[FullHash("AHashFull")].emplace_back(GetUrlSocEngId(), |
| HashPrefix("AHash")); |
| std::vector<FullHashInfo> expected_results; |
| pm->GetFullHashes( |
| matched_locally, |
| base::Bind(&V4GetHashProtocolManagerTest::ValidateGetV4HashResults, |
| base::Unretained(this), expected_results)); |
| |
| net::TestURLFetcher* fetcher = factory.GetFetcherByID(0); |
| DCHECK(fetcher); |
| fetcher->set_status(net::URLRequestStatus()); |
| // Response code of anything other than 200 should result in error. |
| fetcher->set_response_code(204); |
| fetcher->SetResponseString(GetStockV4HashResponse()); |
| fetcher->delegate()->OnURLFetchComplete(fetcher); |
| |
| // Should have recorded one error, but back off multiplier is unchanged. |
| EXPECT_EQ(1ul, pm->gethash_error_count_); |
| EXPECT_EQ(1ul, pm->gethash_back_off_mult_); |
| EXPECT_TRUE(callback_called()); |
| } |
| |
| TEST_F(V4GetHashProtocolManagerTest, TestGetHashErrorHandlingOK) { |
| net::TestURLFetcherFactory factory; |
| std::unique_ptr<V4GetHashProtocolManager> pm(CreateProtocolManager()); |
| |
| base::Time now = base::Time::UnixEpoch(); |
| SetTestClock(now, pm.get()); |
| |
| HashPrefix prefix("Everything"); |
| FullHash full_hash("Everything's shiny, Cap'n."); |
| FullHashToStoreAndHashPrefixesMap matched_locally; |
| matched_locally[full_hash].push_back( |
| StoreAndHashPrefix(GetChromeUrlApiId(), prefix)); |
| std::vector<FullHashInfo> expected_results; |
| FullHashInfo fhi(full_hash, GetChromeUrlApiId(), |
| now + base::TimeDelta::FromSeconds(300)); |
| fhi.metadata.api_permissions.insert("NOTIFICATIONS"); |
| expected_results.push_back(fhi); |
| |
| pm->GetFullHashes( |
| matched_locally, |
| base::Bind(&V4GetHashProtocolManagerTest::ValidateGetV4HashResults, |
| base::Unretained(this), expected_results)); |
| |
| SetupFetcherToReturnOKResponse(factory, GetStockV4HashResponseInfos()); |
| |
| // No error, back off multiplier is unchanged. |
| EXPECT_EQ(0ul, pm->gethash_error_count_); |
| EXPECT_EQ(1ul, pm->gethash_back_off_mult_); |
| |
| // Verify the state of the cache. |
| const FullHashCache* cache = pm->full_hash_cache_for_tests(); |
| // Check the cache. |
| ASSERT_EQ(1u, cache->size()); |
| EXPECT_EQ(1u, cache->count(prefix)); |
| const CachedHashPrefixInfo& cached_result = cache->at(prefix); |
| EXPECT_EQ(cached_result.negative_expiry, |
| now + base::TimeDelta::FromSeconds(600)); |
| ASSERT_EQ(1u, cached_result.full_hash_infos.size()); |
| EXPECT_EQ(FullHash("Everything's shiny, Cap'n."), |
| cached_result.full_hash_infos[0].full_hash); |
| EXPECT_TRUE(callback_called()); |
| } |
| |
| TEST_F(V4GetHashProtocolManagerTest, |
| TestResultsNotCachedForNegativeCacheDuration) { |
| net::TestURLFetcherFactory factory; |
| std::unique_ptr<V4GetHashProtocolManager> pm(CreateProtocolManager()); |
| |
| HashPrefix prefix("Everything"); |
| std::vector<HashPrefix> prefixes_requested({prefix}); |
| base::Time negative_cache_expire; |
| FullHash full_hash("Everything's shiny, Cap'n."); |
| std::vector<FullHashInfo> fhis; |
| fhis.emplace_back(full_hash, GetChromeUrlApiId(), base::Time::UnixEpoch()); |
| |
| pm->UpdateCache(prefixes_requested, fhis, negative_cache_expire); |
| |
| // Verify the state of the cache. |
| const FullHashCache* cache = pm->full_hash_cache_for_tests(); |
| // Check the cache. |
| EXPECT_EQ(0u, cache->size()); |
| } |
| |
| TEST_F(V4GetHashProtocolManagerTest, TestGetHashRequest) { |
| FindFullHashesRequest req; |
| ThreatInfo* info = req.mutable_threat_info(); |
| for (const PlatformType& p : |
| std::set<PlatformType>{GetCurrentPlatformType(), CHROME_PLATFORM}) { |
| info->add_platform_types(p); |
| } |
| |
| info->add_threat_entry_types(URL); |
| |
| for (const ThreatType& tt : |
| std::set<ThreatType>{MALWARE_THREAT, SOCIAL_ENGINEERING_PUBLIC, |
| POTENTIALLY_HARMFUL_APPLICATION, API_ABUSE}) { |
| info->add_threat_types(tt); |
| } |
| |
| HashPrefix one = "hashone"; |
| HashPrefix two = "hashtwo"; |
| info->add_threat_entries()->set_hash(one); |
| info->add_threat_entries()->set_hash(two); |
| |
| std::unique_ptr<V4GetHashProtocolManager> pm(CreateProtocolManager()); |
| req.mutable_client()->set_client_id(pm->config_.client_name); |
| req.mutable_client()->set_client_version(pm->config_.version); |
| |
| // Serialize and Base64 encode. |
| std::string req_data, req_base64; |
| req.SerializeToString(&req_data); |
| base::Base64Encode(req_data, &req_base64); |
| |
| std::vector<HashPrefix> prefixes_to_request = {one, two}; |
| EXPECT_EQ(req_base64, pm->GetHashRequest(prefixes_to_request)); |
| } |
| |
| TEST_F(V4GetHashProtocolManagerTest, TestParseHashResponse) { |
| std::unique_ptr<V4GetHashProtocolManager> pm(CreateProtocolManager()); |
| |
| base::Time now = base::Time::UnixEpoch(); |
| SetTestClock(now, pm.get()); |
| |
| FullHash full_hash("Everything's shiny, Cap'n."); |
| FindFullHashesResponse res; |
| res.mutable_negative_cache_duration()->set_seconds(600); |
| res.mutable_minimum_wait_duration()->set_seconds(400); |
| ThreatMatch* m = res.add_matches(); |
| m->set_threat_type(API_ABUSE); |
| m->set_platform_type(CHROME_PLATFORM); |
| m->set_threat_entry_type(URL); |
| m->mutable_cache_duration()->set_seconds(300); |
| m->mutable_threat()->set_hash(full_hash); |
| ThreatEntryMetadata::MetadataEntry* e = |
| m->mutable_threat_entry_metadata()->add_entries(); |
| e->set_key("permission"); |
| e->set_value("NOTIFICATIONS"); |
| // Add another ThreatMatch for a list we don't track. This response should |
| // get dropped. |
| m = res.add_matches(); |
| m->set_threat_type(THREAT_TYPE_UNSPECIFIED); |
| m->set_platform_type(CHROME_PLATFORM); |
| m->set_threat_entry_type(URL); |
| m->mutable_cache_duration()->set_seconds(300); |
| m->mutable_threat()->set_hash(full_hash); |
| |
| // Serialize. |
| std::string res_data; |
| res.SerializeToString(&res_data); |
| |
| std::vector<FullHashInfo> full_hash_infos; |
| base::Time cache_expire; |
| EXPECT_TRUE(pm->ParseHashResponse(res_data, &full_hash_infos, &cache_expire)); |
| |
| EXPECT_EQ(now + base::TimeDelta::FromSeconds(600), cache_expire); |
| // Even though the server responded with two ThreatMatch responses, one |
| // should have been dropped. |
| ASSERT_EQ(1ul, full_hash_infos.size()); |
| const FullHashInfo& fhi = full_hash_infos[0]; |
| EXPECT_EQ(full_hash, fhi.full_hash); |
| EXPECT_EQ(GetChromeUrlApiId(), fhi.list_id); |
| EXPECT_EQ(1ul, fhi.metadata.api_permissions.size()); |
| EXPECT_EQ(1ul, fhi.metadata.api_permissions.count("NOTIFICATIONS")); |
| EXPECT_EQ(now + base::TimeDelta::FromSeconds(300), fhi.positive_expiry); |
| EXPECT_EQ(now + base::TimeDelta::FromSeconds(400), pm->next_gethash_time_); |
| } |
| |
| // Adds an entry with an ignored ThreatEntryType. |
| TEST_F(V4GetHashProtocolManagerTest, |
| TestParseHashResponseWrongThreatEntryType) { |
| std::unique_ptr<V4GetHashProtocolManager> pm(CreateProtocolManager()); |
| |
| base::Time now = base::Time::UnixEpoch(); |
| SetTestClock(now, pm.get()); |
| |
| FindFullHashesResponse res; |
| res.mutable_negative_cache_duration()->set_seconds(600); |
| res.add_matches()->set_threat_entry_type(EXECUTABLE); |
| |
| // Serialize. |
| std::string res_data; |
| res.SerializeToString(&res_data); |
| |
| std::vector<FullHashInfo> full_hash_infos; |
| base::Time cache_expire; |
| EXPECT_FALSE( |
| pm->ParseHashResponse(res_data, &full_hash_infos, &cache_expire)); |
| |
| EXPECT_EQ(now + base::TimeDelta::FromSeconds(600), cache_expire); |
| // There should be no hash results. |
| EXPECT_EQ(0ul, full_hash_infos.size()); |
| } |
| |
| // Adds entries with a ThreatPatternType metadata. |
| TEST_F(V4GetHashProtocolManagerTest, TestParseHashThreatPatternType) { |
| std::unique_ptr<V4GetHashProtocolManager> pm(CreateProtocolManager()); |
| |
| base::Time now = base::Time::UnixEpoch(); |
| SetTestClock(now, pm.get()); |
| |
| { |
| // Test social engineering pattern type. |
| FindFullHashesResponse se_res; |
| se_res.mutable_negative_cache_duration()->set_seconds(600); |
| ThreatMatch* se = se_res.add_matches(); |
| se->set_threat_type(SOCIAL_ENGINEERING_PUBLIC); |
| se->set_platform_type(CHROME_PLATFORM); |
| se->set_threat_entry_type(URL); |
| FullHash full_hash("Everything's shiny, Cap'n."); |
| se->mutable_threat()->set_hash(full_hash); |
| ThreatEntryMetadata::MetadataEntry* se_meta = |
| se->mutable_threat_entry_metadata()->add_entries(); |
| se_meta->set_key("se_pattern_type"); |
| se_meta->set_value("SOCIAL_ENGINEERING_LANDING"); |
| |
| std::string se_data; |
| se_res.SerializeToString(&se_data); |
| |
| std::vector<FullHashInfo> full_hash_infos; |
| base::Time cache_expire; |
| EXPECT_TRUE( |
| pm->ParseHashResponse(se_data, &full_hash_infos, &cache_expire)); |
| EXPECT_EQ(now + base::TimeDelta::FromSeconds(600), cache_expire); |
| |
| // Ensure that the threat remains valid since we found a full hash match, |
| // even though the metadata information could not be parsed correctly. |
| ASSERT_EQ(1ul, full_hash_infos.size()); |
| const FullHashInfo& fhi = full_hash_infos[0]; |
| EXPECT_EQ(full_hash, fhi.full_hash); |
| const ListIdentifier list_id(CHROME_PLATFORM, URL, |
| SOCIAL_ENGINEERING_PUBLIC); |
| EXPECT_EQ(list_id, fhi.list_id); |
| EXPECT_EQ(ThreatPatternType::SOCIAL_ENGINEERING_LANDING, |
| fhi.metadata.threat_pattern_type); |
| } |
| |
| { |
| // Test potentially harmful application pattern type. |
| FindFullHashesResponse pha_res; |
| pha_res.mutable_negative_cache_duration()->set_seconds(600); |
| ThreatMatch* pha = pha_res.add_matches(); |
| pha->set_threat_type(POTENTIALLY_HARMFUL_APPLICATION); |
| pha->set_threat_entry_type(URL); |
| pha->set_platform_type(CHROME_PLATFORM); |
| FullHash full_hash("Not to fret."); |
| pha->mutable_threat()->set_hash(full_hash); |
| ThreatEntryMetadata::MetadataEntry* pha_meta = |
| pha->mutable_threat_entry_metadata()->add_entries(); |
| pha_meta->set_key("pha_pattern_type"); |
| pha_meta->set_value("LANDING"); |
| |
| std::string pha_data; |
| pha_res.SerializeToString(&pha_data); |
| std::vector<FullHashInfo> full_hash_infos; |
| base::Time cache_expire; |
| EXPECT_TRUE( |
| pm->ParseHashResponse(pha_data, &full_hash_infos, &cache_expire)); |
| EXPECT_EQ(now + base::TimeDelta::FromSeconds(600), cache_expire); |
| |
| ASSERT_EQ(1ul, full_hash_infos.size()); |
| const FullHashInfo& fhi = full_hash_infos[0]; |
| EXPECT_EQ(full_hash, fhi.full_hash); |
| const ListIdentifier list_id(CHROME_PLATFORM, URL, |
| POTENTIALLY_HARMFUL_APPLICATION); |
| EXPECT_EQ(list_id, fhi.list_id); |
| EXPECT_EQ(ThreatPatternType::MALWARE_LANDING, |
| fhi.metadata.threat_pattern_type); |
| } |
| |
| { |
| // Test invalid pattern type. |
| FullHash full_hash("Not to fret."); |
| FindFullHashesResponse invalid_res; |
| invalid_res.mutable_negative_cache_duration()->set_seconds(600); |
| ThreatMatch* invalid = invalid_res.add_matches(); |
| invalid->set_threat_type(POTENTIALLY_HARMFUL_APPLICATION); |
| invalid->set_threat_entry_type(URL); |
| invalid->set_platform_type(CHROME_PLATFORM); |
| invalid->mutable_threat()->set_hash(full_hash); |
| ThreatEntryMetadata::MetadataEntry* invalid_meta = |
| invalid->mutable_threat_entry_metadata()->add_entries(); |
| invalid_meta->set_key("pha_pattern_type"); |
| invalid_meta->set_value("INVALIDE_VALUE"); |
| |
| std::string invalid_data; |
| invalid_res.SerializeToString(&invalid_data); |
| std::vector<FullHashInfo> full_hash_infos; |
| base::Time cache_expire; |
| EXPECT_TRUE( |
| pm->ParseHashResponse(invalid_data, &full_hash_infos, &cache_expire)); |
| |
| // Ensure that the threat remains valid since we found a full hash match, |
| // even though the metadata information could not be parsed correctly. |
| ASSERT_EQ(1ul, full_hash_infos.size()); |
| const auto& fhi = full_hash_infos[0]; |
| EXPECT_EQ(full_hash, fhi.full_hash); |
| EXPECT_EQ( |
| ListIdentifier(CHROME_PLATFORM, URL, POTENTIALLY_HARMFUL_APPLICATION), |
| fhi.list_id); |
| EXPECT_EQ(ThreatPatternType::NONE, fhi.metadata.threat_pattern_type); |
| } |
| } |
| |
| // Adds metadata with a key value that is not "permission". |
| TEST_F(V4GetHashProtocolManagerTest, |
| TestParseHashResponseNonPermissionMetadata) { |
| std::unique_ptr<V4GetHashProtocolManager> pm(CreateProtocolManager()); |
| |
| base::Time now = base::Time::UnixEpoch(); |
| SetTestClock(now, pm.get()); |
| |
| FullHash full_hash("Not to fret."); |
| FindFullHashesResponse res; |
| res.mutable_negative_cache_duration()->set_seconds(600); |
| ThreatMatch* m = res.add_matches(); |
| m->set_threat_type(API_ABUSE); |
| m->set_platform_type(CHROME_PLATFORM); |
| m->set_threat_entry_type(URL); |
| m->mutable_threat()->set_hash(full_hash); |
| ThreatEntryMetadata::MetadataEntry* e = |
| m->mutable_threat_entry_metadata()->add_entries(); |
| e->set_key("notpermission"); |
| e->set_value("NOTGEOLOCATION"); |
| |
| // Serialize. |
| std::string res_data; |
| res.SerializeToString(&res_data); |
| |
| std::vector<FullHashInfo> full_hash_infos; |
| base::Time cache_expire; |
| EXPECT_TRUE(pm->ParseHashResponse(res_data, &full_hash_infos, &cache_expire)); |
| |
| EXPECT_EQ(now + base::TimeDelta::FromSeconds(600), cache_expire); |
| ASSERT_EQ(1ul, full_hash_infos.size()); |
| const auto& fhi = full_hash_infos[0]; |
| EXPECT_EQ(full_hash, fhi.full_hash); |
| EXPECT_EQ(GetChromeUrlApiId(), fhi.list_id); |
| EXPECT_TRUE(fhi.metadata.api_permissions.empty()); |
| } |
| |
| TEST_F(V4GetHashProtocolManagerTest, |
| TestParseHashResponseInconsistentThreatTypes) { |
| std::unique_ptr<V4GetHashProtocolManager> pm(CreateProtocolManager()); |
| |
| FindFullHashesResponse res; |
| res.mutable_negative_cache_duration()->set_seconds(600); |
| ThreatMatch* m1 = res.add_matches(); |
| m1->set_threat_type(API_ABUSE); |
| m1->set_platform_type(CHROME_PLATFORM); |
| m1->set_threat_entry_type(URL); |
| m1->mutable_threat()->set_hash(FullHash("Everything's shiny, Cap'n.")); |
| m1->mutable_threat_entry_metadata()->add_entries(); |
| ThreatMatch* m2 = res.add_matches(); |
| m2->set_threat_type(MALWARE_THREAT); |
| m2->set_threat_entry_type(URL); |
| m2->mutable_threat()->set_hash(FullHash("Not to fret.")); |
| |
| // Serialize. |
| std::string res_data; |
| res.SerializeToString(&res_data); |
| |
| std::vector<FullHashInfo> full_hash_infos; |
| base::Time cache_expire; |
| EXPECT_FALSE( |
| pm->ParseHashResponse(res_data, &full_hash_infos, &cache_expire)); |
| } |
| |
| // Checks that results are looked up correctly in the cache. |
| TEST_F(V4GetHashProtocolManagerTest, GetCachedResults) { |
| base::Time now = base::Time::UnixEpoch(); |
| FullHash full_hash("example"); |
| HashPrefix prefix("exam"); |
| FullHashToStoreAndHashPrefixesMap matched_locally; |
| matched_locally[full_hash].emplace_back(GetUrlMalwareId(), prefix); |
| std::unique_ptr<V4GetHashProtocolManager> pm(CreateProtocolManager()); |
| FullHashCache* cache = pm->full_hash_cache_for_tests(); |
| |
| { |
| std::vector<HashPrefix> prefixes_to_request; |
| std::vector<FullHashInfo> cached_full_hash_infos; |
| cache->clear(); |
| |
| // Test with an empty cache. (Case: 2) |
| pm->GetFullHashCachedResults(matched_locally, now, &prefixes_to_request, |
| &cached_full_hash_infos); |
| EXPECT_TRUE(cache->empty()); |
| ASSERT_EQ(1ul, prefixes_to_request.size()); |
| EXPECT_EQ(prefix, prefixes_to_request[0]); |
| EXPECT_TRUE(cached_full_hash_infos.empty()); |
| } |
| |
| { |
| std::vector<HashPrefix> prefixes_to_request; |
| std::vector<FullHashInfo> cached_full_hash_infos; |
| cache->clear(); |
| |
| // Prefix has a cache entry but full hash is not there. (Case: 1-b-i) |
| CachedHashPrefixInfo* entry = &(*cache)[prefix]; |
| entry->negative_expiry = now + base::TimeDelta::FromMinutes(5); |
| pm->GetFullHashCachedResults(matched_locally, now, &prefixes_to_request, |
| &cached_full_hash_infos); |
| EXPECT_TRUE(prefixes_to_request.empty()); |
| EXPECT_TRUE(cached_full_hash_infos.empty()); |
| } |
| |
| { |
| std::vector<HashPrefix> prefixes_to_request; |
| std::vector<FullHashInfo> cached_full_hash_infos; |
| cache->clear(); |
| |
| // Expired negative cache entry. (Case: 1-b-ii) |
| CachedHashPrefixInfo* entry = &(*cache)[prefix]; |
| entry->negative_expiry = now - base::TimeDelta::FromMinutes(5); |
| pm->GetFullHashCachedResults(matched_locally, now, &prefixes_to_request, |
| &cached_full_hash_infos); |
| ASSERT_EQ(1ul, prefixes_to_request.size()); |
| EXPECT_EQ(prefix, prefixes_to_request[0]); |
| EXPECT_TRUE(cached_full_hash_infos.empty()); |
| } |
| |
| { |
| std::vector<HashPrefix> prefixes_to_request; |
| std::vector<FullHashInfo> cached_full_hash_infos; |
| cache->clear(); |
| |
| // Now put unexpired full hash in the cache. (Case: 1-a-i) |
| CachedHashPrefixInfo* entry = &(*cache)[prefix]; |
| entry->negative_expiry = now + base::TimeDelta::FromMinutes(5); |
| entry->full_hash_infos.emplace_back(full_hash, GetUrlMalwareId(), |
| now + base::TimeDelta::FromMinutes(3)); |
| pm->GetFullHashCachedResults(matched_locally, now, &prefixes_to_request, |
| &cached_full_hash_infos); |
| EXPECT_TRUE(prefixes_to_request.empty()); |
| ASSERT_EQ(1ul, cached_full_hash_infos.size()); |
| EXPECT_EQ(full_hash, cached_full_hash_infos[0].full_hash); |
| } |
| |
| { |
| std::vector<HashPrefix> prefixes_to_request; |
| std::vector<FullHashInfo> cached_full_hash_infos; |
| cache->clear(); |
| |
| // Expire the full hash in the cache. (Case: 1-a-ii) |
| CachedHashPrefixInfo* entry = &(*cache)[prefix]; |
| entry->negative_expiry = now + base::TimeDelta::FromMinutes(5); |
| entry->full_hash_infos.emplace_back(full_hash, GetUrlMalwareId(), |
| now - base::TimeDelta::FromMinutes(3)); |
| pm->GetFullHashCachedResults(matched_locally, now, &prefixes_to_request, |
| &cached_full_hash_infos); |
| ASSERT_EQ(1ul, prefixes_to_request.size()); |
| EXPECT_EQ(prefix, prefixes_to_request[0]); |
| EXPECT_TRUE(cached_full_hash_infos.empty()); |
| } |
| } |
| |
| TEST_F(V4GetHashProtocolManagerTest, TestUpdatesAreMerged) { |
| // We'll add one of the requested FullHashInfo objects into the cache, and |
| // inject the other one as a response from the server. The result should |
| // include both FullHashInfo objects. |
| |
| net::TestURLFetcherFactory factory; |
| std::unique_ptr<V4GetHashProtocolManager> pm(CreateProtocolManager()); |
| HashPrefix prefix_1("exam"); |
| FullHash full_hash_1("example"); |
| HashPrefix prefix_2("Everything"); |
| FullHash full_hash_2("Everything's shiny, Cap'n."); |
| |
| base::Time now = Time::Now(); |
| SetTestClock(now, pm.get()); |
| |
| FullHashCache* cache = pm->full_hash_cache_for_tests(); |
| CachedHashPrefixInfo* entry = &(*cache)[prefix_1]; |
| entry->negative_expiry = now + base::TimeDelta::FromMinutes(100); |
| // Put one unexpired full hash in the cache for a store we'll look in. |
| entry->full_hash_infos.emplace_back(full_hash_1, GetUrlMalwareId(), |
| now + base::TimeDelta::FromSeconds(200)); |
| // Put one unexpired full hash in the cache for a store we'll not look in. |
| entry->full_hash_infos.emplace_back(full_hash_1, GetUrlSocEngId(), |
| now + base::TimeDelta::FromSeconds(200)); |
| |
| // Request full hash information from two stores. |
| FullHashToStoreAndHashPrefixesMap matched_locally; |
| matched_locally[full_hash_1].push_back( |
| StoreAndHashPrefix(GetUrlMalwareId(), prefix_1)); |
| matched_locally[full_hash_2].push_back( |
| StoreAndHashPrefix(GetChromeUrlApiId(), prefix_2)); |
| |
| // Expect full hash information from both stores. |
| std::vector<FullHashInfo> expected_results; |
| expected_results.emplace_back(full_hash_1, GetUrlMalwareId(), |
| now + base::TimeDelta::FromSeconds(200)); |
| expected_results.emplace_back(full_hash_2, GetChromeUrlApiId(), |
| now + base::TimeDelta::FromSeconds(300)); |
| expected_results[1].metadata.api_permissions.insert("NOTIFICATIONS"); |
| |
| pm->GetFullHashes( |
| matched_locally, |
| base::Bind(&V4GetHashProtocolManagerTest::ValidateGetV4HashResults, |
| base::Unretained(this), expected_results)); |
| |
| SetupFetcherToReturnOKResponse(factory, GetStockV4HashResponseInfos()); |
| |
| // No error, back off multiplier is unchanged. |
| EXPECT_EQ(0ul, pm->gethash_error_count_); |
| EXPECT_EQ(1ul, pm->gethash_back_off_mult_); |
| |
| // Verify the state of the cache. |
| ASSERT_EQ(2u, cache->size()); |
| const CachedHashPrefixInfo& cached_result_1 = cache->at(prefix_1); |
| EXPECT_EQ(cached_result_1.negative_expiry, |
| now + base::TimeDelta::FromMinutes(100)); |
| ASSERT_EQ(2u, cached_result_1.full_hash_infos.size()); |
| EXPECT_EQ(full_hash_1, cached_result_1.full_hash_infos[0].full_hash); |
| EXPECT_EQ(GetUrlMalwareId(), cached_result_1.full_hash_infos[0].list_id); |
| |
| const CachedHashPrefixInfo& cached_result_2 = cache->at(prefix_2); |
| EXPECT_EQ(cached_result_2.negative_expiry, |
| now + base::TimeDelta::FromSeconds(600)); |
| ASSERT_EQ(1u, cached_result_2.full_hash_infos.size()); |
| EXPECT_EQ(full_hash_2, cached_result_2.full_hash_infos[0].full_hash); |
| EXPECT_EQ(GetChromeUrlApiId(), cached_result_2.full_hash_infos[0].list_id); |
| EXPECT_TRUE(callback_called()); |
| } |
| |
| // The server responds back with full hash information containing metadata |
| // information for one of the full hashes for the URL in test. |
| TEST_F(V4GetHashProtocolManagerTest, TestGetFullHashesWithApisMergesMetadata) { |
| net::TestURLFetcherFactory factory; |
| const GURL url("https://www.example.com/more"); |
| ThreatMetadata expected_md; |
| expected_md.api_permissions.insert("NOTIFICATIONS"); |
| expected_md.api_permissions.insert("AUDIO_CAPTURE"); |
| std::unique_ptr<V4GetHashProtocolManager> pm(CreateProtocolManager()); |
| pm->GetFullHashesWithApis( |
| url, base::Bind(&V4GetHashProtocolManagerTest::ValidateGetV4ApiResults, |
| base::Unretained(this), expected_md)); |
| |
| // The following two random looking strings value are two of the full hashes |
| // produced by UrlToFullHashes in v4_protocol_manager_util.h for the URL: |
| // "https://www.example.com" |
| std::vector<ResponseInfo> infos; |
| FullHash full_hash; |
| base::Base64Decode("1ZzJ0/7NjPkg6t0DAS8L5Jf7jA48Pn7opQcP4UXYeXc=", |
| &full_hash); |
| ResponseInfo info(full_hash, GetChromeUrlApiId()); |
| info.key_values.emplace_back("permission", "NOTIFICATIONS"); |
| infos.push_back(info); |
| |
| base::Base64Decode("c9mG4AkGXxgsELy2pF2z1u2pSY-JMGVK8mU_ipOM2AE=", |
| &full_hash); |
| info = ResponseInfo(full_hash, GetChromeUrlApiId()); |
| info.key_values.emplace_back("permission", "AUDIO_CAPTURE"); |
| infos.push_back(info); |
| |
| full_hash = FullHash("Everything's shiny, Cap'n."); |
| info = ResponseInfo(full_hash, GetChromeUrlApiId()); |
| info.key_values.emplace_back("permission", "GEOLOCATION"); |
| infos.push_back(info); |
| SetupFetcherToReturnOKResponse(factory, infos); |
| |
| EXPECT_TRUE(callback_called()); |
| } |
| |
| } // namespace safe_browsing |