| // 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 "content/browser/interest_group/header_direct_from_seller_signals.h" |
| |
| #include <memory> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/containers/flat_map.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/json/json_string_value_serializer.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/values.h" |
| #include "services/data_decoder/public/cpp/data_decoder.h" |
| #include "url/gurl.h" |
| #include "url/origin.h" |
| #include "url/url_constants.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| // Searches a single Ad-Auction-Signals response header, `original_str` (parsed |
| // as JSON into `result`) for the first encountered dictionary with the the |
| // adSlot value equal to `ad_slot`, returning signals from that dictionary if |
| // found, or null if none are found. |
| // |
| // Errors, if encountered, are appended to `errors`. Errors may be encountered |
| // even if signals matching the `ad_slot` are still ultimately found and |
| // returned. |
| std::unique_ptr<HeaderDirectFromSellerSignals> MaybeFindAdSlotSignalsInResponse( |
| const data_decoder::DataDecoder::ValueOrError& result, |
| const std::string& ad_slot, |
| const std::string& original_str, |
| std::vector<std::string>& errors) { |
| if (!result.has_value()) { |
| errors.push_back(base::StringPrintf( |
| "When looking for directFromSellerSignalsHeaderAdSlot %s, encountered " |
| "invalid JSON: '%s' for Ad-Auction-Signals=%s", |
| ad_slot.c_str(), result.error().c_str(), original_str.c_str())); |
| return nullptr; |
| } |
| |
| const base::Value::List* maybe_list = result->GetIfList(); |
| if (!maybe_list) { |
| errors.push_back(base::StringPrintf( |
| "When looking for directFromSellerSignalsHeaderAdSlot %s, encountered " |
| "response where top-level JSON value isn't an array: " |
| "Ad-Auction-Signals=%s", |
| ad_slot.c_str(), original_str.c_str())); |
| return nullptr; |
| } |
| |
| for (const base::Value& list_item : *maybe_list) { |
| const base::Value::Dict* maybe_dict = list_item.GetIfDict(); |
| if (!maybe_dict) { |
| errors.push_back(base::StringPrintf( |
| "When looking for directFromSellerSignalsHeaderAdSlot %s, " |
| "encountered non-dict list item: Ad-AuctionSignals=%s", |
| ad_slot.c_str(), original_str.c_str())); |
| continue; |
| } |
| |
| const std::string* maybe_ad_slot = maybe_dict->FindString("adSlot"); |
| if (!maybe_ad_slot) { |
| errors.push_back(base::StringPrintf( |
| "When looking for directFromSellerSignalsHeaderAdSlot %s, " |
| "encountered dict without \"adSlot\" key: Ad-Auction-Signals=%s", |
| ad_slot.c_str(), original_str.c_str())); |
| continue; |
| } |
| |
| if (*maybe_ad_slot != ad_slot) { |
| continue; |
| } |
| |
| const base::Value* maybe_seller_signals = maybe_dict->Find("sellerSignals"); |
| absl::optional<std::string> seller_signals; |
| if (maybe_seller_signals) { |
| seller_signals.emplace(); |
| JSONStringValueSerializer serializer(&seller_signals.value()); |
| if (!serializer.Serialize(*maybe_seller_signals)) { |
| errors.push_back(base::StringPrintf( |
| "When looking for directFromSellerSignalsHeaderAdSlot %s, failed " |
| "to re-serialize sellerSignals: Ad-Auction-Signals=%s", |
| ad_slot.c_str(), original_str.c_str())); |
| seller_signals.reset(); |
| } |
| } |
| |
| const base::Value* maybe_auction_signals = |
| maybe_dict->Find("auctionSignals"); |
| absl::optional<std::string> auction_signals; |
| if (maybe_auction_signals) { |
| auction_signals.emplace(); |
| JSONStringValueSerializer serializer(&auction_signals.value()); |
| if (!serializer.Serialize(*maybe_auction_signals)) { |
| errors.push_back(base::StringPrintf( |
| "When looking for directFromSellerSignalsHeaderAdSlot %s, failed " |
| "to re-serialize auctionSignals: Ad-Auction-Signals=%s", |
| ad_slot.c_str(), original_str.c_str())); |
| auction_signals.reset(); |
| } |
| } |
| |
| base::flat_map<url::Origin, std::string> per_buyer_signals; |
| const base::Value::Dict* maybe_per_buyer_signals = |
| maybe_dict->FindDict("perBuyerSignals"); |
| if (maybe_per_buyer_signals) { |
| for (const std::pair<const std::string&, const base::Value&> item : |
| *maybe_per_buyer_signals) { |
| // Checking that the origin isn't opaque, untrustworthy, etc. shouldn't |
| // be necessary, since such origins aren't allowed for the interest |
| // group buyers. But, we check for https as a sanity check, which will |
| // also fail if the origin isn't valid. |
| url::Origin origin = url::Origin::Create(GURL(item.first)); |
| if (origin.scheme() != url::kHttpsScheme) { |
| errors.push_back(base::StringPrintf( |
| "When looking for directFromSellerSignalsHeaderAdSlot %s, " |
| "encountered non-https perBuyerSignals origin '%s': " |
| "Ad-Auction-Signals=%s", |
| ad_slot.c_str(), item.first.c_str(), original_str.c_str())); |
| continue; |
| } |
| std::string origin_signals; |
| JSONStringValueSerializer serializer(&origin_signals); |
| if (serializer.Serialize(item.second)) { |
| per_buyer_signals[origin] = origin_signals; |
| } else { |
| errors.push_back(base::StringPrintf( |
| "When looking for directFromSellerSignalsHeaderAdSlot %s, failed " |
| "to re-serialize perBuyerSignals[%s]: Ad-Auction-Signals=%s", |
| ad_slot.c_str(), item.first.c_str(), original_str.c_str())); |
| } |
| } |
| } |
| |
| return std::make_unique<HeaderDirectFromSellerSignals>( |
| std::move(seller_signals), std::move(auction_signals), |
| std::move(per_buyer_signals)); |
| } |
| |
| return nullptr; |
| } |
| |
| std::string NoMatchError(std::string ad_slot) { |
| return base::StringPrintf( |
| "When looking for directFromSellerSignalsHeaderAdSlot %s, failed to " |
| "find a matching response.", |
| ad_slot.c_str()); |
| } |
| |
| // Searches for a signals dict whose adSlot value matches `ad_slot`, continuing |
| // to the next response if no match is found. |
| void OnJsonDecoded(std::unique_ptr<const std::set<std::string>> responses, |
| std::set<std::string>::iterator it, |
| std::string ad_slot, |
| std::vector<std::string> errors, |
| HeaderDirectFromSellerSignals::CompletionCallback callback, |
| data_decoder::DataDecoder::ValueOrError result) { |
| std::unique_ptr<HeaderDirectFromSellerSignals> maybe_parsed = |
| MaybeFindAdSlotSignalsInResponse(result, ad_slot, *it, errors); |
| if (maybe_parsed) { |
| // Found a match. |
| std::move(callback).Run(std::move(maybe_parsed), std::move(errors)); |
| return; |
| } |
| |
| ++it; |
| if (it == responses->end()) { |
| // No responses matched so add an error and return. |
| errors.push_back(NoMatchError(ad_slot)); |
| std::move(callback).Run(std::make_unique<HeaderDirectFromSellerSignals>(), |
| std::move(errors)); |
| return; |
| } |
| // Decode the next response. |
| data_decoder::DataDecoder::ParseJsonIsolated( |
| *it, base::BindOnce(&OnJsonDecoded, std::move(responses), it, |
| std::move(ad_slot), std::move(errors), |
| std::move(callback))); |
| } |
| |
| } // namespace |
| |
| HeaderDirectFromSellerSignals::HeaderDirectFromSellerSignals() = default; |
| |
| HeaderDirectFromSellerSignals::~HeaderDirectFromSellerSignals() = default; |
| |
| // TODO(crbug.com/1462720): Add UMA for response size and parsing time. |
| /* static */ |
| void HeaderDirectFromSellerSignals::ParseAndFind( |
| const std::set<std::string>& responses, |
| std::string ad_slot, |
| CompletionCallback callback) { |
| std::vector<std::string> errors; |
| if (responses.empty()) { |
| errors.push_back(NoMatchError(ad_slot)); |
| std::move(callback).Run(std::make_unique<HeaderDirectFromSellerSignals>(), |
| std::move(errors)); |
| return; |
| } |
| |
| // The decoding is asynchronous, and it's possible that the original |
| // `responses` may have been destroyed before parsing completes; therefore, a |
| // copy is required. |
| auto my_responses = std::make_unique<const std::set<std::string>>(responses); |
| auto it = my_responses->begin(); |
| data_decoder::DataDecoder::ParseJsonIsolated( |
| *it, base::BindOnce(&OnJsonDecoded, std::move(my_responses), it, |
| std::move(ad_slot), std::move(errors), |
| std::move(callback))); |
| } |
| |
| HeaderDirectFromSellerSignals::HeaderDirectFromSellerSignals( |
| absl::optional<std::string> seller_signals, |
| absl::optional<std::string> auction_signals, |
| base::flat_map<url::Origin, std::string> per_buyer_signals) |
| : seller_signals_(std::move(seller_signals)), |
| auction_signals_(std::move(auction_signals)), |
| per_buyer_signals_(std::move(per_buyer_signals)) {} |
| |
| } // namespace content |