diff --git a/content/services/auction_worklet/BUILD.gn b/content/services/auction_worklet/BUILD.gn index 054880234dd61..4cd7d76797e97 100644 --- a/content/services/auction_worklet/BUILD.gn +++ b/content/services/auction_worklet/BUILD.gn @@ -57,10 +57,8 @@ source_set("auction_worklet") { "report_bindings.h", "seller_worklet.cc", "seller_worklet.h", - "trusted_bidding_signals.cc", - "trusted_bidding_signals.h", - "trusted_scoring_signals.cc", - "trusted_scoring_signals.h", + "trusted_signals.cc", + "trusted_signals.h", "worklet_loader.cc", "worklet_loader.h", ] @@ -98,8 +96,7 @@ source_set("tests") { "bidder_worklet_unittest.cc", "debug_command_queue_unittest.cc", "seller_worklet_unittest.cc", - "trusted_bidding_signals_unittest.cc", - "trusted_scoring_signals_unittest.cc", + "trusted_signals_unittest.cc", "worklet_devtools_debug_test_util.cc", "worklet_devtools_debug_test_util.h", "worklet_loader_unittest.cc", diff --git a/content/services/auction_worklet/bidder_worklet.cc b/content/services/auction_worklet/bidder_worklet.cc index 26597fb924913..3f8f21a4558e4 100644 --- a/content/services/auction_worklet/bidder_worklet.cc +++ b/content/services/auction_worklet/bidder_worklet.cc @@ -21,7 +21,7 @@ #include "content/services/auction_worklet/auction_v8_helper.h" #include "content/services/auction_worklet/public/mojom/auction_worklet_service.mojom.h" #include "content/services/auction_worklet/report_bindings.h" -#include "content/services/auction_worklet/trusted_bidding_signals.h" +#include "content/services/auction_worklet/trusted_signals.h" #include "content/services/auction_worklet/worklet_loader.h" #include "gin/converter.h" #include "gin/dictionary.h" @@ -223,7 +223,7 @@ void BidderWorklet::GenerateBid( trusted_bidding_signals_keys_.has_value() && !trusted_bidding_signals_keys_->empty()) { generate_bid_task->trusted_bidding_signals = - std::make_unique( + TrustedSignals::LoadBiddingSignals( url_loader_factory_.get(), *trusted_bidding_signals_keys_, top_window_origin.host(), *trusted_bidding_signals_url_, v8_helper_, base::BindOnce(&BidderWorklet::OnTrustedBiddingSignalsDownloaded, @@ -386,8 +386,7 @@ void BidderWorklet::V8State::GenerateBid( const url::Origin& browser_signal_top_window_origin, const url::Origin& browser_signal_seller_origin, base::Time auction_start_time, - std::unique_ptr - trusted_bidding_signals_result, + std::unique_ptr trusted_bidding_signals_result, GenerateBidCallbackInternal callback) { DCHECK_CALLED_ON_VALID_SEQUENCE(v8_sequence_checker_); @@ -452,7 +451,7 @@ void BidderWorklet::V8State::GenerateBid( if (!trusted_bidding_signals_result) { trusted_signals = v8::Null(isolate); } else { - trusted_signals = trusted_bidding_signals_result->GetSignals( + trusted_signals = trusted_bidding_signals_result->GetBiddingSignals( v8_helper_.get(), context, *interest_group.trusted_bidding_signals_keys); } @@ -718,12 +717,12 @@ void BidderWorklet::OnScriptDownloaded(WorkletLoader::Result worklet_script, void BidderWorklet::OnTrustedBiddingSignalsDownloaded( GenerateBidTaskList::iterator task, - std::unique_ptr result, + std::unique_ptr result, absl::optional error_msg) { DCHECK_CALLED_ON_VALID_SEQUENCE(user_sequence_checker_); task->trusted_bidding_signals_error_msg = std::move(error_msg); - task->trusted_Bidding_signals_result = std::move(result); + task->trusted_bidding_signals_result = std::move(result); task->trusted_bidding_signals.reset(); GenerateBidIfReady(task); @@ -741,7 +740,7 @@ void BidderWorklet::GenerateBidIfReady(GenerateBidTaskList::iterator task) { base::Unretained(v8_state_.get()), task->auction_signals_json, task->per_buyer_signals_json, task->top_window_origin, task->seller_origin, task->auction_start_time, - std::move(task->trusted_Bidding_signals_result), + std::move(task->trusted_bidding_signals_result), base::BindOnce(&BidderWorklet::DeliverBidCallbackOnUserThread, weak_ptr_factory_.GetWeakPtr(), task))); } diff --git a/content/services/auction_worklet/bidder_worklet.h b/content/services/auction_worklet/bidder_worklet.h index 8ff5d16e2e4cc..f481ec28e9ae4 100644 --- a/content/services/auction_worklet/bidder_worklet.h +++ b/content/services/auction_worklet/bidder_worklet.h @@ -18,7 +18,7 @@ #include "content/services/auction_worklet/auction_v8_helper.h" #include "content/services/auction_worklet/public/mojom/auction_worklet_service.mojom.h" #include "content/services/auction_worklet/public/mojom/bidder_worklet.mojom.h" -#include "content/services/auction_worklet/trusted_bidding_signals.h" +#include "content/services/auction_worklet/trusted_signals.h" #include "content/services/auction_worklet/worklet_loader.h" #include "mojo/public/cpp/bindings/pending_remote.h" #include "mojo/public/cpp/bindings/remote.h" @@ -100,10 +100,9 @@ class BidderWorklet : public mojom::BidderWorklet { base::Time auction_start_time; // Set while loading is in progress. - std::unique_ptr trusted_bidding_signals; + std::unique_ptr trusted_bidding_signals; // Results of loading trusted bidding signals. - std::unique_ptr - trusted_Bidding_signals_result; + std::unique_ptr trusted_bidding_signals_result; // Error message returned by attempt to load `trusted_bidding_signals_`. // Errors loading it are not fatal, so such errors are cached here and only // reported on bid completion. @@ -160,14 +159,14 @@ class BidderWorklet : public mojom::BidderWorklet { double browser_signal_bid, ReportWinCallbackInternal callback); - void GenerateBid(const absl::optional& auction_signals_json, - const absl::optional& per_buyer_signals_json, - const url::Origin& browser_signal_top_window_origin, - const url::Origin& browser_signal_seller_origin, - base::Time auction_start_time, - std::unique_ptr - trusted_bidding_signals_result, - GenerateBidCallbackInternal callback); + void GenerateBid( + const absl::optional& auction_signals_json, + const absl::optional& per_buyer_signals_json, + const url::Origin& browser_signal_top_window_origin, + const url::Origin& browser_signal_seller_origin, + base::Time auction_start_time, + std::unique_ptr trusted_bidding_signals_result, + GenerateBidCallbackInternal callback); void ConnectDevToolsAgent( mojo::PendingReceiver agent); @@ -214,11 +213,11 @@ class BidderWorklet : public mojom::BidderWorklet { void OnTrustedBiddingSignalsDownloaded( GenerateBidTaskList::iterator task, - std::unique_ptr result, + std::unique_ptr result, absl::optional error_msg); // Checks if the script has been loaded successfully, and the - // TrustedBiddingSignals load has finished (successfully or not). If so, calls + // TrustedSignals load has finished (successfully or not). If so, calls // generateBid(), and invokes `load_script_and_generate_bid_callback_` with // the resulting bid, if any. May only be called once BidderWorklet has // successfully loaded. diff --git a/content/services/auction_worklet/seller_worklet.cc b/content/services/auction_worklet/seller_worklet.cc index 1c907997b5acd..a6b661da0eef0 100644 --- a/content/services/auction_worklet/seller_worklet.cc +++ b/content/services/auction_worklet/seller_worklet.cc @@ -19,7 +19,7 @@ #include "content/services/auction_worklet/public/mojom/auction_worklet_service.mojom.h" #include "content/services/auction_worklet/public/mojom/seller_worklet.mojom.h" #include "content/services/auction_worklet/report_bindings.h" -#include "content/services/auction_worklet/trusted_scoring_signals.h" +#include "content/services/auction_worklet/trusted_signals.h" #include "content/services/auction_worklet/worklet_loader.h" #include "gin/converter.h" #include "gin/dictionary.h" @@ -180,25 +180,23 @@ void SellerWorklet::ScoreAd( score_ad_task->browser_signal_interest_group_owner = browser_signal_interest_group_owner; score_ad_task->browser_signal_render_url = browser_signal_render_url; - score_ad_task->browser_signal_ad_components = browser_signal_ad_components; + for (const GURL& url : browser_signal_ad_components) { + score_ad_task->browser_signal_ad_components.emplace_back(url.spec()); + } score_ad_task->browser_signal_bidding_duration_msecs = browser_signal_bidding_duration_msecs; score_ad_task->callback = std::move(callback); - std::set ad_component_render_urls(browser_signal_ad_components.begin(), - browser_signal_ad_components.end()); - if (score_ad_task->auction_config->trusted_scoring_signals_url) { - score_ad_task->trusted_scoring_signals = - std::make_unique( - url_loader_factory_.get(), - /*render_urls=*/std::set{browser_signal_render_url}, - std::move(ad_component_render_urls), - browser_signal_top_window_origin.host(), - *score_ad_task->auction_config->trusted_scoring_signals_url, - v8_helper_, - base::BindOnce(&SellerWorklet::OnTrustedScoringSignalsDownloaded, - base::Unretained(this), score_ad_task)); + score_ad_task->trusted_scoring_signals = TrustedSignals::LoadScoringSignals( + url_loader_factory_.get(), + /*render_urls=*/ + std::vector{browser_signal_render_url.spec()}, + score_ad_task->browser_signal_ad_components, + browser_signal_top_window_origin.host(), + *score_ad_task->auction_config->trusted_scoring_signals_url, v8_helper_, + base::BindOnce(&SellerWorklet::OnTrustedScoringSignalsDownloaded, + base::Unretained(this), score_ad_task)); return; } @@ -262,11 +260,11 @@ void SellerWorklet::V8State::ScoreAd( const std::string& ad_metadata_json, double bid, blink::mojom::AuctionAdConfigPtr auction_config, - std::unique_ptr trusted_scoring_signals, + std::unique_ptr trusted_scoring_signals, const url::Origin& browser_signal_top_window_origin, const url::Origin& browser_signal_interest_group_owner, const GURL& browser_signal_render_url, - const std::vector& browser_signal_ad_components, + const std::vector& browser_signal_ad_components, uint32_t browser_signal_bidding_duration_msecs, ScoreAdCallbackInternal callback) { DCHECK_CALLED_ON_VALID_SEQUENCE(v8_sequence_checker_); @@ -295,15 +293,9 @@ void SellerWorklet::V8State::ScoreAd( v8::Local trusted_scoring_signals_value; if (trusted_scoring_signals) { - // TODO(mmenke): It's doubtless more efficient to just make GetSignals() - // take a vector of ad components instead of a set, though this API will - // likely change when we implement caching, anyways. - std::set ad_component_render_urls( - browser_signal_ad_components.begin(), - browser_signal_ad_components.end()); - trusted_scoring_signals_value = trusted_scoring_signals->GetSignals( + trusted_scoring_signals_value = trusted_scoring_signals->GetScoringSignals( v8_helper_.get(), context, browser_signal_render_url, - ad_component_render_urls); + browser_signal_ad_components); } else { trusted_scoring_signals_value = v8::Null(isolate); } @@ -325,11 +317,8 @@ void SellerWorklet::V8State::ScoreAd( return; } if (!browser_signal_ad_components.empty()) { - std::vector ad_component_url_strings; - for (const GURL& url : browser_signal_ad_components) { - ad_component_url_strings.push_back(url.spec()); - } - if (!browser_signals_dict.Set("adComponents", ad_component_url_strings)) { + if (!browser_signals_dict.Set("adComponents", + browser_signal_ad_components)) { PostScoreAdCallbackToUserThread(std::move(callback), /*score=*/0, /*errors=*/std::vector()); return; @@ -541,7 +530,7 @@ void SellerWorklet::OnDownloadComplete(WorkletLoader::Result worklet_script, void SellerWorklet::OnTrustedScoringSignalsDownloaded( ScoreAdTaskList::iterator task, - std::unique_ptr result, + std::unique_ptr result, absl::optional error_msg) { DCHECK_CALLED_ON_VALID_SEQUENCE(user_sequence_checker_); diff --git a/content/services/auction_worklet/seller_worklet.h b/content/services/auction_worklet/seller_worklet.h index 2db775a95345f..e5d698c4a4a07 100644 --- a/content/services/auction_worklet/seller_worklet.h +++ b/content/services/auction_worklet/seller_worklet.h @@ -18,7 +18,7 @@ #include "content/services/auction_worklet/public/mojom/auction_worklet_service.mojom-forward.h" #include "content/services/auction_worklet/public/mojom/auction_worklet_service.mojom.h" #include "content/services/auction_worklet/public/mojom/seller_worklet.mojom.h" -#include "content/services/auction_worklet/trusted_scoring_signals.h" +#include "content/services/auction_worklet/trusted_signals.h" #include "content/services/auction_worklet/worklet_loader.h" #include "mojo/public/cpp/bindings/pending_remote.h" #include "mojo/public/cpp/bindings/remote.h" @@ -93,12 +93,15 @@ class SellerWorklet : public mojom::SellerWorklet { url::Origin browser_signal_top_window_origin; url::Origin browser_signal_interest_group_owner; GURL browser_signal_render_url; - std::vector browser_signal_ad_components; + // While these are URLs, it's more concenient to store these as strings + // rather than GURLs, both for creating a v8 array from, and for sharing + // ScoringSignals code with BidderWorklets. + std::vector browser_signal_ad_components; uint32_t browser_signal_bidding_duration_msecs; ScoreAdCallback callback; - std::unique_ptr trusted_scoring_signals; + std::unique_ptr trusted_scoring_signals; // Error message from downloading trusted scoring signals, if any. Prepended // to errors passed to the ScoreAdCallback. @@ -129,11 +132,11 @@ class SellerWorklet : public mojom::SellerWorklet { const std::string& ad_metadata_json, double bid, blink::mojom::AuctionAdConfigPtr auction_config, - std::unique_ptr trusted_scoring_signals, + std::unique_ptr trusted_scoring_signals, const url::Origin& browser_signal_top_window_origin, const url::Origin& browser_signal_interest_group_owner, const GURL& browser_signal_render_url, - const std::vector& browser_signal_ad_components, + const std::vector& browser_signal_ad_components, uint32_t browser_signal_bidding_duration_msecs, ScoreAdCallbackInternal callback); @@ -193,7 +196,7 @@ class SellerWorklet : public mojom::SellerWorklet { // V8 thread. void OnTrustedScoringSignalsDownloaded( ScoreAdTaskList::iterator task, - std::unique_ptr result, + std::unique_ptr result, absl::optional error_msg); void DeliverScoreAdCallbackOnUserThread(ScoreAdTaskList::iterator task, diff --git a/content/services/auction_worklet/trusted_bidding_signals.cc b/content/services/auction_worklet/trusted_bidding_signals.cc deleted file mode 100644 index ce23b6b487de5..0000000000000 --- a/content/services/auction_worklet/trusted_bidding_signals.cc +++ /dev/null @@ -1,189 +0,0 @@ -// Copyright 2021 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include "content/services/auction_worklet/trusted_bidding_signals.h" - -#include -#include -#include - -#include "base/bind.h" -#include "base/callback.h" -#include "base/logging.h" -#include "base/strings/strcat.h" -#include "content/services/auction_worklet/auction_downloader.h" -#include "content/services/auction_worklet/auction_v8_helper.h" -#include "gin/converter.h" -#include "net/base/escape.h" -#include "services/network/public/mojom/url_loader_factory.mojom-forward.h" -#include "url/gurl.h" -#include "v8/include/v8-context.h" -#include "v8/include/v8-json.h" -#include "v8/include/v8-object.h" -#include "v8/include/v8-primitive.h" - -namespace auction_worklet { - -TrustedBiddingSignals::Result::Result( - std::map json_data) - : json_data_(std::move(json_data)) {} - -TrustedBiddingSignals::Result::~Result() = default; - -v8::Local TrustedBiddingSignals::Result::GetSignals( - AuctionV8Helper* v8_helper, - v8::Local context, - const std::vector& trusted_bidding_signals_keys) const { - v8::Local v8_object = v8::Object::New(v8_helper->isolate()); - for (const auto& key : trusted_bidding_signals_keys) { - auto data = json_data_.find(key); - // InsertJsonValue() shouldn't be able to fail, but the first check might. - if (data == json_data_.end() || - !v8_helper->InsertJsonValue(context, key, data->second, v8_object)) { - bool result = v8_helper->InsertValue(key, v8::Null(v8_helper->isolate()), - v8_object); - DCHECK(result); - } - } - return v8_object; -} - -TrustedBiddingSignals::TrustedBiddingSignals( - network::mojom::URLLoaderFactory* url_loader_factory, - std::vector trusted_bidding_signals_keys, - const std::string& hostname, - const GURL& trusted_bidding_signals_url, - scoped_refptr v8_helper, - LoadSignalsCallback load_signals_callback) - : trusted_bidding_signals_url_(trusted_bidding_signals_url), - v8_helper_(std::move(v8_helper)), - load_signals_callback_(std::move(load_signals_callback)) { - DCHECK(v8_helper_); - DCHECK(!trusted_bidding_signals_keys.empty()); - DCHECK(load_signals_callback_); - - std::string query_params = - "hostname=" + net::EscapeQueryParamValue(hostname, true); - - query_params += "&keys="; - bool first_key = true; - for (const auto& key : trusted_bidding_signals_keys) { - if (first_key) { - first_key = false; - } else { - query_params.append(","); - } - query_params.append(net::EscapeQueryParamValue(key, true)); - } - - GURL::Replacements replacements; - replacements.SetQueryStr(query_params); - GURL final_url = trusted_bidding_signals_url.ReplaceComponents(replacements); - - auction_downloader_ = std::make_unique( - url_loader_factory, final_url, AuctionDownloader::MimeType::kJson, - base::BindOnce(&TrustedBiddingSignals::OnDownloadComplete, - base::Unretained(this), - std::move(trusted_bidding_signals_keys))); -} - -TrustedBiddingSignals::~TrustedBiddingSignals() = default; - -void TrustedBiddingSignals::OnDownloadComplete( - std::vector trusted_bidding_signals_keys, - std::unique_ptr body, - absl::optional error_msg) { - auction_downloader_.reset(); - - v8_helper_->v8_runner()->PostTask( - FROM_HERE, - base::BindOnce(&TrustedBiddingSignals::HandleDownloadResultOnV8Thread, - v8_helper_, trusted_bidding_signals_url_, - std::move(trusted_bidding_signals_keys), std::move(body), - std::move(error_msg), - base::SequencedTaskRunnerHandle::Get(), - weak_ptr_factory.GetWeakPtr())); -} - -// static -void TrustedBiddingSignals::HandleDownloadResultOnV8Thread( - scoped_refptr v8_helper, - const GURL& trusted_bidding_signals_url, - std::vector trusted_bidding_signals_keys, - std::unique_ptr body, - absl::optional error_msg, - scoped_refptr user_thread_task_runner, - base::WeakPtr weak_instance) { - if (!body) { - PostCallbackToUserThread(std::move(user_thread_task_runner), weak_instance, - nullptr, std::move(error_msg)); - return; - } - - DCHECK(!error_msg.has_value()); - - AuctionV8Helper::FullIsolateScope isolate_scope(v8_helper.get()); - v8::Context::Scope context_scope(v8_helper->scratch_context()); - - v8::Local v8_data; - if (!v8_helper->CreateValueFromJson(v8_helper->scratch_context(), *body) - .ToLocal(&v8_data) || - !v8_data->IsObject()) { - std::string error = base::StrCat({trusted_bidding_signals_url.spec(), - " Unable to parse as a JSON object."}); - PostCallbackToUserThread(std::move(user_thread_task_runner), weak_instance, - nullptr, std::move(error)); - return; - } - - v8::Local v8_object = v8_data.As(); - - std::map json_data; - for (const auto& key : trusted_bidding_signals_keys) { - v8::Local v8_key; - if (!v8_helper->CreateUtf8String(key).ToLocal(&v8_key)) { - PostCallbackToUserThread(std::move(user_thread_task_runner), - weak_instance, nullptr, absl::nullopt); - - return; - } - // Only the Get() call should be able to fail. - v8::Local v8_value; - v8::Local v8_string_value; - std::string value; - if (!v8_object->Get(v8_helper->scratch_context(), v8_key) - .ToLocal(&v8_value) || - !v8::JSON::Stringify(v8_helper->scratch_context(), v8_value) - .ToLocal(&v8_string_value) || - !gin::ConvertFromV8(v8_helper->isolate(), v8_string_value, &value)) { - continue; - } - json_data[key] = std::move(value); - } - - PostCallbackToUserThread(std::move(user_thread_task_runner), weak_instance, - std::make_unique(std::move(json_data)), - absl::nullopt); -} - -// static -void TrustedBiddingSignals::PostCallbackToUserThread( - scoped_refptr user_thread_task_runner, - base::WeakPtr weak_instance, - std::unique_ptr result, - absl::optional error_msg) { - user_thread_task_runner->PostTask( - FROM_HERE, - base::BindOnce(&TrustedBiddingSignals::DeliverCallbackOnUserThread, - weak_instance, std::move(result), std::move(error_msg))); -} - -void TrustedBiddingSignals::DeliverCallbackOnUserThread( - std::unique_ptr result, - absl::optional error_msg) { - std::move(load_signals_callback_) - .Run(std::move(result), std::move(error_msg)); -} - -} // namespace auction_worklet diff --git a/content/services/auction_worklet/trusted_bidding_signals.h b/content/services/auction_worklet/trusted_bidding_signals.h deleted file mode 100644 index 29900c953dad2..0000000000000 --- a/content/services/auction_worklet/trusted_bidding_signals.h +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright 2021 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#ifndef CONTENT_SERVICES_AUCTION_WORKLET_TRUSTED_BIDDING_SIGNALS_H_ -#define CONTENT_SERVICES_AUCTION_WORKLET_TRUSTED_BIDDING_SIGNALS_H_ - -#include -#include -#include -#include - -#include "base/callback.h" -#include "services/network/public/mojom/url_loader_factory.mojom-forward.h" -#include "third_party/abseil-cpp/absl/types/optional.h" -#include "url/gurl.h" -#include "v8/include/v8-forward.h" - -namespace auction_worklet { - -class AuctionDownloader; -class AuctionV8Helper; - -// Represents the trusted bidding signals that are part of the FLEDGE bidding -// system (https://github.com/WICG/turtledove/blob/main/FLEDGE.md). Fetches and -// parses the hosted JSON data files needed by the bidder worklets. -// -// TODO(mmenke): This class currently does 4 copies when loading the data (To V8 -// string, use V8's JSON parser, split data into V8 JSON subcomponent strings, -// convert to C++ strings), and 2 copies of each substring to use the data (To -// V8 per-key JSON string, use V8's JSON parser). Keeping the data stored as V8 -// JSON subcomponents would remove 2 copies, without too much complexity. Could -// even implement V8 deep-copy logic, to remove two more copies (counting the -// clone operation as a copy). -class TrustedBiddingSignals { - public: - // Contains the values returned by the server. - // - // This can be created and destroyed on any thread, but GetSignals() can only - // be used on the V8 thread. - class Result { - public: - explicit Result(std::map json_data); - explicit Result(const Result&) = delete; - ~Result(); - Result& operator=(const Result&) = delete; - - // Get the signals associated with the provided `keys`. `v8_helper`'s - // Isolate must be active (in particular, this must be on the v8 thread), - // and `context` must be the active context. `keys` must be a subset of - // those provided when creating the TrustedBiddingSignals object. Always - // returns a non-empty value (which may be an Object with no fields). - v8::Local GetSignals( - AuctionV8Helper* v8_helper, - v8::Local context, - const std::vector& trusted_bidding_signals_keys) const; - - private: - // Map of keys to their associated JSON data. - std::map json_data_; - }; - - using LoadSignalsCallback = - base::OnceCallback result, - absl::optional error_msg)>; - - // Starts loading the JSON data on construction. `trusted_bidding_signals_url` - // must be the base URL (no query params added). Callback will be invoked - // asynchronously once the data has been fetched or an error has occurred. - // Fails if the URL already has a query param (or has a location or embedded - // credentials) or if the response is not JSON. If some or all keys are - // missing, still succeeds, and GetSignals() will populate them with nulls. - // - // There are no lifetime constraints of `url_loader_factory`. - TrustedBiddingSignals(network::mojom::URLLoaderFactory* url_loader_factory, - std::vector trusted_bidding_signals_keys, - const std::string& hostname, - const GURL& trusted_bidding_signals_url, - scoped_refptr v8_helper, - LoadSignalsCallback load_signals_callback); - explicit TrustedBiddingSignals(const TrustedBiddingSignals&) = delete; - TrustedBiddingSignals& operator=(const TrustedBiddingSignals&) = delete; - ~TrustedBiddingSignals(); - - private: - void OnDownloadComplete(std::vector trusted_bidding_signals_keys, - std::unique_ptr body, - absl::optional error_msg); - - static void HandleDownloadResultOnV8Thread( - scoped_refptr v8_helper, - const GURL& trusted_bidding_signals_url, - std::vector trusted_bidding_signals_keys, - std::unique_ptr body, - absl::optional error_msg, - scoped_refptr user_thread_task_runner, - base::WeakPtr weak_instance); - - // Called from V8 thread. - static void PostCallbackToUserThread( - scoped_refptr user_thread_task_runner, - base::WeakPtr weak_instance, - std::unique_ptr result, - absl::optional error_msg); - - // Called on user thread. - void DeliverCallbackOnUserThread(std::unique_ptr, - absl::optional error_msg); - - const GURL trusted_bidding_signals_url_; // original, for error messages. - const scoped_refptr v8_helper_; - - LoadSignalsCallback load_signals_callback_; - std::unique_ptr auction_downloader_; - - base::WeakPtrFactory weak_ptr_factory{this}; -}; - -} // namespace auction_worklet - -#endif // CONTENT_SERVICES_AUCTION_WORKLET_TRUSTED_BIDDING_SIGNALS_H_ diff --git a/content/services/auction_worklet/trusted_bidding_signals_unittest.cc b/content/services/auction_worklet/trusted_bidding_signals_unittest.cc deleted file mode 100644 index e063d8bc404f5..0000000000000 --- a/content/services/auction_worklet/trusted_bidding_signals_unittest.cc +++ /dev/null @@ -1,241 +0,0 @@ -// Copyright 2021 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include "content/services/auction_worklet/trusted_bidding_signals.h" - -#include -#include -#include - -#include "base/bind.h" -#include "base/run_loop.h" -#include "base/synchronization/waitable_event.h" -#include "base/test/task_environment.h" -#include "content/services/auction_worklet/auction_v8_helper.h" -#include "content/services/auction_worklet/worklet_test_util.h" -#include "net/http/http_status_code.h" -#include "services/network/test/test_url_loader_factory.h" -#include "testing/gtest/include/gtest/gtest.h" -#include "url/gurl.h" -#include "v8/include/v8-context.h" -#include "v8/include/v8-forward.h" - -namespace auction_worklet { -namespace { - -// Common JSON used for most tests. Key 4 is deliberately skipped. -const char kBaseJson[] = R"( - { - "key1": 1, - "key2": [2], - "key3": null, - "key5": "value5", - "key 6": 6, - "key=7": 7, - "key,8": 8 - } -)"; - -const char kHostname[] = "publisher"; - -class TrustedBiddingSignalsTest : public testing::Test { - public: - TrustedBiddingSignalsTest() { - v8_helper_ = AuctionV8Helper::Create(AuctionV8Helper::CreateTaskRunner()); - } - - ~TrustedBiddingSignalsTest() override { task_environment_.RunUntilIdle(); } - - // Sets the HTTP response and then fetches bidding signals and waits for - // completion. Returns nullptr on failure. - std::unique_ptr - FetchBiddingSignalsWithResponse( - const GURL& url, - const std::string& response, - std::vector trusted_bidding_signals_keys, - const std::string& hostname) { - AddJsonResponse(&url_loader_factory_, url, response); - return FetchBiddingSignals(trusted_bidding_signals_keys, hostname); - } - - // Fetches bidding signals and waits for completion. Returns nullptr on - // failure. - std::unique_ptr FetchBiddingSignals( - std::vector trusted_bidding_signals_keys, - const std::string& hostname) { - CHECK(!load_signals_run_loop_); - - DCHECK(!load_signals_result_); - auto bidding_signals = std::make_unique( - &url_loader_factory_, std::move(trusted_bidding_signals_keys), - std::move(hostname), base_url_, v8_helper_, - base::BindOnce(&TrustedBiddingSignalsTest::LoadSignalsCallback, - base::Unretained(this))); - load_signals_run_loop_ = std::make_unique(); - load_signals_run_loop_->Run(); - load_signals_run_loop_.reset(); - return std::move(load_signals_result_); - } - - // Returns the results of calling TrustedBiddingSignals::Result::GetSignals() - // with `trusted_bidding_signals_keys`. Returns value as a JSON std::string, - // for easy testing. - std::string ExtractSignals( - TrustedBiddingSignals::Result* signals, - std::vector trusted_bidding_signals_keys) { - base::RunLoop run_loop; - - std::string result; - v8_helper_->v8_runner()->PostTask( - FROM_HERE, - base::BindOnce( - [](scoped_refptr v8_helper, - TrustedBiddingSignals::Result* signals, - std::vector trusted_bidding_signals_keys, - std::string* result_out, base::OnceClosure quit_closure) { - AuctionV8Helper::FullIsolateScope isolate_scope(v8_helper.get()); - v8::Isolate* isolate = v8_helper->isolate(); - // Could use the scratch context, but using a separate one more - // closely resembles actual use. - v8::Local context = v8::Context::New(isolate); - v8::Context::Scope context_scope(context); - - v8::Local value = signals->GetSignals( - v8_helper.get(), context, trusted_bidding_signals_keys); - - if (!v8_helper->ExtractJson(context, value, result_out)) { - *result_out = "JSON extraction failed."; - } - std::move(quit_closure).Run(); - }, - v8_helper_, signals, std::move(trusted_bidding_signals_keys), - &result, run_loop.QuitClosure())); - run_loop.Run(); - return result; - } - - protected: - void LoadSignalsCallback( - std::unique_ptr result, - absl::optional error_msg) { - load_signals_result_ = std::move(result); - error_msg_ = std::move(error_msg); - EXPECT_EQ(load_signals_result_ == nullptr, error_msg_.has_value()); - load_signals_run_loop_->Quit(); - } - - base::test::TaskEnvironment task_environment_; - - // URL without query params attached. - const GURL base_url_ = GURL("https://url.test/"); - - // Reuseable run loop for loading the signals. It's always populated after - // creating the worklet, to cause a crash if the callback is invoked - // synchronously. - std::unique_ptr load_signals_run_loop_; - std::unique_ptr load_signals_result_; - absl::optional error_msg_; - - network::TestURLLoaderFactory url_loader_factory_; - scoped_refptr v8_helper_; -}; - -TEST_F(TrustedBiddingSignalsTest, NetworkError) { - url_loader_factory_.AddResponse( - "https://url.test/?hostname=publisher&keys=key1", kBaseJson, - net::HTTP_NOT_FOUND); - EXPECT_FALSE(FetchBiddingSignals({"key1"}, kHostname)); - ASSERT_TRUE(error_msg_.has_value()); - EXPECT_EQ( - "Failed to load https://url.test/?hostname=publisher&keys=key1 " - "HTTP status = 404 Not Found.", - error_msg_.value()); -} - -TEST_F(TrustedBiddingSignalsTest, ResponseNotJson) { - EXPECT_FALSE(FetchBiddingSignalsWithResponse( - GURL("https://url.test/?hostname=publisher&keys=key1"), "Not Json", - {"key1"}, kHostname)); - ASSERT_TRUE(error_msg_.has_value()); - EXPECT_EQ("https://url.test/ Unable to parse as a JSON object.", - error_msg_.value()); -} - -TEST_F(TrustedBiddingSignalsTest, ResponseNotObject) { - EXPECT_FALSE(FetchBiddingSignalsWithResponse( - GURL("https://url.test/?hostname=publisher&keys=key1"), "42", {"key1"}, - kHostname)); - ASSERT_TRUE(error_msg_.has_value()); - EXPECT_EQ("https://url.test/ Unable to parse as a JSON object.", - error_msg_.value()); -} - -TEST_F(TrustedBiddingSignalsTest, KeyMissing) { - std::unique_ptr signals = - FetchBiddingSignalsWithResponse( - GURL("https://url.test/?hostname=publisher&keys=key4"), kBaseJson, - {"key4"}, kHostname); - ASSERT_TRUE(signals); - EXPECT_EQ(R"({"key4":null})", ExtractSignals(signals.get(), {"key4"})); -} - -TEST_F(TrustedBiddingSignalsTest, FetchOneKey) { - std::unique_ptr signals = - FetchBiddingSignalsWithResponse( - GURL("https://url.test/?hostname=publisher&keys=key1"), kBaseJson, - {"key1"}, kHostname); - ASSERT_TRUE(signals); - EXPECT_EQ(R"({"key1":1})", ExtractSignals(signals.get(), {"key1"})); -} - -TEST_F(TrustedBiddingSignalsTest, FetchMultipleKeys) { - std::unique_ptr signals = - FetchBiddingSignalsWithResponse( - GURL("https://url.test/?hostname=publisher&keys=key3,key1,key5,key2"), - kBaseJson, {"key3", "key1", "key5", "key2"}, kHostname); - ASSERT_TRUE(signals); - EXPECT_EQ(R"({"key1":1})", ExtractSignals(signals.get(), {"key1"})); - EXPECT_EQ(R"({"key2":[2]})", ExtractSignals(signals.get(), {"key2"})); - EXPECT_EQ(R"({"key3":null})", ExtractSignals(signals.get(), {"key3"})); - EXPECT_EQ(R"({"key5":"value5"})", ExtractSignals(signals.get(), {"key5"})); - EXPECT_EQ(R"({"key1":1,"key2":[2],"key3":null,"key5":"value5"})", - ExtractSignals(signals.get(), {"key1", "key2", "key3", "key5"})); -} - -TEST_F(TrustedBiddingSignalsTest, EscapeQueryParams) { - std::unique_ptr signals = - FetchBiddingSignalsWithResponse( - GURL("https://url.test/" - "?hostname=pub+li%26sher&keys=key+6,key%3D7,key%2C8"), - kBaseJson, {"key 6", "key=7", "key,8"}, "pub li&sher"); - ASSERT_TRUE(signals); - EXPECT_EQ(R"({"key 6":6})", ExtractSignals(signals.get(), {"key 6"})); - EXPECT_EQ(R"({"key=7":7})", ExtractSignals(signals.get(), {"key=7"})); - EXPECT_EQ(R"({"key,8":8})", ExtractSignals(signals.get(), {"key,8"})); -} - -// Testcase where the loader is deleted after it queued the parsing of -// the script on V8 thread, but before it gets to finish. -TEST_F(TrustedBiddingSignalsTest, DeleteBeforeCallback) { - GURL url("https://url.test/?hostname=publisher&keys=key1"); - - AddJsonResponse(&url_loader_factory_, url, kBaseJson); - - // Wedge the V8 thread to control when the JSON parsing takes place. - base::WaitableEvent* event_handle = WedgeV8Thread(v8_helper_.get()); - - auto bidding_signals = std::make_unique( - &url_loader_factory_, std::vector{"key1"}, "publisher", - base_url_, v8_helper_, - base::BindOnce([](std::unique_ptr result, - absl::optional error_msg) { - ADD_FAILURE() << "Callback should not be invoked since loader deleted"; - })); - base::RunLoop().RunUntilIdle(); - bidding_signals.reset(); - event_handle->Signal(); -} - -} // namespace -} // namespace auction_worklet diff --git a/content/services/auction_worklet/trusted_scoring_signals.cc b/content/services/auction_worklet/trusted_scoring_signals.cc deleted file mode 100644 index ffd05992c661d..0000000000000 --- a/content/services/auction_worklet/trusted_scoring_signals.cc +++ /dev/null @@ -1,271 +0,0 @@ -// Copyright 2021 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include "content/services/auction_worklet/trusted_scoring_signals.h" - -#include -#include -#include - -#include "base/bind.h" -#include "base/callback.h" -#include "base/logging.h" -#include "base/strings/strcat.h" -#include "base/strings/stringprintf.h" -#include "content/services/auction_worklet/auction_downloader.h" -#include "content/services/auction_worklet/auction_v8_helper.h" -#include "gin/converter.h" -#include "net/base/escape.h" -#include "services/network/public/mojom/url_loader_factory.mojom-forward.h" -#include "url/gurl.h" -#include "v8/include/v8-context.h" -#include "v8/include/v8-json.h" -#include "v8/include/v8-object.h" -#include "v8/include/v8-primitive.h" - -namespace auction_worklet { - -namespace { - -// Creates a query param of the form `&=`. -// Returns an empty string if `urls` is empty. `name` will not be escaped, but -// entries in `urls` will be. -std::string CreateQueryParam(const char* name, const std::set& urls) { - if (urls.empty()) - return std::string(); - std::string query_param = base::StringPrintf("&%s=", name); - bool first_key = true; - for (const auto& url : urls) { - if (first_key) { - first_key = false; - } else { - query_param.append(","); - } - query_param.append( - net::EscapeQueryParamValue(url.spec(), /*use_plus=*/true)); - } - return query_param; -} - -// Extracts GURL/JSON key/value pairs from the object named `name` in -// `v8_object`, using values in `urls` as keys. Does not add entries to the map -// for keys with missing values. -std::map ExtractUrlMap(AuctionV8Helper* v8_helper, - v8::Local v8_object, - const char* name, - const std::set& urls) { - std::map out; - if (urls.empty()) - return out; - - v8::Local named_object_value; - // Don't consider the entire object missing a fatal error. - if (!v8_object - ->Get(v8_helper->scratch_context(), - v8_helper->CreateStringFromLiteral(name)) - .ToLocal(&named_object_value) || - !named_object_value->IsObject()) { - return out; - } - - v8::Local named_object = named_object_value.As(); - for (const auto& url : urls) { - v8::Local v8_key; - if (!v8_helper->CreateUtf8String(url.spec()).ToLocal(&v8_key)) - continue; - - v8::Local v8_value; - v8::Local v8_string_value; - std::string value; - // Only the Get() call should be able to fail. - if (!named_object->Get(v8_helper->scratch_context(), v8_key) - .ToLocal(&v8_value) || - !v8::JSON::Stringify(v8_helper->scratch_context(), v8_value) - .ToLocal(&v8_string_value) || - !gin::ConvertFromV8(v8_helper->isolate(), v8_string_value, &value)) { - continue; - } - out[url] = std::move(value); - } - return out; -} - -} // namespace - -TrustedScoringSignals::Result::Result( - std::map render_url_json_data, - std::map ad_component_json_data) - : render_url_json_data_(std::move(render_url_json_data)), - ad_component_json_data_(std::move(ad_component_json_data)) {} - -TrustedScoringSignals::Result::~Result() = default; - -v8::Local TrustedScoringSignals::Result::GetSignals( - AuctionV8Helper* v8_helper, - v8::Local context, - const GURL& render_url, - const std::set& ad_component_render_urls) const { - v8::Local out = v8::Object::New(v8_helper->isolate()); - - // Create renderUrl sub-object, and add it to to `out`. - v8::Local render_url_v8_object = - v8::Object::New(v8_helper->isolate()); - auto render_url_data = render_url_json_data_.find(render_url); - // InsertJsonValue() shouldn't be able to fail, but the first check might. - if (render_url_data == render_url_json_data_.end() || - !v8_helper->InsertJsonValue(context, render_url.spec(), - render_url_data->second, - render_url_v8_object)) { - bool result = v8_helper->InsertValue(render_url.spec(), - v8::Null(v8_helper->isolate()), - render_url_v8_object); - DCHECK(result); - } - bool result = v8_helper->InsertValue("renderUrl", render_url_v8_object, out); - DCHECK(result); - - // If there are any ad components, assemble and add an `adComponentRenderUrls` - // object as well. - if (!ad_component_render_urls.empty()) { - v8::Local ad_components_v8_object = - v8::Object::New(v8_helper->isolate()); - for (const auto& url : ad_component_render_urls) { - auto data = ad_component_json_data_.find(url); - // InsertJsonValue() shouldn't be able to fail, but the first check might. - if (data == ad_component_json_data_.end() || - !v8_helper->InsertJsonValue(context, url.spec(), data->second, - ad_components_v8_object)) { - bool result = - v8_helper->InsertValue(url.spec(), v8::Null(v8_helper->isolate()), - ad_components_v8_object); - DCHECK(result); - } - } - bool result = v8_helper->InsertValue("adComponentRenderUrls", - ad_components_v8_object, out); - DCHECK(result); - } - - return out; -} - -TrustedScoringSignals::TrustedScoringSignals( - network::mojom::URLLoaderFactory* url_loader_factory, - std::set render_urls, - std::set ad_component_render_urls, - const std::string& hostname, - const GURL& trusted_scoring_signals_url, - scoped_refptr v8_helper, - LoadSignalsCallback load_signals_callback) - : trusted_scoring_signals_url_(trusted_scoring_signals_url), - v8_helper_(std::move(v8_helper)), - load_signals_callback_(std::move(load_signals_callback)) { - DCHECK(v8_helper_); - // Allow `render_urls` or `ad_component_render_urls` to be empty, but not - // both. - DCHECK(!render_urls.empty() || !ad_component_render_urls.empty()); - DCHECK(load_signals_callback_); - - std::string query_params = - "hostname=" + net::EscapeQueryParamValue(hostname, true) + - CreateQueryParam("renderUrls", render_urls) + - CreateQueryParam("adComponentRenderUrls", ad_component_render_urls); - - GURL::Replacements replacements; - replacements.SetQueryStr(query_params); - GURL final_url = trusted_scoring_signals_url.ReplaceComponents(replacements); - - auction_downloader_ = std::make_unique( - url_loader_factory, final_url, AuctionDownloader::MimeType::kJson, - base::BindOnce(&TrustedScoringSignals::OnDownloadComplete, - base::Unretained(this), std::move(render_urls), - std::move(ad_component_render_urls))); -} - -TrustedScoringSignals::~TrustedScoringSignals() = default; - -void TrustedScoringSignals::OnDownloadComplete( - std::set render_urls, - std::set ad_component_render_urls, - std::unique_ptr body, - absl::optional error_msg) { - auction_downloader_.reset(); - - v8_helper_->v8_runner()->PostTask( - FROM_HERE, - base::BindOnce( - &TrustedScoringSignals::HandleDownloadResultOnV8Thread, v8_helper_, - trusted_scoring_signals_url_, std::move(render_urls), - std::move(ad_component_render_urls), std::move(body), - std::move(error_msg), base::SequencedTaskRunnerHandle::Get(), - weak_ptr_factory.GetWeakPtr())); -} - -// static -void TrustedScoringSignals::HandleDownloadResultOnV8Thread( - scoped_refptr v8_helper, - const GURL& trusted_scoring_signals_url, - std::set render_urls, - std::set ad_component_render_urls, - std::unique_ptr body, - absl::optional error_msg, - scoped_refptr user_thread_task_runner, - base::WeakPtr weak_instance) { - if (!body) { - PostCallbackToUserThread(std::move(user_thread_task_runner), weak_instance, - nullptr, std::move(error_msg)); - return; - } - - DCHECK(!error_msg.has_value()); - - AuctionV8Helper::FullIsolateScope isolate_scope(v8_helper.get()); - v8::Context::Scope context_scope(v8_helper->scratch_context()); - - v8::Local v8_data; - if (!v8_helper->CreateValueFromJson(v8_helper->scratch_context(), *body) - .ToLocal(&v8_data) || - !v8_data->IsObject()) { - std::string error = base::StrCat({trusted_scoring_signals_url.spec(), - " Unable to parse as a JSON object."}); - PostCallbackToUserThread(std::move(user_thread_task_runner), weak_instance, - nullptr, std::move(error)); - return; - } - - v8::Local v8_object = v8_data.As(); - - std::map render_url_json_data = - ExtractUrlMap(v8_helper.get(), v8_object, "renderUrls", render_urls); - std::map ad_component_json_data = - ExtractUrlMap(v8_helper.get(), v8_object, "adComponentRenderUrls", - ad_component_render_urls); - - PostCallbackToUserThread( - std::move(user_thread_task_runner), weak_instance, - std::make_unique(std::move(render_url_json_data), - std::move(ad_component_json_data)), - absl::nullopt); -} - -// static -void TrustedScoringSignals::PostCallbackToUserThread( - scoped_refptr user_thread_task_runner, - base::WeakPtr weak_instance, - std::unique_ptr result, - absl::optional error_msg) { - user_thread_task_runner->PostTask( - FROM_HERE, - base::BindOnce(&TrustedScoringSignals::DeliverCallbackOnUserThread, - weak_instance, std::move(result), std::move(error_msg))); -} - -void TrustedScoringSignals::DeliverCallbackOnUserThread( - std::unique_ptr result, - absl::optional error_msg) { - std::move(load_signals_callback_) - .Run(std::move(result), std::move(error_msg)); -} - -} // namespace auction_worklet diff --git a/content/services/auction_worklet/trusted_scoring_signals.h b/content/services/auction_worklet/trusted_scoring_signals.h deleted file mode 100644 index b0c52f114fbcc..0000000000000 --- a/content/services/auction_worklet/trusted_scoring_signals.h +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright 2021 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#ifndef CONTENT_SERVICES_AUCTION_WORKLET_TRUSTED_SCORING_SIGNALS_H_ -#define CONTENT_SERVICES_AUCTION_WORKLET_TRUSTED_SCORING_SIGNALS_H_ - -#include -#include -#include -#include - -#include "base/callback.h" -#include "services/network/public/mojom/url_loader_factory.mojom-forward.h" -#include "third_party/abseil-cpp/absl/types/optional.h" -#include "url/gurl.h" -#include "v8/include/v8-forward.h" - -namespace auction_worklet { - -class AuctionDownloader; -class AuctionV8Helper; - -// Represents the trusted scoring signals that are part of the FLEDGE bidding -// system (https://github.com/WICG/turtledove/blob/main/FLEDGE.md). Fetches and -// parses the hosted JSON data files needed by the seller worklets. -// -// TODO(mmenke): This class currently does 4 copies when loading the data (To V8 -// string, use V8's JSON parser, split data into V8 JSON subcomponent strings, -// convert to C++ strings), and 2 copies of each substring to use the data (To -// V8 per-key JSON string, use V8's JSON parser). Keeping the data stored as V8 -// JSON subcomponents would remove 2 copies, without too much complexity. Could -// even implement V8 deep-copy logic, to remove two more copies (counting the -// clone operation as a copy). -class TrustedScoringSignals { - public: - // Contains the values returned by the server. - // - // This can be created and destroyed on any thread, but GetSignals() can only - // be used on the V8 thread. - class Result { - public: - Result(std::map render_url_json_data, - std::map ad_component_json_data); - explicit Result(const Result&) = delete; - ~Result(); - Result& operator=(const Result&) = delete; - - // Retrieves the trusted scoring signals associated with the passed in urls, - // in the format expected by a worklet's scoreAd() method. `v8_helper`'s - // Isolate must be active (in particular, this must be on the v8 thread), - // and `context` must be the active context. `render_url` and - // `ad_component_render_urls` must be subsets of the corresponding sets of - // GURLs provided when creating the TrustedScoringSignals object. Always - // returns a non-empty value. - v8::Local GetSignals( - AuctionV8Helper* v8_helper, - v8::Local context, - const GURL& render_url, - const std::set& ad_component_render_urls) const; - - private: - // Map of GURLs to their associated JSON data. - std::map render_url_json_data_; - std::map ad_component_json_data_; - }; - - using LoadSignalsCallback = - base::OnceCallback result, - absl::optional error_msg)>; - - // Starts loading the JSON data on construction. `trusted_scoring_signals_url` - // must be the base URL (no query params added). Callback will be invoked - // asynchronously once the data has been fetched or an error has occurred. - // Fails if the URL already has a query param (or has a location or embedded - // credentials) or if the response is not JSON. If some or all of the render - // URLs are missing, still succeeds, and GetSignals() will populate them with - // nulls. - // - // There are no lifetime constraints of `url_loader_factory`. - TrustedScoringSignals(network::mojom::URLLoaderFactory* url_loader_factory, - std::set render_urls, - std::set ad_component_render_urls, - const std::string& hostname, - const GURL& trusted_scoring_signals_url, - scoped_refptr v8_helper, - LoadSignalsCallback load_signals_callback); - explicit TrustedScoringSignals(const TrustedScoringSignals&) = delete; - TrustedScoringSignals& operator=(const TrustedScoringSignals&) = delete; - ~TrustedScoringSignals(); - - private: - void OnDownloadComplete(std::set render_urls, - std::set ad_component_render_urls, - std::unique_ptr body, - absl::optional error_msg); - - static void HandleDownloadResultOnV8Thread( - scoped_refptr v8_helper, - const GURL& trusted_scoring_signals_url, - std::set render_urls, - std::set ad_component_render_urls, - std::unique_ptr body, - absl::optional error_msg, - scoped_refptr user_thread_task_runner, - base::WeakPtr weak_instance); - - // Called from V8 thread. - static void PostCallbackToUserThread( - scoped_refptr user_thread_task_runner, - base::WeakPtr weak_instance, - std::unique_ptr result, - absl::optional error_msg); - - // Called on user thread. - void DeliverCallbackOnUserThread(std::unique_ptr, - absl::optional error_msg); - - const GURL trusted_scoring_signals_url_; // original, for error messages. - const scoped_refptr v8_helper_; - - LoadSignalsCallback load_signals_callback_; - std::unique_ptr auction_downloader_; - - base::WeakPtrFactory weak_ptr_factory{this}; -}; - -} // namespace auction_worklet - -#endif // CONTENT_SERVICES_AUCTION_WORKLET_TRUSTED_SCORING_SIGNALS_H_ diff --git a/content/services/auction_worklet/trusted_scoring_signals_unittest.cc b/content/services/auction_worklet/trusted_scoring_signals_unittest.cc deleted file mode 100644 index 1ef2787ea7a54..0000000000000 --- a/content/services/auction_worklet/trusted_scoring_signals_unittest.cc +++ /dev/null @@ -1,384 +0,0 @@ -// Copyright 2021 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include "content/services/auction_worklet/trusted_scoring_signals.h" - -#include -#include -#include - -#include "base/bind.h" -#include "base/run_loop.h" -#include "base/synchronization/waitable_event.h" -#include "base/test/bind.h" -#include "base/test/task_environment.h" -#include "content/services/auction_worklet/auction_v8_helper.h" -#include "content/services/auction_worklet/worklet_test_util.h" -#include "net/http/http_status_code.h" -#include "services/network/test/test_url_loader_factory.h" -#include "testing/gtest/include/gtest/gtest.h" -#include "url/gurl.h" -#include "v8/include/v8-context.h" -#include "v8/include/v8-forward.h" - -namespace auction_worklet { -namespace { - -// Common JSON used by a number of tests. -const char kBaseJson[] = R"( - { - "renderUrls": { - "https://foo.test/": 1, - "https://bar.test/": [2], - "https://baz.test/": null, - "https://shared.test/": "render url" - }, - "adComponentRenderUrls": { - "https://foosub.test/": 2, - "https://barsub.test/": [3], - "https://bazsub.test/": null, - "https://shared.test/": "ad component url" - } - } -)"; - -const char kHostname[] = "publisher"; - -class TrustedScoringSignalsTest : public testing::Test { - public: - TrustedScoringSignalsTest() { - v8_helper_ = AuctionV8Helper::Create(AuctionV8Helper::CreateTaskRunner()); - } - - ~TrustedScoringSignalsTest() override { task_environment_.RunUntilIdle(); } - - // Sets the HTTP response and then fetches scoring signals and waits for - // completion. Returns nullptr on failure. - std::unique_ptr - FetchScoringSignalsWithResponse(const GURL& url, - const std::string& response, - std::set render_urls, - std::set ad_component_render_urls, - const std::string& hostname) { - AddJsonResponse(&url_loader_factory_, url, response); - return FetchScoringSignals(render_urls, ad_component_render_urls, hostname); - } - - // Fetches scoring signals and waits for completion. Returns nullptr on - // failure. - std::unique_ptr FetchScoringSignals( - std::set render_urls, - std::set ad_component_render_urls, - const std::string& hostname) { - CHECK(!load_signals_run_loop_); - - DCHECK(!load_signals_result_); - auto scoring_signals = std::make_unique( - &url_loader_factory_, std::move(render_urls), - std::move(ad_component_render_urls), std::move(hostname), base_url_, - v8_helper_, - base::BindOnce(&TrustedScoringSignalsTest::LoadSignalsCallback, - base::Unretained(this))); - load_signals_run_loop_ = std::make_unique(); - load_signals_run_loop_->Run(); - load_signals_run_loop_.reset(); - return std::move(load_signals_result_); - } - - // Returns the results of calling TrustedScoringSignals::Result::GetSignals() - // with the provided parameters. Returns value as a JSON std::string, for easy - // testing. - std::string ExtractSignals(TrustedScoringSignals::Result* signals, - const GURL& render_url, - const std::set& ad_component_render_urls) { - base::RunLoop run_loop; - - std::string result; - v8_helper_->v8_runner()->PostTask( - FROM_HERE, base::BindLambdaForTesting([&]() { - AuctionV8Helper::FullIsolateScope isolate_scope(v8_helper_.get()); - v8::Isolate* isolate = v8_helper_->isolate(); - // Could use the scratch context, but using a separate one more - // closely resembles actual use. - v8::Local context = v8::Context::New(isolate); - v8::Context::Scope context_scope(context); - - v8::Local value = signals->GetSignals( - v8_helper_.get(), context, render_url, ad_component_render_urls); - - if (!v8_helper_->ExtractJson(context, value, &result)) { - result = "JSON extraction failed."; - } - run_loop.Quit(); - })); - run_loop.Run(); - return result; - } - - protected: - void LoadSignalsCallback( - std::unique_ptr result, - absl::optional error_msg) { - load_signals_result_ = std::move(result); - error_msg_ = std::move(error_msg); - EXPECT_EQ(load_signals_result_ == nullptr, error_msg_.has_value()); - load_signals_run_loop_->Quit(); - } - - base::test::TaskEnvironment task_environment_; - - // URL without query params attached. - const GURL base_url_ = GURL("https://url.test/"); - - // Reuseable run loop for loading the signals. It's always populated after - // creating the worklet, to cause a crash if the callback is invoked - // synchronously. - std::unique_ptr load_signals_run_loop_; - std::unique_ptr load_signals_result_; - absl::optional error_msg_; - - network::TestURLLoaderFactory url_loader_factory_; - scoped_refptr v8_helper_; -}; - -TEST_F(TrustedScoringSignalsTest, NetworkError) { - url_loader_factory_.AddResponse( - "https://url.test/" - "?hostname=publisher&renderUrls=https%3A%2F%2Ffoo.test%2F", - kBaseJson, net::HTTP_NOT_FOUND); - EXPECT_FALSE(FetchScoringSignals( - /*render_urls=*/{GURL("https://foo.test/")}, - /*ad_component_render_urls=*/{}, kHostname)); - ASSERT_TRUE(error_msg_.has_value()); - EXPECT_EQ( - "Failed to load " - "https://url.test/" - "?hostname=publisher&renderUrls=https%3A%2F%2Ffoo.test%2F " - "HTTP status = 404 Not Found.", - error_msg_.value()); -} - -TEST_F(TrustedScoringSignalsTest, ResponseNotJson) { - EXPECT_FALSE(FetchScoringSignalsWithResponse( - GURL("https://url.test/" - "?hostname=publisher&renderUrls=https%3A%2F%2Ffoo.test%2F"), - "Not Json", - /*render_urls=*/{GURL("https://foo.test/")}, - /*ad_component_render_urls=*/{}, kHostname)); - ASSERT_TRUE(error_msg_.has_value()); - EXPECT_EQ("https://url.test/ Unable to parse as a JSON object.", - error_msg_.value()); -} - -TEST_F(TrustedScoringSignalsTest, ResponseNotObject) { - EXPECT_FALSE(FetchScoringSignalsWithResponse( - GURL("https://url.test/" - "?hostname=publisher&renderUrls=https%3A%2F%2Ffoo.test%2F"), - "42", /*render_urls=*/{GURL("https://foo.test/")}, - /*ad_component_render_urls=*/{}, kHostname)); - ASSERT_TRUE(error_msg_.has_value()); - EXPECT_EQ("https://url.test/ Unable to parse as a JSON object.", - error_msg_.value()); -} - -TEST_F(TrustedScoringSignalsTest, ExpectedEntriesNotPresent) { - std::unique_ptr signals = - FetchScoringSignalsWithResponse( - GURL("https://url.test/?hostname=publisher" - "&renderUrls=https%3A%2F%2Ffoo.test%2F" - "&adComponentRenderUrls=https%3A%2F%2Fbar.test%2F"), - R"({"foo":4,"bar":5})", - /*render_urls=*/{GURL("https://foo.test/")}, - /*ad_component_render_urls=*/{GURL("https://bar.test/")}, kHostname); - ASSERT_TRUE(signals); - EXPECT_EQ( - R"({"renderUrl":{"https://foo.test/":null},"adComponentRenderUrls":{"https://bar.test/":null}})", - ExtractSignals(signals.get(), /*render_url=*/GURL("https://foo.test/"), - /*ad_component_render_urls=*/{GURL("https://bar.test/")})); - EXPECT_FALSE(error_msg_.has_value()); -} - -TEST_F(TrustedScoringSignalsTest, NestedEntriesNotObjects) { - std::unique_ptr signals = - FetchScoringSignalsWithResponse( - GURL("https://url.test/?hostname=publisher" - "&renderUrls=https%3A%2F%2Ffoo.test%2F" - "&adComponentRenderUrls=https%3A%2F%2Fbar.test%2F"), - R"({"renderUrls":4,"adComponentRenderUrls":5})", - /*render_urls=*/{GURL("https://foo.test/")}, - /*ad_component_render_urls=*/{GURL("https://bar.test/")}, kHostname); - ASSERT_TRUE(signals); - EXPECT_EQ( - R"({"renderUrl":{"https://foo.test/":null},"adComponentRenderUrls":{"https://bar.test/":null}})", - ExtractSignals(signals.get(), /*render_url=*/GURL("https://foo.test/"), - /*ad_component_render_urls=*/{GURL("https://bar.test/")})); - EXPECT_FALSE(error_msg_.has_value()); -} - -TEST_F(TrustedScoringSignalsTest, KeysMissing) { - std::unique_ptr signals = - FetchScoringSignalsWithResponse( - GURL("https://url.test/?hostname=publisher" - "&renderUrls=https%3A%2F%2Ffoo.test%2F" - "&adComponentRenderUrls=https%3A%2F%2Fbar.test%2F"), - R"({"renderUrls":{"these":"are not"},")" - R"(adComponentRenderUrls":{"the values":"you're looking for"}})", - /*render_urls=*/{GURL("https://foo.test/")}, - /*ad_component_render_urls=*/{GURL("https://bar.test/")}, kHostname); - ASSERT_TRUE(signals); - EXPECT_EQ( - R"({"renderUrl":{"https://foo.test/":null},"adComponentRenderUrls":{"https://bar.test/":null}})", - ExtractSignals(signals.get(), /*render_url=*/GURL("https://foo.test/"), - /*ad_component_render_urls=*/{GURL("https://bar.test/")})); - EXPECT_FALSE(error_msg_.has_value()); -} - -TEST_F(TrustedScoringSignalsTest, FetchForOneRenderUrl) { - std::unique_ptr signals = - FetchScoringSignalsWithResponse( - GURL("https://url.test/" - "?hostname=publisher&renderUrls=https%3A%2F%2Ffoo.test%2F"), - kBaseJson, - /*render_urls=*/{GURL("https://foo.test/")}, - /*ad_component_render_urls=*/{}, kHostname); - ASSERT_TRUE(signals); - EXPECT_EQ( - R"({"renderUrl":{"https://foo.test/":1}})", - ExtractSignals(signals.get(), /*render_url=*/GURL("https://foo.test/"), - /*ad_component_render_urls=*/{})); - EXPECT_FALSE(error_msg_.has_value()); -} - -// Currently, there's no case where a fetch will only be for ad components and -// not render URLs, but once requests are batched, it may be useful. That will -// require other API changes and a caching layer, of course. -TEST_F(TrustedScoringSignalsTest, FetchForOneAdComponentUrl) { - std::unique_ptr signals = - FetchScoringSignalsWithResponse( - GURL("https://url.test/" - "?hostname=publisher&adComponentRenderUrls=https%3A%2F%2Ffoosub." - "test%2F"), - kBaseJson, - /*render_urls=*/{}, - /*ad_component_render_urls=*/{GURL("https://foosub.test/")}, - kHostname); - ASSERT_TRUE(signals); - // Currently there's no way to extract only an ad component value. This test - // is really just about the fetching and parsing logic. - EXPECT_EQ( - R"({"renderUrl":{"https://foo.test/":null},"adComponentRenderUrls":{"https://foosub.test/":2}})", - ExtractSignals( - signals.get(), /*render_url=*/GURL("https://foo.test/"), - /*ad_component_render_urls=*/{GURL("https://foosub.test/")})); - EXPECT_FALSE(error_msg_.has_value()); -} - -TEST_F(TrustedScoringSignalsTest, FetchMultipleUrls) { - // URLs are currently added in lexical order. - std::unique_ptr signals = - FetchScoringSignalsWithResponse( - GURL("https://url.test/?hostname=publisher" - "&renderUrls=https%3A%2F%2Fbar.test%2F," - "https%3A%2F%2Fbaz.test%2F,https%3A%2F%2Ffoo.test%2F" - "&adComponentRenderUrls=https%3A%2F%2Fbarsub.test%2F," - "https%3A%2F%2Fbazsub.test%2F,https%3A%2F%2Ffoosub.test%2F"), - kBaseJson, - /*render_urls=*/ - {GURL("https://foo.test/"), GURL("https://bar.test/"), - GURL("https://baz.test/")}, - /*ad_component_render_urls=*/ - {GURL("https://foosub.test/"), GURL("https://barsub.test/"), - GURL("https://bazsub.test/")}, - kHostname); - ASSERT_TRUE(signals); - EXPECT_FALSE(error_msg_.has_value()); - EXPECT_EQ( - R"({"renderUrl":{"https://bar.test/":[2]},")" - R"(adComponentRenderUrls":{"https://barsub.test/":[3],"https://bazsub.test/":null,"https://foosub.test/":2}})", - ExtractSignals( - signals.get(), /*render_url=*/GURL("https://bar.test/"), - /*ad_component_render_urls=*/ - {GURL("https://foosub.test/"), GURL("https://barsub.test/"), - GURL("https://bazsub.test/")})); -} - -// Test when a single URL is used as both a `renderUrl` and -// `adComponentRenderUrl`. -TEST_F(TrustedScoringSignalsTest, FetchSharedUrl) { - // URLs are currently added in lexical order. - std::unique_ptr signals = - FetchScoringSignalsWithResponse( - GURL("https://url.test/?hostname=publisher" - "&renderUrls=https%3A%2F%2Fshared.test%2F" - "&adComponentRenderUrls=https%3A%2F%2Fshared.test%2F"), - kBaseJson, - /*render_urls=*/ - {GURL("https://shared.test/")}, - /*ad_component_render_urls=*/ - {GURL("https://shared.test/")}, kHostname); - ASSERT_TRUE(signals); - EXPECT_FALSE(error_msg_.has_value()); - EXPECT_EQ( - R"({"renderUrl":{"https://shared.test/":"render url"},")" - R"(adComponentRenderUrls":{"https://shared.test/":"ad component url"}})", - ExtractSignals(signals.get(), /*render_url=*/GURL("https://shared.test/"), - /*ad_component_render_urls=*/ - {GURL("https://shared.test/")})); -} - -TEST_F(TrustedScoringSignalsTest, EscapeQueryParams) { - std::unique_ptr signals = - FetchScoringSignalsWithResponse( - GURL("https://url.test/?hostname=pub+li%26sher" - "&renderUrls=https%3A%2F%2Ffoo.test%2F%3F%26%3D" - "&adComponentRenderUrls=https%3A%2F%2Fbar.test%2F%3F%26%3D"), - R"( - { - "renderUrls": { - "https://foo.test/?&=": 4 - }, - "adComponentRenderUrls": { - "https://bar.test/?&=": 5 - } - } -)", - /*render_urls=*/ - {GURL("https://foo.test/?&=")}, /*ad_component_render_urls=*/ - {GURL("https://bar.test/?&=")}, "pub li&sher"); - ASSERT_TRUE(signals); - EXPECT_EQ( - R"({"renderUrl":{"https://foo.test/?&=":4},"adComponentRenderUrls":{"https://bar.test/?&=":5}})", - ExtractSignals(signals.get(), /*render_url=*/ - GURL("https://foo.test/?&="), /*ad_component_render_urls=*/ - {GURL("https://bar.test/?&=")})); - EXPECT_FALSE(error_msg_.has_value()); -} - -// Testcase where the loader is deleted after it queued the parsing of -// the script on V8 thread, but before it gets to finish. -TEST_F(TrustedScoringSignalsTest, DeleteBeforeCallback) { - GURL url( - "https://url.test/" - "?hostname=publisher&renderUrls=https%3A%2F%2Ffoo.test%2F"); - - AddJsonResponse(&url_loader_factory_, url, kBaseJson); - - // Wedge the V8 thread to control when the JSON parsing takes place. - base::WaitableEvent* event_handle = WedgeV8Thread(v8_helper_.get()); - auto scoring_signals = std::make_unique( - &url_loader_factory_, - /*render_urls=*/std::set{GURL("http://foo.test/")}, - /*ad_component_urls=*/std::set(), "publisher", base_url_, - v8_helper_, - base::BindOnce([](std::unique_ptr result, - absl::optional error_msg) { - ADD_FAILURE() << "Callback should not be invoked since loader deleted"; - })); - base::RunLoop().RunUntilIdle(); - scoring_signals.reset(); - event_handle->Signal(); -} - -} // namespace -} // namespace auction_worklet diff --git a/content/services/auction_worklet/trusted_signals.cc b/content/services/auction_worklet/trusted_signals.cc new file mode 100644 index 0000000000000..cae5f6937f755 --- /dev/null +++ b/content/services/auction_worklet/trusted_signals.cc @@ -0,0 +1,380 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "content/services/auction_worklet/trusted_signals.h" + +#include +#include +#include + +#include "base/bind.h" +#include "base/callback.h" +#include "base/check.h" +#include "base/strings/strcat.h" +#include "base/strings/stringprintf.h" +#include "content/services/auction_worklet/auction_downloader.h" +#include "content/services/auction_worklet/auction_v8_helper.h" +#include "gin/converter.h" +#include "net/base/escape.h" +#include "services/network/public/mojom/url_loader_factory.mojom-forward.h" +#include "url/gurl.h" +#include "v8/include/v8-context.h" +#include "v8/include/v8-json.h" +#include "v8/include/v8-object.h" +#include "v8/include/v8-primitive.h" + +namespace auction_worklet { + +namespace { + +// Creates a query param of the form `&=`. +// Returns an empty string if `keys` is empty. `name` will not be escaped, but +// `values` will be. Each entry in `keys` will be added at most once. +std::string CreateQueryParam(const char* name, + const std::set& keys) { + if (keys.empty()) + return std::string(); + + std::string query_param = base::StringPrintf("&%s=", name); + bool first_key = true; + for (const auto& key : keys) { + if (first_key) { + first_key = false; + } else { + query_param.append(","); + } + query_param.append(net::EscapeQueryParamValue(key, /*use_plus=*/true)); + } + return query_param; +} + +GURL SetQueryParam(const GURL& base_url, const std::string& new_query_params) { + GURL::Replacements replacements; + replacements.SetQueryStr(new_query_params); + return base_url.ReplaceComponents(replacements); +} + +// Extracts GURL/JSON key/value pairs from `v8_object`, using values in `keys` +// as keys. Does not add entries to the map for keys with missing values. +std::map ParseKeyValueMap( + AuctionV8Helper* v8_helper, + v8::Local v8_object, + const std::set& keys) { + std::map out; + if (keys.empty()) + return out; + + for (const auto& key : keys) { + v8::Local v8_key; + if (!v8_helper->CreateUtf8String(key).ToLocal(&v8_key)) + continue; + + v8::Local v8_value; + v8::Local v8_string_value; + std::string value; + // Only the Get() call should be able to fail. + if (!v8_object->Get(v8_helper->scratch_context(), v8_key) + .ToLocal(&v8_value) || + !v8::JSON::Stringify(v8_helper->scratch_context(), v8_value) + .ToLocal(&v8_string_value) || + !gin::ConvertFromV8(v8_helper->isolate(), v8_string_value, &value)) { + continue; + } + out[key] = std::move(value); + } + return out; +} + +// Extracts GURL/JSON key/value pairs from the object named `name` in +// `v8_object`, using values in `keys` as keys. Does not add entries to the map +// for keys with missing values. +std::map ParseChildKeyValueMap( + AuctionV8Helper* v8_helper, + v8::Local v8_object, + const char* name, + const std::set& keys) { + std::map out; + if (keys.empty()) + return out; + + v8::Local named_object_value; + // Don't consider the entire object missing a fatal error. + if (!v8_object + ->Get(v8_helper->scratch_context(), + v8_helper->CreateStringFromLiteral(name)) + .ToLocal(&named_object_value) || + !named_object_value->IsObject()) { + return out; + } + + return ParseKeyValueMap(v8_helper, named_object_value.As(), keys); +} + +// Takes a list of keys, a map of strings to JSON strings and creates a +// corresponding v8::Object from the entries with the provided keys. `keys` must +// not be empty. +v8::Local CreateObjectFromMap( + const std::vector& keys, + const std::map& json_data, + AuctionV8Helper* v8_helper, + v8::Local context) { + DCHECK(v8_helper->v8_runner()->RunsTasksInCurrentSequence()); + DCHECK(!keys.empty()); + + v8::Local out = v8::Object::New(v8_helper->isolate()); + for (const auto& key : keys) { + auto data = json_data.find(key); + // InsertJsonValue() shouldn't be able to fail, but the first check might. + if (data == json_data.end() || + !v8_helper->InsertJsonValue(context, key, data->second, out)) { + bool result = + v8_helper->InsertValue(key, v8::Null(v8_helper->isolate()), out); + DCHECK(result); + } + } + return out; +} + +} // namespace + +TrustedSignals::Result::Result( + std::map bidder_json_data) + : bidder_json_data_(std::move(bidder_json_data)) {} + +TrustedSignals::Result::Result( + std::map render_url_json_data, + std::map ad_component_json_data) + : render_url_json_data_(std::move(render_url_json_data)), + ad_component_json_data_(std::move(ad_component_json_data)) {} + +TrustedSignals::Result::~Result() = default; + +v8::Local TrustedSignals::Result::GetBiddingSignals( + AuctionV8Helper* v8_helper, + v8::Local context, + const std::vector& bidding_signals_keys) const { + DCHECK(v8_helper->v8_runner()->RunsTasksInCurrentSequence()); + DCHECK(bidder_json_data_.has_value()); + + return CreateObjectFromMap(bidding_signals_keys, *bidder_json_data_, + v8_helper, context); +} + +v8::Local TrustedSignals::Result::GetScoringSignals( + AuctionV8Helper* v8_helper, + v8::Local context, + const GURL& render_url, + const std::vector& ad_component_render_urls) const { + DCHECK(v8_helper->v8_runner()->RunsTasksInCurrentSequence()); + DCHECK(render_url_json_data_.has_value()); + DCHECK(ad_component_json_data_.has_value()); + + v8::Local out = v8::Object::New(v8_helper->isolate()); + + // Create renderUrl sub-object, and add it to to `out`. + v8::Local render_url_v8_object = + CreateObjectFromMap(std::vector{render_url.spec()}, + *render_url_json_data_, v8_helper, context); + bool result = v8_helper->InsertValue("renderUrl", render_url_v8_object, out); + DCHECK(result); + + // If there are any ad components, assemble and add an `adComponentRenderUrls` + // object as well. + if (!ad_component_render_urls.empty()) { + v8::Local ad_components_v8_object = CreateObjectFromMap( + ad_component_render_urls, *ad_component_json_data_, v8_helper, context); + bool result = v8_helper->InsertValue("adComponentRenderUrls", + ad_components_v8_object, out); + DCHECK(result); + } + + return out; +} + +std::unique_ptr TrustedSignals::LoadBiddingSignals( + network::mojom::URLLoaderFactory* url_loader_factory, + const std::vector& bidding_signals_keys, + const std::string& hostname, + const GURL& trusted_bidding_signals_url, + scoped_refptr v8_helper, + LoadSignalsCallback load_signals_callback) { + DCHECK(!bidding_signals_keys.empty()); + + std::unique_ptr trusted_signals = + base::WrapUnique(new TrustedSignals( + /*bidding_signals_keys=*/std::set( + bidding_signals_keys.begin(), bidding_signals_keys.end()), + /*render_urls=*/absl::nullopt, + /*ad_component_render_urls=*/absl::nullopt, + trusted_bidding_signals_url, std::move(v8_helper), + std::move(load_signals_callback))); + + std::string query_params = + "hostname=" + net::EscapeQueryParamValue(hostname, /*use_plus=*/true) + + CreateQueryParam("keys", *trusted_signals->bidding_signals_keys_); + GURL full_signals_url = + SetQueryParam(trusted_bidding_signals_url, query_params); + trusted_signals->StartDownload(url_loader_factory, full_signals_url); + + return trusted_signals; +} + +std::unique_ptr TrustedSignals::LoadScoringSignals( + network::mojom::URLLoaderFactory* url_loader_factory, + const std::vector& render_urls, + const std::vector& ad_component_render_urls, + const std::string& hostname, + const GURL& trusted_scoring_signals_url, + scoped_refptr v8_helper, + LoadSignalsCallback load_signals_callback) { + DCHECK(!render_urls.empty()); + + std::unique_ptr trusted_signals = + base::WrapUnique(new TrustedSignals( + /*bidding_signals_keys=*/absl::nullopt, + /*render_urls=*/ + std::set(render_urls.begin(), render_urls.end()), + /*ad_component_render_urls=*/ + std::set(ad_component_render_urls.begin(), + ad_component_render_urls.end()), + trusted_scoring_signals_url, std::move(v8_helper), + std::move(load_signals_callback))); + + std::string query_params = + "hostname=" + net::EscapeQueryParamValue(hostname, /*use_plus=*/true) + + CreateQueryParam("renderUrls", *trusted_signals->render_urls_) + + CreateQueryParam("adComponentRenderUrls", + *trusted_signals->ad_component_render_urls_); + GURL full_signals_url = + SetQueryParam(trusted_scoring_signals_url, query_params); + trusted_signals->StartDownload(url_loader_factory, full_signals_url); + + return trusted_signals; +} + +TrustedSignals::TrustedSignals( + absl::optional> bidding_signals_keys, + absl::optional> render_urls, + absl::optional> ad_component_render_urls, + const GURL& trusted_signals_url, + scoped_refptr v8_helper, + LoadSignalsCallback load_signals_callback) + : bidding_signals_keys_(std::move(bidding_signals_keys)), + render_urls_(std::move(render_urls)), + ad_component_render_urls_(std::move(ad_component_render_urls)), + trusted_signals_url_(trusted_signals_url), + v8_helper_(std::move(v8_helper)), + load_signals_callback_(std::move(load_signals_callback)) { + DCHECK(v8_helper_); + DCHECK(load_signals_callback_); + + // Either this should be for bidding signals or scoring signals. + DCHECK(bidding_signals_keys_ || (render_urls_ && ad_component_render_urls_)); + DCHECK(!bidding_signals_keys_ || + (!render_urls_ && !ad_component_render_urls_)); +} + +TrustedSignals::~TrustedSignals() = default; + +void TrustedSignals::StartDownload( + network::mojom::URLLoaderFactory* url_loader_factory, + const GURL& full_signals_url) { + auction_downloader_ = std::make_unique( + url_loader_factory, full_signals_url, AuctionDownloader::MimeType::kJson, + base::BindOnce(&TrustedSignals::OnDownloadComplete, + base::Unretained(this))); +} + +void TrustedSignals::OnDownloadComplete(std::unique_ptr body, + absl::optional error_msg) { + // The downloader's job is done, so clean it up. + auction_downloader_.reset(); + + // Key-related fields aren't needed after this call, so pass ownership of them + // over to the parser on the V8 thread. + v8_helper_->v8_runner()->PostTask( + FROM_HERE, + base::BindOnce(&TrustedSignals::HandleDownloadResultOnV8Thread, + v8_helper_, trusted_signals_url_, + std::move(bidding_signals_keys_), std::move(render_urls_), + std::move(ad_component_render_urls_), std::move(body), + std::move(error_msg), + base::SequencedTaskRunnerHandle::Get(), + weak_ptr_factory.GetWeakPtr())); +} + +// static +void TrustedSignals::HandleDownloadResultOnV8Thread( + scoped_refptr v8_helper, + const GURL& signals_url, + absl::optional> bidding_signals_keys, + absl::optional> render_urls, + absl::optional> ad_component_render_urls, + std::unique_ptr body, + absl::optional error_msg, + scoped_refptr user_thread_task_runner, + base::WeakPtr weak_instance) { + if (!body) { + PostCallbackToUserThread(std::move(user_thread_task_runner), weak_instance, + nullptr, std::move(error_msg)); + return; + } + + DCHECK(!error_msg.has_value()); + + AuctionV8Helper::FullIsolateScope isolate_scope(v8_helper.get()); + v8::Context::Scope context_scope(v8_helper->scratch_context()); + + v8::Local v8_data; + if (!v8_helper->CreateValueFromJson(v8_helper->scratch_context(), *body) + .ToLocal(&v8_data) || + !v8_data->IsObject()) { + std::string error = base::StrCat( + {signals_url.spec(), " Unable to parse as a JSON object."}); + PostCallbackToUserThread(std::move(user_thread_task_runner), weak_instance, + nullptr, std::move(error)); + return; + } + + v8::Local v8_object = v8_data.As(); + + std::unique_ptr result; + + if (bidding_signals_keys) { + // Handle bidding signals case. + result = std::make_unique( + ParseKeyValueMap(v8_helper.get(), v8_object, *bidding_signals_keys)); + } else { + // Handle scoring signals case. + result = std::make_unique( + ParseChildKeyValueMap(v8_helper.get(), v8_object, "renderUrls", + *render_urls), + ParseChildKeyValueMap(v8_helper.get(), v8_object, + "adComponentRenderUrls", + *ad_component_render_urls)); + } + + PostCallbackToUserThread(std::move(user_thread_task_runner), weak_instance, + std::move(result), absl::nullopt); +} + +void TrustedSignals::PostCallbackToUserThread( + scoped_refptr user_thread_task_runner, + base::WeakPtr weak_instance, + std::unique_ptr result, + absl::optional error_msg) { + user_thread_task_runner->PostTask( + FROM_HERE, + base::BindOnce(&TrustedSignals::DeliverCallbackOnUserThread, + weak_instance, std::move(result), std::move(error_msg))); +} + +void TrustedSignals::DeliverCallbackOnUserThread( + std::unique_ptr result, + absl::optional error_msg) { + std::move(load_signals_callback_) + .Run(std::move(result), std::move(error_msg)); +} + +} // namespace auction_worklet diff --git a/content/services/auction_worklet/trusted_signals.h b/content/services/auction_worklet/trusted_signals.h new file mode 100644 index 0000000000000..5aeb5c46d16ee --- /dev/null +++ b/content/services/auction_worklet/trusted_signals.h @@ -0,0 +1,191 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CONTENT_SERVICES_AUCTION_WORKLET_TRUSTED_SIGNALS_H_ +#define CONTENT_SERVICES_AUCTION_WORKLET_TRUSTED_SIGNALS_H_ + +#include +#include +#include +#include +#include + +#include "base/callback.h" +#include "base/memory/scoped_refptr.h" +#include "services/network/public/mojom/url_loader_factory.mojom-forward.h" +#include "third_party/abseil-cpp/absl/types/optional.h" +#include "url/gurl.h" +#include "v8/include/v8-forward.h" + +namespace auction_worklet { + +class AuctionDownloader; +class AuctionV8Helper; + +// Represents the trusted bidding/scoring signals that are part of the FLEDGE +// bidding system (https://github.com/WICG/turtledove/blob/main/FLEDGE.md). +// Fetches and parses the hosted JSON data files needed by worklets. There are +// separate methods for fetching bidding and scoring signals. A single +// TrustedSignals object can only be used to fetch bidding signals or scoring +// signals, even if a single URL is used for both types of signals. +// +// TODO(mmenke): This class currently does 4 copies when loading the data (To V8 +// string, use V8's JSON parser, split data into V8 JSON subcomponent strings, +// convert to C++ strings), and 2 copies of each substring to use the data (To +// V8 per-key JSON string, use V8's JSON parser). Keeping the data stored as V8 +// JSON subcomponents would remove 2 copies, without too much complexity. Could +// even implement V8 deep-copy logic, to remove two more copies (counting the +// clone operation as a copy). +class TrustedSignals { + public: + // Contains the values returned by the server. + // + // This can be created and destroyed on any thread, but GetSignals() can only + // be used on the V8 thread. + class Result { + public: + // Constructor for bidding signals. + explicit Result(std::map bidder_json_data); + + // Constructor for scoring signals. + Result(std::map render_url_json_data, + std::map ad_component_json_data); + + explicit Result(const Result&) = delete; + + ~Result(); + + Result& operator=(const Result&) = delete; + + // Retrieves the trusted bidding signals associated with the passed in keys, + // in the format expected by a worklet's generateBid() method. `this` must + // have been generated by fetching bidding signals. `v8_helper`'s Isolate + // must be active (in particular, this must be on the v8 thread), and + // `context` must be the active context. `bidding_signals_keys` must be + // subsets of the keys provided when creating the TrustedSignals object. + // Always returns a non-empty value. + v8::Local GetBiddingSignals( + AuctionV8Helper* v8_helper, + v8::Local context, + const std::vector& bidding_signals_keys) const; + + // Retrieves the trusted scoring signals associated with the passed in urls, + // in the format expected by a worklet's scoreAd() method. `this` must have + // been generated by fetching scoring signals. `v8_helper`'s Isolate must be + // active (in particular, this must be on the v8 thread), and `context` must + // be the active context. `render_url` and `ad_component_render_urls` must + // be subsets of the corresponding sets of GURLs provided when creating the + // TrustedSignals object. Always returns a non-empty value. + v8::Local GetScoringSignals( + AuctionV8Helper* v8_helper, + v8::Local context, + const GURL& render_url, + const std::vector& ad_component_render_urls) const; + + private: + // Map of keys to the associated JSON data for trusted bidding signals. + absl::optional> bidder_json_data_; + + // Map of keys to the associated JSON data for trusted scoring signals. + absl::optional> render_url_json_data_; + absl::optional> ad_component_json_data_; + }; + + using LoadSignalsCallback = + base::OnceCallback result, + absl::optional error_msg)>; + + explicit TrustedSignals(const TrustedSignals&) = delete; + TrustedSignals& operator=(const TrustedSignals&) = delete; + ~TrustedSignals(); + + // Constructs a TrustedSignals for fetching bidding signals, and starts the + // fetch. `trusted_bidding_signals_url` must be the base URL (no query params + // added). Callback will be invoked asynchronously once the data has been + // fetched or an error has occurred. De-duplicates keys when assembling the + // full URL for the fetch. Fails if the URL already has a query param (or has + // a location or embedded credentials) or if the response is not JSON. If some + // or all of the render URLs are missing, still succeeds, and GetSignals() + // will populate them with nulls. + // + // There are no lifetime constraints of `url_loader_factory`. + static std::unique_ptr LoadBiddingSignals( + network::mojom::URLLoaderFactory* url_loader_factory, + const std::vector& bidding_signals_keys, + const std::string& hostname, + const GURL& trusted_bidding_signals_url, + scoped_refptr v8_helper, + LoadSignalsCallback load_signals_callback); + + // Just like LoadBiddingSignals() above, but for fetching seller signals. + static std::unique_ptr LoadScoringSignals( + network::mojom::URLLoaderFactory* url_loader_factory, + const std::vector& render_urls, + const std::vector& ad_component_render_urls, + const std::string& hostname, + const GURL& trusted_scoring_signals_url, + scoped_refptr v8_helper, + LoadSignalsCallback load_signals_callback); + + private: + TrustedSignals(absl::optional> bidding_signals_keys, + absl::optional> render_urls, + absl::optional> ad_component_render_urls, + const GURL& trusted_signals_url, + scoped_refptr v8_helper, + LoadSignalsCallback load_signals_callback); + + // Starts downloading `url`, which should be the bidding or scoring signals + // URL with the query parameter correctly set. + void StartDownload(network::mojom::URLLoaderFactory* url_loader_factory, + const GURL& full_signals_url); + + void OnDownloadComplete(std::unique_ptr body, + absl::optional error_msg); + + // Parses the response body on the V8 thread, and extracts values associated + // with the requested keys. + static void HandleDownloadResultOnV8Thread( + scoped_refptr v8_helper, + const GURL& signals_url, + absl::optional> bidding_signals_keys, + absl::optional> render_urls, + absl::optional> ad_component_render_urls, + std::unique_ptr body, + absl::optional error_msg, + scoped_refptr user_thread_task_runner, + base::WeakPtr weak_instance); + + // Called from V8 thread. + static void PostCallbackToUserThread( + scoped_refptr user_thread_task_runner, + base::WeakPtr weak_instance, + std::unique_ptr result, + absl::optional error_msg); + + // Called on user thread. + void DeliverCallbackOnUserThread(std::unique_ptr, + absl::optional error_msg); + + // Keys being fetched. For bidding signals, only `bidding_signals_keys_` is + // non-null. For scoring signals, only `render_urls_` and + // `ad_component_render_urls_` are non-null. These are cleared and ownership + // is passed to the V8 thread once the download completes, as they're no + // longer on the main thread after that point. + absl::optional> bidding_signals_keys_; + absl::optional> render_urls_; + absl::optional> ad_component_render_urls_; + + const GURL trusted_signals_url_; // original, for error messages. + const scoped_refptr v8_helper_; + + LoadSignalsCallback load_signals_callback_; + std::unique_ptr auction_downloader_; + + base::WeakPtrFactory weak_ptr_factory{this}; +}; + +} // namespace auction_worklet + +#endif // CONTENT_SERVICES_AUCTION_WORKLET_TRUSTED_SIGNALS_H_ diff --git a/content/services/auction_worklet/trusted_signals_unittest.cc b/content/services/auction_worklet/trusted_signals_unittest.cc new file mode 100644 index 0000000000000..2babf9b88a09a --- /dev/null +++ b/content/services/auction_worklet/trusted_signals_unittest.cc @@ -0,0 +1,577 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "content/services/auction_worklet/trusted_signals.h" + +#include +#include +#include + +#include "base/bind.h" +#include "base/run_loop.h" +#include "base/synchronization/waitable_event.h" +#include "base/test/bind.h" +#include "base/test/task_environment.h" +#include "content/services/auction_worklet/auction_v8_helper.h" +#include "content/services/auction_worklet/worklet_test_util.h" +#include "net/http/http_status_code.h" +#include "services/network/test/test_url_loader_factory.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" +#include "v8/include/v8-context.h" +#include "v8/include/v8-forward.h" + +namespace auction_worklet { +namespace { + +// Common JSON used for most bidding signals tests. Key 4 is deliberately +// skipped. +const char kBaseBiddingJson[] = R"( + { + "key1": 1, + "key2": [2], + "key3": null, + "key5": "value5", + "key 6": 6, + "key=7": 7, + "key,8": 8 + } +)"; + +// Common JSON used for most scoring signals tests. +const char kBaseScoringJson[] = R"( + { + "renderUrls": { + "https://foo.test/": 1, + "https://bar.test/": [2], + "https://baz.test/": null, + "https://shared.test/": "render url" + }, + "adComponentRenderUrls": { + "https://foosub.test/": 2, + "https://barsub.test/": [3], + "https://bazsub.test/": null, + "https://shared.test/": "ad component url" + } + } +)"; + +const char kHostname[] = "publisher"; + +class TrustedSignalsTest : public testing::Test { + public: + TrustedSignalsTest() { + v8_helper_ = AuctionV8Helper::Create(AuctionV8Helper::CreateTaskRunner()); + } + + ~TrustedSignalsTest() override { task_environment_.RunUntilIdle(); } + + // Sets the HTTP response and then fetches bidding signals and waits for + // completion. Returns nullptr on failure. + std::unique_ptr FetchBiddingSignalsWithResponse( + const GURL& url, + const std::string& response, + std::vector trusted_bidding_signals_keys, + const std::string& hostname) { + AddJsonResponse(&url_loader_factory_, url, response); + return FetchBiddingSignals(trusted_bidding_signals_keys, hostname); + } + + // Fetches bidding signals and waits for completion. Returns nullptr on + // failure. + std::unique_ptr FetchBiddingSignals( + std::vector trusted_bidding_signals_keys, + const std::string& hostname) { + CHECK(!load_signals_run_loop_); + + DCHECK(!load_signals_result_); + auto bidding_signals = TrustedSignals::LoadBiddingSignals( + &url_loader_factory_, std::move(trusted_bidding_signals_keys), + std::move(hostname), base_url_, v8_helper_, + base::BindOnce(&TrustedSignalsTest::LoadSignalsCallback, + base::Unretained(this))); + WaitForLoadComplete(); + return std::move(load_signals_result_); + } + + // Sets the HTTP response and then fetches scoring signals and waits for + // completion. Returns nullptr on failure. + std::unique_ptr FetchScoringSignalsWithResponse( + const GURL& url, + const std::string& response, + std::vector render_urls, + std::vector ad_component_render_urls, + const std::string& hostname) { + AddJsonResponse(&url_loader_factory_, url, response); + return FetchScoringSignals(render_urls, ad_component_render_urls, hostname); + } + + // Fetches scoring signals and waits for completion. Returns nullptr on + // failure. + std::unique_ptr FetchScoringSignals( + std::vector render_urls, + std::vector ad_component_render_urls, + const std::string& hostname) { + CHECK(!load_signals_run_loop_); + + DCHECK(!load_signals_result_); + auto scoring_signals = TrustedSignals::LoadScoringSignals( + &url_loader_factory_, std::move(render_urls), + std::move(ad_component_render_urls), std::move(hostname), base_url_, + v8_helper_, + base::BindOnce(&TrustedSignalsTest::LoadSignalsCallback, + base::Unretained(this))); + WaitForLoadComplete(); + return std::move(load_signals_result_); + } + + // Wait for LoadSignalsCallback to be invoked. + void WaitForLoadComplete() { + // Since LoadSignalsCallback is always invoked asynchronously, fine to + // create the RunLoop after creating the TrustedSignals object, which will + // ultimately trigger the invocation. + load_signals_run_loop_ = std::make_unique(); + load_signals_run_loop_->Run(); + load_signals_run_loop_.reset(); + } + + // Returns the results of calling TrustedSignals::Result::GetBiddingSignals() + // with `trusted_bidding_signals_keys`. Returns value as a JSON std::string, + // for easy testing. + std::string ExtractBiddingSignals( + TrustedSignals::Result* signals, + std::vector trusted_bidding_signals_keys) { + base::RunLoop run_loop; + + std::string result; + v8_helper_->v8_runner()->PostTask( + FROM_HERE, base::BindLambdaForTesting([&]() { + AuctionV8Helper::FullIsolateScope isolate_scope(v8_helper_.get()); + v8::Isolate* isolate = v8_helper_->isolate(); + // Could use the scratch context, but using a separate one more + // closely resembles actual use. + v8::Local context = v8::Context::New(isolate); + v8::Context::Scope context_scope(context); + + v8::Local value = signals->GetBiddingSignals( + v8_helper_.get(), context, trusted_bidding_signals_keys); + + if (!v8_helper_->ExtractJson(context, value, &result)) { + result = "JSON extraction failed."; + } + run_loop.Quit(); + })); + run_loop.Run(); + return result; + } + + // Returns the results of calling TrustedSignals::Result::GetScoringSignals() + // with the provided parameters. Returns value as a JSON std::string, for easy + // testing. + std::string ExtractScoringSignals( + TrustedSignals::Result* signals, + const GURL& render_url, + const std::vector& ad_component_render_urls) { + base::RunLoop run_loop; + + std::string result; + v8_helper_->v8_runner()->PostTask( + FROM_HERE, base::BindLambdaForTesting([&]() { + AuctionV8Helper::FullIsolateScope isolate_scope(v8_helper_.get()); + v8::Isolate* isolate = v8_helper_->isolate(); + // Could use the scratch context, but using a separate one more + // closely resembles actual use. + v8::Local context = v8::Context::New(isolate); + v8::Context::Scope context_scope(context); + + v8::Local value = signals->GetScoringSignals( + v8_helper_.get(), context, render_url, ad_component_render_urls); + + if (!v8_helper_->ExtractJson(context, value, &result)) { + result = "JSON extraction failed."; + } + run_loop.Quit(); + })); + run_loop.Run(); + return result; + } + + protected: + void LoadSignalsCallback(std::unique_ptr result, + absl::optional error_msg) { + load_signals_result_ = std::move(result); + error_msg_ = std::move(error_msg); + EXPECT_EQ(load_signals_result_ == nullptr, error_msg_.has_value()); + load_signals_run_loop_->Quit(); + } + + base::test::TaskEnvironment task_environment_; + + // URL without query params attached. + const GURL base_url_ = GURL("https://url.test/"); + + // Reuseable run loop for loading the signals. It's always populated after + // creating the worklet, to cause a crash if the callback is invoked + // synchronously. + std::unique_ptr load_signals_run_loop_; + std::unique_ptr load_signals_result_; + absl::optional error_msg_; + + network::TestURLLoaderFactory url_loader_factory_; + scoped_refptr v8_helper_; +}; + +TEST_F(TrustedSignalsTest, BiddingSignalsNetworkError) { + url_loader_factory_.AddResponse( + "https://url.test/?hostname=publisher&keys=key1", kBaseBiddingJson, + net::HTTP_NOT_FOUND); + EXPECT_FALSE(FetchBiddingSignals({"key1"}, kHostname)); + ASSERT_TRUE(error_msg_.has_value()); + EXPECT_EQ( + "Failed to load https://url.test/?hostname=publisher&keys=key1 " + "HTTP status = 404 Not Found.", + error_msg_.value()); +} + +TEST_F(TrustedSignalsTest, ScoringSignalsNetworkError) { + url_loader_factory_.AddResponse( + "https://url.test/" + "?hostname=publisher&renderUrls=https%3A%2F%2Ffoo.test%2F", + kBaseScoringJson, net::HTTP_NOT_FOUND); + EXPECT_FALSE(FetchScoringSignals( + /*render_urls=*/{"https://foo.test/"}, + /*ad_component_render_urls=*/{}, kHostname)); + ASSERT_TRUE(error_msg_.has_value()); + EXPECT_EQ( + "Failed to load " + "https://url.test/" + "?hostname=publisher&renderUrls=https%3A%2F%2Ffoo.test%2F " + "HTTP status = 404 Not Found.", + error_msg_.value()); +} + +TEST_F(TrustedSignalsTest, BiddingSignalsResponseNotJson) { + EXPECT_FALSE(FetchBiddingSignalsWithResponse( + GURL("https://url.test/?hostname=publisher&keys=key1"), "Not Json", + {"key1"}, kHostname)); + ASSERT_TRUE(error_msg_.has_value()); + EXPECT_EQ("https://url.test/ Unable to parse as a JSON object.", + error_msg_.value()); +} + +TEST_F(TrustedSignalsTest, ScoringSignalsResponseNotJson) { + EXPECT_FALSE(FetchScoringSignalsWithResponse( + GURL("https://url.test/" + "?hostname=publisher&renderUrls=https%3A%2F%2Ffoo.test%2F"), + "Not Json", + /*render_urls=*/{"https://foo.test/"}, + /*ad_component_render_urls=*/{}, kHostname)); + ASSERT_TRUE(error_msg_.has_value()); + EXPECT_EQ("https://url.test/ Unable to parse as a JSON object.", + error_msg_.value()); +} + +TEST_F(TrustedSignalsTest, BiddingSignalsResponseNotObject) { + EXPECT_FALSE(FetchBiddingSignalsWithResponse( + GURL("https://url.test/?hostname=publisher&keys=key1"), "42", {"key1"}, + kHostname)); + ASSERT_TRUE(error_msg_.has_value()); + EXPECT_EQ("https://url.test/ Unable to parse as a JSON object.", + error_msg_.value()); +} + +TEST_F(TrustedSignalsTest, ScoringSignalsResponseNotObject) { + EXPECT_FALSE(FetchScoringSignalsWithResponse( + GURL("https://url.test/" + "?hostname=publisher&renderUrls=https%3A%2F%2Ffoo.test%2F"), + "42", /*render_urls=*/{"https://foo.test/"}, + /*ad_component_render_urls=*/{}, kHostname)); + ASSERT_TRUE(error_msg_.has_value()); + EXPECT_EQ("https://url.test/ Unable to parse as a JSON object.", + error_msg_.value()); +} + +TEST_F(TrustedSignalsTest, ScoringSignalsExpectedEntriesNotPresent) { + std::unique_ptr signals = + FetchScoringSignalsWithResponse( + GURL("https://url.test/?hostname=publisher" + "&renderUrls=https%3A%2F%2Ffoo.test%2F" + "&adComponentRenderUrls=https%3A%2F%2Fbar.test%2F"), + R"({"foo":4,"bar":5})", + /*render_urls=*/{"https://foo.test/"}, + /*ad_component_render_urls=*/{"https://bar.test/"}, kHostname); + ASSERT_TRUE(signals); + EXPECT_EQ(R"({"renderUrl":{"https://foo.test/":null},)" + R"("adComponentRenderUrls":{"https://bar.test/":null}})", + ExtractScoringSignals( + signals.get(), /*render_url=*/GURL("https://foo.test/"), + /*ad_component_render_urls=*/{"https://bar.test/"})); + EXPECT_FALSE(error_msg_.has_value()); +} + +TEST_F(TrustedSignalsTest, ScoringSignalsNestedEntriesNotObjects) { + std::unique_ptr signals = + FetchScoringSignalsWithResponse( + GURL("https://url.test/?hostname=publisher" + "&renderUrls=https%3A%2F%2Ffoo.test%2F" + "&adComponentRenderUrls=https%3A%2F%2Fbar.test%2F"), + R"({"renderUrls":4,"adComponentRenderUrls":5})", + /*render_urls=*/{"https://foo.test/"}, + /*ad_component_render_urls=*/{"https://bar.test/"}, kHostname); + ASSERT_TRUE(signals); + EXPECT_EQ(R"({"renderUrl":{"https://foo.test/":null},)" + R"("adComponentRenderUrls":{"https://bar.test/":null}})", + ExtractScoringSignals( + signals.get(), /*render_url=*/GURL("https://foo.test/"), + /*ad_component_render_urls=*/{"https://bar.test/"})); + EXPECT_FALSE(error_msg_.has_value()); +} + +TEST_F(TrustedSignalsTest, BiddingSignalsKeyMissing) { + std::unique_ptr signals = + FetchBiddingSignalsWithResponse( + GURL("https://url.test/?hostname=publisher&keys=key4"), + kBaseBiddingJson, {"key4"}, kHostname); + ASSERT_TRUE(signals); + EXPECT_EQ(R"({"key4":null})", ExtractBiddingSignals(signals.get(), {"key4"})); +} + +TEST_F(TrustedSignalsTest, ScoringSignalsKeysMissing) { + std::unique_ptr signals = + FetchScoringSignalsWithResponse( + GURL("https://url.test/?hostname=publisher" + "&renderUrls=https%3A%2F%2Ffoo.test%2F" + "&adComponentRenderUrls=https%3A%2F%2Fbar.test%2F"), + R"({"renderUrls":{"these":"are not"},")" + R"(adComponentRenderUrls":{"the values":"you're looking for"}})", + /*render_urls=*/{"https://foo.test/"}, + /*ad_component_render_urls=*/{"https://bar.test/"}, kHostname); + ASSERT_TRUE(signals); + EXPECT_EQ(R"({"renderUrl":{"https://foo.test/":null},)" + R"("adComponentRenderUrls":{"https://bar.test/":null}})", + ExtractScoringSignals( + signals.get(), /*render_url=*/GURL("https://foo.test/"), + /*ad_component_render_urls=*/{"https://bar.test/"})); + EXPECT_FALSE(error_msg_.has_value()); +} + +TEST_F(TrustedSignalsTest, BiddingSignalsOneKey) { + std::unique_ptr signals = + FetchBiddingSignalsWithResponse( + GURL("https://url.test/?hostname=publisher&keys=key1"), + kBaseBiddingJson, {"key1"}, kHostname); + ASSERT_TRUE(signals); + EXPECT_EQ(R"({"key1":1})", ExtractBiddingSignals(signals.get(), {"key1"})); +} + +TEST_F(TrustedSignalsTest, ScoringSignalsForOneRenderUrl) { + std::unique_ptr signals = + FetchScoringSignalsWithResponse( + GURL("https://url.test/" + "?hostname=publisher&renderUrls=https%3A%2F%2Ffoo.test%2F"), + kBaseScoringJson, + /*render_urls=*/{"https://foo.test/"}, + /*ad_component_render_urls=*/{}, kHostname); + ASSERT_TRUE(signals); + EXPECT_EQ(R"({"renderUrl":{"https://foo.test/":1}})", + ExtractScoringSignals(signals.get(), + /*render_url=*/GURL("https://foo.test/"), + /*ad_component_render_urls=*/{})); + EXPECT_FALSE(error_msg_.has_value()); +} + +TEST_F(TrustedSignalsTest, BiddingSignalsMultipleKeys) { + std::unique_ptr signals = + FetchBiddingSignalsWithResponse( + GURL("https://url.test/?hostname=publisher&keys=key1,key2,key3,key5"), + kBaseBiddingJson, {"key3", "key1", "key5", "key2"}, kHostname); + ASSERT_TRUE(signals); + EXPECT_EQ(R"({"key1":1})", ExtractBiddingSignals(signals.get(), {"key1"})); + EXPECT_EQ(R"({"key2":[2]})", ExtractBiddingSignals(signals.get(), {"key2"})); + EXPECT_EQ(R"({"key3":null})", ExtractBiddingSignals(signals.get(), {"key3"})); + EXPECT_EQ(R"({"key5":"value5"})", + ExtractBiddingSignals(signals.get(), {"key5"})); + EXPECT_EQ( + R"({"key1":1,"key2":[2],"key3":null,"key5":"value5"})", + ExtractBiddingSignals(signals.get(), {"key1", "key2", "key3", "key5"})); +} + +TEST_F(TrustedSignalsTest, ScoringSignalsMultipleUrls) { + // URLs are currently added in lexical order. + std::unique_ptr signals = + FetchScoringSignalsWithResponse( + GURL("https://url.test/?hostname=publisher" + "&renderUrls=https%3A%2F%2Fbar.test%2F," + "https%3A%2F%2Fbaz.test%2F,https%3A%2F%2Ffoo.test%2F" + "&adComponentRenderUrls=https%3A%2F%2Fbarsub.test%2F," + "https%3A%2F%2Fbazsub.test%2F,https%3A%2F%2Ffoosub.test%2F"), + kBaseScoringJson, + /*render_urls=*/ + {"https://foo.test/", "https://bar.test/", "https://baz.test/"}, + /*ad_component_render_urls=*/ + {"https://foosub.test/", "https://barsub.test/", + "https://bazsub.test/"}, + kHostname); + ASSERT_TRUE(signals); + EXPECT_FALSE(error_msg_.has_value()); + EXPECT_EQ(R"({"renderUrl":{"https://bar.test/":[2]},")" + R"(adComponentRenderUrls":{"https://foosub.test/":2,)" + R"("https://barsub.test/":[3],"https://bazsub.test/":null}})", + ExtractScoringSignals( + signals.get(), /*render_url=*/GURL("https://bar.test/"), + /*ad_component_render_urls=*/ + {"https://foosub.test/", "https://barsub.test/", + "https://bazsub.test/"})); +} + +TEST_F(TrustedSignalsTest, BiddingSignalsDuplicateKeys) { + std::vector bidder_signals{"key1", "key2", "key2", "key1", + "key2"}; + std::unique_ptr signals = + FetchBiddingSignalsWithResponse( + GURL("https://url.test/?hostname=publisher&keys=key1,key2"), + kBaseBiddingJson, bidder_signals, kHostname); + ASSERT_TRUE(signals); + EXPECT_EQ(R"({"key1":1,"key2":[2]})", + ExtractBiddingSignals(signals.get(), bidder_signals)); +} + +TEST_F(TrustedSignalsTest, ScoringSignalsDuplicateKeys) { + std::vector ad_component_render_urls{ + "https://barsub.test/", "https://foosub.test/", "https://foosub.test/", + "https://barsub.test/"}; + std::unique_ptr signals = + FetchScoringSignalsWithResponse( + GURL("https://url.test/?hostname=publisher" + "&renderUrls=https%3A%2F%2Fbar.test%2F,https%3A%2F%2Ffoo.test%2F" + "&adComponentRenderUrls=https%3A%2F%2Fbarsub.test%2F," + "https%3A%2F%2Ffoosub.test%2F"), + kBaseScoringJson, + /*render_urls=*/ + {"https://foo.test/", "https://foo.test/", "https://bar.test/", + "https://bar.test/", "https://foo.test/"}, + ad_component_render_urls, kHostname); + ASSERT_TRUE(signals); + EXPECT_FALSE(error_msg_.has_value()); + EXPECT_EQ(R"({"renderUrl":{"https://bar.test/":[2]},")" + R"(adComponentRenderUrls":{)" + R"("https://barsub.test/":[3],"https://foosub.test/":2}})", + ExtractScoringSignals(signals.get(), + /*render_url=*/GURL("https://bar.test/"), + ad_component_render_urls)); +} + +// Test when a single URL is used as both a `renderUrl` and +// `adComponentRenderUrl`. +TEST_F(TrustedSignalsTest, ScoringSignalsSharedUrl) { + // URLs are currently added in lexical order. + std::unique_ptr signals = + FetchScoringSignalsWithResponse( + GURL("https://url.test/?hostname=publisher" + "&renderUrls=https%3A%2F%2Fshared.test%2F" + "&adComponentRenderUrls=https%3A%2F%2Fshared.test%2F"), + kBaseScoringJson, + /*render_urls=*/ + {"https://shared.test/"}, + /*ad_component_render_urls=*/ + {"https://shared.test/"}, kHostname); + ASSERT_TRUE(signals); + EXPECT_FALSE(error_msg_.has_value()); + EXPECT_EQ( + R"({"renderUrl":{"https://shared.test/":"render url"},")" + R"(adComponentRenderUrls":{"https://shared.test/":"ad component url"}})", + ExtractScoringSignals(signals.get(), + /*render_url=*/GURL("https://shared.test/"), + /*ad_component_render_urls=*/ + {"https://shared.test/"})); +} + +TEST_F(TrustedSignalsTest, BiddingSignalsEscapeQueryParams) { + std::unique_ptr signals = + FetchBiddingSignalsWithResponse( + GURL("https://url.test/" + "?hostname=pub+li%26sher&keys=key+6,key%2C8,key%3D7"), + kBaseBiddingJson, {"key 6", "key=7", "key,8"}, "pub li&sher"); + ASSERT_TRUE(signals); + EXPECT_EQ(R"({"key 6":6})", ExtractBiddingSignals(signals.get(), {"key 6"})); + EXPECT_EQ(R"({"key=7":7})", ExtractBiddingSignals(signals.get(), {"key=7"})); + EXPECT_EQ(R"({"key,8":8})", ExtractBiddingSignals(signals.get(), {"key,8"})); +} + +TEST_F(TrustedSignalsTest, ScoringSignalsEscapeQueryParams) { + std::unique_ptr signals = + FetchScoringSignalsWithResponse( + GURL("https://url.test/?hostname=pub+li%26sher" + "&renderUrls=https%3A%2F%2Ffoo.test%2F%3F%26%3D" + "&adComponentRenderUrls=https%3A%2F%2Fbar.test%2F%3F%26%3D"), + R"( + { + "renderUrls": { + "https://foo.test/?&=": 4 + }, + "adComponentRenderUrls": { + "https://bar.test/?&=": 5 + } + } +)", + /*render_urls=*/ + {"https://foo.test/?&="}, /*ad_component_render_urls=*/ + {"https://bar.test/?&="}, "pub li&sher"); + ASSERT_TRUE(signals); + EXPECT_EQ(R"({"renderUrl":{"https://foo.test/?&=":4},)" + R"("adComponentRenderUrls":{"https://bar.test/?&=":5}})", + ExtractScoringSignals( + signals.get(), /*render_url=*/ + GURL("https://foo.test/?&="), /*ad_component_render_urls=*/ + {"https://bar.test/?&="})); + EXPECT_FALSE(error_msg_.has_value()); +} + +// Testcase where the loader is deleted after it queued the parsing of +// the script on V8 thread, but before it gets to finish. +TEST_F(TrustedSignalsTest, BiddingSignalsDeleteBeforeCallback) { + GURL url("https://url.test/?hostname=publisher&keys=key1"); + + AddJsonResponse(&url_loader_factory_, url, kBaseBiddingJson); + + // Wedge the V8 thread to control when the JSON parsing takes place. + base::WaitableEvent* event_handle = WedgeV8Thread(v8_helper_.get()); + + auto bidding_signals = TrustedSignals::LoadBiddingSignals( + &url_loader_factory_, {"key1"}, "publisher", base_url_, v8_helper_, + base::BindOnce([](std::unique_ptr result, + absl::optional error_msg) { + ADD_FAILURE() << "Callback should not be invoked since loader deleted"; + })); + base::RunLoop().RunUntilIdle(); + bidding_signals.reset(); + event_handle->Signal(); +} + +// Testcase where the loader is deleted after it queued the parsing of +// the script on V8 thread, but before it gets to finish. +TEST_F(TrustedSignalsTest, ScoringSignalsDeleteBeforeCallback) { + GURL url( + "https://url.test/" + "?hostname=publisher&renderUrls=https%3A%2F%2Ffoo.test%2F"); + + AddJsonResponse(&url_loader_factory_, url, kBaseScoringJson); + + // Wedge the V8 thread to control when the JSON parsing takes place. + base::WaitableEvent* event_handle = WedgeV8Thread(v8_helper_.get()); + auto scoring_signals = TrustedSignals::LoadScoringSignals( + &url_loader_factory_, + /*render_urls=*/{"http://foo.test/"}, + /*ad_component_render_urls=*/{}, "publisher", base_url_, v8_helper_, + base::BindOnce([](std::unique_ptr result, + absl::optional error_msg) { + ADD_FAILURE() << "Callback should not be invoked since loader deleted"; + })); + base::RunLoop().RunUntilIdle(); + scoring_signals.reset(); + event_handle->Signal(); +} + +} // namespace +} // namespace auction_worklet