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