blob: 6bbdba7a06bd206506fac1f8c39e140da781e6ea [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/browser/interest_group/ad_auction_service_impl.h"
#include <string>
#include <vector>
#include "auction_url_loader_factory_proxy.h"
#include "base/callback_helpers.h"
#include "base/check.h"
#include "base/memory/weak_ptr.h"
#include "base/optional.h"
#include "base/strings/stringprintf.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/storage_partition_impl.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/service_process_host.h"
#include "content/public/common/content_client.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "net/http/http_response_headers.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/metrics/public/cpp/ukm_source_id.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/mojom/url_loader_factory.mojom-forward.h"
#include "services/network/public/mojom/url_loader_factory.mojom.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "url/gurl.h"
#include "url/origin.h"
#include "url/url_constants.h"
namespace content {
namespace {
constexpr net::NetworkTrafficAnnotationTag kTrafficAnnotation =
net::DefineNetworkTrafficAnnotation("auction_report_sender", R"(
semantics {
sender: "Interest group based Ad Auction report"
description:
"Facilitates reporting the result of an in-browser interest group "
"based ad auction to an auction participant. "
"See https://github.com/WICG/turtledove/blob/main/FLEDGE.md"
trigger:
"Requested after running a in-browser interest group based ad "
"auction to report the auction result back to auction participants."
data: "URL associated with an interest group or seller."
destination: WEBSITE
}
policy {
cookies_allowed: NO
setting:
"These requests are controlled by a feature flag that is off by "
"default now. When enabled, they can be disabled by the Privacy"
" Sandbox setting."
policy_exception_justification:
"These requests are triggered by a website."
})");
// Makes a self-owned uncredentialed request. It's used to report the result of
// an in-browser interest group based ad auction to an auction participant.
void FetchReport(network::mojom::URLLoaderFactory* url_loader_factory,
const GURL& url,
const url::Origin& frame_origin) {
auto resource_request = std::make_unique<network::ResourceRequest>();
resource_request->url = url;
resource_request->redirect_mode = network::mojom::RedirectMode::kError;
resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
resource_request->request_initiator = frame_origin;
resource_request->trusted_params = network::ResourceRequest::TrustedParams();
resource_request->trusted_params->isolation_info =
net::IsolationInfo::CreateTransient();
auto simple_url_loader = network::SimpleURLLoader::Create(
std::move(resource_request), kTrafficAnnotation);
network::SimpleURLLoader* simple_url_loader_ptr = simple_url_loader.get();
// Pass simple_url_loader to keep it alive until the request fails or succeeds
// to prevent cancelling the request.
// TODO(qingxin): time out these requests if they take too long.
simple_url_loader_ptr->DownloadHeadersOnly(
url_loader_factory,
base::BindOnce(
base::DoNothing::Once<std::unique_ptr<network::SimpleURLLoader>,
scoped_refptr<net::HttpResponseHeaders>>(),
std::move(simple_url_loader)));
}
struct ValidatedResult {
bool is_valid_auction_result = false;
std::string ad_json;
};
ValidatedResult ValidateAuctionResult(
const std::vector<auction_worklet::mojom::BiddingInterestGroupPtr>& bidders,
const GURL& render_url,
const url::Origin& owner,
const std::string& name) {
ValidatedResult result;
if (!render_url.is_valid() || !render_url.SchemeIs(url::kHttpsScheme))
return result;
for (const auto& bidder : bidders) {
// Auction winner must be one of the bidders and bidder must have ads.
if (bidder->group->owner != owner || bidder->group->name != name ||
!bidder->group->ads) {
continue;
}
// `render_url` must be one of the winning bidder's ads.
for (const auto& ad : bidder->group->ads.value()) {
if (ad->render_url == render_url) {
result.is_valid_auction_result = true;
if (ad->metadata) {
//`metadata` is already in JSON so no quotes are needed.
result.ad_json = base::StringPrintf(
R"({"render_url":"%s","metadata":%s})", render_url.spec().c_str(),
ad->metadata.value().c_str());
} else {
result.ad_json = base::StringPrintf(R"({"render_url":"%s"})",
render_url.spec().c_str());
}
return result;
}
}
}
return result;
}
} // namespace
AdAuctionServiceImpl::AdAuctionServiceImpl(
RenderFrameHost* render_frame_host,
mojo::PendingReceiver<blink::mojom::AdAuctionService> receiver)
: FrameServiceBase(render_frame_host, std::move(receiver)) {}
AdAuctionServiceImpl::~AdAuctionServiceImpl() = default;
// static
void AdAuctionServiceImpl::CreateMojoService(
RenderFrameHost* render_frame_host,
mojo::PendingReceiver<blink::mojom::AdAuctionService> receiver) {
DCHECK(render_frame_host);
// The object is bound to the lifetime of `render_frame_host` and the mojo
// connection. See FrameServiceBase for details.
new AdAuctionServiceImpl(render_frame_host, std::move(receiver));
}
void AdAuctionServiceImpl::RunAdAuction(blink::mojom::AuctionAdConfigPtr config,
RunAdAuctionCallback callback) {
const url::Origin frame_origin = origin();
// If the interest group API is not allowed for this seller do nothing.
if (!GetContentClient()->browser()->IsInterestGroupAPIAllowed(
render_frame_host()->GetBrowserContext(), frame_origin,
config->seller.GetURL())) {
std::move(callback).Run(base::nullopt);
return;
}
// The seller origin has to match the `decision_logic_url` origin.
if (config->seller.scheme() != url::kHttpsScheme ||
!config->decision_logic_url.SchemeIs(url::kHttpsScheme) ||
config->seller != url::Origin::Create(config->decision_logic_url)) {
std::move(callback).Run(base::nullopt);
return;
}
if (!config->interest_group_buyers ||
config->interest_group_buyers->is_all_buyers()) {
std::move(callback).Run(base::nullopt);
return;
}
DCHECK(config->interest_group_buyers->is_buyers());
auto buyers = config->interest_group_buyers->get_buyers();
std::vector<url::Origin> trimmed_buyers;
std::copy_if(
buyers.begin(), buyers.end(), std::back_inserter(trimmed_buyers),
[this, &frame_origin](const url::Origin& buyer) {
return GetContentClient()->browser()->IsInterestGroupAPIAllowed(
render_frame_host()->GetBrowserContext(), frame_origin,
buyer.GetURL());
});
if (trimmed_buyers.size() == 0) {
std::move(callback).Run(base::nullopt);
return;
}
GetInterestGroupsFromStorage(std::move(config), trimmed_buyers,
std::move(callback));
}
InterestGroupManager* AdAuctionServiceImpl::GetInterestGroupManager() {
return static_cast<StoragePartitionImpl*>(
render_frame_host()->GetStoragePartition())
->GetInterestGroupStorage();
}
void AdAuctionServiceImpl::LaunchWorkletServiceIfNeeded() {
if (auction_worklet_service_ && auction_worklet_service_.is_connected())
return;
auction_worklet_service_.reset();
content::ServiceProcessHost::Launch(
auction_worklet_service_.BindNewPipeAndPassReceiver(),
ServiceProcessHost::Options()
.WithDisplayName("Auction Worklet Sevice")
.Pass());
}
void AdAuctionServiceImpl::GetInterestGroupsFromStorage(
blink::mojom::AuctionAdConfigPtr config,
const std::vector<url::Origin>& buyers,
RunAdAuctionCallback callback) {
if (buyers.empty()) {
std::move(callback).Run(base::nullopt);
return;
}
// Buyers in `per_buyer_signals` should be a subset of `buyers`.
if (config->per_buyer_signals) {
for (const auto& it : config->per_buyer_signals.value()) {
if (!base::Contains(buyers, it.first)) {
std::move(callback).Run(base::nullopt);
return;
}
}
}
std::vector<auction_worklet::mojom::BiddingInterestGroupPtr> bidders;
std::vector<auction_worklet::mojom::BiddingInterestGroupPtr> interest_groups;
GetInterestGroup(buyers, std::move(bidders), std::move(config),
std::move(callback), std::move(interest_groups));
}
void AdAuctionServiceImpl::GetInterestGroup(
std::vector<url::Origin> buyers,
std::vector<auction_worklet::mojom::BiddingInterestGroupPtr> bidders,
blink::mojom::AuctionAdConfigPtr config,
RunAdAuctionCallback callback,
std::vector<auction_worklet::mojom::BiddingInterestGroupPtr>
interest_groups) {
for (auto& interest_group : interest_groups)
bidders.emplace_back(std::move(interest_group));
// Get interest groups of each buyer from storage and push them to bidders.
if (!buyers.empty()) {
url::Origin buyer = buyers.back();
buyers.pop_back();
if (buyer.scheme() != url::kHttpsScheme) {
std::move(callback).Run(base::nullopt);
return;
}
GetInterestGroupManager()->GetInterestGroupsForOwner(
buyer, base::BindOnce(&AdAuctionServiceImpl::GetInterestGroup,
weak_ptr_factory_.GetWeakPtr(), std::move(buyers),
std::move(bidders), std::move(config),
std::move(callback)));
return;
}
StartAuction(std::move(config), std::move(bidders), std::move(callback));
}
void AdAuctionServiceImpl::StartAuction(
blink::mojom::AuctionAdConfigPtr config,
std::vector<auction_worklet::mojom::BiddingInterestGroupPtr> bidders,
RunAdAuctionCallback callback) {
if (bidders.empty()) {
std::move(callback).Run(base::nullopt);
return;
}
// TODO(qingxin): Determine if one service per auction / frame is good.
LaunchWorkletServiceIfNeeded();
auto browser_signals =
auction_worklet::mojom::BrowserSignals::New(origin(), config->seller);
mojo::PendingRemote<network::mojom::URLLoaderFactory> url_loader_factory;
auto url_loader_factory_proxy =
std::make_unique<AuctionURLLoaderFactoryProxy>(
url_loader_factory.InitWithNewPipeAndPassReceiver(),
base::BindRepeating(&AdAuctionServiceImpl::GetURLLoaderFactory,
base::Unretained(this)),
browser_signals->top_frame_origin, *config, bidders);
// If the AuctionWorklet service crashes, it will silently delete the bound
// WorkletComplete callback. Create a ScopedClosureRunner to invoke the
// RunAdAuctionCallback if the WorkletComplete callback is destroyed without
// being invoked.
// TODO(crbug.com/1201642): Redesign this to handle worklet crashes
// differently.
std::unique_ptr<RunAdAuctionCallback> owned_callback =
std::make_unique<RunAdAuctionCallback>(std::move(callback));
RunAdAuctionCallback* unowned_callback = owned_callback.get();
base::ScopedClosureRunner on_crash(base::BindOnce(
&AdAuctionServiceImpl::OnMaybeWorkletCrashed,
weak_ptr_factory_.GetWeakPtr(), std::move(owned_callback)));
std::vector<auction_worklet::mojom::BiddingInterestGroupPtr> bidders_copy;
bidders_copy.reserve(bidders.size());
for (auto& bidder : bidders)
bidders_copy.emplace_back(bidder.Clone());
++running_auctions_;
auction_worklet_service_->RunAuction(
std::move(url_loader_factory), std::move(config), std::move(bidders),
std::move(browser_signals),
base::BindOnce(&AdAuctionServiceImpl::WorkletComplete,
weak_ptr_factory_.GetWeakPtr(), std::move(bidders_copy),
unowned_callback, std::move(url_loader_factory_proxy),
std::move(on_crash)));
}
void AdAuctionServiceImpl::WorkletComplete(
std::vector<auction_worklet::mojom::BiddingInterestGroupPtr> bidders,
RunAdAuctionCallback* callback,
std::unique_ptr<AuctionURLLoaderFactoryProxy> url_loader_factory_proxy,
base::ScopedClosureRunner on_crash,
const GURL& render_url,
const url::Origin& owner,
const std::string& name,
auction_worklet::mojom::WinningBidderReportPtr bidder_report,
auction_worklet::mojom::SellerReportPtr seller_report) {
// Release process if needed.
AuctionComplete();
// Check if returned winner's information is valid.
ValidatedResult result =
ValidateAuctionResult(bidders, render_url, owner, name);
if (!result.is_valid_auction_result) {
std::move(*callback).Run(base::nullopt);
return;
}
std::move(*callback).Run(render_url);
GetInterestGroupManager()->RecordInterestGroupWin(owner, name,
result.ad_json);
// TODO(qingxin): Decide if we should record a bid if the auction fails, or
// the interest group doesn't make a bid.
for (const auto& bidder : bidders) {
GetInterestGroupManager()->RecordInterestGroupBid(bidder->group->owner,
bidder->group->name);
}
network::mojom::URLLoaderFactory* factory = GetURLLoaderFactory();
if (bidder_report->report_requested && bidder_report->report_url.is_valid() &&
bidder_report->report_url.SchemeIs(url::kHttpsScheme)) {
FetchReport(factory, bidder_report->report_url, origin());
}
if (seller_report->report_requested && seller_report->report_url.is_valid() &&
seller_report->report_url.SchemeIs(url::kHttpsScheme)) {
FetchReport(factory, seller_report->report_url, origin());
}
}
network::mojom::URLLoaderFactory* AdAuctionServiceImpl::GetURLLoaderFactory() {
if (!url_loader_factory_ || !url_loader_factory_.is_connected()) {
url_loader_factory_.reset();
mojo::PendingReceiver<network::mojom::URLLoaderFactory> factory_receiver =
url_loader_factory_.BindNewPipeAndPassReceiver();
// TODO(mmenke): Should this have its own URLLoaderFactoryType? FLEDGE
// requests are very different from subresource requests.
//
// TODO(mmenke): Hook up devtools.
GetContentClient()->browser()->WillCreateURLLoaderFactory(
render_frame_host()->GetSiteInstance()->GetBrowserContext(),
render_frame_host(), render_frame_host()->GetProcess()->GetID(),
ContentBrowserClient::URLLoaderFactoryType::kDocumentSubResource,
url::Origin(), base::nullopt /* navigation_id */,
ukm::SourceIdObj::FromInt64(render_frame_host()->GetPageUkmSourceId()),
&factory_receiver, nullptr /* header_client */,
nullptr /* bypass_redirect_checks */, nullptr /* disable_secure_dns */,
nullptr /* factory_override */);
render_frame_host()
->GetStoragePartition()
->GetURLLoaderFactoryForBrowserProcess()
->Clone(std::move(factory_receiver));
}
return url_loader_factory_.get();
}
void AdAuctionServiceImpl::AuctionComplete() {
DCHECK_GE(running_auctions_, 1);
--running_auctions_;
// Shutdown the process if we don't need it.
if (running_auctions_ == 0)
auction_worklet_service_.reset();
}
// static
void AdAuctionServiceImpl::OnMaybeWorkletCrashed(
base::WeakPtr<AdAuctionServiceImpl> self,
std::unique_ptr<AdAuctionServiceImpl::RunAdAuctionCallback> callback) {
// The callback is null if WorkletComplete already ran it, e.g. no crash has
// happened.
if (callback->is_null())
return;
// self may be null if we got destroyed due to navigating away or the renderer
// closing the pipe. At that point there is no need to call AuctionComplete,
// but the callback may still need to be run in the former case if the pipe is
// still alive for now.
if (self)
self->AuctionComplete();
std::move(*callback).Run(base::nullopt);
}
} // namespace content