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 | |
parent | Initial commit. (diff) | |
download | firefox-e51783d008170d9ab27d25da98ca3a38b0a41b67.tar.xz firefox-e51783d008170d9ab27d25da98ca3a38b0a41b67.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'netwerk/cookie')
82 files changed, 14565 insertions, 0 deletions
diff --git a/netwerk/cookie/Cookie.cpp b/netwerk/cookie/Cookie.cpp new file mode 100644 index 0000000000..e9de561f88 --- /dev/null +++ b/netwerk/cookie/Cookie.cpp @@ -0,0 +1,289 @@ +/* -*- Mode: C++; tab-width: 4; 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 "CookieStorage.h" +#include "mozilla/Encoding.h" +#include "mozilla/dom/ToJSValue.h" +#include "mozilla/glean/GleanMetrics.h" +#include "mozilla/StaticPrefs_network.h" +#include "nsIURLParser.h" +#include "nsURLHelper.h" +#include <cstdlib> + +namespace mozilla { +namespace net { + +/****************************************************************************** + * Cookie: + * creation helper + ******************************************************************************/ + +// This is a counter that keeps track of the last used creation time, each time +// we create a new Cookie. This is nominally the time (in microseconds) the +// cookie was created, but is guaranteed to be monotonically increasing for +// cookies added at runtime after the database has been read in. This is +// necessary to enforce ordering among cookies whose creation times would +// otherwise overlap, since it's possible two cookies may be created at the +// same time, or that the system clock isn't monotonic. +static int64_t gLastCreationTime; + +int64_t Cookie::GenerateUniqueCreationTime(int64_t aCreationTime) { + // Check if the creation time given to us is greater than the running maximum + // (it should always be monotonically increasing). + if (aCreationTime > gLastCreationTime) { + gLastCreationTime = aCreationTime; + return aCreationTime; + } + + // Make up our own. + return ++gLastCreationTime; +} + +already_AddRefed<Cookie> Cookie::Create( + const CookieStruct& aCookieData, + const OriginAttributes& aOriginAttributes) { + RefPtr<Cookie> cookie = + Cookie::FromCookieStruct(aCookieData, aOriginAttributes); + + // If the creationTime given to us is higher than the running maximum, + // update our maximum. + if (cookie->mData.creationTime() > gLastCreationTime) { + gLastCreationTime = cookie->mData.creationTime(); + } + + return cookie.forget(); +} + +already_AddRefed<Cookie> Cookie::FromCookieStruct( + const CookieStruct& aCookieData, + const OriginAttributes& aOriginAttributes) { + RefPtr<Cookie> cookie = new Cookie(aCookieData, aOriginAttributes); + + // Ensure mValue contains a valid UTF-8 sequence. Otherwise XPConnect will + // truncate the string after the first invalid octet. + UTF_8_ENCODING->DecodeWithoutBOMHandling(aCookieData.value(), + cookie->mData.value()); + + // If sameSite/rawSameSite values aren't sensible reset to Default + // cf. 5.4.7 in draft-ietf-httpbis-rfc6265bis-09 + if (!Cookie::ValidateSameSite(cookie->mData)) { + cookie->mData.sameSite() = nsICookie::SAMESITE_LAX; + cookie->mData.rawSameSite() = nsICookie::SAMESITE_NONE; + } + + return cookie.forget(); +} + +already_AddRefed<Cookie> Cookie::CreateValidated( + const CookieStruct& aCookieData, + const OriginAttributes& aOriginAttributes) { + if (!StaticPrefs::network_cookie_fixup_on_db_load()) { + return Cookie::Create(aCookieData, aOriginAttributes); + } + + RefPtr<Cookie> cookie = + Cookie::FromCookieStruct(aCookieData, aOriginAttributes); + + int64_t currentTimeInUsec = PR_Now(); + // Assert that the last creation time is not higher than the current time. + // The 10000 wiggle room accounts for the fact that calling + // GenerateUniqueCreationTime might go over the value of PR_Now(), but we'd + // most likely not add 10000 cookies in a row. + MOZ_ASSERT(gLastCreationTime < currentTimeInUsec + 10000, + "Last creation time must not be higher than NOW"); + + // If the creationTime given to us is higher than the current time then + // update the creation time to now. + if (cookie->mData.creationTime() > currentTimeInUsec) { + uint64_t diffInSeconds = + (cookie->mData.creationTime() - currentTimeInUsec) / PR_USEC_PER_SEC; + mozilla::glean::networking::cookie_creation_fixup_diff.AccumulateSamples( + {diffInSeconds}); + glean::networking::cookie_timestamp_fixed_count.Get("creationTime"_ns) + .Add(1); + + cookie->mData.creationTime() = + GenerateUniqueCreationTime(currentTimeInUsec); + } + + if (cookie->mData.lastAccessed() > currentTimeInUsec) { + uint64_t diffInSeconds = + (cookie->mData.lastAccessed() - currentTimeInUsec) / PR_USEC_PER_SEC; + mozilla::glean::networking::cookie_access_fixup_diff.AccumulateSamples( + {diffInSeconds}); + glean::networking::cookie_timestamp_fixed_count.Get("lastAccessed"_ns) + .Add(1); + + cookie->mData.lastAccessed() = currentTimeInUsec; + } + + return cookie.forget(); +} + +size_t Cookie::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const { + return aMallocSizeOf(this) + + mData.name().SizeOfExcludingThisIfUnshared(MallocSizeOf) + + mData.value().SizeOfExcludingThisIfUnshared(MallocSizeOf) + + mData.host().SizeOfExcludingThisIfUnshared(MallocSizeOf) + + mData.path().SizeOfExcludingThisIfUnshared(MallocSizeOf) + + mFilePathCache.SizeOfExcludingThisIfUnshared(MallocSizeOf); +} + +bool Cookie::IsStale() const { + int64_t currentTimeInUsec = PR_Now(); + + return currentTimeInUsec - LastAccessed() > + StaticPrefs::network_cookie_staleThreshold() * PR_USEC_PER_SEC; +} + +/****************************************************************************** + * Cookie: + * xpcom impl + ******************************************************************************/ + +// xpcom getters +NS_IMETHODIMP Cookie::GetName(nsACString& aName) { + aName = Name(); + return NS_OK; +} +NS_IMETHODIMP Cookie::GetValue(nsACString& aValue) { + aValue = Value(); + return NS_OK; +} +NS_IMETHODIMP Cookie::GetHost(nsACString& aHost) { + aHost = Host(); + return NS_OK; +} +NS_IMETHODIMP Cookie::GetRawHost(nsACString& aHost) { + aHost = RawHost(); + return NS_OK; +} +NS_IMETHODIMP Cookie::GetPath(nsACString& aPath) { + aPath = Path(); + return NS_OK; +} +NS_IMETHODIMP Cookie::GetExpiry(int64_t* aExpiry) { + *aExpiry = Expiry(); + return NS_OK; +} +NS_IMETHODIMP Cookie::GetIsSession(bool* aIsSession) { + *aIsSession = IsSession(); + return NS_OK; +} +NS_IMETHODIMP Cookie::GetIsDomain(bool* aIsDomain) { + *aIsDomain = IsDomain(); + return NS_OK; +} +NS_IMETHODIMP Cookie::GetIsSecure(bool* aIsSecure) { + *aIsSecure = IsSecure(); + return NS_OK; +} +NS_IMETHODIMP Cookie::GetIsHttpOnly(bool* aHttpOnly) { + *aHttpOnly = IsHttpOnly(); + return NS_OK; +} +NS_IMETHODIMP Cookie::GetIsPartitioned(bool* aPartitioned) { + *aPartitioned = IsPartitioned(); + return NS_OK; +} +NS_IMETHODIMP Cookie::GetCreationTime(int64_t* aCreation) { + *aCreation = CreationTime(); + return NS_OK; +} +NS_IMETHODIMP Cookie::GetLastAccessed(int64_t* aTime) { + *aTime = LastAccessed(); + return NS_OK; +} +NS_IMETHODIMP Cookie::GetSameSite(int32_t* aSameSite) { + if (StaticPrefs::network_cookie_sameSite_laxByDefault()) { + *aSameSite = SameSite(); + } else { + *aSameSite = RawSameSite(); + } + return NS_OK; +} +NS_IMETHODIMP Cookie::GetSchemeMap(nsICookie::schemeType* aSchemeMap) { + *aSchemeMap = static_cast<nsICookie::schemeType>(SchemeMap()); + return NS_OK; +} + +NS_IMETHODIMP +Cookie::GetOriginAttributes(JSContext* aCx, JS::MutableHandle<JS::Value> aVal) { + if (NS_WARN_IF(!ToJSValue(aCx, mOriginAttributes, aVal))) { + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +const OriginAttributes& Cookie::OriginAttributesNative() { + return mOriginAttributes; +} + +const Cookie& Cookie::AsCookie() { return *this; } + +const nsCString& Cookie::GetFilePath() { + MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread()); + + if (Path().IsEmpty()) { + // If we don't have a path, just return the (empty) file path cache. + return mFilePathCache; + } + if (!mFilePathCache.IsEmpty()) { + // If we've computed the answer before, just return it. + return mFilePathCache; + } + + nsIURLParser* parser = net_GetStdURLParser(); + NS_ENSURE_TRUE(parser, mFilePathCache); + + int32_t pathLen = Path().Length(); + int32_t filepathLen = 0; + uint32_t filepathPos = 0; + + nsresult rv = parser->ParsePath(PromiseFlatCString(Path()).get(), pathLen, + &filepathPos, &filepathLen, nullptr, + nullptr, // don't care about query + nullptr, nullptr); // don't care about ref + NS_ENSURE_SUCCESS(rv, mFilePathCache); + + mFilePathCache = Substring(Path(), filepathPos, filepathLen); + + return mFilePathCache; +} + +// compatibility method, for use with the legacy nsICookie interface. +// here, expires == 0 denotes a session cookie. +NS_IMETHODIMP +Cookie::GetExpires(uint64_t* aExpires) { + if (IsSession()) { + *aExpires = 0; + } else { + *aExpires = Expiry() > 0 ? Expiry() : 1; + } + return NS_OK; +} + +// static +bool Cookie::ValidateSameSite(const CookieStruct& aCookieData) { + // For proper migration towards a laxByDefault world, + // sameSite is initialized to LAX even though the server + // has never sent it. + if (aCookieData.rawSameSite() == aCookieData.sameSite()) { + return aCookieData.rawSameSite() >= nsICookie::SAMESITE_NONE && + aCookieData.rawSameSite() <= nsICookie::SAMESITE_STRICT; + } + return aCookieData.rawSameSite() == nsICookie::SAMESITE_NONE && + aCookieData.sameSite() == nsICookie::SAMESITE_LAX; +} + +already_AddRefed<Cookie> Cookie::Clone() const { + return Create(mData, OriginAttributesRef()); +} + +NS_IMPL_ISUPPORTS(Cookie, nsICookie) + +} // namespace net +} // namespace mozilla diff --git a/netwerk/cookie/Cookie.h b/netwerk/cookie/Cookie.h new file mode 100644 index 0000000000..5fa3479268 --- /dev/null +++ b/netwerk/cookie/Cookie.h @@ -0,0 +1,165 @@ +/* -*- Mode: C++; tab-width: 4; 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/. */ + +#ifndef mozilla_net_Cookie_h +#define mozilla_net_Cookie_h + +#include "nsICookie.h" +#include "nsIMemoryReporter.h" +#include "nsString.h" + +#include "mozilla/MemoryReporting.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/net/NeckoChannelParams.h" +#include "nsIMemoryReporter.h" + +using mozilla::OriginAttributes; + +namespace mozilla { +namespace net { + +/** + * The Cookie class is the main cookie storage medium for use within cookie + * code. + */ + +/****************************************************************************** + * Cookie: + * implementation + ******************************************************************************/ + +class Cookie final : public nsICookie { + MOZ_DEFINE_MALLOC_SIZE_OF(MallocSizeOf) + + public: + // nsISupports + NS_DECL_ISUPPORTS + NS_DECL_NSICOOKIE + + private: + // for internal use only. see Cookie::Create(). + Cookie(const CookieStruct& aCookieData, + const OriginAttributes& aOriginAttributes) + : mData(aCookieData), mOriginAttributes(aOriginAttributes) {} + + static already_AddRefed<Cookie> FromCookieStruct( + const CookieStruct& aCookieData, + const OriginAttributes& aOriginAttributes); + + public: + // Returns false if rawSameSite has an invalid value, compared to sameSite. + static bool ValidateSameSite(const CookieStruct& aCookieData); + + // Generate a unique and monotonically increasing creation time. See comment + // in Cookie.cpp. + static int64_t GenerateUniqueCreationTime(int64_t aCreationTime); + + // public helper to create an Cookie object. + static already_AddRefed<Cookie> Create( + const CookieStruct& aCookieData, + const OriginAttributes& aOriginAttributes); + + // Same as Cookie::Create but fixes the lastAccessed and creationDates + // if they are set in the future. + // Should only get called from CookiePersistentStorage::InitDBConn + static already_AddRefed<Cookie> CreateValidated( + const CookieStruct& aCookieData, + const OriginAttributes& aOriginAttributes); + + size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const; + + // fast (inline, non-xpcom) getters + inline const nsCString& Name() const { return mData.name(); } + inline const nsCString& Value() const { return mData.value(); } + inline const nsCString& Host() const { return mData.host(); } + inline nsDependentCSubstring RawHost() const { + return nsDependentCSubstring(mData.host(), IsDomain() ? 1 : 0); + } + inline const nsCString& Path() const { return mData.path(); } + const nsCString& GetFilePath(); + inline int64_t Expiry() const { return mData.expiry(); } // in seconds + inline int64_t LastAccessed() const { + return mData.lastAccessed(); + } // in microseconds + inline int64_t CreationTime() const { + return mData.creationTime(); + } // in microseconds + inline bool IsSession() const { return mData.isSession(); } + inline bool IsDomain() const { return *mData.host().get() == '.'; } + inline bool IsSecure() const { return mData.isSecure(); } + inline bool IsHttpOnly() const { return mData.isHttpOnly(); } + inline bool IsPartitioned() const { + return !mOriginAttributes.mPartitionKey.IsEmpty(); + } + inline bool RawIsPartitioned() const { return mData.isPartitioned(); } + inline const OriginAttributes& OriginAttributesRef() const { + return mOriginAttributes; + } + inline int32_t SameSite() const { return mData.sameSite(); } + inline int32_t RawSameSite() const { return mData.rawSameSite(); } + inline bool IsDefaultSameSite() const { + return SameSite() == nsICookie::SAMESITE_LAX && + RawSameSite() == nsICookie::SAMESITE_NONE; + } + inline uint8_t SchemeMap() const { return mData.schemeMap(); } + + // setters + inline void SetExpiry(int64_t aExpiry) { mData.expiry() = aExpiry; } + inline void SetLastAccessed(int64_t aTime) { mData.lastAccessed() = aTime; } + inline void SetIsSession(bool aIsSession) { mData.isSession() = aIsSession; } + inline bool SetIsHttpOnly(bool aIsHttpOnly) { + return mData.isHttpOnly() = aIsHttpOnly; + } + // Set the creation time manually, overriding the monotonicity checks in + // Create(). Use with caution! + inline void SetCreationTime(int64_t aTime) { mData.creationTime() = aTime; } + inline void SetSchemeMap(uint8_t aSchemeMap) { + mData.schemeMap() = aSchemeMap; + } + inline void SetHost(const nsACString& aHost) { mData.host() = aHost; } + + bool IsStale() const; + + const CookieStruct& ToIPC() const { return mData; } + + already_AddRefed<Cookie> Clone() const; + + protected: + virtual ~Cookie() = default; + + private: + // member variables + // + // Please update SizeOfIncludingThis if this strategy changes. + CookieStruct mData; + OriginAttributes mOriginAttributes; + nsCString mFilePathCache; +}; + +// Comparator class for sorting cookies before sending to a server. +class CompareCookiesForSending { + public: + bool Equals(const Cookie* aCookie1, const Cookie* aCookie2) const { + return aCookie1->CreationTime() == aCookie2->CreationTime() && + aCookie2->Path().Length() == aCookie1->Path().Length(); + } + + bool LessThan(const Cookie* aCookie1, const Cookie* aCookie2) const { + // compare by cookie path length in accordance with RFC2109 + int32_t result = aCookie2->Path().Length() - aCookie1->Path().Length(); + if (result != 0) return result < 0; + + // when path lengths match, older cookies should be listed first. this is + // required for backwards compatibility since some websites erroneously + // depend on receiving cookies in the order in which they were sent to the + // browser! see bug 236772. + return aCookie1->CreationTime() < aCookie2->CreationTime(); + } +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_Cookie_h diff --git a/netwerk/cookie/CookieCommons.cpp b/netwerk/cookie/CookieCommons.cpp new file mode 100644 index 0000000000..3708f23daa --- /dev/null +++ b/netwerk/cookie/CookieCommons.cpp @@ -0,0 +1,749 @@ +/* -*- 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 "CookieService.h" +#include "mozilla/ConsoleReportCollector.h" +#include "mozilla/ContentBlockingNotifier.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/StaticPrefs_network.h" +#include "mozilla/StorageAccess.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/nsMixedContentBlocker.h" +#include "mozilla/net/CookieJarSettings.h" +#include "mozIThirdPartyUtil.h" +#include "nsContentUtils.h" +#include "nsICookiePermission.h" +#include "nsICookieService.h" +#include "nsIEffectiveTLDService.h" +#include "nsIRedirectHistoryEntry.h" +#include "nsIWebProgressListener.h" +#include "nsNetUtil.h" +#include "nsScriptSecurityManager.h" +#include "ThirdPartyUtil.h" + +namespace mozilla { + +using dom::Document; + +namespace net { + +// static +bool CookieCommons::DomainMatches(Cookie* aCookie, const nsACString& aHost) { + // first, check for an exact host or domain cookie match, e.g. "google.com" + // or ".google.com"; second a subdomain match, e.g. + // host = "mail.google.com", cookie domain = ".google.com". + return aCookie->RawHost() == aHost || + (aCookie->IsDomain() && StringEndsWith(aHost, aCookie->Host())); +} + +// static +bool CookieCommons::PathMatches(Cookie* aCookie, const nsACString& aPath) { + nsCString cookiePath(aCookie->GetFilePath()); + + // if our cookie path is empty we can't really perform our prefix check, and + // also we can't check the last character of the cookie path, so we would + // never return a successful match. + if (cookiePath.IsEmpty()) { + return false; + } + + // if the cookie path and the request path are identical, they match. + if (cookiePath.Equals(aPath)) { + return true; + } + + // if the cookie path is a prefix of the request path, and the last character + // of the cookie path is %x2F ("/"), they match. + bool isPrefix = StringBeginsWith(aPath, cookiePath); + if (isPrefix && cookiePath.Last() == '/') { + return true; + } + + // if the cookie path is a prefix of the request path, and the first character + // of the request path that is not included in the cookie path is a %x2F ("/") + // character, they match. + uint32_t cookiePathLen = cookiePath.Length(); + return isPrefix && aPath[cookiePathLen] == '/'; +} + +// Get the base domain for aHostURI; e.g. for "www.bbc.co.uk", this would be +// "bbc.co.uk". Only properly-formed URI's are tolerated, though a trailing +// dot may be present. If aHostURI is an IP address, an alias such as +// 'localhost', an eTLD such as 'co.uk', or the empty string, aBaseDomain will +// be the exact host, and aRequireHostMatch will be true to indicate that +// substring matches should not be performed. +nsresult CookieCommons::GetBaseDomain(nsIEffectiveTLDService* aTLDService, + nsIURI* aHostURI, nsACString& aBaseDomain, + bool& aRequireHostMatch) { + // get the base domain. this will fail if the host contains a leading dot, + // more than one trailing dot, or is otherwise malformed. + nsresult rv = aTLDService->GetBaseDomain(aHostURI, 0, aBaseDomain); + aRequireHostMatch = rv == NS_ERROR_HOST_IS_IP_ADDRESS || + rv == NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS; + if (aRequireHostMatch) { + // aHostURI is either an IP address, an alias such as 'localhost', an eTLD + // such as 'co.uk', or the empty string. use the host as a key in such + // cases. + rv = nsContentUtils::GetHostOrIPv6WithBrackets(aHostURI, aBaseDomain); + } + NS_ENSURE_SUCCESS(rv, rv); + + // aHost (and thus aBaseDomain) may be the string '.'. If so, fail. + if (aBaseDomain.Length() == 1 && aBaseDomain.Last() == '.') { + return NS_ERROR_INVALID_ARG; + } + + // block any URIs without a host that aren't file:// URIs. + if (aBaseDomain.IsEmpty() && !aHostURI->SchemeIs("file")) { + return NS_ERROR_INVALID_ARG; + } + + return NS_OK; +} + +nsresult CookieCommons::GetBaseDomain(nsIPrincipal* aPrincipal, + nsACString& aBaseDomain) { + MOZ_ASSERT(aPrincipal); + + // for historical reasons we use ascii host for file:// URLs. + if (aPrincipal->SchemeIs("file")) { + return nsContentUtils::GetHostOrIPv6WithBrackets(aPrincipal, aBaseDomain); + } + + nsresult rv = aPrincipal->GetBaseDomain(aBaseDomain); + if (NS_FAILED(rv)) { + return rv; + } + + nsContentUtils::MaybeFixIPv6Host(aBaseDomain); + return NS_OK; +} + +// Get the base domain for aHost; e.g. for "www.bbc.co.uk", this would be +// "bbc.co.uk". This is done differently than GetBaseDomain(mTLDService, ): it +// is assumed that aHost is already normalized, and it may contain a leading dot +// (indicating that it represents a domain). A trailing dot may be present. +// If aHost is an IP address, an alias such as 'localhost', an eTLD such as +// 'co.uk', or the empty string, aBaseDomain will be the exact host, and a +// leading dot will be treated as an error. +nsresult CookieCommons::GetBaseDomainFromHost( + nsIEffectiveTLDService* aTLDService, const nsACString& aHost, + nsCString& aBaseDomain) { + // aHost must not be the string '.'. + if (aHost.Length() == 1 && aHost.Last() == '.') { + return NS_ERROR_INVALID_ARG; + } + + // aHost may contain a leading dot; if so, strip it now. + bool domain = !aHost.IsEmpty() && aHost.First() == '.'; + + // get the base domain. this will fail if the host contains a leading dot, + // more than one trailing dot, or is otherwise malformed. + nsresult rv = aTLDService->GetBaseDomainFromHost(Substring(aHost, domain), 0, + aBaseDomain); + if (rv == NS_ERROR_HOST_IS_IP_ADDRESS || + rv == NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS) { + // aHost is either an IP address, an alias such as 'localhost', an eTLD + // such as 'co.uk', or the empty string. use the host as a key in such + // cases; however, we reject any such hosts with a leading dot, since it + // doesn't make sense for them to be domain cookies. + if (domain) { + return NS_ERROR_INVALID_ARG; + } + + aBaseDomain = aHost; + return NS_OK; + } + return rv; +} + +namespace { + +void NotifyRejectionToObservers(nsIURI* aHostURI, CookieOperation aOperation) { + if (aOperation == OPERATION_WRITE) { + nsCOMPtr<nsIObserverService> os = services::GetObserverService(); + if (os) { + os->NotifyObservers(aHostURI, "cookie-rejected", nullptr); + } + } else { + MOZ_ASSERT(aOperation == OPERATION_READ); + } +} + +} // namespace + +// Notify observers that a cookie was rejected due to the users' prefs. +void CookieCommons::NotifyRejected(nsIURI* aHostURI, nsIChannel* aChannel, + uint32_t aRejectedReason, + CookieOperation aOperation) { + NotifyRejectionToObservers(aHostURI, aOperation); + + ContentBlockingNotifier::OnDecision( + aChannel, ContentBlockingNotifier::BlockingDecision::eBlock, + aRejectedReason); +} + +bool CookieCommons::CheckPathSize(const CookieStruct& aCookieData) { + return aCookieData.path().Length() <= kMaxBytesPerPath; +} + +bool CookieCommons::CheckNameAndValueSize(const CookieStruct& aCookieData) { + // reject cookie if it's over the size limit, per RFC2109 + return (aCookieData.name().Length() + aCookieData.value().Length()) <= + kMaxBytesPerCookie; +} + +bool CookieCommons::CheckName(const CookieStruct& aCookieData) { + const char illegalNameCharacters[] = { + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, + 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, + 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x3B, 0x3D, 0x7F, 0x00}; + + const auto* start = aCookieData.name().BeginReading(); + const auto* end = aCookieData.name().EndReading(); + + auto charFilter = [&](unsigned char c) { + if (StaticPrefs::network_cookie_blockUnicode() && c >= 0x80) { + return true; + } + return std::find(std::begin(illegalNameCharacters), + std::end(illegalNameCharacters), + c) != std::end(illegalNameCharacters); + }; + + return std::find_if(start, end, charFilter) == end; +} + +bool CookieCommons::CheckValue(const CookieStruct& aCookieData) { + // reject cookie if value contains an RFC 6265 disallowed character - see + // https://bugzilla.mozilla.org/show_bug.cgi?id=1191423 + // NOTE: this is not the full set of characters disallowed by 6265 - notably + // 0x09, 0x20, 0x22, 0x2C, and 0x5C are missing from this list. + const char illegalCharacters[] = { + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x0A, 0x0B, 0x0C, + 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x3B, 0x7F, 0x00}; + + const auto* start = aCookieData.value().BeginReading(); + const auto* end = aCookieData.value().EndReading(); + + auto charFilter = [&](unsigned char c) { + if (StaticPrefs::network_cookie_blockUnicode() && c >= 0x80) { + return true; + } + return std::find(std::begin(illegalCharacters), std::end(illegalCharacters), + c) != std::end(illegalCharacters); + }; + + return std::find_if(start, end, charFilter) == end; +} + +// static +bool CookieCommons::CheckCookiePermission(nsIChannel* aChannel, + CookieStruct& aCookieData) { + if (!aChannel) { + // No channel, let's assume this is a system-principal request. + return true; + } + + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + nsCOMPtr<nsICookieJarSettings> cookieJarSettings; + nsresult rv = + loadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return true; + } + + nsIScriptSecurityManager* ssm = + nsScriptSecurityManager::GetScriptSecurityManager(); + MOZ_ASSERT(ssm); + + nsCOMPtr<nsIPrincipal> channelPrincipal; + rv = ssm->GetChannelURIPrincipal(aChannel, getter_AddRefs(channelPrincipal)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + return CheckCookiePermission(channelPrincipal, cookieJarSettings, + aCookieData); +} + +// static +bool CookieCommons::CheckCookiePermission( + nsIPrincipal* aPrincipal, nsICookieJarSettings* aCookieJarSettings, + CookieStruct& aCookieData) { + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(aCookieJarSettings); + + if (!aPrincipal->GetIsContentPrincipal()) { + return true; + } + + uint32_t cookiePermission = nsICookiePermission::ACCESS_DEFAULT; + nsresult rv = + aCookieJarSettings->CookiePermission(aPrincipal, &cookiePermission); + if (NS_WARN_IF(NS_FAILED(rv))) { + return true; + } + + if (cookiePermission == nsICookiePermission::ACCESS_ALLOW) { + return true; + } + + if (cookiePermission == nsICookiePermission::ACCESS_SESSION) { + aCookieData.isSession() = true; + return true; + } + + if (cookiePermission == nsICookiePermission::ACCESS_DENY) { + return false; + } + + return true; +} + +namespace { + +CookieStatus CookieStatusForWindow(nsPIDOMWindowInner* aWindow, + nsIURI* aDocumentURI) { + MOZ_ASSERT(aWindow); + MOZ_ASSERT(aDocumentURI); + + ThirdPartyUtil* thirdPartyUtil = ThirdPartyUtil::GetInstance(); + if (thirdPartyUtil) { + bool isThirdParty = true; + + nsresult rv = thirdPartyUtil->IsThirdPartyWindow( + aWindow->GetOuterWindow(), aDocumentURI, &isThirdParty); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Third-party window check failed."); + + if (NS_SUCCEEDED(rv) && !isThirdParty) { + return STATUS_ACCEPTED; + } + } + + if (StaticPrefs::network_cookie_thirdparty_sessionOnly()) { + return STATUS_ACCEPT_SESSION; + } + + if (StaticPrefs::network_cookie_thirdparty_nonsecureSessionOnly() && + !nsMixedContentBlocker::IsPotentiallyTrustworthyOrigin(aDocumentURI)) { + return STATUS_ACCEPT_SESSION; + } + + return STATUS_ACCEPTED; +} + +} // namespace + +// static +already_AddRefed<Cookie> CookieCommons::CreateCookieFromDocument( + Document* aDocument, const nsACString& aCookieString, + int64_t currentTimeInUsec, nsIEffectiveTLDService* aTLDService, + mozIThirdPartyUtil* aThirdPartyUtil, + std::function<bool(const nsACString&, const OriginAttributes&)>&& + aHasExistingCookiesLambda, + nsIURI** aDocumentURI, nsACString& aBaseDomain, OriginAttributes& aAttrs) { + nsCOMPtr<nsIPrincipal> storagePrincipal = + aDocument->EffectiveCookiePrincipal(); + MOZ_ASSERT(storagePrincipal); + + nsCOMPtr<nsIURI> principalURI; + auto* basePrincipal = BasePrincipal::Cast(aDocument->NodePrincipal()); + basePrincipal->GetURI(getter_AddRefs(principalURI)); + if (NS_WARN_IF(!principalURI)) { + // Document's principal is not a content or null (may be system), so + // can't set cookies + return nullptr; + } + + if (!CookieCommons::IsSchemeSupported(principalURI)) { + return nullptr; + } + + nsAutoCString baseDomain; + bool requireHostMatch = false; + nsresult rv = CookieCommons::GetBaseDomain(aTLDService, principalURI, + baseDomain, requireHostMatch); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + nsPIDOMWindowInner* innerWindow = aDocument->GetInnerWindow(); + if (NS_WARN_IF(!innerWindow)) { + return nullptr; + } + + // Check if limit-foreign is required. + uint32_t dummyRejectedReason = 0; + if (aDocument->CookieJarSettings()->GetLimitForeignContexts() && + !aHasExistingCookiesLambda(baseDomain, + storagePrincipal->OriginAttributesRef()) && + !ShouldAllowAccessFor(innerWindow, principalURI, &dummyRejectedReason)) { + return nullptr; + } + + bool isForeignAndNotAddon = false; + if (!BasePrincipal::Cast(aDocument->NodePrincipal())->AddonPolicy()) { + rv = aThirdPartyUtil->IsThirdPartyWindow( + innerWindow->GetOuterWindow(), principalURI, &isForeignAndNotAddon); + if (NS_WARN_IF(NS_FAILED(rv))) { + isForeignAndNotAddon = true; + } + } + + bool mustBePartitioned = + isForeignAndNotAddon && + aDocument->CookieJarSettings()->GetCookieBehavior() == + nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN && + !aDocument->UsingStorageAccess(); + + // If we are here, we have been already accepted by the anti-tracking. + // We just need to check if we have to be in session-only mode. + CookieStatus cookieStatus = CookieStatusForWindow(innerWindow, principalURI); + MOZ_ASSERT(cookieStatus == STATUS_ACCEPTED || + cookieStatus == STATUS_ACCEPT_SESSION); + + // Console report takes care of the correct reporting at the exit of this + // method. + RefPtr<ConsoleReportCollector> crc = new ConsoleReportCollector(); + auto scopeExit = MakeScopeExit([&] { crc->FlushConsoleReports(aDocument); }); + + nsCString cookieString(aCookieString); + + CookieStruct cookieData; + MOZ_ASSERT(cookieData.creationTime() == 0, "Must be initialized to 0"); + bool canSetCookie = false; + CookieService::CanSetCookie(principalURI, baseDomain, cookieData, + requireHostMatch, cookieStatus, cookieString, + false, isForeignAndNotAddon, mustBePartitioned, + crc, canSetCookie); + + if (!canSetCookie) { + return nullptr; + } + + // check permissions from site permission list. + if (!CookieCommons::CheckCookiePermission(aDocument->NodePrincipal(), + aDocument->CookieJarSettings(), + cookieData)) { + NotifyRejectionToObservers(principalURI, OPERATION_WRITE); + ContentBlockingNotifier::OnDecision( + innerWindow, ContentBlockingNotifier::BlockingDecision::eBlock, + nsIWebProgressListener::STATE_COOKIES_BLOCKED_BY_PERMISSION); + return nullptr; + } + + RefPtr<Cookie> cookie = + Cookie::Create(cookieData, storagePrincipal->OriginAttributesRef()); + MOZ_ASSERT(cookie); + + cookie->SetLastAccessed(currentTimeInUsec); + cookie->SetCreationTime( + Cookie::GenerateUniqueCreationTime(currentTimeInUsec)); + + aBaseDomain = baseDomain; + aAttrs = storagePrincipal->OriginAttributesRef(); + principalURI.forget(aDocumentURI); + + return cookie.forget(); +} + +// static +already_AddRefed<nsICookieJarSettings> CookieCommons::GetCookieJarSettings( + nsIChannel* aChannel) { + nsCOMPtr<nsICookieJarSettings> cookieJarSettings; + bool shouldResistFingerprinting = nsContentUtils::ShouldResistFingerprinting( + aChannel, RFPTarget::IsAlwaysEnabledForPrecompute); + if (aChannel) { + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + nsresult rv = + loadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings)); + if (NS_WARN_IF(NS_FAILED(rv))) { + cookieJarSettings = + CookieJarSettings::GetBlockingAll(shouldResistFingerprinting); + } + } else { + cookieJarSettings = CookieJarSettings::Create(CookieJarSettings::eRegular, + shouldResistFingerprinting); + } + + MOZ_ASSERT(cookieJarSettings); + return cookieJarSettings.forget(); +} + +// static +bool CookieCommons::ShouldIncludeCrossSiteCookieForDocument( + Cookie* aCookie, dom::Document* aDocument) { + MOZ_ASSERT(aCookie); + MOZ_ASSERT(aDocument); + + int32_t sameSiteAttr = 0; + aCookie->GetSameSite(&sameSiteAttr); + + if (aDocument->CookieJarSettings()->GetPartitionForeign() && + StaticPrefs::network_cookie_cookieBehavior_optInPartitioning()) { + return false; + } + + return sameSiteAttr == nsICookie::SAMESITE_NONE; +} + +bool CookieCommons::IsSafeTopLevelNav(nsIChannel* aChannel) { + if (!aChannel) { + return false; + } + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + nsCOMPtr<nsIInterceptionInfo> interceptionInfo = loadInfo->InterceptionInfo(); + if ((loadInfo->GetExternalContentPolicyType() != + ExtContentPolicy::TYPE_DOCUMENT && + loadInfo->GetExternalContentPolicyType() != + ExtContentPolicy::TYPE_SAVEAS_DOWNLOAD) && + !interceptionInfo) { + return false; + } + + if (interceptionInfo && + interceptionInfo->GetExtContentPolicyType() != + ExtContentPolicy::TYPE_DOCUMENT && + interceptionInfo->GetExtContentPolicyType() != + ExtContentPolicy::TYPE_SAVEAS_DOWNLOAD && + interceptionInfo->GetExtContentPolicyType() != + ExtContentPolicy::TYPE_INVALID) { + return false; + } + + return NS_IsSafeMethodNav(aChannel); +} + +// This function determines if two schemes are equal in the context of +// "Schemeful SameSite cookies". +// +// Two schemes are considered equal: +// - if the "network.cookie.sameSite.schemeful" pref is set to false. +// OR +// - if one of the schemes is not http or https. +// OR +// - if both schemes are equal AND both are either http or https. +bool IsSameSiteSchemeEqual(const nsACString& aFirstScheme, + const nsACString& aSecondScheme) { + if (!StaticPrefs::network_cookie_sameSite_schemeful()) { + return true; + } + + auto isSchemeHttpOrHttps = [](const nsACString& scheme) -> bool { + return scheme.EqualsLiteral("http") || scheme.EqualsLiteral("https"); + }; + + if (!isSchemeHttpOrHttps(aFirstScheme) || + !isSchemeHttpOrHttps(aSecondScheme)) { + return true; + } + + return aFirstScheme.Equals(aSecondScheme); +} + +bool CookieCommons::IsSameSiteForeign(nsIChannel* aChannel, nsIURI* aHostURI, + bool* aHadCrossSiteRedirects) { + *aHadCrossSiteRedirects = false; + + if (!aChannel) { + return false; + } + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + // Do not treat loads triggered by web extensions as foreign + nsCOMPtr<nsIURI> channelURI; + NS_GetFinalChannelURI(aChannel, getter_AddRefs(channelURI)); + + nsCOMPtr<nsIInterceptionInfo> interceptionInfo = loadInfo->InterceptionInfo(); + + RefPtr<BasePrincipal> triggeringPrincipal; + ExtContentPolicy contentPolicyType; + if (interceptionInfo && interceptionInfo->TriggeringPrincipal()) { + triggeringPrincipal = + BasePrincipal::Cast(interceptionInfo->TriggeringPrincipal()); + contentPolicyType = interceptionInfo->GetExtContentPolicyType(); + } else { + triggeringPrincipal = BasePrincipal::Cast(loadInfo->TriggeringPrincipal()); + contentPolicyType = loadInfo->GetExternalContentPolicyType(); + + if (triggeringPrincipal->AddonPolicy() && + triggeringPrincipal->AddonAllowsLoad(channelURI)) { + return false; + } + } + const nsTArray<nsCOMPtr<nsIRedirectHistoryEntry>>& redirectChain( + interceptionInfo && interceptionInfo->TriggeringPrincipal() + ? interceptionInfo->RedirectChain() + : loadInfo->RedirectChain()); + + nsAutoCString hostScheme, otherScheme; + aHostURI->GetScheme(hostScheme); + + bool isForeign = true; + nsresult rv; + if (contentPolicyType == ExtContentPolicy::TYPE_DOCUMENT || + contentPolicyType == ExtContentPolicy::TYPE_SAVEAS_DOWNLOAD) { + // for loads of TYPE_DOCUMENT we query the hostURI from the + // triggeringPrincipal which returns the URI of the document that caused the + // navigation. + rv = triggeringPrincipal->IsThirdPartyChannel(aChannel, &isForeign); + + triggeringPrincipal->GetScheme(otherScheme); + } else { + // If the load is caused by FetchEvent.request or NavigationPreload request, + // check the original InterceptedHttpChannel is a third-party channel or + // not. + if (interceptionInfo && interceptionInfo->TriggeringPrincipal()) { + isForeign = interceptionInfo->FromThirdParty(); + if (isForeign) { + return true; + } + } + nsCOMPtr<mozIThirdPartyUtil> thirdPartyUtil = + do_GetService(THIRDPARTYUTIL_CONTRACTID); + if (!thirdPartyUtil) { + return true; + } + rv = thirdPartyUtil->IsThirdPartyChannel(aChannel, aHostURI, &isForeign); + + channelURI->GetScheme(otherScheme); + } + // if we are dealing with a cross origin request, we can return here + // because we already know the request is 'foreign'. + if (NS_FAILED(rv) || isForeign) { + return true; + } + + if (!IsSameSiteSchemeEqual(otherScheme, hostScheme)) { + // If the two schemes are not of the same http(s) scheme then we + // consider the request as foreign. + return true; + } + + // for loads of TYPE_SUBDOCUMENT we have to perform an additional test, + // because a cross-origin iframe might perform a navigation to a same-origin + // iframe which would send same-site cookies. Hence, if the iframe navigation + // was triggered by a cross-origin triggeringPrincipal, we treat the load as + // foreign. + if (contentPolicyType == ExtContentPolicy::TYPE_SUBDOCUMENT) { + rv = triggeringPrincipal->IsThirdPartyChannel(aChannel, &isForeign); + if (NS_FAILED(rv) || isForeign) { + return true; + } + } + + // for the purpose of same-site cookies we have to treat any cross-origin + // redirects as foreign. E.g. cross-site to same-site redirect is a problem + // with regards to CSRF. + + nsCOMPtr<nsIPrincipal> redirectPrincipal; + for (nsIRedirectHistoryEntry* entry : redirectChain) { + entry->GetPrincipal(getter_AddRefs(redirectPrincipal)); + if (redirectPrincipal) { + rv = redirectPrincipal->IsThirdPartyChannel(aChannel, &isForeign); + // if at any point we encounter a cross-origin redirect we can return. + if (NS_FAILED(rv) || isForeign) { + *aHadCrossSiteRedirects = isForeign; + return true; + } + + nsAutoCString redirectScheme; + redirectPrincipal->GetScheme(redirectScheme); + if (!IsSameSiteSchemeEqual(redirectScheme, hostScheme)) { + // If the two schemes are not of the same http(s) scheme then we + // consider the request as foreign. + *aHadCrossSiteRedirects = true; + return true; + } + } + } + return isForeign; +} + +// static +nsICookie::schemeType CookieCommons::URIToSchemeType(nsIURI* aURI) { + MOZ_ASSERT(aURI); + + nsAutoCString scheme; + nsresult rv = aURI->GetScheme(scheme); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nsICookie::SCHEME_UNSET; + } + + return SchemeToSchemeType(scheme); +} + +// static +nsICookie::schemeType CookieCommons::PrincipalToSchemeType( + nsIPrincipal* aPrincipal) { + MOZ_ASSERT(aPrincipal); + + nsAutoCString scheme; + nsresult rv = aPrincipal->GetScheme(scheme); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nsICookie::SCHEME_UNSET; + } + + return SchemeToSchemeType(scheme); +} + +// static +nsICookie::schemeType CookieCommons::SchemeToSchemeType( + const nsACString& aScheme) { + MOZ_ASSERT(IsSchemeSupported(aScheme)); + + if (aScheme.Equals("https")) { + return nsICookie::SCHEME_HTTPS; + } + + if (aScheme.Equals("http")) { + return nsICookie::SCHEME_HTTP; + } + + if (aScheme.Equals("file")) { + return nsICookie::SCHEME_FILE; + } + + MOZ_CRASH("Unsupported scheme type"); +} + +// static +bool CookieCommons::IsSchemeSupported(nsIPrincipal* aPrincipal) { + MOZ_ASSERT(aPrincipal); + + nsAutoCString scheme; + nsresult rv = aPrincipal->GetScheme(scheme); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + return IsSchemeSupported(scheme); +} + +// static +bool CookieCommons::IsSchemeSupported(nsIURI* aURI) { + MOZ_ASSERT(aURI); + + nsAutoCString scheme; + nsresult rv = aURI->GetScheme(scheme); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + return IsSchemeSupported(scheme); +} + +// static +bool CookieCommons::IsSchemeSupported(const nsACString& aScheme) { + return aScheme.Equals("https") || aScheme.Equals("http") || + aScheme.Equals("file"); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/cookie/CookieCommons.h b/netwerk/cookie/CookieCommons.h new file mode 100644 index 0000000000..f2b9df355a --- /dev/null +++ b/netwerk/cookie/CookieCommons.h @@ -0,0 +1,142 @@ +/* -*- 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/. */ + +#ifndef mozilla_net_CookieCommons_h +#define mozilla_net_CookieCommons_h + +#include <cstdint> +#include <functional> +#include "mozIThirdPartyUtil.h" +#include "prtime.h" +#include "nsString.h" +#include "nsICookie.h" +#include "mozilla/net/NeckoChannelParams.h" + +class nsIChannel; +class nsIConsoleReportCollector; +class nsICookieJarSettings; +class nsIEffectiveTLDService; +class nsIPrincipal; +class nsIURI; + +namespace mozilla { + +namespace dom { +class Document; +} + +namespace net { + +// these constants represent an operation being performed on cookies +enum CookieOperation { OPERATION_READ, OPERATION_WRITE }; + +// these constants represent a decision about a cookie based on user prefs. +enum CookieStatus { + STATUS_ACCEPTED, + STATUS_ACCEPT_SESSION, + STATUS_REJECTED, + // STATUS_REJECTED_WITH_ERROR indicates the cookie should be rejected because + // of an error (rather than something the user can control). this is used for + // notification purposes, since we only want to notify of rejections where + // the user can do something about it (e.g. whitelist the site). + STATUS_REJECTED_WITH_ERROR +}; + +class Cookie; + +// pref string constants +static const char kPrefMaxNumberOfCookies[] = "network.cookie.maxNumber"; +static const char kPrefMaxCookiesPerHost[] = "network.cookie.maxPerHost"; +static const char kPrefCookieQuotaPerHost[] = "network.cookie.quotaPerHost"; +static const char kPrefCookiePurgeAge[] = "network.cookie.purgeAge"; + +// default limits for the cookie list. these can be tuned by the +// network.cookie.maxNumber and network.cookie.maxPerHost prefs respectively. +static const uint32_t kMaxCookiesPerHost = 180; +static const uint32_t kCookieQuotaPerHost = 150; +static const uint32_t kMaxNumberOfCookies = 3000; +static const uint32_t kMaxBytesPerCookie = 4096; +static const uint32_t kMaxBytesPerPath = 1024; + +static const int64_t kCookiePurgeAge = + int64_t(30 * 24 * 60 * 60) * PR_USEC_PER_SEC; // 30 days in microseconds + +class CookieCommons final { + public: + static bool DomainMatches(Cookie* aCookie, const nsACString& aHost); + + static bool PathMatches(Cookie* aCookie, const nsACString& aPath); + + static nsresult GetBaseDomain(nsIEffectiveTLDService* aTLDService, + nsIURI* aHostURI, nsACString& aBaseDomain, + bool& aRequireHostMatch); + + static nsresult GetBaseDomain(nsIPrincipal* aPrincipal, + nsACString& aBaseDomain); + + static nsresult GetBaseDomainFromHost(nsIEffectiveTLDService* aTLDService, + const nsACString& aHost, + nsCString& aBaseDomain); + + static void NotifyRejected(nsIURI* aHostURI, nsIChannel* aChannel, + uint32_t aRejectedReason, + CookieOperation aOperation); + + static bool CheckPathSize(const CookieStruct& aCookieData); + + static bool CheckNameAndValueSize(const CookieStruct& aCookieData); + + static bool CheckName(const CookieStruct& aCookieData); + + static bool CheckValue(const CookieStruct& aCookieData); + + static bool CheckCookiePermission(nsIChannel* aChannel, + CookieStruct& aCookieData); + + static bool CheckCookiePermission(nsIPrincipal* aPrincipal, + nsICookieJarSettings* aCookieJarSettings, + CookieStruct& aCookieData); + + static already_AddRefed<Cookie> CreateCookieFromDocument( + dom::Document* aDocument, const nsACString& aCookieString, + int64_t aCurrentTimeInUsec, nsIEffectiveTLDService* aTLDService, + mozIThirdPartyUtil* aThirdPartyUtil, + std::function<bool(const nsACString&, const OriginAttributes&)>&& + aHasExistingCookiesLambda, + nsIURI** aDocumentURI, nsACString& aBaseDomain, OriginAttributes& aAttrs); + + static already_AddRefed<nsICookieJarSettings> GetCookieJarSettings( + nsIChannel* aChannel); + + static bool ShouldIncludeCrossSiteCookieForDocument(Cookie* aCookie, + dom::Document* aDocument); + + static bool IsSchemeSupported(nsIPrincipal* aPrincipal); + static bool IsSchemeSupported(nsIURI* aURI); + static bool IsSchemeSupported(const nsACString& aScheme); + + static nsICookie::schemeType URIToSchemeType(nsIURI* aURI); + + static nsICookie::schemeType PrincipalToSchemeType(nsIPrincipal* aPrincipal); + + static nsICookie::schemeType SchemeToSchemeType(const nsACString& aScheme); + + // Returns true if the channel is a safe top-level navigation or if it's a + // download request + static bool IsSafeTopLevelNav(nsIChannel* aChannel); + + // Returns true if the channel is a foreign with respect to the host-uri. + // For loads of TYPE_DOCUMENT, this function returns true if it's a cross + // site navigation. + // `aHadCrossSiteRedirects` will be true iff the channel had a cross-site + // redirect before the final URI. + static bool IsSameSiteForeign(nsIChannel* aChannel, nsIURI* aHostURI, + bool* aHadCrossSiteRedirects); +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_CookieCommons_h diff --git a/netwerk/cookie/CookieJarSettings.cpp b/netwerk/cookie/CookieJarSettings.cpp new file mode 100644 index 0000000000..09a4789d85 --- /dev/null +++ b/netwerk/cookie/CookieJarSettings.cpp @@ -0,0 +1,733 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "mozilla/AntiTrackingUtils.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/ContentBlockingAllowList.h" +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/net/CookieJarSettings.h" +#include "mozilla/net/NeckoChannelParams.h" +#include "mozilla/Permission.h" +#include "mozilla/PermissionManager.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/StaticPrefs_network.h" +#include "mozilla/Unused.h" +#include "nsIPrincipal.h" +#if defined(MOZ_THUNDERBIRD) || defined(MOZ_SUITE) +# include "nsIProtocolHandler.h" +#endif +#include "nsIClassInfoImpl.h" +#include "nsIChannel.h" +#include "nsICookieManager.h" +#include "nsICookieService.h" +#include "nsIObjectInputStream.h" +#include "nsIObjectOutputStream.h" +#include "nsNetUtil.h" + +namespace mozilla { +namespace net { + +NS_IMPL_CLASSINFO(CookieJarSettings, nullptr, nsIClassInfo::THREADSAFE, + COOKIEJARSETTINGS_CID) + +NS_IMPL_ISUPPORTS_CI(CookieJarSettings, nsICookieJarSettings, nsISerializable) + +static StaticRefPtr<CookieJarSettings> sBlockinAll; + +namespace { + +class PermissionComparator { + public: + static bool Equals(nsIPermission* aA, nsIPermission* aB) { + nsCOMPtr<nsIPrincipal> principalA; + nsresult rv = aA->GetPrincipal(getter_AddRefs(principalA)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + nsCOMPtr<nsIPrincipal> principalB; + rv = aB->GetPrincipal(getter_AddRefs(principalB)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + bool equals = false; + rv = principalA->Equals(principalB, &equals); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + return equals; + } +}; + +class ReleaseCookiePermissions final : public Runnable { + public: + explicit ReleaseCookiePermissions(nsTArray<RefPtr<nsIPermission>>&& aArray) + : Runnable("ReleaseCookiePermissions"), mArray(std::move(aArray)) {} + + NS_IMETHOD Run() override { + MOZ_ASSERT(NS_IsMainThread()); + mArray.Clear(); + return NS_OK; + } + + private: + nsTArray<RefPtr<nsIPermission>> mArray; +}; + +} // namespace + +// static +already_AddRefed<nsICookieJarSettings> CookieJarSettings::GetBlockingAll( + bool aShouldResistFingerprinting) { + MOZ_ASSERT(NS_IsMainThread()); + + if (sBlockinAll) { + return do_AddRef(sBlockinAll); + } + + sBlockinAll = new CookieJarSettings(nsICookieService::BEHAVIOR_REJECT, + OriginAttributes::IsFirstPartyEnabled(), + aShouldResistFingerprinting, eFixed); + ClearOnShutdown(&sBlockinAll); + + return do_AddRef(sBlockinAll); +} + +// static +already_AddRefed<nsICookieJarSettings> CookieJarSettings::Create( + CreateMode aMode, bool aShouldResistFingerprinting) { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<CookieJarSettings> cookieJarSettings; + + switch (aMode) { + case eRegular: + case ePrivate: + cookieJarSettings = new CookieJarSettings( + nsICookieManager::GetCookieBehavior(aMode == ePrivate), + OriginAttributes::IsFirstPartyEnabled(), aShouldResistFingerprinting, + eProgressive); + break; + + default: + MOZ_CRASH("Unexpected create mode."); + } + + return cookieJarSettings.forget(); +} + +// static +already_AddRefed<nsICookieJarSettings> CookieJarSettings::Create( + nsIPrincipal* aPrincipal) { + MOZ_ASSERT(NS_IsMainThread()); + + bool shouldResistFingerprinting = + nsContentUtils::ShouldResistFingerprinting_dangerous( + aPrincipal, "We are constructing CookieJarSettings here.", + RFPTarget::IsAlwaysEnabledForPrecompute); + + if (aPrincipal && aPrincipal->OriginAttributesRef().mPrivateBrowsingId > 0) { + return Create(ePrivate, shouldResistFingerprinting); + } + + return Create(eRegular, shouldResistFingerprinting); +} + +// static +already_AddRefed<nsICookieJarSettings> CookieJarSettings::Create( + uint32_t aCookieBehavior, const nsAString& aPartitionKey, + bool aIsFirstPartyIsolated, bool aIsOnContentBlockingAllowList, + bool aShouldResistFingerprinting) { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<CookieJarSettings> cookieJarSettings = + new CookieJarSettings(aCookieBehavior, aIsFirstPartyIsolated, + aShouldResistFingerprinting, eProgressive); + cookieJarSettings->mPartitionKey = aPartitionKey; + cookieJarSettings->mIsOnContentBlockingAllowList = + aIsOnContentBlockingAllowList; + + return cookieJarSettings.forget(); +} + +// static +already_AddRefed<nsICookieJarSettings> CookieJarSettings::CreateForXPCOM() { + MOZ_ASSERT(NS_IsMainThread()); + return Create(eRegular, /* shouldResistFingerprinting */ false); +} + +CookieJarSettings::CookieJarSettings(uint32_t aCookieBehavior, + bool aIsFirstPartyIsolated, + bool aShouldResistFingerprinting, + State aState) + : mCookieBehavior(aCookieBehavior), + mIsFirstPartyIsolated(aIsFirstPartyIsolated), + mIsOnContentBlockingAllowList(false), + mIsOnContentBlockingAllowListUpdated(false), + mState(aState), + mToBeMerged(false), + mShouldResistFingerprinting(aShouldResistFingerprinting) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT_IF( + mIsFirstPartyIsolated, + mCookieBehavior != + nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN); +} + +CookieJarSettings::~CookieJarSettings() { + if (!NS_IsMainThread() && !mCookiePermissions.IsEmpty()) { + RefPtr<Runnable> r = + new ReleaseCookiePermissions(std::move(mCookiePermissions)); + MOZ_ASSERT(mCookiePermissions.IsEmpty()); + SchedulerGroup::Dispatch(r.forget()); + } +} + +NS_IMETHODIMP +CookieJarSettings::InitWithURI(nsIURI* aURI, bool aIsPrivate) { + NS_ENSURE_ARG(aURI); + + mCookieBehavior = nsICookieManager::GetCookieBehavior(aIsPrivate); + + SetPartitionKey(aURI); + return NS_OK; +} + +NS_IMETHODIMP +CookieJarSettings::GetCookieBehavior(uint32_t* aCookieBehavior) { + *aCookieBehavior = mCookieBehavior; + return NS_OK; +} + +NS_IMETHODIMP +CookieJarSettings::GetIsFirstPartyIsolated(bool* aIsFirstPartyIsolated) { + *aIsFirstPartyIsolated = mIsFirstPartyIsolated; + return NS_OK; +} + +NS_IMETHODIMP +CookieJarSettings::GetShouldResistFingerprinting( + bool* aShouldResistFingerprinting) { + *aShouldResistFingerprinting = mShouldResistFingerprinting; + return NS_OK; +} + +NS_IMETHODIMP +CookieJarSettings::GetRejectThirdPartyContexts( + bool* aRejectThirdPartyContexts) { + *aRejectThirdPartyContexts = + CookieJarSettings::IsRejectThirdPartyContexts(mCookieBehavior); + return NS_OK; +} + +NS_IMETHODIMP +CookieJarSettings::GetLimitForeignContexts(bool* aLimitForeignContexts) { + *aLimitForeignContexts = + mCookieBehavior == nsICookieService::BEHAVIOR_LIMIT_FOREIGN || + (StaticPrefs::privacy_dynamic_firstparty_limitForeign() && + mCookieBehavior == + nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN); + return NS_OK; +} + +NS_IMETHODIMP +CookieJarSettings::GetBlockingAllThirdPartyContexts( + bool* aBlockingAllThirdPartyContexts) { + // XXX For non-cookie forms of storage, we handle BEHAVIOR_LIMIT_FOREIGN by + // simply rejecting the request to use the storage. In the future, if we + // change the meaning of BEHAVIOR_LIMIT_FOREIGN to be one which makes sense + // for non-cookie storage types, this may change. + *aBlockingAllThirdPartyContexts = + mCookieBehavior == nsICookieService::BEHAVIOR_LIMIT_FOREIGN || + mCookieBehavior == nsICookieService::BEHAVIOR_REJECT_FOREIGN; + return NS_OK; +} + +NS_IMETHODIMP +CookieJarSettings::GetBlockingAllContexts(bool* aBlockingAllContexts) { + *aBlockingAllContexts = mCookieBehavior == nsICookieService::BEHAVIOR_REJECT; + return NS_OK; +} + +NS_IMETHODIMP +CookieJarSettings::GetPartitionForeign(bool* aPartitionForeign) { + *aPartitionForeign = + mCookieBehavior == + nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN; + return NS_OK; +} + +NS_IMETHODIMP +CookieJarSettings::SetPartitionForeign(bool aPartitionForeign) { + if (mIsFirstPartyIsolated) { + return NS_OK; + } + + if (aPartitionForeign) { + mCookieBehavior = + nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN; + } + return NS_OK; +} + +NS_IMETHODIMP +CookieJarSettings::GetIsOnContentBlockingAllowList( + bool* aIsOnContentBlockingAllowList) { + *aIsOnContentBlockingAllowList = mIsOnContentBlockingAllowList; + return NS_OK; +} + +NS_IMETHODIMP +CookieJarSettings::GetPartitionKey(nsAString& aPartitionKey) { + aPartitionKey = mPartitionKey; + return NS_OK; +} + +NS_IMETHODIMP +CookieJarSettings::GetFingerprintingRandomizationKey( + nsTArray<uint8_t>& aFingerprintingRandomizationKey) { + if (!mFingerprintingRandomKey) { + return NS_ERROR_NOT_AVAILABLE; + } + + aFingerprintingRandomizationKey = mFingerprintingRandomKey->Clone(); + return NS_OK; +} + +NS_IMETHODIMP +CookieJarSettings::CookiePermission(nsIPrincipal* aPrincipal, + uint32_t* aCookiePermission) { + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + NS_ENSURE_ARG_POINTER(aPrincipal); + NS_ENSURE_ARG_POINTER(aCookiePermission); + + *aCookiePermission = nsIPermissionManager::UNKNOWN_ACTION; + + nsresult rv; + + // Let's see if we know this permission. + if (!mCookiePermissions.IsEmpty()) { + for (const RefPtr<nsIPermission>& permission : mCookiePermissions) { + bool match = false; + rv = permission->Matches(aPrincipal, false, &match); + if (NS_WARN_IF(NS_FAILED(rv)) || !match) { + continue; + } + + rv = permission->GetCapability(aCookiePermission); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; + } + } + + // Let's ask the permission manager. + PermissionManager* pm = PermissionManager::GetInstance(); + if (NS_WARN_IF(!pm)) { + return NS_ERROR_FAILURE; + } + +#if defined(MOZ_THUNDERBIRD) || defined(MOZ_SUITE) + // Check if this protocol doesn't allow cookies. + bool hasFlags; + nsCOMPtr<nsIURI> uri; + BasePrincipal::Cast(aPrincipal)->GetURI(getter_AddRefs(uri)); + + rv = NS_URIChainHasFlags(uri, nsIProtocolHandler::URI_FORBIDS_COOKIE_ACCESS, + &hasFlags); + if (NS_FAILED(rv) || hasFlags) { + *aCookiePermission = PermissionManager::DENY_ACTION; + rv = NS_OK; // Reset, so it's not caught as a bad status after the `else`. + } else // Note the tricky `else` which controls the call below. +#endif + + rv = pm->TestPermissionFromPrincipal(aPrincipal, "cookie"_ns, + aCookiePermission); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Let's store the permission, also if the result is UNKNOWN in order to avoid + // race conditions. + + nsCOMPtr<nsIPermission> permission = + Permission::Create(aPrincipal, "cookie"_ns, *aCookiePermission, 0, 0, 0); + if (permission) { + mCookiePermissions.AppendElement(permission); + } + + mToBeMerged = true; + return NS_OK; +} + +void CookieJarSettings::Serialize(CookieJarSettingsArgs& aData) { + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + + aData.isFixed() = mState == eFixed; + aData.cookieBehavior() = mCookieBehavior; + aData.isFirstPartyIsolated() = mIsFirstPartyIsolated; + aData.shouldResistFingerprinting() = mShouldResistFingerprinting; + aData.isOnContentBlockingAllowList() = mIsOnContentBlockingAllowList; + aData.partitionKey() = mPartitionKey; + if (mFingerprintingRandomKey) { + aData.hasFingerprintingRandomizationKey() = true; + aData.fingerprintingRandomizationKey() = mFingerprintingRandomKey->Clone(); + } else { + aData.hasFingerprintingRandomizationKey() = false; + } + + for (const RefPtr<nsIPermission>& permission : mCookiePermissions) { + nsCOMPtr<nsIPrincipal> principal; + nsresult rv = permission->GetPrincipal(getter_AddRefs(principal)); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + + ipc::PrincipalInfo principalInfo; + rv = PrincipalToPrincipalInfo(principal, &principalInfo, + true /* aSkipBaseDomain */); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + + uint32_t cookiePermission = 0; + rv = permission->GetCapability(&cookiePermission); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + + aData.cookiePermissions().AppendElement( + CookiePermissionData(principalInfo, cookiePermission)); + } + + mToBeMerged = false; +} + +/* static */ void CookieJarSettings::Deserialize( + const CookieJarSettingsArgs& aData, + nsICookieJarSettings** aCookieJarSettings) { + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + + CookiePermissionList list; + for (const CookiePermissionData& data : aData.cookiePermissions()) { + auto principalOrErr = PrincipalInfoToPrincipal(data.principalInfo()); + if (NS_WARN_IF(principalOrErr.isErr())) { + continue; + } + + nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap(); + + nsCOMPtr<nsIPermission> permission = Permission::Create( + principal, "cookie"_ns, data.cookiePermission(), 0, 0, 0); + if (NS_WARN_IF(!permission)) { + continue; + } + + list.AppendElement(permission); + } + + RefPtr<CookieJarSettings> cookieJarSettings = new CookieJarSettings( + aData.cookieBehavior(), aData.isFirstPartyIsolated(), + aData.shouldResistFingerprinting(), + aData.isFixed() ? eFixed : eProgressive); + + cookieJarSettings->mIsOnContentBlockingAllowList = + aData.isOnContentBlockingAllowList(); + cookieJarSettings->mCookiePermissions = std::move(list); + cookieJarSettings->mPartitionKey = aData.partitionKey(); + cookieJarSettings->mShouldResistFingerprinting = + aData.shouldResistFingerprinting(); + + if (aData.hasFingerprintingRandomizationKey()) { + cookieJarSettings->mFingerprintingRandomKey.emplace( + aData.fingerprintingRandomizationKey().Clone()); + } + + cookieJarSettings.forget(aCookieJarSettings); +} + +void CookieJarSettings::Merge(const CookieJarSettingsArgs& aData) { + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + MOZ_ASSERT( + mCookieBehavior == aData.cookieBehavior() || + (mCookieBehavior == nsICookieService::BEHAVIOR_REJECT_TRACKER && + aData.cookieBehavior() == + nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN) || + (mCookieBehavior == + nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN && + aData.cookieBehavior() == nsICookieService::BEHAVIOR_REJECT_TRACKER)); + + if (mState == eFixed) { + return; + } + + // Merge cookie behavior pref values + if (mCookieBehavior == nsICookieService::BEHAVIOR_REJECT_TRACKER && + aData.cookieBehavior() == + nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN) { + // If the other side has decided to partition third-party cookies, update + // our side when first-party isolation is disabled. + if (!mIsFirstPartyIsolated) { + mCookieBehavior = + nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN; + } + } + if (mCookieBehavior == + nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN && + aData.cookieBehavior() == nsICookieService::BEHAVIOR_REJECT_TRACKER) { + // If we've decided to partition third-party cookies, the other side may not + // have caught up yet unless it has first-party isolation enabled. + if (aData.isFirstPartyIsolated()) { + mCookieBehavior = nsICookieService::BEHAVIOR_REJECT_TRACKER; + mIsFirstPartyIsolated = true; + } + } + // Ignore all other cases. + MOZ_ASSERT_IF( + mIsFirstPartyIsolated, + mCookieBehavior != + nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN); + + if (aData.shouldResistFingerprinting()) { + mShouldResistFingerprinting = true; + } + + PermissionComparator comparator; + + for (const CookiePermissionData& data : aData.cookiePermissions()) { + auto principalOrErr = PrincipalInfoToPrincipal(data.principalInfo()); + if (NS_WARN_IF(principalOrErr.isErr())) { + continue; + } + + nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap(); + nsCOMPtr<nsIPermission> permission = Permission::Create( + principal, "cookie"_ns, data.cookiePermission(), 0, 0, 0); + if (NS_WARN_IF(!permission)) { + continue; + } + + if (!mCookiePermissions.Contains(permission, comparator)) { + mCookiePermissions.AppendElement(permission); + } + } +} + +void CookieJarSettings::SetPartitionKey(nsIURI* aURI) { + MOZ_ASSERT(aURI); + + OriginAttributes attrs; + attrs.SetPartitionKey(aURI); + mPartitionKey = std::move(attrs.mPartitionKey); +} + +void CookieJarSettings::UpdateIsOnContentBlockingAllowList( + nsIChannel* aChannel) { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(aChannel); + + // Early return if the flag was updated before. + if (mIsOnContentBlockingAllowListUpdated) { + return; + } + mIsOnContentBlockingAllowListUpdated = true; + + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + + nsCOMPtr<nsIURI> uri; + nsresult rv = aChannel->GetURI(getter_AddRefs(uri)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + // We need to recompute the ContentBlockingAllowListPrincipal here for the + // top level channel because we might navigate from the the initial + // about:blank page or the existing page which may have a different origin + // than the URI we are going to load here. Thus, we need to recompute the + // prinicpal in order to get the correct ContentBlockingAllowListPrincipal. + nsCOMPtr<nsIPrincipal> contentBlockingAllowListPrincipal; + OriginAttributes attrs; + loadInfo->GetOriginAttributes(&attrs); + ContentBlockingAllowList::RecomputePrincipal( + uri, attrs, getter_AddRefs(contentBlockingAllowListPrincipal)); + + if (!contentBlockingAllowListPrincipal || + !contentBlockingAllowListPrincipal->GetIsContentPrincipal()) { + return; + } + + Unused << ContentBlockingAllowList::Check(contentBlockingAllowListPrincipal, + NS_UsePrivateBrowsing(aChannel), + mIsOnContentBlockingAllowList); +} + +// static +bool CookieJarSettings::IsRejectThirdPartyContexts(uint32_t aCookieBehavior) { + return aCookieBehavior == nsICookieService::BEHAVIOR_REJECT_TRACKER || + aCookieBehavior == + nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN; +} + +NS_IMETHODIMP +CookieJarSettings::Read(nsIObjectInputStream* aStream) { + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + nsresult rv = aStream->Read32(&mCookieBehavior); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aStream->ReadBoolean(&mIsFirstPartyIsolated); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aStream->ReadBoolean(&mShouldResistFingerprinting); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool isFixed; + aStream->ReadBoolean(&isFixed); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + mState = isFixed ? eFixed : eProgressive; + + rv = aStream->ReadBoolean(&mIsOnContentBlockingAllowList); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aStream->ReadString(mPartitionKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Deserializing the cookie permission list. + uint32_t cookiePermissionsLength; + rv = aStream->Read32(&cookiePermissionsLength); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!cookiePermissionsLength) { + // Bailing out early because there is no cookie permission. + return NS_OK; + } + + CookiePermissionList list; + mCookiePermissions.SetCapacity(cookiePermissionsLength); + for (uint32_t i = 0; i < cookiePermissionsLength; ++i) { + nsAutoCString principalJSON; + aStream->ReadCString(principalJSON); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsIPrincipal> principal = BasePrincipal::FromJSON(principalJSON); + + if (NS_WARN_IF(!principal)) { + continue; + } + + uint32_t cookiePermission; + aStream->Read32(&cookiePermission); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsIPermission> permission = + Permission::Create(principal, "cookie"_ns, cookiePermission, 0, 0, 0); + if (NS_WARN_IF(!permission)) { + continue; + } + + list.AppendElement(permission); + } + + mCookiePermissions = std::move(list); + + return NS_OK; +} + +NS_IMETHODIMP +CookieJarSettings::Write(nsIObjectOutputStream* aStream) { + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + nsresult rv = aStream->Write32(mCookieBehavior); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aStream->WriteBoolean(mIsFirstPartyIsolated); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aStream->WriteBoolean(mShouldResistFingerprinting); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aStream->WriteBoolean(mState == eFixed); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aStream->WriteBoolean(mIsOnContentBlockingAllowList); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = aStream->WriteWStringZ(mPartitionKey.get()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Serializing the cookie permission list. It will first write the length of + // the list, and then, write the cookie permission consecutively. + uint32_t cookiePermissionsLength = mCookiePermissions.Length(); + rv = aStream->Write32(cookiePermissionsLength); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + for (const RefPtr<nsIPermission>& permission : mCookiePermissions) { + nsCOMPtr<nsIPrincipal> principal; + nsresult rv = permission->GetPrincipal(getter_AddRefs(principal)); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + + nsAutoCString principalJSON; + BasePrincipal::Cast(principal)->ToJSON(principalJSON); + + rv = aStream->WriteStringZ(principalJSON.get()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + uint32_t cookiePermission = 0; + rv = permission->GetCapability(&cookiePermission); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + + rv = aStream->Write32(cookiePermission); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + return NS_OK; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/cookie/CookieJarSettings.h b/netwerk/cookie/CookieJarSettings.h new file mode 100644 index 0000000000..97f8528a55 --- /dev/null +++ b/netwerk/cookie/CookieJarSettings.h @@ -0,0 +1,267 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_net_CookieJarSettings_h +#define mozilla_net_CookieJarSettings_h + +#include "mozilla/Maybe.h" + +#include "nsICookieJarSettings.h" +#include "nsTArray.h" + +#define COOKIEJARSETTINGS_CONTRACTID "@mozilla.org/cookieJarSettings;1" +// 4ce234f1-52e8-47a9-8c8d-b02f815733c7 +#define COOKIEJARSETTINGS_CID \ + { \ + 0x4ce234f1, 0x52e8, 0x47a9, { \ + 0x8c, 0x8d, 0xb0, 0x2f, 0x81, 0x57, 0x33, 0xc7 \ + } \ + } + +class nsIPermission; + +namespace mozilla { +namespace net { + +class CookieJarSettingsArgs; + +/** + * CookieJarSettings + * ~~~~~~~~~~~~~~ + * + * CookieJarSettings is a snapshot of the cookie jar's configurations in a + * precise moment of time, such as the cookie policy and cookie permissions. + * This object is used by top-level documents to have a consistent cookie jar + * configuration also in case the user changes it. New configurations will apply + * only to new top-level documents. + * + * CookieJarSettings creation + * ~~~~~~~~~~~~~~~~~~~~~~~ + * + * CookieJarSettings is created when the top-level document's nsIChannel's + * nsILoadInfo is constructed. Any sub-resource and any sub-document inherits it + * from that nsILoadInfo. Also dedicated workers and their resources inherit it + * from the parent document. + * + * SharedWorkers and ServiceWorkers have their own CookieJarSettings because + * they don't have a single parent document (SharedWorkers could have more than + * one, ServiceWorkers have none). + * + * In Chrome code, we have a new CookieJarSettings when we download resources + * via 'Save-as...' and we also have a new CookieJarSettings for favicon + * downloading. + * + * Content-scripts WebExtensions also have their own CookieJarSettings because + * they don't have a direct access to the document they are running into. + * + * Anything else will have a special CookieJarSettings which blocks everything + * (CookieJarSettings::GetBlockingAll()) by forcing BEHAVIOR_REJECT as policy. + * When this happens, that context will not have access to the cookie jar and no + * cookies are sent or received. + * + * Propagation of CookieJarSettings + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * CookieJarSettings are shared inside the same top-level document via its + * nsIChannel's nsILoadInfo. This is done automatically if you pass a nsINode + * to NS_NewChannel(), and it must be done manually if you use a different + * channel constructor. For instance, this happens for any worker networking + * operation. + * + * We use the same CookieJarSettings for any resource belonging to the top-level + * document even if cross-origin. This makes the browser behave consistently a + * scenario where A loads B which loads A again, and cookie policy/permission + * changes in the meantime. + * + * Cookie Permissions propagation + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * CookieJarSettings populates the known cookie permissions only when required. + * Initially the list is empty, but when CookieJarSettings::CookiePermission() + * is called, the requested permission is stored in the internal list if it + * doesn't exist yet. + * + * This is actually nice because it relies on the permission propagation from + * parent to content process. No extra IPC is required. + * + * Note that we store permissions with UNKNOWN_ACTION values too because they + * can be set after the loading of the top-level document and we don't want to + * return a different value when this happens. + * + * Use of CookieJarSettings + * ~~~~~~~~~~~~~~~~~~~~~ + * + * In theory, there should not be direct access to cookie permissions or + * cookieBehavior pref. Everything should pass through CookieJarSettings. + * + * A reference to CookieJarSettings can be obtained from + * nsILoadInfo::GetCookieJarSettings(), from Document::CookieJarSettings() and + * from the WorkerPrivate::CookieJarSettings(). + * + * CookieJarSettings is thread-safe, but the permission list must be touched + * only on the main-thread. + * + * Testing + * ~~~~~~~ + * + * If you need to test the changing of cookie policy or a cookie permission, you + * need to workaround CookieJarSettings. This can be done opening a new window + * and running the test into that new global. + */ + +/** + * Class that provides an nsICookieJarSettings implementation. + */ +class CookieJarSettings final : public nsICookieJarSettings { + public: + typedef nsTArray<RefPtr<nsIPermission>> CookiePermissionList; + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSICOOKIEJARSETTINGS + NS_DECL_NSISERIALIZABLE + + static already_AddRefed<nsICookieJarSettings> GetBlockingAll( + bool aShouldResistFingerprinting); + + enum CreateMode { eRegular, ePrivate }; + + static already_AddRefed<nsICookieJarSettings> Create( + CreateMode aMode, bool aShouldResistFingerprinting); + + static already_AddRefed<nsICookieJarSettings> Create( + nsIPrincipal* aPrincipal); + + // This function should be only called for XPCOM. You should never use this + // for other purposes. + static already_AddRefed<nsICookieJarSettings> CreateForXPCOM(); + + static already_AddRefed<nsICookieJarSettings> Create( + uint32_t aCookieBehavior, const nsAString& aPartitionKey, + bool aIsFirstPartyIsolated, bool aIsOnContentBlockingAllowList, + bool aShouldResistFingerprinting); + + static CookieJarSettings* Cast(nsICookieJarSettings* aCS) { + return static_cast<CookieJarSettings*>(aCS); + } + + void Serialize(CookieJarSettingsArgs& aData); + + static void Deserialize(const CookieJarSettingsArgs& aData, + nsICookieJarSettings** aCookieJarSettings); + + void Merge(const CookieJarSettingsArgs& aData); + + // We don't want to send this object from parent to child process if there are + // no reasons. HasBeenChanged() returns true if the object has changed its + // internal state and it must be sent beck to the content process. + bool HasBeenChanged() const { return mToBeMerged; } + + void UpdateIsOnContentBlockingAllowList(nsIChannel* aChannel); + + void SetPartitionKey(nsIURI* aURI); + void SetPartitionKey(const nsAString& aPartitionKey) { + mPartitionKey = aPartitionKey; + } + const nsAString& GetPartitionKey() { return mPartitionKey; }; + + void SetFingerprintingRandomizationKey(const nsTArray<uint8_t>& aKey) { + mFingerprintingRandomKey.reset(); + + mFingerprintingRandomKey.emplace(aKey.Clone()); + } + + // Utility function to test if the passed cookiebahvior is + // BEHAVIOR_REJECT_TRACKER, BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN or + // BEHAVIOR_REJECT_FOREIGN when + // network.cookie.rejectForeignWithExceptions.enabled pref is set to true. + static bool IsRejectThirdPartyContexts(uint32_t aCookieBehavior); + + private: + enum State { + // No cookie permissions are allowed to be stored in this object. + eFixed, + + // Cookie permissions can be stored in case they are unknown when they are + // asked or when they are sent from the parent process. + eProgressive, + }; + + CookieJarSettings(uint32_t aCookieBehavior, bool aIsFirstPartyIsolated, + bool aShouldResistFingerprinting, State aState); + ~CookieJarSettings(); + + uint32_t mCookieBehavior; + bool mIsFirstPartyIsolated; + CookiePermissionList mCookiePermissions; + bool mIsOnContentBlockingAllowList; + bool mIsOnContentBlockingAllowListUpdated; + nsString mPartitionKey; + + State mState; + + bool mToBeMerged; + + // DO NOT USE THIS MEMBER TO CHECK IF YOU SHOULD RESIST FINGERPRINTING. + // USE THE nsContentUtils::ShouldResistFingerprinting() METHODS ONLY. + // + // As we move to fine-grained RFP control, we want to support per-domain + // exemptions from ResistFingerprinting. Specifically the behavior should be + // as such: + // + // Top-Level Document is on an Exempted Domain + // - RFP is disabled. + // + // Top-Level Document on an Exempted Domain embedding a non-exempted + // cross-origin iframe + // - RFP in the iframe is enabled (NOT exempted). (**) + // + // Top-Level Document on an Exempted Domain embedding an exempted cross-origin + // iframe + // - RFP in the iframe is disabled (exempted). + // + // Top-Level Document on a Non-Exempted Domain + // - RFP is enabled (NOT exempted). + // + // Top-Level Document on a Non-Exempted Domain embeds an exempted cross-origin + // iframe + // - RFP in the iframe is enabled (NOT exempted). (*) + // + // Exempted Document (top-level or iframe) contacts any cross-origin domain + // (exempted or non-exempted) + // - RFP is disabled (exempted) for the request + // + // Non-Exempted Document (top-level or iframe) contacts any cross-origin + // domain + // (exempted or non-exempted) + // - RFP is enabled (NOT exempted) for the request + // + // This boolean on CookieJarSettings will enable us to apply the most + // difficult rule, marked in (*). (It is difficult because the + // subdocument's loadinfo will look like it should be exempted.) + // However if we trusted this member blindly, it would not correctly apply + // the one marked with (**). (Because it would inherit an exemption into + // a subdocument that should not be exempted.) + // To handle this case, we only trust a CookieJar's ShouldRFP value if it + // says we should resist fingerprinting. If it says that we _should not_, + // we continue and check the channel's URI or LoadInfo and if + // the domain specified there is not an exempted domain, enforce RFP anyway. + // This all occurrs in the nscontentUtils::ShouldResistFingerprinting + // functions which you should be using. + bool mShouldResistFingerprinting; + + // The key used to generate the random noise for randomizing the browser + // fingerprint. The key is decided by the session key and the top-level site. + // So, the browse fingerprint will look different to the same tracker + // under different top-level sites. Also, the fingerprint will change as + // browsing session changes. This can prevent trackers to identify individuals + // by using browser fingerprints. + Maybe<nsTArray<uint8_t>> mFingerprintingRandomKey; +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_CookieJarSettings_h diff --git a/netwerk/cookie/CookieKey.h b/netwerk/cookie/CookieKey.h new file mode 100644 index 0000000000..86291a187e --- /dev/null +++ b/netwerk/cookie/CookieKey.h @@ -0,0 +1,61 @@ +/* -*- 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/. */ + +#ifndef mozilla_net_CookieKey_h +#define mozilla_net_CookieKey_h + +#include "mozilla/OriginAttributes.h" +#include "nsHashKeys.h" + +namespace mozilla { +namespace net { + +class CookieKey : public PLDHashEntryHdr { + public: + typedef const CookieKey& KeyType; + typedef const CookieKey* KeyTypePointer; + + CookieKey() = default; + + CookieKey(const nsACString& baseDomain, const OriginAttributes& attrs) + : mBaseDomain(baseDomain), mOriginAttributes(attrs) {} + + explicit CookieKey(KeyTypePointer other) + : mBaseDomain(other->mBaseDomain), + mOriginAttributes(other->mOriginAttributes) {} + + CookieKey(CookieKey&& other) = default; + CookieKey& operator=(CookieKey&&) = default; + + bool KeyEquals(KeyTypePointer other) const { + return mBaseDomain == other->mBaseDomain && + mOriginAttributes == other->mOriginAttributes; + } + + static KeyTypePointer KeyToPointer(KeyType aKey) { return &aKey; } + + static PLDHashNumber HashKey(KeyTypePointer aKey) { + nsAutoCString temp(aKey->mBaseDomain); + temp.Append('#'); + nsAutoCString suffix; + aKey->mOriginAttributes.CreateSuffix(suffix); + temp.Append(suffix); + return HashString(temp); + } + + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const { + return mBaseDomain.SizeOfExcludingThisIfUnshared(aMallocSizeOf); + } + + enum { ALLOW_MEMMOVE = true }; + + nsCString mBaseDomain; + OriginAttributes mOriginAttributes; +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_CookieKey_h diff --git a/netwerk/cookie/CookieLogging.cpp b/netwerk/cookie/CookieLogging.cpp new file mode 100644 index 0000000000..9f5262b2bb --- /dev/null +++ b/netwerk/cookie/CookieLogging.cpp @@ -0,0 +1,185 @@ +/* -*- 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 "CookieLogging.h" +#include "Cookie.h" +#include "nsIConsoleReportCollector.h" + +constexpr auto TIME_STRING_LENGTH = 40; + +namespace mozilla { +namespace net { + +LazyLogModule gCookieLog("cookie"); + +static const char* SameSiteToString(uint32_t aSameSite) { + switch (aSameSite) { + case nsICookie::SAMESITE_NONE: + return "none"; + case nsICookie::SAMESITE_LAX: + return "lax"; + case nsICookie::SAMESITE_STRICT: + return "strict"; + default: + MOZ_CRASH("Invalid nsICookie sameSite value"); + return ""; + } +} + +// static +void CookieLogging::LogSuccess(bool aSetCookie, nsIURI* aHostURI, + const nsACString& aCookieString, Cookie* aCookie, + bool aReplacing) { + // if logging isn't enabled, return now to save cycles + if (!MOZ_LOG_TEST(gCookieLog, LogLevel::Debug)) { + return; + } + + nsAutoCString spec; + if (aHostURI) { + aHostURI->GetAsciiSpec(spec); + } + + MOZ_LOG(gCookieLog, LogLevel::Debug, + ("===== %s =====\n", aSetCookie ? "COOKIE ACCEPTED" : "COOKIE SENT")); + MOZ_LOG(gCookieLog, LogLevel::Debug, ("request URL: %s\n", spec.get())); + MOZ_LOG(gCookieLog, LogLevel::Debug, + ("cookie string: %s\n", aCookieString.BeginReading())); + if (aSetCookie) { + MOZ_LOG(gCookieLog, LogLevel::Debug, + ("replaces existing cookie: %s\n", aReplacing ? "true" : "false")); + } + + LogCookie(aCookie); + + MOZ_LOG(gCookieLog, LogLevel::Debug, ("\n")); +} + +// static +void CookieLogging::LogFailure(bool aSetCookie, nsIURI* aHostURI, + const nsACString& aCookieString, + const char* aReason) { + // if logging isn't enabled, return now to save cycles + if (!MOZ_LOG_TEST(gCookieLog, LogLevel::Warning)) { + return; + } + + nsAutoCString spec; + if (aHostURI) { + aHostURI->GetAsciiSpec(spec); + } + + MOZ_LOG(gCookieLog, LogLevel::Warning, + ("===== %s =====\n", + aSetCookie ? "COOKIE NOT ACCEPTED" : "COOKIE NOT SENT")); + MOZ_LOG(gCookieLog, LogLevel::Warning, ("request URL: %s\n", spec.get())); + if (aSetCookie) { + MOZ_LOG(gCookieLog, LogLevel::Warning, + ("cookie string: %s\n", aCookieString.BeginReading())); + } + + PRExplodedTime explodedTime; + PR_ExplodeTime(PR_Now(), PR_GMTParameters, &explodedTime); + char timeString[TIME_STRING_LENGTH]; + PR_FormatTimeUSEnglish(timeString, TIME_STRING_LENGTH, "%c GMT", + &explodedTime); + + MOZ_LOG(gCookieLog, LogLevel::Warning, ("current time: %s", timeString)); + MOZ_LOG(gCookieLog, LogLevel::Warning, ("rejected because %s\n", aReason)); + MOZ_LOG(gCookieLog, LogLevel::Warning, ("\n")); +} + +// static +void CookieLogging::LogCookie(Cookie* aCookie) { + PRExplodedTime explodedTime; + PR_ExplodeTime(PR_Now(), PR_GMTParameters, &explodedTime); + char timeString[TIME_STRING_LENGTH]; + PR_FormatTimeUSEnglish(timeString, TIME_STRING_LENGTH, "%c GMT", + &explodedTime); + + MOZ_LOG(gCookieLog, LogLevel::Debug, ("current time: %s", timeString)); + + if (aCookie) { + MOZ_LOG(gCookieLog, LogLevel::Debug, ("----------------\n")); + MOZ_LOG(gCookieLog, LogLevel::Debug, ("name: %s\n", aCookie->Name().get())); + MOZ_LOG(gCookieLog, LogLevel::Debug, + ("value: %s\n", aCookie->Value().get())); + MOZ_LOG(gCookieLog, LogLevel::Debug, + ("%s: %s\n", aCookie->IsDomain() ? "domain" : "host", + aCookie->Host().get())); + MOZ_LOG(gCookieLog, LogLevel::Debug, ("path: %s\n", aCookie->Path().get())); + + PR_ExplodeTime(aCookie->Expiry() * int64_t(PR_USEC_PER_SEC), + PR_GMTParameters, &explodedTime); + PR_FormatTimeUSEnglish(timeString, TIME_STRING_LENGTH, "%c GMT", + &explodedTime); + MOZ_LOG(gCookieLog, LogLevel::Debug, + ("expires: %s%s", timeString, + aCookie->IsSession() ? " (at end of session)" : "")); + + PR_ExplodeTime(aCookie->CreationTime(), PR_GMTParameters, &explodedTime); + PR_FormatTimeUSEnglish(timeString, TIME_STRING_LENGTH, "%c GMT", + &explodedTime); + MOZ_LOG(gCookieLog, LogLevel::Debug, ("created: %s", timeString)); + + MOZ_LOG(gCookieLog, LogLevel::Debug, + ("is secure: %s\n", aCookie->IsSecure() ? "true" : "false")); + MOZ_LOG(gCookieLog, LogLevel::Debug, + ("is httpOnly: %s\n", aCookie->IsHttpOnly() ? "true" : "false")); + MOZ_LOG(gCookieLog, LogLevel::Debug, + ("sameSite: %s - rawSameSite: %s\n", + SameSiteToString(aCookie->SameSite()), + SameSiteToString(aCookie->RawSameSite()))); + MOZ_LOG( + gCookieLog, LogLevel::Debug, + ("schemeMap %d (http: %s | https: %s | file: %s)\n", + aCookie->SchemeMap(), + (aCookie->SchemeMap() & nsICookie::SCHEME_HTTP ? "true" : "false"), + (aCookie->SchemeMap() & nsICookie::SCHEME_HTTPS ? "true" : "false"), + (aCookie->SchemeMap() & nsICookie::SCHEME_FILE ? "true" : "false"))); + + nsAutoCString suffix; + aCookie->OriginAttributesRef().CreateSuffix(suffix); + MOZ_LOG(gCookieLog, LogLevel::Debug, + ("origin attributes: %s\n", + suffix.IsEmpty() ? "{empty}" : suffix.get())); + } +} + +// static +void CookieLogging::LogEvicted(Cookie* aCookie, const char* details) { + MOZ_LOG(gCookieLog, LogLevel::Debug, ("===== COOKIE EVICTED =====\n")); + MOZ_LOG(gCookieLog, LogLevel::Debug, ("%s\n", details)); + + LogCookie(aCookie); + + MOZ_LOG(gCookieLog, LogLevel::Debug, ("\n")); +} + +// static +void CookieLogging::LogMessageToConsole(nsIConsoleReportCollector* aCRC, + nsIURI* aURI, uint32_t aErrorFlags, + const nsACString& aCategory, + const nsACString& aMsg, + const nsTArray<nsString>& aParams) { + if (!aCRC) { + return; + } + + nsAutoCString uri; + if (aURI) { + nsresult rv = aURI->GetSpec(uri); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + } + + aCRC->AddConsoleReport(aErrorFlags, aCategory, + nsContentUtils::eNECKO_PROPERTIES, uri, 0, 0, aMsg, + aParams); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/cookie/CookieLogging.h b/netwerk/cookie/CookieLogging.h new file mode 100644 index 0000000000..19721914c8 --- /dev/null +++ b/netwerk/cookie/CookieLogging.h @@ -0,0 +1,65 @@ +/* -*- 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/. */ + +#ifndef mozilla_net_CookieLogging_h +#define mozilla_net_CookieLogging_h + +#include "mozilla/Logging.h" +#include "nsString.h" + +class nsIConsoleReportCollector; +class nsIURI; + +namespace mozilla { +namespace net { + +// define logging macros for convenience +#define SET_COOKIE true +#define GET_COOKIE false + +extern LazyLogModule gCookieLog; + +#define COOKIE_LOGFAILURE(a, b, c, d) CookieLogging::LogFailure(a, b, c, d) +#define COOKIE_LOGSUCCESS(a, b, c, d, e) \ + CookieLogging::LogSuccess(a, b, c, d, e) + +#define COOKIE_LOGEVICTED(a, details) \ + PR_BEGIN_MACRO \ + if (MOZ_LOG_TEST(gCookieLog, LogLevel::Debug)) \ + CookieLogging::LogEvicted(a, details); \ + PR_END_MACRO + +#define COOKIE_LOGSTRING(lvl, fmt) \ + PR_BEGIN_MACRO \ + MOZ_LOG(gCookieLog, lvl, fmt); \ + MOZ_LOG(gCookieLog, lvl, ("\n")); \ + PR_END_MACRO + +class Cookie; + +class CookieLogging final { + public: + static void LogSuccess(bool aSetCookie, nsIURI* aHostURI, + const nsACString& aCookieString, Cookie* aCookie, + bool aReplacing); + + static void LogFailure(bool aSetCookie, nsIURI* aHostURI, + const nsACString& aCookieString, const char* aReason); + + static void LogCookie(Cookie* aCookie); + + static void LogEvicted(Cookie* aCookie, const char* aDetails); + + static void LogMessageToConsole(nsIConsoleReportCollector* aCRC, nsIURI* aURI, + uint32_t aErrorFlags, + const nsACString& aCategory, + const nsACString& aMsg, + const nsTArray<nsString>& aParams); +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_CookieLogging_h diff --git a/netwerk/cookie/CookieNotification.cpp b/netwerk/cookie/CookieNotification.cpp new file mode 100644 index 0000000000..43e7364a38 --- /dev/null +++ b/netwerk/cookie/CookieNotification.cpp @@ -0,0 +1,60 @@ +/* -*- Mode: C++; tab-width: 4; 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 "CookieNotification.h" +#include "mozilla/dom/BrowsingContext.h" +#include "nsICookieNotification.h" + +namespace mozilla::net { + +NS_IMETHODIMP +CookieNotification::GetAction(nsICookieNotification::Action* aResult) { + *aResult = mAction; + return NS_OK; +} + +NS_IMETHODIMP +CookieNotification::GetCookie(nsICookie** aResult) { + NS_ENSURE_ARG_POINTER(aResult); + + *aResult = mCookie; + NS_IF_ADDREF(*aResult); + + return NS_OK; +} + +NS_IMETHODIMP CookieNotification::GetBaseDomain(nsACString& aBaseDomain) { + aBaseDomain = mBaseDomain; + + return NS_OK; +} + +NS_IMETHODIMP +CookieNotification::GetBatchDeletedCookies(nsIArray** aResult) { + NS_ENSURE_ARG_POINTER(aResult); + NS_ENSURE_TRUE(mAction == nsICookieNotification::COOKIES_BATCH_DELETED, + NS_ERROR_NOT_AVAILABLE); + + *aResult = mBatchDeletedCookies; + NS_IF_ADDREF(*aResult); + + return NS_OK; +} + +NS_IMETHODIMP +CookieNotification::GetBrowsingContextId(uint64_t* aResult) { + *aResult = mBrowsingContextId; + return NS_OK; +} + +NS_IMETHODIMP +CookieNotification::GetBrowsingContext(dom::BrowsingContext** aResult) { + *aResult = dom::BrowsingContext::Get(mBrowsingContextId).take(); + return NS_OK; +} + +NS_IMPL_ISUPPORTS(CookieNotification, nsICookieNotification) + +} // namespace mozilla::net diff --git a/netwerk/cookie/CookieNotification.h b/netwerk/cookie/CookieNotification.h new file mode 100644 index 0000000000..c619ce56c1 --- /dev/null +++ b/netwerk/cookie/CookieNotification.h @@ -0,0 +1,46 @@ +/* -*- Mode: C++; tab-width: 4; 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/. */ + +#ifndef mozilla_net_CookieNotification_h +#define mozilla_net_CookieNotification_h + +#include "nsIArray.h" +#include "nsICookieNotification.h" +#include "nsICookie.h" +#include "nsCOMPtr.h" +#include "nsTArray.h" +#include "nsString.h" + +namespace mozilla::net { + +class CookieNotification final : public nsICookieNotification { + public: + // nsISupports + NS_DECL_ISUPPORTS + NS_DECL_NSICOOKIENOTIFICATION + + explicit CookieNotification(nsICookieNotification::Action aAction, + nsICookie* aCookie, const nsACString& aBaseDomain, + nsIArray* aBatchDeletedCookies = nullptr, + uint64_t aBrowsingContextId = 0) + : mAction(aAction), + mCookie(aCookie), + mBaseDomain(aBaseDomain), + mBatchDeletedCookies(aBatchDeletedCookies), + mBrowsingContextId(aBrowsingContextId){}; + + private: + nsICookieNotification::Action mAction; + nsCOMPtr<nsICookie> mCookie; + nsCString mBaseDomain; + nsCOMPtr<nsIArray> mBatchDeletedCookies; + uint64_t mBrowsingContextId = 0; + + ~CookieNotification() = default; +}; + +} // namespace mozilla::net + +#endif // mozilla_net_CookieNotification_h diff --git a/netwerk/cookie/CookiePersistentStorage.cpp b/netwerk/cookie/CookiePersistentStorage.cpp new file mode 100644 index 0000000000..bb4e64f0f1 --- /dev/null +++ b/netwerk/cookie/CookiePersistentStorage.cpp @@ -0,0 +1,2134 @@ +/* -*- 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 "CookiePersistentStorage.h" + +#include "mozilla/FileUtils.h" +#include "mozilla/glean/GleanMetrics.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/Telemetry.h" +#include "mozIStorageAsyncStatement.h" +#include "mozIStorageError.h" +#include "mozIStorageFunction.h" +#include "mozIStorageService.h" +#include "mozStorageHelper.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsICookieNotification.h" +#include "nsICookieService.h" +#include "nsIEffectiveTLDService.h" +#include "nsILineInputStream.h" +#include "nsIURIMutator.h" +#include "nsNetUtil.h" +#include "nsVariant.h" +#include "prprf.h" + +// XXX_hack. See bug 178993. +// This is a hack to hide HttpOnly cookies from older browsers +#define HTTP_ONLY_PREFIX "#HttpOnly_" + +constexpr auto COOKIES_SCHEMA_VERSION = 13; + +// parameter indexes; see |Read| +constexpr auto IDX_NAME = 0; +constexpr auto IDX_VALUE = 1; +constexpr auto IDX_HOST = 2; +constexpr auto IDX_PATH = 3; +constexpr auto IDX_EXPIRY = 4; +constexpr auto IDX_LAST_ACCESSED = 5; +constexpr auto IDX_CREATION_TIME = 6; +constexpr auto IDX_SECURE = 7; +constexpr auto IDX_HTTPONLY = 8; +constexpr auto IDX_ORIGIN_ATTRIBUTES = 9; +constexpr auto IDX_SAME_SITE = 10; +constexpr auto IDX_RAW_SAME_SITE = 11; +constexpr auto IDX_SCHEME_MAP = 12; +constexpr auto IDX_PARTITIONED_ATTRIBUTE_SET = 13; + +#define COOKIES_FILE "cookies.sqlite" + +namespace mozilla { +namespace net { + +namespace { + +void BindCookieParameters(mozIStorageBindingParamsArray* aParamsArray, + const CookieKey& aKey, const Cookie* aCookie) { + NS_ASSERTION(aParamsArray, + "Null params array passed to BindCookieParameters!"); + NS_ASSERTION(aCookie, "Null cookie passed to BindCookieParameters!"); + + // Use the asynchronous binding methods to ensure that we do not acquire the + // database lock. + nsCOMPtr<mozIStorageBindingParams> params; + DebugOnly<nsresult> rv = + aParamsArray->NewBindingParams(getter_AddRefs(params)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + nsAutoCString suffix; + aKey.mOriginAttributes.CreateSuffix(suffix); + rv = params->BindUTF8StringByName("originAttributes"_ns, suffix); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + rv = params->BindUTF8StringByName("name"_ns, aCookie->Name()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + rv = params->BindUTF8StringByName("value"_ns, aCookie->Value()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + rv = params->BindUTF8StringByName("host"_ns, aCookie->Host()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + rv = params->BindUTF8StringByName("path"_ns, aCookie->Path()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + rv = params->BindInt64ByName("expiry"_ns, aCookie->Expiry()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + rv = params->BindInt64ByName("lastAccessed"_ns, aCookie->LastAccessed()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + rv = params->BindInt64ByName("creationTime"_ns, aCookie->CreationTime()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + rv = params->BindInt32ByName("isSecure"_ns, aCookie->IsSecure()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + rv = params->BindInt32ByName("isHttpOnly"_ns, aCookie->IsHttpOnly()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + rv = params->BindInt32ByName("sameSite"_ns, aCookie->SameSite()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + rv = params->BindInt32ByName("rawSameSite"_ns, aCookie->RawSameSite()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + rv = params->BindInt32ByName("schemeMap"_ns, aCookie->SchemeMap()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + rv = params->BindInt32ByName("isPartitionedAttributeSet"_ns, + aCookie->RawIsPartitioned()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + // Bind the params to the array. + rv = aParamsArray->AddParams(params); + MOZ_ASSERT(NS_SUCCEEDED(rv)); +} + +class ConvertAppIdToOriginAttrsSQLFunction final : public mozIStorageFunction { + ~ConvertAppIdToOriginAttrsSQLFunction() = default; + + NS_DECL_ISUPPORTS + NS_DECL_MOZISTORAGEFUNCTION +}; + +NS_IMPL_ISUPPORTS(ConvertAppIdToOriginAttrsSQLFunction, mozIStorageFunction); + +NS_IMETHODIMP +ConvertAppIdToOriginAttrsSQLFunction::OnFunctionCall( + mozIStorageValueArray* aFunctionArguments, nsIVariant** aResult) { + nsresult rv; + int32_t inIsolatedMozBrowser; + + rv = aFunctionArguments->GetInt32(1, &inIsolatedMozBrowser); + NS_ENSURE_SUCCESS(rv, rv); + + // Create an originAttributes object by inIsolatedMozBrowser. + // Then create the originSuffix string from this object. + OriginAttributes attrs(inIsolatedMozBrowser != 0); + nsAutoCString suffix; + attrs.CreateSuffix(suffix); + + RefPtr<nsVariant> outVar(new nsVariant()); + rv = outVar->SetAsAUTF8String(suffix); + NS_ENSURE_SUCCESS(rv, rv); + + outVar.forget(aResult); + return NS_OK; +} + +class SetAppIdFromOriginAttributesSQLFunction final + : public mozIStorageFunction { + ~SetAppIdFromOriginAttributesSQLFunction() = default; + + NS_DECL_ISUPPORTS + NS_DECL_MOZISTORAGEFUNCTION +}; + +NS_IMPL_ISUPPORTS(SetAppIdFromOriginAttributesSQLFunction, mozIStorageFunction); + +NS_IMETHODIMP +SetAppIdFromOriginAttributesSQLFunction::OnFunctionCall( + mozIStorageValueArray* aFunctionArguments, nsIVariant** aResult) { + nsresult rv; + nsAutoCString suffix; + OriginAttributes attrs; + + rv = aFunctionArguments->GetUTF8String(0, suffix); + NS_ENSURE_SUCCESS(rv, rv); + bool success = attrs.PopulateFromSuffix(suffix); + NS_ENSURE_TRUE(success, NS_ERROR_FAILURE); + + RefPtr<nsVariant> outVar(new nsVariant()); + rv = outVar->SetAsInt32(0); // deprecated appId! + NS_ENSURE_SUCCESS(rv, rv); + + outVar.forget(aResult); + return NS_OK; +} + +class SetInBrowserFromOriginAttributesSQLFunction final + : public mozIStorageFunction { + ~SetInBrowserFromOriginAttributesSQLFunction() = default; + + NS_DECL_ISUPPORTS + NS_DECL_MOZISTORAGEFUNCTION +}; + +NS_IMPL_ISUPPORTS(SetInBrowserFromOriginAttributesSQLFunction, + mozIStorageFunction); + +NS_IMETHODIMP +SetInBrowserFromOriginAttributesSQLFunction::OnFunctionCall( + mozIStorageValueArray* aFunctionArguments, nsIVariant** aResult) { + nsresult rv; + nsAutoCString suffix; + OriginAttributes attrs; + + rv = aFunctionArguments->GetUTF8String(0, suffix); + NS_ENSURE_SUCCESS(rv, rv); + bool success = attrs.PopulateFromSuffix(suffix); + NS_ENSURE_TRUE(success, NS_ERROR_FAILURE); + + RefPtr<nsVariant> outVar(new nsVariant()); + rv = outVar->SetAsInt32(attrs.mInIsolatedMozBrowser); + NS_ENSURE_SUCCESS(rv, rv); + + outVar.forget(aResult); + return NS_OK; +} + +/****************************************************************************** + * DBListenerErrorHandler impl: + * Parent class for our async storage listeners that handles the logging of + * errors. + ******************************************************************************/ +class DBListenerErrorHandler : public mozIStorageStatementCallback { + protected: + explicit DBListenerErrorHandler(CookiePersistentStorage* dbState) + : mStorage(dbState) {} + RefPtr<CookiePersistentStorage> mStorage; + virtual const char* GetOpType() = 0; + + public: + NS_IMETHOD HandleError(mozIStorageError* aError) override { + if (MOZ_LOG_TEST(gCookieLog, LogLevel::Warning)) { + int32_t result = -1; + aError->GetResult(&result); + + nsAutoCString message; + aError->GetMessage(message); + COOKIE_LOGSTRING( + LogLevel::Warning, + ("DBListenerErrorHandler::HandleError(): Error %d occurred while " + "performing operation '%s' with message '%s'; rebuilding database.", + result, GetOpType(), message.get())); + } + + // Rebuild the database. + mStorage->HandleCorruptDB(); + + return NS_OK; + } +}; + +/****************************************************************************** + * InsertCookieDBListener impl: + * mozIStorageStatementCallback used to track asynchronous insertion operations. + ******************************************************************************/ +class InsertCookieDBListener final : public DBListenerErrorHandler { + private: + const char* GetOpType() override { return "INSERT"; } + + ~InsertCookieDBListener() = default; + + public: + NS_DECL_ISUPPORTS + + explicit InsertCookieDBListener(CookiePersistentStorage* dbState) + : DBListenerErrorHandler(dbState) {} + NS_IMETHOD HandleResult(mozIStorageResultSet* /*aResultSet*/) override { + MOZ_ASSERT_UNREACHABLE( + "Unexpected call to " + "InsertCookieDBListener::HandleResult"); + return NS_OK; + } + NS_IMETHOD HandleCompletion(uint16_t aReason) override { + // If we were rebuilding the db and we succeeded, make our mCorruptFlag say + // so. + if (mStorage->GetCorruptFlag() == CookiePersistentStorage::REBUILDING && + aReason == mozIStorageStatementCallback::REASON_FINISHED) { + COOKIE_LOGSTRING( + LogLevel::Debug, + ("InsertCookieDBListener::HandleCompletion(): rebuild complete")); + mStorage->SetCorruptFlag(CookiePersistentStorage::OK); + } + + // This notification is just for testing. + nsCOMPtr<nsIObserverService> os = services::GetObserverService(); + if (os) { + os->NotifyObservers(nullptr, "cookie-saved-on-disk", nullptr); + } + + return NS_OK; + } +}; + +NS_IMPL_ISUPPORTS(InsertCookieDBListener, mozIStorageStatementCallback) + +/****************************************************************************** + * UpdateCookieDBListener impl: + * mozIStorageStatementCallback used to track asynchronous update operations. + ******************************************************************************/ +class UpdateCookieDBListener final : public DBListenerErrorHandler { + private: + const char* GetOpType() override { return "UPDATE"; } + + ~UpdateCookieDBListener() = default; + + public: + NS_DECL_ISUPPORTS + + explicit UpdateCookieDBListener(CookiePersistentStorage* dbState) + : DBListenerErrorHandler(dbState) {} + NS_IMETHOD HandleResult(mozIStorageResultSet* /*aResultSet*/) override { + MOZ_ASSERT_UNREACHABLE( + "Unexpected call to " + "UpdateCookieDBListener::HandleResult"); + return NS_OK; + } + NS_IMETHOD HandleCompletion(uint16_t /*aReason*/) override { return NS_OK; } +}; + +NS_IMPL_ISUPPORTS(UpdateCookieDBListener, mozIStorageStatementCallback) + +/****************************************************************************** + * RemoveCookieDBListener impl: + * mozIStorageStatementCallback used to track asynchronous removal operations. + ******************************************************************************/ +class RemoveCookieDBListener final : public DBListenerErrorHandler { + private: + const char* GetOpType() override { return "REMOVE"; } + + ~RemoveCookieDBListener() = default; + + public: + NS_DECL_ISUPPORTS + + explicit RemoveCookieDBListener(CookiePersistentStorage* dbState) + : DBListenerErrorHandler(dbState) {} + NS_IMETHOD HandleResult(mozIStorageResultSet* /*aResultSet*/) override { + MOZ_ASSERT_UNREACHABLE( + "Unexpected call to " + "RemoveCookieDBListener::HandleResult"); + return NS_OK; + } + NS_IMETHOD HandleCompletion(uint16_t /*aReason*/) override { return NS_OK; } +}; + +NS_IMPL_ISUPPORTS(RemoveCookieDBListener, mozIStorageStatementCallback) + +/****************************************************************************** + * CloseCookieDBListener imp: + * Static mozIStorageCompletionCallback used to notify when the database is + * successfully closed. + ******************************************************************************/ +class CloseCookieDBListener final : public mozIStorageCompletionCallback { + ~CloseCookieDBListener() = default; + + public: + explicit CloseCookieDBListener(CookiePersistentStorage* dbState) + : mStorage(dbState) {} + RefPtr<CookiePersistentStorage> mStorage; + NS_DECL_ISUPPORTS + + NS_IMETHOD Complete(nsresult /*status*/, nsISupports* /*value*/) override { + mStorage->HandleDBClosed(); + return NS_OK; + } +}; + +NS_IMPL_ISUPPORTS(CloseCookieDBListener, mozIStorageCompletionCallback) + +} // namespace + +// static +already_AddRefed<CookiePersistentStorage> CookiePersistentStorage::Create() { + RefPtr<CookiePersistentStorage> storage = new CookiePersistentStorage(); + storage->Init(); + storage->Activate(); + + return storage.forget(); +} + +CookiePersistentStorage::CookiePersistentStorage() + : mMonitor("CookiePersistentStorage"), + mInitialized(false), + mCorruptFlag(OK) {} + +void CookiePersistentStorage::NotifyChangedInternal( + nsICookieNotification* aNotification, bool aOldCookieIsSession) { + MOZ_ASSERT(aNotification); + // Notify for topic "session-cookie-changed" to update the copy of session + // cookies in session restore component. + + nsICookieNotification::Action action = aNotification->GetAction(); + + // Filter out notifications for individual non-session cookies. + if (action == nsICookieNotification::COOKIE_CHANGED || + action == nsICookieNotification::COOKIE_DELETED || + action == nsICookieNotification::COOKIE_ADDED) { + nsCOMPtr<nsICookie> xpcCookie; + DebugOnly<nsresult> rv = + aNotification->GetCookie(getter_AddRefs(xpcCookie)); + MOZ_ASSERT(NS_SUCCEEDED(rv) && xpcCookie); + const Cookie& cookie = xpcCookie->AsCookie(); + if (!cookie.IsSession() && !aOldCookieIsSession) { + return; + } + } + + nsCOMPtr<nsIObserverService> os = services::GetObserverService(); + if (os) { + os->NotifyObservers(aNotification, "session-cookie-changed", u""); + } +} + +void CookiePersistentStorage::RemoveAllInternal() { + // clear the cookie file + if (mDBConn) { + nsCOMPtr<mozIStorageAsyncStatement> stmt; + nsresult rv = mDBConn->CreateAsyncStatement("DELETE FROM moz_cookies"_ns, + getter_AddRefs(stmt)); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<mozIStoragePendingStatement> handle; + rv = stmt->ExecuteAsync(mRemoveListener, getter_AddRefs(handle)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } else { + // Recreate the database. + COOKIE_LOGSTRING(LogLevel::Debug, + ("RemoveAll(): corruption detected with rv 0x%" PRIx32, + static_cast<uint32_t>(rv))); + HandleCorruptDB(); + } + } +} + +void CookiePersistentStorage::HandleCorruptDB() { + COOKIE_LOGSTRING(LogLevel::Debug, + ("HandleCorruptDB(): CookieStorage %p has mCorruptFlag %u", + this, mCorruptFlag)); + + // Mark the database corrupt, so the close listener can begin reconstructing + // it. + switch (mCorruptFlag) { + case OK: { + // Move to 'closing' state. + mCorruptFlag = CLOSING_FOR_REBUILD; + + CleanupCachedStatements(); + mDBConn->AsyncClose(mCloseListener); + CleanupDBConnection(); + break; + } + case CLOSING_FOR_REBUILD: { + // We had an error while waiting for close completion. That's OK, just + // ignore it -- we're rebuilding anyway. + return; + } + case REBUILDING: { + // We had an error while rebuilding the DB. Game over. Close the database + // and let the close handler do nothing; then we'll move it out of the + // way. + CleanupCachedStatements(); + if (mDBConn) { + mDBConn->AsyncClose(mCloseListener); + } + CleanupDBConnection(); + break; + } + } +} + +void CookiePersistentStorage::RemoveCookiesWithOriginAttributes( + const OriginAttributesPattern& aPattern, const nsACString& aBaseDomain) { + mozStorageTransaction transaction(mDBConn, false); + + // XXX Handle the error, bug 1696130. + Unused << NS_WARN_IF(NS_FAILED(transaction.Start())); + + CookieStorage::RemoveCookiesWithOriginAttributes(aPattern, aBaseDomain); + + DebugOnly<nsresult> rv = transaction.Commit(); + MOZ_ASSERT(NS_SUCCEEDED(rv)); +} + +void CookiePersistentStorage::RemoveCookiesFromExactHost( + const nsACString& aHost, const nsACString& aBaseDomain, + const OriginAttributesPattern& aPattern) { + mozStorageTransaction transaction(mDBConn, false); + + // XXX Handle the error, bug 1696130. + Unused << NS_WARN_IF(NS_FAILED(transaction.Start())); + + CookieStorage::RemoveCookiesFromExactHost(aHost, aBaseDomain, aPattern); + + DebugOnly<nsresult> rv = transaction.Commit(); + MOZ_ASSERT(NS_SUCCEEDED(rv)); +} + +void CookiePersistentStorage::RemoveCookieFromDB(const Cookie& aCookie) { + // if it's a non-session cookie, remove it from the db + if (aCookie.IsSession() || !mDBConn) { + return; + } + + nsCOMPtr<mozIStorageBindingParamsArray> paramsArray; + mStmtDelete->NewBindingParamsArray(getter_AddRefs(paramsArray)); + + PrepareCookieRemoval(aCookie, paramsArray); + + DebugOnly<nsresult> rv = mStmtDelete->BindParameters(paramsArray); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + nsCOMPtr<mozIStoragePendingStatement> handle; + rv = mStmtDelete->ExecuteAsync(mRemoveListener, getter_AddRefs(handle)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); +} + +void CookiePersistentStorage::PrepareCookieRemoval( + const Cookie& aCookie, mozIStorageBindingParamsArray* aParamsArray) { + // if it's a non-session cookie, remove it from the db + if (aCookie.IsSession() || !mDBConn) { + return; + } + + nsCOMPtr<mozIStorageBindingParams> params; + aParamsArray->NewBindingParams(getter_AddRefs(params)); + + DebugOnly<nsresult> rv = + params->BindUTF8StringByName("name"_ns, aCookie.Name()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + rv = params->BindUTF8StringByName("host"_ns, aCookie.Host()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + rv = params->BindUTF8StringByName("path"_ns, aCookie.Path()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + nsAutoCString suffix; + aCookie.OriginAttributesRef().CreateSuffix(suffix); + rv = params->BindUTF8StringByName("originAttributes"_ns, suffix); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + rv = aParamsArray->AddParams(params); + MOZ_ASSERT(NS_SUCCEEDED(rv)); +} + +// Null out the statements. +// This must be done before closing the connection. +void CookiePersistentStorage::CleanupCachedStatements() { + mStmtInsert = nullptr; + mStmtDelete = nullptr; + mStmtUpdate = nullptr; +} + +// Null out the listeners, and the database connection itself. This +// will not null out the statements, cancel a pending read or +// asynchronously close the connection -- these must be done +// beforehand if necessary. +void CookiePersistentStorage::CleanupDBConnection() { + MOZ_ASSERT(!mStmtInsert, "mStmtInsert has been cleaned up"); + MOZ_ASSERT(!mStmtDelete, "mStmtDelete has been cleaned up"); + MOZ_ASSERT(!mStmtUpdate, "mStmtUpdate has been cleaned up"); + + // Null out the database connections. If 'mDBConn' has not been used for any + // asynchronous operations yet, this will synchronously close it; otherwise, + // it's expected that the caller has performed an AsyncClose prior. + mDBConn = nullptr; + + // Manually null out our listeners. This is necessary because they hold a + // strong ref to the CookieStorage itself. They'll stay alive until whatever + // statements are still executing complete. + mInsertListener = nullptr; + mUpdateListener = nullptr; + mRemoveListener = nullptr; + mCloseListener = nullptr; +} + +void CookiePersistentStorage::Close() { + if (mThread) { + mThread->Shutdown(); + mThread = nullptr; + } + + // Cleanup cached statements before we can close anything. + CleanupCachedStatements(); + + if (mDBConn) { + // Asynchronously close the connection. We will null it below. + mDBConn->AsyncClose(mCloseListener); + } + + CleanupDBConnection(); + + mInitialized = false; + mInitializedDBConn = false; +} + +void CookiePersistentStorage::StoreCookie( + const nsACString& aBaseDomain, const OriginAttributes& aOriginAttributes, + Cookie* aCookie) { + // if it's a non-session cookie and hasn't just been read from the db, write + // it out. + if (aCookie->IsSession() || !mDBConn) { + return; + } + + nsCOMPtr<mozIStorageBindingParamsArray> paramsArray; + mStmtInsert->NewBindingParamsArray(getter_AddRefs(paramsArray)); + + CookieKey key(aBaseDomain, aOriginAttributes); + BindCookieParameters(paramsArray, key, aCookie); + + MaybeStoreCookiesToDB(paramsArray); +} + +void CookiePersistentStorage::MaybeStoreCookiesToDB( + mozIStorageBindingParamsArray* aParamsArray) { + if (!aParamsArray) { + return; + } + + uint32_t length; + aParamsArray->GetLength(&length); + if (!length) { + return; + } + + DebugOnly<nsresult> rv = mStmtInsert->BindParameters(aParamsArray); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + nsCOMPtr<mozIStoragePendingStatement> handle; + rv = mStmtInsert->ExecuteAsync(mInsertListener, getter_AddRefs(handle)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); +} + +void CookiePersistentStorage::StaleCookies(const nsTArray<Cookie*>& aCookieList, + int64_t aCurrentTimeInUsec) { + // Create an array of parameters to bind to our update statement. Batching + // is OK here since we're updating cookies with no interleaved operations. + nsCOMPtr<mozIStorageBindingParamsArray> paramsArray; + mozIStorageAsyncStatement* stmt = mStmtUpdate; + if (mDBConn) { + stmt->NewBindingParamsArray(getter_AddRefs(paramsArray)); + } + + int32_t count = aCookieList.Length(); + for (int32_t i = 0; i < count; ++i) { + Cookie* cookie = aCookieList.ElementAt(i); + + if (cookie->IsStale()) { + UpdateCookieInList(cookie, aCurrentTimeInUsec, paramsArray); + } + } + // Update the database now if necessary. + if (paramsArray) { + uint32_t length; + paramsArray->GetLength(&length); + if (length) { + DebugOnly<nsresult> rv = stmt->BindParameters(paramsArray); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + nsCOMPtr<mozIStoragePendingStatement> handle; + rv = stmt->ExecuteAsync(mUpdateListener, getter_AddRefs(handle)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + } +} + +void CookiePersistentStorage::UpdateCookieInList( + Cookie* aCookie, int64_t aLastAccessed, + mozIStorageBindingParamsArray* aParamsArray) { + MOZ_ASSERT(aCookie); + + // udpate the lastAccessed timestamp + aCookie->SetLastAccessed(aLastAccessed); + + // if it's a non-session cookie, update it in the db too + if (!aCookie->IsSession() && aParamsArray) { + // Create our params holder. + nsCOMPtr<mozIStorageBindingParams> params; + aParamsArray->NewBindingParams(getter_AddRefs(params)); + + // Bind our parameters. + DebugOnly<nsresult> rv = + params->BindInt64ByName("lastAccessed"_ns, aLastAccessed); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + rv = params->BindUTF8StringByName("name"_ns, aCookie->Name()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + rv = params->BindUTF8StringByName("host"_ns, aCookie->Host()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + rv = params->BindUTF8StringByName("path"_ns, aCookie->Path()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + nsAutoCString suffix; + aCookie->OriginAttributesRef().CreateSuffix(suffix); + rv = params->BindUTF8StringByName("originAttributes"_ns, suffix); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + // Add our bound parameters to the array. + rv = aParamsArray->AddParams(params); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } +} + +void CookiePersistentStorage::DeleteFromDB( + mozIStorageBindingParamsArray* aParamsArray) { + uint32_t length; + aParamsArray->GetLength(&length); + if (length) { + DebugOnly<nsresult> rv = mStmtDelete->BindParameters(aParamsArray); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + nsCOMPtr<mozIStoragePendingStatement> handle; + rv = mStmtDelete->ExecuteAsync(mRemoveListener, getter_AddRefs(handle)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } +} + +void CookiePersistentStorage::Activate() { + MOZ_ASSERT(!mThread, "already have a cookie thread"); + + mStorageService = do_GetService("@mozilla.org/storage/service;1"); + MOZ_ASSERT(mStorageService); + + mTLDService = do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID); + MOZ_ASSERT(mTLDService); + + // Get our cookie file. + nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(mCookieFile)); + if (NS_FAILED(rv)) { + // We've already set up our CookieStorages appropriately; nothing more to + // do. + COOKIE_LOGSTRING(LogLevel::Warning, + ("InitCookieStorages(): couldn't get cookie file")); + + mInitializedDBConn = true; + mInitialized = true; + return; + } + + mCookieFile->AppendNative(nsLiteralCString(COOKIES_FILE)); + + NS_ENSURE_SUCCESS_VOID(NS_NewNamedThread("Cookie", getter_AddRefs(mThread))); + + RefPtr<CookiePersistentStorage> self = this; + nsCOMPtr<nsIRunnable> runnable = + NS_NewRunnableFunction("CookiePersistentStorage::Activate", [self] { + MonitorAutoLock lock(self->mMonitor); + + // Attempt to open and read the database. If TryInitDB() returns + // RESULT_RETRY, do so. + OpenDBResult result = self->TryInitDB(false); + if (result == RESULT_RETRY) { + // Database may be corrupt. Synchronously close the connection, clean + // up the default CookieStorage, and try again. + COOKIE_LOGSTRING(LogLevel::Warning, + ("InitCookieStorages(): retrying TryInitDB()")); + self->CleanupCachedStatements(); + self->CleanupDBConnection(); + result = self->TryInitDB(true); + if (result == RESULT_RETRY) { + // We're done. Change the code to failure so we clean up below. + result = RESULT_FAILURE; + } + } + + if (result == RESULT_FAILURE) { + COOKIE_LOGSTRING( + LogLevel::Warning, + ("InitCookieStorages(): TryInitDB() failed, closing connection")); + + // Connection failure is unrecoverable. Clean up our connection. We + // can run fine without persistent storage -- e.g. if there's no + // profile. + self->CleanupCachedStatements(); + self->CleanupDBConnection(); + + // No need to initialize mDBConn + self->mInitializedDBConn = true; + } + + self->mInitialized = true; + + NS_DispatchToMainThread( + NS_NewRunnableFunction("CookiePersistentStorage::InitDBConn", + [self] { self->InitDBConn(); })); + self->mMonitor.Notify(); + }); + + mThread->Dispatch(runnable, NS_DISPATCH_NORMAL); +} + +/* Attempt to open and read the database. If 'aRecreateDB' is true, try to + * move the existing database file out of the way and create a new one. + * + * @returns RESULT_OK if opening or creating the database succeeded; + * RESULT_RETRY if the database cannot be opened, is corrupt, or some + * other failure occurred that might be resolved by recreating the + * database; or RESULT_FAILED if there was an unrecoverable error and + * we must run without a database. + * + * If RESULT_RETRY or RESULT_FAILED is returned, the caller should perform + * cleanup of the default CookieStorage. + */ +CookiePersistentStorage::OpenDBResult CookiePersistentStorage::TryInitDB( + bool aRecreateDB) { + NS_ASSERTION(!mDBConn, "nonnull mDBConn"); + NS_ASSERTION(!mStmtInsert, "nonnull mStmtInsert"); + NS_ASSERTION(!mInsertListener, "nonnull mInsertListener"); + NS_ASSERTION(!mSyncConn, "nonnull mSyncConn"); + NS_ASSERTION(NS_GetCurrentThread() == mThread, "non cookie thread"); + + // Ditch an existing db, if we've been told to (i.e. it's corrupt). We don't + // want to delete it outright, since it may be useful for debugging purposes, + // so we move it out of the way. + nsresult rv; + if (aRecreateDB) { + nsCOMPtr<nsIFile> backupFile; + mCookieFile->Clone(getter_AddRefs(backupFile)); + rv = backupFile->MoveToNative(nullptr, + nsLiteralCString(COOKIES_FILE ".bak")); + NS_ENSURE_SUCCESS(rv, RESULT_FAILURE); + } + + // This block provides scope for the Telemetry AutoTimer + { + Telemetry::AutoTimer<Telemetry::MOZ_SQLITE_COOKIES_OPEN_READAHEAD_MS> + telemetry; + ReadAheadFile(mCookieFile); + + // open a connection to the cookie database, and only cache our connection + // and statements upon success. The connection is opened unshared to + // eliminate cache contention between the main and background threads. + rv = mStorageService->OpenUnsharedDatabase( + mCookieFile, mozIStorageService::CONNECTION_DEFAULT, + getter_AddRefs(mSyncConn)); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + } + + auto guard = MakeScopeExit([&] { mSyncConn = nullptr; }); + + bool tableExists = false; + mSyncConn->TableExists("moz_cookies"_ns, &tableExists); + if (!tableExists) { + rv = CreateTable(); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + } else { + // table already exists; check the schema version before reading + int32_t dbSchemaVersion; + rv = mSyncConn->GetSchemaVersion(&dbSchemaVersion); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + // Start a transaction for the whole migration block. + mozStorageTransaction transaction(mSyncConn, true); + + // XXX Handle the error, bug 1696130. + Unused << NS_WARN_IF(NS_FAILED(transaction.Start())); + + switch (dbSchemaVersion) { + // Upgrading. + // Every time you increment the database schema, you need to implement + // the upgrading code from the previous version to the new one. If + // migration fails for any reason, it's a bug -- so we return RESULT_RETRY + // such that the original database will be saved, in the hopes that we + // might one day see it and fix it. + case 1: { + // Add the lastAccessed column to the table. + rv = mSyncConn->ExecuteSimpleSQL(nsLiteralCString( + "ALTER TABLE moz_cookies ADD lastAccessed INTEGER")); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + } + // Fall through to the next upgrade. + [[fallthrough]]; + + case 2: { + // Add the baseDomain column and index to the table. + rv = mSyncConn->ExecuteSimpleSQL( + "ALTER TABLE moz_cookies ADD baseDomain TEXT"_ns); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + // Compute the baseDomains for the table. This must be done eagerly + // otherwise we won't be able to synchronously read in individual + // domains on demand. + const int64_t SCHEMA2_IDX_ID = 0; + const int64_t SCHEMA2_IDX_HOST = 1; + nsCOMPtr<mozIStorageStatement> select; + rv = mSyncConn->CreateStatement("SELECT id, host FROM moz_cookies"_ns, + getter_AddRefs(select)); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + nsCOMPtr<mozIStorageStatement> update; + rv = mSyncConn->CreateStatement( + nsLiteralCString("UPDATE moz_cookies SET baseDomain = " + ":baseDomain WHERE id = :id"), + getter_AddRefs(update)); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + nsCString baseDomain; + nsCString host; + bool hasResult; + while (true) { + rv = select->ExecuteStep(&hasResult); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + if (!hasResult) { + break; + } + + int64_t id = select->AsInt64(SCHEMA2_IDX_ID); + select->GetUTF8String(SCHEMA2_IDX_HOST, host); + + rv = CookieCommons::GetBaseDomainFromHost(mTLDService, host, + baseDomain); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + mozStorageStatementScoper scoper(update); + + rv = update->BindUTF8StringByName("baseDomain"_ns, baseDomain); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + rv = update->BindInt64ByName("id"_ns, id); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + rv = update->ExecuteStep(&hasResult); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + } + + // Create an index on baseDomain. + rv = mSyncConn->ExecuteSimpleSQL(nsLiteralCString( + "CREATE INDEX moz_basedomain ON moz_cookies (baseDomain)")); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + } + // Fall through to the next upgrade. + [[fallthrough]]; + + case 3: { + // Add the creationTime column to the table, and create a unique index + // on (name, host, path). Before we do this, we have to purge the table + // of expired cookies such that we know that the (name, host, path) + // index is truly unique -- otherwise we can't create the index. Note + // that we can't just execute a statement to delete all rows where the + // expiry column is in the past -- doing so would rely on the clock + // (both now and when previous cookies were set) being monotonic. + + // Select the whole table, and order by the fields we're interested in. + // This means we can simply do a linear traversal of the results and + // check for duplicates as we go. + const int64_t SCHEMA3_IDX_ID = 0; + const int64_t SCHEMA3_IDX_NAME = 1; + const int64_t SCHEMA3_IDX_HOST = 2; + const int64_t SCHEMA3_IDX_PATH = 3; + nsCOMPtr<mozIStorageStatement> select; + rv = mSyncConn->CreateStatement( + nsLiteralCString( + "SELECT id, name, host, path FROM moz_cookies " + "ORDER BY name ASC, host ASC, path ASC, expiry ASC"), + getter_AddRefs(select)); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + nsCOMPtr<mozIStorageStatement> deleteExpired; + rv = mSyncConn->CreateStatement( + "DELETE FROM moz_cookies WHERE id = :id"_ns, + getter_AddRefs(deleteExpired)); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + // Read the first row. + bool hasResult; + rv = select->ExecuteStep(&hasResult); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + if (hasResult) { + nsCString name1; + nsCString host1; + nsCString path1; + int64_t id1 = select->AsInt64(SCHEMA3_IDX_ID); + select->GetUTF8String(SCHEMA3_IDX_NAME, name1); + select->GetUTF8String(SCHEMA3_IDX_HOST, host1); + select->GetUTF8String(SCHEMA3_IDX_PATH, path1); + + nsCString name2; + nsCString host2; + nsCString path2; + while (true) { + // Read the second row. + rv = select->ExecuteStep(&hasResult); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + if (!hasResult) { + break; + } + + int64_t id2 = select->AsInt64(SCHEMA3_IDX_ID); + select->GetUTF8String(SCHEMA3_IDX_NAME, name2); + select->GetUTF8String(SCHEMA3_IDX_HOST, host2); + select->GetUTF8String(SCHEMA3_IDX_PATH, path2); + + // If the two rows match in (name, host, path), we know the earlier + // row has an earlier expiry time. Delete it. + if (name1 == name2 && host1 == host2 && path1 == path2) { + mozStorageStatementScoper scoper(deleteExpired); + + rv = deleteExpired->BindInt64ByName("id"_ns, id1); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + rv = deleteExpired->ExecuteStep(&hasResult); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + } + + // Make the second row the first for the next iteration. + name1 = name2; + host1 = host2; + path1 = path2; + id1 = id2; + } + } + + // Add the creationTime column to the table. + rv = mSyncConn->ExecuteSimpleSQL(nsLiteralCString( + "ALTER TABLE moz_cookies ADD creationTime INTEGER")); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + // Copy the id of each row into the new creationTime column. + rv = mSyncConn->ExecuteSimpleSQL( + nsLiteralCString("UPDATE moz_cookies SET creationTime = " + "(SELECT id WHERE id = moz_cookies.id)")); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + // Create a unique index on (name, host, path) to allow fast lookup. + rv = mSyncConn->ExecuteSimpleSQL( + nsLiteralCString("CREATE UNIQUE INDEX moz_uniqueid " + "ON moz_cookies (name, host, path)")); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + } + // Fall through to the next upgrade. + [[fallthrough]]; + + case 4: { + // We need to add appId/inBrowserElement, plus change a constraint on + // the table (unique entries now include appId/inBrowserElement): + // this requires creating a new table and copying the data to it. We + // then rename the new table to the old name. + // + // Why we made this change: appId/inBrowserElement allow "cookie jars" + // for Firefox OS. We create a separate cookie namespace per {appId, + // inBrowserElement}. When upgrading, we convert existing cookies + // (which imply we're on desktop/mobile) to use {0, false}, as that is + // the only namespace used by a non-Firefox-OS implementation. + + // Rename existing table + rv = mSyncConn->ExecuteSimpleSQL(nsLiteralCString( + "ALTER TABLE moz_cookies RENAME TO moz_cookies_old")); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + // Drop existing index (CreateTable will create new one for new table) + rv = mSyncConn->ExecuteSimpleSQL("DROP INDEX moz_basedomain"_ns); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + // Create new table (with new fields and new unique constraint) + rv = CreateTableForSchemaVersion5(); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + // Copy data from old table, using appId/inBrowser=0 for existing rows + rv = mSyncConn->ExecuteSimpleSQL(nsLiteralCString( + "INSERT INTO moz_cookies " + "(baseDomain, appId, inBrowserElement, name, value, host, path, " + "expiry," + " lastAccessed, creationTime, isSecure, isHttpOnly) " + "SELECT baseDomain, 0, 0, name, value, host, path, expiry," + " lastAccessed, creationTime, isSecure, isHttpOnly " + "FROM moz_cookies_old")); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + // Drop old table + rv = mSyncConn->ExecuteSimpleSQL("DROP TABLE moz_cookies_old"_ns); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + COOKIE_LOGSTRING(LogLevel::Debug, + ("Upgraded database to schema version 5")); + } + // Fall through to the next upgrade. + [[fallthrough]]; + + case 5: { + // Change in the version: Replace the columns |appId| and + // |inBrowserElement| by a single column |originAttributes|. + // + // Why we made this change: FxOS new security model (NSec) encapsulates + // "appId/inIsolatedMozBrowser" in nsIPrincipal::originAttributes to + // make it easier to modify the contents of this structure in the + // future. + // + // We do the migration in several steps: + // 1. Rename the old table. + // 2. Create a new table. + // 3. Copy data from the old table to the new table; convert appId and + // inBrowserElement to originAttributes in the meantime. + + // Rename existing table. + rv = mSyncConn->ExecuteSimpleSQL(nsLiteralCString( + "ALTER TABLE moz_cookies RENAME TO moz_cookies_old")); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + // Drop existing index (CreateTable will create new one for new table). + rv = mSyncConn->ExecuteSimpleSQL("DROP INDEX moz_basedomain"_ns); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + // Create new table with new fields and new unique constraint. + rv = CreateTableForSchemaVersion6(); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + // Copy data from old table without the two deprecated columns appId and + // inBrowserElement. + nsCOMPtr<mozIStorageFunction> convertToOriginAttrs( + new ConvertAppIdToOriginAttrsSQLFunction()); + NS_ENSURE_TRUE(convertToOriginAttrs, RESULT_RETRY); + + constexpr auto convertToOriginAttrsName = + "CONVERT_TO_ORIGIN_ATTRIBUTES"_ns; + + rv = mSyncConn->CreateFunction(convertToOriginAttrsName, 2, + convertToOriginAttrs); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + rv = mSyncConn->ExecuteSimpleSQL(nsLiteralCString( + "INSERT INTO moz_cookies " + "(baseDomain, originAttributes, name, value, host, path, expiry," + " lastAccessed, creationTime, isSecure, isHttpOnly) " + "SELECT baseDomain, " + " CONVERT_TO_ORIGIN_ATTRIBUTES(appId, inBrowserElement)," + " name, value, host, path, expiry, lastAccessed, creationTime, " + " isSecure, isHttpOnly " + "FROM moz_cookies_old")); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + rv = mSyncConn->RemoveFunction(convertToOriginAttrsName); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + // Drop old table + rv = mSyncConn->ExecuteSimpleSQL("DROP TABLE moz_cookies_old"_ns); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + COOKIE_LOGSTRING(LogLevel::Debug, + ("Upgraded database to schema version 6")); + } + [[fallthrough]]; + + case 6: { + // We made a mistake in schema version 6. We cannot remove expected + // columns of any version (checked in the default case) from cookie + // database, because doing this would destroy the possibility of + // downgrading database. + // + // This version simply restores appId and inBrowserElement columns in + // order to fix downgrading issue even though these two columns are no + // longer used in the latest schema. + rv = mSyncConn->ExecuteSimpleSQL(nsLiteralCString( + "ALTER TABLE moz_cookies ADD appId INTEGER DEFAULT 0;")); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + rv = mSyncConn->ExecuteSimpleSQL(nsLiteralCString( + "ALTER TABLE moz_cookies ADD inBrowserElement INTEGER DEFAULT 0;")); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + // Compute and populate the values of appId and inBrwoserElement from + // originAttributes. + nsCOMPtr<mozIStorageFunction> setAppId( + new SetAppIdFromOriginAttributesSQLFunction()); + NS_ENSURE_TRUE(setAppId, RESULT_RETRY); + + constexpr auto setAppIdName = "SET_APP_ID"_ns; + + rv = mSyncConn->CreateFunction(setAppIdName, 1, setAppId); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + nsCOMPtr<mozIStorageFunction> setInBrowser( + new SetInBrowserFromOriginAttributesSQLFunction()); + NS_ENSURE_TRUE(setInBrowser, RESULT_RETRY); + + constexpr auto setInBrowserName = "SET_IN_BROWSER"_ns; + + rv = mSyncConn->CreateFunction(setInBrowserName, 1, setInBrowser); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + rv = mSyncConn->ExecuteSimpleSQL(nsLiteralCString( + "UPDATE moz_cookies SET appId = SET_APP_ID(originAttributes), " + "inBrowserElement = SET_IN_BROWSER(originAttributes);")); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + rv = mSyncConn->RemoveFunction(setAppIdName); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + rv = mSyncConn->RemoveFunction(setInBrowserName); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + COOKIE_LOGSTRING(LogLevel::Debug, + ("Upgraded database to schema version 7")); + } + [[fallthrough]]; + + case 7: { + // Remove the appId field from moz_cookies. + // + // Unfortunately sqlite doesn't support dropping columns using ALTER + // TABLE, so we need to go through the procedure documented in + // https://www.sqlite.org/lang_altertable.html. + + // Drop existing index + rv = mSyncConn->ExecuteSimpleSQL("DROP INDEX moz_basedomain"_ns); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + // Create a new_moz_cookies table without the appId field. + rv = mSyncConn->ExecuteSimpleSQL( + nsLiteralCString("CREATE TABLE new_moz_cookies(" + "id INTEGER PRIMARY KEY, " + "baseDomain TEXT, " + "originAttributes TEXT NOT NULL DEFAULT '', " + "name TEXT, " + "value TEXT, " + "host TEXT, " + "path TEXT, " + "expiry INTEGER, " + "lastAccessed INTEGER, " + "creationTime INTEGER, " + "isSecure INTEGER, " + "isHttpOnly INTEGER, " + "inBrowserElement INTEGER DEFAULT 0, " + "CONSTRAINT moz_uniqueid UNIQUE (name, host, " + "path, originAttributes)" + ")")); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + // Move the data over. + rv = mSyncConn->ExecuteSimpleSQL( + nsLiteralCString("INSERT INTO new_moz_cookies (" + "id, " + "baseDomain, " + "originAttributes, " + "name, " + "value, " + "host, " + "path, " + "expiry, " + "lastAccessed, " + "creationTime, " + "isSecure, " + "isHttpOnly, " + "inBrowserElement " + ") SELECT " + "id, " + "baseDomain, " + "originAttributes, " + "name, " + "value, " + "host, " + "path, " + "expiry, " + "lastAccessed, " + "creationTime, " + "isSecure, " + "isHttpOnly, " + "inBrowserElement " + "FROM moz_cookies;")); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + // Drop the old table + rv = mSyncConn->ExecuteSimpleSQL("DROP TABLE moz_cookies;"_ns); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + // Rename new_moz_cookies to moz_cookies. + rv = mSyncConn->ExecuteSimpleSQL(nsLiteralCString( + "ALTER TABLE new_moz_cookies RENAME TO moz_cookies;")); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + // Recreate our index. + rv = mSyncConn->ExecuteSimpleSQL( + nsLiteralCString("CREATE INDEX moz_basedomain ON moz_cookies " + "(baseDomain, originAttributes)")); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + COOKIE_LOGSTRING(LogLevel::Debug, + ("Upgraded database to schema version 8")); + } + [[fallthrough]]; + + case 8: { + // Add the sameSite column to the table. + rv = mSyncConn->ExecuteSimpleSQL( + "ALTER TABLE moz_cookies ADD sameSite INTEGER"_ns); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + COOKIE_LOGSTRING(LogLevel::Debug, + ("Upgraded database to schema version 9")); + } + [[fallthrough]]; + + case 9: { + // Add the rawSameSite column to the table. + rv = mSyncConn->ExecuteSimpleSQL(nsLiteralCString( + "ALTER TABLE moz_cookies ADD rawSameSite INTEGER")); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + // Copy the current sameSite value into rawSameSite. + rv = mSyncConn->ExecuteSimpleSQL( + "UPDATE moz_cookies SET rawSameSite = sameSite"_ns); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + COOKIE_LOGSTRING(LogLevel::Debug, + ("Upgraded database to schema version 10")); + } + [[fallthrough]]; + + case 10: { + // Rename existing table + rv = mSyncConn->ExecuteSimpleSQL(nsLiteralCString( + "ALTER TABLE moz_cookies RENAME TO moz_cookies_old")); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + // Create a new moz_cookies table without the baseDomain field. + rv = mSyncConn->ExecuteSimpleSQL( + nsLiteralCString("CREATE TABLE moz_cookies(" + "id INTEGER PRIMARY KEY, " + "originAttributes TEXT NOT NULL DEFAULT '', " + "name TEXT, " + "value TEXT, " + "host TEXT, " + "path TEXT, " + "expiry INTEGER, " + "lastAccessed INTEGER, " + "creationTime INTEGER, " + "isSecure INTEGER, " + "isHttpOnly INTEGER, " + "inBrowserElement INTEGER DEFAULT 0, " + "sameSite INTEGER DEFAULT 0, " + "rawSameSite INTEGER DEFAULT 0, " + "CONSTRAINT moz_uniqueid UNIQUE (name, host, " + "path, originAttributes)" + ")")); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + // Move the data over. + rv = mSyncConn->ExecuteSimpleSQL( + nsLiteralCString("INSERT INTO moz_cookies (" + "id, " + "originAttributes, " + "name, " + "value, " + "host, " + "path, " + "expiry, " + "lastAccessed, " + "creationTime, " + "isSecure, " + "isHttpOnly, " + "inBrowserElement, " + "sameSite, " + "rawSameSite " + ") SELECT " + "id, " + "originAttributes, " + "name, " + "value, " + "host, " + "path, " + "expiry, " + "lastAccessed, " + "creationTime, " + "isSecure, " + "isHttpOnly, " + "inBrowserElement, " + "sameSite, " + "rawSameSite " + "FROM moz_cookies_old " + "WHERE baseDomain NOTNULL;")); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + // Drop the old table + rv = mSyncConn->ExecuteSimpleSQL("DROP TABLE moz_cookies_old;"_ns); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + // Drop the moz_basedomain index from the database (if it hasn't been + // removed already by removing the table). + rv = mSyncConn->ExecuteSimpleSQL( + "DROP INDEX IF EXISTS moz_basedomain;"_ns); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + COOKIE_LOGSTRING(LogLevel::Debug, + ("Upgraded database to schema version 11")); + } + [[fallthrough]]; + + case 11: { + // Add the schemeMap column to the table. + rv = mSyncConn->ExecuteSimpleSQL(nsLiteralCString( + "ALTER TABLE moz_cookies ADD schemeMap INTEGER DEFAULT 0;")); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + COOKIE_LOGSTRING(LogLevel::Debug, + ("Upgraded database to schema version 12")); + + // No more upgrades. Update the schema version. + rv = mSyncConn->SetSchemaVersion(COOKIES_SCHEMA_VERSION); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + } + [[fallthrough]]; + + case 12: { + // Add the isPartitionedAttributeSet column to the table. + rv = mSyncConn->ExecuteSimpleSQL( + nsLiteralCString("ALTER TABLE moz_cookies ADD " + "isPartitionedAttributeSet INTEGER DEFAULT 0;")); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + COOKIE_LOGSTRING(LogLevel::Debug, + ("Upgraded database to schema version 13")); + + // No more upgrades. Update the schema version. + rv = mSyncConn->SetSchemaVersion(COOKIES_SCHEMA_VERSION); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + [[fallthrough]]; + } + + case COOKIES_SCHEMA_VERSION: + break; + + case 0: { + NS_WARNING("couldn't get schema version!"); + + // the table may be usable; someone might've just clobbered the schema + // version. we can treat this case like a downgrade using the codepath + // below, by verifying the columns we care about are all there. for now, + // re-set the schema version in the db, in case the checks succeed (if + // they don't, we're dropping the table anyway). + rv = mSyncConn->SetSchemaVersion(COOKIES_SCHEMA_VERSION); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + } + // fall through to downgrade check + [[fallthrough]]; + + // downgrading. + // if columns have been added to the table, we can still use the ones we + // understand safely. if columns have been deleted or altered, just + // blow away the table and start from scratch! if you change the way + // a column is interpreted, make sure you also change its name so this + // check will catch it. + default: { + // check if all the expected columns exist + nsCOMPtr<mozIStorageStatement> stmt; + rv = mSyncConn->CreateStatement( + nsLiteralCString("SELECT " + "id, " + "originAttributes, " + "name, " + "value, " + "host, " + "path, " + "expiry, " + "lastAccessed, " + "creationTime, " + "isSecure, " + "isHttpOnly, " + "sameSite, " + "rawSameSite, " + "schemeMap, " + "isPartitionedAttributeSet " + "FROM moz_cookies"), + getter_AddRefs(stmt)); + if (NS_SUCCEEDED(rv)) { + break; + } + + // our columns aren't there - drop the table! + rv = mSyncConn->ExecuteSimpleSQL("DROP TABLE moz_cookies"_ns); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + rv = CreateTable(); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + } break; + } + } + + // if we deleted a corrupt db, don't attempt to import - return now + if (aRecreateDB) { + return RESULT_OK; + } + + // check whether to import or just read in the db + if (tableExists) { + return Read(); + } + + return RESULT_OK; +} + +void CookiePersistentStorage::RebuildCorruptDB() { + NS_ASSERTION(!mDBConn, "shouldn't have an open db connection"); + NS_ASSERTION(mCorruptFlag == CookiePersistentStorage::CLOSING_FOR_REBUILD, + "should be in CLOSING_FOR_REBUILD state"); + + nsCOMPtr<nsIObserverService> os = services::GetObserverService(); + + mCorruptFlag = CookiePersistentStorage::REBUILDING; + + COOKIE_LOGSTRING(LogLevel::Debug, + ("RebuildCorruptDB(): creating new database")); + + RefPtr<CookiePersistentStorage> self = this; + nsCOMPtr<nsIRunnable> runnable = + NS_NewRunnableFunction("RebuildCorruptDB.TryInitDB", [self] { + // The database has been closed, and we're ready to rebuild. Open a + // connection. + OpenDBResult result = self->TryInitDB(true); + + nsCOMPtr<nsIRunnable> innerRunnable = NS_NewRunnableFunction( + "RebuildCorruptDB.TryInitDBComplete", [self, result] { + nsCOMPtr<nsIObserverService> os = services::GetObserverService(); + if (result != RESULT_OK) { + // We're done. Reset our DB connection and statements, and + // notify of closure. + COOKIE_LOGSTRING( + LogLevel::Warning, + ("RebuildCorruptDB(): TryInitDB() failed with result %u", + result)); + self->CleanupCachedStatements(); + self->CleanupDBConnection(); + self->mCorruptFlag = CookiePersistentStorage::OK; + if (os) { + os->NotifyObservers(nullptr, "cookie-db-closed", nullptr); + } + return; + } + + // Notify observers that we're beginning the rebuild. + if (os) { + os->NotifyObservers(nullptr, "cookie-db-rebuilding", nullptr); + } + + self->InitDBConnInternal(); + + // Enumerate the hash, and add cookies to the params array. + mozIStorageAsyncStatement* stmt = self->mStmtInsert; + nsCOMPtr<mozIStorageBindingParamsArray> paramsArray; + stmt->NewBindingParamsArray(getter_AddRefs(paramsArray)); + for (auto iter = self->mHostTable.Iter(); !iter.Done(); + iter.Next()) { + CookieEntry* entry = iter.Get(); + + const CookieEntry::ArrayType& cookies = entry->GetCookies(); + for (CookieEntry::IndexType i = 0; i < cookies.Length(); ++i) { + Cookie* cookie = cookies[i]; + + if (!cookie->IsSession()) { + BindCookieParameters(paramsArray, CookieKey(entry), cookie); + } + } + } + + // Make sure we've got something to write. If we don't, we're + // done. + uint32_t length; + paramsArray->GetLength(&length); + if (length == 0) { + COOKIE_LOGSTRING( + LogLevel::Debug, + ("RebuildCorruptDB(): nothing to write, rebuild complete")); + self->mCorruptFlag = CookiePersistentStorage::OK; + return; + } + + self->MaybeStoreCookiesToDB(paramsArray); + }); + NS_DispatchToMainThread(innerRunnable); + }); + mThread->Dispatch(runnable, NS_DISPATCH_NORMAL); +} + +void CookiePersistentStorage::HandleDBClosed() { + COOKIE_LOGSTRING(LogLevel::Debug, + ("HandleDBClosed(): CookieStorage %p closed", this)); + + nsCOMPtr<nsIObserverService> os = services::GetObserverService(); + + switch (mCorruptFlag) { + case CookiePersistentStorage::OK: { + // Database is healthy. Notify of closure. + if (os) { + os->NotifyObservers(nullptr, "cookie-db-closed", nullptr); + } + break; + } + case CookiePersistentStorage::CLOSING_FOR_REBUILD: { + // Our close finished. Start the rebuild, and notify of db closure later. + RebuildCorruptDB(); + break; + } + case CookiePersistentStorage::REBUILDING: { + // We encountered an error during rebuild, closed the database, and now + // here we are. We already have a 'cookies.sqlite.bak' from the original + // dead database; we don't want to overwrite it, so let's move this one to + // 'cookies.sqlite.bak-rebuild'. + nsCOMPtr<nsIFile> backupFile; + mCookieFile->Clone(getter_AddRefs(backupFile)); + nsresult rv = backupFile->MoveToNative( + nullptr, nsLiteralCString(COOKIES_FILE ".bak-rebuild")); + + COOKIE_LOGSTRING(LogLevel::Warning, + ("HandleDBClosed(): CookieStorage %p encountered error " + "rebuilding db; move to " + "'cookies.sqlite.bak-rebuild' gave rv 0x%" PRIx32, + this, static_cast<uint32_t>(rv))); + if (os) { + os->NotifyObservers(nullptr, "cookie-db-closed", nullptr); + } + break; + } + } +} + +CookiePersistentStorage::OpenDBResult CookiePersistentStorage::Read() { + MOZ_ASSERT(NS_GetCurrentThread() == mThread); + + // Read in the data synchronously. + // see IDX_NAME, etc. for parameter indexes + nsCOMPtr<mozIStorageStatement> stmt; + nsresult rv = + mSyncConn->CreateStatement(nsLiteralCString("SELECT " + "name, " + "value, " + "host, " + "path, " + "expiry, " + "lastAccessed, " + "creationTime, " + "isSecure, " + "isHttpOnly, " + "originAttributes, " + "sameSite, " + "rawSameSite, " + "schemeMap, " + "isPartitionedAttributeSet " + "FROM moz_cookies"), + getter_AddRefs(stmt)); + + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + if (NS_WARN_IF(!mReadArray.IsEmpty())) { + mReadArray.Clear(); + } + mReadArray.SetCapacity(kMaxNumberOfCookies); + + nsCString baseDomain; + nsCString name; + nsCString value; + nsCString host; + nsCString path; + bool hasResult; + while (true) { + rv = stmt->ExecuteStep(&hasResult); + if (NS_WARN_IF(NS_FAILED(rv))) { + mReadArray.Clear(); + return RESULT_RETRY; + } + + if (!hasResult) { + break; + } + + stmt->GetUTF8String(IDX_HOST, host); + + rv = CookieCommons::GetBaseDomainFromHost(mTLDService, host, baseDomain); + if (NS_FAILED(rv)) { + COOKIE_LOGSTRING(LogLevel::Debug, + ("Read(): Ignoring invalid host '%s'", host.get())); + continue; + } + + nsAutoCString suffix; + OriginAttributes attrs; + stmt->GetUTF8String(IDX_ORIGIN_ATTRIBUTES, suffix); + // If PopulateFromSuffix failed we just ignore the OA attributes + // that we don't support + Unused << attrs.PopulateFromSuffix(suffix); + + CookieKey key(baseDomain, attrs); + CookieDomainTuple* tuple = mReadArray.AppendElement(); + tuple->key = std::move(key); + tuple->originAttributes = attrs; + tuple->cookie = GetCookieFromRow(stmt); + } + + COOKIE_LOGSTRING(LogLevel::Debug, + ("Read(): %zu cookies read", mReadArray.Length())); + + return RESULT_OK; +} + +// Extract data from a single result row and create an Cookie. +UniquePtr<CookieStruct> CookiePersistentStorage::GetCookieFromRow( + mozIStorageStatement* aRow) { + nsCString name; + nsCString value; + nsCString host; + nsCString path; + DebugOnly<nsresult> rv = aRow->GetUTF8String(IDX_NAME, name); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + rv = aRow->GetUTF8String(IDX_VALUE, value); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + rv = aRow->GetUTF8String(IDX_HOST, host); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + rv = aRow->GetUTF8String(IDX_PATH, path); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + int64_t expiry = aRow->AsInt64(IDX_EXPIRY); + int64_t lastAccessed = aRow->AsInt64(IDX_LAST_ACCESSED); + int64_t creationTime = aRow->AsInt64(IDX_CREATION_TIME); + bool isSecure = 0 != aRow->AsInt32(IDX_SECURE); + bool isHttpOnly = 0 != aRow->AsInt32(IDX_HTTPONLY); + int32_t sameSite = aRow->AsInt32(IDX_SAME_SITE); + int32_t rawSameSite = aRow->AsInt32(IDX_RAW_SAME_SITE); + int32_t schemeMap = aRow->AsInt32(IDX_SCHEME_MAP); + bool isPartitionedAttributeSet = + 0 != aRow->AsInt32(IDX_PARTITIONED_ATTRIBUTE_SET); + + // Create a new constCookie and assign the data. + return MakeUnique<CookieStruct>( + name, value, host, path, expiry, lastAccessed, creationTime, isHttpOnly, + false, isSecure, isPartitionedAttributeSet, sameSite, rawSameSite, + static_cast<nsICookie::schemeType>(schemeMap)); +} + +void CookiePersistentStorage::EnsureInitialized() { + MOZ_ASSERT(NS_IsMainThread()); + + bool isAccumulated = false; + + if (!mInitialized) { + TimeStamp startBlockTime = TimeStamp::Now(); + MonitorAutoLock lock(mMonitor); + + while (!mInitialized) { + mMonitor.Wait(); + } + + Telemetry::AccumulateTimeDelta( + Telemetry::MOZ_SQLITE_COOKIES_BLOCK_MAIN_THREAD_MS_V2, startBlockTime); + Telemetry::Accumulate( + Telemetry::MOZ_SQLITE_COOKIES_TIME_TO_BLOCK_MAIN_THREAD_MS, 0); + isAccumulated = true; + } else if (!mEndInitDBConn.IsNull()) { + // We didn't block main thread, and here comes the first cookie request. + // Collect how close we're going to block main thread. + Telemetry::Accumulate( + Telemetry::MOZ_SQLITE_COOKIES_TIME_TO_BLOCK_MAIN_THREAD_MS, + (TimeStamp::Now() - mEndInitDBConn).ToMilliseconds()); + // Nullify the timestamp so wo don't accumulate this telemetry probe again. + mEndInitDBConn = TimeStamp(); + isAccumulated = true; + } else if (!mInitializedDBConn) { + // A request comes while we finished cookie thread task and InitDBConn is + // on the way from cookie thread to main thread. We're very close to block + // main thread. + Telemetry::Accumulate( + Telemetry::MOZ_SQLITE_COOKIES_TIME_TO_BLOCK_MAIN_THREAD_MS, 0); + isAccumulated = true; + } + + if (!mInitializedDBConn) { + InitDBConn(); + if (isAccumulated) { + // Nullify the timestamp so wo don't accumulate this telemetry probe + // again. + mEndInitDBConn = TimeStamp(); + } + } +} + +void CookiePersistentStorage::InitDBConn() { + MOZ_ASSERT(NS_IsMainThread()); + + // We should skip InitDBConn if we close profile during initializing + // CookieStorages and then InitDBConn is called after we close the + // CookieStorages. + if (!mInitialized || mInitializedDBConn) { + return; + } + + nsCOMPtr<nsIURI> dummyUri; + nsresult rv = NS_NewURI(getter_AddRefs(dummyUri), "https://example.com"); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + nsTArray<RefPtr<Cookie>> cleanupCookies; + + for (uint32_t i = 0; i < mReadArray.Length(); ++i) { + CookieDomainTuple& tuple = mReadArray[i]; + MOZ_ASSERT(!tuple.cookie->isSession()); + + // filter invalid non-ipv4 host ending in number from old db values + nsCOMPtr<nsIURIMutator> outMut; + nsCOMPtr<nsIURIMutator> dummyMut; + rv = dummyUri->Mutate(getter_AddRefs(dummyMut)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + rv = dummyMut->SetHost(tuple.cookie->host(), getter_AddRefs(outMut)); + + if (NS_FAILED(rv)) { + COOKIE_LOGSTRING(LogLevel::Debug, ("Removing cookie from db with " + "newly invalid hostname: '%s'", + tuple.cookie->host().get())); + RefPtr<Cookie> cookie = + Cookie::Create(*tuple.cookie, tuple.originAttributes); + cleanupCookies.AppendElement(cookie); + continue; + } + + // CreateValidated fixes up the creation and lastAccessed times. + // If the DB is corrupted and the timestaps are far away in the future + // we don't want the creation timestamp to update gLastCreationTime + // as that would contaminate all the next creation times. + // We fix up these dates to not be later than the current time. + // The downside is that if the user sets the date far away in the past + // then back to the current date, those cookies will be stale, + // but if we don't fix their dates, those cookies might never be + // evicted. + RefPtr<Cookie> cookie = + Cookie::CreateValidated(*tuple.cookie, tuple.originAttributes); + AddCookieToList(tuple.key.mBaseDomain, tuple.key.mOriginAttributes, cookie); + } + + if (NS_FAILED(InitDBConnInternal())) { + COOKIE_LOGSTRING(LogLevel::Warning, + ("InitDBConn(): retrying InitDBConnInternal()")); + CleanupCachedStatements(); + CleanupDBConnection(); + if (NS_FAILED(InitDBConnInternal())) { + COOKIE_LOGSTRING( + LogLevel::Warning, + ("InitDBConn(): InitDBConnInternal() failed, closing connection")); + + // Game over, clean the connections. + CleanupCachedStatements(); + CleanupDBConnection(); + } + } + mInitializedDBConn = true; + + COOKIE_LOGSTRING(LogLevel::Debug, + ("InitDBConn(): mInitializedDBConn = true")); + mEndInitDBConn = TimeStamp::Now(); + + for (const auto& cookie : cleanupCookies) { + RemoveCookieFromDB(*cookie); + } + + nsCOMPtr<nsIObserverService> os = services::GetObserverService(); + if (os) { + os->NotifyObservers(nullptr, "cookie-db-read", nullptr); + mReadArray.Clear(); + } +} + +nsresult CookiePersistentStorage::InitDBConnInternal() { + MOZ_ASSERT(NS_IsMainThread()); + + nsresult rv = mStorageService->OpenUnsharedDatabase( + mCookieFile, mozIStorageService::CONNECTION_DEFAULT, + getter_AddRefs(mDBConn)); + NS_ENSURE_SUCCESS(rv, rv); + + // Set up our listeners. + mInsertListener = new InsertCookieDBListener(this); + mUpdateListener = new UpdateCookieDBListener(this); + mRemoveListener = new RemoveCookieDBListener(this); + mCloseListener = new CloseCookieDBListener(this); + + // Grow cookie db in 512KB increments + mDBConn->SetGrowthIncrement(512 * 1024, ""_ns); + + // make operations on the table asynchronous, for performance + mDBConn->ExecuteSimpleSQL("PRAGMA synchronous = OFF"_ns); + + // Use write-ahead-logging for performance. We cap the autocheckpoint limit at + // 16 pages (around 500KB). + mDBConn->ExecuteSimpleSQL(nsLiteralCString(MOZ_STORAGE_UNIQUIFY_QUERY_STR + "PRAGMA journal_mode = WAL")); + mDBConn->ExecuteSimpleSQL("PRAGMA wal_autocheckpoint = 16"_ns); + + // cache frequently used statements (for insertion, deletion, and updating) + rv = mDBConn->CreateAsyncStatement( + nsLiteralCString("INSERT INTO moz_cookies (" + "originAttributes, " + "name, " + "value, " + "host, " + "path, " + "expiry, " + "lastAccessed, " + "creationTime, " + "isSecure, " + "isHttpOnly, " + "sameSite, " + "rawSameSite, " + "schemeMap, " + "isPartitionedAttributeSet " + ") VALUES (" + ":originAttributes, " + ":name, " + ":value, " + ":host, " + ":path, " + ":expiry, " + ":lastAccessed, " + ":creationTime, " + ":isSecure, " + ":isHttpOnly, " + ":sameSite, " + ":rawSameSite, " + ":schemeMap, " + ":isPartitionedAttributeSet " + ")"), + getter_AddRefs(mStmtInsert)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mDBConn->CreateAsyncStatement( + nsLiteralCString("DELETE FROM moz_cookies " + "WHERE name = :name AND host = :host AND path = :path " + "AND originAttributes = :originAttributes"), + getter_AddRefs(mStmtDelete)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mDBConn->CreateAsyncStatement( + nsLiteralCString("UPDATE moz_cookies SET lastAccessed = :lastAccessed " + "WHERE name = :name AND host = :host AND path = :path " + "AND originAttributes = :originAttributes"), + getter_AddRefs(mStmtUpdate)); + return rv; +} + +// Sets the schema version and creates the moz_cookies table. +nsresult CookiePersistentStorage::CreateTableWorker(const char* aName) { + // Create the table. + // We default originAttributes to empty string: this is so if users revert to + // an older Firefox version that doesn't know about this field, any cookies + // set will still work once they upgrade back. + nsAutoCString command("CREATE TABLE "); + command.Append(aName); + command.AppendLiteral( + " (" + "id INTEGER PRIMARY KEY, " + "originAttributes TEXT NOT NULL DEFAULT '', " + "name TEXT, " + "value TEXT, " + "host TEXT, " + "path TEXT, " + "expiry INTEGER, " + "lastAccessed INTEGER, " + "creationTime INTEGER, " + "isSecure INTEGER, " + "isHttpOnly INTEGER, " + "inBrowserElement INTEGER DEFAULT 0, " + "sameSite INTEGER DEFAULT 0, " + "rawSameSite INTEGER DEFAULT 0, " + "schemeMap INTEGER DEFAULT 0, " + "isPartitionedAttributeSet INTEGER DEFAULT 0, " + "CONSTRAINT moz_uniqueid UNIQUE (name, host, path, originAttributes)" + ")"); + return mSyncConn->ExecuteSimpleSQL(command); +} + +// Sets the schema version and creates the moz_cookies table. +nsresult CookiePersistentStorage::CreateTable() { + // Set the schema version, before creating the table. + nsresult rv = mSyncConn->SetSchemaVersion(COOKIES_SCHEMA_VERSION); + if (NS_FAILED(rv)) { + return rv; + } + + rv = CreateTableWorker("moz_cookies"); + if (NS_FAILED(rv)) { + return rv; + } + + return NS_OK; +} + +// Sets the schema version and creates the moz_cookies table. +nsresult CookiePersistentStorage::CreateTableForSchemaVersion6() { + // Set the schema version, before creating the table. + nsresult rv = mSyncConn->SetSchemaVersion(6); + if (NS_FAILED(rv)) { + return rv; + } + + // Create the table. + // We default originAttributes to empty string: this is so if users revert to + // an older Firefox version that doesn't know about this field, any cookies + // set will still work once they upgrade back. + rv = mSyncConn->ExecuteSimpleSQL(nsLiteralCString( + "CREATE TABLE moz_cookies (" + "id INTEGER PRIMARY KEY, " + "baseDomain TEXT, " + "originAttributes TEXT NOT NULL DEFAULT '', " + "name TEXT, " + "value TEXT, " + "host TEXT, " + "path TEXT, " + "expiry INTEGER, " + "lastAccessed INTEGER, " + "creationTime INTEGER, " + "isSecure INTEGER, " + "isHttpOnly INTEGER, " + "CONSTRAINT moz_uniqueid UNIQUE (name, host, path, originAttributes)" + ")")); + if (NS_FAILED(rv)) { + return rv; + } + + // Create an index on baseDomain. + return mSyncConn->ExecuteSimpleSQL(nsLiteralCString( + "CREATE INDEX moz_basedomain ON moz_cookies (baseDomain, " + "originAttributes)")); +} + +// Sets the schema version and creates the moz_cookies table. +nsresult CookiePersistentStorage::CreateTableForSchemaVersion5() { + // Set the schema version, before creating the table. + nsresult rv = mSyncConn->SetSchemaVersion(5); + if (NS_FAILED(rv)) { + return rv; + } + + // Create the table. We default appId/inBrowserElement to 0: this is so if + // users revert to an older Firefox version that doesn't know about these + // fields, any cookies set will still work once they upgrade back. + rv = mSyncConn->ExecuteSimpleSQL( + nsLiteralCString("CREATE TABLE moz_cookies (" + "id INTEGER PRIMARY KEY, " + "baseDomain TEXT, " + "appId INTEGER DEFAULT 0, " + "inBrowserElement INTEGER DEFAULT 0, " + "name TEXT, " + "value TEXT, " + "host TEXT, " + "path TEXT, " + "expiry INTEGER, " + "lastAccessed INTEGER, " + "creationTime INTEGER, " + "isSecure INTEGER, " + "isHttpOnly INTEGER, " + "CONSTRAINT moz_uniqueid UNIQUE (name, host, path, " + "appId, inBrowserElement)" + ")")); + if (NS_FAILED(rv)) { + return rv; + } + + // Create an index on baseDomain. + return mSyncConn->ExecuteSimpleSQL(nsLiteralCString( + "CREATE INDEX moz_basedomain ON moz_cookies (baseDomain, " + "appId, " + "inBrowserElement)")); +} + +nsresult CookiePersistentStorage::RunInTransaction( + nsICookieTransactionCallback* aCallback) { + if (NS_WARN_IF(!mDBConn)) { + return NS_ERROR_NOT_AVAILABLE; + } + + mozStorageTransaction transaction(mDBConn, true); + + // XXX Handle the error, bug 1696130. + Unused << NS_WARN_IF(NS_FAILED(transaction.Start())); + + if (NS_FAILED(aCallback->Callback())) { + Unused << transaction.Rollback(); + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +// purges expired and old cookies in a batch operation. +already_AddRefed<nsIArray> CookiePersistentStorage::PurgeCookies( + int64_t aCurrentTimeInUsec, uint16_t aMaxNumberOfCookies, + int64_t aCookiePurgeAge) { + // Create a params array to batch the removals. This is OK here because + // all the removals are in order, and there are no interleaved additions. + nsCOMPtr<mozIStorageBindingParamsArray> paramsArray; + if (mDBConn) { + mStmtDelete->NewBindingParamsArray(getter_AddRefs(paramsArray)); + } + + RefPtr<CookiePersistentStorage> self = this; + + return PurgeCookiesWithCallbacks( + aCurrentTimeInUsec, aMaxNumberOfCookies, aCookiePurgeAge, + [paramsArray, self](const CookieListIter& aIter) { + self->PrepareCookieRemoval(*aIter.Cookie(), paramsArray); + self->RemoveCookieFromListInternal(aIter); + }, + [paramsArray, self]() { + if (paramsArray) { + self->DeleteFromDB(paramsArray); + } + }); +} + +void CookiePersistentStorage::CollectCookieJarSizeData() { + COOKIE_LOGSTRING(LogLevel::Debug, + ("CookiePersistentStorage::CollectCookieJarSizeData")); + + uint32_t sumPartitioned = 0; + uint32_t sumUnpartitioned = 0; + for (const auto& cookieEntry : mHostTable) { + if (cookieEntry.IsPartitioned()) { + uint16_t cePartitioned = cookieEntry.GetCookies().Length(); + sumPartitioned += cePartitioned; + mozilla::glean::networking::cookie_count_part_by_key.AccumulateSamples( + {cePartitioned}); + } else { + uint16_t ceUnpartitioned = cookieEntry.GetCookies().Length(); + sumUnpartitioned += ceUnpartitioned; + mozilla::glean::networking::cookie_count_unpart_by_key.AccumulateSamples( + {ceUnpartitioned}); + } + } + + mozilla::glean::networking::cookie_count_total.AccumulateSamples( + {mCookieCount}); + mozilla::glean::networking::cookie_count_partitioned.AccumulateSamples( + {sumPartitioned}); + mozilla::glean::networking::cookie_count_unpartitioned.AccumulateSamples( + {sumUnpartitioned}); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/cookie/CookiePersistentStorage.h b/netwerk/cookie/CookiePersistentStorage.h new file mode 100644 index 0000000000..e973c74ab6 --- /dev/null +++ b/netwerk/cookie/CookiePersistentStorage.h @@ -0,0 +1,160 @@ +/* -*- 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/. */ + +#ifndef mozilla_net_CookiePersistentStorage_h +#define mozilla_net_CookiePersistentStorage_h + +#include "CookieStorage.h" + +#include "mozilla/Atomics.h" +#include "mozilla/Monitor.h" +#include "mozilla/net/NeckoChannelParams.h" +#include "mozIStorageBindingParamsArray.h" +#include "mozIStorageCompletionCallback.h" +#include "mozIStorageStatement.h" +#include "mozIStorageStatementCallback.h" + +class mozIStorageAsyncStatement; +class mozIStorageService; +class nsICookieTransactionCallback; +class nsIEffectiveTLDService; + +namespace mozilla { +namespace net { + +class CookiePersistentStorage final : public CookieStorage { + public: + // Result codes for TryInitDB() and Read(). + enum OpenDBResult { RESULT_OK, RESULT_RETRY, RESULT_FAILURE }; + + static already_AddRefed<CookiePersistentStorage> Create(); + + void HandleCorruptDB(); + + void RemoveCookiesWithOriginAttributes( + const OriginAttributesPattern& aPattern, + const nsACString& aBaseDomain) override; + + void RemoveCookiesFromExactHost( + const nsACString& aHost, const nsACString& aBaseDomain, + const OriginAttributesPattern& aPattern) override; + + void StaleCookies(const nsTArray<Cookie*>& aCookieList, + int64_t aCurrentTimeInUsec) override; + + void Close() override; + + void EnsureInitialized() override; + + void CleanupCachedStatements(); + void CleanupDBConnection(); + + void Activate(); + + void RebuildCorruptDB(); + void HandleDBClosed(); + + nsresult RunInTransaction(nsICookieTransactionCallback* aCallback) override; + + // State of the database connection. + enum CorruptFlag { + OK, // normal + CLOSING_FOR_REBUILD, // corruption detected, connection closing + REBUILDING // close complete, rebuilding database from memory + }; + + CorruptFlag GetCorruptFlag() const { return mCorruptFlag; } + + void SetCorruptFlag(CorruptFlag aFlag) { mCorruptFlag = aFlag; } + + protected: + const char* NotificationTopic() const override { return "cookie-changed"; } + + void NotifyChangedInternal(nsICookieNotification* aNotification, + bool aOldCookieIsSession) override; + + void RemoveAllInternal() override; + + void RemoveCookieFromDB(const Cookie& aCookie) override; + + void StoreCookie(const nsACString& aBaseDomain, + const OriginAttributes& aOriginAttributes, + Cookie* aCookie) override; + + private: + CookiePersistentStorage(); + + static void UpdateCookieInList(Cookie* aCookie, int64_t aLastAccessed, + mozIStorageBindingParamsArray* aParamsArray); + + void PrepareCookieRemoval(const Cookie& aCookie, + mozIStorageBindingParamsArray* aParamsArray); + + void InitDBConn(); + nsresult InitDBConnInternal(); + + OpenDBResult TryInitDB(bool aRecreateDB); + OpenDBResult Read(); + + nsresult CreateTableWorker(const char* aName); + nsresult CreateTable(); + nsresult CreateTableForSchemaVersion6(); + nsresult CreateTableForSchemaVersion5(); + + static UniquePtr<CookieStruct> GetCookieFromRow(mozIStorageStatement* aRow); + + already_AddRefed<nsIArray> PurgeCookies(int64_t aCurrentTimeInUsec, + uint16_t aMaxNumberOfCookies, + int64_t aCookiePurgeAge) override; + + void CollectCookieJarSizeData() override; + + void DeleteFromDB(mozIStorageBindingParamsArray* aParamsArray); + + void MaybeStoreCookiesToDB(mozIStorageBindingParamsArray* aParamsArray); + + nsCOMPtr<nsIThread> mThread; + nsCOMPtr<mozIStorageService> mStorageService; + nsCOMPtr<nsIEffectiveTLDService> mTLDService; + + // encapsulates a (key, Cookie) tuple for temporary storage purposes. + struct CookieDomainTuple { + CookieKey key; + OriginAttributes originAttributes; + UniquePtr<CookieStruct> cookie; + }; + + // thread + TimeStamp mEndInitDBConn; + nsTArray<CookieDomainTuple> mReadArray; + + Monitor mMonitor MOZ_UNANNOTATED; + + Atomic<bool> mInitialized; + Atomic<bool> mInitializedDBConn; + + nsCOMPtr<nsIFile> mCookieFile; + nsCOMPtr<mozIStorageConnection> mDBConn; + nsCOMPtr<mozIStorageAsyncStatement> mStmtInsert; + nsCOMPtr<mozIStorageAsyncStatement> mStmtDelete; + nsCOMPtr<mozIStorageAsyncStatement> mStmtUpdate; + + CorruptFlag mCorruptFlag; + + // Various parts representing asynchronous read state. These are useful + // while the background read is taking place. + nsCOMPtr<mozIStorageConnection> mSyncConn; + + // DB completion handlers. + nsCOMPtr<mozIStorageStatementCallback> mInsertListener; + nsCOMPtr<mozIStorageStatementCallback> mUpdateListener; + nsCOMPtr<mozIStorageStatementCallback> mRemoveListener; + nsCOMPtr<mozIStorageCompletionCallback> mCloseListener; +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_CookiePersistentStorage_h diff --git a/netwerk/cookie/CookiePrivateStorage.cpp b/netwerk/cookie/CookiePrivateStorage.cpp new file mode 100644 index 0000000000..e6d8fd67d2 --- /dev/null +++ b/netwerk/cookie/CookiePrivateStorage.cpp @@ -0,0 +1,44 @@ +/* -*- 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 "CookiePrivateStorage.h" +#include "Cookie.h" + +namespace mozilla { +namespace net { + +// static +already_AddRefed<CookiePrivateStorage> CookiePrivateStorage::Create() { + RefPtr<CookiePrivateStorage> storage = new CookiePrivateStorage(); + storage->Init(); + + return storage.forget(); +} + +void CookiePrivateStorage::StaleCookies(const nsTArray<Cookie*>& aCookieList, + int64_t aCurrentTimeInUsec) { + int32_t count = aCookieList.Length(); + for (int32_t i = 0; i < count; ++i) { + Cookie* cookie = aCookieList.ElementAt(i); + + if (cookie->IsStale()) { + cookie->SetLastAccessed(aCurrentTimeInUsec); + } + } +} + +// purges expired and old cookies in a batch operation. +already_AddRefed<nsIArray> CookiePrivateStorage::PurgeCookies( + int64_t aCurrentTimeInUsec, uint16_t aMaxNumberOfCookies, + int64_t aCookiePurgeAge) { + RefPtr<CookiePrivateStorage> self = this; + return PurgeCookiesWithCallbacks( + aCurrentTimeInUsec, aMaxNumberOfCookies, aCookiePurgeAge, + [self](const CookieListIter& iter) { self->RemoveCookieFromList(iter); }, + nullptr); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/cookie/CookiePrivateStorage.h b/netwerk/cookie/CookiePrivateStorage.h new file mode 100644 index 0000000000..812a7e6764 --- /dev/null +++ b/netwerk/cookie/CookiePrivateStorage.h @@ -0,0 +1,61 @@ +/* -*- 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/. */ + +#ifndef mozilla_net_CookiePrivateStorage_h +#define mozilla_net_CookiePrivateStorage_h + +#include "CookieStorage.h" + +class nsICookieTransactionCallback; + +namespace mozilla { +namespace net { + +class CookiePrivateStorage final : public CookieStorage { + public: + static already_AddRefed<CookiePrivateStorage> Create(); + + void StaleCookies(const nsTArray<Cookie*>& aCookieList, + int64_t aCurrentTimeInUsec) override; + + void Close() override{}; + + void EnsureInitialized() override{}; + + nsresult RunInTransaction(nsICookieTransactionCallback* aCallback) override { + // It might make sense for this to be a no-op, or to return + // `NS_ERROR_NOT_AVAILABLE`, or to evalute `aCallback` (in case it has + // side-effects), but for now, just crash. + MOZ_CRASH("RunInTransaction is not supported for private storage"); + }; + + protected: + const char* NotificationTopic() const override { + return "private-cookie-changed"; + } + + void NotifyChangedInternal(nsICookieNotification* aNotification, + bool aOldCookieIsSession) override {} + + void RemoveAllInternal() override {} + + void RemoveCookieFromDB(const Cookie& aCookie) override {} + + already_AddRefed<nsIArray> PurgeCookies(int64_t aCurrentTimeInUsec, + uint16_t aMaxNumberOfCookies, + int64_t aCookiePurgeAge) override; + + void StoreCookie(const nsACString& aBaseDomain, + const OriginAttributes& aOriginAttributes, + Cookie* aCookie) override {} + + private: + void CollectCookieJarSizeData() override{}; +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_CookiePrivateStorage_h diff --git a/netwerk/cookie/CookieService.cpp b/netwerk/cookie/CookieService.cpp new file mode 100644 index 0000000000..d306203c2f --- /dev/null +++ b/netwerk/cookie/CookieService.cpp @@ -0,0 +1,2586 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et tw=80 : */ +/* 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 "CookieCommons.h" +#include "CookieLogging.h" +#include "mozilla/AppShutdown.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/ContentBlockingNotifier.h" +#include "mozilla/RefPtr.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/nsMixedContentBlocker.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/net/CookieJarSettings.h" +#include "mozilla/net/CookiePersistentStorage.h" +#include "mozilla/net/CookiePrivateStorage.h" +#include "mozilla/net/CookieService.h" +#include "mozilla/net/CookieServiceChild.h" +#include "mozilla/net/HttpBaseChannel.h" +#include "mozilla/net/NeckoCommon.h" +#include "mozilla/StaticPrefs_network.h" +#include "mozilla/StoragePrincipalHelper.h" +#include "mozilla/Telemetry.h" +#include "mozIThirdPartyUtil.h" +#include "nsICookiePermission.h" +#include "nsIConsoleReportCollector.h" +#include "nsIEffectiveTLDService.h" +#include "nsIIDNService.h" +#include "nsIScriptError.h" +#include "nsIURL.h" +#include "nsIURI.h" +#include "nsIWebProgressListener.h" +#include "nsNetUtil.h" +#include "prprf.h" +#include "ThirdPartyUtil.h" + +using namespace mozilla::dom; + +namespace { + +uint32_t MakeCookieBehavior(uint32_t aCookieBehavior) { + bool isFirstPartyIsolated = OriginAttributes::IsFirstPartyEnabled(); + + if (isFirstPartyIsolated && + aCookieBehavior == + nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN) { + return nsICookieService::BEHAVIOR_REJECT_TRACKER; + } + return aCookieBehavior; +} + +/* + Enables sanitizeOnShutdown cleaning prefs and disables the + network.cookie.lifetimePolicy +*/ +void MigrateCookieLifetimePrefs() { + // Former network.cookie.lifetimePolicy values ACCEPT_SESSION/ACCEPT_NORMALLY + // are not available anymore 2 = ACCEPT_SESSION + if (mozilla::Preferences::GetInt("network.cookie.lifetimePolicy") != 2) { + return; + } + if (!mozilla::Preferences::GetBool("privacy.sanitize.sanitizeOnShutdown")) { + mozilla::Preferences::SetBool("privacy.sanitize.sanitizeOnShutdown", true); + // To avoid clearing categories that the user did not intend to clear + mozilla::Preferences::SetBool("privacy.clearOnShutdown.history", false); + mozilla::Preferences::SetBool("privacy.clearOnShutdown.formdata", false); + mozilla::Preferences::SetBool("privacy.clearOnShutdown.downloads", false); + mozilla::Preferences::SetBool("privacy.clearOnShutdown.sessions", false); + mozilla::Preferences::SetBool("privacy.clearOnShutdown.siteSettings", + false); + } + mozilla::Preferences::SetBool("privacy.clearOnShutdown.cookies", true); + mozilla::Preferences::SetBool("privacy.clearOnShutdown.cache", true); + mozilla::Preferences::SetBool("privacy.clearOnShutdown.offlineApps", true); + mozilla::Preferences::ClearUser("network.cookie.lifetimePolicy"); +} + +} // anonymous namespace + +// static +uint32_t nsICookieManager::GetCookieBehavior(bool aIsPrivate) { + if (aIsPrivate) { + // To sync the cookieBehavior pref between regular and private mode in ETP + // custom mode, we will return the regular cookieBehavior pref for private + // mode when + // 1. The regular cookieBehavior pref has a non-default value. + // 2. And the private cookieBehavior pref has a default value. + // Also, this can cover the migration case where the user has a non-default + // cookieBehavior before the private cookieBehavior was introduced. The + // getter here will directly return the regular cookieBehavior, so that the + // cookieBehavior for private mode is consistent. + if (mozilla::Preferences::HasUserValue( + "network.cookie.cookieBehavior.pbmode")) { + return MakeCookieBehavior( + mozilla::StaticPrefs::network_cookie_cookieBehavior_pbmode()); + } + + if (mozilla::Preferences::HasUserValue("network.cookie.cookieBehavior")) { + return MakeCookieBehavior( + mozilla::StaticPrefs::network_cookie_cookieBehavior()); + } + + return MakeCookieBehavior( + mozilla::StaticPrefs::network_cookie_cookieBehavior_pbmode()); + } + return MakeCookieBehavior( + mozilla::StaticPrefs::network_cookie_cookieBehavior()); +} + +namespace mozilla { +namespace net { + +/****************************************************************************** + * CookieService impl: + * useful types & constants + ******************************************************************************/ + +static StaticRefPtr<CookieService> gCookieService; + +constexpr auto CONSOLE_CHIPS_CATEGORY = "cookiesCHIPS"_ns; +constexpr auto CONSOLE_SAMESITE_CATEGORY = "cookieSameSite"_ns; +constexpr auto CONSOLE_OVERSIZE_CATEGORY = "cookiesOversize"_ns; +constexpr auto CONSOLE_REJECTION_CATEGORY = "cookiesRejection"_ns; +constexpr auto SAMESITE_MDN_URL = + "https://developer.mozilla.org/docs/Web/HTTP/Headers/Set-Cookie/" + u"SameSite"_ns; + +namespace { + +void ComposeCookieString(nsTArray<Cookie*>& aCookieList, + nsACString& aCookieString) { + for (Cookie* cookie : aCookieList) { + // check if we have anything to write + if (!cookie->Name().IsEmpty() || !cookie->Value().IsEmpty()) { + // if we've already added a cookie to the return list, append a "; " so + // that subsequent cookies are delimited in the final list. + if (!aCookieString.IsEmpty()) { + aCookieString.AppendLiteral("; "); + } + + if (!cookie->Name().IsEmpty()) { + // we have a name and value - write both + aCookieString += cookie->Name() + "="_ns + cookie->Value(); + } else { + // just write value + aCookieString += cookie->Value(); + } + } + } +} + +// Return false if the cookie should be ignored for the current channel. +bool ProcessSameSiteCookieForForeignRequest(nsIChannel* aChannel, + Cookie* aCookie, + bool aIsSafeTopLevelNav, + bool aHadCrossSiteRedirects, + bool aLaxByDefault) { + // If it's a cross-site request and the cookie is same site only (strict) + // don't send it. + if (aCookie->SameSite() == nsICookie::SAMESITE_STRICT) { + return false; + } + + // Explicit SameSite=None cookies are always processed. When laxByDefault + // is OFF then so are default cookies. + if (aCookie->SameSite() == nsICookie::SAMESITE_NONE || + (!aLaxByDefault && aCookie->IsDefaultSameSite())) { + return true; + } + + // Lax-by-default cookies are processed even with an intermediate + // cross-site redirect (they are treated like aIsSameSiteForeign = false). + if (aLaxByDefault && aCookie->IsDefaultSameSite() && aHadCrossSiteRedirects && + StaticPrefs:: + network_cookie_sameSite_laxByDefault_allowBoomerangRedirect()) { + return true; + } + + int64_t currentTimeInUsec = PR_Now(); + + // 2 minutes of tolerance for 'SameSite=Lax by default' for cookies set + // without a SameSite value when used for unsafe http methods. + if (aLaxByDefault && aCookie->IsDefaultSameSite() && + StaticPrefs::network_cookie_sameSite_laxPlusPOST_timeout() > 0 && + currentTimeInUsec - aCookie->CreationTime() <= + (StaticPrefs::network_cookie_sameSite_laxPlusPOST_timeout() * + PR_USEC_PER_SEC) && + !NS_IsSafeMethodNav(aChannel)) { + return true; + } + + MOZ_ASSERT((aLaxByDefault && aCookie->IsDefaultSameSite()) || + aCookie->SameSite() == nsICookie::SAMESITE_LAX); + // We only have SameSite=Lax or lax-by-default cookies at this point. These + // are processed only if it's a top-level navigation + return aIsSafeTopLevelNav; +} + +} // namespace + +/****************************************************************************** + * CookieService impl: + * singleton instance ctor/dtor methods + ******************************************************************************/ + +already_AddRefed<nsICookieService> CookieService::GetXPCOMSingleton() { + if (IsNeckoChild()) { + return CookieServiceChild::GetSingleton(); + } + + return GetSingleton(); +} + +already_AddRefed<CookieService> CookieService::GetSingleton() { + NS_ASSERTION(!IsNeckoChild(), "not a parent process"); + + if (gCookieService) { + return do_AddRef(gCookieService); + } + + // Create a new singleton CookieService. + // We AddRef only once since XPCOM has rules about the ordering of module + // teardowns - by the time our module destructor is called, it's too late to + // Release our members (e.g. nsIObserverService and nsIPrefBranch), since GC + // cycles have already been completed and would result in serious leaks. + // See bug 209571. + // TODO: Verify what is the earliest point in time during shutdown where + // we can deny the creation of the CookieService as a whole. + gCookieService = new CookieService(); + if (gCookieService) { + if (NS_SUCCEEDED(gCookieService->Init())) { + ClearOnShutdown(&gCookieService); + } else { + gCookieService = nullptr; + } + } + + return do_AddRef(gCookieService); +} + +/****************************************************************************** + * CookieService impl: + * public methods + ******************************************************************************/ + +NS_IMPL_ISUPPORTS(CookieService, nsICookieService, nsICookieManager, + nsIObserver, nsISupportsWeakReference, nsIMemoryReporter) + +CookieService::CookieService() = default; + +nsresult CookieService::Init() { + nsresult rv; + mTLDService = do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + mIDNService = do_GetService(NS_IDNSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + mThirdPartyUtil = do_GetService(THIRDPARTYUTIL_CONTRACTID); + NS_ENSURE_SUCCESS(rv, rv); + + // Init our default, and possibly private CookieStorages. + InitCookieStorages(); + + // Migrate network.cookie.lifetimePolicy pref to sanitizeOnShutdown prefs + MigrateCookieLifetimePrefs(); + + RegisterWeakMemoryReporter(this); + + nsCOMPtr<nsIObserverService> os = services::GetObserverService(); + NS_ENSURE_STATE(os); + os->AddObserver(this, "profile-before-change", true); + os->AddObserver(this, "profile-do-change", true); + os->AddObserver(this, "last-pb-context-exited", true); + + return NS_OK; +} + +void CookieService::InitCookieStorages() { + NS_ASSERTION(!mPersistentStorage, "already have a default CookieStorage"); + NS_ASSERTION(!mPrivateStorage, "already have a private CookieStorage"); + + // Create two new CookieStorages. If we are in or beyond our observed + // shutdown phase, just be non-persistent. + if (MOZ_UNLIKELY(StaticPrefs::network_cookie_noPersistentStorage() || + AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdown))) { + mPersistentStorage = CookiePrivateStorage::Create(); + } else { + mPersistentStorage = CookiePersistentStorage::Create(); + } + + mPrivateStorage = CookiePrivateStorage::Create(); +} + +void CookieService::CloseCookieStorages() { + // return if we already closed + if (!mPersistentStorage) { + return; + } + + // Let's nullify both storages before calling Close(). + RefPtr<CookieStorage> privateStorage; + privateStorage.swap(mPrivateStorage); + + RefPtr<CookieStorage> persistentStorage; + persistentStorage.swap(mPersistentStorage); + + privateStorage->Close(); + persistentStorage->Close(); +} + +CookieService::~CookieService() { + CloseCookieStorages(); + + UnregisterWeakMemoryReporter(this); + + gCookieService = nullptr; +} + +NS_IMETHODIMP +CookieService::Observe(nsISupports* /*aSubject*/, const char* aTopic, + const char16_t* /*aData*/) { + // check the topic + if (!strcmp(aTopic, "profile-before-change")) { + // The profile is about to change, + // or is going away because the application is shutting down. + + // Close the default DB connection and null out our CookieStorages before + // changing. + CloseCookieStorages(); + + } else if (!strcmp(aTopic, "profile-do-change")) { + NS_ASSERTION(!mPersistentStorage, "shouldn't have a default CookieStorage"); + NS_ASSERTION(!mPrivateStorage, "shouldn't have a private CookieStorage"); + + // the profile has already changed; init the db from the new location. + // if we are in the private browsing state, however, we do not want to read + // data into it - we should instead put it into the default state, so it's + // ready for us if and when we switch back to it. + InitCookieStorages(); + + } else if (!strcmp(aTopic, "last-pb-context-exited")) { + // Flush all the cookies stored by private browsing contexts + OriginAttributesPattern pattern; + pattern.mPrivateBrowsingId.Construct(1); + RemoveCookiesWithOriginAttributes(pattern, ""_ns); + mPrivateStorage = CookiePrivateStorage::Create(); + } + + return NS_OK; +} + +NS_IMETHODIMP +CookieService::GetCookieBehavior(bool aIsPrivate, uint32_t* aCookieBehavior) { + NS_ENSURE_ARG_POINTER(aCookieBehavior); + *aCookieBehavior = nsICookieManager::GetCookieBehavior(aIsPrivate); + return NS_OK; +} + +NS_IMETHODIMP +CookieService::GetCookieStringFromDocument(Document* aDocument, + nsACString& aCookie) { + NS_ENSURE_ARG(aDocument); + + nsresult rv; + + aCookie.Truncate(); + + if (!IsInitialized()) { + return NS_OK; + } + + nsCOMPtr<nsIPrincipal> principal = aDocument->EffectiveCookiePrincipal(); + + if (!CookieCommons::IsSchemeSupported(principal)) { + return NS_OK; + } + + CookieStorage* storage = PickStorage(principal->OriginAttributesRef()); + + nsAutoCString baseDomain; + rv = CookieCommons::GetBaseDomain(principal, baseDomain); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_OK; + } + + nsAutoCString hostFromURI; + rv = nsContentUtils::GetHostOrIPv6WithBrackets(principal, hostFromURI); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_OK; + } + + nsAutoCString pathFromURI; + rv = principal->GetFilePath(pathFromURI); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_OK; + } + + int64_t currentTimeInUsec = PR_Now(); + int64_t currentTime = currentTimeInUsec / PR_USEC_PER_SEC; + + const nsTArray<RefPtr<Cookie>>* cookies = + storage->GetCookiesFromHost(baseDomain, principal->OriginAttributesRef()); + if (!cookies) { + return NS_OK; + } + + // check if the nsIPrincipal is using an https secure protocol. + // if it isn't, then we can't send a secure cookie over the connection. + bool potentiallyTrustworthy = principal->GetIsOriginPotentiallyTrustworthy(); + + bool thirdParty = true; + nsPIDOMWindowInner* innerWindow = aDocument->GetInnerWindow(); + // in gtests we don't have a window, let's consider those requests as 3rd + // party. + if (innerWindow) { + ThirdPartyUtil* thirdPartyUtil = ThirdPartyUtil::GetInstance(); + + if (thirdPartyUtil) { + Unused << thirdPartyUtil->IsThirdPartyWindow( + innerWindow->GetOuterWindow(), nullptr, &thirdParty); + } + } + + bool stale = false; + nsTArray<Cookie*> cookieList; + + // iterate the cookies! + for (Cookie* cookie : *cookies) { + // check the host, since the base domain lookup is conservative. + if (!CookieCommons::DomainMatches(cookie, hostFromURI)) { + continue; + } + + // if the cookie is httpOnly and it's not going directly to the HTTP + // connection, don't send it + if (cookie->IsHttpOnly()) { + continue; + } + + if (thirdParty && !CookieCommons::ShouldIncludeCrossSiteCookieForDocument( + cookie, aDocument)) { + continue; + } + + // if the cookie is secure and the host scheme isn't, we can't send it + if (cookie->IsSecure() && !potentiallyTrustworthy) { + continue; + } + + // if the nsIURI path doesn't match the cookie path, don't send it back + if (!CookieCommons::PathMatches(cookie, pathFromURI)) { + continue; + } + + // check if the cookie has expired + if (cookie->Expiry() <= currentTime) { + continue; + } + + // all checks passed - add to list and check if lastAccessed stamp needs + // updating + cookieList.AppendElement(cookie); + if (cookie->IsStale()) { + stale = true; + } + } + + if (cookieList.IsEmpty()) { + return NS_OK; + } + + // update lastAccessed timestamps. we only do this if the timestamp is stale + // by a certain amount, to avoid thrashing the db during pageload. + if (stale) { + storage->StaleCookies(cookieList, currentTimeInUsec); + } + + // return cookies in order of path length; longest to shortest. + // this is required per RFC2109. if cookies match in length, + // then sort by creation time (see bug 236772). + cookieList.Sort(CompareCookiesForSending()); + ComposeCookieString(cookieList, aCookie); + + return NS_OK; +} + +NS_IMETHODIMP +CookieService::GetCookieStringFromHttp(nsIURI* aHostURI, nsIChannel* aChannel, + nsACString& aCookieString) { + NS_ENSURE_ARG(aHostURI); + NS_ENSURE_ARG(aChannel); + + aCookieString.Truncate(); + + if (!CookieCommons::IsSchemeSupported(aHostURI)) { + return NS_OK; + } + + uint32_t rejectedReason = 0; + ThirdPartyAnalysisResult result = mThirdPartyUtil->AnalyzeChannel( + aChannel, false, aHostURI, nullptr, &rejectedReason); + + OriginAttributes attrs; + StoragePrincipalHelper::GetOriginAttributes( + aChannel, attrs, StoragePrincipalHelper::eStorageAccessPrincipal); + + bool isSafeTopLevelNav = CookieCommons::IsSafeTopLevelNav(aChannel); + bool hadCrossSiteRedirects = false; + bool isSameSiteForeign = CookieCommons::IsSameSiteForeign( + aChannel, aHostURI, &hadCrossSiteRedirects); + + AutoTArray<Cookie*, 8> foundCookieList; + GetCookiesForURI( + aHostURI, aChannel, result.contains(ThirdPartyAnalysis::IsForeign), + result.contains(ThirdPartyAnalysis::IsThirdPartyTrackingResource), + result.contains(ThirdPartyAnalysis::IsThirdPartySocialTrackingResource), + result.contains(ThirdPartyAnalysis::IsStorageAccessPermissionGranted), + rejectedReason, isSafeTopLevelNav, isSameSiteForeign, + hadCrossSiteRedirects, true, false, attrs, foundCookieList); + + ComposeCookieString(foundCookieList, aCookieString); + + if (!aCookieString.IsEmpty()) { + COOKIE_LOGSUCCESS(GET_COOKIE, aHostURI, aCookieString, nullptr, false); + } + return NS_OK; +} + +NS_IMETHODIMP +CookieService::SetCookieStringFromDocument(Document* aDocument, + const nsACString& aCookieString) { + NS_ENSURE_ARG(aDocument); + + if (!IsInitialized()) { + return NS_OK; + } + + nsCOMPtr<nsIURI> documentURI; + nsAutoCString baseDomain; + OriginAttributes attrs; + + int64_t currentTimeInUsec = PR_Now(); + + // This function is executed in this context, I don't need to keep objects + // alive. + auto hasExistingCookiesLambda = [&](const nsACString& aBaseDomain, + const OriginAttributes& aAttrs) { + CookieStorage* storage = PickStorage(aAttrs); + return !!storage->CountCookiesFromHost(aBaseDomain, + aAttrs.mPrivateBrowsingId); + }; + + RefPtr<Cookie> cookie = CookieCommons::CreateCookieFromDocument( + aDocument, aCookieString, currentTimeInUsec, mTLDService, mThirdPartyUtil, + hasExistingCookiesLambda, getter_AddRefs(documentURI), baseDomain, attrs); + if (!cookie) { + return NS_OK; + } + + bool thirdParty = true; + nsPIDOMWindowInner* innerWindow = aDocument->GetInnerWindow(); + // in gtests we don't have a window, let's consider those requests as 3rd + // party. + if (innerWindow) { + ThirdPartyUtil* thirdPartyUtil = ThirdPartyUtil::GetInstance(); + + if (thirdPartyUtil) { + Unused << thirdPartyUtil->IsThirdPartyWindow( + innerWindow->GetOuterWindow(), nullptr, &thirdParty); + } + } + + if (thirdParty && !CookieCommons::ShouldIncludeCrossSiteCookieForDocument( + cookie, aDocument)) { + return NS_OK; + } + + nsCOMPtr<nsIConsoleReportCollector> crc = + do_QueryInterface(aDocument->GetChannel()); + + // add the cookie to the list. AddCookie() takes care of logging. + PickStorage(attrs)->AddCookie(crc, baseDomain, attrs, cookie, + currentTimeInUsec, documentURI, aCookieString, + false, aDocument->GetBrowsingContext()); + return NS_OK; +} + +NS_IMETHODIMP +CookieService::SetCookieStringFromHttp(nsIURI* aHostURI, + const nsACString& aCookieHeader, + nsIChannel* aChannel) { + NS_ENSURE_ARG(aHostURI); + NS_ENSURE_ARG(aChannel); + + if (!IsInitialized()) { + return NS_OK; + } + + if (!CookieCommons::IsSchemeSupported(aHostURI)) { + return NS_OK; + } + + uint32_t rejectedReason = 0; + ThirdPartyAnalysisResult result = mThirdPartyUtil->AnalyzeChannel( + aChannel, false, aHostURI, nullptr, &rejectedReason); + + OriginAttributes attrs; + StoragePrincipalHelper::GetOriginAttributes( + aChannel, attrs, StoragePrincipalHelper::eStorageAccessPrincipal); + + // get the base domain for the host URI. + // e.g. for "www.bbc.co.uk", this would be "bbc.co.uk". + // file:// URI's (i.e. with an empty host) are allowed, but any other + // scheme must have a non-empty host. A trailing dot in the host + // is acceptable. + bool requireHostMatch; + nsAutoCString baseDomain; + nsresult rv = CookieCommons::GetBaseDomain(mTLDService, aHostURI, baseDomain, + requireHostMatch); + if (NS_FAILED(rv)) { + COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader, + "couldn't get base domain from URI"); + return NS_OK; + } + + nsCOMPtr<nsICookieJarSettings> cookieJarSettings = + CookieCommons::GetCookieJarSettings(aChannel); + + nsAutoCString hostFromURI; + nsContentUtils::GetHostOrIPv6WithBrackets(aHostURI, hostFromURI); + + nsAutoCString baseDomainFromURI; + rv = CookieCommons::GetBaseDomainFromHost(mTLDService, hostFromURI, + baseDomainFromURI); + NS_ENSURE_SUCCESS(rv, NS_OK); + + CookieStorage* storage = PickStorage(attrs); + + // check default prefs + uint32_t priorCookieCount = storage->CountCookiesFromHost( + baseDomainFromURI, attrs.mPrivateBrowsingId); + + nsCOMPtr<nsIConsoleReportCollector> crc = do_QueryInterface(aChannel); + + CookieStatus cookieStatus = CheckPrefs( + crc, cookieJarSettings, aHostURI, + result.contains(ThirdPartyAnalysis::IsForeign), + result.contains(ThirdPartyAnalysis::IsThirdPartyTrackingResource), + result.contains(ThirdPartyAnalysis::IsThirdPartySocialTrackingResource), + result.contains(ThirdPartyAnalysis::IsStorageAccessPermissionGranted), + aCookieHeader, priorCookieCount, attrs, &rejectedReason); + + MOZ_ASSERT_IF(rejectedReason, cookieStatus == STATUS_REJECTED); + + // fire a notification if third party or if cookie was rejected + // (but not if there was an error) + switch (cookieStatus) { + case STATUS_REJECTED: + CookieCommons::NotifyRejected(aHostURI, aChannel, rejectedReason, + OPERATION_WRITE); + return NS_OK; // Stop here + case STATUS_REJECTED_WITH_ERROR: + CookieCommons::NotifyRejected(aHostURI, aChannel, rejectedReason, + OPERATION_WRITE); + return NS_OK; + case STATUS_ACCEPTED: // Fallthrough + case STATUS_ACCEPT_SESSION: + NotifyAccepted(aChannel); + break; + default: + break; + } + + bool addonAllowsLoad = false; + nsCOMPtr<nsIURI> channelURI; + NS_GetFinalChannelURI(aChannel, getter_AddRefs(channelURI)); + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + addonAllowsLoad = BasePrincipal::Cast(loadInfo->TriggeringPrincipal()) + ->AddonAllowsLoad(channelURI); + + bool isForeignAndNotAddon = false; + if (!addonAllowsLoad) { + mThirdPartyUtil->IsThirdPartyChannel(aChannel, aHostURI, + &isForeignAndNotAddon); + } + + bool mustBePartitioned = + isForeignAndNotAddon && + cookieJarSettings->GetCookieBehavior() == + nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN && + !result.contains(ThirdPartyAnalysis::IsStorageAccessPermissionGranted); + + nsCString cookieHeader(aCookieHeader); + + bool moreCookieToRead = true; + + // process each cookie in the header + while (moreCookieToRead) { + CookieStruct cookieData; + bool canSetCookie = false; + + moreCookieToRead = + CanSetCookie(aHostURI, baseDomain, cookieData, requireHostMatch, + cookieStatus, cookieHeader, true, isForeignAndNotAddon, + mustBePartitioned, crc, canSetCookie); + + if (!canSetCookie) { + continue; + } + + // check permissions from site permission list. + if (!CookieCommons::CheckCookiePermission(aChannel, cookieData)) { + COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader, + "cookie rejected by permission manager"); + CookieCommons::NotifyRejected( + aHostURI, aChannel, + nsIWebProgressListener::STATE_COOKIES_BLOCKED_BY_PERMISSION, + OPERATION_WRITE); + CookieLogging::LogMessageToConsole( + crc, aHostURI, nsIScriptError::warningFlag, + CONSOLE_REJECTION_CATEGORY, "CookieRejectedByPermissionManager"_ns, + AutoTArray<nsString, 1>{ + NS_ConvertUTF8toUTF16(cookieData.name()), + }); + continue; + } + + // create a new Cookie + RefPtr<Cookie> cookie = Cookie::Create(cookieData, attrs); + MOZ_ASSERT(cookie); + + int64_t currentTimeInUsec = PR_Now(); + cookie->SetLastAccessed(currentTimeInUsec); + cookie->SetCreationTime( + Cookie::GenerateUniqueCreationTime(currentTimeInUsec)); + + RefPtr<BrowsingContext> bc = loadInfo->GetBrowsingContext(); + + // add the cookie to the list. AddCookie() takes care of logging. + storage->AddCookie(crc, baseDomain, attrs, cookie, currentTimeInUsec, + aHostURI, aCookieHeader, true, bc); + } + + return NS_OK; +} + +void CookieService::NotifyAccepted(nsIChannel* aChannel) { + ContentBlockingNotifier::OnDecision( + aChannel, ContentBlockingNotifier::BlockingDecision::eAllow, 0); +} + +/****************************************************************************** + * CookieService: + * public transaction helper impl + ******************************************************************************/ + +NS_IMETHODIMP +CookieService::RunInTransaction(nsICookieTransactionCallback* aCallback) { + NS_ENSURE_ARG(aCallback); + + if (!IsInitialized()) { + return NS_ERROR_NOT_AVAILABLE; + } + + mPersistentStorage->EnsureInitialized(); + return mPersistentStorage->RunInTransaction(aCallback); +} + +/****************************************************************************** + * nsICookieManager impl: + * nsICookieManager + ******************************************************************************/ + +NS_IMETHODIMP +CookieService::RemoveAll() { + if (!IsInitialized()) { + return NS_ERROR_NOT_AVAILABLE; + } + + mPersistentStorage->EnsureInitialized(); + mPersistentStorage->RemoveAll(); + return NS_OK; +} + +NS_IMETHODIMP +CookieService::GetCookies(nsTArray<RefPtr<nsICookie>>& aCookies) { + if (!IsInitialized()) { + return NS_ERROR_NOT_AVAILABLE; + } + + mPersistentStorage->EnsureInitialized(); + + // We expose only non-private cookies. + mPersistentStorage->GetCookies(aCookies); + + return NS_OK; +} + +NS_IMETHODIMP +CookieService::GetSessionCookies(nsTArray<RefPtr<nsICookie>>& aCookies) { + if (!IsInitialized()) { + return NS_ERROR_NOT_AVAILABLE; + } + + mPersistentStorage->EnsureInitialized(); + + // We expose only non-private cookies. + mPersistentStorage->GetSessionCookies(aCookies); + + return NS_OK; +} + +NS_IMETHODIMP +CookieService::Add(const nsACString& aHost, const nsACString& aPath, + const nsACString& aName, const nsACString& aValue, + bool aIsSecure, bool aIsHttpOnly, bool aIsSession, + int64_t aExpiry, JS::Handle<JS::Value> aOriginAttributes, + int32_t aSameSite, nsICookie::schemeType aSchemeMap, + JSContext* aCx) { + OriginAttributes attrs; + + if (!aOriginAttributes.isObject() || !attrs.Init(aCx, aOriginAttributes)) { + return NS_ERROR_INVALID_ARG; + } + + return AddNative(aHost, aPath, aName, aValue, aIsSecure, aIsHttpOnly, + aIsSession, aExpiry, &attrs, aSameSite, aSchemeMap); +} + +NS_IMETHODIMP_(nsresult) +CookieService::AddNative(const nsACString& aHost, const nsACString& aPath, + const nsACString& aName, const nsACString& aValue, + bool aIsSecure, bool aIsHttpOnly, bool aIsSession, + int64_t aExpiry, OriginAttributes* aOriginAttributes, + int32_t aSameSite, nsICookie::schemeType aSchemeMap) { + if (NS_WARN_IF(!aOriginAttributes)) { + return NS_ERROR_FAILURE; + } + + if (!IsInitialized()) { + return NS_ERROR_NOT_AVAILABLE; + } + + // first, normalize the hostname, and fail if it contains illegal characters. + nsAutoCString host(aHost); + nsresult rv = NormalizeHost(host); + NS_ENSURE_SUCCESS(rv, rv); + + // get the base domain for the host URI. + // e.g. for "www.bbc.co.uk", this would be "bbc.co.uk". + nsAutoCString baseDomain; + rv = CookieCommons::GetBaseDomainFromHost(mTLDService, host, baseDomain); + NS_ENSURE_SUCCESS(rv, rv); + + int64_t currentTimeInUsec = PR_Now(); + CookieKey key = CookieKey(baseDomain, *aOriginAttributes); + + CookieStruct cookieData(nsCString(aName), nsCString(aValue), nsCString(aHost), + nsCString(aPath), aExpiry, currentTimeInUsec, + Cookie::GenerateUniqueCreationTime(currentTimeInUsec), + aIsHttpOnly, aIsSession, aIsSecure, false, aSameSite, + aSameSite, aSchemeMap); + + RefPtr<Cookie> cookie = Cookie::Create(cookieData, key.mOriginAttributes); + MOZ_ASSERT(cookie); + + CookieStorage* storage = PickStorage(*aOriginAttributes); + storage->AddCookie(nullptr, baseDomain, *aOriginAttributes, cookie, + currentTimeInUsec, nullptr, VoidCString(), true, nullptr); + return NS_OK; +} + +nsresult CookieService::Remove(const nsACString& aHost, + const OriginAttributes& aAttrs, + const nsACString& aName, + const nsACString& aPath) { + // first, normalize the hostname, and fail if it contains illegal characters. + nsAutoCString host(aHost); + nsresult rv = NormalizeHost(host); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString baseDomain; + if (!host.IsEmpty()) { + rv = CookieCommons::GetBaseDomainFromHost(mTLDService, host, baseDomain); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (!IsInitialized()) { + return NS_ERROR_NOT_AVAILABLE; + } + + CookieStorage* storage = PickStorage(aAttrs); + storage->RemoveCookie(baseDomain, aAttrs, host, PromiseFlatCString(aName), + PromiseFlatCString(aPath)); + + return NS_OK; +} + +NS_IMETHODIMP +CookieService::Remove(const nsACString& aHost, const nsACString& aName, + const nsACString& aPath, + JS::Handle<JS::Value> aOriginAttributes, JSContext* aCx) { + OriginAttributes attrs; + + if (!aOriginAttributes.isObject() || !attrs.Init(aCx, aOriginAttributes)) { + return NS_ERROR_INVALID_ARG; + } + + return RemoveNative(aHost, aName, aPath, &attrs); +} + +NS_IMETHODIMP_(nsresult) +CookieService::RemoveNative(const nsACString& aHost, const nsACString& aName, + const nsACString& aPath, + OriginAttributes* aOriginAttributes) { + if (NS_WARN_IF(!aOriginAttributes)) { + return NS_ERROR_FAILURE; + } + + nsresult rv = Remove(aHost, *aOriginAttributes, aName, aPath); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +void CookieService::GetCookiesForURI( + nsIURI* aHostURI, nsIChannel* aChannel, bool aIsForeign, + bool aIsThirdPartyTrackingResource, + bool aIsThirdPartySocialTrackingResource, + bool aStorageAccessPermissionGranted, uint32_t aRejectedReason, + bool aIsSafeTopLevelNav, bool aIsSameSiteForeign, + bool aHadCrossSiteRedirects, bool aHttpBound, + bool aAllowSecureCookiesToInsecureOrigin, + const OriginAttributes& aOriginAttrs, nsTArray<Cookie*>& aCookieList) { + NS_ASSERTION(aHostURI, "null host!"); + + if (!CookieCommons::IsSchemeSupported(aHostURI)) { + return; + } + + if (!IsInitialized()) { + return; + } + + CookieStorage* storage = PickStorage(aOriginAttrs); + + // get the base domain, host, and path from the URI. + // e.g. for "www.bbc.co.uk", the base domain would be "bbc.co.uk". + // file:// URI's (i.e. with an empty host) are allowed, but any other + // scheme must have a non-empty host. A trailing dot in the host + // is acceptable. + bool requireHostMatch; + nsAutoCString baseDomain; + nsAutoCString hostFromURI; + nsAutoCString pathFromURI; + nsresult rv = CookieCommons::GetBaseDomain(mTLDService, aHostURI, baseDomain, + requireHostMatch); + if (NS_SUCCEEDED(rv)) { + rv = nsContentUtils::GetHostOrIPv6WithBrackets(aHostURI, hostFromURI); + } + if (NS_SUCCEEDED(rv)) { + rv = aHostURI->GetFilePath(pathFromURI); + } + if (NS_FAILED(rv)) { + COOKIE_LOGFAILURE(GET_COOKIE, aHostURI, VoidCString(), + "invalid host/path from URI"); + return; + } + + nsCOMPtr<nsICookieJarSettings> cookieJarSettings = + CookieCommons::GetCookieJarSettings(aChannel); + + nsAutoCString normalizedHostFromURI(hostFromURI); + rv = NormalizeHost(normalizedHostFromURI); + NS_ENSURE_SUCCESS_VOID(rv); + + nsAutoCString baseDomainFromURI; + rv = CookieCommons::GetBaseDomainFromHost(mTLDService, normalizedHostFromURI, + baseDomainFromURI); + NS_ENSURE_SUCCESS_VOID(rv); + + // check default prefs + uint32_t rejectedReason = aRejectedReason; + uint32_t priorCookieCount = storage->CountCookiesFromHost( + baseDomainFromURI, aOriginAttrs.mPrivateBrowsingId); + + nsCOMPtr<nsIConsoleReportCollector> crc = do_QueryInterface(aChannel); + CookieStatus cookieStatus = CheckPrefs( + crc, cookieJarSettings, aHostURI, aIsForeign, + aIsThirdPartyTrackingResource, aIsThirdPartySocialTrackingResource, + aStorageAccessPermissionGranted, VoidCString(), priorCookieCount, + aOriginAttrs, &rejectedReason); + + MOZ_ASSERT_IF(rejectedReason, cookieStatus == STATUS_REJECTED); + + // for GetCookie(), we only fire acceptance/rejection notifications + // (but not if there was an error) + switch (cookieStatus) { + case STATUS_REJECTED: + // If we don't have any cookies from this host, fail silently. + if (priorCookieCount) { + CookieCommons::NotifyRejected(aHostURI, aChannel, rejectedReason, + OPERATION_READ); + } + return; + default: + break; + } + + // Note: The following permissions logic is mirrored in + // extensions::MatchPattern::MatchesCookie. + // If it changes, please update that function, or file a bug for someone + // else to do so. + + // check if aHostURI is using an https secure protocol. + // if it isn't, then we can't send a secure cookie over the connection. + // if SchemeIs fails, assume an insecure connection, to be on the safe side + bool potentiallyTrustworthy = + nsMixedContentBlocker::IsPotentiallyTrustworthyOrigin(aHostURI); + + int64_t currentTimeInUsec = PR_Now(); + int64_t currentTime = currentTimeInUsec / PR_USEC_PER_SEC; + bool stale = false; + + const nsTArray<RefPtr<Cookie>>* cookies = + storage->GetCookiesFromHost(baseDomain, aOriginAttrs); + if (!cookies) { + return; + } + + bool laxByDefault = + StaticPrefs::network_cookie_sameSite_laxByDefault() && + !nsContentUtils::IsURIInPrefList( + aHostURI, "network.cookie.sameSite.laxByDefault.disabledHosts"); + + // iterate the cookies! + for (Cookie* cookie : *cookies) { + // check the host, since the base domain lookup is conservative. + if (!CookieCommons::DomainMatches(cookie, hostFromURI)) { + continue; + } + + // if the cookie is secure and the host scheme isn't, we avoid sending + // cookie if possible. But for process synchronization purposes, we may want + // the content process to know about the cookie (without it's value). In + // which case we will wipe the value before sending + if (cookie->IsSecure() && !potentiallyTrustworthy && + !aAllowSecureCookiesToInsecureOrigin) { + continue; + } + + // if the cookie is httpOnly and it's not going directly to the HTTP + // connection, don't send it + if (cookie->IsHttpOnly() && !aHttpBound) { + continue; + } + + // if the nsIURI path doesn't match the cookie path, don't send it back + if (!CookieCommons::PathMatches(cookie, pathFromURI)) { + continue; + } + + // check if the cookie has expired + if (cookie->Expiry() <= currentTime) { + continue; + } + + if (aHttpBound && aIsSameSiteForeign) { + bool blockCookie = !ProcessSameSiteCookieForForeignRequest( + aChannel, cookie, aIsSafeTopLevelNav, aHadCrossSiteRedirects, + laxByDefault); + + if (blockCookie) { + if (aHadCrossSiteRedirects) { + CookieLogging::LogMessageToConsole( + crc, aHostURI, nsIScriptError::warningFlag, + CONSOLE_REJECTION_CATEGORY, "CookieBlockedCrossSiteRedirect"_ns, + AutoTArray<nsString, 1>{ + NS_ConvertUTF8toUTF16(cookie->Name()), + }); + } + continue; + } + } + + // all checks passed - add to list and check if lastAccessed stamp needs + // updating + aCookieList.AppendElement(cookie); + if (cookie->IsStale()) { + stale = true; + } + } + + if (aCookieList.IsEmpty()) { + return; + } + + // Send a notification about the acceptance of the cookies now that we found + // some. + NotifyAccepted(aChannel); + + // update lastAccessed timestamps. we only do this if the timestamp is stale + // by a certain amount, to avoid thrashing the db during pageload. + if (stale) { + storage->StaleCookies(aCookieList, currentTimeInUsec); + } + + // return cookies in order of path length; longest to shortest. + // this is required per RFC2109. if cookies match in length, + // then sort by creation time (see bug 236772). + aCookieList.Sort(CompareCookiesForSending()); +} + +static bool ContainsUnicodeChars(const nsCString& str) { + const auto* start = str.BeginReading(); + const auto* end = str.EndReading(); + + return std::find_if(start, end, [](unsigned char c) { return c >= 0x80; }) != + end; +} + +static void RecordUnicodeTelemetry(const CookieStruct& cookieData) { + auto label = Telemetry::LABELS_NETWORK_COOKIE_UNICODE_BYTE::none; + if (ContainsUnicodeChars(cookieData.name())) { + label = Telemetry::LABELS_NETWORK_COOKIE_UNICODE_BYTE::unicodeName; + } else if (ContainsUnicodeChars(cookieData.value())) { + label = Telemetry::LABELS_NETWORK_COOKIE_UNICODE_BYTE::unicodeValue; + } + Telemetry::AccumulateCategorical(label); +} + +static void RecordPartitionedTelemetry(const CookieStruct& aCookieData, + bool aIsForeign) { + mozilla::glean::networking::set_cookie.Add(1); + if (aCookieData.isPartitioned()) { + mozilla::glean::networking::set_cookie_partitioned.AddToNumerator(1); + } + if (aIsForeign) { + mozilla::glean::networking::set_cookie_foreign.AddToNumerator(1); + } + if (aIsForeign && aCookieData.isPartitioned()) { + mozilla::glean::networking::set_cookie_foreign_partitioned.AddToNumerator( + 1); + } +} + +// processes a single cookie, and returns true if there are more cookies +// to be processed +bool CookieService::CanSetCookie( + nsIURI* aHostURI, const nsACString& aBaseDomain, CookieStruct& aCookieData, + bool aRequireHostMatch, CookieStatus aStatus, nsCString& aCookieHeader, + bool aFromHttp, bool aIsForeignAndNotAddon, bool aPartitionedOnly, + nsIConsoleReportCollector* aCRC, bool& aSetCookie) { + MOZ_ASSERT(aHostURI); + + aSetCookie = false; + + // init expiryTime such that session cookies won't prematurely expire + aCookieData.expiry() = INT64_MAX; + + aCookieData.schemeMap() = CookieCommons::URIToSchemeType(aHostURI); + + // aCookieHeader is an in/out param to point to the next cookie, if + // there is one. Save the present value for logging purposes + nsCString savedCookieHeader(aCookieHeader); + + // newCookie says whether there are multiple cookies in the header; + // so we can handle them separately. + nsAutoCString expires; + nsAutoCString maxage; + bool acceptedByParser = false; + bool newCookie = ParseAttributes(aCRC, aHostURI, aCookieHeader, aCookieData, + expires, maxage, acceptedByParser); + if (!acceptedByParser) { + return newCookie; + } + + // Collect telemetry on how often secure cookies are set from non-secure + // origins, and vice-versa. + // + // 0 = nonsecure and "http:" + // 1 = nonsecure and "https:" + // 2 = secure and "http:" + // 3 = secure and "https:" + bool potentiallyTrustworthy = + nsMixedContentBlocker::IsPotentiallyTrustworthyOrigin(aHostURI); + + int64_t currentTimeInUsec = PR_Now(); + + // calculate expiry time of cookie. + aCookieData.isSession() = + GetExpiry(aCookieData, expires, maxage, + currentTimeInUsec / PR_USEC_PER_SEC, aFromHttp); + if (aStatus == STATUS_ACCEPT_SESSION) { + // force lifetime to session. note that the expiration time, if set above, + // will still apply. + aCookieData.isSession() = true; + } + + // reject cookie if it's over the size limit, per RFC2109 + if (!CookieCommons::CheckNameAndValueSize(aCookieData)) { + COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader, + "cookie too big (> 4kb)"); + + AutoTArray<nsString, 2> params = { + NS_ConvertUTF8toUTF16(aCookieData.name())}; + + nsString size; + size.AppendInt(kMaxBytesPerCookie); + params.AppendElement(size); + + CookieLogging::LogMessageToConsole( + aCRC, aHostURI, nsIScriptError::warningFlag, CONSOLE_OVERSIZE_CATEGORY, + "CookieOversize"_ns, params); + return newCookie; + } + + RecordUnicodeTelemetry(aCookieData); + + // We count SetCookie operations in the parent process only for HTTP set + // cookies to prevent double counting. + if (XRE_IsParentProcess() || !aFromHttp) { + RecordPartitionedTelemetry(aCookieData, aIsForeignAndNotAddon); + } + + if (!CookieCommons::CheckName(aCookieData)) { + COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader, + "invalid name character"); + CookieLogging::LogMessageToConsole( + aCRC, aHostURI, nsIScriptError::warningFlag, CONSOLE_REJECTION_CATEGORY, + "CookieRejectedInvalidCharName"_ns, + AutoTArray<nsString, 1>{ + NS_ConvertUTF8toUTF16(aCookieData.name()), + }); + return newCookie; + } + + // domain & path checks + if (!CheckDomain(aCookieData, aHostURI, aBaseDomain, aRequireHostMatch)) { + COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader, + "failed the domain tests"); + CookieLogging::LogMessageToConsole( + aCRC, aHostURI, nsIScriptError::warningFlag, CONSOLE_REJECTION_CATEGORY, + "CookieRejectedInvalidDomain"_ns, + AutoTArray<nsString, 1>{ + NS_ConvertUTF8toUTF16(aCookieData.name()), + }); + return newCookie; + } + + if (!CheckPath(aCookieData, aCRC, aHostURI)) { + COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader, + "failed the path tests"); + return newCookie; + } + + if (!CheckHiddenPrefix(aCookieData)) { + COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader, + "failed the CheckHiddenPrefix tests"); + CookieLogging::LogMessageToConsole( + aCRC, aHostURI, nsIScriptError::warningFlag, CONSOLE_REJECTION_CATEGORY, + "CookieRejectedInvalidPrefix"_ns, + AutoTArray<nsString, 1>{ + NS_ConvertUTF8toUTF16(aCookieData.name()), + }); + return newCookie; + } + + // magic prefix checks. MUST be run after CheckDomain() and CheckPath() + if (!CheckPrefixes(aCookieData, potentiallyTrustworthy)) { + COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader, + "failed the prefix tests"); + CookieLogging::LogMessageToConsole( + aCRC, aHostURI, nsIScriptError::warningFlag, CONSOLE_REJECTION_CATEGORY, + "CookieRejectedInvalidPrefix"_ns, + AutoTArray<nsString, 1>{ + NS_ConvertUTF8toUTF16(aCookieData.name()), + }); + return newCookie; + } + + if (!CookieCommons::CheckValue(aCookieData)) { + COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader, + "invalid value character"); + CookieLogging::LogMessageToConsole( + aCRC, aHostURI, nsIScriptError::warningFlag, CONSOLE_REJECTION_CATEGORY, + "CookieRejectedInvalidCharValue"_ns, + AutoTArray<nsString, 1>{ + NS_ConvertUTF8toUTF16(aCookieData.name()), + }); + return newCookie; + } + + // if the new cookie is httponly, make sure we're not coming from script + if (!aFromHttp && aCookieData.isHttpOnly()) { + COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader, + "cookie is httponly; coming from script"); + CookieLogging::LogMessageToConsole( + aCRC, aHostURI, nsIScriptError::warningFlag, CONSOLE_REJECTION_CATEGORY, + "CookieRejectedHttpOnlyButFromScript"_ns, + AutoTArray<nsString, 1>{ + NS_ConvertUTF8toUTF16(aCookieData.name()), + }); + return newCookie; + } + + // If the new cookie is non-https and wants to set secure flag, + // browser have to ignore this new cookie. + // (draft-ietf-httpbis-cookie-alone section 3.1) + if (aCookieData.isSecure() && !potentiallyTrustworthy) { + COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader, + "non-https cookie can't set secure flag"); + CookieLogging::LogMessageToConsole( + aCRC, aHostURI, nsIScriptError::warningFlag, CONSOLE_REJECTION_CATEGORY, + "CookieRejectedSecureButNonHttps"_ns, + AutoTArray<nsString, 1>{ + NS_ConvertUTF8toUTF16(aCookieData.name()), + }); + return newCookie; + } + + // If the new cookie is same-site but in a cross site context, + // browser must ignore the cookie. + bool laxByDefault = + StaticPrefs::network_cookie_sameSite_laxByDefault() && + !nsContentUtils::IsURIInPrefList( + aHostURI, "network.cookie.sameSite.laxByDefault.disabledHosts"); + auto effectiveSameSite = + laxByDefault ? aCookieData.sameSite() : aCookieData.rawSameSite(); + if ((effectiveSameSite != nsICookie::SAMESITE_NONE) && + aIsForeignAndNotAddon) { + COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader, + "failed the samesite tests"); + + CookieLogging::LogMessageToConsole( + aCRC, aHostURI, nsIScriptError::warningFlag, CONSOLE_SAMESITE_CATEGORY, + "CookieRejectedForNonSameSiteness"_ns, + AutoTArray<nsString, 1>{ + NS_ConvertUTF8toUTF16(aCookieData.name()), + }); + return newCookie; + } + + // If the cookie does not have the partitioned attribute, + // but is foreign we should give the developer a message. + // If CHIPS isn't required yet, we will warn the console + // that we have upcoming changes. Otherwise we give a rejection message. + if (aPartitionedOnly && !aCookieData.isPartitioned() && + aIsForeignAndNotAddon) { + if (StaticPrefs::network_cookie_cookieBehavior_optInPartitioning()) { + COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader, + "foreign cookies must be partitioned"); + CookieLogging::LogMessageToConsole( + aCRC, aHostURI, nsIScriptError::warningFlag, CONSOLE_CHIPS_CATEGORY, + "CookieForeignNoPartitionedError"_ns, + AutoTArray<nsString, 1>{ + NS_ConvertUTF8toUTF16(aCookieData.name()), + }); + return newCookie; + } + CookieLogging::LogMessageToConsole( + aCRC, aHostURI, nsIScriptError::warningFlag, CONSOLE_CHIPS_CATEGORY, + "CookieForeignNoPartitionedWarning"_ns, + AutoTArray<nsString, 1>{ + NS_ConvertUTF8toUTF16(aCookieData.name()), + }); + } + + aSetCookie = true; + return newCookie; +} + +/****************************************************************************** + * CookieService impl: + * private cookie header parsing functions + ******************************************************************************/ + +// clang-format off +// The following comment block elucidates the function of ParseAttributes. +/****************************************************************************** + ** Augmented BNF, modified from RFC2109 Section 4.2.2 and RFC2616 Section 2.1 + ** please note: this BNF deviates from both specifications, and reflects this + ** implementation. <bnf> indicates a reference to the defined grammar "bnf". + + ** Differences from RFC2109/2616 and explanations: + 1. implied *LWS + The grammar described by this specification is word-based. Except + where noted otherwise, linear white space (<LWS>) can be included + between any two adjacent words (token or quoted-string), and + between adjacent words and separators, without changing the + interpretation of a field. + <LWS> according to spec is SP|HT|CR|LF, but here, we allow only SP | HT. + + 2. We use CR | LF as cookie separators, not ',' per spec, since ',' is in + common use inside values. + + 3. tokens and values have looser restrictions on allowed characters than + spec. This is also due to certain characters being in common use inside + values. We allow only '=' to separate token/value pairs, and ';' to + terminate tokens or values. <LWS> is allowed within tokens and values + (see bug 206022). + + 4. where appropriate, full <OCTET>s are allowed, where the spec dictates to + reject control chars or non-ASCII chars. This is erring on the loose + side, since there's probably no good reason to enforce this strictness. + + 5. Attribute "HttpOnly", not covered in the RFCs, is supported + (see bug 178993). + + ** Begin BNF: + token = 1*<any allowed-chars except separators> + value = 1*<any allowed-chars except value-sep> + separators = ";" | "=" + value-sep = ";" + cookie-sep = CR | LF + allowed-chars = <any OCTET except NUL or cookie-sep> + OCTET = <any 8-bit sequence of data> + LWS = SP | HT + NUL = <US-ASCII NUL, null control character (0)> + CR = <US-ASCII CR, carriage return (13)> + LF = <US-ASCII LF, linefeed (10)> + SP = <US-ASCII SP, space (32)> + HT = <US-ASCII HT, horizontal-tab (9)> + + set-cookie = "Set-Cookie:" cookies + cookies = cookie *( cookie-sep cookie ) + cookie = [NAME "="] VALUE *(";" cookie-av) ; cookie NAME/VALUE must come first + NAME = token ; cookie name + VALUE = value ; cookie value + cookie-av = token ["=" value] + + valid values for cookie-av (checked post-parsing) are: + cookie-av = "Path" "=" value + | "Domain" "=" value + | "Expires" "=" value + | "Max-Age" "=" value + | "Comment" "=" value + | "Version" "=" value + | "Secure" + | "HttpOnly" + +******************************************************************************/ +// clang-format on + +// helper functions for GetTokenValue +static inline bool isnull(char c) { return c == 0; } +static inline bool iswhitespace(char c) { return c == ' ' || c == '\t'; } +static inline bool isterminator(char c) { return c == '\n' || c == '\r'; } +static inline bool isvalueseparator(char c) { + return isterminator(c) || c == ';'; +} +static inline bool istokenseparator(char c) { + return isvalueseparator(c) || c == '='; +} + +// Parse a single token/value pair. +// Returns true if a cookie terminator is found, so caller can parse new cookie. +bool CookieService::GetTokenValue(nsACString::const_char_iterator& aIter, + nsACString::const_char_iterator& aEndIter, + nsDependentCSubstring& aTokenString, + nsDependentCSubstring& aTokenValue, + bool& aEqualsFound) { + nsACString::const_char_iterator start; + nsACString::const_char_iterator lastSpace; + // initialize value string to clear garbage + aTokenValue.Rebind(aIter, aIter); + + // find <token>, including any <LWS> between the end-of-token and the + // token separator. we'll remove trailing <LWS> next + while (aIter != aEndIter && iswhitespace(*aIter)) { + ++aIter; + } + start = aIter; + while (aIter != aEndIter && !isnull(*aIter) && !istokenseparator(*aIter)) { + ++aIter; + } + + // remove trailing <LWS>; first check we're not at the beginning + lastSpace = aIter; + if (lastSpace != start) { + while (--lastSpace != start && iswhitespace(*lastSpace)) { + } + ++lastSpace; + } + aTokenString.Rebind(start, lastSpace); + + aEqualsFound = (*aIter == '='); + if (aEqualsFound) { + // find <value> + while (++aIter != aEndIter && iswhitespace(*aIter)) { + } + + start = aIter; + + // process <token> + // just look for ';' to terminate ('=' allowed) + while (aIter != aEndIter && !isnull(*aIter) && !isvalueseparator(*aIter)) { + ++aIter; + } + + // remove trailing <LWS>; first check we're not at the beginning + if (aIter != start) { + lastSpace = aIter; + while (--lastSpace != start && iswhitespace(*lastSpace)) { + } + + aTokenValue.Rebind(start, ++lastSpace); + } + } + + // aIter is on ';', or terminator, or EOS + if (aIter != aEndIter) { + // if on terminator, increment past & return true to process new cookie + if (isterminator(*aIter)) { + ++aIter; + return true; + } + // fall-through: aIter is on ';', increment and return false + ++aIter; + } + return false; +} + +static inline void SetSameSiteAttributeDefault(CookieStruct& aCookieData) { + // Set cookie with SameSite attribute that is treated as Default + // and doesn't requires changing the DB schema. + aCookieData.sameSite() = nsICookie::SAMESITE_LAX; + aCookieData.rawSameSite() = nsICookie::SAMESITE_NONE; +} + +static inline void SetSameSiteAttribute(CookieStruct& aCookieData, + int32_t aValue) { + aCookieData.sameSite() = aValue; + aCookieData.rawSameSite() = aValue; +} + +// Parses attributes from cookie header. expires/max-age attributes aren't +// folded into the cookie struct here, because we don't know which one to use +// until we've parsed the header. +bool CookieService::ParseAttributes(nsIConsoleReportCollector* aCRC, + nsIURI* aHostURI, nsCString& aCookieHeader, + CookieStruct& aCookieData, + nsACString& aExpires, nsACString& aMaxage, + bool& aAcceptedByParser) { + aAcceptedByParser = false; + + static const char kPath[] = "path"; + static const char kDomain[] = "domain"; + static const char kExpires[] = "expires"; + static const char kMaxage[] = "max-age"; + static const char kSecure[] = "secure"; + static const char kHttpOnly[] = "httponly"; + static const char kSameSite[] = "samesite"; + static const char kSameSiteLax[] = "lax"; + static const char kSameSiteNone[] = "none"; + static const char kSameSiteStrict[] = "strict"; + static const char kPartitioned[] = "partitioned"; + + nsACString::const_char_iterator cookieStart; + aCookieHeader.BeginReading(cookieStart); + + nsACString::const_char_iterator cookieEnd; + aCookieHeader.EndReading(cookieEnd); + + aCookieData.isSecure() = false; + aCookieData.isHttpOnly() = false; + + SetSameSiteAttributeDefault(aCookieData); + + nsDependentCSubstring tokenString(cookieStart, cookieStart); + nsDependentCSubstring tokenValue(cookieStart, cookieStart); + bool newCookie; + bool equalsFound; + + // extract cookie <NAME> & <VALUE> (first attribute), and copy the strings. + // if we find multiple cookies, return for processing + // note: if there's no '=', we assume token is <VALUE>. this is required by + // some sites (see bug 169091). + // XXX fix the parser to parse according to <VALUE> grammar for this case + newCookie = GetTokenValue(cookieStart, cookieEnd, tokenString, tokenValue, + equalsFound); + if (equalsFound) { + aCookieData.name() = tokenString; + aCookieData.value() = tokenValue; + } else { + aCookieData.value() = tokenString; + } + + // extract remaining attributes + while (cookieStart != cookieEnd && !newCookie) { + newCookie = GetTokenValue(cookieStart, cookieEnd, tokenString, tokenValue, + equalsFound); + + // decide which attribute we have, and copy the string + if (tokenString.LowerCaseEqualsLiteral(kPath)) { + aCookieData.path() = tokenValue; + + } else if (tokenString.LowerCaseEqualsLiteral(kDomain)) { + aCookieData.host() = tokenValue; + + } else if (tokenString.LowerCaseEqualsLiteral(kExpires)) { + aExpires = tokenValue; + + } else if (tokenString.LowerCaseEqualsLiteral(kMaxage)) { + aMaxage = tokenValue; + + // ignore any tokenValue for isSecure; just set the boolean + } else if (tokenString.LowerCaseEqualsLiteral(kSecure)) { + aCookieData.isSecure() = true; + + // ignore any tokenValue for isPartitioned; just set the boolean + } else if (tokenString.LowerCaseEqualsLiteral(kPartitioned)) { + aCookieData.isPartitioned() = true; + + // ignore any tokenValue for isHttpOnly (see bug 178993); + // just set the boolean + } else if (tokenString.LowerCaseEqualsLiteral(kHttpOnly)) { + aCookieData.isHttpOnly() = true; + + } else if (tokenString.LowerCaseEqualsLiteral(kSameSite)) { + if (tokenValue.LowerCaseEqualsLiteral(kSameSiteLax)) { + SetSameSiteAttribute(aCookieData, nsICookie::SAMESITE_LAX); + } else if (tokenValue.LowerCaseEqualsLiteral(kSameSiteStrict)) { + SetSameSiteAttribute(aCookieData, nsICookie::SAMESITE_STRICT); + } else if (tokenValue.LowerCaseEqualsLiteral(kSameSiteNone)) { + SetSameSiteAttribute(aCookieData, nsICookie::SAMESITE_NONE); + } else { + // Reset to Default if unknown token value (see Bug 1682450) + SetSameSiteAttributeDefault(aCookieData); + CookieLogging::LogMessageToConsole( + aCRC, aHostURI, nsIScriptError::infoFlag, CONSOLE_SAMESITE_CATEGORY, + "CookieSameSiteValueInvalid2"_ns, + AutoTArray<nsString, 1>{NS_ConvertUTF8toUTF16(aCookieData.name())}); + } + } + } + + // re-assign aCookieHeader, in case we need to process another cookie + aCookieHeader.Assign(Substring(cookieStart, cookieEnd)); + + // If same-site is explicitly set to 'none' but this is not a secure context, + // let's abort the parsing. + if (!aCookieData.isSecure() && + aCookieData.sameSite() == nsICookie::SAMESITE_NONE) { + if (StaticPrefs::network_cookie_sameSite_noneRequiresSecure()) { + CookieLogging::LogMessageToConsole( + aCRC, aHostURI, nsIScriptError::errorFlag, CONSOLE_SAMESITE_CATEGORY, + "CookieRejectedNonRequiresSecure2"_ns, + AutoTArray<nsString, 1>{NS_ConvertUTF8toUTF16(aCookieData.name())}); + return newCookie; + } + + // Still warn about the missing Secure attribute when not enforcing. + CookieLogging::LogMessageToConsole( + aCRC, aHostURI, nsIScriptError::warningFlag, CONSOLE_SAMESITE_CATEGORY, + "CookieRejectedNonRequiresSecureForBeta3"_ns, + AutoTArray<nsString, 2>{NS_ConvertUTF8toUTF16(aCookieData.name()), + SAMESITE_MDN_URL}); + } + + // Ensure the partitioned cookie is set with the secure attribute. + if (aCookieData.isPartitioned() && !aCookieData.isSecure()) { + CookieLogging::LogMessageToConsole( + aCRC, aHostURI, nsIScriptError::errorFlag, CONSOLE_REJECTION_CATEGORY, + "CookieRejectedPartitionedRequiresSecure"_ns, + AutoTArray<nsString, 1>{NS_ConvertUTF8toUTF16(aCookieData.name())}); + + // We only drop the cookie if CHIPS is enabled. + if (StaticPrefs::network_cookie_cookieBehavior_optInPartitioning()) { + return newCookie; + } + } + + if (aCookieData.rawSameSite() == nsICookie::SAMESITE_NONE && + aCookieData.sameSite() == nsICookie::SAMESITE_LAX) { + bool laxByDefault = + StaticPrefs::network_cookie_sameSite_laxByDefault() && + !nsContentUtils::IsURIInPrefList( + aHostURI, "network.cookie.sameSite.laxByDefault.disabledHosts"); + if (laxByDefault) { + CookieLogging::LogMessageToConsole( + aCRC, aHostURI, nsIScriptError::infoFlag, CONSOLE_SAMESITE_CATEGORY, + "CookieLaxForced2"_ns, + AutoTArray<nsString, 1>{NS_ConvertUTF8toUTF16(aCookieData.name())}); + } else { + CookieLogging::LogMessageToConsole( + aCRC, aHostURI, nsIScriptError::warningFlag, + CONSOLE_SAMESITE_CATEGORY, "CookieLaxForcedForBeta2"_ns, + AutoTArray<nsString, 2>{NS_ConvertUTF8toUTF16(aCookieData.name()), + SAMESITE_MDN_URL}); + } + } + + // Cookie accepted. + aAcceptedByParser = true; + + MOZ_ASSERT(Cookie::ValidateSameSite(aCookieData)); + return newCookie; +} + +/****************************************************************************** + * CookieService impl: + * private domain & permission compliance enforcement functions + ******************************************************************************/ + +// Normalizes the given hostname, component by component. ASCII/ACE +// components are lower-cased, and UTF-8 components are normalized per +// RFC 3454 and converted to ACE. +nsresult CookieService::NormalizeHost(nsCString& aHost) { + if (!IsAscii(aHost)) { + nsAutoCString host; + nsresult rv = mIDNService->ConvertUTF8toACE(aHost, host); + if (NS_FAILED(rv)) { + return rv; + } + + aHost = host; + } + + ToLowerCase(aHost); + return NS_OK; +} + +// returns true if 'a' is equal to or a subdomain of 'b', +// assuming no leading dots are present. +static inline bool IsSubdomainOf(const nsACString& a, const nsACString& b) { + if (a == b) { + return true; + } + if (a.Length() > b.Length()) { + return a[a.Length() - b.Length() - 1] == '.' && StringEndsWith(a, b); + } + return false; +} + +CookieStatus CookieService::CheckPrefs( + nsIConsoleReportCollector* aCRC, nsICookieJarSettings* aCookieJarSettings, + nsIURI* aHostURI, bool aIsForeign, bool aIsThirdPartyTrackingResource, + bool aIsThirdPartySocialTrackingResource, + bool aStorageAccessPermissionGranted, const nsACString& aCookieHeader, + const int aNumOfCookies, const OriginAttributes& aOriginAttrs, + uint32_t* aRejectedReason) { + nsresult rv; + + MOZ_ASSERT(aRejectedReason); + + *aRejectedReason = 0; + + // don't let unsupported scheme sites get/set cookies (could be a security + // issue) + if (!CookieCommons::IsSchemeSupported(aHostURI)) { + COOKIE_LOGFAILURE(!aCookieHeader.IsVoid(), aHostURI, aCookieHeader, + "non http/https sites cannot read cookies"); + return STATUS_REJECTED_WITH_ERROR; + } + + nsCOMPtr<nsIPrincipal> principal = + BasePrincipal::CreateContentPrincipal(aHostURI, aOriginAttrs); + + if (!principal) { + COOKIE_LOGFAILURE(!aCookieHeader.IsVoid(), aHostURI, aCookieHeader, + "non-content principals cannot get/set cookies"); + return STATUS_REJECTED_WITH_ERROR; + } + + // check the permission list first; if we find an entry, it overrides + // default prefs. see bug 184059. + uint32_t cookiePermission = nsICookiePermission::ACCESS_DEFAULT; + rv = aCookieJarSettings->CookiePermission(principal, &cookiePermission); + if (NS_SUCCEEDED(rv)) { + switch (cookiePermission) { + case nsICookiePermission::ACCESS_DENY: + COOKIE_LOGFAILURE(!aCookieHeader.IsVoid(), aHostURI, aCookieHeader, + "cookies are blocked for this site"); + CookieLogging::LogMessageToConsole( + aCRC, aHostURI, nsIScriptError::warningFlag, + CONSOLE_REJECTION_CATEGORY, "CookieRejectedByPermissionManager"_ns, + AutoTArray<nsString, 1>{ + NS_ConvertUTF8toUTF16(aCookieHeader), + }); + + *aRejectedReason = + nsIWebProgressListener::STATE_COOKIES_BLOCKED_BY_PERMISSION; + return STATUS_REJECTED; + + case nsICookiePermission::ACCESS_ALLOW: + return STATUS_ACCEPTED; + default: + break; + } + } + + // No cookies allowed if this request comes from a resource in a 3rd party + // context, when anti-tracking protection is enabled and when we don't have + // access to the first-party cookie jar. + if (aIsForeign && aIsThirdPartyTrackingResource && + !aStorageAccessPermissionGranted && + aCookieJarSettings->GetRejectThirdPartyContexts()) { + uint32_t rejectReason = + nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER; + if (StoragePartitioningEnabled(rejectReason, aCookieJarSettings)) { + MOZ_ASSERT(!aOriginAttrs.mPartitionKey.IsEmpty(), + "We must have a StoragePrincipal here!"); + return STATUS_ACCEPTED; + } + + COOKIE_LOGFAILURE(!aCookieHeader.IsVoid(), aHostURI, aCookieHeader, + "cookies are disabled in trackers"); + if (aIsThirdPartySocialTrackingResource) { + *aRejectedReason = + nsIWebProgressListener::STATE_COOKIES_BLOCKED_SOCIALTRACKER; + } else { + *aRejectedReason = nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER; + } + return STATUS_REJECTED; + } + + // check default prefs. + // Check aStorageAccessPermissionGranted when checking aCookieBehavior + // so that we take things such as the content blocking allow list into + // account. + if (aCookieJarSettings->GetCookieBehavior() == + nsICookieService::BEHAVIOR_REJECT && + !aStorageAccessPermissionGranted) { + COOKIE_LOGFAILURE(!aCookieHeader.IsVoid(), aHostURI, aCookieHeader, + "cookies are disabled"); + *aRejectedReason = nsIWebProgressListener::STATE_COOKIES_BLOCKED_ALL; + return STATUS_REJECTED; + } + + // check if cookie is foreign + if (aIsForeign) { + if (aCookieJarSettings->GetCookieBehavior() == + nsICookieService::BEHAVIOR_REJECT_FOREIGN && + !aStorageAccessPermissionGranted) { + COOKIE_LOGFAILURE(!aCookieHeader.IsVoid(), aHostURI, aCookieHeader, + "context is third party"); + CookieLogging::LogMessageToConsole( + aCRC, aHostURI, nsIScriptError::warningFlag, + CONSOLE_REJECTION_CATEGORY, "CookieRejectedThirdParty"_ns, + AutoTArray<nsString, 1>{ + NS_ConvertUTF8toUTF16(aCookieHeader), + }); + *aRejectedReason = nsIWebProgressListener::STATE_COOKIES_BLOCKED_FOREIGN; + return STATUS_REJECTED; + } + + if (aCookieJarSettings->GetLimitForeignContexts() && + !aStorageAccessPermissionGranted && aNumOfCookies == 0) { + COOKIE_LOGFAILURE(!aCookieHeader.IsVoid(), aHostURI, aCookieHeader, + "context is third party"); + CookieLogging::LogMessageToConsole( + aCRC, aHostURI, nsIScriptError::warningFlag, + CONSOLE_REJECTION_CATEGORY, "CookieRejectedThirdParty"_ns, + AutoTArray<nsString, 1>{ + NS_ConvertUTF8toUTF16(aCookieHeader), + }); + *aRejectedReason = nsIWebProgressListener::STATE_COOKIES_BLOCKED_FOREIGN; + return STATUS_REJECTED; + } + + if (StaticPrefs::network_cookie_thirdparty_sessionOnly()) { + return STATUS_ACCEPT_SESSION; + } + + if (StaticPrefs::network_cookie_thirdparty_nonsecureSessionOnly()) { + if (!aHostURI->SchemeIs("https")) { + return STATUS_ACCEPT_SESSION; + } + } + } + + // if nothing has complained, accept cookie + return STATUS_ACCEPTED; +} + +// processes domain attribute, and returns true if host has permission to set +// for this domain. +bool CookieService::CheckDomain(CookieStruct& aCookieData, nsIURI* aHostURI, + const nsACString& aBaseDomain, + bool aRequireHostMatch) { + // Note: The logic in this function is mirrored in + // toolkit/components/extensions/ext-cookies.js:checkSetCookiePermissions(). + // If it changes, please update that function, or file a bug for someone + // else to do so. + + // get host from aHostURI + nsAutoCString hostFromURI; + nsContentUtils::GetHostOrIPv6WithBrackets(aHostURI, hostFromURI); + + // if a domain is given, check the host has permission + if (!aCookieData.host().IsEmpty()) { + // Tolerate leading '.' characters, but not if it's otherwise an empty host. + if (aCookieData.host().Length() > 1 && aCookieData.host().First() == '.') { + aCookieData.host().Cut(0, 1); + } + + // switch to lowercase now, to avoid case-insensitive compares everywhere + ToLowerCase(aCookieData.host()); + + // check whether the host is either an IP address, an alias such as + // 'localhost', an eTLD such as 'co.uk', or the empty string. in these + // cases, require an exact string match for the domain, and leave the cookie + // as a non-domain one. bug 105917 originally noted the requirement to deal + // with IP addresses. + if (aRequireHostMatch) { + return hostFromURI.Equals(aCookieData.host()); + } + + // ensure the proposed domain is derived from the base domain; and also + // that the host domain is derived from the proposed domain (per RFC2109). + if (IsSubdomainOf(aCookieData.host(), aBaseDomain) && + IsSubdomainOf(hostFromURI, aCookieData.host())) { + // prepend a dot to indicate a domain cookie + aCookieData.host().InsertLiteral(".", 0); + return true; + } + + /* + * note: RFC2109 section 4.3.2 requires that we check the following: + * that the portion of host not in domain does not contain a dot. + * this prevents hosts of the form x.y.co.nz from setting cookies in the + * entire .co.nz domain. however, it's only a only a partial solution and + * it breaks sites (IE doesn't enforce it), so we don't perform this check. + */ + return false; + } + + // no domain specified, use hostFromURI + aCookieData.host() = hostFromURI; + return true; +} + +// static +bool CookieService::CheckHiddenPrefix(CookieStruct& aCookieData) { + // If a cookie is nameless, then its value must not start with + // `__Host-` or `__Secure-` + if (!aCookieData.name().IsEmpty()) { + return true; + } + + if (StringBeginsWith(aCookieData.value(), "__Host-"_ns)) { + return false; + } + + if (StringBeginsWith(aCookieData.value(), "__Secure-"_ns)) { + return false; + } + + return true; +} + +namespace { +nsAutoCString GetPathFromURI(nsIURI* aHostURI) { + // strip down everything after the last slash to get the path, + // ignoring slashes in the query string part. + // if we can QI to nsIURL, that'll take care of the query string portion. + // otherwise, it's not an nsIURL and can't have a query string, so just find + // the last slash. + nsAutoCString path; + nsCOMPtr<nsIURL> hostURL = do_QueryInterface(aHostURI); + if (hostURL) { + hostURL->GetDirectory(path); + } else { + aHostURI->GetPathQueryRef(path); + int32_t slash = path.RFindChar('/'); + if (slash != kNotFound) { + path.Truncate(slash + 1); + } + } + + // strip the right-most %x2F ("/") if the path doesn't contain only 1 '/'. + int32_t lastSlash = path.RFindChar('/'); + int32_t firstSlash = path.FindChar('/'); + if (lastSlash != firstSlash && lastSlash != kNotFound && + lastSlash == static_cast<int32_t>(path.Length() - 1)) { + path.Truncate(lastSlash); + } + + return path; +} + +} // namespace + +bool CookieService::CheckPath(CookieStruct& aCookieData, + nsIConsoleReportCollector* aCRC, + nsIURI* aHostURI) { + // if a path is given, check the host has permission + if (aCookieData.path().IsEmpty() || aCookieData.path().First() != '/') { + aCookieData.path() = GetPathFromURI(aHostURI); + } + + if (!CookieCommons::CheckPathSize(aCookieData)) { + AutoTArray<nsString, 2> params = { + NS_ConvertUTF8toUTF16(aCookieData.name())}; + + nsString size; + size.AppendInt(kMaxBytesPerPath); + params.AppendElement(size); + + CookieLogging::LogMessageToConsole( + aCRC, aHostURI, nsIScriptError::warningFlag, CONSOLE_OVERSIZE_CATEGORY, + "CookiePathOversize"_ns, params); + return false; + } + + return !aCookieData.path().Contains('\t'); +} + +// CheckPrefixes +// +// Reject cookies whose name starts with the magic prefixes from +// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis +// if they do not meet the criteria required by the prefix. +// +// Must not be called until after CheckDomain() and CheckPath() have +// regularized and validated the CookieStruct values! +bool CookieService::CheckPrefixes(CookieStruct& aCookieData, + bool aSecureRequest) { + static const char kSecure[] = "__Secure-"; + static const char kHost[] = "__Host-"; + static const int kSecureLen = sizeof(kSecure) - 1; + static const int kHostLen = sizeof(kHost) - 1; + + bool isSecure = strncmp(aCookieData.name().get(), kSecure, kSecureLen) == 0; + bool isHost = strncmp(aCookieData.name().get(), kHost, kHostLen) == 0; + + if (!isSecure && !isHost) { + // not one of the magic prefixes: carry on + return true; + } + + if (!aSecureRequest || !aCookieData.isSecure()) { + // the magic prefixes may only be used from a secure request and + // the secure attribute must be set on the cookie + return false; + } + + if (isHost) { + // The host prefix requires that the path is "/" and that the cookie + // had no domain attribute. CheckDomain() and CheckPath() MUST be run + // first to make sure invalid attributes are rejected and to regularlize + // them. In particular all explicit domain attributes result in a host + // that starts with a dot, and if the host doesn't start with a dot it + // correctly matches the true host. + if (aCookieData.host()[0] == '.' || + !aCookieData.path().EqualsLiteral("/")) { + return false; + } + } + + return true; +} + +bool CookieService::GetExpiry(CookieStruct& aCookieData, + const nsACString& aExpires, + const nsACString& aMaxage, int64_t aCurrentTime, + bool aFromHttp) { + // maxageCap is in seconds. + // Disabled for HTTP cookies. + int64_t maxageCap = + aFromHttp ? 0 : StaticPrefs::privacy_documentCookies_maxage(); + + /* Determine when the cookie should expire. This is done by taking the + * difference between the server time and the time the server wants the cookie + * to expire, and adding that difference to the client time. This localizes + * the client time regardless of whether or not the TZ environment variable + * was set on the client. + * + * Note: We need to consider accounting for network lag here, per RFC. + */ + // check for max-age attribute first; this overrides expires attribute + if (!aMaxage.IsEmpty()) { + // obtain numeric value of maxageAttribute + int64_t maxage; + int32_t numInts = PR_sscanf(aMaxage.BeginReading(), "%lld", &maxage); + + // default to session cookie if the conversion failed + if (numInts != 1) { + return true; + } + + // if this addition overflows, expiryTime will be less than currentTime + // and the cookie will be expired - that's okay. + if (maxageCap) { + aCookieData.expiry() = aCurrentTime + std::min(maxage, maxageCap); + } else { + aCookieData.expiry() = aCurrentTime + maxage; + } + + // check for expires attribute + } else if (!aExpires.IsEmpty()) { + PRTime expires; + + // parse expiry time + if (PR_ParseTimeString(aExpires.BeginReading(), true, &expires) != + PR_SUCCESS) { + return true; + } + + // If set-cookie used absolute time to set expiration, and it can't use + // client time to set expiration. + // Because if current time be set in the future, but the cookie expire + // time be set less than current time and more than server time. + // The cookie item have to be used to the expired cookie. + if (maxageCap) { + aCookieData.expiry() = std::min(expires / int64_t(PR_USEC_PER_SEC), + aCurrentTime + maxageCap); + } else { + aCookieData.expiry() = expires / int64_t(PR_USEC_PER_SEC); + } + + // default to session cookie if no attributes found. Here we don't need to + // enforce the maxage cap, because session cookies are short-lived by + // definition. + } else { + return true; + } + + return false; +} + +/****************************************************************************** + * CookieService impl: + * private cookielist management functions + ******************************************************************************/ + +// find whether a given cookie has been previously set. this is provided by the +// nsICookieManager interface. +NS_IMETHODIMP +CookieService::CookieExists(const nsACString& aHost, const nsACString& aPath, + const nsACString& aName, + JS::Handle<JS::Value> aOriginAttributes, + JSContext* aCx, bool* aFoundCookie) { + NS_ENSURE_ARG_POINTER(aCx); + NS_ENSURE_ARG_POINTER(aFoundCookie); + + OriginAttributes attrs; + if (!aOriginAttributes.isObject() || !attrs.Init(aCx, aOriginAttributes)) { + return NS_ERROR_INVALID_ARG; + } + return CookieExistsNative(aHost, aPath, aName, &attrs, aFoundCookie); +} + +NS_IMETHODIMP_(nsresult) +CookieService::CookieExistsNative(const nsACString& aHost, + const nsACString& aPath, + const nsACString& aName, + OriginAttributes* aOriginAttributes, + bool* aFoundCookie) { + nsCOMPtr<nsICookie> cookie; + nsresult rv = GetCookieNative(aHost, aPath, aName, aOriginAttributes, + getter_AddRefs(cookie)); + NS_ENSURE_SUCCESS(rv, rv); + + *aFoundCookie = cookie != nullptr; + + return NS_OK; +} + +NS_IMETHODIMP_(nsresult) +CookieService::GetCookieNative(const nsACString& aHost, const nsACString& aPath, + const nsACString& aName, + OriginAttributes* aOriginAttributes, + nsICookie** aCookie) { + NS_ENSURE_ARG_POINTER(aOriginAttributes); + NS_ENSURE_ARG_POINTER(aCookie); + + if (!IsInitialized()) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsAutoCString baseDomain; + nsresult rv = + CookieCommons::GetBaseDomainFromHost(mTLDService, aHost, baseDomain); + NS_ENSURE_SUCCESS(rv, rv); + + CookieListIter iter{}; + CookieStorage* storage = PickStorage(*aOriginAttributes); + bool foundCookie = storage->FindCookie(baseDomain, *aOriginAttributes, aHost, + aName, aPath, iter); + + if (foundCookie) { + RefPtr<Cookie> cookie = iter.Cookie(); + NS_ENSURE_TRUE(cookie, NS_ERROR_NULL_POINTER); + + cookie.forget(aCookie); + } + + return NS_OK; +} + +// count the number of cookies stored by a particular host. this is provided by +// the nsICookieManager interface. +NS_IMETHODIMP +CookieService::CountCookiesFromHost(const nsACString& aHost, + uint32_t* aCountFromHost) { + // first, normalize the hostname, and fail if it contains illegal characters. + nsAutoCString host(aHost); + nsresult rv = NormalizeHost(host); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString baseDomain; + rv = CookieCommons::GetBaseDomainFromHost(mTLDService, host, baseDomain); + NS_ENSURE_SUCCESS(rv, rv); + + if (!IsInitialized()) { + return NS_ERROR_NOT_AVAILABLE; + } + + mPersistentStorage->EnsureInitialized(); + + *aCountFromHost = mPersistentStorage->CountCookiesFromHost(baseDomain, 0); + + return NS_OK; +} + +// get an enumerator of cookies stored by a particular host. this is provided by +// the nsICookieManager interface. +NS_IMETHODIMP +CookieService::GetCookiesFromHost(const nsACString& aHost, + JS::Handle<JS::Value> aOriginAttributes, + JSContext* aCx, + nsTArray<RefPtr<nsICookie>>& aResult) { + // first, normalize the hostname, and fail if it contains illegal characters. + nsAutoCString host(aHost); + nsresult rv = NormalizeHost(host); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString baseDomain; + rv = CookieCommons::GetBaseDomainFromHost(mTLDService, host, baseDomain); + NS_ENSURE_SUCCESS(rv, rv); + + OriginAttributes attrs; + if (!aOriginAttributes.isObject() || !attrs.Init(aCx, aOriginAttributes)) { + return NS_ERROR_INVALID_ARG; + } + + if (!IsInitialized()) { + return NS_ERROR_NOT_AVAILABLE; + } + + CookieStorage* storage = PickStorage(attrs); + + const nsTArray<RefPtr<Cookie>>* cookies = + storage->GetCookiesFromHost(baseDomain, attrs); + + if (cookies) { + aResult.SetCapacity(cookies->Length()); + for (Cookie* cookie : *cookies) { + aResult.AppendElement(cookie); + } + } + + return NS_OK; +} + +NS_IMETHODIMP +CookieService::GetCookiesWithOriginAttributes( + const nsAString& aPattern, const nsACString& aHost, + nsTArray<RefPtr<nsICookie>>& aResult) { + OriginAttributesPattern pattern; + if (!pattern.Init(aPattern)) { + return NS_ERROR_INVALID_ARG; + } + + nsAutoCString host(aHost); + nsresult rv = NormalizeHost(host); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString baseDomain; + rv = CookieCommons::GetBaseDomainFromHost(mTLDService, host, baseDomain); + NS_ENSURE_SUCCESS(rv, rv); + + return GetCookiesWithOriginAttributes(pattern, baseDomain, aResult); +} + +nsresult CookieService::GetCookiesWithOriginAttributes( + const OriginAttributesPattern& aPattern, const nsCString& aBaseDomain, + nsTArray<RefPtr<nsICookie>>& aResult) { + CookieStorage* storage = PickStorage(aPattern); + storage->GetCookiesWithOriginAttributes(aPattern, aBaseDomain, aResult); + + return NS_OK; +} + +NS_IMETHODIMP +CookieService::RemoveCookiesWithOriginAttributes(const nsAString& aPattern, + const nsACString& aHost) { + MOZ_ASSERT(XRE_IsParentProcess()); + + OriginAttributesPattern pattern; + if (!pattern.Init(aPattern)) { + return NS_ERROR_INVALID_ARG; + } + + nsAutoCString host(aHost); + nsresult rv = NormalizeHost(host); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString baseDomain; + rv = CookieCommons::GetBaseDomainFromHost(mTLDService, host, baseDomain); + NS_ENSURE_SUCCESS(rv, rv); + + return RemoveCookiesWithOriginAttributes(pattern, baseDomain); +} + +nsresult CookieService::RemoveCookiesWithOriginAttributes( + const OriginAttributesPattern& aPattern, const nsCString& aBaseDomain) { + if (!IsInitialized()) { + return NS_ERROR_NOT_AVAILABLE; + } + + CookieStorage* storage = PickStorage(aPattern); + storage->RemoveCookiesWithOriginAttributes(aPattern, aBaseDomain); + + return NS_OK; +} + +NS_IMETHODIMP +CookieService::RemoveCookiesFromExactHost(const nsACString& aHost, + const nsAString& aPattern) { + MOZ_ASSERT(XRE_IsParentProcess()); + + OriginAttributesPattern pattern; + if (!pattern.Init(aPattern)) { + return NS_ERROR_INVALID_ARG; + } + + return RemoveCookiesFromExactHost(aHost, pattern); +} + +nsresult CookieService::RemoveCookiesFromExactHost( + const nsACString& aHost, const OriginAttributesPattern& aPattern) { + nsAutoCString host(aHost); + nsresult rv = NormalizeHost(host); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString baseDomain; + rv = CookieCommons::GetBaseDomainFromHost(mTLDService, host, baseDomain); + NS_ENSURE_SUCCESS(rv, rv); + + if (!IsInitialized()) { + return NS_ERROR_NOT_AVAILABLE; + } + + CookieStorage* storage = PickStorage(aPattern); + storage->RemoveCookiesFromExactHost(aHost, baseDomain, aPattern); + + return NS_OK; +} + +namespace { + +class RemoveAllSinceRunnable : public Runnable { + public: + using CookieArray = nsTArray<RefPtr<nsICookie>>; + RemoveAllSinceRunnable(Promise* aPromise, CookieService* aSelf, + CookieArray&& aCookieArray, int64_t aSinceWhen) + : Runnable("RemoveAllSinceRunnable"), + mPromise(aPromise), + mSelf(aSelf), + mList(std::move(aCookieArray)), + mIndex(0), + mSinceWhen(aSinceWhen) {} + + NS_IMETHODIMP Run() override { + RemoveSome(); + + if (mIndex < mList.Length()) { + return NS_DispatchToCurrentThread(this); + } + mPromise->MaybeResolveWithUndefined(); + + return NS_OK; + } + + private: + void RemoveSome() { + for (CookieArray::size_type iter = 0; + iter < kYieldPeriod && mIndex < mList.Length(); ++mIndex, ++iter) { + auto* cookie = static_cast<Cookie*>(mList[mIndex].get()); + if (cookie->CreationTime() > mSinceWhen && + NS_FAILED(mSelf->Remove(cookie->Host(), cookie->OriginAttributesRef(), + cookie->Name(), cookie->Path()))) { + continue; + } + } + } + + private: + RefPtr<Promise> mPromise; + RefPtr<CookieService> mSelf; + CookieArray mList; + CookieArray::size_type mIndex; + int64_t mSinceWhen; + static const CookieArray::size_type kYieldPeriod = 10; +}; + +} // namespace + +NS_IMETHODIMP +CookieService::RemoveAllSince(int64_t aSinceWhen, JSContext* aCx, + Promise** aRetVal) { + nsIGlobalObject* globalObject = xpc::CurrentNativeGlobal(aCx); + if (NS_WARN_IF(!globalObject)) { + return NS_ERROR_UNEXPECTED; + } + + ErrorResult result; + RefPtr<Promise> promise = Promise::Create(globalObject, result); + if (NS_WARN_IF(result.Failed())) { + return result.StealNSResult(); + } + + mPersistentStorage->EnsureInitialized(); + + nsTArray<RefPtr<nsICookie>> cookieList; + + // We delete only non-private cookies. + mPersistentStorage->GetAll(cookieList); + + RefPtr<RemoveAllSinceRunnable> runMe = new RemoveAllSinceRunnable( + promise, this, std::move(cookieList), aSinceWhen); + + promise.forget(aRetVal); + + return runMe->Run(); +} + +namespace { + +class CompareCookiesCreationTime { + public: + static bool Equals(const nsICookie* aCookie1, const nsICookie* aCookie2) { + return static_cast<const Cookie*>(aCookie1)->CreationTime() == + static_cast<const Cookie*>(aCookie2)->CreationTime(); + } + + static bool LessThan(const nsICookie* aCookie1, const nsICookie* aCookie2) { + return static_cast<const Cookie*>(aCookie1)->CreationTime() < + static_cast<const Cookie*>(aCookie2)->CreationTime(); + } +}; + +} // namespace + +NS_IMETHODIMP +CookieService::GetCookiesSince(int64_t aSinceWhen, + nsTArray<RefPtr<nsICookie>>& aResult) { + if (!IsInitialized()) { + return NS_OK; + } + + mPersistentStorage->EnsureInitialized(); + + // We expose only non-private cookies. + nsTArray<RefPtr<nsICookie>> cookieList; + mPersistentStorage->GetAll(cookieList); + + for (RefPtr<nsICookie>& cookie : cookieList) { + if (static_cast<Cookie*>(cookie.get())->CreationTime() >= aSinceWhen) { + aResult.AppendElement(cookie); + } + } + + aResult.Sort(CompareCookiesCreationTime()); + return NS_OK; +} + +size_t CookieService::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const { + size_t n = aMallocSizeOf(this); + + if (mPersistentStorage) { + n += mPersistentStorage->SizeOfIncludingThis(aMallocSizeOf); + } + if (mPrivateStorage) { + n += mPrivateStorage->SizeOfIncludingThis(aMallocSizeOf); + } + + return n; +} + +MOZ_DEFINE_MALLOC_SIZE_OF(CookieServiceMallocSizeOf) + +NS_IMETHODIMP +CookieService::CollectReports(nsIHandleReportCallback* aHandleReport, + nsISupports* aData, bool /*aAnonymize*/) { + MOZ_COLLECT_REPORT("explicit/cookie-service", KIND_HEAP, UNITS_BYTES, + SizeOfIncludingThis(CookieServiceMallocSizeOf), + "Memory used by the cookie service."); + + return NS_OK; +} + +bool CookieService::IsInitialized() const { + if (!mPersistentStorage) { + NS_WARNING("No CookieStorage! Profile already close?"); + return false; + } + + MOZ_ASSERT(mPrivateStorage); + return true; +} + +CookieStorage* CookieService::PickStorage(const OriginAttributes& aAttrs) { + MOZ_ASSERT(IsInitialized()); + + if (aAttrs.mPrivateBrowsingId > 0) { + return mPrivateStorage; + } + + mPersistentStorage->EnsureInitialized(); + return mPersistentStorage; +} + +CookieStorage* CookieService::PickStorage( + const OriginAttributesPattern& aAttrs) { + MOZ_ASSERT(IsInitialized()); + + if (aAttrs.mPrivateBrowsingId.WasPassed() && + aAttrs.mPrivateBrowsingId.Value() > 0) { + return mPrivateStorage; + } + + mPersistentStorage->EnsureInitialized(); + return mPersistentStorage; +} + +bool CookieService::SetCookiesFromIPC(const nsACString& aBaseDomain, + const OriginAttributes& aAttrs, + nsIURI* aHostURI, bool aFromHttp, + const nsTArray<CookieStruct>& aCookies, + BrowsingContext* aBrowsingContext) { + if (!IsInitialized()) { + // If we are probably shutting down, we can ignore this cookie. + return true; + } + + CookieStorage* storage = PickStorage(aAttrs); + int64_t currentTimeInUsec = PR_Now(); + + for (const CookieStruct& cookieData : aCookies) { + if (!CookieCommons::CheckPathSize(cookieData)) { + return false; + } + + // reject cookie if it's over the size limit, per RFC2109 + if (!CookieCommons::CheckNameAndValueSize(cookieData)) { + return false; + } + + RecordUnicodeTelemetry(cookieData); + + if (!CookieCommons::CheckName(cookieData)) { + return false; + } + + if (!CookieCommons::CheckValue(cookieData)) { + return false; + } + + // create a new Cookie and copy attributes + RefPtr<Cookie> cookie = Cookie::Create(cookieData, aAttrs); + if (!cookie) { + continue; + } + + cookie->SetLastAccessed(currentTimeInUsec); + cookie->SetCreationTime( + Cookie::GenerateUniqueCreationTime(currentTimeInUsec)); + + storage->AddCookie(nullptr, aBaseDomain, aAttrs, cookie, currentTimeInUsec, + aHostURI, ""_ns, aFromHttp, aBrowsingContext); + } + + return true; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/cookie/CookieService.h b/netwerk/cookie/CookieService.h new file mode 100644 index 0000000000..09eb4c1289 --- /dev/null +++ b/netwerk/cookie/CookieService.h @@ -0,0 +1,163 @@ +/* -*- 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/. */ + +#ifndef mozilla_net_CookieService_h +#define mozilla_net_CookieService_h + +#include "nsICookieService.h" +#include "nsICookieManager.h" +#include "nsIObserver.h" +#include "nsWeakReference.h" + +#include "Cookie.h" +#include "CookieCommons.h" + +#include "nsString.h" +#include "nsIMemoryReporter.h" +#include "mozilla/MemoryReporting.h" + +class nsIConsoleReportCollector; +class nsICookieJarSettings; +class nsIEffectiveTLDService; +class nsIIDNService; +class nsIURI; +class nsIChannel; +class mozIThirdPartyUtil; + +namespace mozilla { +namespace net { + +class CookiePersistentStorage; +class CookiePrivateStorage; +class CookieStorage; + +/****************************************************************************** + * CookieService: + * class declaration + ******************************************************************************/ + +class CookieService final : public nsICookieService, + public nsICookieManager, + public nsIObserver, + public nsSupportsWeakReference, + public nsIMemoryReporter { + private: + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const; + + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + NS_DECL_NSICOOKIESERVICE + NS_DECL_NSICOOKIEMANAGER + NS_DECL_NSIMEMORYREPORTER + + static already_AddRefed<CookieService> GetSingleton(); + + CookieService(); + static already_AddRefed<nsICookieService> GetXPCOMSingleton(); + nsresult Init(); + + /** + * Start watching the observer service for messages indicating that an app has + * been uninstalled. When an app is uninstalled, we get the cookie service + * (thus instantiating it, if necessary) and clear all the cookies for that + * app. + */ + + static bool CanSetCookie(nsIURI* aHostURI, const nsACString& aBaseDomain, + CookieStruct& aCookieData, bool aRequireHostMatch, + CookieStatus aStatus, nsCString& aCookieHeader, + bool aFromHttp, bool aIsForeignAndNotAddon, + bool aPartitionedOnly, + nsIConsoleReportCollector* aCRC, bool& aSetCookie); + static CookieStatus CheckPrefs( + nsIConsoleReportCollector* aCRC, nsICookieJarSettings* aCookieJarSettings, + nsIURI* aHostURI, bool aIsForeign, bool aIsThirdPartyTrackingResource, + bool aIsThirdPartySocialTrackingResource, + bool aStorageAccessPermissionGranted, const nsACString& aCookieHeader, + const int aNumOfCookies, const OriginAttributes& aOriginAttrs, + uint32_t* aRejectedReason); + + void GetCookiesForURI(nsIURI* aHostURI, nsIChannel* aChannel, bool aIsForeign, + bool aIsThirdPartyTrackingResource, + bool aIsThirdPartySocialTrackingResource, + bool aStorageAccessPermissionGranted, + uint32_t aRejectedReason, bool aIsSafeTopLevelNav, + bool aIsSameSiteForeign, bool aHadCrossSiteRedirects, + bool aHttpBound, + bool aAllowSecureCookiesToInsecureOrigin, + const OriginAttributes& aOriginAttrs, + nsTArray<Cookie*>& aCookieList); + + /** + * This method is a helper that allows calling nsICookieManager::Remove() + * with OriginAttributes parameter. + */ + nsresult Remove(const nsACString& aHost, const OriginAttributes& aAttrs, + const nsACString& aName, const nsACString& aPath); + + bool SetCookiesFromIPC(const nsACString& aBaseDomain, + const OriginAttributes& aAttrs, nsIURI* aHostURI, + bool aFromHttp, const nsTArray<CookieStruct>& aCookies, + dom::BrowsingContext* aBrowsingContext); + + protected: + virtual ~CookieService(); + + bool IsInitialized() const; + + void InitCookieStorages(); + void CloseCookieStorages(); + + nsresult NormalizeHost(nsCString& aHost); + static bool GetTokenValue(nsACString::const_char_iterator& aIter, + nsACString::const_char_iterator& aEndIter, + nsDependentCSubstring& aTokenString, + nsDependentCSubstring& aTokenValue, + bool& aEqualsFound); + static bool ParseAttributes(nsIConsoleReportCollector* aCRC, nsIURI* aHostURI, + nsCString& aCookieHeader, + CookieStruct& aCookieData, nsACString& aExpires, + nsACString& aMaxage, bool& aAcceptedByParser); + static bool CheckDomain(CookieStruct& aCookieData, nsIURI* aHostURI, + const nsACString& aBaseDomain, + bool aRequireHostMatch); + static bool CheckHiddenPrefix(CookieStruct& aCookieData); + static bool CheckPath(CookieStruct& aCookieData, + nsIConsoleReportCollector* aCRC, nsIURI* aHostURI); + static bool CheckPrefixes(CookieStruct& aCookieData, bool aSecureRequest); + static bool GetExpiry(CookieStruct& aCookieData, const nsACString& aExpires, + const nsACString& aMaxage, int64_t aCurrentTime, + bool aFromHttp); + void NotifyAccepted(nsIChannel* aChannel); + + nsresult GetCookiesWithOriginAttributes( + const OriginAttributesPattern& aPattern, const nsCString& aBaseDomain, + nsTArray<RefPtr<nsICookie>>& aResult); + nsresult RemoveCookiesWithOriginAttributes( + const OriginAttributesPattern& aPattern, const nsCString& aBaseDomain); + + protected: + CookieStorage* PickStorage(const OriginAttributes& aAttrs); + CookieStorage* PickStorage(const OriginAttributesPattern& aAttrs); + + nsresult RemoveCookiesFromExactHost(const nsACString& aHost, + const OriginAttributesPattern& aPattern); + + // cached members. + nsCOMPtr<mozIThirdPartyUtil> mThirdPartyUtil; + nsCOMPtr<nsIEffectiveTLDService> mTLDService; + nsCOMPtr<nsIIDNService> mIDNService; + + // we have two separate Cookie Storages: one for normal browsing and one for + // private browsing. + RefPtr<CookieStorage> mPersistentStorage; + RefPtr<CookieStorage> mPrivateStorage; +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_CookieService_h diff --git a/netwerk/cookie/CookieServiceChild.cpp b/netwerk/cookie/CookieServiceChild.cpp new file mode 100644 index 0000000000..a005b5dbe7 --- /dev/null +++ b/netwerk/cookie/CookieServiceChild.cpp @@ -0,0 +1,653 @@ +/* -*- 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 "CookieService.h" +#include "mozilla/net/CookieServiceChild.h" +#include "ErrorList.h" +#include "mozilla/net/HttpChannelChild.h" +#include "mozilla/net/NeckoChannelParams.h" +#include "mozilla/LoadInfo.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/Document.h" +#include "mozilla/ipc/URIUtils.h" +#include "mozilla/net/NeckoChild.h" +#include "mozilla/StaticPrefs_network.h" +#include "mozilla/StoragePrincipalHelper.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsICookieJarSettings.h" +#include "nsIChannel.h" +#include "nsIClassifiedChannel.h" +#include "nsIHttpChannel.h" +#include "nsIEffectiveTLDService.h" +#include "nsIURI.h" +#include "nsIPrefBranch.h" +#include "nsIWebProgressListener.h" +#include "nsQueryObject.h" +#include "nsServiceManagerUtils.h" +#include "mozilla/Telemetry.h" +#include "mozilla/TimeStamp.h" +#include "ThirdPartyUtil.h" +#include "nsIConsoleReportCollector.h" +#include "mozilla/dom/WindowGlobalChild.h" + +using namespace mozilla::ipc; + +namespace mozilla { +namespace net { + +static StaticRefPtr<CookieServiceChild> gCookieChildService; + +already_AddRefed<CookieServiceChild> CookieServiceChild::GetSingleton() { + if (!gCookieChildService) { + gCookieChildService = new CookieServiceChild(); + gCookieChildService->Init(); + ClearOnShutdown(&gCookieChildService); + } + + return do_AddRef(gCookieChildService); +} + +NS_IMPL_ISUPPORTS(CookieServiceChild, nsICookieService, + nsISupportsWeakReference) + +CookieServiceChild::CookieServiceChild() { NeckoChild::InitNeckoChild(); } + +CookieServiceChild::~CookieServiceChild() { gCookieChildService = nullptr; } + +void CookieServiceChild::Init() { + auto* cc = static_cast<mozilla::dom::ContentChild*>(gNeckoChild->Manager()); + if (cc->IsShuttingDown()) { + return; + } + + // This corresponds to Release() in DeallocPCookieService. + NS_ADDREF_THIS(); + + // Create a child PCookieService actor. Don't do this in the constructor + // since it could release 'this' on failure + gNeckoChild->SendPCookieServiceConstructor(this); + + mThirdPartyUtil = ThirdPartyUtil::GetInstance(); + NS_ASSERTION(mThirdPartyUtil, "couldn't get ThirdPartyUtil service"); + + mTLDService = do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID); + NS_ASSERTION(mTLDService, "couldn't get TLDService"); +} + +RefPtr<GenericPromise> CookieServiceChild::TrackCookieLoad( + nsIChannel* aChannel) { + if (!CanSend()) { + return GenericPromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE, __func__); + } + + uint32_t rejectedReason = 0; + ThirdPartyAnalysisResult result = mThirdPartyUtil->AnalyzeChannel( + aChannel, true, nullptr, RequireThirdPartyCheck, &rejectedReason); + + nsCOMPtr<nsIURI> uri; + aChannel->GetURI(getter_AddRefs(uri)); + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + + OriginAttributes attrs = loadInfo->GetOriginAttributes(); + StoragePrincipalHelper::PrepareEffectiveStoragePrincipalOriginAttributes( + aChannel, attrs); + + bool isSafeTopLevelNav = CookieCommons::IsSafeTopLevelNav(aChannel); + bool hadCrossSiteRedirects = false; + bool isSameSiteForeign = + CookieCommons::IsSameSiteForeign(aChannel, uri, &hadCrossSiteRedirects); + + RefPtr<CookieServiceChild> self(this); + + return SendGetCookieList( + uri, result.contains(ThirdPartyAnalysis::IsForeign), + result.contains(ThirdPartyAnalysis::IsThirdPartyTrackingResource), + result.contains( + ThirdPartyAnalysis::IsThirdPartySocialTrackingResource), + result.contains( + ThirdPartyAnalysis::IsStorageAccessPermissionGranted), + rejectedReason, isSafeTopLevelNav, isSameSiteForeign, + hadCrossSiteRedirects, attrs) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [self, uri, attrs](const nsTArray<CookieStruct>& aCookiesList) { + for (uint32_t i = 0; i < aCookiesList.Length(); i++) { + RefPtr<Cookie> cookie = Cookie::Create(aCookiesList[i], attrs); + cookie->SetIsHttpOnly(false); + self->RecordDocumentCookie(cookie, attrs); + } + return GenericPromise::CreateAndResolve(true, __func__); + }, + [](const mozilla::ipc::ResponseRejectReason) { + return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); + }); +} + +IPCResult CookieServiceChild::RecvRemoveAll() { + mCookiesMap.Clear(); + + nsCOMPtr<nsIObserverService> obsService = services::GetObserverService(); + if (obsService) { + obsService->NotifyObservers(nullptr, "content-removed-all-cookies", + nullptr); + } + return IPC_OK(); +} + +IPCResult CookieServiceChild::RecvRemoveCookie(const CookieStruct& aCookie, + const OriginAttributes& aAttrs) { + RemoveSingleCookie(aCookie, aAttrs); + + nsCOMPtr<nsIObserverService> obsService = services::GetObserverService(); + if (obsService) { + obsService->NotifyObservers(nullptr, "content-removed-cookie", nullptr); + } + return IPC_OK(); +} + +void CookieServiceChild::RemoveSingleCookie(const CookieStruct& aCookie, + const OriginAttributes& aAttrs) { + nsCString baseDomain; + CookieCommons::GetBaseDomainFromHost(mTLDService, aCookie.host(), baseDomain); + CookieKey key(baseDomain, aAttrs); + CookiesList* cookiesList = nullptr; + mCookiesMap.Get(key, &cookiesList); + + if (!cookiesList) { + return; + } + + for (uint32_t i = 0; i < cookiesList->Length(); i++) { + Cookie* cookie = cookiesList->ElementAt(i); + // bug 1858366: In the case that we are updating a stale cookie + // from the content process: the parent process will signal + // a batch deletion for the old cookie. + // When received by the content process we should not remove + // the new cookie since we have already updated the content + // process cookies. So we also check the expiry here. + if (cookie->Name().Equals(aCookie.name()) && + cookie->Host().Equals(aCookie.host()) && + cookie->Path().Equals(aCookie.path()) && + cookie->Expiry() <= aCookie.expiry()) { + cookiesList->RemoveElementAt(i); + break; + } + } +} + +IPCResult CookieServiceChild::RecvAddCookie(const CookieStruct& aCookie, + const OriginAttributes& aAttrs) { + RefPtr<Cookie> cookie = Cookie::Create(aCookie, aAttrs); + RecordDocumentCookie(cookie, aAttrs); + + // signal test code to check their cookie list + nsCOMPtr<nsIObserverService> obsService = services::GetObserverService(); + if (obsService) { + obsService->NotifyObservers(nullptr, "content-added-cookie", nullptr); + } + + return IPC_OK(); +} + +IPCResult CookieServiceChild::RecvRemoveBatchDeletedCookies( + nsTArray<CookieStruct>&& aCookiesList, + nsTArray<OriginAttributes>&& aAttrsList) { + MOZ_ASSERT(aCookiesList.Length() == aAttrsList.Length()); + for (uint32_t i = 0; i < aCookiesList.Length(); i++) { + CookieStruct cookieStruct = aCookiesList.ElementAt(i); + RemoveSingleCookie(cookieStruct, aAttrsList.ElementAt(i)); + } + + nsCOMPtr<nsIObserverService> obsService = services::GetObserverService(); + if (obsService) { + obsService->NotifyObservers(nullptr, "content-batch-deleted-cookies", + nullptr); + } + return IPC_OK(); +} + +IPCResult CookieServiceChild::RecvTrackCookiesLoad( + nsTArray<CookieStruct>&& aCookiesList, const OriginAttributes& aAttrs) { + for (uint32_t i = 0; i < aCookiesList.Length(); i++) { + RefPtr<Cookie> cookie = Cookie::Create(aCookiesList[i], aAttrs); + cookie->SetIsHttpOnly(false); + RecordDocumentCookie(cookie, aAttrs); + } + + nsCOMPtr<nsIObserverService> obsService = services::GetObserverService(); + if (obsService) { + obsService->NotifyObservers(nullptr, "content-track-cookies-loaded", + nullptr); + } + + return IPC_OK(); +} + +uint32_t CookieServiceChild::CountCookiesFromHashTable( + const nsACString& aBaseDomain, const OriginAttributes& aOriginAttrs) { + CookiesList* cookiesList = nullptr; + + nsCString baseDomain; + CookieKey key(aBaseDomain, aOriginAttrs); + mCookiesMap.Get(key, &cookiesList); + + return cookiesList ? cookiesList->Length() : 0; +} + +/* static */ bool CookieServiceChild::RequireThirdPartyCheck( + nsILoadInfo* aLoadInfo) { + if (!aLoadInfo) { + return false; + } + + nsCOMPtr<nsICookieJarSettings> cookieJarSettings; + nsresult rv = + aLoadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + uint32_t cookieBehavior = cookieJarSettings->GetCookieBehavior(); + return cookieBehavior == nsICookieService::BEHAVIOR_REJECT_FOREIGN || + cookieBehavior == nsICookieService::BEHAVIOR_LIMIT_FOREIGN || + cookieBehavior == nsICookieService::BEHAVIOR_REJECT_TRACKER || + cookieBehavior == + nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN || + StaticPrefs::network_cookie_thirdparty_sessionOnly() || + StaticPrefs::network_cookie_thirdparty_nonsecureSessionOnly(); +} + +void CookieServiceChild::RecordDocumentCookie(Cookie* aCookie, + const OriginAttributes& aAttrs) { + nsAutoCString baseDomain; + CookieCommons::GetBaseDomainFromHost(mTLDService, aCookie->Host(), + baseDomain); + + CookieKey key(baseDomain, aAttrs); + CookiesList* cookiesList = nullptr; + mCookiesMap.Get(key, &cookiesList); + + if (!cookiesList) { + cookiesList = mCookiesMap.GetOrInsertNew(key); + } + for (uint32_t i = 0; i < cookiesList->Length(); i++) { + Cookie* cookie = cookiesList->ElementAt(i); + if (cookie->Name().Equals(aCookie->Name()) && + cookie->Host().Equals(aCookie->Host()) && + cookie->Path().Equals(aCookie->Path())) { + if (cookie->Value().Equals(aCookie->Value()) && + cookie->Expiry() == aCookie->Expiry() && + cookie->IsSecure() == aCookie->IsSecure() && + cookie->SameSite() == aCookie->SameSite() && + cookie->RawSameSite() == aCookie->RawSameSite() && + cookie->IsSession() == aCookie->IsSession() && + cookie->IsHttpOnly() == aCookie->IsHttpOnly()) { + cookie->SetLastAccessed(aCookie->LastAccessed()); + return; + } + cookiesList->RemoveElementAt(i); + break; + } + } + + int64_t currentTime = PR_Now() / PR_USEC_PER_SEC; + if (aCookie->Expiry() <= currentTime) { + return; + } + + cookiesList->AppendElement(aCookie); +} + +NS_IMETHODIMP +CookieServiceChild::GetCookieStringFromDocument(dom::Document* aDocument, + nsACString& aCookieString) { + NS_ENSURE_ARG(aDocument); + + aCookieString.Truncate(); + + nsCOMPtr<nsIPrincipal> principal = aDocument->EffectiveCookiePrincipal(); + + if (!CookieCommons::IsSchemeSupported(principal)) { + return NS_OK; + } + + nsAutoCString baseDomain; + nsresult rv = CookieCommons::GetBaseDomain(principal, baseDomain); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_OK; + } + + CookieKey key(baseDomain, principal->OriginAttributesRef()); + CookiesList* cookiesList = nullptr; + mCookiesMap.Get(key, &cookiesList); + + if (!cookiesList) { + return NS_OK; + } + + nsAutoCString hostFromURI; + rv = nsContentUtils::GetHostOrIPv6WithBrackets(principal, hostFromURI); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_OK; + } + + nsAutoCString pathFromURI; + principal->GetFilePath(pathFromURI); + + bool thirdParty = true; + nsPIDOMWindowInner* innerWindow = aDocument->GetInnerWindow(); + // in gtests we don't have a window, let's consider those requests as 3rd + // party. + if (innerWindow) { + ThirdPartyUtil* thirdPartyUtil = ThirdPartyUtil::GetInstance(); + + if (thirdPartyUtil) { + Unused << thirdPartyUtil->IsThirdPartyWindow( + innerWindow->GetOuterWindow(), nullptr, &thirdParty); + } + } + + bool isPotentiallyTrustworthy = + principal->GetIsOriginPotentiallyTrustworthy(); + int64_t currentTimeInUsec = PR_Now(); + int64_t currentTime = currentTimeInUsec / PR_USEC_PER_SEC; + + cookiesList->Sort(CompareCookiesForSending()); + for (uint32_t i = 0; i < cookiesList->Length(); i++) { + Cookie* cookie = cookiesList->ElementAt(i); + // check the host, since the base domain lookup is conservative. + if (!CookieCommons::DomainMatches(cookie, hostFromURI)) { + continue; + } + + // We don't show HttpOnly cookies in content processes. + if (cookie->IsHttpOnly()) { + continue; + } + + if (thirdParty && !CookieCommons::ShouldIncludeCrossSiteCookieForDocument( + cookie, aDocument)) { + continue; + } + + // do not display the cookie if it is secure and the host scheme isn't + if (cookie->IsSecure() && !isPotentiallyTrustworthy) { + continue; + } + + // if the nsIURI path doesn't match the cookie path, don't send it back + if (!CookieCommons::PathMatches(cookie, pathFromURI)) { + continue; + } + + // check if the cookie has expired + if (cookie->Expiry() <= currentTime) { + continue; + } + + if (!cookie->Name().IsEmpty() || !cookie->Value().IsEmpty()) { + if (!aCookieString.IsEmpty()) { + aCookieString.AppendLiteral("; "); + } + if (!cookie->Name().IsEmpty()) { + aCookieString.Append(cookie->Name().get()); + aCookieString.AppendLiteral("="); + aCookieString.Append(cookie->Value().get()); + } else { + aCookieString.Append(cookie->Value().get()); + } + } + } + + return NS_OK; +} + +NS_IMETHODIMP +CookieServiceChild::GetCookieStringFromHttp(nsIURI* /*aHostURI*/, + nsIChannel* /*aChannel*/, + nsACString& /*aCookieString*/) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +CookieServiceChild::SetCookieStringFromDocument( + dom::Document* aDocument, const nsACString& aCookieString) { + NS_ENSURE_ARG(aDocument); + + nsCOMPtr<nsIURI> documentURI; + nsAutoCString baseDomain; + OriginAttributes attrs; + + // This function is executed in this context, I don't need to keep objects + // alive. + auto hasExistingCookiesLambda = [&](const nsACString& aBaseDomain, + const OriginAttributes& aAttrs) { + return !!CountCookiesFromHashTable(aBaseDomain, aAttrs); + }; + + RefPtr<Cookie> cookie = CookieCommons::CreateCookieFromDocument( + aDocument, aCookieString, PR_Now(), mTLDService, mThirdPartyUtil, + hasExistingCookiesLambda, getter_AddRefs(documentURI), baseDomain, attrs); + if (!cookie) { + return NS_OK; + } + + bool thirdParty = true; + nsPIDOMWindowInner* innerWindow = aDocument->GetInnerWindow(); + // in gtests we don't have a window, let's consider those requests as 3rd + // party. + if (innerWindow) { + ThirdPartyUtil* thirdPartyUtil = ThirdPartyUtil::GetInstance(); + + if (thirdPartyUtil) { + Unused << thirdPartyUtil->IsThirdPartyWindow( + innerWindow->GetOuterWindow(), nullptr, &thirdParty); + } + } + + if (thirdParty && !CookieCommons::ShouldIncludeCrossSiteCookieForDocument( + cookie, aDocument)) { + return NS_OK; + } + + CookieKey key(baseDomain, attrs); + CookiesList* cookies = mCookiesMap.Get(key); + + if (cookies) { + // We need to see if the cookie we're setting would overwrite an httponly + // or a secure one. This would not affect anything we send over the net + // (those come from the parent, which already checks this), + // but script could see an inconsistent view of things. + + nsCOMPtr<nsIPrincipal> principal = aDocument->EffectiveCookiePrincipal(); + bool isPotentiallyTrustworthy = + principal->GetIsOriginPotentiallyTrustworthy(); + + for (uint32_t i = 0; i < cookies->Length(); ++i) { + RefPtr<Cookie> existingCookie = cookies->ElementAt(i); + if (existingCookie->Name().Equals(cookie->Name()) && + existingCookie->Host().Equals(cookie->Host()) && + existingCookie->Path().Equals(cookie->Path())) { + // Can't overwrite an httponly cookie from a script context. + if (existingCookie->IsHttpOnly()) { + return NS_OK; + } + + // prevent insecure cookie from overwriting a secure one in insecure + // context. + if (existingCookie->IsSecure() && !isPotentiallyTrustworthy) { + return NS_OK; + } + } + } + } + + RecordDocumentCookie(cookie, attrs); + + if (CanSend()) { + nsTArray<CookieStruct> cookiesToSend; + cookiesToSend.AppendElement(cookie->ToIPC()); + + // Asynchronously call the parent. + dom::WindowGlobalChild* windowGlobalChild = + aDocument->GetWindowGlobalChild(); + + // If there is no WindowGlobalChild fall back to PCookieService SetCookies. + if (NS_WARN_IF(!windowGlobalChild)) { + SendSetCookies(baseDomain, attrs, documentURI, false, cookiesToSend); + return NS_OK; + } + windowGlobalChild->SendSetCookies(baseDomain, attrs, documentURI, false, + cookiesToSend); + } + + return NS_OK; +} + +NS_IMETHODIMP +CookieServiceChild::SetCookieStringFromHttp(nsIURI* aHostURI, + const nsACString& aCookieString, + nsIChannel* aChannel) { + NS_ENSURE_ARG(aHostURI); + NS_ENSURE_ARG(aChannel); + + if (!CookieCommons::IsSchemeSupported(aHostURI)) { + return NS_OK; + } + + // Fast past: don't bother sending IPC messages about nullprincipal'd + // documents. + nsAutoCString scheme; + aHostURI->GetScheme(scheme); + if (scheme.EqualsLiteral("moz-nullprincipal")) { + return NS_OK; + } + + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + + uint32_t rejectedReason = 0; + ThirdPartyAnalysisResult result = mThirdPartyUtil->AnalyzeChannel( + aChannel, false, aHostURI, RequireThirdPartyCheck, &rejectedReason); + + nsCString cookieString(aCookieString); + + OriginAttributes attrs = loadInfo->GetOriginAttributes(); + StoragePrincipalHelper::PrepareEffectiveStoragePrincipalOriginAttributes( + aChannel, attrs); + + bool requireHostMatch; + nsCString baseDomain; + CookieCommons::GetBaseDomain(mTLDService, aHostURI, baseDomain, + requireHostMatch); + + nsCOMPtr<nsICookieJarSettings> cookieJarSettings = + CookieCommons::GetCookieJarSettings(aChannel); + + nsCOMPtr<nsIConsoleReportCollector> crc = do_QueryInterface(aChannel); + + CookieStatus cookieStatus = CookieService::CheckPrefs( + crc, cookieJarSettings, aHostURI, + result.contains(ThirdPartyAnalysis::IsForeign), + result.contains(ThirdPartyAnalysis::IsThirdPartyTrackingResource), + result.contains(ThirdPartyAnalysis::IsThirdPartySocialTrackingResource), + result.contains(ThirdPartyAnalysis::IsStorageAccessPermissionGranted), + aCookieString, CountCookiesFromHashTable(baseDomain, attrs), attrs, + &rejectedReason); + + if (cookieStatus != STATUS_ACCEPTED && + cookieStatus != STATUS_ACCEPT_SESSION) { + return NS_OK; + } + + CookieKey key(baseDomain, attrs); + + nsTArray<CookieStruct> cookiesToSend; + + int64_t currentTimeInUsec = PR_Now(); + + bool addonAllowsLoad = false; + nsCOMPtr<nsIURI> finalChannelURI; + NS_GetFinalChannelURI(aChannel, getter_AddRefs(finalChannelURI)); + addonAllowsLoad = BasePrincipal::Cast(loadInfo->TriggeringPrincipal()) + ->AddonAllowsLoad(finalChannelURI); + + bool isForeignAndNotAddon = false; + if (!addonAllowsLoad) { + mThirdPartyUtil->IsThirdPartyChannel(aChannel, aHostURI, + &isForeignAndNotAddon); + } + + bool mustBePartitioned = + isForeignAndNotAddon && + cookieJarSettings->GetCookieBehavior() == + nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN && + !result.contains(ThirdPartyAnalysis::IsStorageAccessPermissionGranted); + + bool moreCookies; + do { + CookieStruct cookieData; + bool canSetCookie = false; + moreCookies = CookieService::CanSetCookie( + aHostURI, baseDomain, cookieData, requireHostMatch, cookieStatus, + cookieString, true, isForeignAndNotAddon, mustBePartitioned, crc, + canSetCookie); + if (!canSetCookie) { + continue; + } + + // check permissions from site permission list. + if (!CookieCommons::CheckCookiePermission(aChannel, cookieData)) { + COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieString, + "cookie rejected by permission manager"); + constexpr auto CONSOLE_REJECTION_CATEGORY = "cookiesRejection"_ns; + CookieLogging::LogMessageToConsole( + crc, aHostURI, nsIScriptError::warningFlag, + CONSOLE_REJECTION_CATEGORY, "CookieRejectedByPermissionManager"_ns, + AutoTArray<nsString, 1>{ + NS_ConvertUTF8toUTF16(cookieData.name()), + }); + CookieCommons::NotifyRejected( + aHostURI, aChannel, + nsIWebProgressListener::STATE_COOKIES_BLOCKED_BY_PERMISSION, + OPERATION_WRITE); + continue; + } + + RefPtr<Cookie> cookie = Cookie::Create(cookieData, attrs); + MOZ_ASSERT(cookie); + + cookie->SetLastAccessed(currentTimeInUsec); + cookie->SetCreationTime( + Cookie::GenerateUniqueCreationTime(currentTimeInUsec)); + + RecordDocumentCookie(cookie, attrs); + cookiesToSend.AppendElement(cookieData); + } while (moreCookies); + + // Asynchronously call the parent. + if (CanSend() && !cookiesToSend.IsEmpty()) { + RefPtr<HttpChannelChild> httpChannelChild = do_QueryObject(aChannel); + MOZ_ASSERT(httpChannelChild); + httpChannelChild->SendSetCookies(baseDomain, attrs, aHostURI, true, + cookiesToSend); + } + + return NS_OK; +} + +NS_IMETHODIMP +CookieServiceChild::RunInTransaction( + nsICookieTransactionCallback* /*aCallback*/) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/cookie/CookieServiceChild.h b/netwerk/cookie/CookieServiceChild.h new file mode 100644 index 0000000000..b9caa2aaa7 --- /dev/null +++ b/netwerk/cookie/CookieServiceChild.h @@ -0,0 +1,82 @@ +/* -*- 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/. */ + +#ifndef mozilla_net_CookieServiceChild_h__ +#define mozilla_net_CookieServiceChild_h__ + +#include "CookieKey.h" +#include "mozilla/net/PCookieServiceChild.h" +#include "nsClassHashtable.h" +#include "nsICookieService.h" +#include "mozIThirdPartyUtil.h" +#include "nsWeakReference.h" +#include "nsThreadUtils.h" + +class nsIEffectiveTLDService; +class nsILoadInfo; + +namespace mozilla { +namespace net { + +class Cookie; +class CookieStruct; + +class CookieServiceChild final : public PCookieServiceChild, + public nsICookieService, + public nsSupportsWeakReference { + friend class PCookieServiceChild; + + public: + NS_DECL_ISUPPORTS + NS_DECL_NSICOOKIESERVICE + + typedef nsTArray<RefPtr<Cookie>> CookiesList; + typedef nsClassHashtable<CookieKey, CookiesList> CookiesMap; + + CookieServiceChild(); + + void Init(); + + static already_AddRefed<CookieServiceChild> GetSingleton(); + + RefPtr<GenericPromise> TrackCookieLoad(nsIChannel* aChannel); + + private: + ~CookieServiceChild(); + + void RecordDocumentCookie(Cookie* aCookie, const OriginAttributes& aAttrs); + + uint32_t CountCookiesFromHashTable(const nsACString& aBaseDomain, + const OriginAttributes& aOriginAttrs); + + static bool RequireThirdPartyCheck(nsILoadInfo* aLoadInfo); + + mozilla::ipc::IPCResult RecvTrackCookiesLoad( + nsTArray<CookieStruct>&& aCookiesList, const OriginAttributes& aAttrs); + + mozilla::ipc::IPCResult RecvRemoveAll(); + + mozilla::ipc::IPCResult RecvRemoveBatchDeletedCookies( + nsTArray<CookieStruct>&& aCookiesList, + nsTArray<OriginAttributes>&& aAttrsList); + + mozilla::ipc::IPCResult RecvRemoveCookie(const CookieStruct& aCookie, + const OriginAttributes& aAttrs); + + mozilla::ipc::IPCResult RecvAddCookie(const CookieStruct& aCookie, + const OriginAttributes& aAttrs); + + void RemoveSingleCookie(const CookieStruct& aCookie, + const OriginAttributes& aAttrs); + + CookiesMap mCookiesMap; + nsCOMPtr<mozIThirdPartyUtil> mThirdPartyUtil; + nsCOMPtr<nsIEffectiveTLDService> mTLDService; +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_CookieServiceChild_h__ diff --git a/netwerk/cookie/CookieServiceParent.cpp b/netwerk/cookie/CookieServiceParent.cpp new file mode 100644 index 0000000000..c78a67513f --- /dev/null +++ b/netwerk/cookie/CookieServiceParent.cpp @@ -0,0 +1,268 @@ +/* -*- 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 "CookieCommons.h" +#include "CookieLogging.h" +#include "mozilla/net/CookieService.h" +#include "mozilla/net/CookieServiceParent.h" +#include "mozilla/net/NeckoParent.h" + +#include "mozilla/ipc/URIUtils.h" +#include "mozilla/StoragePrincipalHelper.h" +#include "mozIThirdPartyUtil.h" +#include "nsArrayUtils.h" +#include "nsIChannel.h" +#include "nsIEffectiveTLDService.h" +#include "nsNetCID.h" +#include "nsMixedContentBlocker.h" + +using namespace mozilla::ipc; + +namespace mozilla { +namespace net { + +CookieServiceParent::CookieServiceParent() { + // Instantiate the cookieservice via the service manager, so it sticks around + // until shutdown. + nsCOMPtr<nsICookieService> cs = do_GetService(NS_COOKIESERVICE_CONTRACTID); + + // Get the CookieService instance directly, so we can call internal methods. + mCookieService = CookieService::GetSingleton(); + NS_ASSERTION(mCookieService, "couldn't get nsICookieService"); + + mTLDService = do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID); + MOZ_ALWAYS_TRUE(mTLDService); + + mProcessingCookie = false; +} + +void CookieServiceParent::RemoveBatchDeletedCookies(nsIArray* aCookieList) { + uint32_t len = 0; + aCookieList->GetLength(&len); + OriginAttributes attrs; + CookieStruct cookieStruct; + nsTArray<CookieStruct> cookieStructList; + nsTArray<OriginAttributes> attrsList; + for (uint32_t i = 0; i < len; i++) { + nsCOMPtr<nsICookie> xpcCookie = do_QueryElementAt(aCookieList, i); + const auto& cookie = xpcCookie->AsCookie(); + attrs = cookie.OriginAttributesRef(); + cookieStruct = cookie.ToIPC(); + + // Child only needs to know HttpOnly cookies exists, not its value + // Same for Secure cookies going to a process for an insecure site. + if (cookie.IsHttpOnly() || !InsecureCookieOrSecureOrigin(cookie)) { + cookieStruct.value() = ""; + } + cookieStructList.AppendElement(cookieStruct); + attrsList.AppendElement(attrs); + } + Unused << SendRemoveBatchDeletedCookies(cookieStructList, attrsList); +} + +void CookieServiceParent::RemoveAll() { Unused << SendRemoveAll(); } + +void CookieServiceParent::RemoveCookie(const Cookie& cookie) { + const OriginAttributes& attrs = cookie.OriginAttributesRef(); + CookieStruct cookieStruct = cookie.ToIPC(); + + // Child only needs to know HttpOnly cookies exists, not its value + // Same for Secure cookies going to a process for an insecure site. + if (cookie.IsHttpOnly() || !InsecureCookieOrSecureOrigin(cookie)) { + cookieStruct.value() = ""; + } + Unused << SendRemoveCookie(cookieStruct, attrs); +} + +void CookieServiceParent::AddCookie(const Cookie& cookie) { + const OriginAttributes& attrs = cookie.OriginAttributesRef(); + CookieStruct cookieStruct = cookie.ToIPC(); + + // Child only needs to know HttpOnly cookies exists, not its value + // Same for Secure cookies going to a process for an insecure site. + if (cookie.IsHttpOnly() || !InsecureCookieOrSecureOrigin(cookie)) { + cookieStruct.value() = ""; + } + Unused << SendAddCookie(cookieStruct, attrs); +} + +bool CookieServiceParent::ContentProcessHasCookie(const Cookie& cookie) { + nsCString baseDomain; + if (NS_WARN_IF(NS_FAILED(CookieCommons::GetBaseDomainFromHost( + mTLDService, cookie.Host(), baseDomain)))) { + return false; + } + + CookieKey cookieKey(baseDomain, cookie.OriginAttributesRef()); + return mCookieKeysInContent.MaybeGet(cookieKey).isSome(); +} + +bool CookieServiceParent::InsecureCookieOrSecureOrigin(const Cookie& cookie) { + nsCString baseDomain; + // CookieStorage notifications triggering this won't fail to get base domain + MOZ_ALWAYS_SUCCEEDS(CookieCommons::GetBaseDomainFromHost( + mTLDService, cookie.Host(), baseDomain)); + + // cookie is insecure or cookie is associated with a secure-origin process + CookieKey cookieKey(baseDomain, cookie.OriginAttributesRef()); + if (Maybe<bool> allowSecure = mCookieKeysInContent.MaybeGet(cookieKey)) { + return (!cookie.IsSecure() || *allowSecure); + } + return false; +} + +void CookieServiceParent::TrackCookieLoad(nsIChannel* aChannel) { + nsCOMPtr<nsIURI> uri; + aChannel->GetURI(getter_AddRefs(uri)); + + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + OriginAttributes attrs = loadInfo->GetOriginAttributes(); + bool isSafeTopLevelNav = CookieCommons::IsSafeTopLevelNav(aChannel); + bool hadCrossSiteRedirects = false; + bool isSameSiteForeign = + CookieCommons::IsSameSiteForeign(aChannel, uri, &hadCrossSiteRedirects); + + StoragePrincipalHelper::PrepareEffectiveStoragePrincipalOriginAttributes( + aChannel, attrs); + + nsCOMPtr<mozIThirdPartyUtil> thirdPartyUtil; + thirdPartyUtil = do_GetService(THIRDPARTYUTIL_CONTRACTID); + + uint32_t rejectedReason = 0; + ThirdPartyAnalysisResult result = thirdPartyUtil->AnalyzeChannel( + aChannel, false, nullptr, nullptr, &rejectedReason); + + UpdateCookieInContentList(uri, attrs); + + // Send matching cookies to Child. + nsTArray<Cookie*> foundCookieList; + mCookieService->GetCookiesForURI( + uri, aChannel, result.contains(ThirdPartyAnalysis::IsForeign), + result.contains(ThirdPartyAnalysis::IsThirdPartyTrackingResource), + result.contains(ThirdPartyAnalysis::IsThirdPartySocialTrackingResource), + result.contains(ThirdPartyAnalysis::IsStorageAccessPermissionGranted), + rejectedReason, isSafeTopLevelNav, isSameSiteForeign, + hadCrossSiteRedirects, false, true, attrs, foundCookieList); + nsTArray<CookieStruct> matchingCookiesList; + SerializeCookieList(foundCookieList, matchingCookiesList, uri); + Unused << SendTrackCookiesLoad(matchingCookiesList, attrs); +} + +// we append outgoing cookie info into a list here so the ContentParent can +// filter cookies passing to unnecessary ContentProcesses +void CookieServiceParent::UpdateCookieInContentList( + nsIURI* uri, const OriginAttributes& originAttrs) { + nsCString baseDomain; + bool requireAHostMatch = false; + + // prevent malformed urls from being added to the cookie list + if (NS_WARN_IF(NS_FAILED(CookieCommons::GetBaseDomain( + mTLDService, uri, baseDomain, requireAHostMatch)))) { + return; + } + + CookieKey cookieKey(baseDomain, originAttrs); + bool& allowSecure = mCookieKeysInContent.LookupOrInsert(cookieKey, false); + allowSecure = + allowSecure || nsMixedContentBlocker::IsPotentiallyTrustworthyOrigin(uri); +} + +// static +void CookieServiceParent::SerializeCookieList( + const nsTArray<Cookie*>& aFoundCookieList, + nsTArray<CookieStruct>& aCookiesList, nsIURI* aHostURI) { + for (uint32_t i = 0; i < aFoundCookieList.Length(); i++) { + Cookie* cookie = aFoundCookieList.ElementAt(i); + CookieStruct* cookieStruct = aCookiesList.AppendElement(); + *cookieStruct = cookie->ToIPC(); + + // clear http-only cookie values + if (cookie->IsHttpOnly()) { + // Value only needs to exist if an HttpOnly cookie exists. + cookieStruct->value() = ""; + } + + // clear secure cookie values in insecure context + bool potentiallyTurstworthy = + nsMixedContentBlocker::IsPotentiallyTrustworthyOrigin(aHostURI); + if (cookie->IsSecure() && !potentiallyTurstworthy) { + cookieStruct->value() = ""; + } + } +} + +IPCResult CookieServiceParent::RecvGetCookieList( + nsIURI* aHost, const bool& aIsForeign, + const bool& aIsThirdPartyTrackingResource, + const bool& aIsThirdPartySocialTrackingResource, + const bool& aStorageAccessPermissionGranted, + const uint32_t& aRejectedReason, const bool& aIsSafeTopLevelNav, + const bool& aIsSameSiteForeign, const bool& aHadCrossSiteRedirects, + const OriginAttributes& aAttrs, GetCookieListResolver&& aResolve) { + // Send matching cookies to Child. + if (!aHost) { + return IPC_FAIL(this, "aHost must not be null"); + } + + // we append outgoing cookie info into a list here so the ContentParent can + // filter cookies that do not need to go to certain ContentProcesses + UpdateCookieInContentList(aHost, aAttrs); + + nsTArray<Cookie*> foundCookieList; + // Note: passing nullptr as aChannel to GetCookiesForURI() here is fine since + // this argument is only used for proper reporting of cookie loads, but the + // child process already does the necessary reporting in this case for us. + mCookieService->GetCookiesForURI( + aHost, nullptr, aIsForeign, aIsThirdPartyTrackingResource, + aIsThirdPartySocialTrackingResource, aStorageAccessPermissionGranted, + aRejectedReason, aIsSafeTopLevelNav, aIsSameSiteForeign, + aHadCrossSiteRedirects, false, true, aAttrs, foundCookieList); + + nsTArray<CookieStruct> matchingCookiesList; + SerializeCookieList(foundCookieList, matchingCookiesList, aHost); + + aResolve(matchingCookiesList); + + return IPC_OK(); +} + +void CookieServiceParent::ActorDestroy(ActorDestroyReason aWhy) { + // Nothing needed here. Called right before destructor since this is a + // non-refcounted class. +} + +IPCResult CookieServiceParent::RecvSetCookies( + const nsCString& aBaseDomain, const OriginAttributes& aOriginAttributes, + nsIURI* aHost, bool aFromHttp, const nsTArray<CookieStruct>& aCookies) { + return SetCookies(aBaseDomain, aOriginAttributes, aHost, aFromHttp, aCookies); +} + +IPCResult CookieServiceParent::SetCookies( + const nsCString& aBaseDomain, const OriginAttributes& aOriginAttributes, + nsIURI* aHost, bool aFromHttp, const nsTArray<CookieStruct>& aCookies, + dom::BrowsingContext* aBrowsingContext) { + if (!mCookieService) { + return IPC_OK(); + } + + // Deserialize URI. Having a host URI is mandatory and should always be + // provided by the child; thus we consider failure fatal. + if (!aHost) { + return IPC_FAIL(this, "aHost must not be null"); + } + + // We set this to true while processing this cookie update, to make sure + // we don't send it back to the same content process. + mProcessingCookie = true; + + bool ok = + mCookieService->SetCookiesFromIPC(aBaseDomain, aOriginAttributes, aHost, + aFromHttp, aCookies, aBrowsingContext); + mProcessingCookie = false; + return ok ? IPC_OK() : IPC_FAIL(this, "Invalid cookie received."); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/cookie/CookieServiceParent.h b/netwerk/cookie/CookieServiceParent.h new file mode 100644 index 0000000000..45b46883fb --- /dev/null +++ b/netwerk/cookie/CookieServiceParent.h @@ -0,0 +1,88 @@ +/* -*- 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/. */ + +#ifndef mozilla_net_CookieServiceParent_h +#define mozilla_net_CookieServiceParent_h + +#include "mozilla/net/PCookieServiceParent.h" +#include "mozilla/net/CookieKey.h" + +class nsIArray; +class nsICookie; +namespace mozilla { +class OriginAttributes; +} + +class nsIEffectiveTLDService; + +namespace mozilla { +namespace net { + +class Cookie; +class CookieService; + +class CookieServiceParent : public PCookieServiceParent { + friend class PCookieServiceParent; + + public: + CookieServiceParent(); + virtual ~CookieServiceParent() = default; + + void TrackCookieLoad(nsIChannel* aChannel); + + void RemoveBatchDeletedCookies(nsIArray* aCookieList); + + void RemoveAll(); + + void RemoveCookie(const Cookie& aCookie); + + void AddCookie(const Cookie& aCookie); + + // This will return true if the CookieServiceParent is currently processing + // an update from the content process. This is used in ContentParent to make + // sure that we are only forwarding those cookie updates to other content + // processes, not the one they originated from. + bool ProcessingCookie() { return mProcessingCookie; } + + bool ContentProcessHasCookie(const Cookie& cookie); + bool InsecureCookieOrSecureOrigin(const Cookie& cookie); + void UpdateCookieInContentList(nsIURI* aHostURI, + const OriginAttributes& aOriginAttrs); + + mozilla::ipc::IPCResult SetCookies( + const nsCString& aBaseDomain, const OriginAttributes& aOriginAttributes, + nsIURI* aHost, bool aFromHttp, const nsTArray<CookieStruct>& aCookies, + dom::BrowsingContext* aBrowsingContext = nullptr); + + protected: + virtual void ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult RecvSetCookies( + const nsCString& aBaseDomain, const OriginAttributes& aOriginAttributes, + nsIURI* aHost, bool aFromHttp, const nsTArray<CookieStruct>& aCookies); + + mozilla::ipc::IPCResult RecvGetCookieList( + nsIURI* aHost, const bool& aIsForeign, + const bool& aIsThirdPartyTrackingResource, + const bool& aIsThirdPartySocialTrackingResource, + const bool& aStorageAccessPermissionGranted, + const uint32_t& aRejectedReason, const bool& aIsSafeTopLevelNav, + const bool& aIsSameSiteForeign, const bool& aHadCrossSiteRedirects, + const OriginAttributes& aAttrs, GetCookieListResolver&& aResolve); + + static void SerializeCookieList(const nsTArray<Cookie*>& aFoundCookieList, + nsTArray<CookieStruct>& aCookiesList, + nsIURI* aHostURI); + + nsCOMPtr<nsIEffectiveTLDService> mTLDService; + RefPtr<CookieService> mCookieService; + bool mProcessingCookie; + nsTHashMap<CookieKey, bool> mCookieKeysInContent; +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_CookieServiceParent_h 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 diff --git a/netwerk/cookie/CookieStorage.h b/netwerk/cookie/CookieStorage.h new file mode 100644 index 0000000000..3836edbb9c --- /dev/null +++ b/netwerk/cookie/CookieStorage.h @@ -0,0 +1,216 @@ +/* -*- 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/. */ + +#ifndef mozilla_net_CookieStorage_h +#define mozilla_net_CookieStorage_h + +#include "CookieKey.h" + +#include "nsICookieNotification.h" +#include "nsIObserver.h" +#include "nsTHashtable.h" +#include "nsWeakReference.h" +#include <functional> +#include "CookieCommons.h" + +class nsIArray; +class nsICookie; +class nsICookieTransactionCallback; +class nsIPrefBranch; + +namespace mozilla { +namespace net { + +class Cookie; + +// Inherit from CookieKey so this can be stored in nsTHashTable +// TODO: why aren't we using nsClassHashTable<CookieKey, ArrayType>? +class CookieEntry : public CookieKey { + public: + // Hash methods + using ArrayType = nsTArray<RefPtr<Cookie>>; + using IndexType = ArrayType::index_type; + + explicit CookieEntry(KeyTypePointer aKey) : CookieKey(aKey) {} + + CookieEntry(const CookieEntry& toCopy) { + // if we end up here, things will break. nsTHashtable shouldn't + // allow this, since we set ALLOW_MEMMOVE to true. + MOZ_ASSERT_UNREACHABLE("CookieEntry copy constructor is forbidden!"); + } + + ~CookieEntry() = default; + + inline ArrayType& GetCookies() { return mCookies; } + inline const ArrayType& GetCookies() const { return mCookies; } + + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const; + + bool IsPartitioned() const; + + private: + ArrayType mCookies; +}; + +// stores the CookieEntry entryclass and an index into the cookie array within +// that entryclass, for purposes of storing an iteration state that points to a +// certain cookie. +struct CookieListIter { + // default (non-initializing) constructor. + CookieListIter() = default; + + // explicit constructor to a given iterator state with entryclass 'aEntry' + // and index 'aIndex'. + explicit CookieListIter(CookieEntry* aEntry, CookieEntry::IndexType aIndex) + : entry(aEntry), index(aIndex) {} + + // get the Cookie * the iterator currently points to. + mozilla::net::Cookie* Cookie() const { return entry->GetCookies()[index]; } + + CookieEntry* entry; + CookieEntry::IndexType index; +}; + +class CookieStorage : public nsIObserver, public nsSupportsWeakReference { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIOBSERVER + + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const; + + void GetCookies(nsTArray<RefPtr<nsICookie>>& aCookies) const; + + void GetSessionCookies(nsTArray<RefPtr<nsICookie>>& aCookies) const; + + bool FindCookie(const nsACString& aBaseDomain, + const OriginAttributes& aOriginAttributes, + const nsACString& aHost, const nsACString& aName, + const nsACString& aPath, CookieListIter& aIter); + + uint32_t CountCookiesFromHost(const nsACString& aBaseDomain, + uint32_t aPrivateBrowsingId); + + void GetAll(nsTArray<RefPtr<nsICookie>>& aResult) const; + + const nsTArray<RefPtr<Cookie>>* GetCookiesFromHost( + const nsACString& aBaseDomain, const OriginAttributes& aOriginAttributes); + + void GetCookiesWithOriginAttributes(const OriginAttributesPattern& aPattern, + const nsACString& aBaseDomain, + nsTArray<RefPtr<nsICookie>>& aResult); + + void RemoveCookie(const nsACString& aBaseDomain, + const OriginAttributes& aOriginAttributes, + const nsACString& aHost, const nsACString& aName, + const nsACString& aPath); + + virtual void RemoveCookiesWithOriginAttributes( + const OriginAttributesPattern& aPattern, const nsACString& aBaseDomain); + + virtual void RemoveCookiesFromExactHost( + const nsACString& aHost, const nsACString& aBaseDomain, + const OriginAttributesPattern& aPattern); + + void RemoveAll(); + + void NotifyChanged(nsISupports* aSubject, + nsICookieNotification::Action aAction, + const nsACString& aBaseDomain, + dom::BrowsingContext* aBrowsingContext = nullptr, + bool aOldCookieIsSession = false); + + void AddCookie(nsIConsoleReportCollector* aCRC, const nsACString& aBaseDomain, + const OriginAttributes& aOriginAttributes, Cookie* aCookie, + int64_t aCurrentTimeInUsec, nsIURI* aHostURI, + const nsACString& aCookieHeader, bool aFromHttp, + dom::BrowsingContext* aBrowsingContext); + + static void CreateOrUpdatePurgeList(nsCOMPtr<nsIArray>& aPurgedList, + nsICookie* aCookie); + + virtual void StaleCookies(const nsTArray<Cookie*>& aCookieList, + int64_t aCurrentTimeInUsec) = 0; + + virtual void Close() = 0; + + virtual void EnsureInitialized() = 0; + + virtual nsresult RunInTransaction( + nsICookieTransactionCallback* aCallback) = 0; + + protected: + CookieStorage() = default; + virtual ~CookieStorage() = default; + + void Init(); + + void AddCookieToList(const nsACString& aBaseDomain, + const OriginAttributes& aOriginAttributes, + Cookie* aCookie); + + virtual void StoreCookie(const nsACString& aBaseDomain, + const OriginAttributes& aOriginAttributes, + Cookie* aCookie) = 0; + + virtual const char* NotificationTopic() const = 0; + + virtual void NotifyChangedInternal(nsICookieNotification* aSubject, + bool aOldCookieIsSession) = 0; + + virtual void RemoveAllInternal() = 0; + + // This method calls RemoveCookieFromDB + RemoveCookieFromListInternal. + void RemoveCookieFromList(const CookieListIter& aIter); + + void RemoveCookieFromListInternal(const CookieListIter& aIter); + + virtual void RemoveCookieFromDB(const Cookie& aCookie) = 0; + + already_AddRefed<nsIArray> PurgeCookiesWithCallbacks( + int64_t aCurrentTimeInUsec, uint16_t aMaxNumberOfCookies, + int64_t aCookiePurgeAge, + std::function<void(const CookieListIter&)>&& aRemoveCookieCallback, + std::function<void()>&& aFinalizeCallback); + + nsTHashtable<CookieEntry> mHostTable; + + uint32_t mCookieCount{0}; + + private: + void PrefChanged(nsIPrefBranch* aPrefBranch); + + bool FindSecureCookie(const nsACString& aBaseDomain, + const OriginAttributes& aOriginAttributes, + Cookie* aCookie); + + static void FindStaleCookies(CookieEntry* aEntry, int64_t aCurrentTime, + bool aIsSecure, + nsTArray<CookieListIter>& aOutput, + uint32_t aLimit); + + void UpdateCookieOldestTime(Cookie* aCookie); + + void MergeCookieSchemeMap(Cookie* aOldCookie, Cookie* aNewCookie); + + static already_AddRefed<nsIArray> CreatePurgeList(nsICookie* aCookie); + + virtual already_AddRefed<nsIArray> PurgeCookies(int64_t aCurrentTimeInUsec, + uint16_t aMaxNumberOfCookies, + int64_t aCookiePurgeAge) = 0; + + virtual void CollectCookieJarSizeData() = 0; + + int64_t mCookieOldestTime{INT64_MAX}; + + uint16_t mMaxNumberOfCookies{kMaxNumberOfCookies}; + uint16_t mMaxCookiesPerHost{kMaxCookiesPerHost}; + uint16_t mCookieQuotaPerHost{kCookieQuotaPerHost}; + int64_t mCookiePurgeAge{kCookiePurgeAge}; +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_CookieStorage_h diff --git a/netwerk/cookie/CookieXPCShellUtils.sys.mjs b/netwerk/cookie/CookieXPCShellUtils.sys.mjs new file mode 100644 index 0000000000..b6d0d3f26b --- /dev/null +++ b/netwerk/cookie/CookieXPCShellUtils.sys.mjs @@ -0,0 +1,53 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +/* 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/. */ + +import { ExtensionTestUtils } from "resource://testing-common/ExtensionXPCShellUtils.sys.mjs"; + +import { AddonTestUtils } from "resource://testing-common/AddonTestUtils.sys.mjs"; + +export const CookieXPCShellUtils = { + init(scope) { + AddonTestUtils.maybeInit(scope); + ExtensionTestUtils.init(scope); + }, + + createServer(args) { + const server = AddonTestUtils.createHttpServer(args); + server.registerPathHandler("/", (metadata, response) => { + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html", false); + + let body = "<body><h1>Hello world!</h1></body>"; + response.bodyOutputStream.write(body, body.length); + }); + return server; + }, + + async loadContentPage(uri, options = {}) { + return ExtensionTestUtils.loadContentPage(uri, options); + }, + + async getCookieStringFromDocument(uri, options = {}) { + const contentPage = await this.loadContentPage(uri, options); + const cookies = await contentPage.spawn( + [], + // eslint-disable-next-line no-undef + () => content.document.cookie + ); + await contentPage.close(); + return cookies; + }, + + async setCookieToDocument(uri, set, options = {}) { + const contentPage = await this.loadContentPage(uri, options); + await contentPage.spawn( + [set], + // eslint-disable-next-line no-undef + cookies => (content.document.cookie = cookies) + ); + await contentPage.close(); + }, +}; diff --git a/netwerk/cookie/PCookieService.ipdl b/netwerk/cookie/PCookieService.ipdl new file mode 100644 index 0000000000..f8ec4c8d0f --- /dev/null +++ b/netwerk/cookie/PCookieService.ipdl @@ -0,0 +1,74 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set sw=2 ts=8 et tw=80 ft=cpp : */ + +/* 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 protocol PNecko; +include NeckoChannelParams; + +using mozilla::OriginAttributes from "mozilla/ipc/BackgroundUtils.h"; +[RefCounted] using class nsIURI from "mozilla/ipc/URIUtils.h"; + +namespace mozilla { +namespace net { + +/** + * PCookieService + * + * Provides IPDL methods for setting and getting cookies. These are stored on + * and managed by the parent; the child process goes through the parent for + * all cookie operations. Lower-level programmatic operations (i.e. those + * provided by the nsICookieManager interface) are not + * currently implemented and requesting these interfaces in the child will fail. + * + * @see nsICookieService + * @see nsICookiePermission + */ + +[ManualDealloc, NestedUpTo=inside_cpow] sync protocol PCookieService +{ + manager PNecko; + +parent: + [Nested=inside_cpow] async SetCookies(nsCString baseDomain, + OriginAttributes attrs, + nullable nsIURI host, + bool fromHttp, + CookieStruct[] cookies); + + async GetCookieList(nullable nsIURI host, + bool isForeign, + bool isThirdPartyTrackingResource, + bool isThirdPartySocialTrackingResource, + bool firstPartyStorageAccessPermissionGranted, + uint32_t rejectedReason, + bool isSafeTopLevelNav, + bool isSameSiteForeign, + bool hadCrossSiteRedirects, + OriginAttributes attrs) + returns (CookieStruct[] cookies); + + async __delete__(); + +child: + async TrackCookiesLoad(CookieStruct[] cookiesList, + OriginAttributes attrs); + + async RemoveCookie(CookieStruct cookie, + OriginAttributes attrs); + + async RemoveBatchDeletedCookies(CookieStruct[] cookiesList, + OriginAttributes[] attrsList); + + async RemoveAll(); + + async AddCookie(CookieStruct cookie, + OriginAttributes attrs); + +}; + +} +} + diff --git a/netwerk/cookie/moz.build b/netwerk/cookie/moz.build new file mode 100644 index 0000000000..1667943a31 --- /dev/null +++ b/netwerk/cookie/moz.build @@ -0,0 +1,77 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +with Files("**"): + BUG_COMPONENT = ("Core", "Networking: Cookies") + +# export required interfaces, even if --disable-cookies has been given +XPIDL_SOURCES += [ + "nsICookie.idl", + "nsICookieJarSettings.idl", + "nsICookieManager.idl", + "nsICookieNotification.idl", + "nsICookiePermission.idl", + "nsICookieService.idl", +] + +XPIDL_MODULE = "necko_cookie" + + +EXPORTS.mozilla.net = [ + "Cookie.h", + "CookieJarSettings.h", + "CookieKey.h", + "CookieNotification.h", + "CookiePersistentStorage.h", + "CookiePrivateStorage.h", + "CookieService.h", + "CookieServiceChild.h", + "CookieServiceParent.h", + "CookieStorage.h", +] +UNIFIED_SOURCES += [ + "Cookie.cpp", + "CookieCommons.cpp", + "CookieJarSettings.cpp", + "CookieLogging.cpp", + "CookieNotification.cpp", + "CookiePersistentStorage.cpp", + "CookiePrivateStorage.cpp", + "CookieService.cpp", + "CookieServiceChild.cpp", + "CookieServiceParent.cpp", + "CookieStorage.cpp", +] +XPCSHELL_TESTS_MANIFESTS += [ + "test/unit/xpcshell.toml", +] + +BROWSER_CHROME_MANIFESTS += [ + "test/browser/browser.toml", +] + +MOCHITEST_MANIFESTS += [ + "test/mochitest/mochitest.toml", +] + +IPDL_SOURCES = [ + "PCookieService.ipdl", +] + +LOCAL_INCLUDES += [ + "/dom/base", + "/intl/uconv", + "/netwerk/base", + "/netwerk/protocol/http", +] + +TESTING_JS_MODULES += [ + "CookieXPCShellUtils.sys.mjs", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" diff --git a/netwerk/cookie/nsICookie.idl b/netwerk/cookie/nsICookie.idl new file mode 100644 index 0000000000..6d7fedfd21 --- /dev/null +++ b/netwerk/cookie/nsICookie.idl @@ -0,0 +1,166 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * 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 "nsISupports.idl" + +/** + * Main cookie object interface. + */ + +%{C++ +namespace mozilla { + class OriginAttributes; + + namespace net { + class Cookie; + } +} +%} + +[ref] native const_OriginAttributes(const mozilla::OriginAttributes); +[ref] native const_Cookie(const mozilla::net::Cookie); + +typedef long nsCookieStatus; +typedef long nsCookiePolicy; + +[builtinclass, scriptable, uuid(adf0db5e-211e-45a3-be14-4486ac430a58)] +interface nsICookie : nsISupports { + const uint32_t SAMESITE_NONE = 0; + const uint32_t SAMESITE_LAX = 1; + const uint32_t SAMESITE_STRICT = 2; + + /** + * the name of the cookie + */ + readonly attribute ACString name; + + /** + * the cookie value + */ + readonly attribute AUTF8String value; + + /** + * true if the cookie is a domain cookie, false otherwise + */ + readonly attribute boolean isDomain; + + /** + * the host (possibly fully qualified) of the cookie + */ + readonly attribute AUTF8String host; + + /** + * the host (possibly fully qualified) of the cookie, + * without a leading dot to represent if it is a + * domain cookie. + */ + readonly attribute AUTF8String rawHost; + + /** + * the path pertaining to the cookie + */ + readonly attribute AUTF8String path; + + /** + * true if the cookie was transmitted over ssl, false otherwise + */ + readonly attribute boolean isSecure; + + /** + * @DEPRECATED use nsICookie.expiry and nsICookie.isSession instead. + * + * expiration time in seconds since midnight (00:00:00), January 1, 1970 UTC. + * expires = 0 represents a session cookie. + * expires = 1 represents an expiration time earlier than Jan 1, 1970. + */ + readonly attribute uint64_t expires; + + /** + * the actual expiry time of the cookie, in seconds + * since midnight (00:00:00), January 1, 1970 UTC. + * + * this is distinct from nsICookie::expires, which + * has different and obsolete semantics. + */ + readonly attribute int64_t expiry; + + /** + * The origin attributes for this cookie + */ + [implicit_jscontext] + readonly attribute jsval originAttributes; + + /** + * Native getter for origin attributes + */ + [noscript, notxpcom, nostdcall, binaryname(OriginAttributesNative)] + const_OriginAttributes OriginAttributesNative(); + + [noscript, notxpcom, nostdcall, binaryname(AsCookie)] + const_Cookie AsCookie(); + + /** + * true if the cookie is a session cookie. + * note that expiry time will also be honored + * for session cookies (see below); thus, whichever is + * the more restrictive of the two will take effect. + */ + readonly attribute boolean isSession; + + /** + * true if the cookie is an http only cookie + */ + readonly attribute boolean isHttpOnly; + + /** + * the creation time of the cookie, in microseconds + * since midnight (00:00:00), January 1, 1970 UTC. + */ + readonly attribute int64_t creationTime; + + /** + * the last time the cookie was accessed (i.e. created, + * modified, or read by the server), in microseconds + * since midnight (00:00:00), January 1, 1970 UTC. + * + * note that this time may be approximate. + */ + readonly attribute int64_t lastAccessed; + + /** + * the SameSite attribute; this controls the cookie behavior for cross-site + * requests as per + * https://tools.ietf.org/html/draft-west-first-party-cookies-07 + * + * This should be one of: + * - SAMESITE_NONE - the SameSite attribute is not present + * - SAMESITE_LAX - the SameSite attribute is present, but not strict + * - SAMESITE_STRICT - the SameSite attribute is present and strict + */ + readonly attribute int32_t sameSite; + + /** + * The list of possible schemes of cookies. It's a bitmap because a cookie + * can be set on HTTP and HTTPS. At the moment, we treat it as the same + * cookie. + */ + cenum schemeType : 8 { + SCHEME_UNSET = 0x00, + SCHEME_HTTP = 0x01, + SCHEME_HTTPS = 0x02, + SCHEME_FILE = 0x04, + }; + + /** + * Bitmap of schemes. + */ + readonly attribute nsICookie_schemeType schemeMap; + + /** + * true if the cookie's OriginAttributes PartitionKey is NOT empty + */ + readonly attribute boolean isPartitioned; +}; diff --git a/netwerk/cookie/nsICookieJarSettings.idl b/netwerk/cookie/nsICookieJarSettings.idl new file mode 100644 index 0000000000..d522c96358 --- /dev/null +++ b/netwerk/cookie/nsICookieJarSettings.idl @@ -0,0 +1,86 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: ft=cpp tw=78 sw=2 et ts=2 sts=2 cin + * 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 "nsISupports.idl" +#include "nsISerializable.idl" + +interface nsIPrincipal; +interface nsIURI; + +/** + * Cookie jar settings for top-level documents. Please see CookieJarSettings.h + * for more details. + */ +[scriptable, builtinclass, uuid(3ec40331-7cf0-4b71-ba2a-2265aab8f6bc)] +interface nsICookieJarSettings : nsISerializable +{ + /** + * CookieBehavior at the loading of the document. Any other loadInfo + * inherits it from its document's loadInfo. If there is not a document + * involved, cookieBehavior is reject. + */ + [infallible] readonly attribute unsigned long cookieBehavior; + + /** + * First-Party Isolation state at the loading of the document. + */ + [infallible] readonly attribute boolean isFirstPartyIsolated; + + /** + * Resist Fingerprinting state at the loading of the document. + */ + [infallible] readonly attribute boolean shouldResistFingerprinting; + + /** + * Whether our cookie behavior mandates rejecting third-party contexts. + */ + [infallible] readonly attribute boolean rejectThirdPartyContexts; + + [infallible] readonly attribute boolean limitForeignContexts; + + [infallible] readonly attribute boolean blockingAllThirdPartyContexts; + + [infallible] readonly attribute boolean blockingAllContexts; + /** + * Whether our cookie behavior mandates partitioning third-party content. + */ + [infallible] attribute boolean partitionForeign; + + /** + * Whether the top-level document is on the content blocking allow list. + */ + [infallible] readonly attribute boolean isOnContentBlockingAllowList; + + /** + * The key used for partitioning. + */ + readonly attribute AString partitionKey; + + /** + * The key used for fingerprinting randomization. + */ + readonly attribute Array<uint8_t> fingerprintingRandomizationKey; + + /** + * CookiePermission at the loading of the document for a particular + * principal. It returns the same cookiePermission also in case it changes + * during the life-time of the top document. + */ + unsigned long cookiePermission(in nsIPrincipal aPrincipal); + + /** + * Initiate the cookieJarSettings with a URI. The aURI will be used to build + * the partition key for this cookieJarSettings. This function is added for + * js code to be able to set the partitionKey from a first-party URI. + * + * The aIsPrivate indicates if this cookieJarSettings is initiated for the + * private browsing mode or not. If aIsPrivate was true, it will get + * cookieBehavior from the pref "network.cookie.cookieBehavior" which is for + * the regular browsing mode. Otherwise, it will get from the pref + * "network.cookie.cookieBehavior.pbmode" for the private browsing mode. + */ + void initWithURI(in nsIURI aURI, in boolean aIsPrivate); +}; diff --git a/netwerk/cookie/nsICookieManager.idl b/netwerk/cookie/nsICookieManager.idl new file mode 100644 index 0000000000..ae18ec27c5 --- /dev/null +++ b/netwerk/cookie/nsICookieManager.idl @@ -0,0 +1,278 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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 "nsISupports.idl" +#include "nsICookie.idl" + +%{ C++ +namespace mozilla { +class OriginAttributes; +} // mozilla namespace +%} + +[ptr] native OriginAttributesPtr(mozilla::OriginAttributes); + +/** + * An optional interface for accessing or removing the cookies + * that are in the cookie list + */ + +[scriptable, builtinclass, uuid(AAAB6710-0F2C-11d5-A53B-0010A401EB10)] +interface nsICookieManager : nsISupports +{ + + /** + * Called to remove all cookies from the cookie list + */ + void removeAll(); + + /** + * Returns an array of cookies in the cookie list. + * The objects in the array are of type nsICookie + * This array only contains non-private browsing cookies. + * To retrieve an array of private browsing cookies, use + * getCookiesWithOriginAttributes. + */ + readonly attribute Array<nsICookie> cookies; + + /** + * Returns an array of session cookies in the cookie list. + * The objects in the array are of type nsICookie + * This array only contains non-private browsing cookies. + */ + readonly attribute Array<nsICookie> sessionCookies; + + /** + * Returns current effective value of the cookieBehavior. It will return the + * different pref according to the aIsPrivate. If aIsPrivate is true, it will + * return the pref "network.cookie.cookieBehavior". Otherwise, it will return + * the pref "network.cookie.cookieBehavior.pbmode" + */ + uint32_t getCookieBehavior(in boolean aIsPrivate); + %{C++ + static uint32_t GetCookieBehavior(bool aIsPrivate); + %} + + /** + * Called to remove an individual cookie from the cookie list, specified + * by host, name, and path. If the cookie cannot be found, no exception + * is thrown. Typically, the arguments to this method will be obtained + * directly from the desired nsICookie object. + * + * @param aHost The host or domain for which the cookie was set. @see + * nsICookieManager::add for a description of acceptable host + * strings. If the target cookie is a domain cookie, a leading + * dot must be present. + * @param aName The name specified in the cookie + * @param aPath The path for which the cookie was set + * @param aOriginAttributes The originAttributes of this cookie. + * + */ + [implicit_jscontext] + void remove(in AUTF8String aHost, + in ACString aName, + in AUTF8String aPath, + in jsval aOriginAttributes); + + [notxpcom] + nsresult removeNative(in AUTF8String aHost, + in ACString aName, + in AUTF8String aPath, + in OriginAttributesPtr aOriginAttributes); + + /** + * Add a cookie. nsICookieService is the normal way to do this. This + * method is something of a backdoor. + * + * @param aHost + * the host or domain for which the cookie is set. presence of a + * leading dot indicates a domain cookie; otherwise, the cookie + * is treated as a non-domain cookie (see RFC2109). The host string + * will be normalized to ASCII or ACE; any trailing dot will be + * stripped. To be a domain cookie, the host must have at least two + * subdomain parts (e.g. '.foo.com', not '.com'), otherwise an + * exception will be thrown. An empty string is acceptable + * (e.g. file:// URI's). + * @param aPath + * path within the domain for which the cookie is valid + * @param aName + * cookie name + * @param aValue + * cookie data + * @param aIsSecure + * true if the cookie should only be sent over a secure connection. + * @param aIsHttpOnly + * true if the cookie should only be sent to, and can only be + * modified by, an http connection. + * @param aIsSession + * true if the cookie should exist for the current session only. + * see aExpiry. + * @param aExpiry + * expiration date, in seconds since midnight (00:00:00), January 1, + * 1970 UTC. note that expiry time will also be honored for session cookies; + * in this way, the more restrictive of the two will take effect. + * @param aOriginAttributes + * the originAttributes of this cookie. + * @param aSameSite + * the SameSite attribute. + */ + [implicit_jscontext] + void add(in AUTF8String aHost, + in AUTF8String aPath, + in ACString aName, + in AUTF8String aValue, + in boolean aIsSecure, + in boolean aIsHttpOnly, + in boolean aIsSession, + in int64_t aExpiry, + in jsval aOriginAttributes, + in int32_t aSameSite, + in nsICookie_schemeType aSchemeMap); + + [notxpcom] + nsresult addNative(in AUTF8String aHost, + in AUTF8String aPath, + in ACString aName, + in AUTF8String aValue, + in boolean aIsSecure, + in boolean aIsHttpOnly, + in boolean aIsSession, + in int64_t aExpiry, + in OriginAttributesPtr aOriginAttributes, + in int32_t aSameSite, + in nsICookie_schemeType aSchemeMap); + + /** + * Find whether a given cookie already exists. + * + * @param aHost + * the cookie's host to look for + * @param aPath + * the cookie's path to look for + * @param aName + * the cookie's name to look for + * @param aOriginAttributes + * the cookie's originAttributes to look for + * + * @return true if a cookie was found which matches the host, path, name and + * originAttributes fields of aCookie + */ + [implicit_jscontext] + boolean cookieExists(in AUTF8String aHost, + in AUTF8String aPath, + in ACString aName, + in jsval aOriginAttributes); + + [notxpcom] + nsresult cookieExistsNative(in AUTF8String aHost, + in AUTF8String aPath, + in ACString aName, + in OriginAttributesPtr aOriginAttributes, + out boolean aExists); + + /** + * Get a specific cookie by host, path, name and OriginAttributes. + * + * @param aHost + * the cookie's host to look for + * @param aPath + * the cookie's path to look for + * @param aName + * the cookie's name to look for + * @param aOriginAttributes + * the cookie's originAttributes to look for + * + * @return cookie matching the arguments or nullptr if not existing. + */ + [notxpcom] + nsresult getCookieNative(in AUTF8String aHost, + in AUTF8String aPath, + in ACString aName, + in OriginAttributesPtr aOriginAttributes, + out nsICookie aCookie); + + /** + * Count how many cookies exist within the base domain of 'aHost'. + * Thus, for a host "weather.yahoo.com", the base domain would be "yahoo.com", + * and any host or domain cookies for "yahoo.com" and its subdomains would be + * counted. + * + * @param aHost + * the host string to search for, e.g. "google.com". this should consist + * of only the host portion of a URI. see @add for a description of + * acceptable host strings. + * + * @return the number of cookies found. + */ + unsigned long countCookiesFromHost(in AUTF8String aHost); + + /** + * Returns an array of cookies that exist within the base domain of + * 'aHost'. Thus, for a host "weather.yahoo.com", the base domain would be + * "yahoo.com", and any host or domain cookies for "yahoo.com" and its + * subdomains would be returned. + * + * @param aHost + * the host string to search for, e.g. "google.com". this should consist + * of only the host portion of a URI. see @add for a description of + * acceptable host strings. + * @param aOriginAttributes The originAttributes of cookies that would be + * retrived. + * + * @return an array of nsICookie objects. + * + * @see countCookiesFromHost + */ + [implicit_jscontext] + Array<nsICookie> getCookiesFromHost(in AUTF8String aHost, + in jsval aOriginAttributes); + + /** + * Returns an array of all cookies whose origin attributes matches aPattern + * + * @param aPattern origin attribute pattern in JSON format + * + * @param aHost + * the host string to search for, e.g. "google.com". this should consist + * of only the host portion of a URI. see @add for a description of + * acceptable host strings. This attribute is optional. It will search + * all hosts if this attribute is not given. + */ + Array<nsICookie> getCookiesWithOriginAttributes(in AString aPattern, + [optional] in AUTF8String aHost); + + /** + * Remove all the cookies whose origin attributes matches aPattern + * + * @param aPattern origin attribute pattern in JSON format + */ + void removeCookiesWithOriginAttributes(in AString aPattern, + [optional] in AUTF8String aHost); + + /** + * Remove all the cookies whose origin attributes matches aPattern and the + * host is exactly aHost (without subdomain matching). + * + * @param aHost the host to match + * @param aPattern origin attribute pattern in JSON format + */ + void removeCookiesFromExactHost(in AUTF8String aHost, in AString aPattern); + + /** + * Removes all cookies that were created on or after aSinceWhen, and returns + * a Promise which will be resolved when the last such cookie has been + * removed. + * + * @param aSinceWhen the starting point in time after which no cookies should + * be created when the Promise returned from this method is resolved. + */ + [implicit_jscontext] + Promise removeAllSince(in int64_t aSinceWhen); + + /** + * Retrieves all the cookies that were created on or after aSinceWhen, order + * by creation time */ + Array<nsICookie> getCookiesSince(in int64_t aSinceWhen); +}; diff --git a/netwerk/cookie/nsICookieNotification.idl b/netwerk/cookie/nsICookieNotification.idl new file mode 100644 index 0000000000..b37ae65873 --- /dev/null +++ b/netwerk/cookie/nsICookieNotification.idl @@ -0,0 +1,70 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * 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 "nsISupports.idl" + +interface nsICookie; +interface nsIArray; +webidl BrowsingContext; + +/** + * Meta object dispatched by cookie change notifications. + */ +[builtinclass, scriptable, uuid(5b3490f2-75f0-4e36-9f3d-47c857ecdfbb)] +interface nsICookieNotification : nsISupports { + + cenum Action : 8 { + // A cookie was deleted. cookie contains the deleted cookie. + COOKIE_DELETED, + // A cookie was added. cookie contains the added cookie. + COOKIE_ADDED, + // A cookie was altered. cookie contains the updated version of the + // cookie. Note that host, path, and name are invariant for a given + // cookie; other parameters may change. + COOKIE_CHANGED, + // the entire cookie list was cleared. cookie is null. + ALL_COOKIES_CLEARED, + // A set of cookies was purged. batchDeletedCookies contains the list of + // deleted cookies. cookie is null. + // Purging typically affects expired cookies or cases where the cookie + // list grows too large. + COOKIES_BATCH_DELETED, + }; + + /** + * Describes the cookie operation this notification is for. Cookies may be + * deleted, added or changed. See Action enum above for possible values. + */ + [infallible] readonly attribute nsICookieNotification_Action action; + + + /** + * The cookie the notification is for, may be null depending on the action. + */ + [infallible] readonly attribute nsICookie cookie; + + /** + * Base domain of the cookie. May be empty if cookie is null. + */ + readonly attribute ACString baseDomain; + + /** + * List of cookies purged. + * Only set when action == COOKIES_BATCH_DELETED. + */ + readonly attribute nsIArray batchDeletedCookies; + + /** + * The id of the BrowsingContext the cookie change was triggered from. Set + * to 0 if there is not applicable BrowsingContext. + */ + [infallible] readonly attribute unsigned long long browsingContextId; + + /** + * BrowsingContext associated with browsingContextId. May be nullptr. + */ + [infallible] readonly attribute BrowsingContext browsingContext; +}; diff --git a/netwerk/cookie/nsICookiePermission.idl b/netwerk/cookie/nsICookiePermission.idl new file mode 100644 index 0000000000..dc9b2b0a1b --- /dev/null +++ b/netwerk/cookie/nsICookiePermission.idl @@ -0,0 +1,35 @@ +/* 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 "nsISupports.idl" + +typedef long nsCookieAccess; + +/** + * An interface to test for cookie permissions + */ +[scriptable, uuid(11ddd4ed-8f5b-40b3-b2a0-27c20ea1c88d)] +interface nsICookiePermission : nsISupports +{ + /** + * nsCookieAccess values + */ + const nsCookieAccess ACCESS_DEFAULT = 0; + const nsCookieAccess ACCESS_ALLOW = 1; + const nsCookieAccess ACCESS_DENY = 2; + + /** + * additional values for nsCookieAccess which may not match + * nsIPermissionManager. Keep 3-7 available to allow nsIPermissionManager to + * add values without colliding. ACCESS_SESSION is not directly returned by + * any methods on this interface. + */ + const nsCookieAccess ACCESS_SESSION = 8; + + /** + * Don't use values 9 and 10! They used to be ACCESS_ALLOW_FIRST_PARTY_ONLY + * and ACCESS_LIMIT_THIRD_PARTY, now removed, but maybe still stored in some + * ancient user profiles. + */ +}; diff --git a/netwerk/cookie/nsICookieService.idl b/netwerk/cookie/nsICookieService.idl new file mode 100644 index 0000000000..c69453f6fa --- /dev/null +++ b/netwerk/cookie/nsICookieService.idl @@ -0,0 +1,144 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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 "nsISupports.idl" + +interface nsIURI; +interface nsIChannel; +webidl Document; + +/** + * @see nsICookieService::runInTransaction + */ +[scriptable, function, uuid(0fc41ffb-f1b7-42d9-9a42-8dc420c158c1)] +interface nsICookieTransactionCallback : nsISupports +{ + void callback(); +}; + +/** + * nsICookieService + * + * Provides methods for setting and getting cookies in the context of a + * page load. See nsICookieManager for methods to manipulate the cookie + * database directly. This separation of interface is mainly historical. + * + * This service broadcasts the notifications detailed below when the cookie + * list is changed, or a cookie is rejected. + * + * NOTE: observers of these notifications *must* not attempt to change profile + * or switch into or out of private browsing mode from within the + * observer. Doing so will cause undefined behavior. Mutating the cookie + * list (e.g. by calling methods on nsICookieService and friends) is + * allowed, but beware that there may be pending notifications you haven't + * seen yet -- for instance, a COOKIES_BATCH_DELETED notification will likely be + * immediately followed by COOKIE_ADDED. You may check the state of the cookie + * list to determine if this is the case. + * + * topic : "cookie-changed" + * broadcast whenever the cookie list changes in some way. see + * explanation of data strings below. + * subject: The cookie notification. See nsICookieNotification for details. + * data : none. For possible actions see nsICookieNotification_Action. + * + * topic : "cookie-rejected" + * broadcast whenever a cookie was rejected from being set as a + * result of user prefs. + * subject: an nsIURI interface pointer representing the URI that attempted + * to set the cookie. + * data : none. + */ +[scriptable, uuid(1e94e283-2811-4f43-b947-d22b1549d824)] +interface nsICookieService : nsISupports +{ + /* + * Possible values for the "network.cookie.cookieBehavior" preference. + */ + const uint32_t BEHAVIOR_ACCEPT = 0; // allow all cookies + const uint32_t BEHAVIOR_REJECT_FOREIGN = 1; // reject all third-party cookies + const uint32_t BEHAVIOR_REJECT = 2; // reject all cookies + const uint32_t BEHAVIOR_LIMIT_FOREIGN = 3; // reject third-party cookies unless the + // eTLD already has at least one cookie + const uint32_t BEHAVIOR_REJECT_TRACKER = 4; // reject trackers + const uint32_t BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN = 5; // reject trackers, partition third-party cookies + // When adding a new cookie behavior, please increase this value! + const uint32_t BEHAVIOR_LAST = 5; + + /* + * Get the complete cookie string associated with the document's principal. + * This method is meant to be used for `document.cookie` only. Any security + * check about storage-access permission and cookie behavior must be done by + * the caller. + * + * @param aDocument + * The document. + * + * @return the resulting cookie string + */ + ACString getCookieStringFromDocument(in Document aDocument); + + /* + * Get the complete cookie string associated with the URI. + * + * This function is NOT redundant with getCookieString, as the result + * will be different based on httponly (see bug 178993) + * + * @param aURI + * The URI of the document for which cookies are being queried. + * file:// URIs (i.e. with an empty host) are allowed, but any other + * scheme must have a non-empty host. A trailing dot in the host + * is acceptable, and will be stripped. This argument must not be null. + * @param aChannel + * the channel used to load the document. + * + * @return the resulting cookie string + */ + ACString getCookieStringFromHttp(in nsIURI aURI, in nsIChannel aChannel); + + /* + * Set the cookie string associated with a Document. This method is meant to + * be used for `document.cookie` only. Any security check about + * storage-access permission and cookie behavior must be done by the caller. + * + * @param aDocument + * The document. + * @param aCookie + * the cookie string to set. + */ + void setCookieStringFromDocument(in Document aDocument, in ACString aCookie); + + /* + * Set the cookie string and expires associated with the URI. + * + * This function is NOT redundant with setCookieString, as the result + * will be different based on httponly (see bug 178993) + * + * @param aURI + * The URI of the document for which cookies are being queried. + * file:// URIs (i.e. with an empty host) are allowed, but any other + * scheme must have a non-empty host. A trailing dot in the host + * is acceptable, and will be stripped. This argument must not be null. + * @param aCookie + * the cookie string to set. + * @param aChannel + * the channel used to load the document. + */ + void setCookieStringFromHttp(in nsIURI aURI, in ACString aCookie, + in nsIChannel aChannel); + + /* + * Batch SQLite operations into one transaction. By default each call to + * CookieService that affects the underlying SQLite database (add, remove, + * setCookieString etc.) runs in a separate transaction. If you do this many + * times in a row, it's faster and suggested to wrap them all in a single + * transaction by setting all the operations into the callback parameter. + * Example: test scripts that need to construct a large cookie database. + * @param aCallback + * nsICookieTransactionCallback interface to call + * @throws NS_ERROR_FAILURE if aCallback() fails. + * @throws NS_ERROR_NOT_AVAILABLE if the connection is not established. + */ + void runInTransaction(in nsICookieTransactionCallback aCallback); +}; diff --git a/netwerk/cookie/test/browser/browser.toml b/netwerk/cookie/test/browser/browser.toml new file mode 100644 index 0000000000..05a302ddbd --- /dev/null +++ b/netwerk/cookie/test/browser/browser.toml @@ -0,0 +1,42 @@ +[DEFAULT] + +support-files = [ + "file_empty.html", + "file_empty.js", + "head.js" +] + +["browser_broadcastChannel.js"] + +["browser_cookie_insecure_overwrites_secure.js"] + +["browser_cookie_purge_sync.js"] + +["browser_cookies.js"] +support-files = ["server.sjs"] + +["browser_cookies_ipv6.js"] + +["browser_domCache.js"] + +["browser_indexedDB.js"] + +["browser_originattributes.js"] + +["browser_oversize.js"] +support-files = ["oversize.sjs"] + +["browser_partitionedConsole.js"] +support-files = ["partitioned.sjs"] + +["browser_partitioned_telemetry.js"] +support-files = ["partitioned.sjs"] + +["browser_sameSiteConsole.js"] +support-files = ["sameSite.sjs"] + +["browser_serviceWorker.js"] + +["browser_sharedWorker.js"] + +["browser_storage.js"] diff --git a/netwerk/cookie/test/browser/browser_broadcastChannel.js b/netwerk/cookie/test/browser/browser_broadcastChannel.js new file mode 100644 index 0000000000..ee561e6f0c --- /dev/null +++ b/netwerk/cookie/test/browser/browser_broadcastChannel.js @@ -0,0 +1,80 @@ +// BroadcastChannel is not considered part of CookieJar. It's not allowed to +// communicate with other windows with different cookie jar settings. +"use strict"; + +CookiePolicyHelper.runTest("BroadcastChannel", { + cookieJarAccessAllowed: async w => { + new w.BroadcastChannel("hello"); + ok(true, "BroadcastChannel be used"); + }, + + cookieJarAccessDenied: async w => { + try { + new w.BroadcastChannel("hello"); + ok(false, "BroadcastChannel cannot be used!"); + } catch (e) { + ok(true, "BroadcastChannel cannot be used!"); + is(e.name, "SecurityError", "We want a security error message."); + } + }, +}); + +CookiePolicyHelper.runTest("BroadcastChannel in workers", { + cookieJarAccessAllowed: async w => { + function nonBlockingCode() { + new BroadcastChannel("hello"); + postMessage(true); + } + + let blob = new w.Blob([ + nonBlockingCode.toString() + "; nonBlockingCode();", + ]); + ok(blob, "Blob has been created"); + + let blobURL = w.URL.createObjectURL(blob); + ok(blobURL, "Blob URL has been created"); + + let worker = new w.Worker(blobURL); + ok(worker, "Worker has been created"); + + await new w.Promise((resolve, reject) => { + worker.onmessage = function (e) { + if (e) { + resolve(); + } else { + reject(); + } + }; + }); + }, + + cookieJarAccessDenied: async w => { + function blockingCode() { + try { + new BroadcastChannel("hello"); + postMessage(false); + } catch (e) { + postMessage(e.name == "SecurityError"); + } + } + + let blob = new w.Blob([blockingCode.toString() + "; blockingCode();"]); + ok(blob, "Blob has been created"); + + let blobURL = w.URL.createObjectURL(blob); + ok(blobURL, "Blob URL has been created"); + + let worker = new w.Worker(blobURL); + ok(worker, "Worker has been created"); + + await new w.Promise((resolve, reject) => { + worker.onmessage = function (e) { + if (e) { + resolve(); + } else { + reject(); + } + }; + }); + }, +}); diff --git a/netwerk/cookie/test/browser/browser_cookie_insecure_overwrites_secure.js b/netwerk/cookie/test/browser/browser_cookie_insecure_overwrites_secure.js new file mode 100644 index 0000000000..7461e5cc16 --- /dev/null +++ b/netwerk/cookie/test/browser/browser_cookie_insecure_overwrites_secure.js @@ -0,0 +1,128 @@ +/* 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/. */ + +let { HttpServer } = ChromeUtils.importESModule( + "resource://testing-common/httpd.sys.mjs" +); + +const urlPath = "/browser/netwerk/cookie/test/browser/file_empty.html"; +const baseDomain = "example.com"; + +// eslint doesn't like http +// eslint-disable-next-line @microsoft/sdl/no-insecure-url +const URL_INSECURE_COM = "http://" + baseDomain + urlPath; +const URL_SECURE_COM = "https://" + baseDomain + urlPath; + +// common cookie strings +const COOKIE_BASIC = "foo=one"; +const COOKIE_OTHER = "foo=two"; +const COOKIE_THIRD = "foo=three"; +const COOKIE_FORTH = "foo=four"; + +function securify(cookie) { + return cookie + "; Secure"; +} + +registerCleanupFunction(() => { + Services.prefs.clearUserPref("dom.security.https_first"); + Services.prefs.clearUserPref("network.cookie.cookieBehavior"); + Services.prefs.clearUserPref( + "network.cookieJarSettings.unblocked_for_testing" + ); + Services.prefs.clearUserPref("network.cookie.sameSite.laxByDefault"); + Services.prefs.clearUserPref("network.cookie.sameSite.noneRequiresSecure"); + Services.prefs.clearUserPref("network.cookie.sameSite.schemeful"); + info("Cleaning up the test"); +}); + +async function setup() { + // HTTPS-First would interfere with this test. + Services.prefs.setBoolPref("dom.security.https_first", false); + + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + + Services.prefs.setBoolPref("network.cookie.sameSite.laxByDefault", false); + Services.prefs.setBoolPref( + "network.cookie.sameSite.noneRequiresSecure", + false + ); + Services.prefs.setBoolPref("network.cookie.sameSite.schemeful", true); + Services.cookies.removeAll(); +} +add_task(setup); + +// note: +// 1. The URL scheme will not matter for insecure cookies, since +// cookies are not "schemeful" in this sense. +// So an insecure cookie set anywhere will be visible on http and https sites +// Secure cookies are different, they will only be visible from https sites +// and will prevent cookie setting of the same name on insecure sites. +// +// 2. The different processes (tabs) shouldn't matter since +// cookie adds/changes are distributed to other processes on a need-to-know +// basis. + +add_task(async function test_insecure_cant_overwrite_secure_via_doc() { + // insecure + const tab1 = BrowserTestUtils.addTab(gBrowser, URL_INSECURE_COM); + const browser = gBrowser.getBrowserForTab(tab1); + await BrowserTestUtils.browserLoaded(browser); + + // secure + const tab2 = BrowserTestUtils.addTab(gBrowser, URL_SECURE_COM); + const browser2 = gBrowser.getBrowserForTab(tab2); + await BrowserTestUtils.browserLoaded(browser2); + + // init with insecure cookie on insecure origin child process + await SpecialPowers.spawn( + browser, + [COOKIE_BASIC, COOKIE_BASIC], + (cookie, expected) => { + content.document.cookie = cookie; + is(content.document.cookie, expected); + } + ); + + // insecure cookie visible on secure origin process (sanity check) + await SpecialPowers.spawn(browser2, [COOKIE_BASIC], expected => { + is(content.document.cookie, expected); + }); + + // overwrite insecure cookie on secure origin with secure cookie (sanity check) + await SpecialPowers.spawn( + browser2, + [securify(COOKIE_OTHER), COOKIE_OTHER], + (cookie, expected) => { + content.document.cookie = cookie; + is(content.document.cookie, expected); + } + ); + + // insecure cookie will NOT overwrite the secure one on insecure origin + // and cookie.document appears blank + await SpecialPowers.spawn(browser, [COOKIE_THIRD, ""], (cookie, expected) => { + content.document.cookie = cookie; // quiet failure here + is(content.document.cookie, expected); + }); + + // insecure cookie will overwrite secure cookie on secure origin + // a bit weird, but this is normal + await SpecialPowers.spawn( + browser2, + [COOKIE_FORTH, COOKIE_FORTH], + (cookie, expected) => { + content.document.cookie = cookie; + is(content.document.cookie, expected); + } + ); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); + Services.cookies.removeAll(); +}); diff --git a/netwerk/cookie/test/browser/browser_cookie_purge_sync.js b/netwerk/cookie/test/browser/browser_cookie_purge_sync.js new file mode 100644 index 0000000000..186797d058 --- /dev/null +++ b/netwerk/cookie/test/browser/browser_cookie_purge_sync.js @@ -0,0 +1,141 @@ +/* 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/. */ + +// This test checks that cookie purge broadcast correctly +// updates content process memory as triggered by: +// 1. stale-cookie update from content process +// 2. delete cookie by host from parent process +// 3. clear all cookies from parent process + +const URL_EXAMPLE = "https://example.com"; +const COOKIE_NAMEVALUE = "name=value"; +const COOKIE_NAMEVALUE_2 = COOKIE_NAMEVALUE + "2"; +const COOKIE_STRING = COOKIE_NAMEVALUE + "; Secure; SameSite=None"; +const COOKIE_STRING_2 = COOKIE_NAMEVALUE_2 + "; Secure; SameSite=None"; +const MAX_AGE_OLD = 2; // seconds +const MAX_AGE_NEW = 100; // 100 sec + +registerCleanupFunction(() => { + info("Cleaning up the test setup"); + Services.prefs.clearUserPref("network.cookie.sameSite.laxByDefault"); + Services.cookies.removeAll(); +}); + +add_setup(async function () { + info("Setting up the test"); + Services.prefs.setBoolPref("network.cookie.sameSite.laxByDefault", false); +}); + +function waitForNotificationPromise(notification, expected) { + return new Promise(resolve => { + function observer(subject, topic, data) { + is(content.document.cookie, expected); + Services.obs.removeObserver(observer, notification); + resolve(); + } + Services.obs.addObserver(observer, notification); + }); +} + +add_task(async function test_purge_sync_batch_and_deleted() { + const tab1 = BrowserTestUtils.addTab(gBrowser, URL_EXAMPLE); + const browser = gBrowser.getBrowserForTab(tab1); + await BrowserTestUtils.browserLoaded(browser); + + const tab2 = BrowserTestUtils.addTab(gBrowser, URL_EXAMPLE); + const browser2 = gBrowser.getBrowserForTab(tab2); + await BrowserTestUtils.browserLoaded(browser2); + + let firstCookieAdded = SpecialPowers.spawn( + browser2, + ["content-added-cookie", COOKIE_NAMEVALUE], + waitForNotificationPromise + ); + await TestUtils.waitForTick(); // waiting helps --verify + + // set old cookie in tab 1 and check it in tab 2 + await SpecialPowers.spawn( + browser, + [COOKIE_STRING, MAX_AGE_OLD], + (cookie, max_age) => { + content.document.cookie = cookie + ";Max-Age=" + max_age; + } + ); + await firstCookieAdded; + + // wait until the first cookie expires + await SpecialPowers.spawn(browser2, [], () => { + return ContentTaskUtils.waitForCondition( + () => content.document.cookie == "", + "cookie did not expire in time", + 200 + ).catch(msg => { + is(false, "Cookie did not expire in time"); + }); + }); + + // BATCH_DELETED/BatchDeleted pathway + let batchDeletedPromise = SpecialPowers.spawn( + browser, + ["content-batch-deleted-cookies", COOKIE_NAMEVALUE_2], + waitForNotificationPromise + ); + await TestUtils.waitForTick(); // waiting helps --verify + await SpecialPowers.spawn( + browser, + [COOKIE_STRING_2, MAX_AGE_NEW], + (cookie, max_age) => { + content.document.cookie = cookie + ";Max-Age=" + max_age; + } + ); + await batchDeletedPromise; + + // COOKIE_DELETED/RemoveCookie pathway + let cookieRemovedPromise = SpecialPowers.spawn( + browser, + ["content-removed-cookie", ""], + waitForNotificationPromise + ); + let cookieRemovedPromise2 = SpecialPowers.spawn( + browser2, + ["content-removed-cookie", ""], + waitForNotificationPromise + ); + await TestUtils.waitForTick(); + Services.cookies.removeCookiesFromExactHost( + "example.com", + JSON.stringify({}) + ); + await cookieRemovedPromise; + await cookieRemovedPromise2; + + // cleanup prep + let anotherCookieAdded = SpecialPowers.spawn( + browser2, + ["content-added-cookie", COOKIE_NAMEVALUE], + waitForNotificationPromise + ); + await TestUtils.waitForTick(); // waiting helps --verify + await SpecialPowers.spawn( + browser, + [COOKIE_STRING, MAX_AGE_NEW], + (cookie, max_age) => { + content.document.cookie = cookie + ";Max-Age=" + max_age; + } + ); + await anotherCookieAdded; + + // ALL_COOKIES_CLEARED/RemoveAll pathway + let cleanup = SpecialPowers.spawn( + browser2, + ["content-removed-all-cookies", ""], + waitForNotificationPromise + ); + await TestUtils.waitForTick(); + Services.cookies.removeAll(); + await cleanup; + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); +}); diff --git a/netwerk/cookie/test/browser/browser_cookies.js b/netwerk/cookie/test/browser/browser_cookies.js new file mode 100644 index 0000000000..8a0d8332bf --- /dev/null +++ b/netwerk/cookie/test/browser/browser_cookies.js @@ -0,0 +1,53 @@ +"use strict"; + +CookiePolicyHelper.runTest("document.cookies", { + cookieJarAccessAllowed: async _ => { + let hasCookie = !!content.document.cookie.length; + + await content + .fetch("server.sjs") + .then(r => r.text()) + .then(text => { + is( + text, + hasCookie ? "cookie-present" : "cookie-not-present", + "document.cookie is consistent with fetch requests" + ); + }); + + content.document.cookie = "name=value"; + ok(content.document.cookie.includes("name=value"), "Some cookies for me"); + ok(content.document.cookie.includes("foopy=1"), "Some cookies for me"); + + await content + .fetch("server.sjs") + .then(r => r.text()) + .then(text => { + is(text, "cookie-present", "We should have cookies"); + }); + + ok(!!content.document.cookie.length, "Some Cookies for me"); + }, + + cookieJarAccessDenied: async _ => { + is(content.document.cookie, "", "No cookies for me"); + content.document.cookie = "name=value"; + is(content.document.cookie, "", "No cookies for me"); + + await content + .fetch("server.sjs") + .then(r => r.text()) + .then(text => { + is(text, "cookie-not-present", "We should not have cookies"); + }); + // Let's do it twice. + await content + .fetch("server.sjs") + .then(r => r.text()) + .then(text => { + is(text, "cookie-not-present", "We should not have cookies"); + }); + + is(content.document.cookie, "", "Still no cookies for me"); + }, +}); diff --git a/netwerk/cookie/test/browser/browser_cookies_ipv6.js b/netwerk/cookie/test/browser/browser_cookies_ipv6.js new file mode 100644 index 0000000000..088a76f4e8 --- /dev/null +++ b/netwerk/cookie/test/browser/browser_cookies_ipv6.js @@ -0,0 +1,57 @@ +"use strict"; + +let { HttpServer } = ChromeUtils.importESModule( + "resource://testing-common/httpd.sys.mjs" +); + +let gHttpServer = null; +let ip = "[::1]"; + +function contentHandler(metadata, response) { + response.setStatusLine(metadata.httpVersion, 200, "Ok"); + response.setHeader("Content-Type", "text/html", false); + let body = ` + <!DOCTYPE HTML> + <html> + <head> + <meta charset='utf-8'> + <title>Cookie ipv6 Test</title> + </head> + <body> + </body> + </html>`; + response.bodyOutputStream.write(body, body.length); +} + +add_task(async _ => { + if (!gHttpServer) { + gHttpServer = new HttpServer(); + gHttpServer.registerPathHandler("/content", contentHandler); + gHttpServer._start(-1, ip); + } + + registerCleanupFunction(() => { + gHttpServer.stop(() => { + gHttpServer = null; + }); + }); + + let serverPort = gHttpServer.identity.primaryPort; + let testURL = `http://${ip}:${serverPort}/content`; + + // Let's open our tab. + const tab = BrowserTestUtils.addTab(gBrowser, testURL); + gBrowser.selectedTab = tab; + + const browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + // Test if we can set and get document.cookie successfully. + await SpecialPowers.spawn(browser, [], () => { + content.document.cookie = "foo=bar"; + is(content.document.cookie, "foo=bar"); + }); + + // Let's close the tab. + BrowserTestUtils.removeTab(tab); +}); diff --git a/netwerk/cookie/test/browser/browser_domCache.js b/netwerk/cookie/test/browser/browser_domCache.js new file mode 100644 index 0000000000..5f1aa84d83 --- /dev/null +++ b/netwerk/cookie/test/browser/browser_domCache.js @@ -0,0 +1,25 @@ +"use strict"; + +CookiePolicyHelper.runTest("DOM Cache", { + cookieJarAccessAllowed: async w => { + await w.caches.open("wow").then( + _ => { + ok(true, "DOM Cache can be used!"); + }, + _ => { + ok(false, "DOM Cache can be used!"); + } + ); + }, + + cookieJarAccessDenied: async w => { + await w.caches.open("wow").then( + _ => { + ok(false, "DOM Cache cannot be used!"); + }, + _ => { + ok(true, "DOM Cache cannot be used!"); + } + ); + }, +}); diff --git a/netwerk/cookie/test/browser/browser_indexedDB.js b/netwerk/cookie/test/browser/browser_indexedDB.js new file mode 100644 index 0000000000..7f417077eb --- /dev/null +++ b/netwerk/cookie/test/browser/browser_indexedDB.js @@ -0,0 +1,84 @@ +"use strict"; + +CookiePolicyHelper.runTest("IndexedDB", { + cookieJarAccessAllowed: async w => { + w.indexedDB.open("test", "1"); + ok(true, "IDB should be allowed"); + }, + + cookieJarAccessDenied: async w => { + try { + w.indexedDB.open("test", "1"); + ok(false, "IDB should be blocked"); + } catch (e) { + ok(true, "IDB should be blocked"); + is(e.name, "SecurityError", "We want a security error message."); + } + }, +}); + +CookiePolicyHelper.runTest("IndexedDB in workers", { + cookieJarAccessAllowed: async w => { + function nonBlockCode() { + indexedDB.open("test", "1"); + postMessage(true); + } + + let blob = new w.Blob([nonBlockCode.toString() + "; nonBlockCode();"]); + ok(blob, "Blob has been created"); + + let blobURL = w.URL.createObjectURL(blob); + ok(blobURL, "Blob URL has been created"); + + let worker = new w.Worker(blobURL); + ok(worker, "Worker has been created"); + + await new w.Promise((resolve, reject) => { + worker.onmessage = function (e) { + if (e.data) { + resolve(); + } else { + reject(); + } + }; + + worker.onerror = function (e) { + reject(); + }; + }); + }, + + cookieJarAccessDenied: async w => { + function blockCode() { + try { + indexedDB.open("test", "1"); + postMessage(false); + } catch (e) { + postMessage(e.name == "SecurityError"); + } + } + + let blob = new w.Blob([blockCode.toString() + "; blockCode();"]); + ok(blob, "Blob has been created"); + + let blobURL = w.URL.createObjectURL(blob); + ok(blobURL, "Blob URL has been created"); + + let worker = new w.Worker(blobURL); + ok(worker, "Worker has been created"); + + await new w.Promise((resolve, reject) => { + worker.onmessage = function (e) { + if (e.data) { + resolve(); + } else { + reject(); + } + }; + + worker.onerror = function (e) { + reject(); + }; + }); + }, +}); diff --git a/netwerk/cookie/test/browser/browser_originattributes.js b/netwerk/cookie/test/browser/browser_originattributes.js new file mode 100644 index 0000000000..fab7e67b2e --- /dev/null +++ b/netwerk/cookie/test/browser/browser_originattributes.js @@ -0,0 +1,121 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +const USER_CONTEXTS = ["default", "personal", "work"]; + +const COOKIE_NAMES = ["cookie0", "cookie1", "cookie2"]; + +const TEST_URL = + "http://example.com/browser/netwerk/cookie/test/browser/file_empty.html"; + +// opens `uri' in a new tab with the provided userContextId and focuses it. +// returns the newly opened tab +async function openTabInUserContext(uri, userContextId) { + // open the tab in the correct userContextId + let tab = BrowserTestUtils.addTab(gBrowser, uri, { userContextId }); + + // select tab and make sure its browser is focused + gBrowser.selectedTab = tab; + tab.ownerGlobal.focus(); + + let browser = gBrowser.getBrowserForTab(tab); + // wait for tab load + await BrowserTestUtils.browserLoaded(browser); + + return { tab, browser }; +} + +add_setup(async function () { + // make sure userContext is enabled. + await new Promise(resolve => { + SpecialPowers.pushPrefEnv( + { set: [["privacy.userContext.enabled", true]] }, + resolve + ); + }); +}); + +add_task(async function test() { + // load the page in 3 different contexts and set a cookie + // which should only be visible in that context + for (let userContextId of Object.keys(USER_CONTEXTS)) { + // open our tab in the given user context + let { tab, browser } = await openTabInUserContext(TEST_URL, userContextId); + + await SpecialPowers.spawn( + browser, + [{ names: COOKIE_NAMES, value: USER_CONTEXTS[userContextId] }], + function (opts) { + for (let name of opts.names) { + content.document.cookie = name + "=" + opts.value; + } + } + ); + + // remove the tab + gBrowser.removeTab(tab); + } + + let expectedValues = USER_CONTEXTS.slice(0); + await checkCookies(expectedValues, "before removal"); + + // remove cookies that belongs to user context id #1 + Services.cookies.removeCookiesWithOriginAttributes( + JSON.stringify({ userContextId: 1 }) + ); + + expectedValues[1] = undefined; + await checkCookies(expectedValues, "after removal"); +}); + +async function checkCookies(expectedValues, time) { + for (let userContextId of Object.keys(expectedValues)) { + let cookiesFromTitle = await getCookiesFromJS(userContextId); + let cookiesFromManager = getCookiesFromManager(userContextId); + + let expectedValue = expectedValues[userContextId]; + for (let name of COOKIE_NAMES) { + is( + cookiesFromTitle[name], + expectedValue, + `User context ${userContextId}: ${name} should be correct from title ${time}` + ); + is( + cookiesFromManager[name], + expectedValue, + `User context ${userContextId}: ${name} should be correct from manager ${time}` + ); + } + } +} + +function getCookiesFromManager(userContextId) { + let cookies = {}; + let allCookies = Services.cookies.getCookiesWithOriginAttributes( + JSON.stringify({ userContextId }) + ); + for (let cookie of allCookies) { + cookies[cookie.name] = cookie.value; + } + return cookies; +} + +async function getCookiesFromJS(userContextId) { + let { tab, browser } = await openTabInUserContext(TEST_URL, userContextId); + + // get the cookies + let cookieString = await SpecialPowers.spawn(browser, [], function () { + return content.document.cookie; + }); + + // check each item in the title and validate it meets expectatations + let cookies = {}; + for (let cookie of cookieString.split(";")) { + let [name, value] = cookie.trim().split("="); + cookies[name] = value; + } + + gBrowser.removeTab(tab); + return cookies; +} diff --git a/netwerk/cookie/test/browser/browser_oversize.js b/netwerk/cookie/test/browser/browser_oversize.js new file mode 100644 index 0000000000..f6e1f8a70b --- /dev/null +++ b/netwerk/cookie/test/browser/browser_oversize.js @@ -0,0 +1,96 @@ +"use strict"; + +const OVERSIZE_DOMAIN = "http://example.com/"; +const OVERSIZE_PATH = "browser/netwerk/cookie/test/browser/"; +const OVERSIZE_TOP_PAGE = OVERSIZE_DOMAIN + OVERSIZE_PATH + "oversize.sjs"; + +add_task(async _ => { + const expected = []; + + const consoleListener = { + observe(what) { + if (!(what instanceof Ci.nsIConsoleMessage)) { + return; + } + + info("Console Listener: " + what); + for (let i = expected.length - 1; i >= 0; --i) { + const e = expected[i]; + + if (what.message.includes(e.match)) { + ok(true, "Message received: " + e.match); + expected.splice(i, 1); + e.resolve(); + } + } + }, + }; + + Services.console.registerListener(consoleListener); + + registerCleanupFunction(() => + Services.console.unregisterListener(consoleListener) + ); + + const netPromises = [ + new Promise(resolve => { + expected.push({ + resolve, + match: + "Cookie “a” is invalid because its size is too big. Max size is 4096 B.", + }); + }), + + new Promise(resolve => { + expected.push({ + resolve, + match: + "Cookie “b” is invalid because its path size is too big. Max size is 1024 B.", + }); + }), + ]; + + // Let's open our tab. + const tab = BrowserTestUtils.addTab(gBrowser, OVERSIZE_TOP_PAGE); + gBrowser.selectedTab = tab; + + const browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + // Let's wait for the first set of console events. + await Promise.all(netPromises); + + // the DOM list of events. + const domPromises = [ + new Promise(resolve => { + expected.push({ + resolve, + match: + "Cookie “d” is invalid because its size is too big. Max size is 4096 B.", + }); + }), + + new Promise(resolve => { + expected.push({ + resolve, + match: + "Cookie “e” is invalid because its path size is too big. Max size is 1024 B.", + }); + }), + ]; + + // Let's use document.cookie + SpecialPowers.spawn(browser, [], () => { + const maxBytesPerCookie = 4096; + const maxBytesPerCookiePath = 1024; + content.document.cookie = "d=" + Array(maxBytesPerCookie + 1).join("x"); + content.document.cookie = + "e=f; path=/" + Array(maxBytesPerCookiePath + 1).join("x"); + }); + + // Let's wait for the dom events. + await Promise.all(domPromises); + + // Let's close the tab. + BrowserTestUtils.removeTab(tab); +}); diff --git a/netwerk/cookie/test/browser/browser_partitionedConsole.js b/netwerk/cookie/test/browser/browser_partitionedConsole.js new file mode 100644 index 0000000000..ec834bfbcf --- /dev/null +++ b/netwerk/cookie/test/browser/browser_partitionedConsole.js @@ -0,0 +1,201 @@ +"use strict"; + +const DOMAIN = "https://example.com/"; +const PATH = "browser/netwerk/cookie/test/browser/"; +const TOP_PAGE = DOMAIN + PATH + "file_empty.html"; + +// Run the test with CHIPS disabled, expecting a warning message +add_task(async _ => { + await SpecialPowers.pushPrefEnv({ + set: [["network.cookie.cookieBehavior.optInPartitioning", false]], + }); + + const expected = []; + + const consoleListener = { + observe(what) { + if (!(what instanceof Ci.nsIConsoleMessage)) { + return; + } + + info("Console Listener: " + what); + for (let i = expected.length - 1; i >= 0; --i) { + const e = expected[i]; + + if (what.message.includes(e.match)) { + ok(true, "Message received: " + e.match); + expected.splice(i, 1); + e.resolve(); + } + } + }, + }; + + Services.console.registerListener(consoleListener); + + registerCleanupFunction(() => + Services.console.unregisterListener(consoleListener) + ); + + const netPromises = [ + new Promise(resolve => { + expected.push({ + resolve, + match: + "Cookie “a” will soon be rejected because it is foreign and does not have the “Partitioned“ attribute.", + }); + }), + ]; + + // Let's open our tab. + const tab = BrowserTestUtils.addTab(gBrowser, TOP_PAGE); + gBrowser.selectedTab = tab; + const browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + // Set cookies with cross-site HTTP + await SpecialPowers.spawn(browser, [], async function () { + await content.fetch( + "https://example.org/browser/netwerk/cookie/test/browser/partitioned.sjs", + { credentials: "include" } + ); + }); + + // Let's wait for the first set of console events. + await Promise.all(netPromises); + + // Let's close the tab. + BrowserTestUtils.removeTab(tab); +}); + +// Run the test with CHIPS enabled, expecting a different warning message +add_task(async _ => { + await SpecialPowers.pushPrefEnv({ + set: [["network.cookie.cookieBehavior.optInPartitioning", true]], + }); + + const expected = []; + + const consoleListener = { + observe(what) { + if (!(what instanceof Ci.nsIConsoleMessage)) { + return; + } + + info("Console Listener: " + what); + for (let i = expected.length - 1; i >= 0; --i) { + const e = expected[i]; + + if (what.message.includes(e.match)) { + ok(true, "Message received: " + e.match); + expected.splice(i, 1); + e.resolve(); + } + } + }, + }; + + Services.console.registerListener(consoleListener); + + registerCleanupFunction(() => + Services.console.unregisterListener(consoleListener) + ); + + const netPromises = [ + new Promise(resolve => { + expected.push({ + resolve, + match: + "Cookie “a” has been rejected because it is foreign and does not have the “Partitioned“ attribute.", + }); + }), + ]; + + // Let's open our tab. + const tab = BrowserTestUtils.addTab(gBrowser, TOP_PAGE); + gBrowser.selectedTab = tab; + const browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + // Set cookies with cross-site HTTP + await SpecialPowers.spawn(browser, [], async function () { + await content.fetch( + "https://example.org/browser/netwerk/cookie/test/browser/partitioned.sjs", + { credentials: "include" } + ); + }); + + // Let's wait for the first set of console events. + await Promise.all(netPromises); + + // Let's close the tab. + BrowserTestUtils.removeTab(tab); +}); + +// Run the test with CHIPS enabled, ensuring the partitioned cookies require +// secure context. +add_task(async function partitionedAttrRequiresSecure() { + await SpecialPowers.pushPrefEnv({ + set: [["network.cookie.cookieBehavior.optInPartitioning", true]], + }); + + // Clear all cookies before testing. + Services.cookies.removeAll(); + + const expected = []; + + const consoleListener = { + observe(what) { + if (!(what instanceof Ci.nsIConsoleMessage)) { + return; + } + + info("Console Listener: " + what); + for (let i = expected.length - 1; i >= 0; --i) { + const e = expected[i]; + + if (what.message.includes(e.match)) { + ok(true, "Message received: " + e.match); + expected.splice(i, 1); + e.resolve(); + } + } + }, + }; + + Services.console.registerListener(consoleListener); + + registerCleanupFunction(() => + Services.console.unregisterListener(consoleListener) + ); + + const netPromises = [ + new Promise(resolve => { + expected.push({ + resolve, + match: + "Cookie “c” has been rejected because it has the “Partitioned” attribute but is missing the “secure” attribute.", + }); + }), + ]; + + // Let's open our tab. + const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TOP_PAGE); + + // Set cookies with cross-site HTTP + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + await content.fetch( + "https://example.org/browser/netwerk/cookie/test/browser/partitioned.sjs?nosecure", + { credentials: "include" } + ); + }); + + // Let's wait for the first set of console events. + await Promise.all(netPromises); + + // Ensure no cookie is set. + is(Services.cookies.cookies.length, 0, "No cookie is set."); + + // Let's close the tab. + BrowserTestUtils.removeTab(tab); +}); diff --git a/netwerk/cookie/test/browser/browser_partitioned_telemetry.js b/netwerk/cookie/test/browser/browser_partitioned_telemetry.js new file mode 100644 index 0000000000..f89bcdd189 --- /dev/null +++ b/netwerk/cookie/test/browser/browser_partitioned_telemetry.js @@ -0,0 +1,134 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +const TEST_URL = + "https://example.com/browser/netwerk/cookie/test/browser/file_empty.html"; + +async function validateTelemetryValues( + { setCookies, setForeigns, setPartitioneds, setForeignPartitioneds }, + message +) { + await Services.fog.testFlushAllChildren(); + let setCookieTelemetry = Glean.networking.setCookie.testGetValue(); + is( + setCookieTelemetry ?? undefined, + setCookies, + message + " - all set cookies" + ); + let foreignTelemetry = Glean.networking.setCookieForeign.testGetValue(); + is( + foreignTelemetry?.numerator, + setForeigns, + message + " - foreign set cookies" + ); + is( + foreignTelemetry?.denominator, + setCookies, + message + " - foreign set cookies denominator" + ); + let partitonedTelemetry = + Glean.networking.setCookiePartitioned.testGetValue(); + is( + partitonedTelemetry?.numerator, + setPartitioneds, + message + " - partitioned set cookies" + ); + is( + partitonedTelemetry?.denominator, + setCookies, + message + " - partitioned set cookies denominator" + ); + let foreignPartitonedTelemetry = + Glean.networking.setCookieForeignPartitioned.testGetValue(); + is( + foreignPartitonedTelemetry?.numerator, + setForeignPartitioneds, + message + " - foreign partitioned set cookies" + ); + is( + foreignPartitonedTelemetry?.denominator, + setCookies, + message + " - foreign partitioned set cookies denominator" + ); +} + +add_task(async () => { + await Services.fog.testFlushAllChildren(); + Services.fog.testResetFOG(); + await validateTelemetryValues({}, "initially empty"); + + // open a browser window for the test + let tab = BrowserTestUtils.addTab(gBrowser, TEST_URL); + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + // Set cookies with Javascript + await SpecialPowers.spawn(browser, [], function () { + content.document.cookie = "a=1; Partitioned; SameSite=None; Secure"; + content.document.cookie = "b=2; SameSite=None; Secure"; + }); + await validateTelemetryValues( + { + setCookies: 2, + setForeigns: 0, + setPartitioneds: 1, + setForeignPartitioneds: 0, + }, + "javascript cookie" + ); + + // Set cookies with HTTP + await SpecialPowers.spawn(browser, [], async function () { + await content.fetch("partitioned.sjs"); + }); + await validateTelemetryValues( + { + setCookies: 4, + setForeigns: 0, + setPartitioneds: 2, + setForeignPartitioneds: 0, + }, + "same site fetch" + ); + + // Set cookies with cross-site HTTP + await SpecialPowers.spawn(browser, [], async function () { + await content.fetch( + "https://example.org/browser/netwerk/cookie/test/browser/partitioned.sjs", + { credentials: "include" } + ); + }); + await validateTelemetryValues( + { + setCookies: 6, + setForeigns: 2, + setPartitioneds: 3, + setForeignPartitioneds: 1, + }, + "foreign fetch" + ); + + // Set cookies with cross-site HTTP redirect + await SpecialPowers.spawn(browser, [], async function () { + await content.fetch( + encodeURI( + "https://example.org/browser/netwerk/cookie/test/browser/partitioned.sjs?redirect=https://example.com/browser/netwerk/cookie/test/browser/partitioned.sjs?nocookie" + ), + { credentials: "include" } + ); + }); + + await validateTelemetryValues( + { + setCookies: 8, + setForeigns: 4, + setPartitioneds: 4, + setForeignPartitioneds: 2, + }, + "foreign fetch redirect" + ); + + // remove the tab + gBrowser.removeTab(tab); +}); diff --git a/netwerk/cookie/test/browser/browser_sameSiteConsole.js b/netwerk/cookie/test/browser/browser_sameSiteConsole.js new file mode 100644 index 0000000000..84527296b2 --- /dev/null +++ b/netwerk/cookie/test/browser/browser_sameSiteConsole.js @@ -0,0 +1,133 @@ +"use strict"; + +const SAMESITE_DOMAIN = "http://example.com/"; +const SAMESITE_PATH = "browser/netwerk/cookie/test/browser/"; +const SAMESITE_TOP_PAGE = SAMESITE_DOMAIN + SAMESITE_PATH + "sameSite.sjs"; + +add_task(async _ => { + await SpecialPowers.pushPrefEnv({ + set: [ + ["network.cookie.sameSite.laxByDefault", true], + ["network.cookie.sameSite.noneRequiresSecure", true], + ], + }); + + const expected = []; + + const consoleListener = { + observe(what) { + if (!(what instanceof Ci.nsIConsoleMessage)) { + return; + } + + info("Console Listener: " + what); + for (let i = expected.length - 1; i >= 0; --i) { + const e = expected[i]; + + if (what.message.includes(e.match)) { + ok(true, "Message received: " + e.match); + expected.splice(i, 1); + e.resolve(); + } + } + }, + }; + + Services.console.registerListener(consoleListener); + + registerCleanupFunction(() => + Services.console.unregisterListener(consoleListener) + ); + + const netPromises = [ + new Promise(resolve => { + expected.push({ + resolve, + match: + "Cookie “a” has “SameSite” policy set to “Lax” because it is missing a “SameSite” attribute, and “SameSite=Lax” is the default value for this attribute.", + }); + }), + + new Promise(resolve => { + expected.push({ + resolve, + match: + "Cookie “b” rejected because it has the “SameSite=None” attribute but is missing the “secure” attribute.", + }); + }), + + new Promise(resolve => { + expected.push({ + resolve, + match: + "Invalid “SameSite“ value for cookie “c”. The supported values are: “Lax“, “Strict“, “None“.", + }); + }), + + new Promise(resolve => { + expected.push({ + resolve, + match: + "Cookie “c” has “SameSite” policy set to “Lax” because it is missing a “SameSite” attribute, and “SameSite=Lax” is the default value for this attribute.", + }); + }), + ]; + + // Let's open our tab. + const tab = BrowserTestUtils.addTab(gBrowser, SAMESITE_TOP_PAGE); + gBrowser.selectedTab = tab; + + const browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + // Let's wait for the first set of console events. + await Promise.all(netPromises); + + // the DOM list of events. + const domPromises = [ + new Promise(resolve => { + expected.push({ + resolve, + match: + "Cookie “d” has “SameSite” policy set to “Lax” because it is missing a “SameSite” attribute, and “SameSite=Lax” is the default value for this attribute.", + }); + }), + + new Promise(resolve => { + expected.push({ + resolve, + match: + "Cookie “e” rejected because it has the “SameSite=None” attribute but is missing the “secure” attribute.", + }); + }), + + new Promise(resolve => { + expected.push({ + resolve, + match: + "Invalid “SameSite“ value for cookie “f”. The supported values are: “Lax“, “Strict“, “None“.", + }); + }), + + new Promise(resolve => { + expected.push({ + resolve, + match: + "Cookie “f” has “SameSite” policy set to “Lax” because it is missing a “SameSite” attribute, and “SameSite=Lax” is the default value for this attribute.", + }); + }), + ]; + + // Let's use document.cookie + SpecialPowers.spawn(browser, [], () => { + content.document.cookie = "d=4"; + content.document.cookie = "e=5; sameSite=none"; + content.document.cookie = "f=6; sameSite=batmat"; + }); + + // Let's wait for the dom events. + await Promise.all(domPromises); + + // Let's close the tab. + BrowserTestUtils.removeTab(tab); +}); diff --git a/netwerk/cookie/test/browser/browser_serviceWorker.js b/netwerk/cookie/test/browser/browser_serviceWorker.js new file mode 100644 index 0000000000..2a5c963535 --- /dev/null +++ b/netwerk/cookie/test/browser/browser_serviceWorker.js @@ -0,0 +1,45 @@ +"use strict"; + +CookiePolicyHelper.runTest("ServiceWorker", { + prefs: [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.ipc.processCount", 1], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ], + + cookieJarAccessAllowed: async w => { + await w.navigator.serviceWorker + .register("file_empty.js") + .then( + reg => { + ok(true, "ServiceWorker can be used!"); + return reg; + }, + _ => { + ok(false, "ServiceWorker cannot be used! " + _); + } + ) + .then( + reg => reg.unregister(), + _ => { + ok(false, "unregister failed"); + } + ) + .catch(e => ok(false, "Promise rejected: " + e)); + }, + + cookieJarAccessDenied: async w => { + await w.navigator.serviceWorker + .register("file_empty.js") + .then( + _ => { + ok(false, "ServiceWorker cannot be used!"); + }, + _ => { + ok(true, "ServiceWorker cannot be used!"); + } + ) + .catch(e => ok(false, "Promise rejected: " + e)); + }, +}); diff --git a/netwerk/cookie/test/browser/browser_sharedWorker.js b/netwerk/cookie/test/browser/browser_sharedWorker.js new file mode 100644 index 0000000000..88a8b3f0e7 --- /dev/null +++ b/netwerk/cookie/test/browser/browser_sharedWorker.js @@ -0,0 +1,18 @@ +"use strict"; + +CookiePolicyHelper.runTest("SharedWorker", { + cookieJarAccessAllowed: async w => { + new w.SharedWorker("a.js", "foo"); + ok(true, "SharedWorker is allowed"); + }, + + cookieJarAccessDenied: async w => { + try { + new w.SharedWorker("a.js", "foo"); + ok(false, "SharedWorker cannot be used!"); + } catch (e) { + ok(true, "SharedWorker cannot be used!"); + is(e.name, "SecurityError", "We want a security error message."); + } + }, +}); diff --git a/netwerk/cookie/test/browser/browser_storage.js b/netwerk/cookie/test/browser/browser_storage.js new file mode 100644 index 0000000000..1e37b1a367 --- /dev/null +++ b/netwerk/cookie/test/browser/browser_storage.js @@ -0,0 +1,43 @@ +"use strict"; + +CookiePolicyHelper.runTest("SessionStorage", { + cookieJarAccessAllowed: async w => { + try { + w.sessionStorage.foo = 42; + ok(true, "SessionStorage works"); + } catch (e) { + ok(false, "SessionStorage works"); + } + }, + + cookieJarAccessDenied: async w => { + try { + w.sessionStorage.foo = 42; + ok(false, "SessionStorage doesn't work"); + } catch (e) { + ok(true, "SessionStorage doesn't work"); + is(e.name, "SecurityError", "We want a security error message."); + } + }, +}); + +CookiePolicyHelper.runTest("LocalStorage", { + cookieJarAccessAllowed: async w => { + try { + w.localStorage.foo = 42; + ok(true, "LocalStorage works"); + } catch (e) { + ok(false, "LocalStorage works"); + } + }, + + cookieJarAccessDenied: async w => { + try { + w.localStorage.foo = 42; + ok(false, "LocalStorage doesn't work"); + } catch (e) { + ok(true, "LocalStorage doesn't work"); + is(e.name, "SecurityError", "We want a security error message."); + } + }, +}); diff --git a/netwerk/cookie/test/browser/file_empty.html b/netwerk/cookie/test/browser/file_empty.html new file mode 100644 index 0000000000..78b64149c4 --- /dev/null +++ b/netwerk/cookie/test/browser/file_empty.html @@ -0,0 +1,2 @@ +<html><body> +</body></html> diff --git a/netwerk/cookie/test/browser/file_empty.js b/netwerk/cookie/test/browser/file_empty.js new file mode 100644 index 0000000000..3053583c76 --- /dev/null +++ b/netwerk/cookie/test/browser/file_empty.js @@ -0,0 +1 @@ +/* nothing here */ diff --git a/netwerk/cookie/test/browser/head.js b/netwerk/cookie/test/browser/head.js new file mode 100644 index 0000000000..609ad683db --- /dev/null +++ b/netwerk/cookie/test/browser/head.js @@ -0,0 +1,201 @@ +const { PermissionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PermissionTestUtils.sys.mjs" +); + +const BEHAVIOR_ACCEPT = Ci.nsICookieService.BEHAVIOR_ACCEPT; +const BEHAVIOR_REJECT = Ci.nsICookieService.BEHAVIOR_REJECT; + +const PERM_DEFAULT = Ci.nsICookiePermission.ACCESS_DEFAULT; +const PERM_ALLOW = Ci.nsICookiePermission.ACCESS_ALLOW; +const PERM_DENY = Ci.nsICookiePermission.ACCESS_DENY; + +const TEST_DOMAIN = "https://example.com/"; +const TEST_PATH = "browser/netwerk/cookie/test/browser/"; +const TEST_TOP_PAGE = TEST_DOMAIN + TEST_PATH + "file_empty.html"; + +// Helper to eval() provided cookieJarAccessAllowed and cookieJarAccessDenied +// toString()ed optionally async function in freshly created tabs with +// BEHAVIOR_ACCEPT and BEHAVIOR_REJECT configured, respectively, in a number of +// permutations. This includes verifying that changing the permission while the +// page is open still results in the state of the permission when the +// document/global was created still applying. Code will execute in the +// ContentTask.spawn frame-script context, use content to access the underlying +// page. +this.CookiePolicyHelper = { + runTest(testName, config) { + // Testing allowed to blocked by cookie behavior + this._createTest( + testName, + config.cookieJarAccessAllowed, + config.cookieJarAccessDenied, + config.prefs, + { + fromBehavior: BEHAVIOR_ACCEPT, + toBehavior: BEHAVIOR_REJECT, + fromPermission: PERM_DEFAULT, + toPermission: PERM_DEFAULT, + } + ); + + // Testing blocked to allowed by cookie behavior + this._createTest( + testName, + config.cookieJarAccessDenied, + config.cookieJarAccessAllowed, + config.prefs, + { + fromBehavior: BEHAVIOR_REJECT, + toBehavior: BEHAVIOR_ACCEPT, + fromPermission: PERM_DEFAULT, + toPermission: PERM_DEFAULT, + } + ); + + // Testing allowed to blocked by cookie permission + this._createTest( + testName, + config.cookieJarAccessAllowed, + config.cookieJarAccessDenied, + config.prefs, + { + fromBehavior: BEHAVIOR_REJECT, + toBehavior: BEHAVIOR_REJECT, + fromPermission: PERM_ALLOW, + toPermission: PERM_DEFAULT, + } + ); + + // Testing blocked to allowed by cookie permission + this._createTest( + testName, + config.cookieJarAccessDenied, + config.cookieJarAccessAllowed, + config.prefs, + { + fromBehavior: BEHAVIOR_ACCEPT, + toBehavior: BEHAVIOR_ACCEPT, + fromPermission: PERM_DENY, + toPermission: PERM_DEFAULT, + } + ); + }, + + _createTest(testName, goodCb, badCb, prefs, config) { + add_task(async _ => { + info("Starting " + testName + ": " + config.toSource()); + + await SpecialPowers.flushPrefEnv(); + + if (prefs) { + await SpecialPowers.pushPrefEnv({ set: prefs }); + } + + // Let's set the first cookie pref. + PermissionTestUtils.add(TEST_DOMAIN, "cookie", config.fromPermission); + await SpecialPowers.pushPrefEnv({ + set: [["network.cookie.cookieBehavior", config.fromBehavior]], + }); + + // Let's open a tab and load content. + let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + // Let's create an iframe. + await SpecialPowers.spawn( + browser, + [{ url: TEST_TOP_PAGE }], + async obj => { + return new content.Promise(resolve => { + let ifr = content.document.createElement("iframe"); + ifr.setAttribute("id", "iframe"); + ifr.src = obj.url; + ifr.onload = () => resolve(); + content.document.body.appendChild(ifr); + }); + } + ); + + // Let's exec the "good" callback. + info( + "Executing the test after setting the cookie behavior to " + + config.fromBehavior + + " and permission to " + + config.fromPermission + ); + await SpecialPowers.spawn( + browser, + [{ callback: goodCb.toString() }], + async obj => { + let runnableStr = `(() => {return (${obj.callback});})();`; + let runnable = eval(runnableStr); // eslint-disable-line no-eval + await runnable(content); + + let ifr = content.document.getElementById("iframe"); + await runnable(ifr.contentWindow); + } + ); + + // Now, let's change the cookie settings + PermissionTestUtils.add(TEST_DOMAIN, "cookie", config.toPermission); + await SpecialPowers.pushPrefEnv({ + set: [["network.cookie.cookieBehavior", config.toBehavior]], + }); + + // We still want the good callback to succeed. + info( + "Executing the test after setting the cookie behavior to " + + config.toBehavior + + " and permission to " + + config.toPermission + ); + await SpecialPowers.spawn( + browser, + [{ callback: goodCb.toString() }], + async obj => { + let runnableStr = `(() => {return (${obj.callback});})();`; + let runnable = eval(runnableStr); // eslint-disable-line no-eval + await runnable(content); + + let ifr = content.document.getElementById("iframe"); + await runnable(ifr.contentWindow); + } + ); + + // Let's close the tab. + BrowserTestUtils.removeTab(tab); + + // Let's open a new tab and load content again. + tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE); + gBrowser.selectedTab = tab; + + browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + // Let's exec the "bad" callback. + info("Executing the test in a new tab"); + await SpecialPowers.spawn( + browser, + [{ callback: badCb.toString() }], + async obj => { + let runnableStr = `(() => {return (${obj.callback});})();`; + let runnable = eval(runnableStr); // eslint-disable-line no-eval + await runnable(content); + } + ); + + // Let's close the tab. + BrowserTestUtils.removeTab(tab); + + // Cleanup. + await new Promise(resolve => { + Services.clearData.deleteData( + Ci.nsIClearDataService.CLEAR_ALL, + resolve + ); + }); + }); + }, +}; diff --git a/netwerk/cookie/test/browser/oversize.sjs b/netwerk/cookie/test/browser/oversize.sjs new file mode 100644 index 0000000000..dfe2f31645 --- /dev/null +++ b/netwerk/cookie/test/browser/oversize.sjs @@ -0,0 +1,17 @@ +function handleRequest(aRequest, aResponse) { + aResponse.setStatusLine(aRequest.httpVersion, 200); + + const maxBytesPerCookie = 4096; + const maxBytesPerCookiePath = 1024; + + aResponse.setHeader( + "Set-Cookie", + "a=" + Array(maxBytesPerCookie + 1).join("x"), + true + ); + aResponse.setHeader( + "Set-Cookie", + "b=c; path=/" + Array(maxBytesPerCookiePath + 1).join("x"), + true + ); +} diff --git a/netwerk/cookie/test/browser/partitioned.sjs b/netwerk/cookie/test/browser/partitioned.sjs new file mode 100644 index 0000000000..5649b88f2f --- /dev/null +++ b/netwerk/cookie/test/browser/partitioned.sjs @@ -0,0 +1,32 @@ +function handleRequest(aRequest, aResponse) { + if (aRequest.hasHeader("Origin")) { + let origin = aRequest.getHeader("Origin"); + aResponse.setHeader("Access-Control-Allow-Origin", origin); + aResponse.setHeader("Access-Control-Allow-Credentials", "true"); + } + + var params = new URLSearchParams(aRequest.queryString); + if (params.has("redirect")) { + aResponse.setHeader("Location", params.get("redirect")); + aResponse.setStatusLine(aRequest.httpVersion, 302); + } else { + aResponse.setStatusLine(aRequest.httpVersion, 200); + } + + if (params.has("nocookie")) { + return; + } + + if (params.has("nosecure")) { + aResponse.setHeader("Set-Cookie", "c=3; Partitioned;", true); + + return; + } + + aResponse.setHeader("Set-Cookie", "a=1; SameSite=None; Secure", true); + aResponse.setHeader( + "Set-Cookie", + "b=2; Partitioned; SameSite=None; Secure", + true + ); +} diff --git a/netwerk/cookie/test/browser/sameSite.sjs b/netwerk/cookie/test/browser/sameSite.sjs new file mode 100644 index 0000000000..a19624d2cb --- /dev/null +++ b/netwerk/cookie/test/browser/sameSite.sjs @@ -0,0 +1,7 @@ +function handleRequest(aRequest, aResponse) { + aResponse.setStatusLine(aRequest.httpVersion, 200); + + aResponse.setHeader("Set-Cookie", "a=1", true); + aResponse.setHeader("Set-Cookie", "b=2; sameSite=none", true); + aResponse.setHeader("Set-Cookie", "c=3; sameSite=batman", true); +} diff --git a/netwerk/cookie/test/browser/server.sjs b/netwerk/cookie/test/browser/server.sjs new file mode 100644 index 0000000000..86835914bb --- /dev/null +++ b/netwerk/cookie/test/browser/server.sjs @@ -0,0 +1,9 @@ +function handleRequest(aRequest, aResponse) { + aResponse.setStatusLine(aRequest.httpVersion, 200); + if (aRequest.hasHeader("Cookie")) { + aResponse.write("cookie-present"); + } else { + aResponse.setHeader("Set-Cookie", "foopy=1"); + aResponse.write("cookie-not-present"); + } +} diff --git a/netwerk/cookie/test/mochitest/cookie.sjs b/netwerk/cookie/test/mochitest/cookie.sjs new file mode 100644 index 0000000000..75d4e638b4 --- /dev/null +++ b/netwerk/cookie/test/mochitest/cookie.sjs @@ -0,0 +1,166 @@ +function handleRequest(aRequest, aResponse) { + let parts = aRequest.queryString.split("&"); + if (parts.includes("window")) { + aResponse.setStatusLine(aRequest.httpVersion, 200); + aResponse.setHeader("Content-Type", "text/html"); + aResponse.setHeader("Clear-Site-Data", '"cache", "cookies", "storage"'); + aResponse.write("<body><h1>Welcome</h1></body>"); + return; + } + + if (parts.includes("fetch")) { + setState( + "data", + JSON.stringify({ type: "fetch", hasCookie: aRequest.hasHeader("Cookie") }) + ); + aResponse.write("Hello world!"); + return; + } + + if (parts.includes("xhr")) { + setState( + "data", + JSON.stringify({ type: "xhr", hasCookie: aRequest.hasHeader("Cookie") }) + ); + aResponse.write("Hello world!"); + return; + } + + if (parts.includes("image")) { + setState( + "data", + JSON.stringify({ type: "image", hasCookie: aRequest.hasHeader("Cookie") }) + ); + + // A 1x1 PNG image. + // Source: https://commons.wikimedia.org/wiki/File:1x1.png (Public Domain) + const IMAGE = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAA" + + "ACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=" + ); + + aResponse.setHeader("Content-Type", "image/png", false); + aResponse.write(IMAGE); + return; + } + + if (parts.includes("script")) { + setState( + "data", + JSON.stringify({ + type: "script", + hasCookie: aRequest.hasHeader("Cookie"), + }) + ); + + aResponse.setHeader("Content-Type", "text/javascript", false); + aResponse.write("window.scriptLoaded();"); + return; + } + + if (parts.includes("worker")) { + setState( + "data", + JSON.stringify({ + type: "worker", + hasCookie: aRequest.hasHeader("Cookie"), + }) + ); + + function w() { + onmessage = e => { + if (e.data == "subworker") { + importScripts("cookie.sjs?subworker&" + Math.random()); + postMessage(42); + return; + } + + if (e.data == "fetch") { + fetch("cookie.sjs?fetch&" + Math.random()) + .then(r => r.text()) + .then(_ => postMessage(42)); + return; + } + + if (e.data == "xhr") { + let xhr = new XMLHttpRequest(); + xhr.open("GET", "cookie.sjs?xhr&" + Math.random()); + xhr.send(); + xhr.onload = _ => postMessage(42); + } + }; + postMessage(42); + } + + aResponse.setHeader("Content-Type", "text/javascript", false); + aResponse.write(w.toString() + "; w();"); + return; + } + + if (parts.includes("subworker")) { + setState( + "data", + JSON.stringify({ + type: "subworker", + hasCookie: aRequest.hasHeader("Cookie"), + }) + ); + aResponse.setHeader("Content-Type", "text/javascript", false); + aResponse.write("42"); + return; + } + + if (parts.includes("sharedworker")) { + setState( + "data", + JSON.stringify({ + type: "sharedworker", + hasCookie: aRequest.hasHeader("Cookie"), + }) + ); + + // This function is exported as a string. + /* eslint-disable no-undef */ + function w() { + onconnect = e => { + e.ports[0].onmessage = evt => { + if (evt.data == "subworker") { + importScripts("cookie.sjs?subworker&" + Math.random()); + e.ports[0].postMessage(42); + return; + } + + if (evt.data == "fetch") { + fetch("cookie.sjs?fetch&" + Math.random()) + .then(r => r.text()) + .then(_ => e.ports[0].postMessage(42)); + return; + } + + if (evt.data == "xhr") { + let xhr = new XMLHttpRequest(); + xhr.open("GET", "cookie.sjs?xhr&" + Math.random()); + xhr.send(); + xhr.onload = _ => e.ports[0].postMessage(42); + } + }; + e.ports[0].postMessage(42); + }; + } + /* eslint-enable no-undef */ + + aResponse.setHeader("Content-Type", "text/javascript", false); + aResponse.write(w.toString() + "; w();"); + return; + } + + if (parts.includes("last")) { + let data = getState("data"); + setState("data", ""); + aResponse.write(data); + return; + } + + aResponse.setStatusLine(aRequest.httpVersion, 400); + aResponse.write("Invalid request"); +} diff --git a/netwerk/cookie/test/mochitest/cookiesHelper.js b/netwerk/cookie/test/mochitest/cookiesHelper.js new file mode 100644 index 0000000000..cbff91f2f2 --- /dev/null +++ b/netwerk/cookie/test/mochitest/cookiesHelper.js @@ -0,0 +1,63 @@ +const ALLOWED = 0; +const BLOCKED = 1; + +async function cleanupData() { + await new Promise(resolve => { + const chromeScript = SpecialPowers.loadChromeScript(_ => { + /* eslint-env mozilla/chrome-script */ + addMessageListener("go", __ => { + Services.clearData.deleteData( + Services.clearData.CLEAR_COOKIES | + Services.clearData.CLEAR_ALL_CACHES | + Services.clearData.CLEAR_DOM_STORAGES, + ___ => { + sendAsyncMessage("done"); + } + ); + }); + }); + + chromeScript.addMessageListener("done", _ => { + chromeScript.destroy(); + resolve(); + }); + + chromeScript.sendAsyncMessage("go"); + }); +} + +async function checkLastRequest(type, state) { + let json = await fetch("cookie.sjs?last&" + Math.random()).then(r => + r.json() + ); + is(json.type, type, "Type: " + type); + is(json.hasCookie, state == ALLOWED, "Fetch has cookies"); +} + +async function runTests(currentTest) { + await cleanupData(); + await SpecialPowers.pushPrefEnv({ + set: [["network.cookie.cookieBehavior", 2]], + }); + let windowBlocked = window.open("cookie.sjs?window&" + Math.random()); + await new Promise(resolve => { + windowBlocked.onload = resolve; + }); + await currentTest(windowBlocked, BLOCKED); + windowBlocked.close(); + + await cleanupData(); + await SpecialPowers.pushPrefEnv({ + set: [["network.cookie.cookieBehavior", 1]], + }); + let windowAllowed = window.open("cookie.sjs?window&" + Math.random()); + await new Promise(resolve => { + windowAllowed.onload = resolve; + }); + await currentTest(windowAllowed, ALLOWED); + windowAllowed.close(); + + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); diff --git a/netwerk/cookie/test/mochitest/empty.html b/netwerk/cookie/test/mochitest/empty.html new file mode 100644 index 0000000000..cd161cc52d --- /dev/null +++ b/netwerk/cookie/test/mochitest/empty.html @@ -0,0 +1 @@ +<h1>Nothing here</h1> diff --git a/netwerk/cookie/test/mochitest/mochitest.toml b/netwerk/cookie/test/mochitest/mochitest.toml new file mode 100644 index 0000000000..fdf26a7e94 --- /dev/null +++ b/netwerk/cookie/test/mochitest/mochitest.toml @@ -0,0 +1,27 @@ +[DEFAULT] +scheme = "https" +support-files = [ + "cookie.sjs", + "cookiesHelper.js", +] + +["test_document_cookie.html"] + +["test_document_cookie_notification.html"] + +["test_fetch.html"] + +["test_image.html"] + +["test_metaTag.html"] + +["test_script.html"] + +["test_sharedWorker.html"] + +["test_worker.html"] + +["test_xhr.html"] + +["test_xmlDocument.html"] +support-files = ["empty.html"] diff --git a/netwerk/cookie/test/mochitest/test_document_cookie.html b/netwerk/cookie/test/mochitest/test_document_cookie.html new file mode 100644 index 0000000000..86e7c7f661 --- /dev/null +++ b/netwerk/cookie/test/mochitest/test_document_cookie.html @@ -0,0 +1,20 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for document.cookie when the policy changes</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript" src="cookiesHelper.js"></script> +</head> +<body> +<script type="application/javascript"> + +runTests(async (w, state) => { + is(w.document.cookie.length, 0, "No cookie to start!"); + w.document.cookie = "name=value"; + is(w.document.cookie.includes("name=value"), state == ALLOWED, "Some cookies for me"); +}); + +</script> +</body> +</html> diff --git a/netwerk/cookie/test/mochitest/test_document_cookie_notification.html b/netwerk/cookie/test/mochitest/test_document_cookie_notification.html new file mode 100644 index 0000000000..b84b6ed045 --- /dev/null +++ b/netwerk/cookie/test/mochitest/test_document_cookie_notification.html @@ -0,0 +1,32 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for document.cookie setter + notification</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="application/javascript"> + +function Listener() { + SpecialPowers.addObserver(this, "document-set-cookie"); +} + +Listener.prototype = { + observe(aSubject, aTopic, aData) { + is(aTopic, "document-set-cookie", "Notification received"); + ok(aData.startsWith("a="), "Right cookie received"); + + SpecialPowers.removeObserver(this, "document-set-cookie"); + SimpleTest.finish(); + } +} + +const cl = new Listener(); +document.cookie = "a=" + Math.random(); +SimpleTest.waitForExplicitFinish(); + +</script> +</body> +</html> diff --git a/netwerk/cookie/test/mochitest/test_fetch.html b/netwerk/cookie/test/mochitest/test_fetch.html new file mode 100644 index 0000000000..315d0d7624 --- /dev/null +++ b/netwerk/cookie/test/mochitest/test_fetch.html @@ -0,0 +1,20 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for cookies + fetch when the policy changes</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript" src="cookiesHelper.js"></script> +</head> +<body> +<script type="application/javascript"> + +runTests(async (w, state) => { + w.document.cookie = "name=value"; + await w.fetch("cookie.sjs?fetch&" + Math.random()).then(r => r.text()); + await checkLastRequest("fetch", state); +}); + +</script> +</body> +</html> diff --git a/netwerk/cookie/test/mochitest/test_image.html b/netwerk/cookie/test/mochitest/test_image.html new file mode 100644 index 0000000000..4a49d64169 --- /dev/null +++ b/netwerk/cookie/test/mochitest/test_image.html @@ -0,0 +1,24 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for cookies and image loading when the policy changes</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript" src="cookiesHelper.js"></script> +</head> +<body> +<script type="application/javascript"> + +runTests(async (w, state) => { + w.document.cookie = "name=value"; + + let image = new w.Image(); + image.src = "cookie.sjs?image&" + Math.random(); + w.document.body.appendChild(image); + await new w.Promise(resolve => { image.onload = resolve; }); + await checkLastRequest("image", state); +}); + +</script> +</body> +</html> diff --git a/netwerk/cookie/test/mochitest/test_metaTag.html b/netwerk/cookie/test/mochitest/test_metaTag.html new file mode 100644 index 0000000000..48d360d5b2 --- /dev/null +++ b/netwerk/cookie/test/mochitest/test_metaTag.html @@ -0,0 +1,24 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for meta tag</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<script type="application/javascript"> + +document.addEventListener("DOMContentLoaded", _ => { + try { + document.write('<meta content=a http-equiv="set-cookie">'); + } catch (e) {} + + ok(true, "No crash!"); + SimpleTest.finish(); +}); + +SimpleTest.waitForExplicitFinish(); + +</script> +</body> +</html> diff --git a/netwerk/cookie/test/mochitest/test_script.html b/netwerk/cookie/test/mochitest/test_script.html new file mode 100644 index 0000000000..9f4b9f846d --- /dev/null +++ b/netwerk/cookie/test/mochitest/test_script.html @@ -0,0 +1,25 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for cookies + script loading when the policy changes</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript" src="cookiesHelper.js"></script> +</head> +<body> +<script type="application/javascript"> + +runTests(async (w, state) => { + w.document.cookie = "name=value"; + + let p = new w.Promise(resolve => { w.scriptLoaded = resolve; }); + let script = document.createElement("script"); + script.src = "cookie.sjs?script&" + Math.random(); + w.document.body.appendChild(script); + await p; + await checkLastRequest("script", state); +}); + +</script> +</body> +</html> diff --git a/netwerk/cookie/test/mochitest/test_sharedWorker.html b/netwerk/cookie/test/mochitest/test_sharedWorker.html new file mode 100644 index 0000000000..c29bf86a88 --- /dev/null +++ b/netwerk/cookie/test/mochitest/test_sharedWorker.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for cookies + SharedWorker loading when the policy changes</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript" src="cookiesHelper.js"></script> +</head> +<body> +<script type="application/javascript"> + +runTests(async (w, state) => { + w.document.cookie = "name=value"; + + if (state == BLOCKED) { + try { + new w.SharedWorker("cookie.sjs?sharedworker&" + Math.random()); + ok(false, "SharedWorker should not be allowed!"); + } catch (ex) { + ok(true, "SharedWorker should not be allowed!"); + } + return; + } + + let p = new w.SharedWorker("cookie.sjs?sharedworker&" + Math.random()); + await new w.Promise(resolve => { p.port.onmessage = resolve; }); + await checkLastRequest("sharedworker", state); + + await new w.Promise(resolve => { + p.port.postMessage("subworker"); + p.port.onmessage = resolve; + }); + await checkLastRequest("subworker", state); + + await new w.Promise(resolve => { + p.port.postMessage("fetch"); + p.port.onmessage = resolve; + }); + await checkLastRequest("fetch", state); + + await new w.Promise(resolve => { + p.port.postMessage("xhr"); + p.port.onmessage = resolve; + }); + await checkLastRequest("xhr", state); +}); + +</script> +</body> +</html> diff --git a/netwerk/cookie/test/mochitest/test_worker.html b/netwerk/cookie/test/mochitest/test_worker.html new file mode 100644 index 0000000000..37ab222bce --- /dev/null +++ b/netwerk/cookie/test/mochitest/test_worker.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for cookies + worker loading when the policy changes</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript" src="cookiesHelper.js"></script> +</head> +<body> +<script type="application/javascript"> + +runTests(async (w, state) => { + w.document.cookie = "name=value"; + + let p = new w.Worker("cookie.sjs?worker&" + Math.random()); + await new w.Promise(resolve => { p.onmessage = resolve; }); + await checkLastRequest("worker", state); + + await new w.Promise(resolve => { p.postMessage("subworker"); p.onmessage = resolve; }); + await checkLastRequest("subworker", state); + + await new w.Promise(resolve => { p.postMessage("fetch"); p.onmessage = resolve; }); + await checkLastRequest("fetch", state); + + await new w.Promise(resolve => { p.postMessage("xhr"); p.onmessage = resolve; }); + await checkLastRequest("xhr", state); +}); + +</script> +</body> +</html> diff --git a/netwerk/cookie/test/mochitest/test_xhr.html b/netwerk/cookie/test/mochitest/test_xhr.html new file mode 100644 index 0000000000..d00b5690f8 --- /dev/null +++ b/netwerk/cookie/test/mochitest/test_xhr.html @@ -0,0 +1,25 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for cookies + XHR when the policy changes</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript" src="cookiesHelper.js"></script> +</head> +<body> +<script type="application/javascript"> + +runTests(async (w, state) => { + w.document.cookie = "name=value"; + await new w.Promise(resolve => { + let xhr = new w.XMLHttpRequest(); + xhr.open("GET", "cookie.sjs?xhr&" + Math.random()); + xhr.send(); + xhr.onload = resolve; + }); + await checkLastRequest("xhr", state); +}); + +</script> +</body> +</html> diff --git a/netwerk/cookie/test/mochitest/test_xmlDocument.html b/netwerk/cookie/test/mochitest/test_xmlDocument.html new file mode 100644 index 0000000000..91417c98c4 --- /dev/null +++ b/netwerk/cookie/test/mochitest/test_xmlDocument.html @@ -0,0 +1,37 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Document constructor</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<script type="application/javascript"> + +let w; + +SpecialPowers.pushPrefEnv({set: [ + ["dom.storage_access.enabled", true], + ["dom.storage_access.prompt.testing", true], + ["dom.storage_access.prompt.testing.allow", true], + ["dom.testing.sync-content-blocking-notifications", true], + ["network.cookie.cookieBehavior", 0], +]}).then(_ => { + return new Promise(resolve => { + w = window.open("empty.html"); + w.onload = resolve; + }); +}).then(_ => { + const doc = new w.Document(); + return doc.requestStorageAccess().catch(__ => {}); +}).then(___ => { + w.close(); + ok(true, "No crash!"); + SimpleTest.finish(); +}); + +SimpleTest.waitForExplicitFinish(); + +</script> +</body> +</html> diff --git a/netwerk/cookie/test/unit/test_baseDomain_publicsuffix.js b/netwerk/cookie/test/unit/test_baseDomain_publicsuffix.js new file mode 100644 index 0000000000..94f01b778e --- /dev/null +++ b/netwerk/cookie/test/unit/test_baseDomain_publicsuffix.js @@ -0,0 +1,105 @@ +"use strict"; + +add_task(async () => { + const HOST = "www.bbc.co.uk"; + Assert.equal( + Services.eTLD.getBaseDomainFromHost(HOST), + "bbc.co.uk", + "Sanity check: HOST is an eTLD + 1 with subdomain" + ); + + const tests = [ + { + // Correct baseDomain: eTLD + 1. + baseDomain: "bbc.co.uk", + name: "originally_bbc_co_uk", + }, + { + // Incorrect baseDomain: Part of public suffix list. + baseDomain: "uk", + name: "originally_uk", + }, + { + // Incorrect baseDomain: Part of public suffix list. + baseDomain: "co.uk", + name: "originally_co_uk", + }, + { + // Incorrect baseDomain: eTLD + 2. + baseDomain: "www.bbc.co.uk", + name: "originally_www_bbc_co_uk", + }, + ]; + + do_get_profile(); + + let dbFile = Services.dirsvc.get("ProfD", Ci.nsIFile); + dbFile.append("cookies.sqlite"); + let conn = Services.storage.openDatabase(dbFile); + + conn.schemaVersion = 10; + conn.executeSimpleSQL("DROP TABLE IF EXISTS moz_cookies"); + conn.executeSimpleSQL( + "CREATE TABLE moz_cookies (" + + "id INTEGER PRIMARY KEY, " + + "baseDomain TEXT, " + + "originAttributes TEXT NOT NULL DEFAULT '', " + + "name TEXT, " + + "value TEXT, " + + "host TEXT, " + + "path TEXT, " + + "expiry INTEGER, " + + "lastAccessed INTEGER, " + + "creationTime INTEGER, " + + "isSecure INTEGER, " + + "isHttpOnly INTEGER, " + + "inBrowserElement INTEGER DEFAULT 0, " + + "sameSite INTEGER DEFAULT 0, " + + "rawSameSite INTEGER DEFAULT 0, " + + "CONSTRAINT moz_uniqueid UNIQUE (name, host, path, originAttributes)" + + ")" + ); + + function addCookie(baseDomain, host, name) { + conn.executeSimpleSQL( + "INSERT INTO moz_cookies(" + + "baseDomain, host, name, value, path, expiry, " + + "lastAccessed, creationTime, isSecure, isHttpOnly) VALUES (" + + `'${baseDomain}', '${host}', '${name}', 'thevalue', '/', ` + + (Date.now() + 3600000) + + "," + + Date.now() + + "," + + Date.now() + + ", 1, 1)" + ); + } + + // Prepare the database. + for (let { baseDomain, name } of tests) { + addCookie(baseDomain, HOST, name); + } + // Domain cookies are not supported for IP addresses. + addCookie("127.0.0.1", ".127.0.0.1", "invalid_host"); + conn.close(); + + let cs = Services.cookies; + + // Count excludes the invalid_host cookie. + Assert.equal(cs.cookies.length, tests.length, "Expected number of cookies"); + + // Check whether the database has the expected value, + // despite the incorrect baseDomain. + for (let { name } of tests) { + Assert.ok( + cs.cookieExists(HOST, "/", name, {}), + "Should find cookie with name: " + name + ); + } + + Assert.equal( + cs.cookieExists("127.0.0.1", "/", "invalid_host", {}), + false, + "Should ignore database row with invalid host name" + ); +}); diff --git a/netwerk/cookie/test/unit/test_bug1155169.js b/netwerk/cookie/test/unit/test_bug1155169.js new file mode 100644 index 0000000000..2bf8bd768d --- /dev/null +++ b/netwerk/cookie/test/unit/test_bug1155169.js @@ -0,0 +1,96 @@ +const { NetUtil } = ChromeUtils.importESModule( + "resource://gre/modules/NetUtil.sys.mjs" +); + +const URI = Services.io.newURI("http://example.org/"); + +const { COOKIE_CHANGED, COOKIE_ADDED } = Ci.nsICookieNotification; + +function run_test() { + // Allow all cookies. + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + + // Clear cookies. + Services.cookies.removeAll(); + + // Add a new cookie. + setCookie("foo=bar", { + type: COOKIE_ADDED, + isSession: true, + isSecure: false, + isHttpOnly: false, + }); + + // Update cookie with isHttpOnly=true. + setCookie("foo=bar; HttpOnly", { + type: COOKIE_CHANGED, + isSession: true, + isSecure: false, + isHttpOnly: true, + }); + + // Update cookie with isSecure=true. + setCookie("foo=bar; Secure", { + type: COOKIE_CHANGED, + isSession: true, + isSecure: true, + isHttpOnly: false, + }); + + // Update cookie with isSession=false. + let expiry = new Date(); + expiry.setUTCFullYear(expiry.getUTCFullYear() + 2); + setCookie(`foo=bar; Expires=${expiry.toGMTString()}`, { + type: COOKIE_CHANGED, + isSession: false, + isSecure: false, + isHttpOnly: false, + }); + + // Reset cookie. + setCookie("foo=bar", { + type: COOKIE_CHANGED, + isSession: true, + isSecure: false, + isHttpOnly: false, + }); +} + +function setCookie(value, expected) { + function setCookieInternal(valueInternal, expectedInternal = null) { + function observer(subject) { + if (!expectedInternal) { + do_throw("no notification expected"); + return; + } + + let notification = subject.QueryInterface(Ci.nsICookieNotification); + + // Check we saw the right notification. + Assert.equal(notification.action, expectedInternal.type); + + // Check cookie details. + let cookie = notification.cookie.QueryInterface(Ci.nsICookie); + Assert.equal(cookie.isSession, expectedInternal.isSession); + Assert.equal(cookie.isSecure, expectedInternal.isSecure); + Assert.equal(cookie.isHttpOnly, expectedInternal.isHttpOnly); + } + + Services.obs.addObserver(observer, "cookie-changed"); + + let channel = NetUtil.newChannel({ + uri: URI, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }); + + Services.cookies.setCookieStringFromHttp(URI, valueInternal, channel); + Services.obs.removeObserver(observer, "cookie-changed"); + } + + // Check that updating/inserting the cookie works. + setCookieInternal(value, expected); + + // Check that we ignore identical cookies. + setCookieInternal(value); +} diff --git a/netwerk/cookie/test/unit/test_bug1321912.js b/netwerk/cookie/test/unit/test_bug1321912.js new file mode 100644 index 0000000000..fd24f15bbf --- /dev/null +++ b/netwerk/cookie/test/unit/test_bug1321912.js @@ -0,0 +1,99 @@ +do_get_profile(); +const dirSvc = Services.dirsvc; + +let dbFile = dirSvc.get("ProfD", Ci.nsIFile); +dbFile.append("cookies.sqlite"); + +let storage = Services.storage; +let properties = Cc["@mozilla.org/hash-property-bag;1"].createInstance( + Ci.nsIWritablePropertyBag +); +properties.setProperty("shared", true); +let conn = storage.openDatabase(dbFile); + +// Write the schema v7 to the database. +conn.schemaVersion = 7; +conn.executeSimpleSQL( + "CREATE TABLE moz_cookies (" + + "id INTEGER PRIMARY KEY, " + + "baseDomain TEXT, " + + "originAttributes TEXT NOT NULL DEFAULT '', " + + "name TEXT, " + + "value TEXT, " + + "host TEXT, " + + "path TEXT, " + + "expiry INTEGER, " + + "lastAccessed INTEGER, " + + "creationTime INTEGER, " + + "isSecure INTEGER, " + + "isHttpOnly INTEGER, " + + "appId INTEGER DEFAULT 0, " + + "inBrowserElement INTEGER DEFAULT 0, " + + "CONSTRAINT moz_uniqueid UNIQUE (name, host, path, originAttributes)" + + ")" +); +conn.executeSimpleSQL( + "CREATE INDEX moz_basedomain ON moz_cookies (baseDomain, " + + "originAttributes)" +); + +conn.executeSimpleSQL("PRAGMA synchronous = OFF"); +conn.executeSimpleSQL("PRAGMA journal_mode = WAL"); +conn.executeSimpleSQL("PRAGMA wal_autocheckpoint = 16"); + +let now = Date.now(); +conn.executeSimpleSQL( + "INSERT INTO moz_cookies(" + + "baseDomain, host, name, value, path, expiry, " + + "lastAccessed, creationTime, isSecure, isHttpOnly) VALUES (" + + "'foo.com', '.foo.com', 'foo', 'bar=baz', '/', " + + now + + ", " + + now + + ", " + + now + + ", 1, 1)" +); + +// Now start the cookie service, and then check the fields in the table. +// Get sessionCookies to wait for the initialization in cookie thread +Services.cookies.sessionCookies; + +Assert.equal(conn.schemaVersion, 13); +let stmt = conn.createStatement( + "SELECT sql FROM sqlite_master " + + "WHERE type = 'table' AND " + + " name = 'moz_cookies'" +); +try { + Assert.ok(stmt.executeStep()); + let sql = stmt.getString(0); + Assert.equal(sql.indexOf("appId"), -1); +} finally { + stmt.finalize(); +} + +stmt = conn.createStatement( + "SELECT * FROM moz_cookies " + + "WHERE host = '.foo.com' AND " + + " name = 'foo' AND " + + " value = 'bar=baz' AND " + + " path = '/' AND " + + " expiry = " + + now + + " AND " + + " lastAccessed = " + + now + + " AND " + + " creationTime = " + + now + + " AND " + + " isSecure = 1 AND " + + " isHttpOnly = 1" +); +try { + Assert.ok(stmt.executeStep()); +} finally { + stmt.finalize(); +} +conn.close(); diff --git a/netwerk/cookie/test/unit/test_bug643051.js b/netwerk/cookie/test/unit/test_bug643051.js new file mode 100644 index 0000000000..35b37a5889 --- /dev/null +++ b/netwerk/cookie/test/unit/test_bug643051.js @@ -0,0 +1,43 @@ +const { NetUtil } = ChromeUtils.importESModule( + "resource://gre/modules/NetUtil.sys.mjs" +); +const { CookieXPCShellUtils } = ChromeUtils.importESModule( + "resource://testing-common/CookieXPCShellUtils.sys.mjs" +); + +CookieXPCShellUtils.init(this); +CookieXPCShellUtils.createServer({ hosts: ["example.net"] }); + +add_task(async () => { + Services.prefs.setBoolPref("dom.security.https_first", false); + + // Allow all cookies. + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + + let uri = NetUtil.newURI("http://example.org/"); + let channel = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }); + + let set = "foo=bar\nbaz=foo"; + let expected = "foo=bar; baz=foo"; + Services.cookies.setCookieStringFromHttp(uri, set, channel); + + let actual = Services.cookies.getCookieStringFromHttp(uri, channel); + Assert.equal(actual, expected); + + await CookieXPCShellUtils.setCookieToDocument("http://example.net/", set); + actual = await CookieXPCShellUtils.getCookieStringFromDocument( + "http://example.net/" + ); + + expected = "foo=bar"; + Assert.equal(actual, expected); + Services.prefs.clearUserPref("dom.security.https_first"); +}); diff --git a/netwerk/cookie/test/unit/test_eviction.js b/netwerk/cookie/test/unit/test_eviction.js new file mode 100644 index 0000000000..8c0073a107 --- /dev/null +++ b/netwerk/cookie/test/unit/test_eviction.js @@ -0,0 +1,199 @@ +const { NetUtil } = ChromeUtils.importESModule( + "resource://gre/modules/NetUtil.sys.mjs" +); + +const BASE_HOST = "example.org"; + +const { CookieXPCShellUtils } = ChromeUtils.importESModule( + "resource://testing-common/CookieXPCShellUtils.sys.mjs" +); + +CookieXPCShellUtils.init(this); +CookieXPCShellUtils.createServer({ hosts: ["example.org"] }); + +add_task(async function test_basic_eviction() { + do_get_profile(); + + Services.prefs.setIntPref("network.cookie.staleThreshold", 0); + Services.prefs.setIntPref("network.cookie.quotaPerHost", 2); + Services.prefs.setIntPref("network.cookie.maxPerHost", 5); + + // We don't want to have CookieJarSettings blocking this test. + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + + const BASE_URI = Services.io.newURI("http://" + BASE_HOST); + const FOO_PATH = Services.io.newURI("http://" + BASE_HOST + "/foo/"); + const BAR_PATH = Services.io.newURI("http://" + BASE_HOST + "/bar/"); + + await setCookie("session_foo_path_1", null, "/foo", null, FOO_PATH); + await setCookie("session_foo_path_2", null, "/foo", null, FOO_PATH); + await setCookie("session_foo_path_3", null, "/foo", null, FOO_PATH); + await setCookie("session_foo_path_4", null, "/foo", null, FOO_PATH); + await setCookie("session_foo_path_5", null, "/foo", null, FOO_PATH); + verifyCookies( + [ + "session_foo_path_1", + "session_foo_path_2", + "session_foo_path_3", + "session_foo_path_4", + "session_foo_path_5", + ], + BASE_URI + ); + + // Check if cookies are evicted by creation time. + await setCookie("session_foo_path_6", null, "/foo", null, FOO_PATH); + verifyCookies( + ["session_foo_path_4", "session_foo_path_5", "session_foo_path_6"], + BASE_URI + ); + + await setCookie("session_bar_path_1", null, "/bar", null, BAR_PATH); + await setCookie("session_bar_path_2", null, "/bar", null, BAR_PATH); + + verifyCookies( + [ + "session_foo_path_4", + "session_foo_path_5", + "session_foo_path_6", + "session_bar_path_1", + "session_bar_path_2", + ], + BASE_URI + ); + + // Check if cookies are evicted by last accessed time. + await CookieXPCShellUtils.getCookieStringFromDocument(FOO_PATH.spec); + + await setCookie("session_foo_path_7", null, "/foo", null, FOO_PATH); + verifyCookies( + ["session_foo_path_5", "session_foo_path_6", "session_foo_path_7"], + BASE_URI + ); + + const EXPIRED_TIME = 3; + + await setCookie( + "non_session_expired_foo_path_1", + null, + "/foo", + EXPIRED_TIME, + FOO_PATH + ); + await setCookie( + "non_session_expired_foo_path_2", + null, + "/foo", + EXPIRED_TIME, + FOO_PATH + ); + verifyCookies( + [ + "session_foo_path_5", + "session_foo_path_6", + "session_foo_path_7", + "non_session_expired_foo_path_1", + "non_session_expired_foo_path_2", + ], + BASE_URI + ); + + // Check if expired cookies are evicted first. + await new Promise(resolve => do_timeout(EXPIRED_TIME * 1000, resolve)); + await setCookie("session_foo_path_8", null, "/foo", null, FOO_PATH); + verifyCookies( + ["session_foo_path_6", "session_foo_path_7", "session_foo_path_8"], + BASE_URI + ); + + Services.cookies.removeAll(); +}); + +// Verify that the given cookie names exist, and are ordered from least to most recently accessed +function verifyCookies(names, uri) { + Assert.equal(Services.cookies.countCookiesFromHost(uri.host), names.length); + let actual_cookies = []; + for (let cookie of Services.cookies.getCookiesFromHost(uri.host, {})) { + actual_cookies.push(cookie); + } + if (names.length != actual_cookies.length) { + let left = names.filter(function (n) { + return ( + actual_cookies.findIndex(function (c) { + return c.name == n; + }) == -1 + ); + }); + let right = actual_cookies + .filter(function (c) { + return ( + names.findIndex(function (n) { + return c.name == n; + }) == -1 + ); + }) + .map(function (c) { + return c.name; + }); + if (left.length) { + info("unexpected cookies: " + left); + } + if (right.length) { + info("expected cookies: " + right); + } + } + Assert.equal(names.length, actual_cookies.length); + actual_cookies.sort(function (a, b) { + if (a.lastAccessed < b.lastAccessed) { + return -1; + } + if (a.lastAccessed > b.lastAccessed) { + return 1; + } + return 0; + }); + for (var i = 0; i < names.length; i++) { + Assert.equal(names[i], actual_cookies[i].name); + Assert.equal(names[i].startsWith("session"), actual_cookies[i].isSession); + } +} + +var lastValue = 0; +function setCookie(name, domain, path, maxAge, url) { + let value = name + "=" + ++lastValue; + var s = "setting cookie " + value; + if (domain) { + value += "; Domain=" + domain; + s += " (d=" + domain + ")"; + } + if (path) { + value += "; Path=" + path; + s += " (p=" + path + ")"; + } + if (maxAge) { + value += "; Max-Age=" + maxAge; + s += " (non-session)"; + } else { + s += " (session)"; + } + s += " for " + url.spec; + info(s); + + let channel = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }); + + Services.cookies.setCookieStringFromHttp(url, value, channel); + + return new Promise(function (resolve) { + // Windows XP has low precision timestamps that cause our cookie eviction + // algorithm to produce different results from other platforms. We work around + // this by ensuring that there's a clear gap between each cookie update. + do_timeout(10, resolve); + }); +} diff --git a/netwerk/cookie/test/unit/test_getCookieSince.js b/netwerk/cookie/test/unit/test_getCookieSince.js new file mode 100644 index 0000000000..e58624b6a1 --- /dev/null +++ b/netwerk/cookie/test/unit/test_getCookieSince.js @@ -0,0 +1,72 @@ +const { NetUtil } = ChromeUtils.importESModule( + "resource://gre/modules/NetUtil.sys.mjs" +); + +function setCookie(name, url) { + let value = `${name}=${Math.random()}; Path=/; Max-Age=1000; sameSite=none; Secure`; + info(`Setting cookie ${value} for ${url.spec}`); + + let channel = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }); + + Services.cookies.setCookieStringFromHttp(url, value, channel); +} + +async function sleep() { + await new Promise(resolve => do_timeout(1000, resolve)); +} + +function checkSorting(cookies) { + for (let i = 1; i < cookies.length; ++i) { + Assert.greater( + cookies[i].creationTime, + cookies[i - 1].creationTime, + "Cookie " + cookies[i].name + ); + } +} + +add_task(async function () { + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + + await setCookie("A", Services.io.newURI("https://example.com/A/")); + await sleep(); + + await setCookie("B", Services.io.newURI("https://foo.bar/B/")); + await sleep(); + + await setCookie("C", Services.io.newURI("https://example.org/C/")); + await sleep(); + + await setCookie("D", Services.io.newURI("https://example.com/D/")); + await sleep(); + + Assert.equal(Services.cookies.cookies.length, 4, "Cookie check"); + + const cookies = Services.cookies.getCookiesSince(0); + Assert.equal(cookies.length, 4, "We retrieve all the 4 cookies"); + checkSorting(cookies); + + let someCookies = Services.cookies.getCookiesSince( + cookies[0].creationTime + 1 + ); + Assert.equal(someCookies.length, 3, "We retrieve some cookies"); + checkSorting(someCookies); + + someCookies = Services.cookies.getCookiesSince(cookies[1].creationTime + 1); + Assert.equal(someCookies.length, 2, "We retrieve some cookies"); + checkSorting(someCookies); + + someCookies = Services.cookies.getCookiesSince(cookies[2].creationTime + 1); + Assert.equal(someCookies.length, 1, "We retrieve some cookies"); + checkSorting(someCookies); + + someCookies = Services.cookies.getCookiesSince(cookies[3].creationTime + 1); + Assert.equal(someCookies.length, 0, "We retrieve some cookies"); +}); diff --git a/netwerk/cookie/test/unit/test_migrateCookieLifetimePref.js b/netwerk/cookie/test/unit/test_migrateCookieLifetimePref.js new file mode 100644 index 0000000000..088a909709 --- /dev/null +++ b/netwerk/cookie/test/unit/test_migrateCookieLifetimePref.js @@ -0,0 +1,65 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + Tests that + - the migration code runs, + - the sanitize on shutdown prefs for profiles with the network.cookie.lifetimePolicy enabled are set to true, + - the previous settings for clearOnShutdown prefs will not be applied due to sanitizeOnShutdown being disabled + - the network.cookie.lifetimePolicy is disabled afterwards. +*/ +add_task(async function migrateSanitizationPrefsClearCleaningPrefs() { + // Former network.cookie.lifetimePolicy values ACCEPT_SESSION/ACCEPT_NORMALLY are not available anymore + // 2 = ACCEPT_SESSION + Services.prefs.setIntPref("network.cookie.lifetimePolicy", 2); + Services.prefs.setBoolPref("privacy.sanitize.sanitizeOnShutdown", false); + Services.prefs.setBoolPref("privacy.clearOnShutdown.cache", false); + Services.prefs.setBoolPref("privacy.clearOnShutdown.cookies", false); + Services.prefs.setBoolPref("privacy.clearOnShutdown.offlineApps", false); + Services.prefs.setBoolPref("privacy.clearOnShutdown.downloads", true); + Services.prefs.setBoolPref("privacy.clearOnShutdown.sessions", true); + + // The migration code is called in cookieService::Init + Services.cookies; + + // Former network.cookie.lifetimePolicy values ACCEPT_SESSION/ACCEPT_NORMALLY are not available anymore + // 0 = ACCEPT_NORMALLY + Assert.equal( + Services.prefs.getIntPref("network.cookie.lifetimePolicy", 0), + 0, + "Cookie lifetime policy is off" + ); + + Assert.ok( + Services.prefs.getBoolPref("privacy.sanitize.sanitizeOnShutdown"), + "Sanitize on shutdown is set" + ); + + Assert.ok( + Services.prefs.getBoolPref("privacy.clearOnShutdown.cookies"), + "Clearing cookies on shutdown is selected" + ); + + Assert.ok( + Services.prefs.getBoolPref("privacy.clearOnShutdown.cache"), + "Clearing cache on shutdown is still selected" + ); + + Assert.ok( + Services.prefs.getBoolPref("privacy.clearOnShutdown.offlineApps"), + "Clearing offline apps on shutdown is selected" + ); + + Assert.ok( + !Services.prefs.getBoolPref("privacy.clearOnShutdown.downloads"), + "Clearing downloads on shutdown is not set anymore" + ); + Assert.ok( + !Services.prefs.getBoolPref("privacy.clearOnShutdown.sessions"), + "Clearing active logins on shutdown is not set anymore" + ); + + Services.prefs.resetPrefs(); + + delete Services.cookies; +}); diff --git a/netwerk/cookie/test/unit/test_parser_0001.js b/netwerk/cookie/test/unit/test_parser_0001.js new file mode 100644 index 0000000000..acc2e919ef --- /dev/null +++ b/netwerk/cookie/test/unit/test_parser_0001.js @@ -0,0 +1,32 @@ +const { NetUtil } = ChromeUtils.importESModule( + "resource://gre/modules/NetUtil.sys.mjs" +); + +function inChildProcess() { + return Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; +} + +function run_test() { + // Allow all cookies if the pref service is available in this process. + if (!inChildProcess()) { + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + } + + let uri = NetUtil.newURI("http://example.org/"); + let channel = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }); + + let set = "foo=bar"; + Services.cookies.setCookieStringFromHttp(uri, set, channel); + + let expected = "foo=bar"; + let actual = Services.cookies.getCookieStringFromHttp(uri, channel); + Assert.equal(actual, expected); +} diff --git a/netwerk/cookie/test/unit/test_parser_0019.js b/netwerk/cookie/test/unit/test_parser_0019.js new file mode 100644 index 0000000000..7ba0d4ef79 --- /dev/null +++ b/netwerk/cookie/test/unit/test_parser_0019.js @@ -0,0 +1,32 @@ +const { NetUtil } = ChromeUtils.importESModule( + "resource://gre/modules/NetUtil.sys.mjs" +); + +function inChildProcess() { + return Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; +} + +function run_test() { + // Allow all cookies if the pref service is available in this process. + if (!inChildProcess()) { + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + } + + let uri = NetUtil.newURI("http://example.org/"); + let channel = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }); + + let set = "foo=b;max-age=3600, c=d;path=/"; + Services.cookies.setCookieStringFromHttp(uri, set, channel); + + let expected = "foo=b"; + let actual = Services.cookies.getCookieStringFromHttp(uri, channel); + Assert.equal(actual, expected); +} diff --git a/netwerk/cookie/test/unit/test_rawSameSite.js b/netwerk/cookie/test/unit/test_rawSameSite.js new file mode 100644 index 0000000000..dc739ef852 --- /dev/null +++ b/netwerk/cookie/test/unit/test_rawSameSite.js @@ -0,0 +1,125 @@ +const { NetUtil } = ChromeUtils.importESModule( + "resource://gre/modules/NetUtil.sys.mjs" +); + +function inChildProcess() { + return Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; +} + +add_task(async _ => { + do_get_profile(); + + let dbFile = Services.dirsvc.get("ProfD", Ci.nsIFile); + dbFile.append("cookies.sqlite"); + + let storage = Services.storage; + let properties = Cc["@mozilla.org/hash-property-bag;1"].createInstance( + Ci.nsIWritablePropertyBag + ); + properties.setProperty("shared", true); + let conn = storage.openDatabase(dbFile); + + conn.schemaVersion = 9; + conn.executeSimpleSQL("DROP TABLE IF EXISTS moz_cookies"); + conn.executeSimpleSQL( + "CREATE TABLE moz_cookies (" + + "id INTEGER PRIMARY KEY, " + + "baseDomain TEXT, " + + "originAttributes TEXT NOT NULL DEFAULT '', " + + "name TEXT, " + + "value TEXT, " + + "host TEXT, " + + "path TEXT, " + + "expiry INTEGER, " + + "lastAccessed INTEGER, " + + "creationTime INTEGER, " + + "isSecure INTEGER, " + + "isHttpOnly INTEGER, " + + "inBrowserElement INTEGER DEFAULT 0, " + + "sameSite INTEGER DEFAULT 0, " + + "CONSTRAINT moz_uniqueid UNIQUE (name, host, path, originAttributes)" + + ")" + ); + conn.close(); + + // Allow all cookies if the pref service is available in this process. + if (!inChildProcess()) { + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + Services.prefs.setBoolPref("network.cookie.sameSite.laxByDefault", true); + Services.prefs.setBoolPref( + "network.cookie.sameSite.noneRequiresSecure", + true + ); + } + + let uri = NetUtil.newURI("http://example.org/"); + + let principal = Services.scriptSecurityManager.createContentPrincipal( + uri, + {} + ); + + let channel = NetUtil.newChannel({ + uri, + loadingPrincipal: principal, + securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER, + }); + + let tests = [ + { + cookie: "foo=b;max-age=3600, c=d;path=/; sameSite=strict", + sameSite: 2, + rawSameSite: 2, + }, + { + cookie: "foo=b;max-age=3600, c=d;path=/; sameSite=lax", + sameSite: 1, + rawSameSite: 1, + }, + { cookie: "foo=b;max-age=3600, c=d;path=/", sameSite: 1, rawSameSite: 0 }, + ]; + + for (let i = 0; i < tests.length; ++i) { + let test = tests[i]; + + let promise = new Promise(resolve => { + function observer(subject, topic, data) { + Services.obs.removeObserver(observer, "cookie-saved-on-disk"); + resolve(); + } + + Services.obs.addObserver(observer, "cookie-saved-on-disk"); + }); + + Services.cookies.setCookieStringFromHttp(uri, test.cookie, channel); + + await promise; + + conn = storage.openDatabase(dbFile); + Assert.equal(conn.schemaVersion, 13); + + let stmt = conn.createStatement( + "SELECT sameSite, rawSameSite FROM moz_cookies" + ); + + let success = stmt.executeStep(); + Assert.ok(success); + + let sameSite = stmt.getInt32(0); + let rawSameSite = stmt.getInt32(1); + stmt.finalize(); + + Assert.equal(sameSite, test.sameSite); + Assert.equal(rawSameSite, test.rawSameSite); + + Services.cookies.removeAll(); + + stmt.finalize(); + conn.close(); + } +}); diff --git a/netwerk/cookie/test/unit/test_schemeMap.js b/netwerk/cookie/test/unit/test_schemeMap.js new file mode 100644 index 0000000000..041c24033a --- /dev/null +++ b/netwerk/cookie/test/unit/test_schemeMap.js @@ -0,0 +1,216 @@ +const { NetUtil } = ChromeUtils.importESModule( + "resource://gre/modules/NetUtil.sys.mjs" +); + +function inChildProcess() { + return Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; +} + +const { CookieXPCShellUtils } = ChromeUtils.importESModule( + "resource://testing-common/CookieXPCShellUtils.sys.mjs" +); + +let CookieXPCShellUtilsInitialized = false; +function maybeInitializeCookieXPCShellUtils() { + if (!CookieXPCShellUtilsInitialized) { + CookieXPCShellUtilsInitialized = true; + CookieXPCShellUtils.init(this); + + CookieXPCShellUtils.createServer({ hosts: ["example.org"] }); + } +} + +// Don't pick up default permissions from profile. +Services.prefs.setCharPref("permissions.manager.defaultsUrl", ""); + +add_task(async _ => { + do_get_profile(); + + // Allow all cookies if the pref service is available in this process. + if (!inChildProcess()) { + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + } + + info("Let's set a cookie from HTTP example.org"); + + let uri = NetUtil.newURI("http://example.org/"); + let principal = Services.scriptSecurityManager.createContentPrincipal( + uri, + {} + ); + let channel = NetUtil.newChannel({ + uri, + loadingPrincipal: principal, + securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER, + }); + + Services.cookies.setCookieStringFromHttp(uri, "a=b; sameSite=lax", channel); + + let cookies = Services.cookies.getCookiesFromHost("example.org", {}); + Assert.equal(cookies.length, 1, "We expect 1 cookie only"); + + Assert.equal(cookies[0].schemeMap, Ci.nsICookie.SCHEME_HTTP, "HTTP Scheme"); + + info("Let's set a cookie from HTTPS example.org"); + + uri = NetUtil.newURI("https://example.org/"); + principal = Services.scriptSecurityManager.createContentPrincipal(uri, {}); + channel = NetUtil.newChannel({ + uri, + loadingPrincipal: principal, + securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER, + }); + + Services.cookies.setCookieStringFromHttp(uri, "a=b; sameSite=lax", channel); + + cookies = Services.cookies.getCookiesFromHost("example.org", {}); + Assert.equal(cookies.length, 1, "We expect 1 cookie only"); + + Assert.equal( + cookies[0].schemeMap, + Ci.nsICookie.SCHEME_HTTP | Ci.nsICookie.SCHEME_HTTPS, + "HTTP + HTTPS Schemes" + ); + + Services.cookies.removeAll(); +}); + +[true, false].forEach(schemefulComparison => { + add_task(async () => { + do_get_profile(); + Services.prefs.setBoolPref("dom.security.https_first", false); + + maybeInitializeCookieXPCShellUtils(); + + // Allow all cookies if the pref service is available in this process. + if (!inChildProcess()) { + Services.prefs.setBoolPref( + "network.cookie.sameSite.schemeful", + schemefulComparison + ); + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + } + + info( + `Testing schemefulSameSite=${schemefulComparison}. Let's set a cookie from HTTPS example.org` + ); + + let https_uri = NetUtil.newURI("https://example.org/"); + let https_principal = Services.scriptSecurityManager.createContentPrincipal( + https_uri, + {} + ); + let same_site_channel = NetUtil.newChannel({ + uri: https_uri, + loadingPrincipal: https_principal, + securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER, + }); + + Services.cookies.setCookieStringFromHttp( + https_uri, + "a=b; sameSite=lax", + same_site_channel + ); + + let cookies = Services.cookies.getCookieStringFromHttp( + https_uri, + same_site_channel + ); + Assert.equal(cookies, "a=b", "Cookies match"); + + let http_uri = NetUtil.newURI("http://example.org/"); + let http_principal = Services.scriptSecurityManager.createContentPrincipal( + http_uri, + {} + ); + let cross_site_channel = NetUtil.newChannel({ + uri: https_uri, + loadingPrincipal: http_principal, + securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER, + }); + + cookies = Services.cookies.getCookieStringFromHttp( + http_uri, + cross_site_channel + ); + if (schemefulComparison) { + Assert.equal(cookies, "", "No http(s) cookie for different scheme!"); + } else { + Assert.equal(cookies, "a=b", "http(s) Cookie even for differentscheme!"); + } + + // SameSite cookies are included via document.domain + cookies = await CookieXPCShellUtils.getCookieStringFromDocument( + http_uri.spec + ); + Assert.equal(cookies, "a=b", "document.cookie even for different scheme!"); + + Services.cookies.removeAll(); + Services.prefs.clearUserPref("dom.security.https_first"); + }); +}); + +add_task(async _ => { + do_get_profile(); + Services.prefs.setBoolPref("dom.security.https_first", false); + + // Allow all cookies if the pref service is available in this process. + if (!inChildProcess()) { + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + } + + info("Let's set a cookie without scheme"); + Services.cookies.add( + "example.org", + "/", + "a", + "b", + false, + false, + false, + Math.floor(Date.now() / 1000 + 1000), + {}, + Ci.nsICookie.SAMESITE_LAX, + Ci.nsICookie.SCHEME_UNSET + ); + + let cookies = Services.cookies.getCookiesFromHost("example.org", {}); + Assert.equal(cookies.length, 1, "We expect 1 cookie only"); + Assert.equal(cookies[0].schemeMap, Ci.nsICookie.SCHEME_UNSET, "Unset scheme"); + + ["https", "http"].forEach(scheme => { + let uri = NetUtil.newURI(scheme + "://example.org/"); + let principal = Services.scriptSecurityManager.createContentPrincipal( + uri, + {} + ); + let channel = NetUtil.newChannel({ + uri, + loadingPrincipal: principal, + securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER, + }); + + cookies = Services.cookies.getCookieStringFromHttp(uri, channel); + Assert.equal(cookies, "a=b", "Cookie for unset scheme"); + }); + + Services.cookies.removeAll(); + Services.prefs.clearUserPref("dom.security.https_first"); +}); diff --git a/netwerk/cookie/test/unit/test_timestamp_fixup.js b/netwerk/cookie/test/unit/test_timestamp_fixup.js new file mode 100644 index 0000000000..a6e9642ad7 --- /dev/null +++ b/netwerk/cookie/test/unit/test_timestamp_fixup.js @@ -0,0 +1,130 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +const USEC_PER_SEC = 1000 * 1000; +const ONE_DAY = 60 * 60 * 24 * USEC_PER_SEC; +const ONE_YEAR = ONE_DAY * 365; +const LAST_ACCESSED_DIFF = 10 * ONE_YEAR; +const CREATION_DIFF = 100 * ONE_YEAR; + +function initDB(conn, now) { + // Write the schema v7 to the database. + conn.schemaVersion = 7; + conn.executeSimpleSQL( + "CREATE TABLE moz_cookies (" + + "id INTEGER PRIMARY KEY, " + + "baseDomain TEXT, " + + "originAttributes TEXT NOT NULL DEFAULT '', " + + "name TEXT, " + + "value TEXT, " + + "host TEXT, " + + "path TEXT, " + + "expiry INTEGER, " + + "lastAccessed INTEGER, " + + "creationTime INTEGER, " + + "isSecure INTEGER, " + + "isHttpOnly INTEGER, " + + "appId INTEGER DEFAULT 0, " + + "inBrowserElement INTEGER DEFAULT 0, " + + "CONSTRAINT moz_uniqueid UNIQUE (name, host, path, originAttributes)" + + ")" + ); + conn.executeSimpleSQL( + "CREATE INDEX moz_basedomain ON moz_cookies (baseDomain, " + + "originAttributes)" + ); + + conn.executeSimpleSQL("PRAGMA synchronous = OFF"); + conn.executeSimpleSQL("PRAGMA journal_mode = WAL"); + conn.executeSimpleSQL("PRAGMA wal_autocheckpoint = 16"); + + conn.executeSimpleSQL( + `INSERT INTO moz_cookies(baseDomain, host, name, value, path, expiry, lastAccessed, creationTime, isSecure, isHttpOnly) + VALUES ('foo.com', '.foo.com', 'foo', 'bar=baz', '/', + ${now + ONE_DAY}, ${now + LAST_ACCESSED_DIFF} , ${ + now + CREATION_DIFF + } , 1, 1)` + ); +} + +add_task(async function test_timestamp_fixup() { + let now = Date.now() * 1000; // date in microseconds + Services.prefs.setBoolPref("network.cookie.fixup_on_db_load", true); + do_get_profile(); + let dbFile = Services.dirsvc.get("ProfD", Ci.nsIFile); + dbFile.append("cookies.sqlite"); + let conn = Services.storage.openDatabase(dbFile); + initDB(conn, now); + + if (AppConstants.platform != "android") { + Services.fog.initializeFOG(); + } + Services.fog.testResetFOG(); + + // Now start the cookie service, and then check the fields in the table. + // Get sessionCookies to wait for the initialization in cookie thread + Assert.lessOrEqual( + Math.floor(Services.cookies.cookies[0].creationTime / 1000), + now + ); + Assert.equal(conn.schemaVersion, 13); + + Assert.equal( + await Glean.networking.cookieTimestampFixedCount.creationTime.testGetValue(), + 1, + "One fixup of creation time" + ); + Assert.equal( + await Glean.networking.cookieTimestampFixedCount.lastAccessed.testGetValue(), + 1, + "One fixup of lastAccessed" + ); + { + let { values } = + await Glean.networking.cookieCreationFixupDiff.testGetValue(); + info(JSON.stringify(values)); + let keys = Object.keys(values).splice(-2, 2); + Assert.equal(keys.length, 2, "There should be two entries in telemetry"); + Assert.equal(values[keys[0]], 1, "First entry should have value 1"); + Assert.equal(values[keys[1]], 0, "Second entry should have value 0"); + const creationDiffInSeconds = CREATION_DIFF / USEC_PER_SEC; + Assert.lessOrEqual( + parseInt(keys[0]), + creationDiffInSeconds, + "The bucket should be smaller than time diff" + ); + Assert.lessOrEqual( + creationDiffInSeconds, + parseInt(keys[1]), + "The next bucket should be larger than time diff" + ); + } + + { + let { values } = + await Glean.networking.cookieAccessFixupDiff.testGetValue(); + info(JSON.stringify(values)); + let keys = Object.keys(values).splice(-2, 2); + Assert.equal(keys.length, 2, "There should be two entries in telemetry"); + Assert.equal(values[keys[0]], 1, "First entry should have value 1"); + Assert.equal(values[keys[1]], 0, "Second entry should have value 0"); + info(now); + const lastAccessedDiffInSeconds = LAST_ACCESSED_DIFF / USEC_PER_SEC; + Assert.lessOrEqual( + parseInt(keys[0]), + lastAccessedDiffInSeconds, + "The bucket should be smaller than time diff" + ); + Assert.lessOrEqual( + lastAccessedDiffInSeconds, + parseInt(keys[1]), + "The next bucket should be larger than time diff" + ); + } + + conn.close(); +}); diff --git a/netwerk/cookie/test/unit/xpcshell.toml b/netwerk/cookie/test/unit/xpcshell.toml new file mode 100644 index 0000000000..694d3cb847 --- /dev/null +++ b/netwerk/cookie/test/unit/xpcshell.toml @@ -0,0 +1,26 @@ +[DEFAULT] +head = "" + +["test_baseDomain_publicsuffix.js"] + +["test_bug643051.js"] + +["test_bug1155169.js"] + +["test_bug1321912.js"] + +["test_eviction.js"] + +["test_getCookieSince.js"] + +["test_migrateCookieLifetimePref.js"] + +["test_parser_0001.js"] + +["test_parser_0019.js"] + +["test_rawSameSite.js"] + +["test_schemeMap.js"] + +["test_timestamp_fixup.js"] |