// 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 "base/containers/contains.h" #include "base/containers/flat_set.h" #include "base/types/expected.h" #include "net/base/url_search_params.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 absl::nullopt if unsuccessful. absl::optional> ParseStringList( const std::vector& items) { std::vector keys; keys.reserve(items.size()); for (const auto& item : items) { if (!item.item.is_string()) { return absl::nullopt; } keys.push_back(UnescapePercentEncodedUrl(item.item.GetString())); } return keys; } } // namespace 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; bool HttpNoVarySearchData::AreEquivalent(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); // Ignore all the query search params that the URL is not varying on. if (vary_by_default()) { a_search_params.DeleteAllWithNames(no_vary_params()); b_search_params.DeleteAllWithNames(no_vary_params()); } else { a_search_params.DeleteAllExceptWithNames(vary_params()); b_search_params.DeleteAllExceptWithNames(vary_params()); } // Sort the params if the order of the search params in the query // is ignored. if (!vary_on_key_order()) { a_search_params.Sort(); b_search_params.Sort(); } // 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(); } // static HttpNoVarySearchData HttpNoVarySearchData::CreateFromNoVaryParams( const std::vector& no_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.no_vary_params_.insert(no_vary_params.cbegin(), no_vary_params.cend()); return no_vary_search; } // static HttpNoVarySearchData HttpNoVarySearchData::CreateFromVaryParams( const std::vector& 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.vary_params_.insert(vary_params.cbegin(), vary_params.cend()); return no_vary_search; } // static base::expected HttpNoVarySearchData::ParseFromHeaders( const HttpResponseHeaders& response_headers) { std::string normalized_header; if (!response_headers.GetNormalizedHeader("No-Vary-Search", &normalized_header)) { // This means there is no No-Vary-Search header. Return nullopt. return base::unexpected(ParseErrorEnum::kOk); } // The no-vary-search header is a dictionary type structured field. const auto dict = structured_headers::ParseDictionary(normalized_header); 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()); } const base::flat_set& HttpNoVarySearchData::no_vary_params() const { return no_vary_params_; } const base::flat_set& HttpNoVarySearchData::vary_params() const { return vary_params_; } bool HttpNoVarySearchData::vary_on_key_order() const { return vary_on_key_order_; } bool HttpNoVarySearchData::vary_by_default() const { return vary_by_default_; } // static base::expected HttpNoVarySearchData::ParseNoVarySearchDictionary( const structured_headers::Dictionary& dict) { static constexpr const char* kKeyOrder = "key-order"; static constexpr const char* kParams = "params"; static constexpr const char* kExcept = "except"; constexpr base::StringPiece kValidKeys[] = {kKeyOrder, kParams, kExcept}; base::flat_set no_vary_params; base::flat_set vary_params; bool vary_on_key_order = true; bool vary_by_default = true; // If the dictionary contains unknown keys, fail parsing. for (const auto& [key, value] : dict) { // We don't recognize any other key. So this is an authoring error. if (!base::Contains(kValidKeys, key)) { return base::unexpected(ParseErrorEnum::kUnknownDictionaryKey); } } // Populate `vary_on_key_order` based on the `key-order` key. if (dict.contains(kKeyOrder)) { const auto& key_order = dict.at(kKeyOrder); 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 `no_vary_params` or `vary_by_default` based on the "params" key. if (dict.contains(kParams)) { const auto& params = dict.at(kParams); if (params.member_is_inner_list) { auto keys = ParseStringList(params.member); if (!keys.has_value()) { return base::unexpected(ParseErrorEnum::kParamsNotStringList); } no_vary_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 `vary_params` based on the "except" key. // This should be present only if "params" was true // (i.e., params don't vary by default). if (dict.contains(kExcept)) { const auto& excepted_params = dict.at(kExcept); 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); } vary_params = std::move(*keys); } // "params" controls both `vary_by_default` and `no_vary_params`. Check to // make sure that when "params" is a boolean, `no_vary_params` is empty. if (!vary_by_default) DCHECK(no_vary_params.empty()); if (no_vary_params.empty() && vary_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.no_vary_params_ = std::move(no_vary_params); no_vary_search.vary_params_ = std::move(vary_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); } } // namespace net