// 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 #include "base/base64.h" #include "base/macros.h" #include "base/metrics/histogram_macros.h" #include "base/timer/timer.h" #include "net/base/load_flags.h" #include "net/http/http_response_headers.h" #include "net/http/http_status_code.h" #include "net/url_request/url_fetcher.h" #include "net/url_request/url_request_context_getter.h" using base::Time; using base::TimeDelta; namespace { // Enumerate parsing failures for histogramming purposes. DO NOT CHANGE // THE ORDERING OF THESE VALUES. enum ParseResultType { // Error parsing the protocol buffer from a string. PARSE_FROM_STRING_ERROR = 0, // A match in the response had an unexpected THREAT_ENTRY_TYPE. UNEXPECTED_THREAT_ENTRY_TYPE_ERROR = 1, // A match in the response had an unexpected THREAT_TYPE. UNEXPECTED_THREAT_TYPE_ERROR = 2, // A match in the response had an unexpected PLATFORM_TYPE. UNEXPECTED_PLATFORM_TYPE_ERROR = 3, // A match in the response contained no metadata where metadata was // expected. NO_METADATA_ERROR = 4, // A match in the response contained a ThreatType that was inconsistent // with the other matches. INCONSISTENT_THREAT_TYPE_ERROR = 5, // Memory space for histograms is determined by the max. ALWAYS // ADD NEW VALUES BEFORE THIS ONE. PARSE_RESULT_TYPE_MAX = 6 }; // Record parsing errors of a GetHash result. void RecordParseGetHashResult(ParseResultType result_type) { UMA_HISTOGRAM_ENUMERATION("SafeBrowsing.ParseV4HashResult", result_type, PARSE_RESULT_TYPE_MAX); } } // namespace namespace safe_browsing { const char kUmaV4HashResponseMetricName[] = "SafeBrowsing.GetV4HashHttpResponseOrErrorCode"; // The default V4GetHashProtocolManagerFactory. class V4GetHashProtocolManagerFactoryImpl : public V4GetHashProtocolManagerFactory { public: V4GetHashProtocolManagerFactoryImpl() {} ~V4GetHashProtocolManagerFactoryImpl() override {} V4GetHashProtocolManager* CreateProtocolManager( net::URLRequestContextGetter* request_context_getter, const V4ProtocolConfig& config) override { return new V4GetHashProtocolManager(request_context_getter, config); } private: DISALLOW_COPY_AND_ASSIGN(V4GetHashProtocolManagerFactoryImpl); }; // V4GetHashProtocolManager implementation -------------------------------- // static V4GetHashProtocolManagerFactory* V4GetHashProtocolManager::factory_ = NULL; // static V4GetHashProtocolManager* V4GetHashProtocolManager::Create( net::URLRequestContextGetter* request_context_getter, const V4ProtocolConfig& config) { if (!factory_) factory_ = new V4GetHashProtocolManagerFactoryImpl(); return factory_->CreateProtocolManager(request_context_getter, config); } void V4GetHashProtocolManager::ResetGetHashErrors() { gethash_error_count_ = 0; gethash_back_off_mult_ = 1; } V4GetHashProtocolManager::V4GetHashProtocolManager( net::URLRequestContextGetter* request_context_getter, const V4ProtocolConfig& config) : gethash_error_count_(0), gethash_back_off_mult_(1), next_gethash_time_(Time::FromDoubleT(0)), config_(config), request_context_getter_(request_context_getter), url_fetcher_id_(0) { } // static void V4GetHashProtocolManager::RecordGetHashResult(ResultType result_type) { UMA_HISTOGRAM_ENUMERATION("SafeBrowsing.GetV4HashResult", result_type, GET_HASH_RESULT_MAX); } V4GetHashProtocolManager::~V4GetHashProtocolManager() { // Delete in-progress SafeBrowsing requests. STLDeleteContainerPairFirstPointers(hash_requests_.begin(), hash_requests_.end()); hash_requests_.clear(); } std::string V4GetHashProtocolManager::GetHashRequest( const std::vector& prefixes, const std::vector& platforms, ThreatType threat_type) { // Build the request. Client info and client states are not added to the // request protocol buffer. Client info is passed as params in the url. FindFullHashesRequest req; ThreatInfo* info = req.mutable_threat_info(); info->add_threat_types(threat_type); info->add_threat_entry_types(URL_EXPRESSION); for (const PlatformType p : platforms) { info->add_platform_types(p); } for (const SBPrefix& prefix : prefixes) { std::string hash(reinterpret_cast(&prefix), sizeof(SBPrefix)); info->add_threat_entries()->set_hash(hash); } // Serialize and Base64 encode. std::string req_data, req_base64; req.SerializeToString(&req_data); base::Base64Encode(req_data, &req_base64); return req_base64; } bool V4GetHashProtocolManager::ParseHashResponse( const std::string& data, std::vector* full_hashes, base::TimeDelta* negative_cache_duration) { FindFullHashesResponse response; if (!response.ParseFromString(data)) { RecordParseGetHashResult(PARSE_FROM_STRING_ERROR); return false; } if (response.has_negative_cache_duration()) { // Seconds resolution is good enough so we ignore the nanos field. *negative_cache_duration = base::TimeDelta::FromSeconds( response.negative_cache_duration().seconds()); } if (response.has_minimum_wait_duration()) { // Seconds resolution is good enough so we ignore the nanos field. next_gethash_time_ = Time::Now() + base::TimeDelta::FromSeconds( response.minimum_wait_duration().seconds()); } // We only expect one threat type per request, so we make sure // the threat types are consistent between matches. ThreatType expected_threat_type = THREAT_TYPE_UNSPECIFIED; // Loop over the threat matches and fill in full_hashes. for (const ThreatMatch& match : response.matches()) { // Make sure the platform and threat entry type match. if (!(match.has_threat_entry_type() && match.threat_entry_type() == URL_EXPRESSION && match.has_threat())) { RecordParseGetHashResult(UNEXPECTED_THREAT_ENTRY_TYPE_ERROR); return false; } if (!match.has_threat_type()) { RecordParseGetHashResult(UNEXPECTED_THREAT_TYPE_ERROR); return false; } if (expected_threat_type == THREAT_TYPE_UNSPECIFIED) { expected_threat_type = match.threat_type(); } else if (match.threat_type() != expected_threat_type) { RecordParseGetHashResult(INCONSISTENT_THREAT_TYPE_ERROR); return false; } // Fill in the full hash. SBFullHashResult result; result.hash = StringToSBFullHash(match.threat().hash()); if (match.has_cache_duration()) { // Seconds resolution is good enough so we ignore the nanos field. result.cache_duration = base::TimeDelta::FromSeconds(match.cache_duration().seconds()); } // Different threat types will handle the metadata differently. if (match.threat_type() == API_ABUSE) { if (match.has_platform_type() && match.platform_type() == CHROME_PLATFORM) { if (match.has_threat_entry_metadata()) { // For API Abuse, store a list of the returned permissions. for (const ThreatEntryMetadata::MetadataEntry& m : match.threat_entry_metadata().entries()) { if (m.key() == "permission") { result.metadata.api_permissions.push_back(m.value()); } } } else { RecordParseGetHashResult(NO_METADATA_ERROR); return false; } } else { RecordParseGetHashResult(UNEXPECTED_PLATFORM_TYPE_ERROR); return false; } } else { RecordParseGetHashResult(UNEXPECTED_THREAT_TYPE_ERROR); return false; } full_hashes->push_back(result); } return true; } void V4GetHashProtocolManager::GetFullHashes( const std::vector& prefixes, const std::vector& platforms, ThreatType threat_type, FullHashCallback callback) { DCHECK(CalledOnValidThread()); // We need to wait the minimum waiting duration, and if we are in backoff, // we need to check if we're past the next allowed time. If we are, we can // proceed with the request. If not, we are required to return empty results // (i.e. treat the page as safe). if (Time::Now() <= next_gethash_time_) { if (gethash_error_count_) { RecordGetHashResult(GET_HASH_BACKOFF_ERROR); } else { RecordGetHashResult(GET_HASH_MIN_WAIT_DURATION_ERROR); } std::vector full_hashes; callback.Run(full_hashes, base::TimeDelta()); return; } std::string req_base64 = GetHashRequest(prefixes, platforms, threat_type); GURL gethash_url = GetHashUrl(req_base64); net::URLFetcher* fetcher = net::URLFetcher::Create(url_fetcher_id_++, gethash_url, net::URLFetcher::GET, this) .release(); hash_requests_[fetcher] = callback; fetcher->SetLoadFlags(net::LOAD_DISABLE_CACHE); fetcher->SetRequestContext(request_context_getter_.get()); fetcher->Start(); } void V4GetHashProtocolManager::GetFullHashesWithApis( const std::vector& prefixes, FullHashCallback callback) { std::vector platform = {CHROME_PLATFORM}; GetFullHashes(prefixes, platform, API_ABUSE, callback); } // net::URLFetcherDelegate implementation ---------------------------------- // SafeBrowsing request responses are handled here. void V4GetHashProtocolManager::OnURLFetchComplete( const net::URLFetcher* source) { DCHECK(CalledOnValidThread()); HashRequests::iterator it = hash_requests_.find(source); DCHECK(it != hash_requests_.end()) << "Request not found"; // FindFullHashes response. // Reset the scoped pointer so the fetcher gets destroyed properly. scoped_ptr fetcher(it->first); int response_code = source->GetResponseCode(); net::URLRequestStatus status = source->GetStatus(); V4ProtocolManagerUtil::RecordHttpResponseOrErrorCode( kUmaV4HashResponseMetricName, status, response_code); const FullHashCallback& callback = it->second; std::vector full_hashes; base::TimeDelta negative_cache_duration; if (status.is_success() && response_code == net::HTTP_OK) { RecordGetHashResult(GET_HASH_STATUS_200); ResetGetHashErrors(); std::string data; source->GetResponseAsString(&data); if (!ParseHashResponse(data, &full_hashes, &negative_cache_duration)) { full_hashes.clear(); RecordGetHashResult(GET_HASH_PARSE_ERROR); } } else { HandleGetHashError(Time::Now()); DVLOG(1) << "SafeBrowsing GetEncodedFullHashes request for: " << source->GetURL() << " failed with error: " << status.error() << " and response code: " << response_code; if (status.status() == net::URLRequestStatus::FAILED) { RecordGetHashResult(GET_HASH_NETWORK_ERROR); } else { RecordGetHashResult(GET_HASH_HTTP_ERROR); } } // Invoke the callback with full_hashes, even if there was a parse error or // an error response code (in which case full_hashes will be empty). The // caller can't be blocked indefinitely. callback.Run(full_hashes, negative_cache_duration); hash_requests_.erase(it); } void V4GetHashProtocolManager::HandleGetHashError(const Time& now) { DCHECK(CalledOnValidThread()); base::TimeDelta next = V4ProtocolManagerUtil::GetNextBackOffInterval( &gethash_error_count_, &gethash_back_off_mult_); next_gethash_time_ = now + next; } GURL V4GetHashProtocolManager::GetHashUrl(const std::string& req_base64) const { return V4ProtocolManagerUtil::GetRequestUrl(req_base64, "encodedFullHashes", config_); } } // namespace safe_browsing