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 "net/reporting/reporting_service.h" |
| 6 |
| 7 #include "base/bind.h" |
| 8 #include "base/json/json_reader.h" |
| 9 #include "base/json/json_writer.h" |
| 10 #include "base/memory/ptr_util.h" |
| 11 #include "base/metrics/histogram_macros.h" |
| 12 #include "base/strings/string_number_conversions.h" |
| 13 #include "base/strings/string_util.h" |
| 14 #include "base/time/default_tick_clock.h" |
| 15 #include "net/http/http_request_info.h" |
| 16 #include "net/http/http_response_headers.h" |
| 17 #include "net/http/http_response_info.h" |
| 18 #include "net/url_request/url_fetcher.h" |
| 19 #include "net/url_request/url_request_context_getter.h" |
| 20 |
| 21 namespace { |
| 22 |
| 23 const char kDefaultGroupName[] = "default"; |
| 24 |
| 25 // Per |
| 26 // https://greenbytes.de/tech/webdav/draft-reschke-http-jfv-02.html#rfc.section.
4 |
| 27 // assuming |normalized_header| is the result of completing step 1. |
| 28 std::unique_ptr<base::Value> ParseJFV( |
| 29 const std::string& normalized_header_value) { |
| 30 std::string value = "[" + normalized_header_value + "]"; |
| 31 return base::JSONReader::Read(value); |
| 32 } |
| 33 |
| 34 bool IsOriginSecure(const GURL& url) { |
| 35 return url.SchemeIsCryptographic(); |
| 36 } |
| 37 |
| 38 } // namespace |
| 39 |
| 40 namespace net { |
| 41 |
| 42 ReportingService::Policy::Policy() |
| 43 : endpoint_lifetime(base::TimeDelta::FromDays(7)), |
| 44 max_endpoint_failures(5), |
| 45 max_endpoint_count(100), |
| 46 report_lifetime(base::TimeDelta::FromHours(1)), |
| 47 max_report_failures(5), |
| 48 max_report_count(100), |
| 49 persist_reports_across_network_changes(false) { |
| 50 endpoint_backoff.num_errors_to_ignore = 0; |
| 51 endpoint_backoff.initial_delay_ms = 5 * 1000; |
| 52 endpoint_backoff.multiply_factor = 2.0; |
| 53 endpoint_backoff.jitter_factor = 0.1; |
| 54 endpoint_backoff.maximum_backoff_ms = 60 * 60 * 1000; |
| 55 endpoint_backoff.entry_lifetime_ms = 7 * 24 * 60 * 60 * 1000; |
| 56 endpoint_backoff.always_use_initial_delay = false; |
| 57 } |
| 58 |
| 59 // static |
| 60 ReportingService::Policy ReportingService::Policy::GetNonBrowserDefault() { |
| 61 Policy policy; |
| 62 // In non-browser contexts, there is less concern that Reporting could |
| 63 // accidentally track users across networks, so don't discard reports as |
| 64 // readily. |
| 65 policy.report_lifetime = base::TimeDelta::FromDays(2); |
| 66 policy.persist_reports_across_network_changes = true; |
| 67 return policy; |
| 68 } |
| 69 |
| 70 struct ReportingService::Client { |
| 71 public: |
| 72 Client(const GURL& origin, |
| 73 bool subdomains, |
| 74 const std::string& group, |
| 75 base::TimeDelta ttl, |
| 76 base::TimeTicks creation) |
| 77 : origin(origin), |
| 78 subdomains(subdomains), |
| 79 group(group), |
| 80 ttl(ttl), |
| 81 creation(creation) {} |
| 82 |
| 83 GURL origin; |
| 84 bool subdomains; |
| 85 std::string group; |
| 86 base::TimeDelta ttl; |
| 87 base::TimeTicks creation; |
| 88 |
| 89 bool is_expired(base::TimeTicks now) const { return creation + ttl < now; } |
| 90 }; |
| 91 |
| 92 struct ReportingService::Endpoint { |
| 93 public: |
| 94 Endpoint(const GURL& url, |
| 95 const BackoffEntry::Policy& backoff_policy, |
| 96 base::TickClock* clock) |
| 97 : url(url), |
| 98 backoff(&backoff_policy, clock), |
| 99 last_used(clock->NowTicks()), |
| 100 pending(false) {} |
| 101 ~Endpoint() {} |
| 102 |
| 103 const GURL url; |
| 104 |
| 105 BackoffEntry backoff; |
| 106 // For LRU eviction of endpoints. |
| 107 base::TimeTicks last_used; |
| 108 // Whether we currently have an upload in progress to this endpoint. |
| 109 bool pending; |
| 110 |
| 111 // Map from client.origin to client. |
| 112 std::map<GURL, Client> clients; |
| 113 |
| 114 bool is_expired(base::TimeTicks now) const { |
| 115 for (auto& pair : clients) |
| 116 if (!pair.second.is_expired(now)) |
| 117 return false; |
| 118 return true; |
| 119 } |
| 120 }; |
| 121 |
| 122 class ReportingService::Report { |
| 123 public: |
| 124 Report() = default; |
| 125 ~Report() = default; |
| 126 |
| 127 // Report body. Can be any valid JSON. Passed to endpoint unmodified. |
| 128 std::unique_ptr<base::Value> body; |
| 129 |
| 130 // URL of content that triggered report. Passed to endpoint unmodified. |
| 131 GURL url; |
| 132 |
| 133 // Origin of content that triggered report. Should be url.GetOrigin(), but |
| 134 // the spec doesn't guarantee this, and allows callers to provide URL and |
| 135 // origin separately, so they are separate fields for now. Passed to |
| 136 // endpoint unmodified. |
| 137 GURL origin; |
| 138 |
| 139 // Report group. Used to select endpoints for upload. |
| 140 std::string group; |
| 141 |
| 142 // Report type. Passed to endpoint unmodified. |
| 143 std::string type; |
| 144 |
| 145 // Timestamp when the report was queued. Passed to endpoint as a relative |
| 146 // age. |
| 147 base::TimeTicks queued; |
| 148 |
| 149 // Number of delivery attempts made. |
| 150 size_t attempts; |
| 151 |
| 152 // Whether this report is currently part of a pending delivery attempt. |
| 153 bool pending; |
| 154 |
| 155 private: |
| 156 DISALLOW_COPY_AND_ASSIGN(Report); |
| 157 }; |
| 158 |
| 159 struct ReportingService::EndpointTuple { |
| 160 GURL url; |
| 161 bool subdomains; |
| 162 base::TimeDelta ttl; |
| 163 std::string group; |
| 164 |
| 165 static bool FromDictionary(const base::DictionaryValue& dictionary, |
| 166 EndpointTuple* tuple_out, |
| 167 std::string* error_out); |
| 168 static bool FromHeader(const std::string& header, |
| 169 std::vector<EndpointTuple>* tuples_out, |
| 170 std::vector<std::string>* errors_out); |
| 171 }; |
| 172 |
| 173 struct ReportingService::Delivery { |
| 174 Delivery(const GURL& endpoint_url, const std::vector<Report*>& reports) |
| 175 : endpoint_url(endpoint_url), reports(reports) {} |
| 176 ~Delivery() = default; |
| 177 |
| 178 const GURL& endpoint_url; |
| 179 const std::vector<Report*> reports; |
| 180 }; |
| 181 |
| 182 ReportingService::ReportingService(const Policy& policy) |
| 183 : policy_(policy), clock_(new base::DefaultTickClock()) { |
| 184 NetworkChangeNotifier::AddNetworkChangeObserver(this); |
| 185 } |
| 186 |
| 187 ReportingService::~ReportingService() { |
| 188 NetworkChangeNotifier::RemoveNetworkChangeObserver(this); |
| 189 } |
| 190 |
| 191 void ReportingService::set_uploader( |
| 192 std::unique_ptr<ReportingUploader> uploader) { |
| 193 uploader_ = std::move(uploader); |
| 194 } |
| 195 |
| 196 void ReportingService::QueueReport(std::unique_ptr<base::Value> body, |
| 197 const GURL& url, |
| 198 const GURL& origin, |
| 199 const std::string& group, |
| 200 const std::string& type) { |
| 201 auto report = base::MakeUnique<Report>(); |
| 202 report->body = std::move(body); |
| 203 report->url = url.GetAsReferrer(); |
| 204 report->origin = origin; |
| 205 report->group = group; |
| 206 report->type = type; |
| 207 report->queued = clock_->NowTicks(); |
| 208 report->attempts = 0; |
| 209 report->pending = false; |
| 210 reports_.push_back(std::move(report)); |
| 211 |
| 212 CollectGarbage(); |
| 213 } |
| 214 |
| 215 void ReportingService::ProcessHeader(const GURL& origin, |
| 216 const std::string& header_value) { |
| 217 if (!IsOriginSecure(origin)) |
| 218 return; |
| 219 |
| 220 std::vector<std::string> errors; |
| 221 std::vector<EndpointTuple> tuples; |
| 222 if (!EndpointTuple::FromHeader(header_value, &tuples, &errors)) |
| 223 return; |
| 224 |
| 225 // TODO: Plumb these out to the DevTools console somehow. |
| 226 for (const std::string& error : errors) { |
| 227 LOG(WARNING) << "Origin " << origin.spec() << " sent " |
| 228 << "Report-To header with error: " << error << ": " |
| 229 << header_value; |
| 230 } |
| 231 |
| 232 for (auto& tuple : tuples) |
| 233 ProcessEndpointTuple(origin, tuple); |
| 234 |
| 235 CollectGarbage(); |
| 236 } |
| 237 |
| 238 void ReportingService::SendReports() { |
| 239 if (!uploader_) |
| 240 return; |
| 241 |
| 242 base::TimeTicks now = clock_->NowTicks(); |
| 243 |
| 244 std::map<Endpoint*, std::vector<Report*>> endpoint_reports; |
| 245 for (auto& report : reports_) { |
| 246 // If the report is already contained in another pending upload, don't |
| 247 // upload it twice. |
| 248 if (report->pending) |
| 249 continue; |
| 250 |
| 251 Endpoint* endpoint = FindEndpointForReport(*report); |
| 252 // If there's no available endpoint for the report, leave it for later. |
| 253 if (!endpoint) |
| 254 continue; |
| 255 |
| 256 // If the chosen endpoint is pending, don't start another upload; let this |
| 257 // report go in the next upload instead. |
| 258 if (endpoint->pending) |
| 259 continue; |
| 260 |
| 261 report->pending = true; |
| 262 endpoint_reports[endpoint].push_back(report.get()); |
| 263 } |
| 264 |
| 265 for (auto& pair : endpoint_reports) { |
| 266 Endpoint* endpoint = pair.first; |
| 267 const std::vector<Report*>& reports = pair.second; |
| 268 |
| 269 std::string json = SerializeReports(reports); |
| 270 |
| 271 uploader_->AttemptDelivery( |
| 272 endpoint->url, json, |
| 273 base::Bind(&ReportingService::OnDeliveryAttemptComplete, |
| 274 base::Unretained(this), |
| 275 base::MakeUnique<Delivery>(endpoint->url, reports))); |
| 276 |
| 277 endpoint->last_used = now; |
| 278 |
| 279 for (auto& report : reports) |
| 280 ++report->attempts; |
| 281 } |
| 282 } |
| 283 |
| 284 void ReportingService::OnNetworkChanged( |
| 285 NetworkChangeNotifier::ConnectionType connection_type) { |
| 286 if (policy_.persist_reports_across_network_changes) |
| 287 return; |
| 288 |
| 289 reports_.clear(); |
| 290 } |
| 291 |
| 292 void ReportingService::set_clock_for_testing( |
| 293 std::unique_ptr<base::TickClock> clock) { |
| 294 clock_ = std::move(clock); |
| 295 } |
| 296 |
| 297 size_t ReportingService::GetReportCountForTesting() { |
| 298 return reports_.size(); |
| 299 } |
| 300 |
| 301 bool ReportingService::HasEndpointForTesting(const GURL& endpoint_url) { |
| 302 return GetEndpointByURL(endpoint_url) != nullptr; |
| 303 } |
| 304 |
| 305 bool ReportingService::HasClientForTesting(const GURL& endpoint_url, |
| 306 const GURL& origin) { |
| 307 Endpoint* endpoint = GetEndpointByURL(endpoint_url); |
| 308 if (!endpoint) |
| 309 return false; |
| 310 return endpoint->clients.count(origin) > 0; |
| 311 } |
| 312 |
| 313 int ReportingService::GetEndpointFailuresForTesting(const GURL& endpoint_url) { |
| 314 Endpoint* endpoint = GetEndpointByURL(endpoint_url); |
| 315 if (!endpoint) |
| 316 return -1; |
| 317 return endpoint->backoff.failure_count(); |
| 318 } |
| 319 |
| 320 void ReportingService::CollectGarbageForTesting() { |
| 321 CollectGarbage(); |
| 322 } |
| 323 |
| 324 // static |
| 325 bool ReportingService::EndpointTuple::FromDictionary( |
| 326 const base::DictionaryValue& dictionary, |
| 327 EndpointTuple* tuple_out, |
| 328 std::string* error_out) { |
| 329 if (!dictionary.HasKey("url")) { |
| 330 *error_out = "url missing"; |
| 331 return false; |
| 332 } |
| 333 std::string url_string; |
| 334 if (!dictionary.GetString("url", &url_string)) { |
| 335 *error_out = "url not a string"; |
| 336 return false; |
| 337 } |
| 338 tuple_out->url = GURL(url_string); |
| 339 |
| 340 tuple_out->group = kDefaultGroupName; |
| 341 if (dictionary.HasKey("group")) { |
| 342 if (!dictionary.GetString("group", &tuple_out->group)) { |
| 343 *error_out = "group present but not a string"; |
| 344 return false; |
| 345 } |
| 346 } |
| 347 |
| 348 tuple_out->subdomains = false; |
| 349 if (dictionary.HasKey("includeSubdomains")) { |
| 350 if (!dictionary.GetBoolean("includeSubdomains", &tuple_out->subdomains)) { |
| 351 *error_out = "includeSubdomains present but not boolean"; |
| 352 return false; |
| 353 } |
| 354 } |
| 355 |
| 356 if (!dictionary.HasKey("max-age")) { |
| 357 *error_out = "max-age missing"; |
| 358 return false; |
| 359 } |
| 360 int ttl_sec; |
| 361 if (!dictionary.GetInteger("max-age", &ttl_sec)) { |
| 362 *error_out = "max-age not an integer"; |
| 363 return false; |
| 364 } |
| 365 tuple_out->ttl = base::TimeDelta::FromSeconds(ttl_sec); |
| 366 |
| 367 return true; |
| 368 } |
| 369 |
| 370 // static |
| 371 bool ReportingService::EndpointTuple::FromHeader( |
| 372 const std::string& header, |
| 373 std::vector<ReportingService::EndpointTuple>* tuples_out, |
| 374 std::vector<std::string>* errors_out) { |
| 375 tuples_out->clear(); |
| 376 errors_out->clear(); |
| 377 |
| 378 std::unique_ptr<base::Value> value(ParseJFV(header)); |
| 379 if (!value) { |
| 380 errors_out->push_back("failed to parse JSON field value."); |
| 381 return false; |
| 382 } |
| 383 |
| 384 base::ListValue* list; |
| 385 bool was_list = value->GetAsList(&list); |
| 386 DCHECK(was_list); |
| 387 |
| 388 base::DictionaryValue* item; |
| 389 for (size_t i = 0; i < list->GetSize(); i++) { |
| 390 std::string error_prefix = "endpoint " + base::SizeTToString(i + 1) + |
| 391 " of " + base::SizeTToString(list->GetSize()) + |
| 392 ": "; |
| 393 if (!list->GetDictionary(i, &item)) { |
| 394 errors_out->push_back(error_prefix + "is not a dictionary"); |
| 395 continue; |
| 396 } |
| 397 EndpointTuple tuple; |
| 398 std::string error; |
| 399 if (!EndpointTuple::FromDictionary(*item, &tuple, &error)) { |
| 400 errors_out->push_back(error_prefix + error); |
| 401 continue; |
| 402 } |
| 403 if (!IsOriginSecure(tuple.url)) { |
| 404 errors_out->push_back(error_prefix + "url " + tuple.url.spec() + |
| 405 " is insecure"); |
| 406 continue; |
| 407 } |
| 408 if (tuple.ttl < base::TimeDelta()) { |
| 409 errors_out->push_back(error_prefix + "ttl is negative"); |
| 410 continue; |
| 411 } |
| 412 tuples_out->push_back(tuple); |
| 413 } |
| 414 return true; |
| 415 } |
| 416 |
| 417 void ReportingService::ProcessEndpointTuple(const GURL& origin, |
| 418 const EndpointTuple& tuple) { |
| 419 Endpoint* endpoint = GetEndpointByURL(tuple.url); |
| 420 |
| 421 bool endpoint_exists = endpoint != nullptr; |
| 422 bool client_exists = endpoint && endpoint->clients.count(origin) > 0; |
| 423 |
| 424 if (client_exists) |
| 425 endpoint->clients.erase(origin); |
| 426 |
| 427 if (tuple.ttl <= base::TimeDelta()) |
| 428 return; |
| 429 |
| 430 if (!endpoint_exists) { |
| 431 endpoint = new Endpoint(tuple.url, policy_.endpoint_backoff, clock_.get()); |
| 432 endpoints_.insert(std::make_pair(tuple.url, base::WrapUnique(endpoint))); |
| 433 } |
| 434 |
| 435 base::TimeTicks now = clock_->NowTicks(); |
| 436 |
| 437 Client client(origin, tuple.subdomains, tuple.group, tuple.ttl, now); |
| 438 endpoint->clients.insert(std::make_pair(origin, client)); |
| 439 } |
| 440 |
| 441 void ReportingService::OnDeliveryAttemptComplete( |
| 442 const std::unique_ptr<Delivery>& delivery, |
| 443 ReportingUploader::Outcome outcome) { |
| 444 for (auto report : delivery->reports) { |
| 445 DCHECK(report->pending); |
| 446 report->pending = false; |
| 447 } |
| 448 |
| 449 Endpoint* endpoint = GetEndpointByURL(delivery->endpoint_url); |
| 450 if (endpoint) { |
| 451 endpoint->backoff.InformOfRequest(outcome == ReportingUploader::SUCCESS); |
| 452 endpoint->pending = false; |
| 453 } |
| 454 |
| 455 switch (outcome) { |
| 456 case ReportingUploader::SUCCESS: |
| 457 for (auto report : delivery->reports) |
| 458 DequeueReport(report); |
| 459 break; |
| 460 case ReportingUploader::FAILURE: |
| 461 // Reports have been marked not-pending and can be retried later. |
| 462 // BackoffEntry has been informed of failure. |
| 463 break; |
| 464 case ReportingUploader::REMOVE_ENDPOINT: |
| 465 // Note: This is not specified, but seems the obvious intention. |
| 466 if (endpoint) |
| 467 endpoints_.erase(delivery->endpoint_url); |
| 468 break; |
| 469 } |
| 470 |
| 471 CollectGarbage(); |
| 472 } |
| 473 |
| 474 void ReportingService::CollectGarbage() { |
| 475 base::TimeTicks now = clock_->NowTicks(); |
| 476 |
| 477 { |
| 478 std::vector<Report*> to_dequeue; |
| 479 |
| 480 for (auto it = reports_.begin(); it != reports_.end(); ++it) { |
| 481 Report* report = it->get(); |
| 482 base::TimeDelta age = now - report->queued; |
| 483 |
| 484 if (report->pending) |
| 485 continue; |
| 486 |
| 487 if (report->attempts > policy_.max_report_failures || |
| 488 age > policy_.report_lifetime) { |
| 489 to_dequeue.push_back(report); |
| 490 } |
| 491 } |
| 492 |
| 493 for (auto it = reports_.begin(); |
| 494 it != reports_.end() && |
| 495 reports_.size() - to_dequeue.size() > policy_.max_report_count; |
| 496 ++it) { |
| 497 if (it->get()->pending) |
| 498 continue; |
| 499 |
| 500 to_dequeue.push_back(it->get()); |
| 501 } |
| 502 |
| 503 for (auto it : to_dequeue) |
| 504 DequeueReport(it); |
| 505 } |
| 506 |
| 507 // TODO: Individually garbage-collect clients. |
| 508 |
| 509 { |
| 510 std::vector<EndpointMap::iterator> to_erase; |
| 511 for (auto it = endpoints_.begin(); it != endpoints_.end(); ++it) { |
| 512 Endpoint* endpoint = it->second.get(); |
| 513 |
| 514 if (endpoint->pending) |
| 515 continue; |
| 516 |
| 517 // Don't remove failed endpoints until the BackoffEntry okays it, to |
| 518 // avoid hammering failing endpoints by removing and re-adding them |
| 519 // constantly. |
| 520 if (endpoint->is_expired(now) || |
| 521 now - endpoint->last_used > policy_.endpoint_lifetime || |
| 522 (endpoint->backoff.CanDiscard() && |
| 523 endpoint->backoff.failure_count() > policy_.max_endpoint_failures)) { |
| 524 to_erase.push_back(it); |
| 525 } |
| 526 } |
| 527 |
| 528 while (endpoints_.size() - to_erase.size() > policy_.max_endpoint_count) { |
| 529 auto oldest_it = endpoints_.end(); |
| 530 |
| 531 for (auto it = endpoints_.begin(); it != endpoints_.end(); ++it) { |
| 532 if (it->second->pending) |
| 533 continue; |
| 534 |
| 535 if (oldest_it == endpoints_.end() || |
| 536 it->second->last_used < oldest_it->second->last_used) { |
| 537 oldest_it = it; |
| 538 } |
| 539 } |
| 540 |
| 541 if (oldest_it == endpoints_.end()) |
| 542 break; |
| 543 |
| 544 to_erase.push_back(oldest_it); |
| 545 |
| 546 // Gross kludge: Keep us from choosing this endpoint again. |
| 547 oldest_it->second->pending = true; |
| 548 } |
| 549 |
| 550 for (auto it : to_erase) |
| 551 endpoints_.erase(it); |
| 552 } |
| 553 } |
| 554 |
| 555 ReportingService::Endpoint* ReportingService::FindEndpointForReport( |
| 556 const Report& report) { |
| 557 // TODO: This is O(count of all endpoints, regardless of client origins). |
| 558 // TODO: The spec doesn't prioritize *.bar.foo.com over *.foo.com when |
| 559 // choosing which endpoint to upload a report for baz.bar.foo.com to. |
| 560 // That seems wrong, but we need clarification on the spec end. |
| 561 for (auto& pair : endpoints_) { |
| 562 Endpoint* endpoint = pair.second.get(); |
| 563 if (endpoint->is_expired(clock_->NowTicks()) || |
| 564 endpoint->backoff.ShouldRejectRequest() || |
| 565 !DoesEndpointMatchReport(*endpoint, report)) { |
| 566 continue; |
| 567 } |
| 568 return endpoint; |
| 569 } |
| 570 return nullptr; |
| 571 } |
| 572 |
| 573 bool ReportingService::DoesEndpointMatchReport(const Endpoint& endpoint, |
| 574 const Report& report) { |
| 575 for (auto& pair : endpoint.clients) { |
| 576 const Client& client = pair.second; |
| 577 if (client.is_expired(clock_->NowTicks())) |
| 578 continue; |
| 579 if (!base::EqualsCaseInsensitiveASCII(client.group, report.group)) |
| 580 continue; |
| 581 if (client.origin == report.origin) |
| 582 return true; |
| 583 if (client.subdomains && report.origin.DomainIs(client.origin.host_piece())) |
| 584 return true; |
| 585 } |
| 586 return false; |
| 587 } |
| 588 |
| 589 std::string ReportingService::SerializeReports( |
| 590 const std::vector<Report*>& reports) { |
| 591 base::ListValue collection; |
| 592 for (auto& report : reports) { |
| 593 std::unique_ptr<base::DictionaryValue> data(new base::DictionaryValue()); |
| 594 data->SetInteger("age", |
| 595 (clock_->NowTicks() - report->queued).InMilliseconds()); |
| 596 data->SetString("type", report->type); |
| 597 data->SetString("url", report->url.spec()); |
| 598 data->Set("report", report->body->DeepCopy()); |
| 599 collection.Append(std::move(data)); |
| 600 } |
| 601 |
| 602 std::string json = ""; |
| 603 bool written = base::JSONWriter::Write(collection, &json); |
| 604 DCHECK(written); |
| 605 return json; |
| 606 } |
| 607 |
| 608 ReportingService::Endpoint* ReportingService::GetEndpointByURL( |
| 609 const GURL& url) { |
| 610 auto it = endpoints_.find(url); |
| 611 if (it == endpoints_.end()) |
| 612 return nullptr; |
| 613 return it->second.get(); |
| 614 } |
| 615 |
| 616 void ReportingService::DequeueReport(Report* report) { |
| 617 // TODO: This is O(N). |
| 618 for (auto it = reports_.begin(); it != reports_.end(); ++it) { |
| 619 if (it->get() == report) { |
| 620 reports_.erase(it); |
| 621 return; |
| 622 } |
| 623 } |
| 624 } |
| 625 |
| 626 } // namespace net |
OLD | NEW |