1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "CookieCommons.h"
8 #include "CookieLogging.h"
9 #include "CookieNotification.h"
10 #include "mozilla/net/MozURL_ffi.h"
12 #include "nsICookieNotification.h"
13 #include "CookieStorage.h"
14 #include "mozilla/dom/nsMixedContentBlocker.h"
15 #include "mozilla/glean/GleanMetrics.h"
16 #include "nsIMutableArray.h"
17 #include "nsTPriorityQueue.h"
18 #include "nsIScriptError.h"
19 #include "nsIUserIdleService.h"
20 #include "nsServiceManagerUtils.h"
21 #include "nsComponentManagerUtils.h"
23 #include "nsIPrefService.h"
25 #undef ADD_TEN_PERCENT
26 #define ADD_TEN_PERCENT(i) static_cast<uint32_t>((i) + (i) / 10)
29 #define LIMIT(x, low, high, default) \
30 ((x) >= (low) && (x) <= (high) ? (x) : (default))
37 // comparator class for lastaccessed times of cookies.
38 class CompareCookiesByAge
{
40 static bool Equals(const CookieListIter
& a
, const CookieListIter
& b
) {
41 return a
.Cookie()->LastAccessed() == b
.Cookie()->LastAccessed() &&
42 a
.Cookie()->CreationTime() == b
.Cookie()->CreationTime();
45 static bool LessThan(const CookieListIter
& a
, const CookieListIter
& b
) {
46 // compare by lastAccessed time, and tiebreak by creationTime.
47 int64_t result
= a
.Cookie()->LastAccessed() - b
.Cookie()->LastAccessed();
52 return a
.Cookie()->CreationTime() < b
.Cookie()->CreationTime();
56 // Cookie comparator for the priority queue used in FindStaleCookies.
57 // Note that the expired cookie has the highest priority.
58 // Other non-expired cookies are sorted by their age.
59 class CookieIterComparator
{
64 explicit CookieIterComparator(int64_t aTime
) : mCurrentTime(aTime
) {}
66 bool LessThan(const CookieListIter
& lhs
, const CookieListIter
& rhs
) {
67 bool lExpired
= lhs
.Cookie()->Expiry() <= mCurrentTime
;
68 bool rExpired
= rhs
.Cookie()->Expiry() <= mCurrentTime
;
69 if (lExpired
&& !rExpired
) {
73 if (!lExpired
&& rExpired
) {
77 return mozilla::net::CompareCookiesByAge::LessThan(lhs
, rhs
);
81 // comparator class for sorting cookies by entry and index.
82 class CompareCookiesByIndex
{
84 static bool Equals(const CookieListIter
& a
, const CookieListIter
& b
) {
85 NS_ASSERTION(a
.entry
!= b
.entry
|| a
.index
!= b
.index
,
86 "cookie indexes should never be equal");
90 static bool LessThan(const CookieListIter
& a
, const CookieListIter
& b
) {
91 // compare by entryclass pointer, then by index.
92 if (a
.entry
!= b
.entry
) {
93 return a
.entry
< b
.entry
;
96 return a
.index
< b
.index
;
102 // ---------------------------------------------------------------------------
105 size_t CookieEntry::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf
) const {
106 size_t amount
= CookieKey::SizeOfExcludingThis(aMallocSizeOf
);
108 amount
+= mCookies
.ShallowSizeOfExcludingThis(aMallocSizeOf
);
109 for (uint32_t i
= 0; i
< mCookies
.Length(); ++i
) {
110 amount
+= mCookies
[i
]->SizeOfIncludingThis(aMallocSizeOf
);
116 bool CookieEntry::IsPartitioned() const {
117 return !mOriginAttributes
.mPartitionKey
.IsEmpty();
120 // ---------------------------------------------------------------------------
123 NS_IMPL_ISUPPORTS(CookieStorage
, nsIObserver
, nsISupportsWeakReference
)
125 void CookieStorage::Init() {
126 // init our pref and observer
127 nsCOMPtr
<nsIPrefBranch
> prefBranch
= do_GetService(NS_PREFSERVICE_CONTRACTID
);
129 prefBranch
->AddObserver(kPrefMaxNumberOfCookies
, this, true);
130 prefBranch
->AddObserver(kPrefMaxCookiesPerHost
, this, true);
131 prefBranch
->AddObserver(kPrefCookiePurgeAge
, this, true);
132 PrefChanged(prefBranch
);
135 nsCOMPtr
<nsIObserverService
> observerService
= services::GetObserverService();
136 NS_ENSURE_TRUE_VOID(observerService
);
139 observerService
->AddObserver(this, OBSERVER_TOPIC_IDLE_DAILY
, true);
140 NS_ENSURE_SUCCESS_VOID(rv
);
143 size_t CookieStorage::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf
) const {
146 amount
+= aMallocSizeOf(this);
147 amount
+= mHostTable
.SizeOfExcludingThis(aMallocSizeOf
);
152 void CookieStorage::GetCookies(nsTArray
<RefPtr
<nsICookie
>>& aCookies
) const {
153 aCookies
.SetCapacity(mCookieCount
);
154 for (const auto& entry
: mHostTable
) {
155 const CookieEntry::ArrayType
& cookies
= entry
.GetCookies();
156 for (CookieEntry::IndexType i
= 0; i
< cookies
.Length(); ++i
) {
157 aCookies
.AppendElement(cookies
[i
]);
162 void CookieStorage::GetSessionCookies(
163 nsTArray
<RefPtr
<nsICookie
>>& aCookies
) const {
164 aCookies
.SetCapacity(mCookieCount
);
165 for (const auto& entry
: mHostTable
) {
166 const CookieEntry::ArrayType
& cookies
= entry
.GetCookies();
167 for (CookieEntry::IndexType i
= 0; i
< cookies
.Length(); ++i
) {
168 Cookie
* cookie
= cookies
[i
];
169 // Filter out non-session cookies.
170 if (cookie
->IsSession()) {
171 aCookies
.AppendElement(cookie
);
177 // find an exact cookie specified by host, name, and path that hasn't expired.
178 bool CookieStorage::FindCookie(const nsACString
& aBaseDomain
,
179 const OriginAttributes
& aOriginAttributes
,
180 const nsACString
& aHost
, const nsACString
& aName
,
181 const nsACString
& aPath
, CookieListIter
& aIter
) {
183 mHostTable
.GetEntry(CookieKey(aBaseDomain
, aOriginAttributes
));
188 const CookieEntry::ArrayType
& cookies
= entry
->GetCookies();
189 for (CookieEntry::IndexType i
= 0; i
< cookies
.Length(); ++i
) {
190 Cookie
* cookie
= cookies
[i
];
192 if (aHost
.Equals(cookie
->Host()) && aPath
.Equals(cookie
->Path()) &&
193 aName
.Equals(cookie
->Name())) {
194 aIter
= CookieListIter(entry
, i
);
202 // find an secure cookie specified by host and name
203 bool CookieStorage::FindSecureCookie(const nsACString
& aBaseDomain
,
204 const OriginAttributes
& aOriginAttributes
,
207 mHostTable
.GetEntry(CookieKey(aBaseDomain
, aOriginAttributes
));
212 const CookieEntry::ArrayType
& cookies
= entry
->GetCookies();
213 for (CookieEntry::IndexType i
= 0; i
< cookies
.Length(); ++i
) {
214 Cookie
* cookie
= cookies
[i
];
215 // isn't a match if insecure or a different name
216 if (!cookie
->IsSecure() || !aCookie
->Name().Equals(cookie
->Name())) {
220 // The host must "domain-match" an existing cookie or vice-versa
221 if (CookieCommons::DomainMatches(cookie
, aCookie
->Host()) ||
222 CookieCommons::DomainMatches(aCookie
, cookie
->Host())) {
223 // If the path of new cookie and the path of existing cookie
224 // aren't "/", then this situation needs to compare paths to
225 // ensure only that a newly-created non-secure cookie does not
226 // overlay an existing secure cookie.
227 if (CookieCommons::PathMatches(cookie
, aCookie
->GetFilePath())) {
236 uint32_t CookieStorage::CountCookiesFromHost(const nsACString
& aBaseDomain
,
237 uint32_t aPrivateBrowsingId
) {
238 OriginAttributes attrs
;
239 attrs
.mPrivateBrowsingId
= aPrivateBrowsingId
;
241 // Return a count of all cookies, including expired.
242 CookieEntry
* entry
= mHostTable
.GetEntry(CookieKey(aBaseDomain
, attrs
));
243 return entry
? entry
->GetCookies().Length() : 0;
246 void CookieStorage::GetAll(nsTArray
<RefPtr
<nsICookie
>>& aResult
) const {
247 aResult
.SetCapacity(mCookieCount
);
249 for (const auto& entry
: mHostTable
) {
250 const CookieEntry::ArrayType
& cookies
= entry
.GetCookies();
251 for (CookieEntry::IndexType i
= 0; i
< cookies
.Length(); ++i
) {
252 aResult
.AppendElement(cookies
[i
]);
257 const nsTArray
<RefPtr
<Cookie
>>* CookieStorage::GetCookiesFromHost(
258 const nsACString
& aBaseDomain
, const OriginAttributes
& aOriginAttributes
) {
260 mHostTable
.GetEntry(CookieKey(aBaseDomain
, aOriginAttributes
));
261 return entry
? &entry
->GetCookies() : nullptr;
264 void CookieStorage::GetCookiesWithOriginAttributes(
265 const OriginAttributesPattern
& aPattern
, const nsACString
& aBaseDomain
,
266 nsTArray
<RefPtr
<nsICookie
>>& aResult
) {
267 for (auto iter
= mHostTable
.Iter(); !iter
.Done(); iter
.Next()) {
268 CookieEntry
* entry
= iter
.Get();
270 if (!aBaseDomain
.IsEmpty() && !aBaseDomain
.Equals(entry
->mBaseDomain
)) {
274 if (!aPattern
.Matches(entry
->mOriginAttributes
)) {
278 const CookieEntry::ArrayType
& entryCookies
= entry
->GetCookies();
280 for (CookieEntry::IndexType i
= 0; i
< entryCookies
.Length(); ++i
) {
281 aResult
.AppendElement(entryCookies
[i
]);
286 void CookieStorage::RemoveCookie(const nsACString
& aBaseDomain
,
287 const OriginAttributes
& aOriginAttributes
,
288 const nsACString
& aHost
,
289 const nsACString
& aName
,
290 const nsACString
& aPath
) {
291 CookieListIter matchIter
{};
292 RefPtr
<Cookie
> cookie
;
293 if (FindCookie(aBaseDomain
, aOriginAttributes
, aHost
, aName
, aPath
,
295 cookie
= matchIter
.Cookie();
296 RemoveCookieFromList(matchIter
);
300 // Everything's done. Notify observers.
301 NotifyChanged(cookie
, nsICookieNotification::COOKIE_DELETED
, aBaseDomain
);
305 void CookieStorage::RemoveCookiesWithOriginAttributes(
306 const OriginAttributesPattern
& aPattern
, const nsACString
& aBaseDomain
) {
307 // Iterate the hash table of CookieEntry.
308 for (auto iter
= mHostTable
.Iter(); !iter
.Done(); iter
.Next()) {
309 CookieEntry
* entry
= iter
.Get();
311 if (!aBaseDomain
.IsEmpty() && !aBaseDomain
.Equals(entry
->mBaseDomain
)) {
315 if (!aPattern
.Matches(entry
->mOriginAttributes
)) {
319 // Pattern matches. Delete all cookies within this CookieEntry.
320 uint32_t cookiesCount
= entry
->GetCookies().Length();
322 for (CookieEntry::IndexType i
= 0; i
< cookiesCount
; ++i
) {
323 // Remove the first cookie from the list.
324 CookieListIter
iter(entry
, 0);
325 RefPtr
<Cookie
> cookie
= iter
.Cookie();
327 // Remove the cookie.
328 RemoveCookieFromList(iter
);
331 NotifyChanged(cookie
, nsICookieNotification::COOKIE_DELETED
,
338 /* static */ bool CookieStorage::isIPv6BaseDomain(
339 const nsACString
& aBaseDomain
) {
340 return aBaseDomain
.Contains(':');
343 /* static */ bool CookieStorage::SerializeIPv6BaseDomain(
344 nsACString
& aBaseDomain
) {
345 bool hasStartBracket
= aBaseDomain
.First() == '[';
346 bool hasEndBracket
= aBaseDomain
.Last() == ']';
348 // If only start or end bracket exists host is malformed.
349 if (hasStartBracket
!= hasEndBracket
) {
353 // If the base domain is not in URL format (e.g. [::1]) add brackets so we
354 // can use rusturl_parse_ipv6addr().
355 if (!hasStartBracket
) {
356 aBaseDomain
.Insert('[', 0);
357 aBaseDomain
.Append(']');
360 // Serialize base domain to "zero abbreviation" and lower-case hex
362 nsAutoCString baseDomain
;
363 nsresult rv
= (nsresult
)rusturl_parse_ipv6addr(&aBaseDomain
, &baseDomain
);
364 NS_ENSURE_SUCCESS(rv
, false);
366 // Strip brackets to match principal representation.
367 aBaseDomain
= Substring(baseDomain
, 1, baseDomain
.Length() - 2);
372 void CookieStorage::RemoveCookiesFromExactHost(
373 const nsACString
& aHost
, const nsACString
& aBaseDomain
,
374 const OriginAttributesPattern
& aPattern
) {
375 // Intermediate fix until Bug 1882259 is resolved.
376 // Bug 1860033 - Cookies do not serialize IPv6 host / base domain in contrast
377 // to principals. To allow deletion by principal serialize before comparison.
378 // We check the base domain since it is used as the CookieList key and equals
379 // the normalized (ASCII) host for IP addresses
380 // (it is equal to the CookieService::NormalizeHost() output).
381 nsAutoCString removeBaseDomain
;
382 bool isIPv6
= isIPv6BaseDomain(aBaseDomain
);
384 MOZ_ASSERT(!aBaseDomain
.IsEmpty());
385 // Copy base domain since argument is immutable.
386 removeBaseDomain
= aBaseDomain
;
387 if (NS_WARN_IF(!SerializeIPv6BaseDomain(removeBaseDomain
))) {
388 // Return on malformed base domains.
393 // Iterate the hash table of CookieEntry.
394 for (auto iter
= mHostTable
.Iter(); !iter
.Done(); iter
.Next()) {
395 CookieEntry
* entry
= iter
.Get();
397 // IPv6 host / base domain cookies
399 // If we look for a IPv6 cookie skip non-IPv6 cookie entries.
400 if (!isIPv6BaseDomain(entry
->mBaseDomain
)) {
403 // Serialize IPv6 base domains before comparison.
404 // Copy base domain since argument is immutable.
405 nsAutoCString entryBaseDomain
;
406 entryBaseDomain
= entry
->mBaseDomain
;
407 if (NS_WARN_IF(!SerializeIPv6BaseDomain(entryBaseDomain
))) {
410 if (!removeBaseDomain
.Equals(entryBaseDomain
)) {
414 } else if (!aBaseDomain
.Equals(entry
->mBaseDomain
)) {
418 if (!aPattern
.Matches(entry
->mOriginAttributes
)) {
422 uint32_t cookiesCount
= entry
->GetCookies().Length();
423 for (CookieEntry::IndexType i
= cookiesCount
; i
!= 0; --i
) {
424 CookieListIter
iter(entry
, i
- 1);
425 RefPtr
<Cookie
> cookie
= iter
.Cookie();
427 // For IP addresses (ASCII normalized) host == baseDomain, we checked
429 if (!isIPv6
&& !aHost
.Equals(cookie
->RawHost())) {
433 // Remove the cookie.
434 RemoveCookieFromList(iter
);
437 NotifyChanged(cookie
, nsICookieNotification::COOKIE_DELETED
,
444 void CookieStorage::RemoveAll() {
445 // clearing the hashtable will call each CookieEntry's dtor,
446 // which releases all their respective children.
449 mCookieOldestTime
= INT64_MAX
;
453 NotifyChanged(nullptr, nsICookieNotification::ALL_COOKIES_CLEARED
, ""_ns
);
456 // notify observers that the cookie list changed.
457 void CookieStorage::NotifyChanged(nsISupports
* aSubject
,
458 nsICookieNotification::Action aAction
,
459 const nsACString
& aBaseDomain
,
460 dom::BrowsingContext
* aBrowsingContext
,
461 bool aOldCookieIsSession
) {
462 nsCOMPtr
<nsIObserverService
> os
= services::GetObserverService();
467 nsCOMPtr
<nsICookie
> cookie
;
468 nsCOMPtr
<nsIArray
> batchDeletedCookies
;
470 if (aAction
== nsICookieNotification::COOKIES_BATCH_DELETED
) {
471 batchDeletedCookies
= do_QueryInterface(aSubject
);
473 cookie
= do_QueryInterface(aSubject
);
476 uint64_t browsingContextId
= 0;
477 if (aBrowsingContext
) {
478 browsingContextId
= aBrowsingContext
->Id();
481 nsCOMPtr
<nsICookieNotification
> notification
= new CookieNotification(
482 aAction
, cookie
, aBaseDomain
, batchDeletedCookies
, browsingContextId
);
483 // Notify for topic "private-cookie-changed" or "cookie-changed"
484 os
->NotifyObservers(notification
, NotificationTopic(), u
"");
486 NotifyChangedInternal(notification
, aOldCookieIsSession
);
489 // this is a backend function for adding a cookie to the list, via SetCookie.
490 // also used in the cookie manager, for profile migration from IE. it either
491 // replaces an existing cookie; or adds the cookie to the hashtable, and
492 // deletes a cookie (if maximum number of cookies has been reached). also
493 // performs list maintenance by removing expired cookies.
494 void CookieStorage::AddCookie(nsIConsoleReportCollector
* aCRC
,
495 const nsACString
& aBaseDomain
,
496 const OriginAttributes
& aOriginAttributes
,
497 Cookie
* aCookie
, int64_t aCurrentTimeInUsec
,
498 nsIURI
* aHostURI
, const nsACString
& aCookieHeader
,
500 dom::BrowsingContext
* aBrowsingContext
) {
501 int64_t currentTime
= aCurrentTimeInUsec
/ PR_USEC_PER_SEC
;
503 CookieListIter exactIter
{};
504 bool foundCookie
= false;
505 foundCookie
= FindCookie(aBaseDomain
, aOriginAttributes
, aCookie
->Host(),
506 aCookie
->Name(), aCookie
->Path(), exactIter
);
507 bool foundSecureExact
= foundCookie
&& exactIter
.Cookie()->IsSecure();
508 bool potentiallyTrustworthy
= true;
510 potentiallyTrustworthy
=
511 nsMixedContentBlocker::IsPotentiallyTrustworthyOrigin(aHostURI
);
513 constexpr auto CONSOLE_REJECTION_CATEGORY
= "cookiesRejection"_ns
;
514 bool oldCookieIsSession
= false;
515 // Step1, call FindSecureCookie(). FindSecureCookie() would
516 // find the existing cookie with the security flag and has
517 // the same name, host and path of the new cookie, if there is any.
518 // Step2, Confirm new cookie's security setting. If any targeted
519 // cookie had been found in Step1, then confirm whether the
520 // new cookie could modify it. If the new created cookie’s
521 // "secure-only-flag" is not set, and the "scheme" component
522 // of the "request-uri" does not denote a "secure" protocol,
523 // then ignore the new cookie.
524 // (draft-ietf-httpbis-cookie-alone section 3.2)
525 if (!aCookie
->IsSecure() &&
527 FindSecureCookie(aBaseDomain
, aOriginAttributes
, aCookie
)) &&
528 !potentiallyTrustworthy
) {
529 COOKIE_LOGFAILURE(SET_COOKIE
, aHostURI
, aCookieHeader
,
530 "cookie can't save because older cookie is secure "
531 "cookie but newer cookie is non-secure cookie");
532 CookieLogging::LogMessageToConsole(
533 aCRC
, aHostURI
, nsIScriptError::warningFlag
, CONSOLE_REJECTION_CATEGORY
,
534 "CookieRejectedNonsecureOverSecure"_ns
,
535 AutoTArray
<nsString
, 1>{
536 NS_ConvertUTF8toUTF16(aCookie
->Name()),
541 RefPtr
<Cookie
> oldCookie
;
542 nsCOMPtr
<nsIArray
> purgedList
;
544 oldCookie
= exactIter
.Cookie();
545 oldCookieIsSession
= oldCookie
->IsSession();
547 // Check if the old cookie is stale (i.e. has already expired). If so, we
548 // need to be careful about the semantics of removing it and adding the new
549 // cookie: we want the behavior wrt adding the new cookie to be the same as
550 // if it didn't exist, but we still want to fire a removal notification.
551 if (oldCookie
->Expiry() <= currentTime
) {
552 if (aCookie
->Expiry() <= currentTime
) {
553 // The new cookie has expired and the old one is stale. Nothing to do.
554 COOKIE_LOGFAILURE(SET_COOKIE
, aHostURI
, aCookieHeader
,
555 "cookie has already expired");
559 // Remove the stale cookie. We save notification for later, once all list
560 // modifications are complete.
561 RemoveCookieFromList(exactIter
);
562 COOKIE_LOGFAILURE(SET_COOKIE
, aHostURI
, aCookieHeader
,
563 "stale cookie was purged");
564 purgedList
= CreatePurgeList(oldCookie
);
566 // We've done all we need to wrt removing and notifying the stale cookie.
567 // From here on out, we pretend pretend it didn't exist, so that we
568 // preserve expected notification semantics when adding the new cookie.
572 // If the old cookie is httponly, make sure we're not coming from script.
573 if (!aFromHttp
&& oldCookie
->IsHttpOnly()) {
575 SET_COOKIE
, aHostURI
, aCookieHeader
,
576 "previously stored cookie is httponly; coming from script");
577 CookieLogging::LogMessageToConsole(
578 aCRC
, aHostURI
, nsIScriptError::warningFlag
,
579 CONSOLE_REJECTION_CATEGORY
,
580 "CookieRejectedHttpOnlyButFromScript"_ns
,
581 AutoTArray
<nsString
, 1>{
582 NS_ConvertUTF8toUTF16(aCookie
->Name()),
587 // If the new cookie has the same value, expiry date, isSecure, isSession,
588 // isHttpOnly and SameSite flags then we can just keep the old one.
589 // Only if any of these differ we would want to override the cookie.
590 if (oldCookie
->Value().Equals(aCookie
->Value()) &&
591 oldCookie
->Expiry() == aCookie
->Expiry() &&
592 oldCookie
->IsSecure() == aCookie
->IsSecure() &&
593 oldCookie
->IsSession() == aCookie
->IsSession() &&
594 oldCookie
->IsHttpOnly() == aCookie
->IsHttpOnly() &&
595 oldCookie
->SameSite() == aCookie
->SameSite() &&
596 oldCookie
->RawSameSite() == aCookie
->RawSameSite() &&
597 oldCookie
->SchemeMap() == aCookie
->SchemeMap() &&
598 // We don't want to perform this optimization if the cookie is
599 // considered stale, since in this case we would need to update the
601 !oldCookie
->IsStale()) {
602 // Update the last access time on the old cookie.
603 oldCookie
->SetLastAccessed(aCookie
->LastAccessed());
604 UpdateCookieOldestTime(oldCookie
);
608 // Merge the scheme map in case the old cookie and the new cookie are
609 // used with different schemes.
610 MergeCookieSchemeMap(oldCookie
, aCookie
);
612 // Remove the old cookie.
613 RemoveCookieFromList(exactIter
);
615 // If the new cookie has expired -- i.e. the intent was simply to delete
616 // the old cookie -- then we're done.
617 if (aCookie
->Expiry() <= currentTime
) {
618 COOKIE_LOGFAILURE(SET_COOKIE
, aHostURI
, aCookieHeader
,
619 "previously stored cookie was deleted");
620 NotifyChanged(oldCookie
, nsICookieNotification::COOKIE_DELETED
,
621 aBaseDomain
, aBrowsingContext
, oldCookieIsSession
);
625 // Preserve creation time of cookie for ordering purposes.
626 aCookie
->SetCreationTime(oldCookie
->CreationTime());
630 // check if cookie has already expired
631 if (aCookie
->Expiry() <= currentTime
) {
632 COOKIE_LOGFAILURE(SET_COOKIE
, aHostURI
, aCookieHeader
,
633 "cookie has already expired");
637 // check if we have to delete an old cookie.
639 mHostTable
.GetEntry(CookieKey(aBaseDomain
, aOriginAttributes
));
640 if (entry
&& entry
->GetCookies().Length() >= mMaxCookiesPerHost
) {
641 nsTArray
<CookieListIter
> removedIterList
;
642 // Prioritize evicting insecure cookies.
643 // (draft-ietf-httpbis-cookie-alone section 3.3)
644 uint32_t limit
= mMaxCookiesPerHost
- mCookieQuotaPerHost
;
645 FindStaleCookies(entry
, currentTime
, false, removedIterList
, limit
);
646 if (removedIterList
.Length() == 0) {
647 if (aCookie
->IsSecure()) {
648 // It's valid to evict a secure cookie for another secure cookie.
649 FindStaleCookies(entry
, currentTime
, true, removedIterList
, limit
);
651 COOKIE_LOGEVICTED(aCookie
,
652 "Too many cookies for this domain and the new "
653 "cookie is not a secure cookie");
658 MOZ_ASSERT(!removedIterList
.IsEmpty());
659 // Sort |removedIterList| by index again, since we have to remove the
660 // cookie in the reverse order.
661 removedIterList
.Sort(CompareCookiesByIndex());
662 for (auto it
= removedIterList
.rbegin(); it
!= removedIterList
.rend();
664 RefPtr
<Cookie
> evictedCookie
= (*it
).Cookie();
665 COOKIE_LOGEVICTED(evictedCookie
, "Too many cookies for this domain");
666 RemoveCookieFromList(*it
);
667 CreateOrUpdatePurgeList(purgedList
, evictedCookie
);
668 MOZ_ASSERT((*it
).entry
);
670 uint32_t purgedLength
= 0;
671 purgedList
->GetLength(&purgedLength
);
672 mozilla::glean::networking::cookie_purge_entry_max
.AccumulateSingleSample(
675 } else if (mCookieCount
>= ADD_TEN_PERCENT(mMaxNumberOfCookies
)) {
676 int64_t maxAge
= aCurrentTimeInUsec
- mCookieOldestTime
;
677 int64_t purgeAge
= ADD_TEN_PERCENT(mCookiePurgeAge
);
678 if (maxAge
>= purgeAge
) {
679 // we're over both size and age limits by 10%; time to purge the table!
681 // 1) removing expired cookies;
682 // 2) evicting the balance of old cookies until we reach the size limit.
683 // note that the mCookieOldestTime indicator can be pessimistic - if
684 // it's older than the actual oldest cookie, we'll just purge more
686 purgedList
= PurgeCookies(aCurrentTimeInUsec
, mMaxNumberOfCookies
,
688 uint32_t purgedLength
= 0;
689 purgedList
->GetLength(&purgedLength
);
690 mozilla::glean::networking::cookie_purge_max
.AccumulateSingleSample(
696 // Add the cookie to the db. We do not supply a params array for batching
697 // because this might result in removals and additions being out of order.
698 AddCookieToList(aBaseDomain
, aOriginAttributes
, aCookie
);
699 StoreCookie(aBaseDomain
, aOriginAttributes
, aCookie
);
701 COOKIE_LOGSUCCESS(SET_COOKIE
, aHostURI
, aCookieHeader
, aCookie
, foundCookie
);
703 // Now that list mutations are complete, notify observers. We do it here
704 // because observers may themselves attempt to mutate the list.
706 NotifyChanged(purgedList
, nsICookieNotification::COOKIES_BATCH_DELETED
,
710 // Notify for topic "private-cookie-changed" or "cookie-changed"
711 NotifyChanged(aCookie
,
712 foundCookie
? nsICookieNotification::COOKIE_CHANGED
713 : nsICookieNotification::COOKIE_ADDED
,
714 aBaseDomain
, aBrowsingContext
, oldCookieIsSession
);
717 void CookieStorage::UpdateCookieOldestTime(Cookie
* aCookie
) {
718 if (aCookie
->LastAccessed() < mCookieOldestTime
) {
719 mCookieOldestTime
= aCookie
->LastAccessed();
723 void CookieStorage::MergeCookieSchemeMap(Cookie
* aOldCookie
,
724 Cookie
* aNewCookie
) {
725 aNewCookie
->SetSchemeMap(aOldCookie
->SchemeMap() | aNewCookie
->SchemeMap());
728 void CookieStorage::AddCookieToList(const nsACString
& aBaseDomain
,
729 const OriginAttributes
& aOriginAttributes
,
732 NS_WARNING("Attempting to AddCookieToList with null cookie");
736 CookieKey
key(aBaseDomain
, aOriginAttributes
);
738 CookieEntry
* entry
= mHostTable
.PutEntry(key
);
739 NS_ASSERTION(entry
, "can't insert element into a null entry!");
741 entry
->GetCookies().AppendElement(aCookie
);
744 // keep track of the oldest cookie, for when it comes time to purge
745 UpdateCookieOldestTime(aCookie
);
749 already_AddRefed
<nsIArray
> CookieStorage::CreatePurgeList(nsICookie
* aCookie
) {
750 nsCOMPtr
<nsIMutableArray
> removedList
=
751 do_CreateInstance(NS_ARRAY_CONTRACTID
);
752 removedList
->AppendElement(aCookie
);
753 return removedList
.forget();
756 // Given the output iter array and the count limit, find cookies
757 // sort by expiry and lastAccessed time.
759 void CookieStorage::FindStaleCookies(CookieEntry
* aEntry
, int64_t aCurrentTime
,
761 nsTArray
<CookieListIter
>& aOutput
,
765 const CookieEntry::ArrayType
& cookies
= aEntry
->GetCookies();
768 CookieIterComparator
comp(aCurrentTime
);
769 nsTPriorityQueue
<CookieListIter
, CookieIterComparator
> queue(comp
);
771 for (CookieEntry::IndexType i
= 0; i
< cookies
.Length(); ++i
) {
772 Cookie
* cookie
= cookies
[i
];
774 if (cookie
->Expiry() <= aCurrentTime
) {
775 queue
.Push(CookieListIter(aEntry
, i
));
780 // We want to look for the non-secure cookie first time through,
781 // then find the secure cookie the second time this function is called.
782 if (cookie
->IsSecure()) {
787 queue
.Push(CookieListIter(aEntry
, i
));
791 while (!queue
.IsEmpty() && count
< aLimit
) {
792 aOutput
.AppendElement(queue
.Pop());
798 void CookieStorage::CreateOrUpdatePurgeList(nsCOMPtr
<nsIArray
>& aPurgedList
,
799 nsICookie
* aCookie
) {
801 COOKIE_LOGSTRING(LogLevel::Debug
, ("Creating new purge list"));
802 aPurgedList
= CreatePurgeList(aCookie
);
806 nsCOMPtr
<nsIMutableArray
> purgedList
= do_QueryInterface(aPurgedList
);
808 COOKIE_LOGSTRING(LogLevel::Debug
, ("Updating existing purge list"));
809 purgedList
->AppendElement(aCookie
);
811 COOKIE_LOGSTRING(LogLevel::Debug
, ("Could not QI aPurgedList!"));
815 // purges expired and old cookies in a batch operation.
816 already_AddRefed
<nsIArray
> CookieStorage::PurgeCookiesWithCallbacks(
817 int64_t aCurrentTimeInUsec
, uint16_t aMaxNumberOfCookies
,
818 int64_t aCookiePurgeAge
,
819 std::function
<void(const CookieListIter
&)>&& aRemoveCookieCallback
,
820 std::function
<void()>&& aFinalizeCallback
) {
821 NS_ASSERTION(mHostTable
.Count() > 0, "table is empty");
823 uint32_t initialCookieCount
= mCookieCount
;
824 COOKIE_LOGSTRING(LogLevel::Debug
,
825 ("PurgeCookies(): beginning purge with %" PRIu32
826 " cookies and %" PRId64
" oldest age",
827 mCookieCount
, aCurrentTimeInUsec
- mCookieOldestTime
));
829 using PurgeList
= nsTArray
<CookieListIter
>;
830 PurgeList
purgeList(kMaxNumberOfCookies
);
832 nsCOMPtr
<nsIMutableArray
> removedList
=
833 do_CreateInstance(NS_ARRAY_CONTRACTID
);
835 int64_t currentTime
= aCurrentTimeInUsec
/ PR_USEC_PER_SEC
;
836 int64_t purgeTime
= aCurrentTimeInUsec
- aCookiePurgeAge
;
837 int64_t oldestTime
= INT64_MAX
;
839 for (auto iter
= mHostTable
.Iter(); !iter
.Done(); iter
.Next()) {
840 CookieEntry
* entry
= iter
.Get();
842 const CookieEntry::ArrayType
& cookies
= entry
->GetCookies();
843 auto length
= cookies
.Length();
844 for (CookieEntry::IndexType i
= 0; i
< length
;) {
845 CookieListIter
iter(entry
, i
);
846 Cookie
* cookie
= cookies
[i
];
848 // check if the cookie has expired
849 if (cookie
->Expiry() <= currentTime
) {
850 removedList
->AppendElement(cookie
);
851 COOKIE_LOGEVICTED(cookie
, "Cookie expired");
853 // remove from list; do not increment our iterator, but stop if we're
855 aRemoveCookieCallback(iter
);
860 // check if the cookie is over the age limit
861 if (cookie
->LastAccessed() <= purgeTime
) {
862 purgeList
.AppendElement(iter
);
864 } else if (cookie
->LastAccessed() < oldestTime
) {
865 // reset our indicator
866 oldestTime
= cookie
->LastAccessed();
871 MOZ_ASSERT(length
== cookies
.Length());
875 uint32_t postExpiryCookieCount
= mCookieCount
;
877 // now we have a list of iterators for cookies over the age limit.
878 // sort them by age, and then we'll see how many to remove...
879 purgeList
.Sort(CompareCookiesByAge());
881 // only remove old cookies until we reach the max cookie limit, no more.
882 uint32_t excess
= mCookieCount
> aMaxNumberOfCookies
883 ? mCookieCount
- aMaxNumberOfCookies
885 if (purgeList
.Length() > excess
) {
886 // We're not purging everything in the list, so update our indicator.
887 oldestTime
= purgeList
[excess
].Cookie()->LastAccessed();
889 purgeList
.SetLength(excess
);
892 // sort the list again, this time grouping cookies with a common entryclass
893 // together, and with ascending index. this allows us to iterate backwards
894 // over the list removing cookies, without having to adjust indexes as we go.
895 purgeList
.Sort(CompareCookiesByIndex());
896 for (PurgeList::index_type i
= purgeList
.Length(); i
--;) {
897 Cookie
* cookie
= purgeList
[i
].Cookie();
898 removedList
->AppendElement(cookie
);
899 COOKIE_LOGEVICTED(cookie
, "Cookie too old");
901 aRemoveCookieCallback(purgeList
[i
]);
904 // Update the database if we have entries to purge.
905 if (aFinalizeCallback
) {
909 // reset the oldest time indicator
910 mCookieOldestTime
= oldestTime
;
912 COOKIE_LOGSTRING(LogLevel::Debug
,
913 ("PurgeCookies(): %" PRIu32
" expired; %" PRIu32
914 " purged; %" PRIu32
" remain; %" PRId64
" oldest age",
915 initialCookieCount
- postExpiryCookieCount
,
916 postExpiryCookieCount
- mCookieCount
, mCookieCount
,
917 aCurrentTimeInUsec
- mCookieOldestTime
));
919 return removedList
.forget();
922 // remove a cookie from the hashtable, and update the iterator state.
923 void CookieStorage::RemoveCookieFromList(const CookieListIter
& aIter
) {
924 RemoveCookieFromDB(*aIter
.Cookie());
925 RemoveCookieFromListInternal(aIter
);
928 void CookieStorage::RemoveCookieFromListInternal(const CookieListIter
& aIter
) {
929 if (aIter
.entry
->GetCookies().Length() == 1) {
930 // we're removing the last element in the array - so just remove the entry
931 // from the hash. note that the entryclass' dtor will take care of
932 // releasing this last element for us!
933 mHostTable
.RawRemoveEntry(aIter
.entry
);
936 // just remove the element from the list
937 aIter
.entry
->GetCookies().RemoveElementAt(aIter
.index
);
943 void CookieStorage::PrefChanged(nsIPrefBranch
* aPrefBranch
) {
945 if (NS_SUCCEEDED(aPrefBranch
->GetIntPref(kPrefMaxNumberOfCookies
, &val
))) {
946 mMaxNumberOfCookies
=
947 static_cast<uint16_t> LIMIT(val
, 1, 0xFFFF, kMaxNumberOfCookies
);
950 if (NS_SUCCEEDED(aPrefBranch
->GetIntPref(kPrefCookieQuotaPerHost
, &val
))) {
951 mCookieQuotaPerHost
= static_cast<uint16_t> LIMIT(
952 val
, 1, mMaxCookiesPerHost
- 1, kCookieQuotaPerHost
);
955 if (NS_SUCCEEDED(aPrefBranch
->GetIntPref(kPrefMaxCookiesPerHost
, &val
))) {
956 mMaxCookiesPerHost
= static_cast<uint16_t> LIMIT(
957 val
, mCookieQuotaPerHost
+ 1, 0xFFFF, kMaxCookiesPerHost
);
960 if (NS_SUCCEEDED(aPrefBranch
->GetIntPref(kPrefCookiePurgeAge
, &val
))) {
962 int64_t(LIMIT(val
, 0, INT32_MAX
, INT32_MAX
)) * PR_USEC_PER_SEC
;
967 CookieStorage::Observe(nsISupports
* aSubject
, const char* aTopic
,
968 const char16_t
* /*aData*/) {
969 if (!strcmp(aTopic
, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID
)) {
970 nsCOMPtr
<nsIPrefBranch
> prefBranch
= do_QueryInterface(aSubject
);
972 PrefChanged(prefBranch
);
974 } else if (!strcmp(aTopic
, OBSERVER_TOPIC_IDLE_DAILY
)) {
975 CollectCookieJarSizeData();
982 } // namespace mozilla