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 |