// 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