| 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
|
|
|