/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "Cookie.h" #include "CookieCommons.h" #include "CookieLogging.h" #include "CookiePersistentStorage.h" #include "mozilla/FileUtils.h" #include "mozilla/glean/GleanMetrics.h" #include "mozilla/ScopeExit.h" #include "mozilla/Telemetry.h" #include "mozIStorageAsyncStatement.h" #include "mozIStorageError.h" #include "mozIStorageFunction.h" #include "mozIStorageService.h" #include "mozStorageHelper.h" #include "nsAppDirectoryServiceDefs.h" #include "nsICookieNotification.h" #include "nsICookieService.h" #include "nsIEffectiveTLDService.h" #include "nsILineInputStream.h" #include "nsIURIMutator.h" #include "nsNetUtil.h" #include "nsVariant.h" #include "prprf.h" // XXX_hack. See bug 178993. // This is a hack to hide HttpOnly cookies from older browsers #define HTTP_ONLY_PREFIX "#HttpOnly_" constexpr auto COOKIES_SCHEMA_VERSION = 13; // parameter indexes; see |Read| constexpr auto IDX_NAME = 0; constexpr auto IDX_VALUE = 1; constexpr auto IDX_HOST = 2; constexpr auto IDX_PATH = 3; constexpr auto IDX_EXPIRY = 4; constexpr auto IDX_LAST_ACCESSED = 5; constexpr auto IDX_CREATION_TIME = 6; constexpr auto IDX_SECURE = 7; constexpr auto IDX_HTTPONLY = 8; constexpr auto IDX_ORIGIN_ATTRIBUTES = 9; constexpr auto IDX_SAME_SITE = 10; constexpr auto IDX_RAW_SAME_SITE = 11; constexpr auto IDX_SCHEME_MAP = 12; constexpr auto IDX_PARTITIONED_ATTRIBUTE_SET = 13; #define COOKIES_FILE "cookies.sqlite" namespace mozilla { namespace net { namespace { void BindCookieParameters(mozIStorageBindingParamsArray* aParamsArray, const CookieKey& aKey, const Cookie* aCookie) { NS_ASSERTION(aParamsArray, "Null params array passed to BindCookieParameters!"); NS_ASSERTION(aCookie, "Null cookie passed to BindCookieParameters!"); // Use the asynchronous binding methods to ensure that we do not acquire the // database lock. nsCOMPtr params; DebugOnly rv = aParamsArray->NewBindingParams(getter_AddRefs(params)); MOZ_ASSERT(NS_SUCCEEDED(rv)); nsAutoCString suffix; aKey.mOriginAttributes.CreateSuffix(suffix); rv = params->BindUTF8StringByName("originAttributes"_ns, suffix); MOZ_ASSERT(NS_SUCCEEDED(rv)); rv = params->BindUTF8StringByName("name"_ns, aCookie->Name()); MOZ_ASSERT(NS_SUCCEEDED(rv)); rv = params->BindUTF8StringByName("value"_ns, aCookie->Value()); MOZ_ASSERT(NS_SUCCEEDED(rv)); rv = params->BindUTF8StringByName("host"_ns, aCookie->Host()); MOZ_ASSERT(NS_SUCCEEDED(rv)); rv = params->BindUTF8StringByName("path"_ns, aCookie->Path()); MOZ_ASSERT(NS_SUCCEEDED(rv)); rv = params->BindInt64ByName("expiry"_ns, aCookie->Expiry()); MOZ_ASSERT(NS_SUCCEEDED(rv)); rv = params->BindInt64ByName("lastAccessed"_ns, aCookie->LastAccessed()); MOZ_ASSERT(NS_SUCCEEDED(rv)); rv = params->BindInt64ByName("creationTime"_ns, aCookie->CreationTime()); MOZ_ASSERT(NS_SUCCEEDED(rv)); rv = params->BindInt32ByName("isSecure"_ns, aCookie->IsSecure()); MOZ_ASSERT(NS_SUCCEEDED(rv)); rv = params->BindInt32ByName("isHttpOnly"_ns, aCookie->IsHttpOnly()); MOZ_ASSERT(NS_SUCCEEDED(rv)); rv = params->BindInt32ByName("sameSite"_ns, aCookie->SameSite()); MOZ_ASSERT(NS_SUCCEEDED(rv)); rv = params->BindInt32ByName("rawSameSite"_ns, aCookie->RawSameSite()); MOZ_ASSERT(NS_SUCCEEDED(rv)); rv = params->BindInt32ByName("schemeMap"_ns, aCookie->SchemeMap()); MOZ_ASSERT(NS_SUCCEEDED(rv)); rv = params->BindInt32ByName("isPartitionedAttributeSet"_ns, aCookie->RawIsPartitioned()); MOZ_ASSERT(NS_SUCCEEDED(rv)); // Bind the params to the array. rv = aParamsArray->AddParams(params); MOZ_ASSERT(NS_SUCCEEDED(rv)); } class ConvertAppIdToOriginAttrsSQLFunction final : public mozIStorageFunction { ~ConvertAppIdToOriginAttrsSQLFunction() = default; NS_DECL_ISUPPORTS NS_DECL_MOZISTORAGEFUNCTION }; NS_IMPL_ISUPPORTS(ConvertAppIdToOriginAttrsSQLFunction, mozIStorageFunction); NS_IMETHODIMP ConvertAppIdToOriginAttrsSQLFunction::OnFunctionCall( mozIStorageValueArray* aFunctionArguments, nsIVariant** aResult) { nsresult rv; OriginAttributes attrs; nsAutoCString suffix; attrs.CreateSuffix(suffix); RefPtr outVar(new nsVariant()); rv = outVar->SetAsAUTF8String(suffix); NS_ENSURE_SUCCESS(rv, rv); outVar.forget(aResult); return NS_OK; } class SetAppIdFromOriginAttributesSQLFunction final : public mozIStorageFunction { ~SetAppIdFromOriginAttributesSQLFunction() = default; NS_DECL_ISUPPORTS NS_DECL_MOZISTORAGEFUNCTION }; NS_IMPL_ISUPPORTS(SetAppIdFromOriginAttributesSQLFunction, mozIStorageFunction); NS_IMETHODIMP SetAppIdFromOriginAttributesSQLFunction::OnFunctionCall( mozIStorageValueArray* aFunctionArguments, nsIVariant** aResult) { nsresult rv; nsAutoCString suffix; OriginAttributes attrs; rv = aFunctionArguments->GetUTF8String(0, suffix); NS_ENSURE_SUCCESS(rv, rv); bool success = attrs.PopulateFromSuffix(suffix); NS_ENSURE_TRUE(success, NS_ERROR_FAILURE); RefPtr outVar(new nsVariant()); rv = outVar->SetAsInt32(0); // deprecated appId! NS_ENSURE_SUCCESS(rv, rv); outVar.forget(aResult); return NS_OK; } class SetInBrowserFromOriginAttributesSQLFunction final : public mozIStorageFunction { ~SetInBrowserFromOriginAttributesSQLFunction() = default; NS_DECL_ISUPPORTS NS_DECL_MOZISTORAGEFUNCTION }; NS_IMPL_ISUPPORTS(SetInBrowserFromOriginAttributesSQLFunction, mozIStorageFunction); NS_IMETHODIMP SetInBrowserFromOriginAttributesSQLFunction::OnFunctionCall( mozIStorageValueArray* aFunctionArguments, nsIVariant** aResult) { nsresult rv; nsAutoCString suffix; OriginAttributes attrs; rv = aFunctionArguments->GetUTF8String(0, suffix); NS_ENSURE_SUCCESS(rv, rv); bool success = attrs.PopulateFromSuffix(suffix); NS_ENSURE_TRUE(success, NS_ERROR_FAILURE); RefPtr outVar(new nsVariant()); rv = outVar->SetAsInt32(false); NS_ENSURE_SUCCESS(rv, rv); outVar.forget(aResult); return NS_OK; } /****************************************************************************** * DBListenerErrorHandler impl: * Parent class for our async storage listeners that handles the logging of * errors. ******************************************************************************/ class DBListenerErrorHandler : public mozIStorageStatementCallback { protected: explicit DBListenerErrorHandler(CookiePersistentStorage* dbState) : mStorage(dbState) {} RefPtr mStorage; virtual const char* GetOpType() = 0; public: NS_IMETHOD HandleError(mozIStorageError* aError) override { if (MOZ_LOG_TEST(gCookieLog, LogLevel::Warning)) { int32_t result = -1; aError->GetResult(&result); nsAutoCString message; aError->GetMessage(message); COOKIE_LOGSTRING( LogLevel::Warning, ("DBListenerErrorHandler::HandleError(): Error %d occurred while " "performing operation '%s' with message '%s'; rebuilding database.", result, GetOpType(), message.get())); } // Rebuild the database. mStorage->HandleCorruptDB(); return NS_OK; } }; /****************************************************************************** * InsertCookieDBListener impl: * mozIStorageStatementCallback used to track asynchronous insertion operations. ******************************************************************************/ class InsertCookieDBListener final : public DBListenerErrorHandler { private: const char* GetOpType() override { return "INSERT"; } ~InsertCookieDBListener() = default; public: NS_DECL_ISUPPORTS explicit InsertCookieDBListener(CookiePersistentStorage* dbState) : DBListenerErrorHandler(dbState) {} NS_IMETHOD HandleResult(mozIStorageResultSet* /*aResultSet*/) override { MOZ_ASSERT_UNREACHABLE( "Unexpected call to " "InsertCookieDBListener::HandleResult"); return NS_OK; } NS_IMETHOD HandleCompletion(uint16_t aReason) override { // If we were rebuilding the db and we succeeded, make our mCorruptFlag say // so. if (mStorage->GetCorruptFlag() == CookiePersistentStorage::REBUILDING && aReason == mozIStorageStatementCallback::REASON_FINISHED) { COOKIE_LOGSTRING( LogLevel::Debug, ("InsertCookieDBListener::HandleCompletion(): rebuild complete")); mStorage->SetCorruptFlag(CookiePersistentStorage::OK); } // This notification is just for testing. nsCOMPtr os = services::GetObserverService(); if (os) { os->NotifyObservers(nullptr, "cookie-saved-on-disk", nullptr); } return NS_OK; } }; NS_IMPL_ISUPPORTS(InsertCookieDBListener, mozIStorageStatementCallback) /****************************************************************************** * UpdateCookieDBListener impl: * mozIStorageStatementCallback used to track asynchronous update operations. ******************************************************************************/ class UpdateCookieDBListener final : public DBListenerErrorHandler { private: const char* GetOpType() override { return "UPDATE"; } ~UpdateCookieDBListener() = default; public: NS_DECL_ISUPPORTS explicit UpdateCookieDBListener(CookiePersistentStorage* dbState) : DBListenerErrorHandler(dbState) {} NS_IMETHOD HandleResult(mozIStorageResultSet* /*aResultSet*/) override { MOZ_ASSERT_UNREACHABLE( "Unexpected call to " "UpdateCookieDBListener::HandleResult"); return NS_OK; } NS_IMETHOD HandleCompletion(uint16_t /*aReason*/) override { return NS_OK; } }; NS_IMPL_ISUPPORTS(UpdateCookieDBListener, mozIStorageStatementCallback) /****************************************************************************** * RemoveCookieDBListener impl: * mozIStorageStatementCallback used to track asynchronous removal operations. ******************************************************************************/ class RemoveCookieDBListener final : public DBListenerErrorHandler { private: const char* GetOpType() override { return "REMOVE"; } ~RemoveCookieDBListener() = default; public: NS_DECL_ISUPPORTS explicit RemoveCookieDBListener(CookiePersistentStorage* dbState) : DBListenerErrorHandler(dbState) {} NS_IMETHOD HandleResult(mozIStorageResultSet* /*aResultSet*/) override { MOZ_ASSERT_UNREACHABLE( "Unexpected call to " "RemoveCookieDBListener::HandleResult"); return NS_OK; } NS_IMETHOD HandleCompletion(uint16_t /*aReason*/) override { return NS_OK; } }; NS_IMPL_ISUPPORTS(RemoveCookieDBListener, mozIStorageStatementCallback) /****************************************************************************** * CloseCookieDBListener imp: * Static mozIStorageCompletionCallback used to notify when the database is * successfully closed. ******************************************************************************/ class CloseCookieDBListener final : public mozIStorageCompletionCallback { ~CloseCookieDBListener() = default; public: explicit CloseCookieDBListener(CookiePersistentStorage* dbState) : mStorage(dbState) {} RefPtr mStorage; NS_DECL_ISUPPORTS NS_IMETHOD Complete(nsresult /*status*/, nsISupports* /*value*/) override { mStorage->HandleDBClosed(); return NS_OK; } }; NS_IMPL_ISUPPORTS(CloseCookieDBListener, mozIStorageCompletionCallback) } // namespace // static already_AddRefed CookiePersistentStorage::Create() { RefPtr storage = new CookiePersistentStorage(); storage->Init(); storage->Activate(); return storage.forget(); } CookiePersistentStorage::CookiePersistentStorage() : mMonitor("CookiePersistentStorage"), mInitialized(false), mCorruptFlag(OK) {} void CookiePersistentStorage::NotifyChangedInternal( nsICookieNotification* aNotification, bool aOldCookieIsSession) { MOZ_ASSERT(aNotification); // Notify for topic "session-cookie-changed" to update the copy of session // cookies in session restore component. nsICookieNotification::Action action = aNotification->GetAction(); // Filter out notifications for individual non-session cookies. if (action == nsICookieNotification::COOKIE_CHANGED || action == nsICookieNotification::COOKIE_DELETED || action == nsICookieNotification::COOKIE_ADDED) { nsCOMPtr xpcCookie; DebugOnly rv = aNotification->GetCookie(getter_AddRefs(xpcCookie)); MOZ_ASSERT(NS_SUCCEEDED(rv) && xpcCookie); const Cookie& cookie = xpcCookie->AsCookie(); if (!cookie.IsSession() && !aOldCookieIsSession) { return; } } nsCOMPtr os = services::GetObserverService(); if (os) { os->NotifyObservers(aNotification, "session-cookie-changed", u""); } } void CookiePersistentStorage::RemoveAllInternal() { // clear the cookie file if (mDBConn) { nsCOMPtr stmt; nsresult rv = mDBConn->CreateAsyncStatement("DELETE FROM moz_cookies"_ns, getter_AddRefs(stmt)); if (NS_SUCCEEDED(rv)) { nsCOMPtr handle; rv = stmt->ExecuteAsync(mRemoveListener, getter_AddRefs(handle)); MOZ_ASSERT(NS_SUCCEEDED(rv)); } else { // Recreate the database. COOKIE_LOGSTRING(LogLevel::Debug, ("RemoveAll(): corruption detected with rv 0x%" PRIx32, static_cast(rv))); HandleCorruptDB(); } } } void CookiePersistentStorage::HandleCorruptDB() { COOKIE_LOGSTRING(LogLevel::Debug, ("HandleCorruptDB(): CookieStorage %p has mCorruptFlag %u", this, mCorruptFlag)); // Mark the database corrupt, so the close listener can begin reconstructing // it. switch (mCorruptFlag) { case OK: { // Move to 'closing' state. mCorruptFlag = CLOSING_FOR_REBUILD; CleanupCachedStatements(); mDBConn->AsyncClose(mCloseListener); CleanupDBConnection(); break; } case CLOSING_FOR_REBUILD: { // We had an error while waiting for close completion. That's OK, just // ignore it -- we're rebuilding anyway. return; } case REBUILDING: { // We had an error while rebuilding the DB. Game over. Close the database // and let the close handler do nothing; then we'll move it out of the // way. CleanupCachedStatements(); if (mDBConn) { mDBConn->AsyncClose(mCloseListener); } CleanupDBConnection(); break; } } } void CookiePersistentStorage::RemoveCookiesWithOriginAttributes( const OriginAttributesPattern& aPattern, const nsACString& aBaseDomain) { mozStorageTransaction transaction(mDBConn, false); // XXX Handle the error, bug 1696130. Unused << NS_WARN_IF(NS_FAILED(transaction.Start())); CookieStorage::RemoveCookiesWithOriginAttributes(aPattern, aBaseDomain); DebugOnly rv = transaction.Commit(); MOZ_ASSERT(NS_SUCCEEDED(rv)); } void CookiePersistentStorage::RemoveCookiesFromExactHost( const nsACString& aHost, const nsACString& aBaseDomain, const OriginAttributesPattern& aPattern) { mozStorageTransaction transaction(mDBConn, false); // XXX Handle the error, bug 1696130. Unused << NS_WARN_IF(NS_FAILED(transaction.Start())); CookieStorage::RemoveCookiesFromExactHost(aHost, aBaseDomain, aPattern); DebugOnly rv = transaction.Commit(); MOZ_ASSERT(NS_SUCCEEDED(rv)); } void CookiePersistentStorage::RemoveCookieFromDB(const Cookie& aCookie) { // if it's a non-session cookie, remove it from the db if (aCookie.IsSession() || !mDBConn) { return; } nsCOMPtr paramsArray; mStmtDelete->NewBindingParamsArray(getter_AddRefs(paramsArray)); PrepareCookieRemoval(aCookie, paramsArray); DebugOnly rv = mStmtDelete->BindParameters(paramsArray); MOZ_ASSERT(NS_SUCCEEDED(rv)); nsCOMPtr handle; rv = mStmtDelete->ExecuteAsync(mRemoveListener, getter_AddRefs(handle)); MOZ_ASSERT(NS_SUCCEEDED(rv)); } void CookiePersistentStorage::PrepareCookieRemoval( const Cookie& aCookie, mozIStorageBindingParamsArray* aParamsArray) { // if it's a non-session cookie, remove it from the db if (aCookie.IsSession() || !mDBConn) { return; } nsCOMPtr params; aParamsArray->NewBindingParams(getter_AddRefs(params)); DebugOnly rv = params->BindUTF8StringByName("name"_ns, aCookie.Name()); MOZ_ASSERT(NS_SUCCEEDED(rv)); rv = params->BindUTF8StringByName("host"_ns, aCookie.Host()); MOZ_ASSERT(NS_SUCCEEDED(rv)); rv = params->BindUTF8StringByName("path"_ns, aCookie.Path()); MOZ_ASSERT(NS_SUCCEEDED(rv)); nsAutoCString suffix; aCookie.OriginAttributesRef().CreateSuffix(suffix); rv = params->BindUTF8StringByName("originAttributes"_ns, suffix); MOZ_ASSERT(NS_SUCCEEDED(rv)); rv = aParamsArray->AddParams(params); MOZ_ASSERT(NS_SUCCEEDED(rv)); } // Null out the statements. // This must be done before closing the connection. void CookiePersistentStorage::CleanupCachedStatements() { mStmtInsert = nullptr; mStmtDelete = nullptr; mStmtUpdate = nullptr; } // Null out the listeners, and the database connection itself. This // will not null out the statements, cancel a pending read or // asynchronously close the connection -- these must be done // beforehand if necessary. void CookiePersistentStorage::CleanupDBConnection() { MOZ_ASSERT(!mStmtInsert, "mStmtInsert has been cleaned up"); MOZ_ASSERT(!mStmtDelete, "mStmtDelete has been cleaned up"); MOZ_ASSERT(!mStmtUpdate, "mStmtUpdate has been cleaned up"); // Null out the database connections. If 'mDBConn' has not been used for any // asynchronous operations yet, this will synchronously close it; otherwise, // it's expected that the caller has performed an AsyncClose prior. mDBConn = nullptr; // Manually null out our listeners. This is necessary because they hold a // strong ref to the CookieStorage itself. They'll stay alive until whatever // statements are still executing complete. mInsertListener = nullptr; mUpdateListener = nullptr; mRemoveListener = nullptr; mCloseListener = nullptr; } void CookiePersistentStorage::Close() { if (mThread) { mThread->Shutdown(); mThread = nullptr; } // Cleanup cached statements before we can close anything. CleanupCachedStatements(); if (mDBConn) { // Asynchronously close the connection. We will null it below. mDBConn->AsyncClose(mCloseListener); } CleanupDBConnection(); mInitialized = false; mInitializedDBConn = false; } void CookiePersistentStorage::StoreCookie( const nsACString& aBaseDomain, const OriginAttributes& aOriginAttributes, Cookie* aCookie) { // if it's a non-session cookie and hasn't just been read from the db, write // it out. if (aCookie->IsSession() || !mDBConn) { return; } nsCOMPtr paramsArray; mStmtInsert->NewBindingParamsArray(getter_AddRefs(paramsArray)); CookieKey key(aBaseDomain, aOriginAttributes); BindCookieParameters(paramsArray, key, aCookie); MaybeStoreCookiesToDB(paramsArray); } void CookiePersistentStorage::MaybeStoreCookiesToDB( mozIStorageBindingParamsArray* aParamsArray) { if (!aParamsArray) { return; } uint32_t length; aParamsArray->GetLength(&length); if (!length) { return; } DebugOnly rv = mStmtInsert->BindParameters(aParamsArray); MOZ_ASSERT(NS_SUCCEEDED(rv)); nsCOMPtr handle; rv = mStmtInsert->ExecuteAsync(mInsertListener, getter_AddRefs(handle)); MOZ_ASSERT(NS_SUCCEEDED(rv)); } void CookiePersistentStorage::StaleCookies(const nsTArray& aCookieList, int64_t aCurrentTimeInUsec) { // Create an array of parameters to bind to our update statement. Batching // is OK here since we're updating cookies with no interleaved operations. nsCOMPtr paramsArray; mozIStorageAsyncStatement* stmt = mStmtUpdate; if (mDBConn) { stmt->NewBindingParamsArray(getter_AddRefs(paramsArray)); } int32_t count = aCookieList.Length(); for (int32_t i = 0; i < count; ++i) { Cookie* cookie = aCookieList.ElementAt(i); if (cookie->IsStale()) { UpdateCookieInList(cookie, aCurrentTimeInUsec, paramsArray); } } // Update the database now if necessary. if (paramsArray) { uint32_t length; paramsArray->GetLength(&length); if (length) { DebugOnly rv = stmt->BindParameters(paramsArray); MOZ_ASSERT(NS_SUCCEEDED(rv)); nsCOMPtr handle; rv = stmt->ExecuteAsync(mUpdateListener, getter_AddRefs(handle)); MOZ_ASSERT(NS_SUCCEEDED(rv)); } } } void CookiePersistentStorage::UpdateCookieInList( Cookie* aCookie, int64_t aLastAccessed, mozIStorageBindingParamsArray* aParamsArray) { MOZ_ASSERT(aCookie); // udpate the lastAccessed timestamp aCookie->SetLastAccessed(aLastAccessed); // if it's a non-session cookie, update it in the db too if (!aCookie->IsSession() && aParamsArray) { // Create our params holder. nsCOMPtr params; aParamsArray->NewBindingParams(getter_AddRefs(params)); // Bind our parameters. DebugOnly rv = params->BindInt64ByName("lastAccessed"_ns, aLastAccessed); MOZ_ASSERT(NS_SUCCEEDED(rv)); rv = params->BindUTF8StringByName("name"_ns, aCookie->Name()); MOZ_ASSERT(NS_SUCCEEDED(rv)); rv = params->BindUTF8StringByName("host"_ns, aCookie->Host()); MOZ_ASSERT(NS_SUCCEEDED(rv)); rv = params->BindUTF8StringByName("path"_ns, aCookie->Path()); MOZ_ASSERT(NS_SUCCEEDED(rv)); nsAutoCString suffix; aCookie->OriginAttributesRef().CreateSuffix(suffix); rv = params->BindUTF8StringByName("originAttributes"_ns, suffix); MOZ_ASSERT(NS_SUCCEEDED(rv)); // Add our bound parameters to the array. rv = aParamsArray->AddParams(params); MOZ_ASSERT(NS_SUCCEEDED(rv)); } } void CookiePersistentStorage::DeleteFromDB( mozIStorageBindingParamsArray* aParamsArray) { uint32_t length; aParamsArray->GetLength(&length); if (length) { DebugOnly rv = mStmtDelete->BindParameters(aParamsArray); MOZ_ASSERT(NS_SUCCEEDED(rv)); nsCOMPtr handle; rv = mStmtDelete->ExecuteAsync(mRemoveListener, getter_AddRefs(handle)); MOZ_ASSERT(NS_SUCCEEDED(rv)); } } void CookiePersistentStorage::Activate() { MOZ_ASSERT(!mThread, "already have a cookie thread"); mStorageService = do_GetService("@mozilla.org/storage/service;1"); MOZ_ASSERT(mStorageService); mTLDService = do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID); MOZ_ASSERT(mTLDService); // Get our cookie file. nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(mCookieFile)); if (NS_FAILED(rv)) { // We've already set up our CookieStorages appropriately; nothing more to // do. COOKIE_LOGSTRING(LogLevel::Warning, ("InitCookieStorages(): couldn't get cookie file")); mInitializedDBConn = true; mInitialized = true; return; } mCookieFile->AppendNative(nsLiteralCString(COOKIES_FILE)); NS_ENSURE_SUCCESS_VOID(NS_NewNamedThread("Cookie", getter_AddRefs(mThread))); RefPtr self = this; nsCOMPtr runnable = NS_NewRunnableFunction("CookiePersistentStorage::Activate", [self] { MonitorAutoLock lock(self->mMonitor); // Attempt to open and read the database. If TryInitDB() returns // RESULT_RETRY, do so. OpenDBResult result = self->TryInitDB(false); if (result == RESULT_RETRY) { // Database may be corrupt. Synchronously close the connection, clean // up the default CookieStorage, and try again. COOKIE_LOGSTRING(LogLevel::Warning, ("InitCookieStorages(): retrying TryInitDB()")); self->CleanupCachedStatements(); self->CleanupDBConnection(); result = self->TryInitDB(true); if (result == RESULT_RETRY) { // We're done. Change the code to failure so we clean up below. result = RESULT_FAILURE; } } if (result == RESULT_FAILURE) { COOKIE_LOGSTRING( LogLevel::Warning, ("InitCookieStorages(): TryInitDB() failed, closing connection")); // Connection failure is unrecoverable. Clean up our connection. We // can run fine without persistent storage -- e.g. if there's no // profile. self->CleanupCachedStatements(); self->CleanupDBConnection(); // No need to initialize mDBConn self->mInitializedDBConn = true; } self->mInitialized = true; NS_DispatchToMainThread( NS_NewRunnableFunction("CookiePersistentStorage::InitDBConn", [self] { self->InitDBConn(); })); self->mMonitor.Notify(); }); mThread->Dispatch(runnable, NS_DISPATCH_NORMAL); } /* Attempt to open and read the database. If 'aRecreateDB' is true, try to * move the existing database file out of the way and create a new one. * * @returns RESULT_OK if opening or creating the database succeeded; * RESULT_RETRY if the database cannot be opened, is corrupt, or some * other failure occurred that might be resolved by recreating the * database; or RESULT_FAILED if there was an unrecoverable error and * we must run without a database. * * If RESULT_RETRY or RESULT_FAILED is returned, the caller should perform * cleanup of the default CookieStorage. */ CookiePersistentStorage::OpenDBResult CookiePersistentStorage::TryInitDB( bool aRecreateDB) { NS_ASSERTION(!mDBConn, "nonnull mDBConn"); NS_ASSERTION(!mStmtInsert, "nonnull mStmtInsert"); NS_ASSERTION(!mInsertListener, "nonnull mInsertListener"); NS_ASSERTION(!mSyncConn, "nonnull mSyncConn"); NS_ASSERTION(NS_GetCurrentThread() == mThread, "non cookie thread"); // Ditch an existing db, if we've been told to (i.e. it's corrupt). We don't // want to delete it outright, since it may be useful for debugging purposes, // so we move it out of the way. nsresult rv; if (aRecreateDB) { nsCOMPtr backupFile; mCookieFile->Clone(getter_AddRefs(backupFile)); rv = backupFile->MoveToNative(nullptr, nsLiteralCString(COOKIES_FILE ".bak")); NS_ENSURE_SUCCESS(rv, RESULT_FAILURE); } // This block provides scope for the Telemetry AutoTimer { Telemetry::AutoTimer telemetry; ReadAheadFile(mCookieFile); // open a connection to the cookie database, and only cache our connection // and statements upon success. The connection is opened unshared to // eliminate cache contention between the main and background threads. rv = mStorageService->OpenUnsharedDatabase( mCookieFile, mozIStorageService::CONNECTION_DEFAULT, getter_AddRefs(mSyncConn)); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); } auto guard = MakeScopeExit([&] { mSyncConn = nullptr; }); bool tableExists = false; mSyncConn->TableExists("moz_cookies"_ns, &tableExists); if (!tableExists) { rv = CreateTable(); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); } else { // table already exists; check the schema version before reading int32_t dbSchemaVersion; rv = mSyncConn->GetSchemaVersion(&dbSchemaVersion); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); // Start a transaction for the whole migration block. mozStorageTransaction transaction(mSyncConn, true); // XXX Handle the error, bug 1696130. Unused << NS_WARN_IF(NS_FAILED(transaction.Start())); switch (dbSchemaVersion) { // Upgrading. // Every time you increment the database schema, you need to implement // the upgrading code from the previous version to the new one. If // migration fails for any reason, it's a bug -- so we return RESULT_RETRY // such that the original database will be saved, in the hopes that we // might one day see it and fix it. case 1: { // Add the lastAccessed column to the table. rv = mSyncConn->ExecuteSimpleSQL(nsLiteralCString( "ALTER TABLE moz_cookies ADD lastAccessed INTEGER")); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); } // Fall through to the next upgrade. [[fallthrough]]; case 2: { // Add the baseDomain column and index to the table. rv = mSyncConn->ExecuteSimpleSQL( "ALTER TABLE moz_cookies ADD baseDomain TEXT"_ns); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); // Compute the baseDomains for the table. This must be done eagerly // otherwise we won't be able to synchronously read in individual // domains on demand. const int64_t SCHEMA2_IDX_ID = 0; const int64_t SCHEMA2_IDX_HOST = 1; nsCOMPtr select; rv = mSyncConn->CreateStatement("SELECT id, host FROM moz_cookies"_ns, getter_AddRefs(select)); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); nsCOMPtr update; rv = mSyncConn->CreateStatement( nsLiteralCString("UPDATE moz_cookies SET baseDomain = " ":baseDomain WHERE id = :id"), getter_AddRefs(update)); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); nsCString baseDomain; nsCString host; bool hasResult; while (true) { rv = select->ExecuteStep(&hasResult); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); if (!hasResult) { break; } int64_t id = select->AsInt64(SCHEMA2_IDX_ID); select->GetUTF8String(SCHEMA2_IDX_HOST, host); rv = CookieCommons::GetBaseDomainFromHost(mTLDService, host, baseDomain); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); mozStorageStatementScoper scoper(update); rv = update->BindUTF8StringByName("baseDomain"_ns, baseDomain); MOZ_ASSERT(NS_SUCCEEDED(rv)); rv = update->BindInt64ByName("id"_ns, id); MOZ_ASSERT(NS_SUCCEEDED(rv)); rv = update->ExecuteStep(&hasResult); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); } // Create an index on baseDomain. rv = mSyncConn->ExecuteSimpleSQL(nsLiteralCString( "CREATE INDEX moz_basedomain ON moz_cookies (baseDomain)")); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); } // Fall through to the next upgrade. [[fallthrough]]; case 3: { // Add the creationTime column to the table, and create a unique index // on (name, host, path). Before we do this, we have to purge the table // of expired cookies such that we know that the (name, host, path) // index is truly unique -- otherwise we can't create the index. Note // that we can't just execute a statement to delete all rows where the // expiry column is in the past -- doing so would rely on the clock // (both now and when previous cookies were set) being monotonic. // Select the whole table, and order by the fields we're interested in. // This means we can simply do a linear traversal of the results and // check for duplicates as we go. const int64_t SCHEMA3_IDX_ID = 0; const int64_t SCHEMA3_IDX_NAME = 1; const int64_t SCHEMA3_IDX_HOST = 2; const int64_t SCHEMA3_IDX_PATH = 3; nsCOMPtr select; rv = mSyncConn->CreateStatement( nsLiteralCString( "SELECT id, name, host, path FROM moz_cookies " "ORDER BY name ASC, host ASC, path ASC, expiry ASC"), getter_AddRefs(select)); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); nsCOMPtr deleteExpired; rv = mSyncConn->CreateStatement( "DELETE FROM moz_cookies WHERE id = :id"_ns, getter_AddRefs(deleteExpired)); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); // Read the first row. bool hasResult; rv = select->ExecuteStep(&hasResult); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); if (hasResult) { nsCString name1; nsCString host1; nsCString path1; int64_t id1 = select->AsInt64(SCHEMA3_IDX_ID); select->GetUTF8String(SCHEMA3_IDX_NAME, name1); select->GetUTF8String(SCHEMA3_IDX_HOST, host1); select->GetUTF8String(SCHEMA3_IDX_PATH, path1); nsCString name2; nsCString host2; nsCString path2; while (true) { // Read the second row. rv = select->ExecuteStep(&hasResult); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); if (!hasResult) { break; } int64_t id2 = select->AsInt64(SCHEMA3_IDX_ID); select->GetUTF8String(SCHEMA3_IDX_NAME, name2); select->GetUTF8String(SCHEMA3_IDX_HOST, host2); select->GetUTF8String(SCHEMA3_IDX_PATH, path2); // If the two rows match in (name, host, path), we know the earlier // row has an earlier expiry time. Delete it. if (name1 == name2 && host1 == host2 && path1 == path2) { mozStorageStatementScoper scoper(deleteExpired); rv = deleteExpired->BindInt64ByName("id"_ns, id1); MOZ_ASSERT(NS_SUCCEEDED(rv)); rv = deleteExpired->ExecuteStep(&hasResult); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); } // Make the second row the first for the next iteration. name1 = name2; host1 = host2; path1 = path2; id1 = id2; } } // Add the creationTime column to the table. rv = mSyncConn->ExecuteSimpleSQL(nsLiteralCString( "ALTER TABLE moz_cookies ADD creationTime INTEGER")); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); // Copy the id of each row into the new creationTime column. rv = mSyncConn->ExecuteSimpleSQL( nsLiteralCString("UPDATE moz_cookies SET creationTime = " "(SELECT id WHERE id = moz_cookies.id)")); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); // Create a unique index on (name, host, path) to allow fast lookup. rv = mSyncConn->ExecuteSimpleSQL( nsLiteralCString("CREATE UNIQUE INDEX moz_uniqueid " "ON moz_cookies (name, host, path)")); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); } // Fall through to the next upgrade. [[fallthrough]]; case 4: { // We need to add appId/inBrowserElement, plus change a constraint on // the table (unique entries now include appId/inBrowserElement): // this requires creating a new table and copying the data to it. We // then rename the new table to the old name. // // Why we made this change: appId/inBrowserElement allow "cookie jars" // for Firefox OS. We create a separate cookie namespace per {appId, // inBrowserElement}. When upgrading, we convert existing cookies // (which imply we're on desktop/mobile) to use {0, false}, as that is // the only namespace used by a non-Firefox-OS implementation. // Rename existing table rv = mSyncConn->ExecuteSimpleSQL(nsLiteralCString( "ALTER TABLE moz_cookies RENAME TO moz_cookies_old")); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); // Drop existing index (CreateTable will create new one for new table) rv = mSyncConn->ExecuteSimpleSQL("DROP INDEX moz_basedomain"_ns); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); // Create new table (with new fields and new unique constraint) rv = CreateTableForSchemaVersion5(); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); // Copy data from old table, using appId/inBrowser=0 for existing rows rv = mSyncConn->ExecuteSimpleSQL(nsLiteralCString( "INSERT INTO moz_cookies " "(baseDomain, appId, inBrowserElement, name, value, host, path, " "expiry," " lastAccessed, creationTime, isSecure, isHttpOnly) " "SELECT baseDomain, 0, 0, name, value, host, path, expiry," " lastAccessed, creationTime, isSecure, isHttpOnly " "FROM moz_cookies_old")); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); // Drop old table rv = mSyncConn->ExecuteSimpleSQL("DROP TABLE moz_cookies_old"_ns); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); COOKIE_LOGSTRING(LogLevel::Debug, ("Upgraded database to schema version 5")); } // Fall through to the next upgrade. [[fallthrough]]; case 5: { // Change in the version: Replace the columns |appId| and // |inBrowserElement| by a single column |originAttributes|. // // Why we made this change: FxOS new security model (NSec) encapsulates // "appId/inIsolatedMozBrowser" in nsIPrincipal::originAttributes to // make it easier to modify the contents of this structure in the // future. // // We do the migration in several steps: // 1. Rename the old table. // 2. Create a new table. // 3. Copy data from the old table to the new table; convert appId and // inBrowserElement to originAttributes in the meantime. // Rename existing table. rv = mSyncConn->ExecuteSimpleSQL(nsLiteralCString( "ALTER TABLE moz_cookies RENAME TO moz_cookies_old")); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); // Drop existing index (CreateTable will create new one for new table). rv = mSyncConn->ExecuteSimpleSQL("DROP INDEX moz_basedomain"_ns); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); // Create new table with new fields and new unique constraint. rv = CreateTableForSchemaVersion6(); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); // Copy data from old table without the two deprecated columns appId and // inBrowserElement. nsCOMPtr convertToOriginAttrs( new ConvertAppIdToOriginAttrsSQLFunction()); NS_ENSURE_TRUE(convertToOriginAttrs, RESULT_RETRY); constexpr auto convertToOriginAttrsName = "CONVERT_TO_ORIGIN_ATTRIBUTES"_ns; rv = mSyncConn->CreateFunction(convertToOriginAttrsName, 2, convertToOriginAttrs); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); rv = mSyncConn->ExecuteSimpleSQL(nsLiteralCString( "INSERT INTO moz_cookies " "(baseDomain, originAttributes, name, value, host, path, expiry," " lastAccessed, creationTime, isSecure, isHttpOnly) " "SELECT baseDomain, " " CONVERT_TO_ORIGIN_ATTRIBUTES(appId, inBrowserElement)," " name, value, host, path, expiry, lastAccessed, creationTime, " " isSecure, isHttpOnly " "FROM moz_cookies_old")); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); rv = mSyncConn->RemoveFunction(convertToOriginAttrsName); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); // Drop old table rv = mSyncConn->ExecuteSimpleSQL("DROP TABLE moz_cookies_old"_ns); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); COOKIE_LOGSTRING(LogLevel::Debug, ("Upgraded database to schema version 6")); } [[fallthrough]]; case 6: { // We made a mistake in schema version 6. We cannot remove expected // columns of any version (checked in the default case) from cookie // database, because doing this would destroy the possibility of // downgrading database. // // This version simply restores appId and inBrowserElement columns in // order to fix downgrading issue even though these two columns are no // longer used in the latest schema. rv = mSyncConn->ExecuteSimpleSQL(nsLiteralCString( "ALTER TABLE moz_cookies ADD appId INTEGER DEFAULT 0;")); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); rv = mSyncConn->ExecuteSimpleSQL(nsLiteralCString( "ALTER TABLE moz_cookies ADD inBrowserElement INTEGER DEFAULT 0;")); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); // Compute and populate the values of appId and inBrwoserElement from // originAttributes. nsCOMPtr setAppId( new SetAppIdFromOriginAttributesSQLFunction()); NS_ENSURE_TRUE(setAppId, RESULT_RETRY); constexpr auto setAppIdName = "SET_APP_ID"_ns; rv = mSyncConn->CreateFunction(setAppIdName, 1, setAppId); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); nsCOMPtr setInBrowser( new SetInBrowserFromOriginAttributesSQLFunction()); NS_ENSURE_TRUE(setInBrowser, RESULT_RETRY); constexpr auto setInBrowserName = "SET_IN_BROWSER"_ns; rv = mSyncConn->CreateFunction(setInBrowserName, 1, setInBrowser); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); rv = mSyncConn->ExecuteSimpleSQL(nsLiteralCString( "UPDATE moz_cookies SET appId = SET_APP_ID(originAttributes), " "inBrowserElement = SET_IN_BROWSER(originAttributes);")); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); rv = mSyncConn->RemoveFunction(setAppIdName); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); rv = mSyncConn->RemoveFunction(setInBrowserName); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); COOKIE_LOGSTRING(LogLevel::Debug, ("Upgraded database to schema version 7")); } [[fallthrough]]; case 7: { // Remove the appId field from moz_cookies. // // Unfortunately sqlite doesn't support dropping columns using ALTER // TABLE, so we need to go through the procedure documented in // https://www.sqlite.org/lang_altertable.html. // Drop existing index rv = mSyncConn->ExecuteSimpleSQL("DROP INDEX moz_basedomain"_ns); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); // Create a new_moz_cookies table without the appId field. rv = mSyncConn->ExecuteSimpleSQL( nsLiteralCString("CREATE TABLE new_moz_cookies(" "id INTEGER PRIMARY KEY, " "baseDomain TEXT, " "originAttributes TEXT NOT NULL DEFAULT '', " "name TEXT, " "value TEXT, " "host TEXT, " "path TEXT, " "expiry INTEGER, " "lastAccessed INTEGER, " "creationTime INTEGER, " "isSecure INTEGER, " "isHttpOnly INTEGER, " "inBrowserElement INTEGER DEFAULT 0, " "CONSTRAINT moz_uniqueid UNIQUE (name, host, " "path, originAttributes)" ")")); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); // Move the data over. rv = mSyncConn->ExecuteSimpleSQL( nsLiteralCString("INSERT INTO new_moz_cookies (" "id, " "baseDomain, " "originAttributes, " "name, " "value, " "host, " "path, " "expiry, " "lastAccessed, " "creationTime, " "isSecure, " "isHttpOnly, " "inBrowserElement " ") SELECT " "id, " "baseDomain, " "originAttributes, " "name, " "value, " "host, " "path, " "expiry, " "lastAccessed, " "creationTime, " "isSecure, " "isHttpOnly, " "inBrowserElement " "FROM moz_cookies;")); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); // Drop the old table rv = mSyncConn->ExecuteSimpleSQL("DROP TABLE moz_cookies;"_ns); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); // Rename new_moz_cookies to moz_cookies. rv = mSyncConn->ExecuteSimpleSQL(nsLiteralCString( "ALTER TABLE new_moz_cookies RENAME TO moz_cookies;")); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); // Recreate our index. rv = mSyncConn->ExecuteSimpleSQL( nsLiteralCString("CREATE INDEX moz_basedomain ON moz_cookies " "(baseDomain, originAttributes)")); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); COOKIE_LOGSTRING(LogLevel::Debug, ("Upgraded database to schema version 8")); } [[fallthrough]]; case 8: { // Add the sameSite column to the table. rv = mSyncConn->ExecuteSimpleSQL( "ALTER TABLE moz_cookies ADD sameSite INTEGER"_ns); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); COOKIE_LOGSTRING(LogLevel::Debug, ("Upgraded database to schema version 9")); } [[fallthrough]]; case 9: { // Add the rawSameSite column to the table. rv = mSyncConn->ExecuteSimpleSQL(nsLiteralCString( "ALTER TABLE moz_cookies ADD rawSameSite INTEGER")); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); // Copy the current sameSite value into rawSameSite. rv = mSyncConn->ExecuteSimpleSQL( "UPDATE moz_cookies SET rawSameSite = sameSite"_ns); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); COOKIE_LOGSTRING(LogLevel::Debug, ("Upgraded database to schema version 10")); } [[fallthrough]]; case 10: { // Rename existing table rv = mSyncConn->ExecuteSimpleSQL(nsLiteralCString( "ALTER TABLE moz_cookies RENAME TO moz_cookies_old")); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); // Create a new moz_cookies table without the baseDomain field. rv = mSyncConn->ExecuteSimpleSQL( nsLiteralCString("CREATE TABLE moz_cookies(" "id INTEGER PRIMARY KEY, " "originAttributes TEXT NOT NULL DEFAULT '', " "name TEXT, " "value TEXT, " "host TEXT, " "path TEXT, " "expiry INTEGER, " "lastAccessed INTEGER, " "creationTime INTEGER, " "isSecure INTEGER, " "isHttpOnly INTEGER, " "inBrowserElement INTEGER DEFAULT 0, " "sameSite INTEGER DEFAULT 0, " "rawSameSite INTEGER DEFAULT 0, " "CONSTRAINT moz_uniqueid UNIQUE (name, host, " "path, originAttributes)" ")")); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); // Move the data over. rv = mSyncConn->ExecuteSimpleSQL( nsLiteralCString("INSERT INTO moz_cookies (" "id, " "originAttributes, " "name, " "value, " "host, " "path, " "expiry, " "lastAccessed, " "creationTime, " "isSecure, " "isHttpOnly, " "inBrowserElement, " "sameSite, " "rawSameSite " ") SELECT " "id, " "originAttributes, " "name, " "value, " "host, " "path, " "expiry, " "lastAccessed, " "creationTime, " "isSecure, " "isHttpOnly, " "inBrowserElement, " "sameSite, " "rawSameSite " "FROM moz_cookies_old " "WHERE baseDomain NOTNULL;")); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); // Drop the old table rv = mSyncConn->ExecuteSimpleSQL("DROP TABLE moz_cookies_old;"_ns); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); // Drop the moz_basedomain index from the database (if it hasn't been // removed already by removing the table). rv = mSyncConn->ExecuteSimpleSQL( "DROP INDEX IF EXISTS moz_basedomain;"_ns); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); COOKIE_LOGSTRING(LogLevel::Debug, ("Upgraded database to schema version 11")); } [[fallthrough]]; case 11: { // Add the schemeMap column to the table. rv = mSyncConn->ExecuteSimpleSQL(nsLiteralCString( "ALTER TABLE moz_cookies ADD schemeMap INTEGER DEFAULT 0;")); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); COOKIE_LOGSTRING(LogLevel::Debug, ("Upgraded database to schema version 12")); // No more upgrades. Update the schema version. rv = mSyncConn->SetSchemaVersion(COOKIES_SCHEMA_VERSION); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); } [[fallthrough]]; case 12: { // Add the isPartitionedAttributeSet column to the table. rv = mSyncConn->ExecuteSimpleSQL( nsLiteralCString("ALTER TABLE moz_cookies ADD " "isPartitionedAttributeSet INTEGER DEFAULT 0;")); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); COOKIE_LOGSTRING(LogLevel::Debug, ("Upgraded database to schema version 13")); // No more upgrades. Update the schema version. rv = mSyncConn->SetSchemaVersion(COOKIES_SCHEMA_VERSION); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); [[fallthrough]]; } case COOKIES_SCHEMA_VERSION: break; case 0: { NS_WARNING("couldn't get schema version!"); // the table may be usable; someone might've just clobbered the schema // version. we can treat this case like a downgrade using the codepath // below, by verifying the columns we care about are all there. for now, // re-set the schema version in the db, in case the checks succeed (if // they don't, we're dropping the table anyway). rv = mSyncConn->SetSchemaVersion(COOKIES_SCHEMA_VERSION); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); } // fall through to downgrade check [[fallthrough]]; // downgrading. // if columns have been added to the table, we can still use the ones we // understand safely. if columns have been deleted or altered, just // blow away the table and start from scratch! if you change the way // a column is interpreted, make sure you also change its name so this // check will catch it. default: { // check if all the expected columns exist nsCOMPtr stmt; rv = mSyncConn->CreateStatement( nsLiteralCString("SELECT " "id, " "originAttributes, " "name, " "value, " "host, " "path, " "expiry, " "lastAccessed, " "creationTime, " "isSecure, " "isHttpOnly, " "sameSite, " "rawSameSite, " "schemeMap, " "isPartitionedAttributeSet " "FROM moz_cookies"), getter_AddRefs(stmt)); if (NS_SUCCEEDED(rv)) { break; } // our columns aren't there - drop the table! rv = mSyncConn->ExecuteSimpleSQL("DROP TABLE moz_cookies"_ns); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); rv = CreateTable(); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); } break; } } // if we deleted a corrupt db, don't attempt to import - return now if (aRecreateDB) { return RESULT_OK; } // check whether to import or just read in the db if (tableExists) { return Read(); } return RESULT_OK; } void CookiePersistentStorage::RebuildCorruptDB() { NS_ASSERTION(!mDBConn, "shouldn't have an open db connection"); NS_ASSERTION(mCorruptFlag == CookiePersistentStorage::CLOSING_FOR_REBUILD, "should be in CLOSING_FOR_REBUILD state"); nsCOMPtr os = services::GetObserverService(); mCorruptFlag = CookiePersistentStorage::REBUILDING; COOKIE_LOGSTRING(LogLevel::Debug, ("RebuildCorruptDB(): creating new database")); RefPtr self = this; nsCOMPtr runnable = NS_NewRunnableFunction("RebuildCorruptDB.TryInitDB", [self] { // The database has been closed, and we're ready to rebuild. Open a // connection. OpenDBResult result = self->TryInitDB(true); nsCOMPtr innerRunnable = NS_NewRunnableFunction( "RebuildCorruptDB.TryInitDBComplete", [self, result] { nsCOMPtr os = services::GetObserverService(); if (result != RESULT_OK) { // We're done. Reset our DB connection and statements, and // notify of closure. COOKIE_LOGSTRING( LogLevel::Warning, ("RebuildCorruptDB(): TryInitDB() failed with result %u", result)); self->CleanupCachedStatements(); self->CleanupDBConnection(); self->mCorruptFlag = CookiePersistentStorage::OK; if (os) { os->NotifyObservers(nullptr, "cookie-db-closed", nullptr); } return; } // Notify observers that we're beginning the rebuild. if (os) { os->NotifyObservers(nullptr, "cookie-db-rebuilding", nullptr); } self->InitDBConnInternal(); // Enumerate the hash, and add cookies to the params array. mozIStorageAsyncStatement* stmt = self->mStmtInsert; nsCOMPtr paramsArray; stmt->NewBindingParamsArray(getter_AddRefs(paramsArray)); for (auto iter = self->mHostTable.Iter(); !iter.Done(); iter.Next()) { CookieEntry* entry = iter.Get(); const CookieEntry::ArrayType& cookies = entry->GetCookies(); for (CookieEntry::IndexType i = 0; i < cookies.Length(); ++i) { Cookie* cookie = cookies[i]; if (!cookie->IsSession()) { BindCookieParameters(paramsArray, CookieKey(entry), cookie); } } } // Make sure we've got something to write. If we don't, we're // done. uint32_t length; paramsArray->GetLength(&length); if (length == 0) { COOKIE_LOGSTRING( LogLevel::Debug, ("RebuildCorruptDB(): nothing to write, rebuild complete")); self->mCorruptFlag = CookiePersistentStorage::OK; return; } self->MaybeStoreCookiesToDB(paramsArray); }); NS_DispatchToMainThread(innerRunnable); }); mThread->Dispatch(runnable, NS_DISPATCH_NORMAL); } void CookiePersistentStorage::HandleDBClosed() { COOKIE_LOGSTRING(LogLevel::Debug, ("HandleDBClosed(): CookieStorage %p closed", this)); nsCOMPtr os = services::GetObserverService(); switch (mCorruptFlag) { case CookiePersistentStorage::OK: { // Database is healthy. Notify of closure. if (os) { os->NotifyObservers(nullptr, "cookie-db-closed", nullptr); } break; } case CookiePersistentStorage::CLOSING_FOR_REBUILD: { // Our close finished. Start the rebuild, and notify of db closure later. RebuildCorruptDB(); break; } case CookiePersistentStorage::REBUILDING: { // We encountered an error during rebuild, closed the database, and now // here we are. We already have a 'cookies.sqlite.bak' from the original // dead database; we don't want to overwrite it, so let's move this one to // 'cookies.sqlite.bak-rebuild'. nsCOMPtr backupFile; mCookieFile->Clone(getter_AddRefs(backupFile)); nsresult rv = backupFile->MoveToNative( nullptr, nsLiteralCString(COOKIES_FILE ".bak-rebuild")); COOKIE_LOGSTRING(LogLevel::Warning, ("HandleDBClosed(): CookieStorage %p encountered error " "rebuilding db; move to " "'cookies.sqlite.bak-rebuild' gave rv 0x%" PRIx32, this, static_cast(rv))); if (os) { os->NotifyObservers(nullptr, "cookie-db-closed", nullptr); } break; } } } CookiePersistentStorage::OpenDBResult CookiePersistentStorage::Read() { MOZ_ASSERT(NS_GetCurrentThread() == mThread); // Read in the data synchronously. // see IDX_NAME, etc. for parameter indexes nsCOMPtr stmt; nsresult rv = mSyncConn->CreateStatement(nsLiteralCString("SELECT " "name, " "value, " "host, " "path, " "expiry, " "lastAccessed, " "creationTime, " "isSecure, " "isHttpOnly, " "originAttributes, " "sameSite, " "rawSameSite, " "schemeMap, " "isPartitionedAttributeSet " "FROM moz_cookies"), getter_AddRefs(stmt)); NS_ENSURE_SUCCESS(rv, RESULT_RETRY); if (NS_WARN_IF(!mReadArray.IsEmpty())) { mReadArray.Clear(); } mReadArray.SetCapacity(kMaxNumberOfCookies); nsCString baseDomain; nsCString name; nsCString value; nsCString host; nsCString path; bool hasResult; while (true) { rv = stmt->ExecuteStep(&hasResult); if (NS_WARN_IF(NS_FAILED(rv))) { mReadArray.Clear(); return RESULT_RETRY; } if (!hasResult) { break; } stmt->GetUTF8String(IDX_HOST, host); rv = CookieCommons::GetBaseDomainFromHost(mTLDService, host, baseDomain); if (NS_FAILED(rv)) { COOKIE_LOGSTRING(LogLevel::Debug, ("Read(): Ignoring invalid host '%s'", host.get())); continue; } nsAutoCString suffix; OriginAttributes attrs; stmt->GetUTF8String(IDX_ORIGIN_ATTRIBUTES, suffix); // If PopulateFromSuffix failed we just ignore the OA attributes // that we don't support Unused << attrs.PopulateFromSuffix(suffix); CookieKey key(baseDomain, attrs); CookieDomainTuple* tuple = mReadArray.AppendElement(); tuple->key = std::move(key); tuple->originAttributes = attrs; tuple->cookie = GetCookieFromRow(stmt); } COOKIE_LOGSTRING(LogLevel::Debug, ("Read(): %zu cookies read", mReadArray.Length())); return RESULT_OK; } // Extract data from a single result row and create an Cookie. UniquePtr CookiePersistentStorage::GetCookieFromRow( mozIStorageStatement* aRow) { nsCString name; nsCString value; nsCString host; nsCString path; DebugOnly rv = aRow->GetUTF8String(IDX_NAME, name); MOZ_ASSERT(NS_SUCCEEDED(rv)); rv = aRow->GetUTF8String(IDX_VALUE, value); MOZ_ASSERT(NS_SUCCEEDED(rv)); rv = aRow->GetUTF8String(IDX_HOST, host); MOZ_ASSERT(NS_SUCCEEDED(rv)); rv = aRow->GetUTF8String(IDX_PATH, path); MOZ_ASSERT(NS_SUCCEEDED(rv)); int64_t expiry = aRow->AsInt64(IDX_EXPIRY); int64_t lastAccessed = aRow->AsInt64(IDX_LAST_ACCESSED); int64_t creationTime = aRow->AsInt64(IDX_CREATION_TIME); bool isSecure = 0 != aRow->AsInt32(IDX_SECURE); bool isHttpOnly = 0 != aRow->AsInt32(IDX_HTTPONLY); int32_t sameSite = aRow->AsInt32(IDX_SAME_SITE); int32_t rawSameSite = aRow->AsInt32(IDX_RAW_SAME_SITE); int32_t schemeMap = aRow->AsInt32(IDX_SCHEME_MAP); bool isPartitionedAttributeSet = 0 != aRow->AsInt32(IDX_PARTITIONED_ATTRIBUTE_SET); // Create a new constCookie and assign the data. return MakeUnique( name, value, host, path, expiry, lastAccessed, creationTime, isHttpOnly, false, isSecure, isPartitionedAttributeSet, sameSite, rawSameSite, static_cast(schemeMap)); } void CookiePersistentStorage::EnsureInitialized() { MOZ_ASSERT(NS_IsMainThread()); bool isAccumulated = false; if (!mInitialized) { TimeStamp startBlockTime = TimeStamp::Now(); MonitorAutoLock lock(mMonitor); while (!mInitialized) { mMonitor.Wait(); } Telemetry::AccumulateTimeDelta( Telemetry::MOZ_SQLITE_COOKIES_BLOCK_MAIN_THREAD_MS_V2, startBlockTime); Telemetry::Accumulate( Telemetry::MOZ_SQLITE_COOKIES_TIME_TO_BLOCK_MAIN_THREAD_MS, 0); isAccumulated = true; } else if (!mEndInitDBConn.IsNull()) { // We didn't block main thread, and here comes the first cookie request. // Collect how close we're going to block main thread. Telemetry::Accumulate( Telemetry::MOZ_SQLITE_COOKIES_TIME_TO_BLOCK_MAIN_THREAD_MS, (TimeStamp::Now() - mEndInitDBConn).ToMilliseconds()); // Nullify the timestamp so wo don't accumulate this telemetry probe again. mEndInitDBConn = TimeStamp(); isAccumulated = true; } else if (!mInitializedDBConn) { // A request comes while we finished cookie thread task and InitDBConn is // on the way from cookie thread to main thread. We're very close to block // main thread. Telemetry::Accumulate( Telemetry::MOZ_SQLITE_COOKIES_TIME_TO_BLOCK_MAIN_THREAD_MS, 0); isAccumulated = true; } if (!mInitializedDBConn) { InitDBConn(); if (isAccumulated) { // Nullify the timestamp so wo don't accumulate this telemetry probe // again. mEndInitDBConn = TimeStamp(); } } } void CookiePersistentStorage::InitDBConn() { MOZ_ASSERT(NS_IsMainThread()); // We should skip InitDBConn if we close profile during initializing // CookieStorages and then InitDBConn is called after we close the // CookieStorages. if (!mInitialized || mInitializedDBConn) { return; } nsCOMPtr dummyUri; nsresult rv = NS_NewURI(getter_AddRefs(dummyUri), "https://example.com"); MOZ_ASSERT(NS_SUCCEEDED(rv)); nsTArray> cleanupCookies; for (uint32_t i = 0; i < mReadArray.Length(); ++i) { CookieDomainTuple& tuple = mReadArray[i]; MOZ_ASSERT(!tuple.cookie->isSession()); // filter invalid non-ipv4 host ending in number from old db values nsCOMPtr outMut; nsCOMPtr dummyMut; rv = dummyUri->Mutate(getter_AddRefs(dummyMut)); MOZ_ASSERT(NS_SUCCEEDED(rv)); rv = dummyMut->SetHost(tuple.cookie->host(), getter_AddRefs(outMut)); if (NS_FAILED(rv)) { COOKIE_LOGSTRING(LogLevel::Debug, ("Removing cookie from db with " "newly invalid hostname: '%s'", tuple.cookie->host().get())); RefPtr cookie = Cookie::Create(*tuple.cookie, tuple.originAttributes); cleanupCookies.AppendElement(cookie); continue; } // CreateValidated fixes up the creation and lastAccessed times. // If the DB is corrupted and the timestaps are far away in the future // we don't want the creation timestamp to update gLastCreationTime // as that would contaminate all the next creation times. // We fix up these dates to not be later than the current time. // The downside is that if the user sets the date far away in the past // then back to the current date, those cookies will be stale, // but if we don't fix their dates, those cookies might never be // evicted. RefPtr cookie = Cookie::CreateValidated(*tuple.cookie, tuple.originAttributes); AddCookieToList(tuple.key.mBaseDomain, tuple.key.mOriginAttributes, cookie); } if (NS_FAILED(InitDBConnInternal())) { COOKIE_LOGSTRING(LogLevel::Warning, ("InitDBConn(): retrying InitDBConnInternal()")); CleanupCachedStatements(); CleanupDBConnection(); if (NS_FAILED(InitDBConnInternal())) { COOKIE_LOGSTRING( LogLevel::Warning, ("InitDBConn(): InitDBConnInternal() failed, closing connection")); // Game over, clean the connections. CleanupCachedStatements(); CleanupDBConnection(); } } mInitializedDBConn = true; COOKIE_LOGSTRING(LogLevel::Debug, ("InitDBConn(): mInitializedDBConn = true")); mEndInitDBConn = TimeStamp::Now(); for (const auto& cookie : cleanupCookies) { RemoveCookieFromDB(*cookie); } nsCOMPtr os = services::GetObserverService(); if (os) { os->NotifyObservers(nullptr, "cookie-db-read", nullptr); mReadArray.Clear(); } } nsresult CookiePersistentStorage::InitDBConnInternal() { MOZ_ASSERT(NS_IsMainThread()); nsresult rv = mStorageService->OpenUnsharedDatabase( mCookieFile, mozIStorageService::CONNECTION_DEFAULT, getter_AddRefs(mDBConn)); NS_ENSURE_SUCCESS(rv, rv); // Set up our listeners. mInsertListener = new InsertCookieDBListener(this); mUpdateListener = new UpdateCookieDBListener(this); mRemoveListener = new RemoveCookieDBListener(this); mCloseListener = new CloseCookieDBListener(this); // Grow cookie db in 512KB increments mDBConn->SetGrowthIncrement(512 * 1024, ""_ns); // make operations on the table asynchronous, for performance mDBConn->ExecuteSimpleSQL("PRAGMA synchronous = OFF"_ns); // Use write-ahead-logging for performance. We cap the autocheckpoint limit at // 16 pages (around 500KB). mDBConn->ExecuteSimpleSQL(nsLiteralCString(MOZ_STORAGE_UNIQUIFY_QUERY_STR "PRAGMA journal_mode = WAL")); mDBConn->ExecuteSimpleSQL("PRAGMA wal_autocheckpoint = 16"_ns); // cache frequently used statements (for insertion, deletion, and updating) rv = mDBConn->CreateAsyncStatement( nsLiteralCString("INSERT INTO moz_cookies (" "originAttributes, " "name, " "value, " "host, " "path, " "expiry, " "lastAccessed, " "creationTime, " "isSecure, " "isHttpOnly, " "sameSite, " "rawSameSite, " "schemeMap, " "isPartitionedAttributeSet " ") VALUES (" ":originAttributes, " ":name, " ":value, " ":host, " ":path, " ":expiry, " ":lastAccessed, " ":creationTime, " ":isSecure, " ":isHttpOnly, " ":sameSite, " ":rawSameSite, " ":schemeMap, " ":isPartitionedAttributeSet " ")"), getter_AddRefs(mStmtInsert)); NS_ENSURE_SUCCESS(rv, rv); rv = mDBConn->CreateAsyncStatement( nsLiteralCString("DELETE FROM moz_cookies " "WHERE name = :name AND host = :host AND path = :path " "AND originAttributes = :originAttributes"), getter_AddRefs(mStmtDelete)); NS_ENSURE_SUCCESS(rv, rv); rv = mDBConn->CreateAsyncStatement( nsLiteralCString("UPDATE moz_cookies SET lastAccessed = :lastAccessed " "WHERE name = :name AND host = :host AND path = :path " "AND originAttributes = :originAttributes"), getter_AddRefs(mStmtUpdate)); return rv; } // Sets the schema version and creates the moz_cookies table. nsresult CookiePersistentStorage::CreateTableWorker(const char* aName) { // Create the table. // We default originAttributes to empty string: this is so if users revert to // an older Firefox version that doesn't know about this field, any cookies // set will still work once they upgrade back. nsAutoCString command("CREATE TABLE "); command.Append(aName); command.AppendLiteral( " (" "id INTEGER PRIMARY KEY, " "originAttributes TEXT NOT NULL DEFAULT '', " "name TEXT, " "value TEXT, " "host TEXT, " "path TEXT, " "expiry INTEGER, " "lastAccessed INTEGER, " "creationTime INTEGER, " "isSecure INTEGER, " "isHttpOnly INTEGER, " "inBrowserElement INTEGER DEFAULT 0, " "sameSite INTEGER DEFAULT 0, " "rawSameSite INTEGER DEFAULT 0, " "schemeMap INTEGER DEFAULT 0, " "isPartitionedAttributeSet INTEGER DEFAULT 0, " "CONSTRAINT moz_uniqueid UNIQUE (name, host, path, originAttributes)" ")"); return mSyncConn->ExecuteSimpleSQL(command); } // Sets the schema version and creates the moz_cookies table. nsresult CookiePersistentStorage::CreateTable() { // Set the schema version, before creating the table. nsresult rv = mSyncConn->SetSchemaVersion(COOKIES_SCHEMA_VERSION); if (NS_FAILED(rv)) { return rv; } rv = CreateTableWorker("moz_cookies"); if (NS_FAILED(rv)) { return rv; } return NS_OK; } // Sets the schema version and creates the moz_cookies table. nsresult CookiePersistentStorage::CreateTableForSchemaVersion6() { // Set the schema version, before creating the table. nsresult rv = mSyncConn->SetSchemaVersion(6); if (NS_FAILED(rv)) { return rv; } // Create the table. // We default originAttributes to empty string: this is so if users revert to // an older Firefox version that doesn't know about this field, any cookies // set will still work once they upgrade back. rv = mSyncConn->ExecuteSimpleSQL(nsLiteralCString( "CREATE TABLE moz_cookies (" "id INTEGER PRIMARY KEY, " "baseDomain TEXT, " "originAttributes TEXT NOT NULL DEFAULT '', " "name TEXT, " "value TEXT, " "host TEXT, " "path TEXT, " "expiry INTEGER, " "lastAccessed INTEGER, " "creationTime INTEGER, " "isSecure INTEGER, " "isHttpOnly INTEGER, " "CONSTRAINT moz_uniqueid UNIQUE (name, host, path, originAttributes)" ")")); if (NS_FAILED(rv)) { return rv; } // Create an index on baseDomain. return mSyncConn->ExecuteSimpleSQL(nsLiteralCString( "CREATE INDEX moz_basedomain ON moz_cookies (baseDomain, " "originAttributes)")); } // Sets the schema version and creates the moz_cookies table. nsresult CookiePersistentStorage::CreateTableForSchemaVersion5() { // Set the schema version, before creating the table. nsresult rv = mSyncConn->SetSchemaVersion(5); if (NS_FAILED(rv)) { return rv; } // Create the table. We default appId/inBrowserElement to 0: this is so if // users revert to an older Firefox version that doesn't know about these // fields, any cookies set will still work once they upgrade back. rv = mSyncConn->ExecuteSimpleSQL( nsLiteralCString("CREATE TABLE moz_cookies (" "id INTEGER PRIMARY KEY, " "baseDomain TEXT, " "appId INTEGER DEFAULT 0, " "inBrowserElement INTEGER DEFAULT 0, " "name TEXT, " "value TEXT, " "host TEXT, " "path TEXT, " "expiry INTEGER, " "lastAccessed INTEGER, " "creationTime INTEGER, " "isSecure INTEGER, " "isHttpOnly INTEGER, " "CONSTRAINT moz_uniqueid UNIQUE (name, host, path, " "appId, inBrowserElement)" ")")); if (NS_FAILED(rv)) { return rv; } // Create an index on baseDomain. return mSyncConn->ExecuteSimpleSQL(nsLiteralCString( "CREATE INDEX moz_basedomain ON moz_cookies (baseDomain, " "appId, " "inBrowserElement)")); } nsresult CookiePersistentStorage::RunInTransaction( nsICookieTransactionCallback* aCallback) { if (NS_WARN_IF(!mDBConn)) { return NS_ERROR_NOT_AVAILABLE; } mozStorageTransaction transaction(mDBConn, true); // XXX Handle the error, bug 1696130. Unused << NS_WARN_IF(NS_FAILED(transaction.Start())); if (NS_FAILED(aCallback->Callback())) { Unused << transaction.Rollback(); return NS_ERROR_FAILURE; } return NS_OK; } // purges expired and old cookies in a batch operation. already_AddRefed CookiePersistentStorage::PurgeCookies( int64_t aCurrentTimeInUsec, uint16_t aMaxNumberOfCookies, int64_t aCookiePurgeAge) { // Create a params array to batch the removals. This is OK here because // all the removals are in order, and there are no interleaved additions. nsCOMPtr paramsArray; if (mDBConn) { mStmtDelete->NewBindingParamsArray(getter_AddRefs(paramsArray)); } RefPtr self = this; return PurgeCookiesWithCallbacks( aCurrentTimeInUsec, aMaxNumberOfCookies, aCookiePurgeAge, [paramsArray, self](const CookieListIter& aIter) { self->PrepareCookieRemoval(*aIter.Cookie(), paramsArray); self->RemoveCookieFromListInternal(aIter); }, [paramsArray, self]() { if (paramsArray) { self->DeleteFromDB(paramsArray); } }); } void CookiePersistentStorage::CollectCookieJarSizeData() { COOKIE_LOGSTRING(LogLevel::Debug, ("CookiePersistentStorage::CollectCookieJarSizeData")); uint32_t sumPartitioned = 0; uint32_t sumUnpartitioned = 0; for (const auto& cookieEntry : mHostTable) { if (cookieEntry.IsPartitioned()) { uint16_t cePartitioned = cookieEntry.GetCookies().Length(); sumPartitioned += cePartitioned; mozilla::glean::networking::cookie_count_part_by_key .AccumulateSingleSample(cePartitioned); } else { uint16_t ceUnpartitioned = cookieEntry.GetCookies().Length(); sumUnpartitioned += ceUnpartitioned; mozilla::glean::networking::cookie_count_unpart_by_key .AccumulateSingleSample(ceUnpartitioned); } } mozilla::glean::networking::cookie_count_total.AccumulateSingleSample( mCookieCount); mozilla::glean::networking::cookie_count_partitioned.AccumulateSingleSample( sumPartitioned); mozilla::glean::networking::cookie_count_unpartitioned.AccumulateSingleSample( sumUnpartitioned); } } // namespace net } // namespace mozilla