// Copyright 2017 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "net/reporting/reporting_header_parser.h" #include #include "base/bind.h" #include "base/json/json_reader.h" #include "base/logging.h" #include "base/metrics/histogram_macros.h" #include "base/time/tick_clock.h" #include "base/time/time.h" #include "base/values.h" #include "net/reporting/reporting_cache.h" #include "net/reporting/reporting_client.h" #include "net/reporting/reporting_context.h" #include "net/reporting/reporting_delegate.h" namespace net { namespace { enum class HeaderOutcome { DISCARDED_NO_REPORTING_SERVICE = 0, DISCARDED_INVALID_SSL_INFO = 1, DISCARDED_CERT_STATUS_ERROR = 2, DISCARDED_JSON_TOO_BIG = 3, DISCARDED_JSON_INVALID = 4, PARSED = 5, MAX }; void RecordHeaderOutcome(HeaderOutcome outcome) { UMA_HISTOGRAM_ENUMERATION("Net.Reporting.HeaderOutcome", outcome, 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("Net.Reporting.HeaderEndpointGroupOutcome", outcome, HeaderEndpointGroupOutcome::MAX); } enum class HeaderEndpointOutcome { DISCARDED_NOT_DICTIONARY = 0, DISCARDED_URL_MISSING = 1, DISCARDED_URL_NOT_STRING = 2, DISCARDED_URL_INVALID = 3, DISCARDED_URL_INSECURE = 4, DISCARDED_PRIORITY_NOT_INTEGER = 5, DISCARDED_WEIGHT_NOT_INTEGER = 6, DISCARDED_WEIGHT_NOT_POSITIVE = 7, REMOVED = 8, SET_REJECTED_BY_DELEGATE = 9, SET = 10, MAX }; bool EndpointParsedSuccessfully(HeaderEndpointOutcome outcome) { return outcome == HeaderEndpointOutcome::REMOVED || outcome == HeaderEndpointOutcome::SET_REJECTED_BY_DELEGATE || outcome == HeaderEndpointOutcome::SET; } void RecordHeaderEndpointOutcome(HeaderEndpointOutcome outcome) { UMA_HISTOGRAM_ENUMERATION("Net.Reporting.HeaderEndpointOutcome", outcome, HeaderEndpointOutcome::MAX); } const char kUrlKey[] = "url"; const char kIncludeSubdomainsKey[] = "include_subdomains"; const char kEndpointsKey[] = "endpoints"; const char kGroupKey[] = "group"; const char kGroupDefaultValue[] = "default"; const char kMaxAgeKey[] = "max_age"; const char kPriorityKey[] = "priority"; const char kWeightKey[] = "weight"; // Processes a single endpoint 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 tuple. // // |*endpoint_out| will contain the endpoint URL parsed out of the tuple. 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) { *endpoint_url_out = GURL(); const base::DictionaryValue* dict = nullptr; if (!value.GetAsDictionary(&dict)) return HeaderEndpointOutcome::DISCARDED_NOT_DICTIONARY; DCHECK(dict); std::string endpoint_url_string; if (!dict->HasKey(kUrlKey)) return HeaderEndpointOutcome::DISCARDED_URL_MISSING; if (!dict->GetString(kUrlKey, &endpoint_url_string)) return HeaderEndpointOutcome::DISCARDED_URL_NOT_STRING; GURL endpoint_url(endpoint_url_string); if (!endpoint_url.is_valid()) return HeaderEndpointOutcome::DISCARDED_URL_INVALID; if (!endpoint_url.SchemeIsCryptographic()) return HeaderEndpointOutcome::DISCARDED_URL_INSECURE; int priority = ReportingClient::kDefaultPriority; if (dict->HasKey(kPriorityKey) && !dict->GetInteger(kPriorityKey, &priority)) return HeaderEndpointOutcome::DISCARDED_PRIORITY_NOT_INTEGER; int weight = ReportingClient::kDefaultWeight; if (dict->HasKey(kWeightKey) && !dict->GetInteger(kWeightKey, &weight)) return HeaderEndpointOutcome::DISCARDED_WEIGHT_NOT_INTEGER; if (weight <= 0) return HeaderEndpointOutcome::DISCARDED_WEIGHT_NOT_POSITIVE; *endpoint_url_out = endpoint_url; if (ttl_sec == 0) { cache->RemoveClientForOriginAndEndpoint(origin, endpoint_url); return HeaderEndpointOutcome::REMOVED; } if (!delegate->CanSetClient(origin, endpoint_url)) return HeaderEndpointOutcome::SET_REJECTED_BY_DELEGATE; cache->SetClient(origin, endpoint_url, subdomains, group, now + base::TimeDelta::FromSeconds(ttl_sec), priority, weight); 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* 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 void ReportingHeaderParser::RecordHeaderDiscardedForNoReportingService() { RecordHeaderOutcome(HeaderOutcome::DISCARDED_NO_REPORTING_SERVICE); } // static void ReportingHeaderParser::RecordHeaderDiscardedForInvalidSSLInfo() { RecordHeaderOutcome(HeaderOutcome::DISCARDED_INVALID_SSL_INFO); } // static void ReportingHeaderParser::RecordHeaderDiscardedForCertStatusError() { RecordHeaderOutcome(HeaderOutcome::DISCARDED_CERT_STATUS_ERROR); } // static void ReportingHeaderParser::RecordHeaderDiscardedForJsonInvalid() { RecordHeaderOutcome(HeaderOutcome::DISCARDED_JSON_INVALID); } // static void ReportingHeaderParser::RecordHeaderDiscardedForJsonTooBig() { RecordHeaderOutcome(HeaderOutcome::DISCARDED_JSON_TOO_BIG); } // static void ReportingHeaderParser::ParseHeader(ReportingContext* context, const GURL& url, std::unique_ptr value) { DCHECK(url.SchemeIsCryptographic()); const base::ListValue* group_list = nullptr; bool is_list = value->GetAsList(&group_list); DCHECK(is_list); ReportingDelegate* delegate = context->delegate(); ReportingCache* cache = context->cache(); url::Origin origin = url::Origin::Create(url); std::vector old_endpoints; cache->GetEndpointsForOrigin(origin, &old_endpoints); std::set new_endpoints; base::TimeTicks now = context->tick_clock()->NowTicks(); 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). for (const GURL& old_endpoint : old_endpoints) { if (new_endpoints.count(old_endpoint) == 0u) cache->RemoveClientForOriginAndEndpoint(origin, old_endpoint); } RecordHeaderOutcome(HeaderOutcome::PARSED); } } // namespace net