diff options
author | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2017-07-12 14:07:37 +0200 |
---|---|---|
committer | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2017-07-17 10:29:26 +0000 |
commit | ec02ee4181c49b61fce1c8fb99292dbb8139cc90 (patch) | |
tree | 25cde714b2b71eb639d1cd53f5a22e9ba76e14ef /chromium/net/reporting | |
parent | bb09965444b5bb20b096a291445170876225268d (diff) | |
download | qtwebengine-chromium-ec02ee4181c49b61fce1c8fb99292dbb8139cc90.tar.gz |
BASELINE: Update Chromium to 59.0.3071.134
Change-Id: Id02ef6fb2204c5fd21668a1c3e6911c83b17585a
Reviewed-by: Alexandru Croitor <alexandru.croitor@qt.io>
Diffstat (limited to 'chromium/net/reporting')
42 files changed, 4811 insertions, 0 deletions
diff --git a/chromium/net/reporting/README.md b/chromium/net/reporting/README.md new file mode 100644 index 00000000000..82d663a25cf --- /dev/null +++ b/chromium/net/reporting/README.md @@ -0,0 +1,77 @@ +# Reporting + +Reporting is a central mechanism for sending out-of-band error reports +to origins from various other components (e.g. HTTP Public Key Pinning, +Interventions, or Content Security Policy could potentially use it). + +The parts of it that are exposed to the web platform are specified in +the [draft spec](http://wicg.github.io/reporting/). This document +assumes that you've read that one. + +## Reporting in Chromium + +Reporting is implemented as part of the network stack in Chromium, such +that it can be used by other parts of the network stack (e.g. HPKP) or +by non-browser embedders as well as by Chromium. + +Almost all of Reporting lives in `//net/reporting`; there is a small +amount of code in `//chrome/browser/net` to set up Reporting in +profiles and provide a persistent store for reports and endpoints +across browser restarts. + +### Inside `//net` + +* The top-level class is the *`ReportingService`*. This lives in the + `URLRequestContext`, and provides the high-level operations used by + other parts of `//net` and other components: queueing reports, + handling configuration headers, clearing browsing data, and so on. + + * Within `ReportingService` lives *`ReportingContext`*, which in turn + contains the inner workings of Reporting, spread across several + classes: + + * The *`ReportingCache`* stores undelivered reports and unexpired + endpoint configurations. + + * The *`ReportingHeaderParser`* parses `Report-To:` headers and + updates the `Cache` accordingly. + + * The *`ReportingDeliveryAgent`* reads reports from the `Cache`, + decides which endpoints to deliver them to, and attempts to + do so. It uses a couple of helper classes: + + * The *`ReportingUploader`* does the low-level work of delivering + reports: accepts a URL and JSON from the `DeliveryAgent`, + creates a `URLRequest`, and parses the result. + + * The *`ReportingEndpointManager`* keeps track of which endpoints + are in use, and manages exponential backoff (using + `BackoffEntry`) for failing endpoints. + + * The *`ReportingGarbageCollector`* periodically examines the + `Cache` and removes reports that have remained undelivered for too + long, or that have failed delivery too many times. + + * The *`ReportingSerializer`* reads the `Cache` and serializes it + into a `base::Value` for persistent storage (in Chromium, as a + pref); it can also deserialize a serialized `Value` back into the + `Cache`. + + * The *`ReportingBrowsingDataRemover`* examines the `Cache` upon + request and removes browsing data (reports and endpoints) of + selected types and origins. + +### Outside `//net` + +* In `*ProfileImplIOData*::InitializeInternal`, the `ReportingService` + is created and set in the `URLRequestContext`, where the net stack + can use it. + + (There is currently no interface to Reporting besides "hop over to + the IO thread and poke the `ReportingService` in your favorite + `URLRequestContext`", but that should change as various components + need to queue reports.) + +* *`ChromeReportingDelegate`* implements `ReportingDelegate` and plumbs + the persistent data interface into prefs. It lives in + `//chrome/browser/net`. diff --git a/chromium/net/reporting/reporting_browsing_data_remover.cc b/chromium/net/reporting/reporting_browsing_data_remover.cc new file mode 100644 index 00000000000..5041a47487c --- /dev/null +++ b/chromium/net/reporting/reporting_browsing_data_remover.cc @@ -0,0 +1,59 @@ +// Copyright 2017 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_browsing_data_remover.h" + +#include <vector> + +#include "net/reporting/reporting_cache.h" +#include "net/reporting/reporting_context.h" +#include "net/reporting/reporting_report.h" + +namespace net { + +// static +void ReportingBrowsingDataRemover::RemoveBrowsingData( + ReportingContext* context, + int data_type_mask, + base::Callback<bool(const GURL&)> origin_filter) { + ReportingCache* cache = context->cache(); + bool remove_reports = (data_type_mask & DATA_TYPE_REPORTS) != 0; + bool remove_clients = (data_type_mask & DATA_TYPE_CLIENTS) != 0; + + if (origin_filter.is_null()) { + if (remove_reports) + cache->RemoveAllReports(); + if (remove_clients) + cache->RemoveAllClients(); + return; + } + + if (remove_reports) { + std::vector<const ReportingReport*> all_reports; + cache->GetReports(&all_reports); + + std::vector<const ReportingReport*> reports_to_remove; + for (const ReportingReport* report : all_reports) { + if (origin_filter.Run(report->url)) + reports_to_remove.push_back(report); + } + + cache->RemoveReports(reports_to_remove); + } + + if (remove_clients) { + std::vector<const ReportingClient*> all_clients; + cache->GetClients(&all_clients); + + std::vector<const ReportingClient*> clients_to_remove; + for (const ReportingClient* client : all_clients) { + if (origin_filter.Run(client->origin.GetURL())) + clients_to_remove.push_back(client); + } + + cache->RemoveClients(clients_to_remove); + } +} + +} // namespace net diff --git a/chromium/net/reporting/reporting_browsing_data_remover.h b/chromium/net/reporting/reporting_browsing_data_remover.h new file mode 100644 index 00000000000..db2bfb6df19 --- /dev/null +++ b/chromium/net/reporting/reporting_browsing_data_remover.h @@ -0,0 +1,36 @@ +// Copyright 2017 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. + +#ifndef NET_REPORTING_REPORTING_BROWSING_DATA_REMOVER_H_ +#define NET_REPORTING_REPORTING_BROWSING_DATA_REMOVER_H_ + +#include "base/callback.h" +#include "base/macros.h" +#include "net/base/net_export.h" +#include "url/gurl.h" + +namespace net { + +class ReportingContext; + +// Clears browsing data (reports and clients) from the Reporting system. +class NET_EXPORT ReportingBrowsingDataRemover { + public: + enum DataType { + DATA_TYPE_REPORTS = 0x1, + DATA_TYPE_CLIENTS = 0x2, + }; + + static void RemoveBrowsingData( + ReportingContext* context, + int data_type_mask, + base::Callback<bool(const GURL&)> origin_filter); + + private: + DISALLOW_IMPLICIT_CONSTRUCTORS(ReportingBrowsingDataRemover); +}; + +} // namespace net + +#endif // NET_REPORTING_REPORTING_BROWSING_DATA_REMOVER_H_ diff --git a/chromium/net/reporting/reporting_browsing_data_remover_unittest.cc b/chromium/net/reporting/reporting_browsing_data_remover_unittest.cc new file mode 100644 index 00000000000..349c5d531ac --- /dev/null +++ b/chromium/net/reporting/reporting_browsing_data_remover_unittest.cc @@ -0,0 +1,193 @@ +// Copyright 2017 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_browsing_data_remover.h" + +#include <string> + +#include "base/bind.h" +#include "base/memory/ptr_util.h" +#include "base/test/simple_test_tick_clock.h" +#include "net/reporting/reporting_cache.h" +#include "net/reporting/reporting_client.h" +#include "net/reporting/reporting_report.h" +#include "net/reporting/reporting_test_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace net { +namespace { + +class ReportingBrowsingDataRemoverTest : public ReportingTestBase { + protected: + void RemoveBrowsingData(bool remove_reports, + bool remove_clients, + std::string host) { + int data_type_mask = 0; + if (remove_reports) + data_type_mask |= ReportingBrowsingDataRemover::DATA_TYPE_REPORTS; + if (remove_clients) + data_type_mask |= ReportingBrowsingDataRemover::DATA_TYPE_CLIENTS; + + base::Callback<bool(const GURL&)> origin_filter; + if (!host.empty()) { + origin_filter = + base::Bind(&ReportingBrowsingDataRemoverTest::HostIs, host); + } + + ReportingBrowsingDataRemover::RemoveBrowsingData(context(), data_type_mask, + origin_filter); + } + + static bool HostIs(std::string host, const GURL& url) { + return url.host() == host; + } + + size_t report_count() { + std::vector<const ReportingReport*> reports; + cache()->GetReports(&reports); + return reports.size(); + } + + size_t client_count() { + std::vector<const ReportingClient*> clients; + cache()->GetClients(&clients); + return clients.size(); + } + + const GURL kUrl1_ = GURL("https://origin1/path"); + const GURL kUrl2_ = GURL("https://origin2/path"); + const url::Origin kOrigin1_ = url::Origin(kUrl1_); + const url::Origin kOrigin2_ = url::Origin(kUrl2_); + const GURL kEndpoint_ = GURL("https://endpoint/"); + const std::string kGroup_ = "group"; + const std::string kType_ = "default"; +}; + +TEST_F(ReportingBrowsingDataRemoverTest, RemoveNothing) { + cache()->AddReport(kUrl1_, kGroup_, kType_, + base::MakeUnique<base::DictionaryValue>(), + tick_clock()->NowTicks(), 0); + cache()->AddReport(kUrl2_, kGroup_, kType_, + base::MakeUnique<base::DictionaryValue>(), + tick_clock()->NowTicks(), 0); + cache()->SetClient(kOrigin1_, kEndpoint_, + ReportingClient::Subdomains::EXCLUDE, kGroup_, + tick_clock()->NowTicks() + base::TimeDelta::FromDays(7)); + cache()->SetClient(kOrigin2_, kEndpoint_, + ReportingClient::Subdomains::EXCLUDE, kGroup_, + tick_clock()->NowTicks() + base::TimeDelta::FromDays(7)); + + RemoveBrowsingData(/* remove_reports= */ false, /* remove_clients= */ false, + /* host= */ ""); + EXPECT_EQ(2u, report_count()); + EXPECT_EQ(2u, client_count()); +} + +TEST_F(ReportingBrowsingDataRemoverTest, RemoveAllReports) { + cache()->AddReport(kUrl1_, kGroup_, kType_, + base::MakeUnique<base::DictionaryValue>(), + tick_clock()->NowTicks(), 0); + cache()->AddReport(kUrl2_, kGroup_, kType_, + base::MakeUnique<base::DictionaryValue>(), + tick_clock()->NowTicks(), 0); + cache()->SetClient(kOrigin1_, kEndpoint_, + ReportingClient::Subdomains::EXCLUDE, kGroup_, + tick_clock()->NowTicks() + base::TimeDelta::FromDays(7)); + cache()->SetClient(kOrigin2_, kEndpoint_, + ReportingClient::Subdomains::EXCLUDE, kGroup_, + tick_clock()->NowTicks() + base::TimeDelta::FromDays(7)); + + RemoveBrowsingData(/* remove_reports= */ true, /* remove_clients= */ false, + /* host= */ ""); + EXPECT_EQ(0u, report_count()); + EXPECT_EQ(2u, client_count()); +} + +TEST_F(ReportingBrowsingDataRemoverTest, RemoveAllClients) { + cache()->AddReport(kUrl1_, kGroup_, kType_, + base::MakeUnique<base::DictionaryValue>(), + tick_clock()->NowTicks(), 0); + cache()->AddReport(kUrl2_, kGroup_, kType_, + base::MakeUnique<base::DictionaryValue>(), + tick_clock()->NowTicks(), 0); + cache()->SetClient(kOrigin1_, kEndpoint_, + ReportingClient::Subdomains::EXCLUDE, kGroup_, + tick_clock()->NowTicks() + base::TimeDelta::FromDays(7)); + cache()->SetClient(kOrigin2_, kEndpoint_, + ReportingClient::Subdomains::EXCLUDE, kGroup_, + tick_clock()->NowTicks() + base::TimeDelta::FromDays(7)); + + RemoveBrowsingData(/* remove_reports= */ false, /* remove_clients= */ true, + /* host= */ ""); + EXPECT_EQ(2u, report_count()); + EXPECT_EQ(0u, client_count()); +} + +TEST_F(ReportingBrowsingDataRemoverTest, RemoveAllReportsAndClients) { + cache()->AddReport(kUrl1_, kGroup_, kType_, + base::MakeUnique<base::DictionaryValue>(), + tick_clock()->NowTicks(), 0); + cache()->AddReport(kUrl2_, kGroup_, kType_, + base::MakeUnique<base::DictionaryValue>(), + tick_clock()->NowTicks(), 0); + cache()->SetClient(kOrigin1_, kEndpoint_, + ReportingClient::Subdomains::EXCLUDE, kGroup_, + tick_clock()->NowTicks() + base::TimeDelta::FromDays(7)); + cache()->SetClient(kOrigin2_, kEndpoint_, + ReportingClient::Subdomains::EXCLUDE, kGroup_, + tick_clock()->NowTicks() + base::TimeDelta::FromDays(7)); + + RemoveBrowsingData(/* remove_reports= */ true, /* remove_clients= */ true, + /* host= */ ""); + EXPECT_EQ(0u, report_count()); + EXPECT_EQ(0u, client_count()); +} + +TEST_F(ReportingBrowsingDataRemoverTest, RemoveSomeReports) { + cache()->AddReport(kUrl1_, kGroup_, kType_, + base::MakeUnique<base::DictionaryValue>(), + tick_clock()->NowTicks(), 0); + cache()->AddReport(kUrl2_, kGroup_, kType_, + base::MakeUnique<base::DictionaryValue>(), + tick_clock()->NowTicks(), 0); + cache()->SetClient(kOrigin1_, kEndpoint_, + ReportingClient::Subdomains::EXCLUDE, kGroup_, + tick_clock()->NowTicks() + base::TimeDelta::FromDays(7)); + cache()->SetClient(kOrigin2_, kEndpoint_, + ReportingClient::Subdomains::EXCLUDE, kGroup_, + tick_clock()->NowTicks() + base::TimeDelta::FromDays(7)); + + RemoveBrowsingData(/* remove_reports= */ true, /* remove_clients= */ false, + /* host= */ kUrl1_.host()); + EXPECT_EQ(2u, client_count()); + + std::vector<const ReportingReport*> reports; + cache()->GetReports(&reports); + ASSERT_EQ(1u, reports.size()); + EXPECT_EQ(kUrl2_, reports[0]->url); +} + +TEST_F(ReportingBrowsingDataRemoverTest, RemoveSomeClients) { + cache()->AddReport(kUrl1_, kGroup_, kType_, + base::MakeUnique<base::DictionaryValue>(), + tick_clock()->NowTicks(), 0); + cache()->AddReport(kUrl2_, kGroup_, kType_, + base::MakeUnique<base::DictionaryValue>(), + tick_clock()->NowTicks(), 0); + cache()->SetClient(kOrigin1_, kEndpoint_, + ReportingClient::Subdomains::EXCLUDE, kGroup_, + tick_clock()->NowTicks() + base::TimeDelta::FromDays(7)); + cache()->SetClient(kOrigin2_, kEndpoint_, + ReportingClient::Subdomains::EXCLUDE, kGroup_, + tick_clock()->NowTicks() + base::TimeDelta::FromDays(7)); + + RemoveBrowsingData(/* remove_reports= */ false, /* remove_clients= */ true, + /* host= */ kUrl1_.host()); + EXPECT_EQ(2u, report_count()); + EXPECT_FALSE(FindClientInCache(cache(), kOrigin1_, kEndpoint_) != nullptr); + EXPECT_TRUE(FindClientInCache(cache(), kOrigin2_, kEndpoint_) != nullptr); +} + +} // namespace +} // namespace net diff --git a/chromium/net/reporting/reporting_cache.cc b/chromium/net/reporting/reporting_cache.cc new file mode 100644 index 00000000000..b3b60101eb8 --- /dev/null +++ b/chromium/net/reporting/reporting_cache.cc @@ -0,0 +1,271 @@ +// Copyright 2017 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_cache.h" + +#include <map> +#include <memory> +#include <set> +#include <string> +#include <vector> + +#include "base/memory/ptr_util.h" +#include "base/stl_util.h" +#include "base/time/time.h" +#include "net/reporting/reporting_client.h" +#include "net/reporting/reporting_context.h" +#include "net/reporting/reporting_report.h" +#include "url/gurl.h" + +namespace net { + +namespace { + +// Returns the superdomain of a given domain, or the empty string if the given +// domain is just a single label. Note that this does not take into account +// anything like the Public Suffix List, so the superdomain may end up being a +// bare TLD. +// +// Examples: +// +// GetSuperdomain("assets.example.com") -> "example.com" +// GetSuperdomain("example.net") -> "net" +// GetSuperdomain("littlebox") -> "" +std::string GetSuperdomain(const std::string& domain) { + size_t dot_pos = domain.find('.'); + if (dot_pos == std::string::npos) + return ""; + + return domain.substr(dot_pos + 1); +} + +} // namespace + +ReportingCache::ReportingCache(ReportingContext* context) : context_(context) { + DCHECK(context_); +} + +ReportingCache::~ReportingCache() {} + +void ReportingCache::AddReport(const GURL& url, + const std::string& group, + const std::string& type, + std::unique_ptr<const base::Value> body, + base::TimeTicks queued, + int attempts) { + auto report = base::MakeUnique<ReportingReport>( + url, group, type, std::move(body), queued, attempts); + + auto inserted = + reports_.insert(std::make_pair(report.get(), std::move(report))); + DCHECK(inserted.second); + + context_->NotifyCacheUpdated(); +} + +void ReportingCache::GetReports( + std::vector<const ReportingReport*>* reports_out) const { + reports_out->clear(); + for (const auto& it : reports_) { + if (!base::ContainsKey(doomed_reports_, it.first)) + reports_out->push_back(it.second.get()); + } +} + +void ReportingCache::SetReportsPending( + const std::vector<const ReportingReport*>& reports) { + for (const ReportingReport* report : reports) { + auto inserted = pending_reports_.insert(report); + DCHECK(inserted.second); + } +} + +void ReportingCache::ClearReportsPending( + const std::vector<const ReportingReport*>& reports) { + std::vector<const ReportingReport*> reports_to_remove; + + for (const ReportingReport* report : reports) { + size_t erased = pending_reports_.erase(report); + DCHECK_EQ(1u, erased); + if (base::ContainsKey(doomed_reports_, report)) { + reports_to_remove.push_back(report); + doomed_reports_.erase(report); + } + } + + RemoveReports(reports_to_remove); +} + +void ReportingCache::IncrementReportsAttempts( + const std::vector<const ReportingReport*>& reports) { + for (const ReportingReport* report : reports) { + DCHECK(base::ContainsKey(reports_, report)); + reports_[report]->attempts++; + } + + context_->NotifyCacheUpdated(); +} + +void ReportingCache::RemoveReports( + const std::vector<const ReportingReport*>& reports) { + for (const ReportingReport* report : reports) { + if (base::ContainsKey(pending_reports_, report)) { + doomed_reports_.insert(report); + } else { + DCHECK(!base::ContainsKey(doomed_reports_, report)); + size_t erased = reports_.erase(report); + DCHECK_EQ(1u, erased); + } + } + + context_->NotifyCacheUpdated(); +} + +void ReportingCache::RemoveAllReports() { + std::vector<std::unordered_map<const ReportingReport*, + std::unique_ptr<ReportingReport>>::iterator> + reports_to_remove; + for (auto it = reports_.begin(); it != reports_.end(); ++it) { + ReportingReport* report = it->second.get(); + if (!base::ContainsKey(pending_reports_, report)) + reports_to_remove.push_back(it); + else + doomed_reports_.insert(report); + } + + for (auto& it : reports_to_remove) + reports_.erase(it); + + context_->NotifyCacheUpdated(); +} + +void ReportingCache::GetClients( + std::vector<const ReportingClient*>* clients_out) const { + clients_out->clear(); + for (const auto& it : clients_) + for (const auto& endpoint_and_client : it.second) + clients_out->push_back(endpoint_and_client.second.get()); +} + +void ReportingCache::GetClientsForOriginAndGroup( + const url::Origin& origin, + const std::string& group, + std::vector<const ReportingClient*>* clients_out) const { + clients_out->clear(); + + const auto it = clients_.find(origin); + if (it != clients_.end()) { + for (const auto& endpoint_and_client : it->second) { + if (endpoint_and_client.second->group == group) + clients_out->push_back(endpoint_and_client.second.get()); + } + } + + // If no clients were found, try successive superdomain suffixes until a + // client with includeSubdomains is found or there are no more domain + // components left. + std::string domain = origin.host(); + while (clients_out->empty() && !domain.empty()) { + GetWildcardClientsForDomainAndGroup(domain, group, clients_out); + domain = GetSuperdomain(domain); + } +} + +void ReportingCache::SetClient(const url::Origin& origin, + const GURL& endpoint, + ReportingClient::Subdomains subdomains, + const std::string& group, + base::TimeTicks expires) { + DCHECK(endpoint.SchemeIsCryptographic()); + + // Since |subdomains| may differ from a previous call to SetClient for this + // origin and endpoint, the cache needs to remove and re-add the client to the + // index of wildcard clients, if applicable. + if (base::ContainsKey(clients_, origin) && + base::ContainsKey(clients_[origin], endpoint)) { + MaybeRemoveWildcardClient(clients_[origin][endpoint].get()); + } + + clients_[origin][endpoint] = base::MakeUnique<ReportingClient>( + origin, endpoint, subdomains, group, expires); + + MaybeAddWildcardClient(clients_[origin][endpoint].get()); + + context_->NotifyCacheUpdated(); +} + +void ReportingCache::RemoveClients( + const std::vector<const ReportingClient*>& clients_to_remove) { + for (const ReportingClient* client : clients_to_remove) { + MaybeRemoveWildcardClient(client); + size_t erased = clients_[client->origin].erase(client->endpoint); + DCHECK_EQ(1u, erased); + } + + context_->NotifyCacheUpdated(); +} + +void ReportingCache::RemoveClientForOriginAndEndpoint(const url::Origin& origin, + const GURL& endpoint) { + MaybeRemoveWildcardClient(clients_[origin][endpoint].get()); + size_t erased = clients_[origin].erase(endpoint); + DCHECK_EQ(1u, erased); + + context_->NotifyCacheUpdated(); +} + +void ReportingCache::RemoveClientsForEndpoint(const GURL& endpoint) { + for (auto& origin_and_endpoints : clients_) { + if (base::ContainsKey(origin_and_endpoints.second, endpoint)) { + MaybeRemoveWildcardClient(origin_and_endpoints.second[endpoint].get()); + origin_and_endpoints.second.erase(endpoint); + } + } + + context_->NotifyCacheUpdated(); +} + +void ReportingCache::RemoveAllClients() { + clients_.clear(); + wildcard_clients_.clear(); + + context_->NotifyCacheUpdated(); +} + +void ReportingCache::MaybeAddWildcardClient(const ReportingClient* client) { + if (client->subdomains != ReportingClient::Subdomains::INCLUDE) + return; + + const std::string& domain = client->origin.host(); + auto inserted = wildcard_clients_[domain].insert(client); + DCHECK(inserted.second); +} + +void ReportingCache::MaybeRemoveWildcardClient(const ReportingClient* client) { + if (client->subdomains != ReportingClient::Subdomains::INCLUDE) + return; + + const std::string& domain = client->origin.host(); + size_t erased = wildcard_clients_[domain].erase(client); + DCHECK_EQ(1u, erased); +} + +void ReportingCache::GetWildcardClientsForDomainAndGroup( + const std::string& domain, + const std::string& group, + std::vector<const ReportingClient*>* clients_out) const { + clients_out->clear(); + + auto it = wildcard_clients_.find(domain); + if (it == wildcard_clients_.end()) + return; + + for (const ReportingClient* client : it->second) { + DCHECK_EQ(ReportingClient::Subdomains::INCLUDE, client->subdomains); + if (client->group == group) + clients_out->push_back(client); + } +} + +} // namespace net diff --git a/chromium/net/reporting/reporting_cache.h b/chromium/net/reporting/reporting_cache.h new file mode 100644 index 00000000000..30ad0429e16 --- /dev/null +++ b/chromium/net/reporting/reporting_cache.h @@ -0,0 +1,201 @@ +// Copyright 2017 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. + +#ifndef NET_REPORTING_REPORTING_CACHE_H_ +#define NET_REPORTING_REPORTING_CACHE_H_ + +#include <map> +#include <memory> +#include <set> +#include <string> +#include <unordered_map> +#include <unordered_set> +#include <vector> + +#include "base/macros.h" +#include "base/stl_util.h" +#include "base/time/time.h" +#include "base/values.h" +#include "net/base/net_export.h" +#include "net/reporting/reporting_client.h" +#include "url/gurl.h" +#include "url/origin.h" + +namespace net { + +class ReportingContext; +struct ReportingReport; + +// The cache holds undelivered reports and clients (per-origin endpoint +// configurations) in memory. (It is not responsible for persisting them.) +// +// This corresponds roughly to the "Reporting cache" in the spec, except that +// endpoints and clients are stored in a more structurally-convenient way, and +// endpoint failures/retry-after are tracked in ReportingEndpointManager. +// +// The cache implementation has the notion of "pending" reports. These are +// reports that are part of an active delivery attempt, so they won't be +// actually deallocated. Any attempt to remove a pending report wil mark it +// "doomed", which will cause it to be deallocated once it is no longer pending. +class NET_EXPORT ReportingCache { + public: + // |context| must outlive the ReportingCache. + ReportingCache(ReportingContext* context); + + ~ReportingCache(); + + // Adds a report to the cache. + // + // All parameters correspond to the desired values for the relevant fields in + // ReportingReport. + void AddReport(const GURL& url, + const std::string& group, + const std::string& type, + std::unique_ptr<const base::Value> body, + base::TimeTicks queued, + int attempts); + + // Gets all reports in the cache. The returned pointers are valid as long as + // either no calls to |RemoveReports| have happened or the reports' |pending| + // flag has been set to true using |SetReportsPending|. Does not return + // doomed reports (pending reports for which removal has been requested). + // + // (Clears any existing data in |*reports_out|.) + void GetReports(std::vector<const ReportingReport*>* reports_out) const; + + // Marks a set of reports as pending. |reports| must not already be marked as + // pending. + void SetReportsPending(const std::vector<const ReportingReport*>& reports); + + // Unmarks a set of reports as pending. |reports| must be previously marked as + // pending. + void ClearReportsPending(const std::vector<const ReportingReport*>& reports); + + // Increments |attempts| on a set of reports. + void IncrementReportsAttempts( + const std::vector<const ReportingReport*>& reports); + + // Removes a set of reports. Any reports that are pending will not be removed + // immediately, but rather marked doomed and removed once they are no longer + // pending. + void RemoveReports(const std::vector<const ReportingReport*>& reports); + + // Removes all reports. Like |RemoveReports()|, pending reports are doomed + // until no longer pending. + void RemoveAllReports(); + + // Creates or updates a client for a particular origin and a particular + // endpoint. + // + // All parameters correspond to the desired values for the fields in + // |Client|. + // + // |endpoint| must use a cryptographic scheme. + void SetClient(const url::Origin& origin, + const GURL& endpoint, + ReportingClient::Subdomains subdomains, + const std::string& group, + base::TimeTicks expires); + + // Gets all of the clients in the cache, regardless of origin or group. + // + // (Clears any existing data in |*clients_out|.) + void GetClients(std::vector<const ReportingClient*>* clients_out) const; + + // Gets all of the clients configured for a particular origin in a particular + // group. The returned pointers are only guaranteed to be valid if no calls + // have been made to |SetClient| or |RemoveEndpoint| in between. + // + // If no origin match is found, the cache will return clients from the most + // specific superdomain which contains any clients with includeSubdomains set. + // For example, given the origin https://foo.bar.baz.com/, the cache would + // prioritize returning each potential match below over the ones below it: + // + // 1. https://foo.bar.baz.com/ (exact origin match) + // 2. https://foo.bar.baz.com:444/ (technically, a superdomain) + // 3. https://bar.baz.com/, https://bar.baz.com:444/, etc. (superdomain) + // 4. https://baz.com/, https://baz.com:444/, etc. (superdomain) + // etc. + // + // (Clears any existing data in |*clients_out|.) + void GetClientsForOriginAndGroup( + const url::Origin& origin, + const std::string& group, + std::vector<const ReportingClient*>* clients_out) const; + + // Removes a set of clients. + // + // May invalidate ReportingClient pointers returned by |GetClients| or + // |GetClientsForOriginAndGroup|. + void RemoveClients(const std::vector<const ReportingClient*>& clients); + + // Removes a client for a particular origin and a particular endpoint. + void RemoveClientForOriginAndEndpoint(const url::Origin& origin, + const GURL& endpoint); + + // Removes all clients whose endpoint is |endpoint|. + // + // May invalidate ReportingClient pointers returned by |GetClients| or + // |GetClientsForOriginAndGroup|. + void RemoveClientsForEndpoint(const GURL& endpoint); + + // Removes all clients. + void RemoveAllClients(); + + // Gets the count of reports in the cache, *including* doomed reports. + // + // Needed to ensure that doomed reports are eventually deleted, since no + // method provides a view of *every* report in the cache, just non-doomed + // ones. + size_t GetFullReportCountForTesting() const { return reports_.size(); } + + bool IsReportPendingForTesting(const ReportingReport* report) const { + return base::ContainsKey(pending_reports_, report); + } + + bool IsReportDoomedForTesting(const ReportingReport* report) const { + return base::ContainsKey(doomed_reports_, report); + } + + private: + void MaybeAddWildcardClient(const ReportingClient* client); + + void MaybeRemoveWildcardClient(const ReportingClient* client); + + void GetWildcardClientsForDomainAndGroup( + const std::string& domain, + const std::string& group, + std::vector<const ReportingClient*>* clients_out) const; + + ReportingContext* context_; + + // Owns all clients, keyed by origin, then endpoint URL. + // (These would be unordered_map, but neither url::Origin nor GURL has a hash + // function implemented.) + std::map<url::Origin, std::map<GURL, std::unique_ptr<ReportingClient>>> + clients_; + + // References but does not own all clients with includeSubdomains set, keyed + // by domain name. + std::unordered_map<std::string, std::unordered_set<const ReportingClient*>> + wildcard_clients_; + + // Owns all reports, keyed by const raw pointer for easier lookup. + std::unordered_map<const ReportingReport*, std::unique_ptr<ReportingReport>> + reports_; + + // Reports that have been marked pending (in use elsewhere and should not be + // deleted until no longer pending). + std::unordered_set<const ReportingReport*> pending_reports_; + + // Reports that have been marked doomed (would have been deleted, but were + // pending when the deletion was requested). + std::unordered_set<const ReportingReport*> doomed_reports_; + + DISALLOW_COPY_AND_ASSIGN(ReportingCache); +}; + +} // namespace net + +#endif // NET_REPORTING_REPORTING_CACHE_H_ diff --git a/chromium/net/reporting/reporting_cache_unittest.cc b/chromium/net/reporting/reporting_cache_unittest.cc new file mode 100644 index 00000000000..73023400392 --- /dev/null +++ b/chromium/net/reporting/reporting_cache_unittest.cc @@ -0,0 +1,404 @@ +// Copyright 2017 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_cache.h" + +#include <string> + +#include "base/memory/ptr_util.h" +#include "base/time/time.h" +#include "base/values.h" +#include "net/reporting/reporting_client.h" +#include "net/reporting/reporting_observer.h" +#include "net/reporting/reporting_report.h" +#include "net/reporting/reporting_test_util.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" +#include "url/origin.h" + +namespace net { +namespace { + +class TestReportingObserver : public ReportingObserver { + public: + TestReportingObserver() : cache_update_count_(0) {} + + void OnCacheUpdated() override { ++cache_update_count_; } + + int cache_update_count() const { return cache_update_count_; } + + private: + int cache_update_count_; +}; + +class ReportingCacheTest : public ReportingTestBase { + protected: + ReportingCacheTest() : ReportingTestBase() { + context()->AddObserver(&observer_); + } + + ~ReportingCacheTest() override { context()->RemoveObserver(&observer_); } + + TestReportingObserver* observer() { return &observer_; } + + const GURL kUrl1_ = GURL("https://origin1/path"); + const url::Origin kOrigin1_ = url::Origin(GURL("https://origin1/")); + const url::Origin kOrigin2_ = url::Origin(GURL("https://origin2/")); + const GURL kEndpoint1_ = GURL("https://endpoint1/"); + const GURL kEndpoint2_ = GURL("https://endpoint2/"); + const std::string kGroup1_ = "group1"; + const std::string kGroup2 = "group2"; + const std::string kType_ = "default"; + const base::TimeTicks kNow_ = base::TimeTicks::Now(); + const base::TimeTicks kExpires1_ = kNow_ + base::TimeDelta::FromDays(7); + const base::TimeTicks kExpires2_ = kExpires1_ + base::TimeDelta::FromDays(7); + + private: + TestReportingObserver observer_; +}; + +TEST_F(ReportingCacheTest, Reports) { + std::vector<const ReportingReport*> reports; + cache()->GetReports(&reports); + EXPECT_TRUE(reports.empty()); + + cache()->AddReport(kUrl1_, kGroup1_, kType_, + base::MakeUnique<base::DictionaryValue>(), kNow_, 0); + EXPECT_EQ(1, observer()->cache_update_count()); + + cache()->GetReports(&reports); + ASSERT_EQ(1u, reports.size()); + const ReportingReport* report = reports[0]; + ASSERT_TRUE(report); + EXPECT_EQ(kUrl1_, report->url); + EXPECT_EQ(kGroup1_, report->group); + EXPECT_EQ(kType_, report->type); + // TODO(juliatuttle): Check body? + EXPECT_EQ(kNow_, report->queued); + EXPECT_EQ(0, report->attempts); + EXPECT_FALSE(cache()->IsReportPendingForTesting(report)); + EXPECT_FALSE(cache()->IsReportDoomedForTesting(report)); + + cache()->IncrementReportsAttempts(reports); + EXPECT_EQ(2, observer()->cache_update_count()); + + cache()->GetReports(&reports); + ASSERT_EQ(1u, reports.size()); + report = reports[0]; + ASSERT_TRUE(report); + EXPECT_EQ(1, report->attempts); + + cache()->RemoveReports(reports); + EXPECT_EQ(3, observer()->cache_update_count()); + + cache()->GetReports(&reports); + EXPECT_TRUE(reports.empty()); +} + +TEST_F(ReportingCacheTest, RemoveAllReports) { + cache()->AddReport(kUrl1_, kGroup1_, kType_, + base::MakeUnique<base::DictionaryValue>(), kNow_, 0); + cache()->AddReport(kUrl1_, kGroup1_, kType_, + base::MakeUnique<base::DictionaryValue>(), kNow_, 0); + EXPECT_EQ(2, observer()->cache_update_count()); + + std::vector<const ReportingReport*> reports; + cache()->GetReports(&reports); + EXPECT_EQ(2u, reports.size()); + + cache()->RemoveAllReports(); + EXPECT_EQ(3, observer()->cache_update_count()); + + cache()->GetReports(&reports); + EXPECT_TRUE(reports.empty()); +} + +TEST_F(ReportingCacheTest, RemovePendingReports) { + cache()->AddReport(kUrl1_, kGroup1_, kType_, + base::MakeUnique<base::DictionaryValue>(), kNow_, 0); + EXPECT_EQ(1, observer()->cache_update_count()); + + std::vector<const ReportingReport*> reports; + cache()->GetReports(&reports); + ASSERT_EQ(1u, reports.size()); + EXPECT_FALSE(cache()->IsReportPendingForTesting(reports[0])); + EXPECT_FALSE(cache()->IsReportDoomedForTesting(reports[0])); + + cache()->SetReportsPending(reports); + EXPECT_TRUE(cache()->IsReportPendingForTesting(reports[0])); + EXPECT_FALSE(cache()->IsReportDoomedForTesting(reports[0])); + + cache()->RemoveReports(reports); + EXPECT_TRUE(cache()->IsReportPendingForTesting(reports[0])); + EXPECT_TRUE(cache()->IsReportDoomedForTesting(reports[0])); + EXPECT_EQ(2, observer()->cache_update_count()); + + // After removing report, future calls to GetReports should not return it. + std::vector<const ReportingReport*> visible_reports; + cache()->GetReports(&visible_reports); + EXPECT_TRUE(visible_reports.empty()); + EXPECT_EQ(1u, cache()->GetFullReportCountForTesting()); + + // After clearing pending flag, report should be deleted. + cache()->ClearReportsPending(reports); + EXPECT_EQ(0u, cache()->GetFullReportCountForTesting()); +} + +TEST_F(ReportingCacheTest, RemoveAllPendingReports) { + cache()->AddReport(kUrl1_, kGroup1_, kType_, + base::MakeUnique<base::DictionaryValue>(), kNow_, 0); + EXPECT_EQ(1, observer()->cache_update_count()); + + std::vector<const ReportingReport*> reports; + cache()->GetReports(&reports); + ASSERT_EQ(1u, reports.size()); + EXPECT_FALSE(cache()->IsReportPendingForTesting(reports[0])); + EXPECT_FALSE(cache()->IsReportDoomedForTesting(reports[0])); + + cache()->SetReportsPending(reports); + EXPECT_TRUE(cache()->IsReportPendingForTesting(reports[0])); + EXPECT_FALSE(cache()->IsReportDoomedForTesting(reports[0])); + + cache()->RemoveAllReports(); + EXPECT_TRUE(cache()->IsReportPendingForTesting(reports[0])); + EXPECT_TRUE(cache()->IsReportDoomedForTesting(reports[0])); + EXPECT_EQ(2, observer()->cache_update_count()); + + // After removing report, future calls to GetReports should not return it. + std::vector<const ReportingReport*> visible_reports; + cache()->GetReports(&visible_reports); + EXPECT_TRUE(visible_reports.empty()); + EXPECT_EQ(1u, cache()->GetFullReportCountForTesting()); + + // After clearing pending flag, report should be deleted. + cache()->ClearReportsPending(reports); + EXPECT_EQ(0u, cache()->GetFullReportCountForTesting()); +} + +TEST_F(ReportingCacheTest, Endpoints) { + cache()->SetClient(kOrigin1_, kEndpoint1_, + ReportingClient::Subdomains::EXCLUDE, kGroup1_, + kExpires1_); + EXPECT_EQ(1, observer()->cache_update_count()); + + const ReportingClient* client = + FindClientInCache(cache(), kOrigin1_, kEndpoint1_); + ASSERT_TRUE(client); + EXPECT_EQ(kOrigin1_, client->origin); + EXPECT_EQ(kEndpoint1_, client->endpoint); + EXPECT_EQ(ReportingClient::Subdomains::EXCLUDE, client->subdomains); + EXPECT_EQ(kGroup1_, client->group); + EXPECT_EQ(kExpires1_, client->expires); + + cache()->SetClient(kOrigin1_, kEndpoint1_, + ReportingClient::Subdomains::INCLUDE, kGroup2, kExpires2_); + EXPECT_EQ(2, observer()->cache_update_count()); + + client = FindClientInCache(cache(), kOrigin1_, kEndpoint1_); + ASSERT_TRUE(client); + EXPECT_EQ(kOrigin1_, client->origin); + EXPECT_EQ(kEndpoint1_, client->endpoint); + EXPECT_EQ(ReportingClient::Subdomains::INCLUDE, client->subdomains); + EXPECT_EQ(kGroup2, client->group); + EXPECT_EQ(kExpires2_, client->expires); + + cache()->RemoveClients(std::vector<const ReportingClient*>{client}); + EXPECT_EQ(3, observer()->cache_update_count()); + + client = FindClientInCache(cache(), kOrigin1_, kEndpoint1_); + EXPECT_FALSE(client); +} + +TEST_F(ReportingCacheTest, GetClientsForOriginAndGroup) { + cache()->SetClient(kOrigin1_, kEndpoint1_, + ReportingClient::Subdomains::EXCLUDE, kGroup1_, + kExpires1_); + cache()->SetClient(kOrigin1_, kEndpoint2_, + ReportingClient::Subdomains::EXCLUDE, kGroup2, kExpires1_); + cache()->SetClient(kOrigin2_, kEndpoint1_, + ReportingClient::Subdomains::EXCLUDE, kGroup1_, + kExpires1_); + + std::vector<const ReportingClient*> clients; + cache()->GetClientsForOriginAndGroup(kOrigin1_, kGroup1_, &clients); + ASSERT_EQ(1u, clients.size()); + const ReportingClient* client = clients[0]; + ASSERT_TRUE(client); + EXPECT_EQ(kOrigin1_, client->origin); + EXPECT_EQ(kGroup1_, client->group); +} + +TEST_F(ReportingCacheTest, RemoveClientForOriginAndEndpoint) { + cache()->SetClient(kOrigin1_, kEndpoint1_, + ReportingClient::Subdomains::EXCLUDE, kGroup1_, + kExpires1_); + cache()->SetClient(kOrigin1_, kEndpoint2_, + ReportingClient::Subdomains::EXCLUDE, kGroup2, kExpires1_); + cache()->SetClient(kOrigin2_, kEndpoint1_, + ReportingClient::Subdomains::EXCLUDE, kGroup1_, + kExpires1_); + EXPECT_EQ(3, observer()->cache_update_count()); + + cache()->RemoveClientForOriginAndEndpoint(kOrigin1_, kEndpoint1_); + EXPECT_EQ(4, observer()->cache_update_count()); + + std::vector<const ReportingClient*> clients; + cache()->GetClientsForOriginAndGroup(kOrigin1_, kGroup1_, &clients); + EXPECT_TRUE(clients.empty()); + + cache()->GetClientsForOriginAndGroup(kOrigin1_, kGroup2, &clients); + EXPECT_EQ(1u, clients.size()); + + cache()->GetClientsForOriginAndGroup(kOrigin2_, kGroup1_, &clients); + EXPECT_EQ(1u, clients.size()); +} + +TEST_F(ReportingCacheTest, RemoveClientsForEndpoint) { + cache()->SetClient(kOrigin1_, kEndpoint1_, + ReportingClient::Subdomains::EXCLUDE, kGroup1_, + kExpires1_); + cache()->SetClient(kOrigin1_, kEndpoint2_, + ReportingClient::Subdomains::EXCLUDE, kGroup2, kExpires1_); + cache()->SetClient(kOrigin2_, kEndpoint1_, + ReportingClient::Subdomains::EXCLUDE, kGroup1_, + kExpires1_); + EXPECT_EQ(3, observer()->cache_update_count()); + + cache()->RemoveClientsForEndpoint(kEndpoint1_); + EXPECT_EQ(4, observer()->cache_update_count()); + + std::vector<const ReportingClient*> clients; + cache()->GetClientsForOriginAndGroup(kOrigin1_, kGroup1_, &clients); + EXPECT_TRUE(clients.empty()); + + cache()->GetClientsForOriginAndGroup(kOrigin1_, kGroup2, &clients); + EXPECT_EQ(1u, clients.size()); + + cache()->GetClientsForOriginAndGroup(kOrigin2_, kGroup1_, &clients); + EXPECT_TRUE(clients.empty()); +} + +TEST_F(ReportingCacheTest, RemoveAllClients) { + cache()->SetClient(kOrigin1_, kEndpoint1_, + ReportingClient::Subdomains::EXCLUDE, kGroup1_, + kExpires1_); + cache()->SetClient(kOrigin2_, kEndpoint2_, + ReportingClient::Subdomains::EXCLUDE, kGroup1_, + kExpires1_); + EXPECT_EQ(2, observer()->cache_update_count()); + + cache()->RemoveAllClients(); + EXPECT_EQ(3, observer()->cache_update_count()); + + std::vector<const ReportingClient*> clients; + cache()->GetClients(&clients); + EXPECT_TRUE(clients.empty()); +} + +TEST_F(ReportingCacheTest, ExcludeSubdomainsDifferentPort) { + const url::Origin kOrigin(GURL("https://example/")); + const url::Origin kDifferentPortOrigin(GURL("https://example:444/")); + + cache()->SetClient(kDifferentPortOrigin, kEndpoint1_, + ReportingClient::Subdomains::EXCLUDE, kGroup1_, + kExpires1_); + + std::vector<const ReportingClient*> clients; + cache()->GetClientsForOriginAndGroup(kOrigin, kGroup1_, &clients); + EXPECT_TRUE(clients.empty()); +} + +TEST_F(ReportingCacheTest, ExcludeSubdomainsSuperdomain) { + const url::Origin kOrigin(GURL("https://foo.example/")); + const url::Origin kSuperOrigin(GURL("https://example/")); + + cache()->SetClient(kSuperOrigin, kEndpoint1_, + ReportingClient::Subdomains::EXCLUDE, kGroup1_, + kExpires1_); + + std::vector<const ReportingClient*> clients; + cache()->GetClientsForOriginAndGroup(kOrigin, kGroup1_, &clients); + EXPECT_TRUE(clients.empty()); +} + +TEST_F(ReportingCacheTest, IncludeSubdomainsDifferentPort) { + const url::Origin kOrigin(GURL("https://example/")); + const url::Origin kDifferentPortOrigin(GURL("https://example:444/")); + + cache()->SetClient(kDifferentPortOrigin, kEndpoint1_, + ReportingClient::Subdomains::INCLUDE, kGroup1_, + kExpires1_); + + std::vector<const ReportingClient*> clients; + cache()->GetClientsForOriginAndGroup(kOrigin, kGroup1_, &clients); + ASSERT_EQ(1u, clients.size()); + EXPECT_EQ(kDifferentPortOrigin, clients[0]->origin); +} + +TEST_F(ReportingCacheTest, IncludeSubdomainsSuperdomain) { + const url::Origin kOrigin(GURL("https://foo.example/")); + const url::Origin kSuperOrigin(GURL("https://example/")); + + cache()->SetClient(kSuperOrigin, kEndpoint1_, + ReportingClient::Subdomains::INCLUDE, kGroup1_, + kExpires1_); + + std::vector<const ReportingClient*> clients; + cache()->GetClientsForOriginAndGroup(kOrigin, kGroup1_, &clients); + ASSERT_EQ(1u, clients.size()); + EXPECT_EQ(kSuperOrigin, clients[0]->origin); +} + +TEST_F(ReportingCacheTest, IncludeSubdomainsPreferOriginToDifferentPort) { + const url::Origin kOrigin(GURL("https://foo.example/")); + const url::Origin kDifferentPortOrigin(GURL("https://example:444/")); + + cache()->SetClient(kOrigin, kEndpoint1_, ReportingClient::Subdomains::INCLUDE, + kGroup1_, kExpires1_); + cache()->SetClient(kDifferentPortOrigin, kEndpoint1_, + ReportingClient::Subdomains::INCLUDE, kGroup1_, + kExpires1_); + + std::vector<const ReportingClient*> clients; + cache()->GetClientsForOriginAndGroup(kOrigin, kGroup1_, &clients); + ASSERT_EQ(1u, clients.size()); + EXPECT_EQ(kOrigin, clients[0]->origin); +} + +TEST_F(ReportingCacheTest, IncludeSubdomainsPreferOriginToSuperdomain) { + const url::Origin kOrigin(GURL("https://foo.example/")); + const url::Origin kSuperOrigin(GURL("https://example/")); + + cache()->SetClient(kOrigin, kEndpoint1_, ReportingClient::Subdomains::INCLUDE, + kGroup1_, kExpires1_); + cache()->SetClient(kSuperOrigin, kEndpoint1_, + ReportingClient::Subdomains::INCLUDE, kGroup1_, + kExpires1_); + + std::vector<const ReportingClient*> clients; + cache()->GetClientsForOriginAndGroup(kOrigin, kGroup1_, &clients); + ASSERT_EQ(1u, clients.size()); + EXPECT_EQ(kOrigin, clients[0]->origin); +} + +TEST_F(ReportingCacheTest, IncludeSubdomainsPreferMoreSpecificSuperdomain) { + const url::Origin kOrigin(GURL("https://foo.bar.example/")); + const url::Origin kSuperOrigin(GURL("https://bar.example/")); + const url::Origin kSuperSuperOrigin(GURL("https://example/")); + + cache()->SetClient(kSuperOrigin, kEndpoint1_, + ReportingClient::Subdomains::INCLUDE, kGroup1_, + kExpires1_); + cache()->SetClient(kSuperSuperOrigin, kEndpoint1_, + ReportingClient::Subdomains::INCLUDE, kGroup1_, + kExpires1_); + + std::vector<const ReportingClient*> clients; + cache()->GetClientsForOriginAndGroup(kOrigin, kGroup1_, &clients); + ASSERT_EQ(1u, clients.size()); + EXPECT_EQ(kSuperOrigin, clients[0]->origin); +} + +} // namespace +} // namespace net diff --git a/chromium/net/reporting/reporting_client.cc b/chromium/net/reporting/reporting_client.cc new file mode 100644 index 00000000000..d8c8a0f35b7 --- /dev/null +++ b/chromium/net/reporting/reporting_client.cc @@ -0,0 +1,28 @@ +// Copyright 2017 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_client.h" + +#include <string> + +#include "base/time/time.h" +#include "url/gurl.h" +#include "url/origin.h" + +namespace net { + +ReportingClient::ReportingClient(const url::Origin& origin, + const GURL& endpoint, + Subdomains subdomains, + const std::string& group, + base::TimeTicks expires) + : origin(origin), + endpoint(endpoint), + subdomains(subdomains), + group(group), + expires(expires) {} + +ReportingClient::~ReportingClient() {} + +} // namespace net diff --git a/chromium/net/reporting/reporting_client.h b/chromium/net/reporting/reporting_client.h new file mode 100644 index 00000000000..0aa11bea57d --- /dev/null +++ b/chromium/net/reporting/reporting_client.h @@ -0,0 +1,53 @@ +// Copyright 2017 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. + +#ifndef NET_REPORTING_REPORTING_CLIENT_H_ +#define NET_REPORTING_REPORTING_CLIENT_H_ + +#include <string> + +#include "base/macros.h" +#include "base/time/time.h" +#include "net/base/net_export.h" +#include "url/gurl.h" +#include "url/origin.h" + +namespace net { + +// The configuration by an origin to use an endpoint for report delivery. +struct NET_EXPORT ReportingClient { + public: + enum class Subdomains { EXCLUDE = 0, INCLUDE = 1 }; + + ReportingClient(const url::Origin& origin, + const GURL& endpoint, + Subdomains subdomains, + const std::string& group, + base::TimeTicks expires); + ~ReportingClient(); + + // The origin from which reports will be delivered. + url::Origin origin; + + // The endpoint to which reports may be delivered. (Origins may configure + // many.) + GURL endpoint; + + // Whether subdomains of the host of |origin| should also be handled by this + // client. + Subdomains subdomains = Subdomains::EXCLUDE; + + // The endpoint group to which this client belongs. + std::string group = "default"; + + // When this client's max-age has expired. + base::TimeTicks expires; + + private: + DISALLOW_COPY_AND_ASSIGN(ReportingClient); +}; + +} // namespace net + +#endif // NET_REPORTING_REPORTING_CLIENT_H_ diff --git a/chromium/net/reporting/reporting_context.cc b/chromium/net/reporting/reporting_context.cc new file mode 100644 index 00000000000..1dd255bcb1b --- /dev/null +++ b/chromium/net/reporting/reporting_context.cc @@ -0,0 +1,102 @@ +// Copyright 2017 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_context.h" + +#include <memory> + +#include "base/memory/ptr_util.h" +#include "base/observer_list.h" +#include "base/time/clock.h" +#include "base/time/default_clock.h" +#include "base/time/default_tick_clock.h" +#include "base/time/tick_clock.h" +#include "base/time/time.h" +#include "base/timer/timer.h" +#include "net/base/backoff_entry.h" +#include "net/reporting/reporting_cache.h" +#include "net/reporting/reporting_delegate.h" +#include "net/reporting/reporting_delivery_agent.h" +#include "net/reporting/reporting_endpoint_manager.h" +#include "net/reporting/reporting_garbage_collector.h" +#include "net/reporting/reporting_observer.h" +#include "net/reporting/reporting_persister.h" +#include "net/reporting/reporting_policy.h" + +namespace net { + +class URLRequestContext; + +namespace { + +class ReportingContextImpl : public ReportingContext { + public: + ReportingContextImpl(const ReportingPolicy& policy, + std::unique_ptr<ReportingDelegate> delegate, + URLRequestContext* request_context) + : ReportingContext(policy, + std::move(delegate), + base::MakeUnique<base::DefaultClock>(), + base::MakeUnique<base::DefaultTickClock>(), + ReportingUploader::Create(request_context)) {} +}; + +} // namespace + +// static +std::unique_ptr<ReportingContext> ReportingContext::Create( + const ReportingPolicy& policy, + std::unique_ptr<ReportingDelegate> delegate, + URLRequestContext* request_context) { + return base::MakeUnique<ReportingContextImpl>(policy, std::move(delegate), + request_context); +} + +ReportingContext::~ReportingContext() {} + +void ReportingContext::Initialize() { + DCHECK(!initialized_); + + persister_->Initialize(); + garbage_collector_->Initialize(); + + initialized_ = true; +} + +void ReportingContext::AddObserver(ReportingObserver* observer) { + DCHECK(!observers_.HasObserver(observer)); + observers_.AddObserver(observer); +} + +void ReportingContext::RemoveObserver(ReportingObserver* observer) { + DCHECK(observers_.HasObserver(observer)); + observers_.RemoveObserver(observer); +} + +void ReportingContext::NotifyCacheUpdated() { + if (!initialized_) + return; + + for (auto& observer : observers_) + observer.OnCacheUpdated(); +} + +ReportingContext::ReportingContext(const ReportingPolicy& policy, + std::unique_ptr<ReportingDelegate> delegate, + std::unique_ptr<base::Clock> clock, + std::unique_ptr<base::TickClock> tick_clock, + std::unique_ptr<ReportingUploader> uploader) + : policy_(policy), + delegate_(std::move(delegate)), + clock_(std::move(clock)), + tick_clock_(std::move(tick_clock)), + uploader_(std::move(uploader)), + initialized_(false), + cache_(base::MakeUnique<ReportingCache>(this)), + endpoint_manager_(base::MakeUnique<ReportingEndpointManager>(this)), + delivery_agent_(base::MakeUnique<ReportingDeliveryAgent>(this)), + persister_(ReportingPersister::Create(this)), + garbage_collector_(ReportingGarbageCollector::Create(this)) {} + +} // namespace net diff --git a/chromium/net/reporting/reporting_context.h b/chromium/net/reporting/reporting_context.h new file mode 100644 index 00000000000..75aa11918ae --- /dev/null +++ b/chromium/net/reporting/reporting_context.h @@ -0,0 +1,116 @@ +// Copyright 2017 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. + +#ifndef NET_REPORTING_REPORTING_CONTEXT_H_ +#define NET_REPORTING_REPORTING_CONTEXT_H_ + +#include <memory> + +#include "base/observer_list.h" +#include "base/time/time.h" +#include "net/base/backoff_entry.h" +#include "net/base/net_export.h" +#include "net/reporting/reporting_policy.h" + +namespace base { +class Clock; +class TickClock; +} // namespace base + +namespace net { + +class ReportingCache; +class ReportingDelegate; +class ReportingDeliveryAgent; +class ReportingEndpointManager; +class ReportingGarbageCollector; +class ReportingObserver; +class ReportingPersister; +class ReportingUploader; +class URLRequestContext; + +// Contains the various internal classes that make up the Reporting system. +// Wrapped by ReportingService, which provides the external interface. +class NET_EXPORT ReportingContext { + public: + static std::unique_ptr<ReportingContext> Create( + const ReportingPolicy& policy, + std::unique_ptr<ReportingDelegate> delegate, + URLRequestContext* request_context); + + ~ReportingContext(); + + // Initializes the ReportingContext. This may take a while (e.g. it may + // involve reloading state persisted to disk). Should be called only once. + // + // Components of the ReportingContext won't reference their dependencies (e.g. + // the Clock/TickClock or Timers inside the individual components) until + // during/after the call to Init. + void Initialize(); + + bool initialized() const { return initialized_; } + + const ReportingPolicy& policy() { return policy_; } + ReportingDelegate* delegate() { return delegate_.get(); } + + base::Clock* clock() { return clock_.get(); } + base::TickClock* tick_clock() { return tick_clock_.get(); } + ReportingUploader* uploader() { return uploader_.get(); } + + ReportingCache* cache() { return cache_.get(); } + ReportingEndpointManager* endpoint_manager() { + return endpoint_manager_.get(); + } + ReportingDeliveryAgent* delivery_agent() { return delivery_agent_.get(); } + ReportingGarbageCollector* garbage_collector() { + return garbage_collector_.get(); + } + + ReportingPersister* persister() { return persister_.get(); } + + void AddObserver(ReportingObserver* observer); + void RemoveObserver(ReportingObserver* observer); + + void NotifyCacheUpdated(); + + protected: + ReportingContext(const ReportingPolicy& policy, + std::unique_ptr<ReportingDelegate> delegate, + std::unique_ptr<base::Clock> clock, + std::unique_ptr<base::TickClock> tick_clock, + std::unique_ptr<ReportingUploader> uploader); + + private: + ReportingPolicy policy_; + std::unique_ptr<ReportingDelegate> delegate_; + + std::unique_ptr<base::Clock> clock_; + std::unique_ptr<base::TickClock> tick_clock_; + std::unique_ptr<ReportingUploader> uploader_; + + base::ObserverList<ReportingObserver, /* check_empty= */ true> observers_; + bool initialized_; + + std::unique_ptr<ReportingCache> cache_; + + // |endpoint_manager_| must come after |tick_clock_| and |cache_|. + std::unique_ptr<ReportingEndpointManager> endpoint_manager_; + + // |delivery_agent_| must come after |tick_clock_|, |uploader_|, |cache_|, + // and |endpoint_manager_|. + std::unique_ptr<ReportingDeliveryAgent> delivery_agent_; + + // |persister_| must come after |delegate_|, |clock_|, |tick_clock_|, and + // |cache_|. + std::unique_ptr<ReportingPersister> persister_; + + // |garbage_collector_| must come after |tick_clock_| and |cache_|. + std::unique_ptr<ReportingGarbageCollector> garbage_collector_; + + DISALLOW_COPY_AND_ASSIGN(ReportingContext); +}; + +} // namespace net + +#endif // NET_REPORTING_REPORTING_CONTEXT_H_ diff --git a/chromium/net/reporting/reporting_delegate.cc b/chromium/net/reporting/reporting_delegate.cc new file mode 100644 index 00000000000..cef5bafa28c --- /dev/null +++ b/chromium/net/reporting/reporting_delegate.cc @@ -0,0 +1,13 @@ +// Copyright 2017 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_delegate.h" + +namespace net { + +ReportingDelegate::~ReportingDelegate() {} + +ReportingDelegate::ReportingDelegate() {} + +} // namespace net diff --git a/chromium/net/reporting/reporting_delegate.h b/chromium/net/reporting/reporting_delegate.h new file mode 100644 index 00000000000..57d01ab4a6a --- /dev/null +++ b/chromium/net/reporting/reporting_delegate.h @@ -0,0 +1,45 @@ +// Copyright 2017 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. + +#ifndef NET_REPORTING_REPORTING_DELEGATE_H_ +#define NET_REPORTING_REPORTING_DELEGATE_H_ + +#include <memory> + +#include "base/macros.h" +#include "net/base/net_export.h" + +namespace base { +class Value; +} // namespace base + +namespace net { + +// Delegate for things that the Reporting system can't do by itself, like +// persisting data across embedder restarts. +class NET_EXPORT ReportingDelegate { + public: + virtual ~ReportingDelegate(); + + // Gets previously persisted data, if any is available. Returns a null pointer + // if no data is available. Can be called any number of times. + virtual std::unique_ptr<const base::Value> GetPersistedData() = 0; + + // Sets data to be persisted across embedder restarts. Ideally, this data will + // be returned by any future calls to GetPersistedData() in this or future + // sessions (until newer data is persisted), but no guarantee is made, since + // the underlying persistence mechanism may or may not be reliable. + virtual void PersistData( + std::unique_ptr<const base::Value> persisted_data) = 0; + + protected: + ReportingDelegate(); + + private: + DISALLOW_COPY_AND_ASSIGN(ReportingDelegate); +}; + +} // namespace net + +#endif // NET_REPORTING_REPORTING_DELEGATE_H_ diff --git a/chromium/net/reporting/reporting_delivery_agent.cc b/chromium/net/reporting/reporting_delivery_agent.cc new file mode 100644 index 00000000000..f39dde936ea --- /dev/null +++ b/chromium/net/reporting/reporting_delivery_agent.cc @@ -0,0 +1,142 @@ +// Copyright 2017 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_delivery_agent.h" + +#include <map> +#include <string> +#include <vector> + +#include "base/bind.h" +#include "base/json/json_writer.h" +#include "base/logging.h" +#include "base/memory/ptr_util.h" +#include "base/time/tick_clock.h" +#include "base/values.h" +#include "net/reporting/reporting_cache.h" +#include "net/reporting/reporting_endpoint_manager.h" +#include "net/reporting/reporting_report.h" +#include "net/reporting/reporting_uploader.h" +#include "url/gurl.h" +#include "url/origin.h" + +namespace net { + +namespace { + +void SerializeReports(const std::vector<const ReportingReport*>& reports, + base::TimeTicks now, + std::string* json_out) { + base::ListValue reports_value; + + for (const ReportingReport* report : reports) { + std::unique_ptr<base::DictionaryValue> report_value = + base::MakeUnique<base::DictionaryValue>(); + + report_value->SetInteger("age", (now - report->queued).InMilliseconds()); + report_value->SetString("type", report->type); + report_value->SetString("url", report->url.spec()); + report_value->Set("report", report->body->DeepCopy()); + + reports_value.Append(std::move(report_value)); + } + + bool json_written = base::JSONWriter::Write(reports_value, json_out); + DCHECK(json_written); +} + +} // namespace + +ReportingDeliveryAgent::ReportingDeliveryAgent(ReportingContext* context) + : context_(context), weak_factory_(this) {} +ReportingDeliveryAgent::~ReportingDeliveryAgent() {} + +class ReportingDeliveryAgent::Delivery { + public: + Delivery(const GURL& endpoint, + const std::vector<const ReportingReport*>& reports) + : endpoint(endpoint), reports(reports) {} + + ~Delivery() {} + + const GURL endpoint; + const std::vector<const ReportingReport*> reports; +}; + +void ReportingDeliveryAgent::SendReports() { + std::vector<const ReportingReport*> reports; + cache()->GetReports(&reports); + + // Sort reports into (origin, group) buckets. + std::map<OriginGroup, std::vector<const ReportingReport*>> + origin_group_reports; + for (const ReportingReport* report : reports) { + OriginGroup origin_group(url::Origin(report->url), report->group); + origin_group_reports[origin_group].push_back(report); + } + + // Find endpoint for each (origin, group) bucket and sort reports into + // endpoint buckets. Don't allow concurrent deliveries to the same (origin, + // group) bucket. + std::map<GURL, std::vector<const ReportingReport*>> endpoint_reports; + for (auto& it : origin_group_reports) { + const OriginGroup& origin_group = it.first; + + if (base::ContainsKey(pending_origin_groups_, origin_group)) + continue; + + GURL endpoint_url; + if (!endpoint_manager()->FindEndpointForOriginAndGroup( + origin_group.first, origin_group.second, &endpoint_url)) { + continue; + } + + endpoint_reports[endpoint_url].insert(endpoint_reports[endpoint_url].end(), + it.second.begin(), it.second.end()); + pending_origin_groups_.insert(origin_group); + } + + // Start a delivery to each endpoint. + for (auto& it : endpoint_reports) { + const GURL& endpoint = it.first; + const std::vector<const ReportingReport*>& reports = it.second; + + endpoint_manager()->SetEndpointPending(endpoint); + cache()->SetReportsPending(reports); + + std::string json; + SerializeReports(reports, tick_clock()->NowTicks(), &json); + + uploader()->StartUpload( + endpoint, json, + base::Bind(&ReportingDeliveryAgent::OnUploadComplete, + weak_factory_.GetWeakPtr(), + base::MakeUnique<Delivery>(endpoint, reports))); + } +} + +void ReportingDeliveryAgent::OnUploadComplete( + const std::unique_ptr<Delivery>& delivery, + ReportingUploader::Outcome outcome) { + if (outcome == ReportingUploader::Outcome::SUCCESS) { + cache()->RemoveReports(delivery->reports); + endpoint_manager()->InformOfEndpointRequest(delivery->endpoint, true); + } else { + cache()->IncrementReportsAttempts(delivery->reports); + endpoint_manager()->InformOfEndpointRequest(delivery->endpoint, false); + } + + if (outcome == ReportingUploader::Outcome::REMOVE_ENDPOINT) + cache()->RemoveClientsForEndpoint(delivery->endpoint); + + for (const ReportingReport* report : delivery->reports) { + pending_origin_groups_.erase( + OriginGroup(url::Origin(report->url), report->group)); + } + + endpoint_manager()->ClearEndpointPending(delivery->endpoint); + cache()->ClearReportsPending(delivery->reports); +} + +} // namespace net diff --git a/chromium/net/reporting/reporting_delivery_agent.h b/chromium/net/reporting/reporting_delivery_agent.h new file mode 100644 index 00000000000..eaae8a3ae62 --- /dev/null +++ b/chromium/net/reporting/reporting_delivery_agent.h @@ -0,0 +1,99 @@ +// Copyright 2017 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. + +#ifndef NET_REPORTING_REPORTING_DELIVERY_AGENT_H_ +#define NET_REPORTING_REPORTING_DELIVERY_AGENT_H_ + +#include <memory> +#include <set> +#include <string> +#include <utility> + +#include "base/macros.h" +#include "base/memory/weak_ptr.h" +#include "net/base/backoff_entry.h" +#include "net/base/net_export.h" +#include "net/reporting/reporting_context.h" +#include "net/reporting/reporting_uploader.h" +#include "url/gurl.h" +#include "url/origin.h" + +namespace base { +class TickClock; +} // namespace base + +namespace net { + +class ReportingCache; +class ReportingEndpointManager; + +// Takes reports from the ReportingCache, assembles reports into deliveries to +// endpoints, and sends those deliveries using ReportingUploader. +// +// Since the Reporting spec is completely silent on issues of concurrency, the +// delivery agent handles it as so: +// +// 1. An individual report can only be included in one delivery at once -- if +// SendReports is called again while a report is being delivered, it won't +// be included in another delivery during that call to SendReports. (This is, +// in fact, made redundant by rule 3, but it's included anyway in case rule 3 +// changes.) +// +// 2. An endpoint can only be the target of one delivery at once -- if +// SendReports is called again with reports that could be delivered to that +// endpoint, they won't be delivered to that endpoint. +// +// 3. Reports for an (origin, group) tuple can only be included in one delivery +// at once -- if SendReports is called again with reports in that (origin, +// group), they won't be included in any delivery during that call to +// SendReports. (This prevents the agent from getting around rule 2 by using +// other endpoints in the same group.) +// +// 4. Reports for the same origin *can* be included in multiple parallel +// deliveries if they are in different groups within that origin. +// +// (Note that a single delivery can contain an infinite number of reports.) +// +// TODO(juliatuttle): Consider capping the maximum number of reports per +// delivery attempt. +class NET_EXPORT ReportingDeliveryAgent { + public: + // |context| must outlive the ReportingDeliveryAgent. + ReportingDeliveryAgent(ReportingContext* context); + ~ReportingDeliveryAgent(); + + // Tries to deliver all of the reports in the cache. Reports that are already + // being delivered will not be attempted a second time, and reports that do + // not have a viable endpoint will be neither attempted nor removed. + void SendReports(); + + private: + class Delivery; + + using OriginGroup = std::pair<url::Origin, std::string>; + + void OnUploadComplete(const std::unique_ptr<Delivery>& delivery, + ReportingUploader::Outcome outcome); + + base::TickClock* tick_clock() { return context_->tick_clock(); } + ReportingCache* cache() { return context_->cache(); } + ReportingUploader* uploader() { return context_->uploader(); } + ReportingEndpointManager* endpoint_manager() { + return context_->endpoint_manager(); + } + + ReportingContext* context_; + + // Tracks OriginGroup tuples for which there is a pending delivery running. + // (Would be an unordered_set, but there's no hash on pair.) + std::set<OriginGroup> pending_origin_groups_; + + base::WeakPtrFactory<ReportingDeliveryAgent> weak_factory_; + + DISALLOW_COPY_AND_ASSIGN(ReportingDeliveryAgent); +}; + +} // namespace net + +#endif // NET_REPORTING_REPORTING_DELIVERY_AGENT_H_ diff --git a/chromium/net/reporting/reporting_delivery_agent_unittest.cc b/chromium/net/reporting/reporting_delivery_agent_unittest.cc new file mode 100644 index 00000000000..d62f97c0133 --- /dev/null +++ b/chromium/net/reporting/reporting_delivery_agent_unittest.cc @@ -0,0 +1,318 @@ +// Copyright 2017 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_delivery_agent.h" + +#include <vector> + +#include "base/json/json_reader.h" +#include "base/memory/ptr_util.h" +#include "base/test/simple_test_tick_clock.h" +#include "base/test/values_test_util.h" +#include "base/time/time.h" +#include "base/values.h" +#include "net/base/backoff_entry.h" +#include "net/reporting/reporting_cache.h" +#include "net/reporting/reporting_report.h" +#include "net/reporting/reporting_test_util.h" +#include "net/reporting/reporting_uploader.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" +#include "url/origin.h" + +namespace net { +namespace { + +class ReportingDeliveryAgentTest : public ReportingTestBase { + protected: + ReportingDeliveryAgentTest() { + ReportingPolicy policy; + policy.endpoint_backoff_policy.num_errors_to_ignore = 0; + policy.endpoint_backoff_policy.initial_delay_ms = 60000; + policy.endpoint_backoff_policy.multiply_factor = 2.0; + policy.endpoint_backoff_policy.jitter_factor = 0.0; + policy.endpoint_backoff_policy.maximum_backoff_ms = -1; + policy.endpoint_backoff_policy.entry_lifetime_ms = 0; + policy.endpoint_backoff_policy.always_use_initial_delay = false; + UsePolicy(policy); + } + + base::TimeTicks tomorrow() { + return tick_clock()->NowTicks() + base::TimeDelta::FromDays(1); + } + + const std::vector<std::unique_ptr<TestReportingUploader::PendingUpload>>& + pending_uploads() { + return uploader()->pending_uploads(); + } + + const GURL kUrl_ = GURL("https://origin/path"); + const url::Origin kOrigin_ = url::Origin(GURL("https://origin/")); + const GURL kEndpoint_ = GURL("https://endpoint/"); + const std::string kGroup_ = "group"; + const std::string kType_ = "type"; +}; + +TEST_F(ReportingDeliveryAgentTest, SuccessfulUpload) { + static const int kAgeMillis = 12345; + + base::DictionaryValue body; + body.SetString("key", "value"); + + cache()->SetClient(kOrigin_, kEndpoint_, ReportingClient::Subdomains::EXCLUDE, + kGroup_, tomorrow()); + cache()->AddReport(kUrl_, kGroup_, kType_, body.CreateDeepCopy(), + tick_clock()->NowTicks(), 0); + + tick_clock()->Advance(base::TimeDelta::FromMilliseconds(kAgeMillis)); + + delivery_agent()->SendReports(); + + ASSERT_EQ(1u, pending_uploads().size()); + EXPECT_EQ(kEndpoint_, pending_uploads()[0]->url()); + { + auto value = pending_uploads()[0]->GetValue(); + + base::ListValue* list; + ASSERT_TRUE(value->GetAsList(&list)); + EXPECT_EQ(1u, list->GetSize()); + + base::DictionaryValue* report; + ASSERT_TRUE(list->GetDictionary(0, &report)); + EXPECT_EQ(4u, report->size()); + + ExpectDictIntegerValue(kAgeMillis, *report, "age"); + ExpectDictStringValue(kType_, *report, "type"); + ExpectDictStringValue(kUrl_.spec(), *report, "url"); + ExpectDictDictionaryValue(body, *report, "report"); + } + pending_uploads()[0]->Complete(ReportingUploader::Outcome::SUCCESS); + + // Successful upload should remove delivered reports. + std::vector<const ReportingReport*> reports; + cache()->GetReports(&reports); + EXPECT_TRUE(reports.empty()); + + // TODO(juliatuttle): Check that BackoffEntry was informed of success. +} + +TEST_F(ReportingDeliveryAgentTest, FailedUpload) { + cache()->SetClient(kOrigin_, kEndpoint_, ReportingClient::Subdomains::EXCLUDE, + kGroup_, tomorrow()); + cache()->AddReport(kUrl_, kGroup_, kType_, + base::MakeUnique<base::DictionaryValue>(), + tick_clock()->NowTicks(), 0); + + delivery_agent()->SendReports(); + + ASSERT_EQ(1u, pending_uploads().size()); + pending_uploads()[0]->Complete(ReportingUploader::Outcome::FAILURE); + + // Failed upload should increment reports' attempts. + std::vector<const ReportingReport*> reports; + cache()->GetReports(&reports); + ASSERT_EQ(1u, reports.size()); + EXPECT_EQ(1, reports[0]->attempts); + + // Since endpoint is now failing, an upload won't be started despite a pending + // report. + ASSERT_TRUE(pending_uploads().empty()); + delivery_agent()->SendReports(); + EXPECT_TRUE(pending_uploads().empty()); +} + +TEST_F(ReportingDeliveryAgentTest, RemoveEndpointUpload) { + static const url::Origin kDifferentOrigin(GURL("https://origin2/")); + + cache()->SetClient(kOrigin_, kEndpoint_, ReportingClient::Subdomains::EXCLUDE, + kGroup_, tomorrow()); + cache()->SetClient(kDifferentOrigin, kEndpoint_, + ReportingClient::Subdomains::EXCLUDE, kGroup_, tomorrow()); + ASSERT_TRUE(FindClientInCache(cache(), kOrigin_, kEndpoint_)); + ASSERT_TRUE(FindClientInCache(cache(), kDifferentOrigin, kEndpoint_)); + + cache()->AddReport(kUrl_, kGroup_, kType_, + base::MakeUnique<base::DictionaryValue>(), + tick_clock()->NowTicks(), 0); + + delivery_agent()->SendReports(); + + ASSERT_EQ(1u, pending_uploads().size()); + pending_uploads()[0]->Complete(ReportingUploader::Outcome::REMOVE_ENDPOINT); + + // "Remove endpoint" upload should remove endpoint from *all* origins and + // increment reports' attempts. + std::vector<const ReportingReport*> reports; + cache()->GetReports(&reports); + ASSERT_EQ(1u, reports.size()); + EXPECT_EQ(1, reports[0]->attempts); + + EXPECT_FALSE(FindClientInCache(cache(), kOrigin_, kEndpoint_)); + EXPECT_FALSE(FindClientInCache(cache(), kDifferentOrigin, kEndpoint_)); + + // Since endpoint is now failing, an upload won't be started despite a pending + // report. + delivery_agent()->SendReports(); + EXPECT_TRUE(pending_uploads().empty()); +} + +TEST_F(ReportingDeliveryAgentTest, ConcurrentRemove) { + cache()->SetClient(kOrigin_, kEndpoint_, ReportingClient::Subdomains::EXCLUDE, + kGroup_, tomorrow()); + cache()->AddReport(kUrl_, kGroup_, kType_, + base::MakeUnique<base::DictionaryValue>(), + tick_clock()->NowTicks(), 0); + + delivery_agent()->SendReports(); + ASSERT_EQ(1u, pending_uploads().size()); + + // Remove the report while the upload is running. + std::vector<const ReportingReport*> reports; + cache()->GetReports(&reports); + EXPECT_EQ(1u, reports.size()); + + const ReportingReport* report = reports[0]; + EXPECT_FALSE(cache()->IsReportDoomedForTesting(report)); + + // Report should appear removed, even though the cache has doomed it. + cache()->RemoveReports(reports); + cache()->GetReports(&reports); + EXPECT_TRUE(reports.empty()); + EXPECT_TRUE(cache()->IsReportDoomedForTesting(report)); + + // Completing upload shouldn't crash, and report should still be gone. + pending_uploads()[0]->Complete(ReportingUploader::Outcome::SUCCESS); + cache()->GetReports(&reports); + EXPECT_TRUE(reports.empty()); + // This is slightly sketchy since |report| has been freed, but it nonetheless + // should not be in the set of doomed reports. + EXPECT_FALSE(cache()->IsReportDoomedForTesting(report)); +} + +// Test that the agent will combine reports destined for the same endpoint, even +// if the reports are from different origins. +TEST_F(ReportingDeliveryAgentTest, + BatchReportsFromDifferentOriginsToSameEndpoint) { + static const GURL kDifferentUrl("https://origin2/path"); + static const url::Origin kDifferentOrigin(kDifferentUrl); + + cache()->SetClient(kOrigin_, kEndpoint_, ReportingClient::Subdomains::EXCLUDE, + kGroup_, tomorrow()); + cache()->SetClient(kDifferentOrigin, kEndpoint_, + ReportingClient::Subdomains::EXCLUDE, kGroup_, tomorrow()); + + cache()->AddReport(kUrl_, kGroup_, kType_, + base::MakeUnique<base::DictionaryValue>(), + tick_clock()->NowTicks(), 0); + cache()->AddReport(kDifferentUrl, kGroup_, kType_, + base::MakeUnique<base::DictionaryValue>(), + tick_clock()->NowTicks(), 0); + + delivery_agent()->SendReports(); + ASSERT_EQ(1u, pending_uploads().size()); + + pending_uploads()[0]->Complete(ReportingUploader::Outcome::SUCCESS); + EXPECT_EQ(0u, pending_uploads().size()); +} + +// Test that the agent won't start a second upload to the same endpoint (even +// for a different origin) while one is pending, but will once it is no longer +// pending. +TEST_F(ReportingDeliveryAgentTest, SerializeUploadsToEndpoint) { + static const GURL kDifferentUrl("https://origin2/path"); + static const url::Origin kDifferentOrigin(kDifferentUrl); + + cache()->SetClient(kOrigin_, kEndpoint_, ReportingClient::Subdomains::EXCLUDE, + kGroup_, tomorrow()); + cache()->SetClient(kDifferentOrigin, kEndpoint_, + ReportingClient::Subdomains::EXCLUDE, kGroup_, tomorrow()); + + cache()->AddReport(kUrl_, kGroup_, kType_, + base::MakeUnique<base::DictionaryValue>(), + tick_clock()->NowTicks(), 0); + + delivery_agent()->SendReports(); + EXPECT_EQ(1u, pending_uploads().size()); + + cache()->AddReport(kDifferentUrl, kGroup_, kType_, + base::MakeUnique<base::DictionaryValue>(), + tick_clock()->NowTicks(), 0); + + delivery_agent()->SendReports(); + ASSERT_EQ(1u, pending_uploads().size()); + + pending_uploads()[0]->Complete(ReportingUploader::Outcome::SUCCESS); + EXPECT_EQ(0u, pending_uploads().size()); + + delivery_agent()->SendReports(); + ASSERT_EQ(1u, pending_uploads().size()); + + pending_uploads()[0]->Complete(ReportingUploader::Outcome::SUCCESS); + EXPECT_EQ(0u, pending_uploads().size()); +} + +// Test that the agent won't start a second upload for an (origin, group) while +// one is pending, even if a different endpoint is available, but will once the +// original delivery is complete and the (origin, group) is no longer pending. +TEST_F(ReportingDeliveryAgentTest, SerializeUploadsToGroup) { + static const GURL kDifferentEndpoint("https://endpoint2/"); + + cache()->SetClient(kOrigin_, kEndpoint_, ReportingClient::Subdomains::EXCLUDE, + kGroup_, tomorrow()); + cache()->SetClient(kOrigin_, kDifferentEndpoint, + ReportingClient::Subdomains::EXCLUDE, kGroup_, tomorrow()); + + cache()->AddReport(kUrl_, kGroup_, kType_, + base::MakeUnique<base::DictionaryValue>(), + tick_clock()->NowTicks(), 0); + + delivery_agent()->SendReports(); + EXPECT_EQ(1u, pending_uploads().size()); + + cache()->AddReport(kUrl_, kGroup_, kType_, + base::MakeUnique<base::DictionaryValue>(), + tick_clock()->NowTicks(), 0); + + delivery_agent()->SendReports(); + ASSERT_EQ(1u, pending_uploads().size()); + + pending_uploads()[0]->Complete(ReportingUploader::Outcome::SUCCESS); + EXPECT_EQ(0u, pending_uploads().size()); + + delivery_agent()->SendReports(); + ASSERT_EQ(1u, pending_uploads().size()); + + pending_uploads()[0]->Complete(ReportingUploader::Outcome::SUCCESS); + EXPECT_EQ(0u, pending_uploads().size()); +} + +// Tests that the agent will start parallel uploads to different groups within +// the same origin. +TEST_F(ReportingDeliveryAgentTest, ParallelizeUploadsAcrossGroups) { + static const GURL kDifferentEndpoint("https://endpoint2/"); + static const std::string kDifferentGroup("group2"); + + cache()->SetClient(kOrigin_, kEndpoint_, ReportingClient::Subdomains::EXCLUDE, + kGroup_, tomorrow()); + cache()->SetClient(kOrigin_, kDifferentEndpoint, + ReportingClient::Subdomains::EXCLUDE, kDifferentGroup, + tomorrow()); + + cache()->AddReport(kUrl_, kGroup_, kType_, + base::MakeUnique<base::DictionaryValue>(), + tick_clock()->NowTicks(), 0); + cache()->AddReport(kUrl_, kDifferentGroup, kType_, + base::MakeUnique<base::DictionaryValue>(), + tick_clock()->NowTicks(), 0); + + delivery_agent()->SendReports(); + ASSERT_EQ(2u, pending_uploads().size()); + + pending_uploads()[1]->Complete(ReportingUploader::Outcome::SUCCESS); + pending_uploads()[0]->Complete(ReportingUploader::Outcome::SUCCESS); + EXPECT_EQ(0u, pending_uploads().size()); +} + +} // namespace +} // namespace net diff --git a/chromium/net/reporting/reporting_endpoint_manager.cc b/chromium/net/reporting/reporting_endpoint_manager.cc new file mode 100644 index 00000000000..3d533dafbdf --- /dev/null +++ b/chromium/net/reporting/reporting_endpoint_manager.cc @@ -0,0 +1,80 @@ +// Copyright 2017 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_endpoint_manager.h" + +#include <string> +#include <vector> + +#include "base/logging.h" +#include "base/memory/ptr_util.h" +#include "base/rand_util.h" +#include "base/stl_util.h" +#include "base/time/tick_clock.h" +#include "net/base/backoff_entry.h" +#include "net/reporting/reporting_cache.h" +#include "net/reporting/reporting_client.h" +#include "net/reporting/reporting_policy.h" +#include "url/gurl.h" +#include "url/origin.h" + +namespace net { + +ReportingEndpointManager::ReportingEndpointManager(ReportingContext* context) + : context_(context) {} + +ReportingEndpointManager::~ReportingEndpointManager() {} + +bool ReportingEndpointManager::FindEndpointForOriginAndGroup( + const url::Origin& origin, + const std::string& group, + GURL* endpoint_url_out) { + std::vector<const ReportingClient*> clients; + cache()->GetClientsForOriginAndGroup(origin, group, &clients); + + // Filter out expired, pending, and backed-off endpoints. + std::vector<const ReportingClient*> available_clients; + base::TimeTicks now = tick_clock()->NowTicks(); + for (const ReportingClient* client : clients) { + if (client->expires < now) + continue; + if (base::ContainsKey(pending_endpoints_, client->endpoint)) + continue; + if (base::ContainsKey(endpoint_backoff_, client->endpoint) && + endpoint_backoff_[client->endpoint]->ShouldRejectRequest()) { + continue; + } + available_clients.push_back(client); + } + + if (available_clients.empty()) { + *endpoint_url_out = GURL(); + return false; + } + + int random_index = base::RandInt(0, available_clients.size() - 1); + *endpoint_url_out = available_clients[random_index]->endpoint; + return true; +} + +void ReportingEndpointManager::SetEndpointPending(const GURL& endpoint) { + DCHECK(!base::ContainsKey(pending_endpoints_, endpoint)); + pending_endpoints_.insert(endpoint); +} + +void ReportingEndpointManager::ClearEndpointPending(const GURL& endpoint) { + DCHECK(base::ContainsKey(pending_endpoints_, endpoint)); + pending_endpoints_.erase(endpoint); +} + +void ReportingEndpointManager::InformOfEndpointRequest(const GURL& endpoint, + bool succeeded) { + if (!base::ContainsKey(endpoint_backoff_, endpoint)) { + endpoint_backoff_[endpoint] = base::MakeUnique<BackoffEntry>( + &policy().endpoint_backoff_policy, tick_clock()); + } + endpoint_backoff_[endpoint]->InformOfRequest(succeeded); +} + +} // namespace net diff --git a/chromium/net/reporting/reporting_endpoint_manager.h b/chromium/net/reporting/reporting_endpoint_manager.h new file mode 100644 index 00000000000..e3cc8277fa7 --- /dev/null +++ b/chromium/net/reporting/reporting_endpoint_manager.h @@ -0,0 +1,82 @@ +// Copyright 2017 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. + +#ifndef NET_REPORTING_REPORTING_ENDPOINT_MANAGER_H_ +#define NET_REPORTING_REPORTING_ENDPOINT_MANAGER_H_ + +#include <map> +#include <memory> +#include <set> +#include <string> + +#include "base/macros.h" +#include "base/time/tick_clock.h" +#include "net/base/backoff_entry.h" +#include "net/base/net_export.h" +#include "net/reporting/reporting_context.h" + +class GURL; + +namespace base { +class TickClock; +} // namespace base + +namespace url { +class Origin; +} // namespace url + +namespace net { + +class ReportingCache; +struct ReportingPolicy; + +// Keeps track of which endpoints are pending (have active delivery attempts to +// them) or in exponential backoff after one or more failures, and chooses an +// endpoint from an endpoint group to receive reports for an origin. +class NET_EXPORT ReportingEndpointManager { + public: + // |context| must outlive the ReportingEndpointManager. + ReportingEndpointManager(ReportingContext* context); + ~ReportingEndpointManager(); + + // Finds an endpoint configured by |origin| in group |group| that is not + // pending, in exponential backoff from failed requests, or expired. + // + // Deliberately chooses an endpoint randomly to ensure sites aren't relying on + // any sort of fallback ordering. + // + // Returns true and sets |*endpoint_url_out| to the endpoint URL if an + // endpoint was chosen; returns false (and leaves |*endpoint_url_out| invalid) + // if no endpoint was found. + bool FindEndpointForOriginAndGroup(const url::Origin& origin, + const std::string& group, + GURL* endpoint_url_out); + + // Adds |endpoint| to the set of pending endpoints, preventing it from being + // chosen for a second parallel delivery attempt. + void SetEndpointPending(const GURL& endpoint); + + // Removes |endpoint| from the set of pending endpoints. + void ClearEndpointPending(const GURL& endpoint); + + // Informs the EndpointManager of a successful or unsuccessful request made to + // |endpoint| so it can manage exponential backoff of failing endpoints. + void InformOfEndpointRequest(const GURL& endpoint, bool succeeded); + + private: + const ReportingPolicy& policy() { return context_->policy(); } + base::TickClock* tick_clock() { return context_->tick_clock(); } + ReportingCache* cache() { return context_->cache(); } + + ReportingContext* context_; + + std::set<GURL> pending_endpoints_; + std::map<GURL, std::unique_ptr<net::BackoffEntry>> endpoint_backoff_; + + DISALLOW_COPY_AND_ASSIGN(ReportingEndpointManager); +}; + +} // namespace net + +#endif // NET_REPORTING_REPORTING_ENDPOINT_MANAGER_H_ diff --git a/chromium/net/reporting/reporting_endpoint_manager_unittest.cc b/chromium/net/reporting/reporting_endpoint_manager_unittest.cc new file mode 100644 index 00000000000..113128dbf50 --- /dev/null +++ b/chromium/net/reporting/reporting_endpoint_manager_unittest.cc @@ -0,0 +1,178 @@ +// Copyright 2017 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_endpoint_manager.h" + +#include <string> + +#include "base/test/simple_test_tick_clock.h" +#include "base/time/time.h" +#include "net/base/backoff_entry.h" +#include "net/reporting/reporting_cache.h" +#include "net/reporting/reporting_client.h" +#include "net/reporting/reporting_policy.h" +#include "net/reporting/reporting_test_util.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" +#include "url/origin.h" + +namespace net { +namespace { + +class ReportingEndpointManagerTest : public ReportingTestBase { + protected: + const url::Origin kOrigin_ = url::Origin(GURL("https://origin/")); + const GURL kEndpoint_ = GURL("https://endpoint/"); + const std::string kGroup_ = "group"; +}; + +TEST_F(ReportingEndpointManagerTest, NoEndpoint) { + GURL endpoint_url; + bool found_endpoint = endpoint_manager()->FindEndpointForOriginAndGroup( + kOrigin_, kGroup_, &endpoint_url); + EXPECT_FALSE(found_endpoint); +} + +TEST_F(ReportingEndpointManagerTest, Endpoint) { + cache()->SetClient(kOrigin_, kEndpoint_, ReportingClient::Subdomains::EXCLUDE, + kGroup_, tomorrow()); + + GURL endpoint_url; + bool found_endpoint = endpoint_manager()->FindEndpointForOriginAndGroup( + kOrigin_, kGroup_, &endpoint_url); + EXPECT_TRUE(found_endpoint); + EXPECT_EQ(kEndpoint_, endpoint_url); +} + +TEST_F(ReportingEndpointManagerTest, ExpiredEndpoint) { + cache()->SetClient(kOrigin_, kEndpoint_, ReportingClient::Subdomains::EXCLUDE, + kGroup_, yesterday()); + + GURL endpoint_url; + bool found_endpoint = endpoint_manager()->FindEndpointForOriginAndGroup( + kOrigin_, kGroup_, &endpoint_url); + EXPECT_FALSE(found_endpoint); +} + +TEST_F(ReportingEndpointManagerTest, PendingEndpoint) { + cache()->SetClient(kOrigin_, kEndpoint_, ReportingClient::Subdomains::EXCLUDE, + kGroup_, tomorrow()); + + endpoint_manager()->SetEndpointPending(kEndpoint_); + + GURL endpoint_url; + bool found_endpoint = endpoint_manager()->FindEndpointForOriginAndGroup( + kOrigin_, kGroup_, &endpoint_url); + EXPECT_FALSE(found_endpoint); + + endpoint_manager()->ClearEndpointPending(kEndpoint_); + + found_endpoint = endpoint_manager()->FindEndpointForOriginAndGroup( + kOrigin_, kGroup_, &endpoint_url); + EXPECT_TRUE(found_endpoint); + EXPECT_EQ(kEndpoint_, endpoint_url); +} + +TEST_F(ReportingEndpointManagerTest, BackedOffEndpoint) { + ASSERT_EQ(2.0, policy().endpoint_backoff_policy.multiply_factor); + + base::TimeDelta initial_delay = base::TimeDelta::FromMilliseconds( + policy().endpoint_backoff_policy.initial_delay_ms); + + cache()->SetClient(kOrigin_, kEndpoint_, ReportingClient::Subdomains::EXCLUDE, + kGroup_, tomorrow()); + + endpoint_manager()->InformOfEndpointRequest(kEndpoint_, false); + + // After one failure, endpoint is in exponential backoff. + GURL endpoint_url; + bool found_endpoint = endpoint_manager()->FindEndpointForOriginAndGroup( + kOrigin_, kGroup_, &endpoint_url); + EXPECT_FALSE(found_endpoint); + + // After initial delay, endpoint is usable again. + tick_clock()->Advance(initial_delay); + + found_endpoint = endpoint_manager()->FindEndpointForOriginAndGroup( + kOrigin_, kGroup_, &endpoint_url); + EXPECT_TRUE(found_endpoint); + EXPECT_EQ(kEndpoint_, endpoint_url); + + endpoint_manager()->InformOfEndpointRequest(kEndpoint_, false); + + // After a second failure, endpoint is backed off again. + found_endpoint = endpoint_manager()->FindEndpointForOriginAndGroup( + kOrigin_, kGroup_, &endpoint_url); + EXPECT_FALSE(found_endpoint); + + tick_clock()->Advance(initial_delay); + + // Next backoff is longer -- 2x the first -- so endpoint isn't usable yet. + found_endpoint = endpoint_manager()->FindEndpointForOriginAndGroup( + kOrigin_, kGroup_, &endpoint_url); + EXPECT_FALSE(found_endpoint); + + tick_clock()->Advance(initial_delay); + + // After 2x the initial delay, the endpoint is usable again. + found_endpoint = endpoint_manager()->FindEndpointForOriginAndGroup( + kOrigin_, kGroup_, &endpoint_url); + EXPECT_TRUE(found_endpoint); + EXPECT_EQ(kEndpoint_, endpoint_url); + + endpoint_manager()->InformOfEndpointRequest(kEndpoint_, true); + endpoint_manager()->InformOfEndpointRequest(kEndpoint_, true); + + // Two more successful requests should reset the backoff to the initial delay + // again. + endpoint_manager()->InformOfEndpointRequest(kEndpoint_, false); + + found_endpoint = endpoint_manager()->FindEndpointForOriginAndGroup( + kOrigin_, kGroup_, &endpoint_url); + EXPECT_FALSE(found_endpoint); + + tick_clock()->Advance(initial_delay); + + found_endpoint = endpoint_manager()->FindEndpointForOriginAndGroup( + kOrigin_, kGroup_, &endpoint_url); + EXPECT_TRUE(found_endpoint); +} + +// Make sure that multiple endpoints will all be returned at some point, to +// avoid accidentally or intentionally implementing any priority ordering. +TEST_F(ReportingEndpointManagerTest, RandomEndpoint) { + static const GURL kEndpoint_1("https://endpoint1/"); + static const GURL kEndpoint_2("https://endpoint2/"); + static const int kMaxAttempts = 20; + + cache()->SetClient(kOrigin_, kEndpoint_1, + ReportingClient::Subdomains::EXCLUDE, kGroup_, tomorrow()); + cache()->SetClient(kOrigin_, kEndpoint_2, + ReportingClient::Subdomains::EXCLUDE, kGroup_, tomorrow()); + + bool endpoint1_seen = false; + bool endpoint2_seen = false; + + for (int i = 0; i < kMaxAttempts; i++) { + GURL endpoint_url; + bool found_endpoint = endpoint_manager()->FindEndpointForOriginAndGroup( + kOrigin_, kGroup_, &endpoint_url); + ASSERT_TRUE(found_endpoint); + ASSERT_TRUE(endpoint_url == kEndpoint_1 || endpoint_url == kEndpoint_2); + + if (endpoint_url == kEndpoint_1) + endpoint1_seen = true; + else if (endpoint_url == kEndpoint_2) + endpoint2_seen = true; + + if (endpoint1_seen && endpoint2_seen) + break; + } + + EXPECT_TRUE(endpoint1_seen); + EXPECT_TRUE(endpoint2_seen); +} + +} // namespace +} // namespace net diff --git a/chromium/net/reporting/reporting_garbage_collector.cc b/chromium/net/reporting/reporting_garbage_collector.cc new file mode 100644 index 00000000000..76dbf3ad0e4 --- /dev/null +++ b/chromium/net/reporting/reporting_garbage_collector.cc @@ -0,0 +1,95 @@ +// Copyright 2017 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_garbage_collector.h" + +#include <vector> + +#include "base/memory/ptr_util.h" +#include "base/time/tick_clock.h" +#include "base/time/time.h" +#include "base/timer/timer.h" +#include "net/reporting/reporting_cache.h" +#include "net/reporting/reporting_context.h" +#include "net/reporting/reporting_observer.h" +#include "net/reporting/reporting_policy.h" +#include "net/reporting/reporting_report.h" + +namespace net { + +namespace { + +class ReportingGarbageCollectorImpl : public ReportingGarbageCollector, + public ReportingObserver { + public: + ReportingGarbageCollectorImpl(ReportingContext* context) + : context_(context), timer_(base::MakeUnique<base::OneShotTimer>()) {} + + // ReportingGarbageCollector implementation: + + ~ReportingGarbageCollectorImpl() override { + DCHECK(context_->initialized()); + context_->RemoveObserver(this); + } + + void Initialize() override { + context_->AddObserver(this); + CollectGarbage(); + } + + void SetTimerForTesting(std::unique_ptr<base::Timer> timer) override { + DCHECK(!context_->initialized()); + timer_ = std::move(timer); + } + + // ReportingObserver implementation: + void OnCacheUpdated() override { + DCHECK(context_->initialized()); + if (!timer_->IsRunning()) + StartTimer(); + } + + private: + void StartTimer() { + timer_->Start(FROM_HERE, context_->policy().garbage_collection_interval, + base::Bind(&ReportingGarbageCollectorImpl::CollectGarbage, + base::Unretained(this))); + } + + void CollectGarbage() { + base::TimeTicks now = context_->tick_clock()->NowTicks(); + const ReportingPolicy& policy = context_->policy(); + + std::vector<const ReportingReport*> all_reports; + context_->cache()->GetReports(&all_reports); + + std::vector<const ReportingReport*> reports_to_remove; + for (const ReportingReport* report : all_reports) { + if (now - report->queued >= policy.max_report_age || + report->attempts >= policy.max_report_attempts) { + reports_to_remove.push_back(report); + } + } + + // Don't restart the timer on the garbage collector's own updates. + context_->RemoveObserver(this); + context_->cache()->RemoveReports(reports_to_remove); + context_->AddObserver(this); + } + + ReportingContext* context_; + std::unique_ptr<base::Timer> timer_; +}; + +} // namespace + +// static +std::unique_ptr<ReportingGarbageCollector> ReportingGarbageCollector::Create( + ReportingContext* context) { + return base::MakeUnique<ReportingGarbageCollectorImpl>(context); +} + +ReportingGarbageCollector::~ReportingGarbageCollector() {} + +} // namespace net diff --git a/chromium/net/reporting/reporting_garbage_collector.h b/chromium/net/reporting/reporting_garbage_collector.h new file mode 100644 index 00000000000..17bd9ab5626 --- /dev/null +++ b/chromium/net/reporting/reporting_garbage_collector.h @@ -0,0 +1,42 @@ +// Copyright 2017 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. + +#ifndef NET_REPORTING_REPORTING_GARBAGE_COLLECTOR_H_ +#define NET_REPORTING_REPORTING_GARBAGE_COLLECTOR_H_ + +#include <memory> + +#include "net/base/net_export.h" + +namespace base { +class Timer; +} // namespace base + +namespace net { + +class ReportingContext; + +// Removes reports that have remained undelivered for too long or that have been +// included in too many failed delivery attempts. +class NET_EXPORT ReportingGarbageCollector { + public: + // Creates a ReportingGarbageCollector. |context| must outlive the garbage + // collector. + static std::unique_ptr<ReportingGarbageCollector> Create( + ReportingContext* context); + + virtual ~ReportingGarbageCollector(); + + // Initializes the GarbageCollector, which performs an initial garbage + // collection pass over any data already in the Cache. + virtual void Initialize() = 0; + + // Replaces the internal Timer used for scheduling garbage collection passes + // with a caller-specified one so that unittests can provide a MockTimer. + virtual void SetTimerForTesting(std::unique_ptr<base::Timer> timer) = 0; +}; + +} // namespace net + +#endif // NET_REPORTING_REPORTING_GARBAGE_COLLECTOR_H_ diff --git a/chromium/net/reporting/reporting_garbage_collector_unittest.cc b/chromium/net/reporting/reporting_garbage_collector_unittest.cc new file mode 100644 index 00000000000..19ed2b30052 --- /dev/null +++ b/chromium/net/reporting/reporting_garbage_collector_unittest.cc @@ -0,0 +1,91 @@ +// Copyright 2017 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_garbage_collector.h" + +#include <string> + +#include "base/memory/ptr_util.h" +#include "base/test/simple_test_tick_clock.h" +#include "base/time/time.h" +#include "base/timer/mock_timer.h" +#include "net/reporting/reporting_cache.h" +#include "net/reporting/reporting_policy.h" +#include "net/reporting/reporting_report.h" +#include "net/reporting/reporting_test_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace net { +namespace { + +class ReportingGarbageCollectorTest : public ReportingTestBase { + protected: + size_t report_count() { + std::vector<const ReportingReport*> reports; + cache()->GetReports(&reports); + return reports.size(); + } + + const GURL kUrl_ = GURL("https://origin/path"); + const std::string kGroup_ = "group"; + const std::string kType_ = "default"; +}; + +// Make sure the garbage collector is actually present in the context. +TEST_F(ReportingGarbageCollectorTest, Created) { + EXPECT_NE(nullptr, garbage_collector()); +} + +// Make sure that the garbage collection timer is started and stopped correctly. +TEST_F(ReportingGarbageCollectorTest, Timer) { + EXPECT_FALSE(garbage_collection_timer()->IsRunning()); + + cache()->AddReport(kUrl_, kGroup_, kType_, + base::MakeUnique<base::DictionaryValue>(), + tick_clock()->NowTicks(), 0); + + EXPECT_TRUE(garbage_collection_timer()->IsRunning()); + + garbage_collection_timer()->Fire(); + + EXPECT_FALSE(garbage_collection_timer()->IsRunning()); +} + +TEST_F(ReportingGarbageCollectorTest, Report) { + cache()->AddReport(kUrl_, kGroup_, kType_, + base::MakeUnique<base::DictionaryValue>(), + tick_clock()->NowTicks(), 0); + garbage_collection_timer()->Fire(); + + EXPECT_EQ(1u, report_count()); +} + +TEST_F(ReportingGarbageCollectorTest, ExpiredReport) { + cache()->AddReport(kUrl_, kGroup_, kType_, + base::MakeUnique<base::DictionaryValue>(), + tick_clock()->NowTicks(), 0); + tick_clock()->Advance(2 * policy().max_report_age); + garbage_collection_timer()->Fire(); + + EXPECT_EQ(0u, report_count()); +} + +TEST_F(ReportingGarbageCollectorTest, FailedReport) { + cache()->AddReport(kUrl_, kGroup_, kType_, + base::MakeUnique<base::DictionaryValue>(), + tick_clock()->NowTicks(), 0); + + std::vector<const ReportingReport*> reports; + cache()->GetReports(&reports); + for (int i = 0; i < policy().max_report_attempts; i++) { + cache()->IncrementReportsAttempts(reports); + } + + garbage_collection_timer()->Fire(); + + EXPECT_EQ(0u, report_count()); +} + +} // namespace +} // namespace net diff --git a/chromium/net/reporting/reporting_header_parser.cc b/chromium/net/reporting/reporting_header_parser.cc new file mode 100644 index 00000000000..8dbcb8e20ab --- /dev/null +++ b/chromium/net/reporting/reporting_header_parser.cc @@ -0,0 +1,98 @@ +// Copyright 2017 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_header_parser.h" + +#include <string> + +#include "base/json/json_reader.h" +#include "base/logging.h" +#include "base/time/tick_clock.h" +#include "base/time/time.h" +#include "base/values.h" +#include "net/reporting/reporting_cache.h" +#include "net/reporting/reporting_context.h" + +namespace net { + +namespace { + +const char kUrlKey[] = "url"; +const char kIncludeSubdomainsKey[] = "includeSubdomains"; +const char kGroupKey[] = "group"; +const char kGroupDefaultValue[] = "default"; +const char kMaxAgeKey[] = "max-age"; + +} // namespace + +// static +void ReportingHeaderParser::ParseHeader(ReportingContext* context, + const GURL& url, + const std::string& json_value) { + DCHECK(url.SchemeIsCryptographic()); + + std::unique_ptr<base::Value> value = + base::JSONReader::Read("[" + json_value + "]"); + if (!value) + return; + + const base::ListValue* list = nullptr; + bool is_list = value->GetAsList(&list); + DCHECK(is_list); + + ReportingCache* cache = context->cache(); + base::TimeTicks now = context->tick_clock()->NowTicks(); + for (size_t i = 0; i < list->GetSize(); i++) { + const base::Value* endpoint = nullptr; + bool got_endpoint = list->Get(i, &endpoint); + DCHECK(got_endpoint); + ProcessEndpoint(cache, now, url, *endpoint); + } +} + +// static +void ReportingHeaderParser::ProcessEndpoint(ReportingCache* cache, + base::TimeTicks now, + const GURL& url, + const base::Value& value) { + const base::DictionaryValue* dict = nullptr; + if (!value.GetAsDictionary(&dict)) + return; + DCHECK(dict); + + std::string endpoint_url_string; + if (!dict->GetString(kUrlKey, &endpoint_url_string)) + return; + + GURL endpoint_url(endpoint_url_string); + if (!endpoint_url.is_valid()) + return; + if (!endpoint_url.SchemeIsCryptographic()) + return; + + int ttl_sec = -1; + if (!dict->GetInteger(kMaxAgeKey, &ttl_sec) || ttl_sec < 0) + return; + + std::string group = kGroupDefaultValue; + if (dict->HasKey(kGroupKey) && !dict->GetString(kGroupKey, &group)) + return; + + ReportingClient::Subdomains subdomains = ReportingClient::Subdomains::EXCLUDE; + bool subdomains_bool = false; + if (dict->HasKey(kIncludeSubdomainsKey) && + dict->GetBoolean(kIncludeSubdomainsKey, &subdomains_bool) && + subdomains_bool == true) { + subdomains = ReportingClient::Subdomains::INCLUDE; + } + + if (ttl_sec > 0) { + cache->SetClient(url::Origin(url), endpoint_url, subdomains, group, + now + base::TimeDelta::FromSeconds(ttl_sec)); + } else { + cache->RemoveClientForOriginAndEndpoint(url::Origin(url), endpoint_url); + } +} + +} // namespace net diff --git a/chromium/net/reporting/reporting_header_parser.h b/chromium/net/reporting/reporting_header_parser.h new file mode 100644 index 00000000000..2d7ac6ee242 --- /dev/null +++ b/chromium/net/reporting/reporting_header_parser.h @@ -0,0 +1,48 @@ +// Copyright 2017 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. + +#ifndef NET_REPORTING_REPORTING_HEADER_PARSER_H_ +#define NET_REPORTING_REPORTING_HEADER_PARSER_H_ + +#include <string> + +#include "base/macros.h" +#include "base/time/time.h" +#include "net/base/net_export.h" + +class GURL; + +namespace base { +class Value; +} // namespace base + +namespace net { + +class ReportingCache; +class ReportingContext; + +class NET_EXPORT ReportingHeaderParser { + public: + static void ParseHeader(ReportingContext* context, + const GURL& url, + const std::string& json_value); + + private: + // Processes a single endpoint's parsed value from the Report-To header(s). + // Creates, updates, or removes a client in the cache as needed. + // + // |url| is the URL that the header came from. + // + // |value| is the parsed value. + static void ProcessEndpoint(ReportingCache* cache, + base::TimeTicks now, + const GURL& url, + const base::Value& value); + + DISALLOW_IMPLICIT_CONSTRUCTORS(ReportingHeaderParser); +}; + +} // namespace net + +#endif // NET_REPORTING_REPORTING_HEADER_PARSER_H_ diff --git a/chromium/net/reporting/reporting_header_parser_unittest.cc b/chromium/net/reporting/reporting_header_parser_unittest.cc new file mode 100644 index 00000000000..69210f2aa2f --- /dev/null +++ b/chromium/net/reporting/reporting_header_parser_unittest.cc @@ -0,0 +1,109 @@ +// Copyright 2017 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_header_parser.h" + +#include <string> +#include <vector> + +#include "base/memory/ptr_util.h" +#include "base/test/simple_test_tick_clock.h" +#include "base/time/time.h" +#include "base/values.h" +#include "net/reporting/reporting_cache.h" +#include "net/reporting/reporting_client.h" +#include "net/reporting/reporting_test_util.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" +#include "url/origin.h" + +namespace net { +namespace { + +class ReportingHeaderParserTest : public ReportingTestBase { + protected: + const GURL kUrl_ = GURL("https://origin/path"); + const url::Origin kOrigin_ = url::Origin(GURL("https://origin/")); + const GURL kEndpoint_ = GURL("https://endpoint/"); + const std::string kGroup_ = "group"; + const std::string kType_ = "type"; +}; + +TEST_F(ReportingHeaderParserTest, Invalid) { + static const struct { + const char* header_value; + const char* description; + } kInvalidHeaderTestCases[] = { + {"{\"max-age\":1}", "missing url"}, + {"{\"url\":0,\"max-age\":1}", "non-string url"}, + {"{\"url\":\"http://insecure/\",\"max-age\":1}", "insecure url"}, + + {"{\"url\":\"https://endpoint/\"}", "missing max-age"}, + {"{\"url\":\"https://endpoint/\",\"max-age\":\"\"}", + "non-integer max-age"}, + {"{\"url\":\"https://endpoint/\",\"max-age\":-1}", "negative max-age"}, + + {"{\"url\":\"https://endpoint/\",\"max-age\":1,\"group\":0}", + "non-string group"}, + + // Note that a non-boolean includeSubdomains field is *not* invalid, per + // the spec. + + {"[{\"url\":\"https://a/\",\"max-age\":1}," + "{\"url\":\"https://b/\",\"max-age\":1}]", + "wrapped in list"}}; + + for (size_t i = 0; i < arraysize(kInvalidHeaderTestCases); ++i) { + auto& test_case = kInvalidHeaderTestCases[i]; + ReportingHeaderParser::ParseHeader(context(), kUrl_, + test_case.header_value); + + std::vector<const ReportingClient*> clients; + cache()->GetClients(&clients); + EXPECT_TRUE(clients.empty()) + << "Invalid Report-To header (" << test_case.description << ": \"" + << test_case.header_value << "\") parsed as valid."; + } +} + +TEST_F(ReportingHeaderParserTest, Valid) { + ReportingHeaderParser::ParseHeader( + context(), kUrl_, + "{\"url\":\"" + kEndpoint_.spec() + "\",\"max-age\":86400}"); + + const ReportingClient* client = + FindClientInCache(cache(), kOrigin_, kEndpoint_); + ASSERT_TRUE(client); + EXPECT_EQ(kOrigin_, client->origin); + EXPECT_EQ(kEndpoint_, client->endpoint); + EXPECT_EQ(ReportingClient::Subdomains::EXCLUDE, client->subdomains); + EXPECT_EQ(86400, (client->expires - tick_clock()->NowTicks()).InSeconds()); +} + +TEST_F(ReportingHeaderParserTest, Subdomains) { + ReportingHeaderParser::ParseHeader(context(), kUrl_, + "{\"url\":\"" + kEndpoint_.spec() + + "\",\"max-age\":86400," + "\"includeSubdomains\":true}"); + + const ReportingClient* client = + FindClientInCache(cache(), kOrigin_, kEndpoint_); + ASSERT_TRUE(client); + EXPECT_EQ(ReportingClient::Subdomains::INCLUDE, client->subdomains); +} + +TEST_F(ReportingHeaderParserTest, ZeroMaxAge) { + cache()->SetClient(kOrigin_, kEndpoint_, ReportingClient::Subdomains::EXCLUDE, + kGroup_, + tick_clock()->NowTicks() + base::TimeDelta::FromDays(1)); + + ReportingHeaderParser::ParseHeader( + context(), kUrl_, + "{\"url\":\"" + kEndpoint_.spec() + "\",\"max-age\":0}"); + + EXPECT_EQ(nullptr, FindClientInCache(cache(), kOrigin_, kEndpoint_)); +} + +} // namespace +} // namespace net diff --git a/chromium/net/reporting/reporting_observer.cc b/chromium/net/reporting/reporting_observer.cc new file mode 100644 index 00000000000..5e8d778a61b --- /dev/null +++ b/chromium/net/reporting/reporting_observer.cc @@ -0,0 +1,15 @@ +// Copyright 2017 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_observer.h" + +namespace net { + +void ReportingObserver::OnCacheUpdated() {} + +ReportingObserver::ReportingObserver() {} + +ReportingObserver::~ReportingObserver() {} + +} // namespace net diff --git a/chromium/net/reporting/reporting_observer.h b/chromium/net/reporting/reporting_observer.h new file mode 100644 index 00000000000..cb05389b6f3 --- /dev/null +++ b/chromium/net/reporting/reporting_observer.h @@ -0,0 +1,28 @@ +// Copyright 2017 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. + +#ifndef NET_REPORTING_REPORTING_OBSERVER_H_ +#define NET_REPORTING_REPORTING_OBSERVER_H_ + +#include "base/macros.h" +#include "net/base/net_export.h" + +namespace net { + +class NET_EXPORT ReportingObserver { + public: + // Called whenever any change is made to the ReportingCache. + virtual void OnCacheUpdated(); + + protected: + ReportingObserver(); + + ~ReportingObserver(); + + DISALLOW_COPY_AND_ASSIGN(ReportingObserver); +}; + +} // namespace net + +#endif // NET_REPORTING_REPORTING_OBSERVER_H_ diff --git a/chromium/net/reporting/reporting_persister.cc b/chromium/net/reporting/reporting_persister.cc new file mode 100644 index 00000000000..ec2a37b7e48 --- /dev/null +++ b/chromium/net/reporting/reporting_persister.cc @@ -0,0 +1,358 @@ +// Copyright 2017 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_persister.h" + +#include <vector> + +#include "base/memory/ptr_util.h" +#include "base/strings/string_number_conversions.h" +#include "base/time/clock.h" +#include "base/time/tick_clock.h" +#include "base/time/time.h" +#include "base/timer/timer.h" +#include "base/values.h" +#include "net/reporting/reporting_cache.h" +#include "net/reporting/reporting_client.h" +#include "net/reporting/reporting_context.h" +#include "net/reporting/reporting_delegate.h" +#include "net/reporting/reporting_observer.h" +#include "net/reporting/reporting_policy.h" +#include "net/reporting/reporting_report.h" + +namespace net { +namespace { + +std::unique_ptr<base::Value> SerializeOrigin(const url::Origin& origin) { + auto serialized = base::MakeUnique<base::DictionaryValue>(); + + serialized->SetString("scheme", origin.scheme()); + serialized->SetString("host", origin.host()); + serialized->SetInteger("port", origin.port()); + serialized->SetString("suborigin", origin.suborigin()); + + return std::move(serialized); +} + +bool DeserializeOrigin(const base::DictionaryValue& serialized, + url::Origin* origin_out) { + std::string scheme; + if (!serialized.GetString("scheme", &scheme)) + return false; + + std::string host; + if (!serialized.GetString("host", &host)) + return false; + + int port_int; + if (!serialized.GetInteger("port", &port_int)) + return false; + uint16_t port = static_cast<uint16_t>(port_int); + if (port_int != port) + return false; + + std::string suborigin; + if (!serialized.GetString("suborigin", &suborigin)) + return false; + + *origin_out = url::Origin::CreateFromNormalizedTupleWithSuborigin( + scheme, host, port, suborigin); + return true; +} + +class ReportingPersisterImpl : public ReportingPersister, + public ReportingObserver { + public: + ReportingPersisterImpl(ReportingContext* context) + : context_(context), timer_(base::MakeUnique<base::OneShotTimer>()) {} + + // ReportingPersister implementation: + + ~ReportingPersisterImpl() override { + DCHECK(context_->initialized()); + context_->RemoveObserver(this); + } + + void Initialize() override { + std::unique_ptr<const base::Value> persisted_data = + context_->delegate()->GetPersistedData(); + if (persisted_data) + Deserialize(*persisted_data); + context_->AddObserver(this); + } + + void SetTimerForTesting(std::unique_ptr<base::Timer> timer) override { + DCHECK(!context_->initialized()); + timer_ = std::move(timer); + } + + // ReportingObserver implementation: + + void OnCacheUpdated() override { + DCHECK(context_->initialized()); + if (!timer_->IsRunning()) + StartTimer(); + } + + private: + void StartTimer() { + timer_->Start( + FROM_HERE, context_->policy().persistence_interval, + base::Bind(&ReportingPersisterImpl::Persist, base::Unretained(this))); + } + + void Persist() { delegate()->PersistData(Serialize()); } + + std::string SerializeTicks(base::TimeTicks time_ticks) { + base::Time time = time_ticks - tick_clock()->NowTicks() + clock()->Now(); + return base::Int64ToString(time.ToInternalValue()); + } + + bool DeserializeTicks(const std::string& serialized, + base::TimeTicks* time_ticks_out) { + int64_t internal; + if (!base::StringToInt64(serialized, &internal)) + return false; + + base::Time time = base::Time::FromInternalValue(internal); + *time_ticks_out = time - clock()->Now() + tick_clock()->NowTicks(); + return true; + } + + std::unique_ptr<base::Value> SerializeReport(const ReportingReport& report) { + auto serialized = base::MakeUnique<base::DictionaryValue>(); + + serialized->SetString("url", report.url.spec()); + serialized->SetString("group", report.group); + serialized->SetString("type", report.type); + serialized->Set("body", report.body->CreateDeepCopy()); + serialized->SetString("queued", SerializeTicks(report.queued)); + serialized->SetInteger("attempts", report.attempts); + + return std::move(serialized); + } + + bool DeserializeReport(const base::DictionaryValue& report) { + std::string url_string; + if (!report.GetString("url", &url_string)) + return false; + GURL url(url_string); + if (!url.is_valid()) + return false; + + std::string group; + if (!report.GetString("group", &group)) + return false; + + std::string type; + if (!report.GetString("type", &type)) + return false; + + const base::Value* body_original; + if (!report.Get("body", &body_original)) + return false; + std::unique_ptr<base::Value> body = body_original->CreateDeepCopy(); + + std::string queued_string; + if (!report.GetString("queued", &queued_string)) + return false; + base::TimeTicks queued; + if (!DeserializeTicks(queued_string, &queued)) + return false; + + int attempts; + if (!report.GetInteger("attempts", &attempts)) + return false; + if (attempts < 0) + return false; + + cache()->AddReport(url, group, type, std::move(body), queued, attempts); + return true; + } + + std::unique_ptr<base::Value> SerializeReports() { + std::vector<const ReportingReport*> reports; + cache()->GetReports(&reports); + + auto serialized = base::MakeUnique<base::ListValue>(); + for (const ReportingReport* report : reports) + serialized->Append(SerializeReport(*report)); + + return std::move(serialized); + } + + bool DeserializeReports(const base::ListValue& reports) { + for (size_t i = 0; i < reports.GetSize(); ++i) { + const base::DictionaryValue* report; + if (!reports.GetDictionary(i, &report)) + return false; + if (!DeserializeReport(*report)) + return false; + } + + return true; + } + + std::unique_ptr<base::Value> SerializeClient(const ReportingClient& client) { + auto serialized = base::MakeUnique<base::DictionaryValue>(); + + serialized->Set("origin", SerializeOrigin(client.origin)); + serialized->SetString("endpoint", client.endpoint.spec()); + serialized->SetBoolean( + "subdomains", + client.subdomains == ReportingClient::Subdomains::INCLUDE); + serialized->SetString("group", client.group); + serialized->SetString("expires", SerializeTicks(client.expires)); + + return std::move(serialized); + } + + bool DeserializeClient(const base::DictionaryValue& client) { + const base::DictionaryValue* origin_value; + if (!client.GetDictionary("origin", &origin_value)) + return false; + url::Origin origin; + if (!DeserializeOrigin(*origin_value, &origin)) + return false; + + std::string endpoint_string; + if (!client.GetString("endpoint", &endpoint_string)) + return false; + GURL endpoint(endpoint_string); + if (!endpoint.is_valid()) + return false; + + bool subdomains_bool; + if (!client.GetBoolean("subdomains", &subdomains_bool)) + return false; + ReportingClient::Subdomains subdomains = + subdomains_bool ? ReportingClient::Subdomains::INCLUDE + : ReportingClient::Subdomains::EXCLUDE; + + std::string group; + if (!client.GetString("group", &group)) + return false; + + std::string expires_string; + if (!client.GetString("expires", &expires_string)) + return false; + base::TimeTicks expires; + if (!DeserializeTicks(expires_string, &expires)) + return false; + + cache()->SetClient(origin, endpoint, subdomains, group, expires); + return true; + } + + std::unique_ptr<base::Value> SerializeClients() { + std::vector<const ReportingClient*> clients; + cache()->GetClients(&clients); + + auto serialized = base::MakeUnique<base::ListValue>(); + for (const ReportingClient* client : clients) + serialized->Append(SerializeClient(*client)); + + return std::move(serialized); + } + + bool DeserializeClients(const base::ListValue& clients) { + for (size_t i = 0; i < clients.GetSize(); ++i) { + const base::DictionaryValue* client; + if (!clients.GetDictionary(i, &client)) + return false; + if (!DeserializeClient(*client)) + return false; + } + + return true; + } + + static const int kSupportedVersion = 1; + + std::unique_ptr<base::Value> Serialize() { + auto serialized = base::MakeUnique<base::DictionaryValue>(); + + serialized->SetInteger("reporting_serialized_cache_version", + kSupportedVersion); + + bool persist_reports = policy().persist_reports_across_restarts; + serialized->SetBoolean("includes_reports", persist_reports); + if (persist_reports) + serialized->Set("reports", SerializeReports()); + + bool persist_clients = policy().persist_clients_across_restarts; + serialized->SetBoolean("includes_clients", persist_clients); + if (persist_clients) + serialized->Set("clients", SerializeClients()); + + return std::move(serialized); + } + + bool Deserialize(const base::Value& serialized_value) { + std::vector<const ReportingReport*> reports; + cache()->GetReports(&reports); + DCHECK(reports.empty()); + + std::vector<const ReportingClient*> clients; + cache()->GetClients(&clients); + DCHECK(clients.empty()); + + int version; + + const base::DictionaryValue* serialized; + if (!serialized_value.GetAsDictionary(&serialized)) + return false; + + if (!serialized->GetInteger("reporting_serialized_cache_version", &version)) + return false; + if (version != kSupportedVersion) + return false; + + bool includes_reports; + bool includes_clients; + if (!serialized->GetBoolean("includes_reports", &includes_reports) || + !serialized->GetBoolean("includes_clients", &includes_clients)) { + return false; + } + + if (includes_reports) { + const base::ListValue* reports; + if (!serialized->GetList("reports", &reports)) + return false; + if (!DeserializeReports(*reports)) + return false; + } + + if (includes_clients) { + const base::ListValue* clients; + if (!serialized->GetList("clients", &clients)) + return false; + if (!DeserializeClients(*clients)) + return false; + } + + return true; + } + + const ReportingPolicy& policy() { return context_->policy(); } + ReportingDelegate* delegate() { return context_->delegate(); } + base::Clock* clock() { return context_->clock(); } + base::TickClock* tick_clock() { return context_->tick_clock(); } + ReportingCache* cache() { return context_->cache(); } + + ReportingContext* context_; + std::unique_ptr<base::Timer> timer_; +}; + +} // namespace + +// static +std::unique_ptr<ReportingPersister> ReportingPersister::Create( + ReportingContext* context) { + return base::MakeUnique<ReportingPersisterImpl>(context); +} + +ReportingPersister::~ReportingPersister() {} + +} // namespace net diff --git a/chromium/net/reporting/reporting_persister.h b/chromium/net/reporting/reporting_persister.h new file mode 100644 index 00000000000..27463a516bf --- /dev/null +++ b/chromium/net/reporting/reporting_persister.h @@ -0,0 +1,40 @@ +// Copyright 2017 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. + +#ifndef NET_REPORTING_REPORTING_PERSISTER_H_ +#define NET_REPORTING_REPORTING_PERSISTER_H_ + +#include <memory> + +#include "net/base/net_export.h" + +namespace base { +class Timer; +} // namespace base + +namespace net { + +class ReportingContext; + +// Periodically persists the state of the Reporting system to (reasonably) +// stable storage using the methods provided by the ReportingDelegate. +class NET_EXPORT ReportingPersister { + public: + // Creates a ReportingPersister. |context| must outlive the persister. + static std::unique_ptr<ReportingPersister> Create(ReportingContext* context); + + virtual ~ReportingPersister(); + + // Initializes the Persister, which deserializes any previously-persisted data + // that is available through the Context's Delegate. + virtual void Initialize() = 0; + + // Replaces the internal Timer used for scheduling writes to stable storage + // with a caller-specified one so that unittests can provide a MockTimer. + virtual void SetTimerForTesting(std::unique_ptr<base::Timer> timer) = 0; +}; + +} // namespace net + +#endif // NET_REPORTING_REPORTING_PERSISTER_H_ diff --git a/chromium/net/reporting/reporting_persister_unittest.cc b/chromium/net/reporting/reporting_persister_unittest.cc new file mode 100644 index 00000000000..ed109edacae --- /dev/null +++ b/chromium/net/reporting/reporting_persister_unittest.cc @@ -0,0 +1,80 @@ +// Copyright 2017 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_persister.h" + +#include "base/json/json_writer.h" +#include "base/memory/ptr_util.h" +#include "base/test/simple_test_clock.h" +#include "base/test/simple_test_tick_clock.h" +#include "base/time/time.h" +#include "base/timer/mock_timer.h" +#include "base/values.h" +#include "net/reporting/reporting_cache.h" +#include "net/reporting/reporting_client.h" +#include "net/reporting/reporting_policy.h" +#include "net/reporting/reporting_report.h" +#include "net/reporting/reporting_test_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace net { +namespace { + +class ReportingPersisterTest : public ReportingTestBase { + protected: + const GURL kUrl_ = GURL("https://origin/path"); + const url::Origin kOrigin_ = url::Origin(kUrl_); + const GURL kEndpoint_ = GURL("https://endpoint/"); + const std::string kGroup_ = "group"; + const std::string kType_ = "default"; +}; + +TEST_F(ReportingPersisterTest, Test) { + ReportingPolicy policy; + policy.persist_reports_across_restarts = true; + policy.persist_clients_across_restarts = true; + // Make sure reports don't expire on our simulated restart. + policy.max_report_age = base::TimeDelta::FromDays(30); + UsePolicy(policy); + + static const int kAttempts = 3; + + base::DictionaryValue body; + body.SetString("key", "value"); + + cache()->AddReport(kUrl_, kGroup_, kType_, body.CreateDeepCopy(), + tick_clock()->NowTicks(), kAttempts); + cache()->SetClient(kOrigin_, kEndpoint_, ReportingClient::Subdomains::EXCLUDE, + kGroup_, + tick_clock()->NowTicks() + base::TimeDelta::FromDays(1)); + + EXPECT_TRUE(persistence_timer()->IsRunning()); + persistence_timer()->Fire(); + + SimulateRestart(/* delta= */ base::TimeDelta::FromHours(1), + /* delta_ticks= */ base::TimeDelta::FromHours(-3)); + + std::vector<const ReportingReport*> reports; + cache()->GetReports(&reports); + ASSERT_EQ(1u, reports.size()); + EXPECT_EQ(kUrl_, reports[0]->url); + EXPECT_EQ(kGroup_, reports[0]->group); + EXPECT_EQ(kType_, reports[0]->type); + EXPECT_TRUE(base::Value::Equals(&body, reports[0]->body.get())); + EXPECT_EQ(tick_clock()->NowTicks() - base::TimeDelta::FromHours(1), + reports[0]->queued); + EXPECT_EQ(kAttempts, reports[0]->attempts); + + const ReportingClient* client = + FindClientInCache(cache(), kOrigin_, kEndpoint_); + ASSERT_TRUE(client); + EXPECT_EQ(ReportingClient::Subdomains::EXCLUDE, client->subdomains); + EXPECT_EQ(kGroup_, client->group); + EXPECT_EQ(tick_clock()->NowTicks() + base::TimeDelta::FromDays(1) - + base::TimeDelta::FromHours(1), + client->expires); +} + +} // namespace +} // namespace net diff --git a/chromium/net/reporting/reporting_policy.cc b/chromium/net/reporting/reporting_policy.cc new file mode 100644 index 00000000000..9461bfcb5cd --- /dev/null +++ b/chromium/net/reporting/reporting_policy.cc @@ -0,0 +1,38 @@ +// Copyright 2017 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_policy.h" + +#include "base/time/time.h" + +namespace net { + +ReportingPolicy::ReportingPolicy() + : persistence_interval(base::TimeDelta::FromMinutes(1)), + persist_reports_across_restarts(false), + persist_clients_across_restarts(true), + garbage_collection_interval(base::TimeDelta::FromMinutes(5)), + max_report_age(base::TimeDelta::FromMinutes(15)), + max_report_attempts(5) { + endpoint_backoff_policy.num_errors_to_ignore = 0; + endpoint_backoff_policy.initial_delay_ms = 60 * 1000; // 1 minute + endpoint_backoff_policy.multiply_factor = 2.0; + endpoint_backoff_policy.jitter_factor = 0.1; + endpoint_backoff_policy.maximum_backoff_ms = -1; // 1 hour + endpoint_backoff_policy.entry_lifetime_ms = -1; // infinite + endpoint_backoff_policy.always_use_initial_delay = false; +} + +ReportingPolicy::ReportingPolicy(const ReportingPolicy& other) + : endpoint_backoff_policy(other.endpoint_backoff_policy), + persistence_interval(other.persistence_interval), + persist_reports_across_restarts(other.persist_reports_across_restarts), + persist_clients_across_restarts(other.persist_clients_across_restarts), + garbage_collection_interval(other.garbage_collection_interval), + max_report_age(other.max_report_age), + max_report_attempts(other.max_report_attempts) {} + +ReportingPolicy::~ReportingPolicy() {} + +} // namespace net diff --git a/chromium/net/reporting/reporting_policy.h b/chromium/net/reporting/reporting_policy.h new file mode 100644 index 00000000000..51bba69b403 --- /dev/null +++ b/chromium/net/reporting/reporting_policy.h @@ -0,0 +1,48 @@ +// Copyright 2017 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. + +#ifndef NET_REPORTING_REPORTING_POLICY_H_ +#define NET_REPORTING_REPORTING_POLICY_H_ + +#include "base/time/time.h" +#include "net/base/backoff_entry.h" +#include "net/base/net_export.h" + +namespace net { + +// Various policy knobs for the Reporting system. +struct NET_EXPORT ReportingPolicy { + // Provides a reasonable default for use in a browser embedder. + ReportingPolicy(); + ReportingPolicy(const ReportingPolicy& other); + ~ReportingPolicy(); + + // Backoff policy for failing endpoints. + BackoffEntry::Policy endpoint_backoff_policy; + + // Minimum interval at which Reporting will persist state to (relatively) + // stable storage to be restored if the embedder restarts. + base::TimeDelta persistence_interval; + + // Whether to persist undelivered reports across embedder restarts. + bool persist_reports_across_restarts; + + // Whether to persist clients (per-origin endpoint configurations) across + // embedder restarts. + bool persist_clients_across_restarts; + + // Minimum interval at which to garbage-collect the cache. + base::TimeDelta garbage_collection_interval; + + // Maximum age a report can be queued for before being discarded as expired. + base::TimeDelta max_report_age; + + // Maximum number of delivery attempts a report can have before being + // discarded as failed. + int max_report_attempts; +}; + +} // namespace net + +#endif // NET_REPORTING_REPORTING_POLICY_H_ diff --git a/chromium/net/reporting/reporting_report.cc b/chromium/net/reporting/reporting_report.cc new file mode 100644 index 00000000000..24e571a8eef --- /dev/null +++ b/chromium/net/reporting/reporting_report.cc @@ -0,0 +1,31 @@ +// Copyright 2017 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_report.h" + +#include <memory> +#include <string> + +#include "base/time/time.h" +#include "base/values.h" +#include "url/gurl.h" + +namespace net { + +ReportingReport::ReportingReport(const GURL& url, + const std::string& group, + const std::string& type, + std::unique_ptr<const base::Value> body, + base::TimeTicks queued, + int attempts) + : url(url), + group(group), + type(type), + body(std::move(body)), + queued(queued), + attempts(attempts) {} + +ReportingReport::~ReportingReport() {} + +} // namespace net diff --git a/chromium/net/reporting/reporting_report.h b/chromium/net/reporting/reporting_report.h new file mode 100644 index 00000000000..b1b310171f9 --- /dev/null +++ b/chromium/net/reporting/reporting_report.h @@ -0,0 +1,60 @@ +// Copyright 2017 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. + +#ifndef NET_REPORTING_REPORTING_REPORT_H_ +#define NET_REPORTING_REPORTING_REPORT_H_ + +#include <memory> +#include <string> + +#include "base/time/time.h" +#include "net/base/net_export.h" +#include "url/gurl.h" + +namespace base { +class Value; +} // namespace base + +namespace net { + +// An undelivered report. +struct NET_EXPORT ReportingReport { + public: + ReportingReport(const GURL& url, + const std::string& group, + const std::string& type, + std::unique_ptr<const base::Value> body, + base::TimeTicks queued, + int attempts); + ~ReportingReport(); + + // The URL of the document that triggered the report. (Included in the + // delivered report.) + GURL url; + + // The endpoint group that should be used to deliver the report. (Not included + // in the delivered report.) + std::string group; + + // The type of the report. (Included in the delivered report.) + std::string type; + + // The body of the report. (Included in the delivered report.) + std::unique_ptr<const base::Value> body; + + // When the report was queued. (Included in the delivered report as an age + // relative to the time of the delivery attempt.) + base::TimeTicks queued; + + // The number of delivery attempts made so far, not including an active + // attempt. (Not included in the delivered report.) + int attempts = 0; + + private: + DISALLOW_COPY_AND_ASSIGN(ReportingReport); +}; + +} // namespace net + +#endif // NET_REPORTING_REPORTING_REPORT_H_ diff --git a/chromium/net/reporting/reporting_service.cc b/chromium/net/reporting/reporting_service.cc new file mode 100644 index 00000000000..6d2b215ce5f --- /dev/null +++ b/chromium/net/reporting/reporting_service.cc @@ -0,0 +1,76 @@ +// Copyright 2017 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 <memory> + +#include "base/callback.h" +#include "base/macros.h" +#include "base/memory/ptr_util.h" +#include "base/time/tick_clock.h" +#include "base/time/time.h" +#include "base/values.h" +#include "net/reporting/reporting_cache.h" +#include "net/reporting/reporting_context.h" +#include "net/reporting/reporting_delegate.h" +#include "net/reporting/reporting_header_parser.h" +#include "url/gurl.h" + +namespace net { + +namespace { + +class ReportingServiceImpl : public ReportingService { + public: + ReportingServiceImpl(std::unique_ptr<ReportingContext> context) + : context_(std::move(context)) { + // TODO(juliatuttle): This can be slow, so it might be better to expose it + // as a separate method and call it separately from constructing everything. + context_->Initialize(); + } + + ~ReportingServiceImpl() override {} + + void QueueReport(const GURL& url, + const std::string& group, + const std::string& type, + std::unique_ptr<const base::Value> body) override { + DCHECK(context_->initialized()); + context_->cache()->AddReport(url, group, type, std::move(body), + context_->tick_clock()->NowTicks(), 0); + } + + void ProcessHeader(const GURL& url, + const std::string& header_value) override { + DCHECK(context_->initialized()); + ReportingHeaderParser::ParseHeader(context_.get(), url, header_value); + } + + private: + std::unique_ptr<ReportingContext> context_; + + DISALLOW_COPY_AND_ASSIGN(ReportingServiceImpl); +}; + +} // namespace + +ReportingService::~ReportingService() {} + +// static +std::unique_ptr<ReportingService> ReportingService::Create( + const ReportingPolicy& policy, + URLRequestContext* request_context, + std::unique_ptr<ReportingDelegate> delegate) { + return base::MakeUnique<ReportingServiceImpl>( + ReportingContext::Create(policy, std::move(delegate), request_context)); +} + +// static +std::unique_ptr<ReportingService> ReportingService::CreateForTesting( + std::unique_ptr<ReportingContext> reporting_context) { + return base::MakeUnique<ReportingServiceImpl>(std::move(reporting_context)); +} + +} // namespace net diff --git a/chromium/net/reporting/reporting_service.h b/chromium/net/reporting/reporting_service.h new file mode 100644 index 00000000000..c305410a1a6 --- /dev/null +++ b/chromium/net/reporting/reporting_service.h @@ -0,0 +1,74 @@ +// Copyright 2017 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. + +#ifndef NET_REPORTING_REPORTING_SERVICE_H_ +#define NET_REPORTING_REPORTING_SERVICE_H_ + +#include <memory> +#include <string> + +#include "base/callback.h" +#include "base/macros.h" +#include "net/base/net_export.h" + +class GURL; + +namespace base { +class Value; +} // namespace + +namespace net { + +class ReportingContext; +class ReportingDelegate; +struct ReportingPolicy; +class URLRequestContext; + +// The external interface to the Reporting system, used by the embedder of //net +// and also other parts of //net. +class NET_EXPORT ReportingService { + public: + virtual ~ReportingService(); + + // Creates a ReportingService. |policy| will be copied. |request_context| must + // outlive the ReportingService. The ReportingService will take ownership of + // |delegate| and destroy it when the service is destroyed. + static std::unique_ptr<ReportingService> Create( + const ReportingPolicy& policy, + URLRequestContext* request_context, + std::unique_ptr<ReportingDelegate> delegate); + + // Creates a ReportingService for testing purposes using an + // already-constructed ReportingContext. The ReportingService will take + // ownership of |reporting_context| and destroy it when the service is + // destroyed. + static std::unique_ptr<ReportingService> CreateForTesting( + std::unique_ptr<ReportingContext> reporting_context); + + // Queues a report for delivery. |url| is the URL that originated the report. + // |group| is the endpoint group to which the report should be delivered. + // |type| is the type of the report. |body| is the body of the report. + // + // The Reporting system will take ownership of |body|; all other parameters + // will be copied. + virtual void QueueReport(const GURL& url, + const std::string& group, + const std::string& type, + std::unique_ptr<const base::Value> body) = 0; + + // Processes a Report-To header. |url| is the URL that originated the header; + // |header_value| is the normalized value of the Report-To header. + virtual void ProcessHeader(const GURL& url, + const std::string& header_value) = 0; + + protected: + ReportingService() {} + + private: + DISALLOW_COPY_AND_ASSIGN(ReportingService); +}; + +} // namespace net + +#endif // NET_REPORTING_REPORTING_SERVICE_H_ diff --git a/chromium/net/reporting/reporting_service_unittest.cc b/chromium/net/reporting/reporting_service_unittest.cc new file mode 100644 index 00000000000..20c86560d22 --- /dev/null +++ b/chromium/net/reporting/reporting_service_unittest.cc @@ -0,0 +1,78 @@ +// Copyright 2017 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 <memory> +#include <string> + +#include "base/memory/ptr_util.h" +#include "base/time/tick_clock.h" +#include "base/values.h" +#include "net/reporting/reporting_cache.h" +#include "net/reporting/reporting_context.h" +#include "net/reporting/reporting_delegate.h" +#include "net/reporting/reporting_policy.h" +#include "net/reporting/reporting_report.h" +#include "net/reporting/reporting_service.h" +#include "net/reporting/reporting_test_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace net { +namespace { + +class ReportingServiceTest : public ::testing::Test { + protected: + const GURL kUrl_ = GURL("https://origin/path"); + const url::Origin kOrigin_ = url::Origin(kUrl_); + const GURL kEndpoint_ = GURL("https://endpoint/"); + const std::string kGroup_ = "group"; + const std::string kType_ = "type"; + + ReportingServiceTest() + : context_(new TestReportingContext(ReportingPolicy())), + service_( + ReportingService::CreateForTesting(base::WrapUnique(context_))) {} + + TestReportingContext* context() { return context_; } + ReportingService* service() { return service_.get(); } + + private: + TestReportingContext* context_; + std::unique_ptr<ReportingService> service_; +}; + +TEST_F(ReportingServiceTest, QueueReport) { + service()->QueueReport(kUrl_, kGroup_, kType_, + base::MakeUnique<base::DictionaryValue>()); + + std::vector<const ReportingReport*> reports; + context()->cache()->GetReports(&reports); + ASSERT_EQ(1u, reports.size()); + EXPECT_EQ(kUrl_, reports[0]->url); + EXPECT_EQ(kGroup_, reports[0]->group); + EXPECT_EQ(kType_, reports[0]->type); +} + +TEST_F(ReportingServiceTest, ProcessHeader) { + service()->ProcessHeader(kUrl_, "{\"url\":\"" + kEndpoint_.spec() + + "\"," + "\"group\":\"" + + kGroup_ + + "\"," + "\"max-age\":86400}"); + + const ReportingClient* client = + FindClientInCache(context()->cache(), kOrigin_, kEndpoint_); + ASSERT_TRUE(client != nullptr); + EXPECT_EQ(kOrigin_, client->origin); + EXPECT_EQ(kEndpoint_, client->endpoint); + EXPECT_EQ(ReportingClient::Subdomains::EXCLUDE, client->subdomains); + EXPECT_EQ(kGroup_, client->group); + EXPECT_EQ(context()->tick_clock()->NowTicks() + base::TimeDelta::FromDays(1), + client->expires); +} + +} // namespace +} // namespace net diff --git a/chromium/net/reporting/reporting_test_util.cc b/chromium/net/reporting/reporting_test_util.cc new file mode 100644 index 00000000000..8a7dbd06f4d --- /dev/null +++ b/chromium/net/reporting/reporting_test_util.cc @@ -0,0 +1,185 @@ +// Copyright 2017 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_test_util.h" + +#include <memory> +#include <string> +#include <vector> + +#include "base/bind.h" +#include "base/json/json_reader.h" +#include "base/memory/ptr_util.h" +#include "base/test/simple_test_clock.h" +#include "base/test/simple_test_tick_clock.h" +#include "base/timer/mock_timer.h" +#include "base/timer/timer.h" +#include "net/reporting/reporting_cache.h" +#include "net/reporting/reporting_client.h" +#include "net/reporting/reporting_context.h" +#include "net/reporting/reporting_delegate.h" +#include "net/reporting/reporting_garbage_collector.h" +#include "net/reporting/reporting_persister.h" +#include "net/reporting/reporting_policy.h" +#include "net/reporting/reporting_uploader.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" +#include "url/origin.h" + +namespace net { + +namespace { + +class PendingUploadImpl : public TestReportingUploader::PendingUpload { + public: + PendingUploadImpl( + const GURL& url, + const std::string& json, + const ReportingUploader::Callback& callback, + const base::Callback<void(PendingUpload*)>& complete_callback) + : url_(url), + json_(json), + callback_(callback), + complete_callback_(complete_callback) {} + + ~PendingUploadImpl() override {} + + // PendingUpload implementationP: + const GURL& url() const override { return url_; } + const std::string& json() const override { return json_; } + std::unique_ptr<base::Value> GetValue() const override { + return base::JSONReader::Read(json_); + } + + void Complete(ReportingUploader::Outcome outcome) override { + callback_.Run(outcome); + // Deletes |this|. + complete_callback_.Run(this); + } + + private: + GURL url_; + std::string json_; + ReportingUploader::Callback callback_; + base::Callback<void(PendingUpload*)> complete_callback_; +}; + +void ErasePendingUpload( + std::vector<std::unique_ptr<TestReportingUploader::PendingUpload>>* uploads, + TestReportingUploader::PendingUpload* upload) { + for (auto it = uploads->begin(); it != uploads->end(); ++it) { + if (it->get() == upload) { + uploads->erase(it); + return; + } + } + NOTREACHED(); +} + +} // namespace + +const ReportingClient* FindClientInCache(const ReportingCache* cache, + const url::Origin& origin, + const GURL& endpoint) { + std::vector<const ReportingClient*> clients; + cache->GetClients(&clients); + for (const ReportingClient* client : clients) { + if (client->origin == origin && client->endpoint == endpoint) + return client; + } + return nullptr; +} + +TestReportingDelegate::TestReportingDelegate() {} +TestReportingDelegate::~TestReportingDelegate() {} + +void TestReportingDelegate::PersistData( + std::unique_ptr<const base::Value> persisted_data) { + persisted_data_ = std::move(persisted_data); +} + +std::unique_ptr<const base::Value> TestReportingDelegate::GetPersistedData() { + if (!persisted_data_) + return std::unique_ptr<const base::Value>(); + return persisted_data_->CreateDeepCopy(); +} + +TestReportingUploader::PendingUpload::~PendingUpload() {} +TestReportingUploader::PendingUpload::PendingUpload() {} + +TestReportingUploader::TestReportingUploader() {} +TestReportingUploader::~TestReportingUploader() {} + +void TestReportingUploader::StartUpload(const GURL& url, + const std::string& json, + const Callback& callback) { + pending_uploads_.push_back(base::MakeUnique<PendingUploadImpl>( + url, json, callback, base::Bind(&ErasePendingUpload, &pending_uploads_))); +} + +TestReportingContext::TestReportingContext(const ReportingPolicy& policy) + : ReportingContext(policy, + base::MakeUnique<TestReportingDelegate>(), + base::MakeUnique<base::SimpleTestClock>(), + base::MakeUnique<base::SimpleTestTickClock>(), + base::MakeUnique<TestReportingUploader>()), + persistence_timer_(new base::MockTimer(/* retain_user_task= */ false, + /* is_repeating= */ false)), + garbage_collection_timer_( + new base::MockTimer(/* retain_user_task= */ false, + /* is_repeating= */ false)) { + persister()->SetTimerForTesting(base::WrapUnique(persistence_timer_)); + garbage_collector()->SetTimerForTesting( + base::WrapUnique(garbage_collection_timer_)); +} + +TestReportingContext::~TestReportingContext() { + persistence_timer_ = nullptr; + garbage_collection_timer_ = nullptr; +} + +ReportingTestBase::ReportingTestBase() { + // For tests, disable jitter. + ReportingPolicy policy; + policy.endpoint_backoff_policy.jitter_factor = 0.0; + + CreateAndInitializeContext(policy, std::unique_ptr<const base::Value>(), + base::Time::Now(), base::TimeTicks::Now()); +} + +ReportingTestBase::~ReportingTestBase() {} + +void ReportingTestBase::UsePolicy(const ReportingPolicy& new_policy) { + CreateAndInitializeContext(new_policy, delegate()->GetPersistedData(), + clock()->Now(), tick_clock()->NowTicks()); +} + +void ReportingTestBase::SimulateRestart(base::TimeDelta delta, + base::TimeDelta delta_ticks) { + CreateAndInitializeContext(policy(), delegate()->GetPersistedData(), + clock()->Now() + delta, + tick_clock()->NowTicks() + delta_ticks); +} + +void ReportingTestBase::CreateAndInitializeContext( + const ReportingPolicy& policy, + std::unique_ptr<const base::Value> persisted_data, + base::Time now, + base::TimeTicks now_ticks) { + context_ = base::MakeUnique<TestReportingContext>(policy); + delegate()->PersistData(std::move(persisted_data)); + clock()->SetNow(now); + tick_clock()->SetNowTicks(now_ticks); + context_->Initialize(); +} + +base::TimeTicks ReportingTestBase::yesterday() { + return tick_clock()->NowTicks() - base::TimeDelta::FromDays(1); +} + +base::TimeTicks ReportingTestBase::tomorrow() { + return tick_clock()->NowTicks() + base::TimeDelta::FromDays(1); +} + +} // namespace net diff --git a/chromium/net/reporting/reporting_test_util.h b/chromium/net/reporting/reporting_test_util.h new file mode 100644 index 00000000000..098fe0f4e21 --- /dev/null +++ b/chromium/net/reporting/reporting_test_util.h @@ -0,0 +1,195 @@ +// Copyright 2017 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. + +#ifndef NET_REPORTING_REPORTING_TEST_UTIL_H_ +#define NET_REPORTING_REPORTING_TEST_UTIL_H_ + +#include <memory> +#include <string> +#include <vector> + +#include "base/macros.h" +#include "net/reporting/reporting_context.h" +#include "net/reporting/reporting_delegate.h" +#include "net/reporting/reporting_uploader.h" +#include "testing/gtest/include/gtest/gtest.h" + +class GURL; + +namespace base { +class MockTimer; +class SimpleTestClock; +class SimpleTestTickClock; +class Value; +} // namespace base + +namespace url { +class Origin; +} // namespace url + +namespace net { + +class ReportingCache; +struct ReportingClient; +class ReportingGarbageCollector; + +// Finds a particular client (by origin and endpoint) in the cache and returns +// it (or nullptr if not found). +const ReportingClient* FindClientInCache(const ReportingCache* cache, + const url::Origin& origin, + const GURL& endpoint); + +// A simple implementation of ReportingDelegate that only persists data in RAM. +class TestReportingDelegate : public ReportingDelegate { + public: + TestReportingDelegate(); + + ~TestReportingDelegate() override; + + // ReportingDelegate implementation: + std::unique_ptr<const base::Value> GetPersistedData() override; + + void PersistData(std::unique_ptr<const base::Value> persisted_data) override; + + private: + std::unique_ptr<const base::Value> persisted_data_; + + DISALLOW_COPY_AND_ASSIGN(TestReportingDelegate); +}; + +// A test implementation of ReportingUploader that holds uploads for tests to +// examine and complete with a specified outcome. +class TestReportingUploader : public ReportingUploader { + public: + class PendingUpload { + public: + virtual ~PendingUpload(); + + virtual const GURL& url() const = 0; + virtual const std::string& json() const = 0; + virtual std::unique_ptr<base::Value> GetValue() const = 0; + + virtual void Complete(Outcome outcome) = 0; + + protected: + PendingUpload(); + }; + + TestReportingUploader(); + ~TestReportingUploader() override; + + const std::vector<std::unique_ptr<PendingUpload>>& pending_uploads() const { + return pending_uploads_; + } + + // ReportingUploader implementation: + void StartUpload(const GURL& url, + const std::string& json, + const Callback& callback) override; + + private: + std::vector<std::unique_ptr<PendingUpload>> pending_uploads_; + + DISALLOW_COPY_AND_ASSIGN(TestReportingUploader); +}; + +// A test implementation of ReportingContext that uses test versions of +// ReportingDelegate, Clock, TickClock, and ReportingUploader. +class TestReportingContext : public ReportingContext { + public: + TestReportingContext(const ReportingPolicy& policy); + ~TestReportingContext(); + + TestReportingDelegate* test_delegate() { + return reinterpret_cast<TestReportingDelegate*>(delegate()); + } + base::SimpleTestClock* test_clock() { + return reinterpret_cast<base::SimpleTestClock*>(clock()); + } + base::SimpleTestTickClock* test_tick_clock() { + return reinterpret_cast<base::SimpleTestTickClock*>(tick_clock()); + } + base::MockTimer* test_persistence_timer() { return persistence_timer_; } + base::MockTimer* test_garbage_collection_timer() { + return garbage_collection_timer_; + } + TestReportingUploader* test_uploader() { + return reinterpret_cast<TestReportingUploader*>(uploader()); + } + + private: + // Owned by the Persister and GarbageCollector, respectively, but referenced + // here to preserve type: + + base::MockTimer* persistence_timer_; + base::MockTimer* garbage_collection_timer_; + + DISALLOW_COPY_AND_ASSIGN(TestReportingContext); +}; + +// A unit test base class that provides a TestReportingContext and shorthand +// getters. +class ReportingTestBase : public ::testing::Test { + protected: + ReportingTestBase(); + ~ReportingTestBase() override; + + void UsePolicy(const ReportingPolicy& policy); + + // Simulates an embedder restart, preserving the ReportingPolicy and any data + // persisted via the TestReportingDelegate, but nothing else. + // + // Advances the Clock by |delta|, and the TickClock by |delta_ticks|. Both can + // be zero or negative. + void SimulateRestart(base::TimeDelta delta, base::TimeDelta delta_ticks); + + TestReportingContext* context() { return context_.get(); } + + const ReportingPolicy& policy() { return context_->policy(); } + + TestReportingDelegate* delegate() { return context_->test_delegate(); } + base::SimpleTestClock* clock() { return context_->test_clock(); } + base::SimpleTestTickClock* tick_clock() { + return context_->test_tick_clock(); + } + base::MockTimer* persistence_timer() { + return context_->test_persistence_timer(); + } + base::MockTimer* garbage_collection_timer() { + return context_->test_garbage_collection_timer(); + } + TestReportingUploader* uploader() { return context_->test_uploader(); } + + ReportingCache* cache() { return context_->cache(); } + ReportingEndpointManager* endpoint_manager() { + return context_->endpoint_manager(); + } + ReportingDeliveryAgent* delivery_agent() { + return context_->delivery_agent(); + } + ReportingGarbageCollector* garbage_collector() { + return context_->garbage_collector(); + } + + ReportingPersister* persister() { return context_->persister(); } + + base::TimeTicks yesterday(); + + base::TimeTicks tomorrow(); + + private: + void CreateAndInitializeContext( + const ReportingPolicy& policy, + std::unique_ptr<const base::Value> persisted_data, + base::Time now, + base::TimeTicks now_ticks); + + std::unique_ptr<TestReportingContext> context_; + + DISALLOW_COPY_AND_ASSIGN(ReportingTestBase); +}; + +} // namespace net + +#endif // NET_REPORING_REPORTING_TEST_UTIL_H_ diff --git a/chromium/net/reporting/reporting_uploader.cc b/chromium/net/reporting/reporting_uploader.cc new file mode 100644 index 00000000000..19dbecb740a --- /dev/null +++ b/chromium/net/reporting/reporting_uploader.cc @@ -0,0 +1,154 @@ +// 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_uploader.h" + +#include <memory> +#include <string> +#include <vector> + +#include "base/callback_helpers.h" +#include "base/memory/ptr_util.h" +#include "base/metrics/histogram_macros.h" +#include "net/base/elements_upload_data_stream.h" +#include "net/base/load_flags.h" +#include "net/base/upload_bytes_element_reader.h" +#include "net/http/http_response_headers.h" +#include "net/url_request/redirect_info.h" +#include "net/url_request/url_request_context.h" +#include "url/gurl.h" + +namespace net { + +namespace { + +ReportingUploader::Outcome ResponseCodeToOutcome(int response_code) { + if (response_code >= 200 && response_code <= 299) + return ReportingUploader::Outcome::SUCCESS; + if (response_code == 410) + return ReportingUploader::Outcome::REMOVE_ENDPOINT; + return ReportingUploader::Outcome::FAILURE; +} + +class ReportingUploaderImpl : public ReportingUploader, URLRequest::Delegate { + public: + ReportingUploaderImpl(const URLRequestContext* context) : context_(context) { + DCHECK(context_); + } + + ~ReportingUploaderImpl() override { + for (auto& it : uploads_) { + base::ResetAndReturn(&it.second->second).Run(Outcome::FAILURE); + it.second->first->Cancel(); + } + uploads_.clear(); + } + + void StartUpload(const GURL& url, + const std::string& json, + const Callback& callback) override { + std::unique_ptr<URLRequest> request = + context_->CreateRequest(url, IDLE, this); + + request->set_method("POST"); + + request->SetLoadFlags(LOAD_DISABLE_CACHE | LOAD_DO_NOT_SAVE_COOKIES | + LOAD_DO_NOT_SEND_COOKIES); + + request->SetExtraRequestHeaderByName(HttpRequestHeaders::kContentType, + kUploadContentType, true); + + std::vector<char> json_data(json.begin(), json.end()); + std::unique_ptr<UploadElementReader> reader( + new UploadOwnedBytesElementReader(&json_data)); + request->set_upload( + ElementsUploadDataStream::CreateWithReader(std::move(reader), 0)); + + // This inherently sets mode = "no-cors", but that doesn't matter, because + // the origins that are included in the upload don't actually get to see + // the response. + // + // This inherently skips Service Worker, too. + request->Start(); + + // Have to grab the unique_ptr* first to ensure request.get() happens + // before std::move(request). + std::unique_ptr<Upload>* upload = &uploads_[request.get()]; + *upload = base::MakeUnique<Upload>(std::move(request), callback); + } + + // URLRequest::Delegate implementation: + + void OnReceivedRedirect(URLRequest* request, + const RedirectInfo& redirect_info, + bool* defer_redirect) override { + if (!redirect_info.new_url.SchemeIsCryptographic()) { + request->Cancel(); + return; + } + } + + void OnAuthRequired(URLRequest* request, + AuthChallengeInfo* auth_info) override { + request->Cancel(); + } + + void OnCertificateRequested(URLRequest* request, + SSLCertRequestInfo* cert_request_info) override { + request->Cancel(); + } + + void OnSSLCertificateError(URLRequest* request, + const SSLInfo& ssl_info, + bool fatal) override { + request->Cancel(); + } + + void OnResponseStarted(URLRequest* request, int net_error) override { + // Grab Upload from map, and hold on to it in a local unique_ptr so it's + // removed at the end of the method. + auto it = uploads_.find(request); + DCHECK(it != uploads_.end()); + std::unique_ptr<Upload> upload = std::move(it->second); + uploads_.erase(it); + + // request->GetResponseCode() should work, but doesn't in the cases above + // where the request was canceled, so get the response code by hand. + // TODO(juliatuttle): Check if mmenke fixed this yet. + HttpResponseHeaders* headers = request->response_headers(); + int response_code = headers ? headers->response_code() : 0; + Outcome outcome = ResponseCodeToOutcome(response_code); + + upload->second.Run(outcome); + + request->Cancel(); + } + + void OnReadCompleted(URLRequest* request, int bytes_read) override { + // Reporting doesn't need anything in the body of the response, so it + // doesn't read it, so it should never get OnReadCompleted calls. + NOTREACHED(); + } + + private: + using Upload = std::pair<std::unique_ptr<URLRequest>, Callback>; + + const URLRequestContext* context_; + std::map<const URLRequest*, std::unique_ptr<Upload>> uploads_; +}; + +} // namespace + +// static +const char ReportingUploader::kUploadContentType[] = "application/report"; + +ReportingUploader::~ReportingUploader() {} + +// static +std::unique_ptr<ReportingUploader> ReportingUploader::Create( + const URLRequestContext* context) { + return base::MakeUnique<ReportingUploaderImpl>(context); +} + +} // namespace net diff --git a/chromium/net/reporting/reporting_uploader.h b/chromium/net/reporting/reporting_uploader.h new file mode 100644 index 00000000000..dfe50e73624 --- /dev/null +++ b/chromium/net/reporting/reporting_uploader.h @@ -0,0 +1,46 @@ +// 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. + +#ifndef NET_REPORTING_REPORTING_UPLOADER_H_ +#define NET_REPORTING_REPORTING_UPLOADER_H_ + +#include <memory> +#include <string> + +#include "base/callback.h" +#include "net/base/net_export.h" + +class GURL; + +namespace net { + +class URLRequestContext; + +// Uploads already-serialized reports and converts responses to one of the +// specified outcomes. +class NET_EXPORT ReportingUploader { + public: + enum class Outcome { SUCCESS, REMOVE_ENDPOINT, FAILURE }; + + using Callback = base::Callback<void(Outcome outcome)>; + + static const char kUploadContentType[]; + + virtual ~ReportingUploader(); + + // Starts to upload the reports in |json| (properly tagged as JSON data) to + // |url|, and calls |callback| when complete (whether successful or not). + virtual void StartUpload(const GURL& url, + const std::string& json, + const Callback& callback) = 0; + + // Creates a real implementation of |ReportingUploader| that uploads reports + // using |context|. + static std::unique_ptr<ReportingUploader> Create( + const URLRequestContext* context); +}; + +} // namespace net + +#endif // NET_REPORTING_REPORTING_UPLOADER_H_ diff --git a/chromium/net/reporting/reporting_uploader_unittest.cc b/chromium/net/reporting/reporting_uploader_unittest.cc new file mode 100644 index 00000000000..8dc7e6ec73c --- /dev/null +++ b/chromium/net/reporting/reporting_uploader_unittest.cc @@ -0,0 +1,325 @@ +// Copyright 2017 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_uploader.h" + +#include <memory> +#include <string> + +#include "base/bind.h" +#include "base/callback.h" +#include "base/run_loop.h" +#include "net/cookies/cookie_store.h" +#include "net/cookies/cookie_store_test_callbacks.h" +#include "net/http/http_status_code.h" +#include "net/test/embedded_test_server/embedded_test_server.h" +#include "net/test/embedded_test_server/http_request.h" +#include "net/test/embedded_test_server/http_response.h" +#include "net/url_request/url_request_test_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace net { +namespace { + +class ReportingUploaderTest : public ::testing::Test { + protected: + ReportingUploaderTest() + : server_(test_server::EmbeddedTestServer::TYPE_HTTPS), + uploader_(ReportingUploader::Create(&context_)) {} + + TestURLRequestContext context_; + test_server::EmbeddedTestServer server_; + std::unique_ptr<ReportingUploader> uploader_; +}; + +const char kUploadBody[] = "{}"; + +void CheckUpload(const test_server::HttpRequest& request) { + EXPECT_EQ("POST", request.method_string); + auto it = request.headers.find("Content-Type"); + EXPECT_TRUE(it != request.headers.end()); + EXPECT_EQ(ReportingUploader::kUploadContentType, it->second); + EXPECT_TRUE(request.has_content); + EXPECT_EQ(kUploadBody, request.content); +} + +std::unique_ptr<test_server::HttpResponse> ReturnResponse( + HttpStatusCode code, + const test_server::HttpRequest& request) { + auto response = base::MakeUnique<test_server::BasicHttpResponse>(); + response->set_code(code); + response->set_content(""); + response->set_content_type("text/plain"); + return std::move(response); +} + +std::unique_ptr<test_server::HttpResponse> ReturnInvalidResponse( + const test_server::HttpRequest& request) { + return base::MakeUnique<test_server::RawHttpResponse>( + "", "Not a valid HTTP response."); +} + +class TestUploadCallback { + public: + TestUploadCallback() : called_(false), waiting_(false) {} + + ReportingUploader::Callback callback() { + return base::Bind(&TestUploadCallback::OnUploadComplete, + base::Unretained(this)); + } + + void WaitForCall() { + if (called_) + return; + + base::RunLoop run_loop; + + waiting_ = true; + closure_ = run_loop.QuitClosure(); + run_loop.Run(); + } + + ReportingUploader::Outcome outcome() const { return outcome_; } + + private: + void OnUploadComplete(ReportingUploader::Outcome outcome) { + EXPECT_FALSE(called_); + + called_ = true; + outcome_ = outcome; + + if (waiting_) { + waiting_ = false; + closure_.Run(); + } + } + + bool called_; + ReportingUploader::Outcome outcome_; + + bool waiting_; + base::Closure closure_; +}; + +TEST_F(ReportingUploaderTest, Upload) { + server_.RegisterRequestMonitor(base::Bind(&CheckUpload)); + server_.RegisterRequestHandler(base::Bind(&ReturnResponse, HTTP_OK)); + ASSERT_TRUE(server_.Start()); + + TestUploadCallback callback; + uploader_->StartUpload(server_.GetURL("/"), kUploadBody, callback.callback()); + callback.WaitForCall(); +} + +TEST_F(ReportingUploaderTest, Success) { + server_.RegisterRequestHandler(base::Bind(&ReturnResponse, HTTP_OK)); + ASSERT_TRUE(server_.Start()); + + TestUploadCallback callback; + uploader_->StartUpload(server_.GetURL("/"), kUploadBody, callback.callback()); + callback.WaitForCall(); + + EXPECT_EQ(ReportingUploader::Outcome::SUCCESS, callback.outcome()); +} + +TEST_F(ReportingUploaderTest, NetworkError1) { + ASSERT_TRUE(server_.Start()); + GURL url = server_.GetURL("/"); + ASSERT_TRUE(server_.ShutdownAndWaitUntilComplete()); + + TestUploadCallback callback; + uploader_->StartUpload(url, kUploadBody, callback.callback()); + callback.WaitForCall(); + + EXPECT_EQ(ReportingUploader::Outcome::FAILURE, callback.outcome()); +} + +TEST_F(ReportingUploaderTest, NetworkError2) { + server_.RegisterRequestHandler(base::Bind(&ReturnInvalidResponse)); + ASSERT_TRUE(server_.Start()); + + TestUploadCallback callback; + uploader_->StartUpload(server_.GetURL("/"), kUploadBody, callback.callback()); + callback.WaitForCall(); + + EXPECT_EQ(ReportingUploader::Outcome::FAILURE, callback.outcome()); +} + +TEST_F(ReportingUploaderTest, ServerError) { + server_.RegisterRequestHandler( + base::Bind(&ReturnResponse, HTTP_INTERNAL_SERVER_ERROR)); + ASSERT_TRUE(server_.Start()); + + TestUploadCallback callback; + uploader_->StartUpload(server_.GetURL("/"), kUploadBody, callback.callback()); + callback.WaitForCall(); + + EXPECT_EQ(ReportingUploader::Outcome::FAILURE, callback.outcome()); +} + +TEST_F(ReportingUploaderTest, RemoveEndpoint) { + server_.RegisterRequestHandler(base::Bind(&ReturnResponse, HTTP_GONE)); + ASSERT_TRUE(server_.Start()); + + TestUploadCallback callback; + uploader_->StartUpload(server_.GetURL("/"), kUploadBody, callback.callback()); + callback.WaitForCall(); + + EXPECT_EQ(ReportingUploader::Outcome::REMOVE_ENDPOINT, callback.outcome()); +} + +const char kRedirectPath[] = "/redirect"; + +std::unique_ptr<test_server::HttpResponse> ReturnRedirect( + const std::string& location, + const test_server::HttpRequest& request) { + if (request.relative_url != "/") + return std::unique_ptr<test_server::HttpResponse>(); + + auto response = base::MakeUnique<test_server::BasicHttpResponse>(); + response->set_code(HTTP_FOUND); + response->AddCustomHeader("Location", location); + response->set_content( + "Thank you, Mario! But our Princess is in another castle."); + response->set_content_type("text/plain"); + return std::move(response); +} + +std::unique_ptr<test_server::HttpResponse> CheckRedirect( + bool* redirect_followed_out, + const test_server::HttpRequest& request) { + if (request.relative_url != kRedirectPath) + return std::unique_ptr<test_server::HttpResponse>(); + + *redirect_followed_out = true; + return ReturnResponse(HTTP_OK, request); +} + +TEST_F(ReportingUploaderTest, FollowHttpsRedirect) { + bool followed = false; + server_.RegisterRequestHandler(base::Bind(&ReturnRedirect, kRedirectPath)); + server_.RegisterRequestHandler(base::Bind(&CheckRedirect, &followed)); + ASSERT_TRUE(server_.Start()); + + TestUploadCallback callback; + uploader_->StartUpload(server_.GetURL("/"), kUploadBody, callback.callback()); + callback.WaitForCall(); + + EXPECT_TRUE(followed); + EXPECT_EQ(ReportingUploader::Outcome::SUCCESS, callback.outcome()); +} + +TEST_F(ReportingUploaderTest, DontFollowHttpRedirect) { + bool followed = false; + + test_server::EmbeddedTestServer http_server_; + http_server_.RegisterRequestHandler(base::Bind(&CheckRedirect, &followed)); + ASSERT_TRUE(http_server_.Start()); + + const GURL target = http_server_.GetURL(kRedirectPath); + server_.RegisterRequestHandler(base::Bind(&ReturnRedirect, target.spec())); + ASSERT_TRUE(server_.Start()); + + TestUploadCallback callback; + uploader_->StartUpload(server_.GetURL("/"), kUploadBody, callback.callback()); + callback.WaitForCall(); + + EXPECT_FALSE(followed); + EXPECT_EQ(ReportingUploader::Outcome::FAILURE, callback.outcome()); +} + +void CheckNoCookie(const test_server::HttpRequest& request) { + auto it = request.headers.find("Cookie"); + EXPECT_TRUE(it == request.headers.end()); +} + +TEST_F(ReportingUploaderTest, DontSendCookies) { + server_.RegisterRequestMonitor(base::Bind(&CheckNoCookie)); + server_.RegisterRequestHandler(base::Bind(&ReturnResponse, HTTP_OK)); + ASSERT_TRUE(server_.Start()); + + ResultSavingCookieCallback<bool> cookie_callback; + context_.cookie_store()->SetCookieWithOptionsAsync( + server_.GetURL("/"), "foo=bar", CookieOptions(), + base::Bind(&ResultSavingCookieCallback<bool>::Run, + base::Unretained(&cookie_callback))); + cookie_callback.WaitUntilDone(); + ASSERT_TRUE(cookie_callback.result()); + + TestUploadCallback upload_callback; + uploader_->StartUpload(server_.GetURL("/"), kUploadBody, + upload_callback.callback()); + upload_callback.WaitForCall(); +} + +std::unique_ptr<test_server::HttpResponse> SendCookie( + const test_server::HttpRequest& request) { + auto response = base::MakeUnique<test_server::BasicHttpResponse>(); + response->set_code(HTTP_OK); + response->AddCustomHeader("Set-Cookie", "foo=bar"); + response->set_content(""); + response->set_content_type("text/plain"); + return std::move(response); +} + +TEST_F(ReportingUploaderTest, DontSaveCookies) { + server_.RegisterRequestHandler(base::Bind(&SendCookie)); + ASSERT_TRUE(server_.Start()); + + TestUploadCallback upload_callback; + uploader_->StartUpload(server_.GetURL("/"), kUploadBody, + upload_callback.callback()); + upload_callback.WaitForCall(); + + GetCookieListCallback cookie_callback; + context_.cookie_store()->GetCookieListWithOptionsAsync( + server_.GetURL("/"), CookieOptions(), + base::Bind(&GetCookieListCallback::Run, + base::Unretained(&cookie_callback))); + cookie_callback.WaitUntilDone(); + + EXPECT_TRUE(cookie_callback.cookies().empty()); +} + +std::unique_ptr<test_server::HttpResponse> ReturnCacheableResponse( + int* request_count_out, + const test_server::HttpRequest& request) { + ++*request_count_out; + auto response = base::MakeUnique<test_server::BasicHttpResponse>(); + response->set_code(HTTP_OK); + response->AddCustomHeader("Cache-Control", "max-age=86400"); + response->set_content(""); + response->set_content_type("text/plain"); + return std::move(response); +} + +// TODO(juliatuttle): This passes even if the uploader doesn't set +// LOAD_DISABLE_CACHE. Maybe that's okay -- Chromium might not cache POST +// responses ever -- but this test should either not exist or be sure that it is +// testing actual functionality, not a default. +TEST_F(ReportingUploaderTest, DontCacheResponse) { + int request_count = 0; + server_.RegisterRequestHandler( + base::Bind(&ReturnCacheableResponse, &request_count)); + ASSERT_TRUE(server_.Start()); + + { + TestUploadCallback callback; + uploader_->StartUpload(server_.GetURL("/"), kUploadBody, + callback.callback()); + callback.WaitForCall(); + } + EXPECT_EQ(1, request_count); + + { + TestUploadCallback callback; + uploader_->StartUpload(server_.GetURL("/"), kUploadBody, + callback.callback()); + callback.WaitForCall(); + } + EXPECT_EQ(2, request_count); +} + +} // namespace +} // namespace net |