Index: net/reporting/reporting_service.cc |
diff --git a/net/reporting/reporting_service.cc b/net/reporting/reporting_service.cc |
new file mode 100644 |
index 0000000000000000000000000000000000000000..c2b55fd2f3d452998d1ea8b54c751b21cebef4bc |
--- /dev/null |
+++ b/net/reporting/reporting_service.cc |
@@ -0,0 +1,590 @@ |
+// 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 "net/reporting/reporting_service.h" |
+ |
+#include "base/bind.h" |
+#include "base/json/json_reader.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 "net/http/http_request_info.h" |
+#include "net/http/http_response_headers.h" |
+#include "net/http/http_response_info.h" |
+#include "net/reporting/reporting_metrics.h" |
+#include "net/url_request/url_fetcher.h" |
+#include "net/url_request/url_request_context_getter.h" |
+ |
+namespace { |
+const char kDefaultGroupName[] = "default"; |
+ |
+// Per |
+// https://greenbytes.de/tech/webdav/draft-reschke-http-jfv-02.html#rfc.section.4 |
+// assuming |normalized_header| is the result of completing step 1. |
+std::unique_ptr<base::Value> ParseJFV( |
+ const std::string& normalized_header_value) { |
+ std::string value = "[" + normalized_header_value + "]"; |
+ return base::JSONReader::Read(value); |
+} |
+ |
+bool IsOriginSecure(const GURL& url) { |
+ return url.SchemeIsCryptographic(); |
+} |
+} // namespace |
+ |
+namespace net { |
+ |
+// static |
+ReportingService::Policy ReportingService::Policy::GetDefault() { |
+ Policy policy; |
+ |
+ policy.endpoint_lifetime = base::TimeDelta::FromDays(7); |
+ policy.endpoint_backoff.num_errors_to_ignore = 0; |
+ policy.endpoint_backoff.initial_delay_ms = 5 * 1000; |
+ policy.endpoint_backoff.multiply_factor = 2.0; |
+ policy.endpoint_backoff.jitter_factor = 0.1; |
+ policy.endpoint_backoff.maximum_backoff_ms = 60 * 60 * 1000; |
+ policy.endpoint_backoff.entry_lifetime_ms = 7 * 24 * 60 * 60 * 1000; |
+ policy.endpoint_backoff.always_use_initial_delay = false; |
+ policy.max_endpoint_failures = 5; |
+ policy.max_endpoint_count = 100; |
+ |
+ policy.report_lifetime = base::TimeDelta::FromDays(2); |
+ policy.max_report_failures = 5; |
+ policy.max_report_count = 100; |
+ |
+ policy.persist_reports_across_network_changes = false; |
+ |
+ return policy; |
+} |
+ |
+ReportingService::ReportingService(const Policy& policy) |
+ : policy_(policy), clock_(new base::DefaultTickClock()) {} |
+ |
+ReportingService::~ReportingService() { |
+ for (auto& report : reports_) |
+ HistogramReportInternal(REPORT_FATE_SHUTDOWN, *report); |
+ size_t endpoint_count = endpoints_.size(); |
+ for (size_t i = 0; i < endpoint_count; ++i) |
+ HistogramEndpoint(ENDPOINT_FATE_SHUTDOWN); |
+} |
+ |
+void ReportingService::set_uploader( |
+ std::unique_ptr<ReportingUploader> uploader) { |
+ uploader_ = std::move(uploader); |
+} |
+ |
+void ReportingService::QueueReport(std::unique_ptr<base::Value> body, |
+ const GURL& url, |
+ const GURL& origin, |
+ const std::string& group, |
+ const std::string& type) { |
+ auto report = base::MakeUnique<ReportingReport>(); |
+ report->body = std::move(body); |
+ report->url = url.GetAsReferrer(); |
+ report->origin = origin; |
+ report->group = group; |
+ report->type = type; |
+ report->timestamp = clock_->NowTicks(); |
+ report->attempts = 0; |
+ report->pending = false; |
+ reports_.push_back(std::move(report)); |
+} |
+ |
+void ReportingService::ProcessHeader(const GURL& origin, |
+ const std::string& header_value) { |
+ if (!IsOriginSecure(origin)) { |
+ HistogramHeader(HEADER_FATE_REJECTED_INSECURE_ORIGIN); |
+ return; |
+ } |
+ |
+ std::vector<std::string> errors; |
+ std::vector<EndpointTuple> tuples; |
+ if (!EndpointTuple::FromHeader(header_value, &tuples, &errors)) { |
+ HistogramHeader(HEADER_FATE_REJECTED_INVALID_JSON); |
+ return; |
+ } |
+ |
+ if (errors.empty()) |
+ HistogramHeader(HEADER_FATE_ACCEPTED); |
+ else |
+ HistogramHeader(HEADER_FATE_ACCEPTED_WITH_INVALID_ENDPOINT); |
+ |
+ // TODO: Plumb these out to the DevTools console somehow. |
+ for (const std::string& error : errors) { |
+ LOG(WARNING) << "Origin " << origin.spec() << " sent " |
+ << "Report-To header with error: " << error << ": " |
+ << header_value; |
+ } |
+ |
+ for (auto& tuple : tuples) |
+ ProcessEndpointTuple(origin, tuple); |
+ |
+ CollectGarbage(); |
+} |
+ |
+void ReportingService::SendReports() { |
+ if (!uploader_) |
+ return; |
+ |
+ std::map<Endpoint*, std::vector<ReportingReport*>> endpoint_reports; |
+ for (auto& report : reports_) { |
+ // If the report is already contained in another pending upload, don't |
+ // upload it twice. |
+ if (report->pending) |
+ continue; |
+ Endpoint* endpoint = FindEndpointForReport(*report); |
+ // If there's no available endpoint for the report, leave it for later. |
+ if (!endpoint) |
+ continue; |
+ // If the chosen endpoint is pending, don't start another upload; let this |
+ // report go in the next upload instead. |
+ if (endpoint->pending) |
+ continue; |
+ |
+ report->pending = true; |
+ endpoint_reports[endpoint].push_back(report.get()); |
+ } |
+ |
+ for (auto& pair : endpoint_reports) { |
+ Endpoint* endpoint = pair.first; |
+ const std::vector<ReportingReport*>& reports = pair.second; |
+ |
+ std::string json = SerializeReports(reports); |
+ |
+ uploader_->AttemptDelivery( |
+ endpoint->url, json, |
+ base::Bind(&ReportingService::OnDeliveryAttemptComplete, |
+ base::Unretained(this), |
+ base::MakeUnique<Delivery>(endpoint->url, reports))); |
+ |
+ endpoint->last_used = clock_->NowTicks(); |
+ |
+ for (auto& report : reports) |
+ ++report->attempts; |
+ |
+ HistogramDeliveryContent(reports.size(), json.length()); |
+ } |
+} |
+ |
+void ReportingService::set_clock_for_testing( |
+ std::unique_ptr<base::TickClock> clock) { |
+ clock_ = std::move(clock); |
+} |
+ |
+bool ReportingService::HasEndpointForTesting(const GURL& endpoint_url) { |
+ return GetEndpointByURL(endpoint_url); |
+} |
+ |
+bool ReportingService::HasClientForTesting(const GURL& endpoint_url, |
+ const GURL& origin) { |
+ Endpoint* endpoint = GetEndpointByURL(endpoint_url); |
+ if (!endpoint) |
+ return false; |
+ return endpoint->clients.count(origin) > 0; |
+} |
+ |
+int ReportingService::GetEndpointFailuresForTesting(const GURL& endpoint_url) { |
+ Endpoint* endpoint = GetEndpointByURL(endpoint_url); |
+ if (!endpoint) |
+ return -1; |
+ return endpoint->backoff.failure_count(); |
+} |
+ |
+void ReportingService::CollectGarbageForTesting() { |
+ CollectGarbage(); |
+} |
+ |
+ReportingService::Client::Client(const GURL& origin, |
+ bool subdomains, |
+ const std::string& group, |
+ base::TimeDelta ttl, |
+ base::TimeTicks creation) |
+ : origin(origin), |
+ subdomains(subdomains), |
+ group(group), |
+ ttl(ttl), |
+ creation(creation) {} |
+ |
+ReportingService::Endpoint::Endpoint(const GURL& url, |
+ const BackoffEntry::Policy& backoff_policy, |
+ base::TickClock* clock) |
+ : url(url), |
+ backoff(&backoff_policy, clock), |
+ last_used(clock->NowTicks()), |
+ pending(false) {} |
+ReportingService::Endpoint::~Endpoint() {} |
+ |
+bool ReportingService::Endpoint::is_expired(base::TimeTicks now) const { |
+ for (auto& pair : clients) |
+ if (!pair.second.is_expired(now)) |
+ return false; |
+ return true; |
+} |
+ |
+// static |
+bool ReportingService::EndpointTuple::FromDictionary( |
+ const base::DictionaryValue& dictionary, |
+ EndpointTuple* tuple_out, |
+ std::string* error_out) { |
+ if (!dictionary.HasKey("url")) { |
+ *error_out = "url missing"; |
+ return false; |
+ } |
+ std::string url_string; |
+ if (!dictionary.GetString("url", &url_string)) { |
+ *error_out = "url not a string"; |
+ return false; |
+ } |
+ tuple_out->url = GURL(url_string); |
+ |
+ tuple_out->group = kDefaultGroupName; |
+ if (dictionary.HasKey("group")) { |
+ if (!dictionary.GetString("group", &tuple_out->group)) { |
+ *error_out = "group present but not a string"; |
+ return false; |
+ } |
+ } |
+ |
+ tuple_out->subdomains = false; |
+ if (dictionary.HasKey("includeSubdomains")) { |
+ if (!dictionary.GetBoolean("includeSubdomains", &tuple_out->subdomains)) { |
+ *error_out = "includeSubdomains present but not boolean"; |
+ return false; |
+ } |
+ } |
+ |
+ if (!dictionary.HasKey("max-age")) { |
+ *error_out = "max-age missing"; |
+ return false; |
+ } |
+ int ttl_sec; |
+ if (!dictionary.GetInteger("max-age", &ttl_sec)) { |
+ *error_out = "max-age not an integer"; |
+ return false; |
+ } |
+ tuple_out->ttl = base::TimeDelta::FromSeconds(ttl_sec); |
+ |
+ return true; |
+} |
+ |
+// static |
+bool ReportingService::EndpointTuple::FromHeader( |
+ const std::string& header, |
+ std::vector<ReportingService::EndpointTuple>* tuples_out, |
+ std::vector<std::string>* errors_out) { |
+ tuples_out->clear(); |
+ errors_out->clear(); |
+ |
+ std::unique_ptr<base::Value> value(ParseJFV(header)); |
+ if (!value) { |
+ errors_out->push_back("failed to parse JSON field value."); |
+ return false; |
+ } |
+ |
+ 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 (!IsOriginSecure(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_out->push_back(tuple); |
+ } |
+ return true; |
+} |
+ |
+std::string ReportingService::EndpointTuple::ToString() const { |
+ return "(url=" + url.spec() + ", subdomains=" + |
+ (subdomains ? "true" : "false") + ", ttl=" + |
+ base::Int64ToString(ttl.InSeconds()) + "s" + ", group=" + group + ")"; |
+} |
+ |
+ReportingService::Delivery::Delivery( |
+ const GURL& endpoint_url, |
+ const std::vector<ReportingReport*>& reports) |
+ : endpoint_url(endpoint_url), reports(reports) {} |
+ |
+ReportingService::Delivery::~Delivery() {} |
+ |
+void ReportingService::ProcessEndpointTuple(const GURL& origin, |
+ const EndpointTuple& tuple) { |
+ Endpoint* endpoint = GetEndpointByURL(tuple.url); |
+ |
+ bool endpoint_exists = endpoint; |
+ bool client_exists = endpoint && endpoint->clients.count(origin) > 0; |
+ |
+ HistogramHeaderEndpointInternal(endpoint_exists, client_exists, tuple.ttl); |
+ |
+ if (client_exists) |
+ endpoint->clients.erase(origin); |
+ |
+ if (tuple.ttl <= base::TimeDelta()) |
+ return; |
+ |
+ if (!endpoint_exists) { |
+ endpoint = new Endpoint(tuple.url, policy_.endpoint_backoff, clock_.get()); |
+ endpoints_.insert(std::make_pair(tuple.url, base::WrapUnique(endpoint))); |
+ } |
+ |
+ if (!client_exists) |
+ HistogramClient(tuple.ttl); |
+ |
+ Client client(origin, tuple.subdomains, tuple.group, tuple.ttl, |
+ clock_->NowTicks()); |
+ endpoint->clients.insert(std::make_pair(origin, client)); |
+} |
+ |
+void ReportingService::OnDeliveryAttemptComplete( |
+ const std::unique_ptr<Delivery>& delivery, |
+ ReportingUploader::Outcome outcome) { |
+ // Note: HistogramDeliveryOutcome is called from within the uploader since it |
+ // has access to the net error code and HTTP response. |
+ |
+ for (auto report : delivery->reports) { |
+ DCHECK(report->pending); |
+ report->pending = false; |
+ } |
+ |
+ Endpoint* endpoint = GetEndpointByURL(delivery->endpoint_url); |
+ if (endpoint) { |
+ endpoint->backoff.InformOfRequest(outcome == ReportingUploader::SUCCESS); |
+ endpoint->pending = false; |
+ } |
+ |
+ switch (outcome) { |
+ case ReportingUploader::SUCCESS: |
+ for (auto report : delivery->reports) { |
+ HistogramReportInternal(REPORT_FATE_DELIVERED, *report); |
+ DequeueReport(report); |
+ } |
+ break; |
+ case ReportingUploader::FAILURE: |
+ // Reports have been marked not-pending and can be retried later. |
+ // BackoffEntry has been informed of failure. |
+ break; |
+ case ReportingUploader::REMOVE_ENDPOINT: |
+ // Note: This is not specified, but seems the obvious intention. |
+ if (endpoint) { |
+ HistogramEndpoint(ENDPOINT_FATE_REQUESTED_REMOVAL); |
+ endpoints_.erase(delivery->endpoint_url); |
+ } |
+ break; |
+ } |
+ |
+ CollectGarbage(); |
+} |
+ |
+void ReportingService::CollectGarbage() { |
+ base::TimeTicks now = clock_->NowTicks(); |
+ |
+ { |
+ std::vector<ReportVector::iterator> to_erase; |
+ for (auto it = reports_.begin(); it != reports_.end(); ++it) { |
+ ReportingReport* report = it->get(); |
+ if (report->pending) |
+ continue; |
+ if (policy_.max_report_failures > 0 && |
+ report->attempts >= policy_.max_report_failures) { |
+ HistogramReportInternal(REPORT_FATE_FAILED, *report); |
+ to_erase.push_back(it); |
+ } else if (!policy_.report_lifetime.is_zero() && |
+ now - report->timestamp > policy_.report_lifetime) { |
+ HistogramReportInternal(REPORT_FATE_EXPIRED, *report); |
+ to_erase.push_back(it); |
+ } |
+ } |
+ |
+ for (auto it = reports_.begin(); |
+ it != reports_.end() && |
+ reports_.size() - to_erase.size() > policy_.max_report_count; |
+ ++it) { |
+ if (it->get()->pending) |
+ continue; |
+ |
+ HistogramReportInternal(REPORT_FATE_EVICTED, *it->get()); |
+ to_erase.push_back(it); |
+ } |
+ |
+ for (auto it : to_erase) |
+ reports_.erase(it); |
+ } |
+ |
+ { |
+ std::vector<EndpointMap::iterator> to_erase; |
+ for (auto it = endpoints_.begin(); it != endpoints_.end(); ++it) { |
+ Endpoint* endpoint = it->second.get(); |
+ if (endpoint->pending) |
+ continue; |
+ if (endpoint->is_expired(now)) { |
+ HistogramEndpoint(ENDPOINT_FATE_EXPIRED); |
+ to_erase.push_back(it); |
+ } else if (!policy_.endpoint_lifetime.is_zero() && |
+ now - endpoint->last_used > policy_.endpoint_lifetime) { |
+ HistogramEndpoint(ENDPOINT_FATE_UNUSED); |
+ to_erase.push_back(it); |
+ // Don't remove failed endpoints until the BackoffEntry okays it, to |
+ // avoid |
+ // hammering failing endpoints by removing and re-adding them |
+ // constantly. |
+ } else if (policy_.max_endpoint_failures >= 0 && |
+ endpoint->backoff.CanDiscard() && |
+ endpoint->backoff.failure_count() > |
+ policy_.max_endpoint_failures) { |
+ HistogramEndpoint(ENDPOINT_FATE_FAILED); |
+ to_erase.push_back(it); |
+ } |
+ } |
+ |
+ while (endpoints_.size() - to_erase.size() > policy_.max_endpoint_count) { |
+ auto oldest_it = endpoints_.end(); |
+ |
+ for (auto it = endpoints_.begin(); it != endpoints_.end(); ++it) { |
+ if (it->second->pending) |
+ continue; |
+ if (oldest_it == endpoints_.end() || |
+ it->second->last_used < oldest_it->second->last_used) { |
+ oldest_it = it; |
+ } |
+ } |
+ |
+ if (oldest_it == endpoints_.end()) |
+ break; |
+ |
+ HistogramEndpoint(ENDPOINT_FATE_EVICTED); |
+ to_erase.push_back(oldest_it); |
+ |
+ // Gross kludge: Keep us from picking this endpoint again. |
+ oldest_it->second->pending = true; |
+ } |
+ |
+ for (auto it : to_erase) |
+ endpoints_.erase(it); |
+ } |
+} |
+ |
+ReportingService::Endpoint* ReportingService::FindEndpointForReport( |
+ const ReportingReport& report) { |
+ // TODO: This is O(count of all endpoints, regardless of client origins). |
+ // TODO: The spec doesn't prioritize *.bar.foo.com over *.foo.com when |
+ // choosing which endpoint to upload a report for baz.bar.foo.com to. |
+ // That seems wrong, but we need clarification on the spec end. |
+ for (auto& pair : endpoints_) { |
+ Endpoint* endpoint = pair.second.get(); |
+ if (endpoint->is_expired(clock_->NowTicks()) || |
+ endpoint->backoff.ShouldRejectRequest() || |
+ !DoesEndpointMatchReport(*endpoint, report)) { |
+ continue; |
+ } |
+ return endpoint; |
+ } |
+ return nullptr; |
+} |
+ |
+bool ReportingService::DoesEndpointMatchReport(const Endpoint& endpoint, |
+ const ReportingReport& report) { |
+ for (auto& pair : endpoint.clients) { |
+ const Client& 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; |
+} |
+ |
+std::string ReportingService::SerializeReports( |
+ const std::vector<ReportingReport*>& reports) { |
+ base::ListValue collection; |
+ for (auto& report : reports) { |
+ 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; |
+} |
+ |
+ReportingService::Endpoint* ReportingService::GetEndpointByURL( |
+ const GURL& url) { |
+ auto it = endpoints_.find(url); |
+ if (it == endpoints_.end()) |
+ return nullptr; |
+ return it->second.get(); |
+} |
+ |
+void ReportingService::DequeueReport(ReportingReport* report) { |
+ // TODO: This is O(N). |
+ for (auto it = reports_.begin(); it != reports_.end(); ++it) { |
+ if (it->get() == report) { |
+ reports_.erase(it); |
+ return; |
+ } |
+ } |
+} |
+ |
+void ReportingService::HistogramHeaderEndpointInternal( |
+ bool endpoint_exists, |
+ bool client_exists, |
+ base::TimeDelta ttl) const { |
+ HeaderEndpointFate fate; |
+ if (ttl > base::TimeDelta()) { |
+ if (client_exists) |
+ fate = HEADER_ENDPOINT_FATE_SET_CLIENT_UPDATED; |
+ else if (endpoint_exists) |
+ fate = HEADER_ENDPOINT_FATE_SET_CLIENT_CREATED; |
+ else |
+ fate = HEADER_ENDPOINT_FATE_SET_ENDPOINT_CREATED; |
+ } else { |
+ if (client_exists) |
+ fate = HEADER_ENDPOINT_FATE_CLEAR_CLIENT_REMOVED; |
+ else if (endpoint_exists) |
+ fate = HEADER_ENDPOINT_FATE_CLEAR_NO_CLIENT; |
+ else |
+ fate = HEADER_ENDPOINT_FATE_CLEAR_NO_ENDPOINT; |
+ } |
+ HistogramHeaderEndpoint(fate, ttl); |
+} |
+ |
+void ReportingService::HistogramReportInternal( |
+ ReportFate fate, |
+ const ReportingReport& report) const { |
+ HistogramReport(fate, clock_->NowTicks() - report.timestamp, report.attempts); |
+} |
+ |
+} // namespace net |