| // Copyright 2022 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/interest_group_update_manager.h" |
| |
| #include <stddef.h> |
| #include <stdint.h> |
| |
| #include <algorithm> |
| #include <cstddef> |
| #include <map> |
| #include <memory> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/base64.h" |
| #include "base/containers/flat_map.h" |
| #include "base/containers/span.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/json/json_string_value_serializer.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/rand_util.h" |
| #include "base/time/time.h" |
| #include "base/values.h" |
| #include "components/aggregation_service/aggregation_coordinator_utils.h" |
| #include "components/aggregation_service/features.h" |
| #include "content/browser/interest_group/interest_group_manager_impl.h" |
| #include "content/browser/interest_group/interest_group_storage.h" |
| #include "content/browser/interest_group/interest_group_update.h" |
| #include "content/browser/interest_group/storage_interest_group.h" |
| #include "content/common/features.h" |
| #include "content/public/browser/interest_group_manager.h" |
| #include "content/services/auction_worklet/public/cpp/auction_downloader.h" |
| #include "net/base/isolation_info.h" |
| #include "net/base/net_errors.h" |
| #include "net/traffic_annotation/network_traffic_annotation.h" |
| #include "services/data_decoder/public/cpp/data_decoder.h" |
| #include "services/network/public/cpp/resource_request.h" |
| #include "services/network/public/cpp/shared_url_loader_factory.h" |
| #include "services/network/public/cpp/simple_url_loader.h" |
| #include "services/network/public/mojom/client_security_state.mojom.h" |
| #include "third_party/abseil-cpp/absl/types/optional.h" |
| #include "third_party/blink/public/common/features.h" |
| #include "third_party/blink/public/common/interest_group/ad_display_size_utils.h" |
| #include "third_party/blink/public/common/interest_group/interest_group.h" |
| #include "third_party/blink/public/mojom/interest_group/interest_group_types.mojom.h" |
| #include "third_party/boringssl/src/include/openssl/curve25519.h" |
| #include "url/gurl.h" |
| #include "url/origin.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| // 1 MB update size limit. We are potentially fetching many interest group |
| // updates, so don't let this get too large. |
| constexpr size_t kMaxUpdateSize = 1 * 1024 * 1024; |
| |
| // The maximum amount of time that the update process can run before it gets |
| // cancelled for taking too long. |
| constexpr base::TimeDelta kMaxUpdateRoundDuration = base::Minutes(10); |
| |
| // The maximum number of groups that can be updated at the same time. |
| constexpr int kMaxParallelUpdates = 5; |
| |
| constexpr net::NetworkTrafficAnnotationTag kTrafficAnnotation = |
| net::DefineNetworkTrafficAnnotation("interest_group_update_fetcher", R"( |
| semantics { |
| sender: "Interest group periodic update fetcher" |
| description: |
| "Fetches periodic updates of FLEDGE interest groups previously " |
| "joined by navigator.joinAdInterestGroup(). FLEDGE allow sites to " |
| "store persistent interest groups that are only accessible to " |
| "special on-device ad auction worklets run via " |
| "navigator.runAdAuction(). JavaScript running in the context of a " |
| "frame cannot read interest groups, but it can request that all " |
| "interest groups owned by the current frame's origin be updated by " |
| "fetching JSON from the registered update URL for each interest " |
| "group." |
| "See https://github.com/WICG/turtledove/blob/main/FLEDGE.md and " |
| "https://developer.chrome.com/docs/privacy-sandbox/fledge/" |
| trigger: |
| "Fetched upon a navigator.updateAdInterestGroups() call. Also " |
| "triggered upon navigator.runAdAuction() completion for interest " |
| "groups that participated in the auction." |
| data: "URL registered for updating this interest group." |
| 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." |
| })"); |
| |
| // TODO(crbug.com/1186444): Report errors to devtools for the TryToCopy*(). |
| // functions. |
| |
| // Name and owner are optional in `dict` (parsed server JSON response), but |
| // must match `name` and `owner`, respectively, if either is specified. Returns |
| // true if the check passes, and false otherwise. |
| [[nodiscard]] bool ValidateNameAndOwnerIfPresent( |
| const blink::InterestGroupKey& group_key, |
| const base::Value::Dict& dict) { |
| const std::string* maybe_owner = dict.FindString("owner"); |
| if (maybe_owner && |
| url::Origin::Create(GURL(*maybe_owner)) != group_key.owner) { |
| return false; |
| } |
| const std::string* maybe_name = dict.FindString("name"); |
| if (maybe_name && *maybe_name != group_key.name) { |
| return false; |
| } |
| return true; |
| } |
| |
| // Copies the `priorityVector` JSON field into `priority_vector`. Returns |
| // true if the JSON is valid and the copy completed. |
| [[nodiscard]] bool TryToCopyPriorityVector( |
| const base::Value::Dict& dict, |
| absl::optional<base::flat_map<std::string, double>>& priority_vector) { |
| const base::Value* maybe_dict = dict.Find("priorityVector"); |
| if (!maybe_dict) { |
| return true; |
| } |
| if (!maybe_dict->is_dict()) { |
| return false; |
| } |
| |
| // Extract all key/value pairs to a vector before writing to a flat_map, since |
| // flat_map insertion is O(n). |
| std::vector<std::pair<std::string, double>> pairs; |
| for (const std::pair<const std::string&, const base::Value&> pair : |
| maybe_dict->GetDict()) { |
| if (pair.second.is_int() || pair.second.is_double()) { |
| pairs.emplace_back(pair.first, pair.second.GetDouble()); |
| continue; |
| } |
| return false; |
| } |
| priority_vector = base::flat_map<std::string, double>(std::move(pairs)); |
| return true; |
| } |
| |
| // Copies the prioritySignalsOverrides JSON field into |
| // `priority_signals_overrides`, returns true if the JSON is valid and the copy |
| // completed. Maps nulls to nullopt, which means a value should be deleted from |
| // the stored interest group. |
| [[nodiscard]] bool TryToCopyPrioritySignalsOverrides( |
| const base::Value::Dict& dict, |
| absl::optional<base::flat_map<std::string, absl::optional<double>>>& |
| priority_signals_overrides) { |
| const base::Value* maybe_dict = dict.Find("prioritySignalsOverrides"); |
| if (!maybe_dict) { |
| return true; |
| } |
| if (!maybe_dict->is_dict()) { |
| return false; |
| } |
| |
| std::vector<std::pair<std::string, absl::optional<double>>> pairs; |
| for (const std::pair<const std::string&, const base::Value&> pair : |
| maybe_dict->GetDict()) { |
| if (pair.second.is_none()) { |
| pairs.emplace_back(pair.first, absl::nullopt); |
| continue; |
| } |
| if (pair.second.is_int() || pair.second.is_double()) { |
| pairs.emplace_back(pair.first, pair.second.GetDouble()); |
| continue; |
| } |
| return false; |
| } |
| priority_signals_overrides = |
| base::flat_map<std::string, absl::optional<double>>(std::move(pairs)); |
| return true; |
| } |
| |
| // Copies the sellerCapabilities JSON field into |
| // `seller_capabilities` and `all_sellers_capabilities`, returns true if the |
| // JSON is valid and the copy completed. |
| [[nodiscard]] bool TryToCopySellerCapabilities( |
| const base::Value::Dict& dict, |
| InterestGroupUpdate& interest_group_update) { |
| const base::Value* maybe_dict = dict.Find("sellerCapabilities"); |
| if (!maybe_dict) { |
| return true; |
| } |
| if (!maybe_dict->is_dict()) { |
| return false; |
| } |
| |
| std::vector<std::pair<url::Origin, blink::SellerCapabilitiesType>> |
| seller_capabilities_vec; |
| for (const std::pair<const std::string&, const base::Value&> pair : |
| maybe_dict->GetDict()) { |
| if (!pair.second.is_list()) { |
| return false; |
| } |
| blink::SellerCapabilitiesType capabilities; |
| for (const base::Value& maybe_capability : pair.second.GetList()) { |
| if (!maybe_capability.is_string()) { |
| return false; |
| } |
| const std::string& capability = maybe_capability.GetString(); |
| base::UmaHistogramBoolean( |
| "Ads.InterestGroup.EnumNaming.Update.SellerCapabilities", |
| capability == "interestGroupCounts" || capability == "latencyStats"); |
| if (capability == "interest-group-counts" || |
| capability == "interestGroupCounts") { |
| capabilities.Put(blink::SellerCapabilities::kInterestGroupCounts); |
| } else if (capability == "latency-stats" || |
| capability == "latencyStats") { |
| capabilities.Put(blink::SellerCapabilities::kLatencyStats); |
| } else { |
| continue; |
| } |
| } |
| if (pair.first == "*") { |
| interest_group_update.all_sellers_capabilities = capabilities; |
| } else { |
| seller_capabilities_vec.emplace_back( |
| url::Origin::Create(GURL(pair.first)), capabilities); |
| } |
| } |
| if (!seller_capabilities_vec.empty()) { |
| interest_group_update.seller_capabilities.emplace(seller_capabilities_vec); |
| } |
| return true; |
| } |
| |
| // Copies the executionMode JSON field into `interest_group_update`, returns |
| // true iff the JSON is valid and the copy completed. |
| [[nodiscard]] bool TryToCopyExecutionMode( |
| const base::Value::Dict& dict, |
| InterestGroupUpdate& interest_group_update) { |
| const std::string* maybe_execution_mode = dict.FindString("executionMode"); |
| if (!maybe_execution_mode) { |
| return true; |
| } |
| base::UmaHistogramBoolean( |
| "Ads.InterestGroup.EnumNaming.Update.WorkletExecutionMode", |
| *maybe_execution_mode == "groupByOrigin"); |
| if (*maybe_execution_mode == "compatibility") { |
| interest_group_update.execution_mode = |
| blink::InterestGroup::ExecutionMode::kCompatibilityMode; |
| } else if (*maybe_execution_mode == "group-by-origin" || |
| *maybe_execution_mode == "groupByOrigin") { |
| interest_group_update.execution_mode = |
| blink::InterestGroup::ExecutionMode::kGroupedByOriginMode; |
| } |
| return true; |
| } |
| |
| // Copies the trustedBiddingSignals list JSON field into |
| // `interest_group_update`, returns true iff the JSON is valid and the copy |
| // completed. |
| [[nodiscard]] bool TryToCopyTrustedBiddingSignalsKeys( |
| const base::Value::Dict& dict, |
| InterestGroupUpdate& interest_group_update) { |
| const base::Value::List* maybe_update_trusted_bidding_signals_keys = |
| dict.FindList("trustedBiddingSignalsKeys"); |
| if (!maybe_update_trusted_bidding_signals_keys) { |
| return true; |
| } |
| std::vector<std::string> trusted_bidding_signals_keys; |
| for (const base::Value& keys_value : |
| *maybe_update_trusted_bidding_signals_keys) { |
| const std::string* maybe_key = keys_value.GetIfString(); |
| if (!maybe_key) { |
| return false; |
| } |
| trusted_bidding_signals_keys.push_back(*maybe_key); |
| } |
| interest_group_update.trusted_bidding_signals_keys = |
| trusted_bidding_signals_keys; |
| return true; |
| } |
| |
| // Helper for TryToCopyAds() and TryToCopyAdComponents(). |
| [[nodiscard]] absl::optional<std::vector<blink::InterestGroup::Ad>> ExtractAds( |
| const base::Value::List& ads_list, |
| bool for_components) { |
| std::vector<blink::InterestGroup::Ad> ads; |
| for (const base::Value& ads_value : ads_list) { |
| const base::Value::Dict* ads_dict = ads_value.GetIfDict(); |
| if (!ads_dict) { |
| return absl::nullopt; |
| } |
| const std::string* maybe_render_url = ads_dict->FindString("renderURL"); |
| const std::string* maybe_render_url_deprecated = |
| ads_dict->FindString("renderUrl"); |
| if (maybe_render_url_deprecated) { |
| if (maybe_render_url) { |
| if (*maybe_render_url != *maybe_render_url_deprecated) { |
| return absl::nullopt; |
| } |
| } else { |
| maybe_render_url = maybe_render_url_deprecated; |
| } |
| } |
| if (!maybe_render_url) { |
| return absl::nullopt; |
| } |
| blink::InterestGroup::Ad ad; |
| ad.render_url = GURL(*maybe_render_url); |
| const std::string* maybe_size_group = ads_dict->FindString("sizeGroup"); |
| if (maybe_size_group) { |
| ad.size_group = *maybe_size_group; |
| } |
| if (!for_components) { |
| const std::string* maybe_buyer_reporting_id = |
| ads_dict->FindString("buyerReportingId"); |
| if (maybe_buyer_reporting_id) { |
| ad.buyer_reporting_id = *maybe_buyer_reporting_id; |
| } |
| const std::string* maybe_buyer_and_seller_reporting_id = |
| ads_dict->FindString("buyerAndSellerReportingId"); |
| if (maybe_buyer_and_seller_reporting_id) { |
| ad.buyer_and_seller_reporting_id = *maybe_buyer_and_seller_reporting_id; |
| } |
| const base::Value::List* maybe_allowed_reporting_origins = |
| ads_dict->FindList("allowedReportingOrigins"); |
| if (maybe_allowed_reporting_origins) { |
| ad.allowed_reporting_origins.emplace(); |
| for (const auto& maybe_origin : *maybe_allowed_reporting_origins) { |
| const std::string* origin_string = maybe_origin.GetIfString(); |
| if (origin_string) { |
| ad.allowed_reporting_origins->emplace_back( |
| url::Origin::Create(GURL(*origin_string))); |
| } |
| } |
| } |
| } |
| const base::Value* maybe_metadata = ads_dict->Find("metadata"); |
| if (maybe_metadata) { |
| std::string metadata; |
| JSONStringValueSerializer serializer(&metadata); |
| if (!serializer.Serialize(*maybe_metadata)) { |
| // Binary blobs shouldn't be present, but it's possible we exceeded the |
| // max JSON depth. |
| return absl::nullopt; |
| } |
| ad.metadata = std::move(metadata); |
| } |
| const std::string* maybe_ad_render_id = ads_dict->FindString("adRenderId"); |
| if (maybe_ad_render_id) { |
| ad.ad_render_id = *maybe_ad_render_id; |
| } |
| ads.push_back(std::move(ad)); |
| } |
| return ads; |
| } |
| |
| // Copies the `ads` list JSON field into `interest_group_update`, returns true |
| // iff the JSON is valid and the copy completed. |
| [[nodiscard]] bool TryToCopyAds(const base::Value::Dict& dict, |
| InterestGroupUpdate& interest_group_update) { |
| const base::Value::List* maybe_ads = dict.FindList("ads"); |
| if (!maybe_ads) { |
| return true; |
| } |
| absl::optional<std::vector<blink::InterestGroup::Ad>> maybe_extracted_ads = |
| ExtractAds(*maybe_ads, /*for_components=*/false); |
| if (!maybe_extracted_ads) { |
| return false; |
| } |
| interest_group_update.ads = std::move(*maybe_extracted_ads); |
| return true; |
| } |
| |
| // Copies the `adComponents` list JSON field into `interest_group_update`, |
| // returns true iff the JSON is valid and the copy completed. |
| [[nodiscard]] bool TryToCopyAdComponents( |
| const base::Value::Dict& dict, |
| InterestGroupUpdate& interest_group_update) { |
| const base::Value::List* maybe_ads = dict.FindList("adComponents"); |
| if (!maybe_ads) { |
| return true; |
| } |
| absl::optional<std::vector<blink::InterestGroup::Ad>> maybe_extracted_ads = |
| ExtractAds(*maybe_ads, /*for_components=*/true); |
| if (!maybe_extracted_ads) { |
| return false; |
| } |
| interest_group_update.ad_components = std::move(*maybe_extracted_ads); |
| return true; |
| } |
| |
| [[nodiscard]] bool TryToCopyAdSizes( |
| const base::Value::Dict& dict, |
| InterestGroupUpdate& interest_group_update) { |
| const base::Value::Dict* maybe_sizes = dict.FindDict("adSizes"); |
| if (!maybe_sizes) { |
| return true; |
| } |
| base::flat_map<std::string, blink::AdSize> size_map; |
| for (std::pair<const std::string&, const base::Value&> pair : *maybe_sizes) { |
| const base::Value::Dict* maybe_size = pair.second.GetIfDict(); |
| if (!maybe_size) { |
| return false; |
| } |
| const std::string* width_str = maybe_size->FindString("width"); |
| const std::string* height_str = maybe_size->FindString("height"); |
| if (!width_str || !height_str) { |
| return false; |
| } |
| |
| auto [width_val, width_units] = blink::ParseAdSizeString(*width_str); |
| auto [height_val, height_units] = blink::ParseAdSizeString(*height_str); |
| |
| size_map.emplace(pair.first, blink::AdSize(width_val, width_units, |
| height_val, height_units)); |
| } |
| interest_group_update.ad_sizes.emplace(size_map); |
| return true; |
| } |
| |
| [[nodiscard]] bool TryToCopySizeGroups( |
| const base::Value::Dict& dict, |
| InterestGroupUpdate& interest_group_update) { |
| const base::Value::Dict* maybe_groups = dict.FindDict("sizeGroups"); |
| if (!maybe_groups) { |
| return true; |
| } |
| base::flat_map<std::string, std::vector<std::string>> group_map; |
| for (std::pair<const std::string&, const base::Value&> pair : *maybe_groups) { |
| const base::Value::List* maybe_sizes = pair.second.GetIfList(); |
| if (!maybe_sizes) { |
| return false; |
| } |
| std::vector<std::string> pair_sizes; |
| for (const base::Value& size : *maybe_sizes) { |
| const std::string* maybe_string = size.GetIfString(); |
| if (!maybe_string) { |
| return false; |
| } |
| pair_sizes.emplace_back(*maybe_string); |
| } |
| group_map.emplace(pair.first, pair_sizes); |
| } |
| interest_group_update.size_groups.emplace(group_map); |
| return true; |
| } |
| |
| [[nodiscard]] bool TryToCopyAuctionServerRequestFlags( |
| const base::Value::Dict& dict, |
| InterestGroupUpdate& interest_group_update) { |
| const base::Value::List* maybe_flags = |
| dict.FindList("auctionServerRequestFlags"); |
| if (!maybe_flags) { |
| return true; |
| } |
| blink::AuctionServerRequestFlags auction_server_request_flags; |
| for (const base::Value& maybe_flag : *maybe_flags) { |
| if (!maybe_flag.is_string()) { |
| return false; |
| } |
| const std::string& flag = maybe_flag.GetString(); |
| if (flag == "omit-ads") { |
| auction_server_request_flags.Put( |
| blink::AuctionServerRequestFlagsEnum::kOmitAds); |
| } else if (flag == "include-full-ads") { |
| auction_server_request_flags.Put( |
| blink::AuctionServerRequestFlagsEnum::kIncludeFullAds); |
| } |
| } |
| interest_group_update.auction_server_request_flags = |
| auction_server_request_flags; |
| return true; |
| } |
| |
| [[nodiscard]] bool TryToCopyPrivateAggregationConfig( |
| const base::Value::Dict& dict, |
| InterestGroupUpdate& interest_group_update) { |
| if (!base::FeatureList::IsEnabled( |
| blink::features::kPrivateAggregationApiMultipleCloudProviders) || |
| !base::FeatureList::IsEnabled( |
| aggregation_service::kAggregationServiceMultipleCloudProviders)) { |
| // Ignore the specified aggregation coordinator unless the feature is |
| // enabled. |
| return true; |
| } |
| |
| const base::Value::Dict* maybe_config = |
| dict.FindDict("privateAggregationConfig"); |
| if (!maybe_config) { |
| return true; |
| } |
| const std::string* maybe_aggregation_coordinator_origin = |
| maybe_config->FindString("aggregationCoordinatorOrigin"); |
| if (!maybe_aggregation_coordinator_origin) { |
| return true; |
| } |
| |
| url::Origin aggregation_coordinator_origin = |
| url::Origin::Create(GURL(*maybe_aggregation_coordinator_origin)); |
| |
| if (!aggregation_service::IsAggregationCoordinatorOriginAllowed( |
| aggregation_coordinator_origin)) { |
| return false; |
| } |
| |
| interest_group_update.aggregation_coordinator_origin = |
| std::move(aggregation_coordinator_origin); |
| return true; |
| } |
| |
| absl::optional<InterestGroupUpdate> ParseUpdateJson( |
| const blink::InterestGroupKey& group_key, |
| const data_decoder::DataDecoder::ValueOrError& result) { |
| // TODO(crbug.com/1186444): Report to devtools. |
| if (!result.has_value()) { |
| return absl::nullopt; |
| } |
| const base::Value::Dict* dict = result->GetIfDict(); |
| if (!dict) { |
| return absl::nullopt; |
| } |
| if (!ValidateNameAndOwnerIfPresent(group_key, *dict)) { |
| return absl::nullopt; |
| } |
| InterestGroupUpdate interest_group_update; |
| const base::Value* maybe_priority_value = dict->Find("priority"); |
| if (maybe_priority_value) { |
| // If the field is specified, it must be an integer or a double. |
| if (!maybe_priority_value->is_int() && !maybe_priority_value->is_double()) { |
| return absl::nullopt; |
| } |
| interest_group_update.priority = maybe_priority_value->GetDouble(); |
| } |
| const base::Value* maybe_enable_bidding_signals_prioritization = |
| dict->Find("enableBiddingSignalsPrioritization"); |
| if (maybe_enable_bidding_signals_prioritization) { |
| // If the field is specified, it must be a bool. |
| if (!maybe_enable_bidding_signals_prioritization->is_bool()) { |
| return absl::nullopt; |
| } |
| interest_group_update.enable_bidding_signals_prioritization = |
| maybe_enable_bidding_signals_prioritization->GetBool(); |
| } |
| if (!TryToCopyPriorityVector(*dict, interest_group_update.priority_vector) || |
| !TryToCopyPrioritySignalsOverrides( |
| *dict, interest_group_update.priority_signals_overrides) || |
| !TryToCopyExecutionMode(*dict, interest_group_update)) { |
| return absl::nullopt; |
| } |
| if (!TryToCopySellerCapabilities(*dict, interest_group_update)) { |
| return absl::nullopt; |
| } |
| const std::string* maybe_bidding_url = dict->FindString("biddingLogicURL"); |
| const std::string* maybe_bidding_url_deprecated = |
| dict->FindString("biddingLogicUrl"); |
| if (maybe_bidding_url_deprecated) { |
| if (maybe_bidding_url) { |
| if (*maybe_bidding_url_deprecated != *maybe_bidding_url) { |
| return absl::nullopt; |
| } |
| } else { |
| maybe_bidding_url = maybe_bidding_url_deprecated; |
| } |
| } |
| if (maybe_bidding_url) { |
| interest_group_update.bidding_url = GURL(*maybe_bidding_url); |
| } |
| const std::string* maybe_bidding_wasm_helper_url = |
| dict->FindString("biddingWasmHelperURL"); |
| const std::string* maybe_bidding_wasm_helper_url_deprecated = |
| dict->FindString("biddingWasmHelperUrl"); |
| if (maybe_bidding_wasm_helper_url_deprecated) { |
| if (maybe_bidding_wasm_helper_url) { |
| if (*maybe_bidding_wasm_helper_url != |
| *maybe_bidding_wasm_helper_url_deprecated) { |
| return absl::nullopt; |
| } |
| } else { |
| maybe_bidding_wasm_helper_url = maybe_bidding_wasm_helper_url_deprecated; |
| } |
| } |
| if (maybe_bidding_wasm_helper_url) { |
| interest_group_update.bidding_wasm_helper_url = |
| GURL(*maybe_bidding_wasm_helper_url); |
| } |
| const std::string* maybe_update_url = |
| dict->FindString("updateURL"); // TODO check if we use this or updateURL |
| if (maybe_update_url) { |
| interest_group_update.daily_update_url = GURL(*maybe_update_url); |
| } |
| const std::string* maybe_trusted_bidding_signals_url = |
| dict->FindString("trustedBiddingSignalsURL"); |
| const std::string* maybe_trusted_bidding_signals_url_deprecated = |
| dict->FindString("trustedBiddingSignalsUrl"); |
| if (maybe_trusted_bidding_signals_url_deprecated) { |
| if (maybe_trusted_bidding_signals_url) { |
| if (*maybe_trusted_bidding_signals_url != |
| *maybe_trusted_bidding_signals_url_deprecated) { |
| return absl::nullopt; |
| } |
| } else { |
| maybe_trusted_bidding_signals_url = |
| maybe_trusted_bidding_signals_url_deprecated; |
| } |
| } |
| if (maybe_trusted_bidding_signals_url) { |
| interest_group_update.trusted_bidding_signals_url = |
| GURL(*maybe_trusted_bidding_signals_url); |
| } |
| if (!TryToCopyTrustedBiddingSignalsKeys(*dict, interest_group_update)) { |
| return absl::nullopt; |
| } |
| if (!TryToCopyAds(*dict, interest_group_update)) { |
| return absl::nullopt; |
| } |
| if (!TryToCopyAdComponents(*dict, interest_group_update)) { |
| return absl::nullopt; |
| } |
| if (!TryToCopyAdSizes(*dict, interest_group_update)) { |
| return absl::nullopt; |
| } |
| if (!TryToCopySizeGroups(*dict, interest_group_update)) { |
| return absl::nullopt; |
| } |
| if (!TryToCopyAuctionServerRequestFlags(*dict, interest_group_update)) { |
| return absl::nullopt; |
| } |
| if (!TryToCopyPrivateAggregationConfig(*dict, interest_group_update)) { |
| return absl::nullopt; |
| } |
| return interest_group_update; |
| } |
| |
| } // namespace |
| |
| InterestGroupUpdateManager::InterestGroupUpdateManager( |
| InterestGroupManagerImpl* manager, |
| scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory) |
| : manager_(manager), |
| max_update_round_duration_(kMaxUpdateRoundDuration), |
| max_parallel_updates_(kMaxParallelUpdates), |
| url_loader_factory_(std::move(url_loader_factory)) {} |
| |
| InterestGroupUpdateManager::~InterestGroupUpdateManager() = default; |
| |
| void InterestGroupUpdateManager::UpdateInterestGroupsOfOwner( |
| const url::Origin& owner, |
| network::mojom::ClientSecurityStatePtr client_security_state, |
| AreReportingOriginsAttestedCallback callback) { |
| attestation_callback_ = std::move(callback); |
| owners_to_update_.Enqueue(owner, std::move(client_security_state)); |
| MaybeContinueUpdatingCurrentOwner(); |
| } |
| |
| void InterestGroupUpdateManager::UpdateInterestGroupsOfOwners( |
| base::span<url::Origin> owners, |
| network::mojom::ClientSecurityStatePtr client_security_state, |
| AreReportingOriginsAttestedCallback callback) { |
| // Shuffle the list of interest group owners for fairness. |
| base::RandomShuffle(owners.begin(), owners.end()); |
| for (const url::Origin& owner : owners) { |
| UpdateInterestGroupsOfOwner(owner, client_security_state.Clone(), callback); |
| } |
| } |
| |
| void InterestGroupUpdateManager::set_max_update_round_duration_for_testing( |
| base::TimeDelta delta) { |
| max_update_round_duration_ = delta; |
| } |
| |
| void InterestGroupUpdateManager::set_max_parallel_updates_for_testing( |
| int max_parallel_updates) { |
| max_parallel_updates_ = max_parallel_updates; |
| } |
| |
| InterestGroupUpdateManager::OwnersToUpdate::OwnersToUpdate() = default; |
| |
| InterestGroupUpdateManager::OwnersToUpdate::~OwnersToUpdate() = default; |
| |
| bool InterestGroupUpdateManager::OwnersToUpdate::Empty() const { |
| return owners_to_update_.empty(); |
| } |
| |
| const url::Origin& InterestGroupUpdateManager::OwnersToUpdate::FrontOwner() |
| const { |
| return owners_to_update_.front(); |
| } |
| |
| network::mojom::ClientSecurityStatePtr |
| InterestGroupUpdateManager::OwnersToUpdate::FrontSecurityState() const { |
| return security_state_map_.at(FrontOwner()).Clone(); |
| } |
| |
| bool InterestGroupUpdateManager::OwnersToUpdate::Enqueue( |
| const url::Origin& owner, |
| network::mojom::ClientSecurityStatePtr client_security_state) { |
| if (!security_state_map_.emplace(owner, std::move(client_security_state)) |
| .second) { |
| return false; |
| } |
| owners_to_update_.emplace_back(owner); |
| return true; |
| } |
| |
| void InterestGroupUpdateManager::OwnersToUpdate::PopFront() { |
| security_state_map_.erase(owners_to_update_.front()); |
| owners_to_update_.pop_front(); |
| |
| if (owners_to_update_.empty()) { |
| joining_origin_isolation_info_map_.clear(); |
| } |
| } |
| |
| net::IsolationInfo* |
| InterestGroupUpdateManager::OwnersToUpdate::GetIsolationInfoByJoiningOrigin( |
| const url::Origin& joining_origin) { |
| auto isolation_info_it = |
| joining_origin_isolation_info_map_.find(joining_origin); |
| if (isolation_info_it != joining_origin_isolation_info_map_.end()) { |
| return &isolation_info_it->second; |
| } else { |
| net::IsolationInfo isolation_info = net::IsolationInfo::CreateTransient(); |
| const auto [it, success] = joining_origin_isolation_info_map_.insert( |
| {joining_origin, std::move(isolation_info)}); |
| CHECK(success); |
| return &it->second; |
| } |
| } |
| |
| void InterestGroupUpdateManager::OwnersToUpdate:: |
| ClearJoiningOriginIsolationInfoMap() { |
| joining_origin_isolation_info_map_.clear(); |
| } |
| |
| void InterestGroupUpdateManager::OwnersToUpdate::Clear() { |
| owners_to_update_.clear(); |
| security_state_map_.clear(); |
| joining_origin_isolation_info_map_.clear(); |
| } |
| |
| void InterestGroupUpdateManager::MaybeContinueUpdatingCurrentOwner() { |
| if (num_in_flight_updates_ > 0 || waiting_on_db_read_) { |
| return; |
| } |
| |
| if (owners_to_update_.Empty()) { |
| // This update round is finished, there's no more work to do. |
| last_update_started_ = base::TimeTicks::Min(); |
| return; |
| } |
| |
| if (last_update_started_ == base::TimeTicks::Min()) { |
| // It appears we're staring a new update round; mark the time we started the |
| // round. |
| last_update_started_ = base::TimeTicks::Now(); |
| } else if (base::TimeTicks::Now() - last_update_started_ > |
| max_update_round_duration_) { |
| // We've been updating for too long; cancel all outstanding updates. |
| owners_to_update_.Clear(); |
| last_update_started_ = base::TimeTicks::Min(); |
| return; |
| } |
| |
| GetInterestGroupsForUpdate( |
| owners_to_update_.FrontOwner(), |
| base::BindOnce( |
| &InterestGroupUpdateManager::DidUpdateInterestGroupsOfOwnerDbLoad, |
| weak_factory_.GetWeakPtr(), owners_to_update_.FrontOwner())); |
| } |
| |
| void InterestGroupUpdateManager::GetInterestGroupsForUpdate( |
| const url::Origin& owner, |
| base::OnceCallback<void(std::vector<InterestGroupUpdateParameter>)> |
| callback) { |
| DCHECK_EQ(num_in_flight_updates_, 0); |
| DCHECK(!waiting_on_db_read_); |
| waiting_on_db_read_ = true; |
| |
| // Read one more interest group than `max_parallel_updates_` from database to |
| // support the batching logic in `DidUpdateInterestGroupsOfOwnerDbLoad()`. |
| manager_->GetInterestGroupsForUpdate( |
| owner, /*groups_limit=*/max_parallel_updates_ + 1, std::move(callback)); |
| } |
| |
| void InterestGroupUpdateManager::UpdateInterestGroupByBatch( |
| const url::Origin& owner, |
| std::vector<InterestGroupUpdateParameter> update_parameters) { |
| DCHECK_LE(update_parameters.size(), |
| static_cast<unsigned int>(max_parallel_updates_)); |
| |
| // If feature kGroupNIKByJoiningOriginPerOwner is not enabled, use one single |
| // NIK for all storage interest groups. |
| net::IsolationInfo per_update_isolation_info; |
| if (!base::FeatureList::IsEnabled(features::kGroupNIKByJoiningOrigin)) { |
| per_update_isolation_info = net::IsolationInfo::CreateTransient(); |
| } |
| |
| for (auto& [interest_group_key, update_url, joining_origin] : |
| update_parameters) { |
| manager_->QueueKAnonymityUpdateForInterestGroup(interest_group_key); |
| ++num_in_flight_updates_; |
| base::UmaHistogramCounts100000( |
| "Ads.InterestGroup.Net.RequestUrlSizeBytes.Update", |
| update_url.spec().size()); |
| auto resource_request = std::make_unique<network::ResourceRequest>(); |
| resource_request->url = std::move(update_url); |
| resource_request->redirect_mode = network::mojom::RedirectMode::kError; |
| resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit; |
| resource_request->request_initiator = owner; |
| resource_request->trusted_params = |
| network::ResourceRequest::TrustedParams(); |
| if (base::FeatureList::IsEnabled(features::kGroupNIKByJoiningOrigin)) { |
| resource_request->trusted_params->isolation_info = |
| *owners_to_update_.GetIsolationInfoByJoiningOrigin(joining_origin); |
| } else { |
| resource_request->trusted_params->isolation_info = |
| per_update_isolation_info; |
| } |
| resource_request->trusted_params->client_security_state = |
| owners_to_update_.FrontSecurityState(); |
| auto simple_url_loader = network::SimpleURLLoader::Create( |
| std::move(resource_request), kTrafficAnnotation); |
| simple_url_loader->SetTimeoutDuration(base::Seconds(30)); |
| auto simple_url_loader_it = |
| url_loaders_.insert(url_loaders_.end(), std::move(simple_url_loader)); |
| (*simple_url_loader_it) |
| ->DownloadToString( |
| url_loader_factory_.get(), |
| base::BindOnce(&InterestGroupUpdateManager:: |
| DidUpdateInterestGroupsOfOwnerNetFetch, |
| weak_factory_.GetWeakPtr(), simple_url_loader_it, |
| interest_group_key), |
| kMaxUpdateSize); |
| } |
| |
| // To avoid the possibility of groups that join during the current update |
| // process being updated in the next batch with the same isolation |
| // information, clear the `joining_origin_isolation_info_map_` when mixed |
| // joining origins are detected in a single update batch. |
| if (base::FeatureList::IsEnabled(features::kGroupNIKByJoiningOrigin)) { |
| if (update_parameters.size() > 1 && |
| !update_parameters.at(0).joining_origin.IsSameOriginWith( |
| update_parameters.back().joining_origin)) { |
| owners_to_update_.ClearJoiningOriginIsolationInfoMap(); |
| } |
| } |
| } |
| |
| void InterestGroupUpdateManager::DidUpdateInterestGroupsOfOwnerDbLoad( |
| url::Origin owner, |
| std::vector<InterestGroupUpdateParameter> update_parameters) { |
| DCHECK_EQ(owner, owners_to_update_.FrontOwner()); |
| DCHECK_EQ(num_in_flight_updates_, 0); |
| DCHECK(waiting_on_db_read_); |
| waiting_on_db_read_ = false; |
| |
| if (update_parameters.empty()) { |
| // All interest groups for `owner` are up to date, so we can pop it off the |
| // queue. |
| owners_to_update_.PopFront(); |
| MaybeContinueUpdatingCurrentOwner(); |
| return; |
| } |
| |
| if (!base::FeatureList::IsEnabled(features::kGroupNIKByJoiningOrigin)) { |
| update_parameters.resize(std::min( |
| update_parameters.size(), static_cast<size_t>(max_parallel_updates_))); |
| DCHECK_LE(update_parameters.size(), |
| static_cast<unsigned int>(max_parallel_updates_)); |
| UpdateInterestGroupByBatch(owner, std::move(update_parameters)); |
| return; |
| } |
| |
| // A group of IGs of the same joining origin and update NIK may only be |
| // updated across batches if all but the last of those batches contain only |
| // IGs of that joining origin / NIK -- otherwise, a server that knows the |
| // batch size could deduce information about the number of interest groups |
| // that had a different joining origin for prior batches. For details, see the |
| // discussion at |
| // https://chromium-review.googlesource.com/c/chromium/src/+/4794574/17..20/content/browser/interest_group/interest_group_update_manager.cc#b736. |
| |
| // If the size of storage groups vector is not larger than the limitation, |
| // the storage groups can be put into one batch and update together. |
| if (update_parameters.size() <= static_cast<size_t>(max_parallel_updates_)) { |
| UpdateInterestGroupByBatch(owner, std::move(update_parameters)); |
| return; |
| } |
| |
| // If the first group and the last group have same joining origin, it is |
| // safe to put them in the same update batch. |
| if (update_parameters.at(0).joining_origin.IsSameOriginWith( |
| update_parameters.at(static_cast<size_t>(max_parallel_updates_) - 1) |
| .joining_origin)) { |
| update_parameters.resize(max_parallel_updates_); |
| UpdateInterestGroupByBatch(owner, std::move(update_parameters)); |
| return; |
| } |
| |
| // Resize the interest group to the limit if the last storage group has |
| // different joining origin than the next storage group after the batch |
| // limit. |
| if (!update_parameters.at(max_parallel_updates_ - 1) |
| .joining_origin.IsSameOriginWith( |
| update_parameters.at(max_parallel_updates_).joining_origin)) { |
| update_parameters.resize(max_parallel_updates_); |
| } else { |
| // Interest groups with same joining origin cannot be put into |
| // different batches, unless it can fill all the batches except the |
| // last one. Therefore, after resize, all the interest groups with |
| // same joining origin as the last one need to be popped out to be |
| // loaded in the next batch. |
| update_parameters.resize(max_parallel_updates_); |
| url::Origin pop_out_origin = |
| update_parameters.at(max_parallel_updates_ - 1).joining_origin; |
| |
| while (update_parameters.size() > 0 and |
| update_parameters.back().joining_origin.IsSameOriginWith( |
| pop_out_origin)) { |
| update_parameters.pop_back(); |
| } |
| } |
| |
| UpdateInterestGroupByBatch(owner, std::move(update_parameters)); |
| return; |
| } |
| |
| void InterestGroupUpdateManager::DidUpdateInterestGroupsOfOwnerNetFetch( |
| UrlLoadersList::iterator simple_url_loader_it, |
| blink::InterestGroupKey group_key, |
| std::unique_ptr<std::string> fetch_body) { |
| DCHECK_EQ(group_key.owner, owners_to_update_.FrontOwner()); |
| DCHECK_GT(num_in_flight_updates_, 0); |
| DCHECK(!waiting_on_db_read_); |
| std::unique_ptr<network::SimpleURLLoader> simple_url_loader = |
| std::move(*simple_url_loader_it); |
| url_loaders_.erase(simple_url_loader_it); |
| // TODO(crbug.com/1186444): Report HTTP error info to devtools. |
| if (!fetch_body) { |
| ReportUpdateFailed(group_key, |
| /*delay_type=*/simple_url_loader->NetError() == |
| net::ERR_INTERNET_DISCONNECTED |
| ? UpdateDelayType::kNoInternet |
| : UpdateDelayType::kNetFailure); |
| return; |
| } |
| base::UmaHistogramCounts100000( |
| "Ads.InterestGroup.Net.ResponseSizeBytes.Update", fetch_body->size()); |
| data_decoder::DataDecoder::ParseJsonIsolated( |
| *fetch_body, |
| base::BindOnce( |
| &InterestGroupUpdateManager::DidUpdateInterestGroupsOfOwnerJsonParse, |
| weak_factory_.GetWeakPtr(), std::move(group_key))); |
| } |
| |
| void InterestGroupUpdateManager::DidUpdateInterestGroupsOfOwnerJsonParse( |
| blink::InterestGroupKey group_key, |
| data_decoder::DataDecoder::ValueOrError result) { |
| DCHECK_EQ(group_key.owner, owners_to_update_.FrontOwner()); |
| DCHECK_GT(num_in_flight_updates_, 0); |
| DCHECK(!waiting_on_db_read_); |
| absl::optional<InterestGroupUpdate> interest_group_update = |
| ParseUpdateJson(group_key, result); |
| if (!interest_group_update) { |
| ReportUpdateFailed(group_key, UpdateDelayType::kParseFailure); |
| return; |
| } |
| // All ads' allowed reporting origins must be attested. Otherwise don't update |
| // the interest group. |
| if (interest_group_update->ads) { |
| for (auto& ad : *interest_group_update->ads) { |
| if (ad.allowed_reporting_origins) { |
| // Sort and de-duplicate by passing it through a flat_set. |
| ad.allowed_reporting_origins = |
| base::flat_set<url::Origin>( |
| std::move(ad.allowed_reporting_origins.value())) |
| .extract(); |
| if (!attestation_callback_.Run(ad.allowed_reporting_origins.value())) { |
| // Treat this the same way as a parse failure. |
| ReportUpdateFailed(group_key, UpdateDelayType::kParseFailure); |
| return; |
| } |
| } |
| } |
| } |
| UpdateInterestGroup(group_key, std::move(*interest_group_update)); |
| } |
| |
| void InterestGroupUpdateManager::UpdateInterestGroup( |
| const blink::InterestGroupKey& group_key, |
| InterestGroupUpdate update) { |
| manager_->UpdateInterestGroup( |
| group_key, std::move(update), |
| base::BindOnce( |
| &InterestGroupUpdateManager::OnUpdateInterestGroupCompleted, |
| weak_factory_.GetWeakPtr(), group_key)); |
| } |
| |
| void InterestGroupUpdateManager::OnUpdateInterestGroupCompleted( |
| const blink::InterestGroupKey& group_key, |
| bool success) { |
| if (!success) { |
| ReportUpdateFailed(group_key, UpdateDelayType::kParseFailure); |
| return; |
| } |
| OnOneUpdateCompleted(); |
| } |
| |
| void InterestGroupUpdateManager::OnOneUpdateCompleted() { |
| DCHECK_GT(num_in_flight_updates_, 0); |
| --num_in_flight_updates_; |
| MaybeContinueUpdatingCurrentOwner(); |
| } |
| |
| void InterestGroupUpdateManager::ReportUpdateFailed( |
| const blink::InterestGroupKey& group_key, |
| UpdateDelayType delay_type) { |
| if (delay_type != UpdateDelayType::kNoInternet) { |
| manager_->ReportUpdateFailed( |
| group_key, |
| /*parse_failure=*/delay_type == UpdateDelayType::kParseFailure); |
| } |
| |
| if (delay_type == UpdateDelayType::kNoInternet) { |
| // If the internet is disconnected, no more updating is possible at the |
| // moment. As we are now stopping update work, and it is an invariant |
| // that `owners_to_update_` only stores owners that will eventually |
| // be processed, we clear `owners_to_update_` to ensure that future |
| // update attempts aren't blocked. |
| // |
| // To avoid violating the invariant that we're always updating the front of |
| // the queue, only clear we encounter this error on the last in-flight |
| // update. |
| if (num_in_flight_updates_ == 1) { |
| owners_to_update_.Clear(); |
| } |
| } |
| |
| OnOneUpdateCompleted(); |
| } |
| |
| } // namespace content |