Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(353)

Side by Side Diff: net/reporting/reporting_service.cc

Issue 2249213002: [OBSOLETE] Reporting: Initial implementation. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Try fixing unittest compile error on Android? Created 3 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « net/reporting/reporting_service.h ('k') | net/reporting/reporting_service_unittest.cc » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 // Copyright 2016 The Chromium Authors. All rights reserved.
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_service.h"
6
7 #include "base/bind.h"
8 #include "base/json/json_reader.h"
9 #include "base/json/json_writer.h"
10 #include "base/memory/ptr_util.h"
11 #include "base/metrics/histogram_macros.h"
12 #include "base/strings/string_number_conversions.h"
13 #include "base/strings/string_util.h"
14 #include "base/time/default_tick_clock.h"
15 #include "net/http/http_request_info.h"
16 #include "net/http/http_response_headers.h"
17 #include "net/http/http_response_info.h"
18 #include "net/url_request/url_fetcher.h"
19 #include "net/url_request/url_request_context_getter.h"
20
21 namespace {
22
23 const char kDefaultGroupName[] = "default";
24
25 // Per
26 // https://greenbytes.de/tech/webdav/draft-reschke-http-jfv-02.html#rfc.section. 4
27 // assuming |normalized_header| is the result of completing step 1.
28 std::unique_ptr<base::Value> ParseJFV(
29 const std::string& normalized_header_value) {
30 std::string value = "[" + normalized_header_value + "]";
31 return base::JSONReader::Read(value);
32 }
33
34 bool IsOriginSecure(const GURL& url) {
35 return url.SchemeIsCryptographic();
36 }
37
38 } // namespace
39
40 namespace net {
41
42 ReportingService::Policy::Policy()
43 : endpoint_lifetime(base::TimeDelta::FromDays(7)),
44 max_endpoint_failures(5),
45 max_endpoint_count(100),
46 report_lifetime(base::TimeDelta::FromHours(1)),
47 max_report_failures(5),
48 max_report_count(100),
49 persist_reports_across_network_changes(false) {
50 endpoint_backoff.num_errors_to_ignore = 0;
51 endpoint_backoff.initial_delay_ms = 5 * 1000;
52 endpoint_backoff.multiply_factor = 2.0;
53 endpoint_backoff.jitter_factor = 0.1;
54 endpoint_backoff.maximum_backoff_ms = 60 * 60 * 1000;
55 endpoint_backoff.entry_lifetime_ms = 7 * 24 * 60 * 60 * 1000;
56 endpoint_backoff.always_use_initial_delay = false;
57 }
58
59 // static
60 ReportingService::Policy ReportingService::Policy::GetNonBrowserDefault() {
61 Policy policy;
62 // In non-browser contexts, there is less concern that Reporting could
63 // accidentally track users across networks, so don't discard reports as
64 // readily.
65 policy.report_lifetime = base::TimeDelta::FromDays(2);
66 policy.persist_reports_across_network_changes = true;
67 return policy;
68 }
69
70 struct ReportingService::Client {
71 public:
72 Client(const GURL& origin,
73 bool subdomains,
74 const std::string& group,
75 base::TimeDelta ttl,
76 base::TimeTicks creation)
77 : origin(origin),
78 subdomains(subdomains),
79 group(group),
80 ttl(ttl),
81 creation(creation) {}
82
83 GURL origin;
84 bool subdomains;
85 std::string group;
86 base::TimeDelta ttl;
87 base::TimeTicks creation;
88
89 bool is_expired(base::TimeTicks now) const { return creation + ttl < now; }
90 };
91
92 struct ReportingService::Endpoint {
93 public:
94 Endpoint(const GURL& url,
95 const BackoffEntry::Policy& backoff_policy,
96 base::TickClock* clock)
97 : url(url),
98 backoff(&backoff_policy, clock),
99 last_used(clock->NowTicks()),
100 pending(false) {}
101 ~Endpoint() {}
102
103 const GURL url;
104
105 BackoffEntry backoff;
106 // For LRU eviction of endpoints.
107 base::TimeTicks last_used;
108 // Whether we currently have an upload in progress to this endpoint.
109 bool pending;
110
111 // Map from client.origin to client.
112 std::map<GURL, Client> clients;
113
114 bool is_expired(base::TimeTicks now) const {
115 for (auto& pair : clients)
116 if (!pair.second.is_expired(now))
117 return false;
118 return true;
119 }
120 };
121
122 class ReportingService::Report {
123 public:
124 Report() = default;
125 ~Report() = default;
126
127 // Report body. Can be any valid JSON. Passed to endpoint unmodified.
128 std::unique_ptr<base::Value> body;
129
130 // URL of content that triggered report. Passed to endpoint unmodified.
131 GURL url;
132
133 // Origin of content that triggered report. Should be url.GetOrigin(), but
134 // the spec doesn't guarantee this, and allows callers to provide URL and
135 // origin separately, so they are separate fields for now. Passed to
136 // endpoint unmodified.
137 GURL origin;
138
139 // Report group. Used to select endpoints for upload.
140 std::string group;
141
142 // Report type. Passed to endpoint unmodified.
143 std::string type;
144
145 // Timestamp when the report was queued. Passed to endpoint as a relative
146 // age.
147 base::TimeTicks queued;
148
149 // Number of delivery attempts made.
150 size_t attempts;
151
152 // Whether this report is currently part of a pending delivery attempt.
153 bool pending;
154
155 private:
156 DISALLOW_COPY_AND_ASSIGN(Report);
157 };
158
159 struct ReportingService::EndpointTuple {
160 GURL url;
161 bool subdomains;
162 base::TimeDelta ttl;
163 std::string group;
164
165 static bool FromDictionary(const base::DictionaryValue& dictionary,
166 EndpointTuple* tuple_out,
167 std::string* error_out);
168 static bool FromHeader(const std::string& header,
169 std::vector<EndpointTuple>* tuples_out,
170 std::vector<std::string>* errors_out);
171 };
172
173 struct ReportingService::Delivery {
174 Delivery(const GURL& endpoint_url, const std::vector<Report*>& reports)
175 : endpoint_url(endpoint_url), reports(reports) {}
176 ~Delivery() = default;
177
178 const GURL& endpoint_url;
179 const std::vector<Report*> reports;
180 };
181
182 ReportingService::ReportingService(const Policy& policy)
183 : policy_(policy), clock_(new base::DefaultTickClock()) {
184 NetworkChangeNotifier::AddNetworkChangeObserver(this);
185 }
186
187 ReportingService::~ReportingService() {
188 NetworkChangeNotifier::RemoveNetworkChangeObserver(this);
189 }
190
191 void ReportingService::set_uploader(
192 std::unique_ptr<ReportingUploader> uploader) {
193 uploader_ = std::move(uploader);
194 }
195
196 void ReportingService::QueueReport(std::unique_ptr<base::Value> body,
197 const GURL& url,
198 const GURL& origin,
199 const std::string& group,
200 const std::string& type) {
201 auto report = base::MakeUnique<Report>();
202 report->body = std::move(body);
203 report->url = url.GetAsReferrer();
204 report->origin = origin;
205 report->group = group;
206 report->type = type;
207 report->queued = clock_->NowTicks();
208 report->attempts = 0;
209 report->pending = false;
210 reports_.push_back(std::move(report));
211
212 CollectGarbage();
213 }
214
215 void ReportingService::ProcessHeader(const GURL& origin,
216 const std::string& header_value) {
217 if (!IsOriginSecure(origin))
218 return;
219
220 std::vector<std::string> errors;
221 std::vector<EndpointTuple> tuples;
222 if (!EndpointTuple::FromHeader(header_value, &tuples, &errors))
223 return;
224
225 // TODO: Plumb these out to the DevTools console somehow.
226 for (const std::string& error : errors) {
227 LOG(WARNING) << "Origin " << origin.spec() << " sent "
228 << "Report-To header with error: " << error << ": "
229 << header_value;
230 }
231
232 for (auto& tuple : tuples)
233 ProcessEndpointTuple(origin, tuple);
234
235 CollectGarbage();
236 }
237
238 void ReportingService::SendReports() {
239 if (!uploader_)
240 return;
241
242 base::TimeTicks now = clock_->NowTicks();
243
244 std::map<Endpoint*, std::vector<Report*>> endpoint_reports;
245 for (auto& report : reports_) {
246 // If the report is already contained in another pending upload, don't
247 // upload it twice.
248 if (report->pending)
249 continue;
250
251 Endpoint* endpoint = FindEndpointForReport(*report);
252 // If there's no available endpoint for the report, leave it for later.
253 if (!endpoint)
254 continue;
255
256 // If the chosen endpoint is pending, don't start another upload; let this
257 // report go in the next upload instead.
258 if (endpoint->pending)
259 continue;
260
261 report->pending = true;
262 endpoint_reports[endpoint].push_back(report.get());
263 }
264
265 for (auto& pair : endpoint_reports) {
266 Endpoint* endpoint = pair.first;
267 const std::vector<Report*>& reports = pair.second;
268
269 std::string json = SerializeReports(reports);
270
271 uploader_->AttemptDelivery(
272 endpoint->url, json,
273 base::Bind(&ReportingService::OnDeliveryAttemptComplete,
274 base::Unretained(this),
275 base::MakeUnique<Delivery>(endpoint->url, reports)));
276
277 endpoint->last_used = now;
278
279 for (auto& report : reports)
280 ++report->attempts;
281 }
282 }
283
284 void ReportingService::OnNetworkChanged(
285 NetworkChangeNotifier::ConnectionType connection_type) {
286 if (policy_.persist_reports_across_network_changes)
287 return;
288
289 reports_.clear();
290 }
291
292 void ReportingService::set_clock_for_testing(
293 std::unique_ptr<base::TickClock> clock) {
294 clock_ = std::move(clock);
295 }
296
297 size_t ReportingService::GetReportCountForTesting() {
298 return reports_.size();
299 }
300
301 bool ReportingService::HasEndpointForTesting(const GURL& endpoint_url) {
302 return GetEndpointByURL(endpoint_url) != nullptr;
303 }
304
305 bool ReportingService::HasClientForTesting(const GURL& endpoint_url,
306 const GURL& origin) {
307 Endpoint* endpoint = GetEndpointByURL(endpoint_url);
308 if (!endpoint)
309 return false;
310 return endpoint->clients.count(origin) > 0;
311 }
312
313 int ReportingService::GetEndpointFailuresForTesting(const GURL& endpoint_url) {
314 Endpoint* endpoint = GetEndpointByURL(endpoint_url);
315 if (!endpoint)
316 return -1;
317 return endpoint->backoff.failure_count();
318 }
319
320 void ReportingService::CollectGarbageForTesting() {
321 CollectGarbage();
322 }
323
324 // static
325 bool ReportingService::EndpointTuple::FromDictionary(
326 const base::DictionaryValue& dictionary,
327 EndpointTuple* tuple_out,
328 std::string* error_out) {
329 if (!dictionary.HasKey("url")) {
330 *error_out = "url missing";
331 return false;
332 }
333 std::string url_string;
334 if (!dictionary.GetString("url", &url_string)) {
335 *error_out = "url not a string";
336 return false;
337 }
338 tuple_out->url = GURL(url_string);
339
340 tuple_out->group = kDefaultGroupName;
341 if (dictionary.HasKey("group")) {
342 if (!dictionary.GetString("group", &tuple_out->group)) {
343 *error_out = "group present but not a string";
344 return false;
345 }
346 }
347
348 tuple_out->subdomains = false;
349 if (dictionary.HasKey("includeSubdomains")) {
350 if (!dictionary.GetBoolean("includeSubdomains", &tuple_out->subdomains)) {
351 *error_out = "includeSubdomains present but not boolean";
352 return false;
353 }
354 }
355
356 if (!dictionary.HasKey("max-age")) {
357 *error_out = "max-age missing";
358 return false;
359 }
360 int ttl_sec;
361 if (!dictionary.GetInteger("max-age", &ttl_sec)) {
362 *error_out = "max-age not an integer";
363 return false;
364 }
365 tuple_out->ttl = base::TimeDelta::FromSeconds(ttl_sec);
366
367 return true;
368 }
369
370 // static
371 bool ReportingService::EndpointTuple::FromHeader(
372 const std::string& header,
373 std::vector<ReportingService::EndpointTuple>* tuples_out,
374 std::vector<std::string>* errors_out) {
375 tuples_out->clear();
376 errors_out->clear();
377
378 std::unique_ptr<base::Value> value(ParseJFV(header));
379 if (!value) {
380 errors_out->push_back("failed to parse JSON field value.");
381 return false;
382 }
383
384 base::ListValue* list;
385 bool was_list = value->GetAsList(&list);
386 DCHECK(was_list);
387
388 base::DictionaryValue* item;
389 for (size_t i = 0; i < list->GetSize(); i++) {
390 std::string error_prefix = "endpoint " + base::SizeTToString(i + 1) +
391 " of " + base::SizeTToString(list->GetSize()) +
392 ": ";
393 if (!list->GetDictionary(i, &item)) {
394 errors_out->push_back(error_prefix + "is not a dictionary");
395 continue;
396 }
397 EndpointTuple tuple;
398 std::string error;
399 if (!EndpointTuple::FromDictionary(*item, &tuple, &error)) {
400 errors_out->push_back(error_prefix + error);
401 continue;
402 }
403 if (!IsOriginSecure(tuple.url)) {
404 errors_out->push_back(error_prefix + "url " + tuple.url.spec() +
405 " is insecure");
406 continue;
407 }
408 if (tuple.ttl < base::TimeDelta()) {
409 errors_out->push_back(error_prefix + "ttl is negative");
410 continue;
411 }
412 tuples_out->push_back(tuple);
413 }
414 return true;
415 }
416
417 void ReportingService::ProcessEndpointTuple(const GURL& origin,
418 const EndpointTuple& tuple) {
419 Endpoint* endpoint = GetEndpointByURL(tuple.url);
420
421 bool endpoint_exists = endpoint != nullptr;
422 bool client_exists = endpoint && endpoint->clients.count(origin) > 0;
423
424 if (client_exists)
425 endpoint->clients.erase(origin);
426
427 if (tuple.ttl <= base::TimeDelta())
428 return;
429
430 if (!endpoint_exists) {
431 endpoint = new Endpoint(tuple.url, policy_.endpoint_backoff, clock_.get());
432 endpoints_.insert(std::make_pair(tuple.url, base::WrapUnique(endpoint)));
433 }
434
435 base::TimeTicks now = clock_->NowTicks();
436
437 Client client(origin, tuple.subdomains, tuple.group, tuple.ttl, now);
438 endpoint->clients.insert(std::make_pair(origin, client));
439 }
440
441 void ReportingService::OnDeliveryAttemptComplete(
442 const std::unique_ptr<Delivery>& delivery,
443 ReportingUploader::Outcome outcome) {
444 for (auto report : delivery->reports) {
445 DCHECK(report->pending);
446 report->pending = false;
447 }
448
449 Endpoint* endpoint = GetEndpointByURL(delivery->endpoint_url);
450 if (endpoint) {
451 endpoint->backoff.InformOfRequest(outcome == ReportingUploader::SUCCESS);
452 endpoint->pending = false;
453 }
454
455 switch (outcome) {
456 case ReportingUploader::SUCCESS:
457 for (auto report : delivery->reports)
458 DequeueReport(report);
459 break;
460 case ReportingUploader::FAILURE:
461 // Reports have been marked not-pending and can be retried later.
462 // BackoffEntry has been informed of failure.
463 break;
464 case ReportingUploader::REMOVE_ENDPOINT:
465 // Note: This is not specified, but seems the obvious intention.
466 if (endpoint)
467 endpoints_.erase(delivery->endpoint_url);
468 break;
469 }
470
471 CollectGarbage();
472 }
473
474 void ReportingService::CollectGarbage() {
475 base::TimeTicks now = clock_->NowTicks();
476
477 {
478 std::vector<Report*> to_dequeue;
479
480 for (auto it = reports_.begin(); it != reports_.end(); ++it) {
481 Report* report = it->get();
482 base::TimeDelta age = now - report->queued;
483
484 if (report->pending)
485 continue;
486
487 if (report->attempts > policy_.max_report_failures ||
488 age > policy_.report_lifetime) {
489 to_dequeue.push_back(report);
490 }
491 }
492
493 for (auto it = reports_.begin();
494 it != reports_.end() &&
495 reports_.size() - to_dequeue.size() > policy_.max_report_count;
496 ++it) {
497 if (it->get()->pending)
498 continue;
499
500 to_dequeue.push_back(it->get());
501 }
502
503 for (auto it : to_dequeue)
504 DequeueReport(it);
505 }
506
507 // TODO: Individually garbage-collect clients.
508
509 {
510 std::vector<EndpointMap::iterator> to_erase;
511 for (auto it = endpoints_.begin(); it != endpoints_.end(); ++it) {
512 Endpoint* endpoint = it->second.get();
513
514 if (endpoint->pending)
515 continue;
516
517 // Don't remove failed endpoints until the BackoffEntry okays it, to
518 // avoid hammering failing endpoints by removing and re-adding them
519 // constantly.
520 if (endpoint->is_expired(now) ||
521 now - endpoint->last_used > policy_.endpoint_lifetime ||
522 (endpoint->backoff.CanDiscard() &&
523 endpoint->backoff.failure_count() > policy_.max_endpoint_failures)) {
524 to_erase.push_back(it);
525 }
526 }
527
528 while (endpoints_.size() - to_erase.size() > policy_.max_endpoint_count) {
529 auto oldest_it = endpoints_.end();
530
531 for (auto it = endpoints_.begin(); it != endpoints_.end(); ++it) {
532 if (it->second->pending)
533 continue;
534
535 if (oldest_it == endpoints_.end() ||
536 it->second->last_used < oldest_it->second->last_used) {
537 oldest_it = it;
538 }
539 }
540
541 if (oldest_it == endpoints_.end())
542 break;
543
544 to_erase.push_back(oldest_it);
545
546 // Gross kludge: Keep us from choosing this endpoint again.
547 oldest_it->second->pending = true;
548 }
549
550 for (auto it : to_erase)
551 endpoints_.erase(it);
552 }
553 }
554
555 ReportingService::Endpoint* ReportingService::FindEndpointForReport(
556 const Report& report) {
557 // TODO: This is O(count of all endpoints, regardless of client origins).
558 // TODO: The spec doesn't prioritize *.bar.foo.com over *.foo.com when
559 // choosing which endpoint to upload a report for baz.bar.foo.com to.
560 // That seems wrong, but we need clarification on the spec end.
561 for (auto& pair : endpoints_) {
562 Endpoint* endpoint = pair.second.get();
563 if (endpoint->is_expired(clock_->NowTicks()) ||
564 endpoint->backoff.ShouldRejectRequest() ||
565 !DoesEndpointMatchReport(*endpoint, report)) {
566 continue;
567 }
568 return endpoint;
569 }
570 return nullptr;
571 }
572
573 bool ReportingService::DoesEndpointMatchReport(const Endpoint& endpoint,
574 const Report& report) {
575 for (auto& pair : endpoint.clients) {
576 const Client& client = pair.second;
577 if (client.is_expired(clock_->NowTicks()))
578 continue;
579 if (!base::EqualsCaseInsensitiveASCII(client.group, report.group))
580 continue;
581 if (client.origin == report.origin)
582 return true;
583 if (client.subdomains && report.origin.DomainIs(client.origin.host_piece()))
584 return true;
585 }
586 return false;
587 }
588
589 std::string ReportingService::SerializeReports(
590 const std::vector<Report*>& reports) {
591 base::ListValue collection;
592 for (auto& report : reports) {
593 std::unique_ptr<base::DictionaryValue> data(new base::DictionaryValue());
594 data->SetInteger("age",
595 (clock_->NowTicks() - report->queued).InMilliseconds());
596 data->SetString("type", report->type);
597 data->SetString("url", report->url.spec());
598 data->Set("report", report->body->DeepCopy());
599 collection.Append(std::move(data));
600 }
601
602 std::string json = "";
603 bool written = base::JSONWriter::Write(collection, &json);
604 DCHECK(written);
605 return json;
606 }
607
608 ReportingService::Endpoint* ReportingService::GetEndpointByURL(
609 const GURL& url) {
610 auto it = endpoints_.find(url);
611 if (it == endpoints_.end())
612 return nullptr;
613 return it->second.get();
614 }
615
616 void ReportingService::DequeueReport(Report* report) {
617 // TODO: This is O(N).
618 for (auto it = reports_.begin(); it != reports_.end(); ++it) {
619 if (it->get() == report) {
620 reports_.erase(it);
621 return;
622 }
623 }
624 }
625
626 } // namespace net
OLDNEW
« no previous file with comments | « net/reporting/reporting_service.h ('k') | net/reporting/reporting_service_unittest.cc » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698