[Protected Audiences] Key value server V2 dispatcher, bidder signals.
This is an initial implementation of the new trusted signals network request dispatcher logic for the The Protected Audiences trusted bidding signals API. The new API uses encrypted blobs in the request and response bodies, so HTTP caching semantics do not apply. As a result, if we want to cache response bodies, we need storage in the browser process of the responses. To do that, we're going to move the logic to send and received network responses into the browser process, as well as the cache. This CL introduces a TrustedSignalsCacheImpl class that receives trusted bidding signals requests from a browser-side caller, uses a helper class in the browser process (not yet implemented) to send the requests over the network and parse the responses, and then provides the responses to short-lived per-origin Javascript processes via a Mojo pipe. Despite the "cache" in the class name, this CL does not implement caching. Instead, requests/responses are kept around (and reused) as long as there's a live consumer of the response, and immediately thrown away afterwards. Response TTLs are not yet respected - will add basic support for that in a smaller followup CL. Bug: 333445540 Change-Id: Ia6979b9c09e0597472a5339cf56cbbb1452d8f43 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5530764 Reviewed-by: Maks Orlovich <morlovich@chromium.org> Reviewed-by: Ken Buchanan <kenrb@chromium.org> Commit-Queue: mmenke <mmenke@chromium.org> Cr-Commit-Position: refs/heads/main@{#1320380}
This commit is contained in:

committed by
Chromium LUCI CQ

parent
c22ad06a25
commit
be4cde7095
content
@ -1192,6 +1192,10 @@ source_set("browser") {
|
||||
"interest_group/subresource_url_authorizations.h",
|
||||
"interest_group/subresource_url_builder.cc",
|
||||
"interest_group/subresource_url_builder.h",
|
||||
"interest_group/trusted_signals_cache_impl.cc",
|
||||
"interest_group/trusted_signals_cache_impl.h",
|
||||
"interest_group/trusted_signals_fetcher.cc",
|
||||
"interest_group/trusted_signals_fetcher.h",
|
||||
"isolated_origin_util.cc",
|
||||
"isolated_origin_util.h",
|
||||
"isolation_context.cc",
|
||||
|
696
content/browser/interest_group/trusted_signals_cache_impl.cc
Normal file
696
content/browser/interest_group/trusted_signals_cache_impl.cc
Normal file
@ -0,0 +1,696 @@
|
||||
// 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 "content/browser/interest_group/trusted_signals_cache_impl.h"
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "base/check.h"
|
||||
#include "base/functional/bind.h"
|
||||
#include "base/functional/callback.h"
|
||||
#include "base/memory/raw_ptr.h"
|
||||
#include "base/memory/ref_counted.h"
|
||||
#include "base/memory/scoped_refptr.h"
|
||||
#include "base/timer/timer.h"
|
||||
#include "base/types/expected.h"
|
||||
#include "base/unguessable_token.h"
|
||||
#include "content/browser/interest_group/trusted_signals_fetcher.h"
|
||||
#include "content/services/auction_worklet/public/mojom/trusted_signals_cache.mojom.h"
|
||||
#include "mojo/public/cpp/bindings/pending_receiver.h"
|
||||
#include "mojo/public/cpp/bindings/pending_remote.h"
|
||||
#include "mojo/public/cpp/bindings/receiver_set.h"
|
||||
#include "mojo/public/cpp/bindings/remote.h"
|
||||
#include "services/network/public/cpp/shared_url_loader_factory.h"
|
||||
#include "services/network/public/cpp/simple_url_loader.h"
|
||||
#include "url/gurl.h"
|
||||
#include "url/origin.h"
|
||||
|
||||
namespace content {
|
||||
|
||||
namespace {
|
||||
|
||||
// The data stored in each CompressionGroupData.
|
||||
using CachedResult =
|
||||
base::expected<TrustedSignalsFetcher::CompressionGroupResult,
|
||||
TrustedSignalsFetcher::ErrorInfo>;
|
||||
|
||||
// Bind `pending_client` and then send result` to it.
|
||||
void SendResultToClient(
|
||||
mojo::PendingRemote<auction_worklet::mojom::TrustedSignalsCacheClient>
|
||||
pending_client,
|
||||
const CachedResult& result) {
|
||||
mojo::Remote<auction_worklet::mojom::TrustedSignalsCacheClient> client(
|
||||
std::move(pending_client));
|
||||
if (!client.is_connected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.has_value()) {
|
||||
client->OnSuccess(result.value().compression_scheme,
|
||||
result.value().compression_group_data);
|
||||
} else {
|
||||
client->OnError(result.error().error_msg);
|
||||
}
|
||||
}
|
||||
|
||||
// Sends an error to `pending_client` in the case there's no live cache entry.
|
||||
// Used both when an unrecognized signals request ID is received, and when the
|
||||
// last Handle to an entry is destroyed, and there are pending requests to it.
|
||||
void SendNoLiveEntryErrorToClient(
|
||||
mojo::PendingRemote<auction_worklet::mojom::TrustedSignalsCacheClient>
|
||||
pending_client) {
|
||||
SendResultToClient(
|
||||
std::move(pending_client),
|
||||
base::unexpected(TrustedSignalsFetcher::ErrorInfo{"Request cancelled"}));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TrustedSignalsCacheImpl::Handle::Handle() = default;
|
||||
|
||||
TrustedSignalsCacheImpl::Handle::~Handle() = default;
|
||||
|
||||
TrustedSignalsCacheImpl::FetchKey::FetchKey() = default;
|
||||
|
||||
TrustedSignalsCacheImpl::FetchKey::FetchKey(
|
||||
const url::Origin& main_frame_origin,
|
||||
SignalsType signals_type,
|
||||
const url::Origin& owner,
|
||||
const GURL& trusted_signals_url)
|
||||
: owner(owner),
|
||||
signals_type(signals_type),
|
||||
main_frame_origin(main_frame_origin),
|
||||
trusted_signals_url(trusted_signals_url) {}
|
||||
|
||||
TrustedSignalsCacheImpl::FetchKey::FetchKey(const FetchKey&) = default;
|
||||
TrustedSignalsCacheImpl::FetchKey::FetchKey(FetchKey&&) = default;
|
||||
|
||||
TrustedSignalsCacheImpl::FetchKey& TrustedSignalsCacheImpl::FetchKey::operator=(
|
||||
const FetchKey&) = default;
|
||||
TrustedSignalsCacheImpl::FetchKey& TrustedSignalsCacheImpl::FetchKey::operator=(
|
||||
FetchKey&&) = default;
|
||||
|
||||
TrustedSignalsCacheImpl::FetchKey::~FetchKey() = default;
|
||||
|
||||
bool TrustedSignalsCacheImpl::FetchKey::operator<(const FetchKey& other) const {
|
||||
return std::tie(owner, signals_type, main_frame_origin, trusted_signals_url) <
|
||||
std::tie(other.owner, other.signals_type, other.main_frame_origin,
|
||||
other.trusted_signals_url);
|
||||
}
|
||||
|
||||
struct TrustedSignalsCacheImpl::Fetch {
|
||||
struct CompressionGroup {
|
||||
// The CompressionGroupData corresponding to this fetch. No need to store
|
||||
// anything else - the details about the partition can be retrieved when it
|
||||
// comes time to make a request from the CacheEntries that
|
||||
// `compression_group_data` has iterators for.
|
||||
raw_ptr<CompressionGroupData> compression_group_data;
|
||||
|
||||
// Compression group IDs are assigned when a Fetch is started. They are
|
||||
// assigned then to more easily handle deletion.
|
||||
int compression_group_id = -1;
|
||||
};
|
||||
|
||||
// Map of joining origin to the corresponding compression group.
|
||||
using CompressionGroupMap = std::map<url::Origin, CompressionGroup>;
|
||||
CompressionGroupMap compression_groups;
|
||||
|
||||
// Timer to start request. At all points in time, either this should be
|
||||
// running (possibly with a 0 delay) or `fetcher` should be non-null.
|
||||
base::OneShotTimer timer_;
|
||||
|
||||
std::unique_ptr<TrustedSignalsFetcher> fetcher;
|
||||
};
|
||||
|
||||
struct TrustedSignalsCacheImpl::BiddingCacheEntry {
|
||||
explicit BiddingCacheEntry(base::optional_ref<const std::vector<std::string>>
|
||||
trusted_bidding_signals_keys) {
|
||||
AddKeys(trusted_bidding_signals_keys);
|
||||
}
|
||||
|
||||
// Returns `true` if `keys` contains all elements of
|
||||
// `trusted_bidding_signals_keys`. Always returns `true` if
|
||||
// `trusted_bidding_signals_keys` is empty.
|
||||
bool ContainsKeys(base::optional_ref<const std::vector<std::string>>
|
||||
trusted_bidding_signals_keys) const {
|
||||
if (trusted_bidding_signals_keys.has_value()) {
|
||||
for (const auto& key : *trusted_bidding_signals_keys) {
|
||||
if (!keys.contains(key)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// If `trusted_bidding_signals_keys` is non-null, merges it into `keys`.
|
||||
void AddKeys(base::optional_ref<const std::vector<std::string>>
|
||||
trusted_bidding_signals_keys) {
|
||||
if (trusted_bidding_signals_keys.has_value()) {
|
||||
keys.insert(trusted_bidding_signals_keys->begin(),
|
||||
trusted_bidding_signals_keys->end());
|
||||
}
|
||||
}
|
||||
|
||||
std::set<std::string> keys;
|
||||
|
||||
// A pointer to the associated CompressionGroupData. When the
|
||||
// CompressionGroupData is destroyed, `this` will be as well.
|
||||
raw_ptr<CompressionGroupData> compression_group_data;
|
||||
|
||||
// Partition within the CompressionGroupData corresponding to this CacheEntry.
|
||||
// All CacheEntries with the same CompressionGroupData have unique
|
||||
// `partition_ids`. Default value should never be used.
|
||||
int partition_id = 0;
|
||||
};
|
||||
|
||||
TrustedSignalsCacheImpl::BiddingCacheKey::BiddingCacheKey() = default;
|
||||
|
||||
TrustedSignalsCacheImpl::BiddingCacheKey::BiddingCacheKey(
|
||||
const url::Origin& owner,
|
||||
const std::string& interest_group_name,
|
||||
const GURL& trusted_signals_url,
|
||||
const url::Origin& main_frame_origin,
|
||||
const url::Origin& joining_origin,
|
||||
base::Value::Dict additional_params)
|
||||
: interest_group_name(interest_group_name),
|
||||
fetch_key(main_frame_origin,
|
||||
SignalsType::kBidding,
|
||||
owner,
|
||||
trusted_signals_url),
|
||||
joining_origin(joining_origin),
|
||||
additional_params(std::move(additional_params)) {}
|
||||
|
||||
TrustedSignalsCacheImpl::BiddingCacheKey::BiddingCacheKey(BiddingCacheKey&&) =
|
||||
default;
|
||||
|
||||
TrustedSignalsCacheImpl::BiddingCacheKey::~BiddingCacheKey() = default;
|
||||
|
||||
TrustedSignalsCacheImpl::BiddingCacheKey&
|
||||
TrustedSignalsCacheImpl::BiddingCacheKey::operator=(BiddingCacheKey&&) =
|
||||
default;
|
||||
|
||||
bool TrustedSignalsCacheImpl::BiddingCacheKey::operator<(
|
||||
const BiddingCacheKey& other) const {
|
||||
return std::tie(interest_group_name, fetch_key, joining_origin,
|
||||
additional_params) <
|
||||
std::tie(other.interest_group_name, other.fetch_key,
|
||||
other.joining_origin, other.additional_params);
|
||||
}
|
||||
|
||||
class TrustedSignalsCacheImpl::CompressionGroupData : public Handle {
|
||||
public:
|
||||
// Creates a CompressionGroupData.
|
||||
//
|
||||
// In addition to owning the Fetch (possibly jointly with other
|
||||
// CompressionGroupData objects) and the CachedResult once the fetch
|
||||
// completes, CompressionGroupData tracks and implicitly owns the CacheEntries
|
||||
// associated with the data..
|
||||
//
|
||||
// `cache` must outlive the created object,
|
||||
// and `fetch` must remain valid until the CompressionGroupData is destroyed
|
||||
// or SetData() is invoked.
|
||||
//
|
||||
// `fetch` and `fetch_compression_group` are iterators to the pending fetch
|
||||
// that will populate the CompressionGroupData, and the compression group
|
||||
// within that fetch that corresponds to the created CompressionGroupData.
|
||||
//
|
||||
// Informs `cache` when it's destroyed, so all references must be released
|
||||
// before the TrustedSignalsCacheImpl is destroyed.
|
||||
CompressionGroupData(
|
||||
TrustedSignalsCacheImpl* cache,
|
||||
FetchMap::iterator fetch,
|
||||
Fetch::CompressionGroupMap::iterator fetch_compression_group)
|
||||
: cache_(cache),
|
||||
fetch_(fetch),
|
||||
fetch_compression_group_(fetch_compression_group) {}
|
||||
|
||||
// Sets the received data. May only be called once. Clears information about
|
||||
// the Fetch, since it's now completed.
|
||||
void SetData(CachedResult data) {
|
||||
DCHECK(!data_);
|
||||
data_ = std::make_unique<CachedResult>(std::move(data));
|
||||
|
||||
// The fetch has now completed and the caller will delete it once it's done
|
||||
// sending the data to any consumers.
|
||||
fetch_ = std::nullopt;
|
||||
fetch_compression_group_ = std::nullopt;
|
||||
}
|
||||
|
||||
// True if SetData() has been invoked.
|
||||
bool has_data() const { return !!data_; }
|
||||
|
||||
// May only be called if has_data() returns true.
|
||||
const CachedResult& data() const { return *data_; }
|
||||
|
||||
// Associates a BiddingCacheEntry with the CompressionGroupData. When the
|
||||
// CompressionGroupData is destroyed, this is used by the cache to destroy all
|
||||
// associated CacheEntries.
|
||||
void AddBiddingEntry(BiddingCacheEntryMap::iterator bidding_cache_entry) {
|
||||
bidding_cache_entries_.emplace(bidding_cache_entry->second.partition_id,
|
||||
bidding_cache_entry);
|
||||
}
|
||||
|
||||
// Removes `bidding_cache_entry` from `bidding_cache_entries_`.
|
||||
// `bidding_cache_entry` must be present in `bidding_cache_entries_`.
|
||||
void RemoveBiddingCacheEntry(BiddingCacheEntry* bidding_cache_entry) {
|
||||
CHECK_EQ(1u,
|
||||
bidding_cache_entries_.erase(bidding_cache_entry->partition_id));
|
||||
}
|
||||
|
||||
// Contains iterators to associated BiddingCacheEntries, indexed by partition
|
||||
// ID.
|
||||
const std::map<int, BiddingCacheEntryMap::iterator>& bidding_cache_entries()
|
||||
const {
|
||||
return bidding_cache_entries_;
|
||||
}
|
||||
|
||||
// The Fetch associated with the CompressionGroup, if the Fetch has not yet
|
||||
// completed. It may or may not be started. May only be called before the
|
||||
// Fetch completes.
|
||||
FetchMap::iterator fetch() const {
|
||||
DCHECK(fetch_);
|
||||
return *fetch_;
|
||||
}
|
||||
|
||||
// The CompressionGroup of the Fetch associated with `this`. May only be
|
||||
// called before the Fetch completes.
|
||||
Fetch::CompressionGroupMap::iterator fetch_compression_group() const {
|
||||
DCHECK(fetch_compression_group_);
|
||||
return *fetch_compression_group_;
|
||||
}
|
||||
|
||||
void AddPendingClient(
|
||||
mojo::PendingRemote<auction_worklet::mojom::TrustedSignalsCacheClient>
|
||||
pending_client) {
|
||||
pending_clients_.emplace_back(std::move(pending_client));
|
||||
}
|
||||
|
||||
std::vector<
|
||||
mojo::PendingRemote<auction_worklet::mojom::TrustedSignalsCacheClient>>
|
||||
TakePendingClients() {
|
||||
return std::move(pending_clients_);
|
||||
}
|
||||
|
||||
// Returns the ID for the next partition. Technically could use
|
||||
// `bidding_cache_entries_.size()`, since BiddingCacheEntries can can only be
|
||||
// added to the compression group before its fetch starts, and can only be
|
||||
// removed from a compression group (thus reducing size()) after the group's
|
||||
// Fetch starts, but safest to track this separately.
|
||||
int GetNextPartitionId() { return next_partition_id_++; }
|
||||
|
||||
private:
|
||||
friend class base::RefCounted<CompressionGroupData>;
|
||||
|
||||
~CompressionGroupData() override {
|
||||
cache_->OnCompressionGroupDataDestroyed(*this);
|
||||
}
|
||||
|
||||
const raw_ptr<TrustedSignalsCacheImpl> cache_;
|
||||
|
||||
// Information about a pending or live Fetch. Iterators make it convenient for
|
||||
// TrustedSignalsCacheImpl::OnCompressionGroupDataDestroyed() to remove the
|
||||
// corresponding objects on cancellation, if needed, both in terms of
|
||||
// performance and in terms of not having to worry about the keys for the
|
||||
// corresponding maps in this class.
|
||||
//
|
||||
// Cleared when TrustedSignalsCacheImpl::OnFetchComplete() calls SetData().
|
||||
// OnFetchComplete() will also delete the underlying Fetch.
|
||||
std::optional<FetchMap::iterator> fetch_;
|
||||
std::optional<Fetch::CompressionGroupMap::iterator> fetch_compression_group_;
|
||||
|
||||
std::unique_ptr<CachedResult> data_;
|
||||
|
||||
// All BiddingCacheEntries associated with this CompressionGroupData. The map
|
||||
// is indexed by partition ID.
|
||||
//
|
||||
// Using a map allows for log(n) removal from this map when a
|
||||
// BiddingCacheEntry is individually destroyed, tracking iterators allows for
|
||||
// O(1) removal from the TrustedSignalsCacheImpl's map of all
|
||||
// BiddingCacheEntries when the CompressionGroupData is destroyed.
|
||||
//
|
||||
// Iterators are also needed because the Fetch needs access to the
|
||||
// BiddingCacheKey.
|
||||
std::map<int, BiddingCacheEntryMap::iterator> bidding_cache_entries_;
|
||||
|
||||
// Requests for this cache entry. Probably not worth binding them to watch for
|
||||
// cancellation, since can't cancel unless there's no handle, at which point,
|
||||
// pending requests can all be ignored, anyways.
|
||||
std::vector<
|
||||
mojo::PendingRemote<auction_worklet::mojom::TrustedSignalsCacheClient>>
|
||||
pending_clients_;
|
||||
|
||||
int next_partition_id_ = 0;
|
||||
};
|
||||
|
||||
TrustedSignalsCacheImpl::TrustedSignalsCacheImpl(
|
||||
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory) {}
|
||||
|
||||
TrustedSignalsCacheImpl::~TrustedSignalsCacheImpl() = default;
|
||||
|
||||
mojo::PendingRemote<auction_worklet::mojom::TrustedSignalsCache>
|
||||
TrustedSignalsCacheImpl::CreateMojoPipe() {
|
||||
mojo::PendingRemote<auction_worklet::mojom::TrustedSignalsCache> out;
|
||||
receiver_set_.Add(this, out.InitWithNewPipeAndPassReceiver());
|
||||
return out;
|
||||
}
|
||||
|
||||
scoped_refptr<TrustedSignalsCacheImpl::Handle>
|
||||
TrustedSignalsCacheImpl::RequestTrustedBiddingSignals(
|
||||
const url::Origin& main_frame_origin,
|
||||
const url::Origin& owner,
|
||||
const std::string& interest_group_name,
|
||||
const url::Origin& joining_origin,
|
||||
const GURL& trusted_signals_url,
|
||||
base::optional_ref<const std::vector<std::string>>
|
||||
trusted_bidding_signals_keys,
|
||||
base::Value::Dict additional_params,
|
||||
int& partition_id) {
|
||||
BiddingCacheKey cache_key(owner, interest_group_name, trusted_signals_url,
|
||||
main_frame_origin, joining_origin,
|
||||
std::move(additional_params));
|
||||
|
||||
BiddingCacheEntryMap::iterator cache_entry_it =
|
||||
bidding_cache_entries_.find(cache_key);
|
||||
if (cache_entry_it != bidding_cache_entries_.end()) {
|
||||
BiddingCacheEntry* cache_entry = &cache_entry_it->second;
|
||||
CompressionGroupData* compression_group_data =
|
||||
cache_entry->compression_group_data;
|
||||
|
||||
// If `cache_entry`'s Fetch hasn't yet started, update the BiddingCacheEntry
|
||||
// to include any new keys, and return the entry's CompressionGroupData. The
|
||||
// Fetch will get the updated keys when it's started, so it does not need to
|
||||
// be modified.
|
||||
if (!compression_group_data->has_data() &&
|
||||
!compression_group_data->fetch()->second.fetcher) {
|
||||
cache_entry->AddKeys(trusted_bidding_signals_keys);
|
||||
partition_id = cache_entry->partition_id;
|
||||
return scoped_refptr<Handle>(compression_group_data);
|
||||
}
|
||||
|
||||
// Otherwise, check if all keys (if there are any) already appear in the
|
||||
// entry. If so, can reuse the cache entry without doing any more work.
|
||||
if (cache_entry->ContainsKeys(trusted_bidding_signals_keys)) {
|
||||
partition_id = cache_entry->partition_id;
|
||||
return scoped_refptr<Handle>(compression_group_data);
|
||||
}
|
||||
|
||||
// Otherwise, delete the cache entry. Even if its `compression_group_data`
|
||||
// is still in use, this is fine, as the CacheEntry only serves two
|
||||
// purposes: 1) It allows new requests to find the entry. 2) It's used to
|
||||
// populate fields for the Fetch.
|
||||
//
|
||||
// 1) doesn't create any issues - the new entry will be returned instead, if
|
||||
// it's usable. 2) is also not a problem, since we checked just above if
|
||||
// there was a Fetch that hadn't started yet, and if so, reused the entry.
|
||||
//
|
||||
// This behavior allows `bidding_cache_entries_` to be a map instead of a
|
||||
// multimap, to avoid having to worry about multiple live fetches. This path
|
||||
// should be uncommon - it's only hit when an interest group is modified, or
|
||||
// a group-by-origin IG is joined between auctions.
|
||||
DestroyBiddingCacheEntry(cache_entry_it);
|
||||
}
|
||||
|
||||
// If there was no matching cache entry, create a new one, and set up the
|
||||
// Fetch.
|
||||
|
||||
// Create a new cache entry, moving `cache_key` and creating a CacheEntry
|
||||
// in-place.
|
||||
cache_entry_it =
|
||||
bidding_cache_entries_
|
||||
.emplace(std::piecewise_construct,
|
||||
std::forward_as_tuple(std::move(cache_key)),
|
||||
std::forward_as_tuple(trusted_bidding_signals_keys))
|
||||
.first;
|
||||
|
||||
scoped_refptr<CompressionGroupData> compression_group_data =
|
||||
FindOrCreateCompressionGroupDataAndQueueFetch(
|
||||
cache_entry_it->first.fetch_key,
|
||||
cache_entry_it->first.joining_origin);
|
||||
|
||||
// The only thing left to do is set up pointers so objects can look up each
|
||||
// other and return the result. When it's time to send a request, the Fetch
|
||||
// can look up the associated CacheEntries for each compression group to get
|
||||
// the data it needs to pass on.
|
||||
|
||||
cache_entry_it->second.compression_group_data = compression_group_data.get();
|
||||
|
||||
// Note that partition ID must be assigned before adding the entry to the
|
||||
// CompressionGroupData, since CompressionGroupData uses the partition ID as
|
||||
// the index.
|
||||
cache_entry_it->second.partition_id =
|
||||
compression_group_data->GetNextPartitionId();
|
||||
compression_group_data->AddBiddingEntry(cache_entry_it);
|
||||
|
||||
partition_id = cache_entry_it->second.partition_id;
|
||||
return compression_group_data;
|
||||
}
|
||||
|
||||
scoped_refptr<TrustedSignalsCacheImpl::CompressionGroupData>
|
||||
TrustedSignalsCacheImpl::FindOrCreateCompressionGroupDataAndQueueFetch(
|
||||
const FetchKey& fetch_key,
|
||||
const url::Origin& joining_origin) {
|
||||
// If there are any Fetches with the correct FetchKey, check if the last one
|
||||
// is still pending. If so, reuse it. Otherwise, will need to create a new
|
||||
// Fetch. Don't need to check the others because multimaps insert in FIFO
|
||||
// order, and so this logic ensures that only the most recent fetch may not
|
||||
// have been started yet.
|
||||
auto [first, end] = fetches_.equal_range(fetch_key);
|
||||
FetchMap::iterator fetch_it = fetches_.end();
|
||||
if (first != end) {
|
||||
auto last = std::prev(end, 1);
|
||||
if (!last->second.fetcher) {
|
||||
fetch_it = last;
|
||||
}
|
||||
}
|
||||
|
||||
if (fetch_it == fetches_.end()) {
|
||||
fetch_it = fetches_.emplace(std::piecewise_construct,
|
||||
std::forward_as_tuple(fetch_key),
|
||||
std::forward_as_tuple());
|
||||
|
||||
// If the fetch is new, post a task to start it asynchronously. This should
|
||||
// allow all the interest groups from a single auction with the same owner
|
||||
// have their fetches group, if possible.
|
||||
//
|
||||
// * TODO(https://crbug.com/333445540): The fact that
|
||||
// AuctionWorkletManager::WorkletOwner::MaybeQueueNotifications() splits up
|
||||
// notifications is an issue that can cause problems with this assumption,
|
||||
// potentially reducing cache hit rates in the case where multiple requests
|
||||
// share a partition. This should only be an issue in the group-by-origin
|
||||
// case, but is still worth investigating.
|
||||
//
|
||||
// TODO(https://crbug.com/333445540): This also doesn't work at all for
|
||||
// sellers. Once this API has been extended to support sellers as well,
|
||||
// figure out something better for them. Maybe a 10 ms delay + flush
|
||||
// messages, like we do for the legacy non-TEE requests?
|
||||
fetch_it->second.timer_.Start(
|
||||
FROM_HERE, base::TimeDelta(),
|
||||
base::BindOnce(&TrustedSignalsCacheImpl::StartFetch,
|
||||
base::Unretained(this), fetch_it));
|
||||
}
|
||||
|
||||
Fetch* fetch = &fetch_it->second;
|
||||
|
||||
// Now that we have a matching Fetch, check if there's an existing compression
|
||||
// group that can be reused.
|
||||
auto [compression_group_it, new_element_created] =
|
||||
fetch->compression_groups.try_emplace(joining_origin);
|
||||
|
||||
// Return existing CompressionGroupData if there's already a matching
|
||||
// compression group.
|
||||
if (!new_element_created) {
|
||||
return scoped_refptr<CompressionGroupData>(
|
||||
compression_group_it->second.compression_group_data);
|
||||
}
|
||||
|
||||
// Create a CompressionGroupData if a new compression group was created.
|
||||
// `compression_group_id` is left as -1. One will be assigned when the request
|
||||
// is sent over the wire.
|
||||
scoped_refptr<CompressionGroupData> compression_group_data =
|
||||
base::MakeRefCounted<CompressionGroupData>(this, fetch_it,
|
||||
compression_group_it);
|
||||
compression_group_it->second.compression_group_data =
|
||||
compression_group_data.get();
|
||||
compression_group_data_map_.emplace(
|
||||
compression_group_data->compression_group_token(),
|
||||
compression_group_data.get());
|
||||
return compression_group_data;
|
||||
}
|
||||
|
||||
void TrustedSignalsCacheImpl::GetTrustedSignals(
|
||||
const base::UnguessableToken& compression_group_token,
|
||||
mojo::PendingRemote<auction_worklet::mojom::TrustedSignalsCacheClient>
|
||||
client) {
|
||||
auto compression_group_data_it =
|
||||
compression_group_data_map_.find(compression_group_token);
|
||||
// This can racily happen if a an auction is cancelled, so silently ignore
|
||||
// unrecognized IDs. This can also happen if a random ID is arbitrarily
|
||||
// requested, but the error message is for the common case.
|
||||
if (compression_group_data_it == compression_group_data_map_.end()) {
|
||||
// An error message shouldn't make it back to the browser process if this
|
||||
// happens, but provide one just in case it unexpectedly does.
|
||||
SendNoLiveEntryErrorToClient(std::move(client));
|
||||
return;
|
||||
}
|
||||
|
||||
CompressionGroupData* compression_group_data =
|
||||
compression_group_data_it->second;
|
||||
// If the fetch is still pending, add to the list of pending clients.
|
||||
if (!compression_group_data->has_data()) {
|
||||
compression_group_data->AddPendingClient(std::move(client));
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, provide the cached data immediately, which will then also
|
||||
// destroy `client`.
|
||||
SendResultToClient(std::move(client), compression_group_data->data());
|
||||
}
|
||||
|
||||
void TrustedSignalsCacheImpl::StartFetch(FetchMap::iterator fetch_it) {
|
||||
Fetch* fetch = &fetch_it->second;
|
||||
DCHECK(!fetch->fetcher);
|
||||
fetch->fetcher = CreateFetcher();
|
||||
// If all the compression groups were deleted, the Fetch should have been
|
||||
// destroyed.
|
||||
DCHECK(!fetch->compression_groups.empty());
|
||||
|
||||
std::map<int, std::vector<TrustedSignalsFetcher::BiddingPartition>>
|
||||
bidding_partition_map;
|
||||
int next_compression_group_id = 0;
|
||||
for (auto& compression_group_pair : fetch->compression_groups) {
|
||||
auto* compression_group = &compression_group_pair.second;
|
||||
compression_group->compression_group_id = next_compression_group_id++;
|
||||
|
||||
// Note that this will insert a new compression group.
|
||||
auto& bidding_partitions =
|
||||
bidding_partition_map[compression_group->compression_group_id];
|
||||
for (const auto& cache_entry_it :
|
||||
compression_group->compression_group_data->bidding_cache_entries()) {
|
||||
auto* cache_entry = &cache_entry_it.second->second;
|
||||
auto* cache_key = &cache_entry_it.second->first;
|
||||
bidding_partitions.emplace_back();
|
||||
auto* bidding_partition = &bidding_partitions.back();
|
||||
|
||||
bidding_partition->partition_id = cache_entry->partition_id;
|
||||
bidding_partition->interest_group_names.insert(
|
||||
cache_key->interest_group_name);
|
||||
bidding_partition->keys = cache_entry->keys;
|
||||
bidding_partition->hostname =
|
||||
cache_key->fetch_key.main_frame_origin.host();
|
||||
bidding_partition->additional_params =
|
||||
cache_key->additional_params.Clone();
|
||||
}
|
||||
}
|
||||
fetch->fetcher->FetchBiddingSignals(
|
||||
fetch_it->first.trusted_signals_url, bidding_partition_map,
|
||||
base::BindOnce(&TrustedSignalsCacheImpl::OnFetchComplete,
|
||||
base::Unretained(this), fetch_it));
|
||||
}
|
||||
|
||||
void TrustedSignalsCacheImpl::OnFetchComplete(
|
||||
FetchMap::iterator fetch_it,
|
||||
TrustedSignalsFetcher::SignalsFetchResult signals_fetch_result) {
|
||||
Fetch* fetch = &fetch_it->second;
|
||||
|
||||
for (auto& compression_group_pair : fetch->compression_groups) {
|
||||
Fetch::CompressionGroup* compression_group = &compression_group_pair.second;
|
||||
CachedResult result;
|
||||
|
||||
if (!signals_fetch_result.has_value()) {
|
||||
// On error, copy the shared error value to each group's
|
||||
// CompressionGroupData.
|
||||
result = base::unexpected(signals_fetch_result.error());
|
||||
} else {
|
||||
auto signals_fetch_result_it = signals_fetch_result.value().find(
|
||||
compression_group->compression_group_id);
|
||||
// The fetcher should fail the entire request if any compression group is
|
||||
// missing from the response.
|
||||
CHECK(signals_fetch_result_it != signals_fetch_result.value().end());
|
||||
|
||||
result = std::move(signals_fetch_result_it->second);
|
||||
}
|
||||
|
||||
CompressionGroupData* compression_group_data =
|
||||
compression_group->compression_group_data;
|
||||
auto pending_clients = compression_group_data->TakePendingClients();
|
||||
for (auto& pending_client : pending_clients) {
|
||||
SendResultToClient(std::move(pending_client), result);
|
||||
}
|
||||
compression_group_data->SetData(std::move(result));
|
||||
}
|
||||
|
||||
// The SetData() calls above cleared the references to the fetch held by the
|
||||
// CompressionGroupData, so it's now safe to remove.
|
||||
fetches_.erase(fetch_it);
|
||||
}
|
||||
|
||||
void TrustedSignalsCacheImpl::OnCompressionGroupDataDestroyed(
|
||||
CompressionGroupData& compression_group_data) {
|
||||
// Need to clean up the BiddingCacheEntries associated with the
|
||||
// CompressionGroupData.
|
||||
for (auto cache_entry_it : compression_group_data.bidding_cache_entries()) {
|
||||
bidding_cache_entries_.erase(cache_entry_it.second);
|
||||
}
|
||||
|
||||
// If `compression_group_data` has a fetch, started or not, need to update the
|
||||
// fetch and send an error to any Mojo clients waiting on the
|
||||
// CompressionGroupData.
|
||||
if (!compression_group_data.has_data()) {
|
||||
Fetch* fetch = &compression_group_data.fetch()->second;
|
||||
|
||||
DCHECK_EQ(compression_group_data.fetch_compression_group()
|
||||
->second.compression_group_data,
|
||||
&compression_group_data);
|
||||
|
||||
// Erase the compression group from the fetch. If the request hasn't yet
|
||||
// started, the group won't be requested. If it has started, any response
|
||||
// for the (now unknown) compression group will be discarded.
|
||||
fetch->compression_groups.erase(
|
||||
compression_group_data.fetch_compression_group());
|
||||
|
||||
// Abort the fetch, if it has no remaining compression groups.
|
||||
if (fetch->compression_groups.empty()) {
|
||||
fetches_.erase(compression_group_data.fetch());
|
||||
}
|
||||
|
||||
// Inform all pending clients waiting on the CompressionGroupData that the
|
||||
// request was cancelled.
|
||||
auto pending_clients = compression_group_data.TakePendingClients();
|
||||
for (auto& pending_client : pending_clients) {
|
||||
SendNoLiveEntryErrorToClient(std::move(pending_client));
|
||||
}
|
||||
}
|
||||
|
||||
compression_group_data_map_.erase(
|
||||
compression_group_data.compression_group_token());
|
||||
}
|
||||
|
||||
void TrustedSignalsCacheImpl::DestroyBiddingCacheEntry(
|
||||
BiddingCacheEntryMap::iterator cache_entry_it) {
|
||||
CompressionGroupData* compression_group_data =
|
||||
cache_entry_it->second.compression_group_data;
|
||||
// The compression group's fetch must either have completed, or its Fetch must
|
||||
// have already started.
|
||||
CHECK(compression_group_data->has_data() ||
|
||||
compression_group_data->fetch()->second.fetcher);
|
||||
compression_group_data->RemoveBiddingCacheEntry(&cache_entry_it->second);
|
||||
bidding_cache_entries_.erase(cache_entry_it);
|
||||
}
|
||||
|
||||
std::unique_ptr<TrustedSignalsFetcher>
|
||||
TrustedSignalsCacheImpl::CreateFetcher() {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace content
|
386
content/browser/interest_group/trusted_signals_cache_impl.h
Normal file
386
content/browser/interest_group/trusted_signals_cache_impl.h
Normal file
@ -0,0 +1,386 @@
|
||||
// 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.
|
||||
|
||||
#ifndef CONTENT_BROWSER_INTEREST_GROUP_TRUSTED_SIGNALS_CACHE_IMPL_H_
|
||||
#define CONTENT_BROWSER_INTEREST_GROUP_TRUSTED_SIGNALS_CACHE_IMPL_H_
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
|
||||
#include "base/functional/callback_forward.h"
|
||||
#include "base/memory/raw_ptr.h"
|
||||
#include "base/memory/ref_counted.h"
|
||||
#include "base/memory/scoped_refptr.h"
|
||||
#include "base/types/optional_ref.h"
|
||||
#include "base/unguessable_token.h"
|
||||
#include "base/values.h"
|
||||
#include "content/browser/interest_group/trusted_signals_fetcher.h"
|
||||
#include "content/common/content_export.h"
|
||||
#include "content/services/auction_worklet/public/mojom/trusted_signals_cache.mojom.h"
|
||||
#include "mojo/public/cpp/bindings/pending_remote.h"
|
||||
#include "mojo/public/cpp/bindings/receiver_set.h"
|
||||
#include "services/network/public/cpp/shared_url_loader_factory.h"
|
||||
#include "url/gurl.h"
|
||||
#include "url/origin.h"
|
||||
|
||||
namespace content {
|
||||
|
||||
// Handles caching (not yet implemented) and dispatching of trusted bidding and
|
||||
// scoring signals requests. Only handles requests to Trusted Execution
|
||||
// Environments (TEEs), i.e., versions 2+ of the protocol, so does not handle
|
||||
// legacy bring-your-own-server (BYOS) requests. The browser process makes a
|
||||
// request and gets a Handle and a partition ID, which can then be used to fetch
|
||||
// the response through the Mojo auction_worklet::mojom::TrustedSignalsCache
|
||||
// API provided by the Cache. The Handle and partition ID are provided
|
||||
// immediately on invocation, but the network request may not be sent out
|
||||
// immediately.
|
||||
//
|
||||
// The values it vends are guaranteed to remain valid at least until the Handle
|
||||
// they were returned with is destroyed. Having the cache in the browser process
|
||||
// allows requests to be sent while the Javascript process is still starting up,
|
||||
// and allows the cache to live beyond the shutdown of the often short-live
|
||||
// Javascript processes.
|
||||
//
|
||||
// Internally, it uses 4 maps:
|
||||
//
|
||||
// * `fetches_`, a multimap of pending/live Fetches, with FetchKeys consisting
|
||||
// of what must be the same to share a fetch. On fetch completion, ownership of
|
||||
// the response is passed to the corresponding CompressionGroupData(s) and the
|
||||
// Fetch is deleted. See FetchKey for more details on why this is a multimap
|
||||
// rather than a map.
|
||||
//
|
||||
// * `compression_group_data_map_`, a map of UnguessableTokens
|
||||
// (`compression_group_tokens`) to CompressionGroupData, which contain the still
|
||||
// compressed response for a single partition group within a fetch. A
|
||||
// CompressionGroupData may have one or more partitions, each of which
|
||||
// corresponds to a single [Bidding|Scoring]CacheEntry. The lifetime of
|
||||
// CompressionGroupData is scoped to the Handle objects returned by the Cache.
|
||||
//
|
||||
// * `bidding_cache_entries_`, a map of BiddingCacheEntries, with
|
||||
// BiddingCacheKeys consisting of what must be the same to share a Fetch, a
|
||||
// compression group, and partition within the group. Fields that can be merged
|
||||
// between requests to share a partitiong (e.g., trusted signals keys) are part
|
||||
// of entry itself, not the key. This is a map, not a multimap, so if a
|
||||
// BiddingCacheEntry cannot be reused (with or without modification) to suit the
|
||||
// needs of an incoming request, the BiddingCacheEntry is deleted, and removed
|
||||
// from its CompressionGroupData. Destroying a BiddingCacheEntry in this way
|
||||
// will not destroy the CompressionGroupData, or the CompressionGroupData's
|
||||
// fetch, if it has one.
|
||||
//
|
||||
// * TODO(https://crbug.com/333445540): A map of ScoringCacheEntries much akin
|
||||
// to the map of BiddingCacheEntries.
|
||||
//
|
||||
// Fetches and CacheEntries have pointers to the corresponding
|
||||
// CompressionGroupData, while the CompressionGroupData owns the corresponding
|
||||
// values in the other two maps. Deleting a CompressionGroupData removes the
|
||||
// corresponding values in the two maps. One CompressionGroupData may own
|
||||
// multiple CacheEntries, but will only own one live/pending Fetch. Ownership of
|
||||
// a Fetch may be shared by multiple CompressionGroupData objects with matching
|
||||
// FetchKeys.
|
||||
//
|
||||
// Each handed out Handle object will keep its corresponding
|
||||
// CompressionGroupData alive until the handle is destroyed.
|
||||
//
|
||||
// TODO(https://crbug.com/333445540): Respect TTL.
|
||||
//
|
||||
// TODO(https://crbug.com/333445540): Add caching support. Right now, entries
|
||||
// are cached only as long as there's something that owns a Handle, but should
|
||||
// instead cache for at least a short duration as long as an entry's TTL hasn't
|
||||
// expired. Holding onto a CompressionGroupData reference, which is refcounted,
|
||||
// is all that's needed to keep an entry alive.
|
||||
//
|
||||
// TODO(https://crbug.com/333445540): May need some sort of rate limit and size
|
||||
// cap. Currently, this class creates an arbitrary number of downloads, and
|
||||
// potentially stores an unlimited amount of data in browser process memory.
|
||||
class CONTENT_EXPORT TrustedSignalsCacheImpl
|
||||
: public auction_worklet::mojom::TrustedSignalsCache {
|
||||
public:
|
||||
// As long as a Handle is alive, any Mojo
|
||||
// auction_worklet::mojom::TrustedSignalsCache created by invoking
|
||||
// CreateMojoPipe() can retrieve the response associated with the
|
||||
// corresponding signals response ID, which will not change for the lifetime
|
||||
// of the handle. The ID can be used to request a response from the cache at
|
||||
// any point in time, but the fetch may be made asynchronously, so there's no
|
||||
// guarantee of a timely response.
|
||||
//
|
||||
// Refcounted so that one handle can be reused for all requests with the same
|
||||
// `compression_group_token`, so when the Handle is destroyed, we know there
|
||||
// are no Handles that refer to the corresponding entry in the cache, and it
|
||||
// may be deleted.
|
||||
//
|
||||
// Any pending or future requests through a handed out
|
||||
// auction_worklet::mojom::TrustedSignalsCache pipe for the
|
||||
// `compression_group_token` associated with a destroyed Handle will be sent
|
||||
// an error message.
|
||||
//
|
||||
// All outstanding Handles must be released before the TrustedSignalsCacheImpl
|
||||
// may be destroyed.
|
||||
//
|
||||
// Currently, the internal CompressionGroupData class is a subclass of this,
|
||||
// so callers are hanging on to data associated with a compression group
|
||||
// directly, but that's not a fundamental design requirement of the API.
|
||||
class Handle : public base::RefCounted<Handle> {
|
||||
public:
|
||||
Handle(Handle&) = delete;
|
||||
Handle& operator=(Handle&) = delete;
|
||||
|
||||
// The token that needs to be passed to GetTrustedSignals() to retrieve the
|
||||
// response through the auction_worklet::mojom::TrustedSignalsCache API.
|
||||
// Will not change for the lifetime of the handle.
|
||||
const base::UnguessableToken& compression_group_token() const {
|
||||
return compression_group_token_;
|
||||
}
|
||||
|
||||
protected:
|
||||
friend class base::RefCounted<Handle>;
|
||||
|
||||
Handle();
|
||||
virtual ~Handle();
|
||||
|
||||
const base::UnguessableToken compression_group_token_{
|
||||
base::UnguessableToken::Create()};
|
||||
};
|
||||
|
||||
explicit TrustedSignalsCacheImpl(
|
||||
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory);
|
||||
~TrustedSignalsCacheImpl() override;
|
||||
|
||||
TrustedSignalsCacheImpl(const TrustedSignalsCacheImpl&) = delete;
|
||||
TrustedSignalsCacheImpl& operator=(const TrustedSignalsCacheImpl&) = delete;
|
||||
|
||||
// Creates a TrustedSignalsCache pipe for a bidder script process.
|
||||
mojo::PendingRemote<auction_worklet::mojom::TrustedSignalsCache>
|
||||
CreateMojoPipe();
|
||||
|
||||
// Requests bidding signals for the specified interest group. Return value is
|
||||
// a Handle which must be kept alive until the response to the request is no
|
||||
// longer needed, and which provides a key to identify the response. Also
|
||||
// returns `partition_id`, which identifies the partition within the
|
||||
// compression group identified by Handle::compression_group_token() that will
|
||||
// have the relevant response.
|
||||
//
|
||||
// Never starts a network fetch synchronously. Bidder signals are requested
|
||||
// over the network after a post task.
|
||||
scoped_refptr<Handle> RequestTrustedBiddingSignals(
|
||||
const url::Origin& main_frame_origin,
|
||||
const url::Origin& owner,
|
||||
const std::string& interest_group_name,
|
||||
const url::Origin& joining_origin,
|
||||
const GURL& trusted_signals_url,
|
||||
base::optional_ref<const std::vector<std::string>>
|
||||
trusted_bidding_signals_keys,
|
||||
base::Value::Dict additional_params,
|
||||
int& partition_id);
|
||||
|
||||
// TrustedSignalsFetcher implementation:
|
||||
void GetTrustedSignals(
|
||||
const base::UnguessableToken& compression_group_token,
|
||||
mojo::PendingRemote<auction_worklet::mojom::TrustedSignalsCacheClient>
|
||||
client) override;
|
||||
|
||||
private:
|
||||
enum class SignalsType {
|
||||
kBidding,
|
||||
kScoring,
|
||||
};
|
||||
|
||||
// Key used for live or pending requests to a trusted server. Two request with
|
||||
// the same FetchKey can be merged together, but the requests themselves may
|
||||
// differ in other fields. Before the network request is started, any request
|
||||
// with a matching fetch key may be merged into a single request. Once the
|
||||
// network request is started, however, new requests may only be merged into
|
||||
// the live request if there's a matching CacheEntry that has already
|
||||
// requested all information needed for the request.
|
||||
//
|
||||
// There may be multiple requests at once with the same FetchKey, in the case
|
||||
// a network request was started before a new request came in with values that
|
||||
// do not match any of those in the live fetch.
|
||||
//
|
||||
// Combining requests across main frame origins or owners seems potentially
|
||||
// problematic in terms of cross-origin leaks, so partition on those for now,
|
||||
// at least.
|
||||
struct FetchKey {
|
||||
FetchKey();
|
||||
FetchKey(const url::Origin& main_frame_origin,
|
||||
SignalsType signals_type,
|
||||
const url::Origin& owner,
|
||||
const GURL& trusted_signals_url);
|
||||
FetchKey(const FetchKey&);
|
||||
FetchKey(FetchKey&&);
|
||||
|
||||
~FetchKey();
|
||||
|
||||
FetchKey& operator=(const FetchKey&);
|
||||
FetchKey& operator=(FetchKey&&);
|
||||
|
||||
bool operator<(const FetchKey& other) const;
|
||||
|
||||
// Order here matches comparison order in operator<(), and is based on a
|
||||
// guess on what order will result in the most performant comparisons.
|
||||
|
||||
url::Origin owner;
|
||||
SignalsType signals_type;
|
||||
|
||||
// The origin of the frame running the auction that needs the signals. This
|
||||
// could potentially be used to separate compression groups instead of
|
||||
// fetches, but best to be safe.
|
||||
url::Origin main_frame_origin;
|
||||
|
||||
GURL trusted_signals_url;
|
||||
};
|
||||
|
||||
// A pending or live network request. May be for bidding signals or scoring
|
||||
// signals, but not both.
|
||||
struct Fetch;
|
||||
using FetchMap = std::multimap<FetchKey, Fetch>;
|
||||
|
||||
// The cached compression group of a trusted signals response, or an error
|
||||
// message. May be for bidding signals or scoring signals, but not both.
|
||||
// CompressionGroupData are indexed by UnguessableTokens which can be used to
|
||||
// retrieve them over the auction_worklet::mojom::TrustedSignalsCache Mojo
|
||||
// interface.
|
||||
//
|
||||
// CompressionGroupData objects are created when RequestTrusted*Signals() is
|
||||
// called and can't reuse an existing one, at which point a new or existing
|
||||
// Fetch in `fetch_map_` is also associated with the CompressionGroupData.
|
||||
// Each CompressionGroupData owns all CacheEntries that refer to it, and the
|
||||
// compression group of the associated fetch as well. No two
|
||||
// CompressionGroupData objects represent the same compression group from a
|
||||
// single Fetch.
|
||||
//
|
||||
// CompressionGroupData objects are refcounted, and when the last reference is
|
||||
// released, all associated CacheEntries are destroyed, and the compression
|
||||
// group of the associated fetch (if the fetche associated with the
|
||||
// CompressionGroupData has not yet completed) is destroyed as well.
|
||||
class CompressionGroupData;
|
||||
|
||||
// A key that distinguishes bidding signals entries in the cache. The key is
|
||||
// used to find all potential matching entries whenever
|
||||
// RequestTrusted*Signals() is invoked. A response with one key cannot be used
|
||||
// to satisfy a request with another. There are some cases where even when the
|
||||
// BiddingCacheKey of a new request matches an existing BiddingCacheEntry, the
|
||||
// entry cannot be reused, in which case a new Entry is used and the old one
|
||||
// is thrown out (though the CompressionGroupData will remain valid). This can
|
||||
// happen in the case of cache expiration or the Entry not having the
|
||||
// necessary `trusted_bidding_signals_keys` after the corresponding network
|
||||
// request has been sent over the wire.
|
||||
struct BiddingCacheKey {
|
||||
BiddingCacheKey();
|
||||
BiddingCacheKey(BiddingCacheKey&&);
|
||||
BiddingCacheKey(const url::Origin& owner,
|
||||
const std::string& interest_group_name,
|
||||
const GURL& trusted_signals_url,
|
||||
const url::Origin& main_frame_origin,
|
||||
const url::Origin& joining_origin,
|
||||
base::Value::Dict additional_params);
|
||||
|
||||
~BiddingCacheKey();
|
||||
|
||||
BiddingCacheKey& operator=(BiddingCacheKey&&);
|
||||
|
||||
bool operator<(const BiddingCacheKey& other) const;
|
||||
|
||||
// Values where mismatches are expected to be more likely are listed
|
||||
// earlier.
|
||||
|
||||
// TODO(https://crbug.com/333445540): Switch this to an optional, and in the
|
||||
// case of group-by-origin mode, make it null, and allow adding trusted
|
||||
// bidding signals keys while network requests are still pending.
|
||||
std::string interest_group_name;
|
||||
|
||||
FetchKey fetch_key;
|
||||
url::Origin joining_origin;
|
||||
base::Value::Dict additional_params;
|
||||
};
|
||||
|
||||
// An indexed entry in the cache for callers of
|
||||
// RequestTrustedBiddingSignals(). It maps InterestGroup information and main
|
||||
// frame origins to CompressionGroupData objects and partition IDs.
|
||||
// BiddingCacheEntries that are sent to a TEE together in the same compressed
|
||||
// partition share a CompressionGroupData, but have different partition ids.
|
||||
// BiddingCacheEntries are only destroyed when the corresponding
|
||||
// BiddingCompressionGroupData is destroyed, or when a new BiddingCacheEntry
|
||||
// with the same key replaces them.
|
||||
struct BiddingCacheEntry;
|
||||
using BiddingCacheEntryMap = std::map<BiddingCacheKey, BiddingCacheEntry>;
|
||||
|
||||
// Returns a CompressionGroupData that can be used to fetch and store data
|
||||
// associated with the provided FetchKey and joining origin. The returned
|
||||
// CompressionGroupData will be associated with a Fetch that has not yet
|
||||
// started, either a new one or a shared one. May return a new or existing
|
||||
// CompressionGroupData. Queues any newly created fetch. After calling, the
|
||||
// caller must associate the returned CompressionGroupData with its
|
||||
// CacheEntry.
|
||||
scoped_refptr<TrustedSignalsCacheImpl::CompressionGroupData>
|
||||
FindOrCreateCompressionGroupDataAndQueueFetch(
|
||||
const FetchKey& fetch_key,
|
||||
const url::Origin& joining_origin);
|
||||
|
||||
// Starts the corresponding queued network fetch.
|
||||
void StartFetch(FetchMap::iterator fetch_it);
|
||||
|
||||
void OnFetchComplete(
|
||||
FetchMap::iterator fetch_it,
|
||||
TrustedSignalsFetcher::SignalsFetchResult signals_fetch_result);
|
||||
|
||||
// Called when the last reference of a CompressionGroupData object has been
|
||||
// released, and it's about to be destroyed. Does the following:
|
||||
//
|
||||
// * Removes the CompressionGroupData from `compression_group_data_`.
|
||||
//
|
||||
// * Destroys all CacheEntries associated with it.
|
||||
//
|
||||
// * If there is a pending Fetch associated with the CompressionGroupData,
|
||||
// removes the associated compression block from the Fetch (since the
|
||||
// CompressionGroupData corresponds to an entire block), cancelling the
|
||||
// Fetch if it has no non-empty cache blocks. Since compression block IDs
|
||||
// are not exposed by the API (only partition IDs within the block are),
|
||||
// there's no need to maintain compression block IDs.
|
||||
//
|
||||
// * If there is a live Fetch associated request, the associated compression
|
||||
// block isn't cleared, but its pointer to the CompressionGroupData is, and
|
||||
// the Fetch is cancelled if it has no remaining compression blocks
|
||||
// associated with CompressionGroupData objects.
|
||||
void OnCompressionGroupDataDestroyed(
|
||||
CompressionGroupData& compression_group_data);
|
||||
|
||||
// Destroys `cache_entry_it` and removes it from the CompressionGroupData that
|
||||
// owns it. This does not remove data from the compression group. Its
|
||||
// CompressionGroupData must not have a pending fetch, as that would mean the
|
||||
// compression group may not retrieve data that a consumer expects it to
|
||||
// retrieve, since Fetches rely on cache entries to know what to retrieve when
|
||||
// they're started.
|
||||
void DestroyBiddingCacheEntry(BiddingCacheEntryMap::iterator cache_entry_it);
|
||||
|
||||
// Virtual for testing.
|
||||
virtual std::unique_ptr<TrustedSignalsFetcher> CreateFetcher();
|
||||
|
||||
mojo::ReceiverSet<auction_worklet::mojom::TrustedSignalsCache> receiver_set_;
|
||||
|
||||
// Multimap of live and pending fetches. Fetches are removed on completion and
|
||||
// cancellation. When data is requested from the cache, if data needs to be
|
||||
// fetched from the network and there's an unstarted pending Fetch with a
|
||||
// matching FetchKey, the pending Fetch will always be used to request the
|
||||
// additional data. As a result, for any FetchKey, there will be at most one
|
||||
// pending Fetch, which will be the last Fetch with that FetchKey, since
|
||||
// multimap entries are stored in FIFO order.
|
||||
FetchMap fetches_;
|
||||
|
||||
BiddingCacheEntryMap bidding_cache_entries_;
|
||||
|
||||
// Map of IDs to CompressionGroupData. CompressionGroupData objects are
|
||||
// refcounted, and removed from the map whenever the last reference is
|
||||
// released, at which point any associated BiddingCacheEntries are destroyed,
|
||||
// and the CompressionGroupData removed from any associated Fetch, destroying
|
||||
// the Fetch if no longer needed.
|
||||
std::map<base::UnguessableToken, CompressionGroupData*>
|
||||
compression_group_data_map_;
|
||||
};
|
||||
|
||||
} // namespace content
|
||||
|
||||
#endif // CONTENT_BROWSER_INTEREST_GROUP_TRUSTED_SIGNALS_CACHE_IMPL_H_
|
File diff suppressed because it is too large
Load Diff
57
content/browser/interest_group/trusted_signals_fetcher.cc
Normal file
57
content/browser/interest_group/trusted_signals_fetcher.cc
Normal file
@ -0,0 +1,57 @@
|
||||
// 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 "content/browser/interest_group/trusted_signals_fetcher.h"
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "base/functional/callback.h"
|
||||
#include "base/memory/raw_ptr.h"
|
||||
#include "base/types/expected.h"
|
||||
#include "content/common/content_export.h"
|
||||
#include "content/services/auction_worklet/public/mojom/trusted_signals_cache.mojom.h"
|
||||
#include "services/network/public/cpp/shared_url_loader_factory.h"
|
||||
#include "url/gurl.h"
|
||||
#include "url/origin.h"
|
||||
|
||||
namespace content {
|
||||
|
||||
TrustedSignalsFetcher::BiddingPartition::BiddingPartition() = default;
|
||||
|
||||
TrustedSignalsFetcher::BiddingPartition::BiddingPartition(BiddingPartition&&) =
|
||||
default;
|
||||
|
||||
TrustedSignalsFetcher::BiddingPartition::~BiddingPartition() = default;
|
||||
|
||||
TrustedSignalsFetcher::BiddingPartition&
|
||||
TrustedSignalsFetcher::BiddingPartition::operator=(BiddingPartition&&) =
|
||||
default;
|
||||
|
||||
TrustedSignalsFetcher::CompressionGroupResult::CompressionGroupResult() =
|
||||
default;
|
||||
TrustedSignalsFetcher::CompressionGroupResult::CompressionGroupResult(
|
||||
CompressionGroupResult&&) = default;
|
||||
|
||||
TrustedSignalsFetcher::CompressionGroupResult::~CompressionGroupResult() =
|
||||
default;
|
||||
|
||||
TrustedSignalsFetcher::CompressionGroupResult&
|
||||
TrustedSignalsFetcher::CompressionGroupResult::operator=(
|
||||
CompressionGroupResult&&) = default;
|
||||
|
||||
TrustedSignalsFetcher::TrustedSignalsFetcher(
|
||||
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory) {}
|
||||
TrustedSignalsFetcher::~TrustedSignalsFetcher() = default;
|
||||
|
||||
void TrustedSignalsFetcher::FetchBiddingSignals(
|
||||
const GURL& trusted_bidding_signals_url,
|
||||
const std::map<int, std::vector<BiddingPartition>>& compression_groups,
|
||||
Callback callback) {}
|
||||
|
||||
} // namespace content
|
126
content/browser/interest_group/trusted_signals_fetcher.h
Normal file
126
content/browser/interest_group/trusted_signals_fetcher.h
Normal file
@ -0,0 +1,126 @@
|
||||
// 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.
|
||||
|
||||
#ifndef CONTENT_BROWSER_INTEREST_GROUP_TRUSTED_SIGNALS_FETCHER_H_
|
||||
#define CONTENT_BROWSER_INTEREST_GROUP_TRUSTED_SIGNALS_FETCHER_H_
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "base/functional/callback.h"
|
||||
#include "base/types/expected.h"
|
||||
#include "base/values.h"
|
||||
#include "content/common/content_export.h"
|
||||
#include "content/services/auction_worklet/public/mojom/trusted_signals_cache.mojom.h"
|
||||
#include "services/network/public/cpp/shared_url_loader_factory.h"
|
||||
#include "url/gurl.h"
|
||||
#include "url/origin.h"
|
||||
|
||||
namespace content {
|
||||
|
||||
// Single-use network fetcher for versions 2+ of the key-value server API.
|
||||
// It takes a list compression groups and partitions, and asynchronously returns
|
||||
// a set of responses, one per compression group. The responses are provided as
|
||||
// still compressed compression group bodies, so the cache layer can store
|
||||
// compressed responses and to minimize IPC size. The responses will be
|
||||
// decompressed before use in the appropriate Javascript process.
|
||||
//
|
||||
// Bidding and scoring signals need different structs when sending requests, but
|
||||
// they use the same response format, since it's only the compressed data itself
|
||||
// that varies based on signals type.
|
||||
//
|
||||
// TODO(https://crbug.com/333445540): This is currently only an API, with no
|
||||
// implementation. Need to actually implement the API.
|
||||
class CONTENT_EXPORT TrustedSignalsFetcher {
|
||||
public:
|
||||
// All the data needed to request a particular bidding signals partition.
|
||||
//
|
||||
// TODO(https://crbug.com/333445540): Consider making some of these fields
|
||||
// pointers to reduce copies. Since tests use this class to store arguments,
|
||||
// would need to rework that as well.
|
||||
struct CONTENT_EXPORT BiddingPartition {
|
||||
BiddingPartition();
|
||||
BiddingPartition(BiddingPartition&&);
|
||||
|
||||
~BiddingPartition();
|
||||
|
||||
BiddingPartition& operator=(BiddingPartition&&);
|
||||
|
||||
int partition_id;
|
||||
|
||||
std::set<std::string> interest_group_names;
|
||||
std::set<std::string> keys;
|
||||
std::string hostname;
|
||||
|
||||
// At the moment, valid keys are "experimentGroupId", "slotSize", and
|
||||
// "allSlotsRequestedSizes". We could take them separately, but seems better
|
||||
// to take one field rather than several?
|
||||
base::Value::Dict additional_params;
|
||||
};
|
||||
|
||||
// While buying and scoring signals partitions need different structs when
|
||||
// sending requests, the responses use the same format.
|
||||
|
||||
// The received result for a particular compression group. Only returned on
|
||||
// success.
|
||||
struct CONTENT_EXPORT CompressionGroupResult {
|
||||
CompressionGroupResult();
|
||||
CompressionGroupResult(CompressionGroupResult&&);
|
||||
|
||||
~CompressionGroupResult();
|
||||
|
||||
CompressionGroupResult& operator=(CompressionGroupResult&&);
|
||||
|
||||
// The compression scheme used by `compression_group_data`, as indicated by
|
||||
// the server.
|
||||
auction_worklet::mojom::TrustedSignalsCompressionScheme compression_scheme;
|
||||
|
||||
// The still-compressed data for the compression group.
|
||||
std::vector<uint8_t> compression_group_data;
|
||||
|
||||
// Time until the response expires.
|
||||
base::TimeDelta ttl;
|
||||
};
|
||||
|
||||
// A map of compression group ids to results, in the case of success.
|
||||
using CompressionGroupResultMap = std::map<int, CompressionGroupResult>;
|
||||
|
||||
// The result type in the case of an error. Errors don't have a TTL.
|
||||
struct CONTENT_EXPORT ErrorInfo {
|
||||
std::string error_msg;
|
||||
};
|
||||
|
||||
// The result of a fetch. Either the entire fetch succeeds or it fails with a
|
||||
// single error.
|
||||
using SignalsFetchResult =
|
||||
base::expected<CompressionGroupResultMap, ErrorInfo>;
|
||||
|
||||
using Callback = base::OnceCallback<void(SignalsFetchResult)>;
|
||||
|
||||
explicit TrustedSignalsFetcher(
|
||||
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory);
|
||||
|
||||
// Virtual for tests.
|
||||
virtual ~TrustedSignalsFetcher();
|
||||
|
||||
TrustedSignalsFetcher(const TrustedSignalsFetcher&) = delete;
|
||||
TrustedSignalsFetcher& operator=(const TrustedSignalsFetcher&) = delete;
|
||||
|
||||
// `partitions` is a map of all partitions in the request, indexed by
|
||||
// compression group id. Virtual for tests.
|
||||
virtual void FetchBiddingSignals(
|
||||
const GURL& trusted_bidding_signals_url,
|
||||
const std::map<int, std::vector<BiddingPartition>>& compression_groups,
|
||||
Callback callback);
|
||||
|
||||
private:
|
||||
};
|
||||
|
||||
} // namespace content
|
||||
|
||||
#endif // CONTENT_BROWSER_INTEREST_GROUP_TRUSTED_SIGNALS_FETCHER_H_
|
@ -23,6 +23,7 @@ mojom("mojom") {
|
||||
"real_time_reporting.mojom",
|
||||
"reject_reason.mojom",
|
||||
"seller_worklet.mojom",
|
||||
"trusted_signals_cache.mojom",
|
||||
]
|
||||
deps = [
|
||||
"//content/common:mojo_bindings",
|
||||
|
@ -0,0 +1,63 @@
|
||||
// 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.
|
||||
|
||||
module auction_worklet.mojom;
|
||||
|
||||
import "mojo/public/mojom/base/big_buffer.mojom";
|
||||
import "mojo/public/mojom/base/unguessable_token.mojom";
|
||||
|
||||
// Compression schemes that can be used by Trusted Signals response bodies.
|
||||
enum TrustedSignalsCompressionScheme {
|
||||
kNone,
|
||||
kGzip,
|
||||
kBrotli,
|
||||
};
|
||||
|
||||
// Client that receives a trusted signals response for a single request.
|
||||
// There will be only one method call for a single
|
||||
// TrustedSignalsCache::GetTrustedSignals() invocation. The
|
||||
// remote side of the pipe will close it immediately after calling one of the
|
||||
// two available methods.
|
||||
//
|
||||
// An interface is used for this rather than a callback to allow for
|
||||
// cancellation.
|
||||
//
|
||||
// Note that multiple requests may be combined into a single network
|
||||
// request. On success, the response will be the response for an entire
|
||||
// compression group, still compressed.
|
||||
interface TrustedSignalsCacheClient {
|
||||
// Called on success, with the response data associated with the
|
||||
// provided `compression_group_compression_group_key`.
|
||||
// `compression_group_data` is data from the server, which the server claims
|
||||
// was compressed with `compression_scheme`.
|
||||
OnSuccess(TrustedSignalsCompressionScheme compression_scheme,
|
||||
mojo_base.mojom.BigBuffer compression_group_data);
|
||||
|
||||
// Called on error. Errors can happen in the case of a network error,
|
||||
// an error parsing the response, or if the corresponding auction is
|
||||
// cancelled.
|
||||
OnError(string error_message);
|
||||
};
|
||||
|
||||
// An interface to allow a Protected Audience script process to request a
|
||||
// trusted bidding or scoring signals response fetched by the browser process.
|
||||
// Each applicable GenerateBid() or ScoreAd() call configured to request
|
||||
// signals from a TEE ("Trusted signals API version 2") receives a
|
||||
// `compression_group_token`, which can then be passed to the global
|
||||
// TrustedSignalsCache to retrieve the corresponding response.
|
||||
//
|
||||
// The reason for this architecture is that multiple scripts may be able to use
|
||||
// the same response, so passing in IDs instead of the response itself reduces
|
||||
// copies sent between processes. The fetching is done in the browser process to
|
||||
// allow responses to be cached in the browser process for longer than script
|
||||
// processes last.
|
||||
interface TrustedSignalsCache {
|
||||
// Requests the compression group associated `compression_group_token`. A
|
||||
// single call will be made to `client` with an error or the corresponding
|
||||
// trusted signals response. If the response is not immediately available,
|
||||
// the `client` will only receive a message notified once it has been
|
||||
// received.
|
||||
GetTrustedSignals(mojo_base.mojom.UnguessableToken compression_group_token,
|
||||
pending_remote<TrustedSignalsCacheClient> client);
|
||||
};
|
@ -2513,6 +2513,7 @@ test("content_unittests") {
|
||||
"../browser/interest_group/test_interest_group_manager_impl.h",
|
||||
"../browser/interest_group/test_interest_group_private_aggregation_manager.cc",
|
||||
"../browser/interest_group/test_interest_group_private_aggregation_manager.h",
|
||||
"../browser/interest_group/trusted_signals_cache_impl_unittest.cc",
|
||||
"../browser/loader/cors_origin_pattern_setter_unittest.cc",
|
||||
"../browser/loader/file_url_loader_factory_unittest.cc",
|
||||
"../browser/loader/keep_alive_attribution_request_helper_unittest.cc",
|
||||
|
Reference in New Issue
Block a user