blob: 7349dcd041af14e72a85c99304a210460cb2b716 [file] [log] [blame]
// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "extensions/browser/api/declarative_net_request/composite_matcher.h"
#include <algorithm>
#include <cstdint>
#include <functional>
#include <iterator>
#include <optional>
#include <set>
#include <utility>
#include <vector>
#include "base/containers/contains.h"
#include "base/metrics/histogram_macros.h"
#include "base/time/time.h"
#include "base/timer/elapsed_timer.h"
#include "components/url_pattern_index/flat/url_pattern_index_generated.h"
#include "extensions/browser/api/declarative_net_request/constants.h"
#include "extensions/browser/api/declarative_net_request/flat/extension_ruleset_generated.h"
#include "extensions/browser/api/declarative_net_request/request_action.h"
#include "extensions/browser/api/declarative_net_request/request_params.h"
#include "extensions/browser/api/declarative_net_request/utils.h"
#include "extensions/common/api/declarative_net_request/constants.h"
namespace extensions::declarative_net_request {
namespace flat_rule = url_pattern_index::flat;
using PageAccess = PermissionsData::PageAccess;
using ActionInfo = CompositeMatcher::ActionInfo;
namespace {
bool AreIDsUnique(const CompositeMatcher::MatcherList& matchers) {
std::set<RulesetID> ids;
for (const auto& matcher : matchers) {
bool did_insert = ids.insert(matcher->id()).second;
if (!did_insert) {
return false;
}
}
return true;
}
// Helper to log the time taken in CompositeMatcher::GetAction.
class ScopedGetBeforeRequestActionTimer {
public:
explicit ScopedGetBeforeRequestActionTimer(RulesetMatchingStage stage)
: stage_(stage) {}
~ScopedGetBeforeRequestActionTimer() {
switch (stage_) {
case RulesetMatchingStage::kOnBeforeRequest:
UMA_HISTOGRAM_CUSTOM_MICROSECONDS_TIMES(
"Extensions.DeclarativeNetRequest.EvaluateBeforeRequestTime."
"SingleExtension2",
timer_.Elapsed(), base::Microseconds(1), base::Milliseconds(50),
50);
break;
case RulesetMatchingStage::kOnHeadersReceived:
UMA_HISTOGRAM_CUSTOM_MICROSECONDS_TIMES(
"Extensions.DeclarativeNetRequest.EvaluateHeadersReceivedTime."
"SingleExtension2",
timer_.Elapsed(), base::Microseconds(1), base::Milliseconds(50),
50);
break;
}
}
private:
RulesetMatchingStage stage_;
base::ElapsedTimer timer_;
};
} // namespace
ActionInfo::ActionInfo() = default;
ActionInfo::ActionInfo(std::optional<RequestAction> action,
bool notify_request_withheld)
: action(std::move(action)),
notify_request_withheld(notify_request_withheld) {}
ActionInfo::~ActionInfo() = default;
ActionInfo::ActionInfo(ActionInfo&&) = default;
ActionInfo& ActionInfo::operator=(ActionInfo&& other) = default;
CompositeMatcher::CompositeMatcher(MatcherList matchers,
const ExtensionId& extension_id,
HostPermissionsAlwaysRequired mode)
: matchers_(std::move(matchers)),
extension_id_(extension_id),
host_permissions_always_required_(mode) {
DCHECK(AreIDsUnique(matchers_));
}
CompositeMatcher::~CompositeMatcher() = default;
const RulesetMatcher* CompositeMatcher::GetMatcherWithID(RulesetID id) const {
auto it = std::ranges::find(matchers_, id, &RulesetMatcher::id);
return it == matchers_.end() ? nullptr : it->get();
}
void CompositeMatcher::AddOrUpdateRuleset(
std::unique_ptr<RulesetMatcher> matcher) {
MatcherList matchers;
matchers.push_back(std::move(matcher));
AddOrUpdateRulesets(std::move(matchers));
}
void CompositeMatcher::AddOrUpdateRulesets(MatcherList matchers) {
std::set<RulesetID> ids_to_remove;
for (const auto& matcher : matchers) {
ids_to_remove.insert(matcher->id());
}
RemoveRulesetsWithIDs(ids_to_remove);
matchers_.insert(matchers_.end(), std::make_move_iterator(matchers.begin()),
std::make_move_iterator(matchers.end()));
OnMatchersModified();
}
void CompositeMatcher::RemoveRulesetsWithIDs(const std::set<RulesetID>& ids) {
size_t erased_count = std::erase_if(
matchers_, [&ids](const std::unique_ptr<RulesetMatcher>& matcher) {
return base::Contains(ids, matcher->id());
});
if (erased_count > 0) {
OnMatchersModified();
}
}
std::set<RulesetID> CompositeMatcher::ComputeStaticRulesetIDs() const {
std::set<RulesetID> result;
for (const std::unique_ptr<RulesetMatcher>& matcher : matchers_) {
if (matcher->id() == kDynamicRulesetID) {
continue;
}
result.insert(matcher->id());
}
return result;
}
ActionInfo CompositeMatcher::GetAction(
const RequestParams& params,
RulesetMatchingStage stage,
PermissionsData::PageAccess page_access) const {
ScopedGetBeforeRequestActionTimer timer(stage);
bool always_require_host_permissions =
host_permissions_always_required_ == HostPermissionsAlwaysRequired::kTrue;
if (always_require_host_permissions) {
// We shouldn't be evaluating this ruleset if host permissions are always
// required but this extension doesn't have access to the request.
DCHECK(page_access == PermissionsData::PageAccess::kAllowed ||
page_access == PermissionsData::PageAccess::kWithheld);
}
// Get the max priority allow action for this extension, or implicitly assign
// it as nullopt in `params.max_priority_allow_action` if there isn't one.
auto& max_priority_allow_action_for_extension =
params.max_priority_allow_action[extension_id_];
// Assign `final_action` to the max priority allow action matched in previous
// request stages (if any). This way, that action will be returned again if it
// outprioritizes all rules that are matched in the current request `stage`.
std::optional<RequestAction> final_action =
max_priority_allow_action_for_extension.has_value()
? std::make_optional(max_priority_allow_action_for_extension->Clone())
: std::nullopt;
for (const auto& matcher : matchers_) {
std::optional<RequestAction> action = matcher->GetAction(params, stage);
uint64_t max_allow_rule_priority =
max_priority_allow_action_for_extension.has_value()
? max_priority_allow_action_for_extension->index_priority
: 0u;
if (!action || action->index_priority <= max_allow_rule_priority) {
continue;
}
if (action->IsAllowOrAllowAllRequests()) {
// This will update `max_priority_allow_action_for_extension`.
params.max_priority_allow_action.insert_or_assign(extension_id_,
action->Clone());
}
final_action =
GetMaxPriorityAction(std::move(final_action), std::move(action));
}
if (!final_action) {
return ActionInfo();
}
bool requires_host_permission =
always_require_host_permissions ||
final_action->type == RequestAction::Type::REDIRECT;
if (!requires_host_permission || page_access == PageAccess::kAllowed) {
return ActionInfo(std::move(final_action),
/*notify_request_withheld=*/false);
}
// `requires_host_permission` is true and `page_access` is withheld or denied.
bool notify_request_withheld = page_access == PageAccess::kWithheld &&
!final_action->IsAllowOrAllowAllRequests();
return ActionInfo(std::nullopt, notify_request_withheld);
}
std::vector<RequestAction> CompositeMatcher::GetModifyHeadersActions(
const RequestParams& params,
RulesetMatchingStage stage) const {
std::vector<RequestAction> modify_headers_actions;
// The priority of the highest priority matching allow or allowAllRequests
// rule within this matcher, or 0 if no such rule exists (the minimum priority
// for a rule, specified in `kMinValidPriority`, is 1.)
uint64_t max_allow_rule_priority = 0u;
DCHECK(base::Contains(params.max_priority_allow_action, extension_id_));
if (auto& allow_action = params.max_priority_allow_action.at(extension_id_)) {
max_allow_rule_priority = allow_action->index_priority;
}
for (const auto& matcher : matchers_) {
// Plumb `max_allow_rule_priority` into GetModifyHeadersActions so that
// modifyHeaders rules with priorities less than or equal to the highest
// priority matching allow/allowAllRequests rule are ignored.
std::vector<RequestAction> actions_for_matcher =
matcher->GetModifyHeadersActions(params, stage,
max_allow_rule_priority);
modify_headers_actions.insert(
modify_headers_actions.end(),
std::make_move_iterator(actions_for_matcher.begin()),
std::make_move_iterator(actions_for_matcher.end()));
}
// Sort `modify_headers_actions` in descending order of priority.
std::sort(modify_headers_actions.begin(), modify_headers_actions.end(),
std::greater<>());
return modify_headers_actions;
}
bool CompositeMatcher::HasAnyExtraHeadersMatcher() const {
if (!has_any_extra_headers_matcher_.has_value()) {
has_any_extra_headers_matcher_ = ComputeHasAnyExtraHeadersMatcher();
}
return has_any_extra_headers_matcher_.value();
}
void CompositeMatcher::OnRenderFrameCreated(content::RenderFrameHost* host) {
for (auto& matcher : matchers_) {
matcher->OnRenderFrameCreated(host);
}
}
void CompositeMatcher::OnRenderFrameDeleted(content::RenderFrameHost* host) {
for (auto& matcher : matchers_) {
matcher->OnRenderFrameDeleted(host);
}
}
void CompositeMatcher::OnDidFinishNavigation(
content::NavigationHandle* navigation_handle) {
for (auto& matcher : matchers_) {
matcher->OnDidFinishNavigation(navigation_handle);
}
}
bool CompositeMatcher::HasRulesets(RulesetMatchingStage stage) const {
return std::ranges::any_of(
matchers_, [stage](const std::unique_ptr<RulesetMatcher>& matcher) {
return matcher->GetRulesCount(stage) > 0;
});
}
void CompositeMatcher::OnMatchersModified() {
DCHECK(AreIDsUnique(matchers_));
// Clear the renderers' cache so that they take the updated rules into
// account.
ClearRendererCacheOnNavigation();
has_any_extra_headers_matcher_.reset();
}
bool CompositeMatcher::ComputeHasAnyExtraHeadersMatcher() const {
for (const auto& matcher : matchers_) {
if (matcher->IsExtraHeadersMatcher()) {
return true;
}
}
return false;
}
} // namespace extensions::declarative_net_request