Chromium Code Reviews| Index: components/reporting/core/browser/reporting_manager.cc |
| diff --git a/components/reporting/core/browser/reporting_manager.cc b/components/reporting/core/browser/reporting_manager.cc |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..abb0b33b023c7426dfd105ad43f21efcebe03919 |
| --- /dev/null |
| +++ b/components/reporting/core/browser/reporting_manager.cc |
| @@ -0,0 +1,302 @@ |
| +// 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/reporting/core/browser/reporting_manager.h" |
| + |
| +#include "base/bind.h" |
| +#include "base/json/json_writer.h" |
| +#include "base/memory/ptr_util.h" |
| +#include "base/strings/string_number_conversions.h" |
| +#include "base/strings/string_util.h" |
| +#include "base/time/default_tick_clock.h" |
| +#include "components/reporting/core/browser/reporting_util.h" |
| +#include "net/http/http_request_info.h" |
| +#include "net/http/http_response_headers.h" |
| +#include "net/http/http_response_info.h" |
| +#include "net/url_request/url_fetcher.h" |
| +#include "net/url_request/url_request_context_getter.h" |
| + |
| +namespace { |
| +const char kDefaultGroupName[] = "default"; |
| +} // namespace |
| + |
| +namespace reporting { |
| + |
| +ReportingManager::ReportingManager( |
| + std::unique_ptr<ReportingUploader> uploader, |
| + const base::Callback<bool(const GURL&)>& is_origin_secure) |
| + : is_origin_secure_(is_origin_secure), |
| + clock_(new base::DefaultTickClock()), |
| + uploader_(std::move(uploader)) {} |
| +ReportingManager::~ReportingManager() {} |
| + |
| +void ReportingManager::QueueReport(std::unique_ptr<ReportingReport> report) { |
| + report->timestamp = clock_->NowTicks(); |
| + report->attempts = 0; |
| + cache_.EnqueueReport(std::move(report)); |
| +} |
| + |
| +void ReportingManager::ProcessHeader(const GURL& origin, |
| + const std::string& header_value) { |
| + DCHECK(is_origin_secure_.Run(origin)); |
| + |
| + std::vector<std::string> errors; |
| + std::vector<EndpointTuple> tuples = |
| + EndpointTuple::FromHeader(header_value, is_origin_secure_, &errors); |
| + |
| + // TODO: Plumb these out in a more modular way and expose them somewhere more |
| + // visible? |
|
Randy Smith (Not in Mondays)
2016/10/21 20:15:11
I'm trying to think what the right place would be.
Julia Tuttle
2016/11/02 20:44:39
The DevTools console, most likely.
|
| + for (const std::string& error : errors) { |
| + LOG(WARNING) << "Origin " << origin.spec() << " sent " |
| + << "Report-To header with error: " << error << ": " |
| + << header_value; |
| + } |
| + |
| + for (auto& tuple : tuples) |
| + ProcessEndpoint(origin, tuple); |
| +} |
| + |
| +void ReportingManager::SendReports() { |
| + const ReportingCache::ReportSet& reports = cache_.GetReports(); |
| + const ReportingCache::EndpointMap& endpoints = cache_.GetEndpoints(); |
| + |
| + std::map<const std::unique_ptr<ReportingEndpoint>*, |
| + std::vector<const std::unique_ptr<ReportingReport>*>> |
| + endpoint_map; |
| + for (auto& report : reports) { |
| + for (auto& pair : endpoints) { |
| + const std::unique_ptr<ReportingEndpoint>& endpoint = pair.second; |
| + if (endpoint->is_expired(clock_->NowTicks()) || |
| + endpoint->backoff.ShouldRejectRequest() || |
| + !DoesEndpointMatchReport(*endpoint, *report)) { |
| + continue; |
| + } |
| + endpoint_map[&endpoint].push_back(&report); |
| + goto next_report; |
|
Randy Smith (Not in Mondays)
2016/10/21 20:15:11
I think this matching-and-exit algorithm means tha
Randy Smith (Not in Mondays)
2016/10/21 20:15:11
Why not "break;"?
Julia Tuttle
2016/11/02 20:44:39
I think I was worried I'd end up breaking out of t
Julia Tuttle
2016/11/02 20:44:39
...I *don't* think that's what the spec wants. *I*
|
| + } |
| + next_report:; |
| + } |
| + |
| + for (auto& pair : endpoint_map) { |
| + std::unique_ptr<Delivery> delivery(new Delivery(*pair.first, pair.second)); |
| + delivery->endpoint->last_used = clock_->NowTicks(); |
| + const GURL url = delivery->endpoint->url; |
| + std::string json = SerializeReports(delivery->reports); |
| + uploader_->AttemptDelivery( |
| + url, json, base::Bind(&ReportingManager::OnDeliveryAttemptComplete, |
| + base::Unretained(this), std::move(delivery))); |
| + } |
| +} |
| + |
| +// static |
| +bool ReportingManager::EndpointTuple::FromDictionary( |
| + const base::DictionaryValue& dictionary, |
| + EndpointTuple* tuple_out, |
| + std::string* error_out) { |
| + std::string url_string; |
| + if (!dictionary.GetString("url", &url_string)) { |
| + *error_out = "url missing or not a string"; |
| + return false; |
| + } |
| + tuple_out->url = GURL(url_string); |
| + |
| + tuple_out->subdomains = false; |
| + dictionary.GetBoolean("includeSubdomains", &tuple_out->subdomains); |
| + |
| + int ttl_sec; |
| + if (!dictionary.GetInteger("max-age", &ttl_sec)) { |
| + *error_out = "max-age missing or not an integer"; |
| + return false; |
| + } |
| + tuple_out->ttl = base::TimeDelta::FromSeconds(ttl_sec); |
| + |
| + tuple_out->group = kDefaultGroupName; |
| + dictionary.GetString("group", &tuple_out->group); |
| + |
| + return true; |
| +} |
| + |
| +// static |
| +std::vector<ReportingManager::EndpointTuple> |
| +ReportingManager::EndpointTuple::FromHeader( |
| + const std::string& header, |
| + const base::Callback<bool(const GURL&)>& is_origin_secure, |
| + std::vector<std::string>* errors_out) { |
| + std::vector<ReportingManager::EndpointTuple> tuples; |
| + |
| + errors_out->clear(); |
| + |
| + std::unique_ptr<base::Value> value(ParseJFV(header)); |
| + if (!value) { |
| + errors_out->push_back("failed to parse JSON field value."); |
| + return tuples; |
| + } |
| + |
| + base::ListValue* list; |
| + bool was_list = value->GetAsList(&list); |
| + DCHECK(was_list); |
| + |
| + base::DictionaryValue* item; |
| + for (size_t i = 0; i < list->GetSize(); i++) { |
| + std::string error_prefix = "endpoint " + base::SizeTToString(i + 1) + |
| + " of " + base::SizeTToString(list->GetSize()) + |
| + ":"; |
| + if (!list->GetDictionary(i, &item)) { |
| + errors_out->push_back(error_prefix + "is not a dictionary."); |
| + continue; |
| + } |
| + EndpointTuple tuple; |
| + std::string error; |
| + if (!EndpointTuple::FromDictionary(*item, &tuple, &error)) { |
| + errors_out->push_back(error_prefix + error); |
| + continue; |
| + } |
| + if (!is_origin_secure.Run(tuple.url)) { |
| + errors_out->push_back(error_prefix + "url " + tuple.url.spec() + |
| + " is insecure."); |
| + continue; |
| + } |
| + if (tuple.ttl < base::TimeDelta()) { |
| + errors_out->push_back(error_prefix + "ttl is negative."); |
| + continue; |
| + } |
| + tuples.push_back(tuple); |
| + } |
| + return tuples; |
| +} |
| + |
| +std::string ReportingManager::EndpointTuple::ToString() const { |
| + return "(url=" + url.spec() + ", subdomains=" + |
| + (subdomains ? "true" : "false") + ", ttl=" + |
| + base::Int64ToString(ttl.InSeconds()) + "s" + ", group=" + group + ")"; |
| +} |
| + |
| +ReportingManager::Delivery::Delivery( |
| + const std::unique_ptr<ReportingEndpoint>& endpoint, |
| + const std::vector<const std::unique_ptr<ReportingReport>*>& reports) |
| + : endpoint(endpoint), reports(reports) {} |
| + |
| +ReportingManager::Delivery::~Delivery() {} |
| + |
| +void ReportingManager::ProcessEndpoint(const GURL& origin, |
| + const EndpointTuple& tuple) { |
| + const std::unique_ptr<ReportingEndpoint>* endpoint_ptr = |
| + cache_.GetEndpoint(tuple.url); |
| + ReportingEndpoint* endpoint = endpoint_ptr ? endpoint_ptr->get() : nullptr; |
| + |
| + if (endpoint) |
| + endpoint->clients.erase(origin); |
| + |
| + if (tuple.ttl <= base::TimeDelta()) |
| + return; |
| + |
| + if (!endpoint) { |
| + endpoint = |
| + new ReportingEndpoint(tuple.url, backoff_policy_, clock_->NowTicks()); |
| + cache_.InsertEndpoint(base::WrapUnique(endpoint)); |
| + } |
| + |
| + endpoint->clients.insert(std::make_pair( |
| + GURL(origin), ReportingClient(origin, tuple.subdomains, tuple.group, |
| + tuple.ttl, clock_->NowTicks()))); |
| +} |
| + |
| +bool ReportingManager::DoesEndpointMatchReport( |
| + const ReportingEndpoint& endpoint, |
| + const ReportingReport& report) { |
| + for (auto& pair : endpoint.clients) { |
| + const ReportingClient& client = pair.second; |
| + if (client.is_expired(clock_->NowTicks())) |
| + continue; |
| + if (!base::EqualsCaseInsensitiveASCII(client.group, report.group)) |
| + continue; |
| + if (client.origin == report.origin) |
| + return true; |
| + if (client.subdomains && report.origin.DomainIs(client.origin.host_piece())) |
| + return true; |
| + } |
| + return false; |
| +} |
| + |
| +void ReportingManager::OnDeliveryAttemptComplete( |
| + const std::unique_ptr<Delivery>& delivery, |
| + ReportingUploader::Outcome outcome) { |
| + switch (outcome) { |
| + case ReportingUploader::SUCCESS: |
| + delivery->endpoint->backoff.InformOfRequest(true); |
| + for (auto report_ptr : delivery->reports) |
| + cache_.DequeueReport(*report_ptr); |
| + break; |
| + case ReportingUploader::FAILURE: |
| + delivery->endpoint->backoff.InformOfRequest(false); |
| + break; |
| + case ReportingUploader::REMOVE_ENDPOINT: |
| + // NOTE: This is not specified, but seems the obvious intention. |
| + cache_.RemoveEndpoint(delivery->endpoint); |
| + break; |
| + } |
| +} |
| + |
| +std::string ReportingManager::SerializeReports( |
| + const std::vector<const std::unique_ptr<ReportingReport>*>& reports) { |
| + base::ListValue collection; |
| + for (auto& report_ptr : reports) { |
| + ReportingReport* report = report_ptr->get(); |
| + ++report->attempts; |
| + |
| + std::unique_ptr<base::DictionaryValue> data(new base::DictionaryValue()); |
| + data->SetInteger("age", |
| + (clock_->NowTicks() - report->timestamp).InMilliseconds()); |
| + data->SetString("type", report->type); |
| + data->SetString("url", report->url.spec()); |
| + data->Set("report", report->body->DeepCopy()); |
| + collection.Append(std::move(data)); |
| + } |
| + |
| + std::string json = ""; |
| + bool written = base::JSONWriter::Write(collection, &json); |
| + DCHECK(written); |
| + return json; |
| +} |
| + |
| +void ReportingManager::CollectGarbage() { |
| + const base::TimeDelta kEndpointMaxUnusedTime = base::TimeDelta::FromDays(7); |
| + const base::TimeDelta kReportMaxUndeliveredTime = |
| + base::TimeDelta::FromDays(2); |
| + const int kEndpointMaxFailures = 5; |
| + const int kReportMaxFailures = 5; |
| + |
| + // TODO: Histogram report and endpoint removal reasons and maybe ages. |
| + |
| + const ReportingCache::EndpointMap& endpoints = cache_.GetEndpoints(); |
| + for (auto& pair : endpoints) { |
| + const std::unique_ptr<ReportingEndpoint>& endpoint = pair.second; |
| + if (endpoint->is_expired(clock_->NowTicks())) { |
| + cache_.RemoveEndpoint(endpoint); |
| + continue; |
| + } |
| + if (clock_->NowTicks() - endpoint->last_used > kEndpointMaxUnusedTime) { |
| + cache_.RemoveEndpoint(endpoint); |
| + continue; |
| + } |
| + if (endpoint->backoff.failure_count() > kEndpointMaxFailures) { |
| + cache_.RemoveEndpoint(endpoint); |
| + continue; |
| + } |
| + } |
| + |
| + const ReportingCache::ReportSet& reports = cache_.GetReports(); |
| + for (auto& report : reports) { |
| + if (report->attempts > kReportMaxFailures) { |
| + cache_.DequeueReport(report); |
| + continue; |
| + } |
| + if (clock_->NowTicks() - report->timestamp > kReportMaxUndeliveredTime) { |
| + cache_.DequeueReport(report); |
| + continue; |
| + } |
| + } |
| +} |
| + |
| +} // namespace reporting |