diff options
Diffstat (limited to 'netwerk/cookie')
-rw-r--r-- | netwerk/cookie/CookieCommons.cpp | 42 | ||||
-rw-r--r-- | netwerk/cookie/CookieService.cpp | 207 | ||||
-rw-r--r-- | netwerk/cookie/CookieService.h | 1 | ||||
-rw-r--r-- | netwerk/cookie/CookieServiceChild.cpp | 140 | ||||
-rw-r--r-- | netwerk/cookie/CookieServiceParent.cpp | 35 | ||||
-rw-r--r-- | netwerk/cookie/test/browser/browser.toml | 6 | ||||
-rw-r--r-- | netwerk/cookie/test/browser/browser_cookie_chips.js | 539 | ||||
-rw-r--r-- | netwerk/cookie/test/browser/browser_cookies_serviceWorker.js | 540 | ||||
-rw-r--r-- | netwerk/cookie/test/browser/chips.sjs | 28 | ||||
-rw-r--r-- | netwerk/cookie/test/browser/cookies.sjs | 17 | ||||
-rw-r--r-- | netwerk/cookie/test/browser/serviceWorker.js | 21 |
11 files changed, 1459 insertions, 117 deletions
diff --git a/netwerk/cookie/CookieCommons.cpp b/netwerk/cookie/CookieCommons.cpp index 9b26fc4a6e..4c4ae0c848 100644 --- a/netwerk/cookie/CookieCommons.cpp +++ b/netwerk/cookie/CookieCommons.cpp @@ -349,10 +349,6 @@ already_AddRefed<Cookie> CookieCommons::CreateCookieFromDocument( 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)); @@ -379,15 +375,6 @@ already_AddRefed<Cookie> CookieCommons::CreateCookieFromDocument( 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( @@ -439,8 +426,29 @@ already_AddRefed<Cookie> CookieCommons::CreateCookieFromDocument( return nullptr; } + // CHIPS - If the partitioned attribute is set, store cookie in partitioned + // cookie jar independent of context. If the cookies are stored in the + // partitioned cookie jar anyway no special treatment of CHIPS cookies + // necessary. + bool needPartitioned = + StaticPrefs::network_cookie_cookieBehavior_optInPartitioning() && + cookieData.isPartitioned(); + nsCOMPtr<nsIPrincipal> cookiePrincipal = + needPartitioned ? aDocument->PartitionedPrincipal() + : aDocument->EffectiveCookiePrincipal(); + MOZ_ASSERT(cookiePrincipal); + + // Check if limit-foreign is required. + uint32_t dummyRejectedReason = 0; + if (aDocument->CookieJarSettings()->GetLimitForeignContexts() && + !aHasExistingCookiesLambda(baseDomain, + cookiePrincipal->OriginAttributesRef()) && + !ShouldAllowAccessFor(innerWindow, principalURI, &dummyRejectedReason)) { + return nullptr; + } + RefPtr<Cookie> cookie = - Cookie::Create(cookieData, storagePrincipal->OriginAttributesRef()); + Cookie::Create(cookieData, cookiePrincipal->OriginAttributesRef()); MOZ_ASSERT(cookie); cookie->SetLastAccessed(currentTimeInUsec); @@ -448,7 +456,7 @@ already_AddRefed<Cookie> CookieCommons::CreateCookieFromDocument( Cookie::GenerateUniqueCreationTime(currentTimeInUsec)); aBaseDomain = baseDomain; - aAttrs = storagePrincipal->OriginAttributesRef(); + aAttrs = cookiePrincipal->OriginAttributesRef(); principalURI.forget(aDocumentURI); return cookie.forget(); @@ -486,9 +494,11 @@ bool CookieCommons::ShouldIncludeCrossSiteCookieForDocument( int32_t sameSiteAttr = 0; aCookie->GetSameSite(&sameSiteAttr); + // CHIPS - If a third-party has storage access it can access both it's + // partitioned and unpartitioned cookie jars, else its cookies are blocked. if (aDocument->CookieJarSettings()->GetPartitionForeign() && StaticPrefs::network_cookie_cookieBehavior_optInPartitioning() && - !aCookie->IsPartitioned()) { + !aCookie->IsPartitioned() && !aDocument->UsingStorageAccess()) { return false; } diff --git a/netwerk/cookie/CookieService.cpp b/netwerk/cookie/CookieService.cpp index 0e90b8cbae..78698d44a0 100644 --- a/netwerk/cookie/CookieService.cpp +++ b/netwerk/cookie/CookieService.cpp @@ -373,15 +373,6 @@ CookieService::GetCookieStringFromDocument(Document* aDocument, return NS_OK; } - nsCOMPtr<nsIPrincipal> cookiePrincipal = - aDocument->EffectiveCookiePrincipal(); - - // TODO (Bug 1874174): A document could access both unpartitioned and - // partitioned cookie jars. We will need to prepare partitioned and - // unpartitioned principals for access both cookie jars. - nsTArray<nsCOMPtr<nsIPrincipal>> principals; - principals.AppendElement(cookiePrincipal); - bool thirdParty = true; nsPIDOMWindowInner* innerWindow = aDocument->GetInnerWindow(); // in gtests we don't have a window, let's consider those requests as 3rd @@ -395,6 +386,26 @@ CookieService::GetCookieStringFromDocument(Document* aDocument, } } + nsCOMPtr<nsIPrincipal> cookiePrincipal = + aDocument->EffectiveCookiePrincipal(); + + nsTArray<nsCOMPtr<nsIPrincipal>> principals; + principals.AppendElement(cookiePrincipal); + + // CHIPS - If CHIPS is enabled the partitioned cookie jar is always available + // (and therefore the partitioned principal), the unpartitioned cookie jar is + // only available in first-party or third-party with storageAccess contexts. + bool isCHIPS = StaticPrefs::network_cookie_cookieBehavior_optInPartitioning(); + bool isUnpartitioned = + cookiePrincipal->OriginAttributesRef().mPartitionKey.IsEmpty(); + if (isCHIPS && isUnpartitioned) { + // Assert that we are only doing this if we are first-party or third-party + // with storageAccess. + MOZ_ASSERT(!thirdParty || aDocument->UsingStorageAccess()); + // Add the partitioned principal to principals + principals.AppendElement(aDocument->PartitionedPrincipal()); + } + nsTArray<Cookie*> cookieList; for (auto& principal : principals) { @@ -519,20 +530,40 @@ CookieService::GetCookieStringFromHttp(nsIURI* aHostURI, nsIChannel* aChannel, 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); - // TODO (Bug 1874174): A channel could load both unpartitioned and partitioned - // cookie jars together. We will need to get cookies from both unpartitioned - // and partitioned cookie jars according to storage access. + OriginAttributes storageOriginAttributes; + StoragePrincipalHelper::GetOriginAttributes( + aChannel, storageOriginAttributes, + StoragePrincipalHelper::eStorageAccessPrincipal); + nsTArray<OriginAttributes> originAttributesList; - originAttributesList.AppendElement(attrs); + originAttributesList.AppendElement(storageOriginAttributes); + + // CHIPS - If CHIPS is enabled the partitioned cookie jar is always available + // (and therefore the partitioned OriginAttributes), the unpartitioned cookie + // jar is only available in first-party or third-party with storageAccess + // contexts. + bool isCHIPS = StaticPrefs::network_cookie_cookieBehavior_optInPartitioning(); + bool isUnpartitioned = storageOriginAttributes.mPartitionKey.IsEmpty(); + if (isCHIPS && isUnpartitioned) { + // Assert that we are only doing this if we are first-party or third-party + // with storageAccess. + MOZ_ASSERT( + !result.contains(ThirdPartyAnalysis::IsForeign) || + result.contains(ThirdPartyAnalysis::IsStorageAccessPermissionGranted)); + // Add the partitioned principal to principals + OriginAttributes partitionedOriginAttributes; + StoragePrincipalHelper::GetOriginAttributes( + aChannel, partitionedOriginAttributes, + StoragePrincipalHelper::ePartitionedPrincipal); + originAttributesList.AppendElement(partitionedOriginAttributes); + // Assert partitionedOAs have partitioneKey set. + MOZ_ASSERT(!partitionedOriginAttributes.mPartitionKey.IsEmpty()); + } AutoTArray<Cookie*, 8> foundCookieList; GetCookiesForURI( @@ -630,9 +661,10 @@ CookieService::SetCookieStringFromHttp(nsIURI* aHostURI, ThirdPartyAnalysisResult result = mThirdPartyUtil->AnalyzeChannel( aChannel, false, aHostURI, nullptr, &rejectedReason); - OriginAttributes attrs; + OriginAttributes storagePrincipalOriginAttributes; StoragePrincipalHelper::GetOriginAttributes( - aChannel, attrs, StoragePrincipalHelper::eStorageAccessPrincipal); + aChannel, storagePrincipalOriginAttributes, + StoragePrincipalHelper::eStorageAccessPrincipal); // get the base domain for the host URI. // e.g. for "www.bbc.co.uk", this would be "bbc.co.uk". @@ -660,11 +692,11 @@ CookieService::SetCookieStringFromHttp(nsIURI* aHostURI, baseDomainFromURI); NS_ENSURE_SUCCESS(rv, NS_OK); - CookieStorage* storage = PickStorage(attrs); + CookieStorage* storage = PickStorage(storagePrincipalOriginAttributes); // check default prefs uint32_t priorCookieCount = storage->CountCookiesFromHost( - baseDomainFromURI, attrs.mPrivateBrowsingId); + baseDomainFromURI, storagePrincipalOriginAttributes.mPrivateBrowsingId); nsCOMPtr<nsIConsoleReportCollector> crc = do_QueryInterface(aChannel); @@ -674,7 +706,8 @@ CookieService::SetCookieStringFromHttp(nsIURI* aHostURI, result.contains(ThirdPartyAnalysis::IsThirdPartyTrackingResource), result.contains(ThirdPartyAnalysis::IsThirdPartySocialTrackingResource), result.contains(ThirdPartyAnalysis::IsStorageAccessPermissionGranted), - aCookieHeader, priorCookieCount, attrs, &rejectedReason); + aCookieHeader, priorCookieCount, storagePrincipalOriginAttributes, + &rejectedReason); MOZ_ASSERT_IF(rejectedReason, cookieStatus == STATUS_REJECTED); @@ -718,9 +751,24 @@ CookieService::SetCookieStringFromHttp(nsIURI* aHostURI, nsCString cookieHeader(aCookieHeader); - bool moreCookieToRead = true; + // CHIPS - The partitioned cookie jar is always available and it is always + // possible to store cookies in it using the "Partitioned" attribute. + // Prepare the partitioned principals OAs to enable possible partitioned + // cookie storing from first-party or with StorageAccess. + // Similar behavior to CookieServiceChild::SetCookieStringFromHttp(). + OriginAttributes partitionedPrincipalOriginAttributes; + bool isPartitionedPrincipal = + !storagePrincipalOriginAttributes.mPartitionKey.IsEmpty(); + bool isCHIPS = StaticPrefs::network_cookie_cookieBehavior_optInPartitioning(); + // Only need to get OAs if we don't already use the partitioned principal. + if (isCHIPS && !isPartitionedPrincipal) { + StoragePrincipalHelper::GetOriginAttributes( + aChannel, partitionedPrincipalOriginAttributes, + StoragePrincipalHelper::ePartitionedPrincipal); + } // process each cookie in the header + bool moreCookieToRead = true; while (moreCookieToRead) { CookieStruct cookieData; bool canSetCookie = false; @@ -751,8 +799,22 @@ CookieService::SetCookieStringFromHttp(nsIURI* aHostURI, continue; } + // CHIPS - If the partitioned attribute is set, store cookie in partitioned + // cookie jar independent of context. If the cookies are stored in the + // partitioned cookie jar anyway no special treatment of CHIPS cookies + // necessary. + bool needPartitioned = + isCHIPS && cookieData.isPartitioned() && !isPartitionedPrincipal; + OriginAttributes& cookieOriginAttributes = + needPartitioned ? partitionedPrincipalOriginAttributes + : storagePrincipalOriginAttributes; + // Assert that partitionedPrincipalOriginAttributes are initialized if used. + MOZ_ASSERT_IF( + needPartitioned, + !partitionedPrincipalOriginAttributes.mPartitionKey.IsEmpty()); + // create a new Cookie - RefPtr<Cookie> cookie = Cookie::Create(cookieData, attrs); + RefPtr<Cookie> cookie = Cookie::Create(cookieData, cookieOriginAttributes); MOZ_ASSERT(cookie); int64_t currentTimeInUsec = PR_Now(); @@ -763,8 +825,8 @@ CookieService::SetCookieStringFromHttp(nsIURI* aHostURI, 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); + storage->AddCookie(crc, baseDomain, cookieOriginAttributes, cookie, + currentTimeInUsec, aHostURI, aCookieHeader, true, bc); } return NS_OK; @@ -1180,6 +1242,16 @@ static void RecordPartitionedTelemetry(const CookieStruct& aCookieData, } } +static bool HasSecurePrefix(const nsACString& aString) { + return StringBeginsWith(aString, "__Secure-"_ns, + nsCaseInsensitiveCStringComparator); +} + +static bool HasHostPrefix(const nsACString& aString) { + return StringBeginsWith(aString, "__Host-"_ns, + nsCaseInsensitiveCStringComparator); +} + // processes a single cookie, and returns true if there are more cookies // to be processed bool CookieService::CanSetCookie( @@ -1290,9 +1362,12 @@ bool CookieService::CanSetCookie( return newCookie; } - if (!CheckHiddenPrefix(aCookieData)) { + // If a cookie is nameless, then its value must not start with + // `__Host-` or `__Secure-` + if (aCookieData.name().IsEmpty() && (HasSecurePrefix(aCookieData.value()) || + HasHostPrefix(aCookieData.value()))) { COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader, - "failed the CheckHiddenPrefix tests"); + "failed hidden prefix tests"); CookieLogging::LogMessageToConsole( aCRC, aHostURI, nsIScriptError::warningFlag, CONSOLE_REJECTION_CATEGORY, "CookieRejectedInvalidPrefix"_ns, @@ -1449,10 +1524,9 @@ bool CookieService::CanSetCookie( separators = ";" | "=" value-sep = ";" cookie-sep = CR | LF - allowed-chars = <any OCTET except NUL or cookie-sep> + allowed-chars = <any OCTET except 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)> @@ -1472,6 +1546,8 @@ bool CookieService::CanSetCookie( | "Max-Age" "=" value | "Comment" "=" value | "Version" "=" value + | "Partitioned" + | "SameSite" | "Secure" | "HttpOnly" @@ -1479,7 +1555,6 @@ bool CookieService::CanSetCookie( // 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) { @@ -1507,7 +1582,7 @@ bool CookieService::GetTokenValue(nsACString::const_char_iterator& aIter, ++aIter; } start = aIter; - while (aIter != aEndIter && !isnull(*aIter) && !istokenseparator(*aIter)) { + while (aIter != aEndIter && !istokenseparator(*aIter)) { ++aIter; } @@ -1530,7 +1605,7 @@ bool CookieService::GetTokenValue(nsACString::const_char_iterator& aIter, // process <token> // just look for ';' to terminate ('=' allowed) - while (aIter != aEndIter && !isnull(*aIter) && !isvalueseparator(*aIter)) { + while (aIter != aEndIter && !isvalueseparator(*aIter)) { ++aIter; } @@ -1549,6 +1624,21 @@ bool CookieService::GetTokenValue(nsACString::const_char_iterator& aIter, // if on terminator, increment past & return true to process new cookie if (isterminator(*aIter)) { ++aIter; + while (aIter != aEndIter && isvalueseparator(*aIter)) { + ++aIter; + } + nsACString::const_char_iterator end = aIter - 1; + if (!isterminator(*end)) { + // The cookie isn't valid because we have multiple terminators or + // a terminator followed by a value separator. Add those invalid + // characters to the cookie string or value so it will be rejected. + if (aEqualsFound) { + aTokenString.Rebind(start, end); + } else { + aTokenValue.Rebind(start, end); + } + return false; + } return true; } // fall-through: aIter is on ';', increment and return false @@ -1570,6 +1660,17 @@ static inline void SetSameSiteAttribute(CookieStruct& aCookieData, aCookieData.rawSameSite() = aValue; } +// Tests for control characters, defined by RFC 5234 to be %x00-1F / %x7F. +// An exception is made for HTAB as the cookie spec treats that as whitespace. +static bool ContainsControlChars(const nsACString& aString) { + const auto* start = aString.BeginReading(); + const auto* end = aString.EndReading(); + + return std::find_if(start, end, [](unsigned char c) { + return (c <= 0x1F && c != 0x09) || c == 0x7F; + }) != end; +} + // 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. @@ -1627,6 +1728,14 @@ bool CookieService::ParseAttributes(nsIConsoleReportCollector* aCRC, newCookie = GetTokenValue(cookieStart, cookieEnd, tokenString, tokenValue, equalsFound); + if (ContainsControlChars(tokenString) || ContainsControlChars(tokenValue)) { + CookieLogging::LogMessageToConsole( + aCRC, aHostURI, nsIScriptError::errorFlag, CONSOLE_REJECTION_CATEGORY, + "CookieRejectedInvalidCharAttributes"_ns, + AutoTArray<nsString, 1>{NS_ConvertUTF8toUTF16(aCookieData.name())}); + return newCookie; + } + // decide which attribute we have, and copy the string if (tokenString.LowerCaseEqualsLiteral(kPath)) { aCookieData.path() = tokenValue; @@ -1967,25 +2076,6 @@ bool CookieService::CheckDomain(CookieStruct& aCookieData, nsIURI* aHostURI, 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, @@ -2053,15 +2143,10 @@ bool CookieService::CheckPath(CookieStruct& aCookieData, // 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; + bool hasSecurePrefix = HasSecurePrefix(aCookieData.name()); + bool hasHostPrefix = HasHostPrefix(aCookieData.name()); - if (!isSecure && !isHost) { + if (!hasSecurePrefix && !hasHostPrefix) { // not one of the magic prefixes: carry on return true; } @@ -2072,7 +2157,7 @@ bool CookieService::CheckPrefixes(CookieStruct& aCookieData, return false; } - if (isHost) { + if (hasHostPrefix) { // 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 diff --git a/netwerk/cookie/CookieService.h b/netwerk/cookie/CookieService.h index 51344d6909..ca409cb12f 100644 --- a/netwerk/cookie/CookieService.h +++ b/netwerk/cookie/CookieService.h @@ -124,7 +124,6 @@ class CookieService final : public nsICookieService, 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); diff --git a/netwerk/cookie/CookieServiceChild.cpp b/netwerk/cookie/CookieServiceChild.cpp index 84f34a9a37..dab62d1523 100644 --- a/netwerk/cookie/CookieServiceChild.cpp +++ b/netwerk/cookie/CookieServiceChild.cpp @@ -96,9 +96,9 @@ RefPtr<GenericPromise> CookieServiceChild::TrackCookieLoad( aChannel->GetURI(getter_AddRefs(uri)); nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); - OriginAttributes attrs = loadInfo->GetOriginAttributes(); + OriginAttributes storageOriginAttributes = loadInfo->GetOriginAttributes(); StoragePrincipalHelper::PrepareEffectiveStoragePrincipalOriginAttributes( - aChannel, attrs); + aChannel, storageOriginAttributes); bool isSafeTopLevelNav = CookieCommons::IsSafeTopLevelNav(aChannel); bool hadCrossSiteRedirects = false; @@ -107,11 +107,30 @@ RefPtr<GenericPromise> CookieServiceChild::TrackCookieLoad( RefPtr<CookieServiceChild> self(this); - // TODO (Bug 1874174): A channel could access both unpartitioned and - // partitioned cookie jars. We will need to pass partitioned and unpartitioned - // originAttributes according the storage access. - nsTArray<OriginAttributes> attrsList; - attrsList.AppendElement(attrs); + nsTArray<OriginAttributes> originAttributesList; + originAttributesList.AppendElement(storageOriginAttributes); + + // CHIPS - If CHIPS is enabled the partitioned cookie jar is always available + // (and therefore the partitioned OriginAttributes), the unpartitioned cookie + // jar is only available in first-party or third-party with storageAccess + // contexts. + bool isCHIPS = StaticPrefs::network_cookie_cookieBehavior_optInPartitioning(); + bool isUnpartitioned = storageOriginAttributes.mPartitionKey.IsEmpty(); + if (isCHIPS && isUnpartitioned) { + // Assert that we are only doing this if we are first-party or third-party + // with storageAccess. + MOZ_ASSERT( + !result.contains(ThirdPartyAnalysis::IsForeign) || + result.contains(ThirdPartyAnalysis::IsStorageAccessPermissionGranted)); + // Add the partitioned principal to principals. + OriginAttributes partitionedOriginAttributes; + StoragePrincipalHelper::GetOriginAttributes( + aChannel, partitionedOriginAttributes, + StoragePrincipalHelper::ePartitionedPrincipal); + originAttributesList.AppendElement(partitionedOriginAttributes); + // Assert partitionedOAs have partitioneKey set. + MOZ_ASSERT(!partitionedOriginAttributes.mPartitionKey.IsEmpty()); + } return SendGetCookieList( uri, result.contains(ThirdPartyAnalysis::IsForeign), @@ -121,7 +140,7 @@ RefPtr<GenericPromise> CookieServiceChild::TrackCookieLoad( result.contains( ThirdPartyAnalysis::IsStorageAccessPermissionGranted), rejectedReason, isSafeTopLevelNav, isSameSiteForeign, - hadCrossSiteRedirects, attrsList) + hadCrossSiteRedirects, originAttributesList) ->Then( GetCurrentSerialEventTarget(), __func__, [self, uri](const nsTArray<CookieStructTable>& aCookiesListTable) { @@ -325,15 +344,6 @@ CookieServiceChild::GetCookieStringFromDocument(dom::Document* aDocument, aCookieString.Truncate(); - nsCOMPtr<nsIPrincipal> cookiePrincipal = - aDocument->EffectiveCookiePrincipal(); - - // TODO (Bug 1874174): A document could access both unpartitioned and - // partitioned cookie jars. We will need to prepare partitioned and - // unpartitioned principals for access both cookie jars. - nsTArray<nsCOMPtr<nsIPrincipal>> principals; - principals.AppendElement(cookiePrincipal); - bool thirdParty = true; nsPIDOMWindowInner* innerWindow = aDocument->GetInnerWindow(); // in gtests we don't have a window, let's consider those requests as 3rd @@ -347,6 +357,26 @@ CookieServiceChild::GetCookieStringFromDocument(dom::Document* aDocument, } } + nsCOMPtr<nsIPrincipal> cookiePrincipal = + aDocument->EffectiveCookiePrincipal(); + + nsTArray<nsCOMPtr<nsIPrincipal>> principals; + principals.AppendElement(cookiePrincipal); + + // CHIPS - If CHIPS is enabled the partitioned cookie jar is always available + // (and therefore the partitioned principal), the unpartitioned cookie jar is + // only available in first-party or third-party with storageAccess contexts. + bool isCHIPS = StaticPrefs::network_cookie_cookieBehavior_optInPartitioning(); + bool isUnpartitioned = + cookiePrincipal->OriginAttributesRef().mPartitionKey.IsEmpty(); + if (isCHIPS && isUnpartitioned) { + // Assert that we are only doing this if we are first-party or third-party + // with storageAccess. + MOZ_ASSERT(!thirdParty || aDocument->UsingStorageAccess()); + // Add the partitioned principal to principals + principals.AppendElement(aDocument->PartitionedPrincipal()); + } + for (auto& principal : principals) { if (!CookieCommons::IsSchemeSupported(principal)) { return NS_OK; @@ -488,7 +518,14 @@ CookieServiceChild::SetCookieStringFromDocument( // (those come from the parent, which already checks this), // but script could see an inconsistent view of things. - nsCOMPtr<nsIPrincipal> principal = aDocument->EffectiveCookiePrincipal(); + // CHIPS - If the cookie has the "Partitioned" attribute set it will be + // stored in the partitioned cookie jar. + bool needPartitioned = + StaticPrefs::network_cookie_cookieBehavior_optInPartitioning() && + cookie->RawIsPartitioned(); + nsCOMPtr<nsIPrincipal> principal = + needPartitioned ? aDocument->PartitionedPrincipal() + : aDocument->EffectiveCookiePrincipal(); bool isPotentiallyTrustworthy = principal->GetIsOriginPotentiallyTrustworthy(); @@ -560,9 +597,10 @@ CookieServiceChild::SetCookieStringFromHttp(nsIURI* aHostURI, nsCString cookieString(aCookieString); - OriginAttributes attrs = loadInfo->GetOriginAttributes(); + OriginAttributes storagePrincipalOriginAttributes = + loadInfo->GetOriginAttributes(); StoragePrincipalHelper::PrepareEffectiveStoragePrincipalOriginAttributes( - aChannel, attrs); + aChannel, storagePrincipalOriginAttributes); bool requireHostMatch; nsCString baseDomain; @@ -580,18 +618,15 @@ CookieServiceChild::SetCookieStringFromHttp(nsIURI* aHostURI, result.contains(ThirdPartyAnalysis::IsThirdPartyTrackingResource), result.contains(ThirdPartyAnalysis::IsThirdPartySocialTrackingResource), result.contains(ThirdPartyAnalysis::IsStorageAccessPermissionGranted), - aCookieString, CountCookiesFromHashTable(baseDomain, attrs), attrs, - &rejectedReason); + aCookieString, + CountCookiesFromHashTable(baseDomain, storagePrincipalOriginAttributes), + storagePrincipalOriginAttributes, &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; @@ -612,6 +647,23 @@ CookieServiceChild::SetCookieStringFromHttp(nsIURI* aHostURI, nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN && !result.contains(ThirdPartyAnalysis::IsStorageAccessPermissionGranted); + // CHIPS - The partitioned cookie jar is always available and it is always + // possible to store cookies in it using the "Partitioned" attribute. + // Prepare the partitioned principals OAs to enable possible partitioned + // cookie storing from first-party or with StorageAccess. + // Similar behavior to CookieService::SetCookieStringFromHttp(). + OriginAttributes partitionedPrincipalOriginAttributes; + bool isPartitionedPrincipal = + !storagePrincipalOriginAttributes.mPartitionKey.IsEmpty(); + bool isCHIPS = StaticPrefs::network_cookie_cookieBehavior_optInPartitioning(); + // Only need to get OAs if we don't already use the partitioned principal. + if (isCHIPS && !isPartitionedPrincipal) { + StoragePrincipalHelper::GetOriginAttributes( + aChannel, partitionedPrincipalOriginAttributes, + StoragePrincipalHelper::ePartitionedPrincipal); + } + + nsTArray<CookieStruct> cookiesToSend, partitionedCookiesToSend; bool moreCookies; do { CookieStruct cookieData; @@ -642,23 +694,47 @@ CookieServiceChild::SetCookieStringFromHttp(nsIURI* aHostURI, continue; } - RefPtr<Cookie> cookie = Cookie::Create(cookieData, attrs); + // CHIPS - If the partitioned attribute is set, store cookie in partitioned + // cookie jar independent of context. If the cookies are stored in the + // partitioned cookie jar anyway no special treatment of CHIPS cookies + // necessary. + bool needPartitioned = + isCHIPS && cookieData.isPartitioned() && !isPartitionedPrincipal; + nsTArray<CookieStruct>& cookiesToSendRef = + needPartitioned ? partitionedCookiesToSend : cookiesToSend; + OriginAttributes& cookieOriginAttributes = + needPartitioned ? partitionedPrincipalOriginAttributes + : storagePrincipalOriginAttributes; + // Assert that partitionedPrincipalOriginAttributes are initialized if used. + MOZ_ASSERT_IF( + needPartitioned, + !partitionedPrincipalOriginAttributes.mPartitionKey.IsEmpty()); + + RefPtr<Cookie> cookie = Cookie::Create(cookieData, cookieOriginAttributes); MOZ_ASSERT(cookie); cookie->SetLastAccessed(currentTimeInUsec); cookie->SetCreationTime( Cookie::GenerateUniqueCreationTime(currentTimeInUsec)); - RecordDocumentCookie(cookie, attrs); - cookiesToSend.AppendElement(cookieData); + RecordDocumentCookie(cookie, cookieOriginAttributes); + cookiesToSendRef.AppendElement(cookieData); } while (moreCookies); // Asynchronously call the parent. - if (CanSend() && !cookiesToSend.IsEmpty()) { + if (CanSend()) { RefPtr<HttpChannelChild> httpChannelChild = do_QueryObject(aChannel); MOZ_ASSERT(httpChannelChild); - httpChannelChild->SendSetCookies(baseDomain, attrs, aHostURI, true, - cookiesToSend); + if (!cookiesToSend.IsEmpty()) { + httpChannelChild->SendSetCookies(baseDomain, + storagePrincipalOriginAttributes, + aHostURI, true, cookiesToSend); + } + if (!partitionedCookiesToSend.IsEmpty()) { + httpChannelChild->SendSetCookies( + baseDomain, partitionedPrincipalOriginAttributes, aHostURI, true, + partitionedCookiesToSend); + } } return NS_OK; diff --git a/netwerk/cookie/CookieServiceParent.cpp b/netwerk/cookie/CookieServiceParent.cpp index 75c024cec6..187a31e002 100644 --- a/netwerk/cookie/CookieServiceParent.cpp +++ b/netwerk/cookie/CookieServiceParent.cpp @@ -15,6 +15,7 @@ #include "mozIThirdPartyUtil.h" #include "nsArrayUtils.h" #include "nsIChannel.h" +#include "mozilla/StaticPrefs_network.h" #include "nsIEffectiveTLDService.h" #include "nsNetCID.h" #include "nsMixedContentBlocker.h" @@ -119,17 +120,11 @@ void CookieServiceParent::TrackCookieLoad(nsIChannel* aChannel) { 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); - // TODO (Bug 1874174): A channel could load both unpartitioned and partitioned - // cookie jars together. We will need to track both originAttributes for them. - StoragePrincipalHelper::PrepareEffectiveStoragePrincipalOriginAttributes( - aChannel, attrs); - nsCOMPtr<mozIThirdPartyUtil> thirdPartyUtil; thirdPartyUtil = do_GetService(THIRDPARTYUTIL_CONTRACTID); @@ -137,8 +132,34 @@ void CookieServiceParent::TrackCookieLoad(nsIChannel* aChannel) { ThirdPartyAnalysisResult result = thirdPartyUtil->AnalyzeChannel( aChannel, false, nullptr, nullptr, &rejectedReason); + OriginAttributes storageOriginAttributes = loadInfo->GetOriginAttributes(); + StoragePrincipalHelper::PrepareEffectiveStoragePrincipalOriginAttributes( + aChannel, storageOriginAttributes); + nsTArray<OriginAttributes> originAttributesList; - originAttributesList.AppendElement(attrs); + originAttributesList.AppendElement(storageOriginAttributes); + + // CHIPS - If CHIPS is enabled the partitioned cookie jar is always available + // (and therefore the partitioned OriginAttributes), the unpartitioned cookie + // jar is only available in first-party or third-party with storageAccess + // contexts. + bool isCHIPS = StaticPrefs::network_cookie_cookieBehavior_optInPartitioning(); + bool isUnpartitioned = storageOriginAttributes.mPartitionKey.IsEmpty(); + if (isCHIPS && isUnpartitioned) { + // Assert that we are only doing this if we are first-party or third-party + // with storageAccess. + MOZ_ASSERT( + !result.contains(ThirdPartyAnalysis::IsForeign) || + result.contains(ThirdPartyAnalysis::IsStorageAccessPermissionGranted)); + // Add the partitioned principal to principals + OriginAttributes partitionedOriginAttributes; + StoragePrincipalHelper::GetOriginAttributes( + aChannel, partitionedOriginAttributes, + StoragePrincipalHelper::ePartitionedPrincipal); + originAttributesList.AppendElement(partitionedOriginAttributes); + // Assert partitionedOAs have partitioneKey set. + MOZ_ASSERT(!partitionedOriginAttributes.mPartitionKey.IsEmpty()); + } for (auto& originAttributes : originAttributesList) { UpdateCookieInContentList(uri, originAttributes); diff --git a/netwerk/cookie/test/browser/browser.toml b/netwerk/cookie/test/browser/browser.toml index 05a302ddbd..56807e4bf1 100644 --- a/netwerk/cookie/test/browser/browser.toml +++ b/netwerk/cookie/test/browser/browser.toml @@ -8,6 +8,9 @@ support-files = [ ["browser_broadcastChannel.js"] +["browser_cookie_chips.js"] +support-files = ["chips.sjs"] + ["browser_cookie_insecure_overwrites_secure.js"] ["browser_cookie_purge_sync.js"] @@ -17,6 +20,9 @@ support-files = ["server.sjs"] ["browser_cookies_ipv6.js"] +["browser_cookies_serviceWorker.js"] +support-files = ["cookies.sjs", "serviceWorker.js"] + ["browser_domCache.js"] ["browser_indexedDB.js"] diff --git a/netwerk/cookie/test/browser/browser_cookie_chips.js b/netwerk/cookie/test/browser/browser_cookie_chips.js new file mode 100644 index 0000000000..ba0c8f3a0e --- /dev/null +++ b/netwerk/cookie/test/browser/browser_cookie_chips.js @@ -0,0 +1,539 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_setup(() => { + // These are functional and not integration tests, cookieBehavior accept lets + // us have StorageAccess on thirparty. + Services.prefs.setIntPref( + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_ACCEPT + ); + Services.prefs.setBoolPref( + "network.cookieJarSettings.unblocked_for_testing", + true + ); + Services.prefs.setBoolPref( + "network.cookie.cookieBehavior.optInPartitioning", + true + ); + Services.prefs.setBoolPref("dom.storage_access.enabled", true); + Services.prefs.setBoolPref("dom.storage_access.prompt.testing", true); + Services.cookies.removeAll(); + Services.perms.removeAll(); +}); + +registerCleanupFunction(() => { + Services.prefs.clearUserPref("network.cookie.cookieBehavior"); + Services.prefs.clearUserPref( + "network.cookieJarSettings.unblocked_for_testing" + ); + Services.prefs.clearUserPref( + "network.cookie.cookieBehavior.optInPartitioning" + ); + Services.prefs.clearUserPref("dom.storage_access.enabled"); + Services.prefs.clearUserPref("dom.storage_access.prompt.testing"); + Services.cookies.removeAll(); + Services.perms.removeAll(); +}); + +const COOKIE_PARTITIONED = + "cookie=partitioned; Partitioned; Secure; SameSite=None;"; +const COOKIE_UNPARTITIONED = "cookie=unpartitioned; Secure; SameSite=None;"; + +const PATH = "/browser/netwerk/cookie/test/browser/"; +const PATH_EMPTY = PATH + "file_empty.html"; +const HTTP_COOKIE_SET = PATH + "chips.sjs?set"; +const HTTP_COOKIE_GET = PATH + "chips.sjs?get"; + +const FIRST_PARTY = "example.com"; +const THIRD_PARTY = "example.org"; + +const URL_DOCUMENT_FIRSTPARTY = "https://" + FIRST_PARTY + PATH_EMPTY; +const URL_DOCUMENT_THIRDPARTY = "https://" + THIRD_PARTY + PATH_EMPTY; +const URL_HTTP_FIRSTPARTY = "https://" + FIRST_PARTY + "/" + HTTP_COOKIE_SET; +const URL_HTTP_THIRDPARTY = "https://" + THIRD_PARTY + "/" + HTTP_COOKIE_SET; + +function createOriginAttributes(partitionKey) { + return JSON.stringify({ + firstPartyDomain: "", + geckoViewSessionContextId: "", + inIsolatedMozBrowser: false, + partitionKey, + privateBrowsingId: 0, + userContextId: 0, + }); +} + +function createPartitonKey(url) { + let uri = NetUtil.newURI(url); + return `(${uri.scheme},${uri.host})`; +} + +// OriginAttributes used to access partitioned and unpartitioned cookie jars +// in all tests. +const partitionedOAs = createOriginAttributes( + createPartitonKey(URL_DOCUMENT_FIRSTPARTY) +); +const unpartitionedOAs = createOriginAttributes(""); + +// Set partitioned and unpartitioned cookie from first-party document. +// CHIPS "Partitioned" cookie MUST always be stored in partitioned jar. +// This calls CookieServiceChild::SetCookieStringFromDocument() internally. +// CookieService::SetCookieStringFromDocument() is not explicitly tested since +// CHIPS are in the common function CookieCommons::CreateCookieFromDocument(). +add_task( + async function test_chips_store_partitioned_document_first_party_child() { + const tab = BrowserTestUtils.addTab(gBrowser, URL_DOCUMENT_FIRSTPARTY); + const browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + // Set partitioned and unpartitioned cookie from document child-side + await SpecialPowers.spawn( + browser, + [COOKIE_PARTITIONED, COOKIE_UNPARTITIONED], + (partitioned, unpartitioned) => { + content.document.cookie = partitioned; + content.document.cookie = unpartitioned; + } + ); + + // Get cookies from partitioned jar + let partitioned = Services.cookies.getCookiesWithOriginAttributes( + partitionedOAs, + FIRST_PARTY + ); + // Get cookies from unpartitioned jar + let unpartitioned = Services.cookies.getCookiesWithOriginAttributes( + unpartitionedOAs, + FIRST_PARTY + ); + + // Assert partitioned/unpartitioned cookie were stored in correct jars + Assert.equal(partitioned.length, 1); + Assert.equal(partitioned[0].value, "partitioned"); + Assert.equal(unpartitioned.length, 1); + Assert.equal(unpartitioned[0].value, "unpartitioned"); + + // Cleanup + BrowserTestUtils.removeTab(tab); + Services.cookies.removeAll(); + } +); + +// Set partitioned and unpartitioned cookie from third-party document with storage +// access. CHIPS "Partitioned" cookie MUST always be stored in partitioned jar. +// This calls CookieServiceChild::SetCookieStringFromDocument() internally. +// CookieService::SetCookieStringFromDocument() is not explicitly tested since +// CHIPS are in the common function CookieCommons::CreateCookieFromDocument(). +add_task( + async function test_chips_store_partitioned_document_third_party_storage_access_child() { + const tab = BrowserTestUtils.addTab(gBrowser, URL_DOCUMENT_FIRSTPARTY); + const browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + // Spawn document bc + await SpecialPowers.spawn( + browser, + [URL_DOCUMENT_THIRDPARTY, COOKIE_PARTITIONED, COOKIE_UNPARTITIONED], + async (url, partitioned, unpartitioned) => { + let ifr = content.document.createElement("iframe"); + ifr.src = url; + content.document.body.appendChild(ifr); + await ContentTaskUtils.waitForEvent(ifr, "load"); + + // Spawn iframe bc + await SpecialPowers.spawn( + await ifr.browsingContext, + [partitioned, unpartitioned], + async (partitioned, unpartitioned) => { + ok( + await content.document.hasStorageAccess(), + "example.org should have storageAccess by CookieBehavior 0 / test setup" + ); + + content.document.cookie = partitioned; + content.document.cookie = unpartitioned; + } + ); + } + ); + + // Get cookies from partitioned jar + let partitioned = Services.cookies.getCookiesWithOriginAttributes( + partitionedOAs, + THIRD_PARTY + ); + // Get cookies from unpartitioned jar + let unpartitioned = Services.cookies.getCookiesWithOriginAttributes( + unpartitionedOAs, + THIRD_PARTY + ); + + // Assert partitioned/unpartitioned cookie were stored in correct jars + Assert.equal(partitioned.length, 1); + Assert.equal(partitioned[0].value, "partitioned"); + Assert.equal(unpartitioned.length, 1); + Assert.equal(unpartitioned[0].value, "unpartitioned"); + + // Cleanup + BrowserTestUtils.removeTab(tab); + Services.cookies.removeAll(); + } +); + +// Set partitioned and unpartitioned cookie from first-party http load. +// CHIPS "Partitioned" cookie MUST always be stored in partitioned jar. +// This calls CookieService::SetCookieStringFromHttp() internally. +add_task(async function test_chips_store_partitioned_http_first_party_parent() { + // Set partitioned and unpartitioned cookie from http parent side through + // chips.sjs being loaded. + const tab = BrowserTestUtils.addTab(gBrowser, URL_HTTP_FIRSTPARTY); + const browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + // Get cookies from partitioned jar + let partitioned = Services.cookies.getCookiesWithOriginAttributes( + partitionedOAs, + FIRST_PARTY + ); + // Get cookies from unpartitioned jar + let unpartitioned = Services.cookies.getCookiesWithOriginAttributes( + unpartitionedOAs, + FIRST_PARTY + ); + + // Assert partitioned/unpartitioned cookie were stored in correct jars + Assert.equal(partitioned.length, 1); + Assert.equal(partitioned[0].value, "partitioned"); + Assert.equal(unpartitioned.length, 1); + Assert.equal(unpartitioned[0].value, "unpartitioned"); + + // Cleanup + BrowserTestUtils.removeTab(tab); + Services.cookies.removeAll(); +}); + +// Set partitioned and unpartitioned cookie from third-party http load. +// CHIPS "Partitioned" cookie MUST always be stored in partitioned jar. +// This calls CookieService::SetCookieStringFromHttp() internally. +add_task( + async function test_chips_store_partitioned_http_third_party_storage_access_parent() { + const tab = BrowserTestUtils.addTab(gBrowser, URL_DOCUMENT_FIRSTPARTY); + const browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + // Spawn document bc + await SpecialPowers.spawn(browser, [URL_HTTP_THIRDPARTY], async url => { + let ifr = content.document.createElement("iframe"); + ifr.src = url; + content.document.body.appendChild(ifr); + // Send http request with "set" query parameter, partitioned and + // unpartitioned cookie will be set through http response from chips.sjs. + await ContentTaskUtils.waitForEvent(ifr, "load"); + + // Spawn iframe bc + await SpecialPowers.spawn(await ifr.browsingContext, [], async () => { + ok( + await content.document.hasStorageAccess(), + "example.org should have storageAccess by CookieBehavior 0 / test setup" + ); + }); + }); + + // Get cookies from partitioned jar + let partitioned = Services.cookies.getCookiesWithOriginAttributes( + partitionedOAs, + THIRD_PARTY + ); + // Get cookies from unpartitioned jar + let unpartitioned = Services.cookies.getCookiesWithOriginAttributes( + unpartitionedOAs, + THIRD_PARTY + ); + + // Assert partitioned/unpartitioned cookie were stored in correct jars + Assert.equal(partitioned.length, 1); + Assert.equal(partitioned[0].value, "partitioned"); + Assert.equal(unpartitioned.length, 1); + Assert.equal(unpartitioned[0].value, "unpartitioned"); + + // Cleanup + BrowserTestUtils.removeTab(tab); + Services.cookies.removeAll(); + } +); + +// TODO CHIPS - Tests for CookieServiceChild::SetCookieStringFromHttp() need +// to be added. Since this is only checkable on onProxyConnectSuccess needs +// proxy setup test harness. It is also called after onStartRequest() (Http) +// but cookies are already set by the parents +// CookieService::SetCookieStringFromHttp() call. + +// Get partitioned and unpartitioned cookies from document (child). +// This calls CookieServiceChild::GetCookieStringFromDocument() internally. +add_task( + async function test_chips_send_partitioned_and_unpartitioned_document_child() { + const tab = BrowserTestUtils.addTab(gBrowser, URL_DOCUMENT_FIRSTPARTY); + const browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + // Spawn document bc + await SpecialPowers.spawn( + browser, + [COOKIE_PARTITIONED, COOKIE_UNPARTITIONED], + async (partitioned, unpartitioned) => { + content.document.cookie = partitioned; + content.document.cookie = unpartitioned; + + // Assert both unpartitioned and partitioned cookie are returned. + let cookies = content.document.cookie; + ok( + cookies.includes("cookie=partitioned"), + "Cookie from partitioned jar was sent." + ); + ok( + cookies.includes("cookie=unpartitioned"), + "Cookie from unpartitioned jar was sent." + ); + } + ); + + // Cleanup + BrowserTestUtils.removeTab(tab); + Services.cookies.removeAll(); + } +); + +// Get partitioned and unpartitioned cookies from document (child) after +// storageAccess was granted. This calls CookieServiceChild::TrackCookieLoad() +// internally to update child's cookies. +add_task( + async function test_chips_send_partitioned_and_unpartitioned_on_storage_access_child() { + // Set cookieBehavior to BEHAVIOR_REJECT_TRACKERS_AND_PARTITION_FOREIGN for + // requestStorageAccess() based test. + Services.prefs.setIntPref("network.cookie.cookieBehavior", 5); + + // Set example.org first-party unpartitioned cookie + await BrowserTestUtils.withNewTab( + URL_DOCUMENT_THIRDPARTY, + async browser => { + info("Set a first party cookie via `document.cookie`."); + await SpecialPowers.spawn( + browser, + [COOKIE_UNPARTITIONED], + async unpartitioned => { + content.document.cookie = unpartitioned; + is( + content.document.cookie, + "cookie=unpartitioned", + "Unpartitioned cookie was set." + ); + } + ); + } + ); + + // Assert cookie was set on parent cookie service for example.org. + // Get cookies from unpartitioned jar + let unpartitioned = Services.cookies.getCookiesWithOriginAttributes( + unpartitionedOAs, + THIRD_PARTY + ); + Assert.equal(unpartitioned.length, 1); + Assert.equal(unpartitioned[0].value, "unpartitioned"); + + // Load example.com as first-party in tab + const tab = BrowserTestUtils.addTab(gBrowser, URL_DOCUMENT_FIRSTPARTY); + const browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + // Set third-party cookie from example.org iframe, get storageAccess and + // check cookies. + // Spawn document bc + await SpecialPowers.spawn( + browser, + [URL_DOCUMENT_THIRDPARTY, COOKIE_PARTITIONED], + async (url, partitioned) => { + // Create third-party iframe + let ifr = content.document.createElement("iframe"); + ifr.src = url; + content.document.body.appendChild(ifr); + await ContentTaskUtils.waitForEvent(ifr, "load"); + + // Spawn iframe bc + await SpecialPowers.spawn( + await ifr.browsingContext, + [partitioned], + async partitioned => { + ok( + !(await content.document.hasStorageAccess()), + "example.org should not have storageAccess initially." + ); + + // Set a partitioned third-party cookie and assert its the only. + content.document.cookie = partitioned; + is( + content.document.cookie, + "cookie=partitioned", + "Partitioned cookie was set." + ); + + info("Simulate user activation."); + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + + info("Request storage access."); + await content.document.requestStorageAccess(); + + ok( + await content.document.hasStorageAccess(), + "example.org should now have storageAccess." + ); + + // Assert both unpartitioned and partitioned cookie are returned. + let cookies = content.document.cookie; + ok( + cookies.includes("cookie=partitioned"), + "Cookie from partitioned jar was sent." + ); + ok( + cookies.includes("cookie=unpartitioned"), + "Cookie from unpartitioned jar was sent." + ); + } + ); + } + ); + + // Cleanup + BrowserTestUtils.removeTab(tab); + Services.cookies.removeAll(); + Services.perms.removeAll(); + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + } +); + +// Set partitioned and unpartitioned cookies for URL_DOCUMENT_FIRSTPARTY, then +// load URL again, assure cookies are correctly send to content child process. +// This tests CookieServiceParent::TrackCookieLoad() internally. +add_task( + async function test_chips_send_partitioned_and_unpartitioned_document_parent() { + // Set example.com first-party unpartitioned and partitioned cookie, then + // close tab. + await BrowserTestUtils.withNewTab( + URL_DOCUMENT_FIRSTPARTY, + async browser => { + await SpecialPowers.spawn( + browser, + [COOKIE_PARTITIONED, COOKIE_UNPARTITIONED], + async (partitioned, unpartitioned) => { + content.document.cookie = unpartitioned; + content.document.cookie = partitioned; + let cookies = content.document.cookie; + ok( + cookies.includes("cookie=unpartitioned"), + "Unpartitioned cookie was set." + ); + ok( + cookies.includes("cookie=partitioned"), + "Partitioned cookie was set." + ); + } + ); + } + ); + + // Assert we have one partitioned and one unpartitioned cookie set. + // Get cookies from partitioned jar + let partitioned = Services.cookies.getCookiesWithOriginAttributes( + partitionedOAs, + FIRST_PARTY + ); + // Get cookies from unpartitioned jar + let unpartitioned = Services.cookies.getCookiesWithOriginAttributes( + unpartitionedOAs, + FIRST_PARTY + ); + Assert.equal(partitioned.length, 1); + Assert.equal(partitioned[0].value, "partitioned"); + Assert.equal(unpartitioned.length, 1); + Assert.equal(unpartitioned[0].value, "unpartitioned"); + + // Reload example.com and assert previously set cookies are correctly + // send to content child document. + await BrowserTestUtils.withNewTab( + URL_DOCUMENT_FIRSTPARTY, + async browser => { + await SpecialPowers.spawn(browser, [], () => { + let cookies = content.document.cookie; + ok( + cookies.includes("cookie=unpartitioned"), + "Unpartitioned cookie was sent." + ); + ok( + cookies.includes("cookie=partitioned"), + "Partitioned cookie was sent." + ); + }); + } + ); + + // Cleanup + Services.cookies.removeAll(); + } +); + +// Set partitioned and unpartitioned cookies for URL_DOCUMENT_FIRSTPARTY, then +// send http request, assure cookies are correctly send in "Cookie" header. +// This tests CookieService::GetCookieStringFromHttp() internally. +add_task( + async function test_chips_send_partitioned_and_unpartitioned_http_parent() { + // Load empty document. + let tab = BrowserTestUtils.addTab(gBrowser, URL_DOCUMENT_FIRSTPARTY); + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + await SpecialPowers.spawn( + browser, + [HTTP_COOKIE_SET, HTTP_COOKIE_GET], + async (set, get) => { + // Send http request with "set" query parameter, partitioned and + // unpartitioned cookie will be set through http response. + await content.fetch(set); + + // Assert cookies were set to document. + let cookies = content.document.cookie; + ok( + cookies.includes("cookie=unpartitioned"), + "Unpartitioned cookie was set to document." + ); + ok( + cookies.includes("cookie=partitioned"), + "Partitioned cookie was set to document." + ); + + // Send http request with "get" query parameter, chips.sjs will return + // the request "Cookie" header string. + await content + .fetch(get) + .then(response => response.text()) + .then(requestCookies => { + // Assert cookies were sent in http request. + ok( + requestCookies.includes("cookie=unpartitioned"), + "Unpartitioned cookie was sent in http request." + ); + ok( + requestCookies.includes("cookie=partitioned"), + "Partitioned cookie was sent in http request." + ); + }); + } + ); + + // Cleanup + BrowserTestUtils.removeTab(tab); + Services.cookies.removeAll(); + } +); diff --git a/netwerk/cookie/test/browser/browser_cookies_serviceWorker.js b/netwerk/cookie/test/browser/browser_cookies_serviceWorker.js new file mode 100644 index 0000000000..813e1fe7cb --- /dev/null +++ b/netwerk/cookie/test/browser/browser_cookies_serviceWorker.js @@ -0,0 +1,540 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_CROSS_SITE_DOMAIN = "https://example.net/"; +const TEST_CROSS_SITE_PAGE = + TEST_CROSS_SITE_DOMAIN + TEST_PATH + "file_empty.html"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ], + }); + + Services.cookies.removeAll(); +}); + +function registerSW(browser) { + return SpecialPowers.spawn(browser, [], async _ => { + let reg = await content.navigator.serviceWorker.register( + "serviceWorker.js" + ); + + await ContentTaskUtils.waitForCondition(() => { + return reg.active && reg.active.state === "activated"; + }, "The service worker is activated"); + + ok( + content.navigator.serviceWorker.controller, + "The service worker controls the document successfully." + ); + }); +} + +function fetchCookiesFromSW(browser) { + return SpecialPowers.spawn(browser, [], async _ => { + return new content.Promise(resolve => { + content.navigator.serviceWorker.addEventListener("message", event => { + resolve(event.data.content); + }); + + content.navigator.serviceWorker.controller.postMessage({ + action: "fetch", + url: `cookies.sjs`, + }); + }); + }); +} + +function setCookiesFromSW(browser, cookies) { + let setCookieQuery = ""; + + for (let cookie of cookies) { + setCookieQuery += `Set-Cookie=${cookie}&`; + } + + return SpecialPowers.spawn(browser, [setCookieQuery], async query => { + return new content.Promise(resolve => { + content.navigator.serviceWorker.addEventListener("message", event => { + resolve(event.data.content); + }); + + content.navigator.serviceWorker.controller.postMessage({ + action: "fetch", + url: `cookies.sjs?${query}`, + }); + }); + }); +} + +function unregisterSW(browser) { + return SpecialPowers.spawn(browser, [], async _ => { + const regs = await content.navigator.serviceWorker.getRegistrations(); + for (const reg of regs) { + await reg.unregister(); + } + }); +} + +/** + * Verify a first-party service worker can access both SameSite=None and + * SameSite=Lax cookies set in the first-party context. + */ +add_task(async function testCookiesWithFirstPartyServiceWorker() { + info("Open a tab"); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_TOP_PAGE + ); + + info("Writing cookies to the first-party context."); + await SpecialPowers.spawn(tab.linkedBrowser, [], async _ => { + const cookies = [ + "foo=bar; SameSite=None; Secure", + "fooLax=barLax; SameSite=Lax; Secure", + ]; + + let query = ""; + + for (let cookie of cookies) { + query += `Set-Cookie=${cookie}&`; + } + + await content.fetch(`cookies.sjs?${query}`); + }); + + info("Register a service worker and trigger a fetch request to get cookies."); + await registerSW(tab.linkedBrowser); + let cookieStr = await fetchCookiesFromSW(tab.linkedBrowser); + + is(cookieStr, "foo=bar; fooLax=barLax", "The cookies are expected"); + + info("Set cookies from the service worker."); + await setCookiesFromSW(tab.linkedBrowser, [ + "foo=barSW; SameSite=None; Secure", + "fooLax=barLaxSW; SameSite=Lax; Secure", + ]); + + info("Get cookies from the service worker."); + cookieStr = await fetchCookiesFromSW(tab.linkedBrowser); + + is(cookieStr, "foo=barSW; fooLax=barLaxSW", "The cookies are expected"); + + await unregisterSW(tab.linkedBrowser); + BrowserTestUtils.removeTab(tab); + Services.cookies.removeAll(); +}); + +/** + * Verify a cross-site service worker can only access cookies set in the + * same cross-site context. + */ +add_task(async function testCookiesWithCrossSiteServiceWorker() { + // Disable blocking third-party cookies. + await SpecialPowers.pushPrefEnv({ + set: [["network.cookie.cookieBehavior.optInPartitioning", false]], + }); + + info("Open a cross-site tab"); + let crossSiteTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_CROSS_SITE_PAGE + ); + + info("Writing cookies to the cross site in the first-party context."); + await SpecialPowers.spawn(crossSiteTab.linkedBrowser, [], async _ => { + const cookies = [ + "foo=bar; SameSite=None; Secure", + "fooLax=barLax; SameSite=Lax; Secure", + ]; + + let query = ""; + + for (let cookie of cookies) { + query += `Set-Cookie=${cookie}&`; + } + + await content.fetch(`cookies.sjs?${query}`); + }); + + info("Open a tab"); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_TOP_PAGE + ); + + info("Load a cross-site iframe"); + let crossSiteBc = await SpecialPowers.spawn( + tab.linkedBrowser, + [TEST_CROSS_SITE_PAGE], + async url => { + let ifr = content.document.createElement("iframe"); + + await new content.Promise(resolve => { + ifr.onload = resolve; + content.document.body.appendChild(ifr); + ifr.src = url; + }); + + return ifr.browsingContext; + } + ); + + info("Write cookies in the cross-site iframe"); + await SpecialPowers.spawn(crossSiteBc, [], async _ => { + const cookies = [ + "foo=crossBar; SameSite=None; Secure", + "fooLax=crossBarLax; SameSite=Lax; Secure", + ]; + + let query = ""; + + for (let cookie of cookies) { + query += `Set-Cookie=${cookie}&`; + } + + await content.fetch(`cookies.sjs?${query}`); + }); + + info( + "Register a service worker and trigger a fetch request to get cookies in cross-site context." + ); + await registerSW(crossSiteBc); + let cookieStr = await fetchCookiesFromSW(crossSiteBc); + + is( + cookieStr, + "foo=crossBar", + "Only the SameSite=None cookie set in the third-party iframe is available." + ); + + info("Set cookies from the third-party service worker."); + await setCookiesFromSW(crossSiteBc, [ + "foo=crossBarSW; SameSite=None; Secure", + "fooLax=crossBarLaxSW; SameSite=Lax; Secure", + ]); + + info("Get cookies from the third-party service worker."); + cookieStr = await fetchCookiesFromSW(crossSiteBc); + + is( + cookieStr, + "foo=crossBarSW", + "Only the SameSite=None cookie set in the third-party service worker is available." + ); + + await unregisterSW(crossSiteBc); + BrowserTestUtils.removeTab(crossSiteTab); + BrowserTestUtils.removeTab(tab); + Services.cookies.removeAll(); +}); + +/** + * Verify a cross-site service worker can only access partitioned cookies set in + * the same cross-site context if third-party cookies are blocked. + */ +add_task(async function testPartitionedCookiesWithCrossSiteServiceWorker() { + // Enable blocking third-party cookies. + await SpecialPowers.pushPrefEnv({ + set: [["network.cookie.cookieBehavior.optInPartitioning", true]], + }); + + info("Open a tab"); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_TOP_PAGE + ); + + info("Load a cross-site iframe"); + let crossSiteBc = await SpecialPowers.spawn( + tab.linkedBrowser, + [TEST_CROSS_SITE_PAGE], + async url => { + let ifr = content.document.createElement("iframe"); + + await new content.Promise(resolve => { + ifr.onload = resolve; + content.document.body.appendChild(ifr); + ifr.src = url; + }); + + return ifr.browsingContext; + } + ); + + info("Write cookies in the cross-site iframe"); + await SpecialPowers.spawn(crossSiteBc, [], async _ => { + const cookies = [ + "foo=crossBar; SameSite=None; Secure", + "fooLax=crossBarLax; SameSite=Lax; Secure", + "fooPartitioned=crossBar; SameSite=None; Secure; Partitioned;", + "fooLaxPartitioned=crossBarLax; SameSite=Lax; Secure; Partitioned;", + ]; + + let query = ""; + + for (let cookie of cookies) { + query += `Set-Cookie=${cookie}&`; + } + + await content.fetch(`cookies.sjs?${query}`); + }); + + info( + "Register a service worker and trigger a fetch request to get cookies in cross-site context." + ); + await registerSW(crossSiteBc); + let cookieStr = await fetchCookiesFromSW(crossSiteBc); + + is( + cookieStr, + "fooPartitioned=crossBar", + "Only the SameSite=None partitioned cookie set in the third-party iframe is available." + ); + + info("Set cookies from the third-party service worker."); + await setCookiesFromSW(crossSiteBc, [ + "foo=crossBarSW; SameSite=None; Secure", + "fooLax=crossBarLaxSW; SameSite=Lax; Secure", + "fooPartitioned=crossBarSW; SameSite=None; Secure; Partitioned;", + "fooLaxPartitioned=crossBarLaxSW; SameSite=Lax; Secure; Partitioned;", + ]); + + info("Get cookies from the third-party service worker."); + cookieStr = await fetchCookiesFromSW(crossSiteBc); + + is( + cookieStr, + "fooPartitioned=crossBarSW", + "Only the SameSite=None partitioned cookie set in the third-party service worker is available." + ); + + await unregisterSW(crossSiteBc); + BrowserTestUtils.removeTab(tab); + Services.cookies.removeAll(); +}); + +/** + * Verify a ABA service worker can only access cookies set in the ABA context. + */ +add_task(async function testCookiesWithABAServiceWorker() { + // Disable blocking third-party cookies. + await SpecialPowers.pushPrefEnv({ + set: [["network.cookie.cookieBehavior.optInPartitioning", false]], + }); + + info("Open a tab"); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_TOP_PAGE + ); + + info("Writing cookies to the first-party context."); + await SpecialPowers.spawn(tab.linkedBrowser, [], async _ => { + const cookies = [ + "foo=bar; SameSite=None; Secure", + "fooLax=barLax; SameSite=Lax; Secure", + ]; + + let query = ""; + + for (let cookie of cookies) { + query += `Set-Cookie=${cookie}&`; + } + + await content.fetch(`cookies.sjs?${query}`); + }); + + info("Load a ABA iframe"); + let crossSiteBc = await SpecialPowers.spawn( + tab.linkedBrowser, + [TEST_CROSS_SITE_PAGE], + async url => { + let ifr = content.document.createElement("iframe"); + + await new content.Promise(resolve => { + ifr.onload = resolve; + content.document.body.appendChild(ifr); + ifr.src = url; + }); + + return ifr.browsingContext; + } + ); + + let ABABc = await SpecialPowers.spawn( + crossSiteBc, + [TEST_TOP_PAGE], + async url => { + let ifr = content.document.createElement("iframe"); + + await new content.Promise(resolve => { + ifr.onload = resolve; + content.document.body.appendChild(ifr); + ifr.src = url; + }); + + return ifr.browsingContext; + } + ); + + info( + "Register a service worker and trigger a fetch request to get cookies in the ABA context." + ); + + await registerSW(ABABc); + let cookieStr = await fetchCookiesFromSW(ABABc); + is(cookieStr, "", "No cookie should be available in ABA context."); + + info("Set cookies in the ABA iframe"); + await SpecialPowers.spawn(ABABc, [], async _ => { + const cookies = [ + "fooABA=barABA; SameSite=None; Secure", + "fooABALax=BarABALax; SameSite=Lax; Secure", + ]; + + let query = ""; + + for (let cookie of cookies) { + query += `Set-Cookie=${cookie}&`; + } + + await content.fetch(`cookies.sjs?${query}`); + }); + + info("Get cookies in the ABA service worker."); + cookieStr = await fetchCookiesFromSW(ABABc); + + is( + cookieStr, + "fooABA=barABA", + "Only the SameSite=None cookie set in ABA iframe is available." + ); + + info("Set cookies from the service worker in ABA context"); + await setCookiesFromSW(ABABc, [ + "fooABA=barABASW; SameSite=None; Secure", + "fooABALax=BarABALaxSW; SameSite=Lax; Secure", + ]); + + info("Get cookies from the service worker in the ABA context."); + cookieStr = await fetchCookiesFromSW(ABABc); + + is( + cookieStr, + "fooABA=barABASW", + "Only the SameSite=None cookie set in ABA service worker is available." + ); + + await unregisterSW(ABABc); + BrowserTestUtils.removeTab(tab); + Services.cookies.removeAll(); +}); + +/** + * Verify a ABA service worker can only access partitioned cookies set in the + * ABA context if third-party cookies are blocked. + */ +add_task(async function testCookiesWithABAServiceWorker() { + // Disable blocking third-party cookies. + await SpecialPowers.pushPrefEnv({ + set: [["network.cookie.cookieBehavior.optInPartitioning", true]], + }); + + info("Open a tab"); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_TOP_PAGE + ); + + info("Load a ABA iframe"); + let crossSiteBc = await SpecialPowers.spawn( + tab.linkedBrowser, + [TEST_CROSS_SITE_PAGE], + async url => { + let ifr = content.document.createElement("iframe"); + + await new content.Promise(resolve => { + ifr.onload = resolve; + content.document.body.appendChild(ifr); + ifr.src = url; + }); + + return ifr.browsingContext; + } + ); + + let ABABc = await SpecialPowers.spawn( + crossSiteBc, + [TEST_TOP_PAGE], + async url => { + let ifr = content.document.createElement("iframe"); + + await new content.Promise(resolve => { + ifr.onload = resolve; + content.document.body.appendChild(ifr); + ifr.src = url; + }); + + return ifr.browsingContext; + } + ); + + info("Set cookies in the ABA iframe"); + await SpecialPowers.spawn(ABABc, [], async _ => { + const cookies = [ + "fooABA=barABA; SameSite=None; Secure", + "fooABALax=BarABALax; SameSite=Lax; Secure", + "fooABAPartitioned=barABA; SameSite=None; Secure; Partitioned;", + "fooABALaxPartitioned=BarABALax; SameSite=Lax; Secure; Partitioned;", + ]; + + let query = ""; + + for (let cookie of cookies) { + query += `Set-Cookie=${cookie}&`; + } + + await content.fetch(`cookies.sjs?${query}`); + }); + + info( + "Register a service worker and trigger a fetch request to get cookies in the ABA context." + ); + + await registerSW(ABABc); + + info("Get cookies in the ABA service worker."); + let cookieStr = await fetchCookiesFromSW(ABABc); + + is( + cookieStr, + "fooABAPartitioned=barABA", + "Only the SameSite=None partitioned cookie set in ABA iframe is available." + ); + + info("Set cookies from the service worker in ABA context"); + await setCookiesFromSW(ABABc, [ + "fooABA=barABASW; SameSite=None; Secure", + "fooABALax=BarABALaxSW; SameSite=Lax; Secure", + "fooABAPartitioned=barABASW; SameSite=None; Secure; Partitioned;", + "fooABALaxPartitioned=BarABALaxSW; SameSite=Lax; Secure; Partitioned;", + ]); + + info("Get cookies from the service worker in the ABA context."); + cookieStr = await fetchCookiesFromSW(ABABc); + + is( + cookieStr, + "fooABAPartitioned=barABASW", + "Only the SameSite=None partitioned cookie set in ABA service worker is available." + ); + + await unregisterSW(ABABc); + BrowserTestUtils.removeTab(tab); + Services.cookies.removeAll(); +}); diff --git a/netwerk/cookie/test/browser/chips.sjs b/netwerk/cookie/test/browser/chips.sjs new file mode 100644 index 0000000000..e60109f6fa --- /dev/null +++ b/netwerk/cookie/test/browser/chips.sjs @@ -0,0 +1,28 @@ +function handleRequest(aRequest, aResponse) { + aResponse.setStatusLine(aRequest.httpVersion, 200); + + var params = new URLSearchParams(aRequest.queryString); + + // Get Cookie header string. + if (params.has("get")) { + if (aRequest.hasHeader("Cookie")) { + let cookie = aRequest.getHeader("Cookie"); + aResponse.write(cookie); + } + return; + } + + // Set a partitioned and a unpartitioned cookie. + if (params.has("set")) { + aResponse.setHeader( + "Set-Cookie", + "cookie=partitioned; Partitioned; SameSite=None; Secure", + true + ); + aResponse.setHeader( + "Set-Cookie", + "cookie=unpartitioned; SameSite=None; Secure", + true + ); + } +} diff --git a/netwerk/cookie/test/browser/cookies.sjs b/netwerk/cookie/test/browser/cookies.sjs new file mode 100644 index 0000000000..9beb861d44 --- /dev/null +++ b/netwerk/cookie/test/browser/cookies.sjs @@ -0,0 +1,17 @@ +function handleRequest(aRequest, aResponse) { + aResponse.setStatusLine(aRequest.httpVersion, 200); + let query = new URLSearchParams(aRequest.queryString); + + if (query.has("Set-Cookie")) { + for (let value of query.getAll("Set-Cookie")) { + aResponse.setHeader("Set-Cookie", value, true); + } + return; + } + + let cookieStr = ""; + if (aRequest.hasHeader("Cookie")) { + cookieStr = aRequest.getHeader("Cookie"); + } + aResponse.write(cookieStr); +} diff --git a/netwerk/cookie/test/browser/serviceWorker.js b/netwerk/cookie/test/browser/serviceWorker.js new file mode 100644 index 0000000000..b2eab40ce4 --- /dev/null +++ b/netwerk/cookie/test/browser/serviceWorker.js @@ -0,0 +1,21 @@ +self.addEventListener("install", function () { + self.skipWaiting(); +}); + +self.addEventListener("activate", function (event) { + event.waitUntil(self.clients.claim()); +}); + +self.addEventListener("message", function (event) { + if (event.data.action === "fetch") { + fetch(event.data.url) + .then(response => response.text()) + .then(data => { + self.clients.matchAll().then(clients => { + clients.forEach(client => { + client.postMessage({ content: data }); + }); + }); + }); + } +}); |