| // 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 "net/http/http_no_vary_search_data.h" |
| |
| #include <algorithm> |
| #include <optional> |
| #include <string_view> |
| |
| #include "base/containers/flat_set.h" |
| #include "base/debug/crash_logging.h" |
| #include "base/debug/dump_without_crashing.h" |
| #include "base/feature_list.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/types/expected.h" |
| #include "net/base/features.h" |
| #include "net/base/pickle.h" |
| #include "net/base/url_search_params.h" |
| #include "net/base/url_search_params_view.h" |
| #include "net/base/url_util.h" |
| #include "net/http/http_response_headers.h" |
| #include "net/http/structured_headers.h" |
| #include "url/gurl.h" |
| |
| namespace net { |
| |
| namespace { |
| // Tries to parse a list of ParameterizedItem as a list of strings. |
| // Returns std::nullopt if unsuccessful. |
| std::optional<std::vector<std::string>> ParseStringList( |
| const std::vector<structured_headers::ParameterizedItem>& items) { |
| std::vector<std::string> keys; |
| keys.reserve(items.size()); |
| for (const auto& item : items) { |
| if (!item.item.is_string()) { |
| return std::nullopt; |
| } |
| keys.push_back(UnescapePercentEncodedUrl(item.item.GetString())); |
| } |
| return keys; |
| } |
| |
| // Extracts the "base URL" (everything before the query or fragment) from `url`. |
| // It relies on the fact that GURL canonicalizes http(s) URLs to not contain '?' |
| // or '#' before the start of the query. It's a lot faster than using |
| // GURL::Replacements to do the same thing, as no allocations or copies are |
| // needed. |
| std::string_view ExtractBaseUrl(const GURL& url) { |
| const std::string_view view(url.possibly_invalid_spec()); |
| size_t end_of_base = view.find_first_of("?#"); |
| // This returns the whole of `view` if `end_of_base` is std::string::npos. |
| return view.substr(0, end_of_base); |
| } |
| |
| std::optional<bool>& GetHttpNoVarySearchDataUseNewAreEquivalentOverride() { |
| static constinit std::optional<bool> override_value; |
| return override_value; |
| } |
| |
| bool IsHttpNoVarySearchDataUseNewAreEquivalentEnabled() { |
| if (GetHttpNoVarySearchDataUseNewAreEquivalentOverride().has_value()) { |
| return *GetHttpNoVarySearchDataUseNewAreEquivalentOverride(); |
| } |
| |
| static const bool kEnabled = base::FeatureList::IsEnabled( |
| features::kHttpNoVarySearchDataUseNewAreEquivalent); |
| |
| return kEnabled; |
| } |
| |
| } // namespace |
| |
| ScopedHttpNoVarySearchDataEquivalentImplementationOverrideForTesting:: |
| ScopedHttpNoVarySearchDataEquivalentImplementationOverrideForTesting( |
| bool use_new_implementation) { |
| GetHttpNoVarySearchDataUseNewAreEquivalentOverride() = use_new_implementation; |
| } |
| |
| ScopedHttpNoVarySearchDataEquivalentImplementationOverrideForTesting:: |
| ~ScopedHttpNoVarySearchDataEquivalentImplementationOverrideForTesting() { |
| GetHttpNoVarySearchDataUseNewAreEquivalentOverride() = std::nullopt; |
| } |
| |
| HttpNoVarySearchData::HttpNoVarySearchData() = default; |
| HttpNoVarySearchData::HttpNoVarySearchData(const HttpNoVarySearchData&) = |
| default; |
| HttpNoVarySearchData::HttpNoVarySearchData(HttpNoVarySearchData&&) = default; |
| HttpNoVarySearchData::~HttpNoVarySearchData() = default; |
| HttpNoVarySearchData& HttpNoVarySearchData::operator=( |
| const HttpNoVarySearchData&) = default; |
| HttpNoVarySearchData& HttpNoVarySearchData::operator=(HttpNoVarySearchData&&) = |
| default; |
| |
| std::vector<std::string> HttpNoVarySearchData::GetAffectedParams() const { |
| return std::vector<std::string>(affected_params_.begin(), |
| affected_params_.end()); |
| } |
| |
| template <typename ParamsType> |
| void HttpNoVarySearchData::ApplyRulesToParams(ParamsType& params) const { |
| if (vary_by_default_) { |
| params.DeleteAllWithNames(affected_params_); |
| } else { |
| params.DeleteAllExceptWithNames(affected_params_); |
| } |
| if (!vary_on_key_order_) { |
| params.Sort(); |
| } |
| } |
| |
| bool HttpNoVarySearchData::AreEquivalent(const GURL& a, const GURL& b) const { |
| CHECK(a.is_valid()); |
| CHECK(b.is_valid()); |
| if (IsHttpNoVarySearchDataUseNewAreEquivalentEnabled()) { |
| return AreEquivalentNewImpl(a, b); |
| } |
| |
| return AreEquivalentOldImpl(a, b); |
| } |
| |
| std::string HttpNoVarySearchData::CanonicalizeQuery(const GURL& url) const { |
| UrlSearchParamsView search_params(url); |
| ApplyRulesToParams(search_params); |
| |
| return search_params.SerializeAsUtf8(); |
| } |
| |
| // static |
| HttpNoVarySearchData HttpNoVarySearchData::CreateFromNoVaryParams( |
| const std::vector<std::string>& no_vary_params, |
| bool vary_on_key_order) { |
| // Check that this call creates a non-default configuration. |
| CHECK(!vary_on_key_order || !no_vary_params.empty()); |
| |
| HttpNoVarySearchData no_vary_search; |
| no_vary_search.vary_on_key_order_ = vary_on_key_order; |
| no_vary_search.affected_params_.insert(no_vary_params.cbegin(), |
| no_vary_params.cend()); |
| return no_vary_search; |
| } |
| |
| // static |
| HttpNoVarySearchData HttpNoVarySearchData::CreateFromVaryParams( |
| const std::vector<std::string>& vary_params, |
| bool vary_on_key_order) { |
| HttpNoVarySearchData no_vary_search; |
| no_vary_search.vary_on_key_order_ = vary_on_key_order; |
| no_vary_search.vary_by_default_ = false; |
| no_vary_search.affected_params_.insert(vary_params.cbegin(), |
| vary_params.cend()); |
| return no_vary_search; |
| } |
| |
| // static |
| base::expected<HttpNoVarySearchData, HttpNoVarySearchData::ParseErrorEnum> |
| HttpNoVarySearchData::ParseFromHeaderValue(std::string_view value) { |
| // The no-vary-search header is a dictionary type structured field. |
| const auto dict = structured_headers::ParseDictionary(value); |
| if (!dict.has_value()) { |
| // We don't recognize anything else. So this is an authoring error. |
| return base::unexpected(ParseErrorEnum::kNotDictionary); |
| } |
| |
| return ParseNoVarySearchDictionary(dict.value()); |
| } |
| |
| // static |
| base::expected<HttpNoVarySearchData, HttpNoVarySearchData::ParseErrorEnum> |
| HttpNoVarySearchData::ParseFromHeaders( |
| const HttpResponseHeaders& response_headers) { |
| std::optional<std::string> normalized_header = |
| response_headers.GetNormalizedHeader("No-Vary-Search"); |
| if (!normalized_header) { |
| // This means there is no No-Vary-Search header. |
| return base::unexpected(ParseErrorEnum::kOk); |
| } |
| |
| return ParseFromHeaderValue(*normalized_header); |
| } |
| |
| // static |
| bool HttpNoVarySearchData::HasBooleanParamsMember( |
| std::string_view header_value) { |
| const auto dict = structured_headers::ParseDictionary(header_value); |
| if (!dict.has_value()) { |
| return false; |
| } |
| auto it = dict->find("params"); |
| if (it == dict->end()) { |
| return false; |
| } |
| const auto& member = it->second; |
| if (member.member_is_inner_list) { |
| return false; |
| } |
| // This is guaranteed by the structured headers parser API. |
| CHECK_EQ(member.member.size(), 1u); |
| return member.member[0].item.is_boolean(); |
| } |
| |
| bool HttpNoVarySearchData::operator==(const HttpNoVarySearchData& rhs) const = |
| default; |
| std::strong_ordering HttpNoVarySearchData::operator<=>( |
| const HttpNoVarySearchData& rhs) const = default; |
| |
| bool HttpNoVarySearchData::AreEquivalentOldImplForTesting(const GURL& a, |
| const GURL& b) const { |
| return AreEquivalentOldImpl(a, b); |
| } |
| |
| bool HttpNoVarySearchData::AreEquivalentNewImplForTesting(const GURL& a, |
| const GURL& b) const { |
| return AreEquivalentNewImpl(a, b); |
| } |
| |
| // static |
| base::expected<HttpNoVarySearchData, HttpNoVarySearchData::ParseErrorEnum> |
| HttpNoVarySearchData::ParseNoVarySearchDictionary( |
| const structured_headers::Dictionary& dict) { |
| static constexpr std::string_view kKeyOrder = "key-order"; |
| static constexpr std::string_view kParams = "params"; |
| static constexpr std::string_view kExcept = "except"; |
| constexpr std::string_view kValidKeys[] = {kKeyOrder, kParams, kExcept}; |
| |
| base::flat_set<std::string> affected_params; |
| bool vary_on_key_order = true; |
| bool vary_by_default = true; |
| |
| // If the dictionary contains unknown keys, maybe fail parsing. |
| const bool has_unrecognized_keys = |
| !std::ranges::all_of(dict, [&](const auto& pair) { |
| return std::ranges::contains(kValidKeys, pair.first); |
| }); |
| |
| UMA_HISTOGRAM_BOOLEAN("Net.HttpNoVarySearch.HasUnrecognizedKeys", |
| has_unrecognized_keys); |
| if (has_unrecognized_keys && |
| !base::FeatureList::IsEnabled( |
| features::kNoVarySearchIgnoreUnrecognizedKeys)) { |
| return base::unexpected(ParseErrorEnum::kUnknownDictionaryKey); |
| } |
| |
| // Populate `vary_on_key_order` based on the `key-order` key. |
| if (auto keyorder_it = dict.find(kKeyOrder); keyorder_it != dict.end()) { |
| const auto& key_order = keyorder_it->second; |
| if (key_order.member_is_inner_list || |
| !key_order.member[0].item.is_boolean()) { |
| return base::unexpected(ParseErrorEnum::kNonBooleanKeyOrder); |
| } |
| vary_on_key_order = !key_order.member[0].item.GetBoolean(); |
| } |
| |
| // Populate `affected_params` or `vary_by_default` based on the "params" key. |
| if (auto params_it = dict.find(kParams); params_it != dict.end()) { |
| const auto& params = params_it->second; |
| if (params.member_is_inner_list) { |
| auto keys = ParseStringList(params.member); |
| if (!keys.has_value()) { |
| return base::unexpected(ParseErrorEnum::kParamsNotStringList); |
| } |
| affected_params = std::move(*keys); |
| } else if (params.member[0].item.is_boolean()) { |
| vary_by_default = !params.member[0].item.GetBoolean(); |
| } else { |
| return base::unexpected(ParseErrorEnum::kParamsNotStringList); |
| } |
| } |
| |
| // Populate `affected_params` based on the "except" key. |
| // This should be present only if "params" was true |
| // (i.e., params don't vary by default). |
| if (auto except_it = dict.find(kExcept); except_it != dict.end()) { |
| const auto& excepted_params = except_it->second; |
| if (vary_by_default) { |
| return base::unexpected(ParseErrorEnum::kExceptWithoutTrueParams); |
| } |
| if (!excepted_params.member_is_inner_list) { |
| return base::unexpected(ParseErrorEnum::kExceptNotStringList); |
| } |
| auto keys = ParseStringList(excepted_params.member); |
| if (!keys.has_value()) { |
| return base::unexpected(ParseErrorEnum::kExceptNotStringList); |
| } |
| affected_params = std::move(*keys); |
| } |
| |
| if (affected_params.empty() && vary_by_default && vary_on_key_order) { |
| // If header is present but it's value is equivalent to only default values |
| // then it is the same as if there were no header present. |
| return base::unexpected(ParseErrorEnum::kDefaultValue); |
| } |
| |
| HttpNoVarySearchData no_vary_search; |
| no_vary_search.affected_params_ = std::move(affected_params); |
| no_vary_search.vary_on_key_order_ = vary_on_key_order; |
| no_vary_search.vary_by_default_ = vary_by_default; |
| |
| return base::ok(no_vary_search); |
| } |
| |
| bool HttpNoVarySearchData::AreEquivalentOldImpl(const GURL& a, |
| const GURL& b) const { |
| // Check urls without query and reference (fragment) for equality first. |
| GURL::Replacements replacements; |
| replacements.ClearRef(); |
| replacements.ClearQuery(); |
| if (a.ReplaceComponents(replacements) != b.ReplaceComponents(replacements)) { |
| return false; |
| } |
| |
| // If equal, look at how HttpNoVarySearchData argument affects |
| // search params variance. |
| UrlSearchParams a_search_params(a); |
| UrlSearchParams b_search_params(b); |
| ApplyRulesToParams(a_search_params); |
| ApplyRulesToParams(b_search_params); |
| |
| // Check Search Params for equality |
| // All search params, in order, need to have the same keys and the same |
| // values. |
| return a_search_params.params() == b_search_params.params(); |
| } |
| |
| bool HttpNoVarySearchData::AreEquivalentNewImpl(const GURL& a, |
| const GURL& b) const { |
| if (ExtractBaseUrl(a) != ExtractBaseUrl(b)) { |
| return false; |
| } |
| |
| // If equal, look at how HttpNoVarySearchData argument affects |
| // search params variance. |
| UrlSearchParamsView a_search_params(a); |
| UrlSearchParamsView b_search_params(b); |
| ApplyRulesToParams(a_search_params); |
| ApplyRulesToParams(b_search_params); |
| |
| return a_search_params == b_search_params; |
| } |
| |
| // LINT.IfChange(Serialization) |
| void PickleTraits<HttpNoVarySearchData>::Serialize( |
| base::Pickle& pickle, |
| const HttpNoVarySearchData& value) { |
| WriteToPickle(pickle, HttpNoVarySearchData::kMagicNumber, |
| value.affected_params_, value.vary_on_key_order_, |
| value.vary_by_default_); |
| } |
| |
| std::optional<HttpNoVarySearchData> |
| PickleTraits<HttpNoVarySearchData>::Deserialize(base::PickleIterator& iter) { |
| HttpNoVarySearchData result; |
| uint32_t magic_number = 0u; |
| if (!ReadPickleInto(iter, magic_number, result.affected_params_, |
| result.vary_on_key_order_, result.vary_by_default_)) { |
| return std::nullopt; |
| } |
| |
| if (magic_number != HttpNoVarySearchData::kMagicNumber) { |
| return std::nullopt; |
| } |
| |
| if (result.vary_by_default_ && result.vary_on_key_order_ && |
| result.affected_params_.empty()) { |
| // This is the default configuration in the absence of a No-Vary-Search |
| // header, and should never be stored in a HttpNoVarySearchData object. |
| return std::nullopt; |
| } |
| |
| return result; |
| } |
| |
| size_t PickleTraits<HttpNoVarySearchData>::PickleSize( |
| const HttpNoVarySearchData& value) { |
| return EstimatePickleSize(HttpNoVarySearchData::kMagicNumber, |
| value.affected_params_, value.vary_on_key_order_, |
| value.vary_by_default_); |
| } |
| // LINT.ThenChange(//net/http/http_no_vary_search_data.h:MagicNumber) |
| |
| } // namespace net |