| // 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 <algorithm> | |
| #include <map> | |
| #include <set> | |
| #include <string> | |
| #include <unordered_map> | |
| #include <unordered_set> | |
| #include <utility> | |
| #include <vector> | |
| #include "base/stl_util.h" | |
| #include "base/time/tick_clock.h" | |
| #include "base/time/time.h" | |
| #include "net/log/net_log.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); | |
| } | |
| struct ClientMetadata { | |
| base::TimeTicks last_used; | |
| ReportingCache::ClientStatistics stats; | |
| }; | |
| class ReportingCacheImpl : public ReportingCache { | |
| public: | |
| ReportingCacheImpl(ReportingContext* context) : context_(context) { | |
| DCHECK(context_); | |
| } | |
| ~ReportingCacheImpl() override { | |
| base::TimeTicks now = tick_clock()->NowTicks(); | |
| // Mark all undoomed reports as erased at shutdown, and record outcomes of | |
| // all remaining reports (doomed or not). | |
| for (auto it = reports_.begin(); it != reports_.end(); ++it) { | |
| ReportingReport* report = it->second.get(); | |
| if (!base::ContainsKey(doomed_reports_, report)) | |
| report->outcome = ReportingReport::Outcome::ERASED_REPORTING_SHUT_DOWN; | |
| report->RecordOutcome(now); | |
| } | |
| reports_.clear(); | |
| } | |
| void AddReport(const GURL& url, | |
| const std::string& user_agent, | |
| const std::string& group, | |
| const std::string& type, | |
| std::unique_ptr<const base::Value> body, | |
| int depth, | |
| base::TimeTicks queued, | |
| int attempts) override { | |
| auto report = std::make_unique<ReportingReport>( | |
| url, user_agent, group, type, std::move(body), depth, queued, attempts); | |
| auto inserted = | |
| reports_.insert(std::make_pair(report.get(), std::move(report))); | |
| DCHECK(inserted.second); | |
| if (reports_.size() > context_->policy().max_report_count) { | |
| // There should be at most one extra report (the one added above). | |
| DCHECK_EQ(context_->policy().max_report_count + 1, reports_.size()); | |
| const ReportingReport* to_evict = FindReportToEvict(); | |
| DCHECK_NE(nullptr, to_evict); | |
| // The newly-added report isn't pending, so even if all other reports are | |
| // pending, the cache should have a report to evict. | |
| DCHECK(!base::ContainsKey(pending_reports_, to_evict)); | |
| reports_[to_evict]->outcome = ReportingReport::Outcome::ERASED_EVICTED; | |
| RemoveReportInternal(to_evict); | |
| } | |
| context_->NotifyCacheUpdated(); | |
| } | |
| void GetReports( | |
| std::vector<const ReportingReport*>* reports_out) const override { | |
| reports_out->clear(); | |
| for (const auto& it : reports_) { | |
| if (!base::ContainsKey(doomed_reports_, it.first)) | |
| reports_out->push_back(it.second.get()); | |
| } | |
| } | |
| base::Value GetReportsAsValue() const override { | |
| // Sort the queued reports by origin and timestamp. | |
| std::vector<const ReportingReport*> sorted_reports; | |
| sorted_reports.reserve(reports_.size()); | |
| for (const auto& it : reports_) { | |
| sorted_reports.push_back(it.second.get()); | |
| } | |
| std::sort( | |
| sorted_reports.begin(), sorted_reports.end(), | |
| [](const ReportingReport* report1, const ReportingReport* report2) { | |
| if (report1->queued < report2->queued) | |
| return true; | |
| else if (report1->queued > report2->queued) | |
| return false; | |
| else | |
| return report1->url < report2->url; | |
| }); | |
| std::vector<base::Value> report_list; | |
| for (const ReportingReport* report : sorted_reports) { | |
| base::Value report_dict(base::Value::Type::DICTIONARY); | |
| #if defined(STARBOARD) | |
| report_dict.SetKey("url", base::Value(report->url.spec().c_str())); | |
| report_dict.SetKey("group", base::Value(report->group.c_str())); | |
| report_dict.SetKey("type", base::Value(report->type.c_str())); | |
| #else | |
| report_dict.SetKey("url", base::Value(report->url.spec())); | |
| report_dict.SetKey("group", base::Value(report->group)); | |
| report_dict.SetKey("type", base::Value(report->type)); | |
| #endif | |
| report_dict.SetKey("depth", base::Value(report->depth)); | |
| report_dict.SetKey( | |
| "queued", base::Value(NetLog::TickCountToString(report->queued))); | |
| report_dict.SetKey("attempts", base::Value(report->attempts)); | |
| if (report->body) { | |
| report_dict.SetKey("body", report->body->Clone()); | |
| } | |
| if (base::ContainsKey(doomed_reports_, report)) { | |
| report_dict.SetKey("status", base::Value("doomed")); | |
| } else if (base::ContainsKey(pending_reports_, report)) { | |
| report_dict.SetKey("status", base::Value("pending")); | |
| } else { | |
| report_dict.SetKey("status", base::Value("queued")); | |
| } | |
| report_list.push_back(std::move(report_dict)); | |
| } | |
| return base::Value(std::move(report_list)); | |
| } | |
| void GetNonpendingReports( | |
| std::vector<const ReportingReport*>* reports_out) const override { | |
| reports_out->clear(); | |
| for (const auto& it : reports_) { | |
| if (!base::ContainsKey(pending_reports_, it.first) && | |
| !base::ContainsKey(doomed_reports_, it.first)) { | |
| reports_out->push_back(it.second.get()); | |
| } | |
| } | |
| } | |
| void SetReportsPending( | |
| const std::vector<const ReportingReport*>& reports) override { | |
| for (const ReportingReport* report : reports) { | |
| auto inserted = pending_reports_.insert(report); | |
| DCHECK(inserted.second); | |
| } | |
| } | |
| void ClearReportsPending( | |
| const std::vector<const ReportingReport*>& reports) override { | |
| 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); | |
| } | |
| } | |
| for (const ReportingReport* report : reports_to_remove) | |
| RemoveReportInternal(report); | |
| } | |
| void IncrementReportsAttempts( | |
| const std::vector<const ReportingReport*>& reports) override { | |
| for (const ReportingReport* report : reports) { | |
| DCHECK(base::ContainsKey(reports_, report)); | |
| reports_[report]->attempts++; | |
| } | |
| context_->NotifyCacheUpdated(); | |
| } | |
| void IncrementEndpointDeliveries(const url::Origin& origin, | |
| const GURL& endpoint, | |
| int reports_delivered, | |
| bool successful) override { | |
| const ReportingClient* client = | |
| GetClientByOriginAndEndpoint(origin, endpoint); | |
| if (client) { | |
| auto& metadata = client_metadata_[client]; | |
| metadata.stats.attempted_uploads++; | |
| metadata.stats.attempted_reports += reports_delivered; | |
| if (successful) { | |
| metadata.stats.successful_uploads++; | |
| metadata.stats.successful_reports += reports_delivered; | |
| } | |
| } | |
| } | |
| void RemoveReports(const std::vector<const ReportingReport*>& reports, | |
| ReportingReport::Outcome outcome) override { | |
| for (const ReportingReport* report : reports) { | |
| reports_[report]->outcome = outcome; | |
| if (base::ContainsKey(pending_reports_, report)) { | |
| doomed_reports_.insert(report); | |
| } else { | |
| DCHECK(!base::ContainsKey(doomed_reports_, report)); | |
| RemoveReportInternal(report); | |
| } | |
| } | |
| context_->NotifyCacheUpdated(); | |
| } | |
| void RemoveAllReports(ReportingReport::Outcome outcome) override { | |
| std::vector<const ReportingReport*> reports_to_remove; | |
| for (auto it = reports_.begin(); it != reports_.end(); ++it) { | |
| ReportingReport* report = it->second.get(); | |
| report->outcome = outcome; | |
| if (!base::ContainsKey(pending_reports_, report)) | |
| reports_to_remove.push_back(report); | |
| else | |
| doomed_reports_.insert(report); | |
| } | |
| for (const ReportingReport* report : reports_to_remove) | |
| RemoveReportInternal(report); | |
| context_->NotifyCacheUpdated(); | |
| } | |
| void SetClient(const url::Origin& origin, | |
| const GURL& endpoint, | |
| ReportingClient::Subdomains subdomains, | |
| const std::string& group, | |
| base::TimeTicks expires, | |
| int priority, | |
| int weight) override { | |
| DCHECK(endpoint.SchemeIsCryptographic()); | |
| base::TimeTicks last_used = tick_clock()->NowTicks(); | |
| const ReportingClient* old_client = | |
| GetClientByOriginAndEndpoint(origin, endpoint); | |
| if (old_client) { | |
| last_used = client_metadata_[old_client].last_used; | |
| RemoveClient(old_client); | |
| } | |
| AddClient( | |
| std::make_unique<ReportingClient>(origin, endpoint, subdomains, group, | |
| expires, priority, weight), | |
| last_used); | |
| if (client_metadata_.size() > context_->policy().max_client_count) { | |
| // There should only ever be one extra client, added above. | |
| DCHECK_EQ(context_->policy().max_client_count + 1, | |
| client_metadata_.size()); | |
| // And that shouldn't happen if it was replaced, not added. | |
| DCHECK(!old_client); | |
| const ReportingClient* to_evict = | |
| FindClientToEvict(tick_clock()->NowTicks()); | |
| DCHECK(to_evict); | |
| RemoveClient(to_evict); | |
| } | |
| context_->NotifyCacheUpdated(); | |
| } | |
| void MarkClientUsed(const ReportingClient* client) override { | |
| DCHECK(client); | |
| client_metadata_[client].last_used = tick_clock()->NowTicks(); | |
| } | |
| void GetClients( | |
| std::vector<const ReportingClient*>* clients_out) const override { | |
| 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()); | |
| } | |
| base::Value GetClientsAsValue() const override { | |
| std::map<const url::Origin, | |
| std::map<const std::string, std::vector<const ReportingClient*>>> | |
| clients_by_origin_and_group; | |
| for (const auto& it : clients_) { | |
| const url::Origin& origin = it.first; | |
| for (const auto& endpoint_and_client : it.second) { | |
| const ReportingClient* client = endpoint_and_client.second.get(); | |
| clients_by_origin_and_group[origin][client->group].push_back(client); | |
| } | |
| } | |
| std::vector<base::Value> origin_list; | |
| for (const auto& it : clients_by_origin_and_group) { | |
| const url::Origin& origin = it.first; | |
| base::Value origin_dict(base::Value::Type::DICTIONARY); | |
| origin_dict.SetKey("origin", base::Value(origin.Serialize())); | |
| std::vector<base::Value> group_list; | |
| for (const auto& group_and_clients : it.second) { | |
| const std::string& group = group_and_clients.first; | |
| const std::vector<const ReportingClient*>& clients = | |
| group_and_clients.second; | |
| base::Value group_dict(base::Value::Type::DICTIONARY); | |
| #if defined(STARBOARD) | |
| group_dict.SetKey("name", base::Value(group.c_str())); | |
| #else | |
| group_dict.SetKey("name", base::Value(group)); | |
| #endif | |
| std::vector<base::Value> endpoint_list; | |
| for (const ReportingClient* client : clients) { | |
| base::Value endpoint_dict(base::Value::Type::DICTIONARY); | |
| // Reporting defines the group as a whole to have an expiration time | |
| // and subdomains flag, not the individual endpoints within the group. | |
| group_dict.SetKey( | |
| "expires", | |
| #if defined(STARBOARD) | |
| base::Value(NetLog::TickCountToString(client->expires).c_str())); | |
| #else | |
| base::Value(NetLog::TickCountToString(client->expires))); | |
| #endif | |
| group_dict.SetKey("includeSubdomains", | |
| base::Value(client->subdomains == | |
| ReportingClient::Subdomains::INCLUDE)); | |
| endpoint_dict.SetKey("url", base::Value(client->endpoint.spec())); | |
| endpoint_dict.SetKey("priority", base::Value(client->priority)); | |
| endpoint_dict.SetKey("weight", base::Value(client->weight)); | |
| auto metadata_it = client_metadata_.find(client); | |
| if (metadata_it != client_metadata_.end()) { | |
| const ClientStatistics& stats = metadata_it->second.stats; | |
| base::Value successful_dict(base::Value::Type::DICTIONARY); | |
| successful_dict.SetKey("uploads", | |
| base::Value(stats.successful_uploads)); | |
| successful_dict.SetKey("reports", | |
| base::Value(stats.successful_reports)); | |
| endpoint_dict.SetKey("successful", std::move(successful_dict)); | |
| base::Value failed_dict(base::Value::Type::DICTIONARY); | |
| failed_dict.SetKey("uploads", | |
| base::Value(stats.attempted_uploads - | |
| stats.successful_uploads)); | |
| failed_dict.SetKey("reports", | |
| base::Value(stats.attempted_reports - | |
| stats.successful_reports)); | |
| endpoint_dict.SetKey("failed", std::move(failed_dict)); | |
| } | |
| endpoint_list.push_back(std::move(endpoint_dict)); | |
| } | |
| group_dict.SetKey("endpoints", base::Value(std::move(endpoint_list))); | |
| group_list.push_back(std::move(group_dict)); | |
| } | |
| origin_dict.SetKey("groups", base::Value(std::move(group_list))); | |
| origin_list.push_back(std::move(origin_dict)); | |
| } | |
| return base::Value(std::move(origin_list)); | |
| } | |
| void GetClientsForOriginAndGroup( | |
| const url::Origin& origin, | |
| const std::string& group, | |
| std::vector<const ReportingClient*>* clients_out) const override { | |
| 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 include_subdomains 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); | |
| } | |
| } | |
| // TODO(juliatuttle): Unittests. | |
| void GetEndpointsForOrigin(const url::Origin& origin, | |
| std::vector<GURL>* endpoints_out) const override { | |
| endpoints_out->clear(); | |
| const auto it = clients_.find(origin); | |
| if (it == clients_.end()) | |
| return; | |
| for (const auto& endpoint_and_client : it->second) | |
| endpoints_out->push_back(endpoint_and_client.first); | |
| } | |
| void RemoveClients( | |
| const std::vector<const ReportingClient*>& clients_to_remove) override { | |
| for (const ReportingClient* client : clients_to_remove) | |
| RemoveClient(client); | |
| context_->NotifyCacheUpdated(); | |
| } | |
| void RemoveClientForOriginAndEndpoint(const url::Origin& origin, | |
| const GURL& endpoint) override { | |
| const ReportingClient* client = | |
| GetClientByOriginAndEndpoint(origin, endpoint); | |
| RemoveClient(client); | |
| context_->NotifyCacheUpdated(); | |
| } | |
| void RemoveClientsForEndpoint(const GURL& endpoint) override { | |
| std::vector<const ReportingClient*> clients_to_remove; | |
| for (auto& origin_and_endpoints : clients_) | |
| if (base::ContainsKey(origin_and_endpoints.second, endpoint)) | |
| clients_to_remove.push_back( | |
| origin_and_endpoints.second[endpoint].get()); | |
| for (const ReportingClient* client : clients_to_remove) | |
| RemoveClient(client); | |
| if (!clients_to_remove.empty()) | |
| context_->NotifyCacheUpdated(); | |
| } | |
| void RemoveAllClients() override { | |
| clients_.clear(); | |
| wildcard_clients_.clear(); | |
| client_metadata_.clear(); | |
| context_->NotifyCacheUpdated(); | |
| } | |
| ClientStatistics GetStatisticsForOriginAndEndpoint( | |
| const url::Origin& origin, | |
| const GURL& endpoint) const override { | |
| const ReportingClient* client = | |
| GetClientByOriginAndEndpoint(origin, endpoint); | |
| auto it = client_metadata_.find(client); | |
| if (it == client_metadata_.end()) { | |
| return ClientStatistics(); | |
| } | |
| return it->second.stats; | |
| } | |
| size_t GetFullReportCountForTesting() const override { | |
| return reports_.size(); | |
| } | |
| bool IsReportPendingForTesting(const ReportingReport* report) const override { | |
| return base::ContainsKey(pending_reports_, report); | |
| } | |
| bool IsReportDoomedForTesting(const ReportingReport* report) const override { | |
| return base::ContainsKey(doomed_reports_, report); | |
| } | |
| private: | |
| void RemoveReportInternal(const ReportingReport* report) { | |
| reports_[report]->RecordOutcome(tick_clock()->NowTicks()); | |
| size_t erased = reports_.erase(report); | |
| DCHECK_EQ(1u, erased); | |
| } | |
| const ReportingReport* FindReportToEvict() const { | |
| const ReportingReport* earliest_queued = nullptr; | |
| for (const auto& it : reports_) { | |
| const ReportingReport* report = it.first; | |
| if (base::ContainsKey(pending_reports_, report)) | |
| continue; | |
| if (!earliest_queued || report->queued < earliest_queued->queued) { | |
| earliest_queued = report; | |
| } | |
| } | |
| return earliest_queued; | |
| } | |
| void AddClient(std::unique_ptr<ReportingClient> client, | |
| base::TimeTicks last_used) { | |
| DCHECK(client); | |
| url::Origin origin = client->origin; | |
| GURL endpoint = client->endpoint; | |
| auto inserted_metadata = client_metadata_.insert( | |
| std::make_pair(client.get(), ClientMetadata{last_used})); | |
| DCHECK(inserted_metadata.second); | |
| if (client->subdomains == ReportingClient::Subdomains::INCLUDE) { | |
| const std::string& domain = origin.host(); | |
| auto inserted_wildcard_client = | |
| wildcard_clients_[domain].insert(client.get()); | |
| DCHECK(inserted_wildcard_client.second); | |
| } | |
| auto inserted_client = | |
| clients_[origin].insert(std::make_pair(endpoint, std::move(client))); | |
| DCHECK(inserted_client.second); | |
| } | |
| void RemoveClient(const ReportingClient* client) { | |
| DCHECK(client); | |
| url::Origin origin = client->origin; | |
| GURL endpoint = client->endpoint; | |
| if (client->subdomains == ReportingClient::Subdomains::INCLUDE) { | |
| const std::string& domain = origin.host(); | |
| size_t erased_wildcard_client = wildcard_clients_[domain].erase(client); | |
| DCHECK_EQ(1u, erased_wildcard_client); | |
| if (wildcard_clients_[domain].empty()) { | |
| size_t erased_wildcard_domain = wildcard_clients_.erase(domain); | |
| DCHECK_EQ(1u, erased_wildcard_domain); | |
| } | |
| } | |
| size_t erased_metadata = client_metadata_.erase(client); | |
| DCHECK_EQ(1u, erased_metadata); | |
| size_t erased_endpoint = clients_[origin].erase(endpoint); | |
| DCHECK_EQ(1u, erased_endpoint); | |
| if (clients_[origin].empty()) { | |
| size_t erased_origin = clients_.erase(origin); | |
| DCHECK_EQ(1u, erased_origin); | |
| } | |
| } | |
| const ReportingClient* GetClientByOriginAndEndpoint( | |
| const url::Origin& origin, | |
| const GURL& endpoint) const { | |
| const auto& origin_it = clients_.find(origin); | |
| if (origin_it == clients_.end()) | |
| return nullptr; | |
| const auto& endpoint_it = origin_it->second.find(endpoint); | |
| if (endpoint_it == origin_it->second.end()) | |
| return nullptr; | |
| return endpoint_it->second.get(); | |
| } | |
| void 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); | |
| } | |
| } | |
| const ReportingClient* FindClientToEvict(base::TimeTicks now) const { | |
| DCHECK(!client_metadata_.empty()); | |
| const ReportingClient* earliest_used = nullptr; | |
| base::TimeTicks earliest_used_last_used; | |
| const ReportingClient* earliest_expired = nullptr; | |
| for (const auto& it : client_metadata_) { | |
| const ReportingClient* client = it.first; | |
| base::TimeTicks client_last_used = it.second.last_used; | |
| if (earliest_used == nullptr || | |
| client_last_used < earliest_used_last_used) { | |
| earliest_used = client; | |
| earliest_used_last_used = client_last_used; | |
| } | |
| if (earliest_expired == nullptr || | |
| client->expires < earliest_expired->expires) { | |
| earliest_expired = client; | |
| } | |
| } | |
| // If there are expired clients, return the earliest-expired. | |
| if (earliest_expired->expires < now) | |
| return earliest_expired; | |
| else | |
| return earliest_used; | |
| } | |
| const base::TickClock* tick_clock() { return context_->tick_clock(); } | |
| ReportingContext* context_; | |
| // 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_; | |
| // 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 include_subdomains set, keyed | |
| // by domain name. | |
| std::unordered_map<std::string, std::unordered_set<const ReportingClient*>> | |
| wildcard_clients_; | |
| // The time that each client has last been used. | |
| std::unordered_map<const ReportingClient*, ClientMetadata> client_metadata_; | |
| }; | |
| } // namespace | |
| // static | |
| std::unique_ptr<ReportingCache> ReportingCache::Create( | |
| ReportingContext* context) { | |
| return std::make_unique<ReportingCacheImpl>(context); | |
| } | |
| ReportingCache::~ReportingCache() = default; | |
| } // namespace net |