| // Copyright 2022 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "content/browser/attribution_reporting/attribution_interop_parser.h" |
| |
| #include <stddef.h> |
| #include <stdint.h> |
| |
| #include <ostream> |
| #include <utility> |
| |
| #include "base/callback.h" |
| #include "base/check.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_piece.h" |
| #include "base/test/bind.h" |
| #include "base/time/time.h" |
| #include "content/public/test/attribution_config.h" |
| #include "third_party/abseil-cpp/absl/types/optional.h" |
| #include "url/gurl.h" |
| #include "url/origin.h" |
| |
| namespace content { |
| |
| AttributionInteropParser::AttributionInteropParser(std::ostream& stream) |
| : error_manager_(stream) {} |
| |
| AttributionInteropParser::~AttributionInteropParser() = default; |
| |
| bool AttributionInteropParser::has_error() const { |
| return error_manager_.has_error(); |
| } |
| |
| std::unique_ptr<AttributionParserErrorManager::ScopedContext> |
| AttributionInteropParser::PushContext( |
| AttributionParserErrorManager::Context context) { |
| return error_manager_.PushContext(context); |
| } |
| |
| AttributionParserErrorManager::ErrorWriter AttributionInteropParser::Error() { |
| return error_manager_.Error(); |
| } |
| |
| void AttributionInteropParser::MoveDictValues(base::Value::Dict& in, |
| base::Value::Dict& out) { |
| for (auto [key, value] : in) { |
| auto context = PushContext(key); |
| if (out.contains(key)) { |
| *Error() << "must not be present"; |
| return; |
| } |
| out.Set(key, std::move(value)); |
| } |
| } |
| |
| void AttributionInteropParser::MoveValue( |
| base::Value::Dict& in, |
| base::StringPiece in_key, |
| base::Value::Dict& out, |
| absl::optional<base::StringPiece> out_key_opt) { |
| auto context = PushContext(in_key); |
| |
| base::Value* value = in.Find(in_key); |
| if (!value) { |
| *Error() << "must be present"; |
| return; |
| } |
| |
| base::StringPiece out_key = out_key_opt.value_or(in_key); |
| DCHECK(!out.contains(out_key)); |
| out.Set(out_key, std::move(*value)); |
| } |
| |
| bool AttributionInteropParser::EnsureDictionary(const base::Value* value) { |
| if (!value) { |
| *Error() << "must be present"; |
| return false; |
| } |
| |
| if (!value->is_dict()) { |
| *Error() << "must be a dictionary"; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| absl::optional<std::string> AttributionInteropParser::ExtractString( |
| base::Value::Dict& dict, |
| base::StringPiece key) { |
| auto context = PushContext(key); |
| |
| absl::optional<base::Value> value = dict.Extract(key); |
| if (!value) { |
| *Error() << "must be present"; |
| return absl::nullopt; |
| } |
| |
| if (std::string* str = value->GetIfString()) |
| return std::move(*str); |
| |
| *Error() << "must be a string"; |
| return absl::nullopt; |
| } |
| |
| void AttributionInteropParser::ParseList( |
| base::Value* values, |
| base::RepeatingCallback<void(base::Value)> callback, |
| size_t expected_size) { |
| if (!values) { |
| *Error() << "must be present"; |
| return; |
| } |
| |
| base::Value::List* list = values->GetIfList(); |
| if (!list) { |
| *Error() << "must be a list"; |
| return; |
| } |
| |
| if (expected_size > 0 && list->size() != expected_size) { |
| *Error() << "must have size " << expected_size; |
| return; |
| } |
| |
| size_t index = 0; |
| for (auto& value : values->GetList()) { |
| auto context = PushContext(index); |
| callback.Run(std::move(value)); |
| index++; |
| } |
| } |
| |
| absl::optional<std::string> AttributionInteropParser::ParseRequest( |
| base::Value::Dict& in, |
| base::Value::Dict& out) { |
| static constexpr char kKey[] = "registration_request"; |
| |
| auto context = PushContext(kKey); |
| |
| base::Value* request = in.Find(kKey); |
| if (!EnsureDictionary(request)) |
| return absl::nullopt; |
| |
| absl::optional<std::string> str = |
| ExtractString(request->GetDict(), "attribution_src_url"); |
| |
| MoveDictValues(request->GetDict(), out); |
| |
| return str; |
| } |
| |
| void AttributionInteropParser::ParseResponse( |
| base::Value::Dict& in, |
| base::Value::Dict& out, |
| const std::string& attribution_src_url) { |
| static constexpr char kKey[] = "responses"; |
| |
| auto context = PushContext(kKey); |
| |
| ParseList(in.Find(kKey), base::BindLambdaForTesting([&](base::Value value) { |
| if (!EnsureDictionary(&value)) |
| return; |
| |
| static constexpr char kKeyUrl[] = "url"; |
| if (absl::optional<std::string> url = |
| ExtractString(value.GetDict(), kKeyUrl); |
| url && *url != attribution_src_url) { |
| auto inner_context = PushContext(kKeyUrl); |
| *Error() << "must match " << attribution_src_url; |
| } |
| |
| static constexpr char kKeyResponse[] = "response"; |
| auto inner_context = PushContext(kKeyResponse); |
| base::Value* response = value.FindKey(kKeyResponse); |
| if (!EnsureDictionary(response)) |
| return; |
| |
| MoveDictValues(response->GetDict(), out); |
| }), |
| /*expected_size=*/1); |
| } |
| |
| base::Value::List AttributionInteropParser::ParseEvents(base::Value::Dict& dict, |
| base::StringPiece key) { |
| auto context = PushContext(key); |
| |
| base::Value::List results; |
| |
| ParseList(dict.Find(key), base::BindLambdaForTesting([&](base::Value value) { |
| if (!EnsureDictionary(&value)) |
| return; |
| |
| static constexpr char kKeyReportingOrigin[] = "reporting_origin"; |
| |
| base::Value::Dict dict; |
| MoveValue(value.GetDict(), "timestamp", dict); |
| |
| // Placeholder so that it errors out if request or response |
| // contains this field. |
| dict.Set(kKeyReportingOrigin, ""); |
| |
| absl::optional<std::string> attribution_src_url = |
| ParseRequest(value.GetDict(), dict); |
| |
| if (has_error()) |
| return; |
| |
| DCHECK(attribution_src_url); |
| |
| ParseResponse(value.GetDict(), dict, *attribution_src_url); |
| |
| if (has_error()) |
| return; |
| |
| dict.Set( |
| kKeyReportingOrigin, |
| url::Origin::Create(GURL(std::move(*attribution_src_url))) |
| .Serialize()); |
| |
| results.Append(std::move(dict)); |
| })); |
| |
| return results; |
| } |
| |
| absl::optional<base::Value> |
| AttributionInteropParser::SimulatorInputFromInteropInput( |
| base::Value::Dict& input) { |
| static constexpr char kKey[] = "input"; |
| |
| error_manager_.ResetErrorState(); |
| |
| auto context = PushContext(kKey); |
| |
| base::Value* dict = input.Find(kKey); |
| if (!EnsureDictionary(dict)) |
| return absl::nullopt; |
| |
| base::Value::List sources = ParseEvents(dict->GetDict(), "sources"); |
| base::Value::List triggers = ParseEvents(dict->GetDict(), "triggers"); |
| |
| if (has_error()) |
| return absl::nullopt; |
| |
| base::Value::Dict result; |
| result.Set("sources", std::move(sources)); |
| result.Set("triggers", std::move(triggers)); |
| return base::Value(std::move(result)); |
| } |
| |
| base::Value::List AttributionInteropParser::ParseEventLevelReports( |
| base::Value::Dict& output) { |
| static constexpr char kKey[] = "event_level_reports"; |
| |
| base::Value::List event_level_results; |
| |
| base::Value* value = output.Find(kKey); |
| if (!value) |
| return event_level_results; |
| |
| auto context = PushContext(kKey); |
| ParseList(output.Find(kKey), |
| base::BindLambdaForTesting([&](base::Value value) { |
| if (!EnsureDictionary(&value)) |
| return; |
| |
| base::Value::Dict result; |
| |
| base::Value::Dict& value_dict = value.GetDict(); |
| MoveValue(value_dict, "report", result, "payload"); |
| MoveValue(value_dict, "report_url", result); |
| MoveValue(value_dict, "report_time", result); |
| |
| if (has_error()) |
| return; |
| |
| event_level_results.Append(std::move(result)); |
| })); |
| |
| return event_level_results; |
| } |
| |
| base::Value::List AttributionInteropParser::ParseAggregatableReports( |
| base::Value::Dict& output) { |
| static constexpr char kKey[] = "aggregatable_reports"; |
| |
| base::Value::List aggregatable_results; |
| |
| base::Value* value = output.Find(kKey); |
| if (!value) |
| return aggregatable_results; |
| |
| auto context = PushContext(kKey); |
| ParseList(output.Find(kKey), |
| base::BindLambdaForTesting([&](base::Value value) { |
| if (!EnsureDictionary(&value)) |
| return; |
| |
| base::Value::Dict result; |
| |
| base::Value::Dict& value_dict = value.GetDict(); |
| MoveValue(value_dict, "report_url", result); |
| MoveValue(value_dict, "report_time", result); |
| |
| static constexpr char kKeyTestInfo[] = "test_info"; |
| base::Value* test_info; |
| { |
| auto test_info_context = PushContext(kKeyTestInfo); |
| test_info = value_dict.Find(kKeyTestInfo); |
| if (!EnsureDictionary(test_info)) |
| return; |
| } |
| |
| static constexpr char kKeyReport[] = "report"; |
| { |
| auto report_context = PushContext(kKeyReport); |
| base::Value* report = value_dict.Find(kKeyReport); |
| if (!EnsureDictionary(report)) |
| return; |
| |
| MoveDictValues(test_info->GetDict(), report->GetDict()); |
| } |
| |
| MoveValue(value_dict, "report", result, "payload"); |
| |
| if (has_error()) |
| return; |
| |
| aggregatable_results.Append(std::move(result)); |
| })); |
| |
| return aggregatable_results; |
| } |
| |
| absl::optional<base::Value> |
| AttributionInteropParser::InteropOutputFromSimulatorOutput(base::Value output) { |
| error_manager_.ResetErrorState(); |
| |
| if (!EnsureDictionary(&output)) |
| return absl::nullopt; |
| |
| base::Value::List event_level_results = |
| ParseEventLevelReports(output.GetDict()); |
| |
| base::Value::List aggregatable_results = |
| ParseAggregatableReports(output.GetDict()); |
| |
| if (has_error()) |
| return absl::nullopt; |
| |
| base::Value::Dict dict; |
| if (!event_level_results.empty()) |
| dict.Set("event_level_results", std::move(event_level_results)); |
| |
| if (!aggregatable_results.empty()) |
| dict.Set("aggregatable_results", std::move(aggregatable_results)); |
| |
| return base::Value(std::move(dict)); |
| } |
| |
| bool AttributionInteropParser::ParseInt(const base::Value::Dict& dict, |
| base::StringPiece key, |
| int& result, |
| bool required, |
| bool allow_zero) { |
| return ParseInteger(dict, key, result, &base::StringToInt, required, |
| allow_zero); |
| } |
| |
| bool AttributionInteropParser::ParseUint64(const base::Value::Dict& dict, |
| base::StringPiece key, |
| uint64_t& result, |
| bool required, |
| bool allow_zero) { |
| return ParseInteger(dict, key, result, &base::StringToUint64, required, |
| allow_zero); |
| } |
| |
| bool AttributionInteropParser::ParseInt64(const base::Value::Dict& dict, |
| base::StringPiece key, |
| int64_t& result, |
| bool required, |
| bool allow_zero) { |
| return ParseInteger(dict, key, result, &base::StringToInt64, required, |
| allow_zero); |
| } |
| |
| void AttributionInteropParser::ParseRandomizedResponseRate( |
| const base::Value::Dict& dict, |
| base::StringPiece key, |
| double& result, |
| bool required) { |
| auto context = PushContext(key); |
| |
| const base::Value* value = dict.Find(key); |
| |
| if (value) { |
| absl::optional<double> d = value->GetIfDouble(); |
| if (d && *d >= 0 && *d <= 1) { |
| result = *d; |
| return; |
| } |
| } else if (!required) { |
| return; |
| } |
| |
| *Error() << "must be a double between 0 and 1 formatted as string"; |
| } |
| |
| bool AttributionInteropParser::ParseConfig(const base::Value& value, |
| AttributionConfig& config, |
| bool required, |
| base::StringPiece key) { |
| error_manager_.ResetErrorState(); |
| |
| std::unique_ptr<AttributionParserErrorManager::ScopedContext> context; |
| if (!key.empty()) |
| context = PushContext(key); |
| |
| if (!EnsureDictionary(&value)) |
| return false; |
| |
| const base::Value::Dict& dict = value.GetDict(); |
| |
| ParseInt(dict, "max_sources_per_origin", config.max_sources_per_origin, |
| required); |
| |
| ParseInt(dict, "max_destinations_per_source_site_reporting_origin", |
| config.max_destinations_per_source_site_reporting_origin, required); |
| |
| uint64_t source_event_id_cardinality; |
| if (ParseUint64(dict, "source_event_id_cardinality", |
| source_event_id_cardinality, required, |
| /*allow_zero=*/true)) { |
| if (source_event_id_cardinality == 0u) { |
| config.source_event_id_cardinality = absl::nullopt; |
| } else { |
| config.source_event_id_cardinality = source_event_id_cardinality; |
| } |
| } |
| |
| int rate_limit_time_window; |
| if (ParseInt(dict, "rate_limit_time_window", rate_limit_time_window, |
| required)) { |
| config.rate_limit.time_window = base::Days(rate_limit_time_window); |
| } |
| |
| ParseInt64(dict, "rate_limit_max_source_registration_reporting_origins", |
| config.rate_limit.max_source_registration_reporting_origins, |
| required); |
| ParseInt64(dict, "rate_limit_max_attribution_reporting_origins", |
| config.rate_limit.max_attribution_reporting_origins, required); |
| ParseInt64(dict, "rate_limit_max_attributions", |
| config.rate_limit.max_attributions, required); |
| |
| ParseInt(dict, "max_event_level_reports_per_destination", |
| config.event_level_limit.max_reports_per_destination, required); |
| ParseInt(dict, "max_attributions_per_navigation_source", |
| config.event_level_limit.max_attributions_per_navigation_source, |
| required); |
| ParseInt(dict, "max_attributions_per_event_source", |
| config.event_level_limit.max_attributions_per_event_source, |
| required); |
| ParseUint64( |
| dict, "navigation_source_trigger_data_cardinality", |
| config.event_level_limit.navigation_source_trigger_data_cardinality, |
| required); |
| ParseUint64(dict, "event_source_trigger_data_cardinality", |
| config.event_level_limit.event_source_trigger_data_cardinality, |
| required); |
| ParseRandomizedResponseRate( |
| dict, "navigation_source_randomized_response_rate", |
| config.event_level_limit.navigation_source_randomized_response_rate, |
| required); |
| ParseRandomizedResponseRate( |
| dict, "event_source_randomized_response_rate", |
| config.event_level_limit.event_source_randomized_response_rate, required); |
| |
| ParseInt(dict, "max_aggregatable_reports_per_destination", |
| config.aggregate_limit.max_reports_per_destination, required); |
| ParseInt64(dict, "aggregatable_budget_per_source", |
| config.aggregate_limit.aggregatable_budget_per_source, required); |
| |
| int aggregatable_report_min_delay; |
| if (ParseInt(dict, "aggregatable_report_min_delay", |
| aggregatable_report_min_delay, required, |
| /*allow_zero=*/true)) { |
| config.aggregate_limit.min_delay = |
| base::Minutes(aggregatable_report_min_delay); |
| } |
| |
| int aggregatable_report_delay_span; |
| if (ParseInt(dict, "aggregatable_report_delay_span", |
| aggregatable_report_delay_span, required, |
| /*allow_zero=*/true)) { |
| config.aggregate_limit.delay_span = |
| base::Minutes(aggregatable_report_delay_span); |
| } |
| |
| return !has_error(); |
| } |
| |
| } // namespace content |