| // 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 <list> | 
 | #include <map> | 
 |  | 
 | #include "base/logging.h" | 
 | #include "base/memory/raw_ref.h" | 
 | #include "base/metrics/histogram_macros.h" | 
 | #include "base/strings/string_util.h" | 
 | #include "url/gurl.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() && path.starts_with(container))); | 
 | } | 
 |  | 
 | #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 std::erase_if. | 
 | struct IsEnclosedBy { | 
 |   explicit IsEnclosedBy(const std::string& path) : path(path) { } | 
 |   bool operator() (const std::string& x) const { | 
 |     return IsEnclosingPath(*path, x); | 
 |   } | 
 |   const raw_ref<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; | 
 |   std::erase_if(entries_, [](const 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 <scheme, realm> 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_ | 
 |               .insert({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<std::string> lhs_paths(paths_.begin(), paths_.end()); | 
 |   std::set<std::string> 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. | 
 |     std::erase_if(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; | 
 | } | 
 |  | 
 | bool HttpAuthCache::ClearEntriesAddedBetween( | 
 |     base::Time begin_time, | 
 |     base::Time end_time, | 
 |     base::RepeatingCallback<bool(const GURL&)> url_matcher) { | 
 |   if (begin_time.is_min() && end_time.is_max() && !url_matcher) { | 
 |     return ClearAllEntries(); | 
 |   } | 
 |   const size_t num_entries_before = entries_.size(); | 
 |   std::erase_if(entries_, [begin_time, end_time, url_matcher]( | 
 |                               const EntryMap::value_type& entry_map_pair) { | 
 |     const Entry& entry = entry_map_pair.second; | 
 |     return entry.creation_time_ >= begin_time && | 
 |            entry.creation_time_ < end_time && | 
 |            (url_matcher ? url_matcher.Run(entry.scheme_host_port().GetURL()) | 
 |                         : true); | 
 |   }); | 
 |   return entries_.size() != num_entries_before; | 
 | } | 
 |  | 
 | bool HttpAuthCache::ClearAllEntries() { | 
 |   if (entries_.empty()) { | 
 |     return false; | 
 |   } | 
 |   entries_.clear(); | 
 |   return true; | 
 | } | 
 |  | 
 | 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 <scheme, realm> 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_; | 
 |     } | 
 |   } | 
 |   CHECK(oldest_entry_it != entries_.end()); | 
 |   entries_.erase(oldest_entry_it); | 
 | } | 
 |  | 
 | }  // namespace net |