| // Copyright 2021 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "content/browser/interest_group/ad_auction_service_impl.h" |
| |
| #include <algorithm> |
| #include <cstddef> |
| #include <map> |
| #include <memory> |
| #include <optional> |
| #include <set> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/check.h" |
| #include "base/check_op.h" |
| #include "base/containers/contains.h" |
| #include "base/containers/span.h" |
| #include "base/feature_list.h" |
| #include "base/functional/bind.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/time/time.h" |
| #include "base/trace_event/named_trigger.h" |
| #include "base/types/expected.h" |
| #include "base/uuid.h" |
| #include "components/aggregation_service/aggregation_coordinator_utils.h" |
| #include "content/browser/attribution_reporting/attribution_manager.h" |
| #include "content/browser/devtools/devtools_instrumentation.h" |
| #include "content/browser/fenced_frame/fenced_frame_reporter.h" |
| #include "content/browser/fenced_frame/fenced_frame_url_mapping.h" |
| #include "content/browser/interest_group/ad_auction_document_data.h" |
| #include "content/browser/interest_group/ad_auction_page_data.h" |
| #include "content/browser/interest_group/ad_auction_result_metrics.h" |
| #include "content/browser/interest_group/auction_runner.h" |
| #include "content/browser/interest_group/auction_worklet_manager.h" |
| #include "content/browser/interest_group/interest_group_features.h" |
| #include "content/browser/interest_group/interest_group_manager_impl.h" |
| #include "content/browser/interest_group/protected_audience_network_util.h" |
| #include "content/browser/loader/reconnectable_url_loader_factory.h" |
| #include "content/browser/loader/url_loader_factory_utils.h" |
| #include "content/browser/private_aggregation/private_aggregation_manager.h" |
| #include "content/browser/renderer_host/frame_tree_node.h" |
| #include "content/browser/renderer_host/page_impl.h" |
| #include "content/browser/renderer_host/policy_container_host.h" |
| #include "content/browser/renderer_host/render_frame_host_impl.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/content_browser_client.h" |
| #include "content/public/browser/cookie_deprecation_label_manager.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/render_process_host.h" |
| #include "content/public/common/content_client.h" |
| #include "content/services/auction_worklet/public/mojom/private_aggregation_request.mojom.h" |
| #include "mojo/public/cpp/bindings/pending_receiver.h" |
| #include "mojo/public/cpp/bindings/remote.h" |
| #include "net/base/network_anonymization_key.h" |
| #include "net/http/http_response_headers.h" |
| #include "net/third_party/quiche/src/quiche/oblivious_http/oblivious_http_client.h" |
| #include "net/traffic_annotation/network_traffic_annotation.h" |
| #include "services/data_decoder/public/cpp/data_decoder.h" |
| #include "services/metrics/public/cpp/ukm_source_id.h" |
| #include "services/network/public/cpp/simple_url_loader.h" |
| #include "services/network/public/mojom/client_security_state.mojom.h" |
| #include "services/network/public/mojom/connection_change_observer_client.mojom.h" |
| #include "services/network/public/mojom/permissions_policy/permissions_policy_feature.mojom.h" |
| #include "services/network/public/mojom/url_loader_factory.mojom.h" |
| #include "services/network/public/mojom/url_response_head.mojom.h" |
| #include "third_party/blink/public/common/features.h" |
| #include "third_party/blink/public/common/fenced_frame/fenced_frame_utils.h" |
| #include "third_party/blink/public/common/interest_group/ad_auction_constants.h" |
| #include "third_party/blink/public/common/interest_group/auction_config.h" |
| #include "third_party/blink/public/common/interest_group/interest_group.h" |
| #include "third_party/blink/public/common/permissions_policy/policy_helper_public.h" |
| #include "third_party/blink/public/mojom/aggregation_service/aggregatable_report.mojom.h" |
| #include "third_party/blink/public/mojom/interest_group/interest_group_types.mojom.h" |
| #include "third_party/blink/public/mojom/private_aggregation/private_aggregation_host.mojom.h" |
| #include "url/gurl.h" |
| #include "url/origin.h" |
| #include "url/scheme_host_port.h" |
| #include "url/url_constants.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| bool IsAdRequestValid(const blink::mojom::AdRequestConfig& config) { |
| // The ad_request_url origin has to be HTTPS. |
| if (config.ad_request_url.scheme() != url::kHttpsScheme) { |
| return false; |
| } |
| |
| // At least one adProperties is required to request potential ads. |
| if (config.ad_properties.size() <= 0) { |
| return false; |
| } |
| |
| // If a fallback source is specified it must be HTTPS. |
| if (config.fallback_source && |
| (config.fallback_source->scheme() != url::kHttpsScheme)) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| // This function is used as a callback to verify |
| // `InterestGroup::Ad::allowed_reporting_origins` are attested. These origins |
| // are specified as part of the ads during `joinAdInterestGroup()` and |
| // `updateAdInterestGroups()`. They receive reporting beacons sent by |
| // `reportEvent()` when reporting to custom urls. |
| bool AreAllowedReportingOriginsAttested( |
| BrowserContext* browser_context, |
| const std::vector<url::Origin>& origins) { |
| for (const auto& origin : origins) { |
| if (!GetContentClient() |
| ->browser() |
| ->IsPrivacySandboxReportingDestinationAttested( |
| browser_context, origin, |
| PrivacySandboxInvokingAPI::kProtectedAudience)) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| // Returns true if changing the default permission policy for `feature` from |
| // EnableForAll to EnableForSelf would disable `feature` for frame, and so a |
| // warning should be displayed when a call relies on EnableForAll. This method |
| // assumes the permission is enabled with the current EnableForAll policy, so it |
| // only needs to check if every cross-origin frame up to the root frame allows |
| // `feature` for the child frame's origin. |
| bool ShouldWarnAboutPermissionPolicyDefault( |
| RenderFrameHostImpl& frame, |
| network::mojom::PermissionsPolicyFeature feature) { |
| RenderFrameHostImpl* parent = frame.GetParent(); |
| if (!parent) { |
| return false; |
| } |
| const auto container_policy = |
| frame.browsing_context_state()->effective_frame_policy().container_policy; |
| const url::Origin& frame_origin = frame.GetLastCommittedOrigin(); |
| if (parent->GetLastCommittedOrigin() != frame_origin) { |
| bool found_match = false; |
| for (const auto& declaration : container_policy) { |
| if (declaration.feature == feature) { |
| auto allowlist = |
| network::PermissionsPolicy::Allowlist::FromDeclaration(declaration); |
| if (!allowlist.Contains(frame_origin)) { |
| return true; |
| } |
| found_match = true; |
| } |
| } |
| if (!found_match) { |
| return true; |
| } |
| } |
| return ShouldWarnAboutPermissionPolicyDefault(*parent, feature); |
| } |
| |
| void RecordBaDataConstructionResultMetric(size_t data_size, |
| base::TimeTicks start_time) { |
| // Request sizes only increase by factors of two so we only need to sample |
| // the powers of two. The maximum of 1 GB size is much larger than it should |
| // ever be. |
| base::UmaHistogramCustomCounts(/*name=*/"Ads.InterestGroup.BaDataSize2", |
| /*sample=*/data_size, /*min=*/1, |
| /*exclusive_max=*/1 << 30, /*buckets=*/30); |
| |
| base::UmaHistogramTimes(/*name=*/"Ads.InterestGroup.BaDataConstructionTime2", |
| /*sample=*/base::TimeTicks::Now() - start_time); |
| } |
| |
| } // namespace |
| |
| AdAuctionServiceImpl::BiddingAndAuctionDataConstructionState:: |
| BiddingAndAuctionDataConstructionState() |
| : start_time(base::TimeTicks::Now()), |
| request_id(base::Uuid::GenerateRandomV4()) {} |
| AdAuctionServiceImpl::BiddingAndAuctionDataConstructionState:: |
| BiddingAndAuctionDataConstructionState( |
| BiddingAndAuctionDataConstructionState&& other) = default; |
| AdAuctionServiceImpl::BiddingAndAuctionDataConstructionState:: |
| ~BiddingAndAuctionDataConstructionState() = default; |
| |
| // static |
| void AdAuctionServiceImpl::CreateMojoService( |
| RenderFrameHost* render_frame_host, |
| mojo::PendingReceiver<blink::mojom::AdAuctionService> receiver) { |
| CHECK(render_frame_host); |
| |
| // The object is bound to the lifetime of `render_frame_host` and the mojo |
| // connection. See DocumentService for details. |
| new AdAuctionServiceImpl(*render_frame_host, std::move(receiver)); |
| } |
| |
| void AdAuctionServiceImpl::JoinInterestGroup( |
| const blink::InterestGroup& group, |
| JoinInterestGroupCallback callback) { |
| if (!JoinOrLeaveApiAllowedFromRenderer(group.owner, "joinAdInterestGroup")) { |
| // TODO(https://crbug.com/382786767): Remove this call once the issue has |
| // been fixed, since JoinOrLeaveApiAllowedFromRenderer() will then always |
| // delete `this` and closed the Mojo pipe when it returns false. |
| std::move(callback).Run(/*failed_well_known_check=*/true); |
| return; |
| } |
| |
| // If the interest group API is not allowed for this origin, report the result |
| // of the permissions check, but don't actually join the interest group. |
| // The return value of IsInterestGroupAPIAllowed() is potentially affected by |
| // a user's browser configuration, which shouldn't be leaked to sites to |
| // protect against fingerprinting. |
| bool report_result_only = !IsInterestGroupAPIAllowed( |
| ContentBrowserClient::InterestGroupApiOperation::kJoin, group.owner); |
| |
| // If the group is allowed, we also do a permissions/attestation check on |
| // trusted bidding signals URL, in case it's 3rd party. |
| if (!report_result_only && group.trusted_bidding_signals_url.has_value()) { |
| url::Origin trusted_bidding_signals_origin = |
| url::Origin::Create(group.trusted_bidding_signals_url.value()); |
| if (!trusted_bidding_signals_origin.IsSameOriginWith(group.owner) && |
| !IsInterestGroupAPIAllowed( |
| ContentBrowserClient::InterestGroupApiOperation::kJoin, |
| trusted_bidding_signals_origin)) { |
| report_result_only = true; |
| render_frame_host().AddMessageToConsole( |
| blink::mojom::ConsoleMessageLevel::kWarning, |
| base::StringPrintf( |
| "joinAdInterestGroup of interest group with owner '%s' blocked " |
| "because it lacks attestation of cross-origin trusted signals " |
| "origin '%s' or that origin is disallowed by user preferences", |
| group.owner.Serialize().c_str(), |
| trusted_bidding_signals_origin.Serialize().c_str())); |
| } |
| } |
| |
| blink::InterestGroup updated_group = group; |
| base::Time max_expiry = base::Time::Now() + blink::MaxInterestGroupLifetime(); |
| if (updated_group.expiry > max_expiry) { |
| updated_group.expiry = max_expiry; |
| } |
| |
| if (updated_group.aggregation_coordinator_origin && |
| !aggregation_service::IsAggregationCoordinatorOriginAllowed( |
| updated_group.aggregation_coordinator_origin.value())) { |
| ReportBadMessageAndDeleteThis( |
| "Unexpected request: aggregationCoordinatorOrigin is not supported."); |
| return; |
| } |
| |
| // `base::Unretained` is safe here since the `BrowserContext` owns the |
| // `StoragePartition` that owns the interest group manager. |
| GetInterestGroupManager().CheckPermissionsAndJoinInterestGroup( |
| std::move(updated_group), main_frame_url_, origin(), |
| GetFrame()->GetNetworkIsolationKey(), report_result_only, |
| *GetFrameURLLoaderFactory(), |
| base::BindRepeating( |
| &AreAllowedReportingOriginsAttested, |
| base::Unretained(render_frame_host().GetBrowserContext())), |
| std::move(callback)); |
| } |
| |
| void AdAuctionServiceImpl::LeaveInterestGroup( |
| const url::Origin& owner, |
| const std::string& name, |
| LeaveInterestGroupCallback callback) { |
| if (!JoinOrLeaveApiAllowedFromRenderer(owner, "leaveAdInterestGroup")) { |
| // TODO(https://crbug.com/382786767): Remove this call once the issue has |
| // been fixed, since JoinOrLeaveApiAllowedFromRenderer() will then always |
| // delete `this` and closed the Mojo pipe when it returns false. |
| std::move(callback).Run(/*failed_well_known_check=*/true); |
| return; |
| } |
| |
| // If the interest group API is not allowed for this origin, report the result |
| // of the permissions check, but don't actually join the interest group. |
| // The return value of IsInterestGroupAPIAllowed() is potentially affected by |
| // a user's browser configuration, which shouldn't be leaked to sites to |
| // protect against fingerprinting. |
| bool report_result_only = !IsInterestGroupAPIAllowed( |
| ContentBrowserClient::InterestGroupApiOperation::kLeave, owner); |
| |
| GetInterestGroupManager().CheckPermissionsAndLeaveInterestGroup( |
| blink::InterestGroupKey(owner, name), main_frame_origin_, origin(), |
| GetFrame()->GetNetworkIsolationKey(), report_result_only, |
| *GetFrameURLLoaderFactory(), std::move(callback)); |
| } |
| |
| void AdAuctionServiceImpl::LeaveInterestGroupForDocument() { |
| // Based on the spec, permission policy is bypassed for leaving implicit |
| // interest groups. |
| |
| // If the interest group API is not allowed for this origin do nothing. |
| if (!IsInterestGroupAPIAllowed( |
| ContentBrowserClient::InterestGroupApiOperation::kLeave, origin())) { |
| return; |
| } |
| |
| if (origin().scheme() != url::kHttpsScheme) { |
| ReportBadMessageAndDeleteThis( |
| "Unexpected request: LeaveInterestGroupForDocument only supported for " |
| "secure origins"); |
| return; |
| } |
| |
| // Get interest group owner and name from the ad auction data, which is part |
| // of the fenced frame properties. Here the fenced frame properties are |
| // obtained from the closest ancestor that has valid fenced frame properties. |
| // This is because both top-level ads and ad components may have ad auction |
| // data. |
| const std::optional<FencedFrameProperties>& fenced_frame_properties = |
| GetFrame()->frame_tree_node()->GetFencedFrameProperties( |
| FencedFramePropertiesNodeSource::kClosestAncestor); |
| |
| // This frame is neither a fenced frame or an urn iframe itself, nor it is |
| // nested within a fenced frame or an urn iframe. |
| if (!fenced_frame_properties.has_value()) { |
| devtools_instrumentation::LogWorkletMessage( |
| *GetFrame(), blink::mojom::ConsoleMessageLevel::kError, |
| "Owner and name are required to call LeaveAdInterestGroup outside of " |
| "a fenced frame or an opaque origin iframe."); |
| return; |
| } |
| |
| if (!fenced_frame_properties->ad_auction_data().has_value()) { |
| return; |
| } |
| |
| const blink::FencedFrame::AdAuctionData& auction_data = |
| fenced_frame_properties->ad_auction_data()->GetValueIgnoringVisibility(); |
| |
| if (auction_data.interest_group_owner != origin()) { |
| // The ad page calling LeaveAdInterestGroup is not the owner of the group. |
| return; |
| } |
| |
| GetInterestGroupManager().LeaveInterestGroup( |
| blink::InterestGroupKey(auction_data.interest_group_owner, |
| auction_data.interest_group_name), |
| main_frame_origin_); |
| } |
| |
| void AdAuctionServiceImpl::ClearOriginJoinedInterestGroups( |
| const url::Origin& owner, |
| const std::vector<std::string>& interest_groups_to_keep, |
| ClearOriginJoinedInterestGroupsCallback callback) { |
| if (!JoinOrLeaveApiAllowedFromRenderer(owner, |
| "clearOriginJoinedAdInterestGroups")) { |
| // TODO(https://crbug.com/382786767): Remove this call once the issue has |
| // been fixed, since JoinOrLeaveApiAllowedFromRenderer() will then always |
| // delete `this` and closed the Mojo pipe when it returns false. |
| std::move(callback).Run(/*failed_well_known_check=*/true); |
| return; |
| } |
| |
| // If the interest group leave API is not allowed for this origin, report the |
| // result of the permissions check, but don't actually join the interest |
| // group. The return value of IsInterestGroupAPIAllowed() is potentially |
| // affected by a user's browser configuration, which shouldn't be leaked to |
| // sites to protect against fingerprinting. |
| bool report_result_only = !IsInterestGroupAPIAllowed( |
| ContentBrowserClient::InterestGroupApiOperation::kLeave, owner); |
| |
| GetInterestGroupManager().CheckPermissionsAndClearOriginJoinedInterestGroups( |
| owner, interest_groups_to_keep, main_frame_origin_, origin(), |
| GetFrame()->GetNetworkIsolationKey(), report_result_only, |
| *GetFrameURLLoaderFactory(), std::move(callback)); |
| } |
| |
| void AdAuctionServiceImpl::UpdateAdInterestGroups() { |
| // If the interest group API is not allowed for this context by Permissions |
| // Policy, do nothing |
| if (!IsPermissionPolicyEnabledAndWarnIfNeeded( |
| network::mojom::PermissionsPolicyFeature::kJoinAdInterestGroup, |
| "updateAdInterestGroups")) { |
| // TODO(https://crbug.com/382786767): Figure out why permission policy can |
| // be inconsistent between the browser and renderer policy, fix it, and then |
| // call ReportBadMessageAndDeleteThis() here. |
| return; |
| } |
| // If the interest group API is not allowed for this origin do nothing. |
| if (!IsInterestGroupAPIAllowed( |
| ContentBrowserClient::InterestGroupApiOperation::kUpdate, origin())) { |
| return; |
| } |
| |
| std::optional<std::string> user_agent_override = |
| GetUserAgentOverrideForProtectedAudience(GetFrame()->frame_tree_node()); |
| |
| // `base::Unretained` is safe here since the `BrowserContext` owns the |
| // `StoragePartition` that owns the interest group manager. |
| GetInterestGroupManager().UpdateInterestGroupsOfOwners( |
| {origin()}, GetClientSecurityState(), user_agent_override, |
| base::BindRepeating( |
| &AreAllowedReportingOriginsAttested, |
| base::Unretained(render_frame_host().GetBrowserContext()))); |
| } |
| |
| void AdAuctionServiceImpl::RunAdAuction( |
| const blink::AuctionConfig& config, |
| mojo::PendingReceiver<blink::mojom::AbortableAdAuction> abort_receiver, |
| RunAdAuctionCallback callback) { |
| // Ensure the page is not in prerendering as code belows expect it, i.e. |
| // GetPageUkmSourceId() doesn't work with prerendering pages. |
| CHECK(!render_frame_host().IsInLifecycleState( |
| RenderFrameHost::LifecycleState::kPrerendering)); |
| |
| // If the run ad auction API is not allowed for this context by Permissions |
| // Policy, do nothing. |
| if (!IsPermissionPolicyEnabledAndWarnIfNeeded( |
| network::mojom::PermissionsPolicyFeature::kRunAdAuction, |
| "runAdAuction")) { |
| // TODO(https://crbug.com/382786767): Figure out why permission policy can |
| // be inconsistent between the browser and renderer policy, fix it, and then |
| // call ReportBadMessageAndDeleteThis() here. |
| std::move(callback).Run(/*aborted_by_script=*/false, |
| /*config=*/std::nullopt); |
| return; |
| } |
| |
| // The `PageImpl` recorded at the construction of the AdAuctionServiceImpl has |
| // been invalidated or the current frame's `PageImpl` has changed to a |
| // different one, abort the auction. |
| // See crbug.com/1422301. |
| if (base::FeatureList::IsEnabled(features::kDetectInconsistentPageImpl) && |
| (!GetFrame()->auction_initiator_page() || |
| GetFrame()->auction_initiator_page().get() != |
| &(GetFrame()->GetPage()))) { |
| std::move(callback).Run(/*aborted_by_script=*/false, |
| /*config=*/std::nullopt); |
| return; |
| } |
| |
| auto* auction_result_metrics = |
| AdAuctionResultMetrics::GetOrCreateForPage(render_frame_host().GetPage()); |
| if (!auction_result_metrics->ShouldRunAuction()) { |
| std::move(callback).Run(/*aborted_by_script=*/false, |
| /*config=*/std::nullopt); |
| return; |
| } |
| |
| FencedFrameURLMapping& fenced_frame_urls_map = |
| GetFrame()->GetPage().fenced_frame_urls_map(); |
| auto urn_uuid = fenced_frame_urls_map.GeneratePendingMappedURN(); |
| |
| // If pending mapped URN cannot be generated due to number of mappings has |
| // reached limit, stop the auction. |
| if (!urn_uuid.has_value()) { |
| std::move(callback).Run(/*aborted_by_script=*/false, |
| /*config=*/std::nullopt); |
| return; |
| } |
| |
| // Using Unretained here since `this` owns the AuctionRunner. |
| AuctionRunner::AdAuctionPageDataCallback ad_auction_page_data_callback = |
| base::BindRepeating(&AdAuctionServiceImpl::GetAdAuctionPageData, |
| base::Unretained(this)); |
| |
| base::trace_event::EmitNamedTrigger("fledge-top-level-auction-start"); |
| |
| // Try to preconnect to owner and bidding signals origins if this is an |
| // on-device auction. |
| if (base::FeatureList::IsEnabled(features::kFledgeUsePreconnectCache) && |
| !config.server_response.has_value()) { |
| size_t n_owners_cached = PreconnectToBuyerOrigins(config); |
| for (const blink::AuctionConfig& component_config : |
| config.non_shared_params.component_auctions) { |
| if (!component_config.server_response.has_value()) { |
| n_owners_cached += PreconnectToBuyerOrigins(component_config); |
| } |
| } |
| base::UmaHistogramCounts100( |
| "Ads.InterestGroup.Auction.NumOwnerOriginsCachedForPreconnect", |
| n_owners_cached); |
| } |
| |
| std::unique_ptr<AuctionRunner> auction = AuctionRunner::CreateAndStart( |
| auction_metrics_recorder_manager_.CreateAuctionMetricsRecorder(), |
| &dwa_auction_metrics_manager_, &auction_worklet_manager_, |
| &auction_nonce_manager_, &GetInterestGroupManager(), |
| render_frame_host().GetBrowserContext(), private_aggregation_manager_, |
| std::move(ad_auction_page_data_callback), |
| // Unlike other callbacks, this needs to be safe to call after destruction |
| // of the AdAuctionServiceImpl, so that the reporter can outlive it. |
| base::BindRepeating( |
| &AdAuctionServiceImpl::MaybeLogPrivateAggregationFeatures, |
| weak_ptr_factory_.GetWeakPtr()), |
| config, main_frame_origin_, origin(), |
| GetUserAgentOverrideForProtectedAudience(GetFrame()->frame_tree_node()), |
| GetClientSecurityState(), GetRefCountedTrustedURLLoaderFactory(), |
| base::BindRepeating(&AdAuctionServiceImpl::IsInterestGroupAPIAllowed, |
| base::Unretained(this)), |
| base::BindRepeating( |
| &AreAllowedReportingOriginsAttested, |
| base::Unretained(render_frame_host().GetBrowserContext())), |
| std::move(abort_receiver), |
| base::BindOnce(&AdAuctionServiceImpl::OnAuctionComplete, |
| base::Unretained(this), std::move(callback), |
| std::move(urn_uuid.value()))); |
| AuctionRunner* raw_auction = auction.get(); |
| auctions_.emplace(raw_auction, std::move(auction)); |
| } |
| |
| namespace { |
| |
| // Helper class to retrieve the URL that a given URN is mapped to. |
| class FencedFrameURLMappingObserver |
| : public FencedFrameURLMapping::MappingResultObserver { |
| public: |
| // Retrieves the URL that `urn_url` is mapped to, if any. If `send_reports` is |
| // true, sends the reports associated with `urn_url`, if there are any. |
| static std::optional<GURL> GetURL(RenderFrameHostImpl& render_frame_host, |
| const GURL& urn_url, |
| bool send_reports) { |
| std::optional<GURL> mapped_url; |
| FencedFrameURLMappingObserver obs(&mapped_url, send_reports); |
| content::FencedFrameURLMapping& mapping = |
| render_frame_host.GetPage().fenced_frame_urls_map(); |
| // FLEDGE URN URLs should already be mapped, so the observer will be called |
| // synchronously. |
| mapping.ConvertFencedFrameURNToURL(urn_url, &obs); |
| if (!obs.called_) { |
| mapping.RemoveObserverForURN(urn_url, &obs); |
| } |
| return mapped_url; |
| } |
| |
| private: |
| FencedFrameURLMappingObserver(std::optional<GURL>* mapped_url, |
| bool send_reports) |
| : mapped_url_(mapped_url), send_reports_(send_reports) {} |
| |
| ~FencedFrameURLMappingObserver() override = default; |
| |
| void OnFencedFrameURLMappingComplete( |
| const std::optional<FencedFrameProperties>& properties) override { |
| if (properties) { |
| if (properties->mapped_url()) { |
| *mapped_url_ = properties->mapped_url()->GetValueIgnoringVisibility(); |
| } |
| if (send_reports_ && properties->on_navigate_callback()) { |
| properties->on_navigate_callback().Run(); |
| } |
| } |
| called_ = true; |
| } |
| |
| bool called_ = false; |
| raw_ptr<std::optional<GURL>> mapped_url_; |
| bool send_reports_; |
| }; |
| |
| } // namespace |
| |
| void AdAuctionServiceImpl::DeprecatedGetURLFromURN( |
| const GURL& urn_url, |
| bool send_reports, |
| DeprecatedGetURLFromURNCallback callback) { |
| if (!blink::IsValidUrnUuidURL(urn_url)) { |
| ReportBadMessageAndDeleteThis("Unexpected request: invalid URN"); |
| return; |
| } |
| |
| std::move(callback).Run(FencedFrameURLMappingObserver::GetURL( |
| static_cast<RenderFrameHostImpl&>(render_frame_host()), urn_url, |
| send_reports)); |
| } |
| |
| void AdAuctionServiceImpl::DeprecatedReplaceInURN( |
| const GURL& urn_url, |
| const std::vector<blink::AuctionConfig::AdKeywordReplacement>& replacements, |
| DeprecatedReplaceInURNCallback callback) { |
| if (!blink::IsValidUrnUuidURL(urn_url)) { |
| ReportBadMessageAndDeleteThis("Unexpected request: invalid URN"); |
| return; |
| } |
| std::vector<std::pair<std::string, std::string>> local_replacements; |
| for (const auto& replacement : replacements) { |
| local_replacements.emplace_back(std::move(replacement.match), |
| std::move(replacement.replacement)); |
| } |
| content::FencedFrameURLMapping& mapping = |
| static_cast<RenderFrameHostImpl&>(render_frame_host()) |
| .GetPage() |
| .fenced_frame_urls_map(); |
| mapping.SubstituteMappedURL(urn_url, local_replacements); |
| std::move(callback).Run(); |
| } |
| |
| void AdAuctionServiceImpl::GetInterestGroupAdAuctionData( |
| const base::flat_map<url::Origin, std::optional<url::Origin>>& sellers, |
| blink::mojom::AuctionDataConfigPtr config, |
| GetInterestGroupAdAuctionDataCallback callback) { |
| if (!config->per_buyer_configs.empty() && !config->request_size) { |
| ReportBadMessageAndDeleteThis( |
| "Invalid AuctionDataConfig: Missing request_size"); |
| return; |
| } |
| |
| for (const auto& per_buyer_config : config->per_buyer_configs) { |
| if (per_buyer_config.first.scheme() != url::kHttpsScheme) { |
| ReportBadMessageAndDeleteThis("Invalid Buyer"); |
| return; |
| } |
| } |
| |
| if (!IsPermissionPolicyEnabledAndWarnIfNeeded( |
| network::mojom::PermissionsPolicyFeature::kRunAdAuction, |
| "getInterestGroupAdAuctionData")) { |
| // TODO(https://crbug.com/382786767): Figure out why permission policy can |
| // be inconsistent between the browser and renderer policy, fix it, and then |
| // call ReportBadMessageAndDeleteThis() here. |
| std::vector<blink::mojom::AdAuctionPerSellerRequestPtr> requests; |
| for (const auto& [seller, ignored] : sellers) { |
| requests.emplace_back(blink::mojom::AdAuctionPerSellerRequest::New( |
| std::move(seller), blink::mojom::AdAuctionRequestOrError::NewError( |
| "API Blocked by permission policy"))); |
| } |
| std::move(callback).Run(std::move(requests), std::nullopt); |
| return; |
| } |
| |
| // Sellers disallowed to use the API. |
| std::set<url::Origin> sellers_disallowed; |
| for (const auto& [seller, coordinator] : sellers) { |
| if (seller.scheme() != url::kHttpsScheme) { |
| ReportBadMessageAndDeleteThis("Invalid Seller"); |
| return; |
| } |
| if (coordinator && coordinator->scheme() != url::kHttpsScheme) { |
| ReportBadMessageAndDeleteThis("Invalid Bidding and Auction Coordinator"); |
| return; |
| } |
| } |
| |
| base::trace_event::EmitNamedTrigger( |
| "fledge-get-interest-group-ad-auction-data"); |
| |
| BiddingAndAuctionDataConstructionState state; |
| state.callback = std::move(callback); |
| state.sellers = sellers; // must copy, mojo arg is const. |
| state.timestamp = base::Time::Now(); |
| state.config = std::move(config); |
| |
| ba_data_callbacks_.push(std::move(state)); |
| // Only start this request if there isn't another request pending. |
| if (ba_data_callbacks_.size() == 1) { |
| LoadAuctionDataAndKeyForNextQueuedRequest(); |
| } |
| } |
| |
| void AdAuctionServiceImpl::CreateAdRequest( |
| blink::mojom::AdRequestConfigPtr config, |
| CreateAdRequestCallback callback) { |
| if (!IsAdRequestValid(*config)) { |
| std::move(callback).Run(std::nullopt); |
| return; |
| } |
| |
| // TODO(crbug.com/40197508): Actually request Ads and return a guid. |
| // For now just act like it failed. |
| std::move(callback).Run(std::nullopt); |
| } |
| |
| void AdAuctionServiceImpl::FinalizeAd(const std::string& ads_guid, |
| const blink::AuctionConfig& config, |
| FinalizeAdCallback callback) { |
| if (ads_guid.empty()) { |
| ReportBadMessageAndDeleteThis("GUID empty"); |
| return; |
| } |
| |
| // TODO(crbug.com/40197508): Actually finalize Ad and return an URL. |
| // For now just act like it failed. |
| std::move(callback).Run(std::nullopt); |
| } |
| |
| network::mojom::URLLoaderFactory* |
| AdAuctionServiceImpl::GetFrameURLLoaderFactory() { |
| if (!frame_url_loader_factory_ || !frame_url_loader_factory_.is_connected()) { |
| frame_url_loader_factory_.reset(); |
| render_frame_host().CreateNetworkServiceDefaultFactory( |
| frame_url_loader_factory_.BindNewPipeAndPassReceiver()); |
| } |
| return frame_url_loader_factory_.get(); |
| } |
| |
| network::mojom::URLLoaderFactory* |
| AdAuctionServiceImpl::GetTrustedURLLoaderFactory() { |
| // Ensure the page is not in prerendering as code belows expect it, i.e. |
| // GetPageUkmSourceId() doesn't work with prerendering pages. |
| CHECK(!render_frame_host().IsInLifecycleState( |
| RenderFrameHost::LifecycleState::kPrerendering)); |
| return ref_counted_trusted_url_loader_factory_.get(); |
| } |
| |
| void AdAuctionServiceImpl::CreateUnderlyingTrustedURLLoaderFactory( |
| mojo::PendingRemote<network::mojom::URLLoaderFactory>* out_factory) { |
| // TODO(mmenke): Should this have its own URLLoaderFactoryType? FLEDGE |
| // requests are very different from subresource requests. |
| // |
| // TODO(mmenke): Hook up devtools. |
| *out_factory = url_loader_factory::CreatePendingRemote( |
| ContentBrowserClient::URLLoaderFactoryType::kDocumentSubResource, |
| url_loader_factory::TerminalParams::ForBrowserProcess( |
| static_cast<RenderFrameHostImpl&>(render_frame_host()) |
| .GetStoragePartition()), |
| url_loader_factory::ContentClientParams( |
| render_frame_host().GetSiteInstance()->GetBrowserContext(), |
| &render_frame_host(), |
| render_frame_host().GetProcess()->GetDeprecatedID(), url::Origin(), |
| net::IsolationInfo(), |
| ukm::SourceIdObj::FromInt64( |
| render_frame_host().GetPageUkmSourceId()))); |
| } |
| |
| void AdAuctionServiceImpl::PreconnectSocket( |
| const GURL& url, |
| const net::NetworkAnonymizationKey& network_anonymization_key) { |
| render_frame_host() |
| .GetStoragePartition() |
| ->GetNetworkContext() |
| ->PreconnectSockets( |
| /*num_streams=*/1, url, network::mojom::CredentialsMode::kOmit, |
| network_anonymization_key, net::MutableNetworkTrafficAnnotationTag(), |
| /*keepalive_config=*/std::nullopt, mojo::NullRemote()); |
| } |
| |
| scoped_refptr<network::SharedURLLoaderFactory> |
| AdAuctionServiceImpl::GetRefCountedTrustedURLLoaderFactory() { |
| return ref_counted_trusted_url_loader_factory_; |
| } |
| |
| RenderFrameHostImpl* AdAuctionServiceImpl::GetFrame() { |
| return static_cast<RenderFrameHostImpl*>(&render_frame_host()); |
| } |
| |
| scoped_refptr<SiteInstance> AdAuctionServiceImpl::GetFrameSiteInstance() { |
| return render_frame_host().GetSiteInstance(); |
| } |
| |
| network::mojom::ClientSecurityStatePtr |
| AdAuctionServiceImpl::GetClientSecurityState() { |
| if (base::FeatureList::IsEnabled( |
| features::kFledgeOnlyUseIpAddressSpaceInClientSecurityState)) { |
| PolicyContainerHost* policies = GetFrame()->policy_container_host(); |
| // This matches what GetFrame()->BuildClientSecurityState() does, in the no |
| // PolicyContainer case. According to comments there, PolicyContainerHost |
| // should only be null before commit, which shouldn't be the case when any |
| // of AdAuctionServiceImpl's methods are invoked. |
| network::mojom::IPAddressSpace ip_address_space = |
| policies ? policies->ip_address_space() |
| : network::mojom::IPAddressSpace::kUnknown; |
| return CreateClientSecurityStateForProtectedAudience(ip_address_space); |
| } |
| network::mojom::ClientSecurityStatePtr frame_state = |
| GetFrame()->BuildClientSecurityState(); |
| // Ensure all Local Network Access requests are blocked as this could lead to |
| // information leakage. |
| frame_state->private_network_request_policy = |
| network::mojom::PrivateNetworkRequestPolicy::kBlock; |
| return frame_state; |
| } |
| |
| std::optional<std::string> AdAuctionServiceImpl::GetCookieDeprecationLabel() { |
| if (!base::FeatureList::IsEnabled( |
| features::kFledgeFacilitatedTestingSignalsHeaders)) { |
| return std::nullopt; |
| } |
| |
| CookieDeprecationLabelManager* cdlm = |
| render_frame_host() |
| .GetStoragePartition() |
| ->GetCookieDeprecationLabelManager(); |
| if (cdlm) { |
| return cdlm->GetValue(); |
| } else { |
| return std::nullopt; |
| } |
| } |
| |
| void AdAuctionServiceImpl::GetTrustedKeyValueServerKey( |
| const url::Origin& scope_origin, |
| const std::optional<url::Origin>& coordinator, |
| base::OnceCallback<void( |
| base::expected<BiddingAndAuctionServerKey, std::string>)> callback) { |
| GetInterestGroupManager().GetTrustedServerKey( |
| InterestGroupManager::TrustedServerAPIType::kTrustedKeyValue, |
| scope_origin, coordinator, std::move(callback)); |
| } |
| |
| AdAuctionServiceImpl::AdAuctionServiceImpl( |
| RenderFrameHost& render_frame_host, |
| mojo::PendingReceiver<blink::mojom::AdAuctionService> receiver) |
| : DocumentService(render_frame_host, std::move(receiver)), |
| main_frame_origin_( |
| render_frame_host.GetMainFrame()->GetLastCommittedOrigin()), |
| main_frame_url_(render_frame_host.GetMainFrame()->GetLastCommittedURL()), |
| auction_metrics_recorder_manager_(render_frame_host.GetPageUkmSourceId()), |
| auction_worklet_manager_( |
| &GetInterestGroupManager().auction_process_manager(), |
| GetTopWindowOrigin(), |
| origin(), |
| this), |
| auction_nonce_manager_(GetFrame()), |
| private_aggregation_manager_(PrivateAggregationManager::GetManager( |
| *render_frame_host.GetBrowserContext())) { |
| // Construct `ref_counted_trusted_url_loader_factory_` here because |
| // `weak_ptr_factory_` is not yet initialized during the member initializer |
| // list above. |
| ref_counted_trusted_url_loader_factory_ = |
| base::MakeRefCounted<ReconnectableURLLoaderFactory>(base::BindRepeating( |
| &AdAuctionServiceImpl::CreateUnderlyingTrustedURLLoaderFactory, |
| weak_ptr_factory_.GetWeakPtr())); |
| |
| // Throughout the auction, the `PageImpl` of the frame which initiates the |
| // auction should stay the same. When an inconsistency is detected, the |
| // auction must be aborted. This is done by storing a weak pointer to the |
| // `PageImpl`. It is verified at various stages of the auction. |
| // |
| // Note: `AdAuctionServiceImpl` is constructed upon the first call of a |
| // Protected Audience API. This is why the weak pointer is set here instead of |
| // during frame's `RenderFrameHostImpl` construction. |
| // |
| // See crbug.com/1422301 for a scenario where `PageImpl` can change, and why |
| // this is problematic. |
| // |
| // TODO(crbug.com/40615943): Once RenderDocument is launched, the `PageImpl` |
| // will not change. Remove all logics around this weak pointer. |
| GetFrame()->set_auction_initiator_page( |
| static_cast<PageImpl&>(render_frame_host.GetPage()).GetWeakPtrImpl()); |
| } |
| |
| AdAuctionServiceImpl::~AdAuctionServiceImpl() { |
| while (!auctions_.empty()) { |
| // Need to fail all auctions rather than just deleting them, to ensure Mojo |
| // callbacks from the renderers are invoked. Uninvoked Mojo callbacks may |
| // not be destroyed before the Mojo pipe is, and the parent DocumentService |
| // class owns the pipe, so it may still be open at this point. |
| auctions_.begin()->first->FailAuction(/*aborted_by_script=*/false); |
| } |
| } |
| |
| bool AdAuctionServiceImpl::JoinOrLeaveApiAllowedFromRenderer( |
| const url::Origin& owner, |
| const char* invoked_method) { |
| if (origin().scheme() != url::kHttpsScheme) { |
| ReportBadMessageAndDeleteThis( |
| "Unexpected request: Interest groups may only be joined or left from " |
| "https origins"); |
| return false; |
| } |
| |
| if (owner.scheme() != url::kHttpsScheme) { |
| ReportBadMessageAndDeleteThis( |
| "Unexpected request: Interest groups may only be owned by https " |
| "origins"); |
| return false; |
| } |
| |
| // If the interest group API is not allowed for this context by Permissions |
| // Policy, do nothing. |
| if (!IsPermissionPolicyEnabledAndWarnIfNeeded( |
| network::mojom::PermissionsPolicyFeature::kJoinAdInterestGroup, |
| invoked_method)) { |
| // TODO(https://crbug.com/382786767): Figure out why permission policy can |
| // be inconsistent between the browser and renderer policy, fix it, and then |
| // call ReportBadMessageAndDeleteThis() here. |
| return false; |
| } |
| |
| // join/leaveAdInterestGroup allows igs to be written to `owner` which might |
| // be x-origin. Could the owner origin have called join/leaveAdInterestGroup |
| // in its own iframe with allow=join-ad-interest-group? If not, we should not |
| // allow it in this context either. |
| auto* permissions_policy = |
| static_cast<RenderFrameHostImpl*>(&render_frame_host()) |
| ->GetPermissionsPolicy(); |
| |
| if (!permissions_policy->IsFeatureEnabledForOrigin( |
| network::mojom::PermissionsPolicyFeature::kJoinAdInterestGroup, owner, |
| /*override_default_policy_to_all=*/true)) { |
| GetContentClient()->browser()->LogWebFeatureForCurrentPage( |
| &render_frame_host(), |
| blink::mojom::WebFeature:: |
| kCrossOriginOwnerInterestGroupSubframeCheckFailed); |
| |
| if (base::FeatureList::IsEnabled( |
| features::kFledgeModifyInterestGroupPolicyCheckOnOwner)) { |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| bool AdAuctionServiceImpl::IsPermissionPolicyEnabledAndWarnIfNeeded( |
| network::mojom::PermissionsPolicyFeature feature, |
| const char* invoked_method) { |
| if (!render_frame_host().IsFeatureEnabled(feature)) { |
| return false; |
| } |
| |
| auto warn_it = should_warn_about_feature_.find(feature); |
| if (warn_it == should_warn_about_feature_.end()) { |
| bool should_warn = |
| ShouldWarnAboutPermissionPolicyDefault(*GetFrame(), feature); |
| warn_it = |
| should_warn_about_feature_.emplace(std::pair(feature, should_warn)) |
| .first; |
| } |
| |
| if (warn_it->second) { |
| auto feature_it = |
| blink::GetPermissionsPolicyFeatureToNameMap().find(feature); |
| CHECK(feature_it != blink::GetPermissionsPolicyFeatureToNameMap().end()); |
| |
| render_frame_host().AddMessageToConsole( |
| blink::mojom::ConsoleMessageLevel::kWarning, |
| base::StringPrintf( |
| "In the future, Permissions Policy feature %s will not be enabled " |
| "by default in cross-origin iframes or same-origin iframes nested " |
| "in cross-origin iframes. Calling %s will be rejected with " |
| "NotAllowedError if it is not explicitly enabled", |
| feature_it->second.data(), invoked_method)); |
| } |
| |
| return true; |
| } |
| |
| bool AdAuctionServiceImpl::IsInterestGroupAPIAllowed( |
| ContentBrowserClient::InterestGroupApiOperation |
| interest_group_api_operation, |
| const url::Origin& origin) const { |
| return GetContentClient()->browser()->IsInterestGroupAPIAllowed( |
| render_frame_host().GetBrowserContext(), &render_frame_host(), |
| interest_group_api_operation, main_frame_origin_, origin); |
| } |
| |
| void AdAuctionServiceImpl::OnAuctionComplete( |
| RunAdAuctionCallback callback, |
| GURL urn_uuid, |
| AuctionRunner* auction, |
| bool aborted_by_script, |
| std::optional<blink::InterestGroupKey> winning_group_key, |
| std::optional<blink::AdSize> requested_ad_size, |
| std::optional<blink::AdDescriptor> ad_descriptor, |
| std::vector<blink::AdDescriptor> ad_component_descriptors, |
| std::vector<std::string> errors, |
| std::unique_ptr<InterestGroupAuctionReporter> reporter, |
| bool contained_server_auction, |
| bool contained_on_device_auction, |
| AuctionResult result) { |
| // Remove `auction` from `auctions_` but temporarily keep it alive - on |
| // success, it owns a AuctionWorkletManager::WorkletHandle for the top-level |
| // auction, which `reporter` can reuse once started. Fine to delete after |
| // starting the reporter. |
| auto auction_it = auctions_.find(auction); |
| CHECK(auction_it != auctions_.end()); |
| std::unique_ptr<AuctionRunner> owned_auction = std::move(auction_it->second); |
| auctions_.erase(auction_it); |
| |
| // The `PageImpl` recorded at the construction of the AdAuctionServiceImpl has |
| // been invalidated or the current frame's `PageImpl` has changed to a |
| // different one, abort the auction. |
| // See crbug.com/1422301. |
| if (base::FeatureList::IsEnabled(features::kDetectInconsistentPageImpl) && |
| (!GetFrame()->auction_initiator_page() || |
| GetFrame()->auction_initiator_page().get() != |
| &(GetFrame()->GetPage()))) { |
| std::move(callback).Run(aborted_by_script, /*config=*/std::nullopt); |
| return; |
| } |
| |
| // Forward debug information to devtools. |
| for (const std::string& error : errors) { |
| devtools_instrumentation::LogWorkletMessage( |
| *GetFrame(), blink::mojom::ConsoleMessageLevel::kError, |
| base::StrCat({"Worklet error: ", error})); |
| } |
| |
| auto* auction_result_metrics = |
| AdAuctionResultMetrics::GetForPage(render_frame_host().GetPage()); |
| |
| if (!ad_descriptor) { |
| DCHECK(!reporter); |
| |
| GetContentClient()->browser()->OnAuctionComplete( |
| &render_frame_host(), /*winner_data_key=*/std::nullopt, |
| contained_server_auction, contained_on_device_auction, result); |
| |
| std::move(callback).Run(aborted_by_script, /*config=*/std::nullopt); |
| if (auction_result_metrics) { |
| // `auction_result_metrics` can be null since PageUserData like |
| // AdAuctionResultMetrics isn't guaranteed to be destroyed after document |
| // services like `this`, even though this typically is the case for |
| // destruction of the RenderFrameHost (except for renderer crashes). |
| // |
| // So, we need to guard against this. |
| auction_result_metrics->ReportAuctionResult( |
| AdAuctionResultMetrics::AuctionResult::kFailed); |
| } |
| return; |
| } |
| |
| DCHECK(reporter); |
| // Should always be present with an ad_descriptor->url. |
| DCHECK(winning_group_key); |
| DCHECK(blink::IsValidFencedFrameURL(ad_descriptor->url)); |
| DCHECK(urn_uuid.is_valid()); |
| |
| GetContentClient()->browser()->OnAuctionComplete( |
| &render_frame_host(), |
| InterestGroupManager::InterestGroupDataKey{ |
| reporter->winning_bid_info() |
| .storage_interest_group->interest_group.owner, |
| reporter->winning_bid_info().storage_interest_group->joining_origin, |
| }, |
| contained_server_auction, contained_on_device_auction, result); |
| |
| content::AdAuctionData ad_auction_data{winning_group_key->owner, |
| winning_group_key->name}; |
| FencedFrameURLMapping& current_fenced_frame_urls_map = |
| GetFrame()->GetPage().fenced_frame_urls_map(); |
| |
| // Set up reporting for any fenced frame that's navigated to the winning bid's |
| // URL. Use a URLLoaderFactory that will automatically reconnect on network |
| // process crashes, and can outlive the frame. |
| scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory = |
| render_frame_host() |
| .GetStoragePartition() |
| ->GetURLLoaderFactoryForBrowserProcess(); |
| |
| blink::FencedFrame::RedactedFencedFrameConfig config = |
| current_fenced_frame_urls_map.AssignFencedFrameURLAndInterestGroupInfo( |
| urn_uuid, requested_ad_size, *ad_descriptor, |
| std::move(ad_auction_data), |
| reporter->OnNavigateToWinningAdCallback( |
| GetFrame()->GetFrameTreeNodeId()), |
| ad_component_descriptors, reporter->fenced_frame_reporter()); |
| std::move(callback).Run(/*aborted_by_script=*/false, std::move(config)); |
| |
| // Start the InterestGroupAuctionReporter. It will run reporting scripts, but |
| // nothing will be reported (nor the reporter deleted) until a fenced frame |
| // navigates to the winning ad, which will be signalled by invoking the |
| // callback returned by the InterestGroupAuctionReporter's |
| // OnNavitationToWinningAdCallback() method (invoked just above). |
| reporters_.emplace_front(std::move(reporter)); |
| reporters_.front()->Start( |
| base::BindOnce(&AdAuctionServiceImpl::OnReporterComplete, |
| base::Unretained(this), reporters_.begin())); |
| if (auction_result_metrics) { |
| auction_result_metrics->ReportAuctionResult( |
| AdAuctionResultMetrics::AuctionResult::kSucceeded); |
| } |
| } |
| |
| void AdAuctionServiceImpl::OnReporterComplete( |
| ReporterList::iterator reporter_it) { |
| // Forward debug information to devtools. |
| // |
| // TODO(crbug.com/40248758): Ideally this will share code with the |
| // handling of the errors from the earlier phases of the auction. |
| InterestGroupAuctionReporter* reporter = reporter_it->get(); |
| for (const std::string& error : reporter->errors()) { |
| devtools_instrumentation::LogWorkletMessage( |
| *GetFrame(), blink::mojom::ConsoleMessageLevel::kError, |
| base::StrCat({"Worklet error: ", error})); |
| } |
| |
| reporters_.erase(reporter_it); |
| } |
| |
| void AdAuctionServiceImpl::MaybeLogPrivateAggregationFeatures( |
| const std::vector<auction_worklet::mojom::PrivateAggregationRequestPtr>& |
| private_aggregation_requests) { |
| // TODO(crbug.com/40236382): Improve coverage of these use counters, i.e. |
| // for API usage that does not result in a successful request. |
| if (private_aggregation_requests.empty()) { |
| return; |
| } |
| |
| if (!has_logged_private_aggregation_filtering_id_web_feature_ && |
| std::ranges::any_of( |
| private_aggregation_requests, [](const auto& request) { |
| auction_worklet::mojom::AggregatableReportContributionPtr& |
| contribution = request->contribution; |
| if (contribution->is_histogram_contribution()) { |
| return contribution->get_histogram_contribution() |
| ->filtering_id.has_value(); |
| } |
| CHECK(contribution->is_for_event_contribution()); |
| return contribution->get_for_event_contribution() |
| ->filtering_id.has_value(); |
| })) { |
| has_logged_private_aggregation_filtering_id_web_feature_ = true; |
| GetContentClient()->browser()->LogWebFeatureForCurrentPage( |
| &render_frame_host(), |
| blink::mojom::WebFeature::kPrivateAggregationApiFilteringIds); |
| } |
| |
| if (!has_logged_private_aggregation_error_reporting_web_feature_ && |
| std::ranges::any_of( |
| private_aggregation_requests, [](const auto& request) { |
| auction_worklet::mojom::AggregatableReportContributionPtr& |
| contribution = request->contribution; |
| if (contribution->is_histogram_contribution()) { |
| return false; |
| } |
| CHECK(contribution->is_for_event_contribution()); |
| return contribution->get_for_event_contribution() |
| ->event_type->is_reserved_error(); |
| })) { |
| has_logged_private_aggregation_error_reporting_web_feature_ = true; |
| GetContentClient()->browser()->LogWebFeatureForCurrentPage( |
| &render_frame_host(), |
| blink::mojom::WebFeature::kPrivateAggregationApiErrorReporting); |
| } |
| |
| if (!has_logged_private_aggregation_enable_debug_mode_web_feature_ && |
| std::ranges::any_of(private_aggregation_requests, |
| [](const auto& request) { |
| return request->debug_mode_details->is_enabled; |
| })) { |
| has_logged_private_aggregation_enable_debug_mode_web_feature_ = true; |
| GetContentClient()->browser()->LogWebFeatureForCurrentPage( |
| &render_frame_host(), |
| blink::mojom::WebFeature::kPrivateAggregationApiEnableDebugMode); |
| } |
| |
| if (!has_logged_extended_private_aggregation_web_feature_ && |
| std::ranges::any_of( |
| private_aggregation_requests, [](const auto& request) { |
| return request->contribution->is_for_event_contribution(); |
| })) { |
| has_logged_extended_private_aggregation_web_feature_ = true; |
| GetContentClient()->browser()->LogWebFeatureForCurrentPage( |
| &render_frame_host(), |
| blink::mojom::WebFeature::kPrivateAggregationApiFledgeExtensions); |
| } |
| |
| if (!has_logged_private_aggregation_web_features_) { |
| has_logged_private_aggregation_web_features_ = true; |
| GetContentClient()->browser()->LogWebFeatureForCurrentPage( |
| &render_frame_host(), |
| blink::mojom::WebFeature::kPrivateAggregationApiAll); |
| GetContentClient()->browser()->LogWebFeatureForCurrentPage( |
| &render_frame_host(), |
| blink::mojom::WebFeature::kPrivateAggregationApiFledge); |
| } |
| } |
| |
| void AdAuctionServiceImpl::AddEmptyGetInterestGroupAdAuctionDataRequest( |
| const url::Origin& seller, |
| const std::string& msg) { |
| if (!ba_data_callbacks_.empty()) { |
| BiddingAndAuctionDataConstructionState& state = ba_data_callbacks_.front(); |
| if (msg.empty()) { |
| mojo_base::BigBuffer empty_request; |
| state.requests.emplace_back(blink::mojom::AdAuctionPerSellerRequest::New( |
| std::move(seller), blink::mojom::AdAuctionRequestOrError::NewRequest( |
| std::move(empty_request)))); |
| RecordBaDataConstructionResultMetric(/*data_size=*/0, state.start_time); |
| } else { |
| state.requests.emplace_back(blink::mojom::AdAuctionPerSellerRequest::New( |
| std::move(seller), |
| blink::mojom::AdAuctionRequestOrError::NewError(std::move(msg)))); |
| } |
| if (state.sellers.size() == state.requests.size()) { |
| RunGetInterestGroupAdAuctionDataCallback(state.request_id); |
| } |
| } |
| } |
| |
| void AdAuctionServiceImpl::RunGetInterestGroupAdAuctionDataCallback( |
| base::Uuid request_id) { |
| if (ba_data_callbacks_.empty() || |
| request_id != ba_data_callbacks_.front().request_id) { |
| return; |
| } |
| BiddingAndAuctionDataConstructionState& state = ba_data_callbacks_.front(); |
| std::optional<base::Uuid> id = std::nullopt; |
| if (state.has_valid_request) { |
| id = request_id; |
| } |
| std::move(state.callback).Run(std::move(state.requests), id); |
| ba_data_callbacks_.pop(); |
| if (!ba_data_callbacks_.empty()) { |
| LoadAuctionDataAndKeyForNextQueuedRequest(); |
| } |
| } |
| |
| void AdAuctionServiceImpl::LoadAuctionDataAndKeyForNextQueuedRequest() { |
| BiddingAndAuctionDataConstructionState& state = ba_data_callbacks_.front(); |
| if (state.sellers.size() == 0) { |
| RunGetInterestGroupAdAuctionDataCallback(state.request_id); |
| return; |
| } |
| |
| std::vector<url::Origin> seller_origins; |
| for (const auto& [seller, _] : state.sellers) { |
| seller_origins.push_back(seller); |
| } |
| GetInterestGroupManager().GetInterestGroupAdAuctionData( |
| GetTopWindowOrigin(), |
| /* generation_id=*/base::Uuid::GenerateRandomV4(), state.timestamp, |
| std::move(state.config), std::move(seller_origins), |
| base::BindOnce(&AdAuctionServiceImpl::OnGotAuctionData, |
| weak_ptr_factory_.GetWeakPtr(), state.request_id)); |
| |
| for (const auto& [seller, coordinator] : state.sellers) { |
| // If the interest group API is not allowed for this origin, skip this |
| // origin. |
| bool api_allowed = IsInterestGroupAPIAllowed( |
| ContentBrowserClient::InterestGroupApiOperation::kSell, seller); |
| base::UmaHistogramBoolean( |
| "Ads.InterestGroup.ServerAuction.Request.APIAllowed", api_allowed); |
| if (api_allowed) { |
| // GetBiddingAndAuctionServerKey can call its callback synchronously, and |
| // during the last loop iteration the callback may invalidate `state`. |
| GetInterestGroupManager().GetTrustedServerKey( |
| InterestGroupManager::TrustedServerAPIType::kBiddingAndAuction, |
| seller, coordinator, |
| base::BindOnce( |
| &AdAuctionServiceImpl::OnGotOneBiddingAndAuctionServerKey, |
| weak_ptr_factory_.GetWeakPtr(), state.request_id, seller)); |
| } else { |
| // During the last loop iteration this call may invalidate `state`. |
| AddEmptyGetInterestGroupAdAuctionDataRequest( |
| seller, "API not allowed for this origin"); |
| } |
| } |
| } |
| |
| void AdAuctionServiceImpl::OnGotAuctionData(base::Uuid request_id, |
| BiddingAndAuctionData data) { |
| if (ba_data_callbacks_.empty() || |
| request_id != ba_data_callbacks_.front().request_id) { |
| return; |
| } |
| BiddingAndAuctionDataConstructionState& state = ba_data_callbacks_.front(); |
| state.data = std::make_unique<BiddingAndAuctionData>(std::move(data)); |
| auto pending_keys = std::exchange(state.keys, {}); |
| for (const auto& [seller, key] : pending_keys) { |
| OnGotAuctionDataAndKey(request_id, seller, key); |
| } |
| } |
| |
| void AdAuctionServiceImpl::OnGotOneBiddingAndAuctionServerKey( |
| base::Uuid request_id, |
| const url::Origin& seller, |
| base::expected<BiddingAndAuctionServerKey, std::string> maybe_key) { |
| if (ba_data_callbacks_.empty() || |
| request_id != ba_data_callbacks_.front().request_id) { |
| return; |
| } |
| BiddingAndAuctionDataConstructionState& state = ba_data_callbacks_.front(); |
| if (!maybe_key.has_value()) { |
| AddEmptyGetInterestGroupAdAuctionDataRequest(seller, |
| std::move(maybe_key.error())); |
| return; |
| } |
| |
| if (state.data) { |
| OnGotAuctionDataAndKey(request_id, seller, *maybe_key); |
| } else { |
| if (!state.keys.contains(seller)) { |
| state.keys[seller] = BiddingAndAuctionServerKey(std::move(*maybe_key)); |
| } |
| } |
| } |
| |
| void AdAuctionServiceImpl::OnGotAuctionDataAndKey( |
| base::Uuid request_id, |
| const url::Origin& seller, |
| const BiddingAndAuctionServerKey& ba_key) { |
| if (ba_data_callbacks_.empty() || |
| request_id != ba_data_callbacks_.front().request_id) { |
| return; |
| } |
| |
| BiddingAndAuctionDataConstructionState& state = ba_data_callbacks_.front(); |
| DCHECK(state.data); |
| |
| if (state.data->requests[seller].empty()) { |
| AddEmptyGetInterestGroupAdAuctionDataRequest(seller, ""); |
| return; |
| } |
| |
| uint32_t key_id = 0; |
| bool success = |
| base::HexStringToUInt(std::string_view(ba_key.id).substr(0, 2), &key_id); |
| DCHECK(success); |
| auto maybe_key_config = quiche::ObliviousHttpHeaderKeyConfig::Create( |
| key_id, EVP_HPKE_DHKEM_X25519_HKDF_SHA256, EVP_HPKE_HKDF_SHA256, |
| EVP_HPKE_AES_256_GCM); |
| CHECK(maybe_key_config.ok()) << maybe_key_config.status(); |
| |
| auto maybe_request = |
| quiche::ObliviousHttpRequest::CreateClientObliviousRequest( |
| std::string(state.data->requests[seller].begin(), |
| state.data->requests[seller].end()), |
| ba_key.key, maybe_key_config.value(), |
| kBiddingAndAuctionEncryptionRequestMediaType); |
| if (!maybe_request.ok()) { |
| AddEmptyGetInterestGroupAdAuctionDataRequest(seller, |
| "Could not create request"); |
| return; |
| } |
| |
| std::string data = maybe_request->EncapsulateAndSerialize(); |
| |
| // Preconnect to seller since we know JS will send a request there. |
| render_frame_host() |
| .GetStoragePartition() |
| ->GetNetworkContext() |
| ->PreconnectSockets( |
| /*num_streams=*/1, seller.GetURL(), |
| network::mojom::CredentialsMode::kInclude, |
| render_frame_host() |
| .GetIsolationInfoForSubresources() |
| .network_anonymization_key(), |
| net::MutableNetworkTrafficAnnotationTag(), |
| /*keepalive_config=*/std::nullopt, mojo::NullRemote()); |
| |
| AdAuctionPageData* ad_auction_page_data = GetAdAuctionPageData(); |
| if (!ad_auction_page_data) { |
| AddEmptyGetInterestGroupAdAuctionDataRequest( |
| seller, "Page destruction in progress"); |
| return; |
| } |
| |
| if (!ad_auction_page_data->GetContextForAdAuctionRequest( |
| ContextMapKey(state.request_id, seller))) { |
| AdAuctionRequestContext context( |
| seller, std::move(state.data->group_names), |
| std::move(*maybe_request).ReleaseContext(), state.start_time, |
| std::move(state.data->group_pagg_coordinators)); |
| ad_auction_page_data->RegisterAdAuctionRequestContext(state.request_id, |
| std::move(context)); |
| } |
| |
| // Pre-warm data decoder. |
| ad_auction_page_data->GetDecoderFor(seller)->GetService(); |
| |
| // For the modified request format we need to prepend a version number byte |
| // to the request. |
| size_t start_offset = 1; |
| mojo_base::BigBuffer buf(data.size() + start_offset); |
| |
| // Write the version byte. If we are not using a modified request this will |
| // be immediately overwritten. |
| buf.data()[0] = 0; |
| |
| // Write the request starting at `start_offset` |
| CHECK_EQ(data.size() + start_offset, buf.size()); |
| base::span(buf) |
| .subspan(start_offset) |
| .copy_from_nonoverlapping(base::as_byte_span(data)); |
| state.requests.emplace_back(blink::mojom::AdAuctionPerSellerRequest::New( |
| seller, |
| blink::mojom::AdAuctionRequestOrError::NewRequest(std::move(buf)))); |
| RecordBaDataConstructionResultMetric(data.size(), state.start_time); |
| state.has_valid_request = true; |
| if (state.sellers.size() == state.requests.size()) { |
| RunGetInterestGroupAdAuctionDataCallback(request_id); |
| } |
| } |
| |
| InterestGroupManagerImpl& AdAuctionServiceImpl::GetInterestGroupManager() |
| const { |
| return *static_cast<InterestGroupManagerImpl*>( |
| render_frame_host().GetStoragePartition()->GetInterestGroupManager()); |
| } |
| |
| url::Origin AdAuctionServiceImpl::GetTopWindowOrigin() const { |
| if (!render_frame_host().GetParent()) { |
| return origin(); |
| } |
| return render_frame_host().GetMainFrame()->GetLastCommittedOrigin(); |
| } |
| |
| AdAuctionPageData* AdAuctionServiceImpl::GetAdAuctionPageData() { |
| // The `PageImpl` recorded at the construction of the AdAuctionServiceImpl has |
| // been invalidated or the current frame's `PageImpl` has changed to a |
| // different one, signal that state is no longer available. |
| // See crbug.com/1422301. |
| if (base::FeatureList::IsEnabled(features::kDetectInconsistentPageImpl) && |
| (!GetFrame()->auction_initiator_page() || |
| GetFrame()->auction_initiator_page().get() != |
| &(GetFrame()->GetPage()))) { |
| return nullptr; |
| } |
| |
| return PageUserData<AdAuctionPageData>::GetOrCreateForPage( |
| render_frame_host().GetPage()); |
| } |
| |
| size_t AdAuctionServiceImpl::PreconnectToBuyerOrigins( |
| const blink::AuctionConfig& config) { |
| if (!config.non_shared_params.interest_group_buyers) { |
| return 0; |
| } |
| size_t n_owners_cached = 0; |
| for (const auto& buyer : *config.non_shared_params.interest_group_buyers) { |
| std::optional<url::Origin> signals_origin; |
| if (GetInterestGroupManager().GetCachedOwnerAndSignalsOrigins( |
| buyer, signals_origin)) { |
| net::NetworkAnonymizationKey network_anonymization_key = |
| net::NetworkAnonymizationKey::CreateSameSite( |
| net::SchemefulSite(buyer)); |
| PreconnectSocket(buyer.GetURL(), network_anonymization_key); |
| n_owners_cached += 1; |
| if (signals_origin) { |
| // We preconnect to the signals origin and not the full signals URL so |
| // that we do not need to store the full URL in memory. Preconnecting |
| // to the origin will be roughly equivalent to preconnecting to the |
| // full URL. |
| PreconnectSocket(signals_origin->GetURL(), network_anonymization_key); |
| } |
| } |
| } |
| return n_owners_cached; |
| } |
| |
| } // namespace content |