diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /netwerk/cookie/CookieStorage.cpp | |
parent | Initial commit. (diff) | |
download | firefox-upstream/124.0.1.tar.xz firefox-upstream/124.0.1.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | netwerk/cookie/CookieStorage.cpp | 910 |
1 files changed, 910 insertions, 0 deletions
diff --git a/netwerk/cookie/CookieStorage.cpp b/netwerk/cookie/CookieStorage.cpp new file mode 100644 index 0000000000..cc827a2372 --- /dev/null +++ b/netwerk/cookie/CookieStorage.cpp @@ -0,0 +1,910 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "Cookie.h" +#include "CookieCommons.h" +#include "CookieLogging.h" +#include "CookieNotification.h" +#include "nsCOMPtr.h" +#include "nsICookieNotification.h" +#include "CookieStorage.h" +#include "mozilla/dom/nsMixedContentBlocker.h" +#include "mozilla/glean/GleanMetrics.h" +#include "nsIMutableArray.h" +#include "nsTPriorityQueue.h" +#include "nsIScriptError.h" +#include "nsIUserIdleService.h" +#include "nsServiceManagerUtils.h" +#include "nsComponentManagerUtils.h" +#include "prprf.h" +#include "nsIPrefService.h" + +#undef ADD_TEN_PERCENT +#define ADD_TEN_PERCENT(i) static_cast<uint32_t>((i) + (i) / 10) + +#undef LIMIT +#define LIMIT(x, low, high, default) \ + ((x) >= (low) && (x) <= (high) ? (x) : (default)) + +namespace mozilla { +namespace net { + +namespace { + +// comparator class for lastaccessed times of cookies. +class CompareCookiesByAge { + public: + static bool Equals(const CookieListIter& a, const CookieListIter& b) { + return a.Cookie()->LastAccessed() == b.Cookie()->LastAccessed() && + a.Cookie()->CreationTime() == b.Cookie()->CreationTime(); + } + + static bool LessThan(const CookieListIter& a, const CookieListIter& b) { + // compare by lastAccessed time, and tiebreak by creationTime. + int64_t result = a.Cookie()->LastAccessed() - b.Cookie()->LastAccessed(); + if (result != 0) { + return result < 0; + } + + return a.Cookie()->CreationTime() < b.Cookie()->CreationTime(); + } +}; + +// Cookie comparator for the priority queue used in FindStaleCookies. +// Note that the expired cookie has the highest priority. +// Other non-expired cookies are sorted by their age. +class CookieIterComparator { + private: + int64_t mCurrentTime; + + public: + explicit CookieIterComparator(int64_t aTime) : mCurrentTime(aTime) {} + + bool LessThan(const CookieListIter& lhs, const CookieListIter& rhs) { + bool lExpired = lhs.Cookie()->Expiry() <= mCurrentTime; + bool rExpired = rhs.Cookie()->Expiry() <= mCurrentTime; + if (lExpired && !rExpired) { + return true; + } + + if (!lExpired && rExpired) { + return false; + } + + return mozilla::net::CompareCookiesByAge::LessThan(lhs, rhs); + } +}; + +// comparator class for sorting cookies by entry and index. +class CompareCookiesByIndex { + public: + static bool Equals(const CookieListIter& a, const CookieListIter& b) { + NS_ASSERTION(a.entry != b.entry || a.index != b.index, + "cookie indexes should never be equal"); + return false; + } + + static bool LessThan(const CookieListIter& a, const CookieListIter& b) { + // compare by entryclass pointer, then by index. + if (a.entry != b.entry) { + return a.entry < b.entry; + } + + return a.index < b.index; + } +}; + +} // namespace + +// --------------------------------------------------------------------------- +// CookieEntry + +size_t CookieEntry::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const { + size_t amount = CookieKey::SizeOfExcludingThis(aMallocSizeOf); + + amount += mCookies.ShallowSizeOfExcludingThis(aMallocSizeOf); + for (uint32_t i = 0; i < mCookies.Length(); ++i) { + amount += mCookies[i]->SizeOfIncludingThis(aMallocSizeOf); + } + + return amount; +} + +bool CookieEntry::IsPartitioned() const { + return !mOriginAttributes.mPartitionKey.IsEmpty(); +} + +// --------------------------------------------------------------------------- +// CookieStorage + +NS_IMPL_ISUPPORTS(CookieStorage, nsIObserver, nsISupportsWeakReference) + +void CookieStorage::Init() { + // init our pref and observer + nsCOMPtr<nsIPrefBranch> prefBranch = do_GetService(NS_PREFSERVICE_CONTRACTID); + if (prefBranch) { + prefBranch->AddObserver(kPrefMaxNumberOfCookies, this, true); + prefBranch->AddObserver(kPrefMaxCookiesPerHost, this, true); + prefBranch->AddObserver(kPrefCookiePurgeAge, this, true); + PrefChanged(prefBranch); + } + + nsCOMPtr<nsIObserverService> observerService = services::GetObserverService(); + NS_ENSURE_TRUE_VOID(observerService); + + nsresult rv = + observerService->AddObserver(this, OBSERVER_TOPIC_IDLE_DAILY, true); + NS_ENSURE_SUCCESS_VOID(rv); +} + +size_t CookieStorage::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const { + size_t amount = 0; + + amount += aMallocSizeOf(this); + amount += mHostTable.SizeOfExcludingThis(aMallocSizeOf); + + return amount; +} + +void CookieStorage::GetCookies(nsTArray<RefPtr<nsICookie>>& aCookies) const { + aCookies.SetCapacity(mCookieCount); + for (const auto& entry : mHostTable) { + const CookieEntry::ArrayType& cookies = entry.GetCookies(); + for (CookieEntry::IndexType i = 0; i < cookies.Length(); ++i) { + aCookies.AppendElement(cookies[i]); + } + } +} + +void CookieStorage::GetSessionCookies( + nsTArray<RefPtr<nsICookie>>& aCookies) const { + aCookies.SetCapacity(mCookieCount); + for (const auto& entry : mHostTable) { + const CookieEntry::ArrayType& cookies = entry.GetCookies(); + for (CookieEntry::IndexType i = 0; i < cookies.Length(); ++i) { + Cookie* cookie = cookies[i]; + // Filter out non-session cookies. + if (cookie->IsSession()) { + aCookies.AppendElement(cookie); + } + } + } +} + +// find an exact cookie specified by host, name, and path that hasn't expired. +bool CookieStorage::FindCookie(const nsACString& aBaseDomain, + const OriginAttributes& aOriginAttributes, + const nsACString& aHost, const nsACString& aName, + const nsACString& aPath, CookieListIter& aIter) { + CookieEntry* entry = + mHostTable.GetEntry(CookieKey(aBaseDomain, aOriginAttributes)); + if (!entry) { + return false; + } + + const CookieEntry::ArrayType& cookies = entry->GetCookies(); + for (CookieEntry::IndexType i = 0; i < cookies.Length(); ++i) { + Cookie* cookie = cookies[i]; + + if (aHost.Equals(cookie->Host()) && aPath.Equals(cookie->Path()) && + aName.Equals(cookie->Name())) { + aIter = CookieListIter(entry, i); + return true; + } + } + + return false; +} + +// find an secure cookie specified by host and name +bool CookieStorage::FindSecureCookie(const nsACString& aBaseDomain, + const OriginAttributes& aOriginAttributes, + Cookie* aCookie) { + CookieEntry* entry = + mHostTable.GetEntry(CookieKey(aBaseDomain, aOriginAttributes)); + if (!entry) { + return false; + } + + const CookieEntry::ArrayType& cookies = entry->GetCookies(); + for (CookieEntry::IndexType i = 0; i < cookies.Length(); ++i) { + Cookie* cookie = cookies[i]; + // isn't a match if insecure or a different name + if (!cookie->IsSecure() || !aCookie->Name().Equals(cookie->Name())) { + continue; + } + + // The host must "domain-match" an existing cookie or vice-versa + if (CookieCommons::DomainMatches(cookie, aCookie->Host()) || + CookieCommons::DomainMatches(aCookie, cookie->Host())) { + // If the path of new cookie and the path of existing cookie + // aren't "/", then this situation needs to compare paths to + // ensure only that a newly-created non-secure cookie does not + // overlay an existing secure cookie. + if (CookieCommons::PathMatches(cookie, aCookie->GetFilePath())) { + return true; + } + } + } + + return false; +} + +uint32_t CookieStorage::CountCookiesFromHost(const nsACString& aBaseDomain, + uint32_t aPrivateBrowsingId) { + OriginAttributes attrs; + attrs.mPrivateBrowsingId = aPrivateBrowsingId; + + // Return a count of all cookies, including expired. + CookieEntry* entry = mHostTable.GetEntry(CookieKey(aBaseDomain, attrs)); + return entry ? entry->GetCookies().Length() : 0; +} + +void CookieStorage::GetAll(nsTArray<RefPtr<nsICookie>>& aResult) const { + aResult.SetCapacity(mCookieCount); + + for (const auto& entry : mHostTable) { + const CookieEntry::ArrayType& cookies = entry.GetCookies(); + for (CookieEntry::IndexType i = 0; i < cookies.Length(); ++i) { + aResult.AppendElement(cookies[i]); + } + } +} + +const nsTArray<RefPtr<Cookie>>* CookieStorage::GetCookiesFromHost( + const nsACString& aBaseDomain, const OriginAttributes& aOriginAttributes) { + CookieEntry* entry = + mHostTable.GetEntry(CookieKey(aBaseDomain, aOriginAttributes)); + return entry ? &entry->GetCookies() : nullptr; +} + +void CookieStorage::GetCookiesWithOriginAttributes( + const OriginAttributesPattern& aPattern, const nsACString& aBaseDomain, + nsTArray<RefPtr<nsICookie>>& aResult) { + for (auto iter = mHostTable.Iter(); !iter.Done(); iter.Next()) { + CookieEntry* entry = iter.Get(); + + if (!aBaseDomain.IsEmpty() && !aBaseDomain.Equals(entry->mBaseDomain)) { + continue; + } + + if (!aPattern.Matches(entry->mOriginAttributes)) { + continue; + } + + const CookieEntry::ArrayType& entryCookies = entry->GetCookies(); + + for (CookieEntry::IndexType i = 0; i < entryCookies.Length(); ++i) { + aResult.AppendElement(entryCookies[i]); + } + } +} + +void CookieStorage::RemoveCookie(const nsACString& aBaseDomain, + const OriginAttributes& aOriginAttributes, + const nsACString& aHost, + const nsACString& aName, + const nsACString& aPath) { + CookieListIter matchIter{}; + RefPtr<Cookie> cookie; + if (FindCookie(aBaseDomain, aOriginAttributes, aHost, aName, aPath, + matchIter)) { + cookie = matchIter.Cookie(); + RemoveCookieFromList(matchIter); + } + + if (cookie) { + // Everything's done. Notify observers. + NotifyChanged(cookie, nsICookieNotification::COOKIE_DELETED, aBaseDomain); + } +} + +void CookieStorage::RemoveCookiesWithOriginAttributes( + const OriginAttributesPattern& aPattern, const nsACString& aBaseDomain) { + // Iterate the hash table of CookieEntry. + for (auto iter = mHostTable.Iter(); !iter.Done(); iter.Next()) { + CookieEntry* entry = iter.Get(); + + if (!aBaseDomain.IsEmpty() && !aBaseDomain.Equals(entry->mBaseDomain)) { + continue; + } + + if (!aPattern.Matches(entry->mOriginAttributes)) { + continue; + } + + // Pattern matches. Delete all cookies within this CookieEntry. + uint32_t cookiesCount = entry->GetCookies().Length(); + + for (CookieEntry::IndexType i = 0; i < cookiesCount; ++i) { + // Remove the first cookie from the list. + CookieListIter iter(entry, 0); + RefPtr<Cookie> cookie = iter.Cookie(); + + // Remove the cookie. + RemoveCookieFromList(iter); + + if (cookie) { + NotifyChanged(cookie, nsICookieNotification::COOKIE_DELETED, + aBaseDomain); + } + } + } +} + +void CookieStorage::RemoveCookiesFromExactHost( + const nsACString& aHost, const nsACString& aBaseDomain, + const OriginAttributesPattern& aPattern) { + // Iterate the hash table of CookieEntry. + for (auto iter = mHostTable.Iter(); !iter.Done(); iter.Next()) { + CookieEntry* entry = iter.Get(); + + if (!aBaseDomain.Equals(entry->mBaseDomain)) { + continue; + } + + if (!aPattern.Matches(entry->mOriginAttributes)) { + continue; + } + + uint32_t cookiesCount = entry->GetCookies().Length(); + for (CookieEntry::IndexType i = cookiesCount; i != 0; --i) { + CookieListIter iter(entry, i - 1); + RefPtr<Cookie> cookie = iter.Cookie(); + + if (!aHost.Equals(cookie->RawHost())) { + continue; + } + + // Remove the cookie. + RemoveCookieFromList(iter); + + if (cookie) { + NotifyChanged(cookie, nsICookieNotification::COOKIE_DELETED, + aBaseDomain); + } + } + } +} + +void CookieStorage::RemoveAll() { + // clearing the hashtable will call each CookieEntry's dtor, + // which releases all their respective children. + mHostTable.Clear(); + mCookieCount = 0; + mCookieOldestTime = INT64_MAX; + + RemoveAllInternal(); + + NotifyChanged(nullptr, nsICookieNotification::ALL_COOKIES_CLEARED, ""_ns); +} + +// notify observers that the cookie list changed. +void CookieStorage::NotifyChanged(nsISupports* aSubject, + nsICookieNotification::Action aAction, + const nsACString& aBaseDomain, + dom::BrowsingContext* aBrowsingContext, + bool aOldCookieIsSession) { + nsCOMPtr<nsIObserverService> os = services::GetObserverService(); + if (!os) { + return; + } + + nsCOMPtr<nsICookie> cookie; + nsCOMPtr<nsIArray> batchDeletedCookies; + + if (aAction == nsICookieNotification::COOKIES_BATCH_DELETED) { + batchDeletedCookies = do_QueryInterface(aSubject); + } else { + cookie = do_QueryInterface(aSubject); + } + + uint64_t browsingContextId = 0; + if (aBrowsingContext) { + browsingContextId = aBrowsingContext->Id(); + } + + nsCOMPtr<nsICookieNotification> notification = new CookieNotification( + aAction, cookie, aBaseDomain, batchDeletedCookies, browsingContextId); + // Notify for topic "private-cookie-changed" or "cookie-changed" + os->NotifyObservers(notification, NotificationTopic(), u""); + + NotifyChangedInternal(notification, aOldCookieIsSession); +} + +// this is a backend function for adding a cookie to the list, via SetCookie. +// also used in the cookie manager, for profile migration from IE. it either +// replaces an existing cookie; or adds the cookie to the hashtable, and +// deletes a cookie (if maximum number of cookies has been reached). also +// performs list maintenance by removing expired cookies. +void CookieStorage::AddCookie(nsIConsoleReportCollector* aCRC, + const nsACString& aBaseDomain, + const OriginAttributes& aOriginAttributes, + Cookie* aCookie, int64_t aCurrentTimeInUsec, + nsIURI* aHostURI, const nsACString& aCookieHeader, + bool aFromHttp, + dom::BrowsingContext* aBrowsingContext) { + int64_t currentTime = aCurrentTimeInUsec / PR_USEC_PER_SEC; + + CookieListIter exactIter{}; + bool foundCookie = false; + foundCookie = FindCookie(aBaseDomain, aOriginAttributes, aCookie->Host(), + aCookie->Name(), aCookie->Path(), exactIter); + bool foundSecureExact = foundCookie && exactIter.Cookie()->IsSecure(); + bool potentiallyTrustworthy = true; + if (aHostURI) { + potentiallyTrustworthy = + nsMixedContentBlocker::IsPotentiallyTrustworthyOrigin(aHostURI); + } + constexpr auto CONSOLE_REJECTION_CATEGORY = "cookiesRejection"_ns; + bool oldCookieIsSession = false; + // Step1, call FindSecureCookie(). FindSecureCookie() would + // find the existing cookie with the security flag and has + // the same name, host and path of the new cookie, if there is any. + // Step2, Confirm new cookie's security setting. If any targeted + // cookie had been found in Step1, then confirm whether the + // new cookie could modify it. If the new created cookie’s + // "secure-only-flag" is not set, and the "scheme" component + // of the "request-uri" does not denote a "secure" protocol, + // then ignore the new cookie. + // (draft-ietf-httpbis-cookie-alone section 3.2) + if (!aCookie->IsSecure() && + (foundSecureExact || + FindSecureCookie(aBaseDomain, aOriginAttributes, aCookie)) && + !potentiallyTrustworthy) { + COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader, + "cookie can't save because older cookie is secure " + "cookie but newer cookie is non-secure cookie"); + CookieLogging::LogMessageToConsole( + aCRC, aHostURI, nsIScriptError::warningFlag, CONSOLE_REJECTION_CATEGORY, + "CookieRejectedNonsecureOverSecure"_ns, + AutoTArray<nsString, 1>{ + NS_ConvertUTF8toUTF16(aCookie->Name()), + }); + return; + } + + RefPtr<Cookie> oldCookie; + nsCOMPtr<nsIArray> purgedList; + if (foundCookie) { + oldCookie = exactIter.Cookie(); + oldCookieIsSession = oldCookie->IsSession(); + + // Check if the old cookie is stale (i.e. has already expired). If so, we + // need to be careful about the semantics of removing it and adding the new + // cookie: we want the behavior wrt adding the new cookie to be the same as + // if it didn't exist, but we still want to fire a removal notification. + if (oldCookie->Expiry() <= currentTime) { + if (aCookie->Expiry() <= currentTime) { + // The new cookie has expired and the old one is stale. Nothing to do. + COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader, + "cookie has already expired"); + return; + } + + // Remove the stale cookie. We save notification for later, once all list + // modifications are complete. + RemoveCookieFromList(exactIter); + COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader, + "stale cookie was purged"); + purgedList = CreatePurgeList(oldCookie); + + // We've done all we need to wrt removing and notifying the stale cookie. + // From here on out, we pretend pretend it didn't exist, so that we + // preserve expected notification semantics when adding the new cookie. + foundCookie = false; + + } else { + // If the old cookie is httponly, make sure we're not coming from script. + if (!aFromHttp && oldCookie->IsHttpOnly()) { + COOKIE_LOGFAILURE( + SET_COOKIE, aHostURI, aCookieHeader, + "previously stored cookie is httponly; coming from script"); + CookieLogging::LogMessageToConsole( + aCRC, aHostURI, nsIScriptError::warningFlag, + CONSOLE_REJECTION_CATEGORY, + "CookieRejectedHttpOnlyButFromScript"_ns, + AutoTArray<nsString, 1>{ + NS_ConvertUTF8toUTF16(aCookie->Name()), + }); + return; + } + + // If the new cookie has the same value, expiry date, isSecure, isSession, + // isHttpOnly and SameSite flags then we can just keep the old one. + // Only if any of these differ we would want to override the cookie. + if (oldCookie->Value().Equals(aCookie->Value()) && + oldCookie->Expiry() == aCookie->Expiry() && + oldCookie->IsSecure() == aCookie->IsSecure() && + oldCookie->IsSession() == aCookie->IsSession() && + oldCookie->IsHttpOnly() == aCookie->IsHttpOnly() && + oldCookie->SameSite() == aCookie->SameSite() && + oldCookie->RawSameSite() == aCookie->RawSameSite() && + oldCookie->SchemeMap() == aCookie->SchemeMap() && + // We don't want to perform this optimization if the cookie is + // considered stale, since in this case we would need to update the + // database. + !oldCookie->IsStale()) { + // Update the last access time on the old cookie. + oldCookie->SetLastAccessed(aCookie->LastAccessed()); + UpdateCookieOldestTime(oldCookie); + return; + } + + // Merge the scheme map in case the old cookie and the new cookie are + // used with different schemes. + MergeCookieSchemeMap(oldCookie, aCookie); + + // Remove the old cookie. + RemoveCookieFromList(exactIter); + + // If the new cookie has expired -- i.e. the intent was simply to delete + // the old cookie -- then we're done. + if (aCookie->Expiry() <= currentTime) { + COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader, + "previously stored cookie was deleted"); + NotifyChanged(oldCookie, nsICookieNotification::COOKIE_DELETED, + aBaseDomain, aBrowsingContext, oldCookieIsSession); + return; + } + + // Preserve creation time of cookie for ordering purposes. + aCookie->SetCreationTime(oldCookie->CreationTime()); + } + + } else { + // check if cookie has already expired + if (aCookie->Expiry() <= currentTime) { + COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader, + "cookie has already expired"); + return; + } + + // check if we have to delete an old cookie. + CookieEntry* entry = + mHostTable.GetEntry(CookieKey(aBaseDomain, aOriginAttributes)); + if (entry && entry->GetCookies().Length() >= mMaxCookiesPerHost) { + nsTArray<CookieListIter> removedIterList; + // Prioritize evicting insecure cookies. + // (draft-ietf-httpbis-cookie-alone section 3.3) + uint32_t limit = mMaxCookiesPerHost - mCookieQuotaPerHost; + FindStaleCookies(entry, currentTime, false, removedIterList, limit); + if (removedIterList.Length() == 0) { + if (aCookie->IsSecure()) { + // It's valid to evict a secure cookie for another secure cookie. + FindStaleCookies(entry, currentTime, true, removedIterList, limit); + } else { + COOKIE_LOGEVICTED(aCookie, + "Too many cookies for this domain and the new " + "cookie is not a secure cookie"); + return; + } + } + + MOZ_ASSERT(!removedIterList.IsEmpty()); + // Sort |removedIterList| by index again, since we have to remove the + // cookie in the reverse order. + removedIterList.Sort(CompareCookiesByIndex()); + for (auto it = removedIterList.rbegin(); it != removedIterList.rend(); + it++) { + RefPtr<Cookie> evictedCookie = (*it).Cookie(); + COOKIE_LOGEVICTED(evictedCookie, "Too many cookies for this domain"); + RemoveCookieFromList(*it); + CreateOrUpdatePurgeList(purgedList, evictedCookie); + MOZ_ASSERT((*it).entry); + } + uint32_t purgedLength = 0; + purgedList->GetLength(&purgedLength); + mozilla::glean::networking::cookie_purge_entry_max.AccumulateSamples( + {purgedLength}); + + } else if (mCookieCount >= ADD_TEN_PERCENT(mMaxNumberOfCookies)) { + int64_t maxAge = aCurrentTimeInUsec - mCookieOldestTime; + int64_t purgeAge = ADD_TEN_PERCENT(mCookiePurgeAge); + if (maxAge >= purgeAge) { + // we're over both size and age limits by 10%; time to purge the table! + // do this by: + // 1) removing expired cookies; + // 2) evicting the balance of old cookies until we reach the size limit. + // note that the mCookieOldestTime indicator can be pessimistic - if + // it's older than the actual oldest cookie, we'll just purge more + // eagerly. + purgedList = PurgeCookies(aCurrentTimeInUsec, mMaxNumberOfCookies, + mCookiePurgeAge); + uint32_t purgedLength = 0; + purgedList->GetLength(&purgedLength); + mozilla::glean::networking::cookie_purge_max.AccumulateSamples( + {purgedLength}); + } + } + } + + // Add the cookie to the db. We do not supply a params array for batching + // because this might result in removals and additions being out of order. + AddCookieToList(aBaseDomain, aOriginAttributes, aCookie); + StoreCookie(aBaseDomain, aOriginAttributes, aCookie); + + COOKIE_LOGSUCCESS(SET_COOKIE, aHostURI, aCookieHeader, aCookie, foundCookie); + + // Now that list mutations are complete, notify observers. We do it here + // because observers may themselves attempt to mutate the list. + if (purgedList) { + NotifyChanged(purgedList, nsICookieNotification::COOKIES_BATCH_DELETED, + ""_ns); + } + + // Notify for topic "private-cookie-changed" or "cookie-changed" + NotifyChanged(aCookie, + foundCookie ? nsICookieNotification::COOKIE_CHANGED + : nsICookieNotification::COOKIE_ADDED, + aBaseDomain, aBrowsingContext, oldCookieIsSession); +} + +void CookieStorage::UpdateCookieOldestTime(Cookie* aCookie) { + if (aCookie->LastAccessed() < mCookieOldestTime) { + mCookieOldestTime = aCookie->LastAccessed(); + } +} + +void CookieStorage::MergeCookieSchemeMap(Cookie* aOldCookie, + Cookie* aNewCookie) { + aNewCookie->SetSchemeMap(aOldCookie->SchemeMap() | aNewCookie->SchemeMap()); +} + +void CookieStorage::AddCookieToList(const nsACString& aBaseDomain, + const OriginAttributes& aOriginAttributes, + Cookie* aCookie) { + if (!aCookie) { + NS_WARNING("Attempting to AddCookieToList with null cookie"); + return; + } + + CookieKey key(aBaseDomain, aOriginAttributes); + + CookieEntry* entry = mHostTable.PutEntry(key); + NS_ASSERTION(entry, "can't insert element into a null entry!"); + + entry->GetCookies().AppendElement(aCookie); + ++mCookieCount; + + // keep track of the oldest cookie, for when it comes time to purge + UpdateCookieOldestTime(aCookie); +} + +// static +already_AddRefed<nsIArray> CookieStorage::CreatePurgeList(nsICookie* aCookie) { + nsCOMPtr<nsIMutableArray> removedList = + do_CreateInstance(NS_ARRAY_CONTRACTID); + removedList->AppendElement(aCookie); + return removedList.forget(); +} + +// Given the output iter array and the count limit, find cookies +// sort by expiry and lastAccessed time. +// static +void CookieStorage::FindStaleCookies(CookieEntry* aEntry, int64_t aCurrentTime, + bool aIsSecure, + nsTArray<CookieListIter>& aOutput, + uint32_t aLimit) { + MOZ_ASSERT(aLimit); + + const CookieEntry::ArrayType& cookies = aEntry->GetCookies(); + aOutput.Clear(); + + CookieIterComparator comp(aCurrentTime); + nsTPriorityQueue<CookieListIter, CookieIterComparator> queue(comp); + + for (CookieEntry::IndexType i = 0; i < cookies.Length(); ++i) { + Cookie* cookie = cookies[i]; + + if (cookie->Expiry() <= aCurrentTime) { + queue.Push(CookieListIter(aEntry, i)); + continue; + } + + if (!aIsSecure) { + // We want to look for the non-secure cookie first time through, + // then find the secure cookie the second time this function is called. + if (cookie->IsSecure()) { + continue; + } + } + + queue.Push(CookieListIter(aEntry, i)); + } + + uint32_t count = 0; + while (!queue.IsEmpty() && count < aLimit) { + aOutput.AppendElement(queue.Pop()); + count++; + } +} + +// static +void CookieStorage::CreateOrUpdatePurgeList(nsCOMPtr<nsIArray>& aPurgedList, + nsICookie* aCookie) { + if (!aPurgedList) { + COOKIE_LOGSTRING(LogLevel::Debug, ("Creating new purge list")); + aPurgedList = CreatePurgeList(aCookie); + return; + } + + nsCOMPtr<nsIMutableArray> purgedList = do_QueryInterface(aPurgedList); + if (purgedList) { + COOKIE_LOGSTRING(LogLevel::Debug, ("Updating existing purge list")); + purgedList->AppendElement(aCookie); + } else { + COOKIE_LOGSTRING(LogLevel::Debug, ("Could not QI aPurgedList!")); + } +} + +// purges expired and old cookies in a batch operation. +already_AddRefed<nsIArray> CookieStorage::PurgeCookiesWithCallbacks( + int64_t aCurrentTimeInUsec, uint16_t aMaxNumberOfCookies, + int64_t aCookiePurgeAge, + std::function<void(const CookieListIter&)>&& aRemoveCookieCallback, + std::function<void()>&& aFinalizeCallback) { + NS_ASSERTION(mHostTable.Count() > 0, "table is empty"); + + uint32_t initialCookieCount = mCookieCount; + COOKIE_LOGSTRING(LogLevel::Debug, + ("PurgeCookies(): beginning purge with %" PRIu32 + " cookies and %" PRId64 " oldest age", + mCookieCount, aCurrentTimeInUsec - mCookieOldestTime)); + + using PurgeList = nsTArray<CookieListIter>; + PurgeList purgeList(kMaxNumberOfCookies); + + nsCOMPtr<nsIMutableArray> removedList = + do_CreateInstance(NS_ARRAY_CONTRACTID); + + int64_t currentTime = aCurrentTimeInUsec / PR_USEC_PER_SEC; + int64_t purgeTime = aCurrentTimeInUsec - aCookiePurgeAge; + int64_t oldestTime = INT64_MAX; + + for (auto iter = mHostTable.Iter(); !iter.Done(); iter.Next()) { + CookieEntry* entry = iter.Get(); + + const CookieEntry::ArrayType& cookies = entry->GetCookies(); + auto length = cookies.Length(); + for (CookieEntry::IndexType i = 0; i < length;) { + CookieListIter iter(entry, i); + Cookie* cookie = cookies[i]; + + // check if the cookie has expired + if (cookie->Expiry() <= currentTime) { + removedList->AppendElement(cookie); + COOKIE_LOGEVICTED(cookie, "Cookie expired"); + + // remove from list; do not increment our iterator, but stop if we're + // done already. + aRemoveCookieCallback(iter); + if (i == --length) { + break; + } + } else { + // check if the cookie is over the age limit + if (cookie->LastAccessed() <= purgeTime) { + purgeList.AppendElement(iter); + + } else if (cookie->LastAccessed() < oldestTime) { + // reset our indicator + oldestTime = cookie->LastAccessed(); + } + + ++i; + } + MOZ_ASSERT(length == cookies.Length()); + } + } + + uint32_t postExpiryCookieCount = mCookieCount; + + // now we have a list of iterators for cookies over the age limit. + // sort them by age, and then we'll see how many to remove... + purgeList.Sort(CompareCookiesByAge()); + + // only remove old cookies until we reach the max cookie limit, no more. + uint32_t excess = mCookieCount > aMaxNumberOfCookies + ? mCookieCount - aMaxNumberOfCookies + : 0; + if (purgeList.Length() > excess) { + // We're not purging everything in the list, so update our indicator. + oldestTime = purgeList[excess].Cookie()->LastAccessed(); + + purgeList.SetLength(excess); + } + + // sort the list again, this time grouping cookies with a common entryclass + // together, and with ascending index. this allows us to iterate backwards + // over the list removing cookies, without having to adjust indexes as we go. + purgeList.Sort(CompareCookiesByIndex()); + for (PurgeList::index_type i = purgeList.Length(); i--;) { + Cookie* cookie = purgeList[i].Cookie(); + removedList->AppendElement(cookie); + COOKIE_LOGEVICTED(cookie, "Cookie too old"); + + aRemoveCookieCallback(purgeList[i]); + } + + // Update the database if we have entries to purge. + if (aFinalizeCallback) { + aFinalizeCallback(); + } + + // reset the oldest time indicator + mCookieOldestTime = oldestTime; + + COOKIE_LOGSTRING(LogLevel::Debug, + ("PurgeCookies(): %" PRIu32 " expired; %" PRIu32 + " purged; %" PRIu32 " remain; %" PRId64 " oldest age", + initialCookieCount - postExpiryCookieCount, + postExpiryCookieCount - mCookieCount, mCookieCount, + aCurrentTimeInUsec - mCookieOldestTime)); + + return removedList.forget(); +} + +// remove a cookie from the hashtable, and update the iterator state. +void CookieStorage::RemoveCookieFromList(const CookieListIter& aIter) { + RemoveCookieFromDB(*aIter.Cookie()); + RemoveCookieFromListInternal(aIter); +} + +void CookieStorage::RemoveCookieFromListInternal(const CookieListIter& aIter) { + if (aIter.entry->GetCookies().Length() == 1) { + // we're removing the last element in the array - so just remove the entry + // from the hash. note that the entryclass' dtor will take care of + // releasing this last element for us! + mHostTable.RawRemoveEntry(aIter.entry); + + } else { + // just remove the element from the list + aIter.entry->GetCookies().RemoveElementAt(aIter.index); + } + + --mCookieCount; +} + +void CookieStorage::PrefChanged(nsIPrefBranch* aPrefBranch) { + int32_t val; + if (NS_SUCCEEDED(aPrefBranch->GetIntPref(kPrefMaxNumberOfCookies, &val))) { + mMaxNumberOfCookies = + static_cast<uint16_t> LIMIT(val, 1, 0xFFFF, kMaxNumberOfCookies); + } + + if (NS_SUCCEEDED(aPrefBranch->GetIntPref(kPrefCookieQuotaPerHost, &val))) { + mCookieQuotaPerHost = static_cast<uint16_t> LIMIT( + val, 1, mMaxCookiesPerHost - 1, kCookieQuotaPerHost); + } + + if (NS_SUCCEEDED(aPrefBranch->GetIntPref(kPrefMaxCookiesPerHost, &val))) { + mMaxCookiesPerHost = static_cast<uint16_t> LIMIT( + val, mCookieQuotaPerHost + 1, 0xFFFF, kMaxCookiesPerHost); + } + + if (NS_SUCCEEDED(aPrefBranch->GetIntPref(kPrefCookiePurgeAge, &val))) { + mCookiePurgeAge = + int64_t(LIMIT(val, 0, INT32_MAX, INT32_MAX)) * PR_USEC_PER_SEC; + } +} + +NS_IMETHODIMP +CookieStorage::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* /*aData*/) { + if (!strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) { + nsCOMPtr<nsIPrefBranch> prefBranch = do_QueryInterface(aSubject); + if (prefBranch) { + PrefChanged(prefBranch); + } + } else if (!strcmp(aTopic, OBSERVER_TOPIC_IDLE_DAILY)) { + CollectCookieJarSizeData(); + } + + return NS_OK; +} + +} // namespace net +} // namespace mozilla |