// Copyright 2019 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/cert/coalescing_cert_verifier.h"

#include "base/containers/linked_list.h"
#include "base/containers/unique_ptr_adapters.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_macros.h"
#include "base/ranges/algorithm.h"
#include "base/strings/string_number_conversions.h"
#include "base/time/time.h"
#include "net/base/net_errors.h"
#include "net/cert/cert_verify_result.h"
#include "net/cert/crl_set.h"
#include "net/cert/pem.h"
#include "net/cert/x509_certificate_net_log_param.h"
#include "net/log/net_log_event_type.h"
#include "net/log/net_log_source.h"
#include "net/log/net_log_source_type.h"
#include "net/log/net_log_values.h"
#include "net/log/net_log_with_source.h"

namespace net {

// DESIGN OVERVIEW:
//
// The CoalescingCertVerifier implements an algorithm to group multiple calls
// to Verify() into a single Job. This avoids overloading the underlying
// CertVerifier, particularly those that are expensive to talk to (e.g.
// talking to the system verifier or across processes), batching multiple
// requests to CoaleacingCertVerifier::Verify() into a single underlying call.
//
// However, this makes lifetime management a bit more complex.
//   - The Job object represents all of the state for a single verification to
//     the CoalescingCertVerifier's underlying CertVerifier.
//       * It keeps the CertVerifyResult alive, which is required as long as
//         there is a pending verification.
//       * It keeps the CertVerify::Request to the underlying verifier alive,
//         as long as there is a pending Request attached to the Job.
//       * It keeps track of every CoalescingCertVerifier::Request that is
//         interested in receiving notification. However, it does NOT own
//         these objects, and thus needs to coordinate with the Request (via
//         AddRequest/AbortRequest) to make sure it never has a stale
//         pointer.
//         NB: It would have also been possible for the Job to only
//         hold WeakPtr<Request>s, rather than Request*, but that seemed less
//         clear as to the lifetime invariants, even if it was more clear
//         about how the pointers are used.
//  - The Job object is always owned by the CoalescingCertVerifier. If the
//    CoalescingCertVerifier is deleted, all in-flight requests to the
//    underlying verifier should be cancelled. When the Job goes away, all the
//    Requests will be orphaned.
//  - The Request object is always owned by the CALLER. It is a handle to
//    allow a caller to cancel a request, per the CertVerifier interface. If
//    the Request goes away, no caller callbacks should be invoked if the Job
//    it was (previously) attached to completes.
//  - Per the CertVerifier interface, when the CoalescingCertVerifier is
//    deleted, then regardless of there being any live Requests, none of those
//    caller callbacks should be invoked.
//
// Finally, to add to the complexity, it's possible that, during the handling
// of a result from the underlying CertVerifier, a Job may begin dispatching
// to its Requests. The Request may delete the CoalescingCertVerifier. If that
// happens, then the Job being processed is also deleted, and none of the
// other Requests should be notified.

namespace {

base::Value::Dict CertVerifierParams(
    const CertVerifier::RequestParams& params) {
  base::Value::Dict dict;
  dict.Set("certificates",
           NetLogX509CertificateList(params.certificate().get()));
  if (!params.ocsp_response().empty()) {
    dict.Set("ocsp_response",
             PEMEncode(params.ocsp_response(), "NETLOG OCSP RESPONSE"));
  }
  if (!params.sct_list().empty()) {
    dict.Set("sct_list", PEMEncode(params.sct_list(), "NETLOG SCT LIST"));
  }
  dict.Set("host", NetLogStringValue(params.hostname()));
  dict.Set("verifier_flags", params.flags());

  return dict;
}

}  // namespace

// Job contains all the state for a single verification using the underlying
// verifier.
class CoalescingCertVerifier::Job {
 public:
  Job(CoalescingCertVerifier* parent,
      const CertVerifier::RequestParams& params,
      NetLog* net_log,
      bool is_first_job);
  ~Job();

  const CertVerifier::RequestParams& params() const { return params_; }
  const CertVerifyResult& verify_result() const { return verify_result_; }

  // Attaches |request|, causing it to be notified once this Job completes.
  void AddRequest(CoalescingCertVerifier::Request* request);

  // Stops |request| from being notified. If there are no Requests remaining,
  // the Job will be cancelled.
  // NOTE: It's only necessary to call this if the Job has not yet completed.
  // If the Request has been notified of completion, this should not be called.
  void AbortRequest(CoalescingCertVerifier::Request* request);

  // Starts a verification using |underlying_verifier|. If this completes
  // synchronously, returns the result code, with the associated result being
  // available via |verify_result()|. Otherwise, it will complete
  // asynchronously, notifying any Requests associated via |AttachRequest|.
  int Start(CertVerifier* underlying_verifier);

 private:
  void OnVerifyComplete(int result);

  void LogMetrics();

  raw_ptr<CoalescingCertVerifier> parent_verifier_;
  const CertVerifier::RequestParams params_;
  const NetLogWithSource net_log_;
  bool is_first_job_ = false;
  CertVerifyResult verify_result_;

  base::TimeTicks start_time_;
  std::unique_ptr<CertVerifier::Request> pending_request_;

  base::LinkedList<CoalescingCertVerifier::Request> attached_requests_;
  base::WeakPtrFactory<Job> weak_ptr_factory_{this};
};

// Tracks the state associated with a single CoalescingCertVerifier::Verify
// request.
//
// There are two ways for requests to be cancelled:
//   - The caller of Verify() can delete the Request object, indicating
//     they are no longer interested in this particular request.
//   - The caller can delete the CoalescingCertVerifier, which should cause
//     all in-process Jobs to be aborted and deleted. Any Requests attached to
//     Jobs should be orphaned, and do nothing when the Request is (eventually)
//     deleted.
class CoalescingCertVerifier::Request
    : public base::LinkNode<CoalescingCertVerifier::Request>,
      public CertVerifier::Request {
 public:
  // Create a request that will be attached to |job|, and will notify
  // |callback| and fill |verify_result| if the Job completes successfully.
  // If the Request is deleted, or the Job is deleted, |callback| will not
  // be notified.
  Request(CoalescingCertVerifier::Job* job,
          CertVerifyResult* verify_result,
          CompletionOnceCallback callback,
          const NetLogWithSource& net_log);

  ~Request() override;

  const NetLogWithSource& net_log() const { return net_log_; }

  // Called by Job to complete the requests (either successfully or as a sign
  // that the underlying Job is going away).
  void Complete(int result);

  // Called when |job_| is being deleted, to ensure that the Request does not
  // attempt to access the Job further. No callbacks will be invoked,
  // consistent with the CoalescingCertVerifier's contract.
  void OnJobAbort();

 private:
  raw_ptr<CoalescingCertVerifier::Job, DanglingUntriaged> job_;

  raw_ptr<CertVerifyResult, DanglingUntriaged> verify_result_;
  CompletionOnceCallback callback_;
  const NetLogWithSource net_log_;
};

CoalescingCertVerifier::Job::Job(CoalescingCertVerifier* parent,
                                 const CertVerifier::RequestParams& params,
                                 NetLog* net_log,
                                 bool is_first_job)
    : parent_verifier_(parent),
      params_(params),
      net_log_(
          NetLogWithSource::Make(net_log, NetLogSourceType::CERT_VERIFIER_JOB)),
      is_first_job_(is_first_job) {}

CoalescingCertVerifier::Job::~Job() {
  // If there was at least one outstanding Request still pending, then this
  // Job was aborted, rather than being completed normally and cleaned up.
  if (!attached_requests_.empty() && pending_request_) {
    net_log_.AddEvent(NetLogEventType::CANCELLED);
    net_log_.EndEvent(NetLogEventType::CERT_VERIFIER_JOB);
  }

  while (!attached_requests_.empty()) {
    auto* link_node = attached_requests_.head();
    link_node->RemoveFromList();
    link_node->value()->OnJobAbort();
  }
}

void CoalescingCertVerifier::Job::AddRequest(
    CoalescingCertVerifier::Request* request) {
  // There must be a pending asynchronous verification in process.
  DCHECK(pending_request_);

  request->net_log().AddEventReferencingSource(
      NetLogEventType::CERT_VERIFIER_REQUEST_BOUND_TO_JOB, net_log_.source());
  attached_requests_.Append(request);
}

void CoalescingCertVerifier::Job::AbortRequest(
    CoalescingCertVerifier::Request* request) {
  // Check to make sure |request| hasn't already been removed.
  DCHECK(request->previous() || request->next());

  request->RemoveFromList();

  // If there are no more pending requests, abort. This isn't strictly
  // necessary; the request could be allowed to run to completion (and
  // potentially to allow later Requests to join in), but in keeping with the
  // idea of providing more stable guarantees about resources, clean up early.
  if (attached_requests_.empty()) {
    // If this was the last Request, then the Job had not yet completed; this
    // matches the logic in the dtor, which handles when it's the Job that is
    // deleted first, rather than the last Request.
    net_log_.AddEvent(NetLogEventType::CANCELLED);
    net_log_.EndEvent(NetLogEventType::CERT_VERIFIER_JOB);

    // DANGER: This will cause |this_| to be deleted!
    parent_verifier_->RemoveJob(this);
    return;
  }
}

int CoalescingCertVerifier::Job::Start(CertVerifier* underlying_verifier) {
  // Requests are only attached for asynchronous completion, so they must
  // always be attached after Start() has been called.
  DCHECK(attached_requests_.empty());
  // There should not be a pending request already started (e.g. Start called
  // multiple times).
  DCHECK(!pending_request_);

  net_log_.BeginEvent(NetLogEventType::CERT_VERIFIER_JOB,
                      [&] { return CertVerifierParams(params_); });

  verify_result_.Reset();

  start_time_ = base::TimeTicks::Now();
  int result = underlying_verifier->Verify(
      params_, &verify_result_,
      // Safe, because |verify_request_| is self-owned and guarantees the
      // callback won't be called if |this| is deleted.
      base::BindOnce(&CoalescingCertVerifier::Job::OnVerifyComplete,
                     base::Unretained(this)),
      &pending_request_, net_log_);
  if (result != ERR_IO_PENDING) {
    LogMetrics();
    net_log_.EndEvent(NetLogEventType::CERT_VERIFIER_JOB,
                      [&] { return verify_result_.NetLogParams(result); });
  }

  return result;
}

void CoalescingCertVerifier::Job::OnVerifyComplete(int result) {
  LogMetrics();

  pending_request_.reset();  // Reset to signal clean completion.
  net_log_.EndEvent(NetLogEventType::CERT_VERIFIER_JOB,
                    [&] { return verify_result_.NetLogParams(result); });

  // It's possible that during the process of invoking a callback for a
  // Request, |this| may get deleted (along with the associated parent). If
  // that happens, it's important to ensure that processing of the Job is
  // stopped - i.e. no other callbacks are invoked for other Requests, nor is
  // |this| accessed.
  //
  // To help detect and protect against this, a WeakPtr to |this| is taken. If
  // |this| is deleted, the destructor will have invalidated the WeakPtr.
  //
  // Note that if a Job had already been deleted, this method would not have
  // been invoked in the first place, as the Job (via |pending_request_|) owns
  // the underlying CertVerifier::Request that this method was bound to as a
  // callback. This is why it's OK to grab the WeakPtr from |this| initially.
  base::WeakPtr<Job> weak_this = weak_ptr_factory_.GetWeakPtr();
  while (!attached_requests_.empty()) {
    // Note: It's also possible for additional Requests to be attached to the
    // current Job while processing a Request.
    auto* link_node = attached_requests_.head();
    link_node->RemoveFromList();

    // Note: |this| MAY be deleted here.
    //   - If the CoalescingCertVerifier is deleted, it will delete the
    //     Jobs (including |this|)
    //   - If this is the second-to-last Request, and the completion of this
    //     event causes the other Request to be deleted, detaching that Request
    //     from this Job will lead to this Job being deleted (via
    //     Job::AbortRequest())
    link_node->value()->Complete(result);

    // Check if |this| has been deleted (which implicitly includes
    // |parent_verifier_|), and abort if so, since no further cleanup is
    // needed.
    if (!weak_this)
      return;
  }

  // DANGER: |this| will be invalidated (deleted) after this point.
  return parent_verifier_->RemoveJob(this);
}

void CoalescingCertVerifier::Job::LogMetrics() {
  base::TimeDelta latency = base::TimeTicks::Now() - start_time_;
  UMA_HISTOGRAM_CUSTOM_TIMES("Net.CertVerifier_Job_Latency", latency,
                             base::Milliseconds(1), base::Minutes(10), 100);
  if (is_first_job_) {
    UMA_HISTOGRAM_CUSTOM_TIMES("Net.CertVerifier_First_Job_Latency", latency,
                               base::Milliseconds(1), base::Minutes(10), 100);
  }
}

CoalescingCertVerifier::Request::Request(CoalescingCertVerifier::Job* job,
                                         CertVerifyResult* verify_result,
                                         CompletionOnceCallback callback,
                                         const NetLogWithSource& net_log)
    : job_(job),
      verify_result_(verify_result),
      callback_(std::move(callback)),
      net_log_(net_log) {
  net_log_.BeginEvent(NetLogEventType::CERT_VERIFIER_REQUEST);
}

CoalescingCertVerifier::Request::~Request() {
  if (job_) {
    net_log_.AddEvent(NetLogEventType::CANCELLED);
    net_log_.EndEvent(NetLogEventType::CERT_VERIFIER_REQUEST);

    // If the Request is deleted before the Job, then detach from the Job.
    // Note: This may cause |job_| to be deleted.
    job_->AbortRequest(this);
    job_ = nullptr;
  }
}

void CoalescingCertVerifier::Request::Complete(int result) {
  DCHECK(job_);  // There must be a pending/non-aborted job to complete.

  *verify_result_ = job_->verify_result();

  // On successful completion, the Job removes the Request from its set;
  // similarly, break the association here so that when the Request is
  // deleted, it does not try to abort the (now-completed) Job.
  job_ = nullptr;

  net_log_.EndEvent(NetLogEventType::CERT_VERIFIER_REQUEST);

  // Run |callback_|, which may delete |this|.
  std::move(callback_).Run(result);
}

void CoalescingCertVerifier::Request::OnJobAbort() {
  DCHECK(job_);  // There must be a pending job to abort.

  // If the Job is deleted before the Request, just clean up. The Request will
  // eventually be deleted by the caller.
  net_log_.AddEvent(NetLogEventType::CANCELLED);
  net_log_.EndEvent(NetLogEventType::CERT_VERIFIER_REQUEST);

  job_ = nullptr;
  // Note: May delete |this|, if the caller made |callback_| own the Request.
  callback_.Reset();
}

CoalescingCertVerifier::CoalescingCertVerifier(
    std::unique_ptr<CertVerifier> verifier)
    : verifier_(std::move(verifier)) {
  verifier_->AddObserver(this);
}

CoalescingCertVerifier::~CoalescingCertVerifier() {
  verifier_->RemoveObserver(this);
}

int CoalescingCertVerifier::Verify(
    const RequestParams& params,
    CertVerifyResult* verify_result,
    CompletionOnceCallback callback,
    std::unique_ptr<CertVerifier::Request>* out_req,
    const NetLogWithSource& net_log) {
  DCHECK(verify_result);
  DCHECK(!callback.is_null());

  out_req->reset();
  ++requests_;

  Job* job = FindJob(params);
  if (job) {
    // An identical request is in-flight and joinable, so just attach the
    // callback.
    ++inflight_joins_;
  } else {
    // No existing Jobs can be used. Create and start a new one.
    std::unique_ptr<Job> new_job =
        std::make_unique<Job>(this, params, net_log.net_log(), requests_ == 1);
    int result = new_job->Start(verifier_.get());
    if (result != ERR_IO_PENDING) {
      *verify_result = new_job->verify_result();
      return result;
    }

    job = new_job.get();
    joinable_jobs_[params] = std::move(new_job);
  }

  std::unique_ptr<CoalescingCertVerifier::Request> request =
      std::make_unique<CoalescingCertVerifier::Request>(
          job, verify_result, std::move(callback), net_log);
  job->AddRequest(request.get());
  *out_req = std::move(request);
  return ERR_IO_PENDING;
}

void CoalescingCertVerifier::SetConfig(const CertVerifier::Config& config) {
  verifier_->SetConfig(config);

  IncrementGenerationAndMakeCurrentJobsUnjoinable();
}

void CoalescingCertVerifier::AddObserver(CertVerifier::Observer* observer) {
  verifier_->AddObserver(observer);
}

void CoalescingCertVerifier::RemoveObserver(CertVerifier::Observer* observer) {
  verifier_->RemoveObserver(observer);
}

CoalescingCertVerifier::Job* CoalescingCertVerifier::FindJob(
    const RequestParams& params) {
  auto it = joinable_jobs_.find(params);
  if (it != joinable_jobs_.end())
    return it->second.get();
  return nullptr;
}

void CoalescingCertVerifier::RemoveJob(Job* job) {
  // See if this was a job from the current configuration generation.
  // Note: It's also necessary to compare that the underlying pointer is the
  // same, and not merely a Job with the same parameters.
  auto joinable_it = joinable_jobs_.find(job->params());
  if (joinable_it != joinable_jobs_.end() && joinable_it->second.get() == job) {
    joinable_jobs_.erase(joinable_it);
    return;
  }

  // Otherwise, it MUST have been a job from a previous generation.
  auto inflight_it =
      base::ranges::find_if(inflight_jobs_, base::MatchesUniquePtr(job));
  DCHECK(inflight_it != inflight_jobs_.end());
  inflight_jobs_.erase(inflight_it);
  return;
}

void CoalescingCertVerifier::IncrementGenerationAndMakeCurrentJobsUnjoinable() {
  for (auto& job : joinable_jobs_) {
    inflight_jobs_.emplace_back(std::move(job.second));
  }
  joinable_jobs_.clear();
}

void CoalescingCertVerifier::OnCertVerifierChanged() {
  IncrementGenerationAndMakeCurrentJobsUnjoinable();
}

}  // namespace net
