blob: d20e77b3aeab4c60b629ac4fa5920fc98b115e01 [file] [log] [blame]
// Copyright 2020 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_host.h"
#include <utility>
#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/check.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "build/build_config.h"
#include "content/browser/attribution_reporting/attribution_host_utils.h"
#include "content/browser/attribution_reporting/attribution_manager.h"
#include "content/browser/attribution_reporting/attribution_manager_impl.h"
#include "content/browser/attribution_reporting/attribution_page_metrics.h"
#include "content/browser/attribution_reporting/attribution_policy.h"
#include "content/browser/attribution_reporting/storable_trigger.h"
#include "content/browser/devtools/devtools_instrumentation.h"
#include "content/browser/renderer_host/frame_tree.h"
#include "content/browser/renderer_host/frame_tree_node.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/storage_partition_impl.h"
#include "content/common/url_utils.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_client.h"
#include "content/public/common/url_constants.h"
#include "mojo/public/cpp/bindings/message.h"
#include "net/base/schemeful_site.h"
#include "services/network/public/cpp/is_potentially_trustworthy.h"
#include "third_party/blink/public/mojom/devtools/console_message.mojom.h"
#include "url/gurl.h"
#include "url/origin.h"
namespace content {
namespace {
AttributionHost* g_receiver_for_testing = nullptr;
// Abstraction that wraps an iterator to a map. When this goes out of the scope,
// the underlying iterator is erased from the map. This is useful for control
// flows where map cleanup needs to occur regardless of additional early exit
// logic.
template <typename Map>
class ScopedMapDeleter {
public:
ScopedMapDeleter(Map* map, const typename Map::key_type& key)
: map_(map), it_(map_->find(key)) {}
~ScopedMapDeleter() {
if (*this)
map_->erase(it_);
}
typename Map::iterator* get() { return &it_; }
explicit operator bool() const { return it_ != map_->end(); }
private:
raw_ptr<Map> map_;
typename Map::iterator it_;
};
void RecordRegisterConversionAllowed(bool allowed) {
base::UmaHistogramBoolean("Conversions.RegisterConversionAllowed", allowed);
}
void RecordRegisterImpressionAllowed(bool allowed) {
base::UmaHistogramBoolean("Conversions.RegisterImpressionAllowed", allowed);
}
} // namespace
AttributionHost::AttributionHost(WebContents* web_contents)
: AttributionHost(web_contents,
std::make_unique<AttributionManagerProviderImpl>()) {}
AttributionHost::AttributionHost(
WebContents* web_contents,
std::unique_ptr<AttributionManager::Provider> attribution_manager_provider)
: WebContentsObserver(web_contents),
WebContentsUserData<AttributionHost>(*web_contents),
attribution_manager_provider_(std::move(attribution_manager_provider)),
receivers_(web_contents, this) {
// TODO(csharrison): When https://crbug.com/1051334 is resolved, add a DCHECK
// that the kConversionMeasurement feature is enabled.
}
AttributionHost::~AttributionHost() {
DCHECK_EQ(0u, navigation_impression_origins_.size());
}
void AttributionHost::DidStartNavigation(NavigationHandle* navigation_handle) {
// Impression navigations need to navigate the primary main frame to be valid.
if (!navigation_handle->GetImpression() ||
!navigation_handle->IsInPrimaryMainFrame() ||
!attribution_manager_provider_->GetManager(web_contents())) {
return;
}
// There's no initiator frame for App-initiated origins, and so no work is
// required at navigation start time.
if (IsAndroidAppOrigin(navigation_handle->GetInitiatorOrigin()))
return;
RenderFrameHostImpl* initiator_frame_host =
navigation_handle->GetInitiatorFrameToken().has_value()
? RenderFrameHostImpl::FromFrameToken(
navigation_handle->GetInitiatorProcessID(),
navigation_handle->GetInitiatorFrameToken().value())
: nullptr;
// The initiator frame host may be deleted by this point. In that case, ignore
// this navigation and drop the impression associated with it.
UMA_HISTOGRAM_BOOLEAN("Conversions.ImpressionNavigationHasDeadInitiator",
initiator_frame_host == nullptr);
if (!initiator_frame_host)
return;
// Look up the initiator root's origin which will be used as the impression
// origin. This works because we won't update the origin for the initiator RFH
// until we receive confirmation from the renderer that it has committed.
// Since frame mutation is all serialized on the Blink main thread, we get an
// implicit ordering: a navigation with an impression attached won't be
// processed after a navigation commit in the initiator RFH, so reading the
// origin off is safe at the start of the navigation.
const url::Origin& initiator_root_frame_origin =
initiator_frame_host->frame_tree_node()
->frame_tree()
->root()
->current_origin();
navigation_impression_origins_.emplace(navigation_handle->GetNavigationId(),
initiator_root_frame_origin);
if (auto* initiator_web_contents =
WebContents::FromRenderFrameHost(initiator_frame_host)) {
if (auto* initiator_conversion_host =
AttributionHost::FromWebContents(initiator_web_contents)) {
// This doesn't necessarily mean that the browser will store the report,
// due to the additional logic in DidFinishNavigation(). This records
// that a page /attempted/ to register an impression for a navigation.
initiator_conversion_host->NotifyImpressionInitiatedByPage(
initiator_root_frame_origin, *(navigation_handle->GetImpression()));
}
}
}
void AttributionHost::DidFinishNavigation(NavigationHandle* navigation_handle) {
// Observe only navigation toward a new document in the primary main frame.
// Impressions should never be attached to same-document navigations but can
// be the result of a bad renderer.
if (!navigation_handle->IsInPrimaryMainFrame() ||
navigation_handle->IsSameDocument()) {
return;
}
AttributionManager* attribution_manager =
attribution_manager_provider_->GetManager(web_contents());
if (!attribution_manager) {
DCHECK(navigation_impression_origins_.empty());
DCHECK(!pending_attribution_);
if (navigation_handle->GetImpression())
RecordRegisterImpressionAllowed(false);
return;
}
ScopedMapDeleter<NavigationImpressionOriginMap>
navigation_impression_origin_it(&navigation_impression_origins_,
navigation_handle->GetNavigationId());
absl::optional<PendingAttribution> pending_attribution =
std::move(pending_attribution_);
pending_attribution_ = absl::nullopt;
// Separate from above because we need to clear the navigation related state
if (!navigation_handle->HasCommitted())
return;
// Don't observe error page navs, and don't let impressions be registered for
// error pages.
if (navigation_handle->IsErrorPage()) {
last_navigation_allows_attribution_ = false;
return;
}
// We have a new cross-document navigation.
last_navigation_allows_attribution_ = true;
conversion_page_metrics_ = std::make_unique<AttributionPageMetrics>();
bool is_android_app_origin =
IsAndroidAppOrigin(navigation_handle->GetInitiatorOrigin()) ||
(pending_attribution &&
IsAndroidAppOrigin(pending_attribution->initiator_origin));
// If we were not able to access the impression origin, ignore the
// navigation.
if (!navigation_impression_origin_it && !pending_attribution &&
!is_android_app_origin) {
return;
}
const url::Origin& impression_origin =
pending_attribution
? pending_attribution->initiator_origin
: (is_android_app_origin
? *navigation_handle->GetInitiatorOrigin()
: (*navigation_impression_origin_it.get())->second);
DCHECK(navigation_handle->GetImpression() || pending_attribution);
const blink::Impression& impression =
pending_attribution ? pending_attribution->impression
: *(navigation_handle->GetImpression());
// If the impression's conversion destination does not match the final top
// frame origin of this new navigation ignore it.
if (net::SchemefulSite(impression.conversion_destination) !=
net::SchemefulSite(
navigation_handle->GetRenderFrameHost()->GetLastCommittedOrigin())) {
return;
}
VerifyAndStoreImpression(StorableSource::SourceType::kNavigation,
impression_origin, impression, *attribution_manager);
}
bool AttributionHost::VerifyAndStoreImpression(
StorableSource::SourceType source_type,
const url::Origin& impression_origin,
const blink::Impression& impression,
AttributionManager& attribution_manager) {
attribution_host_utils::VerifyResult result =
attribution_host_utils::VerifyAndStoreImpression(
source_type, impression_origin, impression,
web_contents()->GetBrowserContext(), attribution_manager,
base::Time::Now());
RecordRegisterImpressionAllowed(result.allowed);
return result.stored;
}
void AttributionHost::RegisterConversion(
blink::mojom::ConversionPtr conversion) {
content::RenderFrameHost* render_frame_host =
receivers_.GetCurrentTargetFrame();
// AttributionHost calls are delayed in blink if pre-rendering.
DCHECK_NE(content::RenderFrameHost::LifecycleState::kPrerendering,
render_frame_host->GetLifecycleState());
// If there is no conversion manager available, ignore any conversion
// registrations.
AttributionManager* attribution_manager =
attribution_manager_provider_->GetManager(web_contents());
if (!attribution_manager) {
RecordRegisterConversionAllowed(false);
return;
}
const url::Origin& conversion_origin =
render_frame_host->GetLastCommittedOrigin();
const url::Origin& main_frame_origin =
render_frame_host->GetMainFrame()->GetLastCommittedOrigin();
// Only allow conversion registration on secure pages with a secure conversion
// redirects.
if (!network::IsOriginPotentiallyTrustworthy(conversion_origin) ||
!network::IsOriginPotentiallyTrustworthy(conversion->reporting_origin) ||
!network::IsOriginPotentiallyTrustworthy(main_frame_origin)) {
mojo::ReportBadMessage(
"blink.mojom.ConversionHost can only be used in secure contexts with a "
"secure conversion registration origin.");
return;
}
const bool allowed =
GetContentClient()->browser()->IsConversionMeasurementOperationAllowed(
web_contents()->GetBrowserContext(),
ContentBrowserClient::ConversionMeasurementOperation::kConversion,
/*impression_origin=*/nullptr, &main_frame_origin,
&conversion->reporting_origin);
RecordRegisterConversionAllowed(allowed);
if (!allowed)
return;
net::SchemefulSite conversion_destination(main_frame_origin);
if (!attribution_manager->GetAttributionPolicy().IsTriggerDataInRange(
conversion->conversion_data,
StorableSource::SourceType::kNavigation)) {
devtools_instrumentation::ReportAttributionReportingIssue(
render_frame_host,
devtools_instrumentation::AttributionReportingIssueType::
kAttributionTriggerDataTooLarge,
conversion->devtools_request_id,
base::NumberToString(conversion->conversion_data));
}
if (!attribution_manager->GetAttributionPolicy().IsTriggerDataInRange(
conversion->event_source_trigger_data,
StorableSource::SourceType::kEvent)) {
devtools_instrumentation::ReportAttributionReportingIssue(
render_frame_host,
devtools_instrumentation::AttributionReportingIssueType::
kAttributionEventSourceTriggerDataTooLarge,
conversion->devtools_request_id,
base::NumberToString(conversion->event_source_trigger_data));
}
StorableTrigger storable_conversion(
attribution_manager->GetAttributionPolicy().SanitizeTriggerData(
conversion->conversion_data, StorableSource::SourceType::kNavigation),
conversion_destination, conversion->reporting_origin,
attribution_manager->GetAttributionPolicy().SanitizeTriggerData(
conversion->event_source_trigger_data,
StorableSource::SourceType::kEvent),
conversion->priority,
conversion->dedup_key.is_null()
? absl::nullopt
: absl::make_optional(conversion->dedup_key->value));
if (conversion_page_metrics_)
conversion_page_metrics_->OnConversion(conversion->reporting_origin);
attribution_manager->HandleTrigger(std::move(storable_conversion));
}
void AttributionHost::NotifyImpressionInitiatedByPage(
const url::Origin& impression_origin,
const blink::Impression& impression) {
if (!conversion_page_metrics_)
return;
const url::Origin& reporting_origin = !impression.reporting_origin
? impression_origin
: *impression.reporting_origin;
conversion_page_metrics_->OnImpression(reporting_origin);
}
void AttributionHost::RegisterImpression(const blink::Impression& impression) {
// If there is no conversion manager available, ignore any impression
// registrations.
AttributionManager* attribution_manager =
attribution_manager_provider_->GetManager(web_contents());
if (!attribution_manager)
return;
content::RenderFrameHost* render_frame_host =
receivers_.GetCurrentTargetFrame()->GetMainFrame();
// AttributionHost calls are delayed in blink if pre-rendering.
DCHECK_NE(content::RenderFrameHost::LifecycleState::kPrerendering,
render_frame_host->GetLifecycleState());
const url::Origin& impression_origin =
render_frame_host->GetLastCommittedOrigin();
if (VerifyAndStoreImpression(StorableSource::SourceType::kEvent,
impression_origin, impression,
*attribution_manager)) {
NotifyImpressionInitiatedByPage(impression_origin, impression);
}
}
void AttributionHost::ReportAttributionForCurrentNavigation(
const url::Origin& impression_origin,
const blink::Impression& impression) {
AttributionManager* attribution_manager =
attribution_manager_provider_->GetManager(web_contents());
if (!attribution_manager)
return;
// If a navigation is ongoing, add the attribution to that navigation.
if (web_contents()->GetController().GetPendingEntry()) {
pending_attribution_ = {impression_origin, impression};
return;
}
// The navigation has already committed, so add the attribution to the last
// committed navigation.
if (!last_navigation_allows_attribution_)
return;
// Prevent multiple attributions using the same navigation.
last_navigation_allows_attribution_ = false;
// Ensure the committed origin matches the destination for the conversion,
// but allow subdomains to differ.
if (net::SchemefulSite(
web_contents()->GetMainFrame()->GetLastCommittedOrigin()) !=
net::SchemefulSite(impression.conversion_destination)) {
return;
}
// No navigation in progress and we've already committed the destination for
// the conversion, so just store the impression.
VerifyAndStoreImpression(StorableSource::SourceType::kNavigation,
impression_origin, impression, *attribution_manager);
}
// static
void AttributionHost::BindReceiver(
mojo::PendingAssociatedReceiver<blink::mojom::ConversionHost> receiver,
RenderFrameHost* rfh) {
if (g_receiver_for_testing) {
g_receiver_for_testing->receivers_.Bind(rfh, std::move(receiver));
return;
}
auto* web_contents = WebContents::FromRenderFrameHost(rfh);
if (!web_contents)
return;
auto* conversion_host = AttributionHost::FromWebContents(web_contents);
if (!conversion_host)
return;
conversion_host->receivers_.Bind(rfh, std::move(receiver));
}
// static
blink::mojom::ImpressionPtr AttributionHost::MojoImpressionFromImpression(
const blink::Impression& impression) {
return blink::mojom::Impression::New(
impression.conversion_destination, impression.reporting_origin,
impression.impression_data, impression.expiry, impression.priority);
}
// static
void AttributionHost::SetReceiverImplForTesting(AttributionHost* impl) {
g_receiver_for_testing = impl;
}
WEB_CONTENTS_USER_DATA_KEY_IMPL(AttributionHost);
} // namespace content