Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(860)

Unified Diff: net/reporting/reporting_service.cc

Issue 2249213002: [OBSOLETE] Reporting: Initial implementation. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Try fixing unittest compile error on Android? Created 3 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « net/reporting/reporting_service.h ('k') | net/reporting/reporting_service_unittest.cc » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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..fa7adcc1b82642e69aa082835906c7e9ce8b7a07
--- /dev/null
+++ b/net/reporting/reporting_service.cc
@@ -0,0 +1,626 @@
+// 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/metrics/histogram_macros.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/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 {
+
+ReportingService::Policy::Policy()
+ : endpoint_lifetime(base::TimeDelta::FromDays(7)),
+ max_endpoint_failures(5),
+ max_endpoint_count(100),
+ report_lifetime(base::TimeDelta::FromHours(1)),
+ max_report_failures(5),
+ max_report_count(100),
+ persist_reports_across_network_changes(false) {
+ endpoint_backoff.num_errors_to_ignore = 0;
+ endpoint_backoff.initial_delay_ms = 5 * 1000;
+ endpoint_backoff.multiply_factor = 2.0;
+ endpoint_backoff.jitter_factor = 0.1;
+ endpoint_backoff.maximum_backoff_ms = 60 * 60 * 1000;
+ endpoint_backoff.entry_lifetime_ms = 7 * 24 * 60 * 60 * 1000;
+ endpoint_backoff.always_use_initial_delay = false;
+}
+
+// static
+ReportingService::Policy ReportingService::Policy::GetNonBrowserDefault() {
+ Policy policy;
+ // In non-browser contexts, there is less concern that Reporting could
+ // accidentally track users across networks, so don't discard reports as
+ // readily.
+ policy.report_lifetime = base::TimeDelta::FromDays(2);
+ policy.persist_reports_across_network_changes = true;
+ return policy;
+}
+
+struct ReportingService::Client {
+ public:
+ 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) {}
+
+ GURL origin;
+ bool subdomains;
+ std::string group;
+ base::TimeDelta ttl;
+ base::TimeTicks creation;
+
+ bool is_expired(base::TimeTicks now) const { return creation + ttl < now; }
+};
+
+struct ReportingService::Endpoint {
+ public:
+ Endpoint(const GURL& url,
+ const BackoffEntry::Policy& backoff_policy,
+ base::TickClock* clock)
+ : url(url),
+ backoff(&backoff_policy, clock),
+ last_used(clock->NowTicks()),
+ pending(false) {}
+ ~Endpoint() {}
+
+ const GURL url;
+
+ BackoffEntry backoff;
+ // For LRU eviction of endpoints.
+ base::TimeTicks last_used;
+ // Whether we currently have an upload in progress to this endpoint.
+ bool pending;
+
+ // Map from client.origin to client.
+ std::map<GURL, Client> clients;
+
+ bool is_expired(base::TimeTicks now) const {
+ for (auto& pair : clients)
+ if (!pair.second.is_expired(now))
+ return false;
+ return true;
+ }
+};
+
+class ReportingService::Report {
+ public:
+ Report() = default;
+ ~Report() = default;
+
+ // Report body. Can be any valid JSON. Passed to endpoint unmodified.
+ std::unique_ptr<base::Value> body;
+
+ // URL of content that triggered report. Passed to endpoint unmodified.
+ GURL url;
+
+ // Origin of content that triggered report. Should be url.GetOrigin(), but
+ // the spec doesn't guarantee this, and allows callers to provide URL and
+ // origin separately, so they are separate fields for now. Passed to
+ // endpoint unmodified.
+ GURL origin;
+
+ // Report group. Used to select endpoints for upload.
+ std::string group;
+
+ // Report type. Passed to endpoint unmodified.
+ std::string type;
+
+ // Timestamp when the report was queued. Passed to endpoint as a relative
+ // age.
+ base::TimeTicks queued;
+
+ // Number of delivery attempts made.
+ size_t attempts;
+
+ // Whether this report is currently part of a pending delivery attempt.
+ bool pending;
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(Report);
+};
+
+struct ReportingService::EndpointTuple {
+ GURL url;
+ bool subdomains;
+ base::TimeDelta ttl;
+ std::string group;
+
+ static bool FromDictionary(const base::DictionaryValue& dictionary,
+ EndpointTuple* tuple_out,
+ std::string* error_out);
+ static bool FromHeader(const std::string& header,
+ std::vector<EndpointTuple>* tuples_out,
+ std::vector<std::string>* errors_out);
+};
+
+struct ReportingService::Delivery {
+ Delivery(const GURL& endpoint_url, const std::vector<Report*>& reports)
+ : endpoint_url(endpoint_url), reports(reports) {}
+ ~Delivery() = default;
+
+ const GURL& endpoint_url;
+ const std::vector<Report*> reports;
+};
+
+ReportingService::ReportingService(const Policy& policy)
+ : policy_(policy), clock_(new base::DefaultTickClock()) {
+ NetworkChangeNotifier::AddNetworkChangeObserver(this);
+}
+
+ReportingService::~ReportingService() {
+ NetworkChangeNotifier::RemoveNetworkChangeObserver(this);
+}
+
+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<Report>();
+ report->body = std::move(body);
+ report->url = url.GetAsReferrer();
+ report->origin = origin;
+ report->group = group;
+ report->type = type;
+ report->queued = clock_->NowTicks();
+ report->attempts = 0;
+ report->pending = false;
+ reports_.push_back(std::move(report));
+
+ CollectGarbage();
+}
+
+void ReportingService::ProcessHeader(const GURL& origin,
+ const std::string& header_value) {
+ if (!IsOriginSecure(origin))
+ return;
+
+ std::vector<std::string> errors;
+ std::vector<EndpointTuple> tuples;
+ if (!EndpointTuple::FromHeader(header_value, &tuples, &errors))
+ return;
+
+ // 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;
+
+ base::TimeTicks now = clock_->NowTicks();
+
+ std::map<Endpoint*, std::vector<Report*>> 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<Report*>& 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 = now;
+
+ for (auto& report : reports)
+ ++report->attempts;
+ }
+}
+
+void ReportingService::OnNetworkChanged(
+ NetworkChangeNotifier::ConnectionType connection_type) {
+ if (policy_.persist_reports_across_network_changes)
+ return;
+
+ reports_.clear();
+}
+
+void ReportingService::set_clock_for_testing(
+ std::unique_ptr<base::TickClock> clock) {
+ clock_ = std::move(clock);
+}
+
+size_t ReportingService::GetReportCountForTesting() {
+ return reports_.size();
+}
+
+bool ReportingService::HasEndpointForTesting(const GURL& endpoint_url) {
+ return GetEndpointByURL(endpoint_url) != nullptr;
+}
+
+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();
+}
+
+// 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;
+}
+
+void ReportingService::ProcessEndpointTuple(const GURL& origin,
+ const EndpointTuple& tuple) {
+ Endpoint* endpoint = GetEndpointByURL(tuple.url);
+
+ bool endpoint_exists = endpoint != nullptr;
+ bool client_exists = endpoint && endpoint->clients.count(origin) > 0;
+
+ 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)));
+ }
+
+ base::TimeTicks now = clock_->NowTicks();
+
+ Client client(origin, tuple.subdomains, tuple.group, tuple.ttl, now);
+ endpoint->clients.insert(std::make_pair(origin, client));
+}
+
+void ReportingService::OnDeliveryAttemptComplete(
+ const std::unique_ptr<Delivery>& delivery,
+ ReportingUploader::Outcome outcome) {
+ 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)
+ 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)
+ endpoints_.erase(delivery->endpoint_url);
+ break;
+ }
+
+ CollectGarbage();
+}
+
+void ReportingService::CollectGarbage() {
+ base::TimeTicks now = clock_->NowTicks();
+
+ {
+ std::vector<Report*> to_dequeue;
+
+ for (auto it = reports_.begin(); it != reports_.end(); ++it) {
+ Report* report = it->get();
+ base::TimeDelta age = now - report->queued;
+
+ if (report->pending)
+ continue;
+
+ if (report->attempts > policy_.max_report_failures ||
+ age > policy_.report_lifetime) {
+ to_dequeue.push_back(report);
+ }
+ }
+
+ for (auto it = reports_.begin();
+ it != reports_.end() &&
+ reports_.size() - to_dequeue.size() > policy_.max_report_count;
+ ++it) {
+ if (it->get()->pending)
+ continue;
+
+ to_dequeue.push_back(it->get());
+ }
+
+ for (auto it : to_dequeue)
+ DequeueReport(it);
+ }
+
+ // TODO: Individually garbage-collect clients.
+
+ {
+ std::vector<EndpointMap::iterator> to_erase;
+ for (auto it = endpoints_.begin(); it != endpoints_.end(); ++it) {
+ Endpoint* endpoint = it->second.get();
+
+ if (endpoint->pending)
+ continue;
+
+ // Don't remove failed endpoints until the BackoffEntry okays it, to
+ // avoid hammering failing endpoints by removing and re-adding them
+ // constantly.
+ if (endpoint->is_expired(now) ||
+ now - endpoint->last_used > policy_.endpoint_lifetime ||
+ (endpoint->backoff.CanDiscard() &&
+ endpoint->backoff.failure_count() > policy_.max_endpoint_failures)) {
+ 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;
+
+ to_erase.push_back(oldest_it);
+
+ // Gross kludge: Keep us from choosing this endpoint again.
+ oldest_it->second->pending = true;
+ }
+
+ for (auto it : to_erase)
+ endpoints_.erase(it);
+ }
+}
+
+ReportingService::Endpoint* ReportingService::FindEndpointForReport(
+ const Report& 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 Report& 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<Report*>& reports) {
+ base::ListValue collection;
+ for (auto& report : reports) {
+ std::unique_ptr<base::DictionaryValue> data(new base::DictionaryValue());
+ data->SetInteger("age",
+ (clock_->NowTicks() - report->queued).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(Report* report) {
+ // TODO: This is O(N).
+ for (auto it = reports_.begin(); it != reports_.end(); ++it) {
+ if (it->get() == report) {
+ reports_.erase(it);
+ return;
+ }
+ }
+}
+
+} // namespace net
« no previous file with comments | « net/reporting/reporting_service.h ('k') | net/reporting/reporting_service_unittest.cc » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698