blob: 95d28977348e915bfd90f3b95ff7ac6ad9741c8f [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/plus_addresses/plus_address_http_client_impl.h"
#include <concepts>
#include <memory>
#include <optional>
#include <string>
#include <type_traits>
#include <utility>
#include "base/check_deref.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/json/json_writer.h"
#include "base/sequence_checker.h"
#include "base/strings/strcat.h"
#include "base/task/sequenced_task_runner.h"
#include "base/types/expected.h"
#include "components/plus_addresses/features.h"
#include "components/plus_addresses/metrics/plus_address_metrics.h"
#include "components/plus_addresses/plus_address_parsing_utils.h"
#include "components/plus_addresses/plus_address_types.h"
#include "components/signin/public/base/consent_level.h"
#include "components/signin/public/identity_manager/access_token_info.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "components/signin/public/identity_manager/primary_account_access_token_fetcher.h"
#include "components/signin/public/identity_manager/scope_set.h"
#include "google_apis/gaia/google_service_auth_error.h"
#include "net/base/net_errors.h"
#include "net/http/http_request_headers.h"
#include "net/http/http_status_code.h"
#include "services/data_decoder/public/cpp/data_decoder.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "url/gurl.h"
#include "url/origin.h"
namespace plus_addresses {
namespace {
constexpr auto kSignoutError =
PlusAddressRequestError(PlusAddressRequestErrorType::kUserSignedOut);
// See docs/network_traffic_annotations.md for reference.
// TODO(b/295556954): Update the description and trigger fields when possible.
// Also replace the policy_exception when we have a policy.
constexpr net::NetworkTrafficAnnotationTag kReservePlusAddressAnnotation =
net::DefineNetworkTrafficAnnotation("plus_address_reservation", R"(
semantics {
sender: "Chrome Plus Address Client"
description: "A plus address is reserved for the user on the "
"enterprise-specified server with this request."
trigger: "User enters the create plus address UX flow."
internal {
contacts {
email: "dc-komics@google.com"
}
}
user_data {
type: ACCESS_TOKEN,
type: SENSITIVE_URL
}
data: "The origin that the user may use a plus address on is sent."
destination: GOOGLE_OWNED_SERVICE
last_reviewed: "2023-09-23"
}
policy {
cookies_allowed: NO
setting: "Disable the Plus Addresses feature."
policy_exception_justification: "We don't have an opt-out policy yet"
" as Plus Addresses hasn't launched."
}
)");
// TODO(b/277532955): Update the description and trigger fields when possible.
// Also replace the policy_exception when we have a policy.
constexpr net::NetworkTrafficAnnotationTag kConfirmPlusAddressAnnotation =
net::DefineNetworkTrafficAnnotation("plus_address_confirmation", R"(
semantics {
sender: "Chrome Plus Address Client"
description: "A plus address is confirmed for creation on the "
"enterprise-specified server with this request."
trigger: "User confirms to create the displayed plus address."
internal {
contacts {
email: "dc-komics@google.com"
}
}
user_data {
type: ACCESS_TOKEN,
type: SENSITIVE_URL,
type: USERNAME
}
data: "The plus address and the origin that the user is using it on "
"are both sent."
destination: GOOGLE_OWNED_SERVICE
last_reviewed: "2023-09-23"
}
policy {
cookies_allowed: NO
setting: "Disable the Plus Addresses feature."
policy_exception_justification: "We don't have an opt-out policy yet"
" as Plus Addresses hasn't launched."
}
)");
constexpr net::NetworkTrafficAnnotationTag kPreallocatePlusAddressesAnnotation =
net::DefineNetworkTrafficAnnotation("plus_address_preallocation", R"(
semantics {
sender: "Chrome Plus Address Client"
description: "Pre-allocated Plus Addresses are requested. These may"
"later be used to confirm the creation of a Plus Address."
trigger: "There are three scenarios:"
"1) The user sees the notice screen for the first time."
"2) The pre-allocated Plus Addresses on the device are"
" exhausted because they were confirmed and thus activated"
" for use."
"3) The pre-allocated Plus Address on the device expired."
internal {
contacts {
email: "dc-komics@google.com"
}
}
user_data {
type: ACCESS_TOKEN,
}
data: "n/a"
destination: GOOGLE_OWNED_SERVICE
last_reviewed: "2024-08-08"
}
policy {
cookies_allowed: NO
setting: "No setting."
policy_exception_justification: "We don't have an opt-out policy yet"
" as Plus Addresses hasn't launched."
}
)");
std::optional<GURL> ValidateAndGetUrl() {
GURL maybe_url = GURL(features::kEnterprisePlusAddressServerUrl.Get());
return maybe_url.is_valid() ? std::make_optional(maybe_url) : std::nullopt;
}
// Returns the HTTP response code for the response in the `loader`. If there is
// none, it returns `std::nullopt`.
std::optional<int> GetResponseCode(network::SimpleURLLoader* loader) {
return loader && loader->ResponseInfo() && loader->ResponseInfo()->headers
? std::optional<int>(
loader->ResponseInfo()->headers->response_code())
: std::optional<int>();
}
// Helper wrapper around a `base::OnceCallback` that runs the `callback` with
// argument `arg_on_destroy` if the callback is destroyed and has not been run
// at that point in time.
template <typename T>
class RunOnDestroyHelper final {
public:
using ArgType = std::remove_cvref_t<T>;
RunOnDestroyHelper(base::OnceCallback<void(T)> callback,
ArgType arg_on_destroy)
: callback_(std::move(callback)),
arg_on_destroy_(std::move(arg_on_destroy)) {}
RunOnDestroyHelper(const RunOnDestroyHelper&) = delete;
RunOnDestroyHelper(RunOnDestroyHelper&&) = default;
RunOnDestroyHelper& operator=(const RunOnDestroyHelper&) = delete;
RunOnDestroyHelper& operator=(RunOnDestroyHelper&&) = default;
~RunOnDestroyHelper() {
if (callback_) {
std::move(callback_).Run(arg_on_destroy_);
}
}
void Run(const T& arg) && {
std::move(callback_).Run(arg);
callback_.Reset();
}
private:
base::OnceCallback<void(T)> callback_;
ArgType arg_on_destroy_;
};
// Given a `base::OnceCallback<void(T)>` `callback`, it returns another
// `base::OnceCallback` of the same signature with the property that the
// returned callback is run with argument `arg_on_destroy` on its destruction if
// it has not been run before.
template <typename T, typename V>
requires(std::constructible_from<T, V>)
base::OnceCallback<void(T)> WrapAsAutorunCallback(
base::OnceCallback<void(T)> callback,
V arg_on_destroy) {
return base::BindOnce(
[](RunOnDestroyHelper<T> helper, T profile) {
std::move(helper).Run(profile);
},
RunOnDestroyHelper<T>(std::move(callback), std::move(arg_on_destroy)));
}
} // namespace
PlusAddressHttpClientImpl::PlusAddressHttpClientImpl(
signin::IdentityManager* identity_manager,
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)
: identity_manager_(CHECK_DEREF(identity_manager)),
url_loader_factory_(std::move(url_loader_factory)),
server_url_(ValidateAndGetUrl()),
scopes_({features::kEnterprisePlusAddressOAuthScope.Get()}) {}
PlusAddressHttpClientImpl::~PlusAddressHttpClientImpl() = default;
void PlusAddressHttpClientImpl::ReservePlusAddress(
const url::Origin& origin,
bool refresh,
PlusAddressRequestCallback on_completed) {
if (!server_url_) {
return;
}
GetAuthToken(
base::BindOnce(&PlusAddressHttpClientImpl::ReservePlusAddressInternal,
base::Unretained(this), origin, refresh,
WrapAsAutorunCallback(std::move(on_completed),
base::unexpected(kSignoutError))));
}
void PlusAddressHttpClientImpl::ConfirmPlusAddress(
const url::Origin& origin,
const PlusAddress& plus_address,
PlusAddressRequestCallback on_completed) {
if (!server_url_) {
return;
}
GetAuthToken(
base::BindOnce(&PlusAddressHttpClientImpl::ConfirmPlusAddressInternal,
base::Unretained(this), origin, plus_address,
WrapAsAutorunCallback(std::move(on_completed),
base::unexpected(kSignoutError))));
}
void PlusAddressHttpClientImpl::PreallocatePlusAddresses(
PreallocatePlusAddressesCallback callback) {
if (!server_url_) {
return;
}
GetAuthToken(base::BindOnce(
&PlusAddressHttpClientImpl::PreallocatePlusAddressesInternal,
base::Unretained(this),
WrapAsAutorunCallback<PreallocatePlusAddressesResult>(
std::move(callback), base::unexpected(kSignoutError))));
}
void PlusAddressHttpClientImpl::Reset() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
access_token_fetcher_.reset();
pending_callbacks_ = {};
loaders_.clear();
}
void PlusAddressHttpClientImpl::ReservePlusAddressInternal(
const url::Origin& origin,
bool refresh,
PlusAddressRequestCallback on_completed,
std::optional<std::string> auth_token) {
if (!auth_token.has_value()) {
std::move(on_completed)
.Run(base::unexpected(
PlusAddressRequestError(PlusAddressRequestErrorType::kOAuthError)));
return;
}
std::unique_ptr<network::ResourceRequest> resource_request =
CreateRequest(kServerReservePlusAddressEndpoint,
net::HttpRequestHeaders::kPutMethod, *auth_token);
base::Value::Dict payload;
payload.Set("facet", origin.Serialize());
payload.Set("refresh_email_address", refresh);
std::string request_body;
const bool wrote_payload = base::JSONWriter::Write(payload, &request_body);
DCHECK(wrote_payload);
std::unique_ptr<network::SimpleURLLoader> loader =
network::SimpleURLLoader::Create(std::move(resource_request),
kReservePlusAddressAnnotation);
network::SimpleURLLoader* loader_ptr = loader.get();
loader_ptr->AttachStringForUpload(request_body, "application/json");
loader_ptr->SetTimeoutDuration(features::kPlusAddressRequestTimeout.Get());
// TODO(b/301984623) - Measure average downloadsize and change this.
loader_ptr->DownloadToString(
url_loader_factory_.get(),
base::BindOnce(
&PlusAddressHttpClientImpl::OnReserveOrConfirmPlusAddressComplete,
// Safe since this class owns the list of loaders.
base::Unretained(this),
loaders_.insert(loaders_.begin(), std::move(loader)),
PlusAddressNetworkRequestType::kReserve, base::TimeTicks::Now(),
std::move(on_completed)),
network::SimpleURLLoader::kMaxBoundedStringDownloadSize);
}
void PlusAddressHttpClientImpl::ConfirmPlusAddressInternal(
const url::Origin& origin,
const PlusAddress& plus_address,
PlusAddressRequestCallback on_completed,
std::optional<std::string> auth_token) {
if (!auth_token.has_value()) {
std::move(on_completed)
.Run(base::unexpected(
PlusAddressRequestError(PlusAddressRequestErrorType::kOAuthError)));
return;
}
std::unique_ptr<network::ResourceRequest> resource_request =
CreateRequest(kServerCreatePlusAddressEndpoint,
net::HttpRequestHeaders::kPutMethod, *auth_token);
base::Value::Dict payload;
payload.Set("facet", origin.Serialize());
payload.Set("reserved_email_address", *plus_address);
std::string request_body;
const bool wrote_payload = base::JSONWriter::Write(payload, &request_body);
DCHECK(wrote_payload);
std::unique_ptr<network::SimpleURLLoader> loader =
network::SimpleURLLoader::Create(std::move(resource_request),
kConfirmPlusAddressAnnotation);
network::SimpleURLLoader* loader_ptr = loader.get();
loader_ptr->AttachStringForUpload(request_body, "application/json");
loader_ptr->SetTimeoutDuration(features::kPlusAddressRequestTimeout.Get());
// TODO(b/301984623) - Measure average downloadsize and change this.
loader_ptr->DownloadToString(
url_loader_factory_.get(),
base::BindOnce(
&PlusAddressHttpClientImpl::OnReserveOrConfirmPlusAddressComplete,
// Safe since this class owns the list of loaders.
base::Unretained(this),
loaders_.insert(loaders_.begin(), std::move(loader)),
PlusAddressNetworkRequestType::kCreate, base::TimeTicks::Now(),
std::move(on_completed)),
network::SimpleURLLoader::kMaxBoundedStringDownloadSize);
}
void PlusAddressHttpClientImpl::PreallocatePlusAddressesInternal(
PreallocatePlusAddressesCallback callback,
std::optional<std::string> auth_token) {
if (!auth_token.has_value()) {
std::move(callback).Run(base::unexpected(
PlusAddressRequestError(PlusAddressRequestErrorType::kOAuthError)));
return;
}
std::unique_ptr<network::ResourceRequest> resource_request =
CreateRequest(kServerPreallocatePlusAddressEndpoint,
net::HttpRequestHeaders::kPostMethod, *auth_token);
std::unique_ptr<network::SimpleURLLoader> loader =
network::SimpleURLLoader::Create(std::move(resource_request),
kPreallocatePlusAddressesAnnotation);
network::SimpleURLLoader* loader_ptr = loader.get();
loader_ptr->SetTimeoutDuration(features::kPlusAddressRequestTimeout.Get());
loader_ptr->DownloadToString(
url_loader_factory_.get(),
base::BindOnce(&PlusAddressHttpClientImpl::OnPreallocationComplete,
// Safe since this class owns the list of loaders.
base::Unretained(this),
loaders_.insert(loaders_.begin(), std::move(loader)),
base::TimeTicks::Now(), std::move(callback)),
network::SimpleURLLoader::kMaxBoundedStringDownloadSize);
}
base::expected<void, PlusAddressRequestError>
PlusAddressHttpClientImpl::ProcessNetworkResponse(
UrlLoaderList::iterator it,
PlusAddressNetworkRequestType type,
base::TimeTicks request_start,
base::optional_ref<const std::string> response) {
std::unique_ptr<network::SimpleURLLoader> loader = std::move(*it);
loaders_.erase(it);
const std::optional<int> net_error =
loader ? loader->NetError() : std::optional<int>();
const std::optional<int> response_code = GetResponseCode(loader.get());
metrics::RecordNetworkRequestLatency(type,
base::TimeTicks::Now() - request_start);
if (net_error) {
metrics::RecordNetErrorCode(type, *net_error);
}
if (response_code) {
metrics::RecordNetworkRequestResponseCode(type, *response_code);
}
if (net_error == net::ERR_TIMED_OUT) {
return base::unexpected(
PlusAddressRequestError(PlusAddressRequestErrorType::kClientTimeout));
}
if (!response) {
return base::unexpected(
PlusAddressRequestError::AsNetworkError(response_code));
}
metrics::RecordNetworkRequestResponseSize(type, response->size());
return base::ok();
}
void PlusAddressHttpClientImpl::OnReserveOrConfirmPlusAddressComplete(
UrlLoaderList::iterator it,
PlusAddressNetworkRequestType type,
base::TimeTicks request_start,
PlusAddressRequestCallback on_completed,
std::unique_ptr<std::string> response) {
if (base::expected<void, PlusAddressRequestError> result =
ProcessNetworkResponse(it, type, request_start, response.get());
!result.has_value()) {
std::move(on_completed).Run(base::unexpected(result.error()));
return;
}
// Parse the response & return it via callback.
data_decoder::DataDecoder::ParseJsonIsolated(
*response,
base::BindOnce(&ParsePlusProfileFromV1Create)
.Then(base::BindOnce(
[](PlusAddressRequestCallback callback,
std::optional<PlusProfile> result) {
if (!result.has_value()) {
std::move(callback).Run(
base::unexpected(PlusAddressRequestError(
PlusAddressRequestErrorType::kParsingError)));
return;
}
std::move(callback).Run(result.value());
},
std::move(on_completed))));
}
void PlusAddressHttpClientImpl::OnPreallocationComplete(
UrlLoaderList::iterator it,
base::TimeTicks request_start,
PreallocatePlusAddressesCallback on_completed,
std::unique_ptr<std::string> response) {
if (base::expected<void, PlusAddressRequestError> result =
ProcessNetworkResponse(it,
PlusAddressNetworkRequestType::kPreallocate,
request_start, response.get());
!result.has_value()) {
std::move(on_completed).Run(base::unexpected(result.error()));
return;
}
data_decoder::DataDecoder::ParseJsonIsolated(
*response,
base::BindOnce(&ParsePreallocatedPlusAddresses)
.Then(base::BindOnce(
[](PreallocatePlusAddressesCallback callback,
std::optional<std::vector<PreallocatedPlusAddress>> result) {
if (!result) {
std::move(callback).Run(
base::unexpected(PlusAddressRequestError(
PlusAddressRequestErrorType::kParsingError)));
return;
}
std::move(callback).Run(*std::move(result));
},
std::move(on_completed))));
}
void PlusAddressHttpClientImpl::GetAuthToken(TokenReadyCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (access_token_fetcher_) {
pending_callbacks_.emplace(std::move(callback));
return;
}
access_token_fetcher_ =
std::make_unique<signin::PrimaryAccountAccessTokenFetcher>(
/*consumer_name=*/"PlusAddressHttpClientImpl",
&identity_manager_.get(), scopes_,
base::BindOnce(&PlusAddressHttpClientImpl::OnTokenFetched,
// It is safe to use base::Unretained as
// `this` owns `access_token_fetcher_`.
base::Unretained(this), std::move(callback)),
// Use WaitUntilAvailable to defer getting an OAuth token until
// the user is signed in. We can switch to kImmediate once we
// have a sign in observer that guarantees we're already signed in
// by this point.
signin::PrimaryAccountAccessTokenFetcher::Mode::kWaitUntilAvailable,
// Sync doesn't need to be enabled for us to use PlusAddresses.
signin::ConsentLevel::kSignin);
}
void PlusAddressHttpClientImpl::OnTokenFetched(
TokenReadyCallback callback,
GoogleServiceAuthError error,
signin::AccessTokenInfo access_token_info) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
access_token_fetcher_.reset();
metrics::RecordNetworkRequestOauthError(error);
std::optional<std::string> access_token;
if (error.state() == GoogleServiceAuthError::NONE) {
access_token = access_token_info.token;
}
std::move(callback).Run(access_token);
while (!pending_callbacks_.empty()) {
std::move(pending_callbacks_.front()).Run(access_token);
pending_callbacks_.pop();
}
}
std::unique_ptr<network::ResourceRequest>
PlusAddressHttpClientImpl::CreateRequest(std::string_view endpoint,
std::string_view method,
std::string_view auth_token) const {
auto request = std::make_unique<network::ResourceRequest>();
request->method = std::string(method);
request->url = server_url_.value().Resolve(endpoint);
request->headers.SetHeader(net::HttpRequestHeaders::kAuthorization,
base::StrCat({"Bearer ", auth_token}));
request->credentials_mode = network::mojom::CredentialsMode::kOmit;
return request;
}
} // namespace plus_addresses