| // Copyright 2018 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "services/network/cors/preflight_cache.h" |
| |
| #include <iterator> |
| #include <string> |
| |
| #include "base/containers/flat_set.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/rand_util.h" |
| #include "base/time/time.h" |
| #include "base/values.h" |
| #include "net/base/does_url_match_filter.h" |
| #include "net/base/registry_controlled_domains/registry_controlled_domain.h" |
| #include "net/log/net_log_event_type.h" |
| #include "net/log/net_log_with_source.h" |
| #include "services/network/data_remover_util.h" |
| #include "services/network/public/mojom/clear_data_filter.mojom.h" |
| #include "url/gurl.h" |
| |
| namespace network::cors { |
| |
| namespace { |
| |
| constexpr size_t kMaxCacheEntries = 1024u; |
| constexpr size_t kMaxKeyLength = 1024u; |
| constexpr size_t kPurgeUnit = 10u; |
| |
| // These values are persisted to logs. Entries should not be renumbered and |
| // numeric values should never be reused. |
| enum class CacheMetric { |
| kHitAndPass = 0, |
| kHitAndFail = 1, |
| kMiss = 2, |
| kStale = 3, |
| |
| kMaxValue = kStale, |
| }; |
| |
| base::Value::Dict NetLogCacheStatusParams(const CacheMetric metric) { |
| std::string cache_status; |
| switch (metric) { |
| case CacheMetric::kHitAndPass: |
| cache_status = "hit-and-pass"; |
| break; |
| case CacheMetric::kHitAndFail: |
| cache_status = "hit-and-fail"; |
| break; |
| case CacheMetric::kMiss: |
| cache_status = "miss"; |
| break; |
| case CacheMetric::kStale: |
| cache_status = "stale"; |
| break; |
| } |
| |
| return base::Value::Dict().Set("status", cache_status); |
| } |
| |
| void RecordCacheMetricNetLog(CacheMetric metric, |
| const net::NetLogWithSource& net_log) { |
| net_log.AddEvent(net::NetLogEventType::CHECK_CORS_PREFLIGHT_CACHE, |
| [&] { return NetLogCacheStatusParams(metric); }); |
| } |
| |
| } // namespace |
| |
| PreflightCache::PreflightCache() = default; |
| PreflightCache::~PreflightCache() = default; |
| |
| void PreflightCache::AppendEntry( |
| const url::Origin& origin, |
| const GURL& url, |
| const net::NetworkIsolationKey& network_isolation_key, |
| mojom::IPAddressSpace target_ip_address_space, |
| std::unique_ptr<PreflightResult> preflight_result) { |
| DCHECK(preflight_result); |
| |
| // Do not cache `preflight_result` if `url` is too long. |
| const std::string url_spec = url.spec(); |
| if (url_spec.length() >= kMaxKeyLength) { |
| return; |
| } |
| |
| auto key = std::make_tuple(origin, url_spec, network_isolation_key, |
| target_ip_address_space); |
| const auto existing_entry = cache_.find(key); |
| if (existing_entry == cache_.end()) { |
| // Since one new entry is always added below, let's purge one cache entry |
| // if cache size is larger than kMaxCacheEntries - 1 so that the size to be |
| // kMaxCacheEntries at maximum. |
| MayPurge(kMaxCacheEntries - 1, kPurgeUnit); |
| } |
| cache_[key] = std::move(preflight_result); |
| } |
| |
| bool PreflightCache::CheckIfRequestCanSkipPreflight( |
| const url::Origin& origin, |
| const GURL& url, |
| const net::NetworkIsolationKey& network_isolation_key, |
| mojom::IPAddressSpace target_ip_address_space, |
| mojom::CredentialsMode credentials_mode, |
| const std::string& method, |
| const net::HttpRequestHeaders& request_headers, |
| bool is_revalidating, |
| const net::NetLogWithSource& net_log, |
| bool acam_preflight_spec_conformant) { |
| // Check if the entry exists in the cache. |
| auto key = std::make_tuple(origin, url.spec(), network_isolation_key, |
| target_ip_address_space); |
| auto cache_entry = cache_.find(key); |
| if (cache_entry == cache_.end()) { |
| RecordCacheMetricNetLog(CacheMetric::kMiss, net_log); |
| return false; |
| } |
| |
| // Check if the entry is still valid. |
| if (!cache_entry->second->IsExpired()) { |
| // Both `origin` and `url` are in cache. Check if the entry is sufficient to |
| // skip CORS-preflight. |
| if (cache_entry->second->EnsureAllowedRequest( |
| credentials_mode, method, request_headers, is_revalidating, |
| NonWildcardRequestHeadersSupport(true), |
| acam_preflight_spec_conformant)) { |
| // Note that we always use the "with non-wildcard request headers" |
| // variant, because it is hard to generate the correct error information |
| // from here, and cache miss is in most case recoverable. |
| RecordCacheMetricNetLog(CacheMetric::kHitAndPass, net_log); |
| net_log.AddEvent( |
| net::NetLogEventType::CORS_PREFLIGHT_CACHED_RESULT, |
| [&cache_entry] { return cache_entry->second->NetLogParams(); }); |
| return true; |
| } |
| RecordCacheMetricNetLog(CacheMetric::kHitAndFail, net_log); |
| } else { |
| RecordCacheMetricNetLog(CacheMetric::kStale, net_log); |
| } |
| |
| // The cache entry is either stale or not sufficient. Remove the item from the |
| // cache. |
| cache_.erase(cache_entry); |
| return false; |
| } |
| |
| // Clear browsing history time ranges allow for last 1hr, 24hr, 7d, 4w, |
| // and all time. |
| // The PreflightCache does not contain a timestamp for when the entry was |
| // added to the cache, and since Chrome caps the Access-Control-Max-Age header |
| // value for CORS-preflight responses to 2hrs it doesn't make sense to add the |
| // granularity to remove only entries created in the last 1hr. |
| // Always clears the whole PreflightCache regardless the range selected for |
| // Clear Browsing history |
| void PreflightCache::ClearCache(mojom::ClearDataFilterPtr url_filter) { |
| if (url_filter.is_null()) { |
| cache_.clear(); |
| return; |
| } |
| if (url_filter->origins.empty() && url_filter->domains.empty()) { |
| switch (url_filter->type) { |
| case mojom::ClearDataFilter_Type::DELETE_MATCHES: |
| return; // Nothing to do |
| case mojom::ClearDataFilter_Type::KEEP_MATCHES: |
| cache_.clear(); // Remove all |
| return; |
| } |
| } |
| const net::UrlFilterType url_filter_type = |
| ConvertClearDataFilterType(url_filter->type); |
| const base::flat_set<url::Origin> origins(url_filter->origins.begin(), |
| url_filter->origins.end()); |
| const base::flat_set<std::string> domains(url_filter->domains.begin(), |
| url_filter->domains.end()); |
| |
| for (auto it = cache_.begin(); it != cache_.end();) { |
| auto next_it = std::next(it); |
| auto cached_url = std::get<0>(it->first).GetURL(); |
| if (net::DoesUrlMatchFilter(url_filter_type, origins, domains, |
| cached_url)) { |
| cache_.erase(it); |
| } |
| it = next_it; |
| } |
| } |
| |
| size_t PreflightCache::CountEntriesForTesting() const { |
| return cache_.size(); |
| } |
| |
| bool PreflightCache::DoesEntryExistForTesting( |
| const url::Origin& origin, |
| const std::string& url, |
| const net::NetworkIsolationKey& network_isolation_key, |
| mojom::IPAddressSpace target_ip_address_space) { |
| std::tuple<url::Origin, std::string, net::NetworkIsolationKey, |
| mojom::IPAddressSpace> |
| entry_key = std::make_tuple(origin, url, network_isolation_key, |
| target_ip_address_space); |
| return cache_.find(entry_key) != cache_.end(); |
| } |
| |
| void PreflightCache::MayPurgeForTesting(size_t max_entries, size_t purge_unit) { |
| MayPurge(max_entries, purge_unit); |
| } |
| |
| void PreflightCache::MayPurge(size_t max_entries, size_t purge_unit) { |
| if (cache_.size() <= max_entries) { |
| return; |
| } |
| DCHECK_GE(cache_.size(), purge_unit); |
| auto purge_begin_entry = cache_.begin(); |
| std::advance(purge_begin_entry, base::RandInt(0, cache_.size() - purge_unit)); |
| auto purge_end_entry = purge_begin_entry; |
| std::advance(purge_end_entry, purge_unit); |
| cache_.erase(purge_begin_entry, purge_end_entry); |
| } |
| |
| } // namespace network::cors |