• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2022 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "net/http/http_no_vary_search_data.h"
6 
7 #include <string_view>
8 
9 #include "base/containers/contains.h"
10 #include "base/containers/flat_set.h"
11 #include "base/feature_list.h"
12 #include "base/metrics/histogram_macros.h"
13 #include "base/types/expected.h"
14 #include "net/base/features.h"
15 #include "net/base/url_search_params.h"
16 #include "net/base/url_util.h"
17 #include "net/http/http_response_headers.h"
18 #include "net/http/structured_headers.h"
19 #include "url/gurl.h"
20 
21 namespace net {
22 
23 namespace {
24 // Tries to parse a list of ParameterizedItem as a list of strings.
25 // Returns std::nullopt if unsuccessful.
ParseStringList(const std::vector<structured_headers::ParameterizedItem> & items)26 std::optional<std::vector<std::string>> ParseStringList(
27     const std::vector<structured_headers::ParameterizedItem>& items) {
28   std::vector<std::string> keys;
29   keys.reserve(items.size());
30   for (const auto& item : items) {
31     if (!item.item.is_string()) {
32       return std::nullopt;
33     }
34     keys.push_back(UnescapePercentEncodedUrl(item.item.GetString()));
35   }
36   return keys;
37 }
38 
39 }  // namespace
40 
41 HttpNoVarySearchData::HttpNoVarySearchData() = default;
42 HttpNoVarySearchData::HttpNoVarySearchData(const HttpNoVarySearchData&) =
43     default;
44 HttpNoVarySearchData::HttpNoVarySearchData(HttpNoVarySearchData&&) = default;
45 HttpNoVarySearchData::~HttpNoVarySearchData() = default;
46 HttpNoVarySearchData& HttpNoVarySearchData::operator=(
47     const HttpNoVarySearchData&) = default;
48 HttpNoVarySearchData& HttpNoVarySearchData::operator=(HttpNoVarySearchData&&) =
49     default;
50 
AreEquivalent(const GURL & a,const GURL & b) const51 bool HttpNoVarySearchData::AreEquivalent(const GURL& a, const GURL& b) const {
52   // Check urls without query and reference (fragment) for equality first.
53   GURL::Replacements replacements;
54   replacements.ClearRef();
55   replacements.ClearQuery();
56   if (a.ReplaceComponents(replacements) != b.ReplaceComponents(replacements)) {
57     return false;
58   }
59 
60   // If equal, look at how HttpNoVarySearchData argument affects
61   // search params variance.
62   UrlSearchParams a_search_params(a);
63   UrlSearchParams b_search_params(b);
64   // Ignore all the query search params that the URL is not varying on.
65   if (vary_by_default()) {
66     a_search_params.DeleteAllWithNames(no_vary_params());
67     b_search_params.DeleteAllWithNames(no_vary_params());
68   } else {
69     a_search_params.DeleteAllExceptWithNames(vary_params());
70     b_search_params.DeleteAllExceptWithNames(vary_params());
71   }
72   // Sort the params if the order of the search params in the query
73   // is ignored.
74   if (!vary_on_key_order()) {
75     a_search_params.Sort();
76     b_search_params.Sort();
77   }
78   // Check Search Params for equality
79   // All search params, in order, need to have the same keys and the same
80   // values.
81   return a_search_params.params() == b_search_params.params();
82 }
83 
84 // static
CreateFromNoVaryParams(const std::vector<std::string> & no_vary_params,bool vary_on_key_order)85 HttpNoVarySearchData HttpNoVarySearchData::CreateFromNoVaryParams(
86     const std::vector<std::string>& no_vary_params,
87     bool vary_on_key_order) {
88   HttpNoVarySearchData no_vary_search;
89   no_vary_search.vary_on_key_order_ = vary_on_key_order;
90   no_vary_search.no_vary_params_.insert(no_vary_params.cbegin(),
91                                         no_vary_params.cend());
92   return no_vary_search;
93 }
94 
95 // static
CreateFromVaryParams(const std::vector<std::string> & vary_params,bool vary_on_key_order)96 HttpNoVarySearchData HttpNoVarySearchData::CreateFromVaryParams(
97     const std::vector<std::string>& vary_params,
98     bool vary_on_key_order) {
99   HttpNoVarySearchData no_vary_search;
100   no_vary_search.vary_on_key_order_ = vary_on_key_order;
101   no_vary_search.vary_by_default_ = false;
102   no_vary_search.vary_params_.insert(vary_params.cbegin(), vary_params.cend());
103   return no_vary_search;
104 }
105 
106 // static
107 base::expected<HttpNoVarySearchData, HttpNoVarySearchData::ParseErrorEnum>
ParseFromHeaders(const HttpResponseHeaders & response_headers)108 HttpNoVarySearchData::ParseFromHeaders(
109     const HttpResponseHeaders& response_headers) {
110   std::optional<std::string> normalized_header =
111       response_headers.GetNormalizedHeader("No-Vary-Search");
112   if (!normalized_header) {
113     // This means there is no No-Vary-Search header. Return nullopt.
114     return base::unexpected(ParseErrorEnum::kOk);
115   }
116 
117   // The no-vary-search header is a dictionary type structured field.
118   const auto dict = structured_headers::ParseDictionary(*normalized_header);
119   if (!dict.has_value()) {
120     // We don't recognize anything else. So this is an authoring error.
121     return base::unexpected(ParseErrorEnum::kNotDictionary);
122   }
123 
124   return ParseNoVarySearchDictionary(dict.value());
125 }
126 
no_vary_params() const127 const base::flat_set<std::string>& HttpNoVarySearchData::no_vary_params()
128     const {
129   return no_vary_params_;
130 }
131 
vary_params() const132 const base::flat_set<std::string>& HttpNoVarySearchData::vary_params() const {
133   return vary_params_;
134 }
135 
vary_on_key_order() const136 bool HttpNoVarySearchData::vary_on_key_order() const {
137   return vary_on_key_order_;
138 }
vary_by_default() const139 bool HttpNoVarySearchData::vary_by_default() const {
140   return vary_by_default_;
141 }
142 
143 // static
144 base::expected<HttpNoVarySearchData, HttpNoVarySearchData::ParseErrorEnum>
ParseNoVarySearchDictionary(const structured_headers::Dictionary & dict)145 HttpNoVarySearchData::ParseNoVarySearchDictionary(
146     const structured_headers::Dictionary& dict) {
147   static constexpr const char* kKeyOrder = "key-order";
148   static constexpr const char* kParams = "params";
149   static constexpr const char* kExcept = "except";
150   constexpr std::string_view kValidKeys[] = {kKeyOrder, kParams, kExcept};
151 
152   base::flat_set<std::string> no_vary_params;
153   base::flat_set<std::string> vary_params;
154   bool vary_on_key_order = true;
155   bool vary_by_default = true;
156 
157   // If the dictionary contains unknown keys, maybe fail parsing.
158   const bool has_unrecognized_keys = !base::ranges::all_of(
159       dict,
160       [&](const auto& pair) { return base::Contains(kValidKeys, pair.first); });
161 
162   UMA_HISTOGRAM_BOOLEAN("Net.HttpNoVarySearch.HasUnrecognizedKeys",
163                         has_unrecognized_keys);
164   if (has_unrecognized_keys &&
165       !base::FeatureList::IsEnabled(
166           features::kNoVarySearchIgnoreUnrecognizedKeys)) {
167     return base::unexpected(ParseErrorEnum::kUnknownDictionaryKey);
168   }
169 
170   // Populate `vary_on_key_order` based on the `key-order` key.
171   if (dict.contains(kKeyOrder)) {
172     const auto& key_order = dict.at(kKeyOrder);
173     if (key_order.member_is_inner_list ||
174         !key_order.member[0].item.is_boolean()) {
175       return base::unexpected(ParseErrorEnum::kNonBooleanKeyOrder);
176     }
177     vary_on_key_order = !key_order.member[0].item.GetBoolean();
178   }
179 
180   // Populate `no_vary_params` or `vary_by_default` based on the "params" key.
181   if (dict.contains(kParams)) {
182     const auto& params = dict.at(kParams);
183     if (params.member_is_inner_list) {
184       auto keys = ParseStringList(params.member);
185       if (!keys.has_value()) {
186         return base::unexpected(ParseErrorEnum::kParamsNotStringList);
187       }
188       no_vary_params = std::move(*keys);
189     } else if (params.member[0].item.is_boolean()) {
190       vary_by_default = !params.member[0].item.GetBoolean();
191     } else {
192       return base::unexpected(ParseErrorEnum::kParamsNotStringList);
193     }
194   }
195 
196   // Populate `vary_params` based on the "except" key.
197   // This should be present only if "params" was true
198   // (i.e., params don't vary by default).
199   if (dict.contains(kExcept)) {
200     const auto& excepted_params = dict.at(kExcept);
201     if (vary_by_default) {
202       return base::unexpected(ParseErrorEnum::kExceptWithoutTrueParams);
203     }
204     if (!excepted_params.member_is_inner_list) {
205       return base::unexpected(ParseErrorEnum::kExceptNotStringList);
206     }
207     auto keys = ParseStringList(excepted_params.member);
208     if (!keys.has_value()) {
209       return base::unexpected(ParseErrorEnum::kExceptNotStringList);
210     }
211     vary_params = std::move(*keys);
212   }
213 
214   // "params" controls both `vary_by_default` and `no_vary_params`. Check to
215   // make sure that when "params" is a boolean, `no_vary_params` is empty.
216   if (!vary_by_default)
217     DCHECK(no_vary_params.empty());
218 
219   if (no_vary_params.empty() && vary_params.empty() && vary_by_default &&
220       vary_on_key_order) {
221     // If header is present but it's value is equivalent to only default values
222     // then it is the same as if there were no header present.
223     return base::unexpected(ParseErrorEnum::kDefaultValue);
224   }
225 
226   HttpNoVarySearchData no_vary_search;
227   no_vary_search.no_vary_params_ = std::move(no_vary_params);
228   no_vary_search.vary_params_ = std::move(vary_params);
229   no_vary_search.vary_on_key_order_ = vary_on_key_order;
230   no_vary_search.vary_by_default_ = vary_by_default;
231 
232   return base::ok(no_vary_search);
233 }
234 
235 }  // namespace net
236