blob: b45bb28cd1012412508c9e7ff6f8c939da47efaa [file] [log] [blame]
// 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 <memory>
#include <set>
#include <string>
#include <vector>
#include "base/bind.h"
#include "base/callback.h"
#include "base/check.h"
#include "base/memory/ptr_util.h"
#include "base/memory/scoped_refptr.h"
#include "base/metrics/histogram_functions.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 "net/base/parse_number.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 `&<name>=<values in comma-delimited list>`.
// 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<std::string>& 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<std::string, std::string> ParseKeyValueMap(
AuctionV8Helper* v8_helper,
v8::Local<v8::Object> v8_object,
const std::set<std::string>& keys) {
std::map<std::string, std::string> out;
if (keys.empty())
return out;
for (const auto& key : keys) {
v8::Local<v8::String> v8_key;
if (!v8_helper->CreateUtf8String(key).ToLocal(&v8_key))
continue;
v8::Local<v8::Value> v8_value;
v8::Local<v8::Value> 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<std::string, std::string> ParseChildKeyValueMap(
AuctionV8Helper* v8_helper,
v8::Local<v8::Object> v8_object,
const char* name,
const std::set<std::string>& keys) {
std::map<std::string, std::string> out;
if (keys.empty())
return out;
v8::Local<v8::Value> 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<v8::Object>(), 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<v8::Object> CreateObjectFromMap(
const std::vector<std::string>& keys,
const std::map<std::string, std::string>& json_data,
AuctionV8Helper* v8_helper,
v8::Local<v8::Context> context) {
DCHECK(v8_helper->v8_runner()->RunsTasksInCurrentSequence());
DCHECK(!keys.empty());
v8::Local<v8::Object> 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<std::string, std::string> bidder_json_data,
absl::optional<uint32_t> data_version)
: bidder_json_data_(std::move(bidder_json_data)),
data_version_(data_version) {}
TrustedSignals::Result::Result(
std::map<std::string, std::string> render_url_json_data,
std::map<std::string, std::string> ad_component_json_data,
absl::optional<uint32_t> data_version)
: render_url_json_data_(std::move(render_url_json_data)),
ad_component_json_data_(std::move(ad_component_json_data)),
data_version_(data_version) {}
v8::Local<v8::Object> TrustedSignals::Result::GetBiddingSignals(
AuctionV8Helper* v8_helper,
v8::Local<v8::Context> context,
const std::vector<std::string>& 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<v8::Object> TrustedSignals::Result::GetScoringSignals(
AuctionV8Helper* v8_helper,
v8::Local<v8::Context> context,
const GURL& render_url,
const std::vector<std::string>& 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<v8::Object> out = v8::Object::New(v8_helper->isolate());
// Create renderUrl sub-object, and add it to to `out`.
v8::Local<v8::Object> render_url_v8_object =
CreateObjectFromMap(std::vector<std::string>{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<v8::Object> 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;
}
TrustedSignals::Result::~Result() = default;
std::unique_ptr<TrustedSignals> TrustedSignals::LoadBiddingSignals(
network::mojom::URLLoaderFactory* url_loader_factory,
std::set<std::string> bidding_signals_keys,
const std::string& hostname,
const GURL& trusted_bidding_signals_url,
scoped_refptr<AuctionV8Helper> v8_helper,
LoadSignalsCallback load_signals_callback) {
DCHECK(!bidding_signals_keys.empty());
std::unique_ptr<TrustedSignals> trusted_signals = base::WrapUnique(
new TrustedSignals(std::move(bidding_signals_keys),
/*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);
base::UmaHistogramCounts100000(
"Ads.InterestGroup.Net.RequestUrlSizeBytes.TrustedBidding",
full_signals_url.spec().size());
trusted_signals->StartDownload(url_loader_factory, full_signals_url);
return trusted_signals;
}
std::unique_ptr<TrustedSignals> TrustedSignals::LoadScoringSignals(
network::mojom::URLLoaderFactory* url_loader_factory,
std::set<std::string> render_urls,
std::set<std::string> ad_component_render_urls,
const std::string& hostname,
const GURL& trusted_scoring_signals_url,
scoped_refptr<AuctionV8Helper> v8_helper,
LoadSignalsCallback load_signals_callback) {
DCHECK(!render_urls.empty());
std::unique_ptr<TrustedSignals> trusted_signals =
base::WrapUnique(new TrustedSignals(
/*bidding_signals_keys=*/absl::nullopt, std::move(render_urls),
std::move(ad_component_render_urls), 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);
base::UmaHistogramCounts100000(
"Ads.InterestGroup.Net.RequestUrlSizeBytes.TrustedScoring",
full_signals_url.spec().size());
trusted_signals->StartDownload(url_loader_factory, full_signals_url);
return trusted_signals;
}
TrustedSignals::TrustedSignals(
absl::optional<std::set<std::string>> bidding_signals_keys,
absl::optional<std::set<std::string>> render_urls,
absl::optional<std::set<std::string>> ad_component_render_urls,
const GURL& trusted_signals_url,
scoped_refptr<AuctionV8Helper> 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<AuctionDownloader>(
url_loader_factory, full_signals_url, AuctionDownloader::MimeType::kJson,
base::BindOnce(&TrustedSignals::OnDownloadComplete,
base::Unretained(this)));
}
void TrustedSignals::OnDownloadComplete(
std::unique_ptr<std::string> body,
scoped_refptr<net::HttpResponseHeaders> headers,
absl::optional<std::string> 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(headers), std::move(error_msg),
base::SequencedTaskRunnerHandle::Get(),
weak_ptr_factory.GetWeakPtr()));
}
// static
void TrustedSignals::HandleDownloadResultOnV8Thread(
scoped_refptr<AuctionV8Helper> v8_helper,
const GURL& signals_url,
absl::optional<std::set<std::string>> bidding_signals_keys,
absl::optional<std::set<std::string>> render_urls,
absl::optional<std::set<std::string>> ad_component_render_urls,
std::unique_ptr<std::string> body,
scoped_refptr<net::HttpResponseHeaders> headers,
absl::optional<std::string> error_msg,
scoped_refptr<base::SequencedTaskRunner> user_thread_task_runner,
base::WeakPtr<TrustedSignals> weak_instance) {
if (!body) {
PostCallbackToUserThread(std::move(user_thread_task_runner), weak_instance,
nullptr, std::move(error_msg));
return;
}
DCHECK(!error_msg.has_value());
uint32_t data_version;
std::string data_version_string;
if (headers &&
headers->GetNormalizedHeader("Data-Version", &data_version_string) &&
!net::ParseUint32(data_version_string, &data_version)) {
std::string error = base::StringPrintf(
"Rejecting load of %s due to invalid Data-Version header: %s",
signals_url.spec().c_str(), data_version_string.c_str());
PostCallbackToUserThread(std::move(user_thread_task_runner), weak_instance,
nullptr, std::move(error));
return;
}
AuctionV8Helper::FullIsolateScope isolate_scope(v8_helper.get());
v8::Context::Scope context_scope(v8_helper->scratch_context());
v8::Local<v8::Value> 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_object = v8_data.As<v8::Object>();
scoped_refptr<Result> result;
absl::optional<uint32_t> maybe_data_version;
if (!data_version_string.empty())
maybe_data_version = data_version;
if (bidding_signals_keys) {
// Handle bidding signals case.
base::UmaHistogramCounts10M(
"Ads.InterestGroup.Net.ResponseSizeBytes.TrustedBidding", body->size());
result = base::MakeRefCounted<Result>(
ParseKeyValueMap(v8_helper.get(), v8_object, *bidding_signals_keys),
maybe_data_version);
} else {
// Handle scoring signals case.
base::UmaHistogramCounts10M(
"Ads.InterestGroup.Net.ResponseSizeBytes.TrustedScoring", body->size());
result = base::MakeRefCounted<Result>(
ParseChildKeyValueMap(v8_helper.get(), v8_object, "renderUrls",
*render_urls),
ParseChildKeyValueMap(v8_helper.get(), v8_object,
"adComponentRenderUrls",
*ad_component_render_urls),
maybe_data_version);
}
PostCallbackToUserThread(std::move(user_thread_task_runner), weak_instance,
std::move(result), absl::nullopt);
}
void TrustedSignals::PostCallbackToUserThread(
scoped_refptr<base::SequencedTaskRunner> user_thread_task_runner,
base::WeakPtr<TrustedSignals> weak_instance,
scoped_refptr<Result> result,
absl::optional<std::string> 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(
scoped_refptr<Result> result,
absl::optional<std::string> error_msg) {
std::move(load_signals_callback_)
.Run(std::move(result), std::move(error_msg));
}
} // namespace auction_worklet