Reporting: Use new format for Report-To header

The format of the Report-To header recently changed so that endpoint
groups are the top-level element, instead of individual endpoints.

Bug: 704259
Change-Id: I7534c46abbbb86c367931ffc060972ac36b1084b
Reviewed-on: https://chromium-review.googlesource.com/914687
Commit-Queue: Douglas Creager <[email protected]>
Reviewed-by: Robert Kaplow <[email protected]>
Reviewed-by: Julia Tuttle <[email protected]>
Cr-Commit-Position: refs/heads/master@{#539876}
diff --git a/net/reporting/reporting_header_parser.cc b/net/reporting/reporting_header_parser.cc
index df3f00e0..e482a55e 100644
--- a/net/reporting/reporting_header_parser.cc
+++ b/net/reporting/reporting_header_parser.cc
@@ -37,16 +37,34 @@
                             HeaderOutcome::MAX);
 }
 
+enum class HeaderEndpointGroupOutcome {
+  DISCARDED_NOT_DICTIONARY = 0,
+  DISCARDED_GROUP_NOT_STRING = 1,
+  DISCARDED_TTL_MISSING = 2,
+  DISCARDED_TTL_NOT_INTEGER = 3,
+  DISCARDED_TTL_NEGATIVE = 4,
+  DISCARDED_ENDPOINTS_MISSING = 5,
+  DISCARDED_ENDPOINTS_NOT_LIST = 6,
+  PARSED = 7,
+
+  MAX
+};
+
+void RecordHeaderEndpointGroupOutcome(HeaderEndpointGroupOutcome outcome) {
+  UMA_HISTOGRAM_ENUMERATION("Reporting.HeaderEndpointGroupOutcome", outcome,
+                            HeaderEndpointGroupOutcome::MAX);
+}
+
 enum class HeaderEndpointOutcome {
   DISCARDED_NOT_DICTIONARY = 0,
-  DISCARDED_ENDPOINT_MISSING = 1,
-  DISCARDED_ENDPOINT_NOT_STRING = 2,
-  DISCARDED_ENDPOINT_INVALID = 3,
-  DISCARDED_ENDPOINT_INSECURE = 4,
-  DISCARDED_TTL_MISSING = 5,
-  DISCARDED_TTL_NOT_INTEGER = 6,
-  DISCARDED_TTL_NEGATIVE = 7,
-  DISCARDED_GROUP_NOT_STRING = 8,
+  DISCARDED_ENDPOINT_MISSING = 1,     // obsolete
+  DISCARDED_ENDPOINT_NOT_STRING = 2,  // obsolete
+  DISCARDED_ENDPOINT_INVALID = 3,     // obsolete
+  DISCARDED_ENDPOINT_INSECURE = 4,    // obsolete
+  DISCARDED_TTL_MISSING = 5,          // obsolete
+  DISCARDED_TTL_NOT_INTEGER = 6,      // obsolete
+  DISCARDED_TTL_NEGATIVE = 7,         // obsolete
+  DISCARDED_GROUP_NOT_STRING = 8,     // obsolete
   REMOVED = 9,
   SET_REJECTED_BY_DELEGATE = 10,
   SET = 11,
@@ -55,6 +73,11 @@
   DISCARDED_WEIGHT_NOT_INTEGER = 13,
   DISCARDED_WEIGHT_NOT_POSITIVE = 14,
 
+  DISCARDED_URL_MISSING = 15,
+  DISCARDED_URL_NOT_STRING = 16,
+  DISCARDED_URL_INVALID = 17,
+  DISCARDED_URL_INSECURE = 18,
+
   MAX
 };
 
@@ -70,7 +93,8 @@
 }
 
 const char kUrlKey[] = "url";
-const char kIncludeSubdomainsKey[] = "includeSubdomains";
+const char kIncludeSubdomainsKey[] = "include-subdomains";
+const char kEndpointsKey[] = "endpoints";
 const char kGroupKey[] = "group";
 const char kGroupDefaultValue[] = "default";
 const char kMaxAgeKey[] = "max-age";
@@ -87,6 +111,9 @@
 HeaderEndpointOutcome ProcessEndpoint(ReportingDelegate* delegate,
                                       ReportingCache* cache,
                                       base::TimeTicks now,
+                                      const std::string& group,
+                                      int ttl_sec,
+                                      ReportingClient::Subdomains subdomains,
                                       const url::Origin& origin,
                                       const base::Value& value,
                                       GURL* endpoint_url_out) {
@@ -99,35 +126,15 @@
 
   std::string endpoint_url_string;
   if (!dict->HasKey(kUrlKey))
-    return HeaderEndpointOutcome::DISCARDED_ENDPOINT_MISSING;
+    return HeaderEndpointOutcome::DISCARDED_URL_MISSING;
   if (!dict->GetString(kUrlKey, &endpoint_url_string))
-    return HeaderEndpointOutcome::DISCARDED_ENDPOINT_NOT_STRING;
+    return HeaderEndpointOutcome::DISCARDED_URL_NOT_STRING;
 
   GURL endpoint_url(endpoint_url_string);
   if (!endpoint_url.is_valid())
-    return HeaderEndpointOutcome::DISCARDED_ENDPOINT_INVALID;
+    return HeaderEndpointOutcome::DISCARDED_URL_INVALID;
   if (!endpoint_url.SchemeIsCryptographic())
-    return HeaderEndpointOutcome::DISCARDED_ENDPOINT_INSECURE;
-
-  int ttl_sec = -1;
-  if (!dict->HasKey(kMaxAgeKey))
-    return HeaderEndpointOutcome::DISCARDED_TTL_MISSING;
-  if (!dict->GetInteger(kMaxAgeKey, &ttl_sec))
-    return HeaderEndpointOutcome::DISCARDED_TTL_NOT_INTEGER;
-  if (ttl_sec < 0)
-    return HeaderEndpointOutcome::DISCARDED_TTL_NEGATIVE;
-
-  std::string group = kGroupDefaultValue;
-  if (dict->HasKey(kGroupKey) && !dict->GetString(kGroupKey, &group))
-    return HeaderEndpointOutcome::DISCARDED_GROUP_NOT_STRING;
-
-  ReportingClient::Subdomains subdomains = ReportingClient::Subdomains::EXCLUDE;
-  bool subdomains_bool = false;
-  if (dict->HasKey(kIncludeSubdomainsKey) &&
-      dict->GetBoolean(kIncludeSubdomainsKey, &subdomains_bool) &&
-      subdomains_bool == true) {
-    subdomains = ReportingClient::Subdomains::INCLUDE;
-  }
+    return HeaderEndpointOutcome::DISCARDED_URL_INSECURE;
 
   int priority = ReportingClient::kDefaultPriority;
   if (dict->HasKey(kPriorityKey) && !dict->GetInteger(kPriorityKey, &priority))
@@ -155,6 +162,65 @@
   return HeaderEndpointOutcome::SET;
 }
 
+// Processes a single endpoint group tuple received in a Report-To header.
+//
+// |origin| is the origin that sent the Report-To header.
+//
+// |value| is the parsed JSON value of the endpoint group tuple.
+HeaderEndpointGroupOutcome ProcessEndpointGroup(ReportingDelegate* delegate,
+                                                ReportingCache* cache,
+                                                std::set<GURL>* new_endpoints,
+                                                base::TimeTicks now,
+                                                const url::Origin& origin,
+                                                const base::Value& value) {
+  const base::DictionaryValue* dict = nullptr;
+  if (!value.GetAsDictionary(&dict))
+    return HeaderEndpointGroupOutcome::DISCARDED_NOT_DICTIONARY;
+  DCHECK(dict);
+
+  std::string group = kGroupDefaultValue;
+  if (dict->HasKey(kGroupKey) && !dict->GetString(kGroupKey, &group))
+    return HeaderEndpointGroupOutcome::DISCARDED_GROUP_NOT_STRING;
+
+  int ttl_sec = -1;
+  if (!dict->HasKey(kMaxAgeKey))
+    return HeaderEndpointGroupOutcome::DISCARDED_TTL_MISSING;
+  if (!dict->GetInteger(kMaxAgeKey, &ttl_sec))
+    return HeaderEndpointGroupOutcome::DISCARDED_TTL_NOT_INTEGER;
+  if (ttl_sec < 0)
+    return HeaderEndpointGroupOutcome::DISCARDED_TTL_NEGATIVE;
+
+  ReportingClient::Subdomains subdomains = ReportingClient::Subdomains::EXCLUDE;
+  bool subdomains_bool = false;
+  if (dict->HasKey(kIncludeSubdomainsKey) &&
+      dict->GetBoolean(kIncludeSubdomainsKey, &subdomains_bool) &&
+      subdomains_bool == true) {
+    subdomains = ReportingClient::Subdomains::INCLUDE;
+  }
+
+  const base::ListValue* endpoint_list = nullptr;
+  if (!dict->HasKey(kEndpointsKey))
+    return HeaderEndpointGroupOutcome::DISCARDED_ENDPOINTS_MISSING;
+  if (!dict->GetList(kEndpointsKey, &endpoint_list))
+    return HeaderEndpointGroupOutcome::DISCARDED_ENDPOINTS_NOT_LIST;
+
+  for (size_t i = 0; i < endpoint_list->GetSize(); i++) {
+    const base::Value* endpoint = nullptr;
+    bool got_endpoint = endpoint_list->Get(i, &endpoint);
+    DCHECK(got_endpoint);
+    GURL endpoint_url;
+
+    HeaderEndpointOutcome outcome =
+        ProcessEndpoint(delegate, cache, now, group, ttl_sec, subdomains,
+                        origin, *endpoint, &endpoint_url);
+    if (EndpointParsedSuccessfully(outcome))
+      new_endpoints->insert(endpoint_url);
+    RecordHeaderEndpointOutcome(outcome);
+  }
+
+  return HeaderEndpointGroupOutcome::PARSED;
+}
+
 }  // namespace
 
 // static
@@ -183,8 +249,8 @@
                                         std::unique_ptr<base::Value> value) {
   DCHECK(url.SchemeIsCryptographic());
 
-  const base::ListValue* endpoint_list = nullptr;
-  bool is_list = value->GetAsList(&endpoint_list);
+  const base::ListValue* group_list = nullptr;
+  bool is_list = value->GetAsList(&group_list);
   DCHECK(is_list);
 
   ReportingDelegate* delegate = context->delegate();
@@ -198,16 +264,13 @@
   std::set<GURL> new_endpoints;
 
   base::TimeTicks now = context->tick_clock()->NowTicks();
-  for (size_t i = 0; i < endpoint_list->GetSize(); i++) {
-    const base::Value* endpoint = nullptr;
-    bool got_endpoint = endpoint_list->Get(i, &endpoint);
-    DCHECK(got_endpoint);
-    GURL endpoint_url;
-    HeaderEndpointOutcome outcome =
-        ProcessEndpoint(delegate, cache, now, origin, *endpoint, &endpoint_url);
-    if (EndpointParsedSuccessfully(outcome))
-      new_endpoints.insert(endpoint_url);
-    RecordHeaderEndpointOutcome(outcome);
+  for (size_t i = 0; i < group_list->GetSize(); i++) {
+    const base::Value* group = nullptr;
+    bool got_group = group_list->Get(i, &group);
+    DCHECK(got_group);
+    HeaderEndpointGroupOutcome outcome = ProcessEndpointGroup(
+        delegate, cache, &new_endpoints, now, origin, *group);
+    RecordHeaderEndpointGroupOutcome(outcome);
   }
 
   // Remove any endpoints that weren't specified in the current header(s).