Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright 2016 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 #include "components/safe_browsing_db/v4_get_hash_protocol_manager.h" | 5 #include "components/safe_browsing_db/v4_get_hash_protocol_manager.h" |
| 6 | 6 |
| 7 #include <utility> | 7 #include <utility> |
| 8 | 8 |
| 9 #include "base/base64url.h" | 9 #include "base/base64url.h" |
| 10 #include "base/macros.h" | 10 #include "base/macros.h" |
| (...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 45 INCONSISTENT_THREAT_TYPE_ERROR = 5, | 45 INCONSISTENT_THREAT_TYPE_ERROR = 5, |
| 46 | 46 |
| 47 // A match in the response contained a metadata, but the metadata is invalid. | 47 // A match in the response contained a metadata, but the metadata is invalid. |
| 48 UNEXPECTED_METADATA_VALUE_ERROR = 6, | 48 UNEXPECTED_METADATA_VALUE_ERROR = 6, |
| 49 | 49 |
| 50 // Memory space for histograms is determined by the max. ALWAYS | 50 // Memory space for histograms is determined by the max. ALWAYS |
| 51 // ADD NEW VALUES BEFORE THIS ONE. | 51 // ADD NEW VALUES BEFORE THIS ONE. |
| 52 PARSE_RESULT_TYPE_MAX = 7, | 52 PARSE_RESULT_TYPE_MAX = 7, |
| 53 }; | 53 }; |
| 54 | 54 |
| 55 // Enumerate full hash cache hits/misses for histogramming purposes. | |
| 56 // DO NOT CHANGE THE ORDERING OF THESE VALUES. | |
| 57 enum V4FullHashCacheResultType { | |
| 58 // Full hashes for which there is no cache hit. | |
| 59 FULL_HASH_CACHE_MISS = 0, | |
| 60 | |
| 61 // Full hashes with a cache hit. | |
| 62 FULL_HASH_CACHE_HIT = 1, | |
| 63 | |
| 64 // Full hashes with a negative cache hit. | |
| 65 FULL_HASH_NEGATIVE_CACHE_HIT = 2, | |
| 66 | |
| 67 // Memory space for histograms is determined by the max. ALWAYS | |
| 68 // ADD NEW VALUES BEFORE THIS ONE. | |
| 69 FULL_HASH_CACHE_RESULT_MAX | |
| 70 }; | |
| 71 | |
| 55 // Record parsing errors of a GetHash result. | 72 // Record parsing errors of a GetHash result. |
| 56 void RecordParseGetHashResult(ParseResultType result_type) { | 73 void RecordParseGetHashResult(ParseResultType result_type) { |
| 57 UMA_HISTOGRAM_ENUMERATION("SafeBrowsing.ParseV4HashResult", result_type, | 74 UMA_HISTOGRAM_ENUMERATION("SafeBrowsing.ParseV4HashResult", result_type, |
| 58 PARSE_RESULT_TYPE_MAX); | 75 PARSE_RESULT_TYPE_MAX); |
| 59 } | 76 } |
| 60 | 77 |
| 61 // Record a GetHash result. | 78 // Record a GetHash result. |
| 62 void RecordGetHashResult(safe_browsing::V4OperationResult result) { | 79 void RecordGetHashResult(safe_browsing::V4OperationResult result) { |
| 63 UMA_HISTOGRAM_ENUMERATION( | 80 UMA_HISTOGRAM_ENUMERATION( |
| 64 "SafeBrowsing.GetV4HashResult", result, | 81 "SafeBrowsing.GetV4HashResult", result, |
| 65 safe_browsing::V4OperationResult::OPERATION_RESULT_MAX); | 82 safe_browsing::V4OperationResult::OPERATION_RESULT_MAX); |
| 66 } | 83 } |
| 67 | 84 |
| 85 // Record a full hash cache hit result. | |
| 86 void RecordV4FullHashCacheResult( | |
| 87 V4FullHashCacheResultType result_type) { | |
| 88 UMA_HISTOGRAM_ENUMERATION("SafeBrowsing.V4FullHashCacheResult", result_type, | |
| 89 FULL_HASH_CACHE_RESULT_MAX); | |
| 90 } | |
| 91 | |
| 68 } // namespace | 92 } // namespace |
| 69 | 93 |
| 70 namespace safe_browsing { | 94 namespace safe_browsing { |
| 71 | 95 |
| 72 const char kUmaV4HashResponseMetricName[] = | 96 const char kUmaV4HashResponseMetricName[] = |
| 73 "SafeBrowsing.GetV4HashHttpResponseOrErrorCode"; | 97 "SafeBrowsing.GetV4HashHttpResponseOrErrorCode"; |
| 74 | 98 |
| 75 // The default V4GetHashProtocolManagerFactory. | 99 // The default V4GetHashProtocolManagerFactory. |
| 76 class V4GetHashProtocolManagerFactoryImpl | 100 class V4GetHashProtocolManagerFactoryImpl |
| 77 : public V4GetHashProtocolManagerFactory { | 101 : public V4GetHashProtocolManagerFactory { |
| (...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 113 : gethash_error_count_(0), | 137 : gethash_error_count_(0), |
| 114 gethash_back_off_mult_(1), | 138 gethash_back_off_mult_(1), |
| 115 next_gethash_time_(Time::FromDoubleT(0)), | 139 next_gethash_time_(Time::FromDoubleT(0)), |
| 116 config_(config), | 140 config_(config), |
| 117 request_context_getter_(request_context_getter), | 141 request_context_getter_(request_context_getter), |
| 118 url_fetcher_id_(0), | 142 url_fetcher_id_(0), |
| 119 clock_(new base::DefaultClock()) {} | 143 clock_(new base::DefaultClock()) {} |
| 120 | 144 |
| 121 V4GetHashProtocolManager::~V4GetHashProtocolManager() { | 145 V4GetHashProtocolManager::~V4GetHashProtocolManager() { |
| 122 // Delete in-progress SafeBrowsing requests. | 146 // Delete in-progress SafeBrowsing requests. |
| 123 STLDeleteContainerPairFirstPointers(hash_requests_.begin(), | 147 STLDeleteContainerPairFirstPointers(pending_hash_requests_.begin(), |
| 124 hash_requests_.end()); | 148 pending_hash_requests_.end()); |
| 125 hash_requests_.clear(); | 149 pending_hash_requests_.clear(); |
| 126 } | 150 } |
| 127 | 151 |
| 128 // static | 152 // static |
| 129 void V4GetHashProtocolManager::RegisterFactory( | 153 void V4GetHashProtocolManager::RegisterFactory( |
| 130 std::unique_ptr<V4GetHashProtocolManagerFactory> factory) { | 154 std::unique_ptr<V4GetHashProtocolManagerFactory> factory) { |
| 131 if (factory_) | 155 if (factory_) |
| 132 delete factory_; | 156 delete factory_; |
| 133 factory_ = factory.release(); | 157 factory_ = factory.release(); |
| 134 } | 158 } |
| 135 | 159 |
| (...skipping 69 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 205 if (expected_threat_type == THREAT_TYPE_UNSPECIFIED) { | 229 if (expected_threat_type == THREAT_TYPE_UNSPECIFIED) { |
| 206 expected_threat_type = match.threat_type(); | 230 expected_threat_type = match.threat_type(); |
| 207 } else if (match.threat_type() != expected_threat_type) { | 231 } else if (match.threat_type() != expected_threat_type) { |
| 208 RecordParseGetHashResult(INCONSISTENT_THREAT_TYPE_ERROR); | 232 RecordParseGetHashResult(INCONSISTENT_THREAT_TYPE_ERROR); |
| 209 return false; | 233 return false; |
| 210 } | 234 } |
| 211 | 235 |
| 212 // Fill in the full hash. | 236 // Fill in the full hash. |
| 213 SBFullHashResult result; | 237 SBFullHashResult result; |
| 214 result.hash = StringToSBFullHash(match.threat().hash()); | 238 result.hash = StringToSBFullHash(match.threat().hash()); |
| 215 | |
| 216 if (match.has_cache_duration()) { | 239 if (match.has_cache_duration()) { |
| 217 // Seconds resolution is good enough so we ignore the nanos field. | 240 // Seconds resolution is good enough so we ignore the nanos field. |
| 218 result.cache_expire_after = | 241 result.cache_expire_after = |
| 219 clock_->Now() + | 242 clock_->Now() + |
| 220 base::TimeDelta::FromSeconds(match.cache_duration().seconds()); | 243 base::TimeDelta::FromSeconds(match.cache_duration().seconds()); |
| 221 } else { | 244 } else { |
| 222 result.cache_expire_after = clock_->Now(); | 245 result.cache_expire_after = clock_->Now(); |
| 223 } | 246 } |
| 224 | 247 |
| 225 // Different threat types will handle the metadata differently. | 248 // Different threat types will handle the metadata differently. |
| (...skipping 95 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 321 std::string req_base64 = GetHashRequest(prefixes, platforms, threat_type); | 344 std::string req_base64 = GetHashRequest(prefixes, platforms, threat_type); |
| 322 GURL gethash_url; | 345 GURL gethash_url; |
| 323 net::HttpRequestHeaders headers; | 346 net::HttpRequestHeaders headers; |
| 324 GetHashUrlAndHeaders(req_base64, &gethash_url, &headers); | 347 GetHashUrlAndHeaders(req_base64, &gethash_url, &headers); |
| 325 | 348 |
| 326 net::URLFetcher* fetcher = | 349 net::URLFetcher* fetcher = |
| 327 net::URLFetcher::Create(url_fetcher_id_++, gethash_url, | 350 net::URLFetcher::Create(url_fetcher_id_++, gethash_url, |
| 328 net::URLFetcher::GET, this) | 351 net::URLFetcher::GET, this) |
| 329 .release(); | 352 .release(); |
| 330 fetcher->SetExtraRequestHeaders(headers.ToString()); | 353 fetcher->SetExtraRequestHeaders(headers.ToString()); |
| 331 hash_requests_[fetcher] = callback; | 354 pending_hash_requests_[fetcher] = std::make_pair(prefixes, callback); |
| 332 | 355 |
| 333 fetcher->SetLoadFlags(net::LOAD_DISABLE_CACHE); | 356 fetcher->SetLoadFlags(net::LOAD_DISABLE_CACHE); |
| 334 fetcher->SetRequestContext(request_context_getter_.get()); | 357 fetcher->SetRequestContext(request_context_getter_.get()); |
| 335 fetcher->Start(); | 358 fetcher->Start(); |
| 336 } | 359 } |
| 337 | 360 |
| 338 void V4GetHashProtocolManager::GetFullHashesWithApis( | 361 void V4GetHashProtocolManager::GetFullHashesWithApis( |
| 339 const std::vector<SBPrefix>& prefixes, | 362 const std::vector<SBPrefix>& prefixes, |
| 340 FullHashCallback callback) { | 363 FullHashCallback callback) { |
| 341 std::vector<PlatformType> platform = {CHROME_PLATFORM}; | 364 std::vector<PlatformType> platform = {CHROME_PLATFORM}; |
| 342 GetFullHashes(prefixes, platform, API_ABUSE, callback); | 365 GetFullHashes(prefixes, platform, API_ABUSE, callback); |
| 343 } | 366 } |
| 344 | 367 |
| 345 void V4GetHashProtocolManager::SetClockForTests( | 368 void V4GetHashProtocolManager::SetClockForTests( |
| 346 std::unique_ptr<base::Clock> clock) { | 369 std::unique_ptr<base::Clock> clock) { |
| 347 clock_ = std::move(clock); | 370 clock_ = std::move(clock); |
| 348 } | 371 } |
| 349 | 372 |
| 350 // net::URLFetcherDelegate implementation ---------------------------------- | 373 // net::URLFetcherDelegate implementation ---------------------------------- |
| 351 | 374 |
| 352 // SafeBrowsing request responses are handled here. | 375 // SafeBrowsing request responses are handled here. |
| 353 void V4GetHashProtocolManager::OnURLFetchComplete( | 376 void V4GetHashProtocolManager::OnURLFetchComplete( |
| 354 const net::URLFetcher* source) { | 377 const net::URLFetcher* source) { |
| 355 DCHECK(CalledOnValidThread()); | 378 DCHECK(CalledOnValidThread()); |
| 356 | 379 |
| 357 HashRequests::iterator it = hash_requests_.find(source); | 380 PendingHashRequests::iterator it = pending_hash_requests_.find(source); |
| 358 DCHECK(it != hash_requests_.end()) << "Request not found"; | 381 DCHECK(it != pending_hash_requests_.end()) << "Request not found"; |
| 359 | 382 |
| 360 // FindFullHashes response. | 383 // FindFullHashes response. |
| 361 // Reset the scoped pointer so the fetcher gets destroyed properly. | 384 // Reset the scoped pointer so the fetcher gets destroyed properly. |
| 362 std::unique_ptr<const net::URLFetcher> fetcher(it->first); | 385 std::unique_ptr<const net::URLFetcher> fetcher(it->first); |
| 363 | 386 |
| 364 int response_code = source->GetResponseCode(); | 387 int response_code = source->GetResponseCode(); |
| 365 net::URLRequestStatus status = source->GetStatus(); | 388 net::URLRequestStatus status = source->GetStatus(); |
| 366 V4ProtocolManagerUtil::RecordHttpResponseOrErrorCode( | 389 V4ProtocolManagerUtil::RecordHttpResponseOrErrorCode( |
| 367 kUmaV4HashResponseMetricName, status, response_code); | 390 kUmaV4HashResponseMetricName, status, response_code); |
| 368 | 391 |
| 369 const FullHashCallback& callback = it->second; | 392 const FullHashCallback& callback = it->second.second; |
|
Nathan Parker
2016/08/15 19:54:47
nit: I might vote for a simple struct rather than
| |
| 370 std::vector<SBFullHashResult> full_hashes; | 393 std::vector<SBFullHashResult> full_hashes; |
| 371 base::Time negative_cache_expire; | 394 base::Time negative_cache_expire; |
| 372 if (status.is_success() && response_code == net::HTTP_OK) { | 395 if (status.is_success() && response_code == net::HTTP_OK) { |
| 373 RecordGetHashResult(V4OperationResult::STATUS_200); | 396 RecordGetHashResult(V4OperationResult::STATUS_200); |
| 374 ResetGetHashErrors(); | 397 ResetGetHashErrors(); |
| 375 std::string data; | 398 std::string data; |
| 376 source->GetResponseAsString(&data); | 399 source->GetResponseAsString(&data); |
| 377 if (!ParseHashResponse(data, &full_hashes, &negative_cache_expire)) { | 400 if (!ParseHashResponse(data, &full_hashes, &negative_cache_expire)) { |
| 378 full_hashes.clear(); | 401 full_hashes.clear(); |
| 379 RecordGetHashResult(V4OperationResult::PARSE_ERROR); | 402 RecordGetHashResult(V4OperationResult::PARSE_ERROR); |
| 380 } | 403 } |
| 381 } else { | 404 } else { |
| 382 HandleGetHashError(clock_->Now()); | 405 HandleGetHashError(clock_->Now()); |
| 383 | 406 |
| 384 DVLOG(1) << "SafeBrowsing GetEncodedFullHashes request for: " | 407 DVLOG(1) << "SafeBrowsing GetEncodedFullHashes request for: " |
| 385 << source->GetURL() << " failed with error: " << status.error() | 408 << source->GetURL() << " failed with error: " << status.error() |
| 386 << " and response code: " << response_code; | 409 << " and response code: " << response_code; |
| 387 | 410 |
| 388 if (status.status() == net::URLRequestStatus::FAILED) { | 411 if (status.status() == net::URLRequestStatus::FAILED) { |
| 389 RecordGetHashResult(V4OperationResult::NETWORK_ERROR); | 412 RecordGetHashResult(V4OperationResult::NETWORK_ERROR); |
| 390 } else { | 413 } else { |
| 391 RecordGetHashResult(V4OperationResult::HTTP_ERROR); | 414 RecordGetHashResult(V4OperationResult::HTTP_ERROR); |
| 392 } | 415 } |
| 393 } | 416 } |
| 394 | 417 |
| 418 const std::vector<SBPrefix>& prefixes = it->second.first; | |
| 419 // If the time is uninitialized, don't cache the results. | |
| 420 if (!negative_cache_expire.is_null()) { | |
| 421 // Cache the results. | |
| 422 // Create or reset all cached results for this prefix. | |
| 423 for (const SBPrefix& prefix : prefixes) { | |
| 424 v4_full_hash_cache_[SB_THREAT_TYPE_API_ABUSE][prefix] = | |
|
Nathan Parker
2016/08/15 19:54:47
The threat type shouldn't be hardcoded here... it
| |
| 425 SBCachedFullHashResult(negative_cache_expire); | |
| 426 } | |
| 427 // Insert any full hash hits. Note that there may be one, multiple, or no | |
| 428 // full hashes for any given prefix. | |
| 429 for (const SBFullHashResult& result : full_hashes) { | |
| 430 v4_full_hash_cache_[SB_THREAT_TYPE_API_ABUSE][result.hash.prefix]. | |
| 431 full_hashes.push_back(result); | |
| 432 } | |
| 433 } | |
| 434 | |
| 395 // Invoke the callback with full_hashes, even if there was a parse error or | 435 // Invoke the callback with full_hashes, even if there was a parse error or |
| 396 // an error response code (in which case full_hashes will be empty). The | 436 // an error response code (in which case full_hashes will be empty). The |
| 397 // caller can't be blocked indefinitely. | 437 // caller can't be blocked indefinitely. |
| 398 callback.Run(full_hashes, negative_cache_expire); | 438 callback.Run(full_hashes, negative_cache_expire); |
| 399 | 439 |
| 400 hash_requests_.erase(it); | 440 pending_hash_requests_.erase(it); |
| 401 } | 441 } |
| 402 | 442 |
| 403 void V4GetHashProtocolManager::HandleGetHashError(const Time& now) { | 443 void V4GetHashProtocolManager::HandleGetHashError(const Time& now) { |
| 404 DCHECK(CalledOnValidThread()); | 444 DCHECK(CalledOnValidThread()); |
| 405 base::TimeDelta next = V4ProtocolManagerUtil::GetNextBackOffInterval( | 445 base::TimeDelta next = V4ProtocolManagerUtil::GetNextBackOffInterval( |
| 406 &gethash_error_count_, &gethash_back_off_mult_); | 446 &gethash_error_count_, &gethash_back_off_mult_); |
| 407 next_gethash_time_ = now + next; | 447 next_gethash_time_ = now + next; |
| 408 } | 448 } |
| 409 | 449 |
| 410 void V4GetHashProtocolManager::GetHashUrlAndHeaders( | 450 void V4GetHashProtocolManager::GetHashUrlAndHeaders( |
| 411 const std::string& req_base64, | 451 const std::string& req_base64, |
| 412 GURL* gurl, | 452 GURL* gurl, |
| 413 net::HttpRequestHeaders* headers) const { | 453 net::HttpRequestHeaders* headers) const { |
| 414 V4ProtocolManagerUtil::GetRequestUrlAndHeaders(req_base64, "fullHashes:find", | 454 V4ProtocolManagerUtil::GetRequestUrlAndHeaders(req_base64, "fullHashes:find", |
| 415 config_, gurl, headers); | 455 config_, gurl, headers); |
| 416 } | 456 } |
| 417 | 457 |
| 458 void V4GetHashProtocolManager::GetFullHashCachedResults( | |
| 459 const SBThreatType& threat_type, | |
| 460 const std::vector<SBFullHash>& full_hashes, | |
| 461 base::Time now, | |
| 462 std::vector<SBPrefix>* prefixes_needing_reqs, | |
| 463 std::vector<SBFullHashResult>* cached_results) { | |
| 464 DCHECK(prefixes_needing_reqs); | |
| 465 prefixes_needing_reqs->clear(); | |
| 466 DCHECK(cached_results); | |
| 467 cached_results->clear(); | |
| 468 | |
| 469 // Caching behavior is documented here: | |
| 470 // https://developers.google.com/safe-browsing/v4/caching#about-caching | |
| 471 // | |
| 472 // The cache operates as follows: | |
| 473 // Lookup: | |
| 474 // Case 1: The prefix is in the cache. | |
| 475 // Case a: The full hash is in the cache. | |
| 476 // Case i : The positive full hash result has not expired. | |
| 477 // The result is unsafe and we do not need to send a new | |
| 478 // request. | |
| 479 // Case ii: The positive full hash result has expired. | |
| 480 // We need to send a request for full hashes. | |
| 481 // Case b: The full hash is not in the cache. | |
| 482 // Case i : The negative cache entry has not expired. | |
| 483 // The result is still safe and we do not need to send a | |
| 484 // new request. | |
| 485 // Case ii: The negative cache entry has expired. | |
| 486 // We need to send a request for full hashes. | |
| 487 // Case 2: The prefix is not in the cache. | |
| 488 // We need to send a request for full hashes. | |
| 489 // | |
| 490 // Eviction: | |
| 491 // SBCachedFullHashResult entries can be removed from the cache only when | |
| 492 // the negative cache expire time and the cache expire time of all full | |
| 493 // hash results for that prefix have expired. | |
| 494 // Individual full hash results can be removed from the prefix's | |
| 495 // cache entry if they expire AND their expire time is after the negative | |
| 496 // cache expire time. | |
| 497 for (const SBFullHash& full_hash : full_hashes) { | |
| 498 auto entry = v4_full_hash_cache_[threat_type].find(full_hash.prefix); | |
| 499 if (entry != v4_full_hash_cache_[threat_type].end()) { | |
| 500 // Case 1. | |
| 501 SBCachedFullHashResult& cache_result = entry->second; | |
| 502 | |
| 503 const SBFullHashResult* found_full_hash = nullptr; | |
| 504 size_t matched_idx = 0; | |
| 505 for (const SBFullHashResult& hash_result : cache_result.full_hashes) { | |
| 506 if (SBFullHashEqual(full_hash, hash_result.hash)) { | |
| 507 found_full_hash = &hash_result; | |
| 508 break; | |
| 509 } | |
| 510 ++matched_idx; | |
| 511 } | |
| 512 | |
| 513 if (found_full_hash) { | |
| 514 // Case a. | |
| 515 if (found_full_hash->cache_expire_after > now) { | |
| 516 // Case i. | |
| 517 cached_results->push_back(*found_full_hash); | |
| 518 RecordV4FullHashCacheResult(FULL_HASH_CACHE_HIT); | |
| 519 } else { | |
| 520 // Case ii. | |
| 521 prefixes_needing_reqs->push_back(full_hash.prefix); | |
| 522 RecordV4FullHashCacheResult(FULL_HASH_CACHE_MISS); | |
| 523 // If the negative cache expire time has passed, evict this full hash | |
| 524 // result from the cache. | |
| 525 if (cache_result.expire_after <= now) { | |
| 526 cache_result.full_hashes.erase( | |
| 527 cache_result.full_hashes.begin() + matched_idx); | |
| 528 // If there are no more full hashes, we can evict the entire entry. | |
| 529 if (cache_result.full_hashes.empty()) { | |
| 530 v4_full_hash_cache_[threat_type].erase(entry); | |
| 531 } | |
| 532 } | |
| 533 } | |
| 534 } else { | |
| 535 // Case b. | |
| 536 if (cache_result.expire_after > now) { | |
| 537 // Case i. | |
| 538 RecordV4FullHashCacheResult(FULL_HASH_NEGATIVE_CACHE_HIT); | |
| 539 } else { | |
| 540 // Case ii. | |
| 541 prefixes_needing_reqs->push_back(full_hash.prefix); | |
| 542 RecordV4FullHashCacheResult(FULL_HASH_CACHE_MISS); | |
| 543 } | |
| 544 } | |
| 545 } else { | |
| 546 // Case 2. | |
| 547 prefixes_needing_reqs->push_back(full_hash.prefix); | |
| 548 RecordV4FullHashCacheResult(FULL_HASH_CACHE_MISS); | |
| 549 } | |
| 550 } | |
| 551 | |
| 552 // Multiple full hashes could share a prefix, remove duplicates. | |
| 553 // TODO(kcarattini): Make |prefixes_needing_reqs| a set. | |
| 554 std::sort(prefixes_needing_reqs->begin(), prefixes_needing_reqs->end()); | |
| 555 prefixes_needing_reqs->erase(std::unique(prefixes_needing_reqs->begin(), | |
| 556 prefixes_needing_reqs->end()), prefixes_needing_reqs->end()); | |
| 557 } | |
| 558 | |
| 418 } // namespace safe_browsing | 559 } // namespace safe_browsing |
| OLD | NEW |