// Copyright 2024 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/dns/host_resolver_manager_service_endpoint_request_impl.h"

#include <sstream>

#include "base/containers/to_vector.h"
#include "base/feature_list.h"
#include "base/memory/safe_ref.h"
#include "base/no_destructor.h"
#include "base/notreached.h"
#include "base/types/optional_util.h"
#include "net/base/features.h"
#include "net/base/net_errors.h"
#include "net/base/network_handle.h"
#include "net/dns/dns_alias_utility.h"
#include "net/dns/dns_task_results_manager.h"
#include "net/dns/host_cache.h"
#include "net/dns/host_resolver.h"
#include "net/dns/host_resolver_manager.h"
#include "net/dns/public/resolve_error_info.h"
#include "net/http/http_network_session.h"
#include "net/socket/client_socket_factory.h"
#include "net/url_request/url_request_context.h"
#include "url/scheme_host_port.h"

namespace net {

HostResolverManager::ServiceEndpointRequestImpl::FinalizedResult::
    FinalizedResult(std::vector<ServiceEndpoint> endpoints,
                    std::set<std::string> dns_aliases)
    : endpoints(std::move(endpoints)), dns_aliases(std::move(dns_aliases)) {}

HostResolverManager::ServiceEndpointRequestImpl::FinalizedResult::
    ~FinalizedResult() = default;

HostResolverManager::ServiceEndpointRequestImpl::FinalizedResult::
    FinalizedResult(FinalizedResult&&) = default;
HostResolverManager::ServiceEndpointRequestImpl::FinalizedResult&
HostResolverManager::ServiceEndpointRequestImpl::FinalizedResult::operator=(
    FinalizedResult&&) = default;

HostResolverManager::ServiceEndpointRequestImpl::ServiceEndpointRequestImpl(
    HostResolver::Host host,
    NetworkAnonymizationKey network_anonymization_key,
    handles::NetworkHandle target_network,
    NetLogWithSource net_log,
    ResolveHostParameters parameters,
    base::WeakPtr<ResolveContext> resolve_context,
    base::WeakPtr<HostResolverManager> manager,
    const base::TickClock* tick_clock)
    : host_(std::move(host)),
      network_anonymization_key_(
          NetworkAnonymizationKey::IsPartitioningEnabled() &&
                  base::FeatureList::IsEnabled(
                      features::kSplitHostCacheByNetworkAnonymizationKey)
              ? std::move(network_anonymization_key)
              : NetworkAnonymizationKey()),
      target_network_(target_network),
      net_log_(std::move(net_log)),
      parameters_(std::move(parameters)),
      resolve_context_(std::move(resolve_context)),
      manager_(std::move(manager)),
      tick_clock_(tick_clock),
      priority_(parameters_.initial_priority) {}

HostResolverManager::ServiceEndpointRequestImpl::~ServiceEndpointRequestImpl() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!job_.has_value()) {
    return;
  }

  LogCancelRequest();

  // Clear the delegate to avoid calling delegate's callback after destruction.
  // The following CancelServiceEndpointRequest() could result in calling
  // OnJobCancelled() synchronously.
  delegate_ = nullptr;

  job_.value()->CancelServiceEndpointRequest(this);
  // TODO(crbug.com/397597592): Remove the following CHECKs after we identified
  // the cause of the bug.
  CHECK(previous() == nullptr);
  CHECK(next() == nullptr);
}

int HostResolverManager::ServiceEndpointRequestImpl::Start(Delegate* delegate) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  CHECK(!delegate_);
  CHECK(manager_);

  if (!resolve_context_) {
    error_info_ = ResolveErrorInfo(ERR_CONTEXT_SHUT_DOWN);
    return HostResolver::SquashErrorCode(ERR_CONTEXT_SHUT_DOWN);
  }

  delegate_ = delegate;

  next_state_ = State::kCheckIPv6Reachability;
  // Squash the error code like asynchronous completions. The detailed error
  // is available via GetResolveErrorInfo().
  return HostResolver::SquashErrorCode(DoLoop(OK));
}

const HostCache::EntryStaleness*
HostResolverManager::ServiceEndpointRequestImpl::GetStaleInfo() const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return base::OptionalToPtr(stale_info_);
}

bool HostResolverManager::ServiceEndpointRequestImpl::IsStaleWhileRefreshing()
    const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return parameters_.cache_usage == ResolveHostParameters::CacheUsage::
                                        STALE_ALLOWED_WHILE_REFRESHING &&
         stale_info_.has_value() && stale_info_.value().is_stale();
}

base::span<const ServiceEndpoint>
HostResolverManager::ServiceEndpointRequestImpl::GetEndpointResults() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (finalized_result_.has_value()) {
    return finalized_result_->endpoints;
  }

  // There are two cases where `stale_endpoints_` is empty:
  //  * No stale results received yet.
  //  * The stale result is negative.
  // In either case, providing stale results isn't useful, so provide stale
  // results only if it's not empty.
  if (!stale_endpoints_.empty()) {
    return stale_endpoints_;
  }

  if (job_ && job_.value()->dns_task_results_manager()) {
    return job_.value()->dns_task_results_manager()->GetCurrentEndpoints();
  }

  return {};
}

const std::set<std::string>&
HostResolverManager::ServiceEndpointRequestImpl::GetDnsAliasResults() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (finalized_result_.has_value()) {
    return finalized_result_->dns_aliases;
  }

  if (job_ && job_.value()->dns_task_results_manager()) {
    return job_.value()->dns_task_results_manager()->GetAliases();
  }

  static const base::NoDestructor<std::set<std::string>> kEmptyDnsAliases;
  return *kEmptyDnsAliases.get();
}

bool HostResolverManager::ServiceEndpointRequestImpl::EndpointsCryptoReady() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (finalized_result_.has_value()) {
    // If there are no endpoints in the finalized result, `this` is not ready
    // for cryptographic handshakes.
    return !finalized_result_->endpoints.empty();
  }

  if (job_ && job_.value()->dns_task_results_manager()) {
    return job_.value()->dns_task_results_manager()->IsMetadataReady();
  }

  // If there is no running DnsTask, `this` is not ready for cryptographic
  // handshakes until receiving the final results.
  return false;
}

std::optional<ResolutionDetails>
HostResolverManager::ServiceEndpointRequestImpl::GetResolutionDetails() const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return resolution_details_;
}

ResolveErrorInfo
HostResolverManager::ServiceEndpointRequestImpl::GetResolveErrorInfo() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return error_info_;
}

void HostResolverManager::ServiceEndpointRequestImpl::ChangeRequestPriority(
    RequestPriority priority) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!job_.has_value()) {
    priority_ = priority;
    return;
  }
  job_.value()->ChangeServiceEndpointRequestPriority(this, priority);
}

std::string HostResolverManager::ServiceEndpointRequestImpl::DebugString()
    const {
  std::stringstream ss;
  ss << "it=[";
  for (const auto& task : initial_tasks_) {
    ss << base::strict_cast<int>(task) << ",";
  }
  ss << "],j=" << job_.has_value();
  if (job_) {
    ss << ",rm=" << (!!job_.value()->dns_task_results_manager());
  }
  return ss.str();
}

void HostResolverManager::ServiceEndpointRequestImpl::AssignJob(
    base::SafeRef<Job> job) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  CHECK(!job_.has_value());
  job_ = job;
}

void HostResolverManager::ServiceEndpointRequestImpl::OnJobCompleted(
    const HostCache::Entry& results,
    bool obtained_securely,
    ResolutionDetails resolution_details) {
  CHECK(job_);
  CHECK(delegate_);
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  job_.reset();
  if (results.error() == OK) {
    resolution_details_ = resolution_details;
  }
  SetFinalizedResultFromLegacyResults(results);
  MaybeClearStaleResults();

  const bool is_secure_network_error =
      obtained_securely && results.error() != OK;
  error_info_ = ResolveErrorInfo(results.error(), is_secure_network_error);
  delegate_->OnServiceEndpointRequestFinished(
      HostResolver::SquashErrorCode(results.error()));
  // Do not add code below. `this` may be deleted at this point.
}

void HostResolverManager::ServiceEndpointRequestImpl::OnJobCancelled() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  CHECK(job_);

  job_.reset();

  // The owner of `this` has already destroyed `this`.
  if (!delegate_) {
    return;
  }

  LogCancelRequest();

  finalized_result_ = FinalizedResult(/*endpoints=*/{}, /*dns_aliases=*/{});
  error_info_ = ResolveErrorInfo(ERR_DNS_REQUEST_CANCELLED);
  delegate_->OnServiceEndpointRequestFinished(
      HostResolver::SquashErrorCode(ERR_DNS_REQUEST_CANCELLED));
  // Do not add code below. `this` may be deleted at this point.
}

void HostResolverManager::ServiceEndpointRequestImpl::
    OnServiceEndpointsChanged() {
  // This method is called asynchronously via a posted task. `job_` could
  // be completed or cancelled before executing the task.
  if (finalized_result_.has_value()) {
    return;
  }

  // There are fresh endpoints available. Clear stale endpoints and info if this
  // request allows stale results while refreshing.
  MaybeClearStaleResults();

  CHECK(job_);
  CHECK(delegate_);
  delegate_->OnServiceEndpointsUpdated();
  // Do not add code below. `this` may be deleted at this point.
}

base::WeakPtr<HostResolverManager::ServiceEndpointRequestImpl>
HostResolverManager::ServiceEndpointRequestImpl::GetWeakPtr() {
  return weak_ptr_factory_.GetWeakPtr();
}

int HostResolverManager::ServiceEndpointRequestImpl::DoLoop(int rv) {
  do {
    State state = next_state_;
    next_state_ = State::kNone;
    switch (state) {
      case State::kCheckIPv6Reachability:
        rv = DoCheckIPv6Reachability();
        break;
      case State::kCheckIPv6ReachabilityComplete:
        rv = DoCheckIPv6ReachabilityComplete(rv);
        break;
      case State::kDoResolveLocally:
        rv = DoResolveLocally();
        break;
      case State::kStartJob:
        rv = DoStartJob();
        break;
      case State::kNone:
        NOTREACHED() << "Invalid state";
    }
  } while (next_state_ != State::kNone && rv != ERR_IO_PENDING);

  return rv;
}

int HostResolverManager::ServiceEndpointRequestImpl::DoCheckIPv6Reachability() {
  next_state_ = State::kCheckIPv6ReachabilityComplete;
  // LOCAL_ONLY requires a synchronous response, so it cannot wait on an async
  // reachability check result and cannot make assumptions about reachability.
  // Return ERR_NAME_NOT_RESOLVED when LOCAL_ONLY is specified and the check
  // is blocked. See also the comment in
  // HostResolverManager::RequestImpl::DoIPv6Reachability().
  if (parameters_.source == HostResolverSource::LOCAL_ONLY) {
    int rv = manager_->StartIPv6ReachabilityCheck(
        target_network_, net_log_, GetClientSocketFactory(),
        base::DoNothingAs<void(int)>());
    if (rv == ERR_IO_PENDING) {
      next_state_ = State::kNone;
      finalized_result_ = FinalizedResult(/*endpoints=*/{}, /*dns_aliases=*/{});
      error_info_ = ResolveErrorInfo(ERR_NAME_NOT_RESOLVED);
      return ERR_NAME_NOT_RESOLVED;
    }
    return OK;
  }
  return manager_->StartIPv6ReachabilityCheck(
      target_network_, net_log_, GetClientSocketFactory(),
      base::BindOnce(&ServiceEndpointRequestImpl::OnIOComplete,
                     weak_ptr_factory_.GetWeakPtr()));
}

int HostResolverManager::ServiceEndpointRequestImpl::
    DoCheckIPv6ReachabilityComplete(int rv) {
  next_state_ = rv == OK ? State::kDoResolveLocally : State::kNone;
  return rv;
}

int HostResolverManager::ServiceEndpointRequestImpl::DoResolveLocally() {
  job_key_ = JobKey(host_, target_network_, resolve_context_.get());
  IPAddress ip_address;
  manager_->InitializeJobKeyAndIPAddress(
      network_anonymization_key_, parameters_, net_log_, *job_key_, ip_address);

  const bool only_ipv6_reachable = false;
  const bool stale_allowed_while_refreshing =
      parameters_.cache_usage ==
      ResolveHostParameters::CacheUsage::STALE_ALLOWED_WHILE_REFRESHING;

  // HostResolverManager doesn't recognize STALE_ALLOWED_WHILE_REFRESHING. This
  // class implements stale-while-refreshing logic (see the following comments).
  // Use ALLOWED when the parameter's the source is LOCAL_ONLY. Otherwise, use
  // STALE_ALLOWED to provide stale results as intermediate results.
  ResolveHostParameters::CacheUsage cache_usage = parameters_.cache_usage;
  if (stale_allowed_while_refreshing) {
    cache_usage = parameters_.source == HostResolverSource::LOCAL_ONLY
                      ? ResolveHostParameters::CacheUsage::ALLOWED
                      : ResolveHostParameters::CacheUsage::STALE_ALLOWED;
  }

  HostCache::Entry results = manager_->ResolveLocally(
      only_ipv6_reachable, *job_key_, ip_address, cache_usage,
      parameters_.secure_dns_policy, parameters_.source, net_log_, host_cache(),
      &tasks_, &stale_info_);
  bool is_stale = stale_info_.has_value() && stale_info_->is_stale();

  if (is_stale && stale_allowed_while_refreshing) {
    // When a stale result is found, ResolveLocally() returns the stale result
    // without executing the remaining tasks, including local tasks such as
    // INSECURE_CACHE_LOOKUP and HOSTS. These tasks may be able to provide a
    // fresh result, and are always expected to be tried (and removed from
    // `tasks_`) before starting an async Job. Call ResolveLocally() again with
    // CacheUsage::ALLOWED to see we can get a fresh result.
    // TODO(crbug.com/383174960): Consider refactoring ResolveLocally() so that
    // we don't have to call ResolveLocally() twice.
    CHECK_EQ(cache_usage, ResolveHostParameters::CacheUsage::STALE_ALLOWED);
    tasks_.clear();
    std::optional<HostCache::EntryStaleness> maybe_fresh_info;
    HostCache::Entry maybe_non_stale_results = manager_->ResolveLocally(
        only_ipv6_reachable, *job_key_, ip_address,
        ResolveHostParameters::CacheUsage::ALLOWED,
        parameters_.secure_dns_policy, parameters_.source, net_log_,
        host_cache(), &tasks_, &maybe_fresh_info, /*record_metrics=*/false);
    CHECK(!maybe_fresh_info.has_value() || !maybe_fresh_info->is_stale());
    if (maybe_non_stale_results.error() != ERR_DNS_CACHE_MISS ||
        tasks_.empty()) {
      stale_info_ = maybe_fresh_info;
      results = std::move(maybe_non_stale_results);
      is_stale = false;
    }
    CHECK(parameters_.source != HostResolverSource::LOCAL_ONLY);
  }

  if (is_stale && stale_allowed_while_refreshing) {
    if (results.error() == OK &&
        results.network_changes() == host_cache()->network_changes()) {
      // Allow using stale results only when there is no network change.
      // TODO(crbug.com/383174960): This also excludes results that are obtained
      // from the same network but the device got disconnected/connected
      // events. Ideally we should be able to use such results.
      // TODO(crbug.com/485672648): Consider setting resolution details for
      // stale endpoints.
      stale_endpoints_ = results.ConvertToServiceEndpoints(host_.GetPort());
    } else {
      // A stale negative result or a stale result from a different network
      // isn't useful as an intermediate result. Clear the stale info so that
      // `this` isn't considered to be serving stale results while refreshing.
      stale_info_.reset();
    }
    if (!stale_endpoints_.empty()) {
      net_log_.AddEvent(
          NetLogEventType::HOST_RESOLVER_SERVICE_ENDPOINTS_STALE_RESULTS, [&] {
            base::ListValue endpoints;
            for (const auto& endpoint : stale_endpoints_) {
              endpoints.Append(endpoint.ToValue());
            }
            return base::DictValue().Set("endpoints", std::move(endpoints));
          });

      // Notify delegate of stale results asynchronously because notifying
      // delegate may delete `this`.
      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE,
          base::BindOnce(&ServiceEndpointRequestImpl::NotifyDelegateOfUpdated,
                         weak_ptr_factory_.GetWeakPtr()));
    }
    CHECK(!tasks_.empty());
  } else if (results.error() != ERR_DNS_CACHE_MISS ||
             parameters_.source == HostResolverSource::LOCAL_ONLY ||
             tasks_.empty()) {
    if (results.error() == OK) {
      ResolutionDetails details;
      details.source = stale_info_.has_value() ? ResolutionSource::kCache
                                               : ResolutionSource::kLocal;
      resolution_details_ = details;
    }
    SetFinalizedResultFromLegacyResults(results);
    error_info_ = ResolveErrorInfo(results.error());
    return results.error();
  }

  next_state_ = State::kStartJob;
  return OK;
}

int HostResolverManager::ServiceEndpointRequestImpl::DoStartJob() {
  initial_tasks_ = base::ToVector(tasks_);
  manager_->CreateAndStartJobForServiceEndpointRequest(std::move(*job_key_),
                                                       std::move(tasks_), this);
  return ERR_IO_PENDING;
}

void HostResolverManager::ServiceEndpointRequestImpl::OnIOComplete(int rv) {
  if (!resolve_context_) {
    // The ResolveContext was shut down while `this` was waiting for an
    // asynchronous check. Fail the request without accessing the context.
    next_state_ = State::kNone;
    finalized_result_ = FinalizedResult(/*endpoints=*/{}, /*dns_aliases=*/{});
    error_info_ = ResolveErrorInfo(ERR_CONTEXT_SHUT_DOWN);
    rv = ERR_CONTEXT_SHUT_DOWN;
  } else {
    rv = DoLoop(rv);
  }
  if (rv != ERR_IO_PENDING) {
    // The request finished synchronously in DoLoop() (e.g. resolved locally
    // after an asynchronous IPv6 reachability check). Start() has already
    // returned ERR_IO_PENDING, so the delegate needs to be notified of the
    // completion here.
    CHECK(delegate_);
    delegate_->OnServiceEndpointRequestFinished(
        HostResolver::SquashErrorCode(rv));
    // Do not add code below. `this` may be deleted at this point.
  }
}

void HostResolverManager::ServiceEndpointRequestImpl::
    SetFinalizedResultFromLegacyResults(const HostCache::Entry& results) {
  CHECK(!finalized_result_);
  if (results.error() == OK && !parameters_.is_speculative) {
    std::vector<ServiceEndpoint> endpoints =
        results.ConvertToServiceEndpoints(host_.GetPort());
    finalized_result_ =
        FinalizedResult(std::move(endpoints),
                        dns_alias_utility::FixUpDnsAliases(results.aliases()));
  } else {
    finalized_result_ = FinalizedResult(/*endpoints=*/{}, /*dns_aliases=*/{});
  }
}

void HostResolverManager::ServiceEndpointRequestImpl::MaybeClearStaleResults() {
  if (parameters_.cache_usage ==
          ResolveHostParameters::CacheUsage::STALE_ALLOWED_WHILE_REFRESHING &&
      stale_info_.has_value()) {
    stale_endpoints_.clear();
    stale_info_.reset();
  }
}

void HostResolverManager::ServiceEndpointRequestImpl::LogCancelRequest() {
  net_log_.AddEvent(NetLogEventType::CANCELLED);
  net_log_.EndEvent(NetLogEventType::HOST_RESOLVER_MANAGER_REQUEST);
}

void HostResolverManager::ServiceEndpointRequestImpl::
    NotifyDelegateOfUpdated() {
  // This method is called asynchronously via a posted task. `job_` could
  // be completed or cancelled before executing the task.
  if (finalized_result_.has_value()) {
    return;
  }

  CHECK(job_);
  CHECK(delegate_);
  delegate_->OnServiceEndpointsUpdated();
  // Do not add code below. `this` may be deleted at this point.
}

ClientSocketFactory*
HostResolverManager::ServiceEndpointRequestImpl::GetClientSocketFactory() {
  if (resolve_context_->url_request_context()) {
    return resolve_context_->url_request_context()
        ->GetNetworkSessionContext()
        ->client_socket_factory;
  }
  return ClientSocketFactory::GetDefaultFactory();
}

}  // namespace net
