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