chromium/chrome/browser/apps/almanac_api_client/almanac_api_util.cc

// Copyright 2022 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/apps/almanac_api_client/almanac_api_util.h"

#include <memory>
#include <optional>

#include "ash/constants/ash_switches.h"
#include "base/command_line.h"
#include "base/metrics/histogram_functions.h"
#include "base/no_destructor.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "google_apis/common/api_key_request_util.h"
#include "google_apis/google_api_keys.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "third_party/abseil-cpp/absl/status/status.h"
#include "url/gurl.h"

namespace apps {

namespace {

// Returns a resource request for the specified endpoint for the ChromeOS
// Almanac API.
std::unique_ptr<network::ResourceRequest> GetAlmanacResourceRequest(
    std::string_view endpoint_suffix) {
  auto resource_request = std::make_unique<network::ResourceRequest>();
  resource_request->url = GetAlmanacEndpointUrl(endpoint_suffix);
  CHECK(resource_request->url.is_valid());

  // A POST request is sent with an override to GET due to server requirements.
  resource_request->method = "POST";
  resource_request->headers.SetHeader("X-HTTP-Method-Override", "GET");
  google_apis::AddAPIKeyToRequest(*resource_request, google_apis::GetAPIKey());
  resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
  return resource_request;
}

std::optional<std::string>& GetAlmanacEndpointUrlOverride() {
  static base::NoDestructor<std::optional<std::string>> url_override;
  return *url_override;
}

std::unique_ptr<network::SimpleURLLoader> GetAlmanacUrlLoader(
    const net::NetworkTrafficAnnotationTag& traffic_annotation,
    const std::string& response_body,
    std::string_view endpoint_suffix) {
  std::unique_ptr<network::SimpleURLLoader> loader =
      network::SimpleURLLoader::Create(
          GetAlmanacResourceRequest(endpoint_suffix), traffic_annotation);
  loader->AttachStringForUpload(response_body, "application/x-protobuf");
  // Retry requests twice (so, three requests total) if requests fail due to
  // network issues.
  constexpr int kMaxRetries = 2;
  loader->SetRetryOptions(
      kMaxRetries, network::SimpleURLLoader::RETRY_ON_NETWORK_CHANGE |
                       network::SimpleURLLoader::RETRY_ON_NAME_NOT_RESOLVED);
  return loader;
}

base::expected<std::string, QueryError> ValidateDownloadedString(
    std::unique_ptr<network::SimpleURLLoader> loader,
    std::optional<std::string> error_histogram_name,
    std::unique_ptr<std::string> response_body) {
  int response_code = 0;
  if (loader->ResponseInfo() && loader->ResponseInfo()->headers) {
    response_code = loader->ResponseInfo()->headers->response_code();
  }

  if (error_histogram_name.has_value()) {
    // If there is no response code, there was a net error.
    base::UmaHistogramSparse(
        error_histogram_name.value(),
        response_code > 0 ? response_code : loader->NetError());
  }

  if (loader->NetError() != net::OK &&
      loader->NetError() != net::ERR_HTTP_RESPONSE_CODE_FAILURE) {
    return base::unexpected(QueryError{
        QueryError::kConnectionError,
        base::StrCat({"net error: ", net::ErrorToString(loader->NetError())})});
  }

  if ((response_code >= 200 && response_code < 300) || response_code == 0) {
    if (!response_body) {
      return base::unexpected(
          QueryError{QueryError::kBadResponse, "request body is nullptr"});
    }
    return std::move(*response_body);
  }

  if (response_code >= 400 && response_code < 500) {
    return base::unexpected(
        QueryError{QueryError::kBadRequest,
                   base::StrCat({"HTTP error code: ",
                                 base::NumberToString(response_code)})});
  }

  return base::unexpected(
      QueryError{QueryError::kConnectionError,
                 base::StrCat({"HTTP error code: ",
                               base::NumberToString(response_code)})});
}

}  // namespace

namespace internal {

void QueryAlmanacApiRaw(
    network::mojom::URLLoaderFactory& url_loader_factory,
    const net::NetworkTrafficAnnotationTag& traffic_annotation,
    const std::string& request_body,
    std::string_view endpoint_suffix,
    int max_response_size,
    std::optional<std::string> error_histogram_name,
    base::OnceCallback<void(base::expected<std::string, QueryError>)>
        callback) {
  std::unique_ptr<network::SimpleURLLoader> loader = apps::GetAlmanacUrlLoader(
      traffic_annotation, request_body, endpoint_suffix);

  // Retain a pointer while keeping the loader alive by std::moving it into the
  // callback.
  auto* loader_ptr = loader.get();
  loader_ptr->DownloadToString(
      &url_loader_factory,
      base::BindOnce(&ValidateDownloadedString, std::move(loader),
                     std::move(error_histogram_name))
          .Then(std::move(callback)),
      max_response_size);
}

}  // namespace internal

std::string GetAlmanacApiUrl() {
  auto* command_line = base::CommandLine::ForCurrentProcess();
  if (command_line->HasSwitch(ash::switches::kAlmanacApiUrl)) {
    return command_line->GetSwitchValueASCII(ash::switches::kAlmanacApiUrl);
  }

  return GetAlmanacEndpointUrlOverride().value_or(
      "https://chromeosalmanac-pa.googleapis.com/");
}

GURL GetAlmanacEndpointUrl(std::string_view endpoint_suffix) {
  return GURL(base::StrCat({GetAlmanacApiUrl(), endpoint_suffix}));
}

void SetAlmanacEndpointUrlForTesting(std::optional<std::string> url_override) {
  GetAlmanacEndpointUrlOverride() = std::move(url_override);
}

bool QueryError::operator==(const QueryError& other) const {
  return type == other.type && message == other.message;
}

std::ostream& operator<<(std::ostream& out, const QueryError& error) {
  switch (error.type) {
    case QueryError::kConnectionError:
      out << "Connection error: ";
      break;
    case QueryError::kBadRequest:
      out << "Bad request: ";
      break;
    case QueryError::kBadResponse:
      out << "Bad response: ";
      break;
  }
  out << error.message;
  return out;
}

}  // namespace apps