blob: 869c1dd2a9f3c86b0fc4aa122f06e8773e074740 [file] [log] [blame]
// 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/attribution_reporting/attribution_interop_parser.h"
#include <stddef.h>
#include <stdint.h>
#include <sstream>
#include <string>
#include <utility>
#include <vector>
#include "base/functional/function_ref.h"
#include "base/functional/overloaded.h"
#include "base/memory/raw_ref.h"
#include "base/ranges/algorithm.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_piece.h"
#include "base/time/time.h"
#include "base/types/expected.h"
#include "base/types/optional_util.h"
#include "base/values.h"
#include "components/attribution_reporting/source_registration.h"
#include "components/attribution_reporting/source_registration_error.mojom.h"
#include "components/attribution_reporting/source_type.mojom.h"
#include "components/attribution_reporting/suitable_origin.h"
#include "components/attribution_reporting/test_utils.h"
#include "components/attribution_reporting/trigger_registration.h"
#include "components/attribution_reporting/trigger_registration_error.mojom.h"
#include "content/browser/attribution_reporting/attribution_config.h"
#include "content/browser/attribution_reporting/attribution_trigger.h"
#include "content/browser/attribution_reporting/storable_source.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "third_party/abseil-cpp/absl/types/variant.h"
namespace content {
namespace {
using ::attribution_reporting::SuitableOrigin;
using ::attribution_reporting::mojom::SourceType;
constexpr char kAttributionSrcUrlKey[] = "attribution_src_url";
constexpr char kRegistrationRequestKey[] = "registration_request";
constexpr char kResponseKey[] = "response";
constexpr char kResponsesKey[] = "responses";
using Context = absl::variant<base::StringPiece, size_t>;
using ContextPath = std::vector<Context>;
class ScopedContext {
public:
ScopedContext(ContextPath& path, Context context) : path_(path) {
path_->push_back(context);
}
~ScopedContext() { path_->pop_back(); }
ScopedContext(const ScopedContext&) = delete;
ScopedContext(ScopedContext&&) = delete;
ScopedContext& operator=(const ScopedContext&) = delete;
ScopedContext& operator=(ScopedContext&&) = delete;
private:
const raw_ref<ContextPath> path_;
};
// Writes a newline on destruction.
class ErrorWriter {
public:
explicit ErrorWriter(std::ostringstream& stream) : stream_(stream) {}
~ErrorWriter() { *stream_ << std::endl; }
ErrorWriter(const ErrorWriter&) = delete;
ErrorWriter(ErrorWriter&&) = default;
ErrorWriter& operator=(const ErrorWriter&) = delete;
ErrorWriter& operator=(ErrorWriter&&) = delete;
std::ostringstream& operator*() { return *stream_; }
void operator()(base::StringPiece key) { *stream_ << "[\"" << key << "\"]"; }
void operator()(size_t index) { *stream_ << '[' << index << ']'; }
private:
const raw_ref<std::ostringstream> stream_;
};
class AttributionInteropParser {
public:
explicit AttributionInteropParser(base::Time offset_time = base::Time())
: offset_time_(offset_time) {}
~AttributionInteropParser() = default;
AttributionInteropParser(const AttributionInteropParser&) = delete;
AttributionInteropParser(AttributionInteropParser&&) = delete;
AttributionInteropParser& operator=(const AttributionInteropParser&) = delete;
AttributionInteropParser& operator=(AttributionInteropParser&&) = delete;
base::expected<AttributionSimulationEvents, std::string> ParseInput(
base::Value::Dict input) && {
static constexpr char kKeySources[] = "sources";
if (base::Value* sources = input.Find(kKeySources)) {
auto context = PushContext(kKeySources);
ParseListOfDicts(sources, [&](base::Value::Dict source) {
ParseSource(std::move(source));
});
}
static constexpr char kKeyTriggers[] = "triggers";
if (base::Value* triggers = input.Find(kKeyTriggers)) {
auto context = PushContext(kKeyTriggers);
ParseListOfDicts(triggers, [&](base::Value::Dict trigger) {
ParseTrigger(std::move(trigger));
});
}
if (has_error_) {
return base::unexpected(error_stream_.str());
}
base::ranges::sort(events_, /*comp=*/{}, &GetEventTime);
return std::move(events_);
}
[[nodiscard]] std::string ParseConfig(const base::Value::Dict& dict,
AttributionConfig& config,
bool required) && {
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);
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 error_stream_.str();
}
private:
const base::Time offset_time_;
std::ostringstream error_stream_;
ContextPath context_path_;
bool has_error_ = false;
std::vector<AttributionSimulationEvent> events_;
[[nodiscard]] ScopedContext PushContext(Context context) {
return ScopedContext(context_path_, context);
}
ErrorWriter Error() {
has_error_ = true;
if (context_path_.empty()) {
error_stream_ << "input root";
}
ErrorWriter writer(error_stream_);
for (Context context : context_path_) {
absl::visit(writer, context);
}
error_stream_ << ": ";
return writer;
}
void ParseListOfDicts(
base::Value* values,
base::FunctionRef<void(base::Value::Dict)> parse_element,
size_t expected_size = 0) {
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 : *list) {
auto index_context = PushContext(index);
if (!EnsureDictionary(&value)) {
return;
}
parse_element(std::move(value).TakeDict());
index++;
}
}
void VerifyReportingOrigin(const base::Value::Dict& dict,
const SuitableOrigin& reporting_origin) {
static constexpr char kUrlKey[] = "url";
absl::optional<SuitableOrigin> origin = ParseOrigin(dict, kUrlKey);
if (has_error_) {
return;
}
if (*origin != reporting_origin) {
auto context = PushContext(kUrlKey);
*Error() << "must match " << reporting_origin.Serialize();
}
}
void ParseSource(base::Value::Dict source_dict) {
base::Time source_time = ParseDistinctTime(source_dict);
absl::optional<SuitableOrigin> source_origin;
absl::optional<SuitableOrigin> reporting_origin;
absl::optional<SourceType> source_type;
ParseDict(source_dict, kRegistrationRequestKey,
[&](base::Value::Dict dict) {
source_origin = ParseOrigin(dict, "source_origin");
reporting_origin = ParseOrigin(dict, kAttributionSrcUrlKey);
source_type = ParseSourceType(dict);
});
if (has_error_) {
return;
}
auto context = PushContext(kResponsesKey);
ParseListOfDicts(
source_dict.Find(kResponsesKey),
[&](base::Value::Dict dict) {
VerifyReportingOrigin(dict, *reporting_origin);
bool debug_permission = ParseDebugPermission(dict);
if (has_error_) {
return;
}
ParseDict(dict, kResponseKey, [&](base::Value::Dict response_dict) {
ParseDict(
response_dict, "Attribution-Reporting-Register-Source",
[&](base::Value::Dict registration_dict) {
auto registration =
attribution_reporting::SourceRegistration::Parse(
std::move(registration_dict));
if (!registration.has_value()) {
*Error() << registration.error();
return;
}
events_.emplace_back(
StorableSource(std::move(*reporting_origin),
std::move(*registration), source_time,
std::move(*source_origin), *source_type,
/*is_within_fenced_frame=*/false),
debug_permission);
});
});
},
/*expected_size=*/1);
}
void ParseTrigger(base::Value::Dict trigger_dict) {
base::Time trigger_time = ParseDistinctTime(trigger_dict);
absl::optional<SuitableOrigin> destination_origin;
absl::optional<SuitableOrigin> reporting_origin;
ParseDict(trigger_dict, kRegistrationRequestKey,
[&](base::Value::Dict dict) {
destination_origin = ParseOrigin(dict, "destination_origin");
reporting_origin = ParseOrigin(dict, kAttributionSrcUrlKey);
});
if (has_error_) {
return;
}
auto context = PushContext(kResponsesKey);
ParseListOfDicts(
trigger_dict.Find(kResponsesKey),
[&](base::Value::Dict dict) {
VerifyReportingOrigin(dict, *reporting_origin);
bool debug_permission = ParseDebugPermission(dict);
if (has_error_) {
return;
}
ParseDict(dict, kResponseKey, [&](base::Value::Dict response_dict) {
ParseDict(response_dict, "Attribution-Reporting-Register-Trigger",
[&](base::Value::Dict registration_dict) {
auto trigger_registration =
attribution_reporting::TriggerRegistration::Parse(
std::move(registration_dict));
if (!trigger_registration.has_value()) {
*Error() << trigger_registration.error();
return;
}
events_.emplace_back(
AttributionTriggerAndTime{
.trigger = AttributionTrigger(
std::move(*reporting_origin),
std::move(*trigger_registration),
std::move(*destination_origin),
/*attestation=*/absl::nullopt,
/*is_within_fenced_frame=*/false),
.time = trigger_time,
},
debug_permission);
});
});
},
/*expected_size=*/1);
}
absl::optional<SuitableOrigin> ParseOrigin(const base::Value::Dict& dict,
base::StringPiece key) {
auto context = PushContext(key);
absl::optional<SuitableOrigin> origin;
if (const std::string* s = dict.FindString(key)) {
origin = SuitableOrigin::Deserialize(*s);
}
if (!origin.has_value()) {
*Error() << "must be a valid, secure origin";
}
return origin;
}
base::Time ParseDistinctTime(const base::Value::Dict& dict) {
static constexpr char kTimestampKey[] = "timestamp";
auto context = PushContext(kTimestampKey);
const std::string* v = dict.FindString(kTimestampKey);
int64_t milliseconds;
if (v && base::StringToInt64(*v, &milliseconds)) {
base::Time time = offset_time_ + base::Milliseconds(milliseconds);
if (!time.is_null() && !time.is_inf()) {
if (base::ranges::find(events_, time, &GetEventTime) != events_.end()) {
*Error() << "must be distinct from all others: " << milliseconds;
}
return time;
}
}
*Error() << "must be an integer number of milliseconds since the Unix "
"epoch formatted as a base-10 string";
return base::Time();
}
absl::optional<bool> ParseBool(const base::Value::Dict& dict,
base::StringPiece key) {
auto context = PushContext(key);
const base::Value* v = dict.Find(key);
if (!v) {
return absl::nullopt;
}
if (!v->is_bool()) {
*Error() << "must be a bool";
return absl::nullopt;
}
return v->GetBool();
}
bool ParseDebugPermission(const base::Value::Dict& dict) {
return ParseBool(dict, "debug_permission").value_or(false);
}
absl::optional<SourceType> ParseSourceType(const base::Value::Dict& dict) {
static constexpr char kKey[] = "source_type";
static constexpr char kNavigation[] = "navigation";
static constexpr char kEvent[] = "event";
auto context = PushContext(kKey);
absl::optional<SourceType> source_type;
if (const std::string* v = dict.FindString(kKey)) {
if (*v == kNavigation) {
source_type = SourceType::kNavigation;
} else if (*v == kEvent) {
source_type = SourceType::kEvent;
}
}
if (!source_type) {
*Error() << "must be either \"" << kNavigation << "\" or \"" << kEvent
<< "\"";
}
return source_type;
}
bool ParseDict(base::Value::Dict& value,
base::StringPiece key,
base::FunctionRef<void(base::Value::Dict)> parse_dict) {
auto context = PushContext(key);
base::Value* dict = value.Find(key);
if (!EnsureDictionary(dict)) {
return false;
}
parse_dict(std::move(*dict).TakeDict());
return true;
}
bool 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;
}
// Returns true if `key` is present in `dict` and the integer is parsed
// successfully.
template <typename T>
bool ParseInteger(const base::Value::Dict& dict,
base::StringPiece key,
T& result,
bool (*convert_func)(base::StringPiece, T*),
bool required,
bool allow_zero) {
auto context = PushContext(key);
const base::Value* value = dict.Find(key);
if (value) {
const std::string* s = value->GetIfString();
if (s && convert_func(*s, &result) &&
(result > 0 || (result == 0 && allow_zero))) {
return true;
}
} else if (!required) {
return false;
}
if (allow_zero) {
*Error() << "must be a non-negative integer formatted as base-10 string";
} else {
*Error() << "must be a positive integer formatted as base-10 string";
}
return false;
}
bool ParseInt(const base::Value::Dict& dict,
base::StringPiece key,
int& result,
bool required,
bool allow_zero = false) {
return ParseInteger(dict, key, result, &base::StringToInt, required,
allow_zero);
}
bool ParseUint64(const base::Value::Dict& dict,
base::StringPiece key,
uint64_t& result,
bool required,
bool allow_zero = false) {
return ParseInteger(dict, key, result, &base::StringToUint64, required,
allow_zero);
}
bool ParseInt64(const base::Value::Dict& dict,
base::StringPiece key,
int64_t& result,
bool required,
bool allow_zero = false) {
return ParseInteger(dict, key, result, &base::StringToInt64, required,
allow_zero);
}
void 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";
}
};
} // namespace
AttributionSimulationEvent::AttributionSimulationEvent(
absl::variant<StorableSource, AttributionTriggerAndTime> event,
bool debug_permission)
: event(std::move(event)), debug_permission(debug_permission) {}
AttributionSimulationEvent::~AttributionSimulationEvent() = default;
AttributionSimulationEvent::AttributionSimulationEvent(
AttributionSimulationEvent&&) = default;
AttributionSimulationEvent& AttributionSimulationEvent::operator=(
AttributionSimulationEvent&&) = default;
base::expected<AttributionSimulationEvents, std::string>
ParseAttributionInteropInput(base::Value::Dict input,
const base::Time offset_time) {
return AttributionInteropParser(offset_time).ParseInput(std::move(input));
}
base::expected<AttributionConfig, std::string> ParseAttributionConfig(
const base::Value::Dict& dict) {
AttributionConfig config;
std::string error =
AttributionInteropParser().ParseConfig(dict, config, /*required=*/true);
if (!error.empty()) {
return base::unexpected(std::move(error));
}
return config;
}
std::string MergeAttributionConfig(const base::Value::Dict& dict,
AttributionConfig& config) {
return AttributionInteropParser().ParseConfig(dict, config,
/*required=*/false);
}
base::Time GetEventTime(const AttributionSimulationEvent& event) {
return absl::visit(
base::Overloaded{
[](const StorableSource& source) {
return source.common_info().source_time();
},
[](const AttributionTriggerAndTime& trigger) { return trigger.time; },
},
event.event);
}
} // namespace content