blob: 7c87ce969c500eea6b36d02d0317b29deb7d09af [file] [log] [blame]
// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/subresource_filter/content/browser/child_frame_navigation_filtering_throttle.h"
#include <optional>
#include <sstream>
#include <utility>
#include "base/check_op.h"
#include "base/debug/alias.h"
#include "base/debug/dump_without_crashing.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/stringprintf.h"
#include "components/subresource_filter/content/browser/ad_tagging_utils.h"
#include "components/subresource_filter/content/browser/subresource_filter_observer_manager.h"
#include "components/subresource_filter/content/shared/common/subresource_filter_utils.h"
#include "components/subresource_filter/core/browser/subresource_filter_constants.h"
#include "components/subresource_filter/core/common/common_features.h"
#include "components/subresource_filter/core/common/time_measurements.h"
#include "content/public/browser/global_routing_id.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "third_party/blink/public/mojom/devtools/console_message.mojom.h"
namespace features {
// Enables or disables performing SubresourceFilter checks from the Browser
// against any aliases for the requested URL found from DNS CNAME records.
BASE_FEATURE(kSendCnameAliasesToSubresourceFilterFromBrowser,
"SendCnameAliasesToSubresourceFilterFromBrowser",
base::FEATURE_DISABLED_BY_DEFAULT);
} // namespace features
namespace subresource_filter {
ChildFrameNavigationFilteringThrottle::ChildFrameNavigationFilteringThrottle(
content::NavigationHandle* handle,
AsyncDocumentSubresourceFilter* parent_frame_filter,
bool bypass_alias_check,
base::RepeatingCallback<std::string(const GURL& url)>
disallow_message_callback,
std::optional<blink::FrameAdEvidence> ad_evidence)
: content::NavigationThrottle(handle),
parent_frame_filter_(parent_frame_filter),
alias_check_enabled_(
!bypass_alias_check &&
base::FeatureList::IsEnabled(
features::kSendCnameAliasesToSubresourceFilterFromBrowser)),
disallow_message_callback_(std::move(disallow_message_callback)),
ad_evidence_(std::move(ad_evidence)) {
DCHECK(!IsInSubresourceFilterRoot(handle));
DCHECK(parent_frame_filter_);
if (ad_evidence_.has_value()) {
// Complete the ad evidence as it will be used to make best-effort tagging
// decisions by request time for ongoing subframe navs.
ad_evidence_->set_is_complete();
}
}
ChildFrameNavigationFilteringThrottle::
~ChildFrameNavigationFilteringThrottle() {
switch (load_policy_) {
case LoadPolicy::EXPLICITLY_ALLOW:
[[fallthrough]];
case LoadPolicy::ALLOW:
// TODO(crbug.com/40280666): Split metrics for different filter
// implementations.
UMA_HISTOGRAM_CUSTOM_MICRO_TIMES(
"SubresourceFilter.DocumentLoad.SubframeFilteringDelay.Allowed",
total_defer_time_, base::Microseconds(1), base::Seconds(10), 50);
break;
case LoadPolicy::WOULD_DISALLOW:
UMA_HISTOGRAM_CUSTOM_MICRO_TIMES(
"SubresourceFilter.DocumentLoad.SubframeFilteringDelay.WouldDisallow",
total_defer_time_, base::Microseconds(1), base::Seconds(10), 50);
break;
case LoadPolicy::DISALLOW:
UMA_HISTOGRAM_CUSTOM_MICRO_TIMES(
"SubresourceFilter.DocumentLoad.SubframeFilteringDelay.Disallowed2",
total_defer_time_, base::Microseconds(1), base::Seconds(10), 50);
break;
}
}
content::NavigationThrottle::ThrottleCheckResult
ChildFrameNavigationFilteringThrottle::WillStartRequest() {
return MaybeDeferToCalculateLoadPolicy();
}
content::NavigationThrottle::ThrottleCheckResult
ChildFrameNavigationFilteringThrottle::WillRedirectRequest() {
return MaybeDeferToCalculateLoadPolicy();
}
content::NavigationThrottle::ThrottleCheckResult
ChildFrameNavigationFilteringThrottle::WillProcessResponse() {
DCHECK_NE(load_policy_, LoadPolicy::DISALLOW);
if (alias_check_enabled_) {
std::vector<GURL> alias_urls;
const GURL& base_url = navigation_handle()->GetURL();
for (const auto& alias : navigation_handle()->GetDnsAliases()) {
if (alias == navigation_handle()->GetURL().host_piece()) {
continue;
}
GURL::Replacements replacements;
replacements.SetHostStr(alias);
GURL alias_url = base_url.ReplaceComponents(replacements);
if (alias_url.is_valid()) {
alias_urls.push_back(alias_url);
}
}
if (!alias_urls.empty()) {
pending_load_policy_calculations_++;
parent_frame_filter_->GetLoadPolicyForSubdocumentURLs(
alias_urls, base::BindOnce(&ChildFrameNavigationFilteringThrottle::
OnCalculatedLoadPoliciesFromAliasUrls,
weak_ptr_factory_.GetWeakPtr()));
}
}
// Load policy notifications should go out by WillProcessResponse, unless
// we received CNAME aliases in the response and alias checking is enabled.
// Defer if we are still performing any ruleset checks. If we are here,
// and there are outstanding load policy calculations, we are either in dry
// run mode or checking aliases.
if (pending_load_policy_calculations_ > 0) {
DCHECK(parent_frame_filter_->activation_state().activation_level ==
mojom::ActivationLevel::kDryRun ||
navigation_handle()->GetDnsAliases().size() > 0);
DeferStart(DeferStage::kWillProcessResponse);
return DEFER;
}
NotifyLoadPolicy();
return PROCEED;
}
const char* ChildFrameNavigationFilteringThrottle::GetNameForLogging() {
return "ChildFrameNavigationFilteringThrottle";
}
void ChildFrameNavigationFilteringThrottle::HandleDisallowedLoad() {
if (parent_frame_filter_->activation_state().enable_logging) {
std::string console_message =
disallow_message_callback_.Run(navigation_handle()->GetURL());
// Use the parent's Page to log a message to the console so that if this
// frame is the root of a nested frame tree (e.g. fenced frame), the log
// message won't be associated with a to-be-destroyed Page.
navigation_handle()
->GetParentFrameOrOuterDocument()
->GetPage()
.GetMainDocument()
.AddMessageToConsole(blink::mojom::ConsoleMessageLevel::kError,
console_message);
}
parent_frame_filter_->ReportDisallowedLoad();
}
content::NavigationThrottle::ThrottleCheckResult
ChildFrameNavigationFilteringThrottle::MaybeDeferToCalculateLoadPolicy() {
DCHECK_NE(load_policy_, LoadPolicy::DISALLOW);
if (load_policy_ == LoadPolicy::WOULD_DISALLOW) {
return PROCEED;
}
pending_load_policy_calculations_ += 1;
parent_frame_filter_->GetLoadPolicyForSubdocument(
navigation_handle()->GetURL(),
base::BindOnce(
&ChildFrameNavigationFilteringThrottle::OnCalculatedLoadPolicy,
weak_ptr_factory_.GetWeakPtr()));
// If the embedder document has activation enabled, we calculate frame load
// policy before proceeding with navigation as filtered navigations are not
// allowed to get a response. As a result, we must defer while
// we wait for the ruleset check to complete and pass handling the navigation
// decision to the callback.
//
// If `kTPCDAdHeuristicSubframeRequestTagging`, we always need to defer
// navigation start to ensure we have the load policy calculated in order
// to properly tag the navigation handle as an ad before it goes to the
// network.
if (parent_frame_filter_->activation_state().activation_level ==
mojom::ActivationLevel::kEnabled ||
base::FeatureList::IsEnabled(kTPCDAdHeuristicSubframeRequestTagging)) {
DeferStart(DeferStage::kWillStartOrRedirectRequest);
return DEFER;
}
// Otherwise, issue the ruleset request in parallel as an optimization.
return PROCEED;
}
void ChildFrameNavigationFilteringThrottle::OnCalculatedLoadPolicy(
LoadPolicy policy) {
// TODO(crbug.com/40116607): Modify this call in cases where the new
// |policy| matches an explicitly allowed rule, rather than using the most
// restrictive policy for the redirect chain.
load_policy_ = MoreRestrictiveLoadPolicy(policy, load_policy_);
pending_load_policy_calculations_ -= 1;
// Callback is not responsible for handling navigation if we are not deferred.
if (defer_stage_ == DeferStage::kNotDeferring) {
return;
}
DCHECK(defer_stage_ == DeferStage::kWillProcessResponse ||
defer_stage_ == DeferStage::kWillStartOrRedirectRequest);
// If we have an activation enabled and `load_policy_` is DISALLOW, we need
// to cancel the navigation.
if (parent_frame_filter_->activation_state().activation_level ==
mojom::ActivationLevel::kEnabled &&
load_policy_ == LoadPolicy::DISALLOW) {
CancelNavigation();
return;
}
// If there are still pending load calculations, then don't resume.
if (pending_load_policy_calculations_ > 0) {
return;
}
if (defer_stage_ == DeferStage::kWillStartOrRedirectRequest &&
ad_evidence_.has_value()) {
// Tag the navigation handle based on the current load policy + evidence
// before the request starts.
ad_evidence_->UpdateFilterListResult(
InterpretLoadPolicyAsEvidence(load_policy_));
if (ad_evidence_->IndicatesAdFrame()) {
navigation_handle()->SetIsAdTagged();
}
}
ResumeNavigation();
}
void ChildFrameNavigationFilteringThrottle::
OnCalculatedLoadPoliciesFromAliasUrls(std::vector<LoadPolicy> policies) {
// We deferred to check aliases in WillProcessResponse.
DCHECK(defer_stage_ == DeferStage::kWillProcessResponse);
DCHECK(!policies.empty());
LoadPolicy most_restricive_alias_policy = LoadPolicy::EXPLICITLY_ALLOW;
for (LoadPolicy policy : policies) {
most_restricive_alias_policy =
MoreRestrictiveLoadPolicy(most_restricive_alias_policy, policy);
}
OnCalculatedLoadPolicy(most_restricive_alias_policy);
}
void ChildFrameNavigationFilteringThrottle::DeferStart(DeferStage stage) {
DCHECK(defer_stage_ == DeferStage::kNotDeferring);
DCHECK(stage != DeferStage::kNotDeferring);
defer_stage_ = stage;
last_defer_timestamp_ = base::TimeTicks::Now();
}
void ChildFrameNavigationFilteringThrottle::NotifyLoadPolicy() const {
// TODO(crbug.com/40280666): Separate the notification mechanism from the
// current ContentSubresourceFilterThrottleManager to allow multiple filters
// with different throttle managers to use this class.
auto* observer_manager = SubresourceFilterObserverManager::FromWebContents(
navigation_handle()->GetWebContents());
if (!observer_manager) {
return;
}
observer_manager->NotifyChildFrameNavigationEvaluated(navigation_handle(),
load_policy_);
}
void ChildFrameNavigationFilteringThrottle::UpdateDeferInfo() {
DCHECK(defer_stage_ != DeferStage::kNotDeferring);
DCHECK(!last_defer_timestamp_.is_null());
total_defer_time_ += base::TimeTicks::Now() - last_defer_timestamp_;
defer_stage_ = DeferStage::kNotDeferring;
}
void ChildFrameNavigationFilteringThrottle::CancelNavigation() {
bool defer_stage_was_will_process_response =
defer_stage_ == DeferStage::kWillProcessResponse;
UpdateDeferInfo();
HandleDisallowedLoad();
NotifyLoadPolicy();
if (defer_stage_was_will_process_response) {
CancelDeferredNavigation(CANCEL);
} else {
CancelDeferredNavigation(BLOCK_REQUEST_AND_COLLAPSE);
}
}
void ChildFrameNavigationFilteringThrottle::ResumeNavigation() {
// There are no more pending load calculations. We can toggle back to not
// being deferred.
bool defer_stage_was_will_process_response =
defer_stage_ == DeferStage::kWillProcessResponse;
UpdateDeferInfo();
// If the defer stage was WillProcessResponse, then this is the last
// LoadPolicy that we will calculate.
if (defer_stage_was_will_process_response) {
NotifyLoadPolicy();
}
Resume();
}
} // namespace subresource_filter