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