#include "net/device_bound_sessions/session_inclusion_rules.h"
#include <string_view>
#include "base/check.h"
#include "base/containers/adapters.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/strings/string_util.h"
#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
#include "net/device_bound_sessions/host_patterns.h"
#include "net/device_bound_sessions/proto/storage.pb.h"
#include "net/device_bound_sessions/session.h"
#include "net/device_bound_sessions/session_error.h"
namespace net::device_bound_sessions {
namespace {
bool IsIncludeSiteAllowed(const url::Origin& origin) {
const std::string domain_and_registry =
registry_controlled_domains::GetDomainAndRegistry(
origin, registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES);
return !domain_and_registry.empty() && origin.host() == domain_and_registry;
}
proto::RuleType GetRuleTypeProto(
SessionInclusionRules::InclusionResult result) {
return result == SessionInclusionRules::InclusionResult::kInclude
? proto::RuleType::INCLUDE
: proto::RuleType::EXCLUDE;
}
std::optional<SessionParams::Scope::Specification::Type> GetInclusionResult(
proto::RuleType proto) {
if (proto == proto::RuleType::INCLUDE) {
return SessionParams::Scope::Specification::Type::kInclude;
} else if (proto == proto::RuleType::EXCLUDE) {
return SessionParams::Scope::Specification::Type::kExclude;
}
return std::nullopt;
}
std::string RuleTypeToString(SessionInclusionRules::InclusionResult rule_type) {
switch (rule_type) {
case SessionInclusionRules::InclusionResult::kExclude:
return "exclude";
case SessionInclusionRules::InclusionResult::kInclude:
return "include";
}
}
}
struct SessionInclusionRules::UrlRule {
InclusionResult rule_type;
std::string host_pattern;
std::string path_prefix;
friend bool operator==(const UrlRule& lhs, const UrlRule& rhs) = default;
bool MatchesHostAndPath(const GURL& url) const;
};
base::expected<SessionInclusionRules, SessionError>
SessionInclusionRules::Create(const url::Origin& origin,
const SessionParams::Scope& scope_params,
const GURL& refresh_endpoint) {
SessionInclusionRules rules(origin);
if (scope_params.include_site && !rules.may_include_site_) {
return base::unexpected(
SessionError{SessionError::kInvalidScopeIncludeSite});
}
rules.SetIncludeSite(scope_params.include_site);
for (const auto& spec : scope_params.specifications) {
const auto inclusion_result =
spec.type == SessionParams::Scope::Specification::Type::kExclude
? SessionInclusionRules::InclusionResult::kExclude
: SessionInclusionRules::InclusionResult::kInclude;
SessionError::ErrorType add_url_rule_result =
rules.AddUrlRuleIfValid(inclusion_result, spec.domain, spec.path);
if (add_url_rule_result != SessionError::kSuccess) {
return base::unexpected(SessionError{add_url_rule_result});
}
}
if (refresh_endpoint.is_valid()) {
rules.AddUrlRuleIfValid(SessionInclusionRules::InclusionResult::kExclude,
refresh_endpoint.GetHost(),
refresh_endpoint.GetPath());
}
return rules;
}
SessionInclusionRules::SessionInclusionRules(const url::Origin& origin)
: origin_(origin), may_include_site_(IsIncludeSiteAllowed(origin)) {}
SessionInclusionRules::~SessionInclusionRules() = default;
SessionInclusionRules::SessionInclusionRules(SessionInclusionRules&& other) =
default;
SessionInclusionRules& SessionInclusionRules::operator=(
SessionInclusionRules&& other) = default;
bool SessionInclusionRules::operator==(
const SessionInclusionRules& other) const = default;
void SessionInclusionRules::SetIncludeSite(bool include_site) {
if (!include_site) {
include_site_.reset();
return;
}
include_site_ = SchemefulSite(origin_);
}
SessionError::ErrorType SessionInclusionRules::AddUrlRuleIfValid(
InclusionResult rule_type,
const std::string& host_pattern,
const std::string& path_prefix) {
if (path_prefix.empty() || path_prefix.front() != '/') {
return SessionError::kInvalidScopeRulePath;
}
if (!IsValidHostPattern(host_pattern)) {
return SessionError::kInvalidScopeRuleHostPattern;
}
if (!include_site_ && !MatchesHostPattern(host_pattern, origin_.host())) {
return SessionError::kScopeRuleOriginScopedHostPatternMismatch;
}
if (include_site_ && !MatchesHostPattern(host_pattern, origin_.host())) {
std::string_view hostlike_part = host_pattern;
if (hostlike_part.starts_with("*.")) {
hostlike_part = hostlike_part.substr(2);
}
std::string hostlike_part_domain =
registry_controlled_domains::GetDomainAndRegistry(
hostlike_part,
registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES);
std::string domain_and_registry =
registry_controlled_domains::GetDomainAndRegistry(
origin_, registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES);
if (hostlike_part_domain != domain_and_registry) {
return SessionError::kScopeRuleSiteScopedHostPatternMismatch;
}
}
url_rules_.emplace_back(rule_type, host_pattern, path_prefix);
return SessionError::kSuccess;
}
SessionInclusionRules::InclusionResult
SessionInclusionRules::EvaluateRequestUrl(const GURL& url) const {
bool same_origin = origin_.IsSameOriginWith(url);
if (include_site_ && !include_site_->IsSameSiteWith(url)) {
return SessionInclusionRules::kExclude;
}
if (!include_site_ && !same_origin) {
return SessionInclusionRules::kExclude;
}
for (const UrlRule& rule : base::Reversed(url_rules_)) {
if (rule.MatchesHostAndPath(url) && url.scheme() == origin_.scheme()) {
return rule.rule_type;
}
}
return SessionInclusionRules::kInclude;
}
bool SessionInclusionRules::AllowsRefreshForInitiator(
const url::Origin& initiator) const {
if (include_site_ && include_site_->IsSameSiteWith(initiator)) {
return true;
}
if (!include_site_ && origin_.IsSameOriginWith(initiator)) {
return true;
}
return false;
}
bool SessionInclusionRules::UrlRule::MatchesHostAndPath(const GURL& url) const {
if (!MatchesHostPattern(host_pattern, url.GetHost())) {
return false;
}
std::string_view url_path = url.path();
if (!url_path.starts_with(path_prefix)) {
return false;
}
CHECK(url_path.length() >= path_prefix.length());
if (url_path.length() > path_prefix.length() && path_prefix.back() != '/' &&
url_path[path_prefix.length()] != '/') {
return false;
}
return true;
}
size_t SessionInclusionRules::num_url_rules_for_testing() const {
return url_rules_.size();
}
proto::SessionInclusionRules SessionInclusionRules::ToProto() const {
proto::SessionInclusionRules proto;
proto.set_origin(origin_.Serialize());
proto.set_do_include_site(include_site_.has_value());
for (auto& rule : url_rules_) {
proto::UrlRule rule_proto;
rule_proto.set_rule_type(GetRuleTypeProto(rule.rule_type));
rule_proto.set_host_pattern(rule.host_pattern);
rule_proto.set_path_prefix(rule.path_prefix);
proto.mutable_url_rules()->Add(std::move(rule_proto));
}
return proto;
}
std::optional<SessionInclusionRules> SessionInclusionRules::CreateFromProto(
const proto::SessionInclusionRules& proto) {
if (!proto.has_origin() || !proto.has_do_include_site()) {
return std::nullopt;
}
url::Origin origin = url::Origin::Create(GURL(proto.origin()));
if (origin.opaque()) {
DLOG(ERROR) << "proto origin parse error: " << origin.GetDebugString();
return std::nullopt;
}
SessionParams::Scope params;
params.include_site = proto.do_include_site();
for (const auto& rule_proto : proto.url_rules()) {
std::optional<SessionParams::Scope::Specification::Type> rule_type =
GetInclusionResult(rule_proto.rule_type());
if (!rule_type.has_value()) {
return std::nullopt;
}
params.specifications.emplace_back(*rule_type, rule_proto.host_pattern(),
rule_proto.path_prefix());
}
auto inclusion_rules_or_error =
Create(origin, std::move(params), GURL());
if (!inclusion_rules_or_error.has_value()) {
return std::nullopt;
}
return std::move(*inclusion_rules_or_error);
}
std::string SessionInclusionRules::DebugString() const {
std::string result;
for (const UrlRule& rule : url_rules_) {
base::StrAppend(&result, {"Type=", RuleTypeToString(rule.rule_type),
"; Domain=", rule.host_pattern,
"; Path=", rule.path_prefix, "\n"});
}
return result;
}
}