OLD | NEW |
| (Empty) |
1 // Copyright 2016 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 #include "chrome/browser/safe_browsing/v4_protocol_manager.h" | |
6 | |
7 #include <utility> | |
8 | |
9 #include "base/base64.h" | |
10 #include "base/logging.h" | |
11 #include "base/macros.h" | |
12 #include "base/metrics/histogram_macros.h" | |
13 #include "base/metrics/sparse_histogram.h" | |
14 #include "base/rand_util.h" | |
15 #include "base/stl_util.h" | |
16 #include "base/strings/string_util.h" | |
17 #include "base/strings/stringprintf.h" | |
18 #include "base/timer/timer.h" | |
19 #include "net/base/escape.h" | |
20 #include "net/base/load_flags.h" | |
21 #include "net/base/net_errors.h" | |
22 #include "net/http/http_response_headers.h" | |
23 #include "net/http/http_status_code.h" | |
24 #include "net/url_request/url_fetcher.h" | |
25 #include "net/url_request/url_request_context_getter.h" | |
26 #include "net/url_request/url_request_status.h" | |
27 | |
28 using base::Time; | |
29 using base::TimeDelta; | |
30 | |
31 namespace { | |
32 | |
33 // Enumerate parsing failures for histogramming purposes. DO NOT CHANGE | |
34 // THE ORDERING OF THESE VALUES. | |
35 enum ParseResultType { | |
36 // Error parsing the protocol buffer from a string. | |
37 PARSE_FROM_STRING_ERROR = 0, | |
38 | |
39 // A match in the response had an unexpected THREAT_ENTRY_TYPE. | |
40 UNEXPECTED_THREAT_ENTRY_TYPE_ERROR = 1, | |
41 | |
42 // A match in the response had an unexpected THREAT_TYPE. | |
43 UNEXPECTED_THREAT_TYPE_ERROR = 2, | |
44 | |
45 // A match in the response had an unexpected PLATFORM_TYPE. | |
46 UNEXPECTED_PLATFORM_TYPE_ERROR = 3, | |
47 | |
48 // A match in the response contained no metadata where metadata was | |
49 // expected. | |
50 NO_METADATA_ERROR = 4, | |
51 | |
52 // A match in the response contained a ThreatType that was inconsistent | |
53 // with the other matches. | |
54 INCONSISTENT_THREAT_TYPE_ERROR = 5, | |
55 | |
56 // Memory space for histograms is determined by the max. ALWAYS | |
57 // ADD NEW VALUES BEFORE THIS ONE. | |
58 PARSE_GET_HASH_RESULT_MAX = 6 | |
59 }; | |
60 | |
61 // Record parsing errors of a GetHash result. | |
62 void RecordParseGetHashResult(ParseResultType result_type) { | |
63 UMA_HISTOGRAM_ENUMERATION("SafeBrowsing.ParseV4HashResult", result_type, | |
64 PARSE_GET_HASH_RESULT_MAX); | |
65 } | |
66 | |
67 } // namespace | |
68 | |
69 namespace safe_browsing { | |
70 | |
71 const char kUmaV4ResponseMetricName[] = | |
72 "SafeBrowsing.GetV4HashHttpResponseOrErrorCode"; | |
73 | |
74 // The URL prefix where browser fetches hashes from the server. | |
75 const char kSbV4UrlPrefix[] = "https://safebrowsing.googleapis.com/v4"; | |
76 | |
77 // The default SBProtocolManagerFactory. | |
78 class V4ProtocolManagerFactoryImpl : public V4ProtocolManagerFactory { | |
79 public: | |
80 V4ProtocolManagerFactoryImpl() {} | |
81 ~V4ProtocolManagerFactoryImpl() override {} | |
82 V4ProtocolManager* CreateProtocolManager( | |
83 net::URLRequestContextGetter* request_context_getter, | |
84 const V4ProtocolConfig& config) override { | |
85 return new V4ProtocolManager(request_context_getter, config); | |
86 } | |
87 | |
88 private: | |
89 DISALLOW_COPY_AND_ASSIGN(V4ProtocolManagerFactoryImpl); | |
90 }; | |
91 | |
92 // V4ProtocolManager implementation -------------------------------- | |
93 | |
94 // static | |
95 V4ProtocolManagerFactory* V4ProtocolManager::factory_ = NULL; | |
96 | |
97 // static | |
98 V4ProtocolManager* V4ProtocolManager::Create( | |
99 net::URLRequestContextGetter* request_context_getter, | |
100 const V4ProtocolConfig& config) { | |
101 if (!factory_) | |
102 factory_ = new V4ProtocolManagerFactoryImpl(); | |
103 return factory_->CreateProtocolManager(request_context_getter, config); | |
104 } | |
105 | |
106 // static | |
107 // Backoff interval is MIN(((2^(n-1))*15 minutes) * (RAND + 1), 24 hours) where | |
108 // n is the number of consecutive errors. | |
109 base::TimeDelta V4ProtocolManager::GetNextBackOffInterval(size_t* error_count, | |
110 size_t* multiplier) { | |
111 DCHECK(multiplier && error_count); | |
112 (*error_count)++; | |
113 if (*error_count > 1 && *error_count < 9) { | |
114 // With error count 9 and above we will hit the 24 hour max interval. | |
115 // Cap the multiplier here to prevent integer overflow errors. | |
116 *multiplier *= 2; | |
117 } | |
118 base::TimeDelta next = | |
119 base::TimeDelta::FromMinutes(*multiplier * (1 + base::RandDouble()) * 15); | |
120 | |
121 base::TimeDelta day = base::TimeDelta::FromHours(24); | |
122 | |
123 if (next < day) | |
124 return next; | |
125 else | |
126 return day; | |
127 } | |
128 | |
129 void V4ProtocolManager::ResetGetHashErrors() { | |
130 gethash_error_count_ = 0; | |
131 gethash_back_off_mult_ = 1; | |
132 } | |
133 | |
134 V4ProtocolManager::V4ProtocolManager( | |
135 net::URLRequestContextGetter* request_context_getter, | |
136 const V4ProtocolConfig& config) | |
137 : gethash_error_count_(0), | |
138 gethash_back_off_mult_(1), | |
139 next_gethash_time_(Time::FromDoubleT(0)), | |
140 version_(config.version), | |
141 client_name_(config.client_name), | |
142 key_param_(config.key_param), | |
143 request_context_getter_(request_context_getter), | |
144 url_fetcher_id_(0) { | |
145 DCHECK(!version_.empty()); | |
146 } | |
147 | |
148 // static | |
149 void V4ProtocolManager::RecordGetHashResult(ResultType result_type) { | |
150 UMA_HISTOGRAM_ENUMERATION("SafeBrowsing.GetV4HashResult", result_type, | |
151 GET_HASH_RESULT_MAX); | |
152 } | |
153 | |
154 void V4ProtocolManager::RecordHttpResponseOrErrorCode( | |
155 const char* metric_name, | |
156 const net::URLRequestStatus& status, | |
157 int response_code) { | |
158 UMA_HISTOGRAM_SPARSE_SLOWLY( | |
159 metric_name, status.is_success() ? response_code : status.error()); | |
160 } | |
161 | |
162 V4ProtocolManager::~V4ProtocolManager() { | |
163 // Delete in-progress SafeBrowsing requests. | |
164 STLDeleteContainerPairFirstPointers(hash_requests_.begin(), | |
165 hash_requests_.end()); | |
166 hash_requests_.clear(); | |
167 } | |
168 | |
169 std::string V4ProtocolManager::GetHashRequest( | |
170 const std::vector<SBPrefix>& prefixes, | |
171 const std::vector<PlatformType>& platforms, | |
172 ThreatType threat_type) { | |
173 // Build the request. Client info and client states are not added to the | |
174 // request protocol buffer. Client info is passed as params in the url. | |
175 FindFullHashesRequest req; | |
176 ThreatInfo* info = req.mutable_threat_info(); | |
177 info->add_threat_types(threat_type); | |
178 info->add_threat_entry_types(URL_EXPRESSION); | |
179 for (const PlatformType p : platforms) { | |
180 info->add_platform_types(p); | |
181 } | |
182 for (const SBPrefix& prefix : prefixes) { | |
183 std::string hash(reinterpret_cast<const char*>(&prefix), sizeof(SBPrefix)); | |
184 info->add_threat_entries()->set_hash(hash); | |
185 } | |
186 | |
187 // Serialize and Base64 encode. | |
188 std::string req_data, req_base64; | |
189 req.SerializeToString(&req_data); | |
190 base::Base64Encode(req_data, &req_base64); | |
191 | |
192 return req_base64; | |
193 } | |
194 | |
195 bool V4ProtocolManager::ParseHashResponse( | |
196 const std::string& data, | |
197 std::vector<SBFullHashResult>* full_hashes, | |
198 base::TimeDelta* negative_cache_duration) { | |
199 FindFullHashesResponse response; | |
200 | |
201 if (!response.ParseFromString(data)) { | |
202 RecordParseGetHashResult(PARSE_FROM_STRING_ERROR); | |
203 return false; | |
204 } | |
205 | |
206 if (response.has_negative_cache_duration()) { | |
207 // Seconds resolution is good enough so we ignore the nanos field. | |
208 *negative_cache_duration = base::TimeDelta::FromSeconds( | |
209 response.negative_cache_duration().seconds()); | |
210 } | |
211 | |
212 if (response.has_minimum_wait_duration()) { | |
213 // Seconds resolution is good enough so we ignore the nanos field. | |
214 next_gethash_time_ = | |
215 Time::Now() + base::TimeDelta::FromSeconds( | |
216 response.minimum_wait_duration().seconds()); | |
217 } | |
218 | |
219 // We only expect one threat type per request, so we make sure | |
220 // the threat types are consistent between matches. | |
221 ThreatType expected_threat_type = THREAT_TYPE_UNSPECIFIED; | |
222 | |
223 // Loop over the threat matches and fill in full_hashes. | |
224 for (const ThreatMatch& match : response.matches()) { | |
225 // Make sure the platform and threat entry type match. | |
226 if (!(match.has_threat_entry_type() && | |
227 match.threat_entry_type() == URL_EXPRESSION && match.has_threat())) { | |
228 RecordParseGetHashResult(UNEXPECTED_THREAT_ENTRY_TYPE_ERROR); | |
229 return false; | |
230 } | |
231 | |
232 if (!match.has_threat_type()) { | |
233 RecordParseGetHashResult(UNEXPECTED_THREAT_TYPE_ERROR); | |
234 return false; | |
235 } | |
236 | |
237 if (expected_threat_type == THREAT_TYPE_UNSPECIFIED) { | |
238 expected_threat_type = match.threat_type(); | |
239 } else if (match.threat_type() != expected_threat_type) { | |
240 RecordParseGetHashResult(INCONSISTENT_THREAT_TYPE_ERROR); | |
241 return false; | |
242 } | |
243 | |
244 // Fill in the full hash. | |
245 SBFullHashResult result; | |
246 result.hash = StringToSBFullHash(match.threat().hash()); | |
247 | |
248 if (match.has_cache_duration()) { | |
249 // Seconds resolution is good enough so we ignore the nanos field. | |
250 result.cache_duration = | |
251 base::TimeDelta::FromSeconds(match.cache_duration().seconds()); | |
252 } | |
253 | |
254 // Different threat types will handle the metadata differently. | |
255 if (match.threat_type() == API_ABUSE) { | |
256 if (match.has_platform_type() && | |
257 match.platform_type() == CHROME_PLATFORM) { | |
258 if (match.has_threat_entry_metadata()) { | |
259 // For API Abuse, store a csv of the returned permissions. | |
260 for (const ThreatEntryMetadata::MetadataEntry& m : | |
261 match.threat_entry_metadata().entries()) { | |
262 if (m.key() == "permission") { | |
263 result.metadata += m.value() + ","; | |
264 } | |
265 } | |
266 } else { | |
267 RecordParseGetHashResult(NO_METADATA_ERROR); | |
268 return false; | |
269 } | |
270 } else { | |
271 RecordParseGetHashResult(UNEXPECTED_PLATFORM_TYPE_ERROR); | |
272 return false; | |
273 } | |
274 } else { | |
275 RecordParseGetHashResult(UNEXPECTED_THREAT_TYPE_ERROR); | |
276 return false; | |
277 } | |
278 | |
279 full_hashes->push_back(result); | |
280 } | |
281 return true; | |
282 } | |
283 | |
284 void V4ProtocolManager::GetFullHashes( | |
285 const std::vector<SBPrefix>& prefixes, | |
286 const std::vector<PlatformType>& platforms, | |
287 ThreatType threat_type, | |
288 FullHashCallback callback) { | |
289 DCHECK(CalledOnValidThread()); | |
290 // We need to wait the minimum waiting duration, and if we are in backoff, | |
291 // we need to check if we're past the next allowed time. If we are, we can | |
292 // proceed with the request. If not, we are required to return empty results | |
293 // (i.e. treat the page as safe). | |
294 if (Time::Now() <= next_gethash_time_) { | |
295 if (gethash_error_count_) { | |
296 RecordGetHashResult(GET_HASH_BACKOFF_ERROR); | |
297 } else { | |
298 RecordGetHashResult(GET_HASH_MIN_WAIT_DURATION_ERROR); | |
299 } | |
300 std::vector<SBFullHashResult> full_hashes; | |
301 callback.Run(full_hashes, base::TimeDelta()); | |
302 return; | |
303 } | |
304 | |
305 std::string req_base64 = GetHashRequest(prefixes, platforms, threat_type); | |
306 GURL gethash_url = GetHashUrl(req_base64); | |
307 | |
308 net::URLFetcher* fetcher = | |
309 net::URLFetcher::Create(url_fetcher_id_++, gethash_url, | |
310 net::URLFetcher::GET, this) | |
311 .release(); | |
312 hash_requests_[fetcher] = callback; | |
313 | |
314 fetcher->SetLoadFlags(net::LOAD_DISABLE_CACHE); | |
315 fetcher->SetRequestContext(request_context_getter_.get()); | |
316 fetcher->Start(); | |
317 } | |
318 | |
319 void V4ProtocolManager::GetFullHashesWithApis( | |
320 const std::vector<SBPrefix>& prefixes, | |
321 FullHashCallback callback) { | |
322 std::vector<PlatformType> platform = {CHROME_PLATFORM}; | |
323 GetFullHashes(prefixes, platform, API_ABUSE, callback); | |
324 } | |
325 | |
326 // net::URLFetcherDelegate implementation ---------------------------------- | |
327 | |
328 // SafeBrowsing request responses are handled here. | |
329 void V4ProtocolManager::OnURLFetchComplete(const net::URLFetcher* source) { | |
330 DCHECK(CalledOnValidThread()); | |
331 | |
332 HashRequests::iterator it = hash_requests_.find(source); | |
333 DCHECK(it != hash_requests_.end()) << "Request not found"; | |
334 | |
335 // FindFullHashes response. | |
336 // Reset the scoped pointer so the fetcher gets destroyed properly. | |
337 scoped_ptr<const net::URLFetcher> fetcher(it->first); | |
338 | |
339 int response_code = source->GetResponseCode(); | |
340 net::URLRequestStatus status = source->GetStatus(); | |
341 RecordHttpResponseOrErrorCode(kUmaV4ResponseMetricName, status, | |
342 response_code); | |
343 | |
344 const FullHashCallback& callback = it->second; | |
345 std::vector<SBFullHashResult> full_hashes; | |
346 base::TimeDelta negative_cache_duration; | |
347 if (status.is_success() && response_code == net::HTTP_OK) { | |
348 RecordGetHashResult(GET_HASH_STATUS_200); | |
349 ResetGetHashErrors(); | |
350 std::string data; | |
351 source->GetResponseAsString(&data); | |
352 if (!ParseHashResponse(data, &full_hashes, &negative_cache_duration)) { | |
353 full_hashes.clear(); | |
354 RecordGetHashResult(GET_HASH_PARSE_ERROR); | |
355 } | |
356 } else { | |
357 HandleGetHashError(Time::Now()); | |
358 | |
359 DVLOG(1) << "SafeBrowsing GetEncodedFullHashes request for: " | |
360 << source->GetURL() << " failed with error: " << status.error() | |
361 << " and response code: " << response_code; | |
362 | |
363 if (status.status() == net::URLRequestStatus::FAILED) { | |
364 RecordGetHashResult(GET_HASH_NETWORK_ERROR); | |
365 } else { | |
366 RecordGetHashResult(GET_HASH_HTTP_ERROR); | |
367 } | |
368 } | |
369 | |
370 // Invoke the callback with full_hashes, even if there was a parse error or | |
371 // an error response code (in which case full_hashes will be empty). The | |
372 // caller can't be blocked indefinitely. | |
373 callback.Run(full_hashes, negative_cache_duration); | |
374 | |
375 hash_requests_.erase(it); | |
376 } | |
377 | |
378 void V4ProtocolManager::HandleGetHashError(const Time& now) { | |
379 DCHECK(CalledOnValidThread()); | |
380 base::TimeDelta next = GetNextBackOffInterval(&gethash_error_count_, | |
381 &gethash_back_off_mult_); | |
382 next_gethash_time_ = now + next; | |
383 } | |
384 | |
385 // The API hash call uses the pver4 Safe Browsing server. | |
386 GURL V4ProtocolManager::GetHashUrl(const std::string& request_base64) const { | |
387 std::string url = ComposePver4Url(kSbV4UrlPrefix, "encodedFullHashes", | |
388 request_base64, client_name_, version_, key_param_); | |
389 return GURL(url); | |
390 } | |
391 | |
392 // static | |
393 std::string V4ProtocolManager::ComposePver4Url(const std::string& prefix, | |
394 const std::string& method, | |
395 const std::string& request_base64, | |
396 const std::string& client_id, | |
397 const std::string& version, | |
398 const std::string& key_param) { | |
399 DCHECK(!prefix.empty() && !method.empty() && | |
400 !client_id.empty() && !version.empty()); | |
401 std::string url = base::StringPrintf( | |
402 "%s/%s/%s?alt=proto&client_id=%s&client_version=%s", | |
403 prefix.c_str(), method.c_str(), request_base64.c_str(), | |
404 client_id.c_str(), version.c_str()); | |
405 if (!key_param.empty()) { | |
406 base::StringAppendF(&url, "&key=%s", | |
407 net::EscapeQueryParamValue(key_param, true).c_str()); | |
408 } | |
409 return url; | |
410 } | |
411 | |
412 } // namespace safe_browsing | |
OLD | NEW |