• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2011 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "net/http/http_auth_cache.h"
6 
7 #include <list>
8 #include <map>
9 
10 #include "base/logging.h"
11 #include "base/memory/raw_ptr_exclusion.h"
12 #include "base/metrics/histogram_macros.h"
13 #include "base/strings/string_util.h"
14 #include "url/gurl.h"
15 #include "url/scheme_host_port.h"
16 #include "url/url_constants.h"
17 
18 namespace {
19 
20 // Helper to find the containing directory of path. In RFC 2617 this is what
21 // they call the "last symbolic element in the absolute path".
22 // Examples:
23 //   "/foo/bar.txt" --> "/foo/"
24 //   "/foo/" --> "/foo/"
GetParentDirectory(const std::string & path)25 std::string GetParentDirectory(const std::string& path) {
26   std::string::size_type last_slash = path.rfind("/");
27   if (last_slash == std::string::npos) {
28     // No slash (absolute paths always start with slash, so this must be
29     // the proxy case which uses empty string).
30     DCHECK(path.empty());
31     return path;
32   }
33   return path.substr(0, last_slash + 1);
34 }
35 
36 // Return true if |path| is a subpath of |container|. In other words, is
37 // |container| an ancestor of |path|?
IsEnclosingPath(const std::string & container,const std::string & path)38 bool IsEnclosingPath(const std::string& container, const std::string& path) {
39   DCHECK(container.empty() || *(container.end() - 1) == '/');
40   return ((container.empty() && path.empty()) ||
41           (!container.empty() && path.starts_with(container)));
42 }
43 
44 #if DCHECK_IS_ON()
45 // Debug helper to check that |scheme_host_port| arguments are properly formed.
CheckSchemeHostPortIsValid(const url::SchemeHostPort & scheme_host_port)46 void CheckSchemeHostPortIsValid(const url::SchemeHostPort& scheme_host_port) {
47   DCHECK(scheme_host_port.IsValid());
48   DCHECK(scheme_host_port.scheme() == url::kHttpScheme ||
49          scheme_host_port.scheme() == url::kHttpsScheme ||
50          scheme_host_port.scheme() == url::kWsScheme ||
51          scheme_host_port.scheme() == url::kWssScheme);
52 }
53 
54 // Debug helper to check that |path| arguments are properly formed.
55 // (should be absolute path, or empty string).
CheckPathIsValid(const std::string & path)56 void CheckPathIsValid(const std::string& path) {
57   DCHECK(path.empty() || path[0] == '/');
58 }
59 #endif
60 
61 // Functor used by std::erase_if.
62 struct IsEnclosedBy {
IsEnclosedBy__anon227e3bdc0111::IsEnclosedBy63   explicit IsEnclosedBy(const std::string& path) : path(path) { }
operator ()__anon227e3bdc0111::IsEnclosedBy64   bool operator() (const std::string& x) const {
65     return IsEnclosingPath(path, x);
66   }
67   // This field is not a raw_ref<> because it was filtered by the rewriter for:
68   // #constexpr-ctor-field-initializer
69   RAW_PTR_EXCLUSION const std::string& path;
70 };
71 
72 }  // namespace
73 
74 namespace net {
75 
HttpAuthCache(bool key_server_entries_by_network_anonymization_key)76 HttpAuthCache::HttpAuthCache(
77     bool key_server_entries_by_network_anonymization_key)
78     : key_server_entries_by_network_anonymization_key_(
79           key_server_entries_by_network_anonymization_key) {}
80 
81 HttpAuthCache::~HttpAuthCache() = default;
82 
SetKeyServerEntriesByNetworkAnonymizationKey(bool key_server_entries_by_network_anonymization_key)83 void HttpAuthCache::SetKeyServerEntriesByNetworkAnonymizationKey(
84     bool key_server_entries_by_network_anonymization_key) {
85   if (key_server_entries_by_network_anonymization_key_ ==
86       key_server_entries_by_network_anonymization_key) {
87     return;
88   }
89 
90   key_server_entries_by_network_anonymization_key_ =
91       key_server_entries_by_network_anonymization_key;
92   std::erase_if(entries_, [](EntryMap::value_type& entry_map_pair) {
93     return entry_map_pair.first.target == HttpAuth::AUTH_SERVER;
94   });
95 }
96 
97 // Performance: O(logN+n), where N is the total number of entries, n is the
98 // number of realm entries for the given SchemeHostPort, target, and with a
99 // matching NetworkAnonymizationKey.
Lookup(const url::SchemeHostPort & scheme_host_port,HttpAuth::Target target,const std::string & realm,HttpAuth::Scheme scheme,const NetworkAnonymizationKey & network_anonymization_key)100 HttpAuthCache::Entry* HttpAuthCache::Lookup(
101     const url::SchemeHostPort& scheme_host_port,
102     HttpAuth::Target target,
103     const std::string& realm,
104     HttpAuth::Scheme scheme,
105     const NetworkAnonymizationKey& network_anonymization_key) {
106   EntryMap::iterator entry_it = LookupEntryIt(
107       scheme_host_port, target, realm, scheme, network_anonymization_key);
108   if (entry_it == entries_.end())
109     return nullptr;
110   return &(entry_it->second);
111 }
112 
113 // Performance: O(logN+n*m), where N is the total number of entries, n is the
114 // number of realm entries for the given SchemeHostPort, target, and
115 // NetworkAnonymizationKey, m is the number of path entries per realm. Both n
116 // and m are expected to be small; m is kept small because AddPath() only keeps
117 // the shallowest entry.
LookupByPath(const url::SchemeHostPort & scheme_host_port,HttpAuth::Target target,const NetworkAnonymizationKey & network_anonymization_key,const std::string & path)118 HttpAuthCache::Entry* HttpAuthCache::LookupByPath(
119     const url::SchemeHostPort& scheme_host_port,
120     HttpAuth::Target target,
121     const NetworkAnonymizationKey& network_anonymization_key,
122     const std::string& path) {
123 #if DCHECK_IS_ON()
124   CheckSchemeHostPortIsValid(scheme_host_port);
125   CheckPathIsValid(path);
126 #endif
127 
128   // RFC 2617 section 2:
129   // A client SHOULD assume that all paths at or deeper than the depth of
130   // the last symbolic element in the path field of the Request-URI also are
131   // within the protection space ...
132   std::string parent_dir = GetParentDirectory(path);
133 
134   // Linear scan through the <scheme, realm> entries for the given
135   // SchemeHostPort.
136   auto entry_range = entries_.equal_range(
137       EntryMapKey(scheme_host_port, target, network_anonymization_key,
138                   key_server_entries_by_network_anonymization_key_));
139   auto best_match_it = entries_.end();
140   size_t best_match_length = 0;
141   for (auto it = entry_range.first; it != entry_range.second; ++it) {
142     size_t len = 0;
143     auto& entry = it->second;
144     DCHECK(entry.scheme_host_port() == scheme_host_port);
145     if (entry.HasEnclosingPath(parent_dir, &len) &&
146         (best_match_it == entries_.end() || len > best_match_length)) {
147       best_match_it = it;
148       best_match_length = len;
149     }
150   }
151   if (best_match_it != entries_.end()) {
152     Entry& best_match_entry = best_match_it->second;
153     best_match_entry.last_use_time_ticks_ = tick_clock_->NowTicks();
154     return &best_match_entry;
155   }
156   return nullptr;
157 }
158 
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)159 HttpAuthCache::Entry* HttpAuthCache::Add(
160     const url::SchemeHostPort& scheme_host_port,
161     HttpAuth::Target target,
162     const std::string& realm,
163     HttpAuth::Scheme scheme,
164     const NetworkAnonymizationKey& network_anonymization_key,
165     const std::string& auth_challenge,
166     const AuthCredentials& credentials,
167     const std::string& path) {
168 #if DCHECK_IS_ON()
169   CheckSchemeHostPortIsValid(scheme_host_port);
170   CheckPathIsValid(path);
171 #endif
172 
173   base::TimeTicks now_ticks = tick_clock_->NowTicks();
174 
175   // Check for existing entry (we will re-use it if present).
176   HttpAuthCache::Entry* entry = Lookup(scheme_host_port, target, realm, scheme,
177                                        network_anonymization_key);
178   if (!entry) {
179     // Failsafe to prevent unbounded memory growth of the cache.
180     //
181     // Data was collected in June of 2019, before entries were keyed on either
182     // HttpAuth::Target or NetworkAnonymizationKey. That data indicated that the
183     // eviction rate was at around 0.05%. I.e. 0.05% of the time the number of
184     // entries in the cache exceed kMaxNumRealmEntries. The evicted entry is
185     // roughly half an hour old (median), and it's been around 25 minutes since
186     // its last use (median).
187     if (entries_.size() >= kMaxNumRealmEntries) {
188       DLOG(WARNING) << "Num auth cache entries reached limit -- evicting";
189       EvictLeastRecentlyUsedEntry();
190     }
191     entry =
192         &(entries_
193               .insert({EntryMapKey(
194                            scheme_host_port, target, network_anonymization_key,
195                            key_server_entries_by_network_anonymization_key_),
196                        Entry()})
197               ->second);
198     entry->scheme_host_port_ = scheme_host_port;
199     entry->realm_ = realm;
200     entry->scheme_ = scheme;
201     entry->creation_time_ticks_ = now_ticks;
202     entry->creation_time_ = clock_->Now();
203   }
204   DCHECK_EQ(scheme_host_port, entry->scheme_host_port_);
205   DCHECK_EQ(realm, entry->realm_);
206   DCHECK_EQ(scheme, entry->scheme_);
207 
208   entry->auth_challenge_ = auth_challenge;
209   entry->credentials_ = credentials;
210   entry->nonce_count_ = 1;
211   entry->AddPath(path);
212   entry->last_use_time_ticks_ = now_ticks;
213 
214   return entry;
215 }
216 
217 HttpAuthCache::Entry::Entry(const Entry& other) = default;
218 
219 HttpAuthCache::Entry::~Entry() = default;
220 
UpdateStaleChallenge(const std::string & auth_challenge)221 void HttpAuthCache::Entry::UpdateStaleChallenge(
222     const std::string& auth_challenge) {
223   auth_challenge_ = auth_challenge;
224   nonce_count_ = 1;
225 }
226 
IsEqualForTesting(const Entry & other) const227 bool HttpAuthCache::Entry::IsEqualForTesting(const Entry& other) const {
228   if (scheme_host_port() != other.scheme_host_port())
229     return false;
230   if (realm() != other.realm())
231     return false;
232   if (scheme() != other.scheme())
233     return false;
234   if (auth_challenge() != other.auth_challenge())
235     return false;
236   if (!credentials().Equals(other.credentials()))
237     return false;
238   std::set<std::string> lhs_paths(paths_.begin(), paths_.end());
239   std::set<std::string> rhs_paths(other.paths_.begin(), other.paths_.end());
240   if (lhs_paths != rhs_paths)
241     return false;
242   return true;
243 }
244 
245 HttpAuthCache::Entry::Entry() = default;
246 
AddPath(const std::string & path)247 void HttpAuthCache::Entry::AddPath(const std::string& path) {
248   std::string parent_dir = GetParentDirectory(path);
249   if (!HasEnclosingPath(parent_dir, nullptr)) {
250     // Remove any entries that have been subsumed by the new entry.
251     std::erase_if(paths_, IsEnclosedBy(parent_dir));
252 
253     // Failsafe to prevent unbounded memory growth of the cache.
254     //
255     // Data collected on June of 2019 indicate that when we get here, the list
256     // of paths has reached the 10 entry maximum around 1% of the time.
257     if (paths_.size() >= kMaxNumPathsPerRealmEntry) {
258       DLOG(WARNING) << "Num path entries for " << scheme_host_port()
259                     << " has grown too large -- evicting";
260       paths_.pop_back();
261     }
262 
263     // Add new path.
264     paths_.push_front(parent_dir);
265   }
266 }
267 
HasEnclosingPath(const std::string & dir,size_t * path_len)268 bool HttpAuthCache::Entry::HasEnclosingPath(const std::string& dir,
269                                             size_t* path_len) {
270   DCHECK(GetParentDirectory(dir) == dir);
271   for (PathList::iterator it = paths_.begin(); it != paths_.end(); ++it) {
272     if (IsEnclosingPath(*it, dir)) {
273       // No element of paths_ may enclose any other element.
274       // Therefore this path is the tightest bound.  Important because
275       // the length returned is used to determine the cache entry that
276       // has the closest enclosing path in LookupByPath().
277       if (path_len)
278         *path_len = it->length();
279       // Move the found path up by one place so that more frequently used paths
280       // migrate towards the beginning of the list of paths.
281       if (it != paths_.begin())
282         std::iter_swap(it, std::prev(it));
283       return true;
284     }
285   }
286   return false;
287 }
288 
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)289 bool HttpAuthCache::Remove(
290     const url::SchemeHostPort& scheme_host_port,
291     HttpAuth::Target target,
292     const std::string& realm,
293     HttpAuth::Scheme scheme,
294     const NetworkAnonymizationKey& network_anonymization_key,
295     const AuthCredentials& credentials) {
296   EntryMap::iterator entry_it = LookupEntryIt(
297       scheme_host_port, target, realm, scheme, network_anonymization_key);
298   if (entry_it == entries_.end())
299     return false;
300   Entry& entry = entry_it->second;
301   if (credentials.Equals(entry.credentials())) {
302     entries_.erase(entry_it);
303     return true;
304   }
305   return false;
306 }
307 
ClearEntriesAddedBetween(base::Time begin_time,base::Time end_time,base::RepeatingCallback<bool (const GURL &)> url_matcher)308 void HttpAuthCache::ClearEntriesAddedBetween(
309     base::Time begin_time,
310     base::Time end_time,
311     base::RepeatingCallback<bool(const GURL&)> url_matcher) {
312   if (begin_time.is_min() && end_time.is_max() && !url_matcher) {
313     ClearAllEntries();
314     return;
315   }
316   std::erase_if(entries_, [begin_time, end_time,
317                            url_matcher](EntryMap::value_type& entry_map_pair) {
318     Entry& entry = entry_map_pair.second;
319     return entry.creation_time_ >= begin_time &&
320            entry.creation_time_ < end_time &&
321            (url_matcher ? url_matcher.Run(entry.scheme_host_port().GetURL())
322                         : true);
323   });
324 }
325 
ClearAllEntries()326 void HttpAuthCache::ClearAllEntries() {
327   entries_.clear();
328 }
329 
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)330 bool HttpAuthCache::UpdateStaleChallenge(
331     const url::SchemeHostPort& scheme_host_port,
332     HttpAuth::Target target,
333     const std::string& realm,
334     HttpAuth::Scheme scheme,
335     const NetworkAnonymizationKey& network_anonymization_key,
336     const std::string& auth_challenge) {
337   HttpAuthCache::Entry* entry = Lookup(scheme_host_port, target, realm, scheme,
338                                        network_anonymization_key);
339   if (!entry)
340     return false;
341   entry->UpdateStaleChallenge(auth_challenge);
342   entry->last_use_time_ticks_ = tick_clock_->NowTicks();
343   return true;
344 }
345 
CopyProxyEntriesFrom(const HttpAuthCache & other)346 void HttpAuthCache::CopyProxyEntriesFrom(const HttpAuthCache& other) {
347   for (auto it = other.entries_.begin(); it != other.entries_.end(); ++it) {
348     const Entry& e = it->second;
349 
350     // Skip non-proxy entries.
351     if (it->first.target != HttpAuth::AUTH_PROXY)
352       continue;
353 
354     // Sanity check - proxy entries should have an empty
355     // NetworkAnonymizationKey.
356     DCHECK(NetworkAnonymizationKey() == it->first.network_anonymization_key);
357 
358     // Add an Entry with one of the original entry's paths.
359     DCHECK(e.paths_.size() > 0);
360     Entry* entry = Add(e.scheme_host_port(), it->first.target, e.realm(),
361                        e.scheme(), it->first.network_anonymization_key,
362                        e.auth_challenge(), e.credentials(), e.paths_.back());
363     // Copy all other paths.
364     for (auto it2 = std::next(e.paths_.rbegin()); it2 != e.paths_.rend(); ++it2)
365       entry->AddPath(*it2);
366     // Copy nonce count (for digest authentication).
367     entry->nonce_count_ = e.nonce_count_;
368   }
369 }
370 
EntryMapKey(const url::SchemeHostPort & scheme_host_port,HttpAuth::Target target,const NetworkAnonymizationKey & network_anonymization_key,bool key_server_entries_by_network_anonymization_key)371 HttpAuthCache::EntryMapKey::EntryMapKey(
372     const url::SchemeHostPort& scheme_host_port,
373     HttpAuth::Target target,
374     const NetworkAnonymizationKey& network_anonymization_key,
375     bool key_server_entries_by_network_anonymization_key)
376     : scheme_host_port(scheme_host_port),
377       target(target),
378       network_anonymization_key(
379           target == HttpAuth::AUTH_SERVER &&
380                   key_server_entries_by_network_anonymization_key
381               ? network_anonymization_key
382               : NetworkAnonymizationKey()) {}
383 
384 HttpAuthCache::EntryMapKey::~EntryMapKey() = default;
385 
operator <(const EntryMapKey & other) const386 bool HttpAuthCache::EntryMapKey::operator<(const EntryMapKey& other) const {
387   return std::tie(scheme_host_port, target, network_anonymization_key) <
388          std::tie(other.scheme_host_port, other.target,
389                   other.network_anonymization_key);
390 }
391 
GetEntriesSizeForTesting()392 size_t HttpAuthCache::GetEntriesSizeForTesting() {
393   return entries_.size();
394 }
395 
LookupEntryIt(const url::SchemeHostPort & scheme_host_port,HttpAuth::Target target,const std::string & realm,HttpAuth::Scheme scheme,const NetworkAnonymizationKey & network_anonymization_key)396 HttpAuthCache::EntryMap::iterator HttpAuthCache::LookupEntryIt(
397     const url::SchemeHostPort& scheme_host_port,
398     HttpAuth::Target target,
399     const std::string& realm,
400     HttpAuth::Scheme scheme,
401     const NetworkAnonymizationKey& network_anonymization_key) {
402 #if DCHECK_IS_ON()
403   CheckSchemeHostPortIsValid(scheme_host_port);
404 #endif
405 
406   // Linear scan through the <scheme, realm> entries for the given
407   // SchemeHostPort and NetworkAnonymizationKey.
408   auto entry_range = entries_.equal_range(
409       EntryMapKey(scheme_host_port, target, network_anonymization_key,
410                   key_server_entries_by_network_anonymization_key_));
411   for (auto it = entry_range.first; it != entry_range.second; ++it) {
412     Entry& entry = it->second;
413     DCHECK(entry.scheme_host_port() == scheme_host_port);
414     if (entry.scheme() == scheme && entry.realm() == realm) {
415       entry.last_use_time_ticks_ = tick_clock_->NowTicks();
416       return it;
417     }
418   }
419   return entries_.end();
420 }
421 
422 // Linear scan through all entries to find least recently used entry (by oldest
423 // |last_use_time_ticks_| and evict it from |entries_|.
EvictLeastRecentlyUsedEntry()424 void HttpAuthCache::EvictLeastRecentlyUsedEntry() {
425   DCHECK(entries_.size() == kMaxNumRealmEntries);
426   base::TimeTicks now_ticks = tick_clock_->NowTicks();
427 
428   EntryMap::iterator oldest_entry_it = entries_.end();
429   base::TimeTicks oldest_last_use_time_ticks = now_ticks;
430 
431   for (auto it = entries_.begin(); it != entries_.end(); ++it) {
432     Entry& entry = it->second;
433     if (entry.last_use_time_ticks_ < oldest_last_use_time_ticks ||
434         oldest_entry_it == entries_.end()) {
435       oldest_entry_it = it;
436       oldest_last_use_time_ticks = entry.last_use_time_ticks_;
437     }
438   }
439   DCHECK(oldest_entry_it != entries_.end());
440   entries_.erase(oldest_entry_it);
441 }
442 
443 }  // namespace net
444