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