diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /netwerk/cookie | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'netwerk/cookie')
71 files changed, 12612 insertions, 0 deletions
diff --git a/netwerk/cookie/Cookie.cpp b/netwerk/cookie/Cookie.cpp new file mode 100644 index 0000000000..e83a2fcae2 --- /dev/null +++ b/netwerk/cookie/Cookie.cpp @@ -0,0 +1,219 @@ +/* -*- 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 "mozilla/Encoding.h" +#include "mozilla/dom/ToJSValue.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 = 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 the creationTime given to us is higher than the running maximum, + // update our maximum. + if (cookie->mData.creationTime() > gLastCreationTime) { + gLastCreationTime = cookie->mData.creationTime(); + } + + // If sameSite is not a sensible value, assume strict + if (cookie->mData.sameSite() < 0 || + cookie->mData.sameSite() > nsICookie::SAMESITE_STRICT) { + cookie->mData.sameSite() = nsICookie::SAMESITE_STRICT; + } + + // If rawSameSite is not a sensible value, assume equal to sameSite. + if (!Cookie::ValidateRawSame(cookie->mData)) { + cookie->mData.rawSameSite() = nsICookie::SAMESITE_NONE; + } + + 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::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 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::ValidateRawSame(const CookieStruct& aCookieData) { + return aCookieData.rawSameSite() == aCookieData.sameSite() || + aCookieData.rawSameSite() == nsICookie::SAMESITE_NONE; +} + +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..94a49db7c6 --- /dev/null +++ b/netwerk/cookie/Cookie.h @@ -0,0 +1,145 @@ +/* -*- 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) {} + + public: + // Returns false if rawSameSite has an invalid value, compared to sameSite. + static bool ValidateRawSame(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); + + 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 const OriginAttributes& OriginAttributesRef() const { + return mOriginAttributes; + } + inline int32_t SameSite() const { return mData.sameSite(); } + inline int32_t RawSameSite() const { return mData.rawSameSite(); } + 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; + } + + 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..07cfd9efcb --- /dev/null +++ b/netwerk/cookie/CookieCommons.cpp @@ -0,0 +1,698 @@ +/* -*- 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/ContentBlocking.h" +#include "mozilla/ConsoleReportCollector.h" +#include "mozilla/ContentBlockingNotifier.h" +#include "mozilla/ScopeExit.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" + +constexpr auto CONSOLE_SCHEMEFUL_CATEGORY = "cookieSchemeful"_ns; + +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(); + if (isPrefix && aPath[cookiePathLen] == '/') { + return true; + } + + return false; +} + +// 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); + } + + return aPrincipal->GetBaseDomain(aBaseDomain); +} + +// 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, 0x00}; + + return aCookieData.name().FindCharInSet(illegalNameCharacters, 0) == -1; +} + +bool CookieCommons::CheckHttpValue(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, 0x5C, and 0x7F are missing from this list. This is + // for parity with Chrome. This only applies to cookies set via the Set-Cookie + // header, as document.cookie is defined to be UTF-8. Hooray for + // symmetry!</sarcasm> + 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, 0x00}; + return aCookieData.value().FindCharInSet(illegalCharacters, 0) == -1; +} + +// 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; + } + + // Here we can have any legacy permission value. + + // now we need to figure out what type of accept policy we're dealing with + // if we accept cookies normally, just bail and return + if (StaticPrefs::network_cookie_lifetimePolicy() == + nsICookieService::ACCEPT_NORMALLY) { + return true; + } + + // declare this here since it'll be used in all of the remaining cases + int64_t currentTime = PR_Now() / PR_USEC_PER_SEC; + int64_t delta = aCookieData.expiry() - currentTime; + + // We are accepting the cookie, but, + // if it's not a session cookie, we may have to limit its lifetime. + if (!aCookieData.isSession() && delta > 0) { + if (StaticPrefs::network_cookie_lifetimePolicy() == + nsICookieService::ACCEPT_SESSION) { + // limit lifetime to session + aCookieData.isSession() = true; + } + } + + return true; +} + +namespace { + +CookieStatus CookieStatusForWindow(nsPIDOMWindowInner* aWindow, + nsIURI* aDocumentURI) { + MOZ_ASSERT(aWindow); + MOZ_ASSERT(aDocumentURI); + + if (!nsContentUtils::IsThirdPartyWindowOrChannel(aWindow, nullptr, + aDocumentURI)) { + 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->EffectiveStoragePrincipal(); + 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()) && + !ContentBlocking::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; + } + } + + // 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; + bool canSetCookie = false; + CookieService::CanSetCookie(principalURI, baseDomain, cookieData, + requireHostMatch, cookieStatus, cookieString, + false, isForeignAndNotAddon, 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; + if (aChannel) { + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + nsresult rv = + loadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings)); + if (NS_WARN_IF(NS_FAILED(rv))) { + cookieJarSettings = CookieJarSettings::GetBlockingAll(); + } + } else { + cookieJarSettings = CookieJarSettings::Create(); + } + + MOZ_ASSERT(cookieJarSettings); + return cookieJarSettings.forget(); +} + +// static +bool CookieCommons::ShouldIncludeCrossSiteCookieForDocument(Cookie* aCookie) { + MOZ_ASSERT(aCookie); + + int32_t sameSiteAttr = 0; + aCookie->GetSameSite(&sameSiteAttr); + + return sameSiteAttr == nsICookie::SAMESITE_NONE; +} + +bool CookieCommons::IsSafeTopLevelNav(nsIChannel* aChannel) { + if (!aChannel) { + return false; + } + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + if (loadInfo->GetExternalContentPolicyType() != + ExtContentPolicy::TYPE_DOCUMENT && + loadInfo->GetExternalContentPolicyType() != + ExtContentPolicy::TYPE_SAVEAS_DOWNLOAD) { + return false; + } + return NS_IsSafeMethodNav(aChannel); +} + +bool CookieCommons::IsSameSiteForeign(nsIChannel* aChannel, nsIURI* aHostURI) { + 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)); + RefPtr<BasePrincipal> triggeringPrincipal = + BasePrincipal::Cast(loadInfo->TriggeringPrincipal()); + if (triggeringPrincipal->AddonPolicy() && + triggeringPrincipal->AddonAllowsLoad(channelURI)) { + return false; + } + + bool isForeign = true; + nsresult rv; + if (loadInfo->GetExternalContentPolicyType() == + ExtContentPolicy::TYPE_DOCUMENT || + loadInfo->GetExternalContentPolicyType() == + 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); + } else { + nsCOMPtr<mozIThirdPartyUtil> thirdPartyUtil = + do_GetService(THIRDPARTYUTIL_CONTRACTID); + if (!thirdPartyUtil) { + return true; + } + rv = thirdPartyUtil->IsThirdPartyChannel(aChannel, aHostURI, &isForeign); + } + // 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; + } + + // 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 (loadInfo->GetExternalContentPolicyType() == + ExtContentPolicy::TYPE_SUBDOCUMENT) { + rv = loadInfo->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 : loadInfo->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) { + return true; + } + } + } + return isForeign; +} + +namespace { + +bool MaybeCompareSchemeInternal(Cookie* aCookie, + nsICookie::schemeType aSchemeType) { + MOZ_ASSERT(aCookie); + + // This is an old cookie without a scheme yet. Let's consider it valid. + if (aCookie->SchemeMap() == nsICookie::SCHEME_UNSET) { + return true; + } + + return !!(aCookie->SchemeMap() & aSchemeType); +} + +} // namespace + +// static +bool CookieCommons::MaybeCompareSchemeWithLogging( + nsIConsoleReportCollector* aCRC, nsIURI* aHostURI, Cookie* aCookie, + nsICookie::schemeType aSchemeType) { + MOZ_ASSERT(aCookie); + MOZ_ASSERT(aHostURI); + + if (MaybeCompareSchemeInternal(aCookie, aSchemeType)) { + return true; + } + + nsAutoCString uri; + nsresult rv = aHostURI->GetSpec(uri); + if (NS_WARN_IF(NS_FAILED(rv))) { + return !StaticPrefs::network_cookie_sameSite_schemeful(); + } + + if (!StaticPrefs::network_cookie_sameSite_schemeful()) { + CookieLogging::LogMessageToConsole( + aCRC, aHostURI, nsIScriptError::warningFlag, CONSOLE_SCHEMEFUL_CATEGORY, + "CookieSchemefulRejectForBeta"_ns, + AutoTArray<nsString, 2>{NS_ConvertUTF8toUTF16(aCookie->Name()), + NS_ConvertUTF8toUTF16(uri)}); + return true; + } + + CookieLogging::LogMessageToConsole( + aCRC, aHostURI, nsIScriptError::warningFlag, CONSOLE_SCHEMEFUL_CATEGORY, + "CookieSchemefulReject"_ns, + AutoTArray<nsString, 2>{NS_ConvertUTF8toUTF16(aCookie->Name()), + NS_ConvertUTF8toUTF16(uri)}); + return false; +} + +// static +bool CookieCommons::MaybeCompareScheme(Cookie* aCookie, + nsICookie::schemeType aSchemeType) { + MOZ_ASSERT(aCookie); + + if (!StaticPrefs::network_cookie_sameSite_schemeful()) { + return true; + } + + return MaybeCompareSchemeInternal(aCookie, aSchemeType); +} + +// 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..dcb8ecb6cd --- /dev/null +++ b/netwerk/cookie/CookieCommons.h @@ -0,0 +1,143 @@ +/* -*- 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 "prtime.h" +#include "nsString.h" +#include "nsICookie.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 CheckHttpValue(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); + + static bool MaybeCompareSchemeWithLogging(nsIConsoleReportCollector* aCRC, + nsIURI* aHostURI, Cookie* aCookie, + nsICookie::schemeType aSchemeType); + + static bool MaybeCompareScheme(Cookie* aCookie, + nsICookie::schemeType aSchemeType); + + 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 + // origin navigation. + static bool IsSameSiteForeign(nsIChannel* aChannel, nsIURI* aHostURI); +}; + +} // 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..9da16f7dbc --- /dev/null +++ b/netwerk/cookie/CookieJarSettings.cpp @@ -0,0 +1,619 @@ +/* -*- 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/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 "nsGlobalWindowInner.h" +#include "nsIPrincipal.h" +#if defined(MOZ_THUNDERBIRD) || defined(MOZ_SUITE) +# include "nsIProtocolHandler.h" +#endif +#include "nsIClassInfoImpl.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() { + MOZ_ASSERT(NS_IsMainThread()); + + if (sBlockinAll) { + return do_AddRef(sBlockinAll); + } + + sBlockinAll = + new CookieJarSettings(nsICookieService::BEHAVIOR_REJECT, + OriginAttributes::IsFirstPartyEnabled(), eFixed); + ClearOnShutdown(&sBlockinAll); + + return do_AddRef(sBlockinAll); +} + +// static +already_AddRefed<nsICookieJarSettings> CookieJarSettings::Create() { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<CookieJarSettings> cookieJarSettings = new CookieJarSettings( + nsICookieManager::GetCookieBehavior(), + OriginAttributes::IsFirstPartyEnabled(), eProgressive); + return cookieJarSettings.forget(); +} + +// static +already_AddRefed<nsICookieJarSettings> CookieJarSettings::Create( + uint32_t aCookieBehavior, const nsAString& aPartitionKey, + bool aIsFirstPartyIsolated, bool aIsOnContentBlockingAllowList) { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<CookieJarSettings> cookieJarSettings = new CookieJarSettings( + aCookieBehavior, aIsFirstPartyIsolated, eProgressive); + cookieJarSettings->mPartitionKey = aPartitionKey; + cookieJarSettings->mIsOnContentBlockingAllowList = + aIsOnContentBlockingAllowList; + + return cookieJarSettings.forget(); +} + +CookieJarSettings::CookieJarSettings(uint32_t aCookieBehavior, + bool aIsFirstPartyIsolated, State aState) + : mCookieBehavior(aCookieBehavior), + mIsFirstPartyIsolated(aIsFirstPartyIsolated), + mIsOnContentBlockingAllowList(false), + mState(aState), + mToBeMerged(false) { + 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(TaskCategory::Other, r.forget()); + } +} + +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::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::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::CookiePermission(nsIPrincipal* aPrincipal, + uint32_t* aCookiePermission) { + MOZ_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()) { + nsCOMPtr<nsIPrincipal> principal = + Permission::ClonePrincipalForPermission(aPrincipal); + if (NS_WARN_IF(!principal)) { + return NS_ERROR_FAILURE; + } + + for (const RefPtr<nsIPermission>& permission : mCookiePermissions) { + bool match = false; + rv = permission->MatchesPrincipalForPermission(principal, 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_ASSERT(NS_IsMainThread()); + + aData.isFixed() = mState == eFixed; + aData.cookieBehavior() = mCookieBehavior; + aData.isFirstPartyIsolated() = mIsFirstPartyIsolated; + aData.isOnContentBlockingAllowList() = mIsOnContentBlockingAllowList; + aData.partitionKey() = mPartitionKey; + + 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_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.isFixed() ? eFixed : eProgressive); + + cookieJarSettings->mIsOnContentBlockingAllowList = + aData.isOnContentBlockingAllowList(); + cookieJarSettings->mCookiePermissions = std::move(list); + cookieJarSettings->mPartitionKey = aData.partitionKey(); + + cookieJarSettings.forget(aCookieJarSettings); +} + +void CookieJarSettings::Merge(const CookieJarSettingsArgs& aData) { + MOZ_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); + + 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); + 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 || + IsRejectThirdPartyWithExceptions(aCookieBehavior); +} + +// static +bool CookieJarSettings::IsRejectThirdPartyWithExceptions( + uint32_t aCookieBehavior) { + return aCookieBehavior == nsICookieService::BEHAVIOR_REJECT_FOREIGN && + StaticPrefs::network_cookie_rejectForeignWithExceptions_enabled(); +} + +NS_IMETHODIMP +CookieJarSettings::Read(nsIObjectInputStream* aStream) { + 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; + } + + 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) { + 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(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..34a8e3e796 --- /dev/null +++ b/netwerk/cookie/CookieJarSettings.h @@ -0,0 +1,193 @@ +/* -*- 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 "nsICookieJarSettings.h" +#include "nsDataHashtable.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(); + + static already_AddRefed<nsICookieJarSettings> Create(); + + static already_AddRefed<nsICookieJarSettings> Create( + uint32_t aCookieBehavior, const nsAString& aPartitionKey, + bool aIsFirstPartyIsolated, bool aIsOnContentBlockingAllowList); + + 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); + const nsAString& GetPartitionKey() { return mPartitionKey; }; + + // 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); + + // This static method returns true if aCookieBehavior is + // BEHAVIOR_REJECT_FOREIGN and + // network.cookie.rejectForeignWithExceptions.enabled pref is set to true. + static bool IsRejectThirdPartyWithExceptions(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, + State aState); + ~CookieJarSettings(); + + uint32_t mCookieBehavior; + bool mIsFirstPartyIsolated; + CookiePermissionList mCookiePermissions; + bool mIsOnContentBlockingAllowList; + nsString mPartitionKey; + + State mState; + + bool mToBeMerged; +}; + +} // 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..5724a07083 --- /dev/null +++ b/netwerk/cookie/CookieLogging.cpp @@ -0,0 +1,184 @@ +/* -*- 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" + +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/CookiePersistentStorage.cpp b/netwerk/cookie/CookiePersistentStorage.cpp new file mode 100644 index 0000000000..acd898837a --- /dev/null +++ b/netwerk/cookie/CookiePersistentStorage.cpp @@ -0,0 +1,2017 @@ +/* -*- 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/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 "nsICookieService.h" +#include "nsIEffectiveTLDService.h" +#include "nsILineInputStream.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 = 12; + +// 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; + +#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)); + + // 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 ? true : false); + 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(); + + return storage.forget(); +} + +CookiePersistentStorage::CookiePersistentStorage() + : mMonitor("CookiePersistentStorage"), + mInitialized(false), + mCorruptFlag(OK) {} + +void CookiePersistentStorage::NotifyChangedInternal(nsISupports* aSubject, + const char16_t* aData, + bool aOldCookieIsSession) { + // Notify for topic "session-cookie-changed" to update the copy of session + // cookies in session restore component. + + // Filter out notifications for individual non-session cookies. + if (u"changed"_ns.Equals(aData) || u"deleted"_ns.Equals(aData) || + u"added"_ns.Equals(aData)) { + nsCOMPtr<nsICookie> xpcCookie = do_QueryInterface(aSubject); + MOZ_ASSERT(xpcCookie); + auto cookie = static_cast<Cookie*>(xpcCookie.get()); + if (!cookie->IsSession() && !aOldCookieIsSession) { + return; + } + } + + nsCOMPtr<nsIObserverService> os = services::GetObserverService(); + if (os) { + os->NotifyObservers(aSubject, "session-cookie-changed", aData); + } +} + +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); + + 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); + + CookieStorage::RemoveCookiesFromExactHost(aHost, aBaseDomain, aPattern); + + DebugOnly<nsresult> rv = transaction.Commit(); + MOZ_ASSERT(NS_SUCCEEDED(rv)); +} + +void CookiePersistentStorage::RemoveCookieFromDB(const CookieListIter& aIter) { + // if it's a non-session cookie, remove it from the db + if (aIter.Cookie()->IsSession() || !mDBConn) { + return; + } + + nsCOMPtr<mozIStorageBindingParamsArray> paramsArray; + mStmtDelete->NewBindingParamsArray(getter_AddRefs(paramsArray)); + + PrepareCookieRemoval(aIter, 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 CookieListIter& aIter, mozIStorageBindingParamsArray* aParamsArray) { + // if it's a non-session cookie, remove it from the db + if (aIter.Cookie()->IsSession() || !mDBConn) { + return; + } + + nsCOMPtr<mozIStorageBindingParams> params; + aParamsArray->NewBindingParams(getter_AddRefs(params)); + + DebugOnly<nsresult> rv = + params->BindUTF8StringByName("name"_ns, aIter.Cookie()->Name()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + rv = params->BindUTF8StringByName("host"_ns, aIter.Cookie()->Host()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + rv = params->BindUTF8StringByName("path"_ns, aIter.Cookie()->Path()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + nsAutoCString suffix; + aIter.Cookie()->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, + 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); + + 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 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 " + "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 " + "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); + + // Create a new constCookie and assign the data. + return MakeUnique<CookieStruct>( + name, value, host, path, expiry, lastAccessed, creationTime, isHttpOnly, + false, isSecure, sameSite, rawSameSite, + static_cast<nsICookie::schemeType>(schemeMap)); +} + +void CookiePersistentStorage::EnsureReadComplete() { + 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; + } + + for (uint32_t i = 0; i < mReadArray.Length(); ++i) { + CookieDomainTuple& tuple = mReadArray[i]; + MOZ_ASSERT(!tuple.cookie->isSession()); + + RefPtr<Cookie> cookie = + Cookie::Create(*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(); + + 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, + 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 " + ") VALUES (" + ":originAttributes, " + ":name, " + ":value, " + ":host, " + ":path, " + ":expiry, " + ":lastAccessed, " + ":creationTime, " + ":isSecure, " + ":isHttpOnly, " + ":sameSite, " + ":rawSameSite, " + ":schemeMap " + ")"), + 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, " + "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); + + 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, paramsArray); + self->RemoveCookieFromListInternal(aIter); + }, + [paramsArray, self]() { + if (paramsArray) { + self->DeleteFromDB(paramsArray); + } + }); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/cookie/CookiePersistentStorage.h b/netwerk/cookie/CookiePersistentStorage.h new file mode 100644 index 0000000000..40c969f2a7 --- /dev/null +++ b/netwerk/cookie/CookiePersistentStorage.h @@ -0,0 +1,158 @@ +/* -*- 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 EnsureReadComplete(); + + void CleanupCachedStatements(); + void CleanupDBConnection(); + + void Activate(); + + void RebuildCorruptDB(); + void HandleDBClosed(); + + nsresult RunInTransaction(nsICookieTransactionCallback* aCallback); + + // 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(nsISupports* aSubject, const char16_t* aData, + bool aOldCOokieIsSession) override; + + void RemoveAllInternal() override; + + void RemoveCookieFromDB(const CookieListIter& aIter) 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 CookieListIter& aIter, + 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 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; + + 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..5aaa60c241 --- /dev/null +++ b/netwerk/cookie/CookiePrivateStorage.h @@ -0,0 +1,47 @@ +/* -*- 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" + +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{}; + + protected: + const char* NotificationTopic() const override { + return "private-cookie-changed"; + } + + void NotifyChangedInternal(nsISupports* aSubject, const char16_t* aData, + bool aOldCookieIsSession) override {} + + void RemoveAllInternal() override {} + + void RemoveCookieFromDB(const CookieListIter& aIter) 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 {} +}; + +} // 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..33543891d0 --- /dev/null +++ b/netwerk/cookie/CookieService.cpp @@ -0,0 +1,2374 @@ +/* -*- 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/ClearOnShutdown.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/nsMixedContentBlocker.h" +#include "mozilla/dom/Promise.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 "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" + +using namespace mozilla::dom; + +// static +uint32_t nsICookieManager::GetCookieBehavior() { + bool isFirstPartyIsolated = OriginAttributes::IsFirstPartyEnabled(); + uint32_t cookieBehavior = + mozilla::StaticPrefs::network_cookie_cookieBehavior(); + + if (isFirstPartyIsolated && + cookieBehavior == + nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN) { + cookieBehavior = nsICookieService::BEHAVIOR_REJECT_TRACKER; + } + return cookieBehavior; +} + +namespace mozilla { +namespace net { + +/****************************************************************************** + * CookieService impl: + * useful types & constants + ******************************************************************************/ + +static StaticRefPtr<CookieService> gCookieService; + +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 aLaxByDefault) { + int32_t sameSiteAttr = 0; + aCookie->GetSameSite(&sameSiteAttr); + + // it if's a cross origin request and the cookie is same site only (strict) + // don't send it + if (sameSiteAttr == nsICookie::SAMESITE_STRICT) { + return false; + } + + 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 (StaticPrefs::network_cookie_sameSite_laxPlusPOST_timeout() > 0 && + aLaxByDefault && sameSiteAttr == nsICookie::SAMESITE_LAX && + aCookie->RawSameSite() == nsICookie::SAMESITE_NONE && + currentTimeInUsec - aCookie->CreationTime() <= + (StaticPrefs::network_cookie_sameSite_laxPlusPOST_timeout() * + PR_USEC_PER_SEC) && + !NS_IsSafeMethodNav(aChannel)) { + return true; + } + + // if it's a cross origin request, the cookie is same site lax, but it's not a + // top-level navigation, don't send it + return sameSiteAttr != nsICookie::SAMESITE_LAX || 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. + 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(); + + 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. + mPersistentStorage = CookiePersistentStorage::Create(); + mPrivateStorage = CookiePrivateStorage::Create(); + + mPersistentStorage->Activate(); +} + +void CookieService::CloseCookieStorages() { + // return if we already closed + if (!mPersistentStorage) { + return; + } + + // Let's nullify both storages before calling Close(). + RefPtr<CookiePrivateStorage> privateStorage; + privateStorage.swap(mPrivateStorage); + + RefPtr<CookiePersistentStorage> 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(uint32_t* aCookieBehavior) { + NS_ENSURE_ARG_POINTER(aCookieBehavior); + *aCookieBehavior = nsICookieManager::GetCookieBehavior(); + 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->EffectiveStoragePrincipal(); + + if (!CookieCommons::IsSchemeSupported(principal)) { + return NS_OK; + } + + nsICookie::schemeType schemeType = + CookieCommons::PrincipalToSchemeType(principal); + + 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 potentiallyTurstworthy = 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) { + thirdParty = nsContentUtils::IsThirdPartyWindowOrChannel(innerWindow, + nullptr, nullptr); + } + + 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)) { + continue; + } + + // if the cookie is secure and the host scheme isn't, we can't send it + if (cookie->IsSecure() && !potentiallyTurstworthy) { + continue; + } + + if (!CookieCommons::MaybeCompareScheme(cookie, schemeType)) { + 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 isSameSiteForeign = CookieCommons::IsSameSiteForeign(aChannel, aHostURI); + + 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, true, 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) { + thirdParty = nsContentUtils::IsThirdPartyWindowOrChannel(innerWindow, + nullptr, nullptr); + } + + if (thirdParty && + !CookieCommons::ShouldIncludeCrossSiteCookieForDocument(cookie)) { + 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); + 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); + } + + 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, 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)); + + // add the cookie to the list. AddCookie() takes care of logging. + storage->AddCookie(crc, baseDomain, attrs, cookie, currentTimeInUsec, + aHostURI, aCookieHeader, true); + } + + 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->EnsureReadComplete(); + return mPersistentStorage->RunInTransaction(aCallback); +} + +/****************************************************************************** + * nsICookieManager impl: + * nsICookieManager + ******************************************************************************/ + +NS_IMETHODIMP +CookieService::RemoveAll() { + if (!IsInitialized()) { + return NS_ERROR_NOT_AVAILABLE; + } + + mPersistentStorage->EnsureReadComplete(); + mPersistentStorage->RemoveAll(); + return NS_OK; +} + +NS_IMETHODIMP +CookieService::GetCookies(nsTArray<RefPtr<nsICookie>>& aCookies) { + if (!IsInitialized()) { + return NS_ERROR_NOT_AVAILABLE; + } + + mPersistentStorage->EnsureReadComplete(); + + // 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->EnsureReadComplete(); + + // We expose only non-private cookies. + mPersistentStorage->GetCookies(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::HandleValue 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, 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); + 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::HandleValue 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 aHttpBound, + 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); + + nsICookie::schemeType schemeType = CookieCommons::URIToSchemeType(aHostURI); + + // 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 potentiallyTurstworthy = + 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 can't send it + if (cookie->IsSecure() && !potentiallyTurstworthy) { + continue; + } + + // The scheme doesn't match. + if (!CookieCommons::MaybeCompareSchemeWithLogging(crc, aHostURI, cookie, + schemeType)) { + continue; + } + + if (aHttpBound && aIsSameSiteForeign && + !ProcessSameSiteCookieForForeignRequest( + aChannel, cookie, aIsSafeTopLevelNav, laxByDefault)) { + 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; + } + + // 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()); +} + +// 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, 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 potentiallyTurstworthy = + 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; + } + + 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; + } + + // magic prefix checks. MUST be run after CheckDomain() and CheckPath() + if (!CheckPrefixes(aCookieData, potentiallyTurstworthy)) { + 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 (aFromHttp && !CookieCommons::CheckHttpValue(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() && !potentiallyTurstworthy) { + 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. + if ((aCookieData.sameSite() != 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; + } + + 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)) { + continue; + } + ++lastSpace; + } + aTokenString.Rebind(start, lastSpace); + + aEqualsFound = (*aIter == '='); + if (aEqualsFound) { + // find <value> + while (++aIter != aEndIter && iswhitespace(*aIter)) { + continue; + } + + 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)) { + continue; + } + + 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 SetSameSiteDefaultAttribute(CookieStruct& aCookieData, + bool laxByDefault) { + aCookieData.rawSameSite() = nsICookie::SAMESITE_NONE; + if (laxByDefault) { + aCookieData.sameSite() = nsICookie::SAMESITE_LAX; + } else { + aCookieData.sameSite() = nsICookie::SAMESITE_NONE; + } +} + +// 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"; + + nsACString::const_char_iterator cookieStart; + aCookieHeader.BeginReading(cookieStart); + + nsACString::const_char_iterator cookieEnd; + aCookieHeader.EndReading(cookieEnd); + + aCookieData.isSecure() = false; + aCookieData.isHttpOnly() = false; + + bool laxByDefault = + StaticPrefs::network_cookie_sameSite_laxByDefault() && + !nsContentUtils::IsURIInPrefList( + aHostURI, "network.cookie.sameSite.laxByDefault.disabledHosts"); + SetSameSiteDefaultAttribute(aCookieData, laxByDefault); + + 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; + } + + bool sameSiteSet = false; + + // 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 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)) { + aCookieData.sameSite() = nsICookie::SAMESITE_LAX; + aCookieData.rawSameSite() = nsICookie::SAMESITE_LAX; + sameSiteSet = true; + } else if (tokenValue.LowerCaseEqualsLiteral(kSameSiteStrict)) { + aCookieData.sameSite() = nsICookie::SAMESITE_STRICT; + aCookieData.rawSameSite() = nsICookie::SAMESITE_STRICT; + sameSiteSet = true; + } else if (tokenValue.LowerCaseEqualsLiteral(kSameSiteNone)) { + aCookieData.sameSite() = nsICookie::SAMESITE_NONE; + aCookieData.rawSameSite() = nsICookie::SAMESITE_NONE; + sameSiteSet = true; + } else { + // Reset to defaults if unknown token value (see Bug 1682450) + SetSameSiteDefaultAttribute(aCookieData, laxByDefault); + sameSiteSet = false; + CookieLogging::LogMessageToConsole( + aCRC, aHostURI, nsIScriptError::infoFlag, CONSOLE_SAMESITE_CATEGORY, + "CookieSameSiteValueInvalid2"_ns, + AutoTArray<nsString, 1>{NS_ConvertUTF8toUTF16(aCookieData.name())}); + } + } + } + + Telemetry::Accumulate(Telemetry::COOKIE_SAMESITE_SET_VS_UNSET, + sameSiteSet ? 1 : 0); + + // re-assign aCookieHeader, in case we need to process another cookie + aCookieHeader.Assign(Substring(cookieStart, cookieEnd)); + + // If same-site is set to 'none' but this is not a secure context, let's abort + // the parsing. + if (!aCookieData.isSecure() && + aCookieData.sameSite() == nsICookie::SAMESITE_NONE) { + if (laxByDefault && + StaticPrefs::network_cookie_sameSite_noneRequiresSecure()) { + CookieLogging::LogMessageToConsole( + aCRC, aHostURI, nsIScriptError::infoFlag, CONSOLE_SAMESITE_CATEGORY, + "CookieRejectedNonRequiresSecure2"_ns, + AutoTArray<nsString, 1>{NS_ConvertUTF8toUTF16(aCookieData.name())}); + return newCookie; + } + + // if SameSite=Lax by default is disabled, we want to warn the user. + CookieLogging::LogMessageToConsole( + aCRC, aHostURI, nsIScriptError::warningFlag, CONSOLE_SAMESITE_CATEGORY, + "CookieRejectedNonRequiresSecureForBeta2"_ns, + AutoTArray<nsString, 2>{NS_ConvertUTF8toUTF16(aCookieData.name()), + SAMESITE_MDN_URL}); + } + + if (aCookieData.rawSameSite() == nsICookie::SAMESITE_NONE && + aCookieData.sameSite() == nsICookie::SAMESITE_LAX) { + 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::ValidateRawSame(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() ? GET_COOKIE : SET_COOKIE, + 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() ? GET_COOKIE : SET_COOKIE, + 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() ? GET_COOKIE : SET_COOKIE, + 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()) { + bool rejectThirdPartyWithExceptions = + CookieJarSettings::IsRejectThirdPartyWithExceptions( + aCookieJarSettings->GetCookieBehavior()); + + uint32_t rejectReason = + rejectThirdPartyWithExceptions + ? nsIWebProgressListener::STATE_COOKIES_BLOCKED_FOREIGN + : 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() ? GET_COOKIE : SET_COOKIE, + aHostURI, aCookieHeader, + "cookies are disabled in trackers"); + if (aIsThirdPartySocialTrackingResource) { + *aRejectedReason = + nsIWebProgressListener::STATE_COOKIES_BLOCKED_SOCIALTRACKER; + } else if (rejectThirdPartyWithExceptions) { + *aRejectedReason = nsIWebProgressListener::STATE_COOKIES_BLOCKED_FOREIGN; + } 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() ? GET_COOKIE : SET_COOKIE, + 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() ? GET_COOKIE : SET_COOKIE, + 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() ? GET_COOKIE : SET_COOKIE, + 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; +} + +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 0 + } else { + /** + * The following test is part of the RFC2109 spec. Loosely speaking, it says that a site + * cannot set a cookie for a path that it is not on. See bug 155083. However this patch + * broke several sites -- nordea (bug 155768) and citibank (bug 156725). So this test has + * been disabled, unless we can evangelize these sites. + */ + // get path from aHostURI + nsAutoCString pathFromURI; + if (NS_FAILED(aHostURI->GetPathQueryRef(pathFromURI)) || + !StringBeginsWith(pathFromURI, aCookieData.path())) { + return false; + } +#endif + } + + 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; + } + + if (aCookieData.path().Contains('\t')) { + return false; + } + + return true; +} + +// CheckPrefixes +// +// Reject cookies whose name starts with the magic prefixes from +// https://tools.ietf.org/html/draft-ietf-httpbis-cookie-prefixes-00 +// 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::HandleValue 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) { + NS_ENSURE_ARG_POINTER(aOriginAttributes); + NS_ENSURE_ARG_POINTER(aFoundCookie); + + 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); + *aFoundCookie = storage->FindCookie(baseDomain, *aOriginAttributes, aHost, + aName, aPath, iter); + 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->EnsureReadComplete(); + + *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::HandleValue 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->EnsureReadComplete(); + + 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->EnsureReadComplete(); + + // 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->EnsureReadComplete(); + return mPersistentStorage; +} + +CookieStorage* CookieService::PickStorage( + const OriginAttributesPattern& aAttrs) { + MOZ_ASSERT(IsInitialized()); + + if (aAttrs.mPrivateBrowsingId.WasPassed() && + aAttrs.mPrivateBrowsingId.Value() > 0) { + return mPrivateStorage; + } + + mPersistentStorage->EnsureReadComplete(); + return mPersistentStorage; +} + +bool CookieService::SetCookiesFromIPC(const nsACString& aBaseDomain, + const OriginAttributes& aAttrs, + nsIURI* aHostURI, bool aFromHttp, + const nsTArray<CookieStruct>& aCookies) { + 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; + } + + if (!CookieCommons::CheckName(cookieData)) { + return false; + } + + if (aFromHttp && !CookieCommons::CheckHttpValue(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); + } + + return true; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/cookie/CookieService.h b/netwerk/cookie/CookieService.h new file mode 100644 index 0000000000..fbb5ff04bf --- /dev/null +++ b/netwerk/cookie/CookieService.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_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, + 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 aHttpBound, + 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); + + protected: + virtual ~CookieService(); + + bool IsInitialized() const; + + void InitCookieStorages(); + void CloseCookieStorages(); + + void EnsureReadComplete(bool aInitDBConn); + 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 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<CookiePersistentStorage> mPersistentStorage; + RefPtr<CookiePrivateStorage> 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..a20d1d2b12 --- /dev/null +++ b/netwerk/cookie/CookieServiceChild.cpp @@ -0,0 +1,634 @@ +/* -*- 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 "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 "nsIChannel.h" +#include "nsIClassifiedChannel.h" +#include "nsIHttpChannel.h" +#include "nsIEffectiveTLDService.h" +#include "nsIURI.h" +#include "nsIPrefBranch.h" +#include "nsServiceManagerUtils.h" +#include "mozilla/Telemetry.h" +#include "mozilla/TimeStamp.h" +#include "ThirdPartyUtil.h" + +using namespace mozilla::ipc; + +namespace mozilla { +namespace net { + +// Pref string constants +static const char kCookieMoveIntervalSecs[] = + "network.cookie.move.interval_sec"; + +static StaticRefPtr<CookieServiceChild> gCookieChildService; +static uint32_t gMoveCookiesIntervalSeconds = 10; + +already_AddRefed<CookieServiceChild> CookieServiceChild::GetSingleton() { + if (!gCookieChildService) { + gCookieChildService = new CookieServiceChild(); + ClearOnShutdown(&gCookieChildService); + } + + return do_AddRef(gCookieChildService); +} + +NS_IMPL_ISUPPORTS(CookieServiceChild, nsICookieService, nsIObserver, + nsITimerCallback, nsISupportsWeakReference) + +CookieServiceChild::CookieServiceChild() { + NS_ASSERTION(IsNeckoChild(), "not a child process"); + + auto* cc = static_cast<mozilla::dom::ContentChild*>(gNeckoChild->Manager()); + if (cc->IsShuttingDown()) { + return; + } + + // This corresponds to Release() in DeallocPCookieService. + NS_ADDREF_THIS(); + + NeckoChild::InitNeckoChild(); + + // Create a child PCookieService actor. + 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"); + + // Init our prefs and observer. + nsCOMPtr<nsIPrefBranch> prefBranch = do_GetService(NS_PREFSERVICE_CONTRACTID); + NS_WARNING_ASSERTION(prefBranch, "no prefservice"); + if (prefBranch) { + prefBranch->AddObserver(kCookieMoveIntervalSecs, this, true); + PrefChanged(prefBranch); + } + + nsCOMPtr<nsIObserverService> observerService = services::GetObserverService(); + if (observerService) { + observerService->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false); + } +} + +void CookieServiceChild::MoveCookies() { + TimeStamp start = TimeStamp::Now(); + for (auto iter = mCookiesMap.Iter(); !iter.Done(); iter.Next()) { + CookiesList* cookiesList = iter.UserData(); + CookiesList newCookiesList; + for (uint32_t i = 0; i < cookiesList->Length(); ++i) { + Cookie* cookie = cookiesList->ElementAt(i); + RefPtr<Cookie> newCookie = cookie->Clone(); + newCookiesList.AppendElement(newCookie); + } + *cookiesList = std::move(newCookiesList); + } + + Telemetry::AccumulateTimeDelta(Telemetry::COOKIE_TIME_MOVING_MS, start); +} + +NS_IMETHODIMP +CookieServiceChild::Notify(nsITimer* aTimer) { + if (aTimer == mCookieTimer) { + MoveCookies(); + } else { + MOZ_CRASH("Unknown timer"); + } + return NS_OK; +} + +CookieServiceChild::~CookieServiceChild() { gCookieChildService = nullptr; } + +void CookieServiceChild::TrackCookieLoad(nsIChannel* aChannel) { + if (!CanSend()) { + return; + } + + 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 isSameSiteForeign = CookieCommons::IsSameSiteForeign(aChannel, uri); + SendPrepareCookieList( + uri, result.contains(ThirdPartyAnalysis::IsForeign), + result.contains(ThirdPartyAnalysis::IsThirdPartyTrackingResource), + result.contains(ThirdPartyAnalysis::IsThirdPartySocialTrackingResource), + result.contains(ThirdPartyAnalysis::IsStorageAccessPermissionGranted), + rejectedReason, isSafeTopLevelNav, isSameSiteForeign, attrs); +} + +IPCResult CookieServiceChild::RecvRemoveAll() { + mCookiesMap.Clear(); + return IPC_OK(); +} + +IPCResult CookieServiceChild::RecvRemoveCookie(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 IPC_OK(); + } + + 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())) { + cookiesList->RemoveElementAt(i); + break; + } + } + + return IPC_OK(); +} + +IPCResult CookieServiceChild::RecvAddCookie(const CookieStruct& aCookie, + const OriginAttributes& aAttrs) { + RefPtr<Cookie> cookie = Cookie::Create(aCookie, aAttrs); + RecordDocumentCookie(cookie, aAttrs); + 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); + RecvRemoveCookie(cookieStruct, aAttrsList.ElementAt(i)); + } + 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); + } + + return IPC_OK(); +} + +void CookieServiceChild::PrefChanged(nsIPrefBranch* aPrefBranch) { + int32_t val; + if (NS_SUCCEEDED(aPrefBranch->GetIntPref(kCookieMoveIntervalSecs, &val))) { + gMoveCookiesIntervalSeconds = clamped<uint32_t>(val, 0, 3600); + if (gMoveCookiesIntervalSeconds && !mCookieTimer) { + NS_NewTimerWithCallback(getter_AddRefs(mCookieTimer), this, + gMoveCookiesIntervalSeconds * 1000, + nsITimer::TYPE_REPEATING_SLACK_LOW_PRIORITY); + } + if (!gMoveCookiesIntervalSeconds && mCookieTimer) { + mCookieTimer->Cancel(); + mCookieTimer = nullptr; + } + if (mCookieTimer) { + mCookieTimer->SetDelay(gMoveCookiesIntervalSeconds * 1000); + } + } +} + +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.LookupOrAdd(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->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::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* /*aData*/) { + if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) { + if (mCookieTimer) { + mCookieTimer->Cancel(); + mCookieTimer = nullptr; + } + nsCOMPtr<nsIObserverService> observerService = + services::GetObserverService(); + if (observerService) { + observerService->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); + } + } else if (!strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) { + nsCOMPtr<nsIPrefBranch> prefBranch = do_QueryInterface(aSubject); + if (prefBranch) { + PrefChanged(prefBranch); + } + } else { + MOZ_ASSERT(false, "unexpected topic!"); + } + + return NS_OK; +} + +NS_IMETHODIMP +CookieServiceChild::GetCookieStringFromDocument(Document* aDocument, + nsACString& aCookieString) { + NS_ENSURE_ARG(aDocument); + + aCookieString.Truncate(); + + nsCOMPtr<nsIPrincipal> principal = aDocument->EffectiveStoragePrincipal(); + + if (!CookieCommons::IsSchemeSupported(principal)) { + return NS_OK; + } + + nsICookie::schemeType schemeType = + CookieCommons::PrincipalToSchemeType(principal); + + 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; + principal->GetAsciiHost(hostFromURI); + + 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) { + thirdParty = nsContentUtils::IsThirdPartyWindowOrChannel(innerWindow, + nullptr, nullptr); + } + + 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)) { + continue; + } + + // if the cookie is secure and the host scheme isn't, we can't send it + if (cookie->IsSecure() && !isPotentiallyTrustworthy) { + continue; + } + + if (!CookieCommons::MaybeCompareScheme(cookie, schemeType)) { + 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( + 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) { + thirdParty = nsContentUtils::IsThirdPartyWindowOrChannel(innerWindow, + nullptr, nullptr); + } + + if (thirdParty && + !CookieCommons::ShouldIncludeCrossSiteCookieForDocument(cookie)) { + 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 + // 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. + 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()) && + existingCookie->IsHttpOnly()) { + // Can't overwrite an httponly cookie from a script context. + return NS_OK; + } + } + } + + RecordDocumentCookie(cookie, attrs); + + if (CanSend()) { + nsTArray<CookieStruct> cookiesToSend; + cookiesToSend.AppendElement(cookie->ToIPC()); + + // Asynchronously call the parent. + 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 moreCookies; + do { + CookieStruct cookieData; + bool canSetCookie = false; + moreCookies = CookieService::CanSetCookie( + aHostURI, baseDomain, cookieData, requireHostMatch, cookieStatus, + cookieString, true, isForeignAndNotAddon, 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"); + 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()) { + 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..e0a49e8ebb --- /dev/null +++ b/netwerk/cookie/CookieServiceChild.h @@ -0,0 +1,87 @@ +/* -*- 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 "nsIObserver.h" +#include "nsIPrefBranch.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 nsIObserver, + public nsITimerCallback, + public nsSupportsWeakReference { + friend class PCookieServiceChild; + + public: + NS_DECL_ISUPPORTS + NS_DECL_NSICOOKIESERVICE + NS_DECL_NSIOBSERVER + NS_DECL_NSITIMERCALLBACK + + typedef nsTArray<RefPtr<Cookie>> CookiesList; + typedef nsClassHashtable<CookieKey, CookiesList> CookiesMap; + + CookieServiceChild(); + + static already_AddRefed<CookieServiceChild> GetSingleton(); + + void TrackCookieLoad(nsIChannel* aChannel); + + private: + ~CookieServiceChild(); + void MoveCookies(); + + void RecordDocumentCookie(Cookie* aCookie, const OriginAttributes& aAttrs); + + uint32_t CountCookiesFromHashTable(const nsACString& aBaseDomain, + const OriginAttributes& aOriginAttrs); + + void PrefChanged(nsIPrefBranch* aPrefBranch); + + 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); + + CookiesMap mCookiesMap; + nsCOMPtr<nsITimer> mCookieTimer; + 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..d87c1c551e --- /dev/null +++ b/netwerk/cookie/CookieServiceParent.cpp @@ -0,0 +1,182 @@ +/* -*- 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 "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 "nsNetCID.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"); + 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); + auto cookie = static_cast<Cookie*>(xpcCookie.get()); + attrs = cookie->OriginAttributesRef(); + cookieStruct = cookie->ToIPC(); + if (cookie->IsHttpOnly()) { + // Child only needs to exist if an HttpOnly cookie exists, not its value + cookieStruct.value() = ""; + } + cookieStructList.AppendElement(cookieStruct); + attrsList.AppendElement(attrs); + } + Unused << SendRemoveBatchDeletedCookies(cookieStructList, attrsList); +} + +void CookieServiceParent::RemoveAll() { Unused << SendRemoveAll(); } + +void CookieServiceParent::RemoveCookie(nsICookie* aCookie) { + auto cookie = static_cast<Cookie*>(aCookie); + const OriginAttributes& attrs = cookie->OriginAttributesRef(); + CookieStruct cookieStruct = cookie->ToIPC(); + if (cookie->IsHttpOnly()) { + cookieStruct.value() = ""; + } + Unused << SendRemoveCookie(cookieStruct, attrs); +} + +void CookieServiceParent::AddCookie(nsICookie* aCookie) { + auto cookie = static_cast<Cookie*>(aCookie); + const OriginAttributes& attrs = cookie->OriginAttributesRef(); + CookieStruct cookieStruct = cookie->ToIPC(); + if (cookie->IsHttpOnly()) { + cookieStruct.value() = ""; + } + Unused << SendAddCookie(cookieStruct, attrs); +} + +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 aIsSameSiteForeign = CookieCommons::IsSameSiteForeign(aChannel, uri); + + StoragePrincipalHelper::PrepareEffectiveStoragePrincipalOriginAttributes( + aChannel, attrs); + + // Send matching cookies to Child. + nsCOMPtr<mozIThirdPartyUtil> thirdPartyUtil; + thirdPartyUtil = do_GetService(THIRDPARTYUTIL_CONTRACTID); + + uint32_t rejectedReason = 0; + ThirdPartyAnalysisResult result = thirdPartyUtil->AnalyzeChannel( + aChannel, false, nullptr, nullptr, &rejectedReason); + + 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, aIsSameSiteForeign, false, attrs, + foundCookieList); + nsTArray<CookieStruct> matchingCookiesList; + SerialializeCookieList(foundCookieList, matchingCookiesList); + Unused << SendTrackCookiesLoad(matchingCookiesList, attrs); +} + +// static +void CookieServiceParent::SerialializeCookieList( + const nsTArray<Cookie*>& aFoundCookieList, + nsTArray<CookieStruct>& aCookiesList) { + for (uint32_t i = 0; i < aFoundCookieList.Length(); i++) { + Cookie* cookie = aFoundCookieList.ElementAt(i); + CookieStruct* cookieStruct = aCookiesList.AppendElement(); + *cookieStruct = cookie->ToIPC(); + if (cookie->IsHttpOnly()) { + // Value only needs to exist if an HttpOnly cookie exists. + cookieStruct->value() = ""; + } + } +} + +IPCResult CookieServiceParent::RecvPrepareCookieList( + 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 OriginAttributes& aAttrs) { + // Send matching cookies to Child. + if (!aHost) { + return IPC_FAIL(this, "aHost must not be null"); + } + + 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, false, aAttrs, + foundCookieList); + nsTArray<CookieStruct> matchingCookiesList; + SerialializeCookieList(foundCookieList, matchingCookiesList); + Unused << SendTrackCookiesLoad(matchingCookiesList, aAttrs); + 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) { + 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); + 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..e8ac54671e --- /dev/null +++ b/netwerk/cookie/CookieServiceParent.h @@ -0,0 +1,71 @@ +/* -*- 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" + +class nsIArray; +class nsICookie; +namespace mozilla { +class OriginAttributes; +} + +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(nsICookie* aCookie); + + void AddCookie(nsICookie* 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; } + + 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 RecvPrepareCookieList( + 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 OriginAttributes& aAttrs); + + static void SerialializeCookieList(const nsTArray<Cookie*>& aFoundCookieList, + nsTArray<CookieStruct>& aCookiesList); + + RefPtr<CookieService> mCookieService; + bool mProcessingCookie; +}; + +} // 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..9c22301569 --- /dev/null +++ b/netwerk/cookie/CookieStorage.cpp @@ -0,0 +1,887 @@ +/* -*- 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 "CookieStorage.h" +#include "mozilla/dom/nsMixedContentBlocker.h" +#include "nsIMutableArray.h" +#include "nsTPriorityQueue.h" +#include "prprf.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: + CompareCookiesByAge mAgeComparator; + 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 mAgeComparator.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; +} + +// --------------------------------------------------------------------------- +// CookieStorage + +NS_IMPL_ISUPPORTS(CookieStorage, nsIObserver, nsISupportsWeakReference) + +CookieStorage::CookieStorage() + : mCookieCount(0), + mCookieOldestTime(INT64_MAX), + mMaxNumberOfCookies(kMaxNumberOfCookies), + mMaxCookiesPerHost(kMaxCookiesPerHost), + mCookieQuotaPerHost(kCookieQuotaPerHost), + mCookiePurgeAge(kCookiePurgeAge) {} + +CookieStorage::~CookieStorage() = default; + +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); + } +} + +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 (auto iter = mHostTable.ConstIter(); !iter.Done(); iter.Next()) { + const CookieEntry::ArrayType& cookies = iter.Get()->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 (auto iter = mHostTable.ConstIter(); !iter.Done(); iter.Next()) { + const CookieEntry::ArrayType& cookies = iter.Get()->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 (auto iter = mHostTable.ConstIter(); !iter.Done(); iter.Next()) { + const CookieEntry::ArrayType& cookies = iter.Get()->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, u"deleted"); + } +} + +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, u"deleted"); + } + } + } +} + +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, u"deleted"); + } + } + } +} + +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, u"cleared"); +} + +// notify observers that the cookie list changed. there are five possible +// values for aData: +// "deleted" means a cookie was deleted. aSubject is the deleted cookie. +// "added" means a cookie was added. aSubject is the added cookie. +// "changed" means a cookie was altered. aSubject is the new cookie. +// "cleared" means the entire cookie list was cleared. aSubject is null. +// "batch-deleted" means a set of cookies was purged. aSubject is the list of +// cookies. +void CookieStorage::NotifyChanged(nsISupports* aSubject, const char16_t* aData, + bool aOldCookieIsSession) { + nsCOMPtr<nsIObserverService> os = services::GetObserverService(); + if (!os) { + return; + } + // Notify for topic "private-cookie-changed" or "cookie-changed" + os->NotifyObservers(aSubject, NotificationTopic(), aData); + + NotifyChangedInternal(aSubject, aData, 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) { + 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); + } + 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"); + CookieLogging::LogMessageToConsole( + aCRC, aHostURI, nsIScriptError::warningFlag, + CONSOLE_REJECTION_CATEGORY, "CookieRejectedExpired"_ns, + AutoTArray<nsString, 1>{ + NS_ConvertUTF8toUTF16(aCookie->Name()), + }); + 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->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"); + CookieLogging::LogMessageToConsole( + aCRC, aHostURI, nsIScriptError::warningFlag, + CONSOLE_REJECTION_CATEGORY, "CookieRejectedExpired"_ns, + AutoTArray<nsString, 1>{ + NS_ConvertUTF8toUTF16(aCookie->Name()), + }); + NotifyChanged(oldCookie, u"deleted", 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"); + CookieLogging::LogMessageToConsole( + aCRC, aHostURI, nsIScriptError::warningFlag, + CONSOLE_REJECTION_CATEGORY, "CookieRejectedExpired"_ns, + AutoTArray<nsString, 1>{ + NS_ConvertUTF8toUTF16(aCookie->Name()), + }); + 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(getter_AddRefs(purgedList), evictedCookie); + MOZ_ASSERT((*it).entry); + } + + } 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); + } + } + } + + // 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, u"batch-deleted"); + } + + NotifyChanged(aCookie, foundCookie ? u"changed" : u"added", + 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(nsIArray** aPurgedList, + nsICookie* aCookie) { + if (!*aPurgedList) { + COOKIE_LOGSTRING(LogLevel::Debug, ("Creating new purge list")); + nsCOMPtr<nsIArray> purgedList = CreatePurgeList(aCookie); + purgedList.forget(aPurgedList); + 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); + 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); + } + } + + 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..9db3d8a412 --- /dev/null +++ b/netwerk/cookie/CookieStorage.h @@ -0,0 +1,200 @@ +/* -*- 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 "nsIObserver.h" +#include "nsTHashtable.h" +#include "nsWeakReference.h" +#include <functional> + +class nsIArray; +class nsICookie; +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 + typedef nsTArray<RefPtr<Cookie>> ArrayType; + typedef ArrayType::index_type IndexType; + + 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; } + + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) 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, const char16_t* aData, + 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); + + static void CreateOrUpdatePurgeList(nsIArray** aPurgedList, + nsICookie* aCookie); + + virtual void StaleCookies(const nsTArray<Cookie*>& aCookieList, + int64_t aCurrentTimeInUsec) = 0; + + virtual void Close() = 0; + + protected: + CookieStorage(); + virtual ~CookieStorage(); + + 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(nsISupports* aSubject, + const char16_t* aData, + 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 CookieListIter& aIter) = 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; + + 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; + + int64_t mCookieOldestTime; + + uint16_t mMaxNumberOfCookies; + uint16_t mMaxCookiesPerHost; + uint16_t mCookieQuotaPerHost; + int64_t mCookiePurgeAge; +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_CookieStorage_h diff --git a/netwerk/cookie/CookieXPCShellUtils.jsm b/netwerk/cookie/CookieXPCShellUtils.jsm new file mode 100644 index 0000000000..433151ed92 --- /dev/null +++ b/netwerk/cookie/CookieXPCShellUtils.jsm @@ -0,0 +1,60 @@ +/* -*- 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/. */ +"use strict"; + +var EXPORTED_SYMBOLS = ["CookieXPCShellUtils"]; + +const { ExtensionTestUtils } = ChromeUtils.import( + "resource://testing-common/ExtensionXPCShellUtils.jsm" +); + +const { AddonTestUtils } = ChromeUtils.import( + "resource://testing-common/AddonTestUtils.jsm" +); + +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( + null, + // 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..931299ef44 --- /dev/null +++ b/netwerk/cookie/PCookieService.ipdl @@ -0,0 +1,72 @@ +/* -*- 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"; +using refcounted 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 + */ + +nested(upto inside_cpow) sync protocol PCookieService +{ + manager PNecko; + +parent: + nested(inside_cpow) async SetCookies(nsCString baseDomain, + OriginAttributes attrs, + nsIURI host, + bool fromHttp, + CookieStruct[] cookies); + + async PrepareCookieList(nsIURI host, + bool isForeign, + bool isThirdPartyTrackingResource, + bool isThirdPartySocialTrackingResource, + bool firstPartyStorageAccessPermissionGranted, + uint32_t rejectedReason, + bool isSafeTopLevelNav, + bool isSameSiteForeign, + OriginAttributes attrs); + + 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..b21d234b35 --- /dev/null +++ b/netwerk/cookie/moz.build @@ -0,0 +1,76 @@ +# -*- 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", + "nsICookiePermission.idl", + "nsICookieService.idl", +] + +XPIDL_MODULE = "necko_cookie" + + +EXPORTS.mozilla.net = [ + "CookieJarSettings.h", + "CookieKey.h", + "CookiePersistentStorage.h", + "CookiePrivateStorage.h", + "CookieService.h", + "CookieServiceChild.h", + "CookieServiceParent.h", + "CookieStorage.h", +] +UNIFIED_SOURCES += [ + "Cookie.cpp", + "CookieCommons.cpp", + "CookieJarSettings.cpp", + "CookieLogging.cpp", + "CookiePersistentStorage.cpp", + "CookiePrivateStorage.cpp", + "CookieService.cpp", + "CookieServiceChild.cpp", + "CookieServiceParent.cpp", + "CookieStorage.cpp", +] +XPCSHELL_TESTS_MANIFESTS += [ + "test/unit/xpcshell.ini", +] + +BROWSER_CHROME_MANIFESTS += [ + "test/browser/browser.ini", +] + +MOCHITEST_MANIFESTS += [ + "test/mochitest/mochitest.ini", +] + +IPDL_SOURCES = [ + "PCookieService.ipdl", +] + +LOCAL_INCLUDES += [ + "/dom/base", + "/intl/uconv", + "/netwerk/base", + "/netwerk/protocol/http", +] + +TESTING_JS_MODULES += [ + "CookieXPCShellUtils.jsm", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" + +if CONFIG["CC_TYPE"] in ("clang", "gcc"): + CXXFLAGS += ["-Wno-error=shadow"] diff --git a/netwerk/cookie/nsICookie.idl b/netwerk/cookie/nsICookie.idl new file mode 100644 index 0000000000..0b2e5e97bc --- /dev/null +++ b/netwerk/cookie/nsICookie.idl @@ -0,0 +1,139 @@ +/* -*- 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. + */ + +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; + + /** + * 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; +}; diff --git a/netwerk/cookie/nsICookieJarSettings.idl b/netwerk/cookie/nsICookieJarSettings.idl new file mode 100644 index 0000000000..052c957557 --- /dev/null +++ b/netwerk/cookie/nsICookieJarSettings.idl @@ -0,0 +1,59 @@ +/* -*- 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; + +/** + * 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; + + /** + * Whether our cookie behavior mandates rejecting third-party contexts. + */ + [infallible] readonly attribute boolean rejectThirdPartyContexts; + + [infallible] readonly attribute boolean limitForeignContexts; + + /** + * 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; + + /** + * 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); +}; diff --git a/netwerk/cookie/nsICookieManager.idl b/netwerk/cookie/nsICookieManager.idl new file mode 100644 index 0000000000..3d815846a5 --- /dev/null +++ b/netwerk/cookie/nsICookieManager.idl @@ -0,0 +1,254 @@ +/* -*- 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 "network.cookie.cookieBehavior". + */ + readonly attribute uint32_t cookieBehavior; + %{C++ + static uint32_t GetCookieBehavior(); + %} + + /** + * 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); + + /** + * 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/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..52ed13f61c --- /dev/null +++ b/netwerk/cookie/nsICookieService.idl @@ -0,0 +1,168 @@ +/* -*- 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 "batch-deleted" notification will likely be + * immediately followed by "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: see below. + * data : "deleted" + * a cookie was deleted. the subject is an nsICookie representing + * the deleted cookie. + * "added" + * a cookie was added. the subject is an nsICookie representing + * the added cookie. + * "changed" + * a cookie was changed. the subject is an nsICookie representing + * the new cookie. (note that host, path, and name are invariant + * for a given cookie; other parameters may change.) + * "batch-deleted" + * a set of cookies was purged (typically, because they have either + * expired or because the cookie list has grown too large). The subject + * is an nsIArray of nsICookie's representing the deleted cookies. + * Note that the array could contain a single cookie. + * "cleared" + * the entire cookie list was cleared. the subject is null. + * + * 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; + + /* + * Possible values for the "network.cookie.lifetimePolicy" preference. + */ + const uint32_t ACCEPT_NORMALLY = 0; // accept normally + // Value = 1 is considered the same as 0 (See Bug 606655). + const uint32_t ACCEPT_SESSION = 2; // downgrade to session + // Value = 3 is considered the same as 0 + + /* + * 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.ini b/netwerk/cookie/test/browser/browser.ini new file mode 100644 index 0000000000..9a4ec0bacd --- /dev/null +++ b/netwerk/cookie/test/browser/browser.ini @@ -0,0 +1,19 @@ +[DEFAULT] +support-files = + file_empty.html + file_empty.js + head.js + +[browser_broadcastChannel.js] +[browser_cookies.js] +support-files = server.sjs +[browser_domCache.js] +[browser_indexedDB.js] +[browser_originattributes.js] +[browser_storage.js] +[browser_serviceWorker.js] +[browser_sharedWorker.js] +[browser_sameSiteConsole.js] +support-files = sameSite.sjs +[browser_oversize.js] +support-files = oversize.sjs diff --git a/netwerk/cookie/test/browser/browser_broadcastChannel.js b/netwerk/cookie/test/browser/browser_broadcastChannel.js new file mode 100644 index 0000000000..3882fee98f --- /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_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_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..1da0940c4e --- /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..68a322d98e --- /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"; + +let cm = Cc["@mozilla.org/cookiemanager;1"].getService(Ci.nsICookieManager); + +// 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_task(async function setup() { + // 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 + cm.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 = cm.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_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..ed2587b400 --- /dev/null +++ b/netwerk/cookie/test/browser/head.js @@ -0,0 +1,201 @@ +const { PermissionTestUtils } = ChromeUtils.import( + "resource://testing-common/PermissionTestUtils.jsm" +); + +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..e2275d050c --- /dev/null +++ b/netwerk/cookie/test/browser/oversize.sjs @@ -0,0 +1,9 @@ +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/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..283df46dcd --- /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..3d31aeb1c6 --- /dev/null +++ b/netwerk/cookie/test/mochitest/cookie.sjs @@ -0,0 +1,124 @@ +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") })); + + 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); + }; + }; + + 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..41b0065d41 --- /dev/null +++ b/netwerk/cookie/test/mochitest/cookiesHelper.js @@ -0,0 +1,67 @@ +const ALLOWED = 0; +const BLOCKED = 1; + +async function cleanupData() { + await new Promise(resolve => { + const chromeScript = SpecialPowers.loadChromeScript(_ => { + // eslint-disable-next-line no-undef + addMessageListener("go", __ => { + const { Services } = ChromeUtils.import( + "resource://gre/modules/Services.jsm" + ); + Services.clearData.deleteData( + Services.clearData.CLEAR_COOKIES | + Services.clearData.CLEAR_ALL_CACHES | + Services.clearData.CLEAR_DOM_STORAGES, + ___ => { + // eslint-disable-next-line no-undef + 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.ini b/netwerk/cookie/test/mochitest/mochitest.ini new file mode 100644 index 0000000000..26179d6fea --- /dev/null +++ b/netwerk/cookie/test/mochitest/mochitest.ini @@ -0,0 +1,17 @@ +[DEFAULT] +scheme=https +support-files = + cookie.sjs + cookiesHelper.js + +[test_document_cookie.html] +[test_fetch.html] +[test_image.html] +[test_script.html] +[test_sharedWorker.html] +[test_worker.html] +[test_xhr.html] +[test_metaTag.html] +[test_xmlDocument.html] +support-files = empty.html +[test_document_cookie_notification.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..9d58c06695 --- /dev/null +++ b/netwerk/cookie/test/unit/test_baseDomain_publicsuffix.js @@ -0,0 +1,107 @@ +"use strict"; + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +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..4bccac6d1d --- /dev/null +++ b/netwerk/cookie/test/unit/test_bug1155169.js @@ -0,0 +1,93 @@ +const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +const URI = Services.io.newURI("http://example.org/"); + +const cs = Cc["@mozilla.org/cookieService;1"].getService(Ci.nsICookieService); + +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: "added", + isSession: true, + isSecure: false, + isHttpOnly: false, + }); + + // Update cookie with isHttpOnly=true. + setCookie("foo=bar; HttpOnly", { + type: "changed", + isSession: true, + isSecure: false, + isHttpOnly: true, + }); + + // Update cookie with isSecure=true. + setCookie("foo=bar; Secure", { + type: "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: "changed", + isSession: false, + isSecure: false, + isHttpOnly: false, + }); + + // Reset cookie. + setCookie("foo=bar", { + type: "changed", + isSession: true, + isSecure: false, + isHttpOnly: false, + }); +} + +function setCookie(value, expected) { + function setCookieInternal(valueInternal, expectedInternal = null) { + function observer(subject, topic, data) { + if (!expectedInternal) { + do_throw("no notification expected"); + return; + } + + // Check we saw the right notification. + Assert.equal(data, expectedInternal.type); + + // Check cookie details. + let cookie = subject.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, + }); + + cs.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..46c9bf4a9f --- /dev/null +++ b/netwerk/cookie/test/unit/test_bug1321912.js @@ -0,0 +1,101 @@ +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +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 +const cookies = Services.cookies.sessionCookies; + +Assert.equal(conn.schemaVersion, 12); +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..3aab640773 --- /dev/null +++ b/netwerk/cookie/test/unit/test_bug643051.js @@ -0,0 +1,41 @@ +const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const { CookieXPCShellUtils } = ChromeUtils.import( + "resource://testing-common/CookieXPCShellUtils.jsm" +); + +CookieXPCShellUtils.init(this); +CookieXPCShellUtils.createServer({ hosts: ["example.net"] }); + +add_task(async () => { + // Allow all cookies. + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + + let cs = Cc["@mozilla.org/cookieService;1"].getService(Ci.nsICookieService); + + 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"; + cs.setCookieStringFromHttp(uri, set, channel); + + let actual = cs.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); +}); diff --git a/netwerk/cookie/test/unit/test_eviction.js b/netwerk/cookie/test/unit/test_eviction.js new file mode 100644 index 0000000000..30634d7818 --- /dev/null +++ b/netwerk/cookie/test/unit/test_eviction.js @@ -0,0 +1,212 @@ +const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +var { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); + +const BASE_HOST = "example.org"; +const BASE_HOSTNAMES = ["example.org", "example.co.uk"]; +const SUBDOMAINS = ["", "pub.", "www.", "other."]; + +const { CookieXPCShellUtils } = ChromeUtils.import( + "resource://testing-common/CookieXPCShellUtils.jsm" +); + +CookieXPCShellUtils.init(this); +CookieXPCShellUtils.createServer({ hosts: ["example.org"] }); + +XPCOMUtils.defineLazyServiceGetter( + this, + "cm", + "@mozilla.org/cookiemanager;1", + "nsICookieManager" +); + +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 + ); + + cm.removeAll(); +}); + +// Verify that the given cookie names exist, and are ordered from least to most recently accessed +function verifyCookies(names, uri) { + Assert.equal(cm.countCookiesFromHost(uri.host), names.length); + let actual_cookies = []; + for (let cookie of cm.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, + }); + + const cs = Cc["@mozilla.org/cookieService;1"].getService(Ci.nsICookieService); + cs.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..d807cb548c --- /dev/null +++ b/netwerk/cookie/test/unit/test_getCookieSince.js @@ -0,0 +1,72 @@ +const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +const cs = Cc["@mozilla.org/cookieService;1"].getService(Ci.nsICookieService); +const cm = cs.QueryInterface(Ci.nsICookieManager); + +function setCookie(name, url) { + let value = `${name}=${Math.random()}; Path=/; Max-Age=1000; sameSite=none`; + info(`Setting cookie ${value} for ${url.spec}`); + + let channel = NetUtil.newChannel({ + uri: url, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }); + + cs.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(cm.cookies.length, 4, "Cookie check"); + + const cookies = cm.getCookiesSince(0); + Assert.equal(cookies.length, 4, "We retrieve all the 4 cookies"); + checkSorting(cookies); + + let someCookies = cm.getCookiesSince(cookies[0].creationTime + 1); + Assert.equal(someCookies.length, 3, "We retrieve some cookies"); + checkSorting(someCookies); + + someCookies = cm.getCookiesSince(cookies[1].creationTime + 1); + Assert.equal(someCookies.length, 2, "We retrieve some cookies"); + checkSorting(someCookies); + + someCookies = cm.getCookiesSince(cookies[2].creationTime + 1); + Assert.equal(someCookies.length, 1, "We retrieve some cookies"); + checkSorting(someCookies); + + someCookies = cm.getCookiesSince(cookies[3].creationTime + 1); + Assert.equal(someCookies.length, 0, "We retrieve some 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..9b9f10613f --- /dev/null +++ b/netwerk/cookie/test/unit/test_parser_0001.js @@ -0,0 +1,33 @@ +const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +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 cs = Cc["@mozilla.org/cookieService;1"].getService(Ci.nsICookieService); + + let uri = NetUtil.newURI("http://example.org/"); + let channel = NetUtil.newChannel({ + uri, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }); + + let set = "foo=bar"; + cs.setCookieStringFromHttp(uri, set, channel); + + let expected = "foo=bar"; + let actual = cs.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..acf68d6b78 --- /dev/null +++ b/netwerk/cookie/test/unit/test_parser_0019.js @@ -0,0 +1,33 @@ +const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +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 cs = Cc["@mozilla.org/cookieService;1"].getService(Ci.nsICookieService); + + 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=/"; + cs.setCookieStringFromHttp(uri, set, channel); + + let expected = "foo=b"; + let actual = cs.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..b8f833b7da --- /dev/null +++ b/netwerk/cookie/test/unit/test_rawSameSite.js @@ -0,0 +1,126 @@ +const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +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 cs = Cc["@mozilla.org/cookieService;1"].getService(Ci.nsICookieService); + + 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"); + }); + + cs.setCookieStringFromHttp(uri, test.cookie, channel); + + await promise; + + conn = storage.openDatabase(dbFile); + Assert.equal(conn.schemaVersion, 12); + + 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..e0ab8f6385 --- /dev/null +++ b/netwerk/cookie/test/unit/test_schemeMap.js @@ -0,0 +1,287 @@ +const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +var { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); + +XPCOMUtils.defineLazyServiceGetter( + Services, + "cookiemgr", + "@mozilla.org/cookiemanager;1", + "nsICookieManager" +); + +function inChildProcess() { + return Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; +} + +const { CookieXPCShellUtils } = ChromeUtils.import( + "resource://testing-common/CookieXPCShellUtils.jsm" +); + +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 + ); + } + + let cs = Cc["@mozilla.org/cookieService;1"].getService(Ci.nsICookieService); + + 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, + }); + + cs.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, + }); + + cs.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(); + + 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 + ); + } + + let cs = Cc["@mozilla.org/cookieService;1"].getService(Ci.nsICookieService); + + info("Let's set a cookie from HTTP example.org"); + + let uri = NetUtil.newURI("https://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, + }); + + cs.setCookieStringFromHttp(uri, "a=b; sameSite=lax", channel); + + let cookies = Services.cookies.getCookieStringFromHttp(uri, channel); + Assert.equal(cookies, "a=b", "Cookies match"); + + uri = NetUtil.newURI("http://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, + }); + + cookies = Services.cookies.getCookieStringFromHttp(uri, channel); + if (schemefulComparison) { + Assert.equal(cookies, "", "No cookie for different scheme!"); + } else { + Assert.equal(cookies, "a=b", "Cookie even for different scheme!"); + } + + cookies = await CookieXPCShellUtils.getCookieStringFromDocument(uri.spec); + if (schemefulComparison) { + Assert.equal(cookies, "", "No cookie for different scheme!"); + } else { + Assert.equal(cookies, "a=b", "Cookie even for different scheme!"); + } + + Services.cookies.removeAll(); + }); +}); + +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 without scheme"); + Services.cookiemgr.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(); +}); + +[ + { + prefValue: true, + consoleMessage: `Cookie “a” has been treated as cross-site against “http://example.org/” because the scheme does not match.`, + }, + { + prefValue: false, + consoleMessage: `Cookie “a” will be soon treated as cross-site cookie against “http://example.org/” because the scheme does not match.`, + }, +].forEach(test => { + add_task(async () => { + do_get_profile(); + + maybeInitializeCookieXPCShellUtils(); + + // Allow all cookies if the pref service is available in this process. + if (!inChildProcess()) { + Services.prefs.setBoolPref( + "network.cookie.sameSite.schemeful", + test.prefValue + ); + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + } + + let cs = Cc["@mozilla.org/cookieService;1"].getService(Ci.nsICookieService); + + info("Let's set a cookie from HTTPS example.org"); + + let uri = NetUtil.newURI("https://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, + }); + + cs.setCookieStringFromHttp(uri, "a=b; sameSite=lax", channel); + + // Create a console listener. + let consolePromise = new Promise(resolve => { + let listener = { + observe(message) { + // Ignore unexpected messages. + if (!(message instanceof Ci.nsIConsoleMessage)) { + return; + } + + if (message.message.includes(test.consoleMessage)) { + Services.console.unregisterListener(listener); + resolve(); + } + }, + }; + + Services.console.registerListener(listener); + }); + + const contentPage = await CookieXPCShellUtils.loadContentPage( + "http://example.org/" + ); + await contentPage.close(); + + await consolePromise; + Services.cookies.removeAll(); + }); +}); diff --git a/netwerk/cookie/test/unit/xpcshell.ini b/netwerk/cookie/test/unit/xpcshell.ini new file mode 100644 index 0000000000..3320111637 --- /dev/null +++ b/netwerk/cookie/test/unit/xpcshell.ini @@ -0,0 +1,13 @@ +[DEFAULT] +head = + +[test_baseDomain_publicsuffix.js] +[test_bug643051.js] +[test_bug1155169.js] +[test_bug1321912.js] +[test_parser_0001.js] +[test_parser_0019.js] +[test_eviction.js] +[test_rawSameSite.js] +[test_getCookieSince.js] +[test_schemeMap.js] |