1 // Copyright 2017 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/reporting/reporting_header_parser.h"
6
7 #include <cstring>
8 #include <string>
9 #include <utility>
10 #include <vector>
11
12 #include "base/check.h"
13 #include "base/feature_list.h"
14 #include "base/functional/bind.h"
15 #include "base/json/json_reader.h"
16 #include "base/metrics/histogram_functions.h"
17 #include "base/time/time.h"
18 #include "base/values.h"
19 #include "net/base/features.h"
20 #include "net/base/isolation_info.h"
21 #include "net/base/network_anonymization_key.h"
22 #include "net/base/registry_controlled_domains/registry_controlled_domain.h"
23 #include "net/reporting/reporting_cache.h"
24 #include "net/reporting/reporting_context.h"
25 #include "net/reporting/reporting_delegate.h"
26 #include "net/reporting/reporting_endpoint.h"
27
28 namespace net {
29
30 namespace {
31
32 const char kUrlKey[] = "url";
33 const char kIncludeSubdomainsKey[] = "include_subdomains";
34 const char kEndpointsKey[] = "endpoints";
35 const char kGroupKey[] = "group";
36 const char kDefaultGroupName[] = "default";
37 const char kMaxAgeKey[] = "max_age";
38 const char kPriorityKey[] = "priority";
39 const char kWeightKey[] = "weight";
40
41 // Processes a single endpoint url string parsed from header.
42 //
43 // |endpoint_url_string| is the string value of the endpoint URL.
44 // |header_origin_url| is the origin URL that sent the header.
45 //
46 // |endpoint_url_out| is the endpoint URL parsed out of the string.
47 // Returns true on success or false if url was invalid.
ProcessEndpointURLString(const std::string & endpoint_url_string,const url::Origin & header_origin,GURL & endpoint_url_out)48 bool ProcessEndpointURLString(const std::string& endpoint_url_string,
49 const url::Origin& header_origin,
50 GURL& endpoint_url_out) {
51 // Support path-absolute-URL string with exactly one leading "/"
52 if (std::strspn(endpoint_url_string.c_str(), "/") == 1) {
53 endpoint_url_out = header_origin.GetURL().Resolve(endpoint_url_string);
54 } else {
55 endpoint_url_out = GURL(endpoint_url_string);
56 }
57 if (!endpoint_url_out.is_valid())
58 return false;
59 if (!endpoint_url_out.SchemeIsCryptographic())
60 return false;
61 return true;
62 }
63
64 // Processes a single endpoint tuple received in a Report-To header.
65 //
66 // |origin| is the origin that sent the Report-To header.
67 //
68 // |value| is the parsed JSON value of the endpoint tuple.
69 //
70 // |*endpoint_info_out| will contain the endpoint URL parsed out of the tuple.
71 // Returns true on success or false if endpoint was discarded.
ProcessEndpoint(ReportingDelegate * delegate,const ReportingEndpointGroupKey & group_key,const base::Value & value,ReportingEndpoint::EndpointInfo * endpoint_info_out)72 bool ProcessEndpoint(ReportingDelegate* delegate,
73 const ReportingEndpointGroupKey& group_key,
74 const base::Value& value,
75 ReportingEndpoint::EndpointInfo* endpoint_info_out) {
76 const base::Value::Dict* dict = value.GetIfDict();
77 if (!dict)
78 return false;
79
80 const std::string* endpoint_url_string = dict->FindString(kUrlKey);
81 if (!endpoint_url_string)
82 return false;
83
84 GURL endpoint_url;
85 if (!ProcessEndpointURLString(*endpoint_url_string, group_key.origin,
86 endpoint_url)) {
87 return false;
88 }
89 endpoint_info_out->url = std::move(endpoint_url);
90
91 int priority = ReportingEndpoint::EndpointInfo::kDefaultPriority;
92 if (const base::Value* priority_value = dict->Find(kPriorityKey)) {
93 if (!priority_value->is_int())
94 return false;
95 priority = priority_value->GetInt();
96 }
97 if (priority < 0)
98 return false;
99 endpoint_info_out->priority = priority;
100
101 int weight = ReportingEndpoint::EndpointInfo::kDefaultWeight;
102 if (const base::Value* weight_value = dict->Find(kWeightKey)) {
103 if (!weight_value->is_int())
104 return false;
105 weight = weight_value->GetInt();
106 }
107 if (weight < 0)
108 return false;
109 endpoint_info_out->weight = weight;
110
111 return delegate->CanSetClient(group_key.origin, endpoint_info_out->url);
112 }
113
114 // Processes a single endpoint group tuple received in a Report-To header.
115 //
116 // |origin| is the origin that sent the Report-To header.
117 //
118 // |value| is the parsed JSON value of the endpoint group tuple.
119 // Returns true on successfully adding a non-empty group, or false if endpoint
120 // group was discarded or processed as a deletion.
ProcessEndpointGroup(ReportingDelegate * delegate,ReportingCache * cache,const NetworkAnonymizationKey & network_anonymization_key,const url::Origin & origin,const base::Value & value,ReportingEndpointGroup * parsed_endpoint_group_out)121 bool ProcessEndpointGroup(
122 ReportingDelegate* delegate,
123 ReportingCache* cache,
124 const NetworkAnonymizationKey& network_anonymization_key,
125 const url::Origin& origin,
126 const base::Value& value,
127 ReportingEndpointGroup* parsed_endpoint_group_out) {
128 const base::Value::Dict* dict = value.GetIfDict();
129 if (!dict)
130 return false;
131
132 std::string group_name = kDefaultGroupName;
133 if (const base::Value* maybe_group_name = dict->Find(kGroupKey)) {
134 if (!maybe_group_name->is_string())
135 return false;
136 group_name = maybe_group_name->GetString();
137 }
138 ReportingEndpointGroupKey group_key(network_anonymization_key, origin,
139 group_name);
140 parsed_endpoint_group_out->group_key = group_key;
141
142 int ttl_sec = dict->FindInt(kMaxAgeKey).value_or(-1);
143 if (ttl_sec < 0)
144 return false;
145 // max_age: 0 signifies removal of the endpoint group.
146 if (ttl_sec == 0) {
147 cache->RemoveEndpointGroup(group_key);
148 return false;
149 }
150 parsed_endpoint_group_out->ttl = base::Seconds(ttl_sec);
151
152 absl::optional<bool> subdomains_bool = dict->FindBool(kIncludeSubdomainsKey);
153 if (subdomains_bool && subdomains_bool.value()) {
154 // Disallow eTLDs from setting include_subdomains endpoint groups.
155 if (registry_controlled_domains::GetRegistryLength(
156 origin.GetURL(),
157 registry_controlled_domains::INCLUDE_UNKNOWN_REGISTRIES,
158 registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES) == 0) {
159 return false;
160 }
161
162 parsed_endpoint_group_out->include_subdomains = OriginSubdomains::INCLUDE;
163 }
164
165 const base::Value::List* endpoint_list = dict->FindList(kEndpointsKey);
166 if (!endpoint_list)
167 return false;
168
169 std::vector<ReportingEndpoint::EndpointInfo> endpoints;
170
171 for (const base::Value& endpoint : *endpoint_list) {
172 ReportingEndpoint::EndpointInfo parsed_endpoint;
173 if (ProcessEndpoint(delegate, group_key, endpoint, &parsed_endpoint))
174 endpoints.push_back(std::move(parsed_endpoint));
175 }
176
177 // Remove the group if it is empty.
178 if (endpoints.empty()) {
179 cache->RemoveEndpointGroup(group_key);
180 return false;
181 }
182
183 parsed_endpoint_group_out->endpoints = std::move(endpoints);
184
185 return true;
186 }
187
188 // Processes a single endpoint tuple received in a Reporting-Endpoints header.
189 //
190 // |group_key| is the key for the endpoint group this endpoint belongs.
191 // |endpoint_url_string| is the endpoint url as received in the header.
192 //
193 // |endpoint_info_out| is the endpoint info parsed out of the value.
ProcessEndpoint(ReportingDelegate * delegate,const ReportingEndpointGroupKey & group_key,const std::string & endpoint_url_string,ReportingEndpoint::EndpointInfo & endpoint_info_out)194 bool ProcessEndpoint(ReportingDelegate* delegate,
195 const ReportingEndpointGroupKey& group_key,
196 const std::string& endpoint_url_string,
197 ReportingEndpoint::EndpointInfo& endpoint_info_out) {
198 if (endpoint_url_string.empty())
199 return false;
200
201 GURL endpoint_url;
202 if (!ProcessEndpointURLString(endpoint_url_string, group_key.origin,
203 endpoint_url)) {
204 return false;
205 }
206 endpoint_info_out.url = std::move(endpoint_url);
207 // Reporting-Endpoints endpoint doesn't have prioirty/weight so set to
208 // default.
209 endpoint_info_out.priority =
210 ReportingEndpoint::EndpointInfo::kDefaultPriority;
211 endpoint_info_out.weight = ReportingEndpoint::EndpointInfo::kDefaultWeight;
212
213 return delegate->CanSetClient(group_key.origin, endpoint_info_out.url);
214 }
215
216 // Process a single endpoint received in a Reporting-Endpoints header.
ProcessV1Endpoint(ReportingDelegate * delegate,ReportingCache * cache,const base::UnguessableToken & reporting_source,const NetworkAnonymizationKey & network_anonymization_key,const url::Origin & origin,const std::string & endpoint_name,const std::string & endpoint_url_string,ReportingEndpoint & parsed_endpoint_out)217 bool ProcessV1Endpoint(ReportingDelegate* delegate,
218 ReportingCache* cache,
219 const base::UnguessableToken& reporting_source,
220 const NetworkAnonymizationKey& network_anonymization_key,
221 const url::Origin& origin,
222 const std::string& endpoint_name,
223 const std::string& endpoint_url_string,
224 ReportingEndpoint& parsed_endpoint_out) {
225 DCHECK(!reporting_source.is_empty());
226 ReportingEndpointGroupKey group_key(network_anonymization_key,
227 reporting_source, origin, endpoint_name);
228 parsed_endpoint_out.group_key = group_key;
229
230 ReportingEndpoint::EndpointInfo parsed_endpoint;
231
232 if (!ProcessEndpoint(delegate, group_key, endpoint_url_string,
233 parsed_endpoint)) {
234 return false;
235 }
236 parsed_endpoint_out.info = std::move(parsed_endpoint);
237 return true;
238 }
239
240 } // namespace
241
242 absl::optional<base::flat_map<std::string, std::string>>
ParseReportingEndpoints(const std::string & header)243 ParseReportingEndpoints(const std::string& header) {
244 // Ignore empty header values. Skip logging metric to maintain parity with
245 // ReportingHeaderType::kReportToInvalid.
246 if (header.empty())
247 return absl::nullopt;
248 absl::optional<structured_headers::Dictionary> header_dict =
249 structured_headers::ParseDictionary(header);
250 if (!header_dict) {
251 ReportingHeaderParser::RecordReportingHeaderType(
252 ReportingHeaderParser::ReportingHeaderType::kReportingEndpointsInvalid);
253 return absl::nullopt;
254 }
255 base::flat_map<std::string, std::string> parsed_header;
256 for (const structured_headers::DictionaryMember& entry : *header_dict) {
257 if (entry.second.member_is_inner_list ||
258 !entry.second.member.front().item.is_string()) {
259 ReportingHeaderParser::RecordReportingHeaderType(
260 ReportingHeaderParser::ReportingHeaderType::
261 kReportingEndpointsInvalid);
262 return absl::nullopt;
263 }
264 const std::string& endpoint_url_string =
265 entry.second.member.front().item.GetString();
266 parsed_header[entry.first] = endpoint_url_string;
267 }
268 return parsed_header;
269 }
270
271 // static
RecordReportingHeaderType(ReportingHeaderType header_type)272 void ReportingHeaderParser::RecordReportingHeaderType(
273 ReportingHeaderType header_type) {
274 base::UmaHistogramEnumeration("Net.Reporting.HeaderType", header_type);
275 }
276
277 // static
ParseReportToHeader(ReportingContext * context,const NetworkAnonymizationKey & network_anonymization_key,const url::Origin & origin,const base::Value::List & list)278 void ReportingHeaderParser::ParseReportToHeader(
279 ReportingContext* context,
280 const NetworkAnonymizationKey& network_anonymization_key,
281 const url::Origin& origin,
282 const base::Value::List& list) {
283 DCHECK(GURL::SchemeIsCryptographic(origin.scheme()));
284
285 ReportingDelegate* delegate = context->delegate();
286 ReportingCache* cache = context->cache();
287
288 std::vector<ReportingEndpointGroup> parsed_header;
289
290 for (const auto& group_value : list) {
291 ReportingEndpointGroup parsed_endpoint_group;
292 if (ProcessEndpointGroup(delegate, cache, network_anonymization_key, origin,
293 group_value, &parsed_endpoint_group)) {
294 parsed_header.push_back(std::move(parsed_endpoint_group));
295 }
296 }
297
298 if (parsed_header.empty() && list.size() > 0) {
299 RecordReportingHeaderType(ReportingHeaderType::kReportToInvalid);
300 }
301
302 // Remove the client if it has no valid endpoint groups.
303 if (parsed_header.empty()) {
304 cache->RemoveClient(network_anonymization_key, origin);
305 return;
306 }
307
308 RecordReportingHeaderType(ReportingHeaderType::kReportTo);
309
310 cache->OnParsedHeader(network_anonymization_key, origin,
311 std::move(parsed_header));
312 }
313
314 // static
ProcessParsedReportingEndpointsHeader(ReportingContext * context,const base::UnguessableToken & reporting_source,const IsolationInfo & isolation_info,const NetworkAnonymizationKey & network_anonymization_key,const url::Origin & origin,base::flat_map<std::string,std::string> header)315 void ReportingHeaderParser::ProcessParsedReportingEndpointsHeader(
316 ReportingContext* context,
317 const base::UnguessableToken& reporting_source,
318 const IsolationInfo& isolation_info,
319 const NetworkAnonymizationKey& network_anonymization_key,
320 const url::Origin& origin,
321 base::flat_map<std::string, std::string> header) {
322 DCHECK(base::FeatureList::IsEnabled(net::features::kDocumentReporting));
323 DCHECK(GURL::SchemeIsCryptographic(origin.scheme()));
324 DCHECK(!reporting_source.is_empty());
325 DCHECK(network_anonymization_key.IsEmpty() ||
326 network_anonymization_key ==
327 isolation_info.network_anonymization_key());
328
329 ReportingDelegate* delegate = context->delegate();
330 ReportingCache* cache = context->cache();
331
332 std::vector<ReportingEndpoint> parsed_header;
333
334 for (const auto& member : header) {
335 ReportingEndpoint parsed_endpoint;
336 if (ProcessV1Endpoint(delegate, cache, reporting_source,
337 network_anonymization_key, origin, member.first,
338 member.second, parsed_endpoint)) {
339 parsed_header.push_back(std::move(parsed_endpoint));
340 }
341 }
342
343 if (parsed_header.empty()) {
344 RecordReportingHeaderType(ReportingHeaderType::kReportingEndpointsInvalid);
345 return;
346 }
347
348 RecordReportingHeaderType(ReportingHeaderType::kReportingEndpoints);
349 cache->OnParsedReportingEndpointsHeader(reporting_source, isolation_info,
350 std::move(parsed_header));
351 }
352
353 } // namespace net
354