| // 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 "components/plus_addresses/plus_address_preallocator.h" |
| |
| #include <algorithm> |
| #include <optional> |
| #include <string> |
| #include <utility> |
| |
| #include "base/check_deref.h" |
| #include "base/feature_list.h" |
| #include "base/json/values_util.h" |
| #include "base/notreached.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/values.h" |
| #include "components/affiliations/core/browser/affiliation_utils.h" |
| #include "components/plus_addresses/features.h" |
| #include "components/plus_addresses/plus_address_allocator.h" |
| #include "components/plus_addresses/plus_address_http_client.h" |
| #include "components/plus_addresses/plus_address_http_client_impl.h" |
| #include "components/plus_addresses/plus_address_prefs.h" |
| #include "components/plus_addresses/plus_address_types.h" |
| #include "components/plus_addresses/settings/plus_address_setting_service.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/prefs/scoped_user_pref_update.h" |
| #include "net/base/backoff_entry.h" |
| #include "net/http/http_status_code.h" |
| |
| namespace plus_addresses { |
| |
| namespace { |
| |
| // The policy that governs retries in case of timeout errors. |
| constexpr auto kPreallocateBackoffPolicy = |
| net::BackoffEntry::Policy{.num_errors_to_ignore = 1, |
| .initial_delay_ms = 1000, |
| .multiply_factor = 2, |
| .jitter_factor = 0.3, |
| .maximum_backoff_ms = -1, |
| .entry_lifetime_ms = -1, |
| .always_use_initial_delay = false}; |
| |
| // Returns whether the end of life of the `preallocated_addresses` has been |
| // reached or the serialized entry does not have valid format. |
| bool IsOutdatedOrInvalid(const base::Value& preallocated_address) { |
| if (!preallocated_address.is_dict()) { |
| return true; |
| } |
| const base::Value::Dict& dict = preallocated_address.GetDict(); |
| if (!dict.FindString(PlusAddressPreallocator::kPlusAddressKey)) { |
| return true; |
| } |
| return base::ValueToTime(dict.Find(PlusAddressPreallocator::kEndOfLifeKey)) |
| .value_or(base::Time()) <= base::Time::Now(); |
| } |
| |
| // Stores `profile` in a `base::Value` that can be written to prefs. |
| base::Value SeralizeAndSetEndOfLife(PreallocatedPlusAddress address) { |
| return base::Value( |
| base::Value::Dict() |
| .Set(PlusAddressPreallocator::kEndOfLifeKey, |
| base::TimeToValue(base::Time::Now() + address.lifetime)) |
| .Set(PlusAddressPreallocator::kPlusAddressKey, |
| std::move(*address.plus_address))); |
| } |
| |
| // Returns the plus address from its `base::Value` representation. Assumes that |
| // `preallocated_address` has a valid format. |
| const std::string& GetPlusAddress(const base::Value& preallocated_address) { |
| return *preallocated_address.GetDict().FindString( |
| PlusAddressPreallocator::kPlusAddressKey); |
| } |
| |
| // Returns whether we should retry the network request after receiving `error`. |
| bool ShouldRetryOnError(const PlusAddressRequestError& error) { |
| return error.IsTimeoutError(); |
| } |
| |
| } // namespace |
| |
| PlusAddressPreallocator::PlusAddressPreallocator( |
| PrefService* pref_service, |
| PlusAddressSettingService* setting_service, |
| PlusAddressHttpClient* http_client, |
| IsEnabledCheck is_enabled_check) |
| : pref_service_(CHECK_DEREF(pref_service)), |
| settings_(CHECK_DEREF(setting_service)), |
| http_client_(CHECK_DEREF(http_client)), |
| is_enabled_check_(std::move(is_enabled_check)), |
| backoff_entry_(&kPreallocateBackoffPolicy) { |
| PrunePreallocatedPlusAddresses(); |
| |
| // If the notice has not been accepted, we do not preemptively pre-allocate. |
| if (settings_->GetHasAcceptedNotice()) { |
| base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce( |
| &PlusAddressPreallocator::MaybeRequestNewPreallocatedPlusAddresses, |
| weak_ptr_factory_.GetWeakPtr(), /*is_user_triggered=*/false), |
| kDelayUntilServerRequestAfterStartup); |
| } |
| } |
| |
| PlusAddressPreallocator::~PlusAddressPreallocator() = default; |
| |
| void PlusAddressPreallocator::AllocatePlusAddress( |
| const url::Origin& origin, |
| AllocationMode mode, |
| PlusAddressRequestCallback callback) { |
| auto facet = affiliations::FacetURI::FromPotentiallyInvalidSpec( |
| origin.GetURL().spec()); |
| if (!facet.is_valid()) { |
| std::move(callback).Run(base::unexpected( |
| PlusAddressRequestError(PlusAddressRequestErrorType::kInvalidOrigin))); |
| return; |
| } |
| requests_.emplace(std::move(callback), std::move(facet), mode); |
| ProcessAllocationRequests(/*is_user_triggered=*/true); |
| } |
| |
| std::optional<PlusProfile> |
| PlusAddressPreallocator::AllocatePlusAddressSynchronously( |
| const url::Origin& origin, |
| AllocationMode mode) { |
| auto facet = affiliations::FacetURI::FromPotentiallyInvalidSpec( |
| origin.GetURL().spec()); |
| if (!facet.is_valid()) { |
| return std::nullopt; |
| } |
| if (!IsEnabled()) { |
| return std::nullopt; |
| } |
| if (!requests_.empty()) { |
| return std::nullopt; |
| } |
| |
| if (std::optional<PlusAddress> address = GetFirstAvailablePlusAddress(mode)) { |
| return std::make_optional<PlusProfile>( |
| /*profile_id=*/std::nullopt, |
| /*facet=*/std::move(facet), |
| /*plus_address=*/std::move(address).value(), |
| /*is_confirmed=*/false); |
| } |
| return std::nullopt; |
| } |
| |
| bool PlusAddressPreallocator::IsRefreshingSupported( |
| const url::Origin& origin) const { |
| return true; |
| } |
| |
| void PlusAddressPreallocator::RemoveAllocatedPlusAddress( |
| const PlusAddress& plus_address) { |
| { |
| ScopedListPrefUpdate update(&pref_service_.get(), |
| prefs::kPreallocatedAddresses); |
| update->EraseIf([&](const base::Value& value) { |
| return *value.GetDict().FindString(kPlusAddressKey) == *plus_address; |
| }); |
| } |
| FixIndexOfNextPreallocatedAddress(); |
| } |
| |
| void PlusAddressPreallocator::FixIndexOfNextPreallocatedAddress() { |
| // If there were deletions, update the index of the next plus address to make |
| // sure it is in bounds (if non-zero). |
| const size_t remaining_plus_addresses = GetPreallocatedAddresses().size(); |
| // If the disk value was corrupted to something negative, fix it rather than |
| // running into bounds check errors later on. |
| const int old_index = std::max(GetIndexOfNextPreallocatedAddress(), 0); |
| pref_service_->SetInteger( |
| prefs::kPreallocatedAddressesNext, |
| remaining_plus_addresses |
| ? static_cast<int>(old_index % remaining_plus_addresses) |
| : 0); |
| } |
| |
| void PlusAddressPreallocator::PrunePreallocatedPlusAddresses() { |
| { |
| ScopedListPrefUpdate update(&pref_service_.get(), |
| prefs::kPreallocatedAddresses); |
| update->EraseIf(&IsOutdatedOrInvalid); |
| } |
| FixIndexOfNextPreallocatedAddress(); |
| } |
| |
| void PlusAddressPreallocator::MaybeRequestNewPreallocatedPlusAddresses( |
| bool is_user_triggered) { |
| if (!IsEnabled()) { |
| return; |
| } |
| |
| if (is_server_request_ongoing_) { |
| return; |
| } |
| |
| if (server_request_timer_.IsRunning() && !is_user_triggered) { |
| return; |
| } |
| |
| if (static_cast<int>(GetPreallocatedAddresses().size()) >= |
| features::kPlusAddressPreallocationMinimumSize.Get()) { |
| return; |
| } |
| |
| SendRequestWithDelay(is_user_triggered |
| ? base::TimeDelta() |
| : backoff_entry_.GetTimeUntilRelease()); |
| } |
| |
| bool PlusAddressPreallocator::IsEnabled() const { |
| if (!settings_->GetIsPlusAddressesEnabled()) { |
| return false; |
| } |
| |
| return is_enabled_check_.Run(); |
| } |
| |
| void PlusAddressPreallocator::SendRequestWithDelay(base::TimeDelta delay) { |
| server_request_timer_.Start( |
| FROM_HERE, delay, |
| base::BindOnce(&PlusAddressPreallocator::SendRequest, |
| // Safe because the timer is owned by `this`. |
| base::Unretained(this))); |
| } |
| |
| void PlusAddressPreallocator::SendRequest() { |
| is_server_request_ongoing_ = true; |
| http_client_->PreallocatePlusAddresses(base::BindOnce( |
| &PlusAddressPreallocator::OnReceivePreallocatedPlusAddresses, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void PlusAddressPreallocator::OnReceivePreallocatedPlusAddresses( |
| PlusAddressHttpClient::PreallocatePlusAddressesResult result) { |
| is_server_request_ongoing_ = false; |
| if (!result.has_value()) { |
| if (ShouldRetryOnError(result.error())) { |
| backoff_entry_.InformOfRequest(/*succeeded=*/false); |
| SendRequestWithDelay(backoff_entry_.GetTimeUntilRelease()); |
| } |
| ReplyToRequestsWithError(result.error()); |
| return; |
| } |
| |
| backoff_entry_.InformOfRequest(/*succeeded=*/true); |
| ScopedListPrefUpdate update(&pref_service_.get(), |
| prefs::kPreallocatedAddresses); |
| for (PreallocatedPlusAddress& address : result.value()) { |
| update->Append(SeralizeAndSetEndOfLife(std::move(address))); |
| } |
| |
| ProcessAllocationRequests(/*is_user_triggered=*/false); |
| } |
| |
| void PlusAddressPreallocator::ProcessAllocationRequests( |
| bool is_user_triggered) { |
| if (!IsEnabled()) { |
| ReplyToRequestsWithError( |
| PlusAddressRequestError(PlusAddressRequestErrorType::kUserSignedOut)); |
| return; |
| } |
| |
| while (!requests_.empty()) { |
| std::optional<PlusAddress> next_address = |
| GetFirstAvailablePlusAddress(requests_.front().allocation_mode); |
| if (!next_address) { |
| break; |
| } |
| Request request = std::move(requests_.front()); |
| requests_.pop(); |
| std::move(request.callback) |
| .Run(PlusProfile(/*profile_id=*/std::nullopt, |
| /*facet=*/std::move(request.facet), |
| /*plus_address=*/std::move(next_address).value(), |
| /*is_confirmed=*/false)); |
| } |
| // We may have dipped below the minimum size of the pre-allocated plus address |
| // pool that we want to keep around. If so, request new ones. |
| MaybeRequestNewPreallocatedPlusAddresses(is_user_triggered); |
| } |
| |
| void PlusAddressPreallocator::ReplyToRequestsWithError( |
| const PlusAddressRequestError& error) { |
| while (!requests_.empty()) { |
| Request request = std::move(requests_.front()); |
| requests_.pop(); |
| std::move(request.callback).Run(base::unexpected(error)); |
| } |
| } |
| |
| std::optional<PlusAddress> |
| PlusAddressPreallocator::GetFirstAvailablePlusAddress(AllocationMode mode) { |
| PrunePreallocatedPlusAddresses(); |
| const base::Value::List& preallocated_addresses = GetPreallocatedAddresses(); |
| const int preallocated_addresses_size = |
| static_cast<int>(preallocated_addresses.size()); |
| int index = GetIndexOfNextPreallocatedAddress(); |
| if (index >= preallocated_addresses_size) { |
| return std::nullopt; |
| } |
| |
| if (mode == AllocationMode::kNewPlusAddress) { |
| index = (index + 1) % preallocated_addresses_size; |
| } |
| pref_service_->SetInteger(prefs::kPreallocatedAddressesNext, index); |
| return std::make_optional<PlusAddress>( |
| GetPlusAddress(preallocated_addresses[index])); |
| } |
| |
| const base::Value::List& PlusAddressPreallocator::GetPreallocatedAddresses() |
| const { |
| return pref_service_->GetList(prefs::kPreallocatedAddresses); |
| } |
| |
| int PlusAddressPreallocator::GetIndexOfNextPreallocatedAddress() const { |
| return pref_service_->GetInteger(prefs::kPreallocatedAddressesNext); |
| } |
| |
| PlusAddressPreallocator::Request::Request(PlusAddressRequestCallback callback, |
| affiliations::FacetURI facet, |
| AllocationMode allocation_mode) |
| : callback(std::move(callback)), |
| facet(std::move(facet)), |
| allocation_mode(allocation_mode) {} |
| |
| PlusAddressPreallocator::Request::Request(Request&&) = default; |
| |
| PlusAddressPreallocator::Request& PlusAddressPreallocator::Request::operator=( |
| Request&&) = default; |
| |
| PlusAddressPreallocator::Request::~Request() = default; |
| |
| } // namespace plus_addresses |