// Copyright 2011 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "net/http/http_auth_cache.h" #include "base/containers/cxx20_erase.h" #include "base/logging.h" #include "base/metrics/histogram_macros.h" #include "base/strings/string_util.h" #include "url/scheme_host_port.h" #include "url/url_constants.h" namespace { // Helper to find the containing directory of path. In RFC 2617 this is what // they call the "last symbolic element in the absolute path". // Examples: // "/foo/bar.txt" --> "/foo/" // "/foo/" --> "/foo/" std::string GetParentDirectory(const std::string& path) { std::string::size_type last_slash = path.rfind("/"); if (last_slash == std::string::npos) { // No slash (absolute paths always start with slash, so this must be // the proxy case which uses empty string). DCHECK(path.empty()); return path; } return path.substr(0, last_slash + 1); } // Return true if |path| is a subpath of |container|. In other words, is // |container| an ancestor of |path|? bool IsEnclosingPath(const std::string& container, const std::string& path) { DCHECK(container.empty() || *(container.end() - 1) == '/'); return ((container.empty() && path.empty()) || (!container.empty() && base::StartsWith(path, container, base::CompareCase::SENSITIVE))); } #if DCHECK_IS_ON() // Debug helper to check that |scheme_host_port| arguments are properly formed. void CheckSchemeHostPortIsValid(const url::SchemeHostPort& scheme_host_port) { DCHECK(scheme_host_port.IsValid()); DCHECK(scheme_host_port.scheme() == url::kHttpScheme || scheme_host_port.scheme() == url::kHttpsScheme || scheme_host_port.scheme() == url::kWsScheme || scheme_host_port.scheme() == url::kWssScheme); } // Debug helper to check that |path| arguments are properly formed. // (should be absolute path, or empty string). void CheckPathIsValid(const std::string& path) { DCHECK(path.empty() || path[0] == '/'); } #endif // Functor used by EraseIf. struct IsEnclosedBy { explicit IsEnclosedBy(const std::string& path) : path(path) { } bool operator() (const std::string& x) const { return IsEnclosingPath(path, x); } const std::string& path; }; } // namespace namespace net { HttpAuthCache::HttpAuthCache( bool key_server_entries_by_network_anonymization_key) : key_server_entries_by_network_anonymization_key_( key_server_entries_by_network_anonymization_key) {} HttpAuthCache::~HttpAuthCache() = default; void HttpAuthCache::SetKeyServerEntriesByNetworkAnonymizationKey( bool key_server_entries_by_network_anonymization_key) { if (key_server_entries_by_network_anonymization_key_ == key_server_entries_by_network_anonymization_key) { return; } key_server_entries_by_network_anonymization_key_ = key_server_entries_by_network_anonymization_key; base::EraseIf(entries_, [](EntryMap::value_type& entry_map_pair) { return entry_map_pair.first.target == HttpAuth::AUTH_SERVER; }); } // Performance: O(logN+n), where N is the total number of entries, n is the // number of realm entries for the given SchemeHostPort, target, and with a // matching NetworkAnonymizationKey. HttpAuthCache::Entry* HttpAuthCache::Lookup( const url::SchemeHostPort& scheme_host_port, HttpAuth::Target target, const std::string& realm, HttpAuth::Scheme scheme, const NetworkAnonymizationKey& network_anonymization_key) { EntryMap::iterator entry_it = LookupEntryIt( scheme_host_port, target, realm, scheme, network_anonymization_key); if (entry_it == entries_.end()) return nullptr; return &(entry_it->second); } // Performance: O(logN+n*m), where N is the total number of entries, n is the // number of realm entries for the given SchemeHostPort, target, and // NetworkAnonymizationKey, m is the number of path entries per realm. Both n // and m are expected to be small; m is kept small because AddPath() only keeps // the shallowest entry. HttpAuthCache::Entry* HttpAuthCache::LookupByPath( const url::SchemeHostPort& scheme_host_port, HttpAuth::Target target, const NetworkAnonymizationKey& network_anonymization_key, const std::string& path) { #if DCHECK_IS_ON() CheckSchemeHostPortIsValid(scheme_host_port); CheckPathIsValid(path); #endif // RFC 2617 section 2: // A client SHOULD assume that all paths at or deeper than the depth of // the last symbolic element in the path field of the Request-URI also are // within the protection space ... std::string parent_dir = GetParentDirectory(path); // Linear scan through the entries for the given // SchemeHostPort. auto entry_range = entries_.equal_range( EntryMapKey(scheme_host_port, target, network_anonymization_key, key_server_entries_by_network_anonymization_key_)); auto best_match_it = entries_.end(); size_t best_match_length = 0; for (auto it = entry_range.first; it != entry_range.second; ++it) { size_t len = 0; auto& entry = it->second; DCHECK(entry.scheme_host_port() == scheme_host_port); if (entry.HasEnclosingPath(parent_dir, &len) && (best_match_it == entries_.end() || len > best_match_length)) { best_match_it = it; best_match_length = len; } } if (best_match_it != entries_.end()) { Entry& best_match_entry = best_match_it->second; best_match_entry.last_use_time_ticks_ = tick_clock_->NowTicks(); return &best_match_entry; } return nullptr; } HttpAuthCache::Entry* HttpAuthCache::Add( const url::SchemeHostPort& scheme_host_port, HttpAuth::Target target, const std::string& realm, HttpAuth::Scheme scheme, const NetworkAnonymizationKey& network_anonymization_key, const std::string& auth_challenge, const AuthCredentials& credentials, const std::string& path) { #if DCHECK_IS_ON() CheckSchemeHostPortIsValid(scheme_host_port); CheckPathIsValid(path); #endif base::TimeTicks now_ticks = tick_clock_->NowTicks(); // Check for existing entry (we will re-use it if present). HttpAuthCache::Entry* entry = Lookup(scheme_host_port, target, realm, scheme, network_anonymization_key); if (!entry) { // Failsafe to prevent unbounded memory growth of the cache. // // Data was collected in June of 2019, before entries were keyed on either // HttpAuth::Target or NetworkAnonymizationKey. That data indicated that the // eviction rate was at around 0.05%. I.e. 0.05% of the time the number of // entries in the cache exceed kMaxNumRealmEntries. The evicted entry is // roughly half an hour old (median), and it's been around 25 minutes since // its last use (median). if (entries_.size() >= kMaxNumRealmEntries) { DLOG(WARNING) << "Num auth cache entries reached limit -- evicting"; EvictLeastRecentlyUsedEntry(); } entry = &( entries_ .emplace(std::make_pair( EntryMapKey(scheme_host_port, target, network_anonymization_key, key_server_entries_by_network_anonymization_key_), Entry())) ->second); entry->scheme_host_port_ = scheme_host_port; entry->realm_ = realm; entry->scheme_ = scheme; entry->creation_time_ticks_ = now_ticks; entry->creation_time_ = clock_->Now(); } DCHECK_EQ(scheme_host_port, entry->scheme_host_port_); DCHECK_EQ(realm, entry->realm_); DCHECK_EQ(scheme, entry->scheme_); entry->auth_challenge_ = auth_challenge; entry->credentials_ = credentials; entry->nonce_count_ = 1; entry->AddPath(path); entry->last_use_time_ticks_ = now_ticks; return entry; } HttpAuthCache::Entry::Entry(const Entry& other) = default; HttpAuthCache::Entry::~Entry() = default; void HttpAuthCache::Entry::UpdateStaleChallenge( const std::string& auth_challenge) { auth_challenge_ = auth_challenge; nonce_count_ = 1; } bool HttpAuthCache::Entry::IsEqualForTesting(const Entry& other) const { if (scheme_host_port() != other.scheme_host_port()) return false; if (realm() != other.realm()) return false; if (scheme() != other.scheme()) return false; if (auth_challenge() != other.auth_challenge()) return false; if (!credentials().Equals(other.credentials())) return false; std::set lhs_paths(paths_.begin(), paths_.end()); std::set rhs_paths(other.paths_.begin(), other.paths_.end()); if (lhs_paths != rhs_paths) return false; return true; } HttpAuthCache::Entry::Entry() = default; void HttpAuthCache::Entry::AddPath(const std::string& path) { std::string parent_dir = GetParentDirectory(path); if (!HasEnclosingPath(parent_dir, nullptr)) { // Remove any entries that have been subsumed by the new entry. base::EraseIf(paths_, IsEnclosedBy(parent_dir)); // Failsafe to prevent unbounded memory growth of the cache. // // Data collected on June of 2019 indicate that when we get here, the list // of paths has reached the 10 entry maximum around 1% of the time. if (paths_.size() >= kMaxNumPathsPerRealmEntry) { DLOG(WARNING) << "Num path entries for " << scheme_host_port() << " has grown too large -- evicting"; paths_.pop_back(); } // Add new path. paths_.push_front(parent_dir); } } bool HttpAuthCache::Entry::HasEnclosingPath(const std::string& dir, size_t* path_len) { DCHECK(GetParentDirectory(dir) == dir); for (PathList::iterator it = paths_.begin(); it != paths_.end(); ++it) { if (IsEnclosingPath(*it, dir)) { // No element of paths_ may enclose any other element. // Therefore this path is the tightest bound. Important because // the length returned is used to determine the cache entry that // has the closest enclosing path in LookupByPath(). if (path_len) *path_len = it->length(); // Move the found path up by one place so that more frequently used paths // migrate towards the beginning of the list of paths. if (it != paths_.begin()) std::iter_swap(it, std::prev(it)); return true; } } return false; } bool HttpAuthCache::Remove( const url::SchemeHostPort& scheme_host_port, HttpAuth::Target target, const std::string& realm, HttpAuth::Scheme scheme, const NetworkAnonymizationKey& network_anonymization_key, const AuthCredentials& credentials) { EntryMap::iterator entry_it = LookupEntryIt( scheme_host_port, target, realm, scheme, network_anonymization_key); if (entry_it == entries_.end()) return false; Entry& entry = entry_it->second; if (credentials.Equals(entry.credentials())) { entries_.erase(entry_it); return true; } return false; } void HttpAuthCache::ClearEntriesAddedBetween(base::Time begin_time, base::Time end_time) { if (begin_time.is_min() && end_time.is_max()) { ClearAllEntries(); return; } base::EraseIf(entries_, [begin_time, end_time](EntryMap::value_type& entry_map_pair) { Entry& entry = entry_map_pair.second; return entry.creation_time_ >= begin_time && entry.creation_time_ < end_time; }); } void HttpAuthCache::ClearAllEntries() { entries_.clear(); } bool HttpAuthCache::UpdateStaleChallenge( const url::SchemeHostPort& scheme_host_port, HttpAuth::Target target, const std::string& realm, HttpAuth::Scheme scheme, const NetworkAnonymizationKey& network_anonymization_key, const std::string& auth_challenge) { HttpAuthCache::Entry* entry = Lookup(scheme_host_port, target, realm, scheme, network_anonymization_key); if (!entry) return false; entry->UpdateStaleChallenge(auth_challenge); entry->last_use_time_ticks_ = tick_clock_->NowTicks(); return true; } void HttpAuthCache::CopyProxyEntriesFrom(const HttpAuthCache& other) { for (auto it = other.entries_.begin(); it != other.entries_.end(); ++it) { const Entry& e = it->second; // Skip non-proxy entries. if (it->first.target != HttpAuth::AUTH_PROXY) continue; // Sanity check - proxy entries should have an empty // NetworkAnonymizationKey. DCHECK(NetworkAnonymizationKey() == it->first.network_anonymization_key); // Add an Entry with one of the original entry's paths. DCHECK(e.paths_.size() > 0); Entry* entry = Add(e.scheme_host_port(), it->first.target, e.realm(), e.scheme(), it->first.network_anonymization_key, e.auth_challenge(), e.credentials(), e.paths_.back()); // Copy all other paths. for (auto it2 = std::next(e.paths_.rbegin()); it2 != e.paths_.rend(); ++it2) entry->AddPath(*it2); // Copy nonce count (for digest authentication). entry->nonce_count_ = e.nonce_count_; } } HttpAuthCache::EntryMapKey::EntryMapKey( const url::SchemeHostPort& scheme_host_port, HttpAuth::Target target, const NetworkAnonymizationKey& network_anonymization_key, bool key_server_entries_by_network_anonymization_key) : scheme_host_port(scheme_host_port), target(target), network_anonymization_key( target == HttpAuth::AUTH_SERVER && key_server_entries_by_network_anonymization_key ? network_anonymization_key : NetworkAnonymizationKey()) {} HttpAuthCache::EntryMapKey::~EntryMapKey() = default; bool HttpAuthCache::EntryMapKey::operator<(const EntryMapKey& other) const { return std::tie(scheme_host_port, target, network_anonymization_key) < std::tie(other.scheme_host_port, other.target, other.network_anonymization_key); } size_t HttpAuthCache::GetEntriesSizeForTesting() { return entries_.size(); } HttpAuthCache::EntryMap::iterator HttpAuthCache::LookupEntryIt( const url::SchemeHostPort& scheme_host_port, HttpAuth::Target target, const std::string& realm, HttpAuth::Scheme scheme, const NetworkAnonymizationKey& network_anonymization_key) { #if DCHECK_IS_ON() CheckSchemeHostPortIsValid(scheme_host_port); #endif // Linear scan through the entries for the given // SchemeHostPort and NetworkAnonymizationKey. auto entry_range = entries_.equal_range( EntryMapKey(scheme_host_port, target, network_anonymization_key, key_server_entries_by_network_anonymization_key_)); for (auto it = entry_range.first; it != entry_range.second; ++it) { Entry& entry = it->second; DCHECK(entry.scheme_host_port() == scheme_host_port); if (entry.scheme() == scheme && entry.realm() == realm) { entry.last_use_time_ticks_ = tick_clock_->NowTicks(); return it; } } return entries_.end(); } // Linear scan through all entries to find least recently used entry (by oldest // |last_use_time_ticks_| and evict it from |entries_|. void HttpAuthCache::EvictLeastRecentlyUsedEntry() { DCHECK(entries_.size() == kMaxNumRealmEntries); base::TimeTicks now_ticks = tick_clock_->NowTicks(); EntryMap::iterator oldest_entry_it = entries_.end(); base::TimeTicks oldest_last_use_time_ticks = now_ticks; for (auto it = entries_.begin(); it != entries_.end(); ++it) { Entry& entry = it->second; if (entry.last_use_time_ticks_ < oldest_last_use_time_ticks || oldest_entry_it == entries_.end()) { oldest_entry_it = it; oldest_last_use_time_ticks = entry.last_use_time_ticks_; } } DCHECK(oldest_entry_it != entries_.end()); entries_.erase(oldest_entry_it); } } // namespace net