blob: e546c1a5d521020051a3d03fd266fdb37b378396 [file] [log] [blame]
// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "net/network_error_logging/network_error_logging_service.h"
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/json/json_reader.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/histogram_macros.h"
#include "base/rand_util.h"
#include "base/time/clock.h"
#include "base/time/default_clock.h"
#include "base/time/time.h"
#include "base/values.h"
#include "net/base/features.h"
#include "net/base/ip_address.h"
#include "net/base/net_errors.h"
#include "net/base/network_anonymization_key.h"
#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
#include "net/base/url_util.h"
#include "net/log/net_log.h"
#include "net/reporting/reporting_service.h"
#include "url/gurl.h"
#include "url/origin.h"
namespace net {
namespace {
const int kMaxJsonSize = 16 * 1024;
const int kMaxJsonDepth = 4;
const char kReportToKey[] = "report_to";
const char kMaxAgeKey[] = "max_age";
const char kIncludeSubdomainsKey[] = "include_subdomains";
const char kSuccessFractionKey[] = "success_fraction";
const char kFailureFractionKey[] = "failure_fraction";
const char kApplicationPhase[] = "application";
const char kConnectionPhase[] = "connection";
const char kDnsPhase[] = "dns";
const char kDnsAddressChangedType[] = "dns.address_changed";
const char kHttpErrorType[] = "http.error";
const struct {
Error error;
const char* phase = nullptr;
const char* type = nullptr;
} kErrorTypes[] = {
{OK, kApplicationPhase, "ok"},
// dns.unreachable?
{ERR_NAME_NOT_RESOLVED, kDnsPhase, "dns.name_not_resolved"},
{ERR_NAME_RESOLUTION_FAILED, kDnsPhase, "dns.failed"},
{ERR_DNS_TIMED_OUT, kDnsPhase, "dns.timed_out"},
{ERR_DNS_MALFORMED_RESPONSE, kDnsPhase, "dns.protocol"},
{ERR_DNS_SERVER_FAILED, kDnsPhase, "dns.server"},
{ERR_TIMED_OUT, kConnectionPhase, "tcp.timed_out"},
{ERR_CONNECTION_TIMED_OUT, kConnectionPhase, "tcp.timed_out"},
{ERR_CONNECTION_CLOSED, kConnectionPhase, "tcp.closed"},
{ERR_CONNECTION_RESET, kConnectionPhase, "tcp.reset"},
{ERR_CONNECTION_REFUSED, kConnectionPhase, "tcp.refused"},
{ERR_CONNECTION_ABORTED, kConnectionPhase, "tcp.aborted"},
{ERR_ADDRESS_INVALID, kConnectionPhase, "tcp.address_invalid"},
{ERR_ADDRESS_UNREACHABLE, kConnectionPhase, "tcp.address_unreachable"},
{ERR_CONNECTION_FAILED, kConnectionPhase, "tcp.failed"},
{ERR_SSL_VERSION_OR_CIPHER_MISMATCH, kConnectionPhase,
"tls.version_or_cipher_mismatch"},
{ERR_BAD_SSL_CLIENT_AUTH_CERT, kConnectionPhase,
"tls.bad_client_auth_cert"},
{ERR_CERT_INVALID, kConnectionPhase, "tls.cert.invalid"},
{ERR_CERT_COMMON_NAME_INVALID, kConnectionPhase, "tls.cert.name_invalid"},
{ERR_CERT_DATE_INVALID, kConnectionPhase, "tls.cert.date_invalid"},
{ERR_CERT_AUTHORITY_INVALID, kConnectionPhase,
"tls.cert.authority_invalid"},
{ERR_CERT_REVOKED, kConnectionPhase, "tls.cert.revoked"},
{ERR_SSL_PINNED_KEY_NOT_IN_CERT_CHAIN, kConnectionPhase,
"tls.cert.pinned_key_not_in_cert_chain"},
{ERR_SSL_PROTOCOL_ERROR, kConnectionPhase, "tls.protocol.error"},
{ERR_INSECURE_RESPONSE, kConnectionPhase, "tls.failed"},
{ERR_SSL_UNRECOGNIZED_NAME_ALERT, kConnectionPhase,
"tls.unrecognized_name_alert"},
// tls.failed?
{ERR_HTTP2_PING_FAILED, kApplicationPhase, "h2.ping_failed"},
{ERR_HTTP2_PROTOCOL_ERROR, kConnectionPhase, "h2.protocol.error"},
{ERR_QUIC_PROTOCOL_ERROR, kConnectionPhase, "h3.protocol.error"},
// http.protocol.error?
{ERR_TOO_MANY_REDIRECTS, kApplicationPhase, "http.response.redirect_loop"},
{ERR_INVALID_RESPONSE, kApplicationPhase, "http.response.invalid"},
{ERR_INVALID_HTTP_RESPONSE, kApplicationPhase, "http.response.invalid"},
{ERR_EMPTY_RESPONSE, kApplicationPhase, "http.response.invalid.empty"},
{ERR_CONTENT_LENGTH_MISMATCH, kApplicationPhase,
"http.response.invalid.content_length_mismatch"},
{ERR_INCOMPLETE_CHUNKED_ENCODING, kApplicationPhase,
"http.response.invalid.incomplete_chunked_encoding"},
{ERR_INVALID_CHUNKED_ENCODING, kApplicationPhase,
"http.response.invalid.invalid_chunked_encoding"},
{ERR_REQUEST_RANGE_NOT_SATISFIABLE, kApplicationPhase,
"http.request.range_not_satisfiable"},
{ERR_RESPONSE_HEADERS_TRUNCATED, kApplicationPhase,
"http.response.headers.truncated"},
{ERR_RESPONSE_HEADERS_MULTIPLE_CONTENT_DISPOSITION, kApplicationPhase,
"http.response.headers.multiple_content_disposition"},
{ERR_RESPONSE_HEADERS_MULTIPLE_CONTENT_LENGTH, kApplicationPhase,
"http.response.headers.multiple_content_length"},
// http.failed?
{ERR_ABORTED, kApplicationPhase, "abandoned"},
// unknown?
// TODO(juliatuttle): Surely there are more errors we want here.
};
void GetPhaseAndTypeFromNetError(Error error,
std::string* phase_out,
std::string* type_out) {
for (const auto& error_type : kErrorTypes) {
DCHECK(error_type.phase != nullptr);
DCHECK(error_type.type != nullptr);
if (error_type.error == error) {
*phase_out = error_type.phase;
*type_out = error_type.type;
return;
}
}
*phase_out = IsCertificateError(error) ? kConnectionPhase : kApplicationPhase;
*type_out = "unknown";
}
bool IsHttpError(const NetworkErrorLoggingService::RequestDetails& request) {
return request.status_code >= 400 && request.status_code < 600;
}
void RecordSignedExchangeRequestOutcome(
NetworkErrorLoggingService::RequestOutcome outcome) {
UMA_HISTOGRAM_ENUMERATION(
NetworkErrorLoggingService::kSignedExchangeRequestOutcomeHistogram,
outcome);
}
class NetworkErrorLoggingServiceImpl : public NetworkErrorLoggingService {
public:
explicit NetworkErrorLoggingServiceImpl(PersistentNelStore* store)
: store_(store) {
if (!PoliciesArePersisted())
initialized_ = true;
}
~NetworkErrorLoggingServiceImpl() override {
if (PoliciesArePersisted() && initialized_)
store_->Flush();
}
// NetworkErrorLoggingService implementation:
void OnHeader(const NetworkAnonymizationKey& network_anonymization_key,
const url::Origin& origin,
const IPAddress& received_ip_address,
const std::string& value) override {
// NEL is only available to secure origins, so don't permit insecure origins
// to set policies.
if (!origin.GetURL().SchemeIsCryptographic())
return;
base::Time header_received_time = clock_->Now();
// base::Unretained is safe because the callback gets stored in
// task_backlog_, so the callback will not outlive |*this|.
DoOrBacklogTask(base::BindOnce(
&NetworkErrorLoggingServiceImpl::DoOnHeader, base::Unretained(this),
respect_network_anonymization_key_ ? network_anonymization_key
: NetworkAnonymizationKey(),
origin, received_ip_address, value, header_received_time));
}
void OnRequest(RequestDetails details) override {
// This method is only called on secure requests.
DCHECK(details.uri.SchemeIsCryptographic());
if (!reporting_service_)
return;
if (!respect_network_anonymization_key_)
details.network_anonymization_key = NetworkAnonymizationKey();
base::Time request_received_time = clock_->Now();
// base::Unretained is safe because the callback gets stored in
// task_backlog_, so the callback will not outlive |*this|.
DoOrBacklogTask(base::BindOnce(&NetworkErrorLoggingServiceImpl::DoOnRequest,
base::Unretained(this), std::move(details),
request_received_time));
}
void QueueSignedExchangeReport(SignedExchangeReportDetails details) override {
if (!reporting_service_) {
RecordSignedExchangeRequestOutcome(
RequestOutcome::kDiscardedNoReportingService);
return;
}
if (!details.outer_url.SchemeIsCryptographic()) {
RecordSignedExchangeRequestOutcome(
RequestOutcome::kDiscardedInsecureOrigin);
return;
}
if (!respect_network_anonymization_key_)
details.network_anonymization_key = NetworkAnonymizationKey();
base::Time request_received_time = clock_->Now();
// base::Unretained is safe because the callback gets stored in
// task_backlog_, so the callback will not outlive |*this|.
DoOrBacklogTask(base::BindOnce(
&NetworkErrorLoggingServiceImpl::DoQueueSignedExchangeReport,
base::Unretained(this), std::move(details), request_received_time));
}
void RemoveBrowsingData(
const base::RepeatingCallback<bool(const url::Origin&)>& origin_filter)
override {
// base::Unretained is safe because the callback gets stored in
// task_backlog_, so the callback will not outlive |*this|.
DoOrBacklogTask(
base::BindOnce(&NetworkErrorLoggingServiceImpl::DoRemoveBrowsingData,
base::Unretained(this), origin_filter));
}
void RemoveAllBrowsingData() override {
// base::Unretained is safe because the callback gets stored in
// task_backlog_, so the callback will not outlive |*this|.
DoOrBacklogTask(
base::BindOnce(&NetworkErrorLoggingServiceImpl::DoRemoveAllBrowsingData,
base::Unretained(this)));
}
base::Value StatusAsValue() const override {
base::Value::Dict dict;
base::Value::List policy_list;
// We wanted sorted (or at least reproducible) output; luckily, policies_ is
// a std::map, and therefore already sorted.
for (const auto& key_and_policy : policies_) {
const NelPolicyKey& key = key_and_policy.first;
const NelPolicy& policy = key_and_policy.second;
base::Value::Dict policy_dict;
policy_dict.Set("NetworkAnonymizationKey",
key.network_anonymization_key.ToDebugString());
policy_dict.Set("origin", key.origin.Serialize());
policy_dict.Set("includeSubdomains", policy.include_subdomains);
policy_dict.Set("reportTo", policy.report_to);
policy_dict.Set("expires", NetLog::TimeToString(policy.expires));
policy_dict.Set("successFraction", policy.success_fraction);
policy_dict.Set("failureFraction", policy.failure_fraction);
policy_list.Append(std::move(policy_dict));
}
dict.Set("originPolicies", std::move(policy_list));
return base::Value(std::move(dict));
}
std::set<NelPolicyKey> GetPolicyKeysForTesting() override {
std::set<NelPolicyKey> keys;
for (const auto& entry : policies_) {
keys.insert(entry.first);
}
return keys;
}
NetworkErrorLoggingService::PersistentNelStore*
GetPersistentNelStoreForTesting() override {
return store_;
}
ReportingService* GetReportingServiceForTesting() override {
return reporting_service_;
}
private:
// Map from (NAK, origin) to owned policy.
using PolicyMap = std::map<NelPolicyKey, NelPolicy>;
// Wildcard policies are policies for which the include_subdomains flag is
// true.
//
// Wildcard policies are accessed by domain name, not full origin. The key
// consists of the NetworkAnonymizationKey of the policy, plus a string which
// is the host part of the policy's origin.
//
// Looking up a wildcard policy for a domain yields the wildcard policy with
// the longest host part (most specific subdomain) that is a substring of the
// domain.
//
// When multiple policies with the same (NAK, origin.host()) are present, they
// are all stored, the policy returned is not well defined.
//
// Policies in the map are unowned; they are pointers to the original in
// the PolicyMap.
using WildcardPolicyMap =
std::map<WildcardNelPolicyKey,
std::set<raw_ptr<const NelPolicy, SetExperimental>>>;
PolicyMap policies_;
WildcardPolicyMap wildcard_policies_;
// The persistent store in which NEL policies will be stored to disk, if not
// null. If |store_| is null, then NEL policies will be in-memory only.
// The store is owned by the URLRequestContext because Reporting also needs
// access to it.
raw_ptr<PersistentNelStore> store_;
// Set to true when we have told the store to load NEL policies. This is to
// make sure we don't try to load policies multiple times.
bool started_loading_policies_ = false;
// Set to true when the NEL service has been initialized. Before
// initialization is complete, commands to the NEL service (i.e. public
// method calls) are stashed away in |task_backlog_|, to be executed once
// initialization is complete. Initialization is complete automatically if
// there is no PersistentNelStore. If there is a store, then initialization is
// complete when the NEL policies have finished being loaded from the store
// (either successfully or unsuccessfully).
bool initialized_ = false;
// Backlog of tasks waiting on initialization.
std::vector<base::OnceClosure> task_backlog_;
// Set based on network state partitioning status on construction.
bool respect_network_anonymization_key_ =
NetworkAnonymizationKey::IsPartitioningEnabled();
base::WeakPtrFactory<NetworkErrorLoggingServiceImpl> weak_factory_{this};
bool PoliciesArePersisted() const { return store_ != nullptr; }
void DoOrBacklogTask(base::OnceClosure task) {
if (shut_down_)
return;
FetchAllPoliciesFromStoreIfNecessary();
if (!initialized_) {
task_backlog_.push_back(std::move(task));
return;
}
std::move(task).Run();
}
void ExecuteBacklog() {
DCHECK(initialized_);
if (shut_down_)
return;
for (base::OnceClosure& task : task_backlog_) {
std::move(task).Run();
}
task_backlog_.clear();
}
void DoOnHeader(const NetworkAnonymizationKey& network_anonymization_key,
const url::Origin& origin,
const IPAddress& received_ip_address,
const std::string& value,
base::Time header_received_time) {
DCHECK(initialized_);
NelPolicy policy;
policy.key = NelPolicyKey(network_anonymization_key, origin);
policy.received_ip_address = received_ip_address;
policy.last_used = header_received_time;
if (!ParseHeader(value, clock_->Now(), &policy))
return;
// Disallow eTLDs from setting include_subdomains policies.
if (policy.include_subdomains &&
registry_controlled_domains::GetRegistryLength(
policy.key.origin.GetURL(),
registry_controlled_domains::INCLUDE_UNKNOWN_REGISTRIES,
registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES) == 0) {
return;
}
// If a policy for this NelPolicyKey already existed, remove the old policy.
auto it = policies_.find(policy.key);
if (it != policies_.end())
RemovePolicy(it);
// A policy's |expires| field is set to a null time if the max_age was 0.
// Having a max_age of 0 means that the policy should be removed, so return
// here instead of continuing on to inserting the policy.
if (policy.expires.is_null())
return;
AddPolicy(std::move(policy));
// Evict policies if the policy limit is exceeded.
if (policies_.size() > kMaxPolicies) {
RemoveAllExpiredPolicies();
while (policies_.size() > kMaxPolicies) {
EvictStalestPolicy();
}
}
}
void DoOnRequest(RequestDetails details, base::Time request_received_time) {
DCHECK(reporting_service_);
DCHECK(initialized_);
if (!respect_network_anonymization_key_)
details.network_anonymization_key = NetworkAnonymizationKey();
auto report_origin = url::Origin::Create(details.uri);
const NelPolicy* policy =
FindPolicyForReport(details.network_anonymization_key, report_origin);
if (!policy)
return;
MarkPolicyUsed(policy, request_received_time);
Error type = details.type;
// It is expected for Reporting uploads to terminate with ERR_ABORTED, since
// the ReportingUploader cancels them after receiving the response code and
// headers.
if (details.reporting_upload_depth > 0 && type == ERR_ABORTED) {
// TODO(juliatuttle): Modify ReportingUploader to drain successful uploads
// instead of aborting them, so NEL can properly report on aborted
// requests.
type = OK;
}
std::string phase_string;
std::string type_string;
GetPhaseAndTypeFromNetError(type, &phase_string, &type_string);
if (IsHttpError(details)) {
phase_string = kApplicationPhase;
type_string = kHttpErrorType;
}
// This check would go earlier, but the histogram bucket will be more
// meaningful if it only includes reports that otherwise could have been
// uploaded.
if (details.reporting_upload_depth > kMaxNestedReportDepth)
return;
// include_subdomains policies are only allowed to report on DNS resolution
// errors.
if (phase_string != kDnsPhase &&
IsMismatchingSubdomainReport(*policy, report_origin)) {
return;
}
// If the server that handled the request is different than the server that
// delivered the NEL policy (as determined by their IP address), then we
// have to "downgrade" the NEL report, so that it only includes information
// about DNS resolution.
if (phase_string != kDnsPhase && details.server_ip.IsValid() &&
details.server_ip != policy->received_ip_address) {
phase_string = kDnsPhase;
type_string = kDnsAddressChangedType;
details.elapsed_time = base::TimeDelta();
details.status_code = 0;
}
bool success = (type == OK) && !IsHttpError(details);
const std::optional<double> sampling_fraction =
SampleAndReturnFraction(*policy, success);
if (!sampling_fraction.has_value())
return;
DVLOG(1) << "Created NEL report (" << type_string
<< ", status=" << details.status_code
<< ", depth=" << details.reporting_upload_depth << ") for "
<< details.uri;
// A null reporting source token is used since this report is not associated
// with any particular document.
reporting_service_->QueueReport(
details.uri, std::nullopt, details.network_anonymization_key,
details.user_agent, policy->report_to, kReportType,
CreateReportBody(phase_string, type_string, sampling_fraction.value(),
details),
details.reporting_upload_depth);
}
void DoQueueSignedExchangeReport(SignedExchangeReportDetails details,
base::Time request_received_time) {
DCHECK(reporting_service_);
const auto report_origin = url::Origin::Create(details.outer_url);
const NelPolicy* policy =
FindPolicyForReport(details.network_anonymization_key, report_origin);
if (!policy) {
RecordSignedExchangeRequestOutcome(
RequestOutcome::kDiscardedNoOriginPolicy);
return;
}
MarkPolicyUsed(policy, request_received_time);
if (IsMismatchingSubdomainReport(*policy, report_origin)) {
RecordSignedExchangeRequestOutcome(
RequestOutcome::kDiscardedNonDNSSubdomainReport);
return;
}
// Don't send the report when the IP addresses of the server and the policy
// don’t match. This case is coverd by OnRequest() while processing the HTTP
// response.
// This happens if the server has set the NEL policy previously, but doesn't
// set the NEL policy for the signed exchange response, and the IP address
// has changed due to DNS round robin.
if (details.server_ip_address != policy->received_ip_address) {
RecordSignedExchangeRequestOutcome(
RequestOutcome::kDiscardedIPAddressMismatch);
return;
}
const std::optional<double> sampling_fraction =
SampleAndReturnFraction(*policy, details.success);
if (!sampling_fraction.has_value()) {
RecordSignedExchangeRequestOutcome(
details.success ? RequestOutcome::kDiscardedUnsampledSuccess
: RequestOutcome::kDiscardedUnsampledFailure);
return;
}
// A null reporting source token is used since this report is not associated
// with any particular document.
reporting_service_->QueueReport(
details.outer_url, std::nullopt, details.network_anonymization_key,
details.user_agent, policy->report_to, kReportType,
CreateSignedExchangeReportBody(details, sampling_fraction.value()),
0 /* depth */);
RecordSignedExchangeRequestOutcome(RequestOutcome::kQueued);
}
void DoRemoveBrowsingData(
const base::RepeatingCallback<bool(const url::Origin&)>& origin_filter) {
DCHECK(initialized_);
for (auto it = policies_.begin(); it != policies_.end();) {
const NelPolicyKey& key = it->first;
// Remove policies matching the filter.
if (origin_filter.Run(key.origin)) {
it = RemovePolicy(it);
} else {
++it;
}
}
if (PoliciesArePersisted())
store_->Flush();
}
void DoRemoveAllBrowsingData() {
DCHECK(initialized_);
if (PoliciesArePersisted()) {
// TODO(chlily): Add a DeleteAllNelPolicies command to PersistentNelStore.
for (auto origin_and_policy : policies_) {
store_->DeleteNelPolicy(origin_and_policy.second);
}
store_->Flush();
}
wildcard_policies_.clear();
policies_.clear();
}
// Returns whether the |json_value| was parsed as a valid header that either
// sets a NEL policy (max age > 0) or removes an existing one (max age == 0).
bool ParseHeader(const std::string& json_value,
base::Time now,
NelPolicy* policy_out) const {
DCHECK(policy_out);
// JSON is malformed (too large, syntax error, not a dictionary).
if (json_value.size() > kMaxJsonSize)
return false;
std::optional<base::Value> value =
base::JSONReader::Read(json_value, base::JSON_PARSE_RFC, kMaxJsonDepth);
if (!value)
return false;
base::Value::Dict* dict = value->GetIfDict();
if (!dict)
return false;
// Max-Age property is missing or malformed.
int max_age_sec = dict->FindInt(kMaxAgeKey).value_or(-1);
if (max_age_sec < 0)
return false;
// Report-To property is missing or malformed.
std::string report_to;
if (max_age_sec > 0) {
std::string* maybe_report_to = dict->FindString(kReportToKey);
if (!maybe_report_to)
return false;
report_to = *maybe_report_to;
}
// include_subdomains is optional and defaults to false, so it's okay if
// GetBoolean fails.
bool include_subdomains =
dict->FindBool(kIncludeSubdomainsKey).value_or(false);
// TODO(chlily): According to the spec we should restrict these sampling
// fractions to [0.0, 1.0].
// success_fraction is optional and defaults to 0.0, so it's okay if
// GetDouble fails.
double success_fraction =
dict->FindDouble(kSuccessFractionKey).value_or(0.0);
// failure_fraction is optional and defaults to 1.0, so it's okay if
// GetDouble fails.
double failure_fraction =
dict->FindDouble(kFailureFractionKey).value_or(1.0);
policy_out->report_to = report_to;
policy_out->include_subdomains = include_subdomains;
policy_out->success_fraction = success_fraction;
policy_out->failure_fraction = failure_fraction;
policy_out->expires =
max_age_sec > 0 ? now + base::Seconds(max_age_sec) : base::Time();
return true;
}
const NelPolicy* FindPolicyForReport(
const NetworkAnonymizationKey& network_anonymization_key,
const url::Origin& report_origin) const {
DCHECK(initialized_);
auto it =
policies_.find(NelPolicyKey(network_anonymization_key, report_origin));
if (it != policies_.end() && clock_->Now() < it->second.expires)
return &it->second;
std::string domain = report_origin.host();
const NelPolicy* wildcard_policy = nullptr;
while (!wildcard_policy && !domain.empty()) {
wildcard_policy = FindWildcardPolicy(network_anonymization_key, domain);
domain = GetSuperdomain(domain);
}
return wildcard_policy;
}
const NelPolicy* FindWildcardPolicy(
const NetworkAnonymizationKey& network_anonymization_key,
const std::string& domain) const {
DCHECK(!domain.empty());
auto it = wildcard_policies_.find(
WildcardNelPolicyKey(network_anonymization_key, domain));
if (it == wildcard_policies_.end())
return nullptr;
DCHECK(!it->second.empty());
for (const NelPolicy* policy : it->second) {
if (clock_->Now() < policy->expires)
return policy;
}
return nullptr;
}
// There must be no pre-existing policy for |policy.key|. Returns iterator
// to the inserted policy.
PolicyMap::iterator AddPolicy(NelPolicy policy) {
// If |initialized_| is false, then we are calling this from
// OnPoliciesLoaded(), which means we don't want to add the given policy to
// the store because we have just loaded it from there.
if (PoliciesArePersisted() && initialized_)
store_->AddNelPolicy(policy);
auto iter_and_result = policies_.emplace(policy.key, std::move(policy));
// TODO(crbug.com/1326282): Change this to a DCHECK when we're sure the bug
// is fixed.
CHECK(iter_and_result.second);
const NelPolicy& inserted_policy = iter_and_result.first->second;
MaybeAddWildcardPolicy(inserted_policy.key, &inserted_policy);
return iter_and_result.first;
}
void MaybeAddWildcardPolicy(const NelPolicyKey& origin_key,
const NelPolicy* policy) {
DCHECK(policy);
DCHECK_EQ(policy, &policies_[origin_key]);
if (!policy->include_subdomains)
return;
WildcardNelPolicyKey wildcard_key(origin_key);
auto inserted = wildcard_policies_[wildcard_key].insert(policy);
DCHECK(inserted.second);
}
// Removes the policy pointed to by |policy_it|. Invalidates |policy_it|.
// Returns the iterator to the next element.
PolicyMap::iterator RemovePolicy(PolicyMap::iterator policy_it) {
DCHECK(policy_it != policies_.end());
NelPolicy* policy = &policy_it->second;
MaybeRemoveWildcardPolicy(policy);
if (PoliciesArePersisted() && initialized_)
store_->DeleteNelPolicy(*policy);
return policies_.erase(policy_it);
}
void MaybeRemoveWildcardPolicy(const NelPolicy* policy) {
DCHECK(policy);
if (!policy->include_subdomains)
return;
const NelPolicyKey& origin_key = policy->key;
DCHECK_EQ(policy, &policies_[origin_key]);
auto wildcard_it =
wildcard_policies_.find(WildcardNelPolicyKey(origin_key));
DCHECK(wildcard_it != wildcard_policies_.end());
size_t erased = wildcard_it->second.erase(policy);
DCHECK_EQ(1u, erased);
if (wildcard_it->second.empty())
wildcard_policies_.erase(wildcard_it);
}
void MarkPolicyUsed(const NelPolicy* policy, base::Time time_used) const {
policy->last_used = time_used;
if (PoliciesArePersisted() && initialized_)
store_->UpdateNelPolicyAccessTime(*policy);
}
void RemoveAllExpiredPolicies() {
for (auto it = policies_.begin(); it != policies_.end();) {
if (it->second.expires < clock_->Now()) {
it = RemovePolicy(it);
} else {
++it;
}
}
}
void EvictStalestPolicy() {
PolicyMap::iterator stalest_it = policies_.begin();
for (auto it = policies_.begin(); it != policies_.end(); ++it) {
if (it->second.last_used < stalest_it->second.last_used)
stalest_it = it;
}
// This should only be called if we have hit the max policy limit, so there
// should be at least one policy.
DCHECK(stalest_it != policies_.end());
RemovePolicy(stalest_it);
}
static base::Value::Dict CreateReportBody(const std::string& phase,
const std::string& type,
double sampling_fraction,
const RequestDetails& details) {
base::Value::Dict body;
body.Set(kReferrerKey, details.referrer.spec());
body.Set(kSamplingFractionKey, sampling_fraction);
body.Set(kServerIpKey, details.server_ip.ToString());
body.Set(kProtocolKey, details.protocol);
body.Set(kMethodKey, details.method);
body.Set(kStatusCodeKey, details.status_code);
body.Set(kElapsedTimeKey,
static_cast<int>(details.elapsed_time.InMilliseconds()));
body.Set(kPhaseKey, phase);
body.Set(kTypeKey, type);
return body;
}
static base::Value::Dict CreateSignedExchangeReportBody(
const SignedExchangeReportDetails& details,
double sampling_fraction) {
base::Value::Dict body;
body.Set(kPhaseKey, kSignedExchangePhaseValue);
body.Set(kTypeKey, details.type);
body.Set(kSamplingFractionKey, sampling_fraction);
body.Set(kReferrerKey, details.referrer);
body.Set(kServerIpKey, details.server_ip_address.ToString());
body.Set(kProtocolKey, details.protocol);
body.Set(kMethodKey, details.method);
body.Set(kStatusCodeKey, details.status_code);
body.Set(kElapsedTimeKey,
static_cast<int>(details.elapsed_time.InMilliseconds()));
base::Value::Dict sxg_body;
sxg_body.Set(kOuterUrlKey, details.outer_url.spec());
if (details.inner_url.is_valid())
sxg_body.Set(kInnerUrlKey, details.inner_url.spec());
base::Value::List cert_url_list;
if (details.cert_url.is_valid())
cert_url_list.Append(details.cert_url.spec());
sxg_body.Set(kCertUrlKey, std::move(cert_url_list));
body.Set(kSignedExchangeBodyKey, std::move(sxg_body));
return body;
}
bool IsMismatchingSubdomainReport(const NelPolicy& policy,
const url::Origin& report_origin) const {
return policy.include_subdomains && (policy.key.origin != report_origin);
}
// Returns a valid value of matching fraction iff the event should be sampled.
std::optional<double> SampleAndReturnFraction(const NelPolicy& policy,
bool success) const {
const double sampling_fraction =
success ? policy.success_fraction : policy.failure_fraction;
// Sampling fractions are often either 0.0 or 1.0, so in those cases we
// can avoid having to call RandDouble().
if (sampling_fraction <= 0.0)
return std::nullopt;
if (sampling_fraction >= 1.0)
return sampling_fraction;
if (base::RandDouble() >= sampling_fraction)
return std::nullopt;
return sampling_fraction;
}
void FetchAllPoliciesFromStoreIfNecessary() {
if (!PoliciesArePersisted() || started_loading_policies_)
return;
started_loading_policies_ = true;
FetchAllPoliciesFromStore();
}
void FetchAllPoliciesFromStore() {
DCHECK(PoliciesArePersisted());
DCHECK(!initialized_);
store_->LoadNelPolicies(
base::BindOnce(&NetworkErrorLoggingServiceImpl::OnPoliciesLoaded,
weak_factory_.GetWeakPtr()));
}
// This is called when loading from the store is complete, regardless of
// success or failure.
// DB initialization may have failed, in which case we will receive an empty
// vector from the PersistentNelStore. This is indistinguishable from a
// successful load that happens to not yield any policies, but in
// either case we still want to go through the task backlog.
void OnPoliciesLoaded(std::vector<NelPolicy> loaded_policies) {
DCHECK(PoliciesArePersisted());
DCHECK(!initialized_);
// TODO(chlily): Toss any expired policies we encounter.
for (NelPolicy& policy : loaded_policies) {
if (policies_.find(policy.key) == policies_.end()) {
AddPolicy(std::move(policy));
}
}
initialized_ = true;
ExecuteBacklog();
}
};
} // namespace
NetworkErrorLoggingService::NelPolicyKey::NelPolicyKey() = default;
NetworkErrorLoggingService::NelPolicyKey::NelPolicyKey(
const NetworkAnonymizationKey& network_anonymization_key,
const url::Origin& origin)
: network_anonymization_key(network_anonymization_key), origin(origin) {}
NetworkErrorLoggingService::NelPolicyKey::NelPolicyKey(
const NelPolicyKey& other) = default;
bool NetworkErrorLoggingService::NelPolicyKey::operator<(
const NelPolicyKey& other) const {
return std::tie(network_anonymization_key, origin) <
std::tie(other.network_anonymization_key, other.origin);
}
bool NetworkErrorLoggingService::NelPolicyKey::operator==(
const NelPolicyKey& other) const {
return std::tie(network_anonymization_key, origin) ==
std::tie(other.network_anonymization_key, other.origin);
}
bool NetworkErrorLoggingService::NelPolicyKey::operator!=(
const NelPolicyKey& other) const {
return !(*this == other);
}
NetworkErrorLoggingService::NelPolicyKey::~NelPolicyKey() = default;
NetworkErrorLoggingService::WildcardNelPolicyKey::WildcardNelPolicyKey() =
default;
NetworkErrorLoggingService::WildcardNelPolicyKey::WildcardNelPolicyKey(
const NetworkAnonymizationKey& network_anonymization_key,
const std::string& domain)
: network_anonymization_key(network_anonymization_key), domain(domain) {}
NetworkErrorLoggingService::WildcardNelPolicyKey::WildcardNelPolicyKey(
const NelPolicyKey& origin_key)
: WildcardNelPolicyKey(origin_key.network_anonymization_key,
origin_key.origin.host()) {}
NetworkErrorLoggingService::WildcardNelPolicyKey::WildcardNelPolicyKey(
const WildcardNelPolicyKey& other) = default;
bool NetworkErrorLoggingService::WildcardNelPolicyKey::operator<(
const WildcardNelPolicyKey& other) const {
return std::tie(network_anonymization_key, domain) <
std::tie(other.network_anonymization_key, other.domain);
}
NetworkErrorLoggingService::WildcardNelPolicyKey::~WildcardNelPolicyKey() =
default;
NetworkErrorLoggingService::NelPolicy::NelPolicy() = default;
NetworkErrorLoggingService::NelPolicy::NelPolicy(const NelPolicy& other) =
default;
NetworkErrorLoggingService::NelPolicy::~NelPolicy() = default;
NetworkErrorLoggingService::RequestDetails::RequestDetails() = default;
NetworkErrorLoggingService::RequestDetails::RequestDetails(
const RequestDetails& other) = default;
NetworkErrorLoggingService::RequestDetails::~RequestDetails() = default;
NetworkErrorLoggingService::SignedExchangeReportDetails::
SignedExchangeReportDetails() = default;
NetworkErrorLoggingService::SignedExchangeReportDetails::
SignedExchangeReportDetails(const SignedExchangeReportDetails& other) =
default;
NetworkErrorLoggingService::SignedExchangeReportDetails::
~SignedExchangeReportDetails() = default;
const char NetworkErrorLoggingService::kHeaderName[] = "NEL";
const char NetworkErrorLoggingService::kReportType[] = "network-error";
const char
NetworkErrorLoggingService::kSignedExchangeRequestOutcomeHistogram[] =
"Net.NetworkErrorLogging.SignedExchangeRequestOutcome";
// Allow NEL reports on regular requests, plus NEL reports on Reporting uploads
// containing only regular requests, but do not allow NEL reports on Reporting
// uploads containing Reporting uploads.
//
// This prevents origins from building purposefully-broken Reporting endpoints
// that generate new NEL reports to bypass the age limit on Reporting reports.
const int NetworkErrorLoggingService::kMaxNestedReportDepth = 1;
const char NetworkErrorLoggingService::kReferrerKey[] = "referrer";
const char NetworkErrorLoggingService::kSamplingFractionKey[] =
"sampling_fraction";
const char NetworkErrorLoggingService::kServerIpKey[] = "server_ip";
const char NetworkErrorLoggingService::kProtocolKey[] = "protocol";
const char NetworkErrorLoggingService::kMethodKey[] = "method";
const char NetworkErrorLoggingService::kStatusCodeKey[] = "status_code";
const char NetworkErrorLoggingService::kElapsedTimeKey[] = "elapsed_time";
const char NetworkErrorLoggingService::kPhaseKey[] = "phase";
const char NetworkErrorLoggingService::kTypeKey[] = "type";
const char NetworkErrorLoggingService::kSignedExchangePhaseValue[] = "sxg";
const char NetworkErrorLoggingService::kSignedExchangeBodyKey[] = "sxg";
const char NetworkErrorLoggingService::kOuterUrlKey[] = "outer_url";
const char NetworkErrorLoggingService::kInnerUrlKey[] = "inner_url";
const char NetworkErrorLoggingService::kCertUrlKey[] = "cert_url";
// See also: max number of Reporting endpoints specified in ReportingPolicy.
const size_t NetworkErrorLoggingService::kMaxPolicies = 1000u;
// static
std::unique_ptr<NetworkErrorLoggingService> NetworkErrorLoggingService::Create(
PersistentNelStore* store) {
return std::make_unique<NetworkErrorLoggingServiceImpl>(store);
}
NetworkErrorLoggingService::~NetworkErrorLoggingService() = default;
void NetworkErrorLoggingService::SetReportingService(
ReportingService* reporting_service) {
DCHECK(!reporting_service_);
reporting_service_ = reporting_service;
}
void NetworkErrorLoggingService::OnShutdown() {
shut_down_ = true;
reporting_service_ = nullptr;
}
void NetworkErrorLoggingService::SetClockForTesting(const base::Clock* clock) {
clock_ = clock;
}
base::Value NetworkErrorLoggingService::StatusAsValue() const {
NOTIMPLEMENTED();
return base::Value();
}
std::set<NetworkErrorLoggingService::NelPolicyKey>
NetworkErrorLoggingService::GetPolicyKeysForTesting() {
NOTIMPLEMENTED();
return std::set<NelPolicyKey>();
}
NetworkErrorLoggingService::PersistentNelStore*
NetworkErrorLoggingService::GetPersistentNelStoreForTesting() {
NOTIMPLEMENTED();
return nullptr;
}
ReportingService* NetworkErrorLoggingService::GetReportingServiceForTesting() {
NOTIMPLEMENTED();
return nullptr;
}
NetworkErrorLoggingService::NetworkErrorLoggingService()
: clock_(base::DefaultClock::GetInstance()) {}
} // namespace net