// 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 <string_view>
#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_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/notimplemented.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 "net/reporting/reporting_target_type.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"},
    // The following were all historically mapped to "dns.server".
    {ERR_DNS_FORMAT_ERROR, kDnsPhase, "dns.server"},
    {ERR_DNS_SERVER_FAILURE, kDnsPhase, "dns.server"},
    {ERR_DNS_NOT_IMPLEMENTED, kDnsPhase, "dns.server"},
    {ERR_DNS_REFUSED, kDnsPhase, "dns.server"},
    {ERR_DNS_OTHER_FAILURE, 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::DictValue dict;
    base::ListValue 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::DictValue 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_;
  }

  void LoadPoliciesForTesting(std::vector<NelPolicy> policies) override {
    started_loading_policies_ = true;
    OnPoliciesLoaded(policies);
  }

 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));
      // TODO(crbug.com/450428442): Remove this UMA after we investigate OOM.
      // Sample with a 0.001 probability to reduce metrics overhead.
      if (base::ShouldRecordSubsampledMetric(0.001)) {
        base::UmaHistogramCounts1000(
            "Net.NetworkErrorLoggingService.TaskBacklogSize",
            task_backlog_.size());
      }
      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) {
      GURL gurl = policy.key.origin.GetURL();
      if (registry_controlled_domains::GetRegistry(
              gurl, registry_controlled_domains::INCLUDE_UNKNOWN_REGISTRIES,
              registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES) == "") {
        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. This also applies if any other address contacted
    // during the request differs from the policy's address, since the report
    // would otherwise reflect the behaviour of those addresses too.
    bool server_ip_changed =
        (details.server_ip.IsValid() &&
         details.server_ip != policy->received_ip_address) ||
        std::ranges::any_of(details.other_server_ips, [&](const auto& ip) {
          return ip != policy->received_ip_address;
        });
    if (phase_string != kDnsPhase && server_ip_changed) {
      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, ReportingTargetType::kDeveloper);
  }

  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 */, ReportingTargetType::kDeveloper);
    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 (const 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::DictValue* 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/40225752): 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) {
    CHECK(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));
    CHECK(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.
    CHECK(stalest_it != policies_.end());

    RemovePolicy(stalest_it);
  }

  static base::DictValue CreateReportBody(const std::string& phase,
                                          const std::string& type,
                                          double sampling_fraction,
                                          const RequestDetails& details) {
    base::DictValue 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::DictValue CreateSignedExchangeReportBody(
      const SignedExchangeReportDetails& details,
      double sampling_fraction) {
    base::DictValue 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()));

    // Strip username, password, and ref fragment from the URLs in the body,
    // matching what ReportingService::QueueReport() does for the top-level URL.
    base::DictValue sxg_body;
    sxg_body.Set(kOuterUrlKey, details.outer_url.GetAsReferrer().spec());
    if (details.inner_url.is_valid()) {
      sxg_body.Set(kInnerUrlKey, details.inner_url.GetAsReferrer().spec());
    }

    base::ListValue cert_url_list;
    if (details.cert_url.is_valid()) {
      cert_url_list.Append(details.cert_url.GetAsReferrer().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;

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(
    RequestDetails&& other) = default;

NetworkErrorLoggingService::RequestDetails&
NetworkErrorLoggingService::RequestDetails::operator=(
    const RequestDetails& other) = default;

NetworkErrorLoggingService::RequestDetails&
NetworkErrorLoggingService::RequestDetails::operator=(RequestDetails&& other) =
    default;

NetworkErrorLoggingService::RequestDetails::~RequestDetails() = default;

NetworkErrorLoggingService::SignedExchangeReportDetails::
    SignedExchangeReportDetails() = default;

NetworkErrorLoggingService::SignedExchangeReportDetails::
    SignedExchangeReportDetails(const SignedExchangeReportDetails& other) =
        default;

NetworkErrorLoggingService::SignedExchangeReportDetails::
    SignedExchangeReportDetails(SignedExchangeReportDetails&& other) = default;

NetworkErrorLoggingService::SignedExchangeReportDetails&
NetworkErrorLoggingService::SignedExchangeReportDetails::operator=(
    const SignedExchangeReportDetails& other) = default;

NetworkErrorLoggingService::SignedExchangeReportDetails&
NetworkErrorLoggingService::SignedExchangeReportDetails::operator=(
    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;
}

void NetworkErrorLoggingService::LoadPoliciesForTesting(
    std::vector<NetworkErrorLoggingService::NelPolicy> policies) {
  NOTIMPLEMENTED();
  return;
}

NetworkErrorLoggingService::NetworkErrorLoggingService()
    : clock_(base::DefaultClock::GetInstance()) {}

}  // namespace net
