summaryrefslogtreecommitdiffstats
path: root/netwerk/cookie
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /netwerk/cookie
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'netwerk/cookie')
-rw-r--r--netwerk/cookie/Cookie.cpp289
-rw-r--r--netwerk/cookie/Cookie.h165
-rw-r--r--netwerk/cookie/CookieCommons.cpp749
-rw-r--r--netwerk/cookie/CookieCommons.h142
-rw-r--r--netwerk/cookie/CookieJarSettings.cpp733
-rw-r--r--netwerk/cookie/CookieJarSettings.h267
-rw-r--r--netwerk/cookie/CookieKey.h61
-rw-r--r--netwerk/cookie/CookieLogging.cpp185
-rw-r--r--netwerk/cookie/CookieLogging.h65
-rw-r--r--netwerk/cookie/CookieNotification.cpp60
-rw-r--r--netwerk/cookie/CookieNotification.h46
-rw-r--r--netwerk/cookie/CookiePersistentStorage.cpp2134
-rw-r--r--netwerk/cookie/CookiePersistentStorage.h160
-rw-r--r--netwerk/cookie/CookiePrivateStorage.cpp44
-rw-r--r--netwerk/cookie/CookiePrivateStorage.h61
-rw-r--r--netwerk/cookie/CookieService.cpp2586
-rw-r--r--netwerk/cookie/CookieService.h163
-rw-r--r--netwerk/cookie/CookieServiceChild.cpp653
-rw-r--r--netwerk/cookie/CookieServiceChild.h82
-rw-r--r--netwerk/cookie/CookieServiceParent.cpp268
-rw-r--r--netwerk/cookie/CookieServiceParent.h88
-rw-r--r--netwerk/cookie/CookieStorage.cpp910
-rw-r--r--netwerk/cookie/CookieStorage.h216
-rw-r--r--netwerk/cookie/CookieXPCShellUtils.sys.mjs53
-rw-r--r--netwerk/cookie/PCookieService.ipdl74
-rw-r--r--netwerk/cookie/moz.build77
-rw-r--r--netwerk/cookie/nsICookie.idl166
-rw-r--r--netwerk/cookie/nsICookieJarSettings.idl86
-rw-r--r--netwerk/cookie/nsICookieManager.idl278
-rw-r--r--netwerk/cookie/nsICookieNotification.idl70
-rw-r--r--netwerk/cookie/nsICookiePermission.idl35
-rw-r--r--netwerk/cookie/nsICookieService.idl144
-rw-r--r--netwerk/cookie/test/browser/browser.toml42
-rw-r--r--netwerk/cookie/test/browser/browser_broadcastChannel.js80
-rw-r--r--netwerk/cookie/test/browser/browser_cookie_insecure_overwrites_secure.js128
-rw-r--r--netwerk/cookie/test/browser/browser_cookie_purge_sync.js141
-rw-r--r--netwerk/cookie/test/browser/browser_cookies.js53
-rw-r--r--netwerk/cookie/test/browser/browser_cookies_ipv6.js57
-rw-r--r--netwerk/cookie/test/browser/browser_domCache.js25
-rw-r--r--netwerk/cookie/test/browser/browser_indexedDB.js84
-rw-r--r--netwerk/cookie/test/browser/browser_originattributes.js121
-rw-r--r--netwerk/cookie/test/browser/browser_oversize.js96
-rw-r--r--netwerk/cookie/test/browser/browser_partitionedConsole.js201
-rw-r--r--netwerk/cookie/test/browser/browser_partitioned_telemetry.js134
-rw-r--r--netwerk/cookie/test/browser/browser_sameSiteConsole.js133
-rw-r--r--netwerk/cookie/test/browser/browser_serviceWorker.js45
-rw-r--r--netwerk/cookie/test/browser/browser_sharedWorker.js18
-rw-r--r--netwerk/cookie/test/browser/browser_storage.js43
-rw-r--r--netwerk/cookie/test/browser/file_empty.html2
-rw-r--r--netwerk/cookie/test/browser/file_empty.js1
-rw-r--r--netwerk/cookie/test/browser/head.js201
-rw-r--r--netwerk/cookie/test/browser/oversize.sjs17
-rw-r--r--netwerk/cookie/test/browser/partitioned.sjs32
-rw-r--r--netwerk/cookie/test/browser/sameSite.sjs7
-rw-r--r--netwerk/cookie/test/browser/server.sjs9
-rw-r--r--netwerk/cookie/test/mochitest/cookie.sjs166
-rw-r--r--netwerk/cookie/test/mochitest/cookiesHelper.js63
-rw-r--r--netwerk/cookie/test/mochitest/empty.html1
-rw-r--r--netwerk/cookie/test/mochitest/mochitest.toml27
-rw-r--r--netwerk/cookie/test/mochitest/test_document_cookie.html20
-rw-r--r--netwerk/cookie/test/mochitest/test_document_cookie_notification.html32
-rw-r--r--netwerk/cookie/test/mochitest/test_fetch.html20
-rw-r--r--netwerk/cookie/test/mochitest/test_image.html24
-rw-r--r--netwerk/cookie/test/mochitest/test_metaTag.html24
-rw-r--r--netwerk/cookie/test/mochitest/test_script.html25
-rw-r--r--netwerk/cookie/test/mochitest/test_sharedWorker.html50
-rw-r--r--netwerk/cookie/test/mochitest/test_worker.html31
-rw-r--r--netwerk/cookie/test/mochitest/test_xhr.html25
-rw-r--r--netwerk/cookie/test/mochitest/test_xmlDocument.html37
-rw-r--r--netwerk/cookie/test/unit/test_baseDomain_publicsuffix.js105
-rw-r--r--netwerk/cookie/test/unit/test_bug1155169.js96
-rw-r--r--netwerk/cookie/test/unit/test_bug1321912.js99
-rw-r--r--netwerk/cookie/test/unit/test_bug643051.js43
-rw-r--r--netwerk/cookie/test/unit/test_eviction.js199
-rw-r--r--netwerk/cookie/test/unit/test_getCookieSince.js72
-rw-r--r--netwerk/cookie/test/unit/test_migrateCookieLifetimePref.js65
-rw-r--r--netwerk/cookie/test/unit/test_parser_0001.js32
-rw-r--r--netwerk/cookie/test/unit/test_parser_0019.js32
-rw-r--r--netwerk/cookie/test/unit/test_rawSameSite.js125
-rw-r--r--netwerk/cookie/test/unit/test_schemeMap.js216
-rw-r--r--netwerk/cookie/test/unit/test_timestamp_fixup.js130
-rw-r--r--netwerk/cookie/test/unit/xpcshell.toml26
82 files changed, 14565 insertions, 0 deletions
diff --git a/netwerk/cookie/Cookie.cpp b/netwerk/cookie/Cookie.cpp
new file mode 100644
index 0000000000..e9de561f88
--- /dev/null
+++ b/netwerk/cookie/Cookie.cpp
@@ -0,0 +1,289 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "Cookie.h"
+#include "CookieStorage.h"
+#include "mozilla/Encoding.h"
+#include "mozilla/dom/ToJSValue.h"
+#include "mozilla/glean/GleanMetrics.h"
+#include "mozilla/StaticPrefs_network.h"
+#include "nsIURLParser.h"
+#include "nsURLHelper.h"
+#include <cstdlib>
+
+namespace mozilla {
+namespace net {
+
+/******************************************************************************
+ * Cookie:
+ * creation helper
+ ******************************************************************************/
+
+// This is a counter that keeps track of the last used creation time, each time
+// we create a new Cookie. This is nominally the time (in microseconds) the
+// cookie was created, but is guaranteed to be monotonically increasing for
+// cookies added at runtime after the database has been read in. This is
+// necessary to enforce ordering among cookies whose creation times would
+// otherwise overlap, since it's possible two cookies may be created at the
+// same time, or that the system clock isn't monotonic.
+static int64_t gLastCreationTime;
+
+int64_t Cookie::GenerateUniqueCreationTime(int64_t aCreationTime) {
+ // Check if the creation time given to us is greater than the running maximum
+ // (it should always be monotonically increasing).
+ if (aCreationTime > gLastCreationTime) {
+ gLastCreationTime = aCreationTime;
+ return aCreationTime;
+ }
+
+ // Make up our own.
+ return ++gLastCreationTime;
+}
+
+already_AddRefed<Cookie> Cookie::Create(
+ const CookieStruct& aCookieData,
+ const OriginAttributes& aOriginAttributes) {
+ RefPtr<Cookie> cookie =
+ Cookie::FromCookieStruct(aCookieData, aOriginAttributes);
+
+ // If the creationTime given to us is higher than the running maximum,
+ // update our maximum.
+ if (cookie->mData.creationTime() > gLastCreationTime) {
+ gLastCreationTime = cookie->mData.creationTime();
+ }
+
+ return cookie.forget();
+}
+
+already_AddRefed<Cookie> Cookie::FromCookieStruct(
+ const CookieStruct& aCookieData,
+ const OriginAttributes& aOriginAttributes) {
+ RefPtr<Cookie> cookie = new Cookie(aCookieData, aOriginAttributes);
+
+ // Ensure mValue contains a valid UTF-8 sequence. Otherwise XPConnect will
+ // truncate the string after the first invalid octet.
+ UTF_8_ENCODING->DecodeWithoutBOMHandling(aCookieData.value(),
+ cookie->mData.value());
+
+ // If sameSite/rawSameSite values aren't sensible reset to Default
+ // cf. 5.4.7 in draft-ietf-httpbis-rfc6265bis-09
+ if (!Cookie::ValidateSameSite(cookie->mData)) {
+ cookie->mData.sameSite() = nsICookie::SAMESITE_LAX;
+ cookie->mData.rawSameSite() = nsICookie::SAMESITE_NONE;
+ }
+
+ return cookie.forget();
+}
+
+already_AddRefed<Cookie> Cookie::CreateValidated(
+ const CookieStruct& aCookieData,
+ const OriginAttributes& aOriginAttributes) {
+ if (!StaticPrefs::network_cookie_fixup_on_db_load()) {
+ return Cookie::Create(aCookieData, aOriginAttributes);
+ }
+
+ RefPtr<Cookie> cookie =
+ Cookie::FromCookieStruct(aCookieData, aOriginAttributes);
+
+ int64_t currentTimeInUsec = PR_Now();
+ // Assert that the last creation time is not higher than the current time.
+ // The 10000 wiggle room accounts for the fact that calling
+ // GenerateUniqueCreationTime might go over the value of PR_Now(), but we'd
+ // most likely not add 10000 cookies in a row.
+ MOZ_ASSERT(gLastCreationTime < currentTimeInUsec + 10000,
+ "Last creation time must not be higher than NOW");
+
+ // If the creationTime given to us is higher than the current time then
+ // update the creation time to now.
+ if (cookie->mData.creationTime() > currentTimeInUsec) {
+ uint64_t diffInSeconds =
+ (cookie->mData.creationTime() - currentTimeInUsec) / PR_USEC_PER_SEC;
+ mozilla::glean::networking::cookie_creation_fixup_diff.AccumulateSamples(
+ {diffInSeconds});
+ glean::networking::cookie_timestamp_fixed_count.Get("creationTime"_ns)
+ .Add(1);
+
+ cookie->mData.creationTime() =
+ GenerateUniqueCreationTime(currentTimeInUsec);
+ }
+
+ if (cookie->mData.lastAccessed() > currentTimeInUsec) {
+ uint64_t diffInSeconds =
+ (cookie->mData.lastAccessed() - currentTimeInUsec) / PR_USEC_PER_SEC;
+ mozilla::glean::networking::cookie_access_fixup_diff.AccumulateSamples(
+ {diffInSeconds});
+ glean::networking::cookie_timestamp_fixed_count.Get("lastAccessed"_ns)
+ .Add(1);
+
+ cookie->mData.lastAccessed() = currentTimeInUsec;
+ }
+
+ return cookie.forget();
+}
+
+size_t Cookie::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(this) +
+ mData.name().SizeOfExcludingThisIfUnshared(MallocSizeOf) +
+ mData.value().SizeOfExcludingThisIfUnshared(MallocSizeOf) +
+ mData.host().SizeOfExcludingThisIfUnshared(MallocSizeOf) +
+ mData.path().SizeOfExcludingThisIfUnshared(MallocSizeOf) +
+ mFilePathCache.SizeOfExcludingThisIfUnshared(MallocSizeOf);
+}
+
+bool Cookie::IsStale() const {
+ int64_t currentTimeInUsec = PR_Now();
+
+ return currentTimeInUsec - LastAccessed() >
+ StaticPrefs::network_cookie_staleThreshold() * PR_USEC_PER_SEC;
+}
+
+/******************************************************************************
+ * Cookie:
+ * xpcom impl
+ ******************************************************************************/
+
+// xpcom getters
+NS_IMETHODIMP Cookie::GetName(nsACString& aName) {
+ aName = Name();
+ return NS_OK;
+}
+NS_IMETHODIMP Cookie::GetValue(nsACString& aValue) {
+ aValue = Value();
+ return NS_OK;
+}
+NS_IMETHODIMP Cookie::GetHost(nsACString& aHost) {
+ aHost = Host();
+ return NS_OK;
+}
+NS_IMETHODIMP Cookie::GetRawHost(nsACString& aHost) {
+ aHost = RawHost();
+ return NS_OK;
+}
+NS_IMETHODIMP Cookie::GetPath(nsACString& aPath) {
+ aPath = Path();
+ return NS_OK;
+}
+NS_IMETHODIMP Cookie::GetExpiry(int64_t* aExpiry) {
+ *aExpiry = Expiry();
+ return NS_OK;
+}
+NS_IMETHODIMP Cookie::GetIsSession(bool* aIsSession) {
+ *aIsSession = IsSession();
+ return NS_OK;
+}
+NS_IMETHODIMP Cookie::GetIsDomain(bool* aIsDomain) {
+ *aIsDomain = IsDomain();
+ return NS_OK;
+}
+NS_IMETHODIMP Cookie::GetIsSecure(bool* aIsSecure) {
+ *aIsSecure = IsSecure();
+ return NS_OK;
+}
+NS_IMETHODIMP Cookie::GetIsHttpOnly(bool* aHttpOnly) {
+ *aHttpOnly = IsHttpOnly();
+ return NS_OK;
+}
+NS_IMETHODIMP Cookie::GetIsPartitioned(bool* aPartitioned) {
+ *aPartitioned = IsPartitioned();
+ return NS_OK;
+}
+NS_IMETHODIMP Cookie::GetCreationTime(int64_t* aCreation) {
+ *aCreation = CreationTime();
+ return NS_OK;
+}
+NS_IMETHODIMP Cookie::GetLastAccessed(int64_t* aTime) {
+ *aTime = LastAccessed();
+ return NS_OK;
+}
+NS_IMETHODIMP Cookie::GetSameSite(int32_t* aSameSite) {
+ if (StaticPrefs::network_cookie_sameSite_laxByDefault()) {
+ *aSameSite = SameSite();
+ } else {
+ *aSameSite = RawSameSite();
+ }
+ return NS_OK;
+}
+NS_IMETHODIMP Cookie::GetSchemeMap(nsICookie::schemeType* aSchemeMap) {
+ *aSchemeMap = static_cast<nsICookie::schemeType>(SchemeMap());
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+Cookie::GetOriginAttributes(JSContext* aCx, JS::MutableHandle<JS::Value> aVal) {
+ if (NS_WARN_IF(!ToJSValue(aCx, mOriginAttributes, aVal))) {
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+const OriginAttributes& Cookie::OriginAttributesNative() {
+ return mOriginAttributes;
+}
+
+const Cookie& Cookie::AsCookie() { return *this; }
+
+const nsCString& Cookie::GetFilePath() {
+ MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread());
+
+ if (Path().IsEmpty()) {
+ // If we don't have a path, just return the (empty) file path cache.
+ return mFilePathCache;
+ }
+ if (!mFilePathCache.IsEmpty()) {
+ // If we've computed the answer before, just return it.
+ return mFilePathCache;
+ }
+
+ nsIURLParser* parser = net_GetStdURLParser();
+ NS_ENSURE_TRUE(parser, mFilePathCache);
+
+ int32_t pathLen = Path().Length();
+ int32_t filepathLen = 0;
+ uint32_t filepathPos = 0;
+
+ nsresult rv = parser->ParsePath(PromiseFlatCString(Path()).get(), pathLen,
+ &filepathPos, &filepathLen, nullptr,
+ nullptr, // don't care about query
+ nullptr, nullptr); // don't care about ref
+ NS_ENSURE_SUCCESS(rv, mFilePathCache);
+
+ mFilePathCache = Substring(Path(), filepathPos, filepathLen);
+
+ return mFilePathCache;
+}
+
+// compatibility method, for use with the legacy nsICookie interface.
+// here, expires == 0 denotes a session cookie.
+NS_IMETHODIMP
+Cookie::GetExpires(uint64_t* aExpires) {
+ if (IsSession()) {
+ *aExpires = 0;
+ } else {
+ *aExpires = Expiry() > 0 ? Expiry() : 1;
+ }
+ return NS_OK;
+}
+
+// static
+bool Cookie::ValidateSameSite(const CookieStruct& aCookieData) {
+ // For proper migration towards a laxByDefault world,
+ // sameSite is initialized to LAX even though the server
+ // has never sent it.
+ if (aCookieData.rawSameSite() == aCookieData.sameSite()) {
+ return aCookieData.rawSameSite() >= nsICookie::SAMESITE_NONE &&
+ aCookieData.rawSameSite() <= nsICookie::SAMESITE_STRICT;
+ }
+ return aCookieData.rawSameSite() == nsICookie::SAMESITE_NONE &&
+ aCookieData.sameSite() == nsICookie::SAMESITE_LAX;
+}
+
+already_AddRefed<Cookie> Cookie::Clone() const {
+ return Create(mData, OriginAttributesRef());
+}
+
+NS_IMPL_ISUPPORTS(Cookie, nsICookie)
+
+} // namespace net
+} // namespace mozilla
diff --git a/netwerk/cookie/Cookie.h b/netwerk/cookie/Cookie.h
new file mode 100644
index 0000000000..5fa3479268
--- /dev/null
+++ b/netwerk/cookie/Cookie.h
@@ -0,0 +1,165 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_net_Cookie_h
+#define mozilla_net_Cookie_h
+
+#include "nsICookie.h"
+#include "nsIMemoryReporter.h"
+#include "nsString.h"
+
+#include "mozilla/MemoryReporting.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/net/NeckoChannelParams.h"
+#include "nsIMemoryReporter.h"
+
+using mozilla::OriginAttributes;
+
+namespace mozilla {
+namespace net {
+
+/**
+ * The Cookie class is the main cookie storage medium for use within cookie
+ * code.
+ */
+
+/******************************************************************************
+ * Cookie:
+ * implementation
+ ******************************************************************************/
+
+class Cookie final : public nsICookie {
+ MOZ_DEFINE_MALLOC_SIZE_OF(MallocSizeOf)
+
+ public:
+ // nsISupports
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSICOOKIE
+
+ private:
+ // for internal use only. see Cookie::Create().
+ Cookie(const CookieStruct& aCookieData,
+ const OriginAttributes& aOriginAttributes)
+ : mData(aCookieData), mOriginAttributes(aOriginAttributes) {}
+
+ static already_AddRefed<Cookie> FromCookieStruct(
+ const CookieStruct& aCookieData,
+ const OriginAttributes& aOriginAttributes);
+
+ public:
+ // Returns false if rawSameSite has an invalid value, compared to sameSite.
+ static bool ValidateSameSite(const CookieStruct& aCookieData);
+
+ // Generate a unique and monotonically increasing creation time. See comment
+ // in Cookie.cpp.
+ static int64_t GenerateUniqueCreationTime(int64_t aCreationTime);
+
+ // public helper to create an Cookie object.
+ static already_AddRefed<Cookie> Create(
+ const CookieStruct& aCookieData,
+ const OriginAttributes& aOriginAttributes);
+
+ // Same as Cookie::Create but fixes the lastAccessed and creationDates
+ // if they are set in the future.
+ // Should only get called from CookiePersistentStorage::InitDBConn
+ static already_AddRefed<Cookie> CreateValidated(
+ const CookieStruct& aCookieData,
+ const OriginAttributes& aOriginAttributes);
+
+ size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
+
+ // fast (inline, non-xpcom) getters
+ inline const nsCString& Name() const { return mData.name(); }
+ inline const nsCString& Value() const { return mData.value(); }
+ inline const nsCString& Host() const { return mData.host(); }
+ inline nsDependentCSubstring RawHost() const {
+ return nsDependentCSubstring(mData.host(), IsDomain() ? 1 : 0);
+ }
+ inline const nsCString& Path() const { return mData.path(); }
+ const nsCString& GetFilePath();
+ inline int64_t Expiry() const { return mData.expiry(); } // in seconds
+ inline int64_t LastAccessed() const {
+ return mData.lastAccessed();
+ } // in microseconds
+ inline int64_t CreationTime() const {
+ return mData.creationTime();
+ } // in microseconds
+ inline bool IsSession() const { return mData.isSession(); }
+ inline bool IsDomain() const { return *mData.host().get() == '.'; }
+ inline bool IsSecure() const { return mData.isSecure(); }
+ inline bool IsHttpOnly() const { return mData.isHttpOnly(); }
+ inline bool IsPartitioned() const {
+ return !mOriginAttributes.mPartitionKey.IsEmpty();
+ }
+ inline bool RawIsPartitioned() const { return mData.isPartitioned(); }
+ inline const OriginAttributes& OriginAttributesRef() const {
+ return mOriginAttributes;
+ }
+ inline int32_t SameSite() const { return mData.sameSite(); }
+ inline int32_t RawSameSite() const { return mData.rawSameSite(); }
+ inline bool IsDefaultSameSite() const {
+ return SameSite() == nsICookie::SAMESITE_LAX &&
+ RawSameSite() == nsICookie::SAMESITE_NONE;
+ }
+ inline uint8_t SchemeMap() const { return mData.schemeMap(); }
+
+ // setters
+ inline void SetExpiry(int64_t aExpiry) { mData.expiry() = aExpiry; }
+ inline void SetLastAccessed(int64_t aTime) { mData.lastAccessed() = aTime; }
+ inline void SetIsSession(bool aIsSession) { mData.isSession() = aIsSession; }
+ inline bool SetIsHttpOnly(bool aIsHttpOnly) {
+ return mData.isHttpOnly() = aIsHttpOnly;
+ }
+ // Set the creation time manually, overriding the monotonicity checks in
+ // Create(). Use with caution!
+ inline void SetCreationTime(int64_t aTime) { mData.creationTime() = aTime; }
+ inline void SetSchemeMap(uint8_t aSchemeMap) {
+ mData.schemeMap() = aSchemeMap;
+ }
+ inline void SetHost(const nsACString& aHost) { mData.host() = aHost; }
+
+ bool IsStale() const;
+
+ const CookieStruct& ToIPC() const { return mData; }
+
+ already_AddRefed<Cookie> Clone() const;
+
+ protected:
+ virtual ~Cookie() = default;
+
+ private:
+ // member variables
+ //
+ // Please update SizeOfIncludingThis if this strategy changes.
+ CookieStruct mData;
+ OriginAttributes mOriginAttributes;
+ nsCString mFilePathCache;
+};
+
+// Comparator class for sorting cookies before sending to a server.
+class CompareCookiesForSending {
+ public:
+ bool Equals(const Cookie* aCookie1, const Cookie* aCookie2) const {
+ return aCookie1->CreationTime() == aCookie2->CreationTime() &&
+ aCookie2->Path().Length() == aCookie1->Path().Length();
+ }
+
+ bool LessThan(const Cookie* aCookie1, const Cookie* aCookie2) const {
+ // compare by cookie path length in accordance with RFC2109
+ int32_t result = aCookie2->Path().Length() - aCookie1->Path().Length();
+ if (result != 0) return result < 0;
+
+ // when path lengths match, older cookies should be listed first. this is
+ // required for backwards compatibility since some websites erroneously
+ // depend on receiving cookies in the order in which they were sent to the
+ // browser! see bug 236772.
+ return aCookie1->CreationTime() < aCookie2->CreationTime();
+ }
+};
+
+} // namespace net
+} // namespace mozilla
+
+#endif // mozilla_net_Cookie_h
diff --git a/netwerk/cookie/CookieCommons.cpp b/netwerk/cookie/CookieCommons.cpp
new file mode 100644
index 0000000000..3708f23daa
--- /dev/null
+++ b/netwerk/cookie/CookieCommons.cpp
@@ -0,0 +1,749 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "Cookie.h"
+#include "CookieCommons.h"
+#include "CookieLogging.h"
+#include "CookieService.h"
+#include "mozilla/ConsoleReportCollector.h"
+#include "mozilla/ContentBlockingNotifier.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/StaticPrefs_network.h"
+#include "mozilla/StorageAccess.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/nsMixedContentBlocker.h"
+#include "mozilla/net/CookieJarSettings.h"
+#include "mozIThirdPartyUtil.h"
+#include "nsContentUtils.h"
+#include "nsICookiePermission.h"
+#include "nsICookieService.h"
+#include "nsIEffectiveTLDService.h"
+#include "nsIRedirectHistoryEntry.h"
+#include "nsIWebProgressListener.h"
+#include "nsNetUtil.h"
+#include "nsScriptSecurityManager.h"
+#include "ThirdPartyUtil.h"
+
+namespace mozilla {
+
+using dom::Document;
+
+namespace net {
+
+// static
+bool CookieCommons::DomainMatches(Cookie* aCookie, const nsACString& aHost) {
+ // first, check for an exact host or domain cookie match, e.g. "google.com"
+ // or ".google.com"; second a subdomain match, e.g.
+ // host = "mail.google.com", cookie domain = ".google.com".
+ return aCookie->RawHost() == aHost ||
+ (aCookie->IsDomain() && StringEndsWith(aHost, aCookie->Host()));
+}
+
+// static
+bool CookieCommons::PathMatches(Cookie* aCookie, const nsACString& aPath) {
+ nsCString cookiePath(aCookie->GetFilePath());
+
+ // if our cookie path is empty we can't really perform our prefix check, and
+ // also we can't check the last character of the cookie path, so we would
+ // never return a successful match.
+ if (cookiePath.IsEmpty()) {
+ return false;
+ }
+
+ // if the cookie path and the request path are identical, they match.
+ if (cookiePath.Equals(aPath)) {
+ return true;
+ }
+
+ // if the cookie path is a prefix of the request path, and the last character
+ // of the cookie path is %x2F ("/"), they match.
+ bool isPrefix = StringBeginsWith(aPath, cookiePath);
+ if (isPrefix && cookiePath.Last() == '/') {
+ return true;
+ }
+
+ // if the cookie path is a prefix of the request path, and the first character
+ // of the request path that is not included in the cookie path is a %x2F ("/")
+ // character, they match.
+ uint32_t cookiePathLen = cookiePath.Length();
+ return isPrefix && aPath[cookiePathLen] == '/';
+}
+
+// Get the base domain for aHostURI; e.g. for "www.bbc.co.uk", this would be
+// "bbc.co.uk". Only properly-formed URI's are tolerated, though a trailing
+// dot may be present. If aHostURI is an IP address, an alias such as
+// 'localhost', an eTLD such as 'co.uk', or the empty string, aBaseDomain will
+// be the exact host, and aRequireHostMatch will be true to indicate that
+// substring matches should not be performed.
+nsresult CookieCommons::GetBaseDomain(nsIEffectiveTLDService* aTLDService,
+ nsIURI* aHostURI, nsACString& aBaseDomain,
+ bool& aRequireHostMatch) {
+ // get the base domain. this will fail if the host contains a leading dot,
+ // more than one trailing dot, or is otherwise malformed.
+ nsresult rv = aTLDService->GetBaseDomain(aHostURI, 0, aBaseDomain);
+ aRequireHostMatch = rv == NS_ERROR_HOST_IS_IP_ADDRESS ||
+ rv == NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS;
+ if (aRequireHostMatch) {
+ // aHostURI is either an IP address, an alias such as 'localhost', an eTLD
+ // such as 'co.uk', or the empty string. use the host as a key in such
+ // cases.
+ rv = nsContentUtils::GetHostOrIPv6WithBrackets(aHostURI, aBaseDomain);
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // aHost (and thus aBaseDomain) may be the string '.'. If so, fail.
+ if (aBaseDomain.Length() == 1 && aBaseDomain.Last() == '.') {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ // block any URIs without a host that aren't file:// URIs.
+ if (aBaseDomain.IsEmpty() && !aHostURI->SchemeIs("file")) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ return NS_OK;
+}
+
+nsresult CookieCommons::GetBaseDomain(nsIPrincipal* aPrincipal,
+ nsACString& aBaseDomain) {
+ MOZ_ASSERT(aPrincipal);
+
+ // for historical reasons we use ascii host for file:// URLs.
+ if (aPrincipal->SchemeIs("file")) {
+ return nsContentUtils::GetHostOrIPv6WithBrackets(aPrincipal, aBaseDomain);
+ }
+
+ nsresult rv = aPrincipal->GetBaseDomain(aBaseDomain);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ nsContentUtils::MaybeFixIPv6Host(aBaseDomain);
+ return NS_OK;
+}
+
+// Get the base domain for aHost; e.g. for "www.bbc.co.uk", this would be
+// "bbc.co.uk". This is done differently than GetBaseDomain(mTLDService, ): it
+// is assumed that aHost is already normalized, and it may contain a leading dot
+// (indicating that it represents a domain). A trailing dot may be present.
+// If aHost is an IP address, an alias such as 'localhost', an eTLD such as
+// 'co.uk', or the empty string, aBaseDomain will be the exact host, and a
+// leading dot will be treated as an error.
+nsresult CookieCommons::GetBaseDomainFromHost(
+ nsIEffectiveTLDService* aTLDService, const nsACString& aHost,
+ nsCString& aBaseDomain) {
+ // aHost must not be the string '.'.
+ if (aHost.Length() == 1 && aHost.Last() == '.') {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ // aHost may contain a leading dot; if so, strip it now.
+ bool domain = !aHost.IsEmpty() && aHost.First() == '.';
+
+ // get the base domain. this will fail if the host contains a leading dot,
+ // more than one trailing dot, or is otherwise malformed.
+ nsresult rv = aTLDService->GetBaseDomainFromHost(Substring(aHost, domain), 0,
+ aBaseDomain);
+ if (rv == NS_ERROR_HOST_IS_IP_ADDRESS ||
+ rv == NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS) {
+ // aHost is either an IP address, an alias such as 'localhost', an eTLD
+ // such as 'co.uk', or the empty string. use the host as a key in such
+ // cases; however, we reject any such hosts with a leading dot, since it
+ // doesn't make sense for them to be domain cookies.
+ if (domain) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ aBaseDomain = aHost;
+ return NS_OK;
+ }
+ return rv;
+}
+
+namespace {
+
+void NotifyRejectionToObservers(nsIURI* aHostURI, CookieOperation aOperation) {
+ if (aOperation == OPERATION_WRITE) {
+ nsCOMPtr<nsIObserverService> os = services::GetObserverService();
+ if (os) {
+ os->NotifyObservers(aHostURI, "cookie-rejected", nullptr);
+ }
+ } else {
+ MOZ_ASSERT(aOperation == OPERATION_READ);
+ }
+}
+
+} // namespace
+
+// Notify observers that a cookie was rejected due to the users' prefs.
+void CookieCommons::NotifyRejected(nsIURI* aHostURI, nsIChannel* aChannel,
+ uint32_t aRejectedReason,
+ CookieOperation aOperation) {
+ NotifyRejectionToObservers(aHostURI, aOperation);
+
+ ContentBlockingNotifier::OnDecision(
+ aChannel, ContentBlockingNotifier::BlockingDecision::eBlock,
+ aRejectedReason);
+}
+
+bool CookieCommons::CheckPathSize(const CookieStruct& aCookieData) {
+ return aCookieData.path().Length() <= kMaxBytesPerPath;
+}
+
+bool CookieCommons::CheckNameAndValueSize(const CookieStruct& aCookieData) {
+ // reject cookie if it's over the size limit, per RFC2109
+ return (aCookieData.name().Length() + aCookieData.value().Length()) <=
+ kMaxBytesPerCookie;
+}
+
+bool CookieCommons::CheckName(const CookieStruct& aCookieData) {
+ const char illegalNameCharacters[] = {
+ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C,
+ 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
+ 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x3B, 0x3D, 0x7F, 0x00};
+
+ const auto* start = aCookieData.name().BeginReading();
+ const auto* end = aCookieData.name().EndReading();
+
+ auto charFilter = [&](unsigned char c) {
+ if (StaticPrefs::network_cookie_blockUnicode() && c >= 0x80) {
+ return true;
+ }
+ return std::find(std::begin(illegalNameCharacters),
+ std::end(illegalNameCharacters),
+ c) != std::end(illegalNameCharacters);
+ };
+
+ return std::find_if(start, end, charFilter) == end;
+}
+
+bool CookieCommons::CheckValue(const CookieStruct& aCookieData) {
+ // reject cookie if value contains an RFC 6265 disallowed character - see
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1191423
+ // NOTE: this is not the full set of characters disallowed by 6265 - notably
+ // 0x09, 0x20, 0x22, 0x2C, and 0x5C are missing from this list.
+ const char illegalCharacters[] = {
+ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x0A, 0x0B, 0x0C,
+ 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
+ 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x3B, 0x7F, 0x00};
+
+ const auto* start = aCookieData.value().BeginReading();
+ const auto* end = aCookieData.value().EndReading();
+
+ auto charFilter = [&](unsigned char c) {
+ if (StaticPrefs::network_cookie_blockUnicode() && c >= 0x80) {
+ return true;
+ }
+ return std::find(std::begin(illegalCharacters), std::end(illegalCharacters),
+ c) != std::end(illegalCharacters);
+ };
+
+ return std::find_if(start, end, charFilter) == end;
+}
+
+// static
+bool CookieCommons::CheckCookiePermission(nsIChannel* aChannel,
+ CookieStruct& aCookieData) {
+ if (!aChannel) {
+ // No channel, let's assume this is a system-principal request.
+ return true;
+ }
+
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+ nsCOMPtr<nsICookieJarSettings> cookieJarSettings;
+ nsresult rv =
+ loadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return true;
+ }
+
+ nsIScriptSecurityManager* ssm =
+ nsScriptSecurityManager::GetScriptSecurityManager();
+ MOZ_ASSERT(ssm);
+
+ nsCOMPtr<nsIPrincipal> channelPrincipal;
+ rv = ssm->GetChannelURIPrincipal(aChannel, getter_AddRefs(channelPrincipal));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ return CheckCookiePermission(channelPrincipal, cookieJarSettings,
+ aCookieData);
+}
+
+// static
+bool CookieCommons::CheckCookiePermission(
+ nsIPrincipal* aPrincipal, nsICookieJarSettings* aCookieJarSettings,
+ CookieStruct& aCookieData) {
+ MOZ_ASSERT(aPrincipal);
+ MOZ_ASSERT(aCookieJarSettings);
+
+ if (!aPrincipal->GetIsContentPrincipal()) {
+ return true;
+ }
+
+ uint32_t cookiePermission = nsICookiePermission::ACCESS_DEFAULT;
+ nsresult rv =
+ aCookieJarSettings->CookiePermission(aPrincipal, &cookiePermission);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return true;
+ }
+
+ if (cookiePermission == nsICookiePermission::ACCESS_ALLOW) {
+ return true;
+ }
+
+ if (cookiePermission == nsICookiePermission::ACCESS_SESSION) {
+ aCookieData.isSession() = true;
+ return true;
+ }
+
+ if (cookiePermission == nsICookiePermission::ACCESS_DENY) {
+ return false;
+ }
+
+ return true;
+}
+
+namespace {
+
+CookieStatus CookieStatusForWindow(nsPIDOMWindowInner* aWindow,
+ nsIURI* aDocumentURI) {
+ MOZ_ASSERT(aWindow);
+ MOZ_ASSERT(aDocumentURI);
+
+ ThirdPartyUtil* thirdPartyUtil = ThirdPartyUtil::GetInstance();
+ if (thirdPartyUtil) {
+ bool isThirdParty = true;
+
+ nsresult rv = thirdPartyUtil->IsThirdPartyWindow(
+ aWindow->GetOuterWindow(), aDocumentURI, &isThirdParty);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Third-party window check failed.");
+
+ if (NS_SUCCEEDED(rv) && !isThirdParty) {
+ return STATUS_ACCEPTED;
+ }
+ }
+
+ if (StaticPrefs::network_cookie_thirdparty_sessionOnly()) {
+ return STATUS_ACCEPT_SESSION;
+ }
+
+ if (StaticPrefs::network_cookie_thirdparty_nonsecureSessionOnly() &&
+ !nsMixedContentBlocker::IsPotentiallyTrustworthyOrigin(aDocumentURI)) {
+ return STATUS_ACCEPT_SESSION;
+ }
+
+ return STATUS_ACCEPTED;
+}
+
+} // namespace
+
+// static
+already_AddRefed<Cookie> CookieCommons::CreateCookieFromDocument(
+ Document* aDocument, const nsACString& aCookieString,
+ int64_t currentTimeInUsec, nsIEffectiveTLDService* aTLDService,
+ mozIThirdPartyUtil* aThirdPartyUtil,
+ std::function<bool(const nsACString&, const OriginAttributes&)>&&
+ aHasExistingCookiesLambda,
+ nsIURI** aDocumentURI, nsACString& aBaseDomain, OriginAttributes& aAttrs) {
+ nsCOMPtr<nsIPrincipal> storagePrincipal =
+ aDocument->EffectiveCookiePrincipal();
+ MOZ_ASSERT(storagePrincipal);
+
+ nsCOMPtr<nsIURI> principalURI;
+ auto* basePrincipal = BasePrincipal::Cast(aDocument->NodePrincipal());
+ basePrincipal->GetURI(getter_AddRefs(principalURI));
+ if (NS_WARN_IF(!principalURI)) {
+ // Document's principal is not a content or null (may be system), so
+ // can't set cookies
+ return nullptr;
+ }
+
+ if (!CookieCommons::IsSchemeSupported(principalURI)) {
+ return nullptr;
+ }
+
+ nsAutoCString baseDomain;
+ bool requireHostMatch = false;
+ nsresult rv = CookieCommons::GetBaseDomain(aTLDService, principalURI,
+ baseDomain, requireHostMatch);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nullptr;
+ }
+
+ nsPIDOMWindowInner* innerWindow = aDocument->GetInnerWindow();
+ if (NS_WARN_IF(!innerWindow)) {
+ return nullptr;
+ }
+
+ // Check if limit-foreign is required.
+ uint32_t dummyRejectedReason = 0;
+ if (aDocument->CookieJarSettings()->GetLimitForeignContexts() &&
+ !aHasExistingCookiesLambda(baseDomain,
+ storagePrincipal->OriginAttributesRef()) &&
+ !ShouldAllowAccessFor(innerWindow, principalURI, &dummyRejectedReason)) {
+ return nullptr;
+ }
+
+ bool isForeignAndNotAddon = false;
+ if (!BasePrincipal::Cast(aDocument->NodePrincipal())->AddonPolicy()) {
+ rv = aThirdPartyUtil->IsThirdPartyWindow(
+ innerWindow->GetOuterWindow(), principalURI, &isForeignAndNotAddon);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ isForeignAndNotAddon = true;
+ }
+ }
+
+ bool mustBePartitioned =
+ isForeignAndNotAddon &&
+ aDocument->CookieJarSettings()->GetCookieBehavior() ==
+ nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN &&
+ !aDocument->UsingStorageAccess();
+
+ // If we are here, we have been already accepted by the anti-tracking.
+ // We just need to check if we have to be in session-only mode.
+ CookieStatus cookieStatus = CookieStatusForWindow(innerWindow, principalURI);
+ MOZ_ASSERT(cookieStatus == STATUS_ACCEPTED ||
+ cookieStatus == STATUS_ACCEPT_SESSION);
+
+ // Console report takes care of the correct reporting at the exit of this
+ // method.
+ RefPtr<ConsoleReportCollector> crc = new ConsoleReportCollector();
+ auto scopeExit = MakeScopeExit([&] { crc->FlushConsoleReports(aDocument); });
+
+ nsCString cookieString(aCookieString);
+
+ CookieStruct cookieData;
+ MOZ_ASSERT(cookieData.creationTime() == 0, "Must be initialized to 0");
+ bool canSetCookie = false;
+ CookieService::CanSetCookie(principalURI, baseDomain, cookieData,
+ requireHostMatch, cookieStatus, cookieString,
+ false, isForeignAndNotAddon, mustBePartitioned,
+ crc, canSetCookie);
+
+ if (!canSetCookie) {
+ return nullptr;
+ }
+
+ // check permissions from site permission list.
+ if (!CookieCommons::CheckCookiePermission(aDocument->NodePrincipal(),
+ aDocument->CookieJarSettings(),
+ cookieData)) {
+ NotifyRejectionToObservers(principalURI, OPERATION_WRITE);
+ ContentBlockingNotifier::OnDecision(
+ innerWindow, ContentBlockingNotifier::BlockingDecision::eBlock,
+ nsIWebProgressListener::STATE_COOKIES_BLOCKED_BY_PERMISSION);
+ return nullptr;
+ }
+
+ RefPtr<Cookie> cookie =
+ Cookie::Create(cookieData, storagePrincipal->OriginAttributesRef());
+ MOZ_ASSERT(cookie);
+
+ cookie->SetLastAccessed(currentTimeInUsec);
+ cookie->SetCreationTime(
+ Cookie::GenerateUniqueCreationTime(currentTimeInUsec));
+
+ aBaseDomain = baseDomain;
+ aAttrs = storagePrincipal->OriginAttributesRef();
+ principalURI.forget(aDocumentURI);
+
+ return cookie.forget();
+}
+
+// static
+already_AddRefed<nsICookieJarSettings> CookieCommons::GetCookieJarSettings(
+ nsIChannel* aChannel) {
+ nsCOMPtr<nsICookieJarSettings> cookieJarSettings;
+ bool shouldResistFingerprinting = nsContentUtils::ShouldResistFingerprinting(
+ aChannel, RFPTarget::IsAlwaysEnabledForPrecompute);
+ if (aChannel) {
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+ nsresult rv =
+ loadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ cookieJarSettings =
+ CookieJarSettings::GetBlockingAll(shouldResistFingerprinting);
+ }
+ } else {
+ cookieJarSettings = CookieJarSettings::Create(CookieJarSettings::eRegular,
+ shouldResistFingerprinting);
+ }
+
+ MOZ_ASSERT(cookieJarSettings);
+ return cookieJarSettings.forget();
+}
+
+// static
+bool CookieCommons::ShouldIncludeCrossSiteCookieForDocument(
+ Cookie* aCookie, dom::Document* aDocument) {
+ MOZ_ASSERT(aCookie);
+ MOZ_ASSERT(aDocument);
+
+ int32_t sameSiteAttr = 0;
+ aCookie->GetSameSite(&sameSiteAttr);
+
+ if (aDocument->CookieJarSettings()->GetPartitionForeign() &&
+ StaticPrefs::network_cookie_cookieBehavior_optInPartitioning()) {
+ return false;
+ }
+
+ return sameSiteAttr == nsICookie::SAMESITE_NONE;
+}
+
+bool CookieCommons::IsSafeTopLevelNav(nsIChannel* aChannel) {
+ if (!aChannel) {
+ return false;
+ }
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+ nsCOMPtr<nsIInterceptionInfo> interceptionInfo = loadInfo->InterceptionInfo();
+ if ((loadInfo->GetExternalContentPolicyType() !=
+ ExtContentPolicy::TYPE_DOCUMENT &&
+ loadInfo->GetExternalContentPolicyType() !=
+ ExtContentPolicy::TYPE_SAVEAS_DOWNLOAD) &&
+ !interceptionInfo) {
+ return false;
+ }
+
+ if (interceptionInfo &&
+ interceptionInfo->GetExtContentPolicyType() !=
+ ExtContentPolicy::TYPE_DOCUMENT &&
+ interceptionInfo->GetExtContentPolicyType() !=
+ ExtContentPolicy::TYPE_SAVEAS_DOWNLOAD &&
+ interceptionInfo->GetExtContentPolicyType() !=
+ ExtContentPolicy::TYPE_INVALID) {
+ return false;
+ }
+
+ return NS_IsSafeMethodNav(aChannel);
+}
+
+// This function determines if two schemes are equal in the context of
+// "Schemeful SameSite cookies".
+//
+// Two schemes are considered equal:
+// - if the "network.cookie.sameSite.schemeful" pref is set to false.
+// OR
+// - if one of the schemes is not http or https.
+// OR
+// - if both schemes are equal AND both are either http or https.
+bool IsSameSiteSchemeEqual(const nsACString& aFirstScheme,
+ const nsACString& aSecondScheme) {
+ if (!StaticPrefs::network_cookie_sameSite_schemeful()) {
+ return true;
+ }
+
+ auto isSchemeHttpOrHttps = [](const nsACString& scheme) -> bool {
+ return scheme.EqualsLiteral("http") || scheme.EqualsLiteral("https");
+ };
+
+ if (!isSchemeHttpOrHttps(aFirstScheme) ||
+ !isSchemeHttpOrHttps(aSecondScheme)) {
+ return true;
+ }
+
+ return aFirstScheme.Equals(aSecondScheme);
+}
+
+bool CookieCommons::IsSameSiteForeign(nsIChannel* aChannel, nsIURI* aHostURI,
+ bool* aHadCrossSiteRedirects) {
+ *aHadCrossSiteRedirects = false;
+
+ if (!aChannel) {
+ return false;
+ }
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+ // Do not treat loads triggered by web extensions as foreign
+ nsCOMPtr<nsIURI> channelURI;
+ NS_GetFinalChannelURI(aChannel, getter_AddRefs(channelURI));
+
+ nsCOMPtr<nsIInterceptionInfo> interceptionInfo = loadInfo->InterceptionInfo();
+
+ RefPtr<BasePrincipal> triggeringPrincipal;
+ ExtContentPolicy contentPolicyType;
+ if (interceptionInfo && interceptionInfo->TriggeringPrincipal()) {
+ triggeringPrincipal =
+ BasePrincipal::Cast(interceptionInfo->TriggeringPrincipal());
+ contentPolicyType = interceptionInfo->GetExtContentPolicyType();
+ } else {
+ triggeringPrincipal = BasePrincipal::Cast(loadInfo->TriggeringPrincipal());
+ contentPolicyType = loadInfo->GetExternalContentPolicyType();
+
+ if (triggeringPrincipal->AddonPolicy() &&
+ triggeringPrincipal->AddonAllowsLoad(channelURI)) {
+ return false;
+ }
+ }
+ const nsTArray<nsCOMPtr<nsIRedirectHistoryEntry>>& redirectChain(
+ interceptionInfo && interceptionInfo->TriggeringPrincipal()
+ ? interceptionInfo->RedirectChain()
+ : loadInfo->RedirectChain());
+
+ nsAutoCString hostScheme, otherScheme;
+ aHostURI->GetScheme(hostScheme);
+
+ bool isForeign = true;
+ nsresult rv;
+ if (contentPolicyType == ExtContentPolicy::TYPE_DOCUMENT ||
+ contentPolicyType == ExtContentPolicy::TYPE_SAVEAS_DOWNLOAD) {
+ // for loads of TYPE_DOCUMENT we query the hostURI from the
+ // triggeringPrincipal which returns the URI of the document that caused the
+ // navigation.
+ rv = triggeringPrincipal->IsThirdPartyChannel(aChannel, &isForeign);
+
+ triggeringPrincipal->GetScheme(otherScheme);
+ } else {
+ // If the load is caused by FetchEvent.request or NavigationPreload request,
+ // check the original InterceptedHttpChannel is a third-party channel or
+ // not.
+ if (interceptionInfo && interceptionInfo->TriggeringPrincipal()) {
+ isForeign = interceptionInfo->FromThirdParty();
+ if (isForeign) {
+ return true;
+ }
+ }
+ nsCOMPtr<mozIThirdPartyUtil> thirdPartyUtil =
+ do_GetService(THIRDPARTYUTIL_CONTRACTID);
+ if (!thirdPartyUtil) {
+ return true;
+ }
+ rv = thirdPartyUtil->IsThirdPartyChannel(aChannel, aHostURI, &isForeign);
+
+ channelURI->GetScheme(otherScheme);
+ }
+ // if we are dealing with a cross origin request, we can return here
+ // because we already know the request is 'foreign'.
+ if (NS_FAILED(rv) || isForeign) {
+ return true;
+ }
+
+ if (!IsSameSiteSchemeEqual(otherScheme, hostScheme)) {
+ // If the two schemes are not of the same http(s) scheme then we
+ // consider the request as foreign.
+ return true;
+ }
+
+ // for loads of TYPE_SUBDOCUMENT we have to perform an additional test,
+ // because a cross-origin iframe might perform a navigation to a same-origin
+ // iframe which would send same-site cookies. Hence, if the iframe navigation
+ // was triggered by a cross-origin triggeringPrincipal, we treat the load as
+ // foreign.
+ if (contentPolicyType == ExtContentPolicy::TYPE_SUBDOCUMENT) {
+ rv = triggeringPrincipal->IsThirdPartyChannel(aChannel, &isForeign);
+ if (NS_FAILED(rv) || isForeign) {
+ return true;
+ }
+ }
+
+ // for the purpose of same-site cookies we have to treat any cross-origin
+ // redirects as foreign. E.g. cross-site to same-site redirect is a problem
+ // with regards to CSRF.
+
+ nsCOMPtr<nsIPrincipal> redirectPrincipal;
+ for (nsIRedirectHistoryEntry* entry : redirectChain) {
+ entry->GetPrincipal(getter_AddRefs(redirectPrincipal));
+ if (redirectPrincipal) {
+ rv = redirectPrincipal->IsThirdPartyChannel(aChannel, &isForeign);
+ // if at any point we encounter a cross-origin redirect we can return.
+ if (NS_FAILED(rv) || isForeign) {
+ *aHadCrossSiteRedirects = isForeign;
+ return true;
+ }
+
+ nsAutoCString redirectScheme;
+ redirectPrincipal->GetScheme(redirectScheme);
+ if (!IsSameSiteSchemeEqual(redirectScheme, hostScheme)) {
+ // If the two schemes are not of the same http(s) scheme then we
+ // consider the request as foreign.
+ *aHadCrossSiteRedirects = true;
+ return true;
+ }
+ }
+ }
+ return isForeign;
+}
+
+// static
+nsICookie::schemeType CookieCommons::URIToSchemeType(nsIURI* aURI) {
+ MOZ_ASSERT(aURI);
+
+ nsAutoCString scheme;
+ nsresult rv = aURI->GetScheme(scheme);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nsICookie::SCHEME_UNSET;
+ }
+
+ return SchemeToSchemeType(scheme);
+}
+
+// static
+nsICookie::schemeType CookieCommons::PrincipalToSchemeType(
+ nsIPrincipal* aPrincipal) {
+ MOZ_ASSERT(aPrincipal);
+
+ nsAutoCString scheme;
+ nsresult rv = aPrincipal->GetScheme(scheme);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nsICookie::SCHEME_UNSET;
+ }
+
+ return SchemeToSchemeType(scheme);
+}
+
+// static
+nsICookie::schemeType CookieCommons::SchemeToSchemeType(
+ const nsACString& aScheme) {
+ MOZ_ASSERT(IsSchemeSupported(aScheme));
+
+ if (aScheme.Equals("https")) {
+ return nsICookie::SCHEME_HTTPS;
+ }
+
+ if (aScheme.Equals("http")) {
+ return nsICookie::SCHEME_HTTP;
+ }
+
+ if (aScheme.Equals("file")) {
+ return nsICookie::SCHEME_FILE;
+ }
+
+ MOZ_CRASH("Unsupported scheme type");
+}
+
+// static
+bool CookieCommons::IsSchemeSupported(nsIPrincipal* aPrincipal) {
+ MOZ_ASSERT(aPrincipal);
+
+ nsAutoCString scheme;
+ nsresult rv = aPrincipal->GetScheme(scheme);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ return IsSchemeSupported(scheme);
+}
+
+// static
+bool CookieCommons::IsSchemeSupported(nsIURI* aURI) {
+ MOZ_ASSERT(aURI);
+
+ nsAutoCString scheme;
+ nsresult rv = aURI->GetScheme(scheme);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ return IsSchemeSupported(scheme);
+}
+
+// static
+bool CookieCommons::IsSchemeSupported(const nsACString& aScheme) {
+ return aScheme.Equals("https") || aScheme.Equals("http") ||
+ aScheme.Equals("file");
+}
+
+} // namespace net
+} // namespace mozilla
diff --git a/netwerk/cookie/CookieCommons.h b/netwerk/cookie/CookieCommons.h
new file mode 100644
index 0000000000..f2b9df355a
--- /dev/null
+++ b/netwerk/cookie/CookieCommons.h
@@ -0,0 +1,142 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_net_CookieCommons_h
+#define mozilla_net_CookieCommons_h
+
+#include <cstdint>
+#include <functional>
+#include "mozIThirdPartyUtil.h"
+#include "prtime.h"
+#include "nsString.h"
+#include "nsICookie.h"
+#include "mozilla/net/NeckoChannelParams.h"
+
+class nsIChannel;
+class nsIConsoleReportCollector;
+class nsICookieJarSettings;
+class nsIEffectiveTLDService;
+class nsIPrincipal;
+class nsIURI;
+
+namespace mozilla {
+
+namespace dom {
+class Document;
+}
+
+namespace net {
+
+// these constants represent an operation being performed on cookies
+enum CookieOperation { OPERATION_READ, OPERATION_WRITE };
+
+// these constants represent a decision about a cookie based on user prefs.
+enum CookieStatus {
+ STATUS_ACCEPTED,
+ STATUS_ACCEPT_SESSION,
+ STATUS_REJECTED,
+ // STATUS_REJECTED_WITH_ERROR indicates the cookie should be rejected because
+ // of an error (rather than something the user can control). this is used for
+ // notification purposes, since we only want to notify of rejections where
+ // the user can do something about it (e.g. whitelist the site).
+ STATUS_REJECTED_WITH_ERROR
+};
+
+class Cookie;
+
+// pref string constants
+static const char kPrefMaxNumberOfCookies[] = "network.cookie.maxNumber";
+static const char kPrefMaxCookiesPerHost[] = "network.cookie.maxPerHost";
+static const char kPrefCookieQuotaPerHost[] = "network.cookie.quotaPerHost";
+static const char kPrefCookiePurgeAge[] = "network.cookie.purgeAge";
+
+// default limits for the cookie list. these can be tuned by the
+// network.cookie.maxNumber and network.cookie.maxPerHost prefs respectively.
+static const uint32_t kMaxCookiesPerHost = 180;
+static const uint32_t kCookieQuotaPerHost = 150;
+static const uint32_t kMaxNumberOfCookies = 3000;
+static const uint32_t kMaxBytesPerCookie = 4096;
+static const uint32_t kMaxBytesPerPath = 1024;
+
+static const int64_t kCookiePurgeAge =
+ int64_t(30 * 24 * 60 * 60) * PR_USEC_PER_SEC; // 30 days in microseconds
+
+class CookieCommons final {
+ public:
+ static bool DomainMatches(Cookie* aCookie, const nsACString& aHost);
+
+ static bool PathMatches(Cookie* aCookie, const nsACString& aPath);
+
+ static nsresult GetBaseDomain(nsIEffectiveTLDService* aTLDService,
+ nsIURI* aHostURI, nsACString& aBaseDomain,
+ bool& aRequireHostMatch);
+
+ static nsresult GetBaseDomain(nsIPrincipal* aPrincipal,
+ nsACString& aBaseDomain);
+
+ static nsresult GetBaseDomainFromHost(nsIEffectiveTLDService* aTLDService,
+ const nsACString& aHost,
+ nsCString& aBaseDomain);
+
+ static void NotifyRejected(nsIURI* aHostURI, nsIChannel* aChannel,
+ uint32_t aRejectedReason,
+ CookieOperation aOperation);
+
+ static bool CheckPathSize(const CookieStruct& aCookieData);
+
+ static bool CheckNameAndValueSize(const CookieStruct& aCookieData);
+
+ static bool CheckName(const CookieStruct& aCookieData);
+
+ static bool CheckValue(const CookieStruct& aCookieData);
+
+ static bool CheckCookiePermission(nsIChannel* aChannel,
+ CookieStruct& aCookieData);
+
+ static bool CheckCookiePermission(nsIPrincipal* aPrincipal,
+ nsICookieJarSettings* aCookieJarSettings,
+ CookieStruct& aCookieData);
+
+ static already_AddRefed<Cookie> CreateCookieFromDocument(
+ dom::Document* aDocument, const nsACString& aCookieString,
+ int64_t aCurrentTimeInUsec, nsIEffectiveTLDService* aTLDService,
+ mozIThirdPartyUtil* aThirdPartyUtil,
+ std::function<bool(const nsACString&, const OriginAttributes&)>&&
+ aHasExistingCookiesLambda,
+ nsIURI** aDocumentURI, nsACString& aBaseDomain, OriginAttributes& aAttrs);
+
+ static already_AddRefed<nsICookieJarSettings> GetCookieJarSettings(
+ nsIChannel* aChannel);
+
+ static bool ShouldIncludeCrossSiteCookieForDocument(Cookie* aCookie,
+ dom::Document* aDocument);
+
+ static bool IsSchemeSupported(nsIPrincipal* aPrincipal);
+ static bool IsSchemeSupported(nsIURI* aURI);
+ static bool IsSchemeSupported(const nsACString& aScheme);
+
+ static nsICookie::schemeType URIToSchemeType(nsIURI* aURI);
+
+ static nsICookie::schemeType PrincipalToSchemeType(nsIPrincipal* aPrincipal);
+
+ static nsICookie::schemeType SchemeToSchemeType(const nsACString& aScheme);
+
+ // Returns true if the channel is a safe top-level navigation or if it's a
+ // download request
+ static bool IsSafeTopLevelNav(nsIChannel* aChannel);
+
+ // Returns true if the channel is a foreign with respect to the host-uri.
+ // For loads of TYPE_DOCUMENT, this function returns true if it's a cross
+ // site navigation.
+ // `aHadCrossSiteRedirects` will be true iff the channel had a cross-site
+ // redirect before the final URI.
+ static bool IsSameSiteForeign(nsIChannel* aChannel, nsIURI* aHostURI,
+ bool* aHadCrossSiteRedirects);
+};
+
+} // namespace net
+} // namespace mozilla
+
+#endif // mozilla_net_CookieCommons_h
diff --git a/netwerk/cookie/CookieJarSettings.cpp b/netwerk/cookie/CookieJarSettings.cpp
new file mode 100644
index 0000000000..09a4789d85
--- /dev/null
+++ b/netwerk/cookie/CookieJarSettings.cpp
@@ -0,0 +1,733 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/AntiTrackingUtils.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/ContentBlockingAllowList.h"
+#include "mozilla/dom/BrowsingContext.h"
+#include "mozilla/net/CookieJarSettings.h"
+#include "mozilla/net/NeckoChannelParams.h"
+#include "mozilla/Permission.h"
+#include "mozilla/PermissionManager.h"
+#include "mozilla/SchedulerGroup.h"
+#include "mozilla/StaticPrefs_network.h"
+#include "mozilla/Unused.h"
+#include "nsIPrincipal.h"
+#if defined(MOZ_THUNDERBIRD) || defined(MOZ_SUITE)
+# include "nsIProtocolHandler.h"
+#endif
+#include "nsIClassInfoImpl.h"
+#include "nsIChannel.h"
+#include "nsICookieManager.h"
+#include "nsICookieService.h"
+#include "nsIObjectInputStream.h"
+#include "nsIObjectOutputStream.h"
+#include "nsNetUtil.h"
+
+namespace mozilla {
+namespace net {
+
+NS_IMPL_CLASSINFO(CookieJarSettings, nullptr, nsIClassInfo::THREADSAFE,
+ COOKIEJARSETTINGS_CID)
+
+NS_IMPL_ISUPPORTS_CI(CookieJarSettings, nsICookieJarSettings, nsISerializable)
+
+static StaticRefPtr<CookieJarSettings> sBlockinAll;
+
+namespace {
+
+class PermissionComparator {
+ public:
+ static bool Equals(nsIPermission* aA, nsIPermission* aB) {
+ nsCOMPtr<nsIPrincipal> principalA;
+ nsresult rv = aA->GetPrincipal(getter_AddRefs(principalA));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ nsCOMPtr<nsIPrincipal> principalB;
+ rv = aB->GetPrincipal(getter_AddRefs(principalB));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ bool equals = false;
+ rv = principalA->Equals(principalB, &equals);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ return equals;
+ }
+};
+
+class ReleaseCookiePermissions final : public Runnable {
+ public:
+ explicit ReleaseCookiePermissions(nsTArray<RefPtr<nsIPermission>>&& aArray)
+ : Runnable("ReleaseCookiePermissions"), mArray(std::move(aArray)) {}
+
+ NS_IMETHOD Run() override {
+ MOZ_ASSERT(NS_IsMainThread());
+ mArray.Clear();
+ return NS_OK;
+ }
+
+ private:
+ nsTArray<RefPtr<nsIPermission>> mArray;
+};
+
+} // namespace
+
+// static
+already_AddRefed<nsICookieJarSettings> CookieJarSettings::GetBlockingAll(
+ bool aShouldResistFingerprinting) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (sBlockinAll) {
+ return do_AddRef(sBlockinAll);
+ }
+
+ sBlockinAll = new CookieJarSettings(nsICookieService::BEHAVIOR_REJECT,
+ OriginAttributes::IsFirstPartyEnabled(),
+ aShouldResistFingerprinting, eFixed);
+ ClearOnShutdown(&sBlockinAll);
+
+ return do_AddRef(sBlockinAll);
+}
+
+// static
+already_AddRefed<nsICookieJarSettings> CookieJarSettings::Create(
+ CreateMode aMode, bool aShouldResistFingerprinting) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RefPtr<CookieJarSettings> cookieJarSettings;
+
+ switch (aMode) {
+ case eRegular:
+ case ePrivate:
+ cookieJarSettings = new CookieJarSettings(
+ nsICookieManager::GetCookieBehavior(aMode == ePrivate),
+ OriginAttributes::IsFirstPartyEnabled(), aShouldResistFingerprinting,
+ eProgressive);
+ break;
+
+ default:
+ MOZ_CRASH("Unexpected create mode.");
+ }
+
+ return cookieJarSettings.forget();
+}
+
+// static
+already_AddRefed<nsICookieJarSettings> CookieJarSettings::Create(
+ nsIPrincipal* aPrincipal) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ bool shouldResistFingerprinting =
+ nsContentUtils::ShouldResistFingerprinting_dangerous(
+ aPrincipal, "We are constructing CookieJarSettings here.",
+ RFPTarget::IsAlwaysEnabledForPrecompute);
+
+ if (aPrincipal && aPrincipal->OriginAttributesRef().mPrivateBrowsingId > 0) {
+ return Create(ePrivate, shouldResistFingerprinting);
+ }
+
+ return Create(eRegular, shouldResistFingerprinting);
+}
+
+// static
+already_AddRefed<nsICookieJarSettings> CookieJarSettings::Create(
+ uint32_t aCookieBehavior, const nsAString& aPartitionKey,
+ bool aIsFirstPartyIsolated, bool aIsOnContentBlockingAllowList,
+ bool aShouldResistFingerprinting) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RefPtr<CookieJarSettings> cookieJarSettings =
+ new CookieJarSettings(aCookieBehavior, aIsFirstPartyIsolated,
+ aShouldResistFingerprinting, eProgressive);
+ cookieJarSettings->mPartitionKey = aPartitionKey;
+ cookieJarSettings->mIsOnContentBlockingAllowList =
+ aIsOnContentBlockingAllowList;
+
+ return cookieJarSettings.forget();
+}
+
+// static
+already_AddRefed<nsICookieJarSettings> CookieJarSettings::CreateForXPCOM() {
+ MOZ_ASSERT(NS_IsMainThread());
+ return Create(eRegular, /* shouldResistFingerprinting */ false);
+}
+
+CookieJarSettings::CookieJarSettings(uint32_t aCookieBehavior,
+ bool aIsFirstPartyIsolated,
+ bool aShouldResistFingerprinting,
+ State aState)
+ : mCookieBehavior(aCookieBehavior),
+ mIsFirstPartyIsolated(aIsFirstPartyIsolated),
+ mIsOnContentBlockingAllowList(false),
+ mIsOnContentBlockingAllowListUpdated(false),
+ mState(aState),
+ mToBeMerged(false),
+ mShouldResistFingerprinting(aShouldResistFingerprinting) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT_IF(
+ mIsFirstPartyIsolated,
+ mCookieBehavior !=
+ nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN);
+}
+
+CookieJarSettings::~CookieJarSettings() {
+ if (!NS_IsMainThread() && !mCookiePermissions.IsEmpty()) {
+ RefPtr<Runnable> r =
+ new ReleaseCookiePermissions(std::move(mCookiePermissions));
+ MOZ_ASSERT(mCookiePermissions.IsEmpty());
+ SchedulerGroup::Dispatch(r.forget());
+ }
+}
+
+NS_IMETHODIMP
+CookieJarSettings::InitWithURI(nsIURI* aURI, bool aIsPrivate) {
+ NS_ENSURE_ARG(aURI);
+
+ mCookieBehavior = nsICookieManager::GetCookieBehavior(aIsPrivate);
+
+ SetPartitionKey(aURI);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CookieJarSettings::GetCookieBehavior(uint32_t* aCookieBehavior) {
+ *aCookieBehavior = mCookieBehavior;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CookieJarSettings::GetIsFirstPartyIsolated(bool* aIsFirstPartyIsolated) {
+ *aIsFirstPartyIsolated = mIsFirstPartyIsolated;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CookieJarSettings::GetShouldResistFingerprinting(
+ bool* aShouldResistFingerprinting) {
+ *aShouldResistFingerprinting = mShouldResistFingerprinting;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CookieJarSettings::GetRejectThirdPartyContexts(
+ bool* aRejectThirdPartyContexts) {
+ *aRejectThirdPartyContexts =
+ CookieJarSettings::IsRejectThirdPartyContexts(mCookieBehavior);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CookieJarSettings::GetLimitForeignContexts(bool* aLimitForeignContexts) {
+ *aLimitForeignContexts =
+ mCookieBehavior == nsICookieService::BEHAVIOR_LIMIT_FOREIGN ||
+ (StaticPrefs::privacy_dynamic_firstparty_limitForeign() &&
+ mCookieBehavior ==
+ nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CookieJarSettings::GetBlockingAllThirdPartyContexts(
+ bool* aBlockingAllThirdPartyContexts) {
+ // XXX For non-cookie forms of storage, we handle BEHAVIOR_LIMIT_FOREIGN by
+ // simply rejecting the request to use the storage. In the future, if we
+ // change the meaning of BEHAVIOR_LIMIT_FOREIGN to be one which makes sense
+ // for non-cookie storage types, this may change.
+ *aBlockingAllThirdPartyContexts =
+ mCookieBehavior == nsICookieService::BEHAVIOR_LIMIT_FOREIGN ||
+ mCookieBehavior == nsICookieService::BEHAVIOR_REJECT_FOREIGN;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CookieJarSettings::GetBlockingAllContexts(bool* aBlockingAllContexts) {
+ *aBlockingAllContexts = mCookieBehavior == nsICookieService::BEHAVIOR_REJECT;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CookieJarSettings::GetPartitionForeign(bool* aPartitionForeign) {
+ *aPartitionForeign =
+ mCookieBehavior ==
+ nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CookieJarSettings::SetPartitionForeign(bool aPartitionForeign) {
+ if (mIsFirstPartyIsolated) {
+ return NS_OK;
+ }
+
+ if (aPartitionForeign) {
+ mCookieBehavior =
+ nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CookieJarSettings::GetIsOnContentBlockingAllowList(
+ bool* aIsOnContentBlockingAllowList) {
+ *aIsOnContentBlockingAllowList = mIsOnContentBlockingAllowList;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CookieJarSettings::GetPartitionKey(nsAString& aPartitionKey) {
+ aPartitionKey = mPartitionKey;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CookieJarSettings::GetFingerprintingRandomizationKey(
+ nsTArray<uint8_t>& aFingerprintingRandomizationKey) {
+ if (!mFingerprintingRandomKey) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ aFingerprintingRandomizationKey = mFingerprintingRandomKey->Clone();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CookieJarSettings::CookiePermission(nsIPrincipal* aPrincipal,
+ uint32_t* aCookiePermission) {
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
+ NS_ENSURE_ARG_POINTER(aPrincipal);
+ NS_ENSURE_ARG_POINTER(aCookiePermission);
+
+ *aCookiePermission = nsIPermissionManager::UNKNOWN_ACTION;
+
+ nsresult rv;
+
+ // Let's see if we know this permission.
+ if (!mCookiePermissions.IsEmpty()) {
+ for (const RefPtr<nsIPermission>& permission : mCookiePermissions) {
+ bool match = false;
+ rv = permission->Matches(aPrincipal, false, &match);
+ if (NS_WARN_IF(NS_FAILED(rv)) || !match) {
+ continue;
+ }
+
+ rv = permission->GetCapability(aCookiePermission);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+ }
+ }
+
+ // Let's ask the permission manager.
+ PermissionManager* pm = PermissionManager::GetInstance();
+ if (NS_WARN_IF(!pm)) {
+ return NS_ERROR_FAILURE;
+ }
+
+#if defined(MOZ_THUNDERBIRD) || defined(MOZ_SUITE)
+ // Check if this protocol doesn't allow cookies.
+ bool hasFlags;
+ nsCOMPtr<nsIURI> uri;
+ BasePrincipal::Cast(aPrincipal)->GetURI(getter_AddRefs(uri));
+
+ rv = NS_URIChainHasFlags(uri, nsIProtocolHandler::URI_FORBIDS_COOKIE_ACCESS,
+ &hasFlags);
+ if (NS_FAILED(rv) || hasFlags) {
+ *aCookiePermission = PermissionManager::DENY_ACTION;
+ rv = NS_OK; // Reset, so it's not caught as a bad status after the `else`.
+ } else // Note the tricky `else` which controls the call below.
+#endif
+
+ rv = pm->TestPermissionFromPrincipal(aPrincipal, "cookie"_ns,
+ aCookiePermission);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // Let's store the permission, also if the result is UNKNOWN in order to avoid
+ // race conditions.
+
+ nsCOMPtr<nsIPermission> permission =
+ Permission::Create(aPrincipal, "cookie"_ns, *aCookiePermission, 0, 0, 0);
+ if (permission) {
+ mCookiePermissions.AppendElement(permission);
+ }
+
+ mToBeMerged = true;
+ return NS_OK;
+}
+
+void CookieJarSettings::Serialize(CookieJarSettingsArgs& aData) {
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
+
+ aData.isFixed() = mState == eFixed;
+ aData.cookieBehavior() = mCookieBehavior;
+ aData.isFirstPartyIsolated() = mIsFirstPartyIsolated;
+ aData.shouldResistFingerprinting() = mShouldResistFingerprinting;
+ aData.isOnContentBlockingAllowList() = mIsOnContentBlockingAllowList;
+ aData.partitionKey() = mPartitionKey;
+ if (mFingerprintingRandomKey) {
+ aData.hasFingerprintingRandomizationKey() = true;
+ aData.fingerprintingRandomizationKey() = mFingerprintingRandomKey->Clone();
+ } else {
+ aData.hasFingerprintingRandomizationKey() = false;
+ }
+
+ for (const RefPtr<nsIPermission>& permission : mCookiePermissions) {
+ nsCOMPtr<nsIPrincipal> principal;
+ nsresult rv = permission->GetPrincipal(getter_AddRefs(principal));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+
+ ipc::PrincipalInfo principalInfo;
+ rv = PrincipalToPrincipalInfo(principal, &principalInfo,
+ true /* aSkipBaseDomain */);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+
+ uint32_t cookiePermission = 0;
+ rv = permission->GetCapability(&cookiePermission);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+
+ aData.cookiePermissions().AppendElement(
+ CookiePermissionData(principalInfo, cookiePermission));
+ }
+
+ mToBeMerged = false;
+}
+
+/* static */ void CookieJarSettings::Deserialize(
+ const CookieJarSettingsArgs& aData,
+ nsICookieJarSettings** aCookieJarSettings) {
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
+
+ CookiePermissionList list;
+ for (const CookiePermissionData& data : aData.cookiePermissions()) {
+ auto principalOrErr = PrincipalInfoToPrincipal(data.principalInfo());
+ if (NS_WARN_IF(principalOrErr.isErr())) {
+ continue;
+ }
+
+ nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap();
+
+ nsCOMPtr<nsIPermission> permission = Permission::Create(
+ principal, "cookie"_ns, data.cookiePermission(), 0, 0, 0);
+ if (NS_WARN_IF(!permission)) {
+ continue;
+ }
+
+ list.AppendElement(permission);
+ }
+
+ RefPtr<CookieJarSettings> cookieJarSettings = new CookieJarSettings(
+ aData.cookieBehavior(), aData.isFirstPartyIsolated(),
+ aData.shouldResistFingerprinting(),
+ aData.isFixed() ? eFixed : eProgressive);
+
+ cookieJarSettings->mIsOnContentBlockingAllowList =
+ aData.isOnContentBlockingAllowList();
+ cookieJarSettings->mCookiePermissions = std::move(list);
+ cookieJarSettings->mPartitionKey = aData.partitionKey();
+ cookieJarSettings->mShouldResistFingerprinting =
+ aData.shouldResistFingerprinting();
+
+ if (aData.hasFingerprintingRandomizationKey()) {
+ cookieJarSettings->mFingerprintingRandomKey.emplace(
+ aData.fingerprintingRandomizationKey().Clone());
+ }
+
+ cookieJarSettings.forget(aCookieJarSettings);
+}
+
+void CookieJarSettings::Merge(const CookieJarSettingsArgs& aData) {
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(
+ mCookieBehavior == aData.cookieBehavior() ||
+ (mCookieBehavior == nsICookieService::BEHAVIOR_REJECT_TRACKER &&
+ aData.cookieBehavior() ==
+ nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN) ||
+ (mCookieBehavior ==
+ nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN &&
+ aData.cookieBehavior() == nsICookieService::BEHAVIOR_REJECT_TRACKER));
+
+ if (mState == eFixed) {
+ return;
+ }
+
+ // Merge cookie behavior pref values
+ if (mCookieBehavior == nsICookieService::BEHAVIOR_REJECT_TRACKER &&
+ aData.cookieBehavior() ==
+ nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN) {
+ // If the other side has decided to partition third-party cookies, update
+ // our side when first-party isolation is disabled.
+ if (!mIsFirstPartyIsolated) {
+ mCookieBehavior =
+ nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN;
+ }
+ }
+ if (mCookieBehavior ==
+ nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN &&
+ aData.cookieBehavior() == nsICookieService::BEHAVIOR_REJECT_TRACKER) {
+ // If we've decided to partition third-party cookies, the other side may not
+ // have caught up yet unless it has first-party isolation enabled.
+ if (aData.isFirstPartyIsolated()) {
+ mCookieBehavior = nsICookieService::BEHAVIOR_REJECT_TRACKER;
+ mIsFirstPartyIsolated = true;
+ }
+ }
+ // Ignore all other cases.
+ MOZ_ASSERT_IF(
+ mIsFirstPartyIsolated,
+ mCookieBehavior !=
+ nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN);
+
+ if (aData.shouldResistFingerprinting()) {
+ mShouldResistFingerprinting = true;
+ }
+
+ PermissionComparator comparator;
+
+ for (const CookiePermissionData& data : aData.cookiePermissions()) {
+ auto principalOrErr = PrincipalInfoToPrincipal(data.principalInfo());
+ if (NS_WARN_IF(principalOrErr.isErr())) {
+ continue;
+ }
+
+ nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap();
+ nsCOMPtr<nsIPermission> permission = Permission::Create(
+ principal, "cookie"_ns, data.cookiePermission(), 0, 0, 0);
+ if (NS_WARN_IF(!permission)) {
+ continue;
+ }
+
+ if (!mCookiePermissions.Contains(permission, comparator)) {
+ mCookiePermissions.AppendElement(permission);
+ }
+ }
+}
+
+void CookieJarSettings::SetPartitionKey(nsIURI* aURI) {
+ MOZ_ASSERT(aURI);
+
+ OriginAttributes attrs;
+ attrs.SetPartitionKey(aURI);
+ mPartitionKey = std::move(attrs.mPartitionKey);
+}
+
+void CookieJarSettings::UpdateIsOnContentBlockingAllowList(
+ nsIChannel* aChannel) {
+ MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(aChannel);
+
+ // Early return if the flag was updated before.
+ if (mIsOnContentBlockingAllowListUpdated) {
+ return;
+ }
+ mIsOnContentBlockingAllowListUpdated = true;
+
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = aChannel->GetURI(getter_AddRefs(uri));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ // We need to recompute the ContentBlockingAllowListPrincipal here for the
+ // top level channel because we might navigate from the the initial
+ // about:blank page or the existing page which may have a different origin
+ // than the URI we are going to load here. Thus, we need to recompute the
+ // prinicpal in order to get the correct ContentBlockingAllowListPrincipal.
+ nsCOMPtr<nsIPrincipal> contentBlockingAllowListPrincipal;
+ OriginAttributes attrs;
+ loadInfo->GetOriginAttributes(&attrs);
+ ContentBlockingAllowList::RecomputePrincipal(
+ uri, attrs, getter_AddRefs(contentBlockingAllowListPrincipal));
+
+ if (!contentBlockingAllowListPrincipal ||
+ !contentBlockingAllowListPrincipal->GetIsContentPrincipal()) {
+ return;
+ }
+
+ Unused << ContentBlockingAllowList::Check(contentBlockingAllowListPrincipal,
+ NS_UsePrivateBrowsing(aChannel),
+ mIsOnContentBlockingAllowList);
+}
+
+// static
+bool CookieJarSettings::IsRejectThirdPartyContexts(uint32_t aCookieBehavior) {
+ return aCookieBehavior == nsICookieService::BEHAVIOR_REJECT_TRACKER ||
+ aCookieBehavior ==
+ nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN;
+}
+
+NS_IMETHODIMP
+CookieJarSettings::Read(nsIObjectInputStream* aStream) {
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
+ nsresult rv = aStream->Read32(&mCookieBehavior);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = aStream->ReadBoolean(&mIsFirstPartyIsolated);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = aStream->ReadBoolean(&mShouldResistFingerprinting);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ bool isFixed;
+ aStream->ReadBoolean(&isFixed);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ mState = isFixed ? eFixed : eProgressive;
+
+ rv = aStream->ReadBoolean(&mIsOnContentBlockingAllowList);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = aStream->ReadString(mPartitionKey);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // Deserializing the cookie permission list.
+ uint32_t cookiePermissionsLength;
+ rv = aStream->Read32(&cookiePermissionsLength);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (!cookiePermissionsLength) {
+ // Bailing out early because there is no cookie permission.
+ return NS_OK;
+ }
+
+ CookiePermissionList list;
+ mCookiePermissions.SetCapacity(cookiePermissionsLength);
+ for (uint32_t i = 0; i < cookiePermissionsLength; ++i) {
+ nsAutoCString principalJSON;
+ aStream->ReadCString(principalJSON);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ nsCOMPtr<nsIPrincipal> principal = BasePrincipal::FromJSON(principalJSON);
+
+ if (NS_WARN_IF(!principal)) {
+ continue;
+ }
+
+ uint32_t cookiePermission;
+ aStream->Read32(&cookiePermission);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ nsCOMPtr<nsIPermission> permission =
+ Permission::Create(principal, "cookie"_ns, cookiePermission, 0, 0, 0);
+ if (NS_WARN_IF(!permission)) {
+ continue;
+ }
+
+ list.AppendElement(permission);
+ }
+
+ mCookiePermissions = std::move(list);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CookieJarSettings::Write(nsIObjectOutputStream* aStream) {
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
+ nsresult rv = aStream->Write32(mCookieBehavior);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = aStream->WriteBoolean(mIsFirstPartyIsolated);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = aStream->WriteBoolean(mShouldResistFingerprinting);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = aStream->WriteBoolean(mState == eFixed);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = aStream->WriteBoolean(mIsOnContentBlockingAllowList);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = aStream->WriteWStringZ(mPartitionKey.get());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // Serializing the cookie permission list. It will first write the length of
+ // the list, and then, write the cookie permission consecutively.
+ uint32_t cookiePermissionsLength = mCookiePermissions.Length();
+ rv = aStream->Write32(cookiePermissionsLength);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ for (const RefPtr<nsIPermission>& permission : mCookiePermissions) {
+ nsCOMPtr<nsIPrincipal> principal;
+ nsresult rv = permission->GetPrincipal(getter_AddRefs(principal));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+
+ nsAutoCString principalJSON;
+ BasePrincipal::Cast(principal)->ToJSON(principalJSON);
+
+ rv = aStream->WriteStringZ(principalJSON.get());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ uint32_t cookiePermission = 0;
+ rv = permission->GetCapability(&cookiePermission);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+
+ rv = aStream->Write32(cookiePermission);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+
+ return NS_OK;
+}
+
+} // namespace net
+} // namespace mozilla
diff --git a/netwerk/cookie/CookieJarSettings.h b/netwerk/cookie/CookieJarSettings.h
new file mode 100644
index 0000000000..97f8528a55
--- /dev/null
+++ b/netwerk/cookie/CookieJarSettings.h
@@ -0,0 +1,267 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_net_CookieJarSettings_h
+#define mozilla_net_CookieJarSettings_h
+
+#include "mozilla/Maybe.h"
+
+#include "nsICookieJarSettings.h"
+#include "nsTArray.h"
+
+#define COOKIEJARSETTINGS_CONTRACTID "@mozilla.org/cookieJarSettings;1"
+// 4ce234f1-52e8-47a9-8c8d-b02f815733c7
+#define COOKIEJARSETTINGS_CID \
+ { \
+ 0x4ce234f1, 0x52e8, 0x47a9, { \
+ 0x8c, 0x8d, 0xb0, 0x2f, 0x81, 0x57, 0x33, 0xc7 \
+ } \
+ }
+
+class nsIPermission;
+
+namespace mozilla {
+namespace net {
+
+class CookieJarSettingsArgs;
+
+/**
+ * CookieJarSettings
+ * ~~~~~~~~~~~~~~
+ *
+ * CookieJarSettings is a snapshot of the cookie jar's configurations in a
+ * precise moment of time, such as the cookie policy and cookie permissions.
+ * This object is used by top-level documents to have a consistent cookie jar
+ * configuration also in case the user changes it. New configurations will apply
+ * only to new top-level documents.
+ *
+ * CookieJarSettings creation
+ * ~~~~~~~~~~~~~~~~~~~~~~~
+ *
+ * CookieJarSettings is created when the top-level document's nsIChannel's
+ * nsILoadInfo is constructed. Any sub-resource and any sub-document inherits it
+ * from that nsILoadInfo. Also dedicated workers and their resources inherit it
+ * from the parent document.
+ *
+ * SharedWorkers and ServiceWorkers have their own CookieJarSettings because
+ * they don't have a single parent document (SharedWorkers could have more than
+ * one, ServiceWorkers have none).
+ *
+ * In Chrome code, we have a new CookieJarSettings when we download resources
+ * via 'Save-as...' and we also have a new CookieJarSettings for favicon
+ * downloading.
+ *
+ * Content-scripts WebExtensions also have their own CookieJarSettings because
+ * they don't have a direct access to the document they are running into.
+ *
+ * Anything else will have a special CookieJarSettings which blocks everything
+ * (CookieJarSettings::GetBlockingAll()) by forcing BEHAVIOR_REJECT as policy.
+ * When this happens, that context will not have access to the cookie jar and no
+ * cookies are sent or received.
+ *
+ * Propagation of CookieJarSettings
+ * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ *
+ * CookieJarSettings are shared inside the same top-level document via its
+ * nsIChannel's nsILoadInfo. This is done automatically if you pass a nsINode
+ * to NS_NewChannel(), and it must be done manually if you use a different
+ * channel constructor. For instance, this happens for any worker networking
+ * operation.
+ *
+ * We use the same CookieJarSettings for any resource belonging to the top-level
+ * document even if cross-origin. This makes the browser behave consistently a
+ * scenario where A loads B which loads A again, and cookie policy/permission
+ * changes in the meantime.
+ *
+ * Cookie Permissions propagation
+ * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ *
+ * CookieJarSettings populates the known cookie permissions only when required.
+ * Initially the list is empty, but when CookieJarSettings::CookiePermission()
+ * is called, the requested permission is stored in the internal list if it
+ * doesn't exist yet.
+ *
+ * This is actually nice because it relies on the permission propagation from
+ * parent to content process. No extra IPC is required.
+ *
+ * Note that we store permissions with UNKNOWN_ACTION values too because they
+ * can be set after the loading of the top-level document and we don't want to
+ * return a different value when this happens.
+ *
+ * Use of CookieJarSettings
+ * ~~~~~~~~~~~~~~~~~~~~~
+ *
+ * In theory, there should not be direct access to cookie permissions or
+ * cookieBehavior pref. Everything should pass through CookieJarSettings.
+ *
+ * A reference to CookieJarSettings can be obtained from
+ * nsILoadInfo::GetCookieJarSettings(), from Document::CookieJarSettings() and
+ * from the WorkerPrivate::CookieJarSettings().
+ *
+ * CookieJarSettings is thread-safe, but the permission list must be touched
+ * only on the main-thread.
+ *
+ * Testing
+ * ~~~~~~~
+ *
+ * If you need to test the changing of cookie policy or a cookie permission, you
+ * need to workaround CookieJarSettings. This can be done opening a new window
+ * and running the test into that new global.
+ */
+
+/**
+ * Class that provides an nsICookieJarSettings implementation.
+ */
+class CookieJarSettings final : public nsICookieJarSettings {
+ public:
+ typedef nsTArray<RefPtr<nsIPermission>> CookiePermissionList;
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSICOOKIEJARSETTINGS
+ NS_DECL_NSISERIALIZABLE
+
+ static already_AddRefed<nsICookieJarSettings> GetBlockingAll(
+ bool aShouldResistFingerprinting);
+
+ enum CreateMode { eRegular, ePrivate };
+
+ static already_AddRefed<nsICookieJarSettings> Create(
+ CreateMode aMode, bool aShouldResistFingerprinting);
+
+ static already_AddRefed<nsICookieJarSettings> Create(
+ nsIPrincipal* aPrincipal);
+
+ // This function should be only called for XPCOM. You should never use this
+ // for other purposes.
+ static already_AddRefed<nsICookieJarSettings> CreateForXPCOM();
+
+ static already_AddRefed<nsICookieJarSettings> Create(
+ uint32_t aCookieBehavior, const nsAString& aPartitionKey,
+ bool aIsFirstPartyIsolated, bool aIsOnContentBlockingAllowList,
+ bool aShouldResistFingerprinting);
+
+ static CookieJarSettings* Cast(nsICookieJarSettings* aCS) {
+ return static_cast<CookieJarSettings*>(aCS);
+ }
+
+ void Serialize(CookieJarSettingsArgs& aData);
+
+ static void Deserialize(const CookieJarSettingsArgs& aData,
+ nsICookieJarSettings** aCookieJarSettings);
+
+ void Merge(const CookieJarSettingsArgs& aData);
+
+ // We don't want to send this object from parent to child process if there are
+ // no reasons. HasBeenChanged() returns true if the object has changed its
+ // internal state and it must be sent beck to the content process.
+ bool HasBeenChanged() const { return mToBeMerged; }
+
+ void UpdateIsOnContentBlockingAllowList(nsIChannel* aChannel);
+
+ void SetPartitionKey(nsIURI* aURI);
+ void SetPartitionKey(const nsAString& aPartitionKey) {
+ mPartitionKey = aPartitionKey;
+ }
+ const nsAString& GetPartitionKey() { return mPartitionKey; };
+
+ void SetFingerprintingRandomizationKey(const nsTArray<uint8_t>& aKey) {
+ mFingerprintingRandomKey.reset();
+
+ mFingerprintingRandomKey.emplace(aKey.Clone());
+ }
+
+ // Utility function to test if the passed cookiebahvior is
+ // BEHAVIOR_REJECT_TRACKER, BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN or
+ // BEHAVIOR_REJECT_FOREIGN when
+ // network.cookie.rejectForeignWithExceptions.enabled pref is set to true.
+ static bool IsRejectThirdPartyContexts(uint32_t aCookieBehavior);
+
+ private:
+ enum State {
+ // No cookie permissions are allowed to be stored in this object.
+ eFixed,
+
+ // Cookie permissions can be stored in case they are unknown when they are
+ // asked or when they are sent from the parent process.
+ eProgressive,
+ };
+
+ CookieJarSettings(uint32_t aCookieBehavior, bool aIsFirstPartyIsolated,
+ bool aShouldResistFingerprinting, State aState);
+ ~CookieJarSettings();
+
+ uint32_t mCookieBehavior;
+ bool mIsFirstPartyIsolated;
+ CookiePermissionList mCookiePermissions;
+ bool mIsOnContentBlockingAllowList;
+ bool mIsOnContentBlockingAllowListUpdated;
+ nsString mPartitionKey;
+
+ State mState;
+
+ bool mToBeMerged;
+
+ // DO NOT USE THIS MEMBER TO CHECK IF YOU SHOULD RESIST FINGERPRINTING.
+ // USE THE nsContentUtils::ShouldResistFingerprinting() METHODS ONLY.
+ //
+ // As we move to fine-grained RFP control, we want to support per-domain
+ // exemptions from ResistFingerprinting. Specifically the behavior should be
+ // as such:
+ //
+ // Top-Level Document is on an Exempted Domain
+ // - RFP is disabled.
+ //
+ // Top-Level Document on an Exempted Domain embedding a non-exempted
+ // cross-origin iframe
+ // - RFP in the iframe is enabled (NOT exempted). (**)
+ //
+ // Top-Level Document on an Exempted Domain embedding an exempted cross-origin
+ // iframe
+ // - RFP in the iframe is disabled (exempted).
+ //
+ // Top-Level Document on a Non-Exempted Domain
+ // - RFP is enabled (NOT exempted).
+ //
+ // Top-Level Document on a Non-Exempted Domain embeds an exempted cross-origin
+ // iframe
+ // - RFP in the iframe is enabled (NOT exempted). (*)
+ //
+ // Exempted Document (top-level or iframe) contacts any cross-origin domain
+ // (exempted or non-exempted)
+ // - RFP is disabled (exempted) for the request
+ //
+ // Non-Exempted Document (top-level or iframe) contacts any cross-origin
+ // domain
+ // (exempted or non-exempted)
+ // - RFP is enabled (NOT exempted) for the request
+ //
+ // This boolean on CookieJarSettings will enable us to apply the most
+ // difficult rule, marked in (*). (It is difficult because the
+ // subdocument's loadinfo will look like it should be exempted.)
+ // However if we trusted this member blindly, it would not correctly apply
+ // the one marked with (**). (Because it would inherit an exemption into
+ // a subdocument that should not be exempted.)
+ // To handle this case, we only trust a CookieJar's ShouldRFP value if it
+ // says we should resist fingerprinting. If it says that we _should not_,
+ // we continue and check the channel's URI or LoadInfo and if
+ // the domain specified there is not an exempted domain, enforce RFP anyway.
+ // This all occurrs in the nscontentUtils::ShouldResistFingerprinting
+ // functions which you should be using.
+ bool mShouldResistFingerprinting;
+
+ // The key used to generate the random noise for randomizing the browser
+ // fingerprint. The key is decided by the session key and the top-level site.
+ // So, the browse fingerprint will look different to the same tracker
+ // under different top-level sites. Also, the fingerprint will change as
+ // browsing session changes. This can prevent trackers to identify individuals
+ // by using browser fingerprints.
+ Maybe<nsTArray<uint8_t>> mFingerprintingRandomKey;
+};
+
+} // namespace net
+} // namespace mozilla
+
+#endif // mozilla_net_CookieJarSettings_h
diff --git a/netwerk/cookie/CookieKey.h b/netwerk/cookie/CookieKey.h
new file mode 100644
index 0000000000..86291a187e
--- /dev/null
+++ b/netwerk/cookie/CookieKey.h
@@ -0,0 +1,61 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_net_CookieKey_h
+#define mozilla_net_CookieKey_h
+
+#include "mozilla/OriginAttributes.h"
+#include "nsHashKeys.h"
+
+namespace mozilla {
+namespace net {
+
+class CookieKey : public PLDHashEntryHdr {
+ public:
+ typedef const CookieKey& KeyType;
+ typedef const CookieKey* KeyTypePointer;
+
+ CookieKey() = default;
+
+ CookieKey(const nsACString& baseDomain, const OriginAttributes& attrs)
+ : mBaseDomain(baseDomain), mOriginAttributes(attrs) {}
+
+ explicit CookieKey(KeyTypePointer other)
+ : mBaseDomain(other->mBaseDomain),
+ mOriginAttributes(other->mOriginAttributes) {}
+
+ CookieKey(CookieKey&& other) = default;
+ CookieKey& operator=(CookieKey&&) = default;
+
+ bool KeyEquals(KeyTypePointer other) const {
+ return mBaseDomain == other->mBaseDomain &&
+ mOriginAttributes == other->mOriginAttributes;
+ }
+
+ static KeyTypePointer KeyToPointer(KeyType aKey) { return &aKey; }
+
+ static PLDHashNumber HashKey(KeyTypePointer aKey) {
+ nsAutoCString temp(aKey->mBaseDomain);
+ temp.Append('#');
+ nsAutoCString suffix;
+ aKey->mOriginAttributes.CreateSuffix(suffix);
+ temp.Append(suffix);
+ return HashString(temp);
+ }
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
+ return mBaseDomain.SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ }
+
+ enum { ALLOW_MEMMOVE = true };
+
+ nsCString mBaseDomain;
+ OriginAttributes mOriginAttributes;
+};
+
+} // namespace net
+} // namespace mozilla
+
+#endif // mozilla_net_CookieKey_h
diff --git a/netwerk/cookie/CookieLogging.cpp b/netwerk/cookie/CookieLogging.cpp
new file mode 100644
index 0000000000..9f5262b2bb
--- /dev/null
+++ b/netwerk/cookie/CookieLogging.cpp
@@ -0,0 +1,185 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CookieLogging.h"
+#include "Cookie.h"
+#include "nsIConsoleReportCollector.h"
+
+constexpr auto TIME_STRING_LENGTH = 40;
+
+namespace mozilla {
+namespace net {
+
+LazyLogModule gCookieLog("cookie");
+
+static const char* SameSiteToString(uint32_t aSameSite) {
+ switch (aSameSite) {
+ case nsICookie::SAMESITE_NONE:
+ return "none";
+ case nsICookie::SAMESITE_LAX:
+ return "lax";
+ case nsICookie::SAMESITE_STRICT:
+ return "strict";
+ default:
+ MOZ_CRASH("Invalid nsICookie sameSite value");
+ return "";
+ }
+}
+
+// static
+void CookieLogging::LogSuccess(bool aSetCookie, nsIURI* aHostURI,
+ const nsACString& aCookieString, Cookie* aCookie,
+ bool aReplacing) {
+ // if logging isn't enabled, return now to save cycles
+ if (!MOZ_LOG_TEST(gCookieLog, LogLevel::Debug)) {
+ return;
+ }
+
+ nsAutoCString spec;
+ if (aHostURI) {
+ aHostURI->GetAsciiSpec(spec);
+ }
+
+ MOZ_LOG(gCookieLog, LogLevel::Debug,
+ ("===== %s =====\n", aSetCookie ? "COOKIE ACCEPTED" : "COOKIE SENT"));
+ MOZ_LOG(gCookieLog, LogLevel::Debug, ("request URL: %s\n", spec.get()));
+ MOZ_LOG(gCookieLog, LogLevel::Debug,
+ ("cookie string: %s\n", aCookieString.BeginReading()));
+ if (aSetCookie) {
+ MOZ_LOG(gCookieLog, LogLevel::Debug,
+ ("replaces existing cookie: %s\n", aReplacing ? "true" : "false"));
+ }
+
+ LogCookie(aCookie);
+
+ MOZ_LOG(gCookieLog, LogLevel::Debug, ("\n"));
+}
+
+// static
+void CookieLogging::LogFailure(bool aSetCookie, nsIURI* aHostURI,
+ const nsACString& aCookieString,
+ const char* aReason) {
+ // if logging isn't enabled, return now to save cycles
+ if (!MOZ_LOG_TEST(gCookieLog, LogLevel::Warning)) {
+ return;
+ }
+
+ nsAutoCString spec;
+ if (aHostURI) {
+ aHostURI->GetAsciiSpec(spec);
+ }
+
+ MOZ_LOG(gCookieLog, LogLevel::Warning,
+ ("===== %s =====\n",
+ aSetCookie ? "COOKIE NOT ACCEPTED" : "COOKIE NOT SENT"));
+ MOZ_LOG(gCookieLog, LogLevel::Warning, ("request URL: %s\n", spec.get()));
+ if (aSetCookie) {
+ MOZ_LOG(gCookieLog, LogLevel::Warning,
+ ("cookie string: %s\n", aCookieString.BeginReading()));
+ }
+
+ PRExplodedTime explodedTime;
+ PR_ExplodeTime(PR_Now(), PR_GMTParameters, &explodedTime);
+ char timeString[TIME_STRING_LENGTH];
+ PR_FormatTimeUSEnglish(timeString, TIME_STRING_LENGTH, "%c GMT",
+ &explodedTime);
+
+ MOZ_LOG(gCookieLog, LogLevel::Warning, ("current time: %s", timeString));
+ MOZ_LOG(gCookieLog, LogLevel::Warning, ("rejected because %s\n", aReason));
+ MOZ_LOG(gCookieLog, LogLevel::Warning, ("\n"));
+}
+
+// static
+void CookieLogging::LogCookie(Cookie* aCookie) {
+ PRExplodedTime explodedTime;
+ PR_ExplodeTime(PR_Now(), PR_GMTParameters, &explodedTime);
+ char timeString[TIME_STRING_LENGTH];
+ PR_FormatTimeUSEnglish(timeString, TIME_STRING_LENGTH, "%c GMT",
+ &explodedTime);
+
+ MOZ_LOG(gCookieLog, LogLevel::Debug, ("current time: %s", timeString));
+
+ if (aCookie) {
+ MOZ_LOG(gCookieLog, LogLevel::Debug, ("----------------\n"));
+ MOZ_LOG(gCookieLog, LogLevel::Debug, ("name: %s\n", aCookie->Name().get()));
+ MOZ_LOG(gCookieLog, LogLevel::Debug,
+ ("value: %s\n", aCookie->Value().get()));
+ MOZ_LOG(gCookieLog, LogLevel::Debug,
+ ("%s: %s\n", aCookie->IsDomain() ? "domain" : "host",
+ aCookie->Host().get()));
+ MOZ_LOG(gCookieLog, LogLevel::Debug, ("path: %s\n", aCookie->Path().get()));
+
+ PR_ExplodeTime(aCookie->Expiry() * int64_t(PR_USEC_PER_SEC),
+ PR_GMTParameters, &explodedTime);
+ PR_FormatTimeUSEnglish(timeString, TIME_STRING_LENGTH, "%c GMT",
+ &explodedTime);
+ MOZ_LOG(gCookieLog, LogLevel::Debug,
+ ("expires: %s%s", timeString,
+ aCookie->IsSession() ? " (at end of session)" : ""));
+
+ PR_ExplodeTime(aCookie->CreationTime(), PR_GMTParameters, &explodedTime);
+ PR_FormatTimeUSEnglish(timeString, TIME_STRING_LENGTH, "%c GMT",
+ &explodedTime);
+ MOZ_LOG(gCookieLog, LogLevel::Debug, ("created: %s", timeString));
+
+ MOZ_LOG(gCookieLog, LogLevel::Debug,
+ ("is secure: %s\n", aCookie->IsSecure() ? "true" : "false"));
+ MOZ_LOG(gCookieLog, LogLevel::Debug,
+ ("is httpOnly: %s\n", aCookie->IsHttpOnly() ? "true" : "false"));
+ MOZ_LOG(gCookieLog, LogLevel::Debug,
+ ("sameSite: %s - rawSameSite: %s\n",
+ SameSiteToString(aCookie->SameSite()),
+ SameSiteToString(aCookie->RawSameSite())));
+ MOZ_LOG(
+ gCookieLog, LogLevel::Debug,
+ ("schemeMap %d (http: %s | https: %s | file: %s)\n",
+ aCookie->SchemeMap(),
+ (aCookie->SchemeMap() & nsICookie::SCHEME_HTTP ? "true" : "false"),
+ (aCookie->SchemeMap() & nsICookie::SCHEME_HTTPS ? "true" : "false"),
+ (aCookie->SchemeMap() & nsICookie::SCHEME_FILE ? "true" : "false")));
+
+ nsAutoCString suffix;
+ aCookie->OriginAttributesRef().CreateSuffix(suffix);
+ MOZ_LOG(gCookieLog, LogLevel::Debug,
+ ("origin attributes: %s\n",
+ suffix.IsEmpty() ? "{empty}" : suffix.get()));
+ }
+}
+
+// static
+void CookieLogging::LogEvicted(Cookie* aCookie, const char* details) {
+ MOZ_LOG(gCookieLog, LogLevel::Debug, ("===== COOKIE EVICTED =====\n"));
+ MOZ_LOG(gCookieLog, LogLevel::Debug, ("%s\n", details));
+
+ LogCookie(aCookie);
+
+ MOZ_LOG(gCookieLog, LogLevel::Debug, ("\n"));
+}
+
+// static
+void CookieLogging::LogMessageToConsole(nsIConsoleReportCollector* aCRC,
+ nsIURI* aURI, uint32_t aErrorFlags,
+ const nsACString& aCategory,
+ const nsACString& aMsg,
+ const nsTArray<nsString>& aParams) {
+ if (!aCRC) {
+ return;
+ }
+
+ nsAutoCString uri;
+ if (aURI) {
+ nsresult rv = aURI->GetSpec(uri);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+ }
+
+ aCRC->AddConsoleReport(aErrorFlags, aCategory,
+ nsContentUtils::eNECKO_PROPERTIES, uri, 0, 0, aMsg,
+ aParams);
+}
+
+} // namespace net
+} // namespace mozilla
diff --git a/netwerk/cookie/CookieLogging.h b/netwerk/cookie/CookieLogging.h
new file mode 100644
index 0000000000..19721914c8
--- /dev/null
+++ b/netwerk/cookie/CookieLogging.h
@@ -0,0 +1,65 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_net_CookieLogging_h
+#define mozilla_net_CookieLogging_h
+
+#include "mozilla/Logging.h"
+#include "nsString.h"
+
+class nsIConsoleReportCollector;
+class nsIURI;
+
+namespace mozilla {
+namespace net {
+
+// define logging macros for convenience
+#define SET_COOKIE true
+#define GET_COOKIE false
+
+extern LazyLogModule gCookieLog;
+
+#define COOKIE_LOGFAILURE(a, b, c, d) CookieLogging::LogFailure(a, b, c, d)
+#define COOKIE_LOGSUCCESS(a, b, c, d, e) \
+ CookieLogging::LogSuccess(a, b, c, d, e)
+
+#define COOKIE_LOGEVICTED(a, details) \
+ PR_BEGIN_MACRO \
+ if (MOZ_LOG_TEST(gCookieLog, LogLevel::Debug)) \
+ CookieLogging::LogEvicted(a, details); \
+ PR_END_MACRO
+
+#define COOKIE_LOGSTRING(lvl, fmt) \
+ PR_BEGIN_MACRO \
+ MOZ_LOG(gCookieLog, lvl, fmt); \
+ MOZ_LOG(gCookieLog, lvl, ("\n")); \
+ PR_END_MACRO
+
+class Cookie;
+
+class CookieLogging final {
+ public:
+ static void LogSuccess(bool aSetCookie, nsIURI* aHostURI,
+ const nsACString& aCookieString, Cookie* aCookie,
+ bool aReplacing);
+
+ static void LogFailure(bool aSetCookie, nsIURI* aHostURI,
+ const nsACString& aCookieString, const char* aReason);
+
+ static void LogCookie(Cookie* aCookie);
+
+ static void LogEvicted(Cookie* aCookie, const char* aDetails);
+
+ static void LogMessageToConsole(nsIConsoleReportCollector* aCRC, nsIURI* aURI,
+ uint32_t aErrorFlags,
+ const nsACString& aCategory,
+ const nsACString& aMsg,
+ const nsTArray<nsString>& aParams);
+};
+
+} // namespace net
+} // namespace mozilla
+
+#endif // mozilla_net_CookieLogging_h
diff --git a/netwerk/cookie/CookieNotification.cpp b/netwerk/cookie/CookieNotification.cpp
new file mode 100644
index 0000000000..43e7364a38
--- /dev/null
+++ b/netwerk/cookie/CookieNotification.cpp
@@ -0,0 +1,60 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CookieNotification.h"
+#include "mozilla/dom/BrowsingContext.h"
+#include "nsICookieNotification.h"
+
+namespace mozilla::net {
+
+NS_IMETHODIMP
+CookieNotification::GetAction(nsICookieNotification::Action* aResult) {
+ *aResult = mAction;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CookieNotification::GetCookie(nsICookie** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ *aResult = mCookie;
+ NS_IF_ADDREF(*aResult);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP CookieNotification::GetBaseDomain(nsACString& aBaseDomain) {
+ aBaseDomain = mBaseDomain;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CookieNotification::GetBatchDeletedCookies(nsIArray** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ NS_ENSURE_TRUE(mAction == nsICookieNotification::COOKIES_BATCH_DELETED,
+ NS_ERROR_NOT_AVAILABLE);
+
+ *aResult = mBatchDeletedCookies;
+ NS_IF_ADDREF(*aResult);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CookieNotification::GetBrowsingContextId(uint64_t* aResult) {
+ *aResult = mBrowsingContextId;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CookieNotification::GetBrowsingContext(dom::BrowsingContext** aResult) {
+ *aResult = dom::BrowsingContext::Get(mBrowsingContextId).take();
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(CookieNotification, nsICookieNotification)
+
+} // namespace mozilla::net
diff --git a/netwerk/cookie/CookieNotification.h b/netwerk/cookie/CookieNotification.h
new file mode 100644
index 0000000000..c619ce56c1
--- /dev/null
+++ b/netwerk/cookie/CookieNotification.h
@@ -0,0 +1,46 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_net_CookieNotification_h
+#define mozilla_net_CookieNotification_h
+
+#include "nsIArray.h"
+#include "nsICookieNotification.h"
+#include "nsICookie.h"
+#include "nsCOMPtr.h"
+#include "nsTArray.h"
+#include "nsString.h"
+
+namespace mozilla::net {
+
+class CookieNotification final : public nsICookieNotification {
+ public:
+ // nsISupports
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSICOOKIENOTIFICATION
+
+ explicit CookieNotification(nsICookieNotification::Action aAction,
+ nsICookie* aCookie, const nsACString& aBaseDomain,
+ nsIArray* aBatchDeletedCookies = nullptr,
+ uint64_t aBrowsingContextId = 0)
+ : mAction(aAction),
+ mCookie(aCookie),
+ mBaseDomain(aBaseDomain),
+ mBatchDeletedCookies(aBatchDeletedCookies),
+ mBrowsingContextId(aBrowsingContextId){};
+
+ private:
+ nsICookieNotification::Action mAction;
+ nsCOMPtr<nsICookie> mCookie;
+ nsCString mBaseDomain;
+ nsCOMPtr<nsIArray> mBatchDeletedCookies;
+ uint64_t mBrowsingContextId = 0;
+
+ ~CookieNotification() = default;
+};
+
+} // namespace mozilla::net
+
+#endif // mozilla_net_CookieNotification_h
diff --git a/netwerk/cookie/CookiePersistentStorage.cpp b/netwerk/cookie/CookiePersistentStorage.cpp
new file mode 100644
index 0000000000..bb4e64f0f1
--- /dev/null
+++ b/netwerk/cookie/CookiePersistentStorage.cpp
@@ -0,0 +1,2134 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "Cookie.h"
+#include "CookieCommons.h"
+#include "CookieLogging.h"
+#include "CookiePersistentStorage.h"
+
+#include "mozilla/FileUtils.h"
+#include "mozilla/glean/GleanMetrics.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/Telemetry.h"
+#include "mozIStorageAsyncStatement.h"
+#include "mozIStorageError.h"
+#include "mozIStorageFunction.h"
+#include "mozIStorageService.h"
+#include "mozStorageHelper.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsICookieNotification.h"
+#include "nsICookieService.h"
+#include "nsIEffectiveTLDService.h"
+#include "nsILineInputStream.h"
+#include "nsIURIMutator.h"
+#include "nsNetUtil.h"
+#include "nsVariant.h"
+#include "prprf.h"
+
+// XXX_hack. See bug 178993.
+// This is a hack to hide HttpOnly cookies from older browsers
+#define HTTP_ONLY_PREFIX "#HttpOnly_"
+
+constexpr auto COOKIES_SCHEMA_VERSION = 13;
+
+// parameter indexes; see |Read|
+constexpr auto IDX_NAME = 0;
+constexpr auto IDX_VALUE = 1;
+constexpr auto IDX_HOST = 2;
+constexpr auto IDX_PATH = 3;
+constexpr auto IDX_EXPIRY = 4;
+constexpr auto IDX_LAST_ACCESSED = 5;
+constexpr auto IDX_CREATION_TIME = 6;
+constexpr auto IDX_SECURE = 7;
+constexpr auto IDX_HTTPONLY = 8;
+constexpr auto IDX_ORIGIN_ATTRIBUTES = 9;
+constexpr auto IDX_SAME_SITE = 10;
+constexpr auto IDX_RAW_SAME_SITE = 11;
+constexpr auto IDX_SCHEME_MAP = 12;
+constexpr auto IDX_PARTITIONED_ATTRIBUTE_SET = 13;
+
+#define COOKIES_FILE "cookies.sqlite"
+
+namespace mozilla {
+namespace net {
+
+namespace {
+
+void BindCookieParameters(mozIStorageBindingParamsArray* aParamsArray,
+ const CookieKey& aKey, const Cookie* aCookie) {
+ NS_ASSERTION(aParamsArray,
+ "Null params array passed to BindCookieParameters!");
+ NS_ASSERTION(aCookie, "Null cookie passed to BindCookieParameters!");
+
+ // Use the asynchronous binding methods to ensure that we do not acquire the
+ // database lock.
+ nsCOMPtr<mozIStorageBindingParams> params;
+ DebugOnly<nsresult> rv =
+ aParamsArray->NewBindingParams(getter_AddRefs(params));
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ nsAutoCString suffix;
+ aKey.mOriginAttributes.CreateSuffix(suffix);
+ rv = params->BindUTF8StringByName("originAttributes"_ns, suffix);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ rv = params->BindUTF8StringByName("name"_ns, aCookie->Name());
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ rv = params->BindUTF8StringByName("value"_ns, aCookie->Value());
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ rv = params->BindUTF8StringByName("host"_ns, aCookie->Host());
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ rv = params->BindUTF8StringByName("path"_ns, aCookie->Path());
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ rv = params->BindInt64ByName("expiry"_ns, aCookie->Expiry());
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ rv = params->BindInt64ByName("lastAccessed"_ns, aCookie->LastAccessed());
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ rv = params->BindInt64ByName("creationTime"_ns, aCookie->CreationTime());
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ rv = params->BindInt32ByName("isSecure"_ns, aCookie->IsSecure());
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ rv = params->BindInt32ByName("isHttpOnly"_ns, aCookie->IsHttpOnly());
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ rv = params->BindInt32ByName("sameSite"_ns, aCookie->SameSite());
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ rv = params->BindInt32ByName("rawSameSite"_ns, aCookie->RawSameSite());
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ rv = params->BindInt32ByName("schemeMap"_ns, aCookie->SchemeMap());
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ rv = params->BindInt32ByName("isPartitionedAttributeSet"_ns,
+ aCookie->RawIsPartitioned());
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ // Bind the params to the array.
+ rv = aParamsArray->AddParams(params);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+}
+
+class ConvertAppIdToOriginAttrsSQLFunction final : public mozIStorageFunction {
+ ~ConvertAppIdToOriginAttrsSQLFunction() = default;
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_MOZISTORAGEFUNCTION
+};
+
+NS_IMPL_ISUPPORTS(ConvertAppIdToOriginAttrsSQLFunction, mozIStorageFunction);
+
+NS_IMETHODIMP
+ConvertAppIdToOriginAttrsSQLFunction::OnFunctionCall(
+ mozIStorageValueArray* aFunctionArguments, nsIVariant** aResult) {
+ nsresult rv;
+ int32_t inIsolatedMozBrowser;
+
+ rv = aFunctionArguments->GetInt32(1, &inIsolatedMozBrowser);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Create an originAttributes object by inIsolatedMozBrowser.
+ // Then create the originSuffix string from this object.
+ OriginAttributes attrs(inIsolatedMozBrowser != 0);
+ nsAutoCString suffix;
+ attrs.CreateSuffix(suffix);
+
+ RefPtr<nsVariant> outVar(new nsVariant());
+ rv = outVar->SetAsAUTF8String(suffix);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ outVar.forget(aResult);
+ return NS_OK;
+}
+
+class SetAppIdFromOriginAttributesSQLFunction final
+ : public mozIStorageFunction {
+ ~SetAppIdFromOriginAttributesSQLFunction() = default;
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_MOZISTORAGEFUNCTION
+};
+
+NS_IMPL_ISUPPORTS(SetAppIdFromOriginAttributesSQLFunction, mozIStorageFunction);
+
+NS_IMETHODIMP
+SetAppIdFromOriginAttributesSQLFunction::OnFunctionCall(
+ mozIStorageValueArray* aFunctionArguments, nsIVariant** aResult) {
+ nsresult rv;
+ nsAutoCString suffix;
+ OriginAttributes attrs;
+
+ rv = aFunctionArguments->GetUTF8String(0, suffix);
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool success = attrs.PopulateFromSuffix(suffix);
+ NS_ENSURE_TRUE(success, NS_ERROR_FAILURE);
+
+ RefPtr<nsVariant> outVar(new nsVariant());
+ rv = outVar->SetAsInt32(0); // deprecated appId!
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ outVar.forget(aResult);
+ return NS_OK;
+}
+
+class SetInBrowserFromOriginAttributesSQLFunction final
+ : public mozIStorageFunction {
+ ~SetInBrowserFromOriginAttributesSQLFunction() = default;
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_MOZISTORAGEFUNCTION
+};
+
+NS_IMPL_ISUPPORTS(SetInBrowserFromOriginAttributesSQLFunction,
+ mozIStorageFunction);
+
+NS_IMETHODIMP
+SetInBrowserFromOriginAttributesSQLFunction::OnFunctionCall(
+ mozIStorageValueArray* aFunctionArguments, nsIVariant** aResult) {
+ nsresult rv;
+ nsAutoCString suffix;
+ OriginAttributes attrs;
+
+ rv = aFunctionArguments->GetUTF8String(0, suffix);
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool success = attrs.PopulateFromSuffix(suffix);
+ NS_ENSURE_TRUE(success, NS_ERROR_FAILURE);
+
+ RefPtr<nsVariant> outVar(new nsVariant());
+ rv = outVar->SetAsInt32(attrs.mInIsolatedMozBrowser);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ outVar.forget(aResult);
+ return NS_OK;
+}
+
+/******************************************************************************
+ * DBListenerErrorHandler impl:
+ * Parent class for our async storage listeners that handles the logging of
+ * errors.
+ ******************************************************************************/
+class DBListenerErrorHandler : public mozIStorageStatementCallback {
+ protected:
+ explicit DBListenerErrorHandler(CookiePersistentStorage* dbState)
+ : mStorage(dbState) {}
+ RefPtr<CookiePersistentStorage> mStorage;
+ virtual const char* GetOpType() = 0;
+
+ public:
+ NS_IMETHOD HandleError(mozIStorageError* aError) override {
+ if (MOZ_LOG_TEST(gCookieLog, LogLevel::Warning)) {
+ int32_t result = -1;
+ aError->GetResult(&result);
+
+ nsAutoCString message;
+ aError->GetMessage(message);
+ COOKIE_LOGSTRING(
+ LogLevel::Warning,
+ ("DBListenerErrorHandler::HandleError(): Error %d occurred while "
+ "performing operation '%s' with message '%s'; rebuilding database.",
+ result, GetOpType(), message.get()));
+ }
+
+ // Rebuild the database.
+ mStorage->HandleCorruptDB();
+
+ return NS_OK;
+ }
+};
+
+/******************************************************************************
+ * InsertCookieDBListener impl:
+ * mozIStorageStatementCallback used to track asynchronous insertion operations.
+ ******************************************************************************/
+class InsertCookieDBListener final : public DBListenerErrorHandler {
+ private:
+ const char* GetOpType() override { return "INSERT"; }
+
+ ~InsertCookieDBListener() = default;
+
+ public:
+ NS_DECL_ISUPPORTS
+
+ explicit InsertCookieDBListener(CookiePersistentStorage* dbState)
+ : DBListenerErrorHandler(dbState) {}
+ NS_IMETHOD HandleResult(mozIStorageResultSet* /*aResultSet*/) override {
+ MOZ_ASSERT_UNREACHABLE(
+ "Unexpected call to "
+ "InsertCookieDBListener::HandleResult");
+ return NS_OK;
+ }
+ NS_IMETHOD HandleCompletion(uint16_t aReason) override {
+ // If we were rebuilding the db and we succeeded, make our mCorruptFlag say
+ // so.
+ if (mStorage->GetCorruptFlag() == CookiePersistentStorage::REBUILDING &&
+ aReason == mozIStorageStatementCallback::REASON_FINISHED) {
+ COOKIE_LOGSTRING(
+ LogLevel::Debug,
+ ("InsertCookieDBListener::HandleCompletion(): rebuild complete"));
+ mStorage->SetCorruptFlag(CookiePersistentStorage::OK);
+ }
+
+ // This notification is just for testing.
+ nsCOMPtr<nsIObserverService> os = services::GetObserverService();
+ if (os) {
+ os->NotifyObservers(nullptr, "cookie-saved-on-disk", nullptr);
+ }
+
+ return NS_OK;
+ }
+};
+
+NS_IMPL_ISUPPORTS(InsertCookieDBListener, mozIStorageStatementCallback)
+
+/******************************************************************************
+ * UpdateCookieDBListener impl:
+ * mozIStorageStatementCallback used to track asynchronous update operations.
+ ******************************************************************************/
+class UpdateCookieDBListener final : public DBListenerErrorHandler {
+ private:
+ const char* GetOpType() override { return "UPDATE"; }
+
+ ~UpdateCookieDBListener() = default;
+
+ public:
+ NS_DECL_ISUPPORTS
+
+ explicit UpdateCookieDBListener(CookiePersistentStorage* dbState)
+ : DBListenerErrorHandler(dbState) {}
+ NS_IMETHOD HandleResult(mozIStorageResultSet* /*aResultSet*/) override {
+ MOZ_ASSERT_UNREACHABLE(
+ "Unexpected call to "
+ "UpdateCookieDBListener::HandleResult");
+ return NS_OK;
+ }
+ NS_IMETHOD HandleCompletion(uint16_t /*aReason*/) override { return NS_OK; }
+};
+
+NS_IMPL_ISUPPORTS(UpdateCookieDBListener, mozIStorageStatementCallback)
+
+/******************************************************************************
+ * RemoveCookieDBListener impl:
+ * mozIStorageStatementCallback used to track asynchronous removal operations.
+ ******************************************************************************/
+class RemoveCookieDBListener final : public DBListenerErrorHandler {
+ private:
+ const char* GetOpType() override { return "REMOVE"; }
+
+ ~RemoveCookieDBListener() = default;
+
+ public:
+ NS_DECL_ISUPPORTS
+
+ explicit RemoveCookieDBListener(CookiePersistentStorage* dbState)
+ : DBListenerErrorHandler(dbState) {}
+ NS_IMETHOD HandleResult(mozIStorageResultSet* /*aResultSet*/) override {
+ MOZ_ASSERT_UNREACHABLE(
+ "Unexpected call to "
+ "RemoveCookieDBListener::HandleResult");
+ return NS_OK;
+ }
+ NS_IMETHOD HandleCompletion(uint16_t /*aReason*/) override { return NS_OK; }
+};
+
+NS_IMPL_ISUPPORTS(RemoveCookieDBListener, mozIStorageStatementCallback)
+
+/******************************************************************************
+ * CloseCookieDBListener imp:
+ * Static mozIStorageCompletionCallback used to notify when the database is
+ * successfully closed.
+ ******************************************************************************/
+class CloseCookieDBListener final : public mozIStorageCompletionCallback {
+ ~CloseCookieDBListener() = default;
+
+ public:
+ explicit CloseCookieDBListener(CookiePersistentStorage* dbState)
+ : mStorage(dbState) {}
+ RefPtr<CookiePersistentStorage> mStorage;
+ NS_DECL_ISUPPORTS
+
+ NS_IMETHOD Complete(nsresult /*status*/, nsISupports* /*value*/) override {
+ mStorage->HandleDBClosed();
+ return NS_OK;
+ }
+};
+
+NS_IMPL_ISUPPORTS(CloseCookieDBListener, mozIStorageCompletionCallback)
+
+} // namespace
+
+// static
+already_AddRefed<CookiePersistentStorage> CookiePersistentStorage::Create() {
+ RefPtr<CookiePersistentStorage> storage = new CookiePersistentStorage();
+ storage->Init();
+ storage->Activate();
+
+ return storage.forget();
+}
+
+CookiePersistentStorage::CookiePersistentStorage()
+ : mMonitor("CookiePersistentStorage"),
+ mInitialized(false),
+ mCorruptFlag(OK) {}
+
+void CookiePersistentStorage::NotifyChangedInternal(
+ nsICookieNotification* aNotification, bool aOldCookieIsSession) {
+ MOZ_ASSERT(aNotification);
+ // Notify for topic "session-cookie-changed" to update the copy of session
+ // cookies in session restore component.
+
+ nsICookieNotification::Action action = aNotification->GetAction();
+
+ // Filter out notifications for individual non-session cookies.
+ if (action == nsICookieNotification::COOKIE_CHANGED ||
+ action == nsICookieNotification::COOKIE_DELETED ||
+ action == nsICookieNotification::COOKIE_ADDED) {
+ nsCOMPtr<nsICookie> xpcCookie;
+ DebugOnly<nsresult> rv =
+ aNotification->GetCookie(getter_AddRefs(xpcCookie));
+ MOZ_ASSERT(NS_SUCCEEDED(rv) && xpcCookie);
+ const Cookie& cookie = xpcCookie->AsCookie();
+ if (!cookie.IsSession() && !aOldCookieIsSession) {
+ return;
+ }
+ }
+
+ nsCOMPtr<nsIObserverService> os = services::GetObserverService();
+ if (os) {
+ os->NotifyObservers(aNotification, "session-cookie-changed", u"");
+ }
+}
+
+void CookiePersistentStorage::RemoveAllInternal() {
+ // clear the cookie file
+ if (mDBConn) {
+ nsCOMPtr<mozIStorageAsyncStatement> stmt;
+ nsresult rv = mDBConn->CreateAsyncStatement("DELETE FROM moz_cookies"_ns,
+ getter_AddRefs(stmt));
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<mozIStoragePendingStatement> handle;
+ rv = stmt->ExecuteAsync(mRemoveListener, getter_AddRefs(handle));
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ } else {
+ // Recreate the database.
+ COOKIE_LOGSTRING(LogLevel::Debug,
+ ("RemoveAll(): corruption detected with rv 0x%" PRIx32,
+ static_cast<uint32_t>(rv)));
+ HandleCorruptDB();
+ }
+ }
+}
+
+void CookiePersistentStorage::HandleCorruptDB() {
+ COOKIE_LOGSTRING(LogLevel::Debug,
+ ("HandleCorruptDB(): CookieStorage %p has mCorruptFlag %u",
+ this, mCorruptFlag));
+
+ // Mark the database corrupt, so the close listener can begin reconstructing
+ // it.
+ switch (mCorruptFlag) {
+ case OK: {
+ // Move to 'closing' state.
+ mCorruptFlag = CLOSING_FOR_REBUILD;
+
+ CleanupCachedStatements();
+ mDBConn->AsyncClose(mCloseListener);
+ CleanupDBConnection();
+ break;
+ }
+ case CLOSING_FOR_REBUILD: {
+ // We had an error while waiting for close completion. That's OK, just
+ // ignore it -- we're rebuilding anyway.
+ return;
+ }
+ case REBUILDING: {
+ // We had an error while rebuilding the DB. Game over. Close the database
+ // and let the close handler do nothing; then we'll move it out of the
+ // way.
+ CleanupCachedStatements();
+ if (mDBConn) {
+ mDBConn->AsyncClose(mCloseListener);
+ }
+ CleanupDBConnection();
+ break;
+ }
+ }
+}
+
+void CookiePersistentStorage::RemoveCookiesWithOriginAttributes(
+ const OriginAttributesPattern& aPattern, const nsACString& aBaseDomain) {
+ mozStorageTransaction transaction(mDBConn, false);
+
+ // XXX Handle the error, bug 1696130.
+ Unused << NS_WARN_IF(NS_FAILED(transaction.Start()));
+
+ CookieStorage::RemoveCookiesWithOriginAttributes(aPattern, aBaseDomain);
+
+ DebugOnly<nsresult> rv = transaction.Commit();
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+}
+
+void CookiePersistentStorage::RemoveCookiesFromExactHost(
+ const nsACString& aHost, const nsACString& aBaseDomain,
+ const OriginAttributesPattern& aPattern) {
+ mozStorageTransaction transaction(mDBConn, false);
+
+ // XXX Handle the error, bug 1696130.
+ Unused << NS_WARN_IF(NS_FAILED(transaction.Start()));
+
+ CookieStorage::RemoveCookiesFromExactHost(aHost, aBaseDomain, aPattern);
+
+ DebugOnly<nsresult> rv = transaction.Commit();
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+}
+
+void CookiePersistentStorage::RemoveCookieFromDB(const Cookie& aCookie) {
+ // if it's a non-session cookie, remove it from the db
+ if (aCookie.IsSession() || !mDBConn) {
+ return;
+ }
+
+ nsCOMPtr<mozIStorageBindingParamsArray> paramsArray;
+ mStmtDelete->NewBindingParamsArray(getter_AddRefs(paramsArray));
+
+ PrepareCookieRemoval(aCookie, paramsArray);
+
+ DebugOnly<nsresult> rv = mStmtDelete->BindParameters(paramsArray);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ nsCOMPtr<mozIStoragePendingStatement> handle;
+ rv = mStmtDelete->ExecuteAsync(mRemoveListener, getter_AddRefs(handle));
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+}
+
+void CookiePersistentStorage::PrepareCookieRemoval(
+ const Cookie& aCookie, mozIStorageBindingParamsArray* aParamsArray) {
+ // if it's a non-session cookie, remove it from the db
+ if (aCookie.IsSession() || !mDBConn) {
+ return;
+ }
+
+ nsCOMPtr<mozIStorageBindingParams> params;
+ aParamsArray->NewBindingParams(getter_AddRefs(params));
+
+ DebugOnly<nsresult> rv =
+ params->BindUTF8StringByName("name"_ns, aCookie.Name());
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ rv = params->BindUTF8StringByName("host"_ns, aCookie.Host());
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ rv = params->BindUTF8StringByName("path"_ns, aCookie.Path());
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ nsAutoCString suffix;
+ aCookie.OriginAttributesRef().CreateSuffix(suffix);
+ rv = params->BindUTF8StringByName("originAttributes"_ns, suffix);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ rv = aParamsArray->AddParams(params);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+}
+
+// Null out the statements.
+// This must be done before closing the connection.
+void CookiePersistentStorage::CleanupCachedStatements() {
+ mStmtInsert = nullptr;
+ mStmtDelete = nullptr;
+ mStmtUpdate = nullptr;
+}
+
+// Null out the listeners, and the database connection itself. This
+// will not null out the statements, cancel a pending read or
+// asynchronously close the connection -- these must be done
+// beforehand if necessary.
+void CookiePersistentStorage::CleanupDBConnection() {
+ MOZ_ASSERT(!mStmtInsert, "mStmtInsert has been cleaned up");
+ MOZ_ASSERT(!mStmtDelete, "mStmtDelete has been cleaned up");
+ MOZ_ASSERT(!mStmtUpdate, "mStmtUpdate has been cleaned up");
+
+ // Null out the database connections. If 'mDBConn' has not been used for any
+ // asynchronous operations yet, this will synchronously close it; otherwise,
+ // it's expected that the caller has performed an AsyncClose prior.
+ mDBConn = nullptr;
+
+ // Manually null out our listeners. This is necessary because they hold a
+ // strong ref to the CookieStorage itself. They'll stay alive until whatever
+ // statements are still executing complete.
+ mInsertListener = nullptr;
+ mUpdateListener = nullptr;
+ mRemoveListener = nullptr;
+ mCloseListener = nullptr;
+}
+
+void CookiePersistentStorage::Close() {
+ if (mThread) {
+ mThread->Shutdown();
+ mThread = nullptr;
+ }
+
+ // Cleanup cached statements before we can close anything.
+ CleanupCachedStatements();
+
+ if (mDBConn) {
+ // Asynchronously close the connection. We will null it below.
+ mDBConn->AsyncClose(mCloseListener);
+ }
+
+ CleanupDBConnection();
+
+ mInitialized = false;
+ mInitializedDBConn = false;
+}
+
+void CookiePersistentStorage::StoreCookie(
+ const nsACString& aBaseDomain, const OriginAttributes& aOriginAttributes,
+ Cookie* aCookie) {
+ // if it's a non-session cookie and hasn't just been read from the db, write
+ // it out.
+ if (aCookie->IsSession() || !mDBConn) {
+ return;
+ }
+
+ nsCOMPtr<mozIStorageBindingParamsArray> paramsArray;
+ mStmtInsert->NewBindingParamsArray(getter_AddRefs(paramsArray));
+
+ CookieKey key(aBaseDomain, aOriginAttributes);
+ BindCookieParameters(paramsArray, key, aCookie);
+
+ MaybeStoreCookiesToDB(paramsArray);
+}
+
+void CookiePersistentStorage::MaybeStoreCookiesToDB(
+ mozIStorageBindingParamsArray* aParamsArray) {
+ if (!aParamsArray) {
+ return;
+ }
+
+ uint32_t length;
+ aParamsArray->GetLength(&length);
+ if (!length) {
+ return;
+ }
+
+ DebugOnly<nsresult> rv = mStmtInsert->BindParameters(aParamsArray);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ nsCOMPtr<mozIStoragePendingStatement> handle;
+ rv = mStmtInsert->ExecuteAsync(mInsertListener, getter_AddRefs(handle));
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+}
+
+void CookiePersistentStorage::StaleCookies(const nsTArray<Cookie*>& aCookieList,
+ int64_t aCurrentTimeInUsec) {
+ // Create an array of parameters to bind to our update statement. Batching
+ // is OK here since we're updating cookies with no interleaved operations.
+ nsCOMPtr<mozIStorageBindingParamsArray> paramsArray;
+ mozIStorageAsyncStatement* stmt = mStmtUpdate;
+ if (mDBConn) {
+ stmt->NewBindingParamsArray(getter_AddRefs(paramsArray));
+ }
+
+ int32_t count = aCookieList.Length();
+ for (int32_t i = 0; i < count; ++i) {
+ Cookie* cookie = aCookieList.ElementAt(i);
+
+ if (cookie->IsStale()) {
+ UpdateCookieInList(cookie, aCurrentTimeInUsec, paramsArray);
+ }
+ }
+ // Update the database now if necessary.
+ if (paramsArray) {
+ uint32_t length;
+ paramsArray->GetLength(&length);
+ if (length) {
+ DebugOnly<nsresult> rv = stmt->BindParameters(paramsArray);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ nsCOMPtr<mozIStoragePendingStatement> handle;
+ rv = stmt->ExecuteAsync(mUpdateListener, getter_AddRefs(handle));
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ }
+ }
+}
+
+void CookiePersistentStorage::UpdateCookieInList(
+ Cookie* aCookie, int64_t aLastAccessed,
+ mozIStorageBindingParamsArray* aParamsArray) {
+ MOZ_ASSERT(aCookie);
+
+ // udpate the lastAccessed timestamp
+ aCookie->SetLastAccessed(aLastAccessed);
+
+ // if it's a non-session cookie, update it in the db too
+ if (!aCookie->IsSession() && aParamsArray) {
+ // Create our params holder.
+ nsCOMPtr<mozIStorageBindingParams> params;
+ aParamsArray->NewBindingParams(getter_AddRefs(params));
+
+ // Bind our parameters.
+ DebugOnly<nsresult> rv =
+ params->BindInt64ByName("lastAccessed"_ns, aLastAccessed);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ rv = params->BindUTF8StringByName("name"_ns, aCookie->Name());
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ rv = params->BindUTF8StringByName("host"_ns, aCookie->Host());
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ rv = params->BindUTF8StringByName("path"_ns, aCookie->Path());
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ nsAutoCString suffix;
+ aCookie->OriginAttributesRef().CreateSuffix(suffix);
+ rv = params->BindUTF8StringByName("originAttributes"_ns, suffix);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ // Add our bound parameters to the array.
+ rv = aParamsArray->AddParams(params);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ }
+}
+
+void CookiePersistentStorage::DeleteFromDB(
+ mozIStorageBindingParamsArray* aParamsArray) {
+ uint32_t length;
+ aParamsArray->GetLength(&length);
+ if (length) {
+ DebugOnly<nsresult> rv = mStmtDelete->BindParameters(aParamsArray);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ nsCOMPtr<mozIStoragePendingStatement> handle;
+ rv = mStmtDelete->ExecuteAsync(mRemoveListener, getter_AddRefs(handle));
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ }
+}
+
+void CookiePersistentStorage::Activate() {
+ MOZ_ASSERT(!mThread, "already have a cookie thread");
+
+ mStorageService = do_GetService("@mozilla.org/storage/service;1");
+ MOZ_ASSERT(mStorageService);
+
+ mTLDService = do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID);
+ MOZ_ASSERT(mTLDService);
+
+ // Get our cookie file.
+ nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(mCookieFile));
+ if (NS_FAILED(rv)) {
+ // We've already set up our CookieStorages appropriately; nothing more to
+ // do.
+ COOKIE_LOGSTRING(LogLevel::Warning,
+ ("InitCookieStorages(): couldn't get cookie file"));
+
+ mInitializedDBConn = true;
+ mInitialized = true;
+ return;
+ }
+
+ mCookieFile->AppendNative(nsLiteralCString(COOKIES_FILE));
+
+ NS_ENSURE_SUCCESS_VOID(NS_NewNamedThread("Cookie", getter_AddRefs(mThread)));
+
+ RefPtr<CookiePersistentStorage> self = this;
+ nsCOMPtr<nsIRunnable> runnable =
+ NS_NewRunnableFunction("CookiePersistentStorage::Activate", [self] {
+ MonitorAutoLock lock(self->mMonitor);
+
+ // Attempt to open and read the database. If TryInitDB() returns
+ // RESULT_RETRY, do so.
+ OpenDBResult result = self->TryInitDB(false);
+ if (result == RESULT_RETRY) {
+ // Database may be corrupt. Synchronously close the connection, clean
+ // up the default CookieStorage, and try again.
+ COOKIE_LOGSTRING(LogLevel::Warning,
+ ("InitCookieStorages(): retrying TryInitDB()"));
+ self->CleanupCachedStatements();
+ self->CleanupDBConnection();
+ result = self->TryInitDB(true);
+ if (result == RESULT_RETRY) {
+ // We're done. Change the code to failure so we clean up below.
+ result = RESULT_FAILURE;
+ }
+ }
+
+ if (result == RESULT_FAILURE) {
+ COOKIE_LOGSTRING(
+ LogLevel::Warning,
+ ("InitCookieStorages(): TryInitDB() failed, closing connection"));
+
+ // Connection failure is unrecoverable. Clean up our connection. We
+ // can run fine without persistent storage -- e.g. if there's no
+ // profile.
+ self->CleanupCachedStatements();
+ self->CleanupDBConnection();
+
+ // No need to initialize mDBConn
+ self->mInitializedDBConn = true;
+ }
+
+ self->mInitialized = true;
+
+ NS_DispatchToMainThread(
+ NS_NewRunnableFunction("CookiePersistentStorage::InitDBConn",
+ [self] { self->InitDBConn(); }));
+ self->mMonitor.Notify();
+ });
+
+ mThread->Dispatch(runnable, NS_DISPATCH_NORMAL);
+}
+
+/* Attempt to open and read the database. If 'aRecreateDB' is true, try to
+ * move the existing database file out of the way and create a new one.
+ *
+ * @returns RESULT_OK if opening or creating the database succeeded;
+ * RESULT_RETRY if the database cannot be opened, is corrupt, or some
+ * other failure occurred that might be resolved by recreating the
+ * database; or RESULT_FAILED if there was an unrecoverable error and
+ * we must run without a database.
+ *
+ * If RESULT_RETRY or RESULT_FAILED is returned, the caller should perform
+ * cleanup of the default CookieStorage.
+ */
+CookiePersistentStorage::OpenDBResult CookiePersistentStorage::TryInitDB(
+ bool aRecreateDB) {
+ NS_ASSERTION(!mDBConn, "nonnull mDBConn");
+ NS_ASSERTION(!mStmtInsert, "nonnull mStmtInsert");
+ NS_ASSERTION(!mInsertListener, "nonnull mInsertListener");
+ NS_ASSERTION(!mSyncConn, "nonnull mSyncConn");
+ NS_ASSERTION(NS_GetCurrentThread() == mThread, "non cookie thread");
+
+ // Ditch an existing db, if we've been told to (i.e. it's corrupt). We don't
+ // want to delete it outright, since it may be useful for debugging purposes,
+ // so we move it out of the way.
+ nsresult rv;
+ if (aRecreateDB) {
+ nsCOMPtr<nsIFile> backupFile;
+ mCookieFile->Clone(getter_AddRefs(backupFile));
+ rv = backupFile->MoveToNative(nullptr,
+ nsLiteralCString(COOKIES_FILE ".bak"));
+ NS_ENSURE_SUCCESS(rv, RESULT_FAILURE);
+ }
+
+ // This block provides scope for the Telemetry AutoTimer
+ {
+ Telemetry::AutoTimer<Telemetry::MOZ_SQLITE_COOKIES_OPEN_READAHEAD_MS>
+ telemetry;
+ ReadAheadFile(mCookieFile);
+
+ // open a connection to the cookie database, and only cache our connection
+ // and statements upon success. The connection is opened unshared to
+ // eliminate cache contention between the main and background threads.
+ rv = mStorageService->OpenUnsharedDatabase(
+ mCookieFile, mozIStorageService::CONNECTION_DEFAULT,
+ getter_AddRefs(mSyncConn));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+ }
+
+ auto guard = MakeScopeExit([&] { mSyncConn = nullptr; });
+
+ bool tableExists = false;
+ mSyncConn->TableExists("moz_cookies"_ns, &tableExists);
+ if (!tableExists) {
+ rv = CreateTable();
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ } else {
+ // table already exists; check the schema version before reading
+ int32_t dbSchemaVersion;
+ rv = mSyncConn->GetSchemaVersion(&dbSchemaVersion);
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ // Start a transaction for the whole migration block.
+ mozStorageTransaction transaction(mSyncConn, true);
+
+ // XXX Handle the error, bug 1696130.
+ Unused << NS_WARN_IF(NS_FAILED(transaction.Start()));
+
+ switch (dbSchemaVersion) {
+ // Upgrading.
+ // Every time you increment the database schema, you need to implement
+ // the upgrading code from the previous version to the new one. If
+ // migration fails for any reason, it's a bug -- so we return RESULT_RETRY
+ // such that the original database will be saved, in the hopes that we
+ // might one day see it and fix it.
+ case 1: {
+ // Add the lastAccessed column to the table.
+ rv = mSyncConn->ExecuteSimpleSQL(nsLiteralCString(
+ "ALTER TABLE moz_cookies ADD lastAccessed INTEGER"));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+ }
+ // Fall through to the next upgrade.
+ [[fallthrough]];
+
+ case 2: {
+ // Add the baseDomain column and index to the table.
+ rv = mSyncConn->ExecuteSimpleSQL(
+ "ALTER TABLE moz_cookies ADD baseDomain TEXT"_ns);
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ // Compute the baseDomains for the table. This must be done eagerly
+ // otherwise we won't be able to synchronously read in individual
+ // domains on demand.
+ const int64_t SCHEMA2_IDX_ID = 0;
+ const int64_t SCHEMA2_IDX_HOST = 1;
+ nsCOMPtr<mozIStorageStatement> select;
+ rv = mSyncConn->CreateStatement("SELECT id, host FROM moz_cookies"_ns,
+ getter_AddRefs(select));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ nsCOMPtr<mozIStorageStatement> update;
+ rv = mSyncConn->CreateStatement(
+ nsLiteralCString("UPDATE moz_cookies SET baseDomain = "
+ ":baseDomain WHERE id = :id"),
+ getter_AddRefs(update));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ nsCString baseDomain;
+ nsCString host;
+ bool hasResult;
+ while (true) {
+ rv = select->ExecuteStep(&hasResult);
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ if (!hasResult) {
+ break;
+ }
+
+ int64_t id = select->AsInt64(SCHEMA2_IDX_ID);
+ select->GetUTF8String(SCHEMA2_IDX_HOST, host);
+
+ rv = CookieCommons::GetBaseDomainFromHost(mTLDService, host,
+ baseDomain);
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ mozStorageStatementScoper scoper(update);
+
+ rv = update->BindUTF8StringByName("baseDomain"_ns, baseDomain);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ rv = update->BindInt64ByName("id"_ns, id);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ rv = update->ExecuteStep(&hasResult);
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+ }
+
+ // Create an index on baseDomain.
+ rv = mSyncConn->ExecuteSimpleSQL(nsLiteralCString(
+ "CREATE INDEX moz_basedomain ON moz_cookies (baseDomain)"));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+ }
+ // Fall through to the next upgrade.
+ [[fallthrough]];
+
+ case 3: {
+ // Add the creationTime column to the table, and create a unique index
+ // on (name, host, path). Before we do this, we have to purge the table
+ // of expired cookies such that we know that the (name, host, path)
+ // index is truly unique -- otherwise we can't create the index. Note
+ // that we can't just execute a statement to delete all rows where the
+ // expiry column is in the past -- doing so would rely on the clock
+ // (both now and when previous cookies were set) being monotonic.
+
+ // Select the whole table, and order by the fields we're interested in.
+ // This means we can simply do a linear traversal of the results and
+ // check for duplicates as we go.
+ const int64_t SCHEMA3_IDX_ID = 0;
+ const int64_t SCHEMA3_IDX_NAME = 1;
+ const int64_t SCHEMA3_IDX_HOST = 2;
+ const int64_t SCHEMA3_IDX_PATH = 3;
+ nsCOMPtr<mozIStorageStatement> select;
+ rv = mSyncConn->CreateStatement(
+ nsLiteralCString(
+ "SELECT id, name, host, path FROM moz_cookies "
+ "ORDER BY name ASC, host ASC, path ASC, expiry ASC"),
+ getter_AddRefs(select));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ nsCOMPtr<mozIStorageStatement> deleteExpired;
+ rv = mSyncConn->CreateStatement(
+ "DELETE FROM moz_cookies WHERE id = :id"_ns,
+ getter_AddRefs(deleteExpired));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ // Read the first row.
+ bool hasResult;
+ rv = select->ExecuteStep(&hasResult);
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ if (hasResult) {
+ nsCString name1;
+ nsCString host1;
+ nsCString path1;
+ int64_t id1 = select->AsInt64(SCHEMA3_IDX_ID);
+ select->GetUTF8String(SCHEMA3_IDX_NAME, name1);
+ select->GetUTF8String(SCHEMA3_IDX_HOST, host1);
+ select->GetUTF8String(SCHEMA3_IDX_PATH, path1);
+
+ nsCString name2;
+ nsCString host2;
+ nsCString path2;
+ while (true) {
+ // Read the second row.
+ rv = select->ExecuteStep(&hasResult);
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ if (!hasResult) {
+ break;
+ }
+
+ int64_t id2 = select->AsInt64(SCHEMA3_IDX_ID);
+ select->GetUTF8String(SCHEMA3_IDX_NAME, name2);
+ select->GetUTF8String(SCHEMA3_IDX_HOST, host2);
+ select->GetUTF8String(SCHEMA3_IDX_PATH, path2);
+
+ // If the two rows match in (name, host, path), we know the earlier
+ // row has an earlier expiry time. Delete it.
+ if (name1 == name2 && host1 == host2 && path1 == path2) {
+ mozStorageStatementScoper scoper(deleteExpired);
+
+ rv = deleteExpired->BindInt64ByName("id"_ns, id1);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ rv = deleteExpired->ExecuteStep(&hasResult);
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+ }
+
+ // Make the second row the first for the next iteration.
+ name1 = name2;
+ host1 = host2;
+ path1 = path2;
+ id1 = id2;
+ }
+ }
+
+ // Add the creationTime column to the table.
+ rv = mSyncConn->ExecuteSimpleSQL(nsLiteralCString(
+ "ALTER TABLE moz_cookies ADD creationTime INTEGER"));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ // Copy the id of each row into the new creationTime column.
+ rv = mSyncConn->ExecuteSimpleSQL(
+ nsLiteralCString("UPDATE moz_cookies SET creationTime = "
+ "(SELECT id WHERE id = moz_cookies.id)"));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ // Create a unique index on (name, host, path) to allow fast lookup.
+ rv = mSyncConn->ExecuteSimpleSQL(
+ nsLiteralCString("CREATE UNIQUE INDEX moz_uniqueid "
+ "ON moz_cookies (name, host, path)"));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+ }
+ // Fall through to the next upgrade.
+ [[fallthrough]];
+
+ case 4: {
+ // We need to add appId/inBrowserElement, plus change a constraint on
+ // the table (unique entries now include appId/inBrowserElement):
+ // this requires creating a new table and copying the data to it. We
+ // then rename the new table to the old name.
+ //
+ // Why we made this change: appId/inBrowserElement allow "cookie jars"
+ // for Firefox OS. We create a separate cookie namespace per {appId,
+ // inBrowserElement}. When upgrading, we convert existing cookies
+ // (which imply we're on desktop/mobile) to use {0, false}, as that is
+ // the only namespace used by a non-Firefox-OS implementation.
+
+ // Rename existing table
+ rv = mSyncConn->ExecuteSimpleSQL(nsLiteralCString(
+ "ALTER TABLE moz_cookies RENAME TO moz_cookies_old"));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ // Drop existing index (CreateTable will create new one for new table)
+ rv = mSyncConn->ExecuteSimpleSQL("DROP INDEX moz_basedomain"_ns);
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ // Create new table (with new fields and new unique constraint)
+ rv = CreateTableForSchemaVersion5();
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ // Copy data from old table, using appId/inBrowser=0 for existing rows
+ rv = mSyncConn->ExecuteSimpleSQL(nsLiteralCString(
+ "INSERT INTO moz_cookies "
+ "(baseDomain, appId, inBrowserElement, name, value, host, path, "
+ "expiry,"
+ " lastAccessed, creationTime, isSecure, isHttpOnly) "
+ "SELECT baseDomain, 0, 0, name, value, host, path, expiry,"
+ " lastAccessed, creationTime, isSecure, isHttpOnly "
+ "FROM moz_cookies_old"));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ // Drop old table
+ rv = mSyncConn->ExecuteSimpleSQL("DROP TABLE moz_cookies_old"_ns);
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ COOKIE_LOGSTRING(LogLevel::Debug,
+ ("Upgraded database to schema version 5"));
+ }
+ // Fall through to the next upgrade.
+ [[fallthrough]];
+
+ case 5: {
+ // Change in the version: Replace the columns |appId| and
+ // |inBrowserElement| by a single column |originAttributes|.
+ //
+ // Why we made this change: FxOS new security model (NSec) encapsulates
+ // "appId/inIsolatedMozBrowser" in nsIPrincipal::originAttributes to
+ // make it easier to modify the contents of this structure in the
+ // future.
+ //
+ // We do the migration in several steps:
+ // 1. Rename the old table.
+ // 2. Create a new table.
+ // 3. Copy data from the old table to the new table; convert appId and
+ // inBrowserElement to originAttributes in the meantime.
+
+ // Rename existing table.
+ rv = mSyncConn->ExecuteSimpleSQL(nsLiteralCString(
+ "ALTER TABLE moz_cookies RENAME TO moz_cookies_old"));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ // Drop existing index (CreateTable will create new one for new table).
+ rv = mSyncConn->ExecuteSimpleSQL("DROP INDEX moz_basedomain"_ns);
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ // Create new table with new fields and new unique constraint.
+ rv = CreateTableForSchemaVersion6();
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ // Copy data from old table without the two deprecated columns appId and
+ // inBrowserElement.
+ nsCOMPtr<mozIStorageFunction> convertToOriginAttrs(
+ new ConvertAppIdToOriginAttrsSQLFunction());
+ NS_ENSURE_TRUE(convertToOriginAttrs, RESULT_RETRY);
+
+ constexpr auto convertToOriginAttrsName =
+ "CONVERT_TO_ORIGIN_ATTRIBUTES"_ns;
+
+ rv = mSyncConn->CreateFunction(convertToOriginAttrsName, 2,
+ convertToOriginAttrs);
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ rv = mSyncConn->ExecuteSimpleSQL(nsLiteralCString(
+ "INSERT INTO moz_cookies "
+ "(baseDomain, originAttributes, name, value, host, path, expiry,"
+ " lastAccessed, creationTime, isSecure, isHttpOnly) "
+ "SELECT baseDomain, "
+ " CONVERT_TO_ORIGIN_ATTRIBUTES(appId, inBrowserElement),"
+ " name, value, host, path, expiry, lastAccessed, creationTime, "
+ " isSecure, isHttpOnly "
+ "FROM moz_cookies_old"));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ rv = mSyncConn->RemoveFunction(convertToOriginAttrsName);
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ // Drop old table
+ rv = mSyncConn->ExecuteSimpleSQL("DROP TABLE moz_cookies_old"_ns);
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ COOKIE_LOGSTRING(LogLevel::Debug,
+ ("Upgraded database to schema version 6"));
+ }
+ [[fallthrough]];
+
+ case 6: {
+ // We made a mistake in schema version 6. We cannot remove expected
+ // columns of any version (checked in the default case) from cookie
+ // database, because doing this would destroy the possibility of
+ // downgrading database.
+ //
+ // This version simply restores appId and inBrowserElement columns in
+ // order to fix downgrading issue even though these two columns are no
+ // longer used in the latest schema.
+ rv = mSyncConn->ExecuteSimpleSQL(nsLiteralCString(
+ "ALTER TABLE moz_cookies ADD appId INTEGER DEFAULT 0;"));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ rv = mSyncConn->ExecuteSimpleSQL(nsLiteralCString(
+ "ALTER TABLE moz_cookies ADD inBrowserElement INTEGER DEFAULT 0;"));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ // Compute and populate the values of appId and inBrwoserElement from
+ // originAttributes.
+ nsCOMPtr<mozIStorageFunction> setAppId(
+ new SetAppIdFromOriginAttributesSQLFunction());
+ NS_ENSURE_TRUE(setAppId, RESULT_RETRY);
+
+ constexpr auto setAppIdName = "SET_APP_ID"_ns;
+
+ rv = mSyncConn->CreateFunction(setAppIdName, 1, setAppId);
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ nsCOMPtr<mozIStorageFunction> setInBrowser(
+ new SetInBrowserFromOriginAttributesSQLFunction());
+ NS_ENSURE_TRUE(setInBrowser, RESULT_RETRY);
+
+ constexpr auto setInBrowserName = "SET_IN_BROWSER"_ns;
+
+ rv = mSyncConn->CreateFunction(setInBrowserName, 1, setInBrowser);
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ rv = mSyncConn->ExecuteSimpleSQL(nsLiteralCString(
+ "UPDATE moz_cookies SET appId = SET_APP_ID(originAttributes), "
+ "inBrowserElement = SET_IN_BROWSER(originAttributes);"));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ rv = mSyncConn->RemoveFunction(setAppIdName);
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ rv = mSyncConn->RemoveFunction(setInBrowserName);
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ COOKIE_LOGSTRING(LogLevel::Debug,
+ ("Upgraded database to schema version 7"));
+ }
+ [[fallthrough]];
+
+ case 7: {
+ // Remove the appId field from moz_cookies.
+ //
+ // Unfortunately sqlite doesn't support dropping columns using ALTER
+ // TABLE, so we need to go through the procedure documented in
+ // https://www.sqlite.org/lang_altertable.html.
+
+ // Drop existing index
+ rv = mSyncConn->ExecuteSimpleSQL("DROP INDEX moz_basedomain"_ns);
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ // Create a new_moz_cookies table without the appId field.
+ rv = mSyncConn->ExecuteSimpleSQL(
+ nsLiteralCString("CREATE TABLE new_moz_cookies("
+ "id INTEGER PRIMARY KEY, "
+ "baseDomain TEXT, "
+ "originAttributes TEXT NOT NULL DEFAULT '', "
+ "name TEXT, "
+ "value TEXT, "
+ "host TEXT, "
+ "path TEXT, "
+ "expiry INTEGER, "
+ "lastAccessed INTEGER, "
+ "creationTime INTEGER, "
+ "isSecure INTEGER, "
+ "isHttpOnly INTEGER, "
+ "inBrowserElement INTEGER DEFAULT 0, "
+ "CONSTRAINT moz_uniqueid UNIQUE (name, host, "
+ "path, originAttributes)"
+ ")"));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ // Move the data over.
+ rv = mSyncConn->ExecuteSimpleSQL(
+ nsLiteralCString("INSERT INTO new_moz_cookies ("
+ "id, "
+ "baseDomain, "
+ "originAttributes, "
+ "name, "
+ "value, "
+ "host, "
+ "path, "
+ "expiry, "
+ "lastAccessed, "
+ "creationTime, "
+ "isSecure, "
+ "isHttpOnly, "
+ "inBrowserElement "
+ ") SELECT "
+ "id, "
+ "baseDomain, "
+ "originAttributes, "
+ "name, "
+ "value, "
+ "host, "
+ "path, "
+ "expiry, "
+ "lastAccessed, "
+ "creationTime, "
+ "isSecure, "
+ "isHttpOnly, "
+ "inBrowserElement "
+ "FROM moz_cookies;"));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ // Drop the old table
+ rv = mSyncConn->ExecuteSimpleSQL("DROP TABLE moz_cookies;"_ns);
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ // Rename new_moz_cookies to moz_cookies.
+ rv = mSyncConn->ExecuteSimpleSQL(nsLiteralCString(
+ "ALTER TABLE new_moz_cookies RENAME TO moz_cookies;"));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ // Recreate our index.
+ rv = mSyncConn->ExecuteSimpleSQL(
+ nsLiteralCString("CREATE INDEX moz_basedomain ON moz_cookies "
+ "(baseDomain, originAttributes)"));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ COOKIE_LOGSTRING(LogLevel::Debug,
+ ("Upgraded database to schema version 8"));
+ }
+ [[fallthrough]];
+
+ case 8: {
+ // Add the sameSite column to the table.
+ rv = mSyncConn->ExecuteSimpleSQL(
+ "ALTER TABLE moz_cookies ADD sameSite INTEGER"_ns);
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ COOKIE_LOGSTRING(LogLevel::Debug,
+ ("Upgraded database to schema version 9"));
+ }
+ [[fallthrough]];
+
+ case 9: {
+ // Add the rawSameSite column to the table.
+ rv = mSyncConn->ExecuteSimpleSQL(nsLiteralCString(
+ "ALTER TABLE moz_cookies ADD rawSameSite INTEGER"));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ // Copy the current sameSite value into rawSameSite.
+ rv = mSyncConn->ExecuteSimpleSQL(
+ "UPDATE moz_cookies SET rawSameSite = sameSite"_ns);
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ COOKIE_LOGSTRING(LogLevel::Debug,
+ ("Upgraded database to schema version 10"));
+ }
+ [[fallthrough]];
+
+ case 10: {
+ // Rename existing table
+ rv = mSyncConn->ExecuteSimpleSQL(nsLiteralCString(
+ "ALTER TABLE moz_cookies RENAME TO moz_cookies_old"));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ // Create a new moz_cookies table without the baseDomain field.
+ rv = mSyncConn->ExecuteSimpleSQL(
+ nsLiteralCString("CREATE TABLE moz_cookies("
+ "id INTEGER PRIMARY KEY, "
+ "originAttributes TEXT NOT NULL DEFAULT '', "
+ "name TEXT, "
+ "value TEXT, "
+ "host TEXT, "
+ "path TEXT, "
+ "expiry INTEGER, "
+ "lastAccessed INTEGER, "
+ "creationTime INTEGER, "
+ "isSecure INTEGER, "
+ "isHttpOnly INTEGER, "
+ "inBrowserElement INTEGER DEFAULT 0, "
+ "sameSite INTEGER DEFAULT 0, "
+ "rawSameSite INTEGER DEFAULT 0, "
+ "CONSTRAINT moz_uniqueid UNIQUE (name, host, "
+ "path, originAttributes)"
+ ")"));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ // Move the data over.
+ rv = mSyncConn->ExecuteSimpleSQL(
+ nsLiteralCString("INSERT INTO moz_cookies ("
+ "id, "
+ "originAttributes, "
+ "name, "
+ "value, "
+ "host, "
+ "path, "
+ "expiry, "
+ "lastAccessed, "
+ "creationTime, "
+ "isSecure, "
+ "isHttpOnly, "
+ "inBrowserElement, "
+ "sameSite, "
+ "rawSameSite "
+ ") SELECT "
+ "id, "
+ "originAttributes, "
+ "name, "
+ "value, "
+ "host, "
+ "path, "
+ "expiry, "
+ "lastAccessed, "
+ "creationTime, "
+ "isSecure, "
+ "isHttpOnly, "
+ "inBrowserElement, "
+ "sameSite, "
+ "rawSameSite "
+ "FROM moz_cookies_old "
+ "WHERE baseDomain NOTNULL;"));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ // Drop the old table
+ rv = mSyncConn->ExecuteSimpleSQL("DROP TABLE moz_cookies_old;"_ns);
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ // Drop the moz_basedomain index from the database (if it hasn't been
+ // removed already by removing the table).
+ rv = mSyncConn->ExecuteSimpleSQL(
+ "DROP INDEX IF EXISTS moz_basedomain;"_ns);
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ COOKIE_LOGSTRING(LogLevel::Debug,
+ ("Upgraded database to schema version 11"));
+ }
+ [[fallthrough]];
+
+ case 11: {
+ // Add the schemeMap column to the table.
+ rv = mSyncConn->ExecuteSimpleSQL(nsLiteralCString(
+ "ALTER TABLE moz_cookies ADD schemeMap INTEGER DEFAULT 0;"));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ COOKIE_LOGSTRING(LogLevel::Debug,
+ ("Upgraded database to schema version 12"));
+
+ // No more upgrades. Update the schema version.
+ rv = mSyncConn->SetSchemaVersion(COOKIES_SCHEMA_VERSION);
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+ }
+ [[fallthrough]];
+
+ case 12: {
+ // Add the isPartitionedAttributeSet column to the table.
+ rv = mSyncConn->ExecuteSimpleSQL(
+ nsLiteralCString("ALTER TABLE moz_cookies ADD "
+ "isPartitionedAttributeSet INTEGER DEFAULT 0;"));
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ COOKIE_LOGSTRING(LogLevel::Debug,
+ ("Upgraded database to schema version 13"));
+
+ // No more upgrades. Update the schema version.
+ rv = mSyncConn->SetSchemaVersion(COOKIES_SCHEMA_VERSION);
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ [[fallthrough]];
+ }
+
+ case COOKIES_SCHEMA_VERSION:
+ break;
+
+ case 0: {
+ NS_WARNING("couldn't get schema version!");
+
+ // the table may be usable; someone might've just clobbered the schema
+ // version. we can treat this case like a downgrade using the codepath
+ // below, by verifying the columns we care about are all there. for now,
+ // re-set the schema version in the db, in case the checks succeed (if
+ // they don't, we're dropping the table anyway).
+ rv = mSyncConn->SetSchemaVersion(COOKIES_SCHEMA_VERSION);
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+ }
+ // fall through to downgrade check
+ [[fallthrough]];
+
+ // downgrading.
+ // if columns have been added to the table, we can still use the ones we
+ // understand safely. if columns have been deleted or altered, just
+ // blow away the table and start from scratch! if you change the way
+ // a column is interpreted, make sure you also change its name so this
+ // check will catch it.
+ default: {
+ // check if all the expected columns exist
+ nsCOMPtr<mozIStorageStatement> stmt;
+ rv = mSyncConn->CreateStatement(
+ nsLiteralCString("SELECT "
+ "id, "
+ "originAttributes, "
+ "name, "
+ "value, "
+ "host, "
+ "path, "
+ "expiry, "
+ "lastAccessed, "
+ "creationTime, "
+ "isSecure, "
+ "isHttpOnly, "
+ "sameSite, "
+ "rawSameSite, "
+ "schemeMap, "
+ "isPartitionedAttributeSet "
+ "FROM moz_cookies"),
+ getter_AddRefs(stmt));
+ if (NS_SUCCEEDED(rv)) {
+ break;
+ }
+
+ // our columns aren't there - drop the table!
+ rv = mSyncConn->ExecuteSimpleSQL("DROP TABLE moz_cookies"_ns);
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ rv = CreateTable();
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+ } break;
+ }
+ }
+
+ // if we deleted a corrupt db, don't attempt to import - return now
+ if (aRecreateDB) {
+ return RESULT_OK;
+ }
+
+ // check whether to import or just read in the db
+ if (tableExists) {
+ return Read();
+ }
+
+ return RESULT_OK;
+}
+
+void CookiePersistentStorage::RebuildCorruptDB() {
+ NS_ASSERTION(!mDBConn, "shouldn't have an open db connection");
+ NS_ASSERTION(mCorruptFlag == CookiePersistentStorage::CLOSING_FOR_REBUILD,
+ "should be in CLOSING_FOR_REBUILD state");
+
+ nsCOMPtr<nsIObserverService> os = services::GetObserverService();
+
+ mCorruptFlag = CookiePersistentStorage::REBUILDING;
+
+ COOKIE_LOGSTRING(LogLevel::Debug,
+ ("RebuildCorruptDB(): creating new database"));
+
+ RefPtr<CookiePersistentStorage> self = this;
+ nsCOMPtr<nsIRunnable> runnable =
+ NS_NewRunnableFunction("RebuildCorruptDB.TryInitDB", [self] {
+ // The database has been closed, and we're ready to rebuild. Open a
+ // connection.
+ OpenDBResult result = self->TryInitDB(true);
+
+ nsCOMPtr<nsIRunnable> innerRunnable = NS_NewRunnableFunction(
+ "RebuildCorruptDB.TryInitDBComplete", [self, result] {
+ nsCOMPtr<nsIObserverService> os = services::GetObserverService();
+ if (result != RESULT_OK) {
+ // We're done. Reset our DB connection and statements, and
+ // notify of closure.
+ COOKIE_LOGSTRING(
+ LogLevel::Warning,
+ ("RebuildCorruptDB(): TryInitDB() failed with result %u",
+ result));
+ self->CleanupCachedStatements();
+ self->CleanupDBConnection();
+ self->mCorruptFlag = CookiePersistentStorage::OK;
+ if (os) {
+ os->NotifyObservers(nullptr, "cookie-db-closed", nullptr);
+ }
+ return;
+ }
+
+ // Notify observers that we're beginning the rebuild.
+ if (os) {
+ os->NotifyObservers(nullptr, "cookie-db-rebuilding", nullptr);
+ }
+
+ self->InitDBConnInternal();
+
+ // Enumerate the hash, and add cookies to the params array.
+ mozIStorageAsyncStatement* stmt = self->mStmtInsert;
+ nsCOMPtr<mozIStorageBindingParamsArray> paramsArray;
+ stmt->NewBindingParamsArray(getter_AddRefs(paramsArray));
+ for (auto iter = self->mHostTable.Iter(); !iter.Done();
+ iter.Next()) {
+ CookieEntry* entry = iter.Get();
+
+ const CookieEntry::ArrayType& cookies = entry->GetCookies();
+ for (CookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
+ Cookie* cookie = cookies[i];
+
+ if (!cookie->IsSession()) {
+ BindCookieParameters(paramsArray, CookieKey(entry), cookie);
+ }
+ }
+ }
+
+ // Make sure we've got something to write. If we don't, we're
+ // done.
+ uint32_t length;
+ paramsArray->GetLength(&length);
+ if (length == 0) {
+ COOKIE_LOGSTRING(
+ LogLevel::Debug,
+ ("RebuildCorruptDB(): nothing to write, rebuild complete"));
+ self->mCorruptFlag = CookiePersistentStorage::OK;
+ return;
+ }
+
+ self->MaybeStoreCookiesToDB(paramsArray);
+ });
+ NS_DispatchToMainThread(innerRunnable);
+ });
+ mThread->Dispatch(runnable, NS_DISPATCH_NORMAL);
+}
+
+void CookiePersistentStorage::HandleDBClosed() {
+ COOKIE_LOGSTRING(LogLevel::Debug,
+ ("HandleDBClosed(): CookieStorage %p closed", this));
+
+ nsCOMPtr<nsIObserverService> os = services::GetObserverService();
+
+ switch (mCorruptFlag) {
+ case CookiePersistentStorage::OK: {
+ // Database is healthy. Notify of closure.
+ if (os) {
+ os->NotifyObservers(nullptr, "cookie-db-closed", nullptr);
+ }
+ break;
+ }
+ case CookiePersistentStorage::CLOSING_FOR_REBUILD: {
+ // Our close finished. Start the rebuild, and notify of db closure later.
+ RebuildCorruptDB();
+ break;
+ }
+ case CookiePersistentStorage::REBUILDING: {
+ // We encountered an error during rebuild, closed the database, and now
+ // here we are. We already have a 'cookies.sqlite.bak' from the original
+ // dead database; we don't want to overwrite it, so let's move this one to
+ // 'cookies.sqlite.bak-rebuild'.
+ nsCOMPtr<nsIFile> backupFile;
+ mCookieFile->Clone(getter_AddRefs(backupFile));
+ nsresult rv = backupFile->MoveToNative(
+ nullptr, nsLiteralCString(COOKIES_FILE ".bak-rebuild"));
+
+ COOKIE_LOGSTRING(LogLevel::Warning,
+ ("HandleDBClosed(): CookieStorage %p encountered error "
+ "rebuilding db; move to "
+ "'cookies.sqlite.bak-rebuild' gave rv 0x%" PRIx32,
+ this, static_cast<uint32_t>(rv)));
+ if (os) {
+ os->NotifyObservers(nullptr, "cookie-db-closed", nullptr);
+ }
+ break;
+ }
+ }
+}
+
+CookiePersistentStorage::OpenDBResult CookiePersistentStorage::Read() {
+ MOZ_ASSERT(NS_GetCurrentThread() == mThread);
+
+ // Read in the data synchronously.
+ // see IDX_NAME, etc. for parameter indexes
+ nsCOMPtr<mozIStorageStatement> stmt;
+ nsresult rv =
+ mSyncConn->CreateStatement(nsLiteralCString("SELECT "
+ "name, "
+ "value, "
+ "host, "
+ "path, "
+ "expiry, "
+ "lastAccessed, "
+ "creationTime, "
+ "isSecure, "
+ "isHttpOnly, "
+ "originAttributes, "
+ "sameSite, "
+ "rawSameSite, "
+ "schemeMap, "
+ "isPartitionedAttributeSet "
+ "FROM moz_cookies"),
+ getter_AddRefs(stmt));
+
+ NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
+
+ if (NS_WARN_IF(!mReadArray.IsEmpty())) {
+ mReadArray.Clear();
+ }
+ mReadArray.SetCapacity(kMaxNumberOfCookies);
+
+ nsCString baseDomain;
+ nsCString name;
+ nsCString value;
+ nsCString host;
+ nsCString path;
+ bool hasResult;
+ while (true) {
+ rv = stmt->ExecuteStep(&hasResult);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ mReadArray.Clear();
+ return RESULT_RETRY;
+ }
+
+ if (!hasResult) {
+ break;
+ }
+
+ stmt->GetUTF8String(IDX_HOST, host);
+
+ rv = CookieCommons::GetBaseDomainFromHost(mTLDService, host, baseDomain);
+ if (NS_FAILED(rv)) {
+ COOKIE_LOGSTRING(LogLevel::Debug,
+ ("Read(): Ignoring invalid host '%s'", host.get()));
+ continue;
+ }
+
+ nsAutoCString suffix;
+ OriginAttributes attrs;
+ stmt->GetUTF8String(IDX_ORIGIN_ATTRIBUTES, suffix);
+ // If PopulateFromSuffix failed we just ignore the OA attributes
+ // that we don't support
+ Unused << attrs.PopulateFromSuffix(suffix);
+
+ CookieKey key(baseDomain, attrs);
+ CookieDomainTuple* tuple = mReadArray.AppendElement();
+ tuple->key = std::move(key);
+ tuple->originAttributes = attrs;
+ tuple->cookie = GetCookieFromRow(stmt);
+ }
+
+ COOKIE_LOGSTRING(LogLevel::Debug,
+ ("Read(): %zu cookies read", mReadArray.Length()));
+
+ return RESULT_OK;
+}
+
+// Extract data from a single result row and create an Cookie.
+UniquePtr<CookieStruct> CookiePersistentStorage::GetCookieFromRow(
+ mozIStorageStatement* aRow) {
+ nsCString name;
+ nsCString value;
+ nsCString host;
+ nsCString path;
+ DebugOnly<nsresult> rv = aRow->GetUTF8String(IDX_NAME, name);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ rv = aRow->GetUTF8String(IDX_VALUE, value);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ rv = aRow->GetUTF8String(IDX_HOST, host);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ rv = aRow->GetUTF8String(IDX_PATH, path);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ int64_t expiry = aRow->AsInt64(IDX_EXPIRY);
+ int64_t lastAccessed = aRow->AsInt64(IDX_LAST_ACCESSED);
+ int64_t creationTime = aRow->AsInt64(IDX_CREATION_TIME);
+ bool isSecure = 0 != aRow->AsInt32(IDX_SECURE);
+ bool isHttpOnly = 0 != aRow->AsInt32(IDX_HTTPONLY);
+ int32_t sameSite = aRow->AsInt32(IDX_SAME_SITE);
+ int32_t rawSameSite = aRow->AsInt32(IDX_RAW_SAME_SITE);
+ int32_t schemeMap = aRow->AsInt32(IDX_SCHEME_MAP);
+ bool isPartitionedAttributeSet =
+ 0 != aRow->AsInt32(IDX_PARTITIONED_ATTRIBUTE_SET);
+
+ // Create a new constCookie and assign the data.
+ return MakeUnique<CookieStruct>(
+ name, value, host, path, expiry, lastAccessed, creationTime, isHttpOnly,
+ false, isSecure, isPartitionedAttributeSet, sameSite, rawSameSite,
+ static_cast<nsICookie::schemeType>(schemeMap));
+}
+
+void CookiePersistentStorage::EnsureInitialized() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ bool isAccumulated = false;
+
+ if (!mInitialized) {
+ TimeStamp startBlockTime = TimeStamp::Now();
+ MonitorAutoLock lock(mMonitor);
+
+ while (!mInitialized) {
+ mMonitor.Wait();
+ }
+
+ Telemetry::AccumulateTimeDelta(
+ Telemetry::MOZ_SQLITE_COOKIES_BLOCK_MAIN_THREAD_MS_V2, startBlockTime);
+ Telemetry::Accumulate(
+ Telemetry::MOZ_SQLITE_COOKIES_TIME_TO_BLOCK_MAIN_THREAD_MS, 0);
+ isAccumulated = true;
+ } else if (!mEndInitDBConn.IsNull()) {
+ // We didn't block main thread, and here comes the first cookie request.
+ // Collect how close we're going to block main thread.
+ Telemetry::Accumulate(
+ Telemetry::MOZ_SQLITE_COOKIES_TIME_TO_BLOCK_MAIN_THREAD_MS,
+ (TimeStamp::Now() - mEndInitDBConn).ToMilliseconds());
+ // Nullify the timestamp so wo don't accumulate this telemetry probe again.
+ mEndInitDBConn = TimeStamp();
+ isAccumulated = true;
+ } else if (!mInitializedDBConn) {
+ // A request comes while we finished cookie thread task and InitDBConn is
+ // on the way from cookie thread to main thread. We're very close to block
+ // main thread.
+ Telemetry::Accumulate(
+ Telemetry::MOZ_SQLITE_COOKIES_TIME_TO_BLOCK_MAIN_THREAD_MS, 0);
+ isAccumulated = true;
+ }
+
+ if (!mInitializedDBConn) {
+ InitDBConn();
+ if (isAccumulated) {
+ // Nullify the timestamp so wo don't accumulate this telemetry probe
+ // again.
+ mEndInitDBConn = TimeStamp();
+ }
+ }
+}
+
+void CookiePersistentStorage::InitDBConn() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // We should skip InitDBConn if we close profile during initializing
+ // CookieStorages and then InitDBConn is called after we close the
+ // CookieStorages.
+ if (!mInitialized || mInitializedDBConn) {
+ return;
+ }
+
+ nsCOMPtr<nsIURI> dummyUri;
+ nsresult rv = NS_NewURI(getter_AddRefs(dummyUri), "https://example.com");
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ nsTArray<RefPtr<Cookie>> cleanupCookies;
+
+ for (uint32_t i = 0; i < mReadArray.Length(); ++i) {
+ CookieDomainTuple& tuple = mReadArray[i];
+ MOZ_ASSERT(!tuple.cookie->isSession());
+
+ // filter invalid non-ipv4 host ending in number from old db values
+ nsCOMPtr<nsIURIMutator> outMut;
+ nsCOMPtr<nsIURIMutator> dummyMut;
+ rv = dummyUri->Mutate(getter_AddRefs(dummyMut));
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ rv = dummyMut->SetHost(tuple.cookie->host(), getter_AddRefs(outMut));
+
+ if (NS_FAILED(rv)) {
+ COOKIE_LOGSTRING(LogLevel::Debug, ("Removing cookie from db with "
+ "newly invalid hostname: '%s'",
+ tuple.cookie->host().get()));
+ RefPtr<Cookie> cookie =
+ Cookie::Create(*tuple.cookie, tuple.originAttributes);
+ cleanupCookies.AppendElement(cookie);
+ continue;
+ }
+
+ // CreateValidated fixes up the creation and lastAccessed times.
+ // If the DB is corrupted and the timestaps are far away in the future
+ // we don't want the creation timestamp to update gLastCreationTime
+ // as that would contaminate all the next creation times.
+ // We fix up these dates to not be later than the current time.
+ // The downside is that if the user sets the date far away in the past
+ // then back to the current date, those cookies will be stale,
+ // but if we don't fix their dates, those cookies might never be
+ // evicted.
+ RefPtr<Cookie> cookie =
+ Cookie::CreateValidated(*tuple.cookie, tuple.originAttributes);
+ AddCookieToList(tuple.key.mBaseDomain, tuple.key.mOriginAttributes, cookie);
+ }
+
+ if (NS_FAILED(InitDBConnInternal())) {
+ COOKIE_LOGSTRING(LogLevel::Warning,
+ ("InitDBConn(): retrying InitDBConnInternal()"));
+ CleanupCachedStatements();
+ CleanupDBConnection();
+ if (NS_FAILED(InitDBConnInternal())) {
+ COOKIE_LOGSTRING(
+ LogLevel::Warning,
+ ("InitDBConn(): InitDBConnInternal() failed, closing connection"));
+
+ // Game over, clean the connections.
+ CleanupCachedStatements();
+ CleanupDBConnection();
+ }
+ }
+ mInitializedDBConn = true;
+
+ COOKIE_LOGSTRING(LogLevel::Debug,
+ ("InitDBConn(): mInitializedDBConn = true"));
+ mEndInitDBConn = TimeStamp::Now();
+
+ for (const auto& cookie : cleanupCookies) {
+ RemoveCookieFromDB(*cookie);
+ }
+
+ nsCOMPtr<nsIObserverService> os = services::GetObserverService();
+ if (os) {
+ os->NotifyObservers(nullptr, "cookie-db-read", nullptr);
+ mReadArray.Clear();
+ }
+}
+
+nsresult CookiePersistentStorage::InitDBConnInternal() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsresult rv = mStorageService->OpenUnsharedDatabase(
+ mCookieFile, mozIStorageService::CONNECTION_DEFAULT,
+ getter_AddRefs(mDBConn));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Set up our listeners.
+ mInsertListener = new InsertCookieDBListener(this);
+ mUpdateListener = new UpdateCookieDBListener(this);
+ mRemoveListener = new RemoveCookieDBListener(this);
+ mCloseListener = new CloseCookieDBListener(this);
+
+ // Grow cookie db in 512KB increments
+ mDBConn->SetGrowthIncrement(512 * 1024, ""_ns);
+
+ // make operations on the table asynchronous, for performance
+ mDBConn->ExecuteSimpleSQL("PRAGMA synchronous = OFF"_ns);
+
+ // Use write-ahead-logging for performance. We cap the autocheckpoint limit at
+ // 16 pages (around 500KB).
+ mDBConn->ExecuteSimpleSQL(nsLiteralCString(MOZ_STORAGE_UNIQUIFY_QUERY_STR
+ "PRAGMA journal_mode = WAL"));
+ mDBConn->ExecuteSimpleSQL("PRAGMA wal_autocheckpoint = 16"_ns);
+
+ // cache frequently used statements (for insertion, deletion, and updating)
+ rv = mDBConn->CreateAsyncStatement(
+ nsLiteralCString("INSERT INTO moz_cookies ("
+ "originAttributes, "
+ "name, "
+ "value, "
+ "host, "
+ "path, "
+ "expiry, "
+ "lastAccessed, "
+ "creationTime, "
+ "isSecure, "
+ "isHttpOnly, "
+ "sameSite, "
+ "rawSameSite, "
+ "schemeMap, "
+ "isPartitionedAttributeSet "
+ ") VALUES ("
+ ":originAttributes, "
+ ":name, "
+ ":value, "
+ ":host, "
+ ":path, "
+ ":expiry, "
+ ":lastAccessed, "
+ ":creationTime, "
+ ":isSecure, "
+ ":isHttpOnly, "
+ ":sameSite, "
+ ":rawSameSite, "
+ ":schemeMap, "
+ ":isPartitionedAttributeSet "
+ ")"),
+ getter_AddRefs(mStmtInsert));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = mDBConn->CreateAsyncStatement(
+ nsLiteralCString("DELETE FROM moz_cookies "
+ "WHERE name = :name AND host = :host AND path = :path "
+ "AND originAttributes = :originAttributes"),
+ getter_AddRefs(mStmtDelete));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = mDBConn->CreateAsyncStatement(
+ nsLiteralCString("UPDATE moz_cookies SET lastAccessed = :lastAccessed "
+ "WHERE name = :name AND host = :host AND path = :path "
+ "AND originAttributes = :originAttributes"),
+ getter_AddRefs(mStmtUpdate));
+ return rv;
+}
+
+// Sets the schema version and creates the moz_cookies table.
+nsresult CookiePersistentStorage::CreateTableWorker(const char* aName) {
+ // Create the table.
+ // We default originAttributes to empty string: this is so if users revert to
+ // an older Firefox version that doesn't know about this field, any cookies
+ // set will still work once they upgrade back.
+ nsAutoCString command("CREATE TABLE ");
+ command.Append(aName);
+ command.AppendLiteral(
+ " ("
+ "id INTEGER PRIMARY KEY, "
+ "originAttributes TEXT NOT NULL DEFAULT '', "
+ "name TEXT, "
+ "value TEXT, "
+ "host TEXT, "
+ "path TEXT, "
+ "expiry INTEGER, "
+ "lastAccessed INTEGER, "
+ "creationTime INTEGER, "
+ "isSecure INTEGER, "
+ "isHttpOnly INTEGER, "
+ "inBrowserElement INTEGER DEFAULT 0, "
+ "sameSite INTEGER DEFAULT 0, "
+ "rawSameSite INTEGER DEFAULT 0, "
+ "schemeMap INTEGER DEFAULT 0, "
+ "isPartitionedAttributeSet INTEGER DEFAULT 0, "
+ "CONSTRAINT moz_uniqueid UNIQUE (name, host, path, originAttributes)"
+ ")");
+ return mSyncConn->ExecuteSimpleSQL(command);
+}
+
+// Sets the schema version and creates the moz_cookies table.
+nsresult CookiePersistentStorage::CreateTable() {
+ // Set the schema version, before creating the table.
+ nsresult rv = mSyncConn->SetSchemaVersion(COOKIES_SCHEMA_VERSION);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ rv = CreateTableWorker("moz_cookies");
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+// Sets the schema version and creates the moz_cookies table.
+nsresult CookiePersistentStorage::CreateTableForSchemaVersion6() {
+ // Set the schema version, before creating the table.
+ nsresult rv = mSyncConn->SetSchemaVersion(6);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // Create the table.
+ // We default originAttributes to empty string: this is so if users revert to
+ // an older Firefox version that doesn't know about this field, any cookies
+ // set will still work once they upgrade back.
+ rv = mSyncConn->ExecuteSimpleSQL(nsLiteralCString(
+ "CREATE TABLE moz_cookies ("
+ "id INTEGER PRIMARY KEY, "
+ "baseDomain TEXT, "
+ "originAttributes TEXT NOT NULL DEFAULT '', "
+ "name TEXT, "
+ "value TEXT, "
+ "host TEXT, "
+ "path TEXT, "
+ "expiry INTEGER, "
+ "lastAccessed INTEGER, "
+ "creationTime INTEGER, "
+ "isSecure INTEGER, "
+ "isHttpOnly INTEGER, "
+ "CONSTRAINT moz_uniqueid UNIQUE (name, host, path, originAttributes)"
+ ")"));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // Create an index on baseDomain.
+ return mSyncConn->ExecuteSimpleSQL(nsLiteralCString(
+ "CREATE INDEX moz_basedomain ON moz_cookies (baseDomain, "
+ "originAttributes)"));
+}
+
+// Sets the schema version and creates the moz_cookies table.
+nsresult CookiePersistentStorage::CreateTableForSchemaVersion5() {
+ // Set the schema version, before creating the table.
+ nsresult rv = mSyncConn->SetSchemaVersion(5);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // Create the table. We default appId/inBrowserElement to 0: this is so if
+ // users revert to an older Firefox version that doesn't know about these
+ // fields, any cookies set will still work once they upgrade back.
+ rv = mSyncConn->ExecuteSimpleSQL(
+ nsLiteralCString("CREATE TABLE moz_cookies ("
+ "id INTEGER PRIMARY KEY, "
+ "baseDomain TEXT, "
+ "appId INTEGER DEFAULT 0, "
+ "inBrowserElement INTEGER DEFAULT 0, "
+ "name TEXT, "
+ "value TEXT, "
+ "host TEXT, "
+ "path TEXT, "
+ "expiry INTEGER, "
+ "lastAccessed INTEGER, "
+ "creationTime INTEGER, "
+ "isSecure INTEGER, "
+ "isHttpOnly INTEGER, "
+ "CONSTRAINT moz_uniqueid UNIQUE (name, host, path, "
+ "appId, inBrowserElement)"
+ ")"));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // Create an index on baseDomain.
+ return mSyncConn->ExecuteSimpleSQL(nsLiteralCString(
+ "CREATE INDEX moz_basedomain ON moz_cookies (baseDomain, "
+ "appId, "
+ "inBrowserElement)"));
+}
+
+nsresult CookiePersistentStorage::RunInTransaction(
+ nsICookieTransactionCallback* aCallback) {
+ if (NS_WARN_IF(!mDBConn)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ mozStorageTransaction transaction(mDBConn, true);
+
+ // XXX Handle the error, bug 1696130.
+ Unused << NS_WARN_IF(NS_FAILED(transaction.Start()));
+
+ if (NS_FAILED(aCallback->Callback())) {
+ Unused << transaction.Rollback();
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+// purges expired and old cookies in a batch operation.
+already_AddRefed<nsIArray> CookiePersistentStorage::PurgeCookies(
+ int64_t aCurrentTimeInUsec, uint16_t aMaxNumberOfCookies,
+ int64_t aCookiePurgeAge) {
+ // Create a params array to batch the removals. This is OK here because
+ // all the removals are in order, and there are no interleaved additions.
+ nsCOMPtr<mozIStorageBindingParamsArray> paramsArray;
+ if (mDBConn) {
+ mStmtDelete->NewBindingParamsArray(getter_AddRefs(paramsArray));
+ }
+
+ RefPtr<CookiePersistentStorage> self = this;
+
+ return PurgeCookiesWithCallbacks(
+ aCurrentTimeInUsec, aMaxNumberOfCookies, aCookiePurgeAge,
+ [paramsArray, self](const CookieListIter& aIter) {
+ self->PrepareCookieRemoval(*aIter.Cookie(), paramsArray);
+ self->RemoveCookieFromListInternal(aIter);
+ },
+ [paramsArray, self]() {
+ if (paramsArray) {
+ self->DeleteFromDB(paramsArray);
+ }
+ });
+}
+
+void CookiePersistentStorage::CollectCookieJarSizeData() {
+ COOKIE_LOGSTRING(LogLevel::Debug,
+ ("CookiePersistentStorage::CollectCookieJarSizeData"));
+
+ uint32_t sumPartitioned = 0;
+ uint32_t sumUnpartitioned = 0;
+ for (const auto& cookieEntry : mHostTable) {
+ if (cookieEntry.IsPartitioned()) {
+ uint16_t cePartitioned = cookieEntry.GetCookies().Length();
+ sumPartitioned += cePartitioned;
+ mozilla::glean::networking::cookie_count_part_by_key.AccumulateSamples(
+ {cePartitioned});
+ } else {
+ uint16_t ceUnpartitioned = cookieEntry.GetCookies().Length();
+ sumUnpartitioned += ceUnpartitioned;
+ mozilla::glean::networking::cookie_count_unpart_by_key.AccumulateSamples(
+ {ceUnpartitioned});
+ }
+ }
+
+ mozilla::glean::networking::cookie_count_total.AccumulateSamples(
+ {mCookieCount});
+ mozilla::glean::networking::cookie_count_partitioned.AccumulateSamples(
+ {sumPartitioned});
+ mozilla::glean::networking::cookie_count_unpartitioned.AccumulateSamples(
+ {sumUnpartitioned});
+}
+
+} // namespace net
+} // namespace mozilla
diff --git a/netwerk/cookie/CookiePersistentStorage.h b/netwerk/cookie/CookiePersistentStorage.h
new file mode 100644
index 0000000000..e973c74ab6
--- /dev/null
+++ b/netwerk/cookie/CookiePersistentStorage.h
@@ -0,0 +1,160 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_net_CookiePersistentStorage_h
+#define mozilla_net_CookiePersistentStorage_h
+
+#include "CookieStorage.h"
+
+#include "mozilla/Atomics.h"
+#include "mozilla/Monitor.h"
+#include "mozilla/net/NeckoChannelParams.h"
+#include "mozIStorageBindingParamsArray.h"
+#include "mozIStorageCompletionCallback.h"
+#include "mozIStorageStatement.h"
+#include "mozIStorageStatementCallback.h"
+
+class mozIStorageAsyncStatement;
+class mozIStorageService;
+class nsICookieTransactionCallback;
+class nsIEffectiveTLDService;
+
+namespace mozilla {
+namespace net {
+
+class CookiePersistentStorage final : public CookieStorage {
+ public:
+ // Result codes for TryInitDB() and Read().
+ enum OpenDBResult { RESULT_OK, RESULT_RETRY, RESULT_FAILURE };
+
+ static already_AddRefed<CookiePersistentStorage> Create();
+
+ void HandleCorruptDB();
+
+ void RemoveCookiesWithOriginAttributes(
+ const OriginAttributesPattern& aPattern,
+ const nsACString& aBaseDomain) override;
+
+ void RemoveCookiesFromExactHost(
+ const nsACString& aHost, const nsACString& aBaseDomain,
+ const OriginAttributesPattern& aPattern) override;
+
+ void StaleCookies(const nsTArray<Cookie*>& aCookieList,
+ int64_t aCurrentTimeInUsec) override;
+
+ void Close() override;
+
+ void EnsureInitialized() override;
+
+ void CleanupCachedStatements();
+ void CleanupDBConnection();
+
+ void Activate();
+
+ void RebuildCorruptDB();
+ void HandleDBClosed();
+
+ nsresult RunInTransaction(nsICookieTransactionCallback* aCallback) override;
+
+ // State of the database connection.
+ enum CorruptFlag {
+ OK, // normal
+ CLOSING_FOR_REBUILD, // corruption detected, connection closing
+ REBUILDING // close complete, rebuilding database from memory
+ };
+
+ CorruptFlag GetCorruptFlag() const { return mCorruptFlag; }
+
+ void SetCorruptFlag(CorruptFlag aFlag) { mCorruptFlag = aFlag; }
+
+ protected:
+ const char* NotificationTopic() const override { return "cookie-changed"; }
+
+ void NotifyChangedInternal(nsICookieNotification* aNotification,
+ bool aOldCookieIsSession) override;
+
+ void RemoveAllInternal() override;
+
+ void RemoveCookieFromDB(const Cookie& aCookie) override;
+
+ void StoreCookie(const nsACString& aBaseDomain,
+ const OriginAttributes& aOriginAttributes,
+ Cookie* aCookie) override;
+
+ private:
+ CookiePersistentStorage();
+
+ static void UpdateCookieInList(Cookie* aCookie, int64_t aLastAccessed,
+ mozIStorageBindingParamsArray* aParamsArray);
+
+ void PrepareCookieRemoval(const Cookie& aCookie,
+ mozIStorageBindingParamsArray* aParamsArray);
+
+ void InitDBConn();
+ nsresult InitDBConnInternal();
+
+ OpenDBResult TryInitDB(bool aRecreateDB);
+ OpenDBResult Read();
+
+ nsresult CreateTableWorker(const char* aName);
+ nsresult CreateTable();
+ nsresult CreateTableForSchemaVersion6();
+ nsresult CreateTableForSchemaVersion5();
+
+ static UniquePtr<CookieStruct> GetCookieFromRow(mozIStorageStatement* aRow);
+
+ already_AddRefed<nsIArray> PurgeCookies(int64_t aCurrentTimeInUsec,
+ uint16_t aMaxNumberOfCookies,
+ int64_t aCookiePurgeAge) override;
+
+ void CollectCookieJarSizeData() override;
+
+ void DeleteFromDB(mozIStorageBindingParamsArray* aParamsArray);
+
+ void MaybeStoreCookiesToDB(mozIStorageBindingParamsArray* aParamsArray);
+
+ nsCOMPtr<nsIThread> mThread;
+ nsCOMPtr<mozIStorageService> mStorageService;
+ nsCOMPtr<nsIEffectiveTLDService> mTLDService;
+
+ // encapsulates a (key, Cookie) tuple for temporary storage purposes.
+ struct CookieDomainTuple {
+ CookieKey key;
+ OriginAttributes originAttributes;
+ UniquePtr<CookieStruct> cookie;
+ };
+
+ // thread
+ TimeStamp mEndInitDBConn;
+ nsTArray<CookieDomainTuple> mReadArray;
+
+ Monitor mMonitor MOZ_UNANNOTATED;
+
+ Atomic<bool> mInitialized;
+ Atomic<bool> mInitializedDBConn;
+
+ nsCOMPtr<nsIFile> mCookieFile;
+ nsCOMPtr<mozIStorageConnection> mDBConn;
+ nsCOMPtr<mozIStorageAsyncStatement> mStmtInsert;
+ nsCOMPtr<mozIStorageAsyncStatement> mStmtDelete;
+ nsCOMPtr<mozIStorageAsyncStatement> mStmtUpdate;
+
+ CorruptFlag mCorruptFlag;
+
+ // Various parts representing asynchronous read state. These are useful
+ // while the background read is taking place.
+ nsCOMPtr<mozIStorageConnection> mSyncConn;
+
+ // DB completion handlers.
+ nsCOMPtr<mozIStorageStatementCallback> mInsertListener;
+ nsCOMPtr<mozIStorageStatementCallback> mUpdateListener;
+ nsCOMPtr<mozIStorageStatementCallback> mRemoveListener;
+ nsCOMPtr<mozIStorageCompletionCallback> mCloseListener;
+};
+
+} // namespace net
+} // namespace mozilla
+
+#endif // mozilla_net_CookiePersistentStorage_h
diff --git a/netwerk/cookie/CookiePrivateStorage.cpp b/netwerk/cookie/CookiePrivateStorage.cpp
new file mode 100644
index 0000000000..e6d8fd67d2
--- /dev/null
+++ b/netwerk/cookie/CookiePrivateStorage.cpp
@@ -0,0 +1,44 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CookiePrivateStorage.h"
+#include "Cookie.h"
+
+namespace mozilla {
+namespace net {
+
+// static
+already_AddRefed<CookiePrivateStorage> CookiePrivateStorage::Create() {
+ RefPtr<CookiePrivateStorage> storage = new CookiePrivateStorage();
+ storage->Init();
+
+ return storage.forget();
+}
+
+void CookiePrivateStorage::StaleCookies(const nsTArray<Cookie*>& aCookieList,
+ int64_t aCurrentTimeInUsec) {
+ int32_t count = aCookieList.Length();
+ for (int32_t i = 0; i < count; ++i) {
+ Cookie* cookie = aCookieList.ElementAt(i);
+
+ if (cookie->IsStale()) {
+ cookie->SetLastAccessed(aCurrentTimeInUsec);
+ }
+ }
+}
+
+// purges expired and old cookies in a batch operation.
+already_AddRefed<nsIArray> CookiePrivateStorage::PurgeCookies(
+ int64_t aCurrentTimeInUsec, uint16_t aMaxNumberOfCookies,
+ int64_t aCookiePurgeAge) {
+ RefPtr<CookiePrivateStorage> self = this;
+ return PurgeCookiesWithCallbacks(
+ aCurrentTimeInUsec, aMaxNumberOfCookies, aCookiePurgeAge,
+ [self](const CookieListIter& iter) { self->RemoveCookieFromList(iter); },
+ nullptr);
+}
+
+} // namespace net
+} // namespace mozilla
diff --git a/netwerk/cookie/CookiePrivateStorage.h b/netwerk/cookie/CookiePrivateStorage.h
new file mode 100644
index 0000000000..812a7e6764
--- /dev/null
+++ b/netwerk/cookie/CookiePrivateStorage.h
@@ -0,0 +1,61 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_net_CookiePrivateStorage_h
+#define mozilla_net_CookiePrivateStorage_h
+
+#include "CookieStorage.h"
+
+class nsICookieTransactionCallback;
+
+namespace mozilla {
+namespace net {
+
+class CookiePrivateStorage final : public CookieStorage {
+ public:
+ static already_AddRefed<CookiePrivateStorage> Create();
+
+ void StaleCookies(const nsTArray<Cookie*>& aCookieList,
+ int64_t aCurrentTimeInUsec) override;
+
+ void Close() override{};
+
+ void EnsureInitialized() override{};
+
+ nsresult RunInTransaction(nsICookieTransactionCallback* aCallback) override {
+ // It might make sense for this to be a no-op, or to return
+ // `NS_ERROR_NOT_AVAILABLE`, or to evalute `aCallback` (in case it has
+ // side-effects), but for now, just crash.
+ MOZ_CRASH("RunInTransaction is not supported for private storage");
+ };
+
+ protected:
+ const char* NotificationTopic() const override {
+ return "private-cookie-changed";
+ }
+
+ void NotifyChangedInternal(nsICookieNotification* aNotification,
+ bool aOldCookieIsSession) override {}
+
+ void RemoveAllInternal() override {}
+
+ void RemoveCookieFromDB(const Cookie& aCookie) override {}
+
+ already_AddRefed<nsIArray> PurgeCookies(int64_t aCurrentTimeInUsec,
+ uint16_t aMaxNumberOfCookies,
+ int64_t aCookiePurgeAge) override;
+
+ void StoreCookie(const nsACString& aBaseDomain,
+ const OriginAttributes& aOriginAttributes,
+ Cookie* aCookie) override {}
+
+ private:
+ void CollectCookieJarSizeData() override{};
+};
+
+} // namespace net
+} // namespace mozilla
+
+#endif // mozilla_net_CookiePrivateStorage_h
diff --git a/netwerk/cookie/CookieService.cpp b/netwerk/cookie/CookieService.cpp
new file mode 100644
index 0000000000..d306203c2f
--- /dev/null
+++ b/netwerk/cookie/CookieService.cpp
@@ -0,0 +1,2586 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CookieCommons.h"
+#include "CookieLogging.h"
+#include "mozilla/AppShutdown.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/ContentBlockingNotifier.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/nsMixedContentBlocker.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/net/CookieJarSettings.h"
+#include "mozilla/net/CookiePersistentStorage.h"
+#include "mozilla/net/CookiePrivateStorage.h"
+#include "mozilla/net/CookieService.h"
+#include "mozilla/net/CookieServiceChild.h"
+#include "mozilla/net/HttpBaseChannel.h"
+#include "mozilla/net/NeckoCommon.h"
+#include "mozilla/StaticPrefs_network.h"
+#include "mozilla/StoragePrincipalHelper.h"
+#include "mozilla/Telemetry.h"
+#include "mozIThirdPartyUtil.h"
+#include "nsICookiePermission.h"
+#include "nsIConsoleReportCollector.h"
+#include "nsIEffectiveTLDService.h"
+#include "nsIIDNService.h"
+#include "nsIScriptError.h"
+#include "nsIURL.h"
+#include "nsIURI.h"
+#include "nsIWebProgressListener.h"
+#include "nsNetUtil.h"
+#include "prprf.h"
+#include "ThirdPartyUtil.h"
+
+using namespace mozilla::dom;
+
+namespace {
+
+uint32_t MakeCookieBehavior(uint32_t aCookieBehavior) {
+ bool isFirstPartyIsolated = OriginAttributes::IsFirstPartyEnabled();
+
+ if (isFirstPartyIsolated &&
+ aCookieBehavior ==
+ nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN) {
+ return nsICookieService::BEHAVIOR_REJECT_TRACKER;
+ }
+ return aCookieBehavior;
+}
+
+/*
+ Enables sanitizeOnShutdown cleaning prefs and disables the
+ network.cookie.lifetimePolicy
+*/
+void MigrateCookieLifetimePrefs() {
+ // Former network.cookie.lifetimePolicy values ACCEPT_SESSION/ACCEPT_NORMALLY
+ // are not available anymore 2 = ACCEPT_SESSION
+ if (mozilla::Preferences::GetInt("network.cookie.lifetimePolicy") != 2) {
+ return;
+ }
+ if (!mozilla::Preferences::GetBool("privacy.sanitize.sanitizeOnShutdown")) {
+ mozilla::Preferences::SetBool("privacy.sanitize.sanitizeOnShutdown", true);
+ // To avoid clearing categories that the user did not intend to clear
+ mozilla::Preferences::SetBool("privacy.clearOnShutdown.history", false);
+ mozilla::Preferences::SetBool("privacy.clearOnShutdown.formdata", false);
+ mozilla::Preferences::SetBool("privacy.clearOnShutdown.downloads", false);
+ mozilla::Preferences::SetBool("privacy.clearOnShutdown.sessions", false);
+ mozilla::Preferences::SetBool("privacy.clearOnShutdown.siteSettings",
+ false);
+ }
+ mozilla::Preferences::SetBool("privacy.clearOnShutdown.cookies", true);
+ mozilla::Preferences::SetBool("privacy.clearOnShutdown.cache", true);
+ mozilla::Preferences::SetBool("privacy.clearOnShutdown.offlineApps", true);
+ mozilla::Preferences::ClearUser("network.cookie.lifetimePolicy");
+}
+
+} // anonymous namespace
+
+// static
+uint32_t nsICookieManager::GetCookieBehavior(bool aIsPrivate) {
+ if (aIsPrivate) {
+ // To sync the cookieBehavior pref between regular and private mode in ETP
+ // custom mode, we will return the regular cookieBehavior pref for private
+ // mode when
+ // 1. The regular cookieBehavior pref has a non-default value.
+ // 2. And the private cookieBehavior pref has a default value.
+ // Also, this can cover the migration case where the user has a non-default
+ // cookieBehavior before the private cookieBehavior was introduced. The
+ // getter here will directly return the regular cookieBehavior, so that the
+ // cookieBehavior for private mode is consistent.
+ if (mozilla::Preferences::HasUserValue(
+ "network.cookie.cookieBehavior.pbmode")) {
+ return MakeCookieBehavior(
+ mozilla::StaticPrefs::network_cookie_cookieBehavior_pbmode());
+ }
+
+ if (mozilla::Preferences::HasUserValue("network.cookie.cookieBehavior")) {
+ return MakeCookieBehavior(
+ mozilla::StaticPrefs::network_cookie_cookieBehavior());
+ }
+
+ return MakeCookieBehavior(
+ mozilla::StaticPrefs::network_cookie_cookieBehavior_pbmode());
+ }
+ return MakeCookieBehavior(
+ mozilla::StaticPrefs::network_cookie_cookieBehavior());
+}
+
+namespace mozilla {
+namespace net {
+
+/******************************************************************************
+ * CookieService impl:
+ * useful types & constants
+ ******************************************************************************/
+
+static StaticRefPtr<CookieService> gCookieService;
+
+constexpr auto CONSOLE_CHIPS_CATEGORY = "cookiesCHIPS"_ns;
+constexpr auto CONSOLE_SAMESITE_CATEGORY = "cookieSameSite"_ns;
+constexpr auto CONSOLE_OVERSIZE_CATEGORY = "cookiesOversize"_ns;
+constexpr auto CONSOLE_REJECTION_CATEGORY = "cookiesRejection"_ns;
+constexpr auto SAMESITE_MDN_URL =
+ "https://developer.mozilla.org/docs/Web/HTTP/Headers/Set-Cookie/"
+ u"SameSite"_ns;
+
+namespace {
+
+void ComposeCookieString(nsTArray<Cookie*>& aCookieList,
+ nsACString& aCookieString) {
+ for (Cookie* cookie : aCookieList) {
+ // check if we have anything to write
+ if (!cookie->Name().IsEmpty() || !cookie->Value().IsEmpty()) {
+ // if we've already added a cookie to the return list, append a "; " so
+ // that subsequent cookies are delimited in the final list.
+ if (!aCookieString.IsEmpty()) {
+ aCookieString.AppendLiteral("; ");
+ }
+
+ if (!cookie->Name().IsEmpty()) {
+ // we have a name and value - write both
+ aCookieString += cookie->Name() + "="_ns + cookie->Value();
+ } else {
+ // just write value
+ aCookieString += cookie->Value();
+ }
+ }
+ }
+}
+
+// Return false if the cookie should be ignored for the current channel.
+bool ProcessSameSiteCookieForForeignRequest(nsIChannel* aChannel,
+ Cookie* aCookie,
+ bool aIsSafeTopLevelNav,
+ bool aHadCrossSiteRedirects,
+ bool aLaxByDefault) {
+ // If it's a cross-site request and the cookie is same site only (strict)
+ // don't send it.
+ if (aCookie->SameSite() == nsICookie::SAMESITE_STRICT) {
+ return false;
+ }
+
+ // Explicit SameSite=None cookies are always processed. When laxByDefault
+ // is OFF then so are default cookies.
+ if (aCookie->SameSite() == nsICookie::SAMESITE_NONE ||
+ (!aLaxByDefault && aCookie->IsDefaultSameSite())) {
+ return true;
+ }
+
+ // Lax-by-default cookies are processed even with an intermediate
+ // cross-site redirect (they are treated like aIsSameSiteForeign = false).
+ if (aLaxByDefault && aCookie->IsDefaultSameSite() && aHadCrossSiteRedirects &&
+ StaticPrefs::
+ network_cookie_sameSite_laxByDefault_allowBoomerangRedirect()) {
+ return true;
+ }
+
+ int64_t currentTimeInUsec = PR_Now();
+
+ // 2 minutes of tolerance for 'SameSite=Lax by default' for cookies set
+ // without a SameSite value when used for unsafe http methods.
+ if (aLaxByDefault && aCookie->IsDefaultSameSite() &&
+ StaticPrefs::network_cookie_sameSite_laxPlusPOST_timeout() > 0 &&
+ currentTimeInUsec - aCookie->CreationTime() <=
+ (StaticPrefs::network_cookie_sameSite_laxPlusPOST_timeout() *
+ PR_USEC_PER_SEC) &&
+ !NS_IsSafeMethodNav(aChannel)) {
+ return true;
+ }
+
+ MOZ_ASSERT((aLaxByDefault && aCookie->IsDefaultSameSite()) ||
+ aCookie->SameSite() == nsICookie::SAMESITE_LAX);
+ // We only have SameSite=Lax or lax-by-default cookies at this point. These
+ // are processed only if it's a top-level navigation
+ return aIsSafeTopLevelNav;
+}
+
+} // namespace
+
+/******************************************************************************
+ * CookieService impl:
+ * singleton instance ctor/dtor methods
+ ******************************************************************************/
+
+already_AddRefed<nsICookieService> CookieService::GetXPCOMSingleton() {
+ if (IsNeckoChild()) {
+ return CookieServiceChild::GetSingleton();
+ }
+
+ return GetSingleton();
+}
+
+already_AddRefed<CookieService> CookieService::GetSingleton() {
+ NS_ASSERTION(!IsNeckoChild(), "not a parent process");
+
+ if (gCookieService) {
+ return do_AddRef(gCookieService);
+ }
+
+ // Create a new singleton CookieService.
+ // We AddRef only once since XPCOM has rules about the ordering of module
+ // teardowns - by the time our module destructor is called, it's too late to
+ // Release our members (e.g. nsIObserverService and nsIPrefBranch), since GC
+ // cycles have already been completed and would result in serious leaks.
+ // See bug 209571.
+ // TODO: Verify what is the earliest point in time during shutdown where
+ // we can deny the creation of the CookieService as a whole.
+ gCookieService = new CookieService();
+ if (gCookieService) {
+ if (NS_SUCCEEDED(gCookieService->Init())) {
+ ClearOnShutdown(&gCookieService);
+ } else {
+ gCookieService = nullptr;
+ }
+ }
+
+ return do_AddRef(gCookieService);
+}
+
+/******************************************************************************
+ * CookieService impl:
+ * public methods
+ ******************************************************************************/
+
+NS_IMPL_ISUPPORTS(CookieService, nsICookieService, nsICookieManager,
+ nsIObserver, nsISupportsWeakReference, nsIMemoryReporter)
+
+CookieService::CookieService() = default;
+
+nsresult CookieService::Init() {
+ nsresult rv;
+ mTLDService = do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mIDNService = do_GetService(NS_IDNSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mThirdPartyUtil = do_GetService(THIRDPARTYUTIL_CONTRACTID);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Init our default, and possibly private CookieStorages.
+ InitCookieStorages();
+
+ // Migrate network.cookie.lifetimePolicy pref to sanitizeOnShutdown prefs
+ MigrateCookieLifetimePrefs();
+
+ RegisterWeakMemoryReporter(this);
+
+ nsCOMPtr<nsIObserverService> os = services::GetObserverService();
+ NS_ENSURE_STATE(os);
+ os->AddObserver(this, "profile-before-change", true);
+ os->AddObserver(this, "profile-do-change", true);
+ os->AddObserver(this, "last-pb-context-exited", true);
+
+ return NS_OK;
+}
+
+void CookieService::InitCookieStorages() {
+ NS_ASSERTION(!mPersistentStorage, "already have a default CookieStorage");
+ NS_ASSERTION(!mPrivateStorage, "already have a private CookieStorage");
+
+ // Create two new CookieStorages. If we are in or beyond our observed
+ // shutdown phase, just be non-persistent.
+ if (MOZ_UNLIKELY(StaticPrefs::network_cookie_noPersistentStorage() ||
+ AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdown))) {
+ mPersistentStorage = CookiePrivateStorage::Create();
+ } else {
+ mPersistentStorage = CookiePersistentStorage::Create();
+ }
+
+ mPrivateStorage = CookiePrivateStorage::Create();
+}
+
+void CookieService::CloseCookieStorages() {
+ // return if we already closed
+ if (!mPersistentStorage) {
+ return;
+ }
+
+ // Let's nullify both storages before calling Close().
+ RefPtr<CookieStorage> privateStorage;
+ privateStorage.swap(mPrivateStorage);
+
+ RefPtr<CookieStorage> persistentStorage;
+ persistentStorage.swap(mPersistentStorage);
+
+ privateStorage->Close();
+ persistentStorage->Close();
+}
+
+CookieService::~CookieService() {
+ CloseCookieStorages();
+
+ UnregisterWeakMemoryReporter(this);
+
+ gCookieService = nullptr;
+}
+
+NS_IMETHODIMP
+CookieService::Observe(nsISupports* /*aSubject*/, const char* aTopic,
+ const char16_t* /*aData*/) {
+ // check the topic
+ if (!strcmp(aTopic, "profile-before-change")) {
+ // The profile is about to change,
+ // or is going away because the application is shutting down.
+
+ // Close the default DB connection and null out our CookieStorages before
+ // changing.
+ CloseCookieStorages();
+
+ } else if (!strcmp(aTopic, "profile-do-change")) {
+ NS_ASSERTION(!mPersistentStorage, "shouldn't have a default CookieStorage");
+ NS_ASSERTION(!mPrivateStorage, "shouldn't have a private CookieStorage");
+
+ // the profile has already changed; init the db from the new location.
+ // if we are in the private browsing state, however, we do not want to read
+ // data into it - we should instead put it into the default state, so it's
+ // ready for us if and when we switch back to it.
+ InitCookieStorages();
+
+ } else if (!strcmp(aTopic, "last-pb-context-exited")) {
+ // Flush all the cookies stored by private browsing contexts
+ OriginAttributesPattern pattern;
+ pattern.mPrivateBrowsingId.Construct(1);
+ RemoveCookiesWithOriginAttributes(pattern, ""_ns);
+ mPrivateStorage = CookiePrivateStorage::Create();
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CookieService::GetCookieBehavior(bool aIsPrivate, uint32_t* aCookieBehavior) {
+ NS_ENSURE_ARG_POINTER(aCookieBehavior);
+ *aCookieBehavior = nsICookieManager::GetCookieBehavior(aIsPrivate);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CookieService::GetCookieStringFromDocument(Document* aDocument,
+ nsACString& aCookie) {
+ NS_ENSURE_ARG(aDocument);
+
+ nsresult rv;
+
+ aCookie.Truncate();
+
+ if (!IsInitialized()) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIPrincipal> principal = aDocument->EffectiveCookiePrincipal();
+
+ if (!CookieCommons::IsSchemeSupported(principal)) {
+ return NS_OK;
+ }
+
+ CookieStorage* storage = PickStorage(principal->OriginAttributesRef());
+
+ nsAutoCString baseDomain;
+ rv = CookieCommons::GetBaseDomain(principal, baseDomain);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return NS_OK;
+ }
+
+ nsAutoCString hostFromURI;
+ rv = nsContentUtils::GetHostOrIPv6WithBrackets(principal, hostFromURI);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return NS_OK;
+ }
+
+ nsAutoCString pathFromURI;
+ rv = principal->GetFilePath(pathFromURI);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return NS_OK;
+ }
+
+ int64_t currentTimeInUsec = PR_Now();
+ int64_t currentTime = currentTimeInUsec / PR_USEC_PER_SEC;
+
+ const nsTArray<RefPtr<Cookie>>* cookies =
+ storage->GetCookiesFromHost(baseDomain, principal->OriginAttributesRef());
+ if (!cookies) {
+ return NS_OK;
+ }
+
+ // check if the nsIPrincipal is using an https secure protocol.
+ // if it isn't, then we can't send a secure cookie over the connection.
+ bool potentiallyTrustworthy = principal->GetIsOriginPotentiallyTrustworthy();
+
+ bool thirdParty = true;
+ nsPIDOMWindowInner* innerWindow = aDocument->GetInnerWindow();
+ // in gtests we don't have a window, let's consider those requests as 3rd
+ // party.
+ if (innerWindow) {
+ ThirdPartyUtil* thirdPartyUtil = ThirdPartyUtil::GetInstance();
+
+ if (thirdPartyUtil) {
+ Unused << thirdPartyUtil->IsThirdPartyWindow(
+ innerWindow->GetOuterWindow(), nullptr, &thirdParty);
+ }
+ }
+
+ bool stale = false;
+ nsTArray<Cookie*> cookieList;
+
+ // iterate the cookies!
+ for (Cookie* cookie : *cookies) {
+ // check the host, since the base domain lookup is conservative.
+ if (!CookieCommons::DomainMatches(cookie, hostFromURI)) {
+ continue;
+ }
+
+ // if the cookie is httpOnly and it's not going directly to the HTTP
+ // connection, don't send it
+ if (cookie->IsHttpOnly()) {
+ continue;
+ }
+
+ if (thirdParty && !CookieCommons::ShouldIncludeCrossSiteCookieForDocument(
+ cookie, aDocument)) {
+ continue;
+ }
+
+ // if the cookie is secure and the host scheme isn't, we can't send it
+ if (cookie->IsSecure() && !potentiallyTrustworthy) {
+ continue;
+ }
+
+ // if the nsIURI path doesn't match the cookie path, don't send it back
+ if (!CookieCommons::PathMatches(cookie, pathFromURI)) {
+ continue;
+ }
+
+ // check if the cookie has expired
+ if (cookie->Expiry() <= currentTime) {
+ continue;
+ }
+
+ // all checks passed - add to list and check if lastAccessed stamp needs
+ // updating
+ cookieList.AppendElement(cookie);
+ if (cookie->IsStale()) {
+ stale = true;
+ }
+ }
+
+ if (cookieList.IsEmpty()) {
+ return NS_OK;
+ }
+
+ // update lastAccessed timestamps. we only do this if the timestamp is stale
+ // by a certain amount, to avoid thrashing the db during pageload.
+ if (stale) {
+ storage->StaleCookies(cookieList, currentTimeInUsec);
+ }
+
+ // return cookies in order of path length; longest to shortest.
+ // this is required per RFC2109. if cookies match in length,
+ // then sort by creation time (see bug 236772).
+ cookieList.Sort(CompareCookiesForSending());
+ ComposeCookieString(cookieList, aCookie);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CookieService::GetCookieStringFromHttp(nsIURI* aHostURI, nsIChannel* aChannel,
+ nsACString& aCookieString) {
+ NS_ENSURE_ARG(aHostURI);
+ NS_ENSURE_ARG(aChannel);
+
+ aCookieString.Truncate();
+
+ if (!CookieCommons::IsSchemeSupported(aHostURI)) {
+ return NS_OK;
+ }
+
+ uint32_t rejectedReason = 0;
+ ThirdPartyAnalysisResult result = mThirdPartyUtil->AnalyzeChannel(
+ aChannel, false, aHostURI, nullptr, &rejectedReason);
+
+ OriginAttributes attrs;
+ StoragePrincipalHelper::GetOriginAttributes(
+ aChannel, attrs, StoragePrincipalHelper::eStorageAccessPrincipal);
+
+ bool isSafeTopLevelNav = CookieCommons::IsSafeTopLevelNav(aChannel);
+ bool hadCrossSiteRedirects = false;
+ bool isSameSiteForeign = CookieCommons::IsSameSiteForeign(
+ aChannel, aHostURI, &hadCrossSiteRedirects);
+
+ AutoTArray<Cookie*, 8> foundCookieList;
+ GetCookiesForURI(
+ aHostURI, aChannel, result.contains(ThirdPartyAnalysis::IsForeign),
+ result.contains(ThirdPartyAnalysis::IsThirdPartyTrackingResource),
+ result.contains(ThirdPartyAnalysis::IsThirdPartySocialTrackingResource),
+ result.contains(ThirdPartyAnalysis::IsStorageAccessPermissionGranted),
+ rejectedReason, isSafeTopLevelNav, isSameSiteForeign,
+ hadCrossSiteRedirects, true, false, attrs, foundCookieList);
+
+ ComposeCookieString(foundCookieList, aCookieString);
+
+ if (!aCookieString.IsEmpty()) {
+ COOKIE_LOGSUCCESS(GET_COOKIE, aHostURI, aCookieString, nullptr, false);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CookieService::SetCookieStringFromDocument(Document* aDocument,
+ const nsACString& aCookieString) {
+ NS_ENSURE_ARG(aDocument);
+
+ if (!IsInitialized()) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIURI> documentURI;
+ nsAutoCString baseDomain;
+ OriginAttributes attrs;
+
+ int64_t currentTimeInUsec = PR_Now();
+
+ // This function is executed in this context, I don't need to keep objects
+ // alive.
+ auto hasExistingCookiesLambda = [&](const nsACString& aBaseDomain,
+ const OriginAttributes& aAttrs) {
+ CookieStorage* storage = PickStorage(aAttrs);
+ return !!storage->CountCookiesFromHost(aBaseDomain,
+ aAttrs.mPrivateBrowsingId);
+ };
+
+ RefPtr<Cookie> cookie = CookieCommons::CreateCookieFromDocument(
+ aDocument, aCookieString, currentTimeInUsec, mTLDService, mThirdPartyUtil,
+ hasExistingCookiesLambda, getter_AddRefs(documentURI), baseDomain, attrs);
+ if (!cookie) {
+ return NS_OK;
+ }
+
+ bool thirdParty = true;
+ nsPIDOMWindowInner* innerWindow = aDocument->GetInnerWindow();
+ // in gtests we don't have a window, let's consider those requests as 3rd
+ // party.
+ if (innerWindow) {
+ ThirdPartyUtil* thirdPartyUtil = ThirdPartyUtil::GetInstance();
+
+ if (thirdPartyUtil) {
+ Unused << thirdPartyUtil->IsThirdPartyWindow(
+ innerWindow->GetOuterWindow(), nullptr, &thirdParty);
+ }
+ }
+
+ if (thirdParty && !CookieCommons::ShouldIncludeCrossSiteCookieForDocument(
+ cookie, aDocument)) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIConsoleReportCollector> crc =
+ do_QueryInterface(aDocument->GetChannel());
+
+ // add the cookie to the list. AddCookie() takes care of logging.
+ PickStorage(attrs)->AddCookie(crc, baseDomain, attrs, cookie,
+ currentTimeInUsec, documentURI, aCookieString,
+ false, aDocument->GetBrowsingContext());
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CookieService::SetCookieStringFromHttp(nsIURI* aHostURI,
+ const nsACString& aCookieHeader,
+ nsIChannel* aChannel) {
+ NS_ENSURE_ARG(aHostURI);
+ NS_ENSURE_ARG(aChannel);
+
+ if (!IsInitialized()) {
+ return NS_OK;
+ }
+
+ if (!CookieCommons::IsSchemeSupported(aHostURI)) {
+ return NS_OK;
+ }
+
+ uint32_t rejectedReason = 0;
+ ThirdPartyAnalysisResult result = mThirdPartyUtil->AnalyzeChannel(
+ aChannel, false, aHostURI, nullptr, &rejectedReason);
+
+ OriginAttributes attrs;
+ StoragePrincipalHelper::GetOriginAttributes(
+ aChannel, attrs, StoragePrincipalHelper::eStorageAccessPrincipal);
+
+ // get the base domain for the host URI.
+ // e.g. for "www.bbc.co.uk", this would be "bbc.co.uk".
+ // file:// URI's (i.e. with an empty host) are allowed, but any other
+ // scheme must have a non-empty host. A trailing dot in the host
+ // is acceptable.
+ bool requireHostMatch;
+ nsAutoCString baseDomain;
+ nsresult rv = CookieCommons::GetBaseDomain(mTLDService, aHostURI, baseDomain,
+ requireHostMatch);
+ if (NS_FAILED(rv)) {
+ COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader,
+ "couldn't get base domain from URI");
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsICookieJarSettings> cookieJarSettings =
+ CookieCommons::GetCookieJarSettings(aChannel);
+
+ nsAutoCString hostFromURI;
+ nsContentUtils::GetHostOrIPv6WithBrackets(aHostURI, hostFromURI);
+
+ nsAutoCString baseDomainFromURI;
+ rv = CookieCommons::GetBaseDomainFromHost(mTLDService, hostFromURI,
+ baseDomainFromURI);
+ NS_ENSURE_SUCCESS(rv, NS_OK);
+
+ CookieStorage* storage = PickStorage(attrs);
+
+ // check default prefs
+ uint32_t priorCookieCount = storage->CountCookiesFromHost(
+ baseDomainFromURI, attrs.mPrivateBrowsingId);
+
+ nsCOMPtr<nsIConsoleReportCollector> crc = do_QueryInterface(aChannel);
+
+ CookieStatus cookieStatus = CheckPrefs(
+ crc, cookieJarSettings, aHostURI,
+ result.contains(ThirdPartyAnalysis::IsForeign),
+ result.contains(ThirdPartyAnalysis::IsThirdPartyTrackingResource),
+ result.contains(ThirdPartyAnalysis::IsThirdPartySocialTrackingResource),
+ result.contains(ThirdPartyAnalysis::IsStorageAccessPermissionGranted),
+ aCookieHeader, priorCookieCount, attrs, &rejectedReason);
+
+ MOZ_ASSERT_IF(rejectedReason, cookieStatus == STATUS_REJECTED);
+
+ // fire a notification if third party or if cookie was rejected
+ // (but not if there was an error)
+ switch (cookieStatus) {
+ case STATUS_REJECTED:
+ CookieCommons::NotifyRejected(aHostURI, aChannel, rejectedReason,
+ OPERATION_WRITE);
+ return NS_OK; // Stop here
+ case STATUS_REJECTED_WITH_ERROR:
+ CookieCommons::NotifyRejected(aHostURI, aChannel, rejectedReason,
+ OPERATION_WRITE);
+ return NS_OK;
+ case STATUS_ACCEPTED: // Fallthrough
+ case STATUS_ACCEPT_SESSION:
+ NotifyAccepted(aChannel);
+ break;
+ default:
+ break;
+ }
+
+ bool addonAllowsLoad = false;
+ nsCOMPtr<nsIURI> channelURI;
+ NS_GetFinalChannelURI(aChannel, getter_AddRefs(channelURI));
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+ addonAllowsLoad = BasePrincipal::Cast(loadInfo->TriggeringPrincipal())
+ ->AddonAllowsLoad(channelURI);
+
+ bool isForeignAndNotAddon = false;
+ if (!addonAllowsLoad) {
+ mThirdPartyUtil->IsThirdPartyChannel(aChannel, aHostURI,
+ &isForeignAndNotAddon);
+ }
+
+ bool mustBePartitioned =
+ isForeignAndNotAddon &&
+ cookieJarSettings->GetCookieBehavior() ==
+ nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN &&
+ !result.contains(ThirdPartyAnalysis::IsStorageAccessPermissionGranted);
+
+ nsCString cookieHeader(aCookieHeader);
+
+ bool moreCookieToRead = true;
+
+ // process each cookie in the header
+ while (moreCookieToRead) {
+ CookieStruct cookieData;
+ bool canSetCookie = false;
+
+ moreCookieToRead =
+ CanSetCookie(aHostURI, baseDomain, cookieData, requireHostMatch,
+ cookieStatus, cookieHeader, true, isForeignAndNotAddon,
+ mustBePartitioned, crc, canSetCookie);
+
+ if (!canSetCookie) {
+ continue;
+ }
+
+ // check permissions from site permission list.
+ if (!CookieCommons::CheckCookiePermission(aChannel, cookieData)) {
+ COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader,
+ "cookie rejected by permission manager");
+ CookieCommons::NotifyRejected(
+ aHostURI, aChannel,
+ nsIWebProgressListener::STATE_COOKIES_BLOCKED_BY_PERMISSION,
+ OPERATION_WRITE);
+ CookieLogging::LogMessageToConsole(
+ crc, aHostURI, nsIScriptError::warningFlag,
+ CONSOLE_REJECTION_CATEGORY, "CookieRejectedByPermissionManager"_ns,
+ AutoTArray<nsString, 1>{
+ NS_ConvertUTF8toUTF16(cookieData.name()),
+ });
+ continue;
+ }
+
+ // create a new Cookie
+ RefPtr<Cookie> cookie = Cookie::Create(cookieData, attrs);
+ MOZ_ASSERT(cookie);
+
+ int64_t currentTimeInUsec = PR_Now();
+ cookie->SetLastAccessed(currentTimeInUsec);
+ cookie->SetCreationTime(
+ Cookie::GenerateUniqueCreationTime(currentTimeInUsec));
+
+ RefPtr<BrowsingContext> bc = loadInfo->GetBrowsingContext();
+
+ // add the cookie to the list. AddCookie() takes care of logging.
+ storage->AddCookie(crc, baseDomain, attrs, cookie, currentTimeInUsec,
+ aHostURI, aCookieHeader, true, bc);
+ }
+
+ return NS_OK;
+}
+
+void CookieService::NotifyAccepted(nsIChannel* aChannel) {
+ ContentBlockingNotifier::OnDecision(
+ aChannel, ContentBlockingNotifier::BlockingDecision::eAllow, 0);
+}
+
+/******************************************************************************
+ * CookieService:
+ * public transaction helper impl
+ ******************************************************************************/
+
+NS_IMETHODIMP
+CookieService::RunInTransaction(nsICookieTransactionCallback* aCallback) {
+ NS_ENSURE_ARG(aCallback);
+
+ if (!IsInitialized()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ mPersistentStorage->EnsureInitialized();
+ return mPersistentStorage->RunInTransaction(aCallback);
+}
+
+/******************************************************************************
+ * nsICookieManager impl:
+ * nsICookieManager
+ ******************************************************************************/
+
+NS_IMETHODIMP
+CookieService::RemoveAll() {
+ if (!IsInitialized()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ mPersistentStorage->EnsureInitialized();
+ mPersistentStorage->RemoveAll();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CookieService::GetCookies(nsTArray<RefPtr<nsICookie>>& aCookies) {
+ if (!IsInitialized()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ mPersistentStorage->EnsureInitialized();
+
+ // We expose only non-private cookies.
+ mPersistentStorage->GetCookies(aCookies);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CookieService::GetSessionCookies(nsTArray<RefPtr<nsICookie>>& aCookies) {
+ if (!IsInitialized()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ mPersistentStorage->EnsureInitialized();
+
+ // We expose only non-private cookies.
+ mPersistentStorage->GetSessionCookies(aCookies);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CookieService::Add(const nsACString& aHost, const nsACString& aPath,
+ const nsACString& aName, const nsACString& aValue,
+ bool aIsSecure, bool aIsHttpOnly, bool aIsSession,
+ int64_t aExpiry, JS::Handle<JS::Value> aOriginAttributes,
+ int32_t aSameSite, nsICookie::schemeType aSchemeMap,
+ JSContext* aCx) {
+ OriginAttributes attrs;
+
+ if (!aOriginAttributes.isObject() || !attrs.Init(aCx, aOriginAttributes)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ return AddNative(aHost, aPath, aName, aValue, aIsSecure, aIsHttpOnly,
+ aIsSession, aExpiry, &attrs, aSameSite, aSchemeMap);
+}
+
+NS_IMETHODIMP_(nsresult)
+CookieService::AddNative(const nsACString& aHost, const nsACString& aPath,
+ const nsACString& aName, const nsACString& aValue,
+ bool aIsSecure, bool aIsHttpOnly, bool aIsSession,
+ int64_t aExpiry, OriginAttributes* aOriginAttributes,
+ int32_t aSameSite, nsICookie::schemeType aSchemeMap) {
+ if (NS_WARN_IF(!aOriginAttributes)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!IsInitialized()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // first, normalize the hostname, and fail if it contains illegal characters.
+ nsAutoCString host(aHost);
+ nsresult rv = NormalizeHost(host);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // get the base domain for the host URI.
+ // e.g. for "www.bbc.co.uk", this would be "bbc.co.uk".
+ nsAutoCString baseDomain;
+ rv = CookieCommons::GetBaseDomainFromHost(mTLDService, host, baseDomain);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int64_t currentTimeInUsec = PR_Now();
+ CookieKey key = CookieKey(baseDomain, *aOriginAttributes);
+
+ CookieStruct cookieData(nsCString(aName), nsCString(aValue), nsCString(aHost),
+ nsCString(aPath), aExpiry, currentTimeInUsec,
+ Cookie::GenerateUniqueCreationTime(currentTimeInUsec),
+ aIsHttpOnly, aIsSession, aIsSecure, false, aSameSite,
+ aSameSite, aSchemeMap);
+
+ RefPtr<Cookie> cookie = Cookie::Create(cookieData, key.mOriginAttributes);
+ MOZ_ASSERT(cookie);
+
+ CookieStorage* storage = PickStorage(*aOriginAttributes);
+ storage->AddCookie(nullptr, baseDomain, *aOriginAttributes, cookie,
+ currentTimeInUsec, nullptr, VoidCString(), true, nullptr);
+ return NS_OK;
+}
+
+nsresult CookieService::Remove(const nsACString& aHost,
+ const OriginAttributes& aAttrs,
+ const nsACString& aName,
+ const nsACString& aPath) {
+ // first, normalize the hostname, and fail if it contains illegal characters.
+ nsAutoCString host(aHost);
+ nsresult rv = NormalizeHost(host);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString baseDomain;
+ if (!host.IsEmpty()) {
+ rv = CookieCommons::GetBaseDomainFromHost(mTLDService, host, baseDomain);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (!IsInitialized()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ CookieStorage* storage = PickStorage(aAttrs);
+ storage->RemoveCookie(baseDomain, aAttrs, host, PromiseFlatCString(aName),
+ PromiseFlatCString(aPath));
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CookieService::Remove(const nsACString& aHost, const nsACString& aName,
+ const nsACString& aPath,
+ JS::Handle<JS::Value> aOriginAttributes, JSContext* aCx) {
+ OriginAttributes attrs;
+
+ if (!aOriginAttributes.isObject() || !attrs.Init(aCx, aOriginAttributes)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ return RemoveNative(aHost, aName, aPath, &attrs);
+}
+
+NS_IMETHODIMP_(nsresult)
+CookieService::RemoveNative(const nsACString& aHost, const nsACString& aName,
+ const nsACString& aPath,
+ OriginAttributes* aOriginAttributes) {
+ if (NS_WARN_IF(!aOriginAttributes)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv = Remove(aHost, *aOriginAttributes, aName, aPath);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+void CookieService::GetCookiesForURI(
+ nsIURI* aHostURI, nsIChannel* aChannel, bool aIsForeign,
+ bool aIsThirdPartyTrackingResource,
+ bool aIsThirdPartySocialTrackingResource,
+ bool aStorageAccessPermissionGranted, uint32_t aRejectedReason,
+ bool aIsSafeTopLevelNav, bool aIsSameSiteForeign,
+ bool aHadCrossSiteRedirects, bool aHttpBound,
+ bool aAllowSecureCookiesToInsecureOrigin,
+ const OriginAttributes& aOriginAttrs, nsTArray<Cookie*>& aCookieList) {
+ NS_ASSERTION(aHostURI, "null host!");
+
+ if (!CookieCommons::IsSchemeSupported(aHostURI)) {
+ return;
+ }
+
+ if (!IsInitialized()) {
+ return;
+ }
+
+ CookieStorage* storage = PickStorage(aOriginAttrs);
+
+ // get the base domain, host, and path from the URI.
+ // e.g. for "www.bbc.co.uk", the base domain would be "bbc.co.uk".
+ // file:// URI's (i.e. with an empty host) are allowed, but any other
+ // scheme must have a non-empty host. A trailing dot in the host
+ // is acceptable.
+ bool requireHostMatch;
+ nsAutoCString baseDomain;
+ nsAutoCString hostFromURI;
+ nsAutoCString pathFromURI;
+ nsresult rv = CookieCommons::GetBaseDomain(mTLDService, aHostURI, baseDomain,
+ requireHostMatch);
+ if (NS_SUCCEEDED(rv)) {
+ rv = nsContentUtils::GetHostOrIPv6WithBrackets(aHostURI, hostFromURI);
+ }
+ if (NS_SUCCEEDED(rv)) {
+ rv = aHostURI->GetFilePath(pathFromURI);
+ }
+ if (NS_FAILED(rv)) {
+ COOKIE_LOGFAILURE(GET_COOKIE, aHostURI, VoidCString(),
+ "invalid host/path from URI");
+ return;
+ }
+
+ nsCOMPtr<nsICookieJarSettings> cookieJarSettings =
+ CookieCommons::GetCookieJarSettings(aChannel);
+
+ nsAutoCString normalizedHostFromURI(hostFromURI);
+ rv = NormalizeHost(normalizedHostFromURI);
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ nsAutoCString baseDomainFromURI;
+ rv = CookieCommons::GetBaseDomainFromHost(mTLDService, normalizedHostFromURI,
+ baseDomainFromURI);
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ // check default prefs
+ uint32_t rejectedReason = aRejectedReason;
+ uint32_t priorCookieCount = storage->CountCookiesFromHost(
+ baseDomainFromURI, aOriginAttrs.mPrivateBrowsingId);
+
+ nsCOMPtr<nsIConsoleReportCollector> crc = do_QueryInterface(aChannel);
+ CookieStatus cookieStatus = CheckPrefs(
+ crc, cookieJarSettings, aHostURI, aIsForeign,
+ aIsThirdPartyTrackingResource, aIsThirdPartySocialTrackingResource,
+ aStorageAccessPermissionGranted, VoidCString(), priorCookieCount,
+ aOriginAttrs, &rejectedReason);
+
+ MOZ_ASSERT_IF(rejectedReason, cookieStatus == STATUS_REJECTED);
+
+ // for GetCookie(), we only fire acceptance/rejection notifications
+ // (but not if there was an error)
+ switch (cookieStatus) {
+ case STATUS_REJECTED:
+ // If we don't have any cookies from this host, fail silently.
+ if (priorCookieCount) {
+ CookieCommons::NotifyRejected(aHostURI, aChannel, rejectedReason,
+ OPERATION_READ);
+ }
+ return;
+ default:
+ break;
+ }
+
+ // Note: The following permissions logic is mirrored in
+ // extensions::MatchPattern::MatchesCookie.
+ // If it changes, please update that function, or file a bug for someone
+ // else to do so.
+
+ // check if aHostURI is using an https secure protocol.
+ // if it isn't, then we can't send a secure cookie over the connection.
+ // if SchemeIs fails, assume an insecure connection, to be on the safe side
+ bool potentiallyTrustworthy =
+ nsMixedContentBlocker::IsPotentiallyTrustworthyOrigin(aHostURI);
+
+ int64_t currentTimeInUsec = PR_Now();
+ int64_t currentTime = currentTimeInUsec / PR_USEC_PER_SEC;
+ bool stale = false;
+
+ const nsTArray<RefPtr<Cookie>>* cookies =
+ storage->GetCookiesFromHost(baseDomain, aOriginAttrs);
+ if (!cookies) {
+ return;
+ }
+
+ bool laxByDefault =
+ StaticPrefs::network_cookie_sameSite_laxByDefault() &&
+ !nsContentUtils::IsURIInPrefList(
+ aHostURI, "network.cookie.sameSite.laxByDefault.disabledHosts");
+
+ // iterate the cookies!
+ for (Cookie* cookie : *cookies) {
+ // check the host, since the base domain lookup is conservative.
+ if (!CookieCommons::DomainMatches(cookie, hostFromURI)) {
+ continue;
+ }
+
+ // if the cookie is secure and the host scheme isn't, we avoid sending
+ // cookie if possible. But for process synchronization purposes, we may want
+ // the content process to know about the cookie (without it's value). In
+ // which case we will wipe the value before sending
+ if (cookie->IsSecure() && !potentiallyTrustworthy &&
+ !aAllowSecureCookiesToInsecureOrigin) {
+ continue;
+ }
+
+ // if the cookie is httpOnly and it's not going directly to the HTTP
+ // connection, don't send it
+ if (cookie->IsHttpOnly() && !aHttpBound) {
+ continue;
+ }
+
+ // if the nsIURI path doesn't match the cookie path, don't send it back
+ if (!CookieCommons::PathMatches(cookie, pathFromURI)) {
+ continue;
+ }
+
+ // check if the cookie has expired
+ if (cookie->Expiry() <= currentTime) {
+ continue;
+ }
+
+ if (aHttpBound && aIsSameSiteForeign) {
+ bool blockCookie = !ProcessSameSiteCookieForForeignRequest(
+ aChannel, cookie, aIsSafeTopLevelNav, aHadCrossSiteRedirects,
+ laxByDefault);
+
+ if (blockCookie) {
+ if (aHadCrossSiteRedirects) {
+ CookieLogging::LogMessageToConsole(
+ crc, aHostURI, nsIScriptError::warningFlag,
+ CONSOLE_REJECTION_CATEGORY, "CookieBlockedCrossSiteRedirect"_ns,
+ AutoTArray<nsString, 1>{
+ NS_ConvertUTF8toUTF16(cookie->Name()),
+ });
+ }
+ continue;
+ }
+ }
+
+ // all checks passed - add to list and check if lastAccessed stamp needs
+ // updating
+ aCookieList.AppendElement(cookie);
+ if (cookie->IsStale()) {
+ stale = true;
+ }
+ }
+
+ if (aCookieList.IsEmpty()) {
+ return;
+ }
+
+ // Send a notification about the acceptance of the cookies now that we found
+ // some.
+ NotifyAccepted(aChannel);
+
+ // update lastAccessed timestamps. we only do this if the timestamp is stale
+ // by a certain amount, to avoid thrashing the db during pageload.
+ if (stale) {
+ storage->StaleCookies(aCookieList, currentTimeInUsec);
+ }
+
+ // return cookies in order of path length; longest to shortest.
+ // this is required per RFC2109. if cookies match in length,
+ // then sort by creation time (see bug 236772).
+ aCookieList.Sort(CompareCookiesForSending());
+}
+
+static bool ContainsUnicodeChars(const nsCString& str) {
+ const auto* start = str.BeginReading();
+ const auto* end = str.EndReading();
+
+ return std::find_if(start, end, [](unsigned char c) { return c >= 0x80; }) !=
+ end;
+}
+
+static void RecordUnicodeTelemetry(const CookieStruct& cookieData) {
+ auto label = Telemetry::LABELS_NETWORK_COOKIE_UNICODE_BYTE::none;
+ if (ContainsUnicodeChars(cookieData.name())) {
+ label = Telemetry::LABELS_NETWORK_COOKIE_UNICODE_BYTE::unicodeName;
+ } else if (ContainsUnicodeChars(cookieData.value())) {
+ label = Telemetry::LABELS_NETWORK_COOKIE_UNICODE_BYTE::unicodeValue;
+ }
+ Telemetry::AccumulateCategorical(label);
+}
+
+static void RecordPartitionedTelemetry(const CookieStruct& aCookieData,
+ bool aIsForeign) {
+ mozilla::glean::networking::set_cookie.Add(1);
+ if (aCookieData.isPartitioned()) {
+ mozilla::glean::networking::set_cookie_partitioned.AddToNumerator(1);
+ }
+ if (aIsForeign) {
+ mozilla::glean::networking::set_cookie_foreign.AddToNumerator(1);
+ }
+ if (aIsForeign && aCookieData.isPartitioned()) {
+ mozilla::glean::networking::set_cookie_foreign_partitioned.AddToNumerator(
+ 1);
+ }
+}
+
+// processes a single cookie, and returns true if there are more cookies
+// to be processed
+bool CookieService::CanSetCookie(
+ nsIURI* aHostURI, const nsACString& aBaseDomain, CookieStruct& aCookieData,
+ bool aRequireHostMatch, CookieStatus aStatus, nsCString& aCookieHeader,
+ bool aFromHttp, bool aIsForeignAndNotAddon, bool aPartitionedOnly,
+ nsIConsoleReportCollector* aCRC, bool& aSetCookie) {
+ MOZ_ASSERT(aHostURI);
+
+ aSetCookie = false;
+
+ // init expiryTime such that session cookies won't prematurely expire
+ aCookieData.expiry() = INT64_MAX;
+
+ aCookieData.schemeMap() = CookieCommons::URIToSchemeType(aHostURI);
+
+ // aCookieHeader is an in/out param to point to the next cookie, if
+ // there is one. Save the present value for logging purposes
+ nsCString savedCookieHeader(aCookieHeader);
+
+ // newCookie says whether there are multiple cookies in the header;
+ // so we can handle them separately.
+ nsAutoCString expires;
+ nsAutoCString maxage;
+ bool acceptedByParser = false;
+ bool newCookie = ParseAttributes(aCRC, aHostURI, aCookieHeader, aCookieData,
+ expires, maxage, acceptedByParser);
+ if (!acceptedByParser) {
+ return newCookie;
+ }
+
+ // Collect telemetry on how often secure cookies are set from non-secure
+ // origins, and vice-versa.
+ //
+ // 0 = nonsecure and "http:"
+ // 1 = nonsecure and "https:"
+ // 2 = secure and "http:"
+ // 3 = secure and "https:"
+ bool potentiallyTrustworthy =
+ nsMixedContentBlocker::IsPotentiallyTrustworthyOrigin(aHostURI);
+
+ int64_t currentTimeInUsec = PR_Now();
+
+ // calculate expiry time of cookie.
+ aCookieData.isSession() =
+ GetExpiry(aCookieData, expires, maxage,
+ currentTimeInUsec / PR_USEC_PER_SEC, aFromHttp);
+ if (aStatus == STATUS_ACCEPT_SESSION) {
+ // force lifetime to session. note that the expiration time, if set above,
+ // will still apply.
+ aCookieData.isSession() = true;
+ }
+
+ // reject cookie if it's over the size limit, per RFC2109
+ if (!CookieCommons::CheckNameAndValueSize(aCookieData)) {
+ COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader,
+ "cookie too big (> 4kb)");
+
+ AutoTArray<nsString, 2> params = {
+ NS_ConvertUTF8toUTF16(aCookieData.name())};
+
+ nsString size;
+ size.AppendInt(kMaxBytesPerCookie);
+ params.AppendElement(size);
+
+ CookieLogging::LogMessageToConsole(
+ aCRC, aHostURI, nsIScriptError::warningFlag, CONSOLE_OVERSIZE_CATEGORY,
+ "CookieOversize"_ns, params);
+ return newCookie;
+ }
+
+ RecordUnicodeTelemetry(aCookieData);
+
+ // We count SetCookie operations in the parent process only for HTTP set
+ // cookies to prevent double counting.
+ if (XRE_IsParentProcess() || !aFromHttp) {
+ RecordPartitionedTelemetry(aCookieData, aIsForeignAndNotAddon);
+ }
+
+ if (!CookieCommons::CheckName(aCookieData)) {
+ COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader,
+ "invalid name character");
+ CookieLogging::LogMessageToConsole(
+ aCRC, aHostURI, nsIScriptError::warningFlag, CONSOLE_REJECTION_CATEGORY,
+ "CookieRejectedInvalidCharName"_ns,
+ AutoTArray<nsString, 1>{
+ NS_ConvertUTF8toUTF16(aCookieData.name()),
+ });
+ return newCookie;
+ }
+
+ // domain & path checks
+ if (!CheckDomain(aCookieData, aHostURI, aBaseDomain, aRequireHostMatch)) {
+ COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader,
+ "failed the domain tests");
+ CookieLogging::LogMessageToConsole(
+ aCRC, aHostURI, nsIScriptError::warningFlag, CONSOLE_REJECTION_CATEGORY,
+ "CookieRejectedInvalidDomain"_ns,
+ AutoTArray<nsString, 1>{
+ NS_ConvertUTF8toUTF16(aCookieData.name()),
+ });
+ return newCookie;
+ }
+
+ if (!CheckPath(aCookieData, aCRC, aHostURI)) {
+ COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader,
+ "failed the path tests");
+ return newCookie;
+ }
+
+ if (!CheckHiddenPrefix(aCookieData)) {
+ COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader,
+ "failed the CheckHiddenPrefix tests");
+ CookieLogging::LogMessageToConsole(
+ aCRC, aHostURI, nsIScriptError::warningFlag, CONSOLE_REJECTION_CATEGORY,
+ "CookieRejectedInvalidPrefix"_ns,
+ AutoTArray<nsString, 1>{
+ NS_ConvertUTF8toUTF16(aCookieData.name()),
+ });
+ return newCookie;
+ }
+
+ // magic prefix checks. MUST be run after CheckDomain() and CheckPath()
+ if (!CheckPrefixes(aCookieData, potentiallyTrustworthy)) {
+ COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader,
+ "failed the prefix tests");
+ CookieLogging::LogMessageToConsole(
+ aCRC, aHostURI, nsIScriptError::warningFlag, CONSOLE_REJECTION_CATEGORY,
+ "CookieRejectedInvalidPrefix"_ns,
+ AutoTArray<nsString, 1>{
+ NS_ConvertUTF8toUTF16(aCookieData.name()),
+ });
+ return newCookie;
+ }
+
+ if (!CookieCommons::CheckValue(aCookieData)) {
+ COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader,
+ "invalid value character");
+ CookieLogging::LogMessageToConsole(
+ aCRC, aHostURI, nsIScriptError::warningFlag, CONSOLE_REJECTION_CATEGORY,
+ "CookieRejectedInvalidCharValue"_ns,
+ AutoTArray<nsString, 1>{
+ NS_ConvertUTF8toUTF16(aCookieData.name()),
+ });
+ return newCookie;
+ }
+
+ // if the new cookie is httponly, make sure we're not coming from script
+ if (!aFromHttp && aCookieData.isHttpOnly()) {
+ COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader,
+ "cookie is httponly; coming from script");
+ CookieLogging::LogMessageToConsole(
+ aCRC, aHostURI, nsIScriptError::warningFlag, CONSOLE_REJECTION_CATEGORY,
+ "CookieRejectedHttpOnlyButFromScript"_ns,
+ AutoTArray<nsString, 1>{
+ NS_ConvertUTF8toUTF16(aCookieData.name()),
+ });
+ return newCookie;
+ }
+
+ // If the new cookie is non-https and wants to set secure flag,
+ // browser have to ignore this new cookie.
+ // (draft-ietf-httpbis-cookie-alone section 3.1)
+ if (aCookieData.isSecure() && !potentiallyTrustworthy) {
+ COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader,
+ "non-https cookie can't set secure flag");
+ CookieLogging::LogMessageToConsole(
+ aCRC, aHostURI, nsIScriptError::warningFlag, CONSOLE_REJECTION_CATEGORY,
+ "CookieRejectedSecureButNonHttps"_ns,
+ AutoTArray<nsString, 1>{
+ NS_ConvertUTF8toUTF16(aCookieData.name()),
+ });
+ return newCookie;
+ }
+
+ // If the new cookie is same-site but in a cross site context,
+ // browser must ignore the cookie.
+ bool laxByDefault =
+ StaticPrefs::network_cookie_sameSite_laxByDefault() &&
+ !nsContentUtils::IsURIInPrefList(
+ aHostURI, "network.cookie.sameSite.laxByDefault.disabledHosts");
+ auto effectiveSameSite =
+ laxByDefault ? aCookieData.sameSite() : aCookieData.rawSameSite();
+ if ((effectiveSameSite != nsICookie::SAMESITE_NONE) &&
+ aIsForeignAndNotAddon) {
+ COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader,
+ "failed the samesite tests");
+
+ CookieLogging::LogMessageToConsole(
+ aCRC, aHostURI, nsIScriptError::warningFlag, CONSOLE_SAMESITE_CATEGORY,
+ "CookieRejectedForNonSameSiteness"_ns,
+ AutoTArray<nsString, 1>{
+ NS_ConvertUTF8toUTF16(aCookieData.name()),
+ });
+ return newCookie;
+ }
+
+ // If the cookie does not have the partitioned attribute,
+ // but is foreign we should give the developer a message.
+ // If CHIPS isn't required yet, we will warn the console
+ // that we have upcoming changes. Otherwise we give a rejection message.
+ if (aPartitionedOnly && !aCookieData.isPartitioned() &&
+ aIsForeignAndNotAddon) {
+ if (StaticPrefs::network_cookie_cookieBehavior_optInPartitioning()) {
+ COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader,
+ "foreign cookies must be partitioned");
+ CookieLogging::LogMessageToConsole(
+ aCRC, aHostURI, nsIScriptError::warningFlag, CONSOLE_CHIPS_CATEGORY,
+ "CookieForeignNoPartitionedError"_ns,
+ AutoTArray<nsString, 1>{
+ NS_ConvertUTF8toUTF16(aCookieData.name()),
+ });
+ return newCookie;
+ }
+ CookieLogging::LogMessageToConsole(
+ aCRC, aHostURI, nsIScriptError::warningFlag, CONSOLE_CHIPS_CATEGORY,
+ "CookieForeignNoPartitionedWarning"_ns,
+ AutoTArray<nsString, 1>{
+ NS_ConvertUTF8toUTF16(aCookieData.name()),
+ });
+ }
+
+ aSetCookie = true;
+ return newCookie;
+}
+
+/******************************************************************************
+ * CookieService impl:
+ * private cookie header parsing functions
+ ******************************************************************************/
+
+// clang-format off
+// The following comment block elucidates the function of ParseAttributes.
+/******************************************************************************
+ ** Augmented BNF, modified from RFC2109 Section 4.2.2 and RFC2616 Section 2.1
+ ** please note: this BNF deviates from both specifications, and reflects this
+ ** implementation. <bnf> indicates a reference to the defined grammar "bnf".
+
+ ** Differences from RFC2109/2616 and explanations:
+ 1. implied *LWS
+ The grammar described by this specification is word-based. Except
+ where noted otherwise, linear white space (<LWS>) can be included
+ between any two adjacent words (token or quoted-string), and
+ between adjacent words and separators, without changing the
+ interpretation of a field.
+ <LWS> according to spec is SP|HT|CR|LF, but here, we allow only SP | HT.
+
+ 2. We use CR | LF as cookie separators, not ',' per spec, since ',' is in
+ common use inside values.
+
+ 3. tokens and values have looser restrictions on allowed characters than
+ spec. This is also due to certain characters being in common use inside
+ values. We allow only '=' to separate token/value pairs, and ';' to
+ terminate tokens or values. <LWS> is allowed within tokens and values
+ (see bug 206022).
+
+ 4. where appropriate, full <OCTET>s are allowed, where the spec dictates to
+ reject control chars or non-ASCII chars. This is erring on the loose
+ side, since there's probably no good reason to enforce this strictness.
+
+ 5. Attribute "HttpOnly", not covered in the RFCs, is supported
+ (see bug 178993).
+
+ ** Begin BNF:
+ token = 1*<any allowed-chars except separators>
+ value = 1*<any allowed-chars except value-sep>
+ separators = ";" | "="
+ value-sep = ";"
+ cookie-sep = CR | LF
+ allowed-chars = <any OCTET except NUL or cookie-sep>
+ OCTET = <any 8-bit sequence of data>
+ LWS = SP | HT
+ NUL = <US-ASCII NUL, null control character (0)>
+ CR = <US-ASCII CR, carriage return (13)>
+ LF = <US-ASCII LF, linefeed (10)>
+ SP = <US-ASCII SP, space (32)>
+ HT = <US-ASCII HT, horizontal-tab (9)>
+
+ set-cookie = "Set-Cookie:" cookies
+ cookies = cookie *( cookie-sep cookie )
+ cookie = [NAME "="] VALUE *(";" cookie-av) ; cookie NAME/VALUE must come first
+ NAME = token ; cookie name
+ VALUE = value ; cookie value
+ cookie-av = token ["=" value]
+
+ valid values for cookie-av (checked post-parsing) are:
+ cookie-av = "Path" "=" value
+ | "Domain" "=" value
+ | "Expires" "=" value
+ | "Max-Age" "=" value
+ | "Comment" "=" value
+ | "Version" "=" value
+ | "Secure"
+ | "HttpOnly"
+
+******************************************************************************/
+// clang-format on
+
+// helper functions for GetTokenValue
+static inline bool isnull(char c) { return c == 0; }
+static inline bool iswhitespace(char c) { return c == ' ' || c == '\t'; }
+static inline bool isterminator(char c) { return c == '\n' || c == '\r'; }
+static inline bool isvalueseparator(char c) {
+ return isterminator(c) || c == ';';
+}
+static inline bool istokenseparator(char c) {
+ return isvalueseparator(c) || c == '=';
+}
+
+// Parse a single token/value pair.
+// Returns true if a cookie terminator is found, so caller can parse new cookie.
+bool CookieService::GetTokenValue(nsACString::const_char_iterator& aIter,
+ nsACString::const_char_iterator& aEndIter,
+ nsDependentCSubstring& aTokenString,
+ nsDependentCSubstring& aTokenValue,
+ bool& aEqualsFound) {
+ nsACString::const_char_iterator start;
+ nsACString::const_char_iterator lastSpace;
+ // initialize value string to clear garbage
+ aTokenValue.Rebind(aIter, aIter);
+
+ // find <token>, including any <LWS> between the end-of-token and the
+ // token separator. we'll remove trailing <LWS> next
+ while (aIter != aEndIter && iswhitespace(*aIter)) {
+ ++aIter;
+ }
+ start = aIter;
+ while (aIter != aEndIter && !isnull(*aIter) && !istokenseparator(*aIter)) {
+ ++aIter;
+ }
+
+ // remove trailing <LWS>; first check we're not at the beginning
+ lastSpace = aIter;
+ if (lastSpace != start) {
+ while (--lastSpace != start && iswhitespace(*lastSpace)) {
+ }
+ ++lastSpace;
+ }
+ aTokenString.Rebind(start, lastSpace);
+
+ aEqualsFound = (*aIter == '=');
+ if (aEqualsFound) {
+ // find <value>
+ while (++aIter != aEndIter && iswhitespace(*aIter)) {
+ }
+
+ start = aIter;
+
+ // process <token>
+ // just look for ';' to terminate ('=' allowed)
+ while (aIter != aEndIter && !isnull(*aIter) && !isvalueseparator(*aIter)) {
+ ++aIter;
+ }
+
+ // remove trailing <LWS>; first check we're not at the beginning
+ if (aIter != start) {
+ lastSpace = aIter;
+ while (--lastSpace != start && iswhitespace(*lastSpace)) {
+ }
+
+ aTokenValue.Rebind(start, ++lastSpace);
+ }
+ }
+
+ // aIter is on ';', or terminator, or EOS
+ if (aIter != aEndIter) {
+ // if on terminator, increment past & return true to process new cookie
+ if (isterminator(*aIter)) {
+ ++aIter;
+ return true;
+ }
+ // fall-through: aIter is on ';', increment and return false
+ ++aIter;
+ }
+ return false;
+}
+
+static inline void SetSameSiteAttributeDefault(CookieStruct& aCookieData) {
+ // Set cookie with SameSite attribute that is treated as Default
+ // and doesn't requires changing the DB schema.
+ aCookieData.sameSite() = nsICookie::SAMESITE_LAX;
+ aCookieData.rawSameSite() = nsICookie::SAMESITE_NONE;
+}
+
+static inline void SetSameSiteAttribute(CookieStruct& aCookieData,
+ int32_t aValue) {
+ aCookieData.sameSite() = aValue;
+ aCookieData.rawSameSite() = aValue;
+}
+
+// Parses attributes from cookie header. expires/max-age attributes aren't
+// folded into the cookie struct here, because we don't know which one to use
+// until we've parsed the header.
+bool CookieService::ParseAttributes(nsIConsoleReportCollector* aCRC,
+ nsIURI* aHostURI, nsCString& aCookieHeader,
+ CookieStruct& aCookieData,
+ nsACString& aExpires, nsACString& aMaxage,
+ bool& aAcceptedByParser) {
+ aAcceptedByParser = false;
+
+ static const char kPath[] = "path";
+ static const char kDomain[] = "domain";
+ static const char kExpires[] = "expires";
+ static const char kMaxage[] = "max-age";
+ static const char kSecure[] = "secure";
+ static const char kHttpOnly[] = "httponly";
+ static const char kSameSite[] = "samesite";
+ static const char kSameSiteLax[] = "lax";
+ static const char kSameSiteNone[] = "none";
+ static const char kSameSiteStrict[] = "strict";
+ static const char kPartitioned[] = "partitioned";
+
+ nsACString::const_char_iterator cookieStart;
+ aCookieHeader.BeginReading(cookieStart);
+
+ nsACString::const_char_iterator cookieEnd;
+ aCookieHeader.EndReading(cookieEnd);
+
+ aCookieData.isSecure() = false;
+ aCookieData.isHttpOnly() = false;
+
+ SetSameSiteAttributeDefault(aCookieData);
+
+ nsDependentCSubstring tokenString(cookieStart, cookieStart);
+ nsDependentCSubstring tokenValue(cookieStart, cookieStart);
+ bool newCookie;
+ bool equalsFound;
+
+ // extract cookie <NAME> & <VALUE> (first attribute), and copy the strings.
+ // if we find multiple cookies, return for processing
+ // note: if there's no '=', we assume token is <VALUE>. this is required by
+ // some sites (see bug 169091).
+ // XXX fix the parser to parse according to <VALUE> grammar for this case
+ newCookie = GetTokenValue(cookieStart, cookieEnd, tokenString, tokenValue,
+ equalsFound);
+ if (equalsFound) {
+ aCookieData.name() = tokenString;
+ aCookieData.value() = tokenValue;
+ } else {
+ aCookieData.value() = tokenString;
+ }
+
+ // extract remaining attributes
+ while (cookieStart != cookieEnd && !newCookie) {
+ newCookie = GetTokenValue(cookieStart, cookieEnd, tokenString, tokenValue,
+ equalsFound);
+
+ // decide which attribute we have, and copy the string
+ if (tokenString.LowerCaseEqualsLiteral(kPath)) {
+ aCookieData.path() = tokenValue;
+
+ } else if (tokenString.LowerCaseEqualsLiteral(kDomain)) {
+ aCookieData.host() = tokenValue;
+
+ } else if (tokenString.LowerCaseEqualsLiteral(kExpires)) {
+ aExpires = tokenValue;
+
+ } else if (tokenString.LowerCaseEqualsLiteral(kMaxage)) {
+ aMaxage = tokenValue;
+
+ // ignore any tokenValue for isSecure; just set the boolean
+ } else if (tokenString.LowerCaseEqualsLiteral(kSecure)) {
+ aCookieData.isSecure() = true;
+
+ // ignore any tokenValue for isPartitioned; just set the boolean
+ } else if (tokenString.LowerCaseEqualsLiteral(kPartitioned)) {
+ aCookieData.isPartitioned() = true;
+
+ // ignore any tokenValue for isHttpOnly (see bug 178993);
+ // just set the boolean
+ } else if (tokenString.LowerCaseEqualsLiteral(kHttpOnly)) {
+ aCookieData.isHttpOnly() = true;
+
+ } else if (tokenString.LowerCaseEqualsLiteral(kSameSite)) {
+ if (tokenValue.LowerCaseEqualsLiteral(kSameSiteLax)) {
+ SetSameSiteAttribute(aCookieData, nsICookie::SAMESITE_LAX);
+ } else if (tokenValue.LowerCaseEqualsLiteral(kSameSiteStrict)) {
+ SetSameSiteAttribute(aCookieData, nsICookie::SAMESITE_STRICT);
+ } else if (tokenValue.LowerCaseEqualsLiteral(kSameSiteNone)) {
+ SetSameSiteAttribute(aCookieData, nsICookie::SAMESITE_NONE);
+ } else {
+ // Reset to Default if unknown token value (see Bug 1682450)
+ SetSameSiteAttributeDefault(aCookieData);
+ CookieLogging::LogMessageToConsole(
+ aCRC, aHostURI, nsIScriptError::infoFlag, CONSOLE_SAMESITE_CATEGORY,
+ "CookieSameSiteValueInvalid2"_ns,
+ AutoTArray<nsString, 1>{NS_ConvertUTF8toUTF16(aCookieData.name())});
+ }
+ }
+ }
+
+ // re-assign aCookieHeader, in case we need to process another cookie
+ aCookieHeader.Assign(Substring(cookieStart, cookieEnd));
+
+ // If same-site is explicitly set to 'none' but this is not a secure context,
+ // let's abort the parsing.
+ if (!aCookieData.isSecure() &&
+ aCookieData.sameSite() == nsICookie::SAMESITE_NONE) {
+ if (StaticPrefs::network_cookie_sameSite_noneRequiresSecure()) {
+ CookieLogging::LogMessageToConsole(
+ aCRC, aHostURI, nsIScriptError::errorFlag, CONSOLE_SAMESITE_CATEGORY,
+ "CookieRejectedNonRequiresSecure2"_ns,
+ AutoTArray<nsString, 1>{NS_ConvertUTF8toUTF16(aCookieData.name())});
+ return newCookie;
+ }
+
+ // Still warn about the missing Secure attribute when not enforcing.
+ CookieLogging::LogMessageToConsole(
+ aCRC, aHostURI, nsIScriptError::warningFlag, CONSOLE_SAMESITE_CATEGORY,
+ "CookieRejectedNonRequiresSecureForBeta3"_ns,
+ AutoTArray<nsString, 2>{NS_ConvertUTF8toUTF16(aCookieData.name()),
+ SAMESITE_MDN_URL});
+ }
+
+ // Ensure the partitioned cookie is set with the secure attribute.
+ if (aCookieData.isPartitioned() && !aCookieData.isSecure()) {
+ CookieLogging::LogMessageToConsole(
+ aCRC, aHostURI, nsIScriptError::errorFlag, CONSOLE_REJECTION_CATEGORY,
+ "CookieRejectedPartitionedRequiresSecure"_ns,
+ AutoTArray<nsString, 1>{NS_ConvertUTF8toUTF16(aCookieData.name())});
+
+ // We only drop the cookie if CHIPS is enabled.
+ if (StaticPrefs::network_cookie_cookieBehavior_optInPartitioning()) {
+ return newCookie;
+ }
+ }
+
+ if (aCookieData.rawSameSite() == nsICookie::SAMESITE_NONE &&
+ aCookieData.sameSite() == nsICookie::SAMESITE_LAX) {
+ bool laxByDefault =
+ StaticPrefs::network_cookie_sameSite_laxByDefault() &&
+ !nsContentUtils::IsURIInPrefList(
+ aHostURI, "network.cookie.sameSite.laxByDefault.disabledHosts");
+ if (laxByDefault) {
+ CookieLogging::LogMessageToConsole(
+ aCRC, aHostURI, nsIScriptError::infoFlag, CONSOLE_SAMESITE_CATEGORY,
+ "CookieLaxForced2"_ns,
+ AutoTArray<nsString, 1>{NS_ConvertUTF8toUTF16(aCookieData.name())});
+ } else {
+ CookieLogging::LogMessageToConsole(
+ aCRC, aHostURI, nsIScriptError::warningFlag,
+ CONSOLE_SAMESITE_CATEGORY, "CookieLaxForcedForBeta2"_ns,
+ AutoTArray<nsString, 2>{NS_ConvertUTF8toUTF16(aCookieData.name()),
+ SAMESITE_MDN_URL});
+ }
+ }
+
+ // Cookie accepted.
+ aAcceptedByParser = true;
+
+ MOZ_ASSERT(Cookie::ValidateSameSite(aCookieData));
+ return newCookie;
+}
+
+/******************************************************************************
+ * CookieService impl:
+ * private domain & permission compliance enforcement functions
+ ******************************************************************************/
+
+// Normalizes the given hostname, component by component. ASCII/ACE
+// components are lower-cased, and UTF-8 components are normalized per
+// RFC 3454 and converted to ACE.
+nsresult CookieService::NormalizeHost(nsCString& aHost) {
+ if (!IsAscii(aHost)) {
+ nsAutoCString host;
+ nsresult rv = mIDNService->ConvertUTF8toACE(aHost, host);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ aHost = host;
+ }
+
+ ToLowerCase(aHost);
+ return NS_OK;
+}
+
+// returns true if 'a' is equal to or a subdomain of 'b',
+// assuming no leading dots are present.
+static inline bool IsSubdomainOf(const nsACString& a, const nsACString& b) {
+ if (a == b) {
+ return true;
+ }
+ if (a.Length() > b.Length()) {
+ return a[a.Length() - b.Length() - 1] == '.' && StringEndsWith(a, b);
+ }
+ return false;
+}
+
+CookieStatus CookieService::CheckPrefs(
+ nsIConsoleReportCollector* aCRC, nsICookieJarSettings* aCookieJarSettings,
+ nsIURI* aHostURI, bool aIsForeign, bool aIsThirdPartyTrackingResource,
+ bool aIsThirdPartySocialTrackingResource,
+ bool aStorageAccessPermissionGranted, const nsACString& aCookieHeader,
+ const int aNumOfCookies, const OriginAttributes& aOriginAttrs,
+ uint32_t* aRejectedReason) {
+ nsresult rv;
+
+ MOZ_ASSERT(aRejectedReason);
+
+ *aRejectedReason = 0;
+
+ // don't let unsupported scheme sites get/set cookies (could be a security
+ // issue)
+ if (!CookieCommons::IsSchemeSupported(aHostURI)) {
+ COOKIE_LOGFAILURE(!aCookieHeader.IsVoid(), aHostURI, aCookieHeader,
+ "non http/https sites cannot read cookies");
+ return STATUS_REJECTED_WITH_ERROR;
+ }
+
+ nsCOMPtr<nsIPrincipal> principal =
+ BasePrincipal::CreateContentPrincipal(aHostURI, aOriginAttrs);
+
+ if (!principal) {
+ COOKIE_LOGFAILURE(!aCookieHeader.IsVoid(), aHostURI, aCookieHeader,
+ "non-content principals cannot get/set cookies");
+ return STATUS_REJECTED_WITH_ERROR;
+ }
+
+ // check the permission list first; if we find an entry, it overrides
+ // default prefs. see bug 184059.
+ uint32_t cookiePermission = nsICookiePermission::ACCESS_DEFAULT;
+ rv = aCookieJarSettings->CookiePermission(principal, &cookiePermission);
+ if (NS_SUCCEEDED(rv)) {
+ switch (cookiePermission) {
+ case nsICookiePermission::ACCESS_DENY:
+ COOKIE_LOGFAILURE(!aCookieHeader.IsVoid(), aHostURI, aCookieHeader,
+ "cookies are blocked for this site");
+ CookieLogging::LogMessageToConsole(
+ aCRC, aHostURI, nsIScriptError::warningFlag,
+ CONSOLE_REJECTION_CATEGORY, "CookieRejectedByPermissionManager"_ns,
+ AutoTArray<nsString, 1>{
+ NS_ConvertUTF8toUTF16(aCookieHeader),
+ });
+
+ *aRejectedReason =
+ nsIWebProgressListener::STATE_COOKIES_BLOCKED_BY_PERMISSION;
+ return STATUS_REJECTED;
+
+ case nsICookiePermission::ACCESS_ALLOW:
+ return STATUS_ACCEPTED;
+ default:
+ break;
+ }
+ }
+
+ // No cookies allowed if this request comes from a resource in a 3rd party
+ // context, when anti-tracking protection is enabled and when we don't have
+ // access to the first-party cookie jar.
+ if (aIsForeign && aIsThirdPartyTrackingResource &&
+ !aStorageAccessPermissionGranted &&
+ aCookieJarSettings->GetRejectThirdPartyContexts()) {
+ uint32_t rejectReason =
+ nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER;
+ if (StoragePartitioningEnabled(rejectReason, aCookieJarSettings)) {
+ MOZ_ASSERT(!aOriginAttrs.mPartitionKey.IsEmpty(),
+ "We must have a StoragePrincipal here!");
+ return STATUS_ACCEPTED;
+ }
+
+ COOKIE_LOGFAILURE(!aCookieHeader.IsVoid(), aHostURI, aCookieHeader,
+ "cookies are disabled in trackers");
+ if (aIsThirdPartySocialTrackingResource) {
+ *aRejectedReason =
+ nsIWebProgressListener::STATE_COOKIES_BLOCKED_SOCIALTRACKER;
+ } else {
+ *aRejectedReason = nsIWebProgressListener::STATE_COOKIES_BLOCKED_TRACKER;
+ }
+ return STATUS_REJECTED;
+ }
+
+ // check default prefs.
+ // Check aStorageAccessPermissionGranted when checking aCookieBehavior
+ // so that we take things such as the content blocking allow list into
+ // account.
+ if (aCookieJarSettings->GetCookieBehavior() ==
+ nsICookieService::BEHAVIOR_REJECT &&
+ !aStorageAccessPermissionGranted) {
+ COOKIE_LOGFAILURE(!aCookieHeader.IsVoid(), aHostURI, aCookieHeader,
+ "cookies are disabled");
+ *aRejectedReason = nsIWebProgressListener::STATE_COOKIES_BLOCKED_ALL;
+ return STATUS_REJECTED;
+ }
+
+ // check if cookie is foreign
+ if (aIsForeign) {
+ if (aCookieJarSettings->GetCookieBehavior() ==
+ nsICookieService::BEHAVIOR_REJECT_FOREIGN &&
+ !aStorageAccessPermissionGranted) {
+ COOKIE_LOGFAILURE(!aCookieHeader.IsVoid(), aHostURI, aCookieHeader,
+ "context is third party");
+ CookieLogging::LogMessageToConsole(
+ aCRC, aHostURI, nsIScriptError::warningFlag,
+ CONSOLE_REJECTION_CATEGORY, "CookieRejectedThirdParty"_ns,
+ AutoTArray<nsString, 1>{
+ NS_ConvertUTF8toUTF16(aCookieHeader),
+ });
+ *aRejectedReason = nsIWebProgressListener::STATE_COOKIES_BLOCKED_FOREIGN;
+ return STATUS_REJECTED;
+ }
+
+ if (aCookieJarSettings->GetLimitForeignContexts() &&
+ !aStorageAccessPermissionGranted && aNumOfCookies == 0) {
+ COOKIE_LOGFAILURE(!aCookieHeader.IsVoid(), aHostURI, aCookieHeader,
+ "context is third party");
+ CookieLogging::LogMessageToConsole(
+ aCRC, aHostURI, nsIScriptError::warningFlag,
+ CONSOLE_REJECTION_CATEGORY, "CookieRejectedThirdParty"_ns,
+ AutoTArray<nsString, 1>{
+ NS_ConvertUTF8toUTF16(aCookieHeader),
+ });
+ *aRejectedReason = nsIWebProgressListener::STATE_COOKIES_BLOCKED_FOREIGN;
+ return STATUS_REJECTED;
+ }
+
+ if (StaticPrefs::network_cookie_thirdparty_sessionOnly()) {
+ return STATUS_ACCEPT_SESSION;
+ }
+
+ if (StaticPrefs::network_cookie_thirdparty_nonsecureSessionOnly()) {
+ if (!aHostURI->SchemeIs("https")) {
+ return STATUS_ACCEPT_SESSION;
+ }
+ }
+ }
+
+ // if nothing has complained, accept cookie
+ return STATUS_ACCEPTED;
+}
+
+// processes domain attribute, and returns true if host has permission to set
+// for this domain.
+bool CookieService::CheckDomain(CookieStruct& aCookieData, nsIURI* aHostURI,
+ const nsACString& aBaseDomain,
+ bool aRequireHostMatch) {
+ // Note: The logic in this function is mirrored in
+ // toolkit/components/extensions/ext-cookies.js:checkSetCookiePermissions().
+ // If it changes, please update that function, or file a bug for someone
+ // else to do so.
+
+ // get host from aHostURI
+ nsAutoCString hostFromURI;
+ nsContentUtils::GetHostOrIPv6WithBrackets(aHostURI, hostFromURI);
+
+ // if a domain is given, check the host has permission
+ if (!aCookieData.host().IsEmpty()) {
+ // Tolerate leading '.' characters, but not if it's otherwise an empty host.
+ if (aCookieData.host().Length() > 1 && aCookieData.host().First() == '.') {
+ aCookieData.host().Cut(0, 1);
+ }
+
+ // switch to lowercase now, to avoid case-insensitive compares everywhere
+ ToLowerCase(aCookieData.host());
+
+ // check whether the host is either an IP address, an alias such as
+ // 'localhost', an eTLD such as 'co.uk', or the empty string. in these
+ // cases, require an exact string match for the domain, and leave the cookie
+ // as a non-domain one. bug 105917 originally noted the requirement to deal
+ // with IP addresses.
+ if (aRequireHostMatch) {
+ return hostFromURI.Equals(aCookieData.host());
+ }
+
+ // ensure the proposed domain is derived from the base domain; and also
+ // that the host domain is derived from the proposed domain (per RFC2109).
+ if (IsSubdomainOf(aCookieData.host(), aBaseDomain) &&
+ IsSubdomainOf(hostFromURI, aCookieData.host())) {
+ // prepend a dot to indicate a domain cookie
+ aCookieData.host().InsertLiteral(".", 0);
+ return true;
+ }
+
+ /*
+ * note: RFC2109 section 4.3.2 requires that we check the following:
+ * that the portion of host not in domain does not contain a dot.
+ * this prevents hosts of the form x.y.co.nz from setting cookies in the
+ * entire .co.nz domain. however, it's only a only a partial solution and
+ * it breaks sites (IE doesn't enforce it), so we don't perform this check.
+ */
+ return false;
+ }
+
+ // no domain specified, use hostFromURI
+ aCookieData.host() = hostFromURI;
+ return true;
+}
+
+// static
+bool CookieService::CheckHiddenPrefix(CookieStruct& aCookieData) {
+ // If a cookie is nameless, then its value must not start with
+ // `__Host-` or `__Secure-`
+ if (!aCookieData.name().IsEmpty()) {
+ return true;
+ }
+
+ if (StringBeginsWith(aCookieData.value(), "__Host-"_ns)) {
+ return false;
+ }
+
+ if (StringBeginsWith(aCookieData.value(), "__Secure-"_ns)) {
+ return false;
+ }
+
+ return true;
+}
+
+namespace {
+nsAutoCString GetPathFromURI(nsIURI* aHostURI) {
+ // strip down everything after the last slash to get the path,
+ // ignoring slashes in the query string part.
+ // if we can QI to nsIURL, that'll take care of the query string portion.
+ // otherwise, it's not an nsIURL and can't have a query string, so just find
+ // the last slash.
+ nsAutoCString path;
+ nsCOMPtr<nsIURL> hostURL = do_QueryInterface(aHostURI);
+ if (hostURL) {
+ hostURL->GetDirectory(path);
+ } else {
+ aHostURI->GetPathQueryRef(path);
+ int32_t slash = path.RFindChar('/');
+ if (slash != kNotFound) {
+ path.Truncate(slash + 1);
+ }
+ }
+
+ // strip the right-most %x2F ("/") if the path doesn't contain only 1 '/'.
+ int32_t lastSlash = path.RFindChar('/');
+ int32_t firstSlash = path.FindChar('/');
+ if (lastSlash != firstSlash && lastSlash != kNotFound &&
+ lastSlash == static_cast<int32_t>(path.Length() - 1)) {
+ path.Truncate(lastSlash);
+ }
+
+ return path;
+}
+
+} // namespace
+
+bool CookieService::CheckPath(CookieStruct& aCookieData,
+ nsIConsoleReportCollector* aCRC,
+ nsIURI* aHostURI) {
+ // if a path is given, check the host has permission
+ if (aCookieData.path().IsEmpty() || aCookieData.path().First() != '/') {
+ aCookieData.path() = GetPathFromURI(aHostURI);
+ }
+
+ if (!CookieCommons::CheckPathSize(aCookieData)) {
+ AutoTArray<nsString, 2> params = {
+ NS_ConvertUTF8toUTF16(aCookieData.name())};
+
+ nsString size;
+ size.AppendInt(kMaxBytesPerPath);
+ params.AppendElement(size);
+
+ CookieLogging::LogMessageToConsole(
+ aCRC, aHostURI, nsIScriptError::warningFlag, CONSOLE_OVERSIZE_CATEGORY,
+ "CookiePathOversize"_ns, params);
+ return false;
+ }
+
+ return !aCookieData.path().Contains('\t');
+}
+
+// CheckPrefixes
+//
+// Reject cookies whose name starts with the magic prefixes from
+// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis
+// if they do not meet the criteria required by the prefix.
+//
+// Must not be called until after CheckDomain() and CheckPath() have
+// regularized and validated the CookieStruct values!
+bool CookieService::CheckPrefixes(CookieStruct& aCookieData,
+ bool aSecureRequest) {
+ static const char kSecure[] = "__Secure-";
+ static const char kHost[] = "__Host-";
+ static const int kSecureLen = sizeof(kSecure) - 1;
+ static const int kHostLen = sizeof(kHost) - 1;
+
+ bool isSecure = strncmp(aCookieData.name().get(), kSecure, kSecureLen) == 0;
+ bool isHost = strncmp(aCookieData.name().get(), kHost, kHostLen) == 0;
+
+ if (!isSecure && !isHost) {
+ // not one of the magic prefixes: carry on
+ return true;
+ }
+
+ if (!aSecureRequest || !aCookieData.isSecure()) {
+ // the magic prefixes may only be used from a secure request and
+ // the secure attribute must be set on the cookie
+ return false;
+ }
+
+ if (isHost) {
+ // The host prefix requires that the path is "/" and that the cookie
+ // had no domain attribute. CheckDomain() and CheckPath() MUST be run
+ // first to make sure invalid attributes are rejected and to regularlize
+ // them. In particular all explicit domain attributes result in a host
+ // that starts with a dot, and if the host doesn't start with a dot it
+ // correctly matches the true host.
+ if (aCookieData.host()[0] == '.' ||
+ !aCookieData.path().EqualsLiteral("/")) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool CookieService::GetExpiry(CookieStruct& aCookieData,
+ const nsACString& aExpires,
+ const nsACString& aMaxage, int64_t aCurrentTime,
+ bool aFromHttp) {
+ // maxageCap is in seconds.
+ // Disabled for HTTP cookies.
+ int64_t maxageCap =
+ aFromHttp ? 0 : StaticPrefs::privacy_documentCookies_maxage();
+
+ /* Determine when the cookie should expire. This is done by taking the
+ * difference between the server time and the time the server wants the cookie
+ * to expire, and adding that difference to the client time. This localizes
+ * the client time regardless of whether or not the TZ environment variable
+ * was set on the client.
+ *
+ * Note: We need to consider accounting for network lag here, per RFC.
+ */
+ // check for max-age attribute first; this overrides expires attribute
+ if (!aMaxage.IsEmpty()) {
+ // obtain numeric value of maxageAttribute
+ int64_t maxage;
+ int32_t numInts = PR_sscanf(aMaxage.BeginReading(), "%lld", &maxage);
+
+ // default to session cookie if the conversion failed
+ if (numInts != 1) {
+ return true;
+ }
+
+ // if this addition overflows, expiryTime will be less than currentTime
+ // and the cookie will be expired - that's okay.
+ if (maxageCap) {
+ aCookieData.expiry() = aCurrentTime + std::min(maxage, maxageCap);
+ } else {
+ aCookieData.expiry() = aCurrentTime + maxage;
+ }
+
+ // check for expires attribute
+ } else if (!aExpires.IsEmpty()) {
+ PRTime expires;
+
+ // parse expiry time
+ if (PR_ParseTimeString(aExpires.BeginReading(), true, &expires) !=
+ PR_SUCCESS) {
+ return true;
+ }
+
+ // If set-cookie used absolute time to set expiration, and it can't use
+ // client time to set expiration.
+ // Because if current time be set in the future, but the cookie expire
+ // time be set less than current time and more than server time.
+ // The cookie item have to be used to the expired cookie.
+ if (maxageCap) {
+ aCookieData.expiry() = std::min(expires / int64_t(PR_USEC_PER_SEC),
+ aCurrentTime + maxageCap);
+ } else {
+ aCookieData.expiry() = expires / int64_t(PR_USEC_PER_SEC);
+ }
+
+ // default to session cookie if no attributes found. Here we don't need to
+ // enforce the maxage cap, because session cookies are short-lived by
+ // definition.
+ } else {
+ return true;
+ }
+
+ return false;
+}
+
+/******************************************************************************
+ * CookieService impl:
+ * private cookielist management functions
+ ******************************************************************************/
+
+// find whether a given cookie has been previously set. this is provided by the
+// nsICookieManager interface.
+NS_IMETHODIMP
+CookieService::CookieExists(const nsACString& aHost, const nsACString& aPath,
+ const nsACString& aName,
+ JS::Handle<JS::Value> aOriginAttributes,
+ JSContext* aCx, bool* aFoundCookie) {
+ NS_ENSURE_ARG_POINTER(aCx);
+ NS_ENSURE_ARG_POINTER(aFoundCookie);
+
+ OriginAttributes attrs;
+ if (!aOriginAttributes.isObject() || !attrs.Init(aCx, aOriginAttributes)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ return CookieExistsNative(aHost, aPath, aName, &attrs, aFoundCookie);
+}
+
+NS_IMETHODIMP_(nsresult)
+CookieService::CookieExistsNative(const nsACString& aHost,
+ const nsACString& aPath,
+ const nsACString& aName,
+ OriginAttributes* aOriginAttributes,
+ bool* aFoundCookie) {
+ nsCOMPtr<nsICookie> cookie;
+ nsresult rv = GetCookieNative(aHost, aPath, aName, aOriginAttributes,
+ getter_AddRefs(cookie));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *aFoundCookie = cookie != nullptr;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP_(nsresult)
+CookieService::GetCookieNative(const nsACString& aHost, const nsACString& aPath,
+ const nsACString& aName,
+ OriginAttributes* aOriginAttributes,
+ nsICookie** aCookie) {
+ NS_ENSURE_ARG_POINTER(aOriginAttributes);
+ NS_ENSURE_ARG_POINTER(aCookie);
+
+ if (!IsInitialized()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ nsAutoCString baseDomain;
+ nsresult rv =
+ CookieCommons::GetBaseDomainFromHost(mTLDService, aHost, baseDomain);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ CookieListIter iter{};
+ CookieStorage* storage = PickStorage(*aOriginAttributes);
+ bool foundCookie = storage->FindCookie(baseDomain, *aOriginAttributes, aHost,
+ aName, aPath, iter);
+
+ if (foundCookie) {
+ RefPtr<Cookie> cookie = iter.Cookie();
+ NS_ENSURE_TRUE(cookie, NS_ERROR_NULL_POINTER);
+
+ cookie.forget(aCookie);
+ }
+
+ return NS_OK;
+}
+
+// count the number of cookies stored by a particular host. this is provided by
+// the nsICookieManager interface.
+NS_IMETHODIMP
+CookieService::CountCookiesFromHost(const nsACString& aHost,
+ uint32_t* aCountFromHost) {
+ // first, normalize the hostname, and fail if it contains illegal characters.
+ nsAutoCString host(aHost);
+ nsresult rv = NormalizeHost(host);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString baseDomain;
+ rv = CookieCommons::GetBaseDomainFromHost(mTLDService, host, baseDomain);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!IsInitialized()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ mPersistentStorage->EnsureInitialized();
+
+ *aCountFromHost = mPersistentStorage->CountCookiesFromHost(baseDomain, 0);
+
+ return NS_OK;
+}
+
+// get an enumerator of cookies stored by a particular host. this is provided by
+// the nsICookieManager interface.
+NS_IMETHODIMP
+CookieService::GetCookiesFromHost(const nsACString& aHost,
+ JS::Handle<JS::Value> aOriginAttributes,
+ JSContext* aCx,
+ nsTArray<RefPtr<nsICookie>>& aResult) {
+ // first, normalize the hostname, and fail if it contains illegal characters.
+ nsAutoCString host(aHost);
+ nsresult rv = NormalizeHost(host);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString baseDomain;
+ rv = CookieCommons::GetBaseDomainFromHost(mTLDService, host, baseDomain);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ OriginAttributes attrs;
+ if (!aOriginAttributes.isObject() || !attrs.Init(aCx, aOriginAttributes)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (!IsInitialized()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ CookieStorage* storage = PickStorage(attrs);
+
+ const nsTArray<RefPtr<Cookie>>* cookies =
+ storage->GetCookiesFromHost(baseDomain, attrs);
+
+ if (cookies) {
+ aResult.SetCapacity(cookies->Length());
+ for (Cookie* cookie : *cookies) {
+ aResult.AppendElement(cookie);
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CookieService::GetCookiesWithOriginAttributes(
+ const nsAString& aPattern, const nsACString& aHost,
+ nsTArray<RefPtr<nsICookie>>& aResult) {
+ OriginAttributesPattern pattern;
+ if (!pattern.Init(aPattern)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsAutoCString host(aHost);
+ nsresult rv = NormalizeHost(host);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString baseDomain;
+ rv = CookieCommons::GetBaseDomainFromHost(mTLDService, host, baseDomain);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return GetCookiesWithOriginAttributes(pattern, baseDomain, aResult);
+}
+
+nsresult CookieService::GetCookiesWithOriginAttributes(
+ const OriginAttributesPattern& aPattern, const nsCString& aBaseDomain,
+ nsTArray<RefPtr<nsICookie>>& aResult) {
+ CookieStorage* storage = PickStorage(aPattern);
+ storage->GetCookiesWithOriginAttributes(aPattern, aBaseDomain, aResult);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CookieService::RemoveCookiesWithOriginAttributes(const nsAString& aPattern,
+ const nsACString& aHost) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ OriginAttributesPattern pattern;
+ if (!pattern.Init(aPattern)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsAutoCString host(aHost);
+ nsresult rv = NormalizeHost(host);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString baseDomain;
+ rv = CookieCommons::GetBaseDomainFromHost(mTLDService, host, baseDomain);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return RemoveCookiesWithOriginAttributes(pattern, baseDomain);
+}
+
+nsresult CookieService::RemoveCookiesWithOriginAttributes(
+ const OriginAttributesPattern& aPattern, const nsCString& aBaseDomain) {
+ if (!IsInitialized()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ CookieStorage* storage = PickStorage(aPattern);
+ storage->RemoveCookiesWithOriginAttributes(aPattern, aBaseDomain);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CookieService::RemoveCookiesFromExactHost(const nsACString& aHost,
+ const nsAString& aPattern) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ OriginAttributesPattern pattern;
+ if (!pattern.Init(aPattern)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ return RemoveCookiesFromExactHost(aHost, pattern);
+}
+
+nsresult CookieService::RemoveCookiesFromExactHost(
+ const nsACString& aHost, const OriginAttributesPattern& aPattern) {
+ nsAutoCString host(aHost);
+ nsresult rv = NormalizeHost(host);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString baseDomain;
+ rv = CookieCommons::GetBaseDomainFromHost(mTLDService, host, baseDomain);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!IsInitialized()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ CookieStorage* storage = PickStorage(aPattern);
+ storage->RemoveCookiesFromExactHost(aHost, baseDomain, aPattern);
+
+ return NS_OK;
+}
+
+namespace {
+
+class RemoveAllSinceRunnable : public Runnable {
+ public:
+ using CookieArray = nsTArray<RefPtr<nsICookie>>;
+ RemoveAllSinceRunnable(Promise* aPromise, CookieService* aSelf,
+ CookieArray&& aCookieArray, int64_t aSinceWhen)
+ : Runnable("RemoveAllSinceRunnable"),
+ mPromise(aPromise),
+ mSelf(aSelf),
+ mList(std::move(aCookieArray)),
+ mIndex(0),
+ mSinceWhen(aSinceWhen) {}
+
+ NS_IMETHODIMP Run() override {
+ RemoveSome();
+
+ if (mIndex < mList.Length()) {
+ return NS_DispatchToCurrentThread(this);
+ }
+ mPromise->MaybeResolveWithUndefined();
+
+ return NS_OK;
+ }
+
+ private:
+ void RemoveSome() {
+ for (CookieArray::size_type iter = 0;
+ iter < kYieldPeriod && mIndex < mList.Length(); ++mIndex, ++iter) {
+ auto* cookie = static_cast<Cookie*>(mList[mIndex].get());
+ if (cookie->CreationTime() > mSinceWhen &&
+ NS_FAILED(mSelf->Remove(cookie->Host(), cookie->OriginAttributesRef(),
+ cookie->Name(), cookie->Path()))) {
+ continue;
+ }
+ }
+ }
+
+ private:
+ RefPtr<Promise> mPromise;
+ RefPtr<CookieService> mSelf;
+ CookieArray mList;
+ CookieArray::size_type mIndex;
+ int64_t mSinceWhen;
+ static const CookieArray::size_type kYieldPeriod = 10;
+};
+
+} // namespace
+
+NS_IMETHODIMP
+CookieService::RemoveAllSince(int64_t aSinceWhen, JSContext* aCx,
+ Promise** aRetVal) {
+ nsIGlobalObject* globalObject = xpc::CurrentNativeGlobal(aCx);
+ if (NS_WARN_IF(!globalObject)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ ErrorResult result;
+ RefPtr<Promise> promise = Promise::Create(globalObject, result);
+ if (NS_WARN_IF(result.Failed())) {
+ return result.StealNSResult();
+ }
+
+ mPersistentStorage->EnsureInitialized();
+
+ nsTArray<RefPtr<nsICookie>> cookieList;
+
+ // We delete only non-private cookies.
+ mPersistentStorage->GetAll(cookieList);
+
+ RefPtr<RemoveAllSinceRunnable> runMe = new RemoveAllSinceRunnable(
+ promise, this, std::move(cookieList), aSinceWhen);
+
+ promise.forget(aRetVal);
+
+ return runMe->Run();
+}
+
+namespace {
+
+class CompareCookiesCreationTime {
+ public:
+ static bool Equals(const nsICookie* aCookie1, const nsICookie* aCookie2) {
+ return static_cast<const Cookie*>(aCookie1)->CreationTime() ==
+ static_cast<const Cookie*>(aCookie2)->CreationTime();
+ }
+
+ static bool LessThan(const nsICookie* aCookie1, const nsICookie* aCookie2) {
+ return static_cast<const Cookie*>(aCookie1)->CreationTime() <
+ static_cast<const Cookie*>(aCookie2)->CreationTime();
+ }
+};
+
+} // namespace
+
+NS_IMETHODIMP
+CookieService::GetCookiesSince(int64_t aSinceWhen,
+ nsTArray<RefPtr<nsICookie>>& aResult) {
+ if (!IsInitialized()) {
+ return NS_OK;
+ }
+
+ mPersistentStorage->EnsureInitialized();
+
+ // We expose only non-private cookies.
+ nsTArray<RefPtr<nsICookie>> cookieList;
+ mPersistentStorage->GetAll(cookieList);
+
+ for (RefPtr<nsICookie>& cookie : cookieList) {
+ if (static_cast<Cookie*>(cookie.get())->CreationTime() >= aSinceWhen) {
+ aResult.AppendElement(cookie);
+ }
+ }
+
+ aResult.Sort(CompareCookiesCreationTime());
+ return NS_OK;
+}
+
+size_t CookieService::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
+ size_t n = aMallocSizeOf(this);
+
+ if (mPersistentStorage) {
+ n += mPersistentStorage->SizeOfIncludingThis(aMallocSizeOf);
+ }
+ if (mPrivateStorage) {
+ n += mPrivateStorage->SizeOfIncludingThis(aMallocSizeOf);
+ }
+
+ return n;
+}
+
+MOZ_DEFINE_MALLOC_SIZE_OF(CookieServiceMallocSizeOf)
+
+NS_IMETHODIMP
+CookieService::CollectReports(nsIHandleReportCallback* aHandleReport,
+ nsISupports* aData, bool /*aAnonymize*/) {
+ MOZ_COLLECT_REPORT("explicit/cookie-service", KIND_HEAP, UNITS_BYTES,
+ SizeOfIncludingThis(CookieServiceMallocSizeOf),
+ "Memory used by the cookie service.");
+
+ return NS_OK;
+}
+
+bool CookieService::IsInitialized() const {
+ if (!mPersistentStorage) {
+ NS_WARNING("No CookieStorage! Profile already close?");
+ return false;
+ }
+
+ MOZ_ASSERT(mPrivateStorage);
+ return true;
+}
+
+CookieStorage* CookieService::PickStorage(const OriginAttributes& aAttrs) {
+ MOZ_ASSERT(IsInitialized());
+
+ if (aAttrs.mPrivateBrowsingId > 0) {
+ return mPrivateStorage;
+ }
+
+ mPersistentStorage->EnsureInitialized();
+ return mPersistentStorage;
+}
+
+CookieStorage* CookieService::PickStorage(
+ const OriginAttributesPattern& aAttrs) {
+ MOZ_ASSERT(IsInitialized());
+
+ if (aAttrs.mPrivateBrowsingId.WasPassed() &&
+ aAttrs.mPrivateBrowsingId.Value() > 0) {
+ return mPrivateStorage;
+ }
+
+ mPersistentStorage->EnsureInitialized();
+ return mPersistentStorage;
+}
+
+bool CookieService::SetCookiesFromIPC(const nsACString& aBaseDomain,
+ const OriginAttributes& aAttrs,
+ nsIURI* aHostURI, bool aFromHttp,
+ const nsTArray<CookieStruct>& aCookies,
+ BrowsingContext* aBrowsingContext) {
+ if (!IsInitialized()) {
+ // If we are probably shutting down, we can ignore this cookie.
+ return true;
+ }
+
+ CookieStorage* storage = PickStorage(aAttrs);
+ int64_t currentTimeInUsec = PR_Now();
+
+ for (const CookieStruct& cookieData : aCookies) {
+ if (!CookieCommons::CheckPathSize(cookieData)) {
+ return false;
+ }
+
+ // reject cookie if it's over the size limit, per RFC2109
+ if (!CookieCommons::CheckNameAndValueSize(cookieData)) {
+ return false;
+ }
+
+ RecordUnicodeTelemetry(cookieData);
+
+ if (!CookieCommons::CheckName(cookieData)) {
+ return false;
+ }
+
+ if (!CookieCommons::CheckValue(cookieData)) {
+ return false;
+ }
+
+ // create a new Cookie and copy attributes
+ RefPtr<Cookie> cookie = Cookie::Create(cookieData, aAttrs);
+ if (!cookie) {
+ continue;
+ }
+
+ cookie->SetLastAccessed(currentTimeInUsec);
+ cookie->SetCreationTime(
+ Cookie::GenerateUniqueCreationTime(currentTimeInUsec));
+
+ storage->AddCookie(nullptr, aBaseDomain, aAttrs, cookie, currentTimeInUsec,
+ aHostURI, ""_ns, aFromHttp, aBrowsingContext);
+ }
+
+ return true;
+}
+
+} // namespace net
+} // namespace mozilla
diff --git a/netwerk/cookie/CookieService.h b/netwerk/cookie/CookieService.h
new file mode 100644
index 0000000000..09eb4c1289
--- /dev/null
+++ b/netwerk/cookie/CookieService.h
@@ -0,0 +1,163 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_net_CookieService_h
+#define mozilla_net_CookieService_h
+
+#include "nsICookieService.h"
+#include "nsICookieManager.h"
+#include "nsIObserver.h"
+#include "nsWeakReference.h"
+
+#include "Cookie.h"
+#include "CookieCommons.h"
+
+#include "nsString.h"
+#include "nsIMemoryReporter.h"
+#include "mozilla/MemoryReporting.h"
+
+class nsIConsoleReportCollector;
+class nsICookieJarSettings;
+class nsIEffectiveTLDService;
+class nsIIDNService;
+class nsIURI;
+class nsIChannel;
+class mozIThirdPartyUtil;
+
+namespace mozilla {
+namespace net {
+
+class CookiePersistentStorage;
+class CookiePrivateStorage;
+class CookieStorage;
+
+/******************************************************************************
+ * CookieService:
+ * class declaration
+ ******************************************************************************/
+
+class CookieService final : public nsICookieService,
+ public nsICookieManager,
+ public nsIObserver,
+ public nsSupportsWeakReference,
+ public nsIMemoryReporter {
+ private:
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const;
+
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+ NS_DECL_NSICOOKIESERVICE
+ NS_DECL_NSICOOKIEMANAGER
+ NS_DECL_NSIMEMORYREPORTER
+
+ static already_AddRefed<CookieService> GetSingleton();
+
+ CookieService();
+ static already_AddRefed<nsICookieService> GetXPCOMSingleton();
+ nsresult Init();
+
+ /**
+ * Start watching the observer service for messages indicating that an app has
+ * been uninstalled. When an app is uninstalled, we get the cookie service
+ * (thus instantiating it, if necessary) and clear all the cookies for that
+ * app.
+ */
+
+ static bool CanSetCookie(nsIURI* aHostURI, const nsACString& aBaseDomain,
+ CookieStruct& aCookieData, bool aRequireHostMatch,
+ CookieStatus aStatus, nsCString& aCookieHeader,
+ bool aFromHttp, bool aIsForeignAndNotAddon,
+ bool aPartitionedOnly,
+ nsIConsoleReportCollector* aCRC, bool& aSetCookie);
+ static CookieStatus CheckPrefs(
+ nsIConsoleReportCollector* aCRC, nsICookieJarSettings* aCookieJarSettings,
+ nsIURI* aHostURI, bool aIsForeign, bool aIsThirdPartyTrackingResource,
+ bool aIsThirdPartySocialTrackingResource,
+ bool aStorageAccessPermissionGranted, const nsACString& aCookieHeader,
+ const int aNumOfCookies, const OriginAttributes& aOriginAttrs,
+ uint32_t* aRejectedReason);
+
+ void GetCookiesForURI(nsIURI* aHostURI, nsIChannel* aChannel, bool aIsForeign,
+ bool aIsThirdPartyTrackingResource,
+ bool aIsThirdPartySocialTrackingResource,
+ bool aStorageAccessPermissionGranted,
+ uint32_t aRejectedReason, bool aIsSafeTopLevelNav,
+ bool aIsSameSiteForeign, bool aHadCrossSiteRedirects,
+ bool aHttpBound,
+ bool aAllowSecureCookiesToInsecureOrigin,
+ const OriginAttributes& aOriginAttrs,
+ nsTArray<Cookie*>& aCookieList);
+
+ /**
+ * This method is a helper that allows calling nsICookieManager::Remove()
+ * with OriginAttributes parameter.
+ */
+ nsresult Remove(const nsACString& aHost, const OriginAttributes& aAttrs,
+ const nsACString& aName, const nsACString& aPath);
+
+ bool SetCookiesFromIPC(const nsACString& aBaseDomain,
+ const OriginAttributes& aAttrs, nsIURI* aHostURI,
+ bool aFromHttp, const nsTArray<CookieStruct>& aCookies,
+ dom::BrowsingContext* aBrowsingContext);
+
+ protected:
+ virtual ~CookieService();
+
+ bool IsInitialized() const;
+
+ void InitCookieStorages();
+ void CloseCookieStorages();
+
+ nsresult NormalizeHost(nsCString& aHost);
+ static bool GetTokenValue(nsACString::const_char_iterator& aIter,
+ nsACString::const_char_iterator& aEndIter,
+ nsDependentCSubstring& aTokenString,
+ nsDependentCSubstring& aTokenValue,
+ bool& aEqualsFound);
+ static bool ParseAttributes(nsIConsoleReportCollector* aCRC, nsIURI* aHostURI,
+ nsCString& aCookieHeader,
+ CookieStruct& aCookieData, nsACString& aExpires,
+ nsACString& aMaxage, bool& aAcceptedByParser);
+ static bool CheckDomain(CookieStruct& aCookieData, nsIURI* aHostURI,
+ const nsACString& aBaseDomain,
+ bool aRequireHostMatch);
+ static bool CheckHiddenPrefix(CookieStruct& aCookieData);
+ static bool CheckPath(CookieStruct& aCookieData,
+ nsIConsoleReportCollector* aCRC, nsIURI* aHostURI);
+ static bool CheckPrefixes(CookieStruct& aCookieData, bool aSecureRequest);
+ static bool GetExpiry(CookieStruct& aCookieData, const nsACString& aExpires,
+ const nsACString& aMaxage, int64_t aCurrentTime,
+ bool aFromHttp);
+ void NotifyAccepted(nsIChannel* aChannel);
+
+ nsresult GetCookiesWithOriginAttributes(
+ const OriginAttributesPattern& aPattern, const nsCString& aBaseDomain,
+ nsTArray<RefPtr<nsICookie>>& aResult);
+ nsresult RemoveCookiesWithOriginAttributes(
+ const OriginAttributesPattern& aPattern, const nsCString& aBaseDomain);
+
+ protected:
+ CookieStorage* PickStorage(const OriginAttributes& aAttrs);
+ CookieStorage* PickStorage(const OriginAttributesPattern& aAttrs);
+
+ nsresult RemoveCookiesFromExactHost(const nsACString& aHost,
+ const OriginAttributesPattern& aPattern);
+
+ // cached members.
+ nsCOMPtr<mozIThirdPartyUtil> mThirdPartyUtil;
+ nsCOMPtr<nsIEffectiveTLDService> mTLDService;
+ nsCOMPtr<nsIIDNService> mIDNService;
+
+ // we have two separate Cookie Storages: one for normal browsing and one for
+ // private browsing.
+ RefPtr<CookieStorage> mPersistentStorage;
+ RefPtr<CookieStorage> mPrivateStorage;
+};
+
+} // namespace net
+} // namespace mozilla
+
+#endif // mozilla_net_CookieService_h
diff --git a/netwerk/cookie/CookieServiceChild.cpp b/netwerk/cookie/CookieServiceChild.cpp
new file mode 100644
index 0000000000..a005b5dbe7
--- /dev/null
+++ b/netwerk/cookie/CookieServiceChild.cpp
@@ -0,0 +1,653 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "Cookie.h"
+#include "CookieCommons.h"
+#include "CookieLogging.h"
+#include "CookieService.h"
+#include "mozilla/net/CookieServiceChild.h"
+#include "ErrorList.h"
+#include "mozilla/net/HttpChannelChild.h"
+#include "mozilla/net/NeckoChannelParams.h"
+#include "mozilla/LoadInfo.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/ipc/URIUtils.h"
+#include "mozilla/net/NeckoChild.h"
+#include "mozilla/StaticPrefs_network.h"
+#include "mozilla/StoragePrincipalHelper.h"
+#include "nsNetCID.h"
+#include "nsNetUtil.h"
+#include "nsICookieJarSettings.h"
+#include "nsIChannel.h"
+#include "nsIClassifiedChannel.h"
+#include "nsIHttpChannel.h"
+#include "nsIEffectiveTLDService.h"
+#include "nsIURI.h"
+#include "nsIPrefBranch.h"
+#include "nsIWebProgressListener.h"
+#include "nsQueryObject.h"
+#include "nsServiceManagerUtils.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/TimeStamp.h"
+#include "ThirdPartyUtil.h"
+#include "nsIConsoleReportCollector.h"
+#include "mozilla/dom/WindowGlobalChild.h"
+
+using namespace mozilla::ipc;
+
+namespace mozilla {
+namespace net {
+
+static StaticRefPtr<CookieServiceChild> gCookieChildService;
+
+already_AddRefed<CookieServiceChild> CookieServiceChild::GetSingleton() {
+ if (!gCookieChildService) {
+ gCookieChildService = new CookieServiceChild();
+ gCookieChildService->Init();
+ ClearOnShutdown(&gCookieChildService);
+ }
+
+ return do_AddRef(gCookieChildService);
+}
+
+NS_IMPL_ISUPPORTS(CookieServiceChild, nsICookieService,
+ nsISupportsWeakReference)
+
+CookieServiceChild::CookieServiceChild() { NeckoChild::InitNeckoChild(); }
+
+CookieServiceChild::~CookieServiceChild() { gCookieChildService = nullptr; }
+
+void CookieServiceChild::Init() {
+ auto* cc = static_cast<mozilla::dom::ContentChild*>(gNeckoChild->Manager());
+ if (cc->IsShuttingDown()) {
+ return;
+ }
+
+ // This corresponds to Release() in DeallocPCookieService.
+ NS_ADDREF_THIS();
+
+ // Create a child PCookieService actor. Don't do this in the constructor
+ // since it could release 'this' on failure
+ gNeckoChild->SendPCookieServiceConstructor(this);
+
+ mThirdPartyUtil = ThirdPartyUtil::GetInstance();
+ NS_ASSERTION(mThirdPartyUtil, "couldn't get ThirdPartyUtil service");
+
+ mTLDService = do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID);
+ NS_ASSERTION(mTLDService, "couldn't get TLDService");
+}
+
+RefPtr<GenericPromise> CookieServiceChild::TrackCookieLoad(
+ nsIChannel* aChannel) {
+ if (!CanSend()) {
+ return GenericPromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE, __func__);
+ }
+
+ uint32_t rejectedReason = 0;
+ ThirdPartyAnalysisResult result = mThirdPartyUtil->AnalyzeChannel(
+ aChannel, true, nullptr, RequireThirdPartyCheck, &rejectedReason);
+
+ nsCOMPtr<nsIURI> uri;
+ aChannel->GetURI(getter_AddRefs(uri));
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+
+ OriginAttributes attrs = loadInfo->GetOriginAttributes();
+ StoragePrincipalHelper::PrepareEffectiveStoragePrincipalOriginAttributes(
+ aChannel, attrs);
+
+ bool isSafeTopLevelNav = CookieCommons::IsSafeTopLevelNav(aChannel);
+ bool hadCrossSiteRedirects = false;
+ bool isSameSiteForeign =
+ CookieCommons::IsSameSiteForeign(aChannel, uri, &hadCrossSiteRedirects);
+
+ RefPtr<CookieServiceChild> self(this);
+
+ return SendGetCookieList(
+ uri, result.contains(ThirdPartyAnalysis::IsForeign),
+ result.contains(ThirdPartyAnalysis::IsThirdPartyTrackingResource),
+ result.contains(
+ ThirdPartyAnalysis::IsThirdPartySocialTrackingResource),
+ result.contains(
+ ThirdPartyAnalysis::IsStorageAccessPermissionGranted),
+ rejectedReason, isSafeTopLevelNav, isSameSiteForeign,
+ hadCrossSiteRedirects, attrs)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self, uri, attrs](const nsTArray<CookieStruct>& aCookiesList) {
+ for (uint32_t i = 0; i < aCookiesList.Length(); i++) {
+ RefPtr<Cookie> cookie = Cookie::Create(aCookiesList[i], attrs);
+ cookie->SetIsHttpOnly(false);
+ self->RecordDocumentCookie(cookie, attrs);
+ }
+ return GenericPromise::CreateAndResolve(true, __func__);
+ },
+ [](const mozilla::ipc::ResponseRejectReason) {
+ return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
+ });
+}
+
+IPCResult CookieServiceChild::RecvRemoveAll() {
+ mCookiesMap.Clear();
+
+ nsCOMPtr<nsIObserverService> obsService = services::GetObserverService();
+ if (obsService) {
+ obsService->NotifyObservers(nullptr, "content-removed-all-cookies",
+ nullptr);
+ }
+ return IPC_OK();
+}
+
+IPCResult CookieServiceChild::RecvRemoveCookie(const CookieStruct& aCookie,
+ const OriginAttributes& aAttrs) {
+ RemoveSingleCookie(aCookie, aAttrs);
+
+ nsCOMPtr<nsIObserverService> obsService = services::GetObserverService();
+ if (obsService) {
+ obsService->NotifyObservers(nullptr, "content-removed-cookie", nullptr);
+ }
+ return IPC_OK();
+}
+
+void CookieServiceChild::RemoveSingleCookie(const CookieStruct& aCookie,
+ const OriginAttributes& aAttrs) {
+ nsCString baseDomain;
+ CookieCommons::GetBaseDomainFromHost(mTLDService, aCookie.host(), baseDomain);
+ CookieKey key(baseDomain, aAttrs);
+ CookiesList* cookiesList = nullptr;
+ mCookiesMap.Get(key, &cookiesList);
+
+ if (!cookiesList) {
+ return;
+ }
+
+ for (uint32_t i = 0; i < cookiesList->Length(); i++) {
+ Cookie* cookie = cookiesList->ElementAt(i);
+ // bug 1858366: In the case that we are updating a stale cookie
+ // from the content process: the parent process will signal
+ // a batch deletion for the old cookie.
+ // When received by the content process we should not remove
+ // the new cookie since we have already updated the content
+ // process cookies. So we also check the expiry here.
+ if (cookie->Name().Equals(aCookie.name()) &&
+ cookie->Host().Equals(aCookie.host()) &&
+ cookie->Path().Equals(aCookie.path()) &&
+ cookie->Expiry() <= aCookie.expiry()) {
+ cookiesList->RemoveElementAt(i);
+ break;
+ }
+ }
+}
+
+IPCResult CookieServiceChild::RecvAddCookie(const CookieStruct& aCookie,
+ const OriginAttributes& aAttrs) {
+ RefPtr<Cookie> cookie = Cookie::Create(aCookie, aAttrs);
+ RecordDocumentCookie(cookie, aAttrs);
+
+ // signal test code to check their cookie list
+ nsCOMPtr<nsIObserverService> obsService = services::GetObserverService();
+ if (obsService) {
+ obsService->NotifyObservers(nullptr, "content-added-cookie", nullptr);
+ }
+
+ return IPC_OK();
+}
+
+IPCResult CookieServiceChild::RecvRemoveBatchDeletedCookies(
+ nsTArray<CookieStruct>&& aCookiesList,
+ nsTArray<OriginAttributes>&& aAttrsList) {
+ MOZ_ASSERT(aCookiesList.Length() == aAttrsList.Length());
+ for (uint32_t i = 0; i < aCookiesList.Length(); i++) {
+ CookieStruct cookieStruct = aCookiesList.ElementAt(i);
+ RemoveSingleCookie(cookieStruct, aAttrsList.ElementAt(i));
+ }
+
+ nsCOMPtr<nsIObserverService> obsService = services::GetObserverService();
+ if (obsService) {
+ obsService->NotifyObservers(nullptr, "content-batch-deleted-cookies",
+ nullptr);
+ }
+ return IPC_OK();
+}
+
+IPCResult CookieServiceChild::RecvTrackCookiesLoad(
+ nsTArray<CookieStruct>&& aCookiesList, const OriginAttributes& aAttrs) {
+ for (uint32_t i = 0; i < aCookiesList.Length(); i++) {
+ RefPtr<Cookie> cookie = Cookie::Create(aCookiesList[i], aAttrs);
+ cookie->SetIsHttpOnly(false);
+ RecordDocumentCookie(cookie, aAttrs);
+ }
+
+ nsCOMPtr<nsIObserverService> obsService = services::GetObserverService();
+ if (obsService) {
+ obsService->NotifyObservers(nullptr, "content-track-cookies-loaded",
+ nullptr);
+ }
+
+ return IPC_OK();
+}
+
+uint32_t CookieServiceChild::CountCookiesFromHashTable(
+ const nsACString& aBaseDomain, const OriginAttributes& aOriginAttrs) {
+ CookiesList* cookiesList = nullptr;
+
+ nsCString baseDomain;
+ CookieKey key(aBaseDomain, aOriginAttrs);
+ mCookiesMap.Get(key, &cookiesList);
+
+ return cookiesList ? cookiesList->Length() : 0;
+}
+
+/* static */ bool CookieServiceChild::RequireThirdPartyCheck(
+ nsILoadInfo* aLoadInfo) {
+ if (!aLoadInfo) {
+ return false;
+ }
+
+ nsCOMPtr<nsICookieJarSettings> cookieJarSettings;
+ nsresult rv =
+ aLoadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ uint32_t cookieBehavior = cookieJarSettings->GetCookieBehavior();
+ return cookieBehavior == nsICookieService::BEHAVIOR_REJECT_FOREIGN ||
+ cookieBehavior == nsICookieService::BEHAVIOR_LIMIT_FOREIGN ||
+ cookieBehavior == nsICookieService::BEHAVIOR_REJECT_TRACKER ||
+ cookieBehavior ==
+ nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN ||
+ StaticPrefs::network_cookie_thirdparty_sessionOnly() ||
+ StaticPrefs::network_cookie_thirdparty_nonsecureSessionOnly();
+}
+
+void CookieServiceChild::RecordDocumentCookie(Cookie* aCookie,
+ const OriginAttributes& aAttrs) {
+ nsAutoCString baseDomain;
+ CookieCommons::GetBaseDomainFromHost(mTLDService, aCookie->Host(),
+ baseDomain);
+
+ CookieKey key(baseDomain, aAttrs);
+ CookiesList* cookiesList = nullptr;
+ mCookiesMap.Get(key, &cookiesList);
+
+ if (!cookiesList) {
+ cookiesList = mCookiesMap.GetOrInsertNew(key);
+ }
+ for (uint32_t i = 0; i < cookiesList->Length(); i++) {
+ Cookie* cookie = cookiesList->ElementAt(i);
+ if (cookie->Name().Equals(aCookie->Name()) &&
+ cookie->Host().Equals(aCookie->Host()) &&
+ cookie->Path().Equals(aCookie->Path())) {
+ if (cookie->Value().Equals(aCookie->Value()) &&
+ cookie->Expiry() == aCookie->Expiry() &&
+ cookie->IsSecure() == aCookie->IsSecure() &&
+ cookie->SameSite() == aCookie->SameSite() &&
+ cookie->RawSameSite() == aCookie->RawSameSite() &&
+ cookie->IsSession() == aCookie->IsSession() &&
+ cookie->IsHttpOnly() == aCookie->IsHttpOnly()) {
+ cookie->SetLastAccessed(aCookie->LastAccessed());
+ return;
+ }
+ cookiesList->RemoveElementAt(i);
+ break;
+ }
+ }
+
+ int64_t currentTime = PR_Now() / PR_USEC_PER_SEC;
+ if (aCookie->Expiry() <= currentTime) {
+ return;
+ }
+
+ cookiesList->AppendElement(aCookie);
+}
+
+NS_IMETHODIMP
+CookieServiceChild::GetCookieStringFromDocument(dom::Document* aDocument,
+ nsACString& aCookieString) {
+ NS_ENSURE_ARG(aDocument);
+
+ aCookieString.Truncate();
+
+ nsCOMPtr<nsIPrincipal> principal = aDocument->EffectiveCookiePrincipal();
+
+ if (!CookieCommons::IsSchemeSupported(principal)) {
+ return NS_OK;
+ }
+
+ nsAutoCString baseDomain;
+ nsresult rv = CookieCommons::GetBaseDomain(principal, baseDomain);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return NS_OK;
+ }
+
+ CookieKey key(baseDomain, principal->OriginAttributesRef());
+ CookiesList* cookiesList = nullptr;
+ mCookiesMap.Get(key, &cookiesList);
+
+ if (!cookiesList) {
+ return NS_OK;
+ }
+
+ nsAutoCString hostFromURI;
+ rv = nsContentUtils::GetHostOrIPv6WithBrackets(principal, hostFromURI);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return NS_OK;
+ }
+
+ nsAutoCString pathFromURI;
+ principal->GetFilePath(pathFromURI);
+
+ bool thirdParty = true;
+ nsPIDOMWindowInner* innerWindow = aDocument->GetInnerWindow();
+ // in gtests we don't have a window, let's consider those requests as 3rd
+ // party.
+ if (innerWindow) {
+ ThirdPartyUtil* thirdPartyUtil = ThirdPartyUtil::GetInstance();
+
+ if (thirdPartyUtil) {
+ Unused << thirdPartyUtil->IsThirdPartyWindow(
+ innerWindow->GetOuterWindow(), nullptr, &thirdParty);
+ }
+ }
+
+ bool isPotentiallyTrustworthy =
+ principal->GetIsOriginPotentiallyTrustworthy();
+ int64_t currentTimeInUsec = PR_Now();
+ int64_t currentTime = currentTimeInUsec / PR_USEC_PER_SEC;
+
+ cookiesList->Sort(CompareCookiesForSending());
+ for (uint32_t i = 0; i < cookiesList->Length(); i++) {
+ Cookie* cookie = cookiesList->ElementAt(i);
+ // check the host, since the base domain lookup is conservative.
+ if (!CookieCommons::DomainMatches(cookie, hostFromURI)) {
+ continue;
+ }
+
+ // We don't show HttpOnly cookies in content processes.
+ if (cookie->IsHttpOnly()) {
+ continue;
+ }
+
+ if (thirdParty && !CookieCommons::ShouldIncludeCrossSiteCookieForDocument(
+ cookie, aDocument)) {
+ continue;
+ }
+
+ // do not display the cookie if it is secure and the host scheme isn't
+ if (cookie->IsSecure() && !isPotentiallyTrustworthy) {
+ continue;
+ }
+
+ // if the nsIURI path doesn't match the cookie path, don't send it back
+ if (!CookieCommons::PathMatches(cookie, pathFromURI)) {
+ continue;
+ }
+
+ // check if the cookie has expired
+ if (cookie->Expiry() <= currentTime) {
+ continue;
+ }
+
+ if (!cookie->Name().IsEmpty() || !cookie->Value().IsEmpty()) {
+ if (!aCookieString.IsEmpty()) {
+ aCookieString.AppendLiteral("; ");
+ }
+ if (!cookie->Name().IsEmpty()) {
+ aCookieString.Append(cookie->Name().get());
+ aCookieString.AppendLiteral("=");
+ aCookieString.Append(cookie->Value().get());
+ } else {
+ aCookieString.Append(cookie->Value().get());
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CookieServiceChild::GetCookieStringFromHttp(nsIURI* /*aHostURI*/,
+ nsIChannel* /*aChannel*/,
+ nsACString& /*aCookieString*/) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+CookieServiceChild::SetCookieStringFromDocument(
+ dom::Document* aDocument, const nsACString& aCookieString) {
+ NS_ENSURE_ARG(aDocument);
+
+ nsCOMPtr<nsIURI> documentURI;
+ nsAutoCString baseDomain;
+ OriginAttributes attrs;
+
+ // This function is executed in this context, I don't need to keep objects
+ // alive.
+ auto hasExistingCookiesLambda = [&](const nsACString& aBaseDomain,
+ const OriginAttributes& aAttrs) {
+ return !!CountCookiesFromHashTable(aBaseDomain, aAttrs);
+ };
+
+ RefPtr<Cookie> cookie = CookieCommons::CreateCookieFromDocument(
+ aDocument, aCookieString, PR_Now(), mTLDService, mThirdPartyUtil,
+ hasExistingCookiesLambda, getter_AddRefs(documentURI), baseDomain, attrs);
+ if (!cookie) {
+ return NS_OK;
+ }
+
+ bool thirdParty = true;
+ nsPIDOMWindowInner* innerWindow = aDocument->GetInnerWindow();
+ // in gtests we don't have a window, let's consider those requests as 3rd
+ // party.
+ if (innerWindow) {
+ ThirdPartyUtil* thirdPartyUtil = ThirdPartyUtil::GetInstance();
+
+ if (thirdPartyUtil) {
+ Unused << thirdPartyUtil->IsThirdPartyWindow(
+ innerWindow->GetOuterWindow(), nullptr, &thirdParty);
+ }
+ }
+
+ if (thirdParty && !CookieCommons::ShouldIncludeCrossSiteCookieForDocument(
+ cookie, aDocument)) {
+ return NS_OK;
+ }
+
+ CookieKey key(baseDomain, attrs);
+ CookiesList* cookies = mCookiesMap.Get(key);
+
+ if (cookies) {
+ // We need to see if the cookie we're setting would overwrite an httponly
+ // or a secure one. This would not affect anything we send over the net
+ // (those come from the parent, which already checks this),
+ // but script could see an inconsistent view of things.
+
+ nsCOMPtr<nsIPrincipal> principal = aDocument->EffectiveCookiePrincipal();
+ bool isPotentiallyTrustworthy =
+ principal->GetIsOriginPotentiallyTrustworthy();
+
+ for (uint32_t i = 0; i < cookies->Length(); ++i) {
+ RefPtr<Cookie> existingCookie = cookies->ElementAt(i);
+ if (existingCookie->Name().Equals(cookie->Name()) &&
+ existingCookie->Host().Equals(cookie->Host()) &&
+ existingCookie->Path().Equals(cookie->Path())) {
+ // Can't overwrite an httponly cookie from a script context.
+ if (existingCookie->IsHttpOnly()) {
+ return NS_OK;
+ }
+
+ // prevent insecure cookie from overwriting a secure one in insecure
+ // context.
+ if (existingCookie->IsSecure() && !isPotentiallyTrustworthy) {
+ return NS_OK;
+ }
+ }
+ }
+ }
+
+ RecordDocumentCookie(cookie, attrs);
+
+ if (CanSend()) {
+ nsTArray<CookieStruct> cookiesToSend;
+ cookiesToSend.AppendElement(cookie->ToIPC());
+
+ // Asynchronously call the parent.
+ dom::WindowGlobalChild* windowGlobalChild =
+ aDocument->GetWindowGlobalChild();
+
+ // If there is no WindowGlobalChild fall back to PCookieService SetCookies.
+ if (NS_WARN_IF(!windowGlobalChild)) {
+ SendSetCookies(baseDomain, attrs, documentURI, false, cookiesToSend);
+ return NS_OK;
+ }
+ windowGlobalChild->SendSetCookies(baseDomain, attrs, documentURI, false,
+ cookiesToSend);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CookieServiceChild::SetCookieStringFromHttp(nsIURI* aHostURI,
+ const nsACString& aCookieString,
+ nsIChannel* aChannel) {
+ NS_ENSURE_ARG(aHostURI);
+ NS_ENSURE_ARG(aChannel);
+
+ if (!CookieCommons::IsSchemeSupported(aHostURI)) {
+ return NS_OK;
+ }
+
+ // Fast past: don't bother sending IPC messages about nullprincipal'd
+ // documents.
+ nsAutoCString scheme;
+ aHostURI->GetScheme(scheme);
+ if (scheme.EqualsLiteral("moz-nullprincipal")) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+
+ uint32_t rejectedReason = 0;
+ ThirdPartyAnalysisResult result = mThirdPartyUtil->AnalyzeChannel(
+ aChannel, false, aHostURI, RequireThirdPartyCheck, &rejectedReason);
+
+ nsCString cookieString(aCookieString);
+
+ OriginAttributes attrs = loadInfo->GetOriginAttributes();
+ StoragePrincipalHelper::PrepareEffectiveStoragePrincipalOriginAttributes(
+ aChannel, attrs);
+
+ bool requireHostMatch;
+ nsCString baseDomain;
+ CookieCommons::GetBaseDomain(mTLDService, aHostURI, baseDomain,
+ requireHostMatch);
+
+ nsCOMPtr<nsICookieJarSettings> cookieJarSettings =
+ CookieCommons::GetCookieJarSettings(aChannel);
+
+ nsCOMPtr<nsIConsoleReportCollector> crc = do_QueryInterface(aChannel);
+
+ CookieStatus cookieStatus = CookieService::CheckPrefs(
+ crc, cookieJarSettings, aHostURI,
+ result.contains(ThirdPartyAnalysis::IsForeign),
+ result.contains(ThirdPartyAnalysis::IsThirdPartyTrackingResource),
+ result.contains(ThirdPartyAnalysis::IsThirdPartySocialTrackingResource),
+ result.contains(ThirdPartyAnalysis::IsStorageAccessPermissionGranted),
+ aCookieString, CountCookiesFromHashTable(baseDomain, attrs), attrs,
+ &rejectedReason);
+
+ if (cookieStatus != STATUS_ACCEPTED &&
+ cookieStatus != STATUS_ACCEPT_SESSION) {
+ return NS_OK;
+ }
+
+ CookieKey key(baseDomain, attrs);
+
+ nsTArray<CookieStruct> cookiesToSend;
+
+ int64_t currentTimeInUsec = PR_Now();
+
+ bool addonAllowsLoad = false;
+ nsCOMPtr<nsIURI> finalChannelURI;
+ NS_GetFinalChannelURI(aChannel, getter_AddRefs(finalChannelURI));
+ addonAllowsLoad = BasePrincipal::Cast(loadInfo->TriggeringPrincipal())
+ ->AddonAllowsLoad(finalChannelURI);
+
+ bool isForeignAndNotAddon = false;
+ if (!addonAllowsLoad) {
+ mThirdPartyUtil->IsThirdPartyChannel(aChannel, aHostURI,
+ &isForeignAndNotAddon);
+ }
+
+ bool mustBePartitioned =
+ isForeignAndNotAddon &&
+ cookieJarSettings->GetCookieBehavior() ==
+ nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN &&
+ !result.contains(ThirdPartyAnalysis::IsStorageAccessPermissionGranted);
+
+ bool moreCookies;
+ do {
+ CookieStruct cookieData;
+ bool canSetCookie = false;
+ moreCookies = CookieService::CanSetCookie(
+ aHostURI, baseDomain, cookieData, requireHostMatch, cookieStatus,
+ cookieString, true, isForeignAndNotAddon, mustBePartitioned, crc,
+ canSetCookie);
+ if (!canSetCookie) {
+ continue;
+ }
+
+ // check permissions from site permission list.
+ if (!CookieCommons::CheckCookiePermission(aChannel, cookieData)) {
+ COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieString,
+ "cookie rejected by permission manager");
+ constexpr auto CONSOLE_REJECTION_CATEGORY = "cookiesRejection"_ns;
+ CookieLogging::LogMessageToConsole(
+ crc, aHostURI, nsIScriptError::warningFlag,
+ CONSOLE_REJECTION_CATEGORY, "CookieRejectedByPermissionManager"_ns,
+ AutoTArray<nsString, 1>{
+ NS_ConvertUTF8toUTF16(cookieData.name()),
+ });
+ CookieCommons::NotifyRejected(
+ aHostURI, aChannel,
+ nsIWebProgressListener::STATE_COOKIES_BLOCKED_BY_PERMISSION,
+ OPERATION_WRITE);
+ continue;
+ }
+
+ RefPtr<Cookie> cookie = Cookie::Create(cookieData, attrs);
+ MOZ_ASSERT(cookie);
+
+ cookie->SetLastAccessed(currentTimeInUsec);
+ cookie->SetCreationTime(
+ Cookie::GenerateUniqueCreationTime(currentTimeInUsec));
+
+ RecordDocumentCookie(cookie, attrs);
+ cookiesToSend.AppendElement(cookieData);
+ } while (moreCookies);
+
+ // Asynchronously call the parent.
+ if (CanSend() && !cookiesToSend.IsEmpty()) {
+ RefPtr<HttpChannelChild> httpChannelChild = do_QueryObject(aChannel);
+ MOZ_ASSERT(httpChannelChild);
+ httpChannelChild->SendSetCookies(baseDomain, attrs, aHostURI, true,
+ cookiesToSend);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+CookieServiceChild::RunInTransaction(
+ nsICookieTransactionCallback* /*aCallback*/) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+} // namespace net
+} // namespace mozilla
diff --git a/netwerk/cookie/CookieServiceChild.h b/netwerk/cookie/CookieServiceChild.h
new file mode 100644
index 0000000000..b9caa2aaa7
--- /dev/null
+++ b/netwerk/cookie/CookieServiceChild.h
@@ -0,0 +1,82 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_net_CookieServiceChild_h__
+#define mozilla_net_CookieServiceChild_h__
+
+#include "CookieKey.h"
+#include "mozilla/net/PCookieServiceChild.h"
+#include "nsClassHashtable.h"
+#include "nsICookieService.h"
+#include "mozIThirdPartyUtil.h"
+#include "nsWeakReference.h"
+#include "nsThreadUtils.h"
+
+class nsIEffectiveTLDService;
+class nsILoadInfo;
+
+namespace mozilla {
+namespace net {
+
+class Cookie;
+class CookieStruct;
+
+class CookieServiceChild final : public PCookieServiceChild,
+ public nsICookieService,
+ public nsSupportsWeakReference {
+ friend class PCookieServiceChild;
+
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSICOOKIESERVICE
+
+ typedef nsTArray<RefPtr<Cookie>> CookiesList;
+ typedef nsClassHashtable<CookieKey, CookiesList> CookiesMap;
+
+ CookieServiceChild();
+
+ void Init();
+
+ static already_AddRefed<CookieServiceChild> GetSingleton();
+
+ RefPtr<GenericPromise> TrackCookieLoad(nsIChannel* aChannel);
+
+ private:
+ ~CookieServiceChild();
+
+ void RecordDocumentCookie(Cookie* aCookie, const OriginAttributes& aAttrs);
+
+ uint32_t CountCookiesFromHashTable(const nsACString& aBaseDomain,
+ const OriginAttributes& aOriginAttrs);
+
+ static bool RequireThirdPartyCheck(nsILoadInfo* aLoadInfo);
+
+ mozilla::ipc::IPCResult RecvTrackCookiesLoad(
+ nsTArray<CookieStruct>&& aCookiesList, const OriginAttributes& aAttrs);
+
+ mozilla::ipc::IPCResult RecvRemoveAll();
+
+ mozilla::ipc::IPCResult RecvRemoveBatchDeletedCookies(
+ nsTArray<CookieStruct>&& aCookiesList,
+ nsTArray<OriginAttributes>&& aAttrsList);
+
+ mozilla::ipc::IPCResult RecvRemoveCookie(const CookieStruct& aCookie,
+ const OriginAttributes& aAttrs);
+
+ mozilla::ipc::IPCResult RecvAddCookie(const CookieStruct& aCookie,
+ const OriginAttributes& aAttrs);
+
+ void RemoveSingleCookie(const CookieStruct& aCookie,
+ const OriginAttributes& aAttrs);
+
+ CookiesMap mCookiesMap;
+ nsCOMPtr<mozIThirdPartyUtil> mThirdPartyUtil;
+ nsCOMPtr<nsIEffectiveTLDService> mTLDService;
+};
+
+} // namespace net
+} // namespace mozilla
+
+#endif // mozilla_net_CookieServiceChild_h__
diff --git a/netwerk/cookie/CookieServiceParent.cpp b/netwerk/cookie/CookieServiceParent.cpp
new file mode 100644
index 0000000000..c78a67513f
--- /dev/null
+++ b/netwerk/cookie/CookieServiceParent.cpp
@@ -0,0 +1,268 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CookieCommons.h"
+#include "CookieLogging.h"
+#include "mozilla/net/CookieService.h"
+#include "mozilla/net/CookieServiceParent.h"
+#include "mozilla/net/NeckoParent.h"
+
+#include "mozilla/ipc/URIUtils.h"
+#include "mozilla/StoragePrincipalHelper.h"
+#include "mozIThirdPartyUtil.h"
+#include "nsArrayUtils.h"
+#include "nsIChannel.h"
+#include "nsIEffectiveTLDService.h"
+#include "nsNetCID.h"
+#include "nsMixedContentBlocker.h"
+
+using namespace mozilla::ipc;
+
+namespace mozilla {
+namespace net {
+
+CookieServiceParent::CookieServiceParent() {
+ // Instantiate the cookieservice via the service manager, so it sticks around
+ // until shutdown.
+ nsCOMPtr<nsICookieService> cs = do_GetService(NS_COOKIESERVICE_CONTRACTID);
+
+ // Get the CookieService instance directly, so we can call internal methods.
+ mCookieService = CookieService::GetSingleton();
+ NS_ASSERTION(mCookieService, "couldn't get nsICookieService");
+
+ mTLDService = do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID);
+ MOZ_ALWAYS_TRUE(mTLDService);
+
+ mProcessingCookie = false;
+}
+
+void CookieServiceParent::RemoveBatchDeletedCookies(nsIArray* aCookieList) {
+ uint32_t len = 0;
+ aCookieList->GetLength(&len);
+ OriginAttributes attrs;
+ CookieStruct cookieStruct;
+ nsTArray<CookieStruct> cookieStructList;
+ nsTArray<OriginAttributes> attrsList;
+ for (uint32_t i = 0; i < len; i++) {
+ nsCOMPtr<nsICookie> xpcCookie = do_QueryElementAt(aCookieList, i);
+ const auto& cookie = xpcCookie->AsCookie();
+ attrs = cookie.OriginAttributesRef();
+ cookieStruct = cookie.ToIPC();
+
+ // Child only needs to know HttpOnly cookies exists, not its value
+ // Same for Secure cookies going to a process for an insecure site.
+ if (cookie.IsHttpOnly() || !InsecureCookieOrSecureOrigin(cookie)) {
+ cookieStruct.value() = "";
+ }
+ cookieStructList.AppendElement(cookieStruct);
+ attrsList.AppendElement(attrs);
+ }
+ Unused << SendRemoveBatchDeletedCookies(cookieStructList, attrsList);
+}
+
+void CookieServiceParent::RemoveAll() { Unused << SendRemoveAll(); }
+
+void CookieServiceParent::RemoveCookie(const Cookie& cookie) {
+ const OriginAttributes& attrs = cookie.OriginAttributesRef();
+ CookieStruct cookieStruct = cookie.ToIPC();
+
+ // Child only needs to know HttpOnly cookies exists, not its value
+ // Same for Secure cookies going to a process for an insecure site.
+ if (cookie.IsHttpOnly() || !InsecureCookieOrSecureOrigin(cookie)) {
+ cookieStruct.value() = "";
+ }
+ Unused << SendRemoveCookie(cookieStruct, attrs);
+}
+
+void CookieServiceParent::AddCookie(const Cookie& cookie) {
+ const OriginAttributes& attrs = cookie.OriginAttributesRef();
+ CookieStruct cookieStruct = cookie.ToIPC();
+
+ // Child only needs to know HttpOnly cookies exists, not its value
+ // Same for Secure cookies going to a process for an insecure site.
+ if (cookie.IsHttpOnly() || !InsecureCookieOrSecureOrigin(cookie)) {
+ cookieStruct.value() = "";
+ }
+ Unused << SendAddCookie(cookieStruct, attrs);
+}
+
+bool CookieServiceParent::ContentProcessHasCookie(const Cookie& cookie) {
+ nsCString baseDomain;
+ if (NS_WARN_IF(NS_FAILED(CookieCommons::GetBaseDomainFromHost(
+ mTLDService, cookie.Host(), baseDomain)))) {
+ return false;
+ }
+
+ CookieKey cookieKey(baseDomain, cookie.OriginAttributesRef());
+ return mCookieKeysInContent.MaybeGet(cookieKey).isSome();
+}
+
+bool CookieServiceParent::InsecureCookieOrSecureOrigin(const Cookie& cookie) {
+ nsCString baseDomain;
+ // CookieStorage notifications triggering this won't fail to get base domain
+ MOZ_ALWAYS_SUCCEEDS(CookieCommons::GetBaseDomainFromHost(
+ mTLDService, cookie.Host(), baseDomain));
+
+ // cookie is insecure or cookie is associated with a secure-origin process
+ CookieKey cookieKey(baseDomain, cookie.OriginAttributesRef());
+ if (Maybe<bool> allowSecure = mCookieKeysInContent.MaybeGet(cookieKey)) {
+ return (!cookie.IsSecure() || *allowSecure);
+ }
+ return false;
+}
+
+void CookieServiceParent::TrackCookieLoad(nsIChannel* aChannel) {
+ nsCOMPtr<nsIURI> uri;
+ aChannel->GetURI(getter_AddRefs(uri));
+
+ nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
+ OriginAttributes attrs = loadInfo->GetOriginAttributes();
+ bool isSafeTopLevelNav = CookieCommons::IsSafeTopLevelNav(aChannel);
+ bool hadCrossSiteRedirects = false;
+ bool isSameSiteForeign =
+ CookieCommons::IsSameSiteForeign(aChannel, uri, &hadCrossSiteRedirects);
+
+ StoragePrincipalHelper::PrepareEffectiveStoragePrincipalOriginAttributes(
+ aChannel, attrs);
+
+ nsCOMPtr<mozIThirdPartyUtil> thirdPartyUtil;
+ thirdPartyUtil = do_GetService(THIRDPARTYUTIL_CONTRACTID);
+
+ uint32_t rejectedReason = 0;
+ ThirdPartyAnalysisResult result = thirdPartyUtil->AnalyzeChannel(
+ aChannel, false, nullptr, nullptr, &rejectedReason);
+
+ UpdateCookieInContentList(uri, attrs);
+
+ // Send matching cookies to Child.
+ nsTArray<Cookie*> foundCookieList;
+ mCookieService->GetCookiesForURI(
+ uri, aChannel, result.contains(ThirdPartyAnalysis::IsForeign),
+ result.contains(ThirdPartyAnalysis::IsThirdPartyTrackingResource),
+ result.contains(ThirdPartyAnalysis::IsThirdPartySocialTrackingResource),
+ result.contains(ThirdPartyAnalysis::IsStorageAccessPermissionGranted),
+ rejectedReason, isSafeTopLevelNav, isSameSiteForeign,
+ hadCrossSiteRedirects, false, true, attrs, foundCookieList);
+ nsTArray<CookieStruct> matchingCookiesList;
+ SerializeCookieList(foundCookieList, matchingCookiesList, uri);
+ Unused << SendTrackCookiesLoad(matchingCookiesList, attrs);
+}
+
+// we append outgoing cookie info into a list here so the ContentParent can
+// filter cookies passing to unnecessary ContentProcesses
+void CookieServiceParent::UpdateCookieInContentList(
+ nsIURI* uri, const OriginAttributes& originAttrs) {
+ nsCString baseDomain;
+ bool requireAHostMatch = false;
+
+ // prevent malformed urls from being added to the cookie list
+ if (NS_WARN_IF(NS_FAILED(CookieCommons::GetBaseDomain(
+ mTLDService, uri, baseDomain, requireAHostMatch)))) {
+ return;
+ }
+
+ CookieKey cookieKey(baseDomain, originAttrs);
+ bool& allowSecure = mCookieKeysInContent.LookupOrInsert(cookieKey, false);
+ allowSecure =
+ allowSecure || nsMixedContentBlocker::IsPotentiallyTrustworthyOrigin(uri);
+}
+
+// static
+void CookieServiceParent::SerializeCookieList(
+ const nsTArray<Cookie*>& aFoundCookieList,
+ nsTArray<CookieStruct>& aCookiesList, nsIURI* aHostURI) {
+ for (uint32_t i = 0; i < aFoundCookieList.Length(); i++) {
+ Cookie* cookie = aFoundCookieList.ElementAt(i);
+ CookieStruct* cookieStruct = aCookiesList.AppendElement();
+ *cookieStruct = cookie->ToIPC();
+
+ // clear http-only cookie values
+ if (cookie->IsHttpOnly()) {
+ // Value only needs to exist if an HttpOnly cookie exists.
+ cookieStruct->value() = "";
+ }
+
+ // clear secure cookie values in insecure context
+ bool potentiallyTurstworthy =
+ nsMixedContentBlocker::IsPotentiallyTrustworthyOrigin(aHostURI);
+ if (cookie->IsSecure() && !potentiallyTurstworthy) {
+ cookieStruct->value() = "";
+ }
+ }
+}
+
+IPCResult CookieServiceParent::RecvGetCookieList(
+ nsIURI* aHost, const bool& aIsForeign,
+ const bool& aIsThirdPartyTrackingResource,
+ const bool& aIsThirdPartySocialTrackingResource,
+ const bool& aStorageAccessPermissionGranted,
+ const uint32_t& aRejectedReason, const bool& aIsSafeTopLevelNav,
+ const bool& aIsSameSiteForeign, const bool& aHadCrossSiteRedirects,
+ const OriginAttributes& aAttrs, GetCookieListResolver&& aResolve) {
+ // Send matching cookies to Child.
+ if (!aHost) {
+ return IPC_FAIL(this, "aHost must not be null");
+ }
+
+ // we append outgoing cookie info into a list here so the ContentParent can
+ // filter cookies that do not need to go to certain ContentProcesses
+ UpdateCookieInContentList(aHost, aAttrs);
+
+ nsTArray<Cookie*> foundCookieList;
+ // Note: passing nullptr as aChannel to GetCookiesForURI() here is fine since
+ // this argument is only used for proper reporting of cookie loads, but the
+ // child process already does the necessary reporting in this case for us.
+ mCookieService->GetCookiesForURI(
+ aHost, nullptr, aIsForeign, aIsThirdPartyTrackingResource,
+ aIsThirdPartySocialTrackingResource, aStorageAccessPermissionGranted,
+ aRejectedReason, aIsSafeTopLevelNav, aIsSameSiteForeign,
+ aHadCrossSiteRedirects, false, true, aAttrs, foundCookieList);
+
+ nsTArray<CookieStruct> matchingCookiesList;
+ SerializeCookieList(foundCookieList, matchingCookiesList, aHost);
+
+ aResolve(matchingCookiesList);
+
+ return IPC_OK();
+}
+
+void CookieServiceParent::ActorDestroy(ActorDestroyReason aWhy) {
+ // Nothing needed here. Called right before destructor since this is a
+ // non-refcounted class.
+}
+
+IPCResult CookieServiceParent::RecvSetCookies(
+ const nsCString& aBaseDomain, const OriginAttributes& aOriginAttributes,
+ nsIURI* aHost, bool aFromHttp, const nsTArray<CookieStruct>& aCookies) {
+ return SetCookies(aBaseDomain, aOriginAttributes, aHost, aFromHttp, aCookies);
+}
+
+IPCResult CookieServiceParent::SetCookies(
+ const nsCString& aBaseDomain, const OriginAttributes& aOriginAttributes,
+ nsIURI* aHost, bool aFromHttp, const nsTArray<CookieStruct>& aCookies,
+ dom::BrowsingContext* aBrowsingContext) {
+ if (!mCookieService) {
+ return IPC_OK();
+ }
+
+ // Deserialize URI. Having a host URI is mandatory and should always be
+ // provided by the child; thus we consider failure fatal.
+ if (!aHost) {
+ return IPC_FAIL(this, "aHost must not be null");
+ }
+
+ // We set this to true while processing this cookie update, to make sure
+ // we don't send it back to the same content process.
+ mProcessingCookie = true;
+
+ bool ok =
+ mCookieService->SetCookiesFromIPC(aBaseDomain, aOriginAttributes, aHost,
+ aFromHttp, aCookies, aBrowsingContext);
+ mProcessingCookie = false;
+ return ok ? IPC_OK() : IPC_FAIL(this, "Invalid cookie received.");
+}
+
+} // namespace net
+} // namespace mozilla
diff --git a/netwerk/cookie/CookieServiceParent.h b/netwerk/cookie/CookieServiceParent.h
new file mode 100644
index 0000000000..45b46883fb
--- /dev/null
+++ b/netwerk/cookie/CookieServiceParent.h
@@ -0,0 +1,88 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_net_CookieServiceParent_h
+#define mozilla_net_CookieServiceParent_h
+
+#include "mozilla/net/PCookieServiceParent.h"
+#include "mozilla/net/CookieKey.h"
+
+class nsIArray;
+class nsICookie;
+namespace mozilla {
+class OriginAttributes;
+}
+
+class nsIEffectiveTLDService;
+
+namespace mozilla {
+namespace net {
+
+class Cookie;
+class CookieService;
+
+class CookieServiceParent : public PCookieServiceParent {
+ friend class PCookieServiceParent;
+
+ public:
+ CookieServiceParent();
+ virtual ~CookieServiceParent() = default;
+
+ void TrackCookieLoad(nsIChannel* aChannel);
+
+ void RemoveBatchDeletedCookies(nsIArray* aCookieList);
+
+ void RemoveAll();
+
+ void RemoveCookie(const Cookie& aCookie);
+
+ void AddCookie(const Cookie& aCookie);
+
+ // This will return true if the CookieServiceParent is currently processing
+ // an update from the content process. This is used in ContentParent to make
+ // sure that we are only forwarding those cookie updates to other content
+ // processes, not the one they originated from.
+ bool ProcessingCookie() { return mProcessingCookie; }
+
+ bool ContentProcessHasCookie(const Cookie& cookie);
+ bool InsecureCookieOrSecureOrigin(const Cookie& cookie);
+ void UpdateCookieInContentList(nsIURI* aHostURI,
+ const OriginAttributes& aOriginAttrs);
+
+ mozilla::ipc::IPCResult SetCookies(
+ const nsCString& aBaseDomain, const OriginAttributes& aOriginAttributes,
+ nsIURI* aHost, bool aFromHttp, const nsTArray<CookieStruct>& aCookies,
+ dom::BrowsingContext* aBrowsingContext = nullptr);
+
+ protected:
+ virtual void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ mozilla::ipc::IPCResult RecvSetCookies(
+ const nsCString& aBaseDomain, const OriginAttributes& aOriginAttributes,
+ nsIURI* aHost, bool aFromHttp, const nsTArray<CookieStruct>& aCookies);
+
+ mozilla::ipc::IPCResult RecvGetCookieList(
+ nsIURI* aHost, const bool& aIsForeign,
+ const bool& aIsThirdPartyTrackingResource,
+ const bool& aIsThirdPartySocialTrackingResource,
+ const bool& aStorageAccessPermissionGranted,
+ const uint32_t& aRejectedReason, const bool& aIsSafeTopLevelNav,
+ const bool& aIsSameSiteForeign, const bool& aHadCrossSiteRedirects,
+ const OriginAttributes& aAttrs, GetCookieListResolver&& aResolve);
+
+ static void SerializeCookieList(const nsTArray<Cookie*>& aFoundCookieList,
+ nsTArray<CookieStruct>& aCookiesList,
+ nsIURI* aHostURI);
+
+ nsCOMPtr<nsIEffectiveTLDService> mTLDService;
+ RefPtr<CookieService> mCookieService;
+ bool mProcessingCookie;
+ nsTHashMap<CookieKey, bool> mCookieKeysInContent;
+};
+
+} // namespace net
+} // namespace mozilla
+
+#endif // mozilla_net_CookieServiceParent_h
diff --git a/netwerk/cookie/CookieStorage.cpp b/netwerk/cookie/CookieStorage.cpp
new file mode 100644
index 0000000000..cc827a2372
--- /dev/null
+++ b/netwerk/cookie/CookieStorage.cpp
@@ -0,0 +1,910 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "Cookie.h"
+#include "CookieCommons.h"
+#include "CookieLogging.h"
+#include "CookieNotification.h"
+#include "nsCOMPtr.h"
+#include "nsICookieNotification.h"
+#include "CookieStorage.h"
+#include "mozilla/dom/nsMixedContentBlocker.h"
+#include "mozilla/glean/GleanMetrics.h"
+#include "nsIMutableArray.h"
+#include "nsTPriorityQueue.h"
+#include "nsIScriptError.h"
+#include "nsIUserIdleService.h"
+#include "nsServiceManagerUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "prprf.h"
+#include "nsIPrefService.h"
+
+#undef ADD_TEN_PERCENT
+#define ADD_TEN_PERCENT(i) static_cast<uint32_t>((i) + (i) / 10)
+
+#undef LIMIT
+#define LIMIT(x, low, high, default) \
+ ((x) >= (low) && (x) <= (high) ? (x) : (default))
+
+namespace mozilla {
+namespace net {
+
+namespace {
+
+// comparator class for lastaccessed times of cookies.
+class CompareCookiesByAge {
+ public:
+ static bool Equals(const CookieListIter& a, const CookieListIter& b) {
+ return a.Cookie()->LastAccessed() == b.Cookie()->LastAccessed() &&
+ a.Cookie()->CreationTime() == b.Cookie()->CreationTime();
+ }
+
+ static bool LessThan(const CookieListIter& a, const CookieListIter& b) {
+ // compare by lastAccessed time, and tiebreak by creationTime.
+ int64_t result = a.Cookie()->LastAccessed() - b.Cookie()->LastAccessed();
+ if (result != 0) {
+ return result < 0;
+ }
+
+ return a.Cookie()->CreationTime() < b.Cookie()->CreationTime();
+ }
+};
+
+// Cookie comparator for the priority queue used in FindStaleCookies.
+// Note that the expired cookie has the highest priority.
+// Other non-expired cookies are sorted by their age.
+class CookieIterComparator {
+ private:
+ int64_t mCurrentTime;
+
+ public:
+ explicit CookieIterComparator(int64_t aTime) : mCurrentTime(aTime) {}
+
+ bool LessThan(const CookieListIter& lhs, const CookieListIter& rhs) {
+ bool lExpired = lhs.Cookie()->Expiry() <= mCurrentTime;
+ bool rExpired = rhs.Cookie()->Expiry() <= mCurrentTime;
+ if (lExpired && !rExpired) {
+ return true;
+ }
+
+ if (!lExpired && rExpired) {
+ return false;
+ }
+
+ return mozilla::net::CompareCookiesByAge::LessThan(lhs, rhs);
+ }
+};
+
+// comparator class for sorting cookies by entry and index.
+class CompareCookiesByIndex {
+ public:
+ static bool Equals(const CookieListIter& a, const CookieListIter& b) {
+ NS_ASSERTION(a.entry != b.entry || a.index != b.index,
+ "cookie indexes should never be equal");
+ return false;
+ }
+
+ static bool LessThan(const CookieListIter& a, const CookieListIter& b) {
+ // compare by entryclass pointer, then by index.
+ if (a.entry != b.entry) {
+ return a.entry < b.entry;
+ }
+
+ return a.index < b.index;
+ }
+};
+
+} // namespace
+
+// ---------------------------------------------------------------------------
+// CookieEntry
+
+size_t CookieEntry::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
+ size_t amount = CookieKey::SizeOfExcludingThis(aMallocSizeOf);
+
+ amount += mCookies.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (uint32_t i = 0; i < mCookies.Length(); ++i) {
+ amount += mCookies[i]->SizeOfIncludingThis(aMallocSizeOf);
+ }
+
+ return amount;
+}
+
+bool CookieEntry::IsPartitioned() const {
+ return !mOriginAttributes.mPartitionKey.IsEmpty();
+}
+
+// ---------------------------------------------------------------------------
+// CookieStorage
+
+NS_IMPL_ISUPPORTS(CookieStorage, nsIObserver, nsISupportsWeakReference)
+
+void CookieStorage::Init() {
+ // init our pref and observer
+ nsCOMPtr<nsIPrefBranch> prefBranch = do_GetService(NS_PREFSERVICE_CONTRACTID);
+ if (prefBranch) {
+ prefBranch->AddObserver(kPrefMaxNumberOfCookies, this, true);
+ prefBranch->AddObserver(kPrefMaxCookiesPerHost, this, true);
+ prefBranch->AddObserver(kPrefCookiePurgeAge, this, true);
+ PrefChanged(prefBranch);
+ }
+
+ nsCOMPtr<nsIObserverService> observerService = services::GetObserverService();
+ NS_ENSURE_TRUE_VOID(observerService);
+
+ nsresult rv =
+ observerService->AddObserver(this, OBSERVER_TOPIC_IDLE_DAILY, true);
+ NS_ENSURE_SUCCESS_VOID(rv);
+}
+
+size_t CookieStorage::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
+ size_t amount = 0;
+
+ amount += aMallocSizeOf(this);
+ amount += mHostTable.SizeOfExcludingThis(aMallocSizeOf);
+
+ return amount;
+}
+
+void CookieStorage::GetCookies(nsTArray<RefPtr<nsICookie>>& aCookies) const {
+ aCookies.SetCapacity(mCookieCount);
+ for (const auto& entry : mHostTable) {
+ const CookieEntry::ArrayType& cookies = entry.GetCookies();
+ for (CookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
+ aCookies.AppendElement(cookies[i]);
+ }
+ }
+}
+
+void CookieStorage::GetSessionCookies(
+ nsTArray<RefPtr<nsICookie>>& aCookies) const {
+ aCookies.SetCapacity(mCookieCount);
+ for (const auto& entry : mHostTable) {
+ const CookieEntry::ArrayType& cookies = entry.GetCookies();
+ for (CookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
+ Cookie* cookie = cookies[i];
+ // Filter out non-session cookies.
+ if (cookie->IsSession()) {
+ aCookies.AppendElement(cookie);
+ }
+ }
+ }
+}
+
+// find an exact cookie specified by host, name, and path that hasn't expired.
+bool CookieStorage::FindCookie(const nsACString& aBaseDomain,
+ const OriginAttributes& aOriginAttributes,
+ const nsACString& aHost, const nsACString& aName,
+ const nsACString& aPath, CookieListIter& aIter) {
+ CookieEntry* entry =
+ mHostTable.GetEntry(CookieKey(aBaseDomain, aOriginAttributes));
+ if (!entry) {
+ return false;
+ }
+
+ const CookieEntry::ArrayType& cookies = entry->GetCookies();
+ for (CookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
+ Cookie* cookie = cookies[i];
+
+ if (aHost.Equals(cookie->Host()) && aPath.Equals(cookie->Path()) &&
+ aName.Equals(cookie->Name())) {
+ aIter = CookieListIter(entry, i);
+ return true;
+ }
+ }
+
+ return false;
+}
+
+// find an secure cookie specified by host and name
+bool CookieStorage::FindSecureCookie(const nsACString& aBaseDomain,
+ const OriginAttributes& aOriginAttributes,
+ Cookie* aCookie) {
+ CookieEntry* entry =
+ mHostTable.GetEntry(CookieKey(aBaseDomain, aOriginAttributes));
+ if (!entry) {
+ return false;
+ }
+
+ const CookieEntry::ArrayType& cookies = entry->GetCookies();
+ for (CookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
+ Cookie* cookie = cookies[i];
+ // isn't a match if insecure or a different name
+ if (!cookie->IsSecure() || !aCookie->Name().Equals(cookie->Name())) {
+ continue;
+ }
+
+ // The host must "domain-match" an existing cookie or vice-versa
+ if (CookieCommons::DomainMatches(cookie, aCookie->Host()) ||
+ CookieCommons::DomainMatches(aCookie, cookie->Host())) {
+ // If the path of new cookie and the path of existing cookie
+ // aren't "/", then this situation needs to compare paths to
+ // ensure only that a newly-created non-secure cookie does not
+ // overlay an existing secure cookie.
+ if (CookieCommons::PathMatches(cookie, aCookie->GetFilePath())) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+uint32_t CookieStorage::CountCookiesFromHost(const nsACString& aBaseDomain,
+ uint32_t aPrivateBrowsingId) {
+ OriginAttributes attrs;
+ attrs.mPrivateBrowsingId = aPrivateBrowsingId;
+
+ // Return a count of all cookies, including expired.
+ CookieEntry* entry = mHostTable.GetEntry(CookieKey(aBaseDomain, attrs));
+ return entry ? entry->GetCookies().Length() : 0;
+}
+
+void CookieStorage::GetAll(nsTArray<RefPtr<nsICookie>>& aResult) const {
+ aResult.SetCapacity(mCookieCount);
+
+ for (const auto& entry : mHostTable) {
+ const CookieEntry::ArrayType& cookies = entry.GetCookies();
+ for (CookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
+ aResult.AppendElement(cookies[i]);
+ }
+ }
+}
+
+const nsTArray<RefPtr<Cookie>>* CookieStorage::GetCookiesFromHost(
+ const nsACString& aBaseDomain, const OriginAttributes& aOriginAttributes) {
+ CookieEntry* entry =
+ mHostTable.GetEntry(CookieKey(aBaseDomain, aOriginAttributes));
+ return entry ? &entry->GetCookies() : nullptr;
+}
+
+void CookieStorage::GetCookiesWithOriginAttributes(
+ const OriginAttributesPattern& aPattern, const nsACString& aBaseDomain,
+ nsTArray<RefPtr<nsICookie>>& aResult) {
+ for (auto iter = mHostTable.Iter(); !iter.Done(); iter.Next()) {
+ CookieEntry* entry = iter.Get();
+
+ if (!aBaseDomain.IsEmpty() && !aBaseDomain.Equals(entry->mBaseDomain)) {
+ continue;
+ }
+
+ if (!aPattern.Matches(entry->mOriginAttributes)) {
+ continue;
+ }
+
+ const CookieEntry::ArrayType& entryCookies = entry->GetCookies();
+
+ for (CookieEntry::IndexType i = 0; i < entryCookies.Length(); ++i) {
+ aResult.AppendElement(entryCookies[i]);
+ }
+ }
+}
+
+void CookieStorage::RemoveCookie(const nsACString& aBaseDomain,
+ const OriginAttributes& aOriginAttributes,
+ const nsACString& aHost,
+ const nsACString& aName,
+ const nsACString& aPath) {
+ CookieListIter matchIter{};
+ RefPtr<Cookie> cookie;
+ if (FindCookie(aBaseDomain, aOriginAttributes, aHost, aName, aPath,
+ matchIter)) {
+ cookie = matchIter.Cookie();
+ RemoveCookieFromList(matchIter);
+ }
+
+ if (cookie) {
+ // Everything's done. Notify observers.
+ NotifyChanged(cookie, nsICookieNotification::COOKIE_DELETED, aBaseDomain);
+ }
+}
+
+void CookieStorage::RemoveCookiesWithOriginAttributes(
+ const OriginAttributesPattern& aPattern, const nsACString& aBaseDomain) {
+ // Iterate the hash table of CookieEntry.
+ for (auto iter = mHostTable.Iter(); !iter.Done(); iter.Next()) {
+ CookieEntry* entry = iter.Get();
+
+ if (!aBaseDomain.IsEmpty() && !aBaseDomain.Equals(entry->mBaseDomain)) {
+ continue;
+ }
+
+ if (!aPattern.Matches(entry->mOriginAttributes)) {
+ continue;
+ }
+
+ // Pattern matches. Delete all cookies within this CookieEntry.
+ uint32_t cookiesCount = entry->GetCookies().Length();
+
+ for (CookieEntry::IndexType i = 0; i < cookiesCount; ++i) {
+ // Remove the first cookie from the list.
+ CookieListIter iter(entry, 0);
+ RefPtr<Cookie> cookie = iter.Cookie();
+
+ // Remove the cookie.
+ RemoveCookieFromList(iter);
+
+ if (cookie) {
+ NotifyChanged(cookie, nsICookieNotification::COOKIE_DELETED,
+ aBaseDomain);
+ }
+ }
+ }
+}
+
+void CookieStorage::RemoveCookiesFromExactHost(
+ const nsACString& aHost, const nsACString& aBaseDomain,
+ const OriginAttributesPattern& aPattern) {
+ // Iterate the hash table of CookieEntry.
+ for (auto iter = mHostTable.Iter(); !iter.Done(); iter.Next()) {
+ CookieEntry* entry = iter.Get();
+
+ if (!aBaseDomain.Equals(entry->mBaseDomain)) {
+ continue;
+ }
+
+ if (!aPattern.Matches(entry->mOriginAttributes)) {
+ continue;
+ }
+
+ uint32_t cookiesCount = entry->GetCookies().Length();
+ for (CookieEntry::IndexType i = cookiesCount; i != 0; --i) {
+ CookieListIter iter(entry, i - 1);
+ RefPtr<Cookie> cookie = iter.Cookie();
+
+ if (!aHost.Equals(cookie->RawHost())) {
+ continue;
+ }
+
+ // Remove the cookie.
+ RemoveCookieFromList(iter);
+
+ if (cookie) {
+ NotifyChanged(cookie, nsICookieNotification::COOKIE_DELETED,
+ aBaseDomain);
+ }
+ }
+ }
+}
+
+void CookieStorage::RemoveAll() {
+ // clearing the hashtable will call each CookieEntry's dtor,
+ // which releases all their respective children.
+ mHostTable.Clear();
+ mCookieCount = 0;
+ mCookieOldestTime = INT64_MAX;
+
+ RemoveAllInternal();
+
+ NotifyChanged(nullptr, nsICookieNotification::ALL_COOKIES_CLEARED, ""_ns);
+}
+
+// notify observers that the cookie list changed.
+void CookieStorage::NotifyChanged(nsISupports* aSubject,
+ nsICookieNotification::Action aAction,
+ const nsACString& aBaseDomain,
+ dom::BrowsingContext* aBrowsingContext,
+ bool aOldCookieIsSession) {
+ nsCOMPtr<nsIObserverService> os = services::GetObserverService();
+ if (!os) {
+ return;
+ }
+
+ nsCOMPtr<nsICookie> cookie;
+ nsCOMPtr<nsIArray> batchDeletedCookies;
+
+ if (aAction == nsICookieNotification::COOKIES_BATCH_DELETED) {
+ batchDeletedCookies = do_QueryInterface(aSubject);
+ } else {
+ cookie = do_QueryInterface(aSubject);
+ }
+
+ uint64_t browsingContextId = 0;
+ if (aBrowsingContext) {
+ browsingContextId = aBrowsingContext->Id();
+ }
+
+ nsCOMPtr<nsICookieNotification> notification = new CookieNotification(
+ aAction, cookie, aBaseDomain, batchDeletedCookies, browsingContextId);
+ // Notify for topic "private-cookie-changed" or "cookie-changed"
+ os->NotifyObservers(notification, NotificationTopic(), u"");
+
+ NotifyChangedInternal(notification, aOldCookieIsSession);
+}
+
+// this is a backend function for adding a cookie to the list, via SetCookie.
+// also used in the cookie manager, for profile migration from IE. it either
+// replaces an existing cookie; or adds the cookie to the hashtable, and
+// deletes a cookie (if maximum number of cookies has been reached). also
+// performs list maintenance by removing expired cookies.
+void CookieStorage::AddCookie(nsIConsoleReportCollector* aCRC,
+ const nsACString& aBaseDomain,
+ const OriginAttributes& aOriginAttributes,
+ Cookie* aCookie, int64_t aCurrentTimeInUsec,
+ nsIURI* aHostURI, const nsACString& aCookieHeader,
+ bool aFromHttp,
+ dom::BrowsingContext* aBrowsingContext) {
+ int64_t currentTime = aCurrentTimeInUsec / PR_USEC_PER_SEC;
+
+ CookieListIter exactIter{};
+ bool foundCookie = false;
+ foundCookie = FindCookie(aBaseDomain, aOriginAttributes, aCookie->Host(),
+ aCookie->Name(), aCookie->Path(), exactIter);
+ bool foundSecureExact = foundCookie && exactIter.Cookie()->IsSecure();
+ bool potentiallyTrustworthy = true;
+ if (aHostURI) {
+ potentiallyTrustworthy =
+ nsMixedContentBlocker::IsPotentiallyTrustworthyOrigin(aHostURI);
+ }
+ constexpr auto CONSOLE_REJECTION_CATEGORY = "cookiesRejection"_ns;
+ bool oldCookieIsSession = false;
+ // Step1, call FindSecureCookie(). FindSecureCookie() would
+ // find the existing cookie with the security flag and has
+ // the same name, host and path of the new cookie, if there is any.
+ // Step2, Confirm new cookie's security setting. If any targeted
+ // cookie had been found in Step1, then confirm whether the
+ // new cookie could modify it. If the new created cookie’s
+ // "secure-only-flag" is not set, and the "scheme" component
+ // of the "request-uri" does not denote a "secure" protocol,
+ // then ignore the new cookie.
+ // (draft-ietf-httpbis-cookie-alone section 3.2)
+ if (!aCookie->IsSecure() &&
+ (foundSecureExact ||
+ FindSecureCookie(aBaseDomain, aOriginAttributes, aCookie)) &&
+ !potentiallyTrustworthy) {
+ COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader,
+ "cookie can't save because older cookie is secure "
+ "cookie but newer cookie is non-secure cookie");
+ CookieLogging::LogMessageToConsole(
+ aCRC, aHostURI, nsIScriptError::warningFlag, CONSOLE_REJECTION_CATEGORY,
+ "CookieRejectedNonsecureOverSecure"_ns,
+ AutoTArray<nsString, 1>{
+ NS_ConvertUTF8toUTF16(aCookie->Name()),
+ });
+ return;
+ }
+
+ RefPtr<Cookie> oldCookie;
+ nsCOMPtr<nsIArray> purgedList;
+ if (foundCookie) {
+ oldCookie = exactIter.Cookie();
+ oldCookieIsSession = oldCookie->IsSession();
+
+ // Check if the old cookie is stale (i.e. has already expired). If so, we
+ // need to be careful about the semantics of removing it and adding the new
+ // cookie: we want the behavior wrt adding the new cookie to be the same as
+ // if it didn't exist, but we still want to fire a removal notification.
+ if (oldCookie->Expiry() <= currentTime) {
+ if (aCookie->Expiry() <= currentTime) {
+ // The new cookie has expired and the old one is stale. Nothing to do.
+ COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader,
+ "cookie has already expired");
+ return;
+ }
+
+ // Remove the stale cookie. We save notification for later, once all list
+ // modifications are complete.
+ RemoveCookieFromList(exactIter);
+ COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader,
+ "stale cookie was purged");
+ purgedList = CreatePurgeList(oldCookie);
+
+ // We've done all we need to wrt removing and notifying the stale cookie.
+ // From here on out, we pretend pretend it didn't exist, so that we
+ // preserve expected notification semantics when adding the new cookie.
+ foundCookie = false;
+
+ } else {
+ // If the old cookie is httponly, make sure we're not coming from script.
+ if (!aFromHttp && oldCookie->IsHttpOnly()) {
+ COOKIE_LOGFAILURE(
+ SET_COOKIE, aHostURI, aCookieHeader,
+ "previously stored cookie is httponly; coming from script");
+ CookieLogging::LogMessageToConsole(
+ aCRC, aHostURI, nsIScriptError::warningFlag,
+ CONSOLE_REJECTION_CATEGORY,
+ "CookieRejectedHttpOnlyButFromScript"_ns,
+ AutoTArray<nsString, 1>{
+ NS_ConvertUTF8toUTF16(aCookie->Name()),
+ });
+ return;
+ }
+
+ // If the new cookie has the same value, expiry date, isSecure, isSession,
+ // isHttpOnly and SameSite flags then we can just keep the old one.
+ // Only if any of these differ we would want to override the cookie.
+ if (oldCookie->Value().Equals(aCookie->Value()) &&
+ oldCookie->Expiry() == aCookie->Expiry() &&
+ oldCookie->IsSecure() == aCookie->IsSecure() &&
+ oldCookie->IsSession() == aCookie->IsSession() &&
+ oldCookie->IsHttpOnly() == aCookie->IsHttpOnly() &&
+ oldCookie->SameSite() == aCookie->SameSite() &&
+ oldCookie->RawSameSite() == aCookie->RawSameSite() &&
+ oldCookie->SchemeMap() == aCookie->SchemeMap() &&
+ // We don't want to perform this optimization if the cookie is
+ // considered stale, since in this case we would need to update the
+ // database.
+ !oldCookie->IsStale()) {
+ // Update the last access time on the old cookie.
+ oldCookie->SetLastAccessed(aCookie->LastAccessed());
+ UpdateCookieOldestTime(oldCookie);
+ return;
+ }
+
+ // Merge the scheme map in case the old cookie and the new cookie are
+ // used with different schemes.
+ MergeCookieSchemeMap(oldCookie, aCookie);
+
+ // Remove the old cookie.
+ RemoveCookieFromList(exactIter);
+
+ // If the new cookie has expired -- i.e. the intent was simply to delete
+ // the old cookie -- then we're done.
+ if (aCookie->Expiry() <= currentTime) {
+ COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader,
+ "previously stored cookie was deleted");
+ NotifyChanged(oldCookie, nsICookieNotification::COOKIE_DELETED,
+ aBaseDomain, aBrowsingContext, oldCookieIsSession);
+ return;
+ }
+
+ // Preserve creation time of cookie for ordering purposes.
+ aCookie->SetCreationTime(oldCookie->CreationTime());
+ }
+
+ } else {
+ // check if cookie has already expired
+ if (aCookie->Expiry() <= currentTime) {
+ COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader,
+ "cookie has already expired");
+ return;
+ }
+
+ // check if we have to delete an old cookie.
+ CookieEntry* entry =
+ mHostTable.GetEntry(CookieKey(aBaseDomain, aOriginAttributes));
+ if (entry && entry->GetCookies().Length() >= mMaxCookiesPerHost) {
+ nsTArray<CookieListIter> removedIterList;
+ // Prioritize evicting insecure cookies.
+ // (draft-ietf-httpbis-cookie-alone section 3.3)
+ uint32_t limit = mMaxCookiesPerHost - mCookieQuotaPerHost;
+ FindStaleCookies(entry, currentTime, false, removedIterList, limit);
+ if (removedIterList.Length() == 0) {
+ if (aCookie->IsSecure()) {
+ // It's valid to evict a secure cookie for another secure cookie.
+ FindStaleCookies(entry, currentTime, true, removedIterList, limit);
+ } else {
+ COOKIE_LOGEVICTED(aCookie,
+ "Too many cookies for this domain and the new "
+ "cookie is not a secure cookie");
+ return;
+ }
+ }
+
+ MOZ_ASSERT(!removedIterList.IsEmpty());
+ // Sort |removedIterList| by index again, since we have to remove the
+ // cookie in the reverse order.
+ removedIterList.Sort(CompareCookiesByIndex());
+ for (auto it = removedIterList.rbegin(); it != removedIterList.rend();
+ it++) {
+ RefPtr<Cookie> evictedCookie = (*it).Cookie();
+ COOKIE_LOGEVICTED(evictedCookie, "Too many cookies for this domain");
+ RemoveCookieFromList(*it);
+ CreateOrUpdatePurgeList(purgedList, evictedCookie);
+ MOZ_ASSERT((*it).entry);
+ }
+ uint32_t purgedLength = 0;
+ purgedList->GetLength(&purgedLength);
+ mozilla::glean::networking::cookie_purge_entry_max.AccumulateSamples(
+ {purgedLength});
+
+ } else if (mCookieCount >= ADD_TEN_PERCENT(mMaxNumberOfCookies)) {
+ int64_t maxAge = aCurrentTimeInUsec - mCookieOldestTime;
+ int64_t purgeAge = ADD_TEN_PERCENT(mCookiePurgeAge);
+ if (maxAge >= purgeAge) {
+ // we're over both size and age limits by 10%; time to purge the table!
+ // do this by:
+ // 1) removing expired cookies;
+ // 2) evicting the balance of old cookies until we reach the size limit.
+ // note that the mCookieOldestTime indicator can be pessimistic - if
+ // it's older than the actual oldest cookie, we'll just purge more
+ // eagerly.
+ purgedList = PurgeCookies(aCurrentTimeInUsec, mMaxNumberOfCookies,
+ mCookiePurgeAge);
+ uint32_t purgedLength = 0;
+ purgedList->GetLength(&purgedLength);
+ mozilla::glean::networking::cookie_purge_max.AccumulateSamples(
+ {purgedLength});
+ }
+ }
+ }
+
+ // Add the cookie to the db. We do not supply a params array for batching
+ // because this might result in removals and additions being out of order.
+ AddCookieToList(aBaseDomain, aOriginAttributes, aCookie);
+ StoreCookie(aBaseDomain, aOriginAttributes, aCookie);
+
+ COOKIE_LOGSUCCESS(SET_COOKIE, aHostURI, aCookieHeader, aCookie, foundCookie);
+
+ // Now that list mutations are complete, notify observers. We do it here
+ // because observers may themselves attempt to mutate the list.
+ if (purgedList) {
+ NotifyChanged(purgedList, nsICookieNotification::COOKIES_BATCH_DELETED,
+ ""_ns);
+ }
+
+ // Notify for topic "private-cookie-changed" or "cookie-changed"
+ NotifyChanged(aCookie,
+ foundCookie ? nsICookieNotification::COOKIE_CHANGED
+ : nsICookieNotification::COOKIE_ADDED,
+ aBaseDomain, aBrowsingContext, oldCookieIsSession);
+}
+
+void CookieStorage::UpdateCookieOldestTime(Cookie* aCookie) {
+ if (aCookie->LastAccessed() < mCookieOldestTime) {
+ mCookieOldestTime = aCookie->LastAccessed();
+ }
+}
+
+void CookieStorage::MergeCookieSchemeMap(Cookie* aOldCookie,
+ Cookie* aNewCookie) {
+ aNewCookie->SetSchemeMap(aOldCookie->SchemeMap() | aNewCookie->SchemeMap());
+}
+
+void CookieStorage::AddCookieToList(const nsACString& aBaseDomain,
+ const OriginAttributes& aOriginAttributes,
+ Cookie* aCookie) {
+ if (!aCookie) {
+ NS_WARNING("Attempting to AddCookieToList with null cookie");
+ return;
+ }
+
+ CookieKey key(aBaseDomain, aOriginAttributes);
+
+ CookieEntry* entry = mHostTable.PutEntry(key);
+ NS_ASSERTION(entry, "can't insert element into a null entry!");
+
+ entry->GetCookies().AppendElement(aCookie);
+ ++mCookieCount;
+
+ // keep track of the oldest cookie, for when it comes time to purge
+ UpdateCookieOldestTime(aCookie);
+}
+
+// static
+already_AddRefed<nsIArray> CookieStorage::CreatePurgeList(nsICookie* aCookie) {
+ nsCOMPtr<nsIMutableArray> removedList =
+ do_CreateInstance(NS_ARRAY_CONTRACTID);
+ removedList->AppendElement(aCookie);
+ return removedList.forget();
+}
+
+// Given the output iter array and the count limit, find cookies
+// sort by expiry and lastAccessed time.
+// static
+void CookieStorage::FindStaleCookies(CookieEntry* aEntry, int64_t aCurrentTime,
+ bool aIsSecure,
+ nsTArray<CookieListIter>& aOutput,
+ uint32_t aLimit) {
+ MOZ_ASSERT(aLimit);
+
+ const CookieEntry::ArrayType& cookies = aEntry->GetCookies();
+ aOutput.Clear();
+
+ CookieIterComparator comp(aCurrentTime);
+ nsTPriorityQueue<CookieListIter, CookieIterComparator> queue(comp);
+
+ for (CookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
+ Cookie* cookie = cookies[i];
+
+ if (cookie->Expiry() <= aCurrentTime) {
+ queue.Push(CookieListIter(aEntry, i));
+ continue;
+ }
+
+ if (!aIsSecure) {
+ // We want to look for the non-secure cookie first time through,
+ // then find the secure cookie the second time this function is called.
+ if (cookie->IsSecure()) {
+ continue;
+ }
+ }
+
+ queue.Push(CookieListIter(aEntry, i));
+ }
+
+ uint32_t count = 0;
+ while (!queue.IsEmpty() && count < aLimit) {
+ aOutput.AppendElement(queue.Pop());
+ count++;
+ }
+}
+
+// static
+void CookieStorage::CreateOrUpdatePurgeList(nsCOMPtr<nsIArray>& aPurgedList,
+ nsICookie* aCookie) {
+ if (!aPurgedList) {
+ COOKIE_LOGSTRING(LogLevel::Debug, ("Creating new purge list"));
+ aPurgedList = CreatePurgeList(aCookie);
+ return;
+ }
+
+ nsCOMPtr<nsIMutableArray> purgedList = do_QueryInterface(aPurgedList);
+ if (purgedList) {
+ COOKIE_LOGSTRING(LogLevel::Debug, ("Updating existing purge list"));
+ purgedList->AppendElement(aCookie);
+ } else {
+ COOKIE_LOGSTRING(LogLevel::Debug, ("Could not QI aPurgedList!"));
+ }
+}
+
+// purges expired and old cookies in a batch operation.
+already_AddRefed<nsIArray> CookieStorage::PurgeCookiesWithCallbacks(
+ int64_t aCurrentTimeInUsec, uint16_t aMaxNumberOfCookies,
+ int64_t aCookiePurgeAge,
+ std::function<void(const CookieListIter&)>&& aRemoveCookieCallback,
+ std::function<void()>&& aFinalizeCallback) {
+ NS_ASSERTION(mHostTable.Count() > 0, "table is empty");
+
+ uint32_t initialCookieCount = mCookieCount;
+ COOKIE_LOGSTRING(LogLevel::Debug,
+ ("PurgeCookies(): beginning purge with %" PRIu32
+ " cookies and %" PRId64 " oldest age",
+ mCookieCount, aCurrentTimeInUsec - mCookieOldestTime));
+
+ using PurgeList = nsTArray<CookieListIter>;
+ PurgeList purgeList(kMaxNumberOfCookies);
+
+ nsCOMPtr<nsIMutableArray> removedList =
+ do_CreateInstance(NS_ARRAY_CONTRACTID);
+
+ int64_t currentTime = aCurrentTimeInUsec / PR_USEC_PER_SEC;
+ int64_t purgeTime = aCurrentTimeInUsec - aCookiePurgeAge;
+ int64_t oldestTime = INT64_MAX;
+
+ for (auto iter = mHostTable.Iter(); !iter.Done(); iter.Next()) {
+ CookieEntry* entry = iter.Get();
+
+ const CookieEntry::ArrayType& cookies = entry->GetCookies();
+ auto length = cookies.Length();
+ for (CookieEntry::IndexType i = 0; i < length;) {
+ CookieListIter iter(entry, i);
+ Cookie* cookie = cookies[i];
+
+ // check if the cookie has expired
+ if (cookie->Expiry() <= currentTime) {
+ removedList->AppendElement(cookie);
+ COOKIE_LOGEVICTED(cookie, "Cookie expired");
+
+ // remove from list; do not increment our iterator, but stop if we're
+ // done already.
+ aRemoveCookieCallback(iter);
+ if (i == --length) {
+ break;
+ }
+ } else {
+ // check if the cookie is over the age limit
+ if (cookie->LastAccessed() <= purgeTime) {
+ purgeList.AppendElement(iter);
+
+ } else if (cookie->LastAccessed() < oldestTime) {
+ // reset our indicator
+ oldestTime = cookie->LastAccessed();
+ }
+
+ ++i;
+ }
+ MOZ_ASSERT(length == cookies.Length());
+ }
+ }
+
+ uint32_t postExpiryCookieCount = mCookieCount;
+
+ // now we have a list of iterators for cookies over the age limit.
+ // sort them by age, and then we'll see how many to remove...
+ purgeList.Sort(CompareCookiesByAge());
+
+ // only remove old cookies until we reach the max cookie limit, no more.
+ uint32_t excess = mCookieCount > aMaxNumberOfCookies
+ ? mCookieCount - aMaxNumberOfCookies
+ : 0;
+ if (purgeList.Length() > excess) {
+ // We're not purging everything in the list, so update our indicator.
+ oldestTime = purgeList[excess].Cookie()->LastAccessed();
+
+ purgeList.SetLength(excess);
+ }
+
+ // sort the list again, this time grouping cookies with a common entryclass
+ // together, and with ascending index. this allows us to iterate backwards
+ // over the list removing cookies, without having to adjust indexes as we go.
+ purgeList.Sort(CompareCookiesByIndex());
+ for (PurgeList::index_type i = purgeList.Length(); i--;) {
+ Cookie* cookie = purgeList[i].Cookie();
+ removedList->AppendElement(cookie);
+ COOKIE_LOGEVICTED(cookie, "Cookie too old");
+
+ aRemoveCookieCallback(purgeList[i]);
+ }
+
+ // Update the database if we have entries to purge.
+ if (aFinalizeCallback) {
+ aFinalizeCallback();
+ }
+
+ // reset the oldest time indicator
+ mCookieOldestTime = oldestTime;
+
+ COOKIE_LOGSTRING(LogLevel::Debug,
+ ("PurgeCookies(): %" PRIu32 " expired; %" PRIu32
+ " purged; %" PRIu32 " remain; %" PRId64 " oldest age",
+ initialCookieCount - postExpiryCookieCount,
+ postExpiryCookieCount - mCookieCount, mCookieCount,
+ aCurrentTimeInUsec - mCookieOldestTime));
+
+ return removedList.forget();
+}
+
+// remove a cookie from the hashtable, and update the iterator state.
+void CookieStorage::RemoveCookieFromList(const CookieListIter& aIter) {
+ RemoveCookieFromDB(*aIter.Cookie());
+ RemoveCookieFromListInternal(aIter);
+}
+
+void CookieStorage::RemoveCookieFromListInternal(const CookieListIter& aIter) {
+ if (aIter.entry->GetCookies().Length() == 1) {
+ // we're removing the last element in the array - so just remove the entry
+ // from the hash. note that the entryclass' dtor will take care of
+ // releasing this last element for us!
+ mHostTable.RawRemoveEntry(aIter.entry);
+
+ } else {
+ // just remove the element from the list
+ aIter.entry->GetCookies().RemoveElementAt(aIter.index);
+ }
+
+ --mCookieCount;
+}
+
+void CookieStorage::PrefChanged(nsIPrefBranch* aPrefBranch) {
+ int32_t val;
+ if (NS_SUCCEEDED(aPrefBranch->GetIntPref(kPrefMaxNumberOfCookies, &val))) {
+ mMaxNumberOfCookies =
+ static_cast<uint16_t> LIMIT(val, 1, 0xFFFF, kMaxNumberOfCookies);
+ }
+
+ if (NS_SUCCEEDED(aPrefBranch->GetIntPref(kPrefCookieQuotaPerHost, &val))) {
+ mCookieQuotaPerHost = static_cast<uint16_t> LIMIT(
+ val, 1, mMaxCookiesPerHost - 1, kCookieQuotaPerHost);
+ }
+
+ if (NS_SUCCEEDED(aPrefBranch->GetIntPref(kPrefMaxCookiesPerHost, &val))) {
+ mMaxCookiesPerHost = static_cast<uint16_t> LIMIT(
+ val, mCookieQuotaPerHost + 1, 0xFFFF, kMaxCookiesPerHost);
+ }
+
+ if (NS_SUCCEEDED(aPrefBranch->GetIntPref(kPrefCookiePurgeAge, &val))) {
+ mCookiePurgeAge =
+ int64_t(LIMIT(val, 0, INT32_MAX, INT32_MAX)) * PR_USEC_PER_SEC;
+ }
+}
+
+NS_IMETHODIMP
+CookieStorage::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* /*aData*/) {
+ if (!strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) {
+ nsCOMPtr<nsIPrefBranch> prefBranch = do_QueryInterface(aSubject);
+ if (prefBranch) {
+ PrefChanged(prefBranch);
+ }
+ } else if (!strcmp(aTopic, OBSERVER_TOPIC_IDLE_DAILY)) {
+ CollectCookieJarSizeData();
+ }
+
+ return NS_OK;
+}
+
+} // namespace net
+} // namespace mozilla
diff --git a/netwerk/cookie/CookieStorage.h b/netwerk/cookie/CookieStorage.h
new file mode 100644
index 0000000000..3836edbb9c
--- /dev/null
+++ b/netwerk/cookie/CookieStorage.h
@@ -0,0 +1,216 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_net_CookieStorage_h
+#define mozilla_net_CookieStorage_h
+
+#include "CookieKey.h"
+
+#include "nsICookieNotification.h"
+#include "nsIObserver.h"
+#include "nsTHashtable.h"
+#include "nsWeakReference.h"
+#include <functional>
+#include "CookieCommons.h"
+
+class nsIArray;
+class nsICookie;
+class nsICookieTransactionCallback;
+class nsIPrefBranch;
+
+namespace mozilla {
+namespace net {
+
+class Cookie;
+
+// Inherit from CookieKey so this can be stored in nsTHashTable
+// TODO: why aren't we using nsClassHashTable<CookieKey, ArrayType>?
+class CookieEntry : public CookieKey {
+ public:
+ // Hash methods
+ using ArrayType = nsTArray<RefPtr<Cookie>>;
+ using IndexType = ArrayType::index_type;
+
+ explicit CookieEntry(KeyTypePointer aKey) : CookieKey(aKey) {}
+
+ CookieEntry(const CookieEntry& toCopy) {
+ // if we end up here, things will break. nsTHashtable shouldn't
+ // allow this, since we set ALLOW_MEMMOVE to true.
+ MOZ_ASSERT_UNREACHABLE("CookieEntry copy constructor is forbidden!");
+ }
+
+ ~CookieEntry() = default;
+
+ inline ArrayType& GetCookies() { return mCookies; }
+ inline const ArrayType& GetCookies() const { return mCookies; }
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const;
+
+ bool IsPartitioned() const;
+
+ private:
+ ArrayType mCookies;
+};
+
+// stores the CookieEntry entryclass and an index into the cookie array within
+// that entryclass, for purposes of storing an iteration state that points to a
+// certain cookie.
+struct CookieListIter {
+ // default (non-initializing) constructor.
+ CookieListIter() = default;
+
+ // explicit constructor to a given iterator state with entryclass 'aEntry'
+ // and index 'aIndex'.
+ explicit CookieListIter(CookieEntry* aEntry, CookieEntry::IndexType aIndex)
+ : entry(aEntry), index(aIndex) {}
+
+ // get the Cookie * the iterator currently points to.
+ mozilla::net::Cookie* Cookie() const { return entry->GetCookies()[index]; }
+
+ CookieEntry* entry;
+ CookieEntry::IndexType index;
+};
+
+class CookieStorage : public nsIObserver, public nsSupportsWeakReference {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const;
+
+ void GetCookies(nsTArray<RefPtr<nsICookie>>& aCookies) const;
+
+ void GetSessionCookies(nsTArray<RefPtr<nsICookie>>& aCookies) const;
+
+ bool FindCookie(const nsACString& aBaseDomain,
+ const OriginAttributes& aOriginAttributes,
+ const nsACString& aHost, const nsACString& aName,
+ const nsACString& aPath, CookieListIter& aIter);
+
+ uint32_t CountCookiesFromHost(const nsACString& aBaseDomain,
+ uint32_t aPrivateBrowsingId);
+
+ void GetAll(nsTArray<RefPtr<nsICookie>>& aResult) const;
+
+ const nsTArray<RefPtr<Cookie>>* GetCookiesFromHost(
+ const nsACString& aBaseDomain, const OriginAttributes& aOriginAttributes);
+
+ void GetCookiesWithOriginAttributes(const OriginAttributesPattern& aPattern,
+ const nsACString& aBaseDomain,
+ nsTArray<RefPtr<nsICookie>>& aResult);
+
+ void RemoveCookie(const nsACString& aBaseDomain,
+ const OriginAttributes& aOriginAttributes,
+ const nsACString& aHost, const nsACString& aName,
+ const nsACString& aPath);
+
+ virtual void RemoveCookiesWithOriginAttributes(
+ const OriginAttributesPattern& aPattern, const nsACString& aBaseDomain);
+
+ virtual void RemoveCookiesFromExactHost(
+ const nsACString& aHost, const nsACString& aBaseDomain,
+ const OriginAttributesPattern& aPattern);
+
+ void RemoveAll();
+
+ void NotifyChanged(nsISupports* aSubject,
+ nsICookieNotification::Action aAction,
+ const nsACString& aBaseDomain,
+ dom::BrowsingContext* aBrowsingContext = nullptr,
+ bool aOldCookieIsSession = false);
+
+ void AddCookie(nsIConsoleReportCollector* aCRC, const nsACString& aBaseDomain,
+ const OriginAttributes& aOriginAttributes, Cookie* aCookie,
+ int64_t aCurrentTimeInUsec, nsIURI* aHostURI,
+ const nsACString& aCookieHeader, bool aFromHttp,
+ dom::BrowsingContext* aBrowsingContext);
+
+ static void CreateOrUpdatePurgeList(nsCOMPtr<nsIArray>& aPurgedList,
+ nsICookie* aCookie);
+
+ virtual void StaleCookies(const nsTArray<Cookie*>& aCookieList,
+ int64_t aCurrentTimeInUsec) = 0;
+
+ virtual void Close() = 0;
+
+ virtual void EnsureInitialized() = 0;
+
+ virtual nsresult RunInTransaction(
+ nsICookieTransactionCallback* aCallback) = 0;
+
+ protected:
+ CookieStorage() = default;
+ virtual ~CookieStorage() = default;
+
+ void Init();
+
+ void AddCookieToList(const nsACString& aBaseDomain,
+ const OriginAttributes& aOriginAttributes,
+ Cookie* aCookie);
+
+ virtual void StoreCookie(const nsACString& aBaseDomain,
+ const OriginAttributes& aOriginAttributes,
+ Cookie* aCookie) = 0;
+
+ virtual const char* NotificationTopic() const = 0;
+
+ virtual void NotifyChangedInternal(nsICookieNotification* aSubject,
+ bool aOldCookieIsSession) = 0;
+
+ virtual void RemoveAllInternal() = 0;
+
+ // This method calls RemoveCookieFromDB + RemoveCookieFromListInternal.
+ void RemoveCookieFromList(const CookieListIter& aIter);
+
+ void RemoveCookieFromListInternal(const CookieListIter& aIter);
+
+ virtual void RemoveCookieFromDB(const Cookie& aCookie) = 0;
+
+ already_AddRefed<nsIArray> PurgeCookiesWithCallbacks(
+ int64_t aCurrentTimeInUsec, uint16_t aMaxNumberOfCookies,
+ int64_t aCookiePurgeAge,
+ std::function<void(const CookieListIter&)>&& aRemoveCookieCallback,
+ std::function<void()>&& aFinalizeCallback);
+
+ nsTHashtable<CookieEntry> mHostTable;
+
+ uint32_t mCookieCount{0};
+
+ private:
+ void PrefChanged(nsIPrefBranch* aPrefBranch);
+
+ bool FindSecureCookie(const nsACString& aBaseDomain,
+ const OriginAttributes& aOriginAttributes,
+ Cookie* aCookie);
+
+ static void FindStaleCookies(CookieEntry* aEntry, int64_t aCurrentTime,
+ bool aIsSecure,
+ nsTArray<CookieListIter>& aOutput,
+ uint32_t aLimit);
+
+ void UpdateCookieOldestTime(Cookie* aCookie);
+
+ void MergeCookieSchemeMap(Cookie* aOldCookie, Cookie* aNewCookie);
+
+ static already_AddRefed<nsIArray> CreatePurgeList(nsICookie* aCookie);
+
+ virtual already_AddRefed<nsIArray> PurgeCookies(int64_t aCurrentTimeInUsec,
+ uint16_t aMaxNumberOfCookies,
+ int64_t aCookiePurgeAge) = 0;
+
+ virtual void CollectCookieJarSizeData() = 0;
+
+ int64_t mCookieOldestTime{INT64_MAX};
+
+ uint16_t mMaxNumberOfCookies{kMaxNumberOfCookies};
+ uint16_t mMaxCookiesPerHost{kMaxCookiesPerHost};
+ uint16_t mCookieQuotaPerHost{kCookieQuotaPerHost};
+ int64_t mCookiePurgeAge{kCookiePurgeAge};
+};
+
+} // namespace net
+} // namespace mozilla
+
+#endif // mozilla_net_CookieStorage_h
diff --git a/netwerk/cookie/CookieXPCShellUtils.sys.mjs b/netwerk/cookie/CookieXPCShellUtils.sys.mjs
new file mode 100644
index 0000000000..b6d0d3f26b
--- /dev/null
+++ b/netwerk/cookie/CookieXPCShellUtils.sys.mjs
@@ -0,0 +1,53 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { ExtensionTestUtils } from "resource://testing-common/ExtensionXPCShellUtils.sys.mjs";
+
+import { AddonTestUtils } from "resource://testing-common/AddonTestUtils.sys.mjs";
+
+export const CookieXPCShellUtils = {
+ init(scope) {
+ AddonTestUtils.maybeInit(scope);
+ ExtensionTestUtils.init(scope);
+ },
+
+ createServer(args) {
+ const server = AddonTestUtils.createHttpServer(args);
+ server.registerPathHandler("/", (metadata, response) => {
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/html", false);
+
+ let body = "<body><h1>Hello world!</h1></body>";
+ response.bodyOutputStream.write(body, body.length);
+ });
+ return server;
+ },
+
+ async loadContentPage(uri, options = {}) {
+ return ExtensionTestUtils.loadContentPage(uri, options);
+ },
+
+ async getCookieStringFromDocument(uri, options = {}) {
+ const contentPage = await this.loadContentPage(uri, options);
+ const cookies = await contentPage.spawn(
+ [],
+ // eslint-disable-next-line no-undef
+ () => content.document.cookie
+ );
+ await contentPage.close();
+ return cookies;
+ },
+
+ async setCookieToDocument(uri, set, options = {}) {
+ const contentPage = await this.loadContentPage(uri, options);
+ await contentPage.spawn(
+ [set],
+ // eslint-disable-next-line no-undef
+ cookies => (content.document.cookie = cookies)
+ );
+ await contentPage.close();
+ },
+};
diff --git a/netwerk/cookie/PCookieService.ipdl b/netwerk/cookie/PCookieService.ipdl
new file mode 100644
index 0000000000..f8ec4c8d0f
--- /dev/null
+++ b/netwerk/cookie/PCookieService.ipdl
@@ -0,0 +1,74 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 ft=cpp : */
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include protocol PNecko;
+include NeckoChannelParams;
+
+using mozilla::OriginAttributes from "mozilla/ipc/BackgroundUtils.h";
+[RefCounted] using class nsIURI from "mozilla/ipc/URIUtils.h";
+
+namespace mozilla {
+namespace net {
+
+/**
+ * PCookieService
+ *
+ * Provides IPDL methods for setting and getting cookies. These are stored on
+ * and managed by the parent; the child process goes through the parent for
+ * all cookie operations. Lower-level programmatic operations (i.e. those
+ * provided by the nsICookieManager interface) are not
+ * currently implemented and requesting these interfaces in the child will fail.
+ *
+ * @see nsICookieService
+ * @see nsICookiePermission
+ */
+
+[ManualDealloc, NestedUpTo=inside_cpow] sync protocol PCookieService
+{
+ manager PNecko;
+
+parent:
+ [Nested=inside_cpow] async SetCookies(nsCString baseDomain,
+ OriginAttributes attrs,
+ nullable nsIURI host,
+ bool fromHttp,
+ CookieStruct[] cookies);
+
+ async GetCookieList(nullable nsIURI host,
+ bool isForeign,
+ bool isThirdPartyTrackingResource,
+ bool isThirdPartySocialTrackingResource,
+ bool firstPartyStorageAccessPermissionGranted,
+ uint32_t rejectedReason,
+ bool isSafeTopLevelNav,
+ bool isSameSiteForeign,
+ bool hadCrossSiteRedirects,
+ OriginAttributes attrs)
+ returns (CookieStruct[] cookies);
+
+ async __delete__();
+
+child:
+ async TrackCookiesLoad(CookieStruct[] cookiesList,
+ OriginAttributes attrs);
+
+ async RemoveCookie(CookieStruct cookie,
+ OriginAttributes attrs);
+
+ async RemoveBatchDeletedCookies(CookieStruct[] cookiesList,
+ OriginAttributes[] attrsList);
+
+ async RemoveAll();
+
+ async AddCookie(CookieStruct cookie,
+ OriginAttributes attrs);
+
+};
+
+}
+}
+
diff --git a/netwerk/cookie/moz.build b/netwerk/cookie/moz.build
new file mode 100644
index 0000000000..1667943a31
--- /dev/null
+++ b/netwerk/cookie/moz.build
@@ -0,0 +1,77 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "Networking: Cookies")
+
+# export required interfaces, even if --disable-cookies has been given
+XPIDL_SOURCES += [
+ "nsICookie.idl",
+ "nsICookieJarSettings.idl",
+ "nsICookieManager.idl",
+ "nsICookieNotification.idl",
+ "nsICookiePermission.idl",
+ "nsICookieService.idl",
+]
+
+XPIDL_MODULE = "necko_cookie"
+
+
+EXPORTS.mozilla.net = [
+ "Cookie.h",
+ "CookieJarSettings.h",
+ "CookieKey.h",
+ "CookieNotification.h",
+ "CookiePersistentStorage.h",
+ "CookiePrivateStorage.h",
+ "CookieService.h",
+ "CookieServiceChild.h",
+ "CookieServiceParent.h",
+ "CookieStorage.h",
+]
+UNIFIED_SOURCES += [
+ "Cookie.cpp",
+ "CookieCommons.cpp",
+ "CookieJarSettings.cpp",
+ "CookieLogging.cpp",
+ "CookieNotification.cpp",
+ "CookiePersistentStorage.cpp",
+ "CookiePrivateStorage.cpp",
+ "CookieService.cpp",
+ "CookieServiceChild.cpp",
+ "CookieServiceParent.cpp",
+ "CookieStorage.cpp",
+]
+XPCSHELL_TESTS_MANIFESTS += [
+ "test/unit/xpcshell.toml",
+]
+
+BROWSER_CHROME_MANIFESTS += [
+ "test/browser/browser.toml",
+]
+
+MOCHITEST_MANIFESTS += [
+ "test/mochitest/mochitest.toml",
+]
+
+IPDL_SOURCES = [
+ "PCookieService.ipdl",
+]
+
+LOCAL_INCLUDES += [
+ "/dom/base",
+ "/intl/uconv",
+ "/netwerk/base",
+ "/netwerk/protocol/http",
+]
+
+TESTING_JS_MODULES += [
+ "CookieXPCShellUtils.sys.mjs",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
diff --git a/netwerk/cookie/nsICookie.idl b/netwerk/cookie/nsICookie.idl
new file mode 100644
index 0000000000..6d7fedfd21
--- /dev/null
+++ b/netwerk/cookie/nsICookie.idl
@@ -0,0 +1,166 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+/**
+ * Main cookie object interface.
+ */
+
+%{C++
+namespace mozilla {
+ class OriginAttributes;
+
+ namespace net {
+ class Cookie;
+ }
+}
+%}
+
+[ref] native const_OriginAttributes(const mozilla::OriginAttributes);
+[ref] native const_Cookie(const mozilla::net::Cookie);
+
+typedef long nsCookieStatus;
+typedef long nsCookiePolicy;
+
+[builtinclass, scriptable, uuid(adf0db5e-211e-45a3-be14-4486ac430a58)]
+interface nsICookie : nsISupports {
+ const uint32_t SAMESITE_NONE = 0;
+ const uint32_t SAMESITE_LAX = 1;
+ const uint32_t SAMESITE_STRICT = 2;
+
+ /**
+ * the name of the cookie
+ */
+ readonly attribute ACString name;
+
+ /**
+ * the cookie value
+ */
+ readonly attribute AUTF8String value;
+
+ /**
+ * true if the cookie is a domain cookie, false otherwise
+ */
+ readonly attribute boolean isDomain;
+
+ /**
+ * the host (possibly fully qualified) of the cookie
+ */
+ readonly attribute AUTF8String host;
+
+ /**
+ * the host (possibly fully qualified) of the cookie,
+ * without a leading dot to represent if it is a
+ * domain cookie.
+ */
+ readonly attribute AUTF8String rawHost;
+
+ /**
+ * the path pertaining to the cookie
+ */
+ readonly attribute AUTF8String path;
+
+ /**
+ * true if the cookie was transmitted over ssl, false otherwise
+ */
+ readonly attribute boolean isSecure;
+
+ /**
+ * @DEPRECATED use nsICookie.expiry and nsICookie.isSession instead.
+ *
+ * expiration time in seconds since midnight (00:00:00), January 1, 1970 UTC.
+ * expires = 0 represents a session cookie.
+ * expires = 1 represents an expiration time earlier than Jan 1, 1970.
+ */
+ readonly attribute uint64_t expires;
+
+ /**
+ * the actual expiry time of the cookie, in seconds
+ * since midnight (00:00:00), January 1, 1970 UTC.
+ *
+ * this is distinct from nsICookie::expires, which
+ * has different and obsolete semantics.
+ */
+ readonly attribute int64_t expiry;
+
+ /**
+ * The origin attributes for this cookie
+ */
+ [implicit_jscontext]
+ readonly attribute jsval originAttributes;
+
+ /**
+ * Native getter for origin attributes
+ */
+ [noscript, notxpcom, nostdcall, binaryname(OriginAttributesNative)]
+ const_OriginAttributes OriginAttributesNative();
+
+ [noscript, notxpcom, nostdcall, binaryname(AsCookie)]
+ const_Cookie AsCookie();
+
+ /**
+ * true if the cookie is a session cookie.
+ * note that expiry time will also be honored
+ * for session cookies (see below); thus, whichever is
+ * the more restrictive of the two will take effect.
+ */
+ readonly attribute boolean isSession;
+
+ /**
+ * true if the cookie is an http only cookie
+ */
+ readonly attribute boolean isHttpOnly;
+
+ /**
+ * the creation time of the cookie, in microseconds
+ * since midnight (00:00:00), January 1, 1970 UTC.
+ */
+ readonly attribute int64_t creationTime;
+
+ /**
+ * the last time the cookie was accessed (i.e. created,
+ * modified, or read by the server), in microseconds
+ * since midnight (00:00:00), January 1, 1970 UTC.
+ *
+ * note that this time may be approximate.
+ */
+ readonly attribute int64_t lastAccessed;
+
+ /**
+ * the SameSite attribute; this controls the cookie behavior for cross-site
+ * requests as per
+ * https://tools.ietf.org/html/draft-west-first-party-cookies-07
+ *
+ * This should be one of:
+ * - SAMESITE_NONE - the SameSite attribute is not present
+ * - SAMESITE_LAX - the SameSite attribute is present, but not strict
+ * - SAMESITE_STRICT - the SameSite attribute is present and strict
+ */
+ readonly attribute int32_t sameSite;
+
+ /**
+ * The list of possible schemes of cookies. It's a bitmap because a cookie
+ * can be set on HTTP and HTTPS. At the moment, we treat it as the same
+ * cookie.
+ */
+ cenum schemeType : 8 {
+ SCHEME_UNSET = 0x00,
+ SCHEME_HTTP = 0x01,
+ SCHEME_HTTPS = 0x02,
+ SCHEME_FILE = 0x04,
+ };
+
+ /**
+ * Bitmap of schemes.
+ */
+ readonly attribute nsICookie_schemeType schemeMap;
+
+ /**
+ * true if the cookie's OriginAttributes PartitionKey is NOT empty
+ */
+ readonly attribute boolean isPartitioned;
+};
diff --git a/netwerk/cookie/nsICookieJarSettings.idl b/netwerk/cookie/nsICookieJarSettings.idl
new file mode 100644
index 0000000000..d522c96358
--- /dev/null
+++ b/netwerk/cookie/nsICookieJarSettings.idl
@@ -0,0 +1,86 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: ft=cpp tw=78 sw=2 et ts=2 sts=2 cin
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+#include "nsISerializable.idl"
+
+interface nsIPrincipal;
+interface nsIURI;
+
+/**
+ * Cookie jar settings for top-level documents. Please see CookieJarSettings.h
+ * for more details.
+ */
+[scriptable, builtinclass, uuid(3ec40331-7cf0-4b71-ba2a-2265aab8f6bc)]
+interface nsICookieJarSettings : nsISerializable
+{
+ /**
+ * CookieBehavior at the loading of the document. Any other loadInfo
+ * inherits it from its document's loadInfo. If there is not a document
+ * involved, cookieBehavior is reject.
+ */
+ [infallible] readonly attribute unsigned long cookieBehavior;
+
+ /**
+ * First-Party Isolation state at the loading of the document.
+ */
+ [infallible] readonly attribute boolean isFirstPartyIsolated;
+
+ /**
+ * Resist Fingerprinting state at the loading of the document.
+ */
+ [infallible] readonly attribute boolean shouldResistFingerprinting;
+
+ /**
+ * Whether our cookie behavior mandates rejecting third-party contexts.
+ */
+ [infallible] readonly attribute boolean rejectThirdPartyContexts;
+
+ [infallible] readonly attribute boolean limitForeignContexts;
+
+ [infallible] readonly attribute boolean blockingAllThirdPartyContexts;
+
+ [infallible] readonly attribute boolean blockingAllContexts;
+ /**
+ * Whether our cookie behavior mandates partitioning third-party content.
+ */
+ [infallible] attribute boolean partitionForeign;
+
+ /**
+ * Whether the top-level document is on the content blocking allow list.
+ */
+ [infallible] readonly attribute boolean isOnContentBlockingAllowList;
+
+ /**
+ * The key used for partitioning.
+ */
+ readonly attribute AString partitionKey;
+
+ /**
+ * The key used for fingerprinting randomization.
+ */
+ readonly attribute Array<uint8_t> fingerprintingRandomizationKey;
+
+ /**
+ * CookiePermission at the loading of the document for a particular
+ * principal. It returns the same cookiePermission also in case it changes
+ * during the life-time of the top document.
+ */
+ unsigned long cookiePermission(in nsIPrincipal aPrincipal);
+
+ /**
+ * Initiate the cookieJarSettings with a URI. The aURI will be used to build
+ * the partition key for this cookieJarSettings. This function is added for
+ * js code to be able to set the partitionKey from a first-party URI.
+ *
+ * The aIsPrivate indicates if this cookieJarSettings is initiated for the
+ * private browsing mode or not. If aIsPrivate was true, it will get
+ * cookieBehavior from the pref "network.cookie.cookieBehavior" which is for
+ * the regular browsing mode. Otherwise, it will get from the pref
+ * "network.cookie.cookieBehavior.pbmode" for the private browsing mode.
+ */
+ void initWithURI(in nsIURI aURI, in boolean aIsPrivate);
+};
diff --git a/netwerk/cookie/nsICookieManager.idl b/netwerk/cookie/nsICookieManager.idl
new file mode 100644
index 0000000000..ae18ec27c5
--- /dev/null
+++ b/netwerk/cookie/nsICookieManager.idl
@@ -0,0 +1,278 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+#include "nsICookie.idl"
+
+%{ C++
+namespace mozilla {
+class OriginAttributes;
+} // mozilla namespace
+%}
+
+[ptr] native OriginAttributesPtr(mozilla::OriginAttributes);
+
+/**
+ * An optional interface for accessing or removing the cookies
+ * that are in the cookie list
+ */
+
+[scriptable, builtinclass, uuid(AAAB6710-0F2C-11d5-A53B-0010A401EB10)]
+interface nsICookieManager : nsISupports
+{
+
+ /**
+ * Called to remove all cookies from the cookie list
+ */
+ void removeAll();
+
+ /**
+ * Returns an array of cookies in the cookie list.
+ * The objects in the array are of type nsICookie
+ * This array only contains non-private browsing cookies.
+ * To retrieve an array of private browsing cookies, use
+ * getCookiesWithOriginAttributes.
+ */
+ readonly attribute Array<nsICookie> cookies;
+
+ /**
+ * Returns an array of session cookies in the cookie list.
+ * The objects in the array are of type nsICookie
+ * This array only contains non-private browsing cookies.
+ */
+ readonly attribute Array<nsICookie> sessionCookies;
+
+ /**
+ * Returns current effective value of the cookieBehavior. It will return the
+ * different pref according to the aIsPrivate. If aIsPrivate is true, it will
+ * return the pref "network.cookie.cookieBehavior". Otherwise, it will return
+ * the pref "network.cookie.cookieBehavior.pbmode"
+ */
+ uint32_t getCookieBehavior(in boolean aIsPrivate);
+ %{C++
+ static uint32_t GetCookieBehavior(bool aIsPrivate);
+ %}
+
+ /**
+ * Called to remove an individual cookie from the cookie list, specified
+ * by host, name, and path. If the cookie cannot be found, no exception
+ * is thrown. Typically, the arguments to this method will be obtained
+ * directly from the desired nsICookie object.
+ *
+ * @param aHost The host or domain for which the cookie was set. @see
+ * nsICookieManager::add for a description of acceptable host
+ * strings. If the target cookie is a domain cookie, a leading
+ * dot must be present.
+ * @param aName The name specified in the cookie
+ * @param aPath The path for which the cookie was set
+ * @param aOriginAttributes The originAttributes of this cookie.
+ *
+ */
+ [implicit_jscontext]
+ void remove(in AUTF8String aHost,
+ in ACString aName,
+ in AUTF8String aPath,
+ in jsval aOriginAttributes);
+
+ [notxpcom]
+ nsresult removeNative(in AUTF8String aHost,
+ in ACString aName,
+ in AUTF8String aPath,
+ in OriginAttributesPtr aOriginAttributes);
+
+ /**
+ * Add a cookie. nsICookieService is the normal way to do this. This
+ * method is something of a backdoor.
+ *
+ * @param aHost
+ * the host or domain for which the cookie is set. presence of a
+ * leading dot indicates a domain cookie; otherwise, the cookie
+ * is treated as a non-domain cookie (see RFC2109). The host string
+ * will be normalized to ASCII or ACE; any trailing dot will be
+ * stripped. To be a domain cookie, the host must have at least two
+ * subdomain parts (e.g. '.foo.com', not '.com'), otherwise an
+ * exception will be thrown. An empty string is acceptable
+ * (e.g. file:// URI's).
+ * @param aPath
+ * path within the domain for which the cookie is valid
+ * @param aName
+ * cookie name
+ * @param aValue
+ * cookie data
+ * @param aIsSecure
+ * true if the cookie should only be sent over a secure connection.
+ * @param aIsHttpOnly
+ * true if the cookie should only be sent to, and can only be
+ * modified by, an http connection.
+ * @param aIsSession
+ * true if the cookie should exist for the current session only.
+ * see aExpiry.
+ * @param aExpiry
+ * expiration date, in seconds since midnight (00:00:00), January 1,
+ * 1970 UTC. note that expiry time will also be honored for session cookies;
+ * in this way, the more restrictive of the two will take effect.
+ * @param aOriginAttributes
+ * the originAttributes of this cookie.
+ * @param aSameSite
+ * the SameSite attribute.
+ */
+ [implicit_jscontext]
+ void add(in AUTF8String aHost,
+ in AUTF8String aPath,
+ in ACString aName,
+ in AUTF8String aValue,
+ in boolean aIsSecure,
+ in boolean aIsHttpOnly,
+ in boolean aIsSession,
+ in int64_t aExpiry,
+ in jsval aOriginAttributes,
+ in int32_t aSameSite,
+ in nsICookie_schemeType aSchemeMap);
+
+ [notxpcom]
+ nsresult addNative(in AUTF8String aHost,
+ in AUTF8String aPath,
+ in ACString aName,
+ in AUTF8String aValue,
+ in boolean aIsSecure,
+ in boolean aIsHttpOnly,
+ in boolean aIsSession,
+ in int64_t aExpiry,
+ in OriginAttributesPtr aOriginAttributes,
+ in int32_t aSameSite,
+ in nsICookie_schemeType aSchemeMap);
+
+ /**
+ * Find whether a given cookie already exists.
+ *
+ * @param aHost
+ * the cookie's host to look for
+ * @param aPath
+ * the cookie's path to look for
+ * @param aName
+ * the cookie's name to look for
+ * @param aOriginAttributes
+ * the cookie's originAttributes to look for
+ *
+ * @return true if a cookie was found which matches the host, path, name and
+ * originAttributes fields of aCookie
+ */
+ [implicit_jscontext]
+ boolean cookieExists(in AUTF8String aHost,
+ in AUTF8String aPath,
+ in ACString aName,
+ in jsval aOriginAttributes);
+
+ [notxpcom]
+ nsresult cookieExistsNative(in AUTF8String aHost,
+ in AUTF8String aPath,
+ in ACString aName,
+ in OriginAttributesPtr aOriginAttributes,
+ out boolean aExists);
+
+ /**
+ * Get a specific cookie by host, path, name and OriginAttributes.
+ *
+ * @param aHost
+ * the cookie's host to look for
+ * @param aPath
+ * the cookie's path to look for
+ * @param aName
+ * the cookie's name to look for
+ * @param aOriginAttributes
+ * the cookie's originAttributes to look for
+ *
+ * @return cookie matching the arguments or nullptr if not existing.
+ */
+ [notxpcom]
+ nsresult getCookieNative(in AUTF8String aHost,
+ in AUTF8String aPath,
+ in ACString aName,
+ in OriginAttributesPtr aOriginAttributes,
+ out nsICookie aCookie);
+
+ /**
+ * Count how many cookies exist within the base domain of 'aHost'.
+ * Thus, for a host "weather.yahoo.com", the base domain would be "yahoo.com",
+ * and any host or domain cookies for "yahoo.com" and its subdomains would be
+ * counted.
+ *
+ * @param aHost
+ * the host string to search for, e.g. "google.com". this should consist
+ * of only the host portion of a URI. see @add for a description of
+ * acceptable host strings.
+ *
+ * @return the number of cookies found.
+ */
+ unsigned long countCookiesFromHost(in AUTF8String aHost);
+
+ /**
+ * Returns an array of cookies that exist within the base domain of
+ * 'aHost'. Thus, for a host "weather.yahoo.com", the base domain would be
+ * "yahoo.com", and any host or domain cookies for "yahoo.com" and its
+ * subdomains would be returned.
+ *
+ * @param aHost
+ * the host string to search for, e.g. "google.com". this should consist
+ * of only the host portion of a URI. see @add for a description of
+ * acceptable host strings.
+ * @param aOriginAttributes The originAttributes of cookies that would be
+ * retrived.
+ *
+ * @return an array of nsICookie objects.
+ *
+ * @see countCookiesFromHost
+ */
+ [implicit_jscontext]
+ Array<nsICookie> getCookiesFromHost(in AUTF8String aHost,
+ in jsval aOriginAttributes);
+
+ /**
+ * Returns an array of all cookies whose origin attributes matches aPattern
+ *
+ * @param aPattern origin attribute pattern in JSON format
+ *
+ * @param aHost
+ * the host string to search for, e.g. "google.com". this should consist
+ * of only the host portion of a URI. see @add for a description of
+ * acceptable host strings. This attribute is optional. It will search
+ * all hosts if this attribute is not given.
+ */
+ Array<nsICookie> getCookiesWithOriginAttributes(in AString aPattern,
+ [optional] in AUTF8String aHost);
+
+ /**
+ * Remove all the cookies whose origin attributes matches aPattern
+ *
+ * @param aPattern origin attribute pattern in JSON format
+ */
+ void removeCookiesWithOriginAttributes(in AString aPattern,
+ [optional] in AUTF8String aHost);
+
+ /**
+ * Remove all the cookies whose origin attributes matches aPattern and the
+ * host is exactly aHost (without subdomain matching).
+ *
+ * @param aHost the host to match
+ * @param aPattern origin attribute pattern in JSON format
+ */
+ void removeCookiesFromExactHost(in AUTF8String aHost, in AString aPattern);
+
+ /**
+ * Removes all cookies that were created on or after aSinceWhen, and returns
+ * a Promise which will be resolved when the last such cookie has been
+ * removed.
+ *
+ * @param aSinceWhen the starting point in time after which no cookies should
+ * be created when the Promise returned from this method is resolved.
+ */
+ [implicit_jscontext]
+ Promise removeAllSince(in int64_t aSinceWhen);
+
+ /**
+ * Retrieves all the cookies that were created on or after aSinceWhen, order
+ * by creation time */
+ Array<nsICookie> getCookiesSince(in int64_t aSinceWhen);
+};
diff --git a/netwerk/cookie/nsICookieNotification.idl b/netwerk/cookie/nsICookieNotification.idl
new file mode 100644
index 0000000000..b37ae65873
--- /dev/null
+++ b/netwerk/cookie/nsICookieNotification.idl
@@ -0,0 +1,70 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsICookie;
+interface nsIArray;
+webidl BrowsingContext;
+
+/**
+ * Meta object dispatched by cookie change notifications.
+ */
+[builtinclass, scriptable, uuid(5b3490f2-75f0-4e36-9f3d-47c857ecdfbb)]
+interface nsICookieNotification : nsISupports {
+
+ cenum Action : 8 {
+ // A cookie was deleted. cookie contains the deleted cookie.
+ COOKIE_DELETED,
+ // A cookie was added. cookie contains the added cookie.
+ COOKIE_ADDED,
+ // A cookie was altered. cookie contains the updated version of the
+ // cookie. Note that host, path, and name are invariant for a given
+ // cookie; other parameters may change.
+ COOKIE_CHANGED,
+ // the entire cookie list was cleared. cookie is null.
+ ALL_COOKIES_CLEARED,
+ // A set of cookies was purged. batchDeletedCookies contains the list of
+ // deleted cookies. cookie is null.
+ // Purging typically affects expired cookies or cases where the cookie
+ // list grows too large.
+ COOKIES_BATCH_DELETED,
+ };
+
+ /**
+ * Describes the cookie operation this notification is for. Cookies may be
+ * deleted, added or changed. See Action enum above for possible values.
+ */
+ [infallible] readonly attribute nsICookieNotification_Action action;
+
+
+ /**
+ * The cookie the notification is for, may be null depending on the action.
+ */
+ [infallible] readonly attribute nsICookie cookie;
+
+ /**
+ * Base domain of the cookie. May be empty if cookie is null.
+ */
+ readonly attribute ACString baseDomain;
+
+ /**
+ * List of cookies purged.
+ * Only set when action == COOKIES_BATCH_DELETED.
+ */
+ readonly attribute nsIArray batchDeletedCookies;
+
+ /**
+ * The id of the BrowsingContext the cookie change was triggered from. Set
+ * to 0 if there is not applicable BrowsingContext.
+ */
+ [infallible] readonly attribute unsigned long long browsingContextId;
+
+ /**
+ * BrowsingContext associated with browsingContextId. May be nullptr.
+ */
+ [infallible] readonly attribute BrowsingContext browsingContext;
+};
diff --git a/netwerk/cookie/nsICookiePermission.idl b/netwerk/cookie/nsICookiePermission.idl
new file mode 100644
index 0000000000..dc9b2b0a1b
--- /dev/null
+++ b/netwerk/cookie/nsICookiePermission.idl
@@ -0,0 +1,35 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+typedef long nsCookieAccess;
+
+/**
+ * An interface to test for cookie permissions
+ */
+[scriptable, uuid(11ddd4ed-8f5b-40b3-b2a0-27c20ea1c88d)]
+interface nsICookiePermission : nsISupports
+{
+ /**
+ * nsCookieAccess values
+ */
+ const nsCookieAccess ACCESS_DEFAULT = 0;
+ const nsCookieAccess ACCESS_ALLOW = 1;
+ const nsCookieAccess ACCESS_DENY = 2;
+
+ /**
+ * additional values for nsCookieAccess which may not match
+ * nsIPermissionManager. Keep 3-7 available to allow nsIPermissionManager to
+ * add values without colliding. ACCESS_SESSION is not directly returned by
+ * any methods on this interface.
+ */
+ const nsCookieAccess ACCESS_SESSION = 8;
+
+ /**
+ * Don't use values 9 and 10! They used to be ACCESS_ALLOW_FIRST_PARTY_ONLY
+ * and ACCESS_LIMIT_THIRD_PARTY, now removed, but maybe still stored in some
+ * ancient user profiles.
+ */
+};
diff --git a/netwerk/cookie/nsICookieService.idl b/netwerk/cookie/nsICookieService.idl
new file mode 100644
index 0000000000..c69453f6fa
--- /dev/null
+++ b/netwerk/cookie/nsICookieService.idl
@@ -0,0 +1,144 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIURI;
+interface nsIChannel;
+webidl Document;
+
+/**
+ * @see nsICookieService::runInTransaction
+ */
+[scriptable, function, uuid(0fc41ffb-f1b7-42d9-9a42-8dc420c158c1)]
+interface nsICookieTransactionCallback : nsISupports
+{
+ void callback();
+};
+
+/**
+ * nsICookieService
+ *
+ * Provides methods for setting and getting cookies in the context of a
+ * page load. See nsICookieManager for methods to manipulate the cookie
+ * database directly. This separation of interface is mainly historical.
+ *
+ * This service broadcasts the notifications detailed below when the cookie
+ * list is changed, or a cookie is rejected.
+ *
+ * NOTE: observers of these notifications *must* not attempt to change profile
+ * or switch into or out of private browsing mode from within the
+ * observer. Doing so will cause undefined behavior. Mutating the cookie
+ * list (e.g. by calling methods on nsICookieService and friends) is
+ * allowed, but beware that there may be pending notifications you haven't
+ * seen yet -- for instance, a COOKIES_BATCH_DELETED notification will likely be
+ * immediately followed by COOKIE_ADDED. You may check the state of the cookie
+ * list to determine if this is the case.
+ *
+ * topic : "cookie-changed"
+ * broadcast whenever the cookie list changes in some way. see
+ * explanation of data strings below.
+ * subject: The cookie notification. See nsICookieNotification for details.
+ * data : none. For possible actions see nsICookieNotification_Action.
+ *
+ * topic : "cookie-rejected"
+ * broadcast whenever a cookie was rejected from being set as a
+ * result of user prefs.
+ * subject: an nsIURI interface pointer representing the URI that attempted
+ * to set the cookie.
+ * data : none.
+ */
+[scriptable, uuid(1e94e283-2811-4f43-b947-d22b1549d824)]
+interface nsICookieService : nsISupports
+{
+ /*
+ * Possible values for the "network.cookie.cookieBehavior" preference.
+ */
+ const uint32_t BEHAVIOR_ACCEPT = 0; // allow all cookies
+ const uint32_t BEHAVIOR_REJECT_FOREIGN = 1; // reject all third-party cookies
+ const uint32_t BEHAVIOR_REJECT = 2; // reject all cookies
+ const uint32_t BEHAVIOR_LIMIT_FOREIGN = 3; // reject third-party cookies unless the
+ // eTLD already has at least one cookie
+ const uint32_t BEHAVIOR_REJECT_TRACKER = 4; // reject trackers
+ const uint32_t BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN = 5; // reject trackers, partition third-party cookies
+ // When adding a new cookie behavior, please increase this value!
+ const uint32_t BEHAVIOR_LAST = 5;
+
+ /*
+ * Get the complete cookie string associated with the document's principal.
+ * This method is meant to be used for `document.cookie` only. Any security
+ * check about storage-access permission and cookie behavior must be done by
+ * the caller.
+ *
+ * @param aDocument
+ * The document.
+ *
+ * @return the resulting cookie string
+ */
+ ACString getCookieStringFromDocument(in Document aDocument);
+
+ /*
+ * Get the complete cookie string associated with the URI.
+ *
+ * This function is NOT redundant with getCookieString, as the result
+ * will be different based on httponly (see bug 178993)
+ *
+ * @param aURI
+ * The URI of the document for which cookies are being queried.
+ * file:// URIs (i.e. with an empty host) are allowed, but any other
+ * scheme must have a non-empty host. A trailing dot in the host
+ * is acceptable, and will be stripped. This argument must not be null.
+ * @param aChannel
+ * the channel used to load the document.
+ *
+ * @return the resulting cookie string
+ */
+ ACString getCookieStringFromHttp(in nsIURI aURI, in nsIChannel aChannel);
+
+ /*
+ * Set the cookie string associated with a Document. This method is meant to
+ * be used for `document.cookie` only. Any security check about
+ * storage-access permission and cookie behavior must be done by the caller.
+ *
+ * @param aDocument
+ * The document.
+ * @param aCookie
+ * the cookie string to set.
+ */
+ void setCookieStringFromDocument(in Document aDocument, in ACString aCookie);
+
+ /*
+ * Set the cookie string and expires associated with the URI.
+ *
+ * This function is NOT redundant with setCookieString, as the result
+ * will be different based on httponly (see bug 178993)
+ *
+ * @param aURI
+ * The URI of the document for which cookies are being queried.
+ * file:// URIs (i.e. with an empty host) are allowed, but any other
+ * scheme must have a non-empty host. A trailing dot in the host
+ * is acceptable, and will be stripped. This argument must not be null.
+ * @param aCookie
+ * the cookie string to set.
+ * @param aChannel
+ * the channel used to load the document.
+ */
+ void setCookieStringFromHttp(in nsIURI aURI, in ACString aCookie,
+ in nsIChannel aChannel);
+
+ /*
+ * Batch SQLite operations into one transaction. By default each call to
+ * CookieService that affects the underlying SQLite database (add, remove,
+ * setCookieString etc.) runs in a separate transaction. If you do this many
+ * times in a row, it's faster and suggested to wrap them all in a single
+ * transaction by setting all the operations into the callback parameter.
+ * Example: test scripts that need to construct a large cookie database.
+ * @param aCallback
+ * nsICookieTransactionCallback interface to call
+ * @throws NS_ERROR_FAILURE if aCallback() fails.
+ * @throws NS_ERROR_NOT_AVAILABLE if the connection is not established.
+ */
+ void runInTransaction(in nsICookieTransactionCallback aCallback);
+};
diff --git a/netwerk/cookie/test/browser/browser.toml b/netwerk/cookie/test/browser/browser.toml
new file mode 100644
index 0000000000..05a302ddbd
--- /dev/null
+++ b/netwerk/cookie/test/browser/browser.toml
@@ -0,0 +1,42 @@
+[DEFAULT]
+
+support-files = [
+ "file_empty.html",
+ "file_empty.js",
+ "head.js"
+]
+
+["browser_broadcastChannel.js"]
+
+["browser_cookie_insecure_overwrites_secure.js"]
+
+["browser_cookie_purge_sync.js"]
+
+["browser_cookies.js"]
+support-files = ["server.sjs"]
+
+["browser_cookies_ipv6.js"]
+
+["browser_domCache.js"]
+
+["browser_indexedDB.js"]
+
+["browser_originattributes.js"]
+
+["browser_oversize.js"]
+support-files = ["oversize.sjs"]
+
+["browser_partitionedConsole.js"]
+support-files = ["partitioned.sjs"]
+
+["browser_partitioned_telemetry.js"]
+support-files = ["partitioned.sjs"]
+
+["browser_sameSiteConsole.js"]
+support-files = ["sameSite.sjs"]
+
+["browser_serviceWorker.js"]
+
+["browser_sharedWorker.js"]
+
+["browser_storage.js"]
diff --git a/netwerk/cookie/test/browser/browser_broadcastChannel.js b/netwerk/cookie/test/browser/browser_broadcastChannel.js
new file mode 100644
index 0000000000..ee561e6f0c
--- /dev/null
+++ b/netwerk/cookie/test/browser/browser_broadcastChannel.js
@@ -0,0 +1,80 @@
+// BroadcastChannel is not considered part of CookieJar. It's not allowed to
+// communicate with other windows with different cookie jar settings.
+"use strict";
+
+CookiePolicyHelper.runTest("BroadcastChannel", {
+ cookieJarAccessAllowed: async w => {
+ new w.BroadcastChannel("hello");
+ ok(true, "BroadcastChannel be used");
+ },
+
+ cookieJarAccessDenied: async w => {
+ try {
+ new w.BroadcastChannel("hello");
+ ok(false, "BroadcastChannel cannot be used!");
+ } catch (e) {
+ ok(true, "BroadcastChannel cannot be used!");
+ is(e.name, "SecurityError", "We want a security error message.");
+ }
+ },
+});
+
+CookiePolicyHelper.runTest("BroadcastChannel in workers", {
+ cookieJarAccessAllowed: async w => {
+ function nonBlockingCode() {
+ new BroadcastChannel("hello");
+ postMessage(true);
+ }
+
+ let blob = new w.Blob([
+ nonBlockingCode.toString() + "; nonBlockingCode();",
+ ]);
+ ok(blob, "Blob has been created");
+
+ let blobURL = w.URL.createObjectURL(blob);
+ ok(blobURL, "Blob URL has been created");
+
+ let worker = new w.Worker(blobURL);
+ ok(worker, "Worker has been created");
+
+ await new w.Promise((resolve, reject) => {
+ worker.onmessage = function (e) {
+ if (e) {
+ resolve();
+ } else {
+ reject();
+ }
+ };
+ });
+ },
+
+ cookieJarAccessDenied: async w => {
+ function blockingCode() {
+ try {
+ new BroadcastChannel("hello");
+ postMessage(false);
+ } catch (e) {
+ postMessage(e.name == "SecurityError");
+ }
+ }
+
+ let blob = new w.Blob([blockingCode.toString() + "; blockingCode();"]);
+ ok(blob, "Blob has been created");
+
+ let blobURL = w.URL.createObjectURL(blob);
+ ok(blobURL, "Blob URL has been created");
+
+ let worker = new w.Worker(blobURL);
+ ok(worker, "Worker has been created");
+
+ await new w.Promise((resolve, reject) => {
+ worker.onmessage = function (e) {
+ if (e) {
+ resolve();
+ } else {
+ reject();
+ }
+ };
+ });
+ },
+});
diff --git a/netwerk/cookie/test/browser/browser_cookie_insecure_overwrites_secure.js b/netwerk/cookie/test/browser/browser_cookie_insecure_overwrites_secure.js
new file mode 100644
index 0000000000..7461e5cc16
--- /dev/null
+++ b/netwerk/cookie/test/browser/browser_cookie_insecure_overwrites_secure.js
@@ -0,0 +1,128 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+let { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+const urlPath = "/browser/netwerk/cookie/test/browser/file_empty.html";
+const baseDomain = "example.com";
+
+// eslint doesn't like http
+// eslint-disable-next-line @microsoft/sdl/no-insecure-url
+const URL_INSECURE_COM = "http://" + baseDomain + urlPath;
+const URL_SECURE_COM = "https://" + baseDomain + urlPath;
+
+// common cookie strings
+const COOKIE_BASIC = "foo=one";
+const COOKIE_OTHER = "foo=two";
+const COOKIE_THIRD = "foo=three";
+const COOKIE_FORTH = "foo=four";
+
+function securify(cookie) {
+ return cookie + "; Secure";
+}
+
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("dom.security.https_first");
+ Services.prefs.clearUserPref("network.cookie.cookieBehavior");
+ Services.prefs.clearUserPref(
+ "network.cookieJarSettings.unblocked_for_testing"
+ );
+ Services.prefs.clearUserPref("network.cookie.sameSite.laxByDefault");
+ Services.prefs.clearUserPref("network.cookie.sameSite.noneRequiresSecure");
+ Services.prefs.clearUserPref("network.cookie.sameSite.schemeful");
+ info("Cleaning up the test");
+});
+
+async function setup() {
+ // HTTPS-First would interfere with this test.
+ Services.prefs.setBoolPref("dom.security.https_first", false);
+
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+
+ Services.prefs.setBoolPref(
+ "network.cookieJarSettings.unblocked_for_testing",
+ true
+ );
+
+ Services.prefs.setBoolPref("network.cookie.sameSite.laxByDefault", false);
+ Services.prefs.setBoolPref(
+ "network.cookie.sameSite.noneRequiresSecure",
+ false
+ );
+ Services.prefs.setBoolPref("network.cookie.sameSite.schemeful", true);
+ Services.cookies.removeAll();
+}
+add_task(setup);
+
+// note:
+// 1. The URL scheme will not matter for insecure cookies, since
+// cookies are not "schemeful" in this sense.
+// So an insecure cookie set anywhere will be visible on http and https sites
+// Secure cookies are different, they will only be visible from https sites
+// and will prevent cookie setting of the same name on insecure sites.
+//
+// 2. The different processes (tabs) shouldn't matter since
+// cookie adds/changes are distributed to other processes on a need-to-know
+// basis.
+
+add_task(async function test_insecure_cant_overwrite_secure_via_doc() {
+ // insecure
+ const tab1 = BrowserTestUtils.addTab(gBrowser, URL_INSECURE_COM);
+ const browser = gBrowser.getBrowserForTab(tab1);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ // secure
+ const tab2 = BrowserTestUtils.addTab(gBrowser, URL_SECURE_COM);
+ const browser2 = gBrowser.getBrowserForTab(tab2);
+ await BrowserTestUtils.browserLoaded(browser2);
+
+ // init with insecure cookie on insecure origin child process
+ await SpecialPowers.spawn(
+ browser,
+ [COOKIE_BASIC, COOKIE_BASIC],
+ (cookie, expected) => {
+ content.document.cookie = cookie;
+ is(content.document.cookie, expected);
+ }
+ );
+
+ // insecure cookie visible on secure origin process (sanity check)
+ await SpecialPowers.spawn(browser2, [COOKIE_BASIC], expected => {
+ is(content.document.cookie, expected);
+ });
+
+ // overwrite insecure cookie on secure origin with secure cookie (sanity check)
+ await SpecialPowers.spawn(
+ browser2,
+ [securify(COOKIE_OTHER), COOKIE_OTHER],
+ (cookie, expected) => {
+ content.document.cookie = cookie;
+ is(content.document.cookie, expected);
+ }
+ );
+
+ // insecure cookie will NOT overwrite the secure one on insecure origin
+ // and cookie.document appears blank
+ await SpecialPowers.spawn(browser, [COOKIE_THIRD, ""], (cookie, expected) => {
+ content.document.cookie = cookie; // quiet failure here
+ is(content.document.cookie, expected);
+ });
+
+ // insecure cookie will overwrite secure cookie on secure origin
+ // a bit weird, but this is normal
+ await SpecialPowers.spawn(
+ browser2,
+ [COOKIE_FORTH, COOKIE_FORTH],
+ (cookie, expected) => {
+ content.document.cookie = cookie;
+ is(content.document.cookie, expected);
+ }
+ );
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+ Services.cookies.removeAll();
+});
diff --git a/netwerk/cookie/test/browser/browser_cookie_purge_sync.js b/netwerk/cookie/test/browser/browser_cookie_purge_sync.js
new file mode 100644
index 0000000000..186797d058
--- /dev/null
+++ b/netwerk/cookie/test/browser/browser_cookie_purge_sync.js
@@ -0,0 +1,141 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This test checks that cookie purge broadcast correctly
+// updates content process memory as triggered by:
+// 1. stale-cookie update from content process
+// 2. delete cookie by host from parent process
+// 3. clear all cookies from parent process
+
+const URL_EXAMPLE = "https://example.com";
+const COOKIE_NAMEVALUE = "name=value";
+const COOKIE_NAMEVALUE_2 = COOKIE_NAMEVALUE + "2";
+const COOKIE_STRING = COOKIE_NAMEVALUE + "; Secure; SameSite=None";
+const COOKIE_STRING_2 = COOKIE_NAMEVALUE_2 + "; Secure; SameSite=None";
+const MAX_AGE_OLD = 2; // seconds
+const MAX_AGE_NEW = 100; // 100 sec
+
+registerCleanupFunction(() => {
+ info("Cleaning up the test setup");
+ Services.prefs.clearUserPref("network.cookie.sameSite.laxByDefault");
+ Services.cookies.removeAll();
+});
+
+add_setup(async function () {
+ info("Setting up the test");
+ Services.prefs.setBoolPref("network.cookie.sameSite.laxByDefault", false);
+});
+
+function waitForNotificationPromise(notification, expected) {
+ return new Promise(resolve => {
+ function observer(subject, topic, data) {
+ is(content.document.cookie, expected);
+ Services.obs.removeObserver(observer, notification);
+ resolve();
+ }
+ Services.obs.addObserver(observer, notification);
+ });
+}
+
+add_task(async function test_purge_sync_batch_and_deleted() {
+ const tab1 = BrowserTestUtils.addTab(gBrowser, URL_EXAMPLE);
+ const browser = gBrowser.getBrowserForTab(tab1);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ const tab2 = BrowserTestUtils.addTab(gBrowser, URL_EXAMPLE);
+ const browser2 = gBrowser.getBrowserForTab(tab2);
+ await BrowserTestUtils.browserLoaded(browser2);
+
+ let firstCookieAdded = SpecialPowers.spawn(
+ browser2,
+ ["content-added-cookie", COOKIE_NAMEVALUE],
+ waitForNotificationPromise
+ );
+ await TestUtils.waitForTick(); // waiting helps --verify
+
+ // set old cookie in tab 1 and check it in tab 2
+ await SpecialPowers.spawn(
+ browser,
+ [COOKIE_STRING, MAX_AGE_OLD],
+ (cookie, max_age) => {
+ content.document.cookie = cookie + ";Max-Age=" + max_age;
+ }
+ );
+ await firstCookieAdded;
+
+ // wait until the first cookie expires
+ await SpecialPowers.spawn(browser2, [], () => {
+ return ContentTaskUtils.waitForCondition(
+ () => content.document.cookie == "",
+ "cookie did not expire in time",
+ 200
+ ).catch(msg => {
+ is(false, "Cookie did not expire in time");
+ });
+ });
+
+ // BATCH_DELETED/BatchDeleted pathway
+ let batchDeletedPromise = SpecialPowers.spawn(
+ browser,
+ ["content-batch-deleted-cookies", COOKIE_NAMEVALUE_2],
+ waitForNotificationPromise
+ );
+ await TestUtils.waitForTick(); // waiting helps --verify
+ await SpecialPowers.spawn(
+ browser,
+ [COOKIE_STRING_2, MAX_AGE_NEW],
+ (cookie, max_age) => {
+ content.document.cookie = cookie + ";Max-Age=" + max_age;
+ }
+ );
+ await batchDeletedPromise;
+
+ // COOKIE_DELETED/RemoveCookie pathway
+ let cookieRemovedPromise = SpecialPowers.spawn(
+ browser,
+ ["content-removed-cookie", ""],
+ waitForNotificationPromise
+ );
+ let cookieRemovedPromise2 = SpecialPowers.spawn(
+ browser2,
+ ["content-removed-cookie", ""],
+ waitForNotificationPromise
+ );
+ await TestUtils.waitForTick();
+ Services.cookies.removeCookiesFromExactHost(
+ "example.com",
+ JSON.stringify({})
+ );
+ await cookieRemovedPromise;
+ await cookieRemovedPromise2;
+
+ // cleanup prep
+ let anotherCookieAdded = SpecialPowers.spawn(
+ browser2,
+ ["content-added-cookie", COOKIE_NAMEVALUE],
+ waitForNotificationPromise
+ );
+ await TestUtils.waitForTick(); // waiting helps --verify
+ await SpecialPowers.spawn(
+ browser,
+ [COOKIE_STRING, MAX_AGE_NEW],
+ (cookie, max_age) => {
+ content.document.cookie = cookie + ";Max-Age=" + max_age;
+ }
+ );
+ await anotherCookieAdded;
+
+ // ALL_COOKIES_CLEARED/RemoveAll pathway
+ let cleanup = SpecialPowers.spawn(
+ browser2,
+ ["content-removed-all-cookies", ""],
+ waitForNotificationPromise
+ );
+ await TestUtils.waitForTick();
+ Services.cookies.removeAll();
+ await cleanup;
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+});
diff --git a/netwerk/cookie/test/browser/browser_cookies.js b/netwerk/cookie/test/browser/browser_cookies.js
new file mode 100644
index 0000000000..8a0d8332bf
--- /dev/null
+++ b/netwerk/cookie/test/browser/browser_cookies.js
@@ -0,0 +1,53 @@
+"use strict";
+
+CookiePolicyHelper.runTest("document.cookies", {
+ cookieJarAccessAllowed: async _ => {
+ let hasCookie = !!content.document.cookie.length;
+
+ await content
+ .fetch("server.sjs")
+ .then(r => r.text())
+ .then(text => {
+ is(
+ text,
+ hasCookie ? "cookie-present" : "cookie-not-present",
+ "document.cookie is consistent with fetch requests"
+ );
+ });
+
+ content.document.cookie = "name=value";
+ ok(content.document.cookie.includes("name=value"), "Some cookies for me");
+ ok(content.document.cookie.includes("foopy=1"), "Some cookies for me");
+
+ await content
+ .fetch("server.sjs")
+ .then(r => r.text())
+ .then(text => {
+ is(text, "cookie-present", "We should have cookies");
+ });
+
+ ok(!!content.document.cookie.length, "Some Cookies for me");
+ },
+
+ cookieJarAccessDenied: async _ => {
+ is(content.document.cookie, "", "No cookies for me");
+ content.document.cookie = "name=value";
+ is(content.document.cookie, "", "No cookies for me");
+
+ await content
+ .fetch("server.sjs")
+ .then(r => r.text())
+ .then(text => {
+ is(text, "cookie-not-present", "We should not have cookies");
+ });
+ // Let's do it twice.
+ await content
+ .fetch("server.sjs")
+ .then(r => r.text())
+ .then(text => {
+ is(text, "cookie-not-present", "We should not have cookies");
+ });
+
+ is(content.document.cookie, "", "Still no cookies for me");
+ },
+});
diff --git a/netwerk/cookie/test/browser/browser_cookies_ipv6.js b/netwerk/cookie/test/browser/browser_cookies_ipv6.js
new file mode 100644
index 0000000000..088a76f4e8
--- /dev/null
+++ b/netwerk/cookie/test/browser/browser_cookies_ipv6.js
@@ -0,0 +1,57 @@
+"use strict";
+
+let { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+let gHttpServer = null;
+let ip = "[::1]";
+
+function contentHandler(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 200, "Ok");
+ response.setHeader("Content-Type", "text/html", false);
+ let body = `
+ <!DOCTYPE HTML>
+ <html>
+ <head>
+ <meta charset='utf-8'>
+ <title>Cookie ipv6 Test</title>
+ </head>
+ <body>
+ </body>
+ </html>`;
+ response.bodyOutputStream.write(body, body.length);
+}
+
+add_task(async _ => {
+ if (!gHttpServer) {
+ gHttpServer = new HttpServer();
+ gHttpServer.registerPathHandler("/content", contentHandler);
+ gHttpServer._start(-1, ip);
+ }
+
+ registerCleanupFunction(() => {
+ gHttpServer.stop(() => {
+ gHttpServer = null;
+ });
+ });
+
+ let serverPort = gHttpServer.identity.primaryPort;
+ let testURL = `http://${ip}:${serverPort}/content`;
+
+ // Let's open our tab.
+ const tab = BrowserTestUtils.addTab(gBrowser, testURL);
+ gBrowser.selectedTab = tab;
+
+ const browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ // Test if we can set and get document.cookie successfully.
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.cookie = "foo=bar";
+ is(content.document.cookie, "foo=bar");
+ });
+
+ // Let's close the tab.
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/netwerk/cookie/test/browser/browser_domCache.js b/netwerk/cookie/test/browser/browser_domCache.js
new file mode 100644
index 0000000000..5f1aa84d83
--- /dev/null
+++ b/netwerk/cookie/test/browser/browser_domCache.js
@@ -0,0 +1,25 @@
+"use strict";
+
+CookiePolicyHelper.runTest("DOM Cache", {
+ cookieJarAccessAllowed: async w => {
+ await w.caches.open("wow").then(
+ _ => {
+ ok(true, "DOM Cache can be used!");
+ },
+ _ => {
+ ok(false, "DOM Cache can be used!");
+ }
+ );
+ },
+
+ cookieJarAccessDenied: async w => {
+ await w.caches.open("wow").then(
+ _ => {
+ ok(false, "DOM Cache cannot be used!");
+ },
+ _ => {
+ ok(true, "DOM Cache cannot be used!");
+ }
+ );
+ },
+});
diff --git a/netwerk/cookie/test/browser/browser_indexedDB.js b/netwerk/cookie/test/browser/browser_indexedDB.js
new file mode 100644
index 0000000000..7f417077eb
--- /dev/null
+++ b/netwerk/cookie/test/browser/browser_indexedDB.js
@@ -0,0 +1,84 @@
+"use strict";
+
+CookiePolicyHelper.runTest("IndexedDB", {
+ cookieJarAccessAllowed: async w => {
+ w.indexedDB.open("test", "1");
+ ok(true, "IDB should be allowed");
+ },
+
+ cookieJarAccessDenied: async w => {
+ try {
+ w.indexedDB.open("test", "1");
+ ok(false, "IDB should be blocked");
+ } catch (e) {
+ ok(true, "IDB should be blocked");
+ is(e.name, "SecurityError", "We want a security error message.");
+ }
+ },
+});
+
+CookiePolicyHelper.runTest("IndexedDB in workers", {
+ cookieJarAccessAllowed: async w => {
+ function nonBlockCode() {
+ indexedDB.open("test", "1");
+ postMessage(true);
+ }
+
+ let blob = new w.Blob([nonBlockCode.toString() + "; nonBlockCode();"]);
+ ok(blob, "Blob has been created");
+
+ let blobURL = w.URL.createObjectURL(blob);
+ ok(blobURL, "Blob URL has been created");
+
+ let worker = new w.Worker(blobURL);
+ ok(worker, "Worker has been created");
+
+ await new w.Promise((resolve, reject) => {
+ worker.onmessage = function (e) {
+ if (e.data) {
+ resolve();
+ } else {
+ reject();
+ }
+ };
+
+ worker.onerror = function (e) {
+ reject();
+ };
+ });
+ },
+
+ cookieJarAccessDenied: async w => {
+ function blockCode() {
+ try {
+ indexedDB.open("test", "1");
+ postMessage(false);
+ } catch (e) {
+ postMessage(e.name == "SecurityError");
+ }
+ }
+
+ let blob = new w.Blob([blockCode.toString() + "; blockCode();"]);
+ ok(blob, "Blob has been created");
+
+ let blobURL = w.URL.createObjectURL(blob);
+ ok(blobURL, "Blob URL has been created");
+
+ let worker = new w.Worker(blobURL);
+ ok(worker, "Worker has been created");
+
+ await new w.Promise((resolve, reject) => {
+ worker.onmessage = function (e) {
+ if (e.data) {
+ resolve();
+ } else {
+ reject();
+ }
+ };
+
+ worker.onerror = function (e) {
+ reject();
+ };
+ });
+ },
+});
diff --git a/netwerk/cookie/test/browser/browser_originattributes.js b/netwerk/cookie/test/browser/browser_originattributes.js
new file mode 100644
index 0000000000..fab7e67b2e
--- /dev/null
+++ b/netwerk/cookie/test/browser/browser_originattributes.js
@@ -0,0 +1,121 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const USER_CONTEXTS = ["default", "personal", "work"];
+
+const COOKIE_NAMES = ["cookie0", "cookie1", "cookie2"];
+
+const TEST_URL =
+ "http://example.com/browser/netwerk/cookie/test/browser/file_empty.html";
+
+// opens `uri' in a new tab with the provided userContextId and focuses it.
+// returns the newly opened tab
+async function openTabInUserContext(uri, userContextId) {
+ // open the tab in the correct userContextId
+ let tab = BrowserTestUtils.addTab(gBrowser, uri, { userContextId });
+
+ // select tab and make sure its browser is focused
+ gBrowser.selectedTab = tab;
+ tab.ownerGlobal.focus();
+
+ let browser = gBrowser.getBrowserForTab(tab);
+ // wait for tab load
+ await BrowserTestUtils.browserLoaded(browser);
+
+ return { tab, browser };
+}
+
+add_setup(async function () {
+ // make sure userContext is enabled.
+ await new Promise(resolve => {
+ SpecialPowers.pushPrefEnv(
+ { set: [["privacy.userContext.enabled", true]] },
+ resolve
+ );
+ });
+});
+
+add_task(async function test() {
+ // load the page in 3 different contexts and set a cookie
+ // which should only be visible in that context
+ for (let userContextId of Object.keys(USER_CONTEXTS)) {
+ // open our tab in the given user context
+ let { tab, browser } = await openTabInUserContext(TEST_URL, userContextId);
+
+ await SpecialPowers.spawn(
+ browser,
+ [{ names: COOKIE_NAMES, value: USER_CONTEXTS[userContextId] }],
+ function (opts) {
+ for (let name of opts.names) {
+ content.document.cookie = name + "=" + opts.value;
+ }
+ }
+ );
+
+ // remove the tab
+ gBrowser.removeTab(tab);
+ }
+
+ let expectedValues = USER_CONTEXTS.slice(0);
+ await checkCookies(expectedValues, "before removal");
+
+ // remove cookies that belongs to user context id #1
+ Services.cookies.removeCookiesWithOriginAttributes(
+ JSON.stringify({ userContextId: 1 })
+ );
+
+ expectedValues[1] = undefined;
+ await checkCookies(expectedValues, "after removal");
+});
+
+async function checkCookies(expectedValues, time) {
+ for (let userContextId of Object.keys(expectedValues)) {
+ let cookiesFromTitle = await getCookiesFromJS(userContextId);
+ let cookiesFromManager = getCookiesFromManager(userContextId);
+
+ let expectedValue = expectedValues[userContextId];
+ for (let name of COOKIE_NAMES) {
+ is(
+ cookiesFromTitle[name],
+ expectedValue,
+ `User context ${userContextId}: ${name} should be correct from title ${time}`
+ );
+ is(
+ cookiesFromManager[name],
+ expectedValue,
+ `User context ${userContextId}: ${name} should be correct from manager ${time}`
+ );
+ }
+ }
+}
+
+function getCookiesFromManager(userContextId) {
+ let cookies = {};
+ let allCookies = Services.cookies.getCookiesWithOriginAttributes(
+ JSON.stringify({ userContextId })
+ );
+ for (let cookie of allCookies) {
+ cookies[cookie.name] = cookie.value;
+ }
+ return cookies;
+}
+
+async function getCookiesFromJS(userContextId) {
+ let { tab, browser } = await openTabInUserContext(TEST_URL, userContextId);
+
+ // get the cookies
+ let cookieString = await SpecialPowers.spawn(browser, [], function () {
+ return content.document.cookie;
+ });
+
+ // check each item in the title and validate it meets expectatations
+ let cookies = {};
+ for (let cookie of cookieString.split(";")) {
+ let [name, value] = cookie.trim().split("=");
+ cookies[name] = value;
+ }
+
+ gBrowser.removeTab(tab);
+ return cookies;
+}
diff --git a/netwerk/cookie/test/browser/browser_oversize.js b/netwerk/cookie/test/browser/browser_oversize.js
new file mode 100644
index 0000000000..f6e1f8a70b
--- /dev/null
+++ b/netwerk/cookie/test/browser/browser_oversize.js
@@ -0,0 +1,96 @@
+"use strict";
+
+const OVERSIZE_DOMAIN = "http://example.com/";
+const OVERSIZE_PATH = "browser/netwerk/cookie/test/browser/";
+const OVERSIZE_TOP_PAGE = OVERSIZE_DOMAIN + OVERSIZE_PATH + "oversize.sjs";
+
+add_task(async _ => {
+ const expected = [];
+
+ const consoleListener = {
+ observe(what) {
+ if (!(what instanceof Ci.nsIConsoleMessage)) {
+ return;
+ }
+
+ info("Console Listener: " + what);
+ for (let i = expected.length - 1; i >= 0; --i) {
+ const e = expected[i];
+
+ if (what.message.includes(e.match)) {
+ ok(true, "Message received: " + e.match);
+ expected.splice(i, 1);
+ e.resolve();
+ }
+ }
+ },
+ };
+
+ Services.console.registerListener(consoleListener);
+
+ registerCleanupFunction(() =>
+ Services.console.unregisterListener(consoleListener)
+ );
+
+ const netPromises = [
+ new Promise(resolve => {
+ expected.push({
+ resolve,
+ match:
+ "Cookie “a” is invalid because its size is too big. Max size is 4096 B.",
+ });
+ }),
+
+ new Promise(resolve => {
+ expected.push({
+ resolve,
+ match:
+ "Cookie “b” is invalid because its path size is too big. Max size is 1024 B.",
+ });
+ }),
+ ];
+
+ // Let's open our tab.
+ const tab = BrowserTestUtils.addTab(gBrowser, OVERSIZE_TOP_PAGE);
+ gBrowser.selectedTab = tab;
+
+ const browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ // Let's wait for the first set of console events.
+ await Promise.all(netPromises);
+
+ // the DOM list of events.
+ const domPromises = [
+ new Promise(resolve => {
+ expected.push({
+ resolve,
+ match:
+ "Cookie “d” is invalid because its size is too big. Max size is 4096 B.",
+ });
+ }),
+
+ new Promise(resolve => {
+ expected.push({
+ resolve,
+ match:
+ "Cookie “e” is invalid because its path size is too big. Max size is 1024 B.",
+ });
+ }),
+ ];
+
+ // Let's use document.cookie
+ SpecialPowers.spawn(browser, [], () => {
+ const maxBytesPerCookie = 4096;
+ const maxBytesPerCookiePath = 1024;
+ content.document.cookie = "d=" + Array(maxBytesPerCookie + 1).join("x");
+ content.document.cookie =
+ "e=f; path=/" + Array(maxBytesPerCookiePath + 1).join("x");
+ });
+
+ // Let's wait for the dom events.
+ await Promise.all(domPromises);
+
+ // Let's close the tab.
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/netwerk/cookie/test/browser/browser_partitionedConsole.js b/netwerk/cookie/test/browser/browser_partitionedConsole.js
new file mode 100644
index 0000000000..ec834bfbcf
--- /dev/null
+++ b/netwerk/cookie/test/browser/browser_partitionedConsole.js
@@ -0,0 +1,201 @@
+"use strict";
+
+const DOMAIN = "https://example.com/";
+const PATH = "browser/netwerk/cookie/test/browser/";
+const TOP_PAGE = DOMAIN + PATH + "file_empty.html";
+
+// Run the test with CHIPS disabled, expecting a warning message
+add_task(async _ => {
+ await SpecialPowers.pushPrefEnv({
+ set: [["network.cookie.cookieBehavior.optInPartitioning", false]],
+ });
+
+ const expected = [];
+
+ const consoleListener = {
+ observe(what) {
+ if (!(what instanceof Ci.nsIConsoleMessage)) {
+ return;
+ }
+
+ info("Console Listener: " + what);
+ for (let i = expected.length - 1; i >= 0; --i) {
+ const e = expected[i];
+
+ if (what.message.includes(e.match)) {
+ ok(true, "Message received: " + e.match);
+ expected.splice(i, 1);
+ e.resolve();
+ }
+ }
+ },
+ };
+
+ Services.console.registerListener(consoleListener);
+
+ registerCleanupFunction(() =>
+ Services.console.unregisterListener(consoleListener)
+ );
+
+ const netPromises = [
+ new Promise(resolve => {
+ expected.push({
+ resolve,
+ match:
+ "Cookie “a” will soon be rejected because it is foreign and does not have the “Partitioned“ attribute.",
+ });
+ }),
+ ];
+
+ // Let's open our tab.
+ const tab = BrowserTestUtils.addTab(gBrowser, TOP_PAGE);
+ gBrowser.selectedTab = tab;
+ const browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ // Set cookies with cross-site HTTP
+ await SpecialPowers.spawn(browser, [], async function () {
+ await content.fetch(
+ "https://example.org/browser/netwerk/cookie/test/browser/partitioned.sjs",
+ { credentials: "include" }
+ );
+ });
+
+ // Let's wait for the first set of console events.
+ await Promise.all(netPromises);
+
+ // Let's close the tab.
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Run the test with CHIPS enabled, expecting a different warning message
+add_task(async _ => {
+ await SpecialPowers.pushPrefEnv({
+ set: [["network.cookie.cookieBehavior.optInPartitioning", true]],
+ });
+
+ const expected = [];
+
+ const consoleListener = {
+ observe(what) {
+ if (!(what instanceof Ci.nsIConsoleMessage)) {
+ return;
+ }
+
+ info("Console Listener: " + what);
+ for (let i = expected.length - 1; i >= 0; --i) {
+ const e = expected[i];
+
+ if (what.message.includes(e.match)) {
+ ok(true, "Message received: " + e.match);
+ expected.splice(i, 1);
+ e.resolve();
+ }
+ }
+ },
+ };
+
+ Services.console.registerListener(consoleListener);
+
+ registerCleanupFunction(() =>
+ Services.console.unregisterListener(consoleListener)
+ );
+
+ const netPromises = [
+ new Promise(resolve => {
+ expected.push({
+ resolve,
+ match:
+ "Cookie “a” has been rejected because it is foreign and does not have the “Partitioned“ attribute.",
+ });
+ }),
+ ];
+
+ // Let's open our tab.
+ const tab = BrowserTestUtils.addTab(gBrowser, TOP_PAGE);
+ gBrowser.selectedTab = tab;
+ const browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ // Set cookies with cross-site HTTP
+ await SpecialPowers.spawn(browser, [], async function () {
+ await content.fetch(
+ "https://example.org/browser/netwerk/cookie/test/browser/partitioned.sjs",
+ { credentials: "include" }
+ );
+ });
+
+ // Let's wait for the first set of console events.
+ await Promise.all(netPromises);
+
+ // Let's close the tab.
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Run the test with CHIPS enabled, ensuring the partitioned cookies require
+// secure context.
+add_task(async function partitionedAttrRequiresSecure() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["network.cookie.cookieBehavior.optInPartitioning", true]],
+ });
+
+ // Clear all cookies before testing.
+ Services.cookies.removeAll();
+
+ const expected = [];
+
+ const consoleListener = {
+ observe(what) {
+ if (!(what instanceof Ci.nsIConsoleMessage)) {
+ return;
+ }
+
+ info("Console Listener: " + what);
+ for (let i = expected.length - 1; i >= 0; --i) {
+ const e = expected[i];
+
+ if (what.message.includes(e.match)) {
+ ok(true, "Message received: " + e.match);
+ expected.splice(i, 1);
+ e.resolve();
+ }
+ }
+ },
+ };
+
+ Services.console.registerListener(consoleListener);
+
+ registerCleanupFunction(() =>
+ Services.console.unregisterListener(consoleListener)
+ );
+
+ const netPromises = [
+ new Promise(resolve => {
+ expected.push({
+ resolve,
+ match:
+ "Cookie “c” has been rejected because it has the “Partitioned” attribute but is missing the “secure” attribute.",
+ });
+ }),
+ ];
+
+ // Let's open our tab.
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TOP_PAGE);
+
+ // Set cookies with cross-site HTTP
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ await content.fetch(
+ "https://example.org/browser/netwerk/cookie/test/browser/partitioned.sjs?nosecure",
+ { credentials: "include" }
+ );
+ });
+
+ // Let's wait for the first set of console events.
+ await Promise.all(netPromises);
+
+ // Ensure no cookie is set.
+ is(Services.cookies.cookies.length, 0, "No cookie is set.");
+
+ // Let's close the tab.
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/netwerk/cookie/test/browser/browser_partitioned_telemetry.js b/netwerk/cookie/test/browser/browser_partitioned_telemetry.js
new file mode 100644
index 0000000000..f89bcdd189
--- /dev/null
+++ b/netwerk/cookie/test/browser/browser_partitioned_telemetry.js
@@ -0,0 +1,134 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const TEST_URL =
+ "https://example.com/browser/netwerk/cookie/test/browser/file_empty.html";
+
+async function validateTelemetryValues(
+ { setCookies, setForeigns, setPartitioneds, setForeignPartitioneds },
+ message
+) {
+ await Services.fog.testFlushAllChildren();
+ let setCookieTelemetry = Glean.networking.setCookie.testGetValue();
+ is(
+ setCookieTelemetry ?? undefined,
+ setCookies,
+ message + " - all set cookies"
+ );
+ let foreignTelemetry = Glean.networking.setCookieForeign.testGetValue();
+ is(
+ foreignTelemetry?.numerator,
+ setForeigns,
+ message + " - foreign set cookies"
+ );
+ is(
+ foreignTelemetry?.denominator,
+ setCookies,
+ message + " - foreign set cookies denominator"
+ );
+ let partitonedTelemetry =
+ Glean.networking.setCookiePartitioned.testGetValue();
+ is(
+ partitonedTelemetry?.numerator,
+ setPartitioneds,
+ message + " - partitioned set cookies"
+ );
+ is(
+ partitonedTelemetry?.denominator,
+ setCookies,
+ message + " - partitioned set cookies denominator"
+ );
+ let foreignPartitonedTelemetry =
+ Glean.networking.setCookieForeignPartitioned.testGetValue();
+ is(
+ foreignPartitonedTelemetry?.numerator,
+ setForeignPartitioneds,
+ message + " - foreign partitioned set cookies"
+ );
+ is(
+ foreignPartitonedTelemetry?.denominator,
+ setCookies,
+ message + " - foreign partitioned set cookies denominator"
+ );
+}
+
+add_task(async () => {
+ await Services.fog.testFlushAllChildren();
+ Services.fog.testResetFOG();
+ await validateTelemetryValues({}, "initially empty");
+
+ // open a browser window for the test
+ let tab = BrowserTestUtils.addTab(gBrowser, TEST_URL);
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ // Set cookies with Javascript
+ await SpecialPowers.spawn(browser, [], function () {
+ content.document.cookie = "a=1; Partitioned; SameSite=None; Secure";
+ content.document.cookie = "b=2; SameSite=None; Secure";
+ });
+ await validateTelemetryValues(
+ {
+ setCookies: 2,
+ setForeigns: 0,
+ setPartitioneds: 1,
+ setForeignPartitioneds: 0,
+ },
+ "javascript cookie"
+ );
+
+ // Set cookies with HTTP
+ await SpecialPowers.spawn(browser, [], async function () {
+ await content.fetch("partitioned.sjs");
+ });
+ await validateTelemetryValues(
+ {
+ setCookies: 4,
+ setForeigns: 0,
+ setPartitioneds: 2,
+ setForeignPartitioneds: 0,
+ },
+ "same site fetch"
+ );
+
+ // Set cookies with cross-site HTTP
+ await SpecialPowers.spawn(browser, [], async function () {
+ await content.fetch(
+ "https://example.org/browser/netwerk/cookie/test/browser/partitioned.sjs",
+ { credentials: "include" }
+ );
+ });
+ await validateTelemetryValues(
+ {
+ setCookies: 6,
+ setForeigns: 2,
+ setPartitioneds: 3,
+ setForeignPartitioneds: 1,
+ },
+ "foreign fetch"
+ );
+
+ // Set cookies with cross-site HTTP redirect
+ await SpecialPowers.spawn(browser, [], async function () {
+ await content.fetch(
+ encodeURI(
+ "https://example.org/browser/netwerk/cookie/test/browser/partitioned.sjs?redirect=https://example.com/browser/netwerk/cookie/test/browser/partitioned.sjs?nocookie"
+ ),
+ { credentials: "include" }
+ );
+ });
+
+ await validateTelemetryValues(
+ {
+ setCookies: 8,
+ setForeigns: 4,
+ setPartitioneds: 4,
+ setForeignPartitioneds: 2,
+ },
+ "foreign fetch redirect"
+ );
+
+ // remove the tab
+ gBrowser.removeTab(tab);
+});
diff --git a/netwerk/cookie/test/browser/browser_sameSiteConsole.js b/netwerk/cookie/test/browser/browser_sameSiteConsole.js
new file mode 100644
index 0000000000..84527296b2
--- /dev/null
+++ b/netwerk/cookie/test/browser/browser_sameSiteConsole.js
@@ -0,0 +1,133 @@
+"use strict";
+
+const SAMESITE_DOMAIN = "http://example.com/";
+const SAMESITE_PATH = "browser/netwerk/cookie/test/browser/";
+const SAMESITE_TOP_PAGE = SAMESITE_DOMAIN + SAMESITE_PATH + "sameSite.sjs";
+
+add_task(async _ => {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["network.cookie.sameSite.laxByDefault", true],
+ ["network.cookie.sameSite.noneRequiresSecure", true],
+ ],
+ });
+
+ const expected = [];
+
+ const consoleListener = {
+ observe(what) {
+ if (!(what instanceof Ci.nsIConsoleMessage)) {
+ return;
+ }
+
+ info("Console Listener: " + what);
+ for (let i = expected.length - 1; i >= 0; --i) {
+ const e = expected[i];
+
+ if (what.message.includes(e.match)) {
+ ok(true, "Message received: " + e.match);
+ expected.splice(i, 1);
+ e.resolve();
+ }
+ }
+ },
+ };
+
+ Services.console.registerListener(consoleListener);
+
+ registerCleanupFunction(() =>
+ Services.console.unregisterListener(consoleListener)
+ );
+
+ const netPromises = [
+ new Promise(resolve => {
+ expected.push({
+ resolve,
+ match:
+ "Cookie “a” has “SameSite” policy set to “Lax” because it is missing a “SameSite” attribute, and “SameSite=Lax” is the default value for this attribute.",
+ });
+ }),
+
+ new Promise(resolve => {
+ expected.push({
+ resolve,
+ match:
+ "Cookie “b” rejected because it has the “SameSite=None” attribute but is missing the “secure” attribute.",
+ });
+ }),
+
+ new Promise(resolve => {
+ expected.push({
+ resolve,
+ match:
+ "Invalid “SameSite“ value for cookie “c”. The supported values are: “Lax“, “Strict“, “None“.",
+ });
+ }),
+
+ new Promise(resolve => {
+ expected.push({
+ resolve,
+ match:
+ "Cookie “c” has “SameSite” policy set to “Lax” because it is missing a “SameSite” attribute, and “SameSite=Lax” is the default value for this attribute.",
+ });
+ }),
+ ];
+
+ // Let's open our tab.
+ const tab = BrowserTestUtils.addTab(gBrowser, SAMESITE_TOP_PAGE);
+ gBrowser.selectedTab = tab;
+
+ const browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ // Let's wait for the first set of console events.
+ await Promise.all(netPromises);
+
+ // the DOM list of events.
+ const domPromises = [
+ new Promise(resolve => {
+ expected.push({
+ resolve,
+ match:
+ "Cookie “d” has “SameSite” policy set to “Lax” because it is missing a “SameSite” attribute, and “SameSite=Lax” is the default value for this attribute.",
+ });
+ }),
+
+ new Promise(resolve => {
+ expected.push({
+ resolve,
+ match:
+ "Cookie “e” rejected because it has the “SameSite=None” attribute but is missing the “secure” attribute.",
+ });
+ }),
+
+ new Promise(resolve => {
+ expected.push({
+ resolve,
+ match:
+ "Invalid “SameSite“ value for cookie “f”. The supported values are: “Lax“, “Strict“, “None“.",
+ });
+ }),
+
+ new Promise(resolve => {
+ expected.push({
+ resolve,
+ match:
+ "Cookie “f” has “SameSite” policy set to “Lax” because it is missing a “SameSite” attribute, and “SameSite=Lax” is the default value for this attribute.",
+ });
+ }),
+ ];
+
+ // Let's use document.cookie
+ SpecialPowers.spawn(browser, [], () => {
+ content.document.cookie = "d=4";
+ content.document.cookie = "e=5; sameSite=none";
+ content.document.cookie = "f=6; sameSite=batmat";
+ });
+
+ // Let's wait for the dom events.
+ await Promise.all(domPromises);
+
+ // Let's close the tab.
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/netwerk/cookie/test/browser/browser_serviceWorker.js b/netwerk/cookie/test/browser/browser_serviceWorker.js
new file mode 100644
index 0000000000..2a5c963535
--- /dev/null
+++ b/netwerk/cookie/test/browser/browser_serviceWorker.js
@@ -0,0 +1,45 @@
+"use strict";
+
+CookiePolicyHelper.runTest("ServiceWorker", {
+ prefs: [
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.ipc.processCount", 1],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ],
+
+ cookieJarAccessAllowed: async w => {
+ await w.navigator.serviceWorker
+ .register("file_empty.js")
+ .then(
+ reg => {
+ ok(true, "ServiceWorker can be used!");
+ return reg;
+ },
+ _ => {
+ ok(false, "ServiceWorker cannot be used! " + _);
+ }
+ )
+ .then(
+ reg => reg.unregister(),
+ _ => {
+ ok(false, "unregister failed");
+ }
+ )
+ .catch(e => ok(false, "Promise rejected: " + e));
+ },
+
+ cookieJarAccessDenied: async w => {
+ await w.navigator.serviceWorker
+ .register("file_empty.js")
+ .then(
+ _ => {
+ ok(false, "ServiceWorker cannot be used!");
+ },
+ _ => {
+ ok(true, "ServiceWorker cannot be used!");
+ }
+ )
+ .catch(e => ok(false, "Promise rejected: " + e));
+ },
+});
diff --git a/netwerk/cookie/test/browser/browser_sharedWorker.js b/netwerk/cookie/test/browser/browser_sharedWorker.js
new file mode 100644
index 0000000000..88a8b3f0e7
--- /dev/null
+++ b/netwerk/cookie/test/browser/browser_sharedWorker.js
@@ -0,0 +1,18 @@
+"use strict";
+
+CookiePolicyHelper.runTest("SharedWorker", {
+ cookieJarAccessAllowed: async w => {
+ new w.SharedWorker("a.js", "foo");
+ ok(true, "SharedWorker is allowed");
+ },
+
+ cookieJarAccessDenied: async w => {
+ try {
+ new w.SharedWorker("a.js", "foo");
+ ok(false, "SharedWorker cannot be used!");
+ } catch (e) {
+ ok(true, "SharedWorker cannot be used!");
+ is(e.name, "SecurityError", "We want a security error message.");
+ }
+ },
+});
diff --git a/netwerk/cookie/test/browser/browser_storage.js b/netwerk/cookie/test/browser/browser_storage.js
new file mode 100644
index 0000000000..1e37b1a367
--- /dev/null
+++ b/netwerk/cookie/test/browser/browser_storage.js
@@ -0,0 +1,43 @@
+"use strict";
+
+CookiePolicyHelper.runTest("SessionStorage", {
+ cookieJarAccessAllowed: async w => {
+ try {
+ w.sessionStorage.foo = 42;
+ ok(true, "SessionStorage works");
+ } catch (e) {
+ ok(false, "SessionStorage works");
+ }
+ },
+
+ cookieJarAccessDenied: async w => {
+ try {
+ w.sessionStorage.foo = 42;
+ ok(false, "SessionStorage doesn't work");
+ } catch (e) {
+ ok(true, "SessionStorage doesn't work");
+ is(e.name, "SecurityError", "We want a security error message.");
+ }
+ },
+});
+
+CookiePolicyHelper.runTest("LocalStorage", {
+ cookieJarAccessAllowed: async w => {
+ try {
+ w.localStorage.foo = 42;
+ ok(true, "LocalStorage works");
+ } catch (e) {
+ ok(false, "LocalStorage works");
+ }
+ },
+
+ cookieJarAccessDenied: async w => {
+ try {
+ w.localStorage.foo = 42;
+ ok(false, "LocalStorage doesn't work");
+ } catch (e) {
+ ok(true, "LocalStorage doesn't work");
+ is(e.name, "SecurityError", "We want a security error message.");
+ }
+ },
+});
diff --git a/netwerk/cookie/test/browser/file_empty.html b/netwerk/cookie/test/browser/file_empty.html
new file mode 100644
index 0000000000..78b64149c4
--- /dev/null
+++ b/netwerk/cookie/test/browser/file_empty.html
@@ -0,0 +1,2 @@
+<html><body>
+</body></html>
diff --git a/netwerk/cookie/test/browser/file_empty.js b/netwerk/cookie/test/browser/file_empty.js
new file mode 100644
index 0000000000..3053583c76
--- /dev/null
+++ b/netwerk/cookie/test/browser/file_empty.js
@@ -0,0 +1 @@
+/* nothing here */
diff --git a/netwerk/cookie/test/browser/head.js b/netwerk/cookie/test/browser/head.js
new file mode 100644
index 0000000000..609ad683db
--- /dev/null
+++ b/netwerk/cookie/test/browser/head.js
@@ -0,0 +1,201 @@
+const { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+
+const BEHAVIOR_ACCEPT = Ci.nsICookieService.BEHAVIOR_ACCEPT;
+const BEHAVIOR_REJECT = Ci.nsICookieService.BEHAVIOR_REJECT;
+
+const PERM_DEFAULT = Ci.nsICookiePermission.ACCESS_DEFAULT;
+const PERM_ALLOW = Ci.nsICookiePermission.ACCESS_ALLOW;
+const PERM_DENY = Ci.nsICookiePermission.ACCESS_DENY;
+
+const TEST_DOMAIN = "https://example.com/";
+const TEST_PATH = "browser/netwerk/cookie/test/browser/";
+const TEST_TOP_PAGE = TEST_DOMAIN + TEST_PATH + "file_empty.html";
+
+// Helper to eval() provided cookieJarAccessAllowed and cookieJarAccessDenied
+// toString()ed optionally async function in freshly created tabs with
+// BEHAVIOR_ACCEPT and BEHAVIOR_REJECT configured, respectively, in a number of
+// permutations. This includes verifying that changing the permission while the
+// page is open still results in the state of the permission when the
+// document/global was created still applying. Code will execute in the
+// ContentTask.spawn frame-script context, use content to access the underlying
+// page.
+this.CookiePolicyHelper = {
+ runTest(testName, config) {
+ // Testing allowed to blocked by cookie behavior
+ this._createTest(
+ testName,
+ config.cookieJarAccessAllowed,
+ config.cookieJarAccessDenied,
+ config.prefs,
+ {
+ fromBehavior: BEHAVIOR_ACCEPT,
+ toBehavior: BEHAVIOR_REJECT,
+ fromPermission: PERM_DEFAULT,
+ toPermission: PERM_DEFAULT,
+ }
+ );
+
+ // Testing blocked to allowed by cookie behavior
+ this._createTest(
+ testName,
+ config.cookieJarAccessDenied,
+ config.cookieJarAccessAllowed,
+ config.prefs,
+ {
+ fromBehavior: BEHAVIOR_REJECT,
+ toBehavior: BEHAVIOR_ACCEPT,
+ fromPermission: PERM_DEFAULT,
+ toPermission: PERM_DEFAULT,
+ }
+ );
+
+ // Testing allowed to blocked by cookie permission
+ this._createTest(
+ testName,
+ config.cookieJarAccessAllowed,
+ config.cookieJarAccessDenied,
+ config.prefs,
+ {
+ fromBehavior: BEHAVIOR_REJECT,
+ toBehavior: BEHAVIOR_REJECT,
+ fromPermission: PERM_ALLOW,
+ toPermission: PERM_DEFAULT,
+ }
+ );
+
+ // Testing blocked to allowed by cookie permission
+ this._createTest(
+ testName,
+ config.cookieJarAccessDenied,
+ config.cookieJarAccessAllowed,
+ config.prefs,
+ {
+ fromBehavior: BEHAVIOR_ACCEPT,
+ toBehavior: BEHAVIOR_ACCEPT,
+ fromPermission: PERM_DENY,
+ toPermission: PERM_DEFAULT,
+ }
+ );
+ },
+
+ _createTest(testName, goodCb, badCb, prefs, config) {
+ add_task(async _ => {
+ info("Starting " + testName + ": " + config.toSource());
+
+ await SpecialPowers.flushPrefEnv();
+
+ if (prefs) {
+ await SpecialPowers.pushPrefEnv({ set: prefs });
+ }
+
+ // Let's set the first cookie pref.
+ PermissionTestUtils.add(TEST_DOMAIN, "cookie", config.fromPermission);
+ await SpecialPowers.pushPrefEnv({
+ set: [["network.cookie.cookieBehavior", config.fromBehavior]],
+ });
+
+ // Let's open a tab and load content.
+ let tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE);
+ gBrowser.selectedTab = tab;
+
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ // Let's create an iframe.
+ await SpecialPowers.spawn(
+ browser,
+ [{ url: TEST_TOP_PAGE }],
+ async obj => {
+ return new content.Promise(resolve => {
+ let ifr = content.document.createElement("iframe");
+ ifr.setAttribute("id", "iframe");
+ ifr.src = obj.url;
+ ifr.onload = () => resolve();
+ content.document.body.appendChild(ifr);
+ });
+ }
+ );
+
+ // Let's exec the "good" callback.
+ info(
+ "Executing the test after setting the cookie behavior to " +
+ config.fromBehavior +
+ " and permission to " +
+ config.fromPermission
+ );
+ await SpecialPowers.spawn(
+ browser,
+ [{ callback: goodCb.toString() }],
+ async obj => {
+ let runnableStr = `(() => {return (${obj.callback});})();`;
+ let runnable = eval(runnableStr); // eslint-disable-line no-eval
+ await runnable(content);
+
+ let ifr = content.document.getElementById("iframe");
+ await runnable(ifr.contentWindow);
+ }
+ );
+
+ // Now, let's change the cookie settings
+ PermissionTestUtils.add(TEST_DOMAIN, "cookie", config.toPermission);
+ await SpecialPowers.pushPrefEnv({
+ set: [["network.cookie.cookieBehavior", config.toBehavior]],
+ });
+
+ // We still want the good callback to succeed.
+ info(
+ "Executing the test after setting the cookie behavior to " +
+ config.toBehavior +
+ " and permission to " +
+ config.toPermission
+ );
+ await SpecialPowers.spawn(
+ browser,
+ [{ callback: goodCb.toString() }],
+ async obj => {
+ let runnableStr = `(() => {return (${obj.callback});})();`;
+ let runnable = eval(runnableStr); // eslint-disable-line no-eval
+ await runnable(content);
+
+ let ifr = content.document.getElementById("iframe");
+ await runnable(ifr.contentWindow);
+ }
+ );
+
+ // Let's close the tab.
+ BrowserTestUtils.removeTab(tab);
+
+ // Let's open a new tab and load content again.
+ tab = BrowserTestUtils.addTab(gBrowser, TEST_TOP_PAGE);
+ gBrowser.selectedTab = tab;
+
+ browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ // Let's exec the "bad" callback.
+ info("Executing the test in a new tab");
+ await SpecialPowers.spawn(
+ browser,
+ [{ callback: badCb.toString() }],
+ async obj => {
+ let runnableStr = `(() => {return (${obj.callback});})();`;
+ let runnable = eval(runnableStr); // eslint-disable-line no-eval
+ await runnable(content);
+ }
+ );
+
+ // Let's close the tab.
+ BrowserTestUtils.removeTab(tab);
+
+ // Cleanup.
+ await new Promise(resolve => {
+ Services.clearData.deleteData(
+ Ci.nsIClearDataService.CLEAR_ALL,
+ resolve
+ );
+ });
+ });
+ },
+};
diff --git a/netwerk/cookie/test/browser/oversize.sjs b/netwerk/cookie/test/browser/oversize.sjs
new file mode 100644
index 0000000000..dfe2f31645
--- /dev/null
+++ b/netwerk/cookie/test/browser/oversize.sjs
@@ -0,0 +1,17 @@
+function handleRequest(aRequest, aResponse) {
+ aResponse.setStatusLine(aRequest.httpVersion, 200);
+
+ const maxBytesPerCookie = 4096;
+ const maxBytesPerCookiePath = 1024;
+
+ aResponse.setHeader(
+ "Set-Cookie",
+ "a=" + Array(maxBytesPerCookie + 1).join("x"),
+ true
+ );
+ aResponse.setHeader(
+ "Set-Cookie",
+ "b=c; path=/" + Array(maxBytesPerCookiePath + 1).join("x"),
+ true
+ );
+}
diff --git a/netwerk/cookie/test/browser/partitioned.sjs b/netwerk/cookie/test/browser/partitioned.sjs
new file mode 100644
index 0000000000..5649b88f2f
--- /dev/null
+++ b/netwerk/cookie/test/browser/partitioned.sjs
@@ -0,0 +1,32 @@
+function handleRequest(aRequest, aResponse) {
+ if (aRequest.hasHeader("Origin")) {
+ let origin = aRequest.getHeader("Origin");
+ aResponse.setHeader("Access-Control-Allow-Origin", origin);
+ aResponse.setHeader("Access-Control-Allow-Credentials", "true");
+ }
+
+ var params = new URLSearchParams(aRequest.queryString);
+ if (params.has("redirect")) {
+ aResponse.setHeader("Location", params.get("redirect"));
+ aResponse.setStatusLine(aRequest.httpVersion, 302);
+ } else {
+ aResponse.setStatusLine(aRequest.httpVersion, 200);
+ }
+
+ if (params.has("nocookie")) {
+ return;
+ }
+
+ if (params.has("nosecure")) {
+ aResponse.setHeader("Set-Cookie", "c=3; Partitioned;", true);
+
+ return;
+ }
+
+ aResponse.setHeader("Set-Cookie", "a=1; SameSite=None; Secure", true);
+ aResponse.setHeader(
+ "Set-Cookie",
+ "b=2; Partitioned; SameSite=None; Secure",
+ true
+ );
+}
diff --git a/netwerk/cookie/test/browser/sameSite.sjs b/netwerk/cookie/test/browser/sameSite.sjs
new file mode 100644
index 0000000000..a19624d2cb
--- /dev/null
+++ b/netwerk/cookie/test/browser/sameSite.sjs
@@ -0,0 +1,7 @@
+function handleRequest(aRequest, aResponse) {
+ aResponse.setStatusLine(aRequest.httpVersion, 200);
+
+ aResponse.setHeader("Set-Cookie", "a=1", true);
+ aResponse.setHeader("Set-Cookie", "b=2; sameSite=none", true);
+ aResponse.setHeader("Set-Cookie", "c=3; sameSite=batman", true);
+}
diff --git a/netwerk/cookie/test/browser/server.sjs b/netwerk/cookie/test/browser/server.sjs
new file mode 100644
index 0000000000..86835914bb
--- /dev/null
+++ b/netwerk/cookie/test/browser/server.sjs
@@ -0,0 +1,9 @@
+function handleRequest(aRequest, aResponse) {
+ aResponse.setStatusLine(aRequest.httpVersion, 200);
+ if (aRequest.hasHeader("Cookie")) {
+ aResponse.write("cookie-present");
+ } else {
+ aResponse.setHeader("Set-Cookie", "foopy=1");
+ aResponse.write("cookie-not-present");
+ }
+}
diff --git a/netwerk/cookie/test/mochitest/cookie.sjs b/netwerk/cookie/test/mochitest/cookie.sjs
new file mode 100644
index 0000000000..75d4e638b4
--- /dev/null
+++ b/netwerk/cookie/test/mochitest/cookie.sjs
@@ -0,0 +1,166 @@
+function handleRequest(aRequest, aResponse) {
+ let parts = aRequest.queryString.split("&");
+ if (parts.includes("window")) {
+ aResponse.setStatusLine(aRequest.httpVersion, 200);
+ aResponse.setHeader("Content-Type", "text/html");
+ aResponse.setHeader("Clear-Site-Data", '"cache", "cookies", "storage"');
+ aResponse.write("<body><h1>Welcome</h1></body>");
+ return;
+ }
+
+ if (parts.includes("fetch")) {
+ setState(
+ "data",
+ JSON.stringify({ type: "fetch", hasCookie: aRequest.hasHeader("Cookie") })
+ );
+ aResponse.write("Hello world!");
+ return;
+ }
+
+ if (parts.includes("xhr")) {
+ setState(
+ "data",
+ JSON.stringify({ type: "xhr", hasCookie: aRequest.hasHeader("Cookie") })
+ );
+ aResponse.write("Hello world!");
+ return;
+ }
+
+ if (parts.includes("image")) {
+ setState(
+ "data",
+ JSON.stringify({ type: "image", hasCookie: aRequest.hasHeader("Cookie") })
+ );
+
+ // A 1x1 PNG image.
+ // Source: https://commons.wikimedia.org/wiki/File:1x1.png (Public Domain)
+ const IMAGE = atob(
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAA" +
+ "ACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII="
+ );
+
+ aResponse.setHeader("Content-Type", "image/png", false);
+ aResponse.write(IMAGE);
+ return;
+ }
+
+ if (parts.includes("script")) {
+ setState(
+ "data",
+ JSON.stringify({
+ type: "script",
+ hasCookie: aRequest.hasHeader("Cookie"),
+ })
+ );
+
+ aResponse.setHeader("Content-Type", "text/javascript", false);
+ aResponse.write("window.scriptLoaded();");
+ return;
+ }
+
+ if (parts.includes("worker")) {
+ setState(
+ "data",
+ JSON.stringify({
+ type: "worker",
+ hasCookie: aRequest.hasHeader("Cookie"),
+ })
+ );
+
+ function w() {
+ onmessage = e => {
+ if (e.data == "subworker") {
+ importScripts("cookie.sjs?subworker&" + Math.random());
+ postMessage(42);
+ return;
+ }
+
+ if (e.data == "fetch") {
+ fetch("cookie.sjs?fetch&" + Math.random())
+ .then(r => r.text())
+ .then(_ => postMessage(42));
+ return;
+ }
+
+ if (e.data == "xhr") {
+ let xhr = new XMLHttpRequest();
+ xhr.open("GET", "cookie.sjs?xhr&" + Math.random());
+ xhr.send();
+ xhr.onload = _ => postMessage(42);
+ }
+ };
+ postMessage(42);
+ }
+
+ aResponse.setHeader("Content-Type", "text/javascript", false);
+ aResponse.write(w.toString() + "; w();");
+ return;
+ }
+
+ if (parts.includes("subworker")) {
+ setState(
+ "data",
+ JSON.stringify({
+ type: "subworker",
+ hasCookie: aRequest.hasHeader("Cookie"),
+ })
+ );
+ aResponse.setHeader("Content-Type", "text/javascript", false);
+ aResponse.write("42");
+ return;
+ }
+
+ if (parts.includes("sharedworker")) {
+ setState(
+ "data",
+ JSON.stringify({
+ type: "sharedworker",
+ hasCookie: aRequest.hasHeader("Cookie"),
+ })
+ );
+
+ // This function is exported as a string.
+ /* eslint-disable no-undef */
+ function w() {
+ onconnect = e => {
+ e.ports[0].onmessage = evt => {
+ if (evt.data == "subworker") {
+ importScripts("cookie.sjs?subworker&" + Math.random());
+ e.ports[0].postMessage(42);
+ return;
+ }
+
+ if (evt.data == "fetch") {
+ fetch("cookie.sjs?fetch&" + Math.random())
+ .then(r => r.text())
+ .then(_ => e.ports[0].postMessage(42));
+ return;
+ }
+
+ if (evt.data == "xhr") {
+ let xhr = new XMLHttpRequest();
+ xhr.open("GET", "cookie.sjs?xhr&" + Math.random());
+ xhr.send();
+ xhr.onload = _ => e.ports[0].postMessage(42);
+ }
+ };
+ e.ports[0].postMessage(42);
+ };
+ }
+ /* eslint-enable no-undef */
+
+ aResponse.setHeader("Content-Type", "text/javascript", false);
+ aResponse.write(w.toString() + "; w();");
+ return;
+ }
+
+ if (parts.includes("last")) {
+ let data = getState("data");
+ setState("data", "");
+ aResponse.write(data);
+ return;
+ }
+
+ aResponse.setStatusLine(aRequest.httpVersion, 400);
+ aResponse.write("Invalid request");
+}
diff --git a/netwerk/cookie/test/mochitest/cookiesHelper.js b/netwerk/cookie/test/mochitest/cookiesHelper.js
new file mode 100644
index 0000000000..cbff91f2f2
--- /dev/null
+++ b/netwerk/cookie/test/mochitest/cookiesHelper.js
@@ -0,0 +1,63 @@
+const ALLOWED = 0;
+const BLOCKED = 1;
+
+async function cleanupData() {
+ await new Promise(resolve => {
+ const chromeScript = SpecialPowers.loadChromeScript(_ => {
+ /* eslint-env mozilla/chrome-script */
+ addMessageListener("go", __ => {
+ Services.clearData.deleteData(
+ Services.clearData.CLEAR_COOKIES |
+ Services.clearData.CLEAR_ALL_CACHES |
+ Services.clearData.CLEAR_DOM_STORAGES,
+ ___ => {
+ sendAsyncMessage("done");
+ }
+ );
+ });
+ });
+
+ chromeScript.addMessageListener("done", _ => {
+ chromeScript.destroy();
+ resolve();
+ });
+
+ chromeScript.sendAsyncMessage("go");
+ });
+}
+
+async function checkLastRequest(type, state) {
+ let json = await fetch("cookie.sjs?last&" + Math.random()).then(r =>
+ r.json()
+ );
+ is(json.type, type, "Type: " + type);
+ is(json.hasCookie, state == ALLOWED, "Fetch has cookies");
+}
+
+async function runTests(currentTest) {
+ await cleanupData();
+ await SpecialPowers.pushPrefEnv({
+ set: [["network.cookie.cookieBehavior", 2]],
+ });
+ let windowBlocked = window.open("cookie.sjs?window&" + Math.random());
+ await new Promise(resolve => {
+ windowBlocked.onload = resolve;
+ });
+ await currentTest(windowBlocked, BLOCKED);
+ windowBlocked.close();
+
+ await cleanupData();
+ await SpecialPowers.pushPrefEnv({
+ set: [["network.cookie.cookieBehavior", 1]],
+ });
+ let windowAllowed = window.open("cookie.sjs?window&" + Math.random());
+ await new Promise(resolve => {
+ windowAllowed.onload = resolve;
+ });
+ await currentTest(windowAllowed, ALLOWED);
+ windowAllowed.close();
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
diff --git a/netwerk/cookie/test/mochitest/empty.html b/netwerk/cookie/test/mochitest/empty.html
new file mode 100644
index 0000000000..cd161cc52d
--- /dev/null
+++ b/netwerk/cookie/test/mochitest/empty.html
@@ -0,0 +1 @@
+<h1>Nothing here</h1>
diff --git a/netwerk/cookie/test/mochitest/mochitest.toml b/netwerk/cookie/test/mochitest/mochitest.toml
new file mode 100644
index 0000000000..fdf26a7e94
--- /dev/null
+++ b/netwerk/cookie/test/mochitest/mochitest.toml
@@ -0,0 +1,27 @@
+[DEFAULT]
+scheme = "https"
+support-files = [
+ "cookie.sjs",
+ "cookiesHelper.js",
+]
+
+["test_document_cookie.html"]
+
+["test_document_cookie_notification.html"]
+
+["test_fetch.html"]
+
+["test_image.html"]
+
+["test_metaTag.html"]
+
+["test_script.html"]
+
+["test_sharedWorker.html"]
+
+["test_worker.html"]
+
+["test_xhr.html"]
+
+["test_xmlDocument.html"]
+support-files = ["empty.html"]
diff --git a/netwerk/cookie/test/mochitest/test_document_cookie.html b/netwerk/cookie/test/mochitest/test_document_cookie.html
new file mode 100644
index 0000000000..86e7c7f661
--- /dev/null
+++ b/netwerk/cookie/test/mochitest/test_document_cookie.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for document.cookie when the policy changes</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="cookiesHelper.js"></script>
+</head>
+<body>
+<script type="application/javascript">
+
+runTests(async (w, state) => {
+ is(w.document.cookie.length, 0, "No cookie to start!");
+ w.document.cookie = "name=value";
+ is(w.document.cookie.includes("name=value"), state == ALLOWED, "Some cookies for me");
+});
+
+</script>
+</body>
+</html>
diff --git a/netwerk/cookie/test/mochitest/test_document_cookie_notification.html b/netwerk/cookie/test/mochitest/test_document_cookie_notification.html
new file mode 100644
index 0000000000..b84b6ed045
--- /dev/null
+++ b/netwerk/cookie/test/mochitest/test_document_cookie_notification.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for document.cookie setter + notification</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="application/javascript">
+
+function Listener() {
+ SpecialPowers.addObserver(this, "document-set-cookie");
+}
+
+Listener.prototype = {
+ observe(aSubject, aTopic, aData) {
+ is(aTopic, "document-set-cookie", "Notification received");
+ ok(aData.startsWith("a="), "Right cookie received");
+
+ SpecialPowers.removeObserver(this, "document-set-cookie");
+ SimpleTest.finish();
+ }
+}
+
+const cl = new Listener();
+document.cookie = "a=" + Math.random();
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</body>
+</html>
diff --git a/netwerk/cookie/test/mochitest/test_fetch.html b/netwerk/cookie/test/mochitest/test_fetch.html
new file mode 100644
index 0000000000..315d0d7624
--- /dev/null
+++ b/netwerk/cookie/test/mochitest/test_fetch.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for cookies + fetch when the policy changes</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="cookiesHelper.js"></script>
+</head>
+<body>
+<script type="application/javascript">
+
+runTests(async (w, state) => {
+ w.document.cookie = "name=value";
+ await w.fetch("cookie.sjs?fetch&" + Math.random()).then(r => r.text());
+ await checkLastRequest("fetch", state);
+});
+
+</script>
+</body>
+</html>
diff --git a/netwerk/cookie/test/mochitest/test_image.html b/netwerk/cookie/test/mochitest/test_image.html
new file mode 100644
index 0000000000..4a49d64169
--- /dev/null
+++ b/netwerk/cookie/test/mochitest/test_image.html
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for cookies and image loading when the policy changes</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="cookiesHelper.js"></script>
+</head>
+<body>
+<script type="application/javascript">
+
+runTests(async (w, state) => {
+ w.document.cookie = "name=value";
+
+ let image = new w.Image();
+ image.src = "cookie.sjs?image&" + Math.random();
+ w.document.body.appendChild(image);
+ await new w.Promise(resolve => { image.onload = resolve; });
+ await checkLastRequest("image", state);
+});
+
+</script>
+</body>
+</html>
diff --git a/netwerk/cookie/test/mochitest/test_metaTag.html b/netwerk/cookie/test/mochitest/test_metaTag.html
new file mode 100644
index 0000000000..48d360d5b2
--- /dev/null
+++ b/netwerk/cookie/test/mochitest/test_metaTag.html
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for meta tag</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<script type="application/javascript">
+
+document.addEventListener("DOMContentLoaded", _ => {
+ try {
+ document.write('<meta content=a http-equiv="set-cookie">');
+ } catch (e) {}
+
+ ok(true, "No crash!");
+ SimpleTest.finish();
+});
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</body>
+</html>
diff --git a/netwerk/cookie/test/mochitest/test_script.html b/netwerk/cookie/test/mochitest/test_script.html
new file mode 100644
index 0000000000..9f4b9f846d
--- /dev/null
+++ b/netwerk/cookie/test/mochitest/test_script.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for cookies + script loading when the policy changes</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="cookiesHelper.js"></script>
+</head>
+<body>
+<script type="application/javascript">
+
+runTests(async (w, state) => {
+ w.document.cookie = "name=value";
+
+ let p = new w.Promise(resolve => { w.scriptLoaded = resolve; });
+ let script = document.createElement("script");
+ script.src = "cookie.sjs?script&" + Math.random();
+ w.document.body.appendChild(script);
+ await p;
+ await checkLastRequest("script", state);
+});
+
+</script>
+</body>
+</html>
diff --git a/netwerk/cookie/test/mochitest/test_sharedWorker.html b/netwerk/cookie/test/mochitest/test_sharedWorker.html
new file mode 100644
index 0000000000..c29bf86a88
--- /dev/null
+++ b/netwerk/cookie/test/mochitest/test_sharedWorker.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for cookies + SharedWorker loading when the policy changes</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="cookiesHelper.js"></script>
+</head>
+<body>
+<script type="application/javascript">
+
+runTests(async (w, state) => {
+ w.document.cookie = "name=value";
+
+ if (state == BLOCKED) {
+ try {
+ new w.SharedWorker("cookie.sjs?sharedworker&" + Math.random());
+ ok(false, "SharedWorker should not be allowed!");
+ } catch (ex) {
+ ok(true, "SharedWorker should not be allowed!");
+ }
+ return;
+ }
+
+ let p = new w.SharedWorker("cookie.sjs?sharedworker&" + Math.random());
+ await new w.Promise(resolve => { p.port.onmessage = resolve; });
+ await checkLastRequest("sharedworker", state);
+
+ await new w.Promise(resolve => {
+ p.port.postMessage("subworker");
+ p.port.onmessage = resolve;
+ });
+ await checkLastRequest("subworker", state);
+
+ await new w.Promise(resolve => {
+ p.port.postMessage("fetch");
+ p.port.onmessage = resolve;
+ });
+ await checkLastRequest("fetch", state);
+
+ await new w.Promise(resolve => {
+ p.port.postMessage("xhr");
+ p.port.onmessage = resolve;
+ });
+ await checkLastRequest("xhr", state);
+});
+
+</script>
+</body>
+</html>
diff --git a/netwerk/cookie/test/mochitest/test_worker.html b/netwerk/cookie/test/mochitest/test_worker.html
new file mode 100644
index 0000000000..37ab222bce
--- /dev/null
+++ b/netwerk/cookie/test/mochitest/test_worker.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for cookies + worker loading when the policy changes</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="cookiesHelper.js"></script>
+</head>
+<body>
+<script type="application/javascript">
+
+runTests(async (w, state) => {
+ w.document.cookie = "name=value";
+
+ let p = new w.Worker("cookie.sjs?worker&" + Math.random());
+ await new w.Promise(resolve => { p.onmessage = resolve; });
+ await checkLastRequest("worker", state);
+
+ await new w.Promise(resolve => { p.postMessage("subworker"); p.onmessage = resolve; });
+ await checkLastRequest("subworker", state);
+
+ await new w.Promise(resolve => { p.postMessage("fetch"); p.onmessage = resolve; });
+ await checkLastRequest("fetch", state);
+
+ await new w.Promise(resolve => { p.postMessage("xhr"); p.onmessage = resolve; });
+ await checkLastRequest("xhr", state);
+});
+
+</script>
+</body>
+</html>
diff --git a/netwerk/cookie/test/mochitest/test_xhr.html b/netwerk/cookie/test/mochitest/test_xhr.html
new file mode 100644
index 0000000000..d00b5690f8
--- /dev/null
+++ b/netwerk/cookie/test/mochitest/test_xhr.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for cookies + XHR when the policy changes</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="cookiesHelper.js"></script>
+</head>
+<body>
+<script type="application/javascript">
+
+runTests(async (w, state) => {
+ w.document.cookie = "name=value";
+ await new w.Promise(resolve => {
+ let xhr = new w.XMLHttpRequest();
+ xhr.open("GET", "cookie.sjs?xhr&" + Math.random());
+ xhr.send();
+ xhr.onload = resolve;
+ });
+ await checkLastRequest("xhr", state);
+});
+
+</script>
+</body>
+</html>
diff --git a/netwerk/cookie/test/mochitest/test_xmlDocument.html b/netwerk/cookie/test/mochitest/test_xmlDocument.html
new file mode 100644
index 0000000000..91417c98c4
--- /dev/null
+++ b/netwerk/cookie/test/mochitest/test_xmlDocument.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Document constructor</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<script type="application/javascript">
+
+let w;
+
+SpecialPowers.pushPrefEnv({set: [
+ ["dom.storage_access.enabled", true],
+ ["dom.storage_access.prompt.testing", true],
+ ["dom.storage_access.prompt.testing.allow", true],
+ ["dom.testing.sync-content-blocking-notifications", true],
+ ["network.cookie.cookieBehavior", 0],
+]}).then(_ => {
+ return new Promise(resolve => {
+ w = window.open("empty.html");
+ w.onload = resolve;
+ });
+}).then(_ => {
+ const doc = new w.Document();
+ return doc.requestStorageAccess().catch(__ => {});
+}).then(___ => {
+ w.close();
+ ok(true, "No crash!");
+ SimpleTest.finish();
+});
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</body>
+</html>
diff --git a/netwerk/cookie/test/unit/test_baseDomain_publicsuffix.js b/netwerk/cookie/test/unit/test_baseDomain_publicsuffix.js
new file mode 100644
index 0000000000..94f01b778e
--- /dev/null
+++ b/netwerk/cookie/test/unit/test_baseDomain_publicsuffix.js
@@ -0,0 +1,105 @@
+"use strict";
+
+add_task(async () => {
+ const HOST = "www.bbc.co.uk";
+ Assert.equal(
+ Services.eTLD.getBaseDomainFromHost(HOST),
+ "bbc.co.uk",
+ "Sanity check: HOST is an eTLD + 1 with subdomain"
+ );
+
+ const tests = [
+ {
+ // Correct baseDomain: eTLD + 1.
+ baseDomain: "bbc.co.uk",
+ name: "originally_bbc_co_uk",
+ },
+ {
+ // Incorrect baseDomain: Part of public suffix list.
+ baseDomain: "uk",
+ name: "originally_uk",
+ },
+ {
+ // Incorrect baseDomain: Part of public suffix list.
+ baseDomain: "co.uk",
+ name: "originally_co_uk",
+ },
+ {
+ // Incorrect baseDomain: eTLD + 2.
+ baseDomain: "www.bbc.co.uk",
+ name: "originally_www_bbc_co_uk",
+ },
+ ];
+
+ do_get_profile();
+
+ let dbFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ dbFile.append("cookies.sqlite");
+ let conn = Services.storage.openDatabase(dbFile);
+
+ conn.schemaVersion = 10;
+ conn.executeSimpleSQL("DROP TABLE IF EXISTS moz_cookies");
+ conn.executeSimpleSQL(
+ "CREATE TABLE moz_cookies (" +
+ "id INTEGER PRIMARY KEY, " +
+ "baseDomain TEXT, " +
+ "originAttributes TEXT NOT NULL DEFAULT '', " +
+ "name TEXT, " +
+ "value TEXT, " +
+ "host TEXT, " +
+ "path TEXT, " +
+ "expiry INTEGER, " +
+ "lastAccessed INTEGER, " +
+ "creationTime INTEGER, " +
+ "isSecure INTEGER, " +
+ "isHttpOnly INTEGER, " +
+ "inBrowserElement INTEGER DEFAULT 0, " +
+ "sameSite INTEGER DEFAULT 0, " +
+ "rawSameSite INTEGER DEFAULT 0, " +
+ "CONSTRAINT moz_uniqueid UNIQUE (name, host, path, originAttributes)" +
+ ")"
+ );
+
+ function addCookie(baseDomain, host, name) {
+ conn.executeSimpleSQL(
+ "INSERT INTO moz_cookies(" +
+ "baseDomain, host, name, value, path, expiry, " +
+ "lastAccessed, creationTime, isSecure, isHttpOnly) VALUES (" +
+ `'${baseDomain}', '${host}', '${name}', 'thevalue', '/', ` +
+ (Date.now() + 3600000) +
+ "," +
+ Date.now() +
+ "," +
+ Date.now() +
+ ", 1, 1)"
+ );
+ }
+
+ // Prepare the database.
+ for (let { baseDomain, name } of tests) {
+ addCookie(baseDomain, HOST, name);
+ }
+ // Domain cookies are not supported for IP addresses.
+ addCookie("127.0.0.1", ".127.0.0.1", "invalid_host");
+ conn.close();
+
+ let cs = Services.cookies;
+
+ // Count excludes the invalid_host cookie.
+ Assert.equal(cs.cookies.length, tests.length, "Expected number of cookies");
+
+ // Check whether the database has the expected value,
+ // despite the incorrect baseDomain.
+ for (let { name } of tests) {
+ Assert.ok(
+ cs.cookieExists(HOST, "/", name, {}),
+ "Should find cookie with name: " + name
+ );
+ }
+
+ Assert.equal(
+ cs.cookieExists("127.0.0.1", "/", "invalid_host", {}),
+ false,
+ "Should ignore database row with invalid host name"
+ );
+});
diff --git a/netwerk/cookie/test/unit/test_bug1155169.js b/netwerk/cookie/test/unit/test_bug1155169.js
new file mode 100644
index 0000000000..2bf8bd768d
--- /dev/null
+++ b/netwerk/cookie/test/unit/test_bug1155169.js
@@ -0,0 +1,96 @@
+const { NetUtil } = ChromeUtils.importESModule(
+ "resource://gre/modules/NetUtil.sys.mjs"
+);
+
+const URI = Services.io.newURI("http://example.org/");
+
+const { COOKIE_CHANGED, COOKIE_ADDED } = Ci.nsICookieNotification;
+
+function run_test() {
+ // Allow all cookies.
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+
+ // Clear cookies.
+ Services.cookies.removeAll();
+
+ // Add a new cookie.
+ setCookie("foo=bar", {
+ type: COOKIE_ADDED,
+ isSession: true,
+ isSecure: false,
+ isHttpOnly: false,
+ });
+
+ // Update cookie with isHttpOnly=true.
+ setCookie("foo=bar; HttpOnly", {
+ type: COOKIE_CHANGED,
+ isSession: true,
+ isSecure: false,
+ isHttpOnly: true,
+ });
+
+ // Update cookie with isSecure=true.
+ setCookie("foo=bar; Secure", {
+ type: COOKIE_CHANGED,
+ isSession: true,
+ isSecure: true,
+ isHttpOnly: false,
+ });
+
+ // Update cookie with isSession=false.
+ let expiry = new Date();
+ expiry.setUTCFullYear(expiry.getUTCFullYear() + 2);
+ setCookie(`foo=bar; Expires=${expiry.toGMTString()}`, {
+ type: COOKIE_CHANGED,
+ isSession: false,
+ isSecure: false,
+ isHttpOnly: false,
+ });
+
+ // Reset cookie.
+ setCookie("foo=bar", {
+ type: COOKIE_CHANGED,
+ isSession: true,
+ isSecure: false,
+ isHttpOnly: false,
+ });
+}
+
+function setCookie(value, expected) {
+ function setCookieInternal(valueInternal, expectedInternal = null) {
+ function observer(subject) {
+ if (!expectedInternal) {
+ do_throw("no notification expected");
+ return;
+ }
+
+ let notification = subject.QueryInterface(Ci.nsICookieNotification);
+
+ // Check we saw the right notification.
+ Assert.equal(notification.action, expectedInternal.type);
+
+ // Check cookie details.
+ let cookie = notification.cookie.QueryInterface(Ci.nsICookie);
+ Assert.equal(cookie.isSession, expectedInternal.isSession);
+ Assert.equal(cookie.isSecure, expectedInternal.isSecure);
+ Assert.equal(cookie.isHttpOnly, expectedInternal.isHttpOnly);
+ }
+
+ Services.obs.addObserver(observer, "cookie-changed");
+
+ let channel = NetUtil.newChannel({
+ uri: URI,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ });
+
+ Services.cookies.setCookieStringFromHttp(URI, valueInternal, channel);
+ Services.obs.removeObserver(observer, "cookie-changed");
+ }
+
+ // Check that updating/inserting the cookie works.
+ setCookieInternal(value, expected);
+
+ // Check that we ignore identical cookies.
+ setCookieInternal(value);
+}
diff --git a/netwerk/cookie/test/unit/test_bug1321912.js b/netwerk/cookie/test/unit/test_bug1321912.js
new file mode 100644
index 0000000000..fd24f15bbf
--- /dev/null
+++ b/netwerk/cookie/test/unit/test_bug1321912.js
@@ -0,0 +1,99 @@
+do_get_profile();
+const dirSvc = Services.dirsvc;
+
+let dbFile = dirSvc.get("ProfD", Ci.nsIFile);
+dbFile.append("cookies.sqlite");
+
+let storage = Services.storage;
+let properties = Cc["@mozilla.org/hash-property-bag;1"].createInstance(
+ Ci.nsIWritablePropertyBag
+);
+properties.setProperty("shared", true);
+let conn = storage.openDatabase(dbFile);
+
+// Write the schema v7 to the database.
+conn.schemaVersion = 7;
+conn.executeSimpleSQL(
+ "CREATE TABLE moz_cookies (" +
+ "id INTEGER PRIMARY KEY, " +
+ "baseDomain TEXT, " +
+ "originAttributes TEXT NOT NULL DEFAULT '', " +
+ "name TEXT, " +
+ "value TEXT, " +
+ "host TEXT, " +
+ "path TEXT, " +
+ "expiry INTEGER, " +
+ "lastAccessed INTEGER, " +
+ "creationTime INTEGER, " +
+ "isSecure INTEGER, " +
+ "isHttpOnly INTEGER, " +
+ "appId INTEGER DEFAULT 0, " +
+ "inBrowserElement INTEGER DEFAULT 0, " +
+ "CONSTRAINT moz_uniqueid UNIQUE (name, host, path, originAttributes)" +
+ ")"
+);
+conn.executeSimpleSQL(
+ "CREATE INDEX moz_basedomain ON moz_cookies (baseDomain, " +
+ "originAttributes)"
+);
+
+conn.executeSimpleSQL("PRAGMA synchronous = OFF");
+conn.executeSimpleSQL("PRAGMA journal_mode = WAL");
+conn.executeSimpleSQL("PRAGMA wal_autocheckpoint = 16");
+
+let now = Date.now();
+conn.executeSimpleSQL(
+ "INSERT INTO moz_cookies(" +
+ "baseDomain, host, name, value, path, expiry, " +
+ "lastAccessed, creationTime, isSecure, isHttpOnly) VALUES (" +
+ "'foo.com', '.foo.com', 'foo', 'bar=baz', '/', " +
+ now +
+ ", " +
+ now +
+ ", " +
+ now +
+ ", 1, 1)"
+);
+
+// Now start the cookie service, and then check the fields in the table.
+// Get sessionCookies to wait for the initialization in cookie thread
+Services.cookies.sessionCookies;
+
+Assert.equal(conn.schemaVersion, 13);
+let stmt = conn.createStatement(
+ "SELECT sql FROM sqlite_master " +
+ "WHERE type = 'table' AND " +
+ " name = 'moz_cookies'"
+);
+try {
+ Assert.ok(stmt.executeStep());
+ let sql = stmt.getString(0);
+ Assert.equal(sql.indexOf("appId"), -1);
+} finally {
+ stmt.finalize();
+}
+
+stmt = conn.createStatement(
+ "SELECT * FROM moz_cookies " +
+ "WHERE host = '.foo.com' AND " +
+ " name = 'foo' AND " +
+ " value = 'bar=baz' AND " +
+ " path = '/' AND " +
+ " expiry = " +
+ now +
+ " AND " +
+ " lastAccessed = " +
+ now +
+ " AND " +
+ " creationTime = " +
+ now +
+ " AND " +
+ " isSecure = 1 AND " +
+ " isHttpOnly = 1"
+);
+try {
+ Assert.ok(stmt.executeStep());
+} finally {
+ stmt.finalize();
+}
+conn.close();
diff --git a/netwerk/cookie/test/unit/test_bug643051.js b/netwerk/cookie/test/unit/test_bug643051.js
new file mode 100644
index 0000000000..35b37a5889
--- /dev/null
+++ b/netwerk/cookie/test/unit/test_bug643051.js
@@ -0,0 +1,43 @@
+const { NetUtil } = ChromeUtils.importESModule(
+ "resource://gre/modules/NetUtil.sys.mjs"
+);
+const { CookieXPCShellUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/CookieXPCShellUtils.sys.mjs"
+);
+
+CookieXPCShellUtils.init(this);
+CookieXPCShellUtils.createServer({ hosts: ["example.net"] });
+
+add_task(async () => {
+ Services.prefs.setBoolPref("dom.security.https_first", false);
+
+ // Allow all cookies.
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+ Services.prefs.setBoolPref(
+ "network.cookieJarSettings.unblocked_for_testing",
+ true
+ );
+
+ let uri = NetUtil.newURI("http://example.org/");
+ let channel = NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ });
+
+ let set = "foo=bar\nbaz=foo";
+ let expected = "foo=bar; baz=foo";
+ Services.cookies.setCookieStringFromHttp(uri, set, channel);
+
+ let actual = Services.cookies.getCookieStringFromHttp(uri, channel);
+ Assert.equal(actual, expected);
+
+ await CookieXPCShellUtils.setCookieToDocument("http://example.net/", set);
+ actual = await CookieXPCShellUtils.getCookieStringFromDocument(
+ "http://example.net/"
+ );
+
+ expected = "foo=bar";
+ Assert.equal(actual, expected);
+ Services.prefs.clearUserPref("dom.security.https_first");
+});
diff --git a/netwerk/cookie/test/unit/test_eviction.js b/netwerk/cookie/test/unit/test_eviction.js
new file mode 100644
index 0000000000..8c0073a107
--- /dev/null
+++ b/netwerk/cookie/test/unit/test_eviction.js
@@ -0,0 +1,199 @@
+const { NetUtil } = ChromeUtils.importESModule(
+ "resource://gre/modules/NetUtil.sys.mjs"
+);
+
+const BASE_HOST = "example.org";
+
+const { CookieXPCShellUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/CookieXPCShellUtils.sys.mjs"
+);
+
+CookieXPCShellUtils.init(this);
+CookieXPCShellUtils.createServer({ hosts: ["example.org"] });
+
+add_task(async function test_basic_eviction() {
+ do_get_profile();
+
+ Services.prefs.setIntPref("network.cookie.staleThreshold", 0);
+ Services.prefs.setIntPref("network.cookie.quotaPerHost", 2);
+ Services.prefs.setIntPref("network.cookie.maxPerHost", 5);
+
+ // We don't want to have CookieJarSettings blocking this test.
+ Services.prefs.setBoolPref(
+ "network.cookieJarSettings.unblocked_for_testing",
+ true
+ );
+
+ const BASE_URI = Services.io.newURI("http://" + BASE_HOST);
+ const FOO_PATH = Services.io.newURI("http://" + BASE_HOST + "/foo/");
+ const BAR_PATH = Services.io.newURI("http://" + BASE_HOST + "/bar/");
+
+ await setCookie("session_foo_path_1", null, "/foo", null, FOO_PATH);
+ await setCookie("session_foo_path_2", null, "/foo", null, FOO_PATH);
+ await setCookie("session_foo_path_3", null, "/foo", null, FOO_PATH);
+ await setCookie("session_foo_path_4", null, "/foo", null, FOO_PATH);
+ await setCookie("session_foo_path_5", null, "/foo", null, FOO_PATH);
+ verifyCookies(
+ [
+ "session_foo_path_1",
+ "session_foo_path_2",
+ "session_foo_path_3",
+ "session_foo_path_4",
+ "session_foo_path_5",
+ ],
+ BASE_URI
+ );
+
+ // Check if cookies are evicted by creation time.
+ await setCookie("session_foo_path_6", null, "/foo", null, FOO_PATH);
+ verifyCookies(
+ ["session_foo_path_4", "session_foo_path_5", "session_foo_path_6"],
+ BASE_URI
+ );
+
+ await setCookie("session_bar_path_1", null, "/bar", null, BAR_PATH);
+ await setCookie("session_bar_path_2", null, "/bar", null, BAR_PATH);
+
+ verifyCookies(
+ [
+ "session_foo_path_4",
+ "session_foo_path_5",
+ "session_foo_path_6",
+ "session_bar_path_1",
+ "session_bar_path_2",
+ ],
+ BASE_URI
+ );
+
+ // Check if cookies are evicted by last accessed time.
+ await CookieXPCShellUtils.getCookieStringFromDocument(FOO_PATH.spec);
+
+ await setCookie("session_foo_path_7", null, "/foo", null, FOO_PATH);
+ verifyCookies(
+ ["session_foo_path_5", "session_foo_path_6", "session_foo_path_7"],
+ BASE_URI
+ );
+
+ const EXPIRED_TIME = 3;
+
+ await setCookie(
+ "non_session_expired_foo_path_1",
+ null,
+ "/foo",
+ EXPIRED_TIME,
+ FOO_PATH
+ );
+ await setCookie(
+ "non_session_expired_foo_path_2",
+ null,
+ "/foo",
+ EXPIRED_TIME,
+ FOO_PATH
+ );
+ verifyCookies(
+ [
+ "session_foo_path_5",
+ "session_foo_path_6",
+ "session_foo_path_7",
+ "non_session_expired_foo_path_1",
+ "non_session_expired_foo_path_2",
+ ],
+ BASE_URI
+ );
+
+ // Check if expired cookies are evicted first.
+ await new Promise(resolve => do_timeout(EXPIRED_TIME * 1000, resolve));
+ await setCookie("session_foo_path_8", null, "/foo", null, FOO_PATH);
+ verifyCookies(
+ ["session_foo_path_6", "session_foo_path_7", "session_foo_path_8"],
+ BASE_URI
+ );
+
+ Services.cookies.removeAll();
+});
+
+// Verify that the given cookie names exist, and are ordered from least to most recently accessed
+function verifyCookies(names, uri) {
+ Assert.equal(Services.cookies.countCookiesFromHost(uri.host), names.length);
+ let actual_cookies = [];
+ for (let cookie of Services.cookies.getCookiesFromHost(uri.host, {})) {
+ actual_cookies.push(cookie);
+ }
+ if (names.length != actual_cookies.length) {
+ let left = names.filter(function (n) {
+ return (
+ actual_cookies.findIndex(function (c) {
+ return c.name == n;
+ }) == -1
+ );
+ });
+ let right = actual_cookies
+ .filter(function (c) {
+ return (
+ names.findIndex(function (n) {
+ return c.name == n;
+ }) == -1
+ );
+ })
+ .map(function (c) {
+ return c.name;
+ });
+ if (left.length) {
+ info("unexpected cookies: " + left);
+ }
+ if (right.length) {
+ info("expected cookies: " + right);
+ }
+ }
+ Assert.equal(names.length, actual_cookies.length);
+ actual_cookies.sort(function (a, b) {
+ if (a.lastAccessed < b.lastAccessed) {
+ return -1;
+ }
+ if (a.lastAccessed > b.lastAccessed) {
+ return 1;
+ }
+ return 0;
+ });
+ for (var i = 0; i < names.length; i++) {
+ Assert.equal(names[i], actual_cookies[i].name);
+ Assert.equal(names[i].startsWith("session"), actual_cookies[i].isSession);
+ }
+}
+
+var lastValue = 0;
+function setCookie(name, domain, path, maxAge, url) {
+ let value = name + "=" + ++lastValue;
+ var s = "setting cookie " + value;
+ if (domain) {
+ value += "; Domain=" + domain;
+ s += " (d=" + domain + ")";
+ }
+ if (path) {
+ value += "; Path=" + path;
+ s += " (p=" + path + ")";
+ }
+ if (maxAge) {
+ value += "; Max-Age=" + maxAge;
+ s += " (non-session)";
+ } else {
+ s += " (session)";
+ }
+ s += " for " + url.spec;
+ info(s);
+
+ let channel = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ });
+
+ Services.cookies.setCookieStringFromHttp(url, value, channel);
+
+ return new Promise(function (resolve) {
+ // Windows XP has low precision timestamps that cause our cookie eviction
+ // algorithm to produce different results from other platforms. We work around
+ // this by ensuring that there's a clear gap between each cookie update.
+ do_timeout(10, resolve);
+ });
+}
diff --git a/netwerk/cookie/test/unit/test_getCookieSince.js b/netwerk/cookie/test/unit/test_getCookieSince.js
new file mode 100644
index 0000000000..e58624b6a1
--- /dev/null
+++ b/netwerk/cookie/test/unit/test_getCookieSince.js
@@ -0,0 +1,72 @@
+const { NetUtil } = ChromeUtils.importESModule(
+ "resource://gre/modules/NetUtil.sys.mjs"
+);
+
+function setCookie(name, url) {
+ let value = `${name}=${Math.random()}; Path=/; Max-Age=1000; sameSite=none; Secure`;
+ info(`Setting cookie ${value} for ${url.spec}`);
+
+ let channel = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ });
+
+ Services.cookies.setCookieStringFromHttp(url, value, channel);
+}
+
+async function sleep() {
+ await new Promise(resolve => do_timeout(1000, resolve));
+}
+
+function checkSorting(cookies) {
+ for (let i = 1; i < cookies.length; ++i) {
+ Assert.greater(
+ cookies[i].creationTime,
+ cookies[i - 1].creationTime,
+ "Cookie " + cookies[i].name
+ );
+ }
+}
+
+add_task(async function () {
+ Services.prefs.setBoolPref(
+ "network.cookieJarSettings.unblocked_for_testing",
+ true
+ );
+
+ await setCookie("A", Services.io.newURI("https://example.com/A/"));
+ await sleep();
+
+ await setCookie("B", Services.io.newURI("https://foo.bar/B/"));
+ await sleep();
+
+ await setCookie("C", Services.io.newURI("https://example.org/C/"));
+ await sleep();
+
+ await setCookie("D", Services.io.newURI("https://example.com/D/"));
+ await sleep();
+
+ Assert.equal(Services.cookies.cookies.length, 4, "Cookie check");
+
+ const cookies = Services.cookies.getCookiesSince(0);
+ Assert.equal(cookies.length, 4, "We retrieve all the 4 cookies");
+ checkSorting(cookies);
+
+ let someCookies = Services.cookies.getCookiesSince(
+ cookies[0].creationTime + 1
+ );
+ Assert.equal(someCookies.length, 3, "We retrieve some cookies");
+ checkSorting(someCookies);
+
+ someCookies = Services.cookies.getCookiesSince(cookies[1].creationTime + 1);
+ Assert.equal(someCookies.length, 2, "We retrieve some cookies");
+ checkSorting(someCookies);
+
+ someCookies = Services.cookies.getCookiesSince(cookies[2].creationTime + 1);
+ Assert.equal(someCookies.length, 1, "We retrieve some cookies");
+ checkSorting(someCookies);
+
+ someCookies = Services.cookies.getCookiesSince(cookies[3].creationTime + 1);
+ Assert.equal(someCookies.length, 0, "We retrieve some cookies");
+});
diff --git a/netwerk/cookie/test/unit/test_migrateCookieLifetimePref.js b/netwerk/cookie/test/unit/test_migrateCookieLifetimePref.js
new file mode 100644
index 0000000000..088a909709
--- /dev/null
+++ b/netwerk/cookie/test/unit/test_migrateCookieLifetimePref.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ Tests that
+ - the migration code runs,
+ - the sanitize on shutdown prefs for profiles with the network.cookie.lifetimePolicy enabled are set to true,
+ - the previous settings for clearOnShutdown prefs will not be applied due to sanitizeOnShutdown being disabled
+ - the network.cookie.lifetimePolicy is disabled afterwards.
+*/
+add_task(async function migrateSanitizationPrefsClearCleaningPrefs() {
+ // Former network.cookie.lifetimePolicy values ACCEPT_SESSION/ACCEPT_NORMALLY are not available anymore
+ // 2 = ACCEPT_SESSION
+ Services.prefs.setIntPref("network.cookie.lifetimePolicy", 2);
+ Services.prefs.setBoolPref("privacy.sanitize.sanitizeOnShutdown", false);
+ Services.prefs.setBoolPref("privacy.clearOnShutdown.cache", false);
+ Services.prefs.setBoolPref("privacy.clearOnShutdown.cookies", false);
+ Services.prefs.setBoolPref("privacy.clearOnShutdown.offlineApps", false);
+ Services.prefs.setBoolPref("privacy.clearOnShutdown.downloads", true);
+ Services.prefs.setBoolPref("privacy.clearOnShutdown.sessions", true);
+
+ // The migration code is called in cookieService::Init
+ Services.cookies;
+
+ // Former network.cookie.lifetimePolicy values ACCEPT_SESSION/ACCEPT_NORMALLY are not available anymore
+ // 0 = ACCEPT_NORMALLY
+ Assert.equal(
+ Services.prefs.getIntPref("network.cookie.lifetimePolicy", 0),
+ 0,
+ "Cookie lifetime policy is off"
+ );
+
+ Assert.ok(
+ Services.prefs.getBoolPref("privacy.sanitize.sanitizeOnShutdown"),
+ "Sanitize on shutdown is set"
+ );
+
+ Assert.ok(
+ Services.prefs.getBoolPref("privacy.clearOnShutdown.cookies"),
+ "Clearing cookies on shutdown is selected"
+ );
+
+ Assert.ok(
+ Services.prefs.getBoolPref("privacy.clearOnShutdown.cache"),
+ "Clearing cache on shutdown is still selected"
+ );
+
+ Assert.ok(
+ Services.prefs.getBoolPref("privacy.clearOnShutdown.offlineApps"),
+ "Clearing offline apps on shutdown is selected"
+ );
+
+ Assert.ok(
+ !Services.prefs.getBoolPref("privacy.clearOnShutdown.downloads"),
+ "Clearing downloads on shutdown is not set anymore"
+ );
+ Assert.ok(
+ !Services.prefs.getBoolPref("privacy.clearOnShutdown.sessions"),
+ "Clearing active logins on shutdown is not set anymore"
+ );
+
+ Services.prefs.resetPrefs();
+
+ delete Services.cookies;
+});
diff --git a/netwerk/cookie/test/unit/test_parser_0001.js b/netwerk/cookie/test/unit/test_parser_0001.js
new file mode 100644
index 0000000000..acc2e919ef
--- /dev/null
+++ b/netwerk/cookie/test/unit/test_parser_0001.js
@@ -0,0 +1,32 @@
+const { NetUtil } = ChromeUtils.importESModule(
+ "resource://gre/modules/NetUtil.sys.mjs"
+);
+
+function inChildProcess() {
+ return Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
+}
+
+function run_test() {
+ // Allow all cookies if the pref service is available in this process.
+ if (!inChildProcess()) {
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+ Services.prefs.setBoolPref(
+ "network.cookieJarSettings.unblocked_for_testing",
+ true
+ );
+ }
+
+ let uri = NetUtil.newURI("http://example.org/");
+ let channel = NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ });
+
+ let set = "foo=bar";
+ Services.cookies.setCookieStringFromHttp(uri, set, channel);
+
+ let expected = "foo=bar";
+ let actual = Services.cookies.getCookieStringFromHttp(uri, channel);
+ Assert.equal(actual, expected);
+}
diff --git a/netwerk/cookie/test/unit/test_parser_0019.js b/netwerk/cookie/test/unit/test_parser_0019.js
new file mode 100644
index 0000000000..7ba0d4ef79
--- /dev/null
+++ b/netwerk/cookie/test/unit/test_parser_0019.js
@@ -0,0 +1,32 @@
+const { NetUtil } = ChromeUtils.importESModule(
+ "resource://gre/modules/NetUtil.sys.mjs"
+);
+
+function inChildProcess() {
+ return Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
+}
+
+function run_test() {
+ // Allow all cookies if the pref service is available in this process.
+ if (!inChildProcess()) {
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+ Services.prefs.setBoolPref(
+ "network.cookieJarSettings.unblocked_for_testing",
+ true
+ );
+ }
+
+ let uri = NetUtil.newURI("http://example.org/");
+ let channel = NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ });
+
+ let set = "foo=b;max-age=3600, c=d;path=/";
+ Services.cookies.setCookieStringFromHttp(uri, set, channel);
+
+ let expected = "foo=b";
+ let actual = Services.cookies.getCookieStringFromHttp(uri, channel);
+ Assert.equal(actual, expected);
+}
diff --git a/netwerk/cookie/test/unit/test_rawSameSite.js b/netwerk/cookie/test/unit/test_rawSameSite.js
new file mode 100644
index 0000000000..dc739ef852
--- /dev/null
+++ b/netwerk/cookie/test/unit/test_rawSameSite.js
@@ -0,0 +1,125 @@
+const { NetUtil } = ChromeUtils.importESModule(
+ "resource://gre/modules/NetUtil.sys.mjs"
+);
+
+function inChildProcess() {
+ return Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
+}
+
+add_task(async _ => {
+ do_get_profile();
+
+ let dbFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ dbFile.append("cookies.sqlite");
+
+ let storage = Services.storage;
+ let properties = Cc["@mozilla.org/hash-property-bag;1"].createInstance(
+ Ci.nsIWritablePropertyBag
+ );
+ properties.setProperty("shared", true);
+ let conn = storage.openDatabase(dbFile);
+
+ conn.schemaVersion = 9;
+ conn.executeSimpleSQL("DROP TABLE IF EXISTS moz_cookies");
+ conn.executeSimpleSQL(
+ "CREATE TABLE moz_cookies (" +
+ "id INTEGER PRIMARY KEY, " +
+ "baseDomain TEXT, " +
+ "originAttributes TEXT NOT NULL DEFAULT '', " +
+ "name TEXT, " +
+ "value TEXT, " +
+ "host TEXT, " +
+ "path TEXT, " +
+ "expiry INTEGER, " +
+ "lastAccessed INTEGER, " +
+ "creationTime INTEGER, " +
+ "isSecure INTEGER, " +
+ "isHttpOnly INTEGER, " +
+ "inBrowserElement INTEGER DEFAULT 0, " +
+ "sameSite INTEGER DEFAULT 0, " +
+ "CONSTRAINT moz_uniqueid UNIQUE (name, host, path, originAttributes)" +
+ ")"
+ );
+ conn.close();
+
+ // Allow all cookies if the pref service is available in this process.
+ if (!inChildProcess()) {
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+ Services.prefs.setBoolPref(
+ "network.cookieJarSettings.unblocked_for_testing",
+ true
+ );
+ Services.prefs.setBoolPref("network.cookie.sameSite.laxByDefault", true);
+ Services.prefs.setBoolPref(
+ "network.cookie.sameSite.noneRequiresSecure",
+ true
+ );
+ }
+
+ let uri = NetUtil.newURI("http://example.org/");
+
+ let principal = Services.scriptSecurityManager.createContentPrincipal(
+ uri,
+ {}
+ );
+
+ let channel = NetUtil.newChannel({
+ uri,
+ loadingPrincipal: principal,
+ securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
+ });
+
+ let tests = [
+ {
+ cookie: "foo=b;max-age=3600, c=d;path=/; sameSite=strict",
+ sameSite: 2,
+ rawSameSite: 2,
+ },
+ {
+ cookie: "foo=b;max-age=3600, c=d;path=/; sameSite=lax",
+ sameSite: 1,
+ rawSameSite: 1,
+ },
+ { cookie: "foo=b;max-age=3600, c=d;path=/", sameSite: 1, rawSameSite: 0 },
+ ];
+
+ for (let i = 0; i < tests.length; ++i) {
+ let test = tests[i];
+
+ let promise = new Promise(resolve => {
+ function observer(subject, topic, data) {
+ Services.obs.removeObserver(observer, "cookie-saved-on-disk");
+ resolve();
+ }
+
+ Services.obs.addObserver(observer, "cookie-saved-on-disk");
+ });
+
+ Services.cookies.setCookieStringFromHttp(uri, test.cookie, channel);
+
+ await promise;
+
+ conn = storage.openDatabase(dbFile);
+ Assert.equal(conn.schemaVersion, 13);
+
+ let stmt = conn.createStatement(
+ "SELECT sameSite, rawSameSite FROM moz_cookies"
+ );
+
+ let success = stmt.executeStep();
+ Assert.ok(success);
+
+ let sameSite = stmt.getInt32(0);
+ let rawSameSite = stmt.getInt32(1);
+ stmt.finalize();
+
+ Assert.equal(sameSite, test.sameSite);
+ Assert.equal(rawSameSite, test.rawSameSite);
+
+ Services.cookies.removeAll();
+
+ stmt.finalize();
+ conn.close();
+ }
+});
diff --git a/netwerk/cookie/test/unit/test_schemeMap.js b/netwerk/cookie/test/unit/test_schemeMap.js
new file mode 100644
index 0000000000..041c24033a
--- /dev/null
+++ b/netwerk/cookie/test/unit/test_schemeMap.js
@@ -0,0 +1,216 @@
+const { NetUtil } = ChromeUtils.importESModule(
+ "resource://gre/modules/NetUtil.sys.mjs"
+);
+
+function inChildProcess() {
+ return Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
+}
+
+const { CookieXPCShellUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/CookieXPCShellUtils.sys.mjs"
+);
+
+let CookieXPCShellUtilsInitialized = false;
+function maybeInitializeCookieXPCShellUtils() {
+ if (!CookieXPCShellUtilsInitialized) {
+ CookieXPCShellUtilsInitialized = true;
+ CookieXPCShellUtils.init(this);
+
+ CookieXPCShellUtils.createServer({ hosts: ["example.org"] });
+ }
+}
+
+// Don't pick up default permissions from profile.
+Services.prefs.setCharPref("permissions.manager.defaultsUrl", "");
+
+add_task(async _ => {
+ do_get_profile();
+
+ // Allow all cookies if the pref service is available in this process.
+ if (!inChildProcess()) {
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+ Services.prefs.setBoolPref(
+ "network.cookieJarSettings.unblocked_for_testing",
+ true
+ );
+ }
+
+ info("Let's set a cookie from HTTP example.org");
+
+ let uri = NetUtil.newURI("http://example.org/");
+ let principal = Services.scriptSecurityManager.createContentPrincipal(
+ uri,
+ {}
+ );
+ let channel = NetUtil.newChannel({
+ uri,
+ loadingPrincipal: principal,
+ securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
+ });
+
+ Services.cookies.setCookieStringFromHttp(uri, "a=b; sameSite=lax", channel);
+
+ let cookies = Services.cookies.getCookiesFromHost("example.org", {});
+ Assert.equal(cookies.length, 1, "We expect 1 cookie only");
+
+ Assert.equal(cookies[0].schemeMap, Ci.nsICookie.SCHEME_HTTP, "HTTP Scheme");
+
+ info("Let's set a cookie from HTTPS example.org");
+
+ uri = NetUtil.newURI("https://example.org/");
+ principal = Services.scriptSecurityManager.createContentPrincipal(uri, {});
+ channel = NetUtil.newChannel({
+ uri,
+ loadingPrincipal: principal,
+ securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
+ });
+
+ Services.cookies.setCookieStringFromHttp(uri, "a=b; sameSite=lax", channel);
+
+ cookies = Services.cookies.getCookiesFromHost("example.org", {});
+ Assert.equal(cookies.length, 1, "We expect 1 cookie only");
+
+ Assert.equal(
+ cookies[0].schemeMap,
+ Ci.nsICookie.SCHEME_HTTP | Ci.nsICookie.SCHEME_HTTPS,
+ "HTTP + HTTPS Schemes"
+ );
+
+ Services.cookies.removeAll();
+});
+
+[true, false].forEach(schemefulComparison => {
+ add_task(async () => {
+ do_get_profile();
+ Services.prefs.setBoolPref("dom.security.https_first", false);
+
+ maybeInitializeCookieXPCShellUtils();
+
+ // Allow all cookies if the pref service is available in this process.
+ if (!inChildProcess()) {
+ Services.prefs.setBoolPref(
+ "network.cookie.sameSite.schemeful",
+ schemefulComparison
+ );
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+ Services.prefs.setBoolPref(
+ "network.cookieJarSettings.unblocked_for_testing",
+ true
+ );
+ }
+
+ info(
+ `Testing schemefulSameSite=${schemefulComparison}. Let's set a cookie from HTTPS example.org`
+ );
+
+ let https_uri = NetUtil.newURI("https://example.org/");
+ let https_principal = Services.scriptSecurityManager.createContentPrincipal(
+ https_uri,
+ {}
+ );
+ let same_site_channel = NetUtil.newChannel({
+ uri: https_uri,
+ loadingPrincipal: https_principal,
+ securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
+ });
+
+ Services.cookies.setCookieStringFromHttp(
+ https_uri,
+ "a=b; sameSite=lax",
+ same_site_channel
+ );
+
+ let cookies = Services.cookies.getCookieStringFromHttp(
+ https_uri,
+ same_site_channel
+ );
+ Assert.equal(cookies, "a=b", "Cookies match");
+
+ let http_uri = NetUtil.newURI("http://example.org/");
+ let http_principal = Services.scriptSecurityManager.createContentPrincipal(
+ http_uri,
+ {}
+ );
+ let cross_site_channel = NetUtil.newChannel({
+ uri: https_uri,
+ loadingPrincipal: http_principal,
+ securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
+ });
+
+ cookies = Services.cookies.getCookieStringFromHttp(
+ http_uri,
+ cross_site_channel
+ );
+ if (schemefulComparison) {
+ Assert.equal(cookies, "", "No http(s) cookie for different scheme!");
+ } else {
+ Assert.equal(cookies, "a=b", "http(s) Cookie even for differentscheme!");
+ }
+
+ // SameSite cookies are included via document.domain
+ cookies = await CookieXPCShellUtils.getCookieStringFromDocument(
+ http_uri.spec
+ );
+ Assert.equal(cookies, "a=b", "document.cookie even for different scheme!");
+
+ Services.cookies.removeAll();
+ Services.prefs.clearUserPref("dom.security.https_first");
+ });
+});
+
+add_task(async _ => {
+ do_get_profile();
+ Services.prefs.setBoolPref("dom.security.https_first", false);
+
+ // Allow all cookies if the pref service is available in this process.
+ if (!inChildProcess()) {
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+ Services.prefs.setBoolPref(
+ "network.cookieJarSettings.unblocked_for_testing",
+ true
+ );
+ }
+
+ info("Let's set a cookie without scheme");
+ Services.cookies.add(
+ "example.org",
+ "/",
+ "a",
+ "b",
+ false,
+ false,
+ false,
+ Math.floor(Date.now() / 1000 + 1000),
+ {},
+ Ci.nsICookie.SAMESITE_LAX,
+ Ci.nsICookie.SCHEME_UNSET
+ );
+
+ let cookies = Services.cookies.getCookiesFromHost("example.org", {});
+ Assert.equal(cookies.length, 1, "We expect 1 cookie only");
+ Assert.equal(cookies[0].schemeMap, Ci.nsICookie.SCHEME_UNSET, "Unset scheme");
+
+ ["https", "http"].forEach(scheme => {
+ let uri = NetUtil.newURI(scheme + "://example.org/");
+ let principal = Services.scriptSecurityManager.createContentPrincipal(
+ uri,
+ {}
+ );
+ let channel = NetUtil.newChannel({
+ uri,
+ loadingPrincipal: principal,
+ securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
+ });
+
+ cookies = Services.cookies.getCookieStringFromHttp(uri, channel);
+ Assert.equal(cookies, "a=b", "Cookie for unset scheme");
+ });
+
+ Services.cookies.removeAll();
+ Services.prefs.clearUserPref("dom.security.https_first");
+});
diff --git a/netwerk/cookie/test/unit/test_timestamp_fixup.js b/netwerk/cookie/test/unit/test_timestamp_fixup.js
new file mode 100644
index 0000000000..a6e9642ad7
--- /dev/null
+++ b/netwerk/cookie/test/unit/test_timestamp_fixup.js
@@ -0,0 +1,130 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+const USEC_PER_SEC = 1000 * 1000;
+const ONE_DAY = 60 * 60 * 24 * USEC_PER_SEC;
+const ONE_YEAR = ONE_DAY * 365;
+const LAST_ACCESSED_DIFF = 10 * ONE_YEAR;
+const CREATION_DIFF = 100 * ONE_YEAR;
+
+function initDB(conn, now) {
+ // Write the schema v7 to the database.
+ conn.schemaVersion = 7;
+ conn.executeSimpleSQL(
+ "CREATE TABLE moz_cookies (" +
+ "id INTEGER PRIMARY KEY, " +
+ "baseDomain TEXT, " +
+ "originAttributes TEXT NOT NULL DEFAULT '', " +
+ "name TEXT, " +
+ "value TEXT, " +
+ "host TEXT, " +
+ "path TEXT, " +
+ "expiry INTEGER, " +
+ "lastAccessed INTEGER, " +
+ "creationTime INTEGER, " +
+ "isSecure INTEGER, " +
+ "isHttpOnly INTEGER, " +
+ "appId INTEGER DEFAULT 0, " +
+ "inBrowserElement INTEGER DEFAULT 0, " +
+ "CONSTRAINT moz_uniqueid UNIQUE (name, host, path, originAttributes)" +
+ ")"
+ );
+ conn.executeSimpleSQL(
+ "CREATE INDEX moz_basedomain ON moz_cookies (baseDomain, " +
+ "originAttributes)"
+ );
+
+ conn.executeSimpleSQL("PRAGMA synchronous = OFF");
+ conn.executeSimpleSQL("PRAGMA journal_mode = WAL");
+ conn.executeSimpleSQL("PRAGMA wal_autocheckpoint = 16");
+
+ conn.executeSimpleSQL(
+ `INSERT INTO moz_cookies(baseDomain, host, name, value, path, expiry, lastAccessed, creationTime, isSecure, isHttpOnly)
+ VALUES ('foo.com', '.foo.com', 'foo', 'bar=baz', '/',
+ ${now + ONE_DAY}, ${now + LAST_ACCESSED_DIFF} , ${
+ now + CREATION_DIFF
+ } , 1, 1)`
+ );
+}
+
+add_task(async function test_timestamp_fixup() {
+ let now = Date.now() * 1000; // date in microseconds
+ Services.prefs.setBoolPref("network.cookie.fixup_on_db_load", true);
+ do_get_profile();
+ let dbFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ dbFile.append("cookies.sqlite");
+ let conn = Services.storage.openDatabase(dbFile);
+ initDB(conn, now);
+
+ if (AppConstants.platform != "android") {
+ Services.fog.initializeFOG();
+ }
+ Services.fog.testResetFOG();
+
+ // Now start the cookie service, and then check the fields in the table.
+ // Get sessionCookies to wait for the initialization in cookie thread
+ Assert.lessOrEqual(
+ Math.floor(Services.cookies.cookies[0].creationTime / 1000),
+ now
+ );
+ Assert.equal(conn.schemaVersion, 13);
+
+ Assert.equal(
+ await Glean.networking.cookieTimestampFixedCount.creationTime.testGetValue(),
+ 1,
+ "One fixup of creation time"
+ );
+ Assert.equal(
+ await Glean.networking.cookieTimestampFixedCount.lastAccessed.testGetValue(),
+ 1,
+ "One fixup of lastAccessed"
+ );
+ {
+ let { values } =
+ await Glean.networking.cookieCreationFixupDiff.testGetValue();
+ info(JSON.stringify(values));
+ let keys = Object.keys(values).splice(-2, 2);
+ Assert.equal(keys.length, 2, "There should be two entries in telemetry");
+ Assert.equal(values[keys[0]], 1, "First entry should have value 1");
+ Assert.equal(values[keys[1]], 0, "Second entry should have value 0");
+ const creationDiffInSeconds = CREATION_DIFF / USEC_PER_SEC;
+ Assert.lessOrEqual(
+ parseInt(keys[0]),
+ creationDiffInSeconds,
+ "The bucket should be smaller than time diff"
+ );
+ Assert.lessOrEqual(
+ creationDiffInSeconds,
+ parseInt(keys[1]),
+ "The next bucket should be larger than time diff"
+ );
+ }
+
+ {
+ let { values } =
+ await Glean.networking.cookieAccessFixupDiff.testGetValue();
+ info(JSON.stringify(values));
+ let keys = Object.keys(values).splice(-2, 2);
+ Assert.equal(keys.length, 2, "There should be two entries in telemetry");
+ Assert.equal(values[keys[0]], 1, "First entry should have value 1");
+ Assert.equal(values[keys[1]], 0, "Second entry should have value 0");
+ info(now);
+ const lastAccessedDiffInSeconds = LAST_ACCESSED_DIFF / USEC_PER_SEC;
+ Assert.lessOrEqual(
+ parseInt(keys[0]),
+ lastAccessedDiffInSeconds,
+ "The bucket should be smaller than time diff"
+ );
+ Assert.lessOrEqual(
+ lastAccessedDiffInSeconds,
+ parseInt(keys[1]),
+ "The next bucket should be larger than time diff"
+ );
+ }
+
+ conn.close();
+});
diff --git a/netwerk/cookie/test/unit/xpcshell.toml b/netwerk/cookie/test/unit/xpcshell.toml
new file mode 100644
index 0000000000..694d3cb847
--- /dev/null
+++ b/netwerk/cookie/test/unit/xpcshell.toml
@@ -0,0 +1,26 @@
+[DEFAULT]
+head = ""
+
+["test_baseDomain_publicsuffix.js"]
+
+["test_bug643051.js"]
+
+["test_bug1155169.js"]
+
+["test_bug1321912.js"]
+
+["test_eviction.js"]
+
+["test_getCookieSince.js"]
+
+["test_migrateCookieLifetimePref.js"]
+
+["test_parser_0001.js"]
+
+["test_parser_0019.js"]
+
+["test_rawSameSite.js"]
+
+["test_schemeMap.js"]
+
+["test_timestamp_fixup.js"]