blob: e0c7e94f8fbf0a880eae8412998e25d588216ec6 [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_runner.h"
#include <stddef.h>
#include <stdint.h>
#include <limits>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "base/check.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/functional/overloaded.h"
#include "base/json/json_reader.h"
#include "base/memory/raw_ref.h"
#include "base/notreached.h"
#include "base/numerics/safe_conversions.h"
#include "base/ranges/algorithm.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/task/updateable_sequenced_task_runner.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "base/types/expected.h"
#include "base/types/expected_macros.h"
#include "base/values.h"
#include "components/aggregation_service/features.h"
#include "components/attribution_reporting/parsing_utils.h"
#include "components/attribution_reporting/registration_eligibility.mojom.h"
#include "components/attribution_reporting/source_registration.h"
#include "components/attribution_reporting/source_type.mojom.h"
#include "components/attribution_reporting/trigger_registration.h"
#include "content/browser/aggregation_service/aggregatable_report.h"
#include "content/browser/aggregation_service/aggregation_service_features.h"
#include "content/browser/aggregation_service/aggregation_service_impl.h"
#include "content/browser/aggregation_service/aggregation_service_test_utils.h"
#include "content/browser/attribution_reporting/aggregatable_attribution_utils.h"
#include "content/browser/attribution_reporting/attribution_cookie_checker.h"
#include "content/browser/attribution_reporting/attribution_data_host_manager.h"
#include "content/browser/attribution_reporting/attribution_debug_report.h"
#include "content/browser/attribution_reporting/attribution_input_event.h"
#include "content/browser/attribution_reporting/attribution_interop_parser.h"
#include "content/browser/attribution_reporting/attribution_manager_impl.h"
#include "content/browser/attribution_reporting/attribution_observer.h"
#include "content/browser/attribution_reporting/attribution_os_level_manager.h"
#include "content/browser/attribution_reporting/attribution_report.h"
#include "content/browser/attribution_reporting/attribution_report_sender.h"
#include "content/browser/attribution_reporting/attribution_reporting.mojom.h"
#include "content/browser/attribution_reporting/attribution_storage_delegate_impl.h"
#include "content/browser/attribution_reporting/attribution_trigger.h"
#include "content/browser/attribution_reporting/create_report_result.h"
#include "content/browser/attribution_reporting/send_result.h"
#include "content/browser/storage_partition_impl.h"
#include "content/public/browser/global_routing_id.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/test_browser_context.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "third_party/abseil-cpp/absl/types/variant.h"
#include "third_party/blink/public/mojom/conversions/attribution_data_host.mojom.h"
#include "url/gurl.h"
#include "url/origin.h"
namespace content {
namespace {
using ::attribution_reporting::mojom::RegistrationType;
constexpr int64_t kNavigationId(-1);
base::TimeDelta TimeOffset(base::Time time_origin) {
return time_origin - base::Time::UnixEpoch();
}
class AttributionReportConverter {
public:
explicit AttributionReportConverter(base::Time time_origin)
: time_origin_(time_origin) {}
AttributionInteropOutput::Report ToOutput(const AttributionReport& report,
bool is_debug_report) const {
base::Value::Dict report_body = report.ReportBody();
absl::visit(
base::Overloaded{
[&](const AttributionReport::AggregatableAttributionData&
aggregatable_data) {
// These fields normally encode a random GUID or the absolute time
// and therefore are sources of nondeterminism in the output.
// Output attribution_destination from the shared_info field.
if (absl::optional<base::Value> shared_info =
report_body.Extract("shared_info");
shared_info.has_value() && shared_info->is_string()) {
if (absl::optional<base::Value> shared_info_value =
base::JSONReader::Read(shared_info->GetString(),
base::JSON_PARSE_RFC)) {
static constexpr char kKeyAttributionDestination[] =
"attribution_destination";
if (absl::optional<base::Value> attribution_destination =
shared_info_value->GetDict().Extract(
kKeyAttributionDestination)) {
report_body.Set(kKeyAttributionDestination,
std::move(*attribution_destination));
}
}
}
report_body.Remove("aggregation_service_payloads");
// The aggregation coordinator may be platform specific.
report_body.Remove("aggregation_coordinator_origin");
base::Value::List list;
for (const auto& contribution : aggregatable_data.contributions) {
list.Append(
base::Value::Dict()
.Set("key",
attribution_reporting::HexEncodeAggregationKey(
contribution.key()))
.Set("value",
base::checked_cast<int>(contribution.value())));
}
report_body.Set("histograms", std::move(list));
},
[&](const AttributionReport::EventLevelData&) {
// Report IDs are a source of nondeterminism, so remove
// them.
report_body.Remove("report_id");
AdjustScheduledReportTime(report_body,
report.initial_report_time());
},
[](const AttributionReport::NullAggregatableData&) {
NOTREACHED_NORETURN();
},
},
report.data());
return MakeReport(base::Value(std::move(report_body)),
report.ReportURL(is_debug_report));
}
AttributionInteropOutput::Report ToOutput(
const AttributionDebugReport& report) const {
base::Value::List report_body = report.ReportBody().Clone();
for (auto& value : report_body) {
if (base::Value::Dict* dict = value.GetIfDict()) {
if (base::Value::Dict* body = dict->FindDict("body")) {
// Report IDs are a source of nondeterminism, so remove them.
body->Remove("report_id");
AdjustScheduledReportTime(*body,
report.GetOriginalReportTimeForTesting());
}
}
}
return MakeReport(base::Value(std::move(report_body)), report.ReportUrl());
}
private:
void AdjustScheduledReportTime(base::Value::Dict& report_body,
base::Time original_report_time) const {
// This field contains a string encoding seconds from the UNIX epoch. It
// needs to be adjusted relative to the simulator's origin time in order
// for test output to be consistent.
std::string* str = report_body.FindString("scheduled_report_time");
if (str) {
*str = base::NumberToString(
(original_report_time - time_origin_).InSeconds());
}
}
AttributionInteropOutput::Report MakeReport(base::Value payload,
const GURL& report_url) const {
return AttributionInteropOutput::Report(
base::Time::Now() - TimeOffset(time_origin_), report_url,
std::move(payload));
}
const base::Time time_origin_;
};
class FakeReportSender : public AttributionReportSender {
public:
FakeReportSender(std::vector<AttributionInteropOutput::Report>* reports,
base::Time time_origin)
: reports_(
raw_ref<std::vector<AttributionInteropOutput::Report>>::from_ptr(
reports)),
converter_(time_origin) {}
~FakeReportSender() override = default;
FakeReportSender(const FakeReportSender&) = delete;
FakeReportSender(FakeReportSender&&) = delete;
FakeReportSender& operator=(const FakeReportSender&) = delete;
FakeReportSender& operator=(FakeReportSender&&) = delete;
private:
// AttributionManagerImpl::ReportSender:
void SendReport(AttributionReport report,
bool is_debug_report,
ReportSentCallback sent_callback) override {
reports_->emplace_back(converter_.ToOutput(report, is_debug_report));
std::move(sent_callback)
.Run(std::move(report), SendResult(SendResult::Status::kSent,
/*http_response_code=*/200));
}
void SendReport(AttributionDebugReport report,
DebugReportSentCallback done) override {
reports_->emplace_back(converter_.ToOutput(report));
std::move(done).Run(std::move(report), /*status=*/200);
}
raw_ref<std::vector<AttributionInteropOutput::Report>> reports_;
const AttributionReportConverter converter_;
};
class FakeCookieChecker : public AttributionCookieChecker {
public:
FakeCookieChecker() = default;
~FakeCookieChecker() override = default;
FakeCookieChecker(const FakeCookieChecker&) = delete;
FakeCookieChecker(FakeCookieChecker&&) = delete;
FakeCookieChecker& operator=(const FakeCookieChecker&) = delete;
FakeCookieChecker& operator=(FakeCookieChecker&&) = delete;
void set_debug_cookie_set(bool set) { debug_cookie_set_ = set; }
private:
// AttributionCookieChecker:
void IsDebugCookieSet(const url::Origin& origin,
base::OnceCallback<void(bool)> callback) override {
std::move(callback).Run(debug_cookie_set_);
}
bool debug_cookie_set_ = false;
};
// Registers sources and triggers in the `AttributionManagerImpl` and records
// sent reports.
class AttributionEventHandler : public AttributionObserver {
public:
AttributionEventHandler(std::unique_ptr<AttributionManagerImpl> manager,
FakeCookieChecker* fake_cookie_checker,
base::Time time_origin)
: manager_(std::move(manager)),
fake_cookie_checker_(
raw_ref<FakeCookieChecker>::from_ptr(fake_cookie_checker)),
time_offset_(TimeOffset(time_origin)) {
DCHECK(manager_);
manager_->AddObserver(this);
}
~AttributionEventHandler() override { manager_->RemoveObserver(this); }
void Handle(AttributionSimulationEvent event) {
fake_cookie_checker_->set_debug_cookie_set(event.debug_permission);
base::Value::Dict* dict = event.registration.GetIfDict();
if (!dict) {
AddUnparsableRegistration(event);
return;
}
if (event.source_type.has_value()) {
auto registration = attribution_reporting::SourceRegistration::Parse(
std::move(*dict), *event.source_type);
if (!registration.has_value()) {
AddUnparsableRegistration(event);
return;
}
auto* attribution_data_host_manager = manager_->GetDataHostManager();
mojo::Remote<blink::mojom::AttributionDataHost> data_host_remote;
switch (*event.source_type) {
case attribution_reporting::mojom::SourceType::kNavigation: {
const blink::AttributionSrcToken attribution_src_token;
attribution_data_host_manager->RegisterNavigationDataHost(
data_host_remote.BindNewPipeAndPassReceiver(),
attribution_src_token);
attribution_data_host_manager->NotifyNavigationRegistrationStarted(
attribution_src_token, AttributionInputEvent(),
event.context_origin,
/*is_within_fenced_frame=*/false, GlobalRenderFrameHostId(),
/*navigation_id=*/kNavigationId, /*devtools_request_id=*/"");
attribution_data_host_manager->NotifyNavigationRegistrationCompleted(
attribution_src_token);
break;
}
case attribution_reporting::mojom::SourceType::kEvent:
attribution_data_host_manager->RegisterDataHost(
data_host_remote.BindNewPipeAndPassReceiver(),
std::move(event.context_origin),
/*is_within_fenced_frame=*/false,
attribution_reporting::mojom::RegistrationEligibility::
kSourceOrTrigger,
GlobalRenderFrameHostId(),
/*last_navigation_id=*/kNavigationId);
break;
}
data_host_remote->SourceDataAvailable(std::move(event.reporting_origin),
std::move(*registration));
data_host_remote.FlushForTesting(); // IN-TEST
return;
}
auto registration =
attribution_reporting::TriggerRegistration::Parse(std::move(*dict));
if (!registration.has_value()) {
AddUnparsableRegistration(event);
return;
}
mojo::Remote<blink::mojom::AttributionDataHost> data_host_remote;
manager_->GetDataHostManager()->RegisterDataHost(
data_host_remote.BindNewPipeAndPassReceiver(),
std::move(event.context_origin),
/*is_within_fenced_frame=*/false,
attribution_reporting::mojom::RegistrationEligibility::kSourceOrTrigger,
GlobalRenderFrameHostId(),
/*last_navigation_id=*/kNavigationId);
data_host_remote->TriggerDataAvailable(std::move(event.reporting_origin),
std::move(*registration),
/*verifications=*/{});
data_host_remote.FlushForTesting(); // IN-TEST
}
std::vector<AttributionInteropOutput::UnparsableRegistration>
TakeUnparsable() && {
return std::move(unparsable_);
}
base::Time max_report_time() const { return max_report_time_; }
private:
// AttributionObserver:
void OnTriggerHandled(const AttributionTrigger&,
absl::optional<uint64_t> cleared_debug_key,
const CreateReportResult& result) override {
if (const auto& report = result.new_event_level_report()) {
max_report_time_ = std::max(max_report_time_, report->report_time());
}
if (const auto& report = result.new_aggregatable_report()) {
max_report_time_ = std::max(max_report_time_, report->report_time());
}
}
void AddUnparsableRegistration(const AttributionSimulationEvent& event) {
auto& registration = unparsable_.emplace_back();
registration.time = event.time - time_offset_;
registration.type = event.source_type.has_value()
? RegistrationType::kSource
: RegistrationType::kTrigger;
}
const std::unique_ptr<AttributionManagerImpl> manager_;
const raw_ref<FakeCookieChecker> fake_cookie_checker_;
const base::TimeDelta time_offset_;
base::Time max_report_time_;
std::vector<AttributionInteropOutput::UnparsableRegistration> unparsable_;
};
} // namespace
base::expected<AttributionInteropOutput, std::string>
RunAttributionInteropSimulation(base::Value::Dict input,
const AttributionConfig& config) {
// Prerequisites for using an environment with mock time.
content::BrowserTaskEnvironment task_environment(
base::test::TaskEnvironment::TimeSource::MOCK_TIME);
TestBrowserContext browser_context;
const base::Time time_origin = base::Time::Now();
ASSIGN_OR_RETURN(AttributionSimulationEvents events,
ParseAttributionInteropInput(std::move(input), time_origin));
if (events.empty()) {
return AttributionInteropOutput();
}
DCHECK(base::ranges::is_sorted(events));
DCHECK(base::ranges::adjacent_find(
events, /*pred=*/{},
[](const auto& event) { return event.time; }) == events.end());
const base::Time min_event_time = events.front().time;
const base::Time max_event_time = events.back().time;
task_environment.FastForwardBy(min_event_time - time_origin);
auto* storage_partition = static_cast<StoragePartitionImpl*>(
browser_context.GetDefaultStoragePartition());
auto fake_cookie_checker = std::make_unique<FakeCookieChecker>();
auto* raw_fake_cookie_checker = fake_cookie_checker.get();
AttributionInteropOutput output;
auto manager = AttributionManagerImpl::CreateForTesting(
// Avoid creating an on-disk sqlite DB.
/*user_data_directory=*/base::FilePath(),
/*max_pending_events=*/std::numeric_limits<size_t>::max(),
/*special_storage_policy=*/nullptr,
AttributionStorageDelegateImpl::CreateForTesting(
AttributionNoiseMode::kNone, AttributionDelayMode::kDefault, config),
std::move(fake_cookie_checker),
std::make_unique<FakeReportSender>(&output.reports, time_origin),
std::make_unique<NoOpAttributionOsLevelManager>(), storage_partition,
base::ThreadPool::CreateUpdateableSequencedTaskRunner(
{base::TaskPriority::BEST_EFFORT, base::MayBlock(),
base::TaskShutdownBehavior::BLOCK_SHUTDOWN,
base::ThreadPolicy::MUST_USE_FOREGROUND}));
AttributionEventHandler handler(std::move(manager), raw_fake_cookie_checker,
time_origin);
static_cast<AggregationServiceImpl*>(
storage_partition->GetAggregationService())
->SetPublicKeysForTesting(
GetAggregationServiceProcessingUrl(url::Origin::Create(
GURL(::aggregation_service::kAggregationServiceCoordinatorAwsCloud
.Get()))),
PublicKeyset({aggregation_service::GenerateKey().public_key},
/*fetch_time=*/base::Time::Now(),
/*expiry_time=*/base::Time::Max()));
for (auto& event : events) {
base::Time event_time = event.time;
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&AttributionEventHandler::Handle,
base::Unretained(&handler), std::move(event)),
event_time - base::Time::Now());
}
task_environment.FastForwardBy(max_event_time - base::Time::Now());
if (base::Time max_report_time = handler.max_report_time(),
now = base::Time::Now();
max_report_time >= now) {
task_environment.FastForwardBy(max_report_time - now);
}
output.unparsable_registrations = std::move(handler).TakeUnparsable();
return output;
}
} // namespace content