diff options
Diffstat (limited to 'netwerk/cookie')
29 files changed, 8042 insertions, 0 deletions
diff --git a/netwerk/cookie/CookieServiceChild.cpp b/netwerk/cookie/CookieServiceChild.cpp new file mode 100644 index 000000000..9a13b445c --- /dev/null +++ b/netwerk/cookie/CookieServiceChild.cpp @@ -0,0 +1,243 @@ +/* -*- 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 "mozilla/net/CookieServiceChild.h" +#include "mozilla/LoadInfo.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/ipc/URIUtils.h" +#include "mozilla/net/NeckoChild.h" +#include "nsIChannel.h" +#include "nsIURI.h" +#include "nsIPrefService.h" +#include "nsIPrefBranch.h" +#include "nsServiceManagerUtils.h" + +using namespace mozilla::ipc; + +namespace mozilla { +namespace net { + +// Pref string constants +static const char kPrefCookieBehavior[] = "network.cookie.cookieBehavior"; +static const char kPrefThirdPartySession[] = + "network.cookie.thirdparty.sessionOnly"; + +static CookieServiceChild *gCookieService; + +CookieServiceChild* +CookieServiceChild::GetSingleton() +{ + if (!gCookieService) + gCookieService = new CookieServiceChild(); + + NS_ADDREF(gCookieService); + return gCookieService; +} + +NS_IMPL_ISUPPORTS(CookieServiceChild, + nsICookieService, + nsIObserver, + nsISupportsWeakReference) + +CookieServiceChild::CookieServiceChild() + : mCookieBehavior(nsICookieService::BEHAVIOR_ACCEPT) + , mThirdPartySession(false) +{ + NS_ASSERTION(IsNeckoChild(), "not a child process"); + + // This corresponds to Release() in DeallocPCookieService. + NS_ADDREF_THIS(); + + // Create a child PCookieService actor. + NeckoChild::InitNeckoChild(); + gNeckoChild->SendPCookieServiceConstructor(this); + + // Init our prefs and observer. + nsCOMPtr<nsIPrefBranch> prefBranch = + do_GetService(NS_PREFSERVICE_CONTRACTID); + NS_WARNING_ASSERTION(prefBranch, "no prefservice"); + if (prefBranch) { + prefBranch->AddObserver(kPrefCookieBehavior, this, true); + prefBranch->AddObserver(kPrefThirdPartySession, this, true); + PrefChanged(prefBranch); + } +} + +CookieServiceChild::~CookieServiceChild() +{ + gCookieService = nullptr; +} + +void +CookieServiceChild::PrefChanged(nsIPrefBranch *aPrefBranch) +{ + int32_t val; + if (NS_SUCCEEDED(aPrefBranch->GetIntPref(kPrefCookieBehavior, &val))) + mCookieBehavior = + val >= nsICookieService::BEHAVIOR_ACCEPT && + val <= nsICookieService::BEHAVIOR_LIMIT_FOREIGN + ? val : nsICookieService::BEHAVIOR_ACCEPT; + + bool boolval; + if (NS_SUCCEEDED(aPrefBranch->GetBoolPref(kPrefThirdPartySession, &boolval))) + mThirdPartySession = !!boolval; + + if (!mThirdPartyUtil && RequireThirdPartyCheck()) { + mThirdPartyUtil = do_GetService(THIRDPARTYUTIL_CONTRACTID); + NS_ASSERTION(mThirdPartyUtil, "require ThirdPartyUtil service"); + } +} + +bool +CookieServiceChild::RequireThirdPartyCheck() +{ + return mCookieBehavior == nsICookieService::BEHAVIOR_REJECT_FOREIGN || + mCookieBehavior == nsICookieService::BEHAVIOR_LIMIT_FOREIGN || + mThirdPartySession; +} + +nsresult +CookieServiceChild::GetCookieStringInternal(nsIURI *aHostURI, + nsIChannel *aChannel, + char **aCookieString, + bool aFromHttp) +{ + NS_ENSURE_ARG(aHostURI); + NS_ENSURE_ARG_POINTER(aCookieString); + + *aCookieString = nullptr; + + // 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; + + // Determine whether the request is foreign. Failure is acceptable. + bool isForeign = true; + if (RequireThirdPartyCheck()) + mThirdPartyUtil->IsThirdPartyChannel(aChannel, aHostURI, &isForeign); + + URIParams uriParams; + SerializeURI(aHostURI, uriParams); + + mozilla::NeckoOriginAttributes attrs; + if (aChannel) { + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->GetLoadInfo(); + if (loadInfo) { + attrs = loadInfo->GetOriginAttributes(); + } + } + + // Synchronously call the parent. + nsAutoCString result; + SendGetCookieString(uriParams, !!isForeign, aFromHttp, attrs, &result); + if (!result.IsEmpty()) + *aCookieString = ToNewCString(result); + + return NS_OK; +} + +nsresult +CookieServiceChild::SetCookieStringInternal(nsIURI *aHostURI, + nsIChannel *aChannel, + const char *aCookieString, + const char *aServerTime, + bool aFromHttp) +{ + NS_ENSURE_ARG(aHostURI); + NS_ENSURE_ARG_POINTER(aCookieString); + + // 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; + + // Determine whether the request is foreign. Failure is acceptable. + bool isForeign = true; + if (RequireThirdPartyCheck()) + mThirdPartyUtil->IsThirdPartyChannel(aChannel, aHostURI, &isForeign); + + nsDependentCString cookieString(aCookieString); + nsDependentCString serverTime; + if (aServerTime) + serverTime.Rebind(aServerTime); + + URIParams uriParams; + SerializeURI(aHostURI, uriParams); + + mozilla::NeckoOriginAttributes attrs; + if (aChannel) { + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->GetLoadInfo(); + if (loadInfo) { + attrs = loadInfo->GetOriginAttributes(); + } + } + + // Synchronously call the parent. + SendSetCookieString(uriParams, !!isForeign, cookieString, serverTime, + aFromHttp, attrs); + return NS_OK; +} + +NS_IMETHODIMP +CookieServiceChild::Observe(nsISupports *aSubject, + const char *aTopic, + const char16_t *aData) +{ + NS_ASSERTION(strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID) == 0, + "not a pref change topic!"); + + nsCOMPtr<nsIPrefBranch> prefBranch = do_QueryInterface(aSubject); + if (prefBranch) + PrefChanged(prefBranch); + return NS_OK; +} + +NS_IMETHODIMP +CookieServiceChild::GetCookieString(nsIURI *aHostURI, + nsIChannel *aChannel, + char **aCookieString) +{ + return GetCookieStringInternal(aHostURI, aChannel, aCookieString, false); +} + +NS_IMETHODIMP +CookieServiceChild::GetCookieStringFromHttp(nsIURI *aHostURI, + nsIURI *aFirstURI, + nsIChannel *aChannel, + char **aCookieString) +{ + return GetCookieStringInternal(aHostURI, aChannel, aCookieString, true); +} + +NS_IMETHODIMP +CookieServiceChild::SetCookieString(nsIURI *aHostURI, + nsIPrompt *aPrompt, + const char *aCookieString, + nsIChannel *aChannel) +{ + return SetCookieStringInternal(aHostURI, aChannel, aCookieString, + nullptr, false); +} + +NS_IMETHODIMP +CookieServiceChild::SetCookieStringFromHttp(nsIURI *aHostURI, + nsIURI *aFirstURI, + nsIPrompt *aPrompt, + const char *aCookieString, + const char *aServerTime, + nsIChannel *aChannel) +{ + return SetCookieStringInternal(aHostURI, aChannel, aCookieString, + aServerTime, true); +} + +} // namespace net +} // namespace mozilla + diff --git a/netwerk/cookie/CookieServiceChild.h b/netwerk/cookie/CookieServiceChild.h new file mode 100644 index 000000000..bc6efedf4 --- /dev/null +++ b/netwerk/cookie/CookieServiceChild.h @@ -0,0 +1,67 @@ +/* -*- 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 "mozilla/net/PCookieServiceChild.h" +#include "nsICookieService.h" +#include "nsIObserver.h" +#include "nsIPrefBranch.h" +#include "mozIThirdPartyUtil.h" +#include "nsWeakReference.h" + +namespace mozilla { +namespace net { + +class CookieServiceChild : public PCookieServiceChild + , public nsICookieService + , public nsIObserver + , public nsSupportsWeakReference +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSICOOKIESERVICE + NS_DECL_NSIOBSERVER + + CookieServiceChild(); + + static CookieServiceChild* GetSingleton(); + +protected: + virtual ~CookieServiceChild(); + + void SerializeURIs(nsIURI *aHostURI, + nsIChannel *aChannel, + nsCString &aHostSpec, + nsCString &aHostCharset, + nsCString &aOriginatingSpec, + nsCString &aOriginatingCharset); + + nsresult GetCookieStringInternal(nsIURI *aHostURI, + nsIChannel *aChannel, + char **aCookieString, + bool aFromHttp); + + nsresult SetCookieStringInternal(nsIURI *aHostURI, + nsIChannel *aChannel, + const char *aCookieString, + const char *aServerTime, + bool aFromHttp); + + void PrefChanged(nsIPrefBranch *aPrefBranch); + + bool RequireThirdPartyCheck(); + + nsCOMPtr<mozIThirdPartyUtil> mThirdPartyUtil; + uint8_t mCookieBehavior; + bool mThirdPartySession; +}; + +} // 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 000000000..005ef44b4 --- /dev/null +++ b/netwerk/cookie/CookieServiceParent.cpp @@ -0,0 +1,157 @@ +/* -*- 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 "mozilla/net/CookieServiceParent.h" +#include "mozilla/dom/PContentParent.h" +#include "mozilla/net/NeckoParent.h" + +#include "mozilla/BasePrincipal.h" +#include "mozilla/ipc/URIUtils.h" +#include "nsCookieService.h" +#include "nsIChannel.h" +#include "nsIScriptSecurityManager.h" +#include "nsIPrivateBrowsingChannel.h" +#include "nsNetCID.h" +#include "nsPrintfCString.h" + +using namespace mozilla::ipc; +using mozilla::BasePrincipal; +using mozilla::NeckoOriginAttributes; +using mozilla::PrincipalOriginAttributes; +using mozilla::dom::PContentParent; +using mozilla::net::NeckoParent; + +namespace { + +// Ignore failures from this function, as they only affect whether we do or +// don't show a dialog box in private browsing mode if the user sets a pref. +void +CreateDummyChannel(nsIURI* aHostURI, NeckoOriginAttributes& aAttrs, bool aIsPrivate, + nsIChannel** aChannel) +{ + MOZ_ASSERT(aAttrs.mAppId != nsIScriptSecurityManager::UNKNOWN_APP_ID); + + PrincipalOriginAttributes attrs; + attrs.InheritFromNecko(aAttrs); + + nsCOMPtr<nsIPrincipal> principal = + BasePrincipal::CreateCodebasePrincipal(aHostURI, attrs); + if (!principal) { + return; + } + + nsCOMPtr<nsIURI> dummyURI; + nsresult rv = NS_NewURI(getter_AddRefs(dummyURI), "about:blank"); + if (NS_FAILED(rv)) { + return; + } + + // The following channel is never openend, so it does not matter what + // securityFlags we pass; let's follow the principle of least privilege. + nsCOMPtr<nsIChannel> dummyChannel; + NS_NewChannel(getter_AddRefs(dummyChannel), dummyURI, principal, + nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED, + nsIContentPolicy::TYPE_INVALID); + nsCOMPtr<nsIPrivateBrowsingChannel> pbChannel = do_QueryInterface(dummyChannel); + if (!pbChannel) { + return; + } + + pbChannel->SetPrivate(aIsPrivate); + dummyChannel.forget(aChannel); + return; +} + +} + +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 nsCookieService instance directly, so we can call internal methods. + mCookieService = + already_AddRefed<nsCookieService>(nsCookieService::GetSingleton()); + NS_ASSERTION(mCookieService, "couldn't get nsICookieService"); +} + +CookieServiceParent::~CookieServiceParent() +{ +} + +void +CookieServiceParent::ActorDestroy(ActorDestroyReason aWhy) +{ + // Nothing needed here. Called right before destructor since this is a + // non-refcounted class. +} + +bool +CookieServiceParent::RecvGetCookieString(const URIParams& aHost, + const bool& aIsForeign, + const bool& aFromHttp, + const NeckoOriginAttributes& aAttrs, + nsCString* aResult) +{ + if (!mCookieService) + return true; + + // Deserialize URI. Having a host URI is mandatory and should always be + // provided by the child; thus we consider failure fatal. + nsCOMPtr<nsIURI> hostURI = DeserializeURI(aHost); + if (!hostURI) + return false; + + bool isPrivate = aAttrs.mPrivateBrowsingId > 0; + mCookieService->GetCookieStringInternal(hostURI, aIsForeign, aFromHttp, aAttrs, + isPrivate, *aResult); + return true; +} + +bool +CookieServiceParent::RecvSetCookieString(const URIParams& aHost, + const bool& aIsForeign, + const nsCString& aCookieString, + const nsCString& aServerTime, + const bool& aFromHttp, + const NeckoOriginAttributes& aAttrs) +{ + if (!mCookieService) + return true; + + // Deserialize URI. Having a host URI is mandatory and should always be + // provided by the child; thus we consider failure fatal. + nsCOMPtr<nsIURI> hostURI = DeserializeURI(aHost); + if (!hostURI) + return false; + + bool isPrivate = aAttrs.mPrivateBrowsingId > 0; + + // This is a gross hack. We've already computed everything we need to know + // for whether to set this cookie or not, but we need to communicate all of + // this information through to nsICookiePermission, which indirectly + // computes the information from the channel. We only care about the + // aIsPrivate argument as nsCookieService::SetCookieStringInternal deals + // with aIsForeign before we have to worry about nsCookiePermission trying + // to use the channel to inspect it. + nsCOMPtr<nsIChannel> dummyChannel; + CreateDummyChannel(hostURI, const_cast<NeckoOriginAttributes&>(aAttrs), + isPrivate, getter_AddRefs(dummyChannel)); + + // NB: dummyChannel could be null if something failed in CreateDummyChannel. + nsDependentCString cookieString(aCookieString, 0); + mCookieService->SetCookieStringInternal(hostURI, aIsForeign, cookieString, + aServerTime, aFromHttp, aAttrs, + isPrivate, dummyChannel); + return true; +} + +} // namespace net +} // namespace mozilla + diff --git a/netwerk/cookie/CookieServiceParent.h b/netwerk/cookie/CookieServiceParent.h new file mode 100644 index 000000000..7be2c97e9 --- /dev/null +++ b/netwerk/cookie/CookieServiceParent.h @@ -0,0 +1,46 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_net_CookieServiceParent_h +#define mozilla_net_CookieServiceParent_h + +#include "mozilla/net/PCookieServiceParent.h" + +class nsCookieService; +namespace mozilla { class NeckoOriginAttributes; } + +namespace mozilla { +namespace net { + +class CookieServiceParent : public PCookieServiceParent +{ +public: + CookieServiceParent(); + virtual ~CookieServiceParent(); + +protected: + virtual void ActorDestroy(ActorDestroyReason aWhy) override; + + virtual bool RecvGetCookieString(const URIParams& aHost, + const bool& aIsForeign, + const bool& aFromHttp, + const NeckoOriginAttributes& aAttrs, + nsCString* aResult) override; + + virtual bool RecvSetCookieString(const URIParams& aHost, + const bool& aIsForeign, + const nsCString& aCookieString, + const nsCString& aServerTime, + const bool& aFromHttp, + const NeckoOriginAttributes& aAttrs) override; + + RefPtr<nsCookieService> mCookieService; +}; + +} // namespace net +} // namespace mozilla + +#endif // mozilla_net_CookieServiceParent_h + diff --git a/netwerk/cookie/PCookieService.ipdl b/netwerk/cookie/PCookieService.ipdl new file mode 100644 index 000000000..7d01096d4 --- /dev/null +++ b/netwerk/cookie/PCookieService.ipdl @@ -0,0 +1,109 @@ +/* -*- 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 URIParams; + +using mozilla::NeckoOriginAttributes from "mozilla/ipc/BackgroundUtils.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 and nsICookieManager2 interfaces) are not + * currently implemented and requesting these interfaces in the child will fail. + * + * @see nsICookieService + * @see nsICookiePermission + */ + +nested(upto inside_cpow) sync protocol PCookieService +{ + manager PNecko; + +parent: + + /* + * Get the complete cookie string associated with the URI. This is a sync + * call in order to avoid race conditions -- for instance, an HTTP response + * on the parent and script access on the child. + * + * @param host + * Same as the 'aURI' argument to nsICookieService.getCookieString. + * @param isForeign + * True if the the request is third party, for purposes of allowing + * access to cookies. This should be obtained from + * mozIThirdPartyUtil.isThirdPartyChannel. Third party requests may be + * rejected depending on user preferences; if those checks are + * disabled, this parameter is ignored. + * @param fromHttp + * Whether the result is for an HTTP request header. This should be + * true for nsICookieService.getCookieStringFromHttp calls, false + * otherwise. + * @param attrs + * The origin attributes from the HTTP channel or document that the + * cookie is being set on. + * + * @see nsICookieService.getCookieString + * @see nsICookieService.getCookieStringFromHttp + * @see mozIThirdPartyUtil.isThirdPartyChannel + * + * @return the resulting cookie string. + */ + nested(inside_cpow) sync GetCookieString(URIParams host, + bool isForeign, + bool fromHttp, + NeckoOriginAttributes attrs) + returns (nsCString result); + + /* + * Set a cookie string. + * + * @param host + * Same as the 'aURI' argument to nsICookieService.setCookieString. + * @param isForeign + * True if the the request is third party, for purposes of allowing + * access to cookies. This should be obtained from + * mozIThirdPartyUtil.isThirdPartyChannel. Third party requests may be + * rejected depending on user preferences; if those checks are + * disabled, this parameter is ignored. + * @param cookieString + * Same as the 'aCookie' argument to nsICookieService.setCookieString. + * @param serverTime + * Same as the 'aServerTime' argument to + * nsICookieService.setCookieStringFromHttp. If the string is empty or + * null (e.g. for non-HTTP requests), the current local time is used. + * @param fromHttp + * Whether the result is for an HTTP request header. This should be + * true for nsICookieService.setCookieStringFromHttp calls, false + * otherwise. + * @param attrs + * The origin attributes from the HTTP channel or document that the + * cookie is being set on. + * + * @see nsICookieService.setCookieString + * @see nsICookieService.setCookieStringFromHttp + * @see mozIThirdPartyUtil.isThirdPartyChannel + */ + nested(inside_cpow) async SetCookieString(URIParams host, + bool isForeign, + nsCString cookieString, + nsCString serverTime, + bool fromHttp, + NeckoOriginAttributes attrs); + + async __delete__(); +}; + +} +} + diff --git a/netwerk/cookie/moz.build b/netwerk/cookie/moz.build new file mode 100644 index 000000000..207790008 --- /dev/null +++ b/netwerk/cookie/moz.build @@ -0,0 +1,55 @@ +# -*- 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/. + +# export required interfaces, even if --disable-cookies has been given +XPIDL_SOURCES += [ + 'nsICookie.idl', + 'nsICookie2.idl', + 'nsICookieManager.idl', + 'nsICookieManager2.idl', + 'nsICookiePermission.idl', + 'nsICookieService.idl', +] + +XPIDL_MODULE = 'necko_cookie' + +if CONFIG['NECKO_COOKIES']: + EXPORTS.mozilla.net = [ + 'CookieServiceChild.h', + 'CookieServiceParent.h', + ] + UNIFIED_SOURCES += [ + 'CookieServiceChild.cpp', + 'CookieServiceParent.cpp', + 'nsCookie.cpp', + ] + # nsCookieService.cpp can't be unified because of symbol conflicts + SOURCES += [ + 'nsCookieService.cpp', + ] + LOCAL_INCLUDES += [ + '/intl/uconv', + ] + + XPCSHELL_TESTS_MANIFESTS += [ + 'test/unit/xpcshell.ini', + 'test/unit_ipc/xpcshell.ini', + ] + + BROWSER_CHROME_MANIFESTS += [ + 'test/browser/browser.ini', + ] + +IPDL_SOURCES = [ + 'PCookieService.ipdl', +] + +include('/ipc/chromium/chromium-config.mozbuild') + +FINAL_LIBRARY = 'xul' + +if CONFIG['GNU_CXX']: + CXXFLAGS += ['-Wno-error=shadow'] diff --git a/netwerk/cookie/nsCookie.cpp b/netwerk/cookie/nsCookie.cpp new file mode 100644 index 000000000..5afe6fe80 --- /dev/null +++ b/netwerk/cookie/nsCookie.cpp @@ -0,0 +1,177 @@ +/* -*- 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 "mozilla/dom/ToJSValue.h" +#include "nsAutoPtr.h" +#include "nsCookie.h" +#include "nsUTF8ConverterService.h" +#include <stdlib.h> + +/****************************************************************************** + * nsCookie: + * string helper impl + ******************************************************************************/ + +// copy aSource strings into contiguous storage provided in aDest1, +// providing terminating nulls for each destination string. +static inline void +StrBlockCopy(const nsACString &aSource1, + const nsACString &aSource2, + const nsACString &aSource3, + const nsACString &aSource4, + char *&aDest1, + char *&aDest2, + char *&aDest3, + char *&aDest4, + char *&aDestEnd) +{ + char *toBegin = aDest1; + nsACString::const_iterator fromBegin, fromEnd; + + *copy_string(aSource1.BeginReading(fromBegin), aSource1.EndReading(fromEnd), toBegin) = char(0); + aDest2 = ++toBegin; + *copy_string(aSource2.BeginReading(fromBegin), aSource2.EndReading(fromEnd), toBegin) = char(0); + aDest3 = ++toBegin; + *copy_string(aSource3.BeginReading(fromBegin), aSource3.EndReading(fromEnd), toBegin) = char(0); + aDest4 = ++toBegin; + *copy_string(aSource4.BeginReading(fromBegin), aSource4.EndReading(fromEnd), toBegin) = char(0); + aDestEnd = toBegin; +} + +/****************************************************************************** + * nsCookie: + * creation helper + ******************************************************************************/ + +// This is a counter that keeps track of the last used creation time, each time +// we create a new nsCookie. 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 +nsCookie::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; +} + +nsCookie * +nsCookie::Create(const nsACString &aName, + const nsACString &aValue, + const nsACString &aHost, + const nsACString &aPath, + int64_t aExpiry, + int64_t aLastAccessed, + int64_t aCreationTime, + bool aIsSession, + bool aIsSecure, + bool aIsHttpOnly, + const OriginAttributes& aOriginAttributes) +{ + // Ensure mValue contains a valid UTF-8 sequence. Otherwise XPConnect will + // truncate the string after the first invalid octet. + RefPtr<nsUTF8ConverterService> converter = new nsUTF8ConverterService(); + nsAutoCString aUTF8Value; + converter->ConvertStringToUTF8(aValue, "UTF-8", false, true, 1, aUTF8Value); + + // find the required string buffer size, adding 4 for the terminating nulls + const uint32_t stringLength = aName.Length() + aUTF8Value.Length() + + aHost.Length() + aPath.Length() + 4; + + // allocate contiguous space for the nsCookie and its strings - + // we store the strings in-line with the nsCookie to save allocations + void *place = ::operator new(sizeof(nsCookie) + stringLength); + if (!place) + return nullptr; + + // assign string members + char *name, *value, *host, *path, *end; + name = static_cast<char *>(place) + sizeof(nsCookie); + StrBlockCopy(aName, aUTF8Value, aHost, aPath, + name, value, host, path, end); + + // If the creationTime given to us is higher than the running maximum, update + // our maximum. + if (aCreationTime > gLastCreationTime) + gLastCreationTime = aCreationTime; + + // construct the cookie. placement new, oh yeah! + return new (place) nsCookie(name, value, host, path, end, + aExpiry, aLastAccessed, aCreationTime, + aIsSession, aIsSecure, aIsHttpOnly, + aOriginAttributes); +} + +size_t +nsCookie::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const +{ + // There is no need to measure the sizes of the individual string + // members, since the strings are stored in-line with the nsCookie. + return aMallocSizeOf(this); +} + +bool +nsCookie::IsStale() const +{ + int64_t currentTimeInUsec = PR_Now(); + + return currentTimeInUsec - LastAccessed() > mCookieStaleThreshold * PR_USEC_PER_SEC; +} + +/****************************************************************************** + * nsCookie: + * xpcom impl + ******************************************************************************/ + +// xpcom getters +NS_IMETHODIMP nsCookie::GetName(nsACString &aName) { aName = Name(); return NS_OK; } +NS_IMETHODIMP nsCookie::GetValue(nsACString &aValue) { aValue = Value(); return NS_OK; } +NS_IMETHODIMP nsCookie::GetHost(nsACString &aHost) { aHost = Host(); return NS_OK; } +NS_IMETHODIMP nsCookie::GetRawHost(nsACString &aHost) { aHost = RawHost(); return NS_OK; } +NS_IMETHODIMP nsCookie::GetPath(nsACString &aPath) { aPath = Path(); return NS_OK; } +NS_IMETHODIMP nsCookie::GetExpiry(int64_t *aExpiry) { *aExpiry = Expiry(); return NS_OK; } +NS_IMETHODIMP nsCookie::GetIsSession(bool *aIsSession) { *aIsSession = IsSession(); return NS_OK; } +NS_IMETHODIMP nsCookie::GetIsDomain(bool *aIsDomain) { *aIsDomain = IsDomain(); return NS_OK; } +NS_IMETHODIMP nsCookie::GetIsSecure(bool *aIsSecure) { *aIsSecure = IsSecure(); return NS_OK; } +NS_IMETHODIMP nsCookie::GetIsHttpOnly(bool *aHttpOnly) { *aHttpOnly = IsHttpOnly(); return NS_OK; } +NS_IMETHODIMP nsCookie::GetStatus(nsCookieStatus *aStatus) { *aStatus = 0; return NS_OK; } +NS_IMETHODIMP nsCookie::GetPolicy(nsCookiePolicy *aPolicy) { *aPolicy = 0; return NS_OK; } +NS_IMETHODIMP nsCookie::GetCreationTime(int64_t *aCreation){ *aCreation = CreationTime(); return NS_OK; } +NS_IMETHODIMP nsCookie::GetLastAccessed(int64_t *aTime) { *aTime = LastAccessed(); return NS_OK; } + +NS_IMETHODIMP +nsCookie::GetOriginAttributes(JSContext *aCx, JS::MutableHandle<JS::Value> aVal) +{ + if (NS_WARN_IF(!ToJSValue(aCx, mOriginAttributes, aVal))) { + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +// compatibility method, for use with the legacy nsICookie interface. +// here, expires == 0 denotes a session cookie. +NS_IMETHODIMP +nsCookie::GetExpires(uint64_t *aExpires) +{ + if (IsSession()) { + *aExpires = 0; + } else { + *aExpires = Expiry() > 0 ? Expiry() : 1; + } + return NS_OK; +} + +NS_IMPL_ISUPPORTS(nsCookie, nsICookie2, nsICookie) diff --git a/netwerk/cookie/nsCookie.h b/netwerk/cookie/nsCookie.h new file mode 100644 index 000000000..812db3f32 --- /dev/null +++ b/netwerk/cookie/nsCookie.h @@ -0,0 +1,140 @@ +/* -*- 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/. */ + +#ifndef nsCookie_h__ +#define nsCookie_h__ + +#include "nsICookie.h" +#include "nsICookie2.h" +#include "nsString.h" + +#include "mozilla/MemoryReporting.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/Preferences.h" + +using mozilla::OriginAttributes; + +/** + * The nsCookie class is the main cookie storage medium for use within cookie + * code. It implements nsICookie2, which extends nsICookie, a frozen interface + * for xpcom access of cookie objects. + */ + +/****************************************************************************** + * nsCookie: + * implementation + ******************************************************************************/ + +class nsCookie : public nsICookie2 +{ + public: + // nsISupports + NS_DECL_ISUPPORTS + NS_DECL_NSICOOKIE + NS_DECL_NSICOOKIE2 + + private: + // for internal use only. see nsCookie::Create(). + nsCookie(const char *aName, + const char *aValue, + const char *aHost, + const char *aPath, + const char *aEnd, + int64_t aExpiry, + int64_t aLastAccessed, + int64_t aCreationTime, + bool aIsSession, + bool aIsSecure, + bool aIsHttpOnly, + const OriginAttributes& aOriginAttributes) + : mName(aName) + , mValue(aValue) + , mHost(aHost) + , mPath(aPath) + , mEnd(aEnd) + , mExpiry(aExpiry) + , mLastAccessed(aLastAccessed) + , mCreationTime(aCreationTime) + // Defaults to 60s + , mCookieStaleThreshold(mozilla::Preferences::GetInt("network.cookie.staleThreshold", 60)) + , mIsSession(aIsSession) + , mIsSecure(aIsSecure) + , mIsHttpOnly(aIsHttpOnly) + , mOriginAttributes(aOriginAttributes) + { + } + + public: + // Generate a unique and monotonically increasing creation time. See comment + // in nsCookie.cpp. + static int64_t GenerateUniqueCreationTime(int64_t aCreationTime); + + // public helper to create an nsCookie object. use |operator delete| + // to destroy an object created by this method. + static nsCookie * Create(const nsACString &aName, + const nsACString &aValue, + const nsACString &aHost, + const nsACString &aPath, + int64_t aExpiry, + int64_t aLastAccessed, + int64_t aCreationTime, + bool aIsSession, + bool aIsSecure, + bool aIsHttpOnly, + const OriginAttributes& aOriginAttributes); + + size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const; + + // fast (inline, non-xpcom) getters + inline const nsDependentCString Name() const { return nsDependentCString(mName, mValue - 1); } + inline const nsDependentCString Value() const { return nsDependentCString(mValue, mHost - 1); } + inline const nsDependentCString Host() const { return nsDependentCString(mHost, mPath - 1); } + inline const nsDependentCString RawHost() const { return nsDependentCString(IsDomain() ? mHost + 1 : mHost, mPath - 1); } + inline const nsDependentCString Path() const { return nsDependentCString(mPath, mEnd); } + inline int64_t Expiry() const { return mExpiry; } // in seconds + inline int64_t LastAccessed() const { return mLastAccessed; } // in microseconds + inline int64_t CreationTime() const { return mCreationTime; } // in microseconds + inline bool IsSession() const { return mIsSession; } + inline bool IsDomain() const { return *mHost == '.'; } + inline bool IsSecure() const { return mIsSecure; } + inline bool IsHttpOnly() const { return mIsHttpOnly; } + + // setters + inline void SetExpiry(int64_t aExpiry) { mExpiry = aExpiry; } + inline void SetLastAccessed(int64_t aTime) { mLastAccessed = aTime; } + inline void SetIsSession(bool aIsSession) { mIsSession = aIsSession; } + // Set the creation time manually, overriding the monotonicity checks in + // Create(). Use with caution! + inline void SetCreationTime(int64_t aTime) { mCreationTime = aTime; } + + bool IsStale() const; + + protected: + virtual ~nsCookie() {} + + private: + // member variables + // we use char* ptrs to store the strings in a contiguous block, + // so we save on the overhead of using nsCStrings. However, we + // store a terminating null for each string, so we can hand them + // out as nsAFlatCStrings. + // + // Please update SizeOfIncludingThis if this strategy changes. + const char *mName; + const char *mValue; + const char *mHost; + const char *mPath; + const char *mEnd; + int64_t mExpiry; + int64_t mLastAccessed; + int64_t mCreationTime; + int64_t mCookieStaleThreshold; + bool mIsSession; + bool mIsSecure; + bool mIsHttpOnly; + mozilla::OriginAttributes mOriginAttributes; +}; + +#endif // nsCookie_h__ diff --git a/netwerk/cookie/nsCookieService.cpp b/netwerk/cookie/nsCookieService.cpp new file mode 100644 index 000000000..cf1d91e2d --- /dev/null +++ b/netwerk/cookie/nsCookieService.cpp @@ -0,0 +1,5164 @@ +/* -*- 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 "mozilla/Attributes.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/Likely.h" +#include "mozilla/Unused.h" + +#include "mozilla/net/CookieServiceChild.h" +#include "mozilla/net/NeckoCommon.h" + +#include "nsCookieService.h" +#include "nsContentUtils.h" +#include "nsIServiceManager.h" + +#include "nsIIOService.h" +#include "nsIPrefBranch.h" +#include "nsIPrefService.h" +#include "nsIScriptError.h" +#include "nsICookiePermission.h" +#include "nsIURI.h" +#include "nsIURL.h" +#include "nsIChannel.h" +#include "nsIFile.h" +#include "nsIObserverService.h" +#include "nsILineInputStream.h" +#include "nsIEffectiveTLDService.h" +#include "nsIIDNService.h" +#include "mozIThirdPartyUtil.h" + +#include "nsTArray.h" +#include "nsCOMArray.h" +#include "nsIMutableArray.h" +#include "nsArrayEnumerator.h" +#include "nsEnumeratorUtils.h" +#include "nsAutoPtr.h" +#include "nsReadableUtils.h" +#include "nsCRT.h" +#include "prprf.h" +#include "nsNetUtil.h" +#include "nsNetCID.h" +#include "nsISimpleEnumerator.h" +#include "nsIInputStream.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsNetCID.h" +#include "mozilla/storage.h" +#include "mozilla/AutoRestore.h" +#include "mozilla/FileUtils.h" +#include "mozilla/Telemetry.h" +#include "nsIAppsService.h" +#include "mozIApplication.h" +#include "mozIApplicationClearPrivateDataParams.h" +#include "nsIConsoleService.h" +#include "nsVariant.h" + +using namespace mozilla; +using namespace mozilla::net; + +// Create key from baseDomain that will access the default cookie namespace. +// TODO: When we figure out what the API will look like for nsICookieManager{2} +// on content processes (see bug 777620), change to use the appropriate app +// namespace. For now those IDLs aren't supported on child processes. +#define DEFAULT_APP_KEY(baseDomain) \ + nsCookieKey(baseDomain, NeckoOriginAttributes()) + +/****************************************************************************** + * nsCookieService impl: + * useful types & constants + ******************************************************************************/ + +static nsCookieService *gCookieService; + +// XXX_hack. See bug 178993. +// This is a hack to hide HttpOnly cookies from older browsers +#define HTTP_ONLY_PREFIX "#HttpOnly_" + +#define COOKIES_FILE "cookies.sqlite" +#define COOKIES_SCHEMA_VERSION 7 + +// parameter indexes; see EnsureReadDomain, EnsureReadComplete and +// ReadCookieDBListener::HandleResult +#define IDX_NAME 0 +#define IDX_VALUE 1 +#define IDX_HOST 2 +#define IDX_PATH 3 +#define IDX_EXPIRY 4 +#define IDX_LAST_ACCESSED 5 +#define IDX_CREATION_TIME 6 +#define IDX_SECURE 7 +#define IDX_HTTPONLY 8 +#define IDX_BASE_DOMAIN 9 +#define IDX_ORIGIN_ATTRIBUTES 10 + +static const int64_t kCookiePurgeAge = + int64_t(30 * 24 * 60 * 60) * PR_USEC_PER_SEC; // 30 days in microseconds + +#define OLD_COOKIE_FILE_NAME "cookies.txt" + +#undef LIMIT +#define LIMIT(x, low, high, default) ((x) >= (low) && (x) <= (high) ? (x) : (default)) + +#undef ADD_TEN_PERCENT +#define ADD_TEN_PERCENT(i) static_cast<uint32_t>((i) + (i)/10) + +// 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 kMaxNumberOfCookies = 3000; +static const uint32_t kMaxCookiesPerHost = 150; +static const uint32_t kMaxBytesPerCookie = 4096; +static const uint32_t kMaxBytesPerPath = 1024; + +// pref string constants +static const char kPrefCookieBehavior[] = "network.cookie.cookieBehavior"; +static const char kPrefMaxNumberOfCookies[] = "network.cookie.maxNumber"; +static const char kPrefMaxCookiesPerHost[] = "network.cookie.maxPerHost"; +static const char kPrefCookiePurgeAge[] = "network.cookie.purgeAge"; +static const char kPrefThirdPartySession[] = "network.cookie.thirdparty.sessionOnly"; +static const char kCookieLeaveSecurityAlone[] = "network.cookie.leave-secure-alone"; + +// For telemetry COOKIE_LEAVE_SECURE_ALONE +#define BLOCKED_SECURE_SET_FROM_HTTP 0 +#define BLOCKED_DOWNGRADE_SECURE 1 +#define DOWNGRADE_SECURE_FROM_SECURE 2 +#define EVICTED_NEWER_INSECURE 3 +#define EVICTED_OLDEST_COOKIE 4 +#define EVICTED_PREFERRED_COOKIE 5 +#define EVICTING_SECURE_BLOCKED 6 + +static void +bindCookieParameters(mozIStorageBindingParamsArray *aParamsArray, + const nsCookieKey &aKey, + const nsCookie *aCookie); + +// struct for temporarily storing cookie attributes during header parsing +struct nsCookieAttributes +{ + nsAutoCString name; + nsAutoCString value; + nsAutoCString host; + nsAutoCString path; + nsAutoCString expires; + nsAutoCString maxage; + int64_t expiryTime; + bool isSession; + bool isSecure; + bool isHttpOnly; +}; + +// stores the nsCookieEntry 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 nsListIter +{ + // default (non-initializing) constructor. + nsListIter() = default; + + // explicit constructor to a given iterator state with entryclass 'aEntry' + // and index 'aIndex'. + explicit + nsListIter(nsCookieEntry *aEntry, nsCookieEntry::IndexType aIndex) + : entry(aEntry) + , index(aIndex) + { + } + + // get the nsCookie * the iterator currently points to. + nsCookie * Cookie() const + { + return entry->GetCookies()[index]; + } + + nsCookieEntry *entry; + nsCookieEntry::IndexType index; +}; + +/****************************************************************************** + * Cookie logging handlers + * used for logging in nsCookieService + ******************************************************************************/ + +// logging handlers +#ifdef MOZ_LOGGING +// in order to do logging, the following environment variables need to be set: +// +// set MOZ_LOG=cookie:3 -- shows rejected cookies +// set MOZ_LOG=cookie:4 -- shows accepted and rejected cookies +// set MOZ_LOG_FILE=cookie.log +// +#include "mozilla/Logging.h" +#endif + +// define logging macros for convenience +#define SET_COOKIE true +#define GET_COOKIE false + +static LazyLogModule gCookieLog("cookie"); + +#define COOKIE_LOGFAILURE(a, b, c, d) LogFailure(a, b, c, d) +#define COOKIE_LOGSUCCESS(a, b, c, d, e) LogSuccess(a, b, c, d, e) + +#define COOKIE_LOGEVICTED(a, details) \ + PR_BEGIN_MACRO \ + if (MOZ_LOG_TEST(gCookieLog, LogLevel::Debug)) \ + 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 + +static void +LogFailure(bool aSetCookie, nsIURI *aHostURI, const char *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)); + + PRExplodedTime explodedTime; + PR_ExplodeTime(PR_Now(), PR_GMTParameters, &explodedTime); + char timeString[40]; + PR_FormatTimeUSEnglish(timeString, 40, "%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 +LogCookie(nsCookie *aCookie) +{ + PRExplodedTime explodedTime; + PR_ExplodeTime(PR_Now(), PR_GMTParameters, &explodedTime); + char timeString[40]; + PR_FormatTimeUSEnglish(timeString, 40, "%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, 40, "%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, 40, "%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")); + } +} + +static void +LogSuccess(bool aSetCookie, nsIURI *aHostURI, const char *aCookieString, nsCookie *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)); + 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 +LogEvicted(nsCookie *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")); +} + +// inline wrappers to make passing in nsAFlatCStrings easier +static inline void +LogFailure(bool aSetCookie, nsIURI *aHostURI, const nsAFlatCString &aCookieString, const char *aReason) +{ + LogFailure(aSetCookie, aHostURI, aCookieString.get(), aReason); +} + +static inline void +LogSuccess(bool aSetCookie, nsIURI *aHostURI, const nsAFlatCString &aCookieString, nsCookie *aCookie, bool aReplacing) +{ + LogSuccess(aSetCookie, aHostURI, aCookieString.get(), aCookie, aReplacing); +} + +#ifdef DEBUG +#define NS_ASSERT_SUCCESS(res) \ + PR_BEGIN_MACRO \ + nsresult __rv = res; /* Do not evaluate |res| more than once! */ \ + if (NS_FAILED(__rv)) { \ + char *msg = PR_smprintf("NS_ASSERT_SUCCESS(%s) failed with result 0x%X", \ + #res, __rv); \ + NS_ASSERTION(NS_SUCCEEDED(__rv), msg); \ + PR_smprintf_free(msg); \ + } \ + PR_END_MACRO +#else +#define NS_ASSERT_SUCCESS(res) PR_BEGIN_MACRO /* nothing */ PR_END_MACRO +#endif + +/****************************************************************************** + * DBListenerErrorHandler impl: + * Parent class for our async storage listeners that handles the logging of + * errors. + ******************************************************************************/ +class DBListenerErrorHandler : public mozIStorageStatementCallback +{ +protected: + explicit DBListenerErrorHandler(DBState* dbState) : mDBState(dbState) { } + RefPtr<DBState> mDBState; + 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. + gCookieService->HandleCorruptDB(mDBState); + + 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(DBState* dbState) : DBListenerErrorHandler(dbState) { } + NS_IMETHOD HandleResult(mozIStorageResultSet*) override + { + NS_NOTREACHED("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 corruptFlag say + // so. + if (mDBState->corruptFlag == DBState::REBUILDING && + aReason == mozIStorageStatementCallback::REASON_FINISHED) { + COOKIE_LOGSTRING(LogLevel::Debug, + ("InsertCookieDBListener::HandleCompletion(): rebuild complete")); + mDBState->corruptFlag = DBState::OK; + } + 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(DBState* dbState) : DBListenerErrorHandler(dbState) { } + NS_IMETHOD HandleResult(mozIStorageResultSet*) override + { + NS_NOTREACHED("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(DBState* dbState) : DBListenerErrorHandler(dbState) { } + NS_IMETHOD HandleResult(mozIStorageResultSet*) override + { + NS_NOTREACHED("Unexpected call to RemoveCookieDBListener::HandleResult"); + return NS_OK; + } + NS_IMETHOD HandleCompletion(uint16_t aReason) override + { + return NS_OK; + } +}; + +NS_IMPL_ISUPPORTS(RemoveCookieDBListener, mozIStorageStatementCallback) + +/****************************************************************************** + * ReadCookieDBListener impl: + * mozIStorageStatementCallback used to track asynchronous removal operations. + ******************************************************************************/ +class ReadCookieDBListener final : public DBListenerErrorHandler +{ +private: + const char *GetOpType() override { return "READ"; } + bool mCanceled; + + ~ReadCookieDBListener() = default; + +public: + NS_DECL_ISUPPORTS + + explicit ReadCookieDBListener(DBState* dbState) + : DBListenerErrorHandler(dbState) + , mCanceled(false) + { + } + + void Cancel() { mCanceled = true; } + + NS_IMETHOD HandleResult(mozIStorageResultSet *aResult) override + { + nsCOMPtr<mozIStorageRow> row; + + while (true) { + DebugOnly<nsresult> rv = aResult->GetNextRow(getter_AddRefs(row)); + NS_ASSERT_SUCCESS(rv); + + if (!row) + break; + + CookieDomainTuple *tuple = mDBState->hostArray.AppendElement(); + row->GetUTF8String(IDX_BASE_DOMAIN, tuple->key.mBaseDomain); + + nsAutoCString suffix; + row->GetUTF8String(IDX_ORIGIN_ATTRIBUTES, suffix); + DebugOnly<bool> success = tuple->key.mOriginAttributes.PopulateFromSuffix(suffix); + MOZ_ASSERT(success); + + tuple->cookie = + gCookieService->GetCookieFromRow(row, tuple->key.mOriginAttributes); + } + + return NS_OK; + } + NS_IMETHOD HandleCompletion(uint16_t aReason) override + { + // Process the completion of the read operation. If we have been canceled, + // we cannot assume that the cookieservice still has an open connection + // or that it even refers to the same database, so we must return early. + // Conversely, the cookieservice guarantees that if we have not been + // canceled, the database connection is still alive and we can safely + // operate on it. + + if (mCanceled) { + // We may receive a REASON_FINISHED after being canceled; + // tweak the reason accordingly. + aReason = mozIStorageStatementCallback::REASON_CANCELED; + } + + switch (aReason) { + case mozIStorageStatementCallback::REASON_FINISHED: + gCookieService->AsyncReadComplete(); + break; + case mozIStorageStatementCallback::REASON_CANCELED: + // Nothing more to do here. The partially read data has already been + // thrown away. + COOKIE_LOGSTRING(LogLevel::Debug, ("Read canceled")); + break; + case mozIStorageStatementCallback::REASON_ERROR: + // Nothing more to do here. DBListenerErrorHandler::HandleError() + // can handle it. + COOKIE_LOGSTRING(LogLevel::Debug, ("Read error")); + break; + default: + NS_NOTREACHED("invalid reason"); + } + return NS_OK; + } +}; + +NS_IMPL_ISUPPORTS(ReadCookieDBListener, mozIStorageStatementCallback) + +/****************************************************************************** + * CloseCookieDBListener imp: + * Static mozIStorageCompletionCallback used to notify when the database is + * successfully closed. + ******************************************************************************/ +class CloseCookieDBListener final : public mozIStorageCompletionCallback +{ + ~CloseCookieDBListener() = default; + +public: + explicit CloseCookieDBListener(DBState* dbState) : mDBState(dbState) { } + RefPtr<DBState> mDBState; + NS_DECL_ISUPPORTS + + NS_IMETHOD Complete(nsresult, nsISupports*) override + { + gCookieService->HandleDBClosed(mDBState); + return NS_OK; + } +}; + +NS_IMPL_ISUPPORTS(CloseCookieDBListener, mozIStorageCompletionCallback) + +namespace { + +class AppClearDataObserver final : public nsIObserver { + + ~AppClearDataObserver() = default; + +public: + NS_DECL_ISUPPORTS + + // nsIObserver implementation. + NS_IMETHOD + Observe(nsISupports *aSubject, const char *aTopic, const char16_t *aData) override + { + MOZ_ASSERT(!nsCRT::strcmp(aTopic, TOPIC_CLEAR_ORIGIN_DATA)); + + MOZ_ASSERT(XRE_IsParentProcess()); + + nsCOMPtr<nsICookieManager2> cookieManager + = do_GetService(NS_COOKIEMANAGER_CONTRACTID); + MOZ_ASSERT(cookieManager); + + return cookieManager->RemoveCookiesWithOriginAttributes(nsDependentString(aData), EmptyCString()); + } +}; + +NS_IMPL_ISUPPORTS(AppClearDataObserver, nsIObserver) + +} // namespace + +size_t +nsCookieKey::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const +{ + return mBaseDomain.SizeOfExcludingThisIfUnshared(aMallocSizeOf); +} + +size_t +nsCookieEntry::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const +{ + size_t amount = nsCookieKey::SizeOfExcludingThis(aMallocSizeOf); + + amount += mCookies.ShallowSizeOfExcludingThis(aMallocSizeOf); + for (uint32_t i = 0; i < mCookies.Length(); ++i) { + amount += mCookies[i]->SizeOfIncludingThis(aMallocSizeOf); + } + + return amount; +} + +size_t +CookieDomainTuple::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const +{ + size_t amount = 0; + + amount += key.SizeOfExcludingThis(aMallocSizeOf); + amount += cookie->SizeOfIncludingThis(aMallocSizeOf); + + return amount; +} + +size_t +DBState::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const +{ + size_t amount = 0; + + amount += aMallocSizeOf(this); + amount += hostTable.SizeOfExcludingThis(aMallocSizeOf); + amount += hostArray.ShallowSizeOfExcludingThis(aMallocSizeOf); + for (uint32_t i = 0; i < hostArray.Length(); ++i) { + amount += hostArray[i].SizeOfExcludingThis(aMallocSizeOf); + } + amount += readSet.SizeOfExcludingThis(aMallocSizeOf); + + return amount; +} + +/****************************************************************************** + * nsCookieService impl: + * singleton instance ctor/dtor methods + ******************************************************************************/ + +nsICookieService* +nsCookieService::GetXPCOMSingleton() +{ + if (IsNeckoChild()) + return CookieServiceChild::GetSingleton(); + + return GetSingleton(); +} + +nsCookieService* +nsCookieService::GetSingleton() +{ + NS_ASSERTION(!IsNeckoChild(), "not a parent process"); + + if (gCookieService) { + NS_ADDREF(gCookieService); + return gCookieService; + } + + // Create a new singleton nsCookieService. + // We AddRef only once since XPCOM has rules about the ordering of module + // teardowns - by the time our module destructor is called, it's too late to + // Release our members (e.g. nsIObserverService and nsIPrefBranch), since GC + // cycles have already been completed and would result in serious leaks. + // See bug 209571. + gCookieService = new nsCookieService(); + if (gCookieService) { + NS_ADDREF(gCookieService); + if (NS_FAILED(gCookieService->Init())) { + NS_RELEASE(gCookieService); + } + } + + return gCookieService; +} + +/* static */ void +nsCookieService::AppClearDataObserverInit() +{ + nsCOMPtr<nsIObserverService> observerService = services::GetObserverService(); + nsCOMPtr<nsIObserver> obs = new AppClearDataObserver(); + observerService->AddObserver(obs, TOPIC_CLEAR_ORIGIN_DATA, + /* ownsWeak= */ false); +} + +/****************************************************************************** + * nsCookieService impl: + * public methods + ******************************************************************************/ + +NS_IMPL_ISUPPORTS(nsCookieService, + nsICookieService, + nsICookieManager, + nsICookieManager2, + nsIObserver, + nsISupportsWeakReference, + nsIMemoryReporter) + +nsCookieService::nsCookieService() + : mDBState(nullptr) + , mCookieBehavior(nsICookieService::BEHAVIOR_ACCEPT) + , mThirdPartySession(false) + , mLeaveSecureAlone(true) + , mMaxNumberOfCookies(kMaxNumberOfCookies) + , mMaxCookiesPerHost(kMaxCookiesPerHost) + , mCookiePurgeAge(kCookiePurgeAge) +{ +} + +nsresult +nsCookieService::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 pref and observer + nsCOMPtr<nsIPrefBranch> prefBranch = do_GetService(NS_PREFSERVICE_CONTRACTID); + if (prefBranch) { + prefBranch->AddObserver(kPrefCookieBehavior, this, true); + prefBranch->AddObserver(kPrefMaxNumberOfCookies, this, true); + prefBranch->AddObserver(kPrefMaxCookiesPerHost, this, true); + prefBranch->AddObserver(kPrefCookiePurgeAge, this, true); + prefBranch->AddObserver(kPrefThirdPartySession, this, true); + prefBranch->AddObserver(kCookieLeaveSecurityAlone, this, true); + PrefChanged(prefBranch); + } + + mStorageService = do_GetService("@mozilla.org/storage/service;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // Init our default, and possibly private DBStates. + InitDBStates(); + + RegisterWeakMemoryReporter(this); + + nsCOMPtr<nsIObserverService> os = mozilla::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); + + mPermissionService = do_GetService(NS_COOKIEPERMISSION_CONTRACTID); + if (!mPermissionService) { + NS_WARNING("nsICookiePermission implementation not available - some features won't work!"); + COOKIE_LOGSTRING(LogLevel::Warning, ("Init(): nsICookiePermission implementation not available")); + } + + return NS_OK; +} + +void +nsCookieService::InitDBStates() +{ + NS_ASSERTION(!mDBState, "already have a DBState"); + NS_ASSERTION(!mDefaultDBState, "already have a default DBState"); + NS_ASSERTION(!mPrivateDBState, "already have a private DBState"); + + // Create a new default DBState and set our current one. + mDefaultDBState = new DBState(); + mDBState = mDefaultDBState; + + mPrivateDBState = new DBState(); + + // Get our cookie file. + nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(mDefaultDBState->cookieFile)); + if (NS_FAILED(rv)) { + // We've already set up our DBStates appropriately; nothing more to do. + COOKIE_LOGSTRING(LogLevel::Warning, + ("InitDBStates(): couldn't get cookie file")); + return; + } + mDefaultDBState->cookieFile->AppendNative(NS_LITERAL_CSTRING(COOKIES_FILE)); + + // Attempt to open and read the database. If TryInitDB() returns RESULT_RETRY, + // do so. + OpenDBResult result = TryInitDB(false); + if (result == RESULT_RETRY) { + // Database may be corrupt. Synchronously close the connection, clean up the + // default DBState, and try again. + COOKIE_LOGSTRING(LogLevel::Warning, ("InitDBStates(): retrying TryInitDB()")); + CleanupCachedStatements(); + CleanupDefaultDBConnection(); + result = 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, + ("InitDBStates(): 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. + CleanupCachedStatements(); + CleanupDefaultDBConnection(); + } +} + +namespace { + +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 appId, inIsolatedMozBrowser; + + rv = aFunctionArguments->GetInt32(0, &appId); + NS_ENSURE_SUCCESS(rv, rv); + rv = aFunctionArguments->GetInt32(1, &inIsolatedMozBrowser); + NS_ENSURE_SUCCESS(rv, rv); + + // Create an originAttributes object by appId and inIsolatedMozBrowser. + // Then create the originSuffix string from this object. + NeckoOriginAttributes attrs(appId, (inIsolatedMozBrowser ? 1 : 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; + NeckoOriginAttributes 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.mAppId); + 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; + NeckoOriginAttributes 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; +} + +} // namespace + +/* 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 DBState. + */ +OpenDBResult +nsCookieService::TryInitDB(bool aRecreateDB) +{ + NS_ASSERTION(!mDefaultDBState->dbConn, "nonnull dbConn"); + NS_ASSERTION(!mDefaultDBState->stmtInsert, "nonnull stmtInsert"); + NS_ASSERTION(!mDefaultDBState->insertListener, "nonnull insertListener"); + NS_ASSERTION(!mDefaultDBState->syncConn, "nonnull syncConn"); + + // 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; + mDefaultDBState->cookieFile->Clone(getter_AddRefs(backupFile)); + rv = backupFile->MoveToNative(nullptr, + NS_LITERAL_CSTRING(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(mDefaultDBState->cookieFile); + + // 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(mDefaultDBState->cookieFile, + getter_AddRefs(mDefaultDBState->dbConn)); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + } + + // Set up our listeners. + mDefaultDBState->insertListener = new InsertCookieDBListener(mDefaultDBState); + mDefaultDBState->updateListener = new UpdateCookieDBListener(mDefaultDBState); + mDefaultDBState->removeListener = new RemoveCookieDBListener(mDefaultDBState); + mDefaultDBState->closeListener = new CloseCookieDBListener(mDefaultDBState); + + // Grow cookie db in 512KB increments + mDefaultDBState->dbConn->SetGrowthIncrement(512 * 1024, EmptyCString()); + + bool tableExists = false; + mDefaultDBState->dbConn->TableExists(NS_LITERAL_CSTRING("moz_cookies"), + &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 = mDefaultDBState->dbConn->GetSchemaVersion(&dbSchemaVersion); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + // Start a transaction for the whole migration block. + mozStorageTransaction transaction(mDefaultDBState->dbConn, true); + + switch (dbSchemaVersion) { + // Upgrading. + // Every time you increment the database schema, you need to implement + // the upgrading code from the previous version to the new one. If migration + // fails for any reason, it's a bug -- so we return RESULT_RETRY such that + // the original database will be saved, in the hopes that we might one day + // see it and fix it. + case 1: + { + // Add the lastAccessed column to the table. + rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "ALTER TABLE moz_cookies ADD lastAccessed INTEGER")); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + } + // Fall through to the next upgrade. + MOZ_FALLTHROUGH; + + case 2: + { + // Add the baseDomain column and index to the table. + rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "ALTER TABLE moz_cookies ADD baseDomain TEXT")); + 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 = mDefaultDBState->dbConn->CreateStatement(NS_LITERAL_CSTRING( + "SELECT id, host FROM moz_cookies"), getter_AddRefs(select)); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + nsCOMPtr<mozIStorageStatement> update; + rv = mDefaultDBState->dbConn->CreateStatement(NS_LITERAL_CSTRING( + "UPDATE moz_cookies SET baseDomain = :baseDomain WHERE id = :id"), + getter_AddRefs(update)); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + nsCString baseDomain, 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 = GetBaseDomainFromHost(host, baseDomain); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + mozStorageStatementScoper scoper(update); + + rv = update->BindUTF8StringByName(NS_LITERAL_CSTRING("baseDomain"), + baseDomain); + NS_ASSERT_SUCCESS(rv); + rv = update->BindInt64ByName(NS_LITERAL_CSTRING("id"), + id); + NS_ASSERT_SUCCESS(rv); + + rv = update->ExecuteStep(&hasResult); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + } + + // Create an index on baseDomain. + rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE INDEX moz_basedomain ON moz_cookies (baseDomain)")); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + } + // Fall through to the next upgrade. + MOZ_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 = mDefaultDBState->dbConn->CreateStatement(NS_LITERAL_CSTRING( + "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 = mDefaultDBState->dbConn->CreateStatement(NS_LITERAL_CSTRING( + "DELETE FROM moz_cookies WHERE id = :id"), + 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, host1, 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, host2, 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(NS_LITERAL_CSTRING("id"), + id1); + NS_ASSERT_SUCCESS(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 = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "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 = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "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 = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE UNIQUE INDEX moz_uniqueid " + "ON moz_cookies (name, host, path)")); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + } + // Fall through to the next upgrade. + MOZ_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 = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "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 = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP INDEX moz_basedomain")); + 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 = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "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 = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TABLE moz_cookies_old")); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + COOKIE_LOGSTRING(LogLevel::Debug, + ("Upgraded database to schema version 5")); + } + // Fall through to the next upgrade. + MOZ_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 = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "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 = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP INDEX moz_basedomain")); + 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); + + NS_NAMED_LITERAL_CSTRING(convertToOriginAttrsName, + "CONVERT_TO_ORIGIN_ATTRIBUTES"); + + rv = mDefaultDBState->dbConn->CreateFunction(convertToOriginAttrsName, + 2, convertToOriginAttrs); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "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 = mDefaultDBState->dbConn->RemoveFunction(convertToOriginAttrsName); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + // Drop old table + rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TABLE moz_cookies_old")); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + COOKIE_LOGSTRING(LogLevel::Debug, + ("Upgraded database to schema version 6")); + } + MOZ_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 = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "ALTER TABLE moz_cookies ADD appId INTEGER DEFAULT 0;")); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "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); + + NS_NAMED_LITERAL_CSTRING(setAppIdName, "SET_APP_ID"); + + rv = mDefaultDBState->dbConn->CreateFunction(setAppIdName, 1, setAppId); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + nsCOMPtr<mozIStorageFunction> + setInBrowser(new SetInBrowserFromOriginAttributesSQLFunction()); + NS_ENSURE_TRUE(setInBrowser, RESULT_RETRY); + + NS_NAMED_LITERAL_CSTRING(setInBrowserName, "SET_IN_BROWSER"); + + rv = mDefaultDBState->dbConn->CreateFunction(setInBrowserName, 1, + setInBrowser); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "UPDATE moz_cookies SET appId = SET_APP_ID(originAttributes), " + "inBrowserElement = SET_IN_BROWSER(originAttributes);" + )); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + rv = mDefaultDBState->dbConn->RemoveFunction(setAppIdName); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + rv = mDefaultDBState->dbConn->RemoveFunction(setInBrowserName); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + COOKIE_LOGSTRING(LogLevel::Debug, + ("Upgraded database to schema version 7")); + } + + // No more upgrades. Update the schema version. + rv = mDefaultDBState->dbConn->SetSchemaVersion(COOKIES_SCHEMA_VERSION); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + MOZ_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 = mDefaultDBState->dbConn->SetSchemaVersion(COOKIES_SCHEMA_VERSION); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + } + // fall through to downgrade check + MOZ_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 = mDefaultDBState->dbConn->CreateStatement(NS_LITERAL_CSTRING( + "SELECT " + "id, " + "baseDomain, " + "originAttributes, " + "name, " + "value, " + "host, " + "path, " + "expiry, " + "lastAccessed, " + "creationTime, " + "isSecure, " + "isHttpOnly " + "FROM moz_cookies"), getter_AddRefs(stmt)); + if (NS_SUCCEEDED(rv)) + break; + + // our columns aren't there - drop the table! + rv = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TABLE moz_cookies")); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + rv = CreateTable(); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + } + break; + } + } + + // make operations on the table asynchronous, for performance + mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "PRAGMA synchronous = OFF")); + + // Use write-ahead-logging for performance. We cap the autocheckpoint limit at + // 16 pages (around 500KB). + mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + MOZ_STORAGE_UNIQUIFY_QUERY_STR "PRAGMA journal_mode = WAL")); + mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "PRAGMA wal_autocheckpoint = 16")); + + // cache frequently used statements (for insertion, deletion, and updating) + rv = mDefaultDBState->dbConn->CreateAsyncStatement(NS_LITERAL_CSTRING( + "INSERT INTO moz_cookies (" + "baseDomain, " + "originAttributes, " + "name, " + "value, " + "host, " + "path, " + "expiry, " + "lastAccessed, " + "creationTime, " + "isSecure, " + "isHttpOnly" + ") VALUES (" + ":baseDomain, " + ":originAttributes, " + ":name, " + ":value, " + ":host, " + ":path, " + ":expiry, " + ":lastAccessed, " + ":creationTime, " + ":isSecure, " + ":isHttpOnly" + ")"), + getter_AddRefs(mDefaultDBState->stmtInsert)); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + rv = mDefaultDBState->dbConn->CreateAsyncStatement(NS_LITERAL_CSTRING( + "DELETE FROM moz_cookies " + "WHERE name = :name AND host = :host AND path = :path"), + getter_AddRefs(mDefaultDBState->stmtDelete)); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + rv = mDefaultDBState->dbConn->CreateAsyncStatement(NS_LITERAL_CSTRING( + "UPDATE moz_cookies SET lastAccessed = :lastAccessed " + "WHERE name = :name AND host = :host AND path = :path"), + getter_AddRefs(mDefaultDBState->stmtUpdate)); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + // 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(); + + nsCOMPtr<nsIFile> oldCookieFile; + rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(oldCookieFile)); + if (NS_FAILED(rv)) return RESULT_OK; + + // Import cookies, and clean up the old file regardless of success or failure. + // Note that we have to switch out our DBState temporarily, in case we're in + // private browsing mode; otherwise ImportCookies() won't be happy. + DBState* initialState = mDBState; + mDBState = mDefaultDBState; + oldCookieFile->AppendNative(NS_LITERAL_CSTRING(OLD_COOKIE_FILE_NAME)); + ImportCookies(oldCookieFile); + oldCookieFile->Remove(false); + mDBState = initialState; + + return RESULT_OK; +} + +// Sets the schema version and creates the moz_cookies table. +nsresult +nsCookieService::CreateTable() +{ + // Set the schema version, before creating the table. + nsresult rv = mDefaultDBState->dbConn->SetSchemaVersion( + COOKIES_SCHEMA_VERSION); + 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 = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "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)" + ")")); + if (NS_FAILED(rv)) return rv; + + // Create an index on baseDomain. + return mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE INDEX moz_basedomain ON moz_cookies (baseDomain, " + "originAttributes)")); +} + +// Sets the schema version and creates the moz_cookies table. +nsresult +nsCookieService::CreateTableForSchemaVersion6() +{ + // Set the schema version, before creating the table. + nsresult rv = mDefaultDBState->dbConn->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 = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "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 mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE INDEX moz_basedomain ON moz_cookies (baseDomain, " + "originAttributes)")); +} + +// Sets the schema version and creates the moz_cookies table. +nsresult +nsCookieService::CreateTableForSchemaVersion5() +{ + // Set the schema version, before creating the table. + nsresult rv = mDefaultDBState->dbConn->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 = mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "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 mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE INDEX moz_basedomain ON moz_cookies (baseDomain, " + "appId, " + "inBrowserElement)")); +} + +void +nsCookieService::CloseDBStates() +{ + // Null out our private and pointer DBStates regardless. + mPrivateDBState = nullptr; + mDBState = nullptr; + + // If we don't have a default DBState, we're done. + if (!mDefaultDBState) + return; + + // Cleanup cached statements before we can close anything. + CleanupCachedStatements(); + + if (mDefaultDBState->dbConn) { + // Cancel any pending read. No further results will be received by our + // read listener. + if (mDefaultDBState->pendingRead) { + CancelAsyncRead(true); + } + + // Asynchronously close the connection. We will null it below. + mDefaultDBState->dbConn->AsyncClose(mDefaultDBState->closeListener); + } + + CleanupDefaultDBConnection(); + + mDefaultDBState = nullptr; +} + +// Null out the statements. +// This must be done before closing the connection. +void +nsCookieService::CleanupCachedStatements() +{ + mDefaultDBState->stmtInsert = nullptr; + mDefaultDBState->stmtDelete = nullptr; + mDefaultDBState->stmtUpdate = 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 +nsCookieService::CleanupDefaultDBConnection() +{ + MOZ_ASSERT(!mDefaultDBState->stmtInsert, "stmtInsert has been cleaned up"); + MOZ_ASSERT(!mDefaultDBState->stmtDelete, "stmtDelete has been cleaned up"); + MOZ_ASSERT(!mDefaultDBState->stmtUpdate, "stmtUpdate has been cleaned up"); + + // Null out the database connections. If 'dbConn' 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. + mDefaultDBState->dbConn = nullptr; + mDefaultDBState->syncConn = nullptr; + + // Manually null out our listeners. This is necessary because they hold a + // strong ref to the DBState itself. They'll stay alive until whatever + // statements are still executing complete. + mDefaultDBState->readListener = nullptr; + mDefaultDBState->insertListener = nullptr; + mDefaultDBState->updateListener = nullptr; + mDefaultDBState->removeListener = nullptr; + mDefaultDBState->closeListener = nullptr; +} + +void +nsCookieService::HandleDBClosed(DBState* aDBState) +{ + COOKIE_LOGSTRING(LogLevel::Debug, + ("HandleDBClosed(): DBState %x closed", aDBState)); + + nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService(); + + switch (aDBState->corruptFlag) { + case DBState::OK: { + // Database is healthy. Notify of closure. + if (os) { + os->NotifyObservers(nullptr, "cookie-db-closed", nullptr); + } + break; + } + case DBState::CLOSING_FOR_REBUILD: { + // Our close finished. Start the rebuild, and notify of db closure later. + RebuildCorruptDB(aDBState); + break; + } + case DBState::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; + aDBState->cookieFile->Clone(getter_AddRefs(backupFile)); + nsresult rv = backupFile->MoveToNative(nullptr, + NS_LITERAL_CSTRING(COOKIES_FILE ".bak-rebuild")); + + COOKIE_LOGSTRING(LogLevel::Warning, + ("HandleDBClosed(): DBState %x encountered error rebuilding db; move to " + "'cookies.sqlite.bak-rebuild' gave rv 0x%x", aDBState, rv)); + if (os) { + os->NotifyObservers(nullptr, "cookie-db-closed", nullptr); + } + break; + } + } +} + +void +nsCookieService::HandleCorruptDB(DBState* aDBState) +{ + if (mDefaultDBState != aDBState) { + // We've either closed the state or we've switched profiles. It's getting + // a bit late to rebuild -- bail instead. + COOKIE_LOGSTRING(LogLevel::Warning, + ("HandleCorruptDB(): DBState %x is already closed, aborting", aDBState)); + return; + } + + COOKIE_LOGSTRING(LogLevel::Debug, + ("HandleCorruptDB(): DBState %x has corruptFlag %u", aDBState, + aDBState->corruptFlag)); + + // Mark the database corrupt, so the close listener can begin reconstructing + // it. + switch (mDefaultDBState->corruptFlag) { + case DBState::OK: { + // Move to 'closing' state. + mDefaultDBState->corruptFlag = DBState::CLOSING_FOR_REBUILD; + + // Cancel any pending read and close the database. If we do have an + // in-flight read we want to throw away all the results so far -- we have no + // idea how consistent the database is. Note that we may have already + // canceled the read but not emptied our readSet; do so now. + mDefaultDBState->readSet.Clear(); + if (mDefaultDBState->pendingRead) { + CancelAsyncRead(true); + mDefaultDBState->syncConn = nullptr; + } + + CleanupCachedStatements(); + mDefaultDBState->dbConn->AsyncClose(mDefaultDBState->closeListener); + CleanupDefaultDBConnection(); + break; + } + case DBState::CLOSING_FOR_REBUILD: { + // We had an error while waiting for close completion. That's OK, just + // ignore it -- we're rebuilding anyway. + return; + } + case DBState::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 (mDefaultDBState->dbConn) { + mDefaultDBState->dbConn->AsyncClose(mDefaultDBState->closeListener); + } + CleanupDefaultDBConnection(); + break; + } + } +} + +void +nsCookieService::RebuildCorruptDB(DBState* aDBState) +{ + NS_ASSERTION(!aDBState->dbConn, "shouldn't have an open db connection"); + NS_ASSERTION(aDBState->corruptFlag == DBState::CLOSING_FOR_REBUILD, + "should be in CLOSING_FOR_REBUILD state"); + + nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService(); + + aDBState->corruptFlag = DBState::REBUILDING; + + if (mDefaultDBState != aDBState) { + // We've either closed the state or we've switched profiles. It's getting + // a bit late to rebuild -- bail instead. In any case, we were waiting + // on rebuild completion to notify of the db closure, which won't happen -- + // do so now. + COOKIE_LOGSTRING(LogLevel::Warning, + ("RebuildCorruptDB(): DBState %x is stale, aborting", aDBState)); + if (os) { + os->NotifyObservers(nullptr, "cookie-db-closed", nullptr); + } + return; + } + + COOKIE_LOGSTRING(LogLevel::Debug, + ("RebuildCorruptDB(): creating new database")); + + // The database has been closed, and we're ready to rebuild. Open a + // connection. + OpenDBResult result = TryInitDB(true); + 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)); + CleanupCachedStatements(); + CleanupDefaultDBConnection(); + mDefaultDBState->corruptFlag = DBState::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); + } + + // Enumerate the hash, and add cookies to the params array. + mozIStorageAsyncStatement* stmt = aDBState->stmtInsert; + nsCOMPtr<mozIStorageBindingParamsArray> paramsArray; + stmt->NewBindingParamsArray(getter_AddRefs(paramsArray)); + for (auto iter = aDBState->hostTable.Iter(); !iter.Done(); iter.Next()) { + nsCookieEntry* entry = iter.Get(); + + const nsCookieEntry::ArrayType& cookies = entry->GetCookies(); + for (nsCookieEntry::IndexType i = 0; i < cookies.Length(); ++i) { + nsCookie* cookie = cookies[i]; + + if (!cookie->IsSession()) { + bindCookieParameters(paramsArray, nsCookieKey(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")); + mDefaultDBState->corruptFlag = DBState::OK; + return; + } + + // Execute the statement. If any errors crop up, we won't try again. + DebugOnly<nsresult> rv = stmt->BindParameters(paramsArray); + NS_ASSERT_SUCCESS(rv); + nsCOMPtr<mozIStoragePendingStatement> handle; + rv = stmt->ExecuteAsync(aDBState->insertListener, getter_AddRefs(handle)); + NS_ASSERT_SUCCESS(rv); +} + +nsCookieService::~nsCookieService() +{ + CloseDBStates(); + + UnregisterWeakMemoryReporter(this); + + gCookieService = nullptr; +} + +NS_IMETHODIMP +nsCookieService::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 DBStates before + // changing. + CloseDBStates(); + + } else if (!strcmp(aTopic, "profile-do-change")) { + NS_ASSERTION(!mDefaultDBState, "shouldn't have a default DBState"); + NS_ASSERTION(!mPrivateDBState, "shouldn't have a private DBState"); + + // 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. + InitDBStates(); + + } else if (!strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) { + nsCOMPtr<nsIPrefBranch> prefBranch = do_QueryInterface(aSubject); + if (prefBranch) + PrefChanged(prefBranch); + + } else if (!strcmp(aTopic, "last-pb-context-exited")) { + // Flush all the cookies stored by private browsing contexts + mPrivateDBState = new DBState(); + } + + + return NS_OK; +} + +NS_IMETHODIMP +nsCookieService::GetCookieString(nsIURI *aHostURI, + nsIChannel *aChannel, + char **aCookie) +{ + return GetCookieStringCommon(aHostURI, aChannel, false, aCookie); +} + +NS_IMETHODIMP +nsCookieService::GetCookieStringFromHttp(nsIURI *aHostURI, + nsIURI *aFirstURI, + nsIChannel *aChannel, + char **aCookie) +{ + return GetCookieStringCommon(aHostURI, aChannel, true, aCookie); +} + +nsresult +nsCookieService::GetCookieStringCommon(nsIURI *aHostURI, + nsIChannel *aChannel, + bool aHttpBound, + char** aCookie) +{ + NS_ENSURE_ARG(aHostURI); + NS_ENSURE_ARG(aCookie); + + // Determine whether the request is foreign. Failure is acceptable. + bool isForeign = true; + mThirdPartyUtil->IsThirdPartyChannel(aChannel, aHostURI, &isForeign); + + // Get originAttributes. + NeckoOriginAttributes attrs; + if (aChannel) { + NS_GetOriginAttributes(aChannel, attrs); + } + + bool isPrivate = aChannel && NS_UsePrivateBrowsing(aChannel); + + nsAutoCString result; + GetCookieStringInternal(aHostURI, isForeign, aHttpBound, attrs, + isPrivate, result); + *aCookie = result.IsEmpty() ? nullptr : ToNewCString(result); + return NS_OK; +} + +NS_IMETHODIMP +nsCookieService::SetCookieString(nsIURI *aHostURI, + nsIPrompt *aPrompt, + const char *aCookieHeader, + nsIChannel *aChannel) +{ + // The aPrompt argument is deprecated and unused. Avoid introducing new + // code that uses this argument by warning if the value is non-null. + MOZ_ASSERT(!aPrompt); + if (aPrompt) { + nsCOMPtr<nsIConsoleService> aConsoleService = + do_GetService("@mozilla.org/consoleservice;1"); + if (aConsoleService) { + aConsoleService->LogStringMessage( + u"Non-null prompt ignored by nsCookieService."); + } + } + return SetCookieStringCommon(aHostURI, aCookieHeader, nullptr, aChannel, + false); +} + +NS_IMETHODIMP +nsCookieService::SetCookieStringFromHttp(nsIURI *aHostURI, + nsIURI *aFirstURI, + nsIPrompt *aPrompt, + const char *aCookieHeader, + const char *aServerTime, + nsIChannel *aChannel) +{ + // The aPrompt argument is deprecated and unused. Avoid introducing new + // code that uses this argument by warning if the value is non-null. + MOZ_ASSERT(!aPrompt); + if (aPrompt) { + nsCOMPtr<nsIConsoleService> aConsoleService = + do_GetService("@mozilla.org/consoleservice;1"); + if (aConsoleService) { + aConsoleService->LogStringMessage( + u"Non-null prompt ignored by nsCookieService."); + } + } + return SetCookieStringCommon(aHostURI, aCookieHeader, aServerTime, aChannel, + true); +} + +nsresult +nsCookieService::SetCookieStringCommon(nsIURI *aHostURI, + const char *aCookieHeader, + const char *aServerTime, + nsIChannel *aChannel, + bool aFromHttp) +{ + NS_ENSURE_ARG(aHostURI); + NS_ENSURE_ARG(aCookieHeader); + + // Determine whether the request is foreign. Failure is acceptable. + bool isForeign = true; + mThirdPartyUtil->IsThirdPartyChannel(aChannel, aHostURI, &isForeign); + + // Get originAttributes. + NeckoOriginAttributes attrs; + if (aChannel) { + NS_GetOriginAttributes(aChannel, attrs); + } + + bool isPrivate = aChannel && NS_UsePrivateBrowsing(aChannel); + + nsDependentCString cookieString(aCookieHeader); + nsDependentCString serverTime(aServerTime ? aServerTime : ""); + SetCookieStringInternal(aHostURI, isForeign, cookieString, + serverTime, aFromHttp, attrs, + isPrivate, aChannel); + return NS_OK; +} + +void +nsCookieService::SetCookieStringInternal(nsIURI *aHostURI, + bool aIsForeign, + nsDependentCString &aCookieHeader, + const nsCString &aServerTime, + bool aFromHttp, + const NeckoOriginAttributes &aOriginAttrs, + bool aIsPrivate, + nsIChannel *aChannel) +{ + NS_ASSERTION(aHostURI, "null host!"); + + if (!mDBState) { + NS_WARNING("No DBState! Profile already closed?"); + return; + } + + AutoRestore<DBState*> savePrevDBState(mDBState); + mDBState = aIsPrivate ? mPrivateDBState : mDefaultDBState; + + // 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 = GetBaseDomain(aHostURI, baseDomain, requireHostMatch); + if (NS_FAILED(rv)) { + COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader, + "couldn't get base domain from URI"); + return; + } + + nsCookieKey key(baseDomain, aOriginAttrs); + + // check default prefs + CookieStatus cookieStatus = CheckPrefs(aHostURI, aIsForeign, aCookieHeader.get()); + + // fire a notification if third party or if cookie was rejected + // (but not if there was an error) + switch (cookieStatus) { + case STATUS_REJECTED: + NotifyRejected(aHostURI); + if (aIsForeign) { + NotifyThirdParty(aHostURI, false, aChannel); + } + return; // Stop here + case STATUS_REJECTED_WITH_ERROR: + return; + case STATUS_ACCEPTED: // Fallthrough + case STATUS_ACCEPT_SESSION: + if (aIsForeign) { + NotifyThirdParty(aHostURI, true, aChannel); + } + break; + default: + break; + } + + // parse server local time. this is not just done here for efficiency + // reasons - if there's an error parsing it, and we need to default it + // to the current time, we must do it here since the current time in + // SetCookieInternal() will change for each cookie processed (e.g. if the + // user is prompted). + PRTime tempServerTime; + int64_t serverTime; + PRStatus result = PR_ParseTimeString(aServerTime.get(), true, + &tempServerTime); + if (result == PR_SUCCESS) { + serverTime = tempServerTime / int64_t(PR_USEC_PER_SEC); + } else { + serverTime = PR_Now() / PR_USEC_PER_SEC; + } + + // process each cookie in the header + while (SetCookieInternal(aHostURI, key, requireHostMatch, cookieStatus, + aCookieHeader, serverTime, aFromHttp, aChannel)) { + // document.cookie can only set one cookie at a time + if (!aFromHttp) + break; + } +} + +// notify observers that a cookie was rejected due to the users' prefs. +void +nsCookieService::NotifyRejected(nsIURI *aHostURI) +{ + nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService(); + if (os) { + os->NotifyObservers(aHostURI, "cookie-rejected", nullptr); + } +} + +// notify observers that a third-party cookie was accepted/rejected +// if the cookie issuer is unknown, it defaults to "?" +void +nsCookieService::NotifyThirdParty(nsIURI *aHostURI, bool aIsAccepted, nsIChannel *aChannel) +{ + nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService(); + if (!os) { + return; + } + + const char* topic; + + if (mDBState != mPrivateDBState) { + // Regular (non-private) browsing + if (aIsAccepted) { + topic = "third-party-cookie-accepted"; + } else { + topic = "third-party-cookie-rejected"; + } + } else { + // Private browsing + if (aIsAccepted) { + topic = "private-third-party-cookie-accepted"; + } else { + topic = "private-third-party-cookie-rejected"; + } + } + + do { + // Attempt to find the host of aChannel. + if (!aChannel) { + break; + } + nsCOMPtr<nsIURI> channelURI; + nsresult rv = aChannel->GetURI(getter_AddRefs(channelURI)); + if (NS_FAILED(rv)) { + break; + } + + nsAutoCString referringHost; + rv = channelURI->GetHost(referringHost); + if (NS_FAILED(rv)) { + break; + } + + nsAutoString referringHostUTF16 = NS_ConvertUTF8toUTF16(referringHost); + os->NotifyObservers(aHostURI, topic, referringHostUTF16.get()); + return; + } while (false); + + // This can fail for a number of reasons, in which kind we fallback to "?" + os->NotifyObservers(aHostURI, topic, u"?"); +} + +// notify observers that the cookie list changed. there are five possible +// values for aData: +// "deleted" means a cookie was deleted. aSubject is the deleted cookie. +// "added" means a cookie was added. aSubject is the added cookie. +// "changed" means a cookie was altered. aSubject is the new cookie. +// "cleared" means the entire cookie list was cleared. aSubject is null. +// "batch-deleted" means a set of cookies was purged. aSubject is the list of +// cookies. +void +nsCookieService::NotifyChanged(nsISupports *aSubject, + const char16_t *aData) +{ + const char* topic = mDBState == mPrivateDBState ? + "private-cookie-changed" : "cookie-changed"; + nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService(); + if (os) { + os->NotifyObservers(aSubject, topic, aData); + } +} + +already_AddRefed<nsIArray> +nsCookieService::CreatePurgeList(nsICookie2* aCookie) +{ + nsCOMPtr<nsIMutableArray> removedList = + do_CreateInstance(NS_ARRAY_CONTRACTID); + removedList->AppendElement(aCookie, false); + return removedList.forget(); +} + +/****************************************************************************** + * nsCookieService: + * pref observer impl + ******************************************************************************/ + +void +nsCookieService::PrefChanged(nsIPrefBranch *aPrefBranch) +{ + int32_t val; + if (NS_SUCCEEDED(aPrefBranch->GetIntPref(kPrefCookieBehavior, &val))) + mCookieBehavior = (uint8_t) LIMIT(val, 0, 3, 0); + + if (NS_SUCCEEDED(aPrefBranch->GetIntPref(kPrefMaxNumberOfCookies, &val))) + mMaxNumberOfCookies = (uint16_t) LIMIT(val, 1, 0xFFFF, kMaxNumberOfCookies); + + if (NS_SUCCEEDED(aPrefBranch->GetIntPref(kPrefMaxCookiesPerHost, &val))) + mMaxCookiesPerHost = (uint16_t) LIMIT(val, 1, 0xFFFF, kMaxCookiesPerHost); + + if (NS_SUCCEEDED(aPrefBranch->GetIntPref(kPrefCookiePurgeAge, &val))) { + mCookiePurgeAge = + int64_t(LIMIT(val, 0, INT32_MAX, INT32_MAX)) * PR_USEC_PER_SEC; + } + + bool boolval; + if (NS_SUCCEEDED(aPrefBranch->GetBoolPref(kPrefThirdPartySession, &boolval))) + mThirdPartySession = boolval; + + if (NS_SUCCEEDED(aPrefBranch->GetBoolPref(kCookieLeaveSecurityAlone, &boolval))) + mLeaveSecureAlone = boolval; +} + +/****************************************************************************** + * nsICookieManager impl: + * nsICookieManager + ******************************************************************************/ + +NS_IMETHODIMP +nsCookieService::RemoveAll() +{ + if (!mDBState) { + NS_WARNING("No DBState! Profile already closed?"); + return NS_ERROR_NOT_AVAILABLE; + } + + RemoveAllFromMemory(); + + // clear the cookie file + if (mDBState->dbConn) { + NS_ASSERTION(mDBState == mDefaultDBState, "not in default DB state"); + + // Cancel any pending read. No further results will be received by our + // read listener. + if (mDefaultDBState->pendingRead) { + CancelAsyncRead(true); + } + + nsCOMPtr<mozIStorageAsyncStatement> stmt; + nsresult rv = mDefaultDBState->dbConn->CreateAsyncStatement(NS_LITERAL_CSTRING( + "DELETE FROM moz_cookies"), getter_AddRefs(stmt)); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<mozIStoragePendingStatement> handle; + rv = stmt->ExecuteAsync(mDefaultDBState->removeListener, + getter_AddRefs(handle)); + NS_ASSERT_SUCCESS(rv); + } else { + // Recreate the database. + COOKIE_LOGSTRING(LogLevel::Debug, + ("RemoveAll(): corruption detected with rv 0x%x", rv)); + HandleCorruptDB(mDefaultDBState); + } + } + + NotifyChanged(nullptr, u"cleared"); + return NS_OK; +} + +NS_IMETHODIMP +nsCookieService::GetEnumerator(nsISimpleEnumerator **aEnumerator) +{ + if (!mDBState) { + NS_WARNING("No DBState! Profile already closed?"); + return NS_ERROR_NOT_AVAILABLE; + } + + EnsureReadComplete(); + + nsCOMArray<nsICookie> cookieList(mDBState->cookieCount); + for (auto iter = mDBState->hostTable.Iter(); !iter.Done(); iter.Next()) { + const nsCookieEntry::ArrayType& cookies = iter.Get()->GetCookies(); + for (nsCookieEntry::IndexType i = 0; i < cookies.Length(); ++i) { + cookieList.AppendObject(cookies[i]); + } + } + + return NS_NewArrayEnumerator(aEnumerator, cookieList); +} + +static nsresult +InitializeOriginAttributes(NeckoOriginAttributes* aAttrs, + JS::HandleValue aOriginAttributes, + JSContext* aCx, + uint8_t aArgc, + const char16_t* aAPI, + const char16_t* aInterfaceSuffix) +{ + MOZ_ASSERT(aAttrs); + MOZ_ASSERT(aCx); + MOZ_ASSERT(aAPI); + MOZ_ASSERT(aInterfaceSuffix); + + if (aArgc == 0) { + const char16_t* params[] = { + aAPI, + aInterfaceSuffix + }; + + // This is supposed to be temporary and in 1 or 2 releases we want to + // have originAttributes param as mandatory. But for now, we don't want to + // break existing addons, so we write a console message to inform the addon + // developers about it. + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, + NS_LITERAL_CSTRING("Cookie Manager"), + nullptr, + nsContentUtils::eNECKO_PROPERTIES, + "nsICookieManagerAPIDeprecated", + params, ArrayLength(params)); + } else if (aArgc == 1) { + if (!aOriginAttributes.isObject() || + !aAttrs->Init(aCx, aOriginAttributes)) { + return NS_ERROR_INVALID_ARG; + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsCookieService::Add(const nsACString &aHost, + const nsACString &aPath, + const nsACString &aName, + const nsACString &aValue, + bool aIsSecure, + bool aIsHttpOnly, + bool aIsSession, + int64_t aExpiry, + JS::HandleValue aOriginAttributes, + JSContext* aCx, + uint8_t aArgc) +{ + MOZ_ASSERT(aArgc == 0 || aArgc == 1); + + NeckoOriginAttributes attrs; + nsresult rv = InitializeOriginAttributes(&attrs, + aOriginAttributes, + aCx, + aArgc, + u"nsICookieManager2.add()", + u"2"); + NS_ENSURE_SUCCESS(rv, rv); + + return AddNative(aHost, aPath, aName, aValue, aIsSecure, aIsHttpOnly, + aIsSession, aExpiry, &attrs); +} + +NS_IMETHODIMP_(nsresult) +nsCookieService::AddNative(const nsACString &aHost, + const nsACString &aPath, + const nsACString &aName, + const nsACString &aValue, + bool aIsSecure, + bool aIsHttpOnly, + bool aIsSession, + int64_t aExpiry, + NeckoOriginAttributes* aOriginAttributes) +{ + if (NS_WARN_IF(!aOriginAttributes)) { + return NS_ERROR_FAILURE; + } + + if (!mDBState) { + NS_WARNING("No DBState! Profile already closed?"); + 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 = GetBaseDomainFromHost(host, baseDomain); + NS_ENSURE_SUCCESS(rv, rv); + + int64_t currentTimeInUsec = PR_Now(); + nsCookieKey key = nsCookieKey(baseDomain, *aOriginAttributes); + + RefPtr<nsCookie> cookie = + nsCookie::Create(aName, aValue, host, aPath, + aExpiry, + currentTimeInUsec, + nsCookie::GenerateUniqueCreationTime(currentTimeInUsec), + aIsSession, + aIsSecure, + aIsHttpOnly, + key.mOriginAttributes); + if (!cookie) { + return NS_ERROR_OUT_OF_MEMORY; + } + + AddInternal(key, cookie, currentTimeInUsec, nullptr, nullptr, true); + return NS_OK; +} + + +nsresult +nsCookieService::Remove(const nsACString& aHost, const NeckoOriginAttributes& aAttrs, + const nsACString& aName, const nsACString& aPath, + bool aBlocked) +{ + if (!mDBState) { + NS_WARNING("No DBState! Profile already closed?"); + 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); + + nsAutoCString baseDomain; + rv = GetBaseDomainFromHost(host, baseDomain); + NS_ENSURE_SUCCESS(rv, rv); + + nsListIter matchIter; + RefPtr<nsCookie> cookie; + if (FindCookie(nsCookieKey(baseDomain, aAttrs), + host, + PromiseFlatCString(aName), + PromiseFlatCString(aPath), + matchIter)) { + cookie = matchIter.Cookie(); + RemoveCookieFromList(matchIter); + } + + // check if we need to add the host to the permissions blacklist. + if (aBlocked && mPermissionService) { + // strip off the domain dot, if necessary + if (!host.IsEmpty() && host.First() == '.') + host.Cut(0, 1); + + host.Insert(NS_LITERAL_CSTRING("http://"), 0); + + nsCOMPtr<nsIURI> uri; + NS_NewURI(getter_AddRefs(uri), host); + + if (uri) + mPermissionService->SetAccess(uri, nsICookiePermission::ACCESS_DENY); + } + + if (cookie) { + // Everything's done. Notify observers. + NotifyChanged(cookie, u"deleted"); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsCookieService::Remove(const nsACString &aHost, + const nsACString &aName, + const nsACString &aPath, + bool aBlocked, + JS::HandleValue aOriginAttributes, + JSContext* aCx, + uint8_t aArgc) +{ + MOZ_ASSERT(aArgc == 0 || aArgc == 1); + + NeckoOriginAttributes attrs; + nsresult rv = InitializeOriginAttributes(&attrs, + aOriginAttributes, + aCx, + aArgc, + u"nsICookieManager.remove()", + u""); + NS_ENSURE_SUCCESS(rv, rv); + + return RemoveNative(aHost, aName, aPath, aBlocked, &attrs); +} + +NS_IMETHODIMP_(nsresult) +nsCookieService::RemoveNative(const nsACString &aHost, + const nsACString &aName, + const nsACString &aPath, + bool aBlocked, + NeckoOriginAttributes* aOriginAttributes) +{ + if (NS_WARN_IF(!aOriginAttributes)) { + return NS_ERROR_FAILURE; + } + + nsresult rv = Remove(aHost, *aOriginAttributes, aName, aPath, aBlocked); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsCookieService::UsePrivateMode(bool aIsPrivate, + nsIPrivateModeCallback* aCallback) +{ + if (!aCallback) { + return NS_ERROR_INVALID_ARG; + } + + if (!mDBState) { + NS_WARNING("No DBState! Profile already closed?"); + return NS_ERROR_NOT_AVAILABLE; + } + + AutoRestore<DBState*> savePrevDBState(mDBState); + mDBState = aIsPrivate ? mPrivateDBState : mDefaultDBState; + + return aCallback->Callback(); +} + +/****************************************************************************** + * nsCookieService impl: + * private file I/O functions + ******************************************************************************/ + +// Begin an asynchronous read from the database. +OpenDBResult +nsCookieService::Read() +{ + // Set up a statement for the read. Note that our query specifies that + // 'baseDomain' not be nullptr -- see below for why. + nsCOMPtr<mozIStorageAsyncStatement> stmtRead; + nsresult rv = mDefaultDBState->dbConn->CreateAsyncStatement(NS_LITERAL_CSTRING( + "SELECT " + "name, " + "value, " + "host, " + "path, " + "expiry, " + "lastAccessed, " + "creationTime, " + "isSecure, " + "isHttpOnly, " + "baseDomain, " + "originAttributes " + "FROM moz_cookies " + "WHERE baseDomain NOTNULL"), getter_AddRefs(stmtRead)); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + // Set up a statement to delete any rows with a nullptr 'baseDomain' + // column. This takes care of any cookies set by browsers that don't + // understand the 'baseDomain' column, where the database schema version + // is from one that does. (This would occur when downgrading.) + nsCOMPtr<mozIStorageAsyncStatement> stmtDeleteNull; + rv = mDefaultDBState->dbConn->CreateAsyncStatement(NS_LITERAL_CSTRING( + "DELETE FROM moz_cookies WHERE baseDomain ISNULL"), + getter_AddRefs(stmtDeleteNull)); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + // Start a new connection for sync reads, to reduce contention with the + // background thread. We need to do this before we kick off write statements, + // since they can lock the database and prevent connections from being opened. + rv = mStorageService->OpenUnsharedDatabase(mDefaultDBState->cookieFile, + getter_AddRefs(mDefaultDBState->syncConn)); + NS_ENSURE_SUCCESS(rv, RESULT_RETRY); + + // Init our readSet hash and execute the statements. Note that, after this + // point, we cannot fail without altering the cleanup code in InitDBStates() + // to handle closing of the now-asynchronous connection. + mDefaultDBState->hostArray.SetCapacity(kMaxNumberOfCookies); + + mDefaultDBState->readListener = new ReadCookieDBListener(mDefaultDBState); + rv = stmtRead->ExecuteAsync(mDefaultDBState->readListener, + getter_AddRefs(mDefaultDBState->pendingRead)); + NS_ASSERT_SUCCESS(rv); + + nsCOMPtr<mozIStoragePendingStatement> handle; + rv = stmtDeleteNull->ExecuteAsync(mDefaultDBState->removeListener, + getter_AddRefs(handle)); + NS_ASSERT_SUCCESS(rv); + + return RESULT_OK; +} + +// Extract data from a single result row and create an nsCookie. +// This is templated since 'T' is different for sync vs async results. +template<class T> nsCookie* +nsCookieService::GetCookieFromRow(T &aRow, const OriginAttributes& aOriginAttributes) +{ + // Skip reading 'baseDomain' -- up to the caller. + nsCString name, value, host, path; + DebugOnly<nsresult> rv = aRow->GetUTF8String(IDX_NAME, name); + NS_ASSERT_SUCCESS(rv); + rv = aRow->GetUTF8String(IDX_VALUE, value); + NS_ASSERT_SUCCESS(rv); + rv = aRow->GetUTF8String(IDX_HOST, host); + NS_ASSERT_SUCCESS(rv); + rv = aRow->GetUTF8String(IDX_PATH, path); + NS_ASSERT_SUCCESS(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); + + // Create a new nsCookie and assign the data. + return nsCookie::Create(name, value, host, path, + expiry, + lastAccessed, + creationTime, + false, + isSecure, + isHttpOnly, + aOriginAttributes); +} + +void +nsCookieService::AsyncReadComplete() +{ + // We may be in the private browsing DB state, with a pending read on the + // default DB state. (This would occur if we started up in private browsing + // mode.) As long as we do all our operations on the default state, we're OK. + NS_ASSERTION(mDefaultDBState, "no default DBState"); + NS_ASSERTION(mDefaultDBState->pendingRead, "no pending read"); + NS_ASSERTION(mDefaultDBState->readListener, "no read listener"); + + // Merge the data read on the background thread with the data synchronously + // read on the main thread. Note that transactions on the cookie table may + // have occurred on the main thread since, making the background data stale. + for (uint32_t i = 0; i < mDefaultDBState->hostArray.Length(); ++i) { + const CookieDomainTuple &tuple = mDefaultDBState->hostArray[i]; + + // Tiebreak: if the given base domain has already been read in, ignore + // the background data. Note that readSet may contain domains that were + // queried but found not to be in the db -- that's harmless. + if (mDefaultDBState->readSet.GetEntry(tuple.key)) + continue; + + AddCookieToList(tuple.key, tuple.cookie, mDefaultDBState, nullptr, false); + } + + mDefaultDBState->stmtReadDomain = nullptr; + mDefaultDBState->pendingRead = nullptr; + mDefaultDBState->readListener = nullptr; + mDefaultDBState->syncConn = nullptr; + mDefaultDBState->hostArray.Clear(); + mDefaultDBState->readSet.Clear(); + + COOKIE_LOGSTRING(LogLevel::Debug, ("Read(): %ld cookies read", + mDefaultDBState->cookieCount)); + + nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService(); + if (os) { + os->NotifyObservers(nullptr, "cookie-db-read", nullptr); + } +} + +void +nsCookieService::CancelAsyncRead(bool aPurgeReadSet) +{ + // We may be in the private browsing DB state, with a pending read on the + // default DB state. (This would occur if we started up in private browsing + // mode.) As long as we do all our operations on the default state, we're OK. + NS_ASSERTION(mDefaultDBState, "no default DBState"); + NS_ASSERTION(mDefaultDBState->pendingRead, "no pending read"); + NS_ASSERTION(mDefaultDBState->readListener, "no read listener"); + + // Cancel the pending read, kill the read listener, and empty the array + // of data already read in on the background thread. + mDefaultDBState->readListener->Cancel(); + DebugOnly<nsresult> rv = mDefaultDBState->pendingRead->Cancel(); + NS_ASSERT_SUCCESS(rv); + + mDefaultDBState->stmtReadDomain = nullptr; + mDefaultDBState->pendingRead = nullptr; + mDefaultDBState->readListener = nullptr; + mDefaultDBState->hostArray.Clear(); + + // Only clear the 'readSet' table if we no longer need to know what set of + // data is already accounted for. + if (aPurgeReadSet) + mDefaultDBState->readSet.Clear(); +} + +void +nsCookieService::EnsureReadDomain(const nsCookieKey &aKey) +{ + NS_ASSERTION(!mDBState->dbConn || mDBState == mDefaultDBState, + "not in default db state"); + + // Fast path 1: nothing to read, or we've already finished reading. + if (MOZ_LIKELY(!mDBState->dbConn || !mDefaultDBState->pendingRead)) + return; + + // Fast path 2: already read in this particular domain. + if (MOZ_LIKELY(mDefaultDBState->readSet.GetEntry(aKey))) + return; + + // Read in the data synchronously. + // see IDX_NAME, etc. for parameter indexes + nsresult rv; + if (!mDefaultDBState->stmtReadDomain) { + // Cache the statement, since it's likely to be used again. + rv = mDefaultDBState->syncConn->CreateStatement(NS_LITERAL_CSTRING( + "SELECT " + "name, " + "value, " + "host, " + "path, " + "expiry, " + "lastAccessed, " + "creationTime, " + "isSecure, " + "isHttpOnly " + "FROM moz_cookies " + "WHERE baseDomain = :baseDomain " + " AND originAttributes = :originAttributes"), + getter_AddRefs(mDefaultDBState->stmtReadDomain)); + + if (NS_FAILED(rv)) { + // Recreate the database. + COOKIE_LOGSTRING(LogLevel::Debug, + ("EnsureReadDomain(): corruption detected when creating statement " + "with rv 0x%x", rv)); + HandleCorruptDB(mDefaultDBState); + return; + } + } + + NS_ASSERTION(mDefaultDBState->syncConn, "should have a sync db connection"); + + mozStorageStatementScoper scoper(mDefaultDBState->stmtReadDomain); + + rv = mDefaultDBState->stmtReadDomain->BindUTF8StringByName( + NS_LITERAL_CSTRING("baseDomain"), aKey.mBaseDomain); + NS_ASSERT_SUCCESS(rv); + + nsAutoCString suffix; + aKey.mOriginAttributes.CreateSuffix(suffix); + rv = mDefaultDBState->stmtReadDomain->BindUTF8StringByName( + NS_LITERAL_CSTRING("originAttributes"), suffix); + NS_ASSERT_SUCCESS(rv); + + bool hasResult; + nsCString name, value, host, path; + AutoTArray<RefPtr<nsCookie>, kMaxCookiesPerHost> array; + while (true) { + rv = mDefaultDBState->stmtReadDomain->ExecuteStep(&hasResult); + if (NS_FAILED(rv)) { + // Recreate the database. + COOKIE_LOGSTRING(LogLevel::Debug, + ("EnsureReadDomain(): corruption detected when reading result " + "with rv 0x%x", rv)); + HandleCorruptDB(mDefaultDBState); + return; + } + + if (!hasResult) + break; + + array.AppendElement(GetCookieFromRow(mDefaultDBState->stmtReadDomain, + aKey.mOriginAttributes)); + } + + // Add the cookies to the table in a single operation. This makes sure that + // either all the cookies get added, or in the case of corruption, none. + for (uint32_t i = 0; i < array.Length(); ++i) { + AddCookieToList(aKey, array[i], mDefaultDBState, nullptr, false); + } + + // Add it to the hashset of read entries, so we don't read it again. + mDefaultDBState->readSet.PutEntry(aKey); + + COOKIE_LOGSTRING(LogLevel::Debug, + ("EnsureReadDomain(): %ld cookies read for base domain %s, " + " originAttributes = %s", array.Length(), aKey.mBaseDomain.get(), + suffix.get())); +} + +void +nsCookieService::EnsureReadComplete() +{ + NS_ASSERTION(!mDBState->dbConn || mDBState == mDefaultDBState, + "not in default db state"); + + // Fast path 1: nothing to read, or we've already finished reading. + if (MOZ_LIKELY(!mDBState->dbConn || !mDefaultDBState->pendingRead)) + return; + + // Cancel the pending read, so we don't get any more results. + CancelAsyncRead(false); + + // Read in the data synchronously. + // see IDX_NAME, etc. for parameter indexes + nsCOMPtr<mozIStorageStatement> stmt; + nsresult rv = mDefaultDBState->syncConn->CreateStatement(NS_LITERAL_CSTRING( + "SELECT " + "name, " + "value, " + "host, " + "path, " + "expiry, " + "lastAccessed, " + "creationTime, " + "isSecure, " + "isHttpOnly, " + "baseDomain, " + "originAttributes " + "FROM moz_cookies " + "WHERE baseDomain NOTNULL"), getter_AddRefs(stmt)); + + if (NS_FAILED(rv)) { + // Recreate the database. + COOKIE_LOGSTRING(LogLevel::Debug, + ("EnsureReadComplete(): corruption detected when creating statement " + "with rv 0x%x", rv)); + HandleCorruptDB(mDefaultDBState); + return; + } + + nsCString baseDomain, name, value, host, path; + bool hasResult; + nsTArray<CookieDomainTuple> array(kMaxNumberOfCookies); + while (true) { + rv = stmt->ExecuteStep(&hasResult); + if (NS_FAILED(rv)) { + // Recreate the database. + COOKIE_LOGSTRING(LogLevel::Debug, + ("EnsureReadComplete(): corruption detected when reading result " + "with rv 0x%x", rv)); + HandleCorruptDB(mDefaultDBState); + return; + } + + if (!hasResult) + break; + + // Make sure we haven't already read the data. + stmt->GetUTF8String(IDX_BASE_DOMAIN, baseDomain); + + nsAutoCString suffix; + NeckoOriginAttributes 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); + + nsCookieKey key(baseDomain, attrs); + if (mDefaultDBState->readSet.GetEntry(key)) + continue; + + CookieDomainTuple* tuple = array.AppendElement(); + tuple->key = key; + tuple->cookie = GetCookieFromRow(stmt, attrs); + } + + // Add the cookies to the table in a single operation. This makes sure that + // either all the cookies get added, or in the case of corruption, none. + for (uint32_t i = 0; i < array.Length(); ++i) { + CookieDomainTuple& tuple = array[i]; + AddCookieToList(tuple.key, tuple.cookie, mDefaultDBState, nullptr, + false); + } + + mDefaultDBState->syncConn = nullptr; + mDefaultDBState->readSet.Clear(); + + COOKIE_LOGSTRING(LogLevel::Debug, + ("EnsureReadComplete(): %ld cookies read", array.Length())); +} + +NS_IMETHODIMP +nsCookieService::ImportCookies(nsIFile *aCookieFile) +{ + if (!mDBState) { + NS_WARNING("No DBState! Profile already closed?"); + return NS_ERROR_NOT_AVAILABLE; + } + + // Make sure we're in the default DB state. We don't want people importing + // cookies into a private browsing session! + if (mDBState != mDefaultDBState) { + NS_WARNING("Trying to import cookies in a private browsing session!"); + return NS_ERROR_NOT_AVAILABLE; + } + + nsresult rv; + nsCOMPtr<nsIInputStream> fileInputStream; + rv = NS_NewLocalFileInputStream(getter_AddRefs(fileInputStream), aCookieFile); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsILineInputStream> lineInputStream = do_QueryInterface(fileInputStream, &rv); + if (NS_FAILED(rv)) return rv; + + // First, ensure we've read in everything from the database, if we have one. + EnsureReadComplete(); + + static const char kTrue[] = "TRUE"; + + nsAutoCString buffer, baseDomain; + bool isMore = true; + int32_t hostIndex, isDomainIndex, pathIndex, secureIndex, expiresIndex, nameIndex, cookieIndex; + nsASingleFragmentCString::char_iterator iter; + int32_t numInts; + int64_t expires; + bool isDomain, isHttpOnly = false; + uint32_t originalCookieCount = mDefaultDBState->cookieCount; + + int64_t currentTimeInUsec = PR_Now(); + int64_t currentTime = currentTimeInUsec / PR_USEC_PER_SEC; + // we use lastAccessedCounter to keep cookies in recently-used order, + // so we start by initializing to currentTime (somewhat arbitrary) + int64_t lastAccessedCounter = currentTimeInUsec; + + /* file format is: + * + * host \t isDomain \t path \t secure \t expires \t name \t cookie + * + * if this format isn't respected we move onto the next line in the file. + * isDomain is "TRUE" or "FALSE" (default to "FALSE") + * isSecure is "TRUE" or "FALSE" (default to "TRUE") + * expires is a int64_t integer + * note 1: cookie can contain tabs. + * note 2: cookies will be stored in order of lastAccessed time: + * most-recently used come first; least-recently-used come last. + */ + + /* + * ...but due to bug 178933, we hide HttpOnly cookies from older code + * in a comment, so they don't expose HttpOnly cookies to JS. + * + * The format for HttpOnly cookies is + * + * #HttpOnly_host \t isDomain \t path \t secure \t expires \t name \t cookie + * + */ + + // We will likely be adding a bunch of cookies to the DB, so we use async + // batching with storage to make this super fast. + nsCOMPtr<mozIStorageBindingParamsArray> paramsArray; + if (originalCookieCount == 0 && mDefaultDBState->dbConn) { + mDefaultDBState->stmtInsert->NewBindingParamsArray(getter_AddRefs(paramsArray)); + } + + while (isMore && NS_SUCCEEDED(lineInputStream->ReadLine(buffer, &isMore))) { + if (StringBeginsWith(buffer, NS_LITERAL_CSTRING(HTTP_ONLY_PREFIX))) { + isHttpOnly = true; + hostIndex = sizeof(HTTP_ONLY_PREFIX) - 1; + } else if (buffer.IsEmpty() || buffer.First() == '#') { + continue; + } else { + isHttpOnly = false; + hostIndex = 0; + } + + // this is a cheap, cheesy way of parsing a tab-delimited line into + // string indexes, which can be lopped off into substrings. just for + // purposes of obfuscation, it also checks that each token was found. + // todo: use iterators? + if ((isDomainIndex = buffer.FindChar('\t', hostIndex) + 1) == 0 || + (pathIndex = buffer.FindChar('\t', isDomainIndex) + 1) == 0 || + (secureIndex = buffer.FindChar('\t', pathIndex) + 1) == 0 || + (expiresIndex = buffer.FindChar('\t', secureIndex) + 1) == 0 || + (nameIndex = buffer.FindChar('\t', expiresIndex) + 1) == 0 || + (cookieIndex = buffer.FindChar('\t', nameIndex) + 1) == 0) { + continue; + } + + // check the expirytime first - if it's expired, ignore + // nullstomp the trailing tab, to avoid copying the string + buffer.BeginWriting(iter); + *(iter += nameIndex - 1) = char(0); + numInts = PR_sscanf(buffer.get() + expiresIndex, "%lld", &expires); + if (numInts != 1 || expires < currentTime) { + continue; + } + + isDomain = Substring(buffer, isDomainIndex, pathIndex - isDomainIndex - 1).EqualsLiteral(kTrue); + const nsASingleFragmentCString &host = Substring(buffer, hostIndex, isDomainIndex - hostIndex - 1); + // check for bad legacy cookies (domain not starting with a dot, or containing a port), + // and discard + if ((isDomain && !host.IsEmpty() && host.First() != '.') || + host.Contains(':')) { + continue; + } + + // compute the baseDomain from the host + rv = GetBaseDomainFromHost(host, baseDomain); + if (NS_FAILED(rv)) + continue; + + // pre-existing cookies have appId=0, inIsolatedMozBrowser=false set by default + // constructor of NeckoOriginAttributes(). + nsCookieKey key = DEFAULT_APP_KEY(baseDomain); + + // Create a new nsCookie and assign the data. We don't know the cookie + // creation time, so just use the current time to generate a unique one. + RefPtr<nsCookie> newCookie = + nsCookie::Create(Substring(buffer, nameIndex, cookieIndex - nameIndex - 1), + Substring(buffer, cookieIndex, buffer.Length() - cookieIndex), + host, + Substring(buffer, pathIndex, secureIndex - pathIndex - 1), + expires, + lastAccessedCounter, + nsCookie::GenerateUniqueCreationTime(currentTimeInUsec), + false, + Substring(buffer, secureIndex, expiresIndex - secureIndex - 1).EqualsLiteral(kTrue), + isHttpOnly, + key.mOriginAttributes); + if (!newCookie) { + return NS_ERROR_OUT_OF_MEMORY; + } + + // trick: preserve the most-recently-used cookie ordering, + // by successively decrementing the lastAccessed time + lastAccessedCounter--; + + if (originalCookieCount == 0) { + AddCookieToList(key, newCookie, mDefaultDBState, paramsArray); + } + else { + AddInternal(key, newCookie, currentTimeInUsec, + nullptr, nullptr, true); + } + } + + // If we need to write to disk, do so now. + if (paramsArray) { + uint32_t length; + paramsArray->GetLength(&length); + if (length) { + rv = mDefaultDBState->stmtInsert->BindParameters(paramsArray); + NS_ASSERT_SUCCESS(rv); + nsCOMPtr<mozIStoragePendingStatement> handle; + rv = mDefaultDBState->stmtInsert->ExecuteAsync( + mDefaultDBState->insertListener, getter_AddRefs(handle)); + NS_ASSERT_SUCCESS(rv); + } + } + + + COOKIE_LOGSTRING(LogLevel::Debug, ("ImportCookies(): %ld cookies imported", + mDefaultDBState->cookieCount)); + + return NS_OK; +} + +/****************************************************************************** + * nsCookieService impl: + * private GetCookie/SetCookie helpers + ******************************************************************************/ + +// helper function for GetCookieList +static inline bool ispathdelimiter(char c) { return c == '/' || c == '?' || c == '#' || c == ';'; } + +// Comparator class for sorting cookies before sending to a server. +class CompareCookiesForSending +{ +public: + bool Equals(const nsCookie* aCookie1, const nsCookie* aCookie2) const + { + return aCookie1->CreationTime() == aCookie2->CreationTime() && + aCookie2->Path().Length() == aCookie1->Path().Length(); + } + + bool LessThan(const nsCookie* aCookie1, const nsCookie* 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(); + } +}; + +static bool +DomainMatches(nsCookie* 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 +PathMatches(nsCookie* aCookie, const nsACString& aPath) { + // calculate cookie path length, excluding trailing '/' + uint32_t cookiePathLen = aCookie->Path().Length(); + if (cookiePathLen > 0 && aCookie->Path().Last() == '/') + --cookiePathLen; + + // if the given path is shorter than the cookie path, it doesn't match + // if the given path doesn't start with the cookie path, it doesn't match. + if (!StringBeginsWith(aPath, Substring(aCookie->Path(), 0, cookiePathLen))) + return false; + + // if the given path is longer than the cookie path, and the first char after + // the cookie path is not a path delimiter, it doesn't match. + if (aPath.Length() > cookiePathLen && + !ispathdelimiter(aPath.CharAt(cookiePathLen))) { + /* + * |ispathdelimiter| tests four cases: '/', '?', '#', and ';'. + * '/' is the "standard" case; the '?' test allows a site at host/abc?def + * to receive a cookie that has a path attribute of abc. this seems + * strange but at least one major site (citibank, bug 156725) depends + * on it. The test for # and ; are put in to proactively avoid problems + * with other sites - these are the only other chars allowed in the path. + */ + return false; + } + + // either the paths match exactly, or the cookie path is a prefix of + // the given path. + return true; +} + +void +nsCookieService::GetCookieStringInternal(nsIURI *aHostURI, + bool aIsForeign, + bool aHttpBound, + const NeckoOriginAttributes aOriginAttrs, + bool aIsPrivate, + nsCString &aCookieString) +{ + NS_ASSERTION(aHostURI, "null host!"); + + if (!mDBState) { + NS_WARNING("No DBState! Profile already closed?"); + return; + } + + AutoRestore<DBState*> savePrevDBState(mDBState); + mDBState = aIsPrivate ? mPrivateDBState : mDefaultDBState; + + // 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, hostFromURI, pathFromURI; + nsresult rv = GetBaseDomain(aHostURI, baseDomain, requireHostMatch); + if (NS_SUCCEEDED(rv)) + rv = aHostURI->GetAsciiHost(hostFromURI); + if (NS_SUCCEEDED(rv)) + rv = aHostURI->GetPath(pathFromURI); + if (NS_FAILED(rv)) { + COOKIE_LOGFAILURE(GET_COOKIE, aHostURI, nullptr, "invalid host/path from URI"); + return; + } + + // check default prefs + CookieStatus cookieStatus = CheckPrefs(aHostURI, aIsForeign, nullptr); + + // for GetCookie(), we don't fire rejection notifications. + switch (cookieStatus) { + case STATUS_REJECTED: + case STATUS_REJECTED_WITH_ERROR: + return; + default: + break; + } + + // Note: The following permissions logic is mirrored in + // toolkit/modules/addons/MatchPattern.jsm: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 isSecure; + if (NS_FAILED(aHostURI->SchemeIs("https", &isSecure))) { + isSecure = false; + } + + nsCookie *cookie; + AutoTArray<nsCookie*, 8> foundCookieList; + int64_t currentTimeInUsec = PR_Now(); + int64_t currentTime = currentTimeInUsec / PR_USEC_PER_SEC; + bool stale = false; + + nsCookieKey key(baseDomain, aOriginAttrs); + EnsureReadDomain(key); + + // perform the hash lookup + nsCookieEntry *entry = mDBState->hostTable.GetEntry(key); + if (!entry) + return; + + // iterate the cookies! + const nsCookieEntry::ArrayType &cookies = entry->GetCookies(); + for (nsCookieEntry::IndexType i = 0; i < cookies.Length(); ++i) { + cookie = cookies[i]; + + // check the host, since the base domain lookup is conservative. + if (!DomainMatches(cookie, hostFromURI)) + continue; + + // if the cookie is secure and the host scheme isn't, we can't send it + if (cookie->IsSecure() && !isSecure) + 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 (!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 + foundCookieList.AppendElement(cookie); + if (cookie->IsStale()) { + stale = true; + } + } + + int32_t count = foundCookieList.Length(); + if (count == 0) + return; + + // 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) { + // 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 = mDBState->stmtUpdate; + if (mDBState->dbConn) { + stmt->NewBindingParamsArray(getter_AddRefs(paramsArray)); + } + + for (int32_t i = 0; i < count; ++i) { + cookie = foundCookieList.ElementAt(i); + + if (cookie->IsStale()) { + UpdateCookieInList(cookie, currentTimeInUsec, paramsArray); + } + } + // Update the database now if necessary. + if (paramsArray) { + uint32_t length; + paramsArray->GetLength(&length); + if (length) { + DebugOnly<nsresult> rv = stmt->BindParameters(paramsArray); + NS_ASSERT_SUCCESS(rv); + nsCOMPtr<mozIStoragePendingStatement> handle; + rv = stmt->ExecuteAsync(mDBState->updateListener, + getter_AddRefs(handle)); + NS_ASSERT_SUCCESS(rv); + } + } + } + + // 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). + foundCookieList.Sort(CompareCookiesForSending()); + + for (int32_t i = 0; i < count; ++i) { + cookie = foundCookieList.ElementAt(i); + + // 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_LITERAL_CSTRING("=") + cookie->Value(); + } else { + // just write value + aCookieString += cookie->Value(); + } + } + } + + if (!aCookieString.IsEmpty()) + COOKIE_LOGSUCCESS(GET_COOKIE, aHostURI, aCookieString, nullptr, false); +} + +// processes a single cookie, and returns true if there are more cookies +// to be processed +bool +nsCookieService::SetCookieInternal(nsIURI *aHostURI, + const nsCookieKey &aKey, + bool aRequireHostMatch, + CookieStatus aStatus, + nsDependentCString &aCookieHeader, + int64_t aServerTime, + bool aFromHttp, + nsIChannel *aChannel) +{ + NS_ASSERTION(aHostURI, "null host!"); + + // create a stack-based nsCookieAttributes, to store all the + // attributes parsed from the cookie + nsCookieAttributes cookieAttributes; + + // init expiryTime such that session cookies won't prematurely expire + cookieAttributes.expiryTime = INT64_MAX; + + // aCookieHeader is an in/out param to point to the next cookie, if + // there is one. Save the present value for logging purposes + nsDependentCString savedCookieHeader(aCookieHeader); + + // newCookie says whether there are multiple cookies in the header; + // so we can handle them separately. + bool newCookie = ParseAttributes(aCookieHeader, cookieAttributes); + + // 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 isHTTPS; + nsresult rv = aHostURI->SchemeIs("https", &isHTTPS); + if (NS_SUCCEEDED(rv)) { + Telemetry::Accumulate(Telemetry::COOKIE_SCHEME_SECURITY, + ((cookieAttributes.isSecure)? 0x02 : 0x00) | + ((isHTTPS)? 0x01 : 0x00)); + } + + int64_t currentTimeInUsec = PR_Now(); + + // calculate expiry time of cookie. + cookieAttributes.isSession = GetExpiry(cookieAttributes, aServerTime, + currentTimeInUsec / PR_USEC_PER_SEC); + if (aStatus == STATUS_ACCEPT_SESSION) { + // force lifetime to session. note that the expiration time, if set above, + // will still apply. + cookieAttributes.isSession = true; + } + + // reject cookie if it's over the size limit, per RFC2109 + if ((cookieAttributes.name.Length() + cookieAttributes.value.Length()) > kMaxBytesPerCookie) { + COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader, "cookie too big (> 4kb)"); + return newCookie; + } + + const char illegalNameCharacters[] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, + 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, + 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, + 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, + 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, + 0x1F, 0x00 }; + if (cookieAttributes.name.FindCharInSet(illegalNameCharacters, 0) != -1) { + COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader, "invalid name character"); + return newCookie; + } + + // domain & path checks + if (!CheckDomain(cookieAttributes, aHostURI, aKey.mBaseDomain, aRequireHostMatch)) { + COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader, "failed the domain tests"); + return newCookie; + } + if (!CheckPath(cookieAttributes, aHostURI)) { + COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader, "failed the path tests"); + return newCookie; + } + // magic prefix checks. MUST be run after CheckDomain() and CheckPath() + if (!CheckPrefixes(cookieAttributes, isHTTPS)) { + COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader, "failed the prefix tests"); + return newCookie; + } + + // reject cookie if value contains an RFC 6265 disallowed character - see + // https://bugzilla.mozilla.org/show_bug.cgi?id=1191423 + // NOTE: this is not the full set of characters disallowed by 6265 - notably + // 0x09, 0x20, 0x22, 0x2C, 0x5C, and 0x7F are missing from this list. This is + // for parity with Chrome. This only applies to cookies set via the Set-Cookie + // header, as document.cookie is defined to be UTF-8. Hooray for + // symmetry!</sarcasm> + const char illegalCharacters[] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, + 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, + 0x1E, 0x1F, 0x3B, 0x00 }; + if (aFromHttp && (cookieAttributes.value.FindCharInSet(illegalCharacters, 0) != -1)) { + COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader, "invalid value character"); + return newCookie; + } + + // create a new nsCookie and copy attributes + RefPtr<nsCookie> cookie = + nsCookie::Create(cookieAttributes.name, + cookieAttributes.value, + cookieAttributes.host, + cookieAttributes.path, + cookieAttributes.expiryTime, + currentTimeInUsec, + nsCookie::GenerateUniqueCreationTime(currentTimeInUsec), + cookieAttributes.isSession, + cookieAttributes.isSecure, + cookieAttributes.isHttpOnly, + aKey.mOriginAttributes); + if (!cookie) + return newCookie; + + // check permissions from site permission list, or ask the user, + // to determine if we can set the cookie + if (mPermissionService) { + bool permission; + mPermissionService->CanSetCookie(aHostURI, + aChannel, + static_cast<nsICookie2*>(static_cast<nsCookie*>(cookie)), + &cookieAttributes.isSession, + &cookieAttributes.expiryTime, + &permission); + if (!permission) { + COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader, "cookie rejected by permission manager"); + NotifyRejected(aHostURI); + return newCookie; + } + + // update isSession and expiry attributes, in case they changed + cookie->SetIsSession(cookieAttributes.isSession); + cookie->SetExpiry(cookieAttributes.expiryTime); + } + + // add the cookie to the list. AddInternal() takes care of logging. + // we get the current time again here, since it may have changed during prompting + AddInternal(aKey, cookie, PR_Now(), aHostURI, savedCookieHeader.get(), + aFromHttp); + return newCookie; +} + +// 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 +nsCookieService::AddInternal(const nsCookieKey &aKey, + nsCookie *aCookie, + int64_t aCurrentTimeInUsec, + nsIURI *aHostURI, + const char *aCookieHeader, + bool aFromHttp) +{ + int64_t currentTime = aCurrentTimeInUsec / PR_USEC_PER_SEC; + + // if the new cookie is httponly, make sure we're not coming from script + if (!aFromHttp && aCookie->IsHttpOnly()) { + COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader, + "cookie is httponly; coming from script"); + return; + } + + bool isSecure = true; + if (aHostURI && NS_FAILED(aHostURI->SchemeIs("https", &isSecure))) { + isSecure = false; + } + + // 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 (mLeaveSecureAlone && aCookie->IsSecure() && !isSecure) { + COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader, + "non-https cookie can't set secure flag"); + Telemetry::Accumulate(Telemetry::COOKIE_LEAVE_SECURE_ALONE, + BLOCKED_SECURE_SET_FROM_HTTP); + return; + } + nsListIter exactIter; + bool foundCookie = false; + if (mLeaveSecureAlone) { + // 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) + foundCookie = FindSecureCookie(aKey, aCookie); + if (foundCookie && !aCookie->IsSecure()) { + if (!isSecure) { + COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader, + "cookie can't save because older cookie is secure cookie but newer cookie is non-secure cookie"); + Telemetry::Accumulate(Telemetry::COOKIE_LEAVE_SECURE_ALONE, + BLOCKED_DOWNGRADE_SECURE); + return; + } else { + // A secure site is allowed to downgrade a secure cookie + // but we want to measure anyway + Telemetry::Accumulate(Telemetry::COOKIE_LEAVE_SECURE_ALONE, + DOWNGRADE_SECURE_FROM_SECURE); + } + } + } + + foundCookie = FindCookie(aKey, aCookie->Host(), + aCookie->Name(), aCookie->Path(), exactIter); + + RefPtr<nsCookie> oldCookie; + nsCOMPtr<nsIArray> purgedList; + if (foundCookie) { + oldCookie = exactIter.Cookie(); + + // 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"); + return; + } + + // If the new cookie has the same value, expiry date, and isSecure, + // isSession, and isHttpOnly 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() && + // 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(mDBState, oldCookie); + return; + } + + // 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, u"deleted"); + 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. + nsCookieEntry *entry = mDBState->hostTable.GetEntry(aKey); + if (entry && entry->GetCookies().Length() >= mMaxCookiesPerHost) { + nsListIter iter; + // Prioritize evicting insecure cookies. + // (draft-ietf-httpbis-cookie-alone section 3.3) + mozilla::Maybe<bool> optionalSecurity = mLeaveSecureAlone ? Some(false) : Nothing(); + int64_t oldestCookieTime = FindStaleCookie(entry, currentTime, aHostURI, optionalSecurity, iter); + if (iter.entry == nullptr) { + if (aCookie->IsSecure()) { + // It's valid to evict a secure cookie for another secure cookie. + oldestCookieTime = FindStaleCookie(entry, currentTime, aHostURI, Some(true), iter); + } else { + Telemetry::Accumulate(Telemetry::COOKIE_LEAVE_SECURE_ALONE, + EVICTING_SECURE_BLOCKED); + COOKIE_LOGEVICTED(aCookie, + "Too many cookies for this domain and the new cookie is not a secure cookie"); + return; + } + } + + MOZ_ASSERT(iter.entry); + + oldCookie = iter.Cookie(); + if (oldestCookieTime > 0 && mLeaveSecureAlone) { + TelemetryForEvictingStaleCookie(oldCookie, oldestCookieTime); + } + + // remove the oldest cookie from the domain + RemoveCookieFromList(iter); + COOKIE_LOGEVICTED(oldCookie, "Too many cookies for this domain"); + purgedList = CreatePurgeList(oldCookie); + } else if (mDBState->cookieCount >= ADD_TEN_PERCENT(mMaxNumberOfCookies)) { + int64_t maxAge = aCurrentTimeInUsec - mDBState->cookieOldestTime; + 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 cookieOldestTime indicator can be pessimistic - if it's + // older than the actual oldest cookie, we'll just purge more eagerly. + purgedList = PurgeCookies(aCurrentTimeInUsec); + } + } + } + + // 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(aKey, aCookie, mDBState, nullptr); + COOKIE_LOGSUCCESS(SET_COOKIE, aHostURI, aCookieHeader, aCookie, foundCookie); + + // Now that list mutations are complete, notify observers. We do it here + // because observers may themselves attempt to mutate the list. + if (purgedList) { + NotifyChanged(purgedList, u"batch-deleted"); + } + + NotifyChanged(aCookie, foundCookie ? u"changed" : u"added"); +} + +/****************************************************************************** + * nsCookieService impl: + * private cookie header parsing functions + ******************************************************************************/ + +// 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. cookie <NAME> is optional, where spec requires it. This is a fairly + trivial case, but allows the flexibility of setting only a cookie <VALUE> + with a blank <NAME> and is required by some sites (see bug 169091). + + 6. 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" + +******************************************************************************/ + +// helper functions for GetTokenValue +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 +nsCookieService::GetTokenValue(nsASingleFragmentCString::const_char_iterator &aIter, + nsASingleFragmentCString::const_char_iterator &aEndIter, + nsDependentCSubstring &aTokenString, + nsDependentCSubstring &aTokenValue, + bool &aEqualsFound) +{ + nsASingleFragmentCString::const_char_iterator start, 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 && !istokenseparator(*aIter)) + ++aIter; + + // remove trailing <LWS>; first check we're not at the beginning + lastSpace = aIter; + if (lastSpace != start) { + while (--lastSpace != start && iswhitespace(*lastSpace)) + continue; + ++lastSpace; + } + aTokenString.Rebind(start, lastSpace); + + aEqualsFound = (*aIter == '='); + if (aEqualsFound) { + // find <value> + while (++aIter != aEndIter && iswhitespace(*aIter)) + continue; + + start = aIter; + + // process <token> + // just look for ';' to terminate ('=' allowed) + while (aIter != aEndIter && !isvalueseparator(*aIter)) + ++aIter; + + // remove trailing <LWS>; first check we're not at the beginning + if (aIter != start) { + lastSpace = aIter; + while (--lastSpace != start && iswhitespace(*lastSpace)) + continue; + aTokenValue.Rebind(start, ++lastSpace); + } + } + + // aIter is on ';', or terminator, or EOS + if (aIter != aEndIter) { + // if on terminator, increment past & return true to process new cookie + if (isterminator(*aIter)) { + ++aIter; + return true; + } + // fall-through: aIter is on ';', increment and return false + ++aIter; + } + return false; +} + +// 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 +nsCookieService::ParseAttributes(nsDependentCString &aCookieHeader, + nsCookieAttributes &aCookieAttributes) +{ + 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"; + + nsASingleFragmentCString::const_char_iterator tempBegin, tempEnd; + nsASingleFragmentCString::const_char_iterator cookieStart, cookieEnd; + aCookieHeader.BeginReading(cookieStart); + aCookieHeader.EndReading(cookieEnd); + + aCookieAttributes.isSecure = false; + aCookieAttributes.isHttpOnly = false; + + nsDependentCSubstring tokenString(cookieStart, cookieStart); + nsDependentCSubstring tokenValue (cookieStart, cookieStart); + bool newCookie, 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) { + aCookieAttributes.name = tokenString; + aCookieAttributes.value = tokenValue; + } else { + aCookieAttributes.value = tokenString; + } + + // extract remaining attributes + while (cookieStart != cookieEnd && !newCookie) { + newCookie = GetTokenValue(cookieStart, cookieEnd, tokenString, tokenValue, equalsFound); + + if (!tokenValue.IsEmpty()) { + tokenValue.BeginReading(tempBegin); + tokenValue.EndReading(tempEnd); + } + + // decide which attribute we have, and copy the string + if (tokenString.LowerCaseEqualsLiteral(kPath)) + aCookieAttributes.path = tokenValue; + + else if (tokenString.LowerCaseEqualsLiteral(kDomain)) + aCookieAttributes.host = tokenValue; + + else if (tokenString.LowerCaseEqualsLiteral(kExpires)) + aCookieAttributes.expires = tokenValue; + + else if (tokenString.LowerCaseEqualsLiteral(kMaxage)) + aCookieAttributes.maxage = tokenValue; + + // ignore any tokenValue for isSecure; just set the boolean + else if (tokenString.LowerCaseEqualsLiteral(kSecure)) + aCookieAttributes.isSecure = true; + + // ignore any tokenValue for isHttpOnly (see bug 178993); + // just set the boolean + else if (tokenString.LowerCaseEqualsLiteral(kHttpOnly)) + aCookieAttributes.isHttpOnly = true; + } + + // rebind aCookieHeader, in case we need to process another cookie + aCookieHeader.Rebind(cookieStart, cookieEnd); + return newCookie; +} + +/****************************************************************************** + * nsCookieService impl: + * private domain & permission compliance enforcement functions + ******************************************************************************/ + +// 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 +nsCookieService::GetBaseDomain(nsIURI *aHostURI, + nsCString &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 = mTLDService->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 = aHostURI->GetAsciiHost(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()) { + bool isFileURI = false; + aHostURI->SchemeIs("file", &isFileURI); + if (!isFileURI) + return NS_ERROR_INVALID_ARG; + } + + 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(): 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 +nsCookieService::GetBaseDomainFromHost(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 = mTLDService->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; +} + +// 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 +nsCookieService::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 nsCString &a, const nsCString &b) +{ + if (a == b) + return true; + if (a.Length() > b.Length()) + return a[a.Length() - b.Length() - 1] == '.' && StringEndsWith(a, b); + return false; +} + +CookieStatus +nsCookieService::CheckPrefs(nsIURI *aHostURI, + bool aIsForeign, + const char *aCookieHeader) +{ + nsresult rv; + + // don't let ftp sites get/set cookies (could be a security issue) + bool ftp; + if (NS_SUCCEEDED(aHostURI->SchemeIs("ftp", &ftp)) && ftp) { + COOKIE_LOGFAILURE(aCookieHeader ? SET_COOKIE : GET_COOKIE, aHostURI, aCookieHeader, "ftp sites cannot read cookies"); + return STATUS_REJECTED_WITH_ERROR; + } + + // check the permission list first; if we find an entry, it overrides + // default prefs. see bug 184059. + if (mPermissionService) { + nsCookieAccess access; + // Not passing an nsIChannel here is probably OK; our implementation + // doesn't do anything with it anyway. + rv = mPermissionService->CanAccess(aHostURI, nullptr, &access); + + // if we found an entry, use it + if (NS_SUCCEEDED(rv)) { + switch (access) { + case nsICookiePermission::ACCESS_DENY: + COOKIE_LOGFAILURE(aCookieHeader ? SET_COOKIE : GET_COOKIE, aHostURI, + aCookieHeader, "cookies are blocked for this site"); + return STATUS_REJECTED; + + case nsICookiePermission::ACCESS_ALLOW: + return STATUS_ACCEPTED; + + case nsICookiePermission::ACCESS_ALLOW_FIRST_PARTY_ONLY: + if (aIsForeign) { + COOKIE_LOGFAILURE(aCookieHeader ? SET_COOKIE : GET_COOKIE, aHostURI, + aCookieHeader, "third party cookies are blocked " + "for this site"); + return STATUS_REJECTED; + + } + return STATUS_ACCEPTED; + + case nsICookiePermission::ACCESS_LIMIT_THIRD_PARTY: + if (!aIsForeign) + return STATUS_ACCEPTED; + uint32_t priorCookieCount = 0; + nsAutoCString hostFromURI; + aHostURI->GetHost(hostFromURI); + CountCookiesFromHost(hostFromURI, &priorCookieCount); + if (priorCookieCount == 0) { + COOKIE_LOGFAILURE(aCookieHeader ? SET_COOKIE : GET_COOKIE, aHostURI, + aCookieHeader, "third party cookies are blocked " + "for this site"); + return STATUS_REJECTED; + } + return STATUS_ACCEPTED; + } + } + } + + // check default prefs + if (mCookieBehavior == nsICookieService::BEHAVIOR_REJECT) { + COOKIE_LOGFAILURE(aCookieHeader ? SET_COOKIE : GET_COOKIE, aHostURI, aCookieHeader, "cookies are disabled"); + return STATUS_REJECTED; + } + + // check if cookie is foreign + if (aIsForeign) { + if (mCookieBehavior == nsICookieService::BEHAVIOR_ACCEPT && mThirdPartySession) + return STATUS_ACCEPT_SESSION; + + if (mCookieBehavior == nsICookieService::BEHAVIOR_REJECT_FOREIGN) { + COOKIE_LOGFAILURE(aCookieHeader ? SET_COOKIE : GET_COOKIE, aHostURI, aCookieHeader, "context is third party"); + return STATUS_REJECTED; + } + + if (mCookieBehavior == nsICookieService::BEHAVIOR_LIMIT_FOREIGN) { + uint32_t priorCookieCount = 0; + nsAutoCString hostFromURI; + aHostURI->GetHost(hostFromURI); + CountCookiesFromHost(hostFromURI, &priorCookieCount); + if (priorCookieCount == 0) { + COOKIE_LOGFAILURE(aCookieHeader ? SET_COOKIE : GET_COOKIE, aHostURI, aCookieHeader, "context is third party"); + return STATUS_REJECTED; + } + if (mThirdPartySession) + 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 +nsCookieService::CheckDomain(nsCookieAttributes &aCookieAttributes, + nsIURI *aHostURI, + const nsCString &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; + aHostURI->GetAsciiHost(hostFromURI); + + // if a domain is given, check the host has permission + if (!aCookieAttributes.host.IsEmpty()) { + // Tolerate leading '.' characters, but not if it's otherwise an empty host. + if (aCookieAttributes.host.Length() > 1 && + aCookieAttributes.host.First() == '.') { + aCookieAttributes.host.Cut(0, 1); + } + + // switch to lowercase now, to avoid case-insensitive compares everywhere + ToLowerCase(aCookieAttributes.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(aCookieAttributes.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(aCookieAttributes.host, aBaseDomain) && + IsSubdomainOf(hostFromURI, aCookieAttributes.host)) { + // prepend a dot to indicate a domain cookie + aCookieAttributes.host.Insert(NS_LITERAL_CSTRING("."), 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 + aCookieAttributes.host = hostFromURI; + return true; +} + +nsCString +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->GetPath(path); + int32_t slash = path.RFindChar('/'); + if (slash != kNotFound) { + path.Truncate(slash + 1); + } + } + return path; +} + +bool +nsCookieService::CheckPath(nsCookieAttributes &aCookieAttributes, + nsIURI *aHostURI) +{ + // if a path is given, check the host has permission + if (aCookieAttributes.path.IsEmpty() || aCookieAttributes.path.First() != '/') { + aCookieAttributes.path = GetPathFromURI(aHostURI); + +#if 0 + } else { + /** + * The following test is part of the RFC2109 spec. Loosely speaking, it says that a site + * cannot set a cookie for a path that it is not on. See bug 155083. However this patch + * broke several sites -- nordea (bug 155768) and citibank (bug 156725). So this test has + * been disabled, unless we can evangelize these sites. + */ + // get path from aHostURI + nsAutoCString pathFromURI; + if (NS_FAILED(aHostURI->GetPath(pathFromURI)) || + !StringBeginsWith(pathFromURI, aCookieAttributes.path)) { + return false; + } +#endif + } + + if (aCookieAttributes.path.Length() > kMaxBytesPerPath || + aCookieAttributes.path.Contains('\t')) + return false; + + return true; +} + +// CheckPrefixes +// +// Reject cookies whose name starts with the magic prefixes from +// https://tools.ietf.org/html/draft-ietf-httpbis-cookie-prefixes-00 +// if they do not meet the criteria required by the prefix. +// +// Must not be called until after CheckDomain() and CheckPath() have +// regularized and validated the nsCookieAttributes values! +bool +nsCookieService::CheckPrefixes(nsCookieAttributes &aCookieAttributes, + 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( aCookieAttributes.name.get(), kSecure, kSecureLen ) == 0; + bool isHost = strncmp( aCookieAttributes.name.get(), kHost, kHostLen ) == 0; + + if ( !isSecure && !isHost ) { + // not one of the magic prefixes: carry on + return true; + } + + if ( !aSecureRequest || !aCookieAttributes.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 ( aCookieAttributes.host[0] == '.' || + !aCookieAttributes.path.EqualsLiteral( "/" )) { + return false; + } + } + + return true; +} + +bool +nsCookieService::GetExpiry(nsCookieAttributes &aCookieAttributes, + int64_t aServerTime, + int64_t aCurrentTime) +{ + /* 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 (!aCookieAttributes.maxage.IsEmpty()) { + // obtain numeric value of maxageAttribute + int64_t maxage; + int32_t numInts = PR_sscanf(aCookieAttributes.maxage.get(), "%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. + aCookieAttributes.expiryTime = aCurrentTime + maxage; + + // check for expires attribute + } else if (!aCookieAttributes.expires.IsEmpty()) { + PRTime expires; + + // parse expiry time + if (PR_ParseTimeString(aCookieAttributes.expires.get(), 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. + aCookieAttributes.expiryTime = expires / int64_t(PR_USEC_PER_SEC); + + // default to session cookie if no attributes found + } else { + return true; + } + + return false; +} + +/****************************************************************************** + * nsCookieService impl: + * private cookielist management functions + ******************************************************************************/ + +void +nsCookieService::RemoveAllFromMemory() +{ + // clearing the hashtable will call each nsCookieEntry's dtor, + // which releases all their respective children. + mDBState->hostTable.Clear(); + mDBState->cookieCount = 0; + mDBState->cookieOldestTime = INT64_MAX; +} + +// comparator class for lastaccessed times of cookies. +class CompareCookiesByAge { +public: + bool Equals(const nsListIter &a, const nsListIter &b) const + { + return a.Cookie()->LastAccessed() == b.Cookie()->LastAccessed() && + a.Cookie()->CreationTime() == b.Cookie()->CreationTime(); + } + + bool LessThan(const nsListIter &a, const nsListIter &b) const + { + // 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(); + } +}; + +// comparator class for sorting cookies by entry and index. +class CompareCookiesByIndex { +public: + bool Equals(const nsListIter &a, const nsListIter &b) const + { + NS_ASSERTION(a.entry != b.entry || a.index != b.index, + "cookie indexes should never be equal"); + return false; + } + + bool LessThan(const nsListIter &a, const nsListIter &b) const + { + // compare by entryclass pointer, then by index. + if (a.entry != b.entry) + return a.entry < b.entry; + + return a.index < b.index; + } +}; + +// purges expired and old cookies in a batch operation. +already_AddRefed<nsIArray> +nsCookieService::PurgeCookies(int64_t aCurrentTimeInUsec) +{ + NS_ASSERTION(mDBState->hostTable.Count() > 0, "table is empty"); + EnsureReadComplete(); + + uint32_t initialCookieCount = mDBState->cookieCount; + COOKIE_LOGSTRING(LogLevel::Debug, + ("PurgeCookies(): beginning purge with %ld cookies and %lld oldest age", + mDBState->cookieCount, aCurrentTimeInUsec - mDBState->cookieOldestTime)); + + typedef nsTArray<nsListIter> PurgeList; + PurgeList purgeList(kMaxNumberOfCookies); + + nsCOMPtr<nsIMutableArray> removedList = do_CreateInstance(NS_ARRAY_CONTRACTID); + + // 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. + mozIStorageAsyncStatement *stmt = mDBState->stmtDelete; + nsCOMPtr<mozIStorageBindingParamsArray> paramsArray; + if (mDBState->dbConn) { + stmt->NewBindingParamsArray(getter_AddRefs(paramsArray)); + } + + int64_t currentTime = aCurrentTimeInUsec / PR_USEC_PER_SEC; + int64_t purgeTime = aCurrentTimeInUsec - mCookiePurgeAge; + int64_t oldestTime = INT64_MAX; + + for (auto iter = mDBState->hostTable.Iter(); !iter.Done(); iter.Next()) { + nsCookieEntry* entry = iter.Get(); + + const nsCookieEntry::ArrayType& cookies = entry->GetCookies(); + auto length = cookies.Length(); + for (nsCookieEntry::IndexType i = 0; i < length; ) { + nsListIter iter(entry, i); + nsCookie* cookie = cookies[i]; + + // check if the cookie has expired + if (cookie->Expiry() <= currentTime) { + removedList->AppendElement(cookie, false); + COOKIE_LOGEVICTED(cookie, "Cookie expired"); + + // remove from list; do not increment our iterator unless we're the last + // in the list already. + gCookieService->RemoveCookieFromList(iter, paramsArray); + 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 = mDBState->cookieCount; + + // 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 = mDBState->cookieCount > mMaxNumberOfCookies ? + mDBState->cookieCount - mMaxNumberOfCookies : 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--; ) { + nsCookie *cookie = purgeList[i].Cookie(); + removedList->AppendElement(cookie, false); + COOKIE_LOGEVICTED(cookie, "Cookie too old"); + + RemoveCookieFromList(purgeList[i], paramsArray); + } + + // Update the database if we have entries to purge. + if (paramsArray) { + uint32_t length; + paramsArray->GetLength(&length); + if (length) { + DebugOnly<nsresult> rv = stmt->BindParameters(paramsArray); + NS_ASSERT_SUCCESS(rv); + nsCOMPtr<mozIStoragePendingStatement> handle; + rv = stmt->ExecuteAsync(mDBState->removeListener, getter_AddRefs(handle)); + NS_ASSERT_SUCCESS(rv); + } + } + + // reset the oldest time indicator + mDBState->cookieOldestTime = oldestTime; + + COOKIE_LOGSTRING(LogLevel::Debug, + ("PurgeCookies(): %ld expired; %ld purged; %ld remain; %lld oldest age", + initialCookieCount - postExpiryCookieCount, + postExpiryCookieCount - mDBState->cookieCount, + mDBState->cookieCount, + aCurrentTimeInUsec - mDBState->cookieOldestTime)); + + return removedList.forget(); +} + +// find whether a given cookie has been previously set. this is provided by the +// nsICookieManager2 interface. +NS_IMETHODIMP +nsCookieService::CookieExists(nsICookie2* aCookie, + JS::HandleValue aOriginAttributes, + JSContext* aCx, + uint8_t aArgc, + bool* aFoundCookie) +{ + NS_ENSURE_ARG_POINTER(aCookie); + NS_ENSURE_ARG_POINTER(aCx); + NS_ENSURE_ARG_POINTER(aFoundCookie); + MOZ_ASSERT(aArgc == 0 || aArgc == 1); + + NeckoOriginAttributes attrs; + nsresult rv = InitializeOriginAttributes(&attrs, + aOriginAttributes, + aCx, + aArgc, + u"nsICookieManager2.cookieExists()", + u"2"); + NS_ENSURE_SUCCESS(rv, rv); + + return CookieExistsNative(aCookie, &attrs, aFoundCookie); +} + +NS_IMETHODIMP_(nsresult) +nsCookieService::CookieExistsNative(nsICookie2* aCookie, + NeckoOriginAttributes* aOriginAttributes, + bool* aFoundCookie) +{ + NS_ENSURE_ARG_POINTER(aCookie); + NS_ENSURE_ARG_POINTER(aOriginAttributes); + NS_ENSURE_ARG_POINTER(aFoundCookie); + + if (!mDBState) { + NS_WARNING("No DBState! Profile already closed?"); + return NS_ERROR_NOT_AVAILABLE; + } + + nsAutoCString host, name, path; + nsresult rv = aCookie->GetHost(host); + NS_ENSURE_SUCCESS(rv, rv); + rv = aCookie->GetName(name); + NS_ENSURE_SUCCESS(rv, rv); + rv = aCookie->GetPath(path); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString baseDomain; + rv = GetBaseDomainFromHost(host, baseDomain); + NS_ENSURE_SUCCESS(rv, rv); + + nsListIter iter; + *aFoundCookie = FindCookie(nsCookieKey(baseDomain, *aOriginAttributes), + host, name, path, iter); + return NS_OK; +} + +// For a given base domain, find either an expired cookie or the oldest cookie +// by lastAccessed time. +int64_t +nsCookieService::FindStaleCookie(nsCookieEntry *aEntry, + int64_t aCurrentTime, + nsIURI* aSource, + mozilla::Maybe<bool> aIsSecure, + nsListIter &aIter) +{ + aIter.entry = nullptr; + bool requireHostMatch = true; + nsAutoCString baseDomain, sourceHost, sourcePath; + if (aSource) { + GetBaseDomain(aSource, baseDomain, requireHostMatch); + aSource->GetAsciiHost(sourceHost); + sourcePath = GetPathFromURI(aSource); + } + + const nsCookieEntry::ArrayType &cookies = aEntry->GetCookies(); + + int64_t oldestNonMatchingSessionCookieTime = 0; + nsListIter oldestNonMatchingSessionCookie; + oldestNonMatchingSessionCookie.entry = nullptr; + + int64_t oldestSessionCookieTime = 0; + nsListIter oldestSessionCookie; + oldestSessionCookie.entry = nullptr; + + int64_t oldestNonMatchingNonSessionCookieTime = 0; + nsListIter oldestNonMatchingNonSessionCookie; + oldestNonMatchingNonSessionCookie.entry = nullptr; + + int64_t oldestCookieTime = 0; + nsListIter oldestCookie; + oldestCookie.entry = nullptr; + + int64_t actualOldestCookieTime = cookies.Length() ? cookies[0]->LastAccessed() : 0; + for (nsCookieEntry::IndexType i = 0; i < cookies.Length(); ++i) { + nsCookie *cookie = cookies[i]; + + // If we found an expired cookie, we're done. + if (cookie->Expiry() <= aCurrentTime) { + aIter.entry = aEntry; + aIter.index = i; + return -1; + } + + int64_t lastAccessed = cookie->LastAccessed(); + // Record the age of the oldest cookie that is stored for this host. + // oldestCookieTime is the age of the oldest cookie with a matching + // secure flag, which may be more recent than an older cookie with + // a non-matching secure flag. + if (actualOldestCookieTime > lastAccessed) { + actualOldestCookieTime = lastAccessed; + } + if (aIsSecure.isSome() && !aIsSecure.value()) { + // We want to look for the oldest non-secure cookie first time through, + // then find the oldest secure cookie the second time we are called. + if (cookie->IsSecure()) { + continue; + } + } + + // Update our various records of oldest cookies fitting several restrictions: + // * session cookies + // * non-session cookies + // * cookies with paths and domains that don't match the cookie triggering this purge + + // This cookie is a candidate for eviction if we have no information about + // the source request, or if it is not a path or domain match against the + // source request. + bool isPrimaryEvictionCandidate = true; + if (aSource) { + isPrimaryEvictionCandidate = !PathMatches(cookie, sourcePath) || !DomainMatches(cookie, sourceHost); + } + + if (cookie->IsSession()) { + if (!oldestSessionCookie.entry || oldestSessionCookieTime > lastAccessed) { + oldestSessionCookieTime = lastAccessed; + oldestSessionCookie.entry = aEntry; + oldestSessionCookie.index = i; + } + + if (isPrimaryEvictionCandidate && + (!oldestNonMatchingSessionCookie.entry || + oldestNonMatchingSessionCookieTime > lastAccessed)) { + oldestNonMatchingSessionCookieTime = lastAccessed; + oldestNonMatchingSessionCookie.entry = aEntry; + oldestNonMatchingSessionCookie.index = i; + } + } else if (isPrimaryEvictionCandidate && + (!oldestNonMatchingNonSessionCookie.entry || + oldestNonMatchingNonSessionCookieTime > lastAccessed)) { + oldestNonMatchingNonSessionCookieTime = lastAccessed; + oldestNonMatchingNonSessionCookie.entry = aEntry; + oldestNonMatchingNonSessionCookie.index = i; + } + + // Check if we've found the oldest cookie so far. + if (!oldestCookie.entry || oldestCookieTime > lastAccessed) { + oldestCookieTime = lastAccessed; + oldestCookie.entry = aEntry; + oldestCookie.index = i; + } + } + + // Prefer to evict the oldest session cookies with a non-matching path/domain, + // followed by the oldest session cookie with a matching path/domain, + // followed by the oldest non-session cookie with a non-matching path/domain, + // resorting to the oldest non-session cookie with a matching path/domain. + if (oldestNonMatchingSessionCookie.entry) { + aIter = oldestNonMatchingSessionCookie; + } else if (oldestSessionCookie.entry) { + aIter = oldestSessionCookie; + } else if (oldestNonMatchingNonSessionCookie.entry) { + aIter = oldestNonMatchingNonSessionCookie; + } else { + aIter = oldestCookie; + } + + return actualOldestCookieTime; +} + +void +nsCookieService::TelemetryForEvictingStaleCookie(nsCookie *aEvicted, + int64_t oldestCookieTime) +{ + // We need to record the evicting cookie to telemetry. + if (!aEvicted->IsSecure()) { + if (aEvicted->LastAccessed() > oldestCookieTime) { + Telemetry::Accumulate(Telemetry::COOKIE_LEAVE_SECURE_ALONE, + EVICTED_NEWER_INSECURE); + } else { + Telemetry::Accumulate(Telemetry::COOKIE_LEAVE_SECURE_ALONE, + EVICTED_OLDEST_COOKIE); + } + } else { + Telemetry::Accumulate(Telemetry::COOKIE_LEAVE_SECURE_ALONE, + EVICTED_PREFERRED_COOKIE); + } +} + +// count the number of cookies stored by a particular host. this is provided by the +// nsICookieManager2 interface. +NS_IMETHODIMP +nsCookieService::CountCookiesFromHost(const nsACString &aHost, + uint32_t *aCountFromHost) +{ + if (!mDBState) { + NS_WARNING("No DBState! Profile already closed?"); + 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); + + nsAutoCString baseDomain; + rv = GetBaseDomainFromHost(host, baseDomain); + NS_ENSURE_SUCCESS(rv, rv); + + nsCookieKey key = DEFAULT_APP_KEY(baseDomain); + EnsureReadDomain(key); + + // Return a count of all cookies, including expired. + nsCookieEntry *entry = mDBState->hostTable.GetEntry(key); + *aCountFromHost = entry ? entry->GetCookies().Length() : 0; + return NS_OK; +} + +// get an enumerator of cookies stored by a particular host. this is provided by the +// nsICookieManager2 interface. +NS_IMETHODIMP +nsCookieService::GetCookiesFromHost(const nsACString &aHost, + JS::HandleValue aOriginAttributes, + JSContext* aCx, + uint8_t aArgc, + nsISimpleEnumerator **aEnumerator) +{ + MOZ_ASSERT(aArgc == 0 || aArgc == 1); + + if (!mDBState) { + NS_WARNING("No DBState! Profile already closed?"); + 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); + + nsAutoCString baseDomain; + rv = GetBaseDomainFromHost(host, baseDomain); + NS_ENSURE_SUCCESS(rv, rv); + + NeckoOriginAttributes attrs; + rv = InitializeOriginAttributes(&attrs, + aOriginAttributes, + aCx, + aArgc, + u"nsICookieManager2.getCookiesFromHost()", + u"2"); + NS_ENSURE_SUCCESS(rv, rv); + + nsCookieKey key = nsCookieKey(baseDomain, attrs); + EnsureReadDomain(key); + + nsCookieEntry *entry = mDBState->hostTable.GetEntry(key); + if (!entry) + return NS_NewEmptyEnumerator(aEnumerator); + + nsCOMArray<nsICookie> cookieList(mMaxCookiesPerHost); + const nsCookieEntry::ArrayType &cookies = entry->GetCookies(); + for (nsCookieEntry::IndexType i = 0; i < cookies.Length(); ++i) { + cookieList.AppendObject(cookies[i]); + } + + return NS_NewArrayEnumerator(aEnumerator, cookieList); +} + +NS_IMETHODIMP +nsCookieService::GetCookiesWithOriginAttributes(const nsAString& aPattern, + const nsACString& aHost, + nsISimpleEnumerator **aEnumerator) +{ + mozilla::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 = GetBaseDomainFromHost(host, baseDomain); + NS_ENSURE_SUCCESS(rv, rv); + + return GetCookiesWithOriginAttributes(pattern, baseDomain, aEnumerator); +} + +nsresult +nsCookieService::GetCookiesWithOriginAttributes( + const mozilla::OriginAttributesPattern& aPattern, + const nsCString& aBaseDomain, + nsISimpleEnumerator **aEnumerator) +{ + if (!mDBState) { + NS_WARNING("No DBState! Profile already closed?"); + return NS_ERROR_NOT_AVAILABLE; + } + + if (aPattern.mAppId.WasPassed() && aPattern.mAppId.Value() == NECKO_UNKNOWN_APP_ID) { + return NS_ERROR_INVALID_ARG; + } + + nsCOMArray<nsICookie> cookies; + for (auto iter = mDBState->hostTable.Iter(); !iter.Done(); iter.Next()) { + nsCookieEntry* entry = iter.Get(); + + if (!aBaseDomain.IsEmpty() && !aBaseDomain.Equals(entry->mBaseDomain)) { + continue; + } + + if (!aPattern.Matches(entry->mOriginAttributes)) { + continue; + } + + const nsCookieEntry::ArrayType& entryCookies = entry->GetCookies(); + + for (nsCookieEntry::IndexType i = 0; i < entryCookies.Length(); ++i) { + cookies.AppendObject(entryCookies[i]); + } + } + + return NS_NewArrayEnumerator(aEnumerator, cookies); +} + +NS_IMETHODIMP +nsCookieService::RemoveCookiesWithOriginAttributes(const nsAString& aPattern, + const nsACString& aHost) +{ + MOZ_ASSERT(XRE_IsParentProcess()); + + mozilla::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 = GetBaseDomainFromHost(host, baseDomain); + NS_ENSURE_SUCCESS(rv, rv); + + return RemoveCookiesWithOriginAttributes(pattern, baseDomain); +} + +nsresult +nsCookieService::RemoveCookiesWithOriginAttributes( + const mozilla::OriginAttributesPattern& aPattern, + const nsCString& aBaseDomain) +{ + if (!mDBState) { + NS_WARNING("No DBState! Profile already close?"); + return NS_ERROR_NOT_AVAILABLE; + } + + // Iterate the hash table of nsCookieEntry. + for (auto iter = mDBState->hostTable.Iter(); !iter.Done(); iter.Next()) { + nsCookieEntry* entry = iter.Get(); + + if (!aBaseDomain.IsEmpty() && !aBaseDomain.Equals(entry->mBaseDomain)) { + continue; + } + + if (!aPattern.Matches(entry->mOriginAttributes)) { + continue; + } + + // Pattern matches. Delete all cookies within this nsCookieEntry. + uint32_t cookiesCount = entry->GetCookies().Length(); + + for (nsCookieEntry::IndexType i = 0 ; i < cookiesCount; ++i) { + // Remove the first cookie from the list. + nsListIter iter(entry, 0); + RefPtr<nsCookie> cookie = iter.Cookie(); + + // Remove the cookie. + RemoveCookieFromList(iter); + + if (cookie) { + NotifyChanged(cookie, u"deleted"); + } + } + } + + return NS_OK; +} + +// find an secure cookie specified by host and name +bool +nsCookieService::FindSecureCookie(const nsCookieKey &aKey, + nsCookie *aCookie) +{ + EnsureReadDomain(aKey); + + nsCookieEntry *entry = mDBState->hostTable.GetEntry(aKey); + if (!entry) + return false; + + const nsCookieEntry::ArrayType &cookies = entry->GetCookies(); + for (nsCookieEntry::IndexType i = 0; i < cookies.Length(); ++i) { + nsCookie *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 (DomainMatches(cookie, aCookie->Host()) || + 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 (PathMatches(cookie, aCookie->Path())) { + return true; + } + } + } + + return false; +} + +// find an exact cookie specified by host, name, and path that hasn't expired. +bool +nsCookieService::FindCookie(const nsCookieKey &aKey, + const nsAFlatCString &aHost, + const nsAFlatCString &aName, + const nsAFlatCString &aPath, + nsListIter &aIter) +{ + EnsureReadDomain(aKey); + + nsCookieEntry *entry = mDBState->hostTable.GetEntry(aKey); + if (!entry) + return false; + + const nsCookieEntry::ArrayType &cookies = entry->GetCookies(); + for (nsCookieEntry::IndexType i = 0; i < cookies.Length(); ++i) { + nsCookie *cookie = cookies[i]; + + if (aHost.Equals(cookie->Host()) && + aPath.Equals(cookie->Path()) && + aName.Equals(cookie->Name())) { + aIter = nsListIter(entry, i); + return true; + } + } + + return false; +} + +// remove a cookie from the hashtable, and update the iterator state. +void +nsCookieService::RemoveCookieFromList(const nsListIter &aIter, + mozIStorageBindingParamsArray *aParamsArray) +{ + // if it's a non-session cookie, remove it from the db + if (!aIter.Cookie()->IsSession() && mDBState->dbConn) { + // Use the asynchronous binding methods to ensure that we do not acquire + // the database lock. + mozIStorageAsyncStatement *stmt = mDBState->stmtDelete; + nsCOMPtr<mozIStorageBindingParamsArray> paramsArray(aParamsArray); + if (!paramsArray) { + stmt->NewBindingParamsArray(getter_AddRefs(paramsArray)); + } + + nsCOMPtr<mozIStorageBindingParams> params; + paramsArray->NewBindingParams(getter_AddRefs(params)); + + DebugOnly<nsresult> rv = + params->BindUTF8StringByName(NS_LITERAL_CSTRING("name"), + aIter.Cookie()->Name()); + NS_ASSERT_SUCCESS(rv); + + rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("host"), + aIter.Cookie()->Host()); + NS_ASSERT_SUCCESS(rv); + + rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("path"), + aIter.Cookie()->Path()); + NS_ASSERT_SUCCESS(rv); + + rv = paramsArray->AddParams(params); + NS_ASSERT_SUCCESS(rv); + + // If we weren't given a params array, we'll need to remove it ourselves. + if (!aParamsArray) { + rv = stmt->BindParameters(paramsArray); + NS_ASSERT_SUCCESS(rv); + nsCOMPtr<mozIStoragePendingStatement> handle; + rv = stmt->ExecuteAsync(mDBState->removeListener, getter_AddRefs(handle)); + NS_ASSERT_SUCCESS(rv); + } + } + + 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! + mDBState->hostTable.RawRemoveEntry(aIter.entry); + + } else { + // just remove the element from the list + aIter.entry->GetCookies().RemoveElementAt(aIter.index); + } + + --mDBState->cookieCount; +} + +void +bindCookieParameters(mozIStorageBindingParamsArray *aParamsArray, + const nsCookieKey &aKey, + const nsCookie *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)); + NS_ASSERT_SUCCESS(rv); + + // Bind our values to params + rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("baseDomain"), + aKey.mBaseDomain); + NS_ASSERT_SUCCESS(rv); + + nsAutoCString suffix; + aKey.mOriginAttributes.CreateSuffix(suffix); + rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("originAttributes"), + suffix); + NS_ASSERT_SUCCESS(rv); + + rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("name"), + aCookie->Name()); + NS_ASSERT_SUCCESS(rv); + + rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("value"), + aCookie->Value()); + NS_ASSERT_SUCCESS(rv); + + rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("host"), + aCookie->Host()); + NS_ASSERT_SUCCESS(rv); + + rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("path"), + aCookie->Path()); + NS_ASSERT_SUCCESS(rv); + + rv = params->BindInt64ByName(NS_LITERAL_CSTRING("expiry"), + aCookie->Expiry()); + NS_ASSERT_SUCCESS(rv); + + rv = params->BindInt64ByName(NS_LITERAL_CSTRING("lastAccessed"), + aCookie->LastAccessed()); + NS_ASSERT_SUCCESS(rv); + + rv = params->BindInt64ByName(NS_LITERAL_CSTRING("creationTime"), + aCookie->CreationTime()); + NS_ASSERT_SUCCESS(rv); + + rv = params->BindInt32ByName(NS_LITERAL_CSTRING("isSecure"), + aCookie->IsSecure()); + NS_ASSERT_SUCCESS(rv); + + rv = params->BindInt32ByName(NS_LITERAL_CSTRING("isHttpOnly"), + aCookie->IsHttpOnly()); + NS_ASSERT_SUCCESS(rv); + + // Bind the params to the array. + rv = aParamsArray->AddParams(params); + NS_ASSERT_SUCCESS(rv); +} + +void +nsCookieService::UpdateCookieOldestTime(DBState* aDBState, + nsCookie* aCookie) +{ + if (aCookie->LastAccessed() < aDBState->cookieOldestTime) { + aDBState->cookieOldestTime = aCookie->LastAccessed(); + } +} + +void +nsCookieService::AddCookieToList(const nsCookieKey &aKey, + nsCookie *aCookie, + DBState *aDBState, + mozIStorageBindingParamsArray *aParamsArray, + bool aWriteToDB) +{ + NS_ASSERTION(!(aDBState->dbConn && !aWriteToDB && aParamsArray), + "Not writing to the DB but have a params array?"); + NS_ASSERTION(!(!aDBState->dbConn && aParamsArray), + "Do not have a DB connection but have a params array?"); + + nsCookieEntry *entry = aDBState->hostTable.PutEntry(aKey); + NS_ASSERTION(entry, "can't insert element into a null entry!"); + + entry->GetCookies().AppendElement(aCookie); + ++aDBState->cookieCount; + + // keep track of the oldest cookie, for when it comes time to purge + UpdateCookieOldestTime(aDBState, aCookie); + + // if it's a non-session cookie and hasn't just been read from the db, write it out. + if (aWriteToDB && !aCookie->IsSession() && aDBState->dbConn) { + mozIStorageAsyncStatement *stmt = aDBState->stmtInsert; + nsCOMPtr<mozIStorageBindingParamsArray> paramsArray(aParamsArray); + if (!paramsArray) { + stmt->NewBindingParamsArray(getter_AddRefs(paramsArray)); + } + bindCookieParameters(paramsArray, aKey, aCookie); + + // If we were supplied an array to store parameters, we shouldn't call + // executeAsync - someone up the stack will do this for us. + if (!aParamsArray) { + DebugOnly<nsresult> rv = stmt->BindParameters(paramsArray); + NS_ASSERT_SUCCESS(rv); + nsCOMPtr<mozIStoragePendingStatement> handle; + rv = stmt->ExecuteAsync(mDBState->insertListener, getter_AddRefs(handle)); + NS_ASSERT_SUCCESS(rv); + } + } +} + +void +nsCookieService::UpdateCookieInList(nsCookie *aCookie, + int64_t aLastAccessed, + mozIStorageBindingParamsArray *aParamsArray) +{ + NS_ASSERTION(aCookie, "Passing a null cookie to UpdateCookieInList!"); + + // 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(NS_LITERAL_CSTRING("lastAccessed"), + aLastAccessed); + NS_ASSERT_SUCCESS(rv); + + rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("name"), + aCookie->Name()); + NS_ASSERT_SUCCESS(rv); + + rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("host"), + aCookie->Host()); + NS_ASSERT_SUCCESS(rv); + + rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("path"), + aCookie->Path()); + NS_ASSERT_SUCCESS(rv); + + // Add our bound parameters to the array. + rv = aParamsArray->AddParams(params); + NS_ASSERT_SUCCESS(rv); + } +} + +size_t +nsCookieService::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const +{ + size_t n = aMallocSizeOf(this); + + if (mDefaultDBState) { + n += mDefaultDBState->SizeOfIncludingThis(aMallocSizeOf); + } + if (mPrivateDBState) { + n += mPrivateDBState->SizeOfIncludingThis(aMallocSizeOf); + } + + return n; +} + +MOZ_DEFINE_MALLOC_SIZE_OF(CookieServiceMallocSizeOf) + +NS_IMETHODIMP +nsCookieService::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; +} diff --git a/netwerk/cookie/nsCookieService.h b/netwerk/cookie/nsCookieService.h new file mode 100644 index 000000000..e3b2d3e8a --- /dev/null +++ b/netwerk/cookie/nsCookieService.h @@ -0,0 +1,371 @@ +/* -*- 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 nsCookieService_h__ +#define nsCookieService_h__ + +#include "nsICookieService.h" +#include "nsICookieManager.h" +#include "nsICookieManager2.h" +#include "nsIObserver.h" +#include "nsWeakReference.h" + +#include "nsCookie.h" +#include "nsString.h" +#include "nsAutoPtr.h" +#include "nsHashKeys.h" +#include "nsIMemoryReporter.h" +#include "nsTHashtable.h" +#include "mozIStorageStatement.h" +#include "mozIStorageAsyncStatement.h" +#include "mozIStoragePendingStatement.h" +#include "mozIStorageConnection.h" +#include "mozIStorageRow.h" +#include "mozIStorageCompletionCallback.h" +#include "mozIStorageStatementCallback.h" +#include "mozIStorageFunction.h" +#include "nsIVariant.h" +#include "nsIFile.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/MemoryReporting.h" +#include "mozilla/Maybe.h" + +using mozilla::NeckoOriginAttributes; +using mozilla::OriginAttributes; + +class nsICookiePermission; +class nsIEffectiveTLDService; +class nsIIDNService; +class nsIPrefBranch; +class nsIObserverService; +class nsIURI; +class nsIChannel; +class nsIArray; +class mozIStorageService; +class mozIThirdPartyUtil; +class ReadCookieDBListener; + +struct nsCookieAttributes; +struct nsListIter; + +namespace mozilla { +namespace net { +class CookieServiceParent; +} // namespace net +} // namespace mozilla + +// hash key class +class nsCookieKey : public PLDHashEntryHdr +{ +public: + typedef const nsCookieKey& KeyType; + typedef const nsCookieKey* KeyTypePointer; + + nsCookieKey() + {} + + nsCookieKey(const nsCString &baseDomain, const NeckoOriginAttributes &attrs) + : mBaseDomain(baseDomain) + , mOriginAttributes(attrs) + {} + + explicit nsCookieKey(KeyTypePointer other) + : mBaseDomain(other->mBaseDomain) + , mOriginAttributes(other->mOriginAttributes) + {} + + nsCookieKey(KeyType other) + : mBaseDomain(other.mBaseDomain) + , mOriginAttributes(other.mOriginAttributes) + {} + + ~nsCookieKey() + {} + + bool KeyEquals(KeyTypePointer other) const + { + return mBaseDomain == other->mBaseDomain && + mOriginAttributes == other->mOriginAttributes; + } + + static KeyTypePointer KeyToPointer(KeyType aKey) + { + return &aKey; + } + + static PLDHashNumber HashKey(KeyTypePointer aKey) + { + // TODO: more efficient way to generate hash? + nsAutoCString temp(aKey->mBaseDomain); + temp.Append('#'); + nsAutoCString suffix; + aKey->mOriginAttributes.CreateSuffix(suffix); + temp.Append(suffix); + return mozilla::HashString(temp); + } + + size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const; + + enum { ALLOW_MEMMOVE = true }; + + nsCString mBaseDomain; + NeckoOriginAttributes mOriginAttributes; +}; + +// Inherit from nsCookieKey so this can be stored in nsTHashTable +// TODO: why aren't we using nsClassHashTable<nsCookieKey, ArrayType>? +class nsCookieEntry : public nsCookieKey +{ + public: + // Hash methods + typedef nsTArray< RefPtr<nsCookie> > ArrayType; + typedef ArrayType::index_type IndexType; + + explicit nsCookieEntry(KeyTypePointer aKey) + : nsCookieKey(aKey) + {} + + nsCookieEntry(const nsCookieEntry& toCopy) + { + // if we end up here, things will break. nsTHashtable shouldn't + // allow this, since we set ALLOW_MEMMOVE to true. + NS_NOTREACHED("nsCookieEntry copy constructor is forbidden!"); + } + + ~nsCookieEntry() + {} + + inline ArrayType& GetCookies() { return mCookies; } + + size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const; + + private: + ArrayType mCookies; +}; + +// encapsulates a (key, nsCookie) tuple for temporary storage purposes. +struct CookieDomainTuple +{ + nsCookieKey key; + RefPtr<nsCookie> cookie; + + size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const; +}; + +// encapsulates in-memory and on-disk DB states, so we can +// conveniently switch state when entering or exiting private browsing. +struct DBState final +{ + DBState() : cookieCount(0), cookieOldestTime(INT64_MAX), corruptFlag(OK) + { + } + +private: + // Private destructor, to discourage deletion outside of Release(): + ~DBState() + { + } + +public: + NS_INLINE_DECL_REFCOUNTING(DBState) + + size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const; + + // State of the database connection. + enum CorruptFlag { + OK, // normal + CLOSING_FOR_REBUILD, // corruption detected, connection closing + REBUILDING // close complete, rebuilding database from memory + }; + + nsTHashtable<nsCookieEntry> hostTable; + uint32_t cookieCount; + int64_t cookieOldestTime; + nsCOMPtr<nsIFile> cookieFile; + nsCOMPtr<mozIStorageConnection> dbConn; + nsCOMPtr<mozIStorageAsyncStatement> stmtInsert; + nsCOMPtr<mozIStorageAsyncStatement> stmtDelete; + nsCOMPtr<mozIStorageAsyncStatement> stmtUpdate; + CorruptFlag corruptFlag; + + // Various parts representing asynchronous read state. These are useful + // while the background read is taking place. + nsCOMPtr<mozIStorageConnection> syncConn; + nsCOMPtr<mozIStorageStatement> stmtReadDomain; + nsCOMPtr<mozIStoragePendingStatement> pendingRead; + // The asynchronous read listener. This is a weak ref (storage has ownership) + // since it may need to outlive the DBState's database connection. + ReadCookieDBListener* readListener; + // An array of (baseDomain, cookie) tuples representing data read in + // asynchronously. This is merged into hostTable once read is complete. + nsTArray<CookieDomainTuple> hostArray; + // A hashset of baseDomains read in synchronously, while the async read is + // in flight. This is used to keep track of which data in hostArray is stale + // when the time comes to merge. + nsTHashtable<nsCookieKey> readSet; + + // DB completion handlers. + nsCOMPtr<mozIStorageStatementCallback> insertListener; + nsCOMPtr<mozIStorageStatementCallback> updateListener; + nsCOMPtr<mozIStorageStatementCallback> removeListener; + nsCOMPtr<mozIStorageCompletionCallback> closeListener; +}; + +// 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 +}; + +// Result codes for TryInitDB() and Read(). +enum OpenDBResult +{ + RESULT_OK, + RESULT_RETRY, + RESULT_FAILURE +}; + +/****************************************************************************** + * nsCookieService: + * class declaration + ******************************************************************************/ + +class nsCookieService final : public nsICookieService + , public nsICookieManager2 + , public nsIObserver + , public nsSupportsWeakReference + , public nsIMemoryReporter +{ + private: + size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const; + + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + NS_DECL_NSICOOKIESERVICE + NS_DECL_NSICOOKIEMANAGER + NS_DECL_NSICOOKIEMANAGER2 + NS_DECL_NSIMEMORYREPORTER + + nsCookieService(); + static 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 void AppClearDataObserverInit(); + + protected: + virtual ~nsCookieService(); + + void PrefChanged(nsIPrefBranch *aPrefBranch); + void InitDBStates(); + OpenDBResult TryInitDB(bool aDeleteExistingDB); + nsresult CreateTable(); + nsresult CreateTableForSchemaVersion6(); + nsresult CreateTableForSchemaVersion5(); + void CloseDBStates(); + void CleanupCachedStatements(); + void CleanupDefaultDBConnection(); + void HandleDBClosed(DBState* aDBState); + void HandleCorruptDB(DBState* aDBState); + void RebuildCorruptDB(DBState* aDBState); + OpenDBResult Read(); + template<class T> nsCookie* GetCookieFromRow(T &aRow, const OriginAttributes& aOriginAttributes); + void AsyncReadComplete(); + void CancelAsyncRead(bool aPurgeReadSet); + void EnsureReadDomain(const nsCookieKey &aKey); + void EnsureReadComplete(); + nsresult NormalizeHost(nsCString &aHost); + nsresult GetBaseDomain(nsIURI *aHostURI, nsCString &aBaseDomain, bool &aRequireHostMatch); + nsresult GetBaseDomainFromHost(const nsACString &aHost, nsCString &aBaseDomain); + nsresult GetCookieStringCommon(nsIURI *aHostURI, nsIChannel *aChannel, bool aHttpBound, char** aCookie); + void GetCookieStringInternal(nsIURI *aHostURI, bool aIsForeign, bool aHttpBound, const NeckoOriginAttributes aOriginAttrs, bool aIsPrivate, nsCString &aCookie); + nsresult SetCookieStringCommon(nsIURI *aHostURI, const char *aCookieHeader, const char *aServerTime, nsIChannel *aChannel, bool aFromHttp); + void SetCookieStringInternal(nsIURI *aHostURI, bool aIsForeign, nsDependentCString &aCookieHeader, const nsCString &aServerTime, bool aFromHttp, const NeckoOriginAttributes &aOriginAttrs, bool aIsPrivate, nsIChannel* aChannel); + bool SetCookieInternal(nsIURI *aHostURI, const nsCookieKey& aKey, bool aRequireHostMatch, CookieStatus aStatus, nsDependentCString &aCookieHeader, int64_t aServerTime, bool aFromHttp, nsIChannel* aChannel); + void AddInternal(const nsCookieKey& aKey, nsCookie *aCookie, int64_t aCurrentTimeInUsec, nsIURI *aHostURI, const char *aCookieHeader, bool aFromHttp); + void RemoveCookieFromList(const nsListIter &aIter, mozIStorageBindingParamsArray *aParamsArray = nullptr); + void AddCookieToList(const nsCookieKey& aKey, nsCookie *aCookie, DBState *aDBState, mozIStorageBindingParamsArray *aParamsArray, bool aWriteToDB = true); + void UpdateCookieInList(nsCookie *aCookie, int64_t aLastAccessed, mozIStorageBindingParamsArray *aParamsArray); + static bool GetTokenValue(nsASingleFragmentCString::const_char_iterator &aIter, nsASingleFragmentCString::const_char_iterator &aEndIter, nsDependentCSubstring &aTokenString, nsDependentCSubstring &aTokenValue, bool &aEqualsFound); + static bool ParseAttributes(nsDependentCString &aCookieHeader, nsCookieAttributes &aCookie); + bool RequireThirdPartyCheck(); + CookieStatus CheckPrefs(nsIURI *aHostURI, bool aIsForeign, const char *aCookieHeader); + bool CheckDomain(nsCookieAttributes &aCookie, nsIURI *aHostURI, const nsCString &aBaseDomain, bool aRequireHostMatch); + static bool CheckPath(nsCookieAttributes &aCookie, nsIURI *aHostURI); + static bool CheckPrefixes(nsCookieAttributes &aCookie, bool aSecureRequest); + static bool GetExpiry(nsCookieAttributes &aCookie, int64_t aServerTime, int64_t aCurrentTime); + void RemoveAllFromMemory(); + already_AddRefed<nsIArray> PurgeCookies(int64_t aCurrentTimeInUsec); + bool FindCookie(const nsCookieKey& aKey, const nsAFlatCString &aHost, const nsAFlatCString &aName, const nsAFlatCString &aPath, nsListIter &aIter); + bool FindSecureCookie(const nsCookieKey& aKey, nsCookie* aCookie); + int64_t FindStaleCookie(nsCookieEntry *aEntry, int64_t aCurrentTime, nsIURI* aSource, mozilla::Maybe<bool> aIsSecure, nsListIter &aIter); + void TelemetryForEvictingStaleCookie(nsCookie* aEvicted, int64_t oldestCookieTime); + void NotifyRejected(nsIURI *aHostURI); + void NotifyThirdParty(nsIURI *aHostURI, bool aAccepted, nsIChannel *aChannel); + void NotifyChanged(nsISupports *aSubject, const char16_t *aData); + void NotifyPurged(nsICookie2* aCookie); + already_AddRefed<nsIArray> CreatePurgeList(nsICookie2* aCookie); + void UpdateCookieOldestTime(DBState* aDBState, nsCookie* aCookie); + + nsresult GetCookiesWithOriginAttributes(const mozilla::OriginAttributesPattern& aPattern, const nsCString& aBaseDomain, nsISimpleEnumerator **aEnumerator); + nsresult RemoveCookiesWithOriginAttributes(const mozilla::OriginAttributesPattern& aPattern, const nsCString& aBaseDomain); + + /** + * This method is a helper that allows calling nsICookieManager::Remove() + * with NeckoOriginAttributes parameter. + * NOTE: this could be added to a public interface if we happen to need it. + */ + nsresult Remove(const nsACString& aHost, const NeckoOriginAttributes& aAttrs, + const nsACString& aName, const nsACString& aPath, + bool aBlocked); + + protected: + // cached members. + nsCOMPtr<nsICookiePermission> mPermissionService; + nsCOMPtr<mozIThirdPartyUtil> mThirdPartyUtil; + nsCOMPtr<nsIEffectiveTLDService> mTLDService; + nsCOMPtr<nsIIDNService> mIDNService; + nsCOMPtr<mozIStorageService> mStorageService; + + // we have two separate DB states: one for normal browsing and one for + // private browsing, switching between them on a per-cookie-request basis. + // this state encapsulates both the in-memory table and the on-disk DB. + // note that the private states' dbConn should always be null - we never + // want to be dealing with the on-disk DB when in private browsing. + DBState *mDBState; + RefPtr<DBState> mDefaultDBState; + RefPtr<DBState> mPrivateDBState; + + // cached prefs + uint8_t mCookieBehavior; // BEHAVIOR_{ACCEPT, REJECTFOREIGN, REJECT, LIMITFOREIGN} + bool mThirdPartySession; + bool mLeaveSecureAlone; + uint16_t mMaxNumberOfCookies; + uint16_t mMaxCookiesPerHost; + int64_t mCookiePurgeAge; + + // friends! + friend class DBListenerErrorHandler; + friend class ReadCookieDBListener; + friend class CloseCookieDBListener; + + static nsCookieService* GetSingleton(); + friend class mozilla::net::CookieServiceParent; +}; + +#endif // nsCookieService_h__ diff --git a/netwerk/cookie/nsICookie.idl b/netwerk/cookie/nsICookie.idl new file mode 100644 index 000000000..e8dadb949 --- /dev/null +++ b/netwerk/cookie/nsICookie.idl @@ -0,0 +1,85 @@ +/* -*- 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" + +/** + * An optional interface for accessing the HTTP or + * javascript cookie object + */ + +typedef long nsCookieStatus; +typedef long nsCookiePolicy; + +[scriptable, uuid(adf0db5e-211e-45a3-be14-4486ac430a58)] +interface nsICookie : nsISupports { + + /** + * 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 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 nsICookie2.expiry and nsICookie2.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; + + /** + * @DEPRECATED status implementation will return STATUS_UNKNOWN in all cases. + */ + const nsCookieStatus STATUS_UNKNOWN=0; + const nsCookieStatus STATUS_ACCEPTED=1; + const nsCookieStatus STATUS_DOWNGRADED=2; + const nsCookieStatus STATUS_FLAGGED=3; + const nsCookieStatus STATUS_REJECTED=4; + readonly attribute nsCookieStatus status; + + /** + * @DEPRECATED policy implementation will return POLICY_UNKNOWN in all cases. + */ + const nsCookiePolicy POLICY_UNKNOWN=0; + const nsCookiePolicy POLICY_NONE=1; + const nsCookiePolicy POLICY_NO_CONSENT=2; + const nsCookiePolicy POLICY_IMPLICIT_CONSENT=3; + const nsCookiePolicy POLICY_EXPLICIT_CONSENT=4; + const nsCookiePolicy POLICY_NO_II=5; + readonly attribute nsCookiePolicy policy; + + /** + * The origin attributes for this cookie + */ + [implicit_jscontext] + readonly attribute jsval originAttributes; +}; diff --git a/netwerk/cookie/nsICookie2.idl b/netwerk/cookie/nsICookie2.idl new file mode 100644 index 000000000..62a97f5bb --- /dev/null +++ b/netwerk/cookie/nsICookie2.idl @@ -0,0 +1,63 @@ +/* -*- 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 "nsICookie.idl" + +/** + * Main cookie object interface for use by consumers: + * extends nsICookie, a frozen interface for external + * access of cookie objects + */ + +[scriptable, uuid(05c420e5-03d0-4c7b-a605-df7ebe5ca326)] + +interface nsICookie2 : nsICookie +{ + + /** + * the host (possibly fully qualified) of the cookie, + * without a leading dot to represent if it is a + * domain cookie. + */ + readonly attribute AUTF8String rawHost; + + /** + * 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; + + /** + * 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; + + /** + * 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; + +}; diff --git a/netwerk/cookie/nsICookieManager.idl b/netwerk/cookie/nsICookieManager.idl new file mode 100644 index 000000000..daea98a4e --- /dev/null +++ b/netwerk/cookie/nsICookieManager.idl @@ -0,0 +1,88 @@ +/* -*- 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" + +%{ C++ +namespace mozilla { +class NeckoOriginAttributes; +} // mozilla namespace +%} + +[ptr] native NeckoOriginAttributesPtr(mozilla::NeckoOriginAttributes); + +interface nsISimpleEnumerator; + +[scriptable, function, uuid(20709db8-8dad-4e45-b33e-6e7c761dfc5d)] +interface nsIPrivateModeCallback : nsISupports +{ + void callback(); +}; + +/** + * An optional interface for accessing or removing the cookies + * that are in the cookie list + */ + +[scriptable, uuid(AAAB6710-0F2C-11d5-A53B-0010A401EB10)] +interface nsICookieManager : nsISupports +{ + + /** + * Called to remove all cookies from the cookie list + */ + void removeAll(); + + /** + * Called to enumerate through each cookie in the cookie list. + * The objects enumerated over are of type nsICookie + */ + readonly attribute nsISimpleEnumerator enumerator; + + /** + * 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 + * nsICookieManager2::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. This + * attribute is optional to avoid breaking add-ons. + * In 1 or 2 releases it will be mandatory: bug 1260399. + * @param aBlocked Indicates if cookies from this host should be permanently + * blocked. + * + */ + [implicit_jscontext, optional_argc] + void remove(in AUTF8String aHost, + in ACString aName, + in AUTF8String aPath, + in boolean aBlocked, + [optional] in jsval aOriginAttributes); + + [notxpcom] + nsresult removeNative(in AUTF8String aHost, + in ACString aName, + in AUTF8String aPath, + in boolean aBlocked, + in NeckoOriginAttributesPtr aOriginAttributes); + + /** + * Set the cookie manager to work on private or non-private cookies for the + * duration of the callback. + * + * @param aIsPrivate True to work on private cookies, false to work on + * non-private cookies. + * @param aCallback Methods on the cookie manager interface will work on + * private or non-private cookies for the duration of this + * callback. + */ + void usePrivateMode(in boolean aIsPrivate, in nsIPrivateModeCallback aCallback); +}; diff --git a/netwerk/cookie/nsICookieManager2.idl b/netwerk/cookie/nsICookieManager2.idl new file mode 100644 index 000000000..f20618bfa --- /dev/null +++ b/netwerk/cookie/nsICookieManager2.idl @@ -0,0 +1,164 @@ +/* -*- 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 "nsICookieManager.idl" + +interface nsICookie2; +interface nsIFile; + +/** + * Additions to the frozen nsICookieManager + */ + +[scriptable, uuid(daf0caa7-b431-4b4d-ba51-08c179bb9dfe)] +interface nsICookieManager2 : nsICookieManager +{ + /** + * 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. This + * attribute is optional to avoid breaking add-ons. + */ + [implicit_jscontext, optional_argc] + void add(in AUTF8String aHost, + in AUTF8String aPath, + in ACString aName, + in ACString aValue, + in boolean aIsSecure, + in boolean aIsHttpOnly, + in boolean aIsSession, + in int64_t aExpiry, + [optional] in jsval aOriginAttributes); + + [notxpcom] + nsresult addNative(in AUTF8String aHost, + in AUTF8String aPath, + in ACString aName, + in ACString aValue, + in boolean aIsSecure, + in boolean aIsHttpOnly, + in boolean aIsSession, + in int64_t aExpiry, + in NeckoOriginAttributesPtr aOriginAttributes); + + /** + * Find whether a given cookie already exists. + * + * @param aCookie + * the cookie to look for + * @param aOriginAttributes + * nsICookie2 contains an originAttributes but if nsICookie2 is + * implemented in JS, we can't retrieve its originAttributes because + * the getter is marked [implicit_jscontext]. This optional parameter + * is a workaround. + * + * @return true if a cookie was found which matches the host, path, and name + * fields of aCookie + */ + [implicit_jscontext, optional_argc] + boolean cookieExists(in nsICookie2 aCookie, + [optional] in jsval aOriginAttributes); + + [notxpcom] + nsresult cookieExistsNative(in nsICookie2 aCookie, + in NeckoOriginAttributesPtr aOriginAttributes, + out boolean aExists); + + /** + * Count how many cookies exist within the base domain of 'aHost'. + * Thus, for a host "weather.yahoo.com", the base domain would be "yahoo.com", + * and any host or domain cookies for "yahoo.com" and its subdomains would be + * counted. + * + * @param aHost + * the host string to search for, e.g. "google.com". this should consist + * of only the host portion of a URI. see @add for a description of + * acceptable host strings. + * + * @return the number of cookies found. + */ + unsigned long countCookiesFromHost(in AUTF8String aHost); + + /** + * Returns an enumerator 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. This attribute is optional to avoid + * breaking add-ons. + * + * @return an nsISimpleEnumerator of nsICookie2 objects. + * + * @see countCookiesFromHost + */ + [implicit_jscontext, optional_argc] + nsISimpleEnumerator getCookiesFromHost(in AUTF8String aHost, + [optional] in jsval aOriginAttributes); + + /** + * Import an old-style cookie file. Imported cookies will be added to the + * existing database. If the database contains any cookies the same as those + * being imported (i.e. domain, name, and path match), they will be replaced. + * + * @param aCookieFile the file to import, usually cookies.txt + */ + void importCookies(in nsIFile aCookieFile); + + /** + * Returns an enumerator 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. + */ + nsISimpleEnumerator getCookiesWithOriginAttributes(in DOMString 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 DOMString aPattern, + [optional] in AUTF8String aHost); +}; diff --git a/netwerk/cookie/nsICookiePermission.idl b/netwerk/cookie/nsICookiePermission.idl new file mode 100644 index 000000000..fd4a879f9 --- /dev/null +++ b/netwerk/cookie/nsICookiePermission.idl @@ -0,0 +1,109 @@ +/* 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 nsICookie2; +interface nsIURI; +interface nsIChannel; + +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; + const nsCookieAccess ACCESS_ALLOW_FIRST_PARTY_ONLY = 9; + const nsCookieAccess ACCESS_LIMIT_THIRD_PARTY = 10; + + /** + * setAccess + * + * this method is called to block cookie access for the given URI. this + * may result in other URIs being blocked as well (e.g., URIs which share + * the same host name). + * + * @param aURI + * the URI to block + * @param aAccess + * the new cookie access for the URI. + */ + void setAccess(in nsIURI aURI, + in nsCookieAccess aAccess); + + /** + * canAccess + * + * this method is called to test whether or not the given URI/channel may + * access the cookie database, either to set or get cookies. + * + * @param aURI + * the URI trying to access cookies + * @param aChannel + * the channel corresponding to aURI + * + * @return one of the following nsCookieAccess values: + * ACCESS_DEFAULT, ACCESS_ALLOW, ACCESS_DENY, or + * ACCESS_ALLOW_FIRST_PARTY_ONLY + */ + nsCookieAccess canAccess(in nsIURI aURI, + in nsIChannel aChannel); + + /** + * canSetCookie + * + * this method is called to test whether or not the given URI/channel may + * set a specific cookie. this method is always preceded by a call to + * canAccess. it may modify the isSession and expiry attributes of the + * cookie via the aIsSession and aExpiry parameters, in order to limit + * or extend the lifetime of the cookie. this is useful, for instance, to + * downgrade a cookie to session-only if it fails to meet certain criteria. + * + * @param aURI + * the URI trying to set the cookie + * @param aChannel + * the channel corresponding to aURI + * @param aCookie + * the cookie being added to the cookie database + * @param aIsSession + * when canSetCookie is invoked, this is the current isSession attribute + * of the cookie. canSetCookie may leave this value unchanged to + * preserve this attribute of the cookie. + * @param aExpiry + * when canSetCookie is invoked, this is the current expiry time of + * the cookie. canSetCookie may leave this value unchanged to + * preserve this attribute of the cookie. + * + * @return true if the cookie can be set. + */ + boolean canSetCookie(in nsIURI aURI, + in nsIChannel aChannel, + in nsICookie2 aCookie, + inout boolean aIsSession, + inout int64_t aExpiry); +}; + +%{ C++ +/** + * The nsICookiePermission implementation is an XPCOM service registered + * under the ContractID: + */ +#define NS_COOKIEPERMISSION_CONTRACTID "@mozilla.org/cookie/permission;1" +%} diff --git a/netwerk/cookie/nsICookieService.idl b/netwerk/cookie/nsICookieService.idl new file mode 100644 index 000000000..f876c61b4 --- /dev/null +++ b/netwerk/cookie/nsICookieService.idl @@ -0,0 +1,193 @@ +/* -*- 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 nsIPrompt; +interface nsIChannel; + +/** + * nsICookieService + * + * Provides methods for setting and getting cookies in the context of a + * page load. See nsICookieManager for methods to manipulate the cookie + * database directly. This separation of interface is mainly historical. + * + * This service broadcasts the notifications detailed below when the cookie + * list is changed, or a cookie is rejected. + * + * NOTE: observers of these notifications *must* not attempt to change profile + * or switch into or out of private browsing mode from within the + * observer. Doing so will cause undefined behavior. Mutating the cookie + * list (e.g. by calling methods on nsICookieService and friends) is + * allowed, but beware that there may be pending notifications you haven't + * seen yet -- for instance, a "batch-deleted" notification will likely be + * immediately followed by "added". You may check the state of the cookie + * list to determine if this is the case. + * + * topic : "cookie-changed" + * broadcast whenever the cookie list changes in some way. see + * explanation of data strings below. + * subject: see below. + * data : "deleted" + * a cookie was deleted. the subject is an nsICookie2 representing + * the deleted cookie. + * "added" + * a cookie was added. the subject is an nsICookie2 representing + * the added cookie. + * "changed" + * a cookie was changed. the subject is an nsICookie2 representing + * the new cookie. (note that host, path, and name are invariant + * for a given cookie; other parameters may change.) + * "batch-deleted" + * a set of cookies was purged (typically, because they have either + * expired or because the cookie list has grown too large). The subject + * is an nsIArray of nsICookie2's representing the deleted cookies. + * Note that the array could contain a single cookie. + * "cleared" + * the entire cookie list was cleared. the subject is null. + * + * topic : "cookie-rejected" + * broadcast whenever a cookie was rejected from being set as a + * result of user prefs. + * subject: an nsIURI interface pointer representing the URI that attempted + * to set the cookie. + * data : none. + * + * topic : "third-party-cookie-accepted" + * broadcast whenever a third party cookie was accepted + * subject: an nsIURI interface pointer representing the URI that attempted + * to set the cookie. + * data : the referrer, or "?" if unknown + * + * topic : "third-party-cookie-rejected" + * broadcast whenever a third party cookie was rejected + * subject: an nsIURI interface pointer representing the URI that attempted + * to set the cookie. + * data : the referrer, or "?" if unknown + */ +[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 + + /* + * Possible values for the "network.cookie.lifetimePolicy" preference. + */ + const uint32_t ACCEPT_NORMALLY = 0; // accept normally + // Value = 1 is considered the same as 0 (See Bug 606655). + const uint32_t ACCEPT_SESSION = 2; // downgrade to session + const uint32_t ACCEPT_FOR_N_DAYS = 3; // limit lifetime to N days + + /* + * Get the complete cookie string associated with the URI. + * + * @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. this parameter should not + * be null, otherwise the cookies will not be returned if third-party + * cookies have been disabled by the user. (the channel is used + * to determine the originating URI of the document; if it is not + * provided, the cookies will be assumed third-party.) + * + * @return the resulting cookie string + */ + string getCookieString(in nsIURI aURI, in nsIChannel aChannel); + + /* + * 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 aFirstURI + * the URI that the user originally typed in or clicked on to initiate + * the load of the document referenced by aURI. + * @param aChannel + * the channel used to load the document. this parameter should not + * be null, otherwise the cookies will not be returned if third-party + * cookies have been disabled by the user. (the channel is used + * to determine the originating URI of the document; if it is not + * provided, the cookies will be assumed third-party.) + * + * @return the resulting cookie string + */ + string getCookieStringFromHttp(in nsIURI aURI, in nsIURI aFirstURI, in nsIChannel aChannel); + + /* + * Set the cookie string associated with the URI. + * + * @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 aPrompt + * the prompt to use for all user-level cookie notifications. This is + * presently ignored and can be null. (Prompt information is determined + * from the channel if necessary.) + * @param aCookie + * the cookie string to set. + * @param aChannel + * the channel used to load the document. this parameter should not + * be null, otherwise the cookies will not be set if third-party + * cookies have been disabled by the user. (the channel is used + * to determine the originating URI of the document; if it is not + * provided, the cookies will be assumed third-party.) + */ + void setCookieString(in nsIURI aURI, in nsIPrompt aPrompt, in string aCookie, in nsIChannel aChannel); + + /* + * 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 aFirstURI + * the URI that the user originally typed in or clicked on to initiate + * the load of the document referenced by aURI. + * @param aPrompt + * the prompt to use for all user-level cookie notifications. This is + * presently ignored and can be null. (Prompt information is determined + * from the channel if necessary.) + * @param aCookie + * the cookie string to set. + * @param aServerTime + * the current time reported by the server, if available. This should + * be the string from the Date header in an HTTP response. If the + * string is empty or null, server time is assumed to be the current + * local time. If provided, it will be used to calculate the expiry + * time of the cookie relative to the server's local time. + * @param aChannel + * the channel used to load the document. this parameter should not + * be null, otherwise the cookies will not be set if third-party + * cookies have been disabled by the user. (the channel is used + * to determine the originating URI of the document; if it is not + * provided, the cookies will be assumed third-party.) + */ + void setCookieStringFromHttp(in nsIURI aURI, in nsIURI aFirstURI, in nsIPrompt aPrompt, in string aCookie, in string aServerTime, in nsIChannel aChannel); +}; diff --git a/netwerk/cookie/test/browser/browser.ini b/netwerk/cookie/test/browser/browser.ini new file mode 100644 index 000000000..342e14578 --- /dev/null +++ b/netwerk/cookie/test/browser/browser.ini @@ -0,0 +1,5 @@ +[DEFAULT] +support-files = + file_empty.html + +[browser_originattributes.js] diff --git a/netwerk/cookie/test/browser/browser_originattributes.js b/netwerk/cookie/test/browser/browser_originattributes.js new file mode 100644 index 000000000..617d52e35 --- /dev/null +++ b/netwerk/cookie/test/browser/browser_originattributes.js @@ -0,0 +1,113 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +let { classes: Cc, interfaces: Ci } = Components; + +const USER_CONTEXTS = ["default", "personal", "work"]; + +const COOKIE_NAMES = ["cookie0", "cookie1", "cookie2"]; + +const TEST_URL = + "http://example.com/browser/netwerk/cookie/test/browser/file_empty.html"; + +let cm = Cc["@mozilla.org/cookiemanager;1"].getService(Ci.nsICookieManager2); + +// opens `uri' in a new tab with the provided userContextId and focuses it. +// returns the newly opened tab +function* openTabInUserContext(uri, userContextId) { + // open the tab in the correct userContextId + let tab = gBrowser.addTab(uri, {userContextId}); + + // select tab and make sure its browser is focused + gBrowser.selectedTab = tab; + tab.ownerDocument.defaultView.focus(); + + let browser = gBrowser.getBrowserForTab(tab); + // wait for tab load + yield BrowserTestUtils.browserLoaded(browser); + + return {tab, browser}; +} + +add_task(function* setup() { + // make sure userContext is enabled. + yield new Promise(resolve => { + SpecialPowers.pushPrefEnv({"set": [ + ["privacy.userContext.enabled", true] + ]}, resolve); + }); +}); + +add_task(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} = yield* openTabInUserContext(TEST_URL, userContextId); + + yield ContentTask.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); + yield checkCookies(expectedValues, "before removal"); + + // remove cookies that belongs to user context id #1 + cm.removeCookiesWithOriginAttributes(JSON.stringify({userContextId: 1})); + + expectedValues[1] = undefined; + yield checkCookies(expectedValues, "after removal"); +}); + +function *checkCookies(expectedValues, time) { + for (let userContextId of Object.keys(expectedValues)) { + let cookiesFromTitle = yield* 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 enumerator = cm.getCookiesWithOriginAttributes(JSON.stringify({userContextId})); + while (enumerator.hasMoreElements()) { + let cookie = enumerator.getNext().QueryInterface(Ci.nsICookie); + cookies[cookie.name] = cookie.value; + } + return cookies; +} + +function* getCookiesFromJS(userContextId) { + let {tab, browser} = yield* openTabInUserContext(TEST_URL, userContextId); + + // get the cookies + let cookieString = yield ContentTask.spawn(browser, null, 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/file_empty.html b/netwerk/cookie/test/browser/file_empty.html new file mode 100644 index 000000000..5a08c4205 --- /dev/null +++ b/netwerk/cookie/test/browser/file_empty.html @@ -0,0 +1,3 @@ +<html><body> +</body></html> + diff --git a/netwerk/cookie/test/unit/test_bug1155169.js b/netwerk/cookie/test/unit/test_bug1155169.js new file mode 100644 index 000000000..6806ffe6d --- /dev/null +++ b/netwerk/cookie/test/unit/test_bug1155169.js @@ -0,0 +1,73 @@ +var {utils: Cu, interfaces: Ci, classes: Cc} = Components; + +Cu.import("resource://gre/modules/Services.jsm"); + +const URI = Services.io.newURI("http://example.org/", null, null); + +const cs = Cc["@mozilla.org/cookieService;1"] + .getService(Ci.nsICookieService); + +function run_test() { + // Allow all cookies. + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + + // Clear cookies. + Services.cookies.removeAll(); + + // Add a new cookie. + setCookie("foo=bar", { + type: "added", isSession: true, isSecure: false, isHttpOnly: false + }); + + // Update cookie with isHttpOnly=true. + setCookie("foo=bar; HttpOnly", { + type: "changed", isSession: true, isSecure: false, isHttpOnly: true + }); + + // Update cookie with isSecure=true. + setCookie("foo=bar; Secure", { + type: "changed", isSession: true, isSecure: true, isHttpOnly: false + }); + + // Update cookie with isSession=false. + let expiry = new Date(); + expiry.setUTCFullYear(expiry.getUTCFullYear() + 2); + setCookie(`foo=bar; Expires=${expiry.toGMTString()}`, { + type: "changed", isSession: false, isSecure: false, isHttpOnly: false + }); + + // Reset cookie. + setCookie("foo=bar", { + type: "changed", isSession: true, isSecure: false, isHttpOnly: false + }); +} + +function setCookie(value, expected) { + function setCookieInternal(value, expected = null) { + function observer(subject, topic, data) { + if (!expected) { + do_throw("no notification expected"); + return; + } + + // Check we saw the right notification. + do_check_eq(data, expected.type); + + // Check cookie details. + let cookie = subject.QueryInterface(Ci.nsICookie2); + do_check_eq(cookie.isSession, expected.isSession); + do_check_eq(cookie.isSecure, expected.isSecure); + do_check_eq(cookie.isHttpOnly, expected.isHttpOnly); + } + + Services.obs.addObserver(observer, "cookie-changed", false); + cs.setCookieStringFromHttp(URI, null, null, value, null, null); + 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_bug1267910.js b/netwerk/cookie/test/unit/test_bug1267910.js new file mode 100644 index 000000000..93ea5e132 --- /dev/null +++ b/netwerk/cookie/test/unit/test_bug1267910.js @@ -0,0 +1,196 @@ +/* + * Bug 1267910 - Add test cases for the backward compatiability and originAttributes + * of nsICookieManager2. + */ + +var {utils: Cu, interfaces: Ci, classes: Cc} = Components; + +Cu.import("resource://gre/modules/Services.jsm"); + +const BASE_URL = "http://example.org/"; + +const COOKIE = { + host: BASE_URL, + path: "/", + name: "test1", + value: "yes", + isSecure: false, + isHttpOnly: false, + isSession: true, + expiry: 2145934800, +}; + +const COOKIE_OA_DEFAULT = { + host: BASE_URL, + path: "/", + name: "test0", + value: "yes0", + isSecure: false, + isHttpOnly: false, + isSession: true, + expiry: 2145934800, + originAttributes: {}, +}; + +const COOKIE_OA_1 = { + host: BASE_URL, + path: "/", + name: "test1", + value: "yes1", + isSecure: false, + isHttpOnly: false, + isSession: true, + expiry: 2145934800, + originAttributes: {userContextId: 1}, +}; + +function checkCookie(cookie, cookieObj) { + for (let prop of Object.keys(cookieObj)) { + if (prop === "originAttributes") { + ok(ChromeUtils.isOriginAttributesEqual(cookie[prop], cookieObj[prop]), + "Check cookie: " + prop); + } else { + equal(cookie[prop], cookieObj[prop], "Check cookie: " + prop); + } + } +} + +function countCookies(enumerator) { + let cnt = 0; + + while (enumerator.hasMoreElements()) { + cnt++; + enumerator.getNext(); + } + + return cnt; +} + +function run_test() { + // Allow all cookies. + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + + // Enable user context id + Services.prefs.setBoolPref("privacy.userContext.enabled", true); + + add_test(test_backward_compatiability); + add_test(test_originAttributes); + + + run_next_test(); +} + +/* + * Test for backward compatiablility that APIs works correctly without + * originAttributes. + */ +function test_backward_compatiability() { + // Clear cookies. + Services.cookies.removeAll(); + + // Call Add() to add a cookie without originAttributes + Services.cookies.add(COOKIE.host, + COOKIE.path, + COOKIE.name, + COOKIE.value, + COOKIE.isSecure, + COOKIE.isHttpOnly, + COOKIE.isSession, + COOKIE.expiry); + + // Call getCookiesFromHost() to get cookies without originAttributes + let enumerator = Services.cookies.getCookiesFromHost(BASE_URL); + + ok(enumerator.hasMoreElements(), "Cookies available"); + let foundCookie = enumerator.getNext().QueryInterface(Ci.nsICookie2); + + checkCookie(foundCookie, COOKIE); + + ok(!enumerator.hasMoreElements(), "We should get only one cookie"); + + run_next_test(); +} + +/* + * Test for originAttributes. + */ +function test_originAttributes() { + // Clear cookies. + Services.cookies.removeAll(); + + // Add a cookie for default originAttributes. + Services.cookies.add(COOKIE_OA_DEFAULT.host, + COOKIE_OA_DEFAULT.path, + COOKIE_OA_DEFAULT.name, + COOKIE_OA_DEFAULT.value, + COOKIE_OA_DEFAULT.isSecure, + COOKIE_OA_DEFAULT.isHttpOnly, + COOKIE_OA_DEFAULT.isSession, + COOKIE_OA_DEFAULT.expiry, + COOKIE_OA_DEFAULT.originAttributes); + + // Get cookies for default originAttributes. + let enumerator = Services.cookies.getCookiesFromHost(BASE_URL, COOKIE_OA_DEFAULT.originAttributes); + + // Check that do we get cookie correctly. + ok(enumerator.hasMoreElements(), "Cookies available"); + let foundCookie = enumerator.getNext().QueryInterface(Ci.nsICookie2); + checkCookie(foundCookie, COOKIE_OA_DEFAULT); + + // We should only get one cookie. + ok(!enumerator.hasMoreElements(), "We should get only one cookie"); + + // Get cookies for originAttributes with user context id 1. + enumerator = Services.cookies.getCookiesFromHost(BASE_URL, COOKIE_OA_1.originAttributes); + + // Check that we will not get cookies if the originAttributes is different. + ok(!enumerator.hasMoreElements(), "No cookie should be here"); + + // Add a cookie for originAttributes with user context id 1. + Services.cookies.add(COOKIE_OA_1.host, + COOKIE_OA_1.path, + COOKIE_OA_1.name, + COOKIE_OA_1.value, + COOKIE_OA_1.isSecure, + COOKIE_OA_1.isHttpOnly, + COOKIE_OA_1.isSession, + COOKIE_OA_1.expiry, + COOKIE_OA_1.originAttributes); + + // Get cookies for originAttributes with user context id 1. + enumerator = Services.cookies.getCookiesFromHost(BASE_URL, COOKIE_OA_1.originAttributes); + + // Check that do we get cookie correctly. + ok(enumerator.hasMoreElements(), "Cookies available"); + foundCookie = enumerator.getNext().QueryInterface(Ci.nsICookie2); + checkCookie(foundCookie, COOKIE_OA_1); + + // We should only get one cookie. + ok(!enumerator.hasMoreElements(), "We should get only one cookie"); + + // Check that add a cookie will not affect cookies in different originAttributes. + enumerator = Services.cookies.getCookiesFromHost(BASE_URL, COOKIE_OA_DEFAULT.originAttributes); + equal(countCookies(enumerator), 1, "We should get only one cookie for default originAttributes"); + + // Remove a cookie for originAttributes with user context id 1. + Services.cookies.remove(COOKIE_OA_1.host, COOKIE_OA_1.name, COOKIE_OA_1.path, + false, COOKIE_OA_1.originAttributes); + + // Check that remove will not affect cookies in default originAttributes. + enumerator = Services.cookies.getCookiesFromHost(BASE_URL, COOKIE_OA_DEFAULT.originAttributes); + equal(countCookies(enumerator), 1, "Get one cookie for default originAttributes."); + + // Check that should be no cookie for originAttributes with user context id 1. + enumerator = Services.cookies.getCookiesFromHost(BASE_URL, COOKIE_OA_1.originAttributes); + equal(countCookies(enumerator), 0, "No cookie shold be here"); + + // Remove a cookie for default originAttributes. + Services.cookies.remove(COOKIE_OA_DEFAULT.host, COOKIE_OA_DEFAULT.name, COOKIE_OA_DEFAULT.path, + false, COOKIE_OA_DEFAULT.originAttributes); + + // Check remove() works correctly for default originAttributes. + enumerator = Services.cookies.getCookiesFromHost(BASE_URL, COOKIE_OA_DEFAULT.originAttributes); + equal(countCookies(enumerator), 0, "No cookie shold be here"); + + run_next_test(); +} diff --git a/netwerk/cookie/test/unit/test_bug643051.js b/netwerk/cookie/test/unit/test_bug643051.js new file mode 100644 index 000000000..d6695054e --- /dev/null +++ b/netwerk/cookie/test/unit/test_bug643051.js @@ -0,0 +1,29 @@ +var Cc = Components.classes; +var Ci = Components.interfaces; + +Components.utils.import("resource://gre/modules/NetUtil.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); + +function run_test() { + // Allow all cookies. + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + + let cs = Cc["@mozilla.org/cookieService;1"].getService(Ci.nsICookieService); + + let uri = NetUtil.newURI("http://example.org/"); + + let set = "foo=bar\nbaz=foo"; + let expected = "foo=bar; baz=foo"; + cs.setCookieStringFromHttp(uri, null, null, set, null, null); + + let actual = cs.getCookieStringFromHttp(uri, null, null); + do_check_eq(actual, expected); + + uri = NetUtil.newURI("http://example.com/"); + cs.setCookieString(uri, null, set, null); + + expected = "foo=bar"; + actual = cs.getCookieString(uri, null, null); + do_check_eq(actual, expected); +} + diff --git a/netwerk/cookie/test/unit/test_eviction.js b/netwerk/cookie/test/unit/test_eviction.js new file mode 100644 index 000000000..7f693ee94 --- /dev/null +++ b/netwerk/cookie/test/unit/test_eviction.js @@ -0,0 +1,296 @@ +var {utils: Cu, interfaces: Ci, classes: Cc} = Components; + +Cu.import("resource://gre/modules/Services.jsm"); + +const BASE_HOSTNAMES = ["example.org", "example.co.uk"]; +const SUBDOMAINS = ["", "pub.", "www.", "other."]; + +const cs = Cc["@mozilla.org/cookieService;1"].getService(Ci.nsICookieService); +const cm = cs.QueryInterface(Ci.nsICookieManager2); + +function run_test() { + var tests = []; + Services.prefs.setIntPref("network.cookie.staleThreshold", 0); + for (var host of BASE_HOSTNAMES) { + var base = SUBDOMAINS[0] + host; + var sub = SUBDOMAINS[1] + host; + var other = SUBDOMAINS[2] + host; + var another = SUBDOMAINS[3] + host; + tests.push([host, test_basic_eviction.bind(this, base, sub, other, another)]); + add_task(function* a() { + var t = tests.splice(0, 1)[0]; + do_print('testing with host ' + t[0]); + yield t[1](); + cm.removeAll(); + }); + tests.push([host, test_domain_or_path_matches_not_both.bind(this, base, sub, other, another)]); + add_task(function*() { + var t = tests.splice(0, 1)[0]; + do_print('testing with host ' + t[0]); + yield t[1](); + cm.removeAll(); + }); + } + add_task(function*() { + yield test_localdomain(); + cm.removeAll(); + }); + + add_task(function*() { + yield test_path_prefix(); + }); + + run_next_test(); +} + +// Verify that cookies that share a path prefix with the URI path are still considered +// candidates for eviction, since the paths do not actually match. +function* test_path_prefix() { + Services.prefs.setIntPref("network.cookie.maxPerHost", 2); + + const BASE_URI = Services.io.newURI("http://example.org/", null, null); + const BASE_BAR = Services.io.newURI("http://example.org/bar/", null, null); + const BASE_BARBAR = Services.io.newURI("http://example.org/barbar/", null, null); + + yield setCookie("session_first", null, null, null, BASE_URI); + yield setCookie("session_second", null, "/bar", null, BASE_BAR); + verifyCookies(['session_first', 'session_second'], BASE_URI); + + yield setCookie("session_third", null, "/barbar", null, BASE_BARBAR); + verifyCookies(['session_first', 'session_third'], BASE_URI); +} + +// Verify that subdomains of localhost are treated as separate hosts and aren't considered +// candidates for eviction. +function* test_localdomain() { + Services.prefs.setIntPref("network.cookie.maxPerHost", 2); + + const BASE_URI = Services.io.newURI("http://localhost", null, null); + const BASE_BAR = Services.io.newURI("http://localhost/bar", null, null); + const OTHER_URI = Services.io.newURI("http://other.localhost", null, null); + const OTHER_BAR = Services.io.newURI("http://other.localhost/bar", null, null); + + yield setCookie("session_no_path", null, null, null, BASE_URI); + yield setCookie("session_bar_path", null, "/bar", null, BASE_BAR); + + yield setCookie("session_no_path", null, null, null, OTHER_URI); + yield setCookie("session_bar_path", null, "/bar", null, OTHER_BAR); + + verifyCookies(['session_no_path', + 'session_bar_path'], BASE_URI); + verifyCookies(['session_no_path', + 'session_bar_path'], OTHER_URI); + + yield setCookie("session_another_no_path", null, null, null, BASE_URI); + verifyCookies(['session_no_path', + 'session_another_no_path'], BASE_URI); + + yield setCookie("session_another_no_path", null, null, null, OTHER_URI); + verifyCookies(['session_no_path', + 'session_another_no_path'], OTHER_URI); +} + +// Ensure that cookies are still considered candidates for eviction if either the domain +// or path matches, but not both. +function* test_domain_or_path_matches_not_both(base_host, + subdomain_host, + other_subdomain_host, + another_subdomain_host) { + Services.prefs.setIntPref("network.cookie.maxPerHost", 2); + + const BASE_URI = Services.io.newURI("http://" + base_host, null, null); + const PUB_FOO_PATH = Services.io.newURI("http://" + subdomain_host + "/foo/", null, null); + const WWW_BAR_PATH = Services.io.newURI("http://" + other_subdomain_host + "/bar/", null, null); + const OTHER_BAR_PATH = Services.io.newURI("http://" + another_subdomain_host + "/bar/", null, null); + const PUB_BAR_PATH = Services.io.newURI("http://" + subdomain_host + "/bar/", null, null); + const WWW_FOO_PATH = Services.io.newURI("http://" + other_subdomain_host + "/foo/", null, null); + + yield setCookie("session_pub_with_foo_path", subdomain_host, "/foo", null, PUB_FOO_PATH); + yield setCookie("session_www_with_bar_path", other_subdomain_host, "/bar", null, WWW_BAR_PATH); + verifyCookies(['session_pub_with_foo_path', + 'session_www_with_bar_path'], BASE_URI); + + yield setCookie("session_pub_with_bar_path", subdomain_host, "/bar", null, PUB_BAR_PATH); + verifyCookies(['session_www_with_bar_path', + 'session_pub_with_bar_path'], BASE_URI); + + yield setCookie("session_other_with_bar_path", another_subdomain_host, "/bar", null, OTHER_BAR_PATH); + verifyCookies(['session_pub_with_bar_path', + 'session_other_with_bar_path'], BASE_URI); +} + +function* test_basic_eviction(base_host, subdomain_host, other_subdomain_host) { + Services.prefs.setIntPref("network.cookie.maxPerHost", 5); + + const BASE_URI = Services.io.newURI("http://" + base_host, null, null); + const SUBDOMAIN_URI = Services.io.newURI("http://" + subdomain_host, null, null); + const OTHER_SUBDOMAIN_URI = Services.io.newURI("http://" + other_subdomain_host, null, null); + const FOO_PATH = Services.io.newURI("http://" + base_host + "/foo/", null, null); + const BAR_PATH = Services.io.newURI("http://" + base_host + "/bar/", null, null); + const ALL_SUBDOMAINS = '.' + base_host; + const OTHER_SUBDOMAIN = other_subdomain_host; + + // Initialize the set of cookies with a mix of non-session cookies with no path, + // and session cookies with explicit paths. Any subsequent cookies added will cause + // existing cookies to be evicted. + yield setCookie("non_session_non_path_non_domain", null, null, 100000, BASE_URI); + yield setCookie("non_session_non_path_subdomain", ALL_SUBDOMAINS, null, 100000, SUBDOMAIN_URI); + yield setCookie("session_non_path_pub_domain", OTHER_SUBDOMAIN, null, null, OTHER_SUBDOMAIN_URI); + yield setCookie("session_foo_path", null, "/foo", null, FOO_PATH); + yield setCookie("session_bar_path", null, "/bar", null, BAR_PATH); + verifyCookies(['non_session_non_path_non_domain', + 'non_session_non_path_subdomain', + 'session_non_path_pub_domain', + 'session_foo_path', + 'session_bar_path'], BASE_URI); + + // Ensure that cookies set for the / path appear more recent. + cs.getCookieString(OTHER_SUBDOMAIN_URI, null) + verifyCookies(['non_session_non_path_non_domain', + 'session_foo_path', + 'session_bar_path', + 'non_session_non_path_subdomain', + 'session_non_path_pub_domain'], BASE_URI); + + // Evict oldest session cookie that does not match example.org/foo (session_bar_path) + yield setCookie("session_foo_path_2", null, "/foo", null, FOO_PATH); + verifyCookies(['non_session_non_path_non_domain', + 'session_foo_path', + 'non_session_non_path_subdomain', + 'session_non_path_pub_domain', + 'session_foo_path_2'], BASE_URI); + + // Evict oldest session cookie that does not match example.org/bar (session_foo_path) + yield setCookie("session_bar_path_2", null, "/bar", null, BAR_PATH); + verifyCookies(['non_session_non_path_non_domain', + 'non_session_non_path_subdomain', + 'session_non_path_pub_domain', + 'session_foo_path_2', + 'session_bar_path_2'], BASE_URI); + + // Evict oldest session cookie that does not match example.org/ (session_non_path_pub_domain) + yield setCookie("non_session_non_path_non_domain_2", null, null, 100000, BASE_URI); + verifyCookies(['non_session_non_path_non_domain', + 'non_session_non_path_subdomain', + 'session_foo_path_2', + 'session_bar_path_2', + 'non_session_non_path_non_domain_2'], BASE_URI); + + // Evict oldest session cookie that does not match example.org/ (session_foo_path_2) + yield setCookie("session_non_path_non_domain_3", null, null, null, BASE_URI); + verifyCookies(['non_session_non_path_non_domain', + 'non_session_non_path_subdomain', + 'session_bar_path_2', + 'non_session_non_path_non_domain_2', + 'session_non_path_non_domain_3'], BASE_URI); + + // Evict oldest session cookie; all such cookies match example.org/bar (session_bar_path_2) + // note: this new cookie doesn't have an explicit path, but empty paths inherit the + // request's path + yield setCookie("non_session_bar_path_non_domain", null, null, 100000, BAR_PATH); + verifyCookies(['non_session_non_path_non_domain', + 'non_session_non_path_subdomain', + 'non_session_non_path_non_domain_2', + 'session_non_path_non_domain_3', + 'non_session_bar_path_non_domain'], BASE_URI); + + // Evict oldest session cookie, even though it matches pub.example.org (session_non_path_non_domain_3) + yield setCookie("non_session_non_path_pub_domain", null, null, 100000, OTHER_SUBDOMAIN_URI); + verifyCookies(['non_session_non_path_non_domain', + 'non_session_non_path_subdomain', + 'non_session_non_path_non_domain_2', + 'non_session_bar_path_non_domain', + 'non_session_non_path_pub_domain'], BASE_URI); + + // All session cookies have been evicted. + // Evict oldest non-session non-domain-matching cookie (non_session_non_path_pub_domain) + yield setCookie("non_session_bar_path_non_domain_2", null, '/bar', 100000, BAR_PATH); + verifyCookies(['non_session_non_path_non_domain', + 'non_session_non_path_subdomain', + 'non_session_non_path_non_domain_2', + 'non_session_bar_path_non_domain', + 'non_session_bar_path_non_domain_2'], BASE_URI); + + // Evict oldest non-session non-path-matching cookie (non_session_bar_path_non_domain) + yield setCookie("non_session_non_path_non_domain_4", null, null, 100000, BASE_URI); + verifyCookies(['non_session_non_path_non_domain', + 'non_session_non_path_subdomain', + 'non_session_non_path_non_domain_2', + 'non_session_bar_path_non_domain_2', + 'non_session_non_path_non_domain_4'], BASE_URI); + + // At this point all remaining cookies are non-session cookies, have a path of /, + // and either don't have a domain or have one that matches subdomains. + // They will therefore be evicted from oldest to newest if all new cookies added share + // similar characteristics. +} + +// Verify that the given cookie names exist, and are ordered from least to most recently accessed +function verifyCookies(names, uri) { + do_check_eq(cm.countCookiesFromHost(uri.host), names.length); + let cookies = cm.getCookiesFromHost(uri.host, {}); + let actual_cookies = []; + while (cookies.hasMoreElements()) { + let cookie = cookies.getNext().QueryInterface(Ci.nsICookie2); + 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) { + do_print("unexpected cookies: " + left); + } + if (right.length) { + do_print("expected cookies: " + right); + } + } + do_check_eq(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++) { + do_check_eq(names[i], actual_cookies[i].name); + do_check_eq(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; + do_print(s); + cs.setCookieStringFromHttp(url, null, null, value, null, null); + 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_parser_0001.js b/netwerk/cookie/test/unit/test_parser_0001.js new file mode 100644 index 000000000..f885972a0 --- /dev/null +++ b/netwerk/cookie/test/unit/test_parser_0001.js @@ -0,0 +1,29 @@ +var Cc = Components.classes; +var Ci = Components.interfaces; + +Components.utils.import("resource://gre/modules/NetUtil.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); + +function inChildProcess() { + return Cc["@mozilla.org/xre/app-info;1"] + .getService(Ci.nsIXULRuntime) + .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); + + let cs = Cc["@mozilla.org/cookieService;1"].getService(Ci.nsICookieService); + + let uri = NetUtil.newURI("http://example.org/"); + + let set = "foo=bar"; + cs.setCookieStringFromHttp(uri, null, null, set, null, null); + + let expected = "foo=bar"; + let actual = cs.getCookieStringFromHttp(uri, null, null); + do_check_eq(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 000000000..7811b01fe --- /dev/null +++ b/netwerk/cookie/test/unit/test_parser_0019.js @@ -0,0 +1,29 @@ +var Cc = Components.classes; +var Ci = Components.interfaces; + +Components.utils.import("resource://gre/modules/NetUtil.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); + +function inChildProcess() { + return Cc["@mozilla.org/xre/app-info;1"] + .getService(Ci.nsIXULRuntime) + .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); + + let cs = Cc["@mozilla.org/cookieService;1"].getService(Ci.nsICookieService); + + let uri = NetUtil.newURI("http://example.org/"); + + let set = "foo=b;max-age=3600, c=d;path=/"; + cs.setCookieStringFromHttp(uri, null, null, set, null, null); + + let expected = "foo=b"; + let actual = cs.getCookieStringFromHttp(uri, null, null); + do_check_eq(actual, expected); +} + diff --git a/netwerk/cookie/test/unit/xpcshell.ini b/netwerk/cookie/test/unit/xpcshell.ini new file mode 100644 index 000000000..f9c4093cf --- /dev/null +++ b/netwerk/cookie/test/unit/xpcshell.ini @@ -0,0 +1,10 @@ +[DEFAULT] +head = +tail = + +[test_bug643051.js] +[test_bug1155169.js] +[test_bug1267910.js] +[test_parser_0001.js] +[test_parser_0019.js] +[test_eviction.js]
\ No newline at end of file diff --git a/netwerk/cookie/test/unit_ipc/test_ipc_parser_0001.js b/netwerk/cookie/test/unit_ipc/test_ipc_parser_0001.js new file mode 100644 index 000000000..988c8d196 --- /dev/null +++ b/netwerk/cookie/test/unit_ipc/test_ipc_parser_0001.js @@ -0,0 +1,9 @@ +var Cu = Components.utils; + +Cu.import("resource://gre/modules/Services.jsm"); + +function run_test() { + // Allow all cookies. + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + run_test_in_child("../unit/test_parser_0001.js"); +} diff --git a/netwerk/cookie/test/unit_ipc/test_ipc_parser_0019.js b/netwerk/cookie/test/unit_ipc/test_ipc_parser_0019.js new file mode 100644 index 000000000..535ac6e34 --- /dev/null +++ b/netwerk/cookie/test/unit_ipc/test_ipc_parser_0019.js @@ -0,0 +1,9 @@ +var Cu = Components.utils; + +Cu.import("resource://gre/modules/Services.jsm"); + +function run_test() { + // Allow all cookies. + Services.prefs.setIntPref("network.cookie.cookieBehavior", 0); + run_test_in_child("../unit/test_parser_0019.js"); +} diff --git a/netwerk/cookie/test/unit_ipc/xpcshell.ini b/netwerk/cookie/test/unit_ipc/xpcshell.ini new file mode 100644 index 000000000..922443490 --- /dev/null +++ b/netwerk/cookie/test/unit_ipc/xpcshell.ini @@ -0,0 +1,10 @@ +[DEFAULT] +head = +tail = +skip-if = toolkit == 'android' +support-files = + !/netwerk/cookie/test/unit/test_parser_0001.js + !/netwerk/cookie/test/unit/test_parser_0019.js + +[test_ipc_parser_0001.js] +[test_ipc_parser_0019.js] |