chromium/chrome/browser/ash/net/network_diagnostics/https_latency_routine.cc

// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ash/net/network_diagnostics/https_latency_routine.h"

#include <string>
#include <utility>
#include <vector>

#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/time/default_tick_clock.h"
#include "chrome/browser/ash/net/network_diagnostics/network_diagnostics_util.h"
#include "chrome/browser/profiles/profile.h"
#include "content/public/browser/storage_partition.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "net/base/host_port_pair.h"
#include "net/base/net_errors.h"
#include "services/network/public/cpp/simple_host_resolver.h"
#include "services/network/public/mojom/network_context.mojom.h"

namespace ash::network_diagnostics {

namespace {

namespace mojom = ::chromeos::network_diagnostics::mojom;

constexpr int kTotalHostsToQuery = 3;
// The length of a random eight letter prefix.
constexpr int kHostPrefixLength = 8;
constexpr int kHttpsPort = 443;
constexpr char kHttpsScheme[] = "https://";
constexpr base::TimeDelta kRequestTimeoutMs = base::Milliseconds(5 * 1000);
// Requests taking longer than 1000 ms are problematic.
constexpr base::TimeDelta kProblemLatencyMs = base::Milliseconds(1000);
// Requests lasting between 500 ms and 1000 ms are potentially problematic.
constexpr base::TimeDelta kPotentialProblemLatencyMs = base::Milliseconds(500);

base::TimeDelta MedianLatency(std::vector<base::TimeDelta>& latencies) {
  if (latencies.size() == 0) {
    return base::TimeDelta::Max();
  }
  std::sort(latencies.begin(), latencies.end());
  if (latencies.size() % 2 != 0) {
    return latencies[latencies.size() / 2];
  }
  auto sum =
      latencies[latencies.size() / 2] + latencies[(latencies.size() + 1) / 2];
  return sum / 2.0;
}

network::mojom::NetworkContext* GetNetworkContext() {
  Profile* profile = util::GetUserProfile();

  return profile->GetDefaultStoragePartition()->GetNetworkContext();
}

std::unique_ptr<HttpRequestManager> GetHttpRequestManager() {
  return std::make_unique<HttpRequestManager>(util::GetUserProfile());
}

}  // namespace

HttpsLatencyRoutine::HttpsLatencyRoutine(mojom::RoutineCallSource source)
    : NetworkDiagnosticsRoutine(source),
      network_context_getter_(base::BindRepeating(&GetNetworkContext)),
      http_request_manager_getter_(base::BindRepeating(&GetHttpRequestManager)),
      tick_clock_(base::DefaultTickClock::GetInstance()),
      hostnames_to_query_dns_(
          util::GetRandomHostsWithSchemeAndPortAndGenerate204Path(
              kTotalHostsToQuery,
              kHostPrefixLength,
              kHttpsScheme,
              kHttpsPort)),
      hostnames_to_query_https_(hostnames_to_query_dns_) {
  DCHECK(network_context_getter_);
  DCHECK(http_request_manager_getter_);
  DCHECK(tick_clock_);
}

HttpsLatencyRoutine::~HttpsLatencyRoutine() = default;

mojom::RoutineType HttpsLatencyRoutine::Type() {
  return mojom::RoutineType::kHttpsLatency;
}

void HttpsLatencyRoutine::Run() {
  // Before making HTTPS requests to the hosts, add the IP addresses are added
  // to the DNS cache. This ensures the HTTPS latency does not include DNS
  // resolution time, allowing us to identify issues with HTTPS more precisely.
  AttemptNextResolution();
}

void HttpsLatencyRoutine::AnalyzeResultsAndExecuteCallback() {
  base::TimeDelta median_latency = MedianLatency(latencies_);
  if (!successfully_resolved_hosts_) {
    set_verdict(mojom::RoutineVerdict::kProblem);
    problems_.emplace_back(mojom::HttpsLatencyProblem::kFailedDnsResolutions);
  } else if (failed_connection_) {
    set_verdict(mojom::RoutineVerdict::kProblem);
    problems_.emplace_back(mojom::HttpsLatencyProblem::kFailedHttpsRequests);
  } else {
    auto https_latency_result_value =
        mojom::HttpsLatencyResultValue::New(median_latency);
    set_result_value(mojom::RoutineResultValue::NewHttpsLatencyResultValue(
        std::move(https_latency_result_value)));
    if (median_latency <= kProblemLatencyMs &&
        median_latency > kPotentialProblemLatencyMs) {
      set_verdict(mojom::RoutineVerdict::kProblem);
      problems_.emplace_back(mojom::HttpsLatencyProblem::kHighLatency);
    } else if (median_latency > kProblemLatencyMs) {
      set_verdict(mojom::RoutineVerdict::kProblem);
      problems_.emplace_back(mojom::HttpsLatencyProblem::kVeryHighLatency);
    } else {
      set_verdict(mojom::RoutineVerdict::kNoProblem);
    }
  }
  set_problems(mojom::RoutineProblems::NewHttpsLatencyProblems(problems_));
  ExecuteCallback();
}

void HttpsLatencyRoutine::AttemptNextResolution() {
  network::mojom::NetworkContext* network_context =
      network_context_getter_.Run();
  DCHECK(network_context);

  host_resolver_ = network::SimpleHostResolver::Create(network_context);

  GURL url = hostnames_to_query_dns_.back();
  hostnames_to_query_dns_.pop_back();

  network::mojom::ResolveHostParametersPtr parameters =
      network::mojom::ResolveHostParameters::New();
  parameters->dns_query_type = net::DnsQueryType::A;
  parameters->source = net::HostResolverSource::DNS;
  parameters->cache_usage =
      network::mojom::ResolveHostParameters::CacheUsage::DISALLOWED;

  // TODO(crbug.com/40235854): Consider passing a SchemeHostPort to trigger
  // HTTPS DNS resource record query. Unretained(this) is safe here because the
  // callback is invoked directly by |host_resolver_| which is owned by |this|.
  host_resolver_->ResolveHost(
      network::mojom::HostResolverHost::NewHostPortPair(
          net::HostPortPair::FromURL(url)),
      net::NetworkAnonymizationKey::CreateTransient(), std::move(parameters),
      base::BindOnce(&HttpsLatencyRoutine::OnHostResolutionComplete,
                     base::Unretained(this)));
}

void HttpsLatencyRoutine::OnHostResolutionComplete(
    int result,
    const net::ResolveErrorInfo&,
    const std::optional<net::AddressList>& resolved_addresses,
    const std::optional<net::HostResolverEndpointResults>&) {
  if (result != net::OK) {
    CHECK(!resolved_addresses);
    successfully_resolved_hosts_ = false;
    AnalyzeResultsAndExecuteCallback();
    return;
  }
  if (hostnames_to_query_dns_.size() > 0) {
    AttemptNextResolution();
    return;
  }
  MakeHttpsRequest();
}

void HttpsLatencyRoutine::MakeHttpsRequest() {
  GURL url = hostnames_to_query_https_.back();
  hostnames_to_query_https_.pop_back();
  request_start_time_ = tick_clock_->NowTicks();
  http_request_manager_ = http_request_manager_getter_.Run();
  http_request_manager_->MakeRequest(
      url, kRequestTimeoutMs,
      base::BindOnce(&HttpsLatencyRoutine::OnHttpsRequestComplete, weak_ptr()));
}

void HttpsLatencyRoutine::OnHttpsRequestComplete(bool connected) {
  request_end_time_ = tick_clock_->NowTicks();
  if (!connected) {
    failed_connection_ = true;
    AnalyzeResultsAndExecuteCallback();
    return;
  }
  const base::TimeDelta latency = request_end_time_ - request_start_time_;
  latencies_.emplace_back(latency);
  if (hostnames_to_query_https_.size() > 0) {
    MakeHttpsRequest();
    return;
  }
  AnalyzeResultsAndExecuteCallback();
}

}  // namespace ash::network_diagnostics