From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- .../BounceTrackingProtection.cpp | 596 +++++++++++++++ .../BounceTrackingProtection.h | 84 ++ .../BounceTrackingProtectionStorage.cpp | 841 +++++++++++++++++++++ .../BounceTrackingProtectionStorage.h | 222 ++++++ .../BounceTrackingRecord.cpp | 76 ++ .../BounceTrackingRecord.h | 72 ++ .../BounceTrackingState.cpp | 612 +++++++++++++++ .../bouncetrackingprotection/BounceTrackingState.h | 156 ++++ .../BounceTrackingStateGlobal.cpp | 190 +++++ .../BounceTrackingStateGlobal.h | 110 +++ .../BounceTrackingStorageObserver.cpp | 107 +++ .../BounceTrackingStorageObserver.h | 28 + .../bouncetrackingprotection/components.conf | 19 + .../bouncetrackingprotection/moz.build | 51 ++ .../nsIBounceTrackingProtection.idl | 46 ++ .../test/browser/browser.toml | 20 + .../browser/browser_bouncetracking_oa_isolation.js | 73 ++ .../test/browser/browser_bouncetracking_purge.js | 121 +++ .../test/browser/browser_bouncetracking_simple.js | 89 +++ .../browser/browser_bouncetracking_stateful.js | 63 ++ .../test/browser/file_bounce.html | 59 ++ .../test/browser/file_bounce.sjs | 19 + .../test/browser/file_start.html | 11 + .../bouncetrackingprotection/test/browser/head.js | 275 +++++++ .../test/marionette/manifest.toml | 7 + .../test_bouncetracking_storage_persistence.py | 133 ++++ .../test/xpcshell/test_bouncetracking_purge.js | 307 ++++++++ .../test/xpcshell/xpcshell.toml | 8 + 28 files changed, 4395 insertions(+) create mode 100644 toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtection.cpp create mode 100644 toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtection.h create mode 100644 toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtectionStorage.cpp create mode 100644 toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtectionStorage.h create mode 100644 toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingRecord.cpp create mode 100644 toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingRecord.h create mode 100644 toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingState.cpp create mode 100644 toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingState.h create mode 100644 toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStateGlobal.cpp create mode 100644 toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStateGlobal.h create mode 100644 toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStorageObserver.cpp create mode 100644 toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStorageObserver.h create mode 100644 toolkit/components/antitracking/bouncetrackingprotection/components.conf create mode 100644 toolkit/components/antitracking/bouncetrackingprotection/moz.build create mode 100644 toolkit/components/antitracking/bouncetrackingprotection/nsIBounceTrackingProtection.idl create mode 100644 toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser.toml create mode 100644 toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_oa_isolation.js create mode 100644 toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_purge.js create mode 100644 toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_simple.js create mode 100644 toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful.js create mode 100644 toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.html create mode 100644 toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.sjs create mode 100644 toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_start.html create mode 100644 toolkit/components/antitracking/bouncetrackingprotection/test/browser/head.js create mode 100644 toolkit/components/antitracking/bouncetrackingprotection/test/marionette/manifest.toml create mode 100644 toolkit/components/antitracking/bouncetrackingprotection/test/marionette/test_bouncetracking_storage_persistence.py create mode 100644 toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_purge.js create mode 100644 toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/xpcshell.toml (limited to 'toolkit/components/antitracking/bouncetrackingprotection') diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtection.cpp b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtection.cpp new file mode 100644 index 0000000000..e5d9ccfea9 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtection.cpp @@ -0,0 +1,596 @@ +/* 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 "BounceTrackingProtection.h" + +#include "BounceTrackingProtectionStorage.h" +#include "BounceTrackingState.h" +#include "BounceTrackingRecord.h" + +#include "BounceTrackingStateGlobal.h" +#include "ErrorList.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/Logging.h" +#include "mozilla/Services.h" +#include "mozilla/StaticPrefs_privacy.h" +#include "mozilla/dom/Promise.h" +#include "nsDebug.h" +#include "nsHashPropertyBag.h" +#include "nsIClearDataService.h" +#include "nsIObserverService.h" +#include "nsIPrincipal.h" +#include "nsISupports.h" +#include "nsServiceManagerUtils.h" +#include "nscore.h" +#include "prtime.h" +#include "mozilla/dom/BrowsingContext.h" +#include "xpcpublic.h" + +#define TEST_OBSERVER_MSG_RECORD_BOUNCES_FINISHED "test-record-bounces-finished" + +namespace mozilla { + +NS_IMPL_ISUPPORTS(BounceTrackingProtection, nsIBounceTrackingProtection); + +LazyLogModule gBounceTrackingProtectionLog("BounceTrackingProtection"); + +static StaticRefPtr sBounceTrackingProtection; + +static constexpr uint32_t TRACKER_PURGE_FLAGS = + nsIClearDataService::CLEAR_ALL_CACHES | nsIClearDataService::CLEAR_COOKIES | + nsIClearDataService::CLEAR_DOM_STORAGES | + nsIClearDataService::CLEAR_CLIENT_AUTH_REMEMBER_SERVICE | + nsIClearDataService::CLEAR_EME | nsIClearDataService::CLEAR_MEDIA_DEVICES | + nsIClearDataService::CLEAR_STORAGE_ACCESS | + nsIClearDataService::CLEAR_AUTH_TOKENS | + nsIClearDataService::CLEAR_AUTH_CACHE; + +// static +already_AddRefed +BounceTrackingProtection::GetSingleton() { + MOZ_ASSERT(XRE_IsParentProcess()); + + if (!StaticPrefs::privacy_bounceTrackingProtection_enabled_AtStartup()) { + return nullptr; + } + + if (!sBounceTrackingProtection) { + sBounceTrackingProtection = new BounceTrackingProtection(); + + RunOnShutdown([] { sBounceTrackingProtection = nullptr; }); + } + + return do_AddRef(sBounceTrackingProtection); +} + +BounceTrackingProtection::BounceTrackingProtection() { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, ("constructor")); + + mStorage = new BounceTrackingProtectionStorage(); + + nsresult rv = mStorage->Init(); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Error, + ("storage init failed")); + return; + } + + // Schedule timer for tracker purging. The timer interval is determined by + // pref. + uint32_t purgeTimerPeriod = StaticPrefs:: + privacy_bounceTrackingProtection_bounceTrackingPurgeTimerPeriodSec(); + + // The pref can be set to 0 to disable interval purging. + if (purgeTimerPeriod == 0) { + return; + } + + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("Scheduling mBounceTrackingPurgeTimer. Interval: %d seconds.", + purgeTimerPeriod)); + + rv = NS_NewTimerWithCallback( + getter_AddRefs(mBounceTrackingPurgeTimer), + [](auto) { + if (!sBounceTrackingProtection) { + return; + } + sBounceTrackingProtection->PurgeBounceTrackers()->Then( + GetMainThreadSerialEventTarget(), __func__, + [] { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: PurgeBounceTrackers finished after timer call.", + __FUNCTION__)); + }, + [] { NS_WARNING("RunPurgeBounceTrackers failed"); }); + }, + purgeTimerPeriod * PR_MSEC_PER_SEC, nsITimer::TYPE_REPEATING_SLACK, + "mBounceTrackingPurgeTimer"); + + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "Failed to schedule timer for RunPurgeBounceTrackers."); +} + +nsresult BounceTrackingProtection::RecordStatefulBounces( + BounceTrackingState* aBounceTrackingState) { + NS_ENSURE_ARG_POINTER(aBounceTrackingState); + + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: aBounceTrackingState: %s", __FUNCTION__, + aBounceTrackingState->Describe().get())); + + // Assert: navigable’s bounce tracking record is not null. + BounceTrackingRecord* record = + aBounceTrackingState->GetBounceTrackingRecord(); + NS_ENSURE_TRUE(record, NS_ERROR_FAILURE); + + // Get the bounce tracker map and the user activation map. + RefPtr globalState = + mStorage->GetOrCreateStateGlobal(aBounceTrackingState); + MOZ_ASSERT(globalState); + + // For each host in navigable’s bounce tracking record's bounce set: + for (const nsACString& host : record->GetBounceHosts()) { + // If host equals navigable’s bounce tracking record's initial host, + // continue. + if (host == record->GetInitialHost()) { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: Skip host == initialHost: %s", __FUNCTION__, + PromiseFlatCString(host).get())); + continue; + } + // If host equals navigable’s bounce tracking record's final host, continue. + if (host == record->GetFinalHost()) { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: Skip host == finalHost: %s", __FUNCTION__, + PromiseFlatCString(host).get())); + continue; + } + + // If user activation map contains host, continue. + if (globalState->HasUserActivation(host)) { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: Skip host with recent user activation: %s", __FUNCTION__, + PromiseFlatCString(host).get())); + continue; + } + + // If stateful bounce tracking map contains host, continue. + if (globalState->HasBounceTracker(host)) { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: Skip already existing host: %s", __FUNCTION__, + PromiseFlatCString(host).get())); + continue; + } + + // If navigable’s bounce tracking record's storage access set does not + // contain host, continue. + if (StaticPrefs:: + privacy_bounceTrackingProtection_requireStatefulBounces() && + !record->GetStorageAccessHosts().Contains(host)) { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: Skip host without storage access: %s", __FUNCTION__, + PromiseFlatCString(host).get())); + continue; + } + + // Set stateful bounce tracking map[host] to topDocument’s relevant settings + // object's current wall time. + PRTime now = PR_Now(); + MOZ_ASSERT(!globalState->HasBounceTracker(host)); + nsresult rv = globalState->RecordBounceTracker(host, now); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Info, + ("%s: Added candidate to mBounceTrackers: %s, Time: %" PRIu64, + __FUNCTION__, PromiseFlatCString(host).get(), + static_cast(now))); + } + + // Set navigable’s bounce tracking record to null. + aBounceTrackingState->ResetBounceTrackingRecord(); + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: Done, reset aBounceTrackingState: %s", __FUNCTION__, + aBounceTrackingState->Describe().get())); + + // If running in test automation, dispatch an observer message indicating + // we're finished recording bounces. + if (StaticPrefs::privacy_bounceTrackingProtection_enableTestMode()) { + nsCOMPtr obsSvc = + mozilla::services::GetObserverService(); + NS_ENSURE_TRUE(obsSvc, NS_ERROR_FAILURE); + + RefPtr props = new nsHashPropertyBag(); + + nsresult rv = props->SetPropertyAsUint64( + u"browserId"_ns, aBounceTrackingState->GetBrowserId()); + NS_ENSURE_SUCCESS(rv, rv); + + rv = obsSvc->NotifyObservers( + ToSupports(props), TEST_OBSERVER_MSG_RECORD_BOUNCES_FINISHED, nullptr); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +nsresult BounceTrackingProtection::RecordUserActivation( + nsIPrincipal* aPrincipal) { + MOZ_ASSERT(XRE_IsParentProcess()); + + NS_ENSURE_ARG_POINTER(aPrincipal); + NS_ENSURE_TRUE(aPrincipal->GetIsContentPrincipal(), NS_ERROR_FAILURE); + + nsAutoCString siteHost; + nsresult rv = aPrincipal->GetBaseDomain(siteHost); + NS_ENSURE_SUCCESS(rv, rv); + + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Info, + ("%s: siteHost: %s", __FUNCTION__, siteHost.get())); + + RefPtr globalState = + mStorage->GetOrCreateStateGlobal(aPrincipal); + MOZ_ASSERT(globalState); + + return globalState->RecordUserActivation(siteHost, PR_Now()); +} + +NS_IMETHODIMP +BounceTrackingProtection::TestGetBounceTrackerCandidateHosts( + JS::Handle aOriginAttributes, JSContext* aCx, + nsTArray& aCandidates) { + MOZ_ASSERT(aCx); + + OriginAttributes oa; + if (!aOriginAttributes.isObject() || !oa.Init(aCx, aOriginAttributes)) { + return NS_ERROR_INVALID_ARG; + } + + BounceTrackingStateGlobal* globalState = mStorage->GetOrCreateStateGlobal(oa); + MOZ_ASSERT(globalState); + + for (const nsACString& host : globalState->BounceTrackersMapRef().Keys()) { + aCandidates.AppendElement(host); + } + + return NS_OK; +} + +NS_IMETHODIMP +BounceTrackingProtection::TestGetUserActivationHosts( + JS::Handle aOriginAttributes, JSContext* aCx, + nsTArray& aHosts) { + MOZ_ASSERT(aCx); + + OriginAttributes oa; + if (!aOriginAttributes.isObject() || !oa.Init(aCx, aOriginAttributes)) { + return NS_ERROR_INVALID_ARG; + } + + BounceTrackingStateGlobal* globalState = mStorage->GetOrCreateStateGlobal(oa); + MOZ_ASSERT(globalState); + + for (const nsACString& host : globalState->UserActivationMapRef().Keys()) { + aHosts.AppendElement(host); + } + + return NS_OK; +} + +NS_IMETHODIMP +BounceTrackingProtection::ClearAll() { + BounceTrackingState::ResetAll(); + return mStorage->Clear(); +} + +NS_IMETHODIMP +BounceTrackingProtection::ClearBySiteHostAndOA( + const nsACString& aSiteHost, JS::Handle aOriginAttributes, + JSContext* aCx) { + NS_ENSURE_ARG_POINTER(aCx); + + OriginAttributes originAttributes; + if (!aOriginAttributes.isObject() || + !originAttributes.Init(aCx, aOriginAttributes)) { + return NS_ERROR_INVALID_ARG; + } + + // Reset per tab state for tabs matching the given OriginAttributes. + BounceTrackingState::ResetAllForOriginAttributes(originAttributes); + + return mStorage->ClearBySiteHost(aSiteHost, &originAttributes); +} + +NS_IMETHODIMP +BounceTrackingProtection::ClearBySiteHost(const nsACString& aSiteHost) { + BounceTrackingState::ResetAll(); + + return mStorage->ClearBySiteHost(aSiteHost, nullptr); +} + +NS_IMETHODIMP +BounceTrackingProtection::ClearByTimeRange(PRTime aFrom, PRTime aTo) { + NS_ENSURE_TRUE(aFrom >= 0, NS_ERROR_INVALID_ARG); + NS_ENSURE_TRUE(aFrom < aTo, NS_ERROR_INVALID_ARG); + + // Clear all BounceTrackingState, we don't keep track of time ranges. + BounceTrackingState::ResetAll(); + + return mStorage->ClearByTimeRange(aFrom, aTo); +} + +NS_IMETHODIMP +BounceTrackingProtection::ClearByOriginAttributesPattern( + const nsAString& aPattern) { + OriginAttributesPattern pattern; + if (!pattern.Init(aPattern)) { + return NS_ERROR_INVALID_ARG; + } + + // Reset all per-tab state matching the given OriginAttributesPattern. + BounceTrackingState::ResetAllForOriginAttributesPattern(pattern); + + return mStorage->ClearByOriginAttributesPattern(pattern); +} + +NS_IMETHODIMP +BounceTrackingProtection::TestRunPurgeBounceTrackers( + JSContext* aCx, mozilla::dom::Promise** aPromise) { + NS_ENSURE_ARG_POINTER(aCx); + NS_ENSURE_ARG_POINTER(aPromise); + + nsIGlobalObject* globalObject = xpc::CurrentNativeGlobal(aCx); + if (!globalObject) { + return NS_ERROR_UNEXPECTED; + } + + ErrorResult result; + RefPtr promise = dom::Promise::Create(globalObject, result); + if (result.Failed()) { + return result.StealNSResult(); + } + + // PurgeBounceTrackers returns a MozPromise, wrap it in a dom::Promise + // required for XPCOM. + PurgeBounceTrackers()->Then( + GetMainThreadSerialEventTarget(), __func__, + [promise](const PurgeBounceTrackersMozPromise::ResolveValueType& + purgedSiteHosts) { + promise->MaybeResolve(purgedSiteHosts); + }, + [promise] { promise->MaybeRejectWithUndefined(); }); + + promise.forget(aPromise); + return NS_OK; +} + +NS_IMETHODIMP +BounceTrackingProtection::TestAddBounceTrackerCandidate( + JS::Handle aOriginAttributes, const nsACString& aHost, + const PRTime aBounceTime, JSContext* aCx) { + MOZ_ASSERT(aCx); + + OriginAttributes oa; + if (!aOriginAttributes.isObject() || !oa.Init(aCx, aOriginAttributes)) { + return NS_ERROR_INVALID_ARG; + } + + BounceTrackingStateGlobal* stateGlobal = mStorage->GetOrCreateStateGlobal(oa); + MOZ_ASSERT(stateGlobal); + + // Ensure aHost is lowercase to match nsIURI and nsIPrincipal. + nsAutoCString host(aHost); + ToLowerCase(host); + + // Can not have a host in both maps. + nsresult rv = stateGlobal->TestRemoveUserActivation(host); + NS_ENSURE_SUCCESS(rv, rv); + return stateGlobal->RecordBounceTracker(host, aBounceTime); +} + +NS_IMETHODIMP +BounceTrackingProtection::TestAddUserActivation( + JS::Handle aOriginAttributes, const nsACString& aHost, + const PRTime aActivationTime, JSContext* aCx) { + MOZ_ASSERT(aCx); + + OriginAttributes oa; + if (!aOriginAttributes.isObject() || !oa.Init(aCx, aOriginAttributes)) { + return NS_ERROR_INVALID_ARG; + } + + BounceTrackingStateGlobal* stateGlobal = mStorage->GetOrCreateStateGlobal(oa); + MOZ_ASSERT(stateGlobal); + + // Ensure aHost is lowercase to match nsIURI and nsIPrincipal. + nsAutoCString host(aHost); + ToLowerCase(host); + + return stateGlobal->RecordUserActivation(host, aActivationTime); +} + +RefPtr +BounceTrackingProtection::PurgeBounceTrackers() { + // Run the purging algorithm for all global state objects. + for (const auto& entry : mStorage->StateGlobalMapRef()) { + const OriginAttributes& originAttributes = entry.GetKey(); + BounceTrackingStateGlobal* stateGlobal = entry.GetData(); + MOZ_ASSERT(stateGlobal); + + if (MOZ_LOG_TEST(gBounceTrackingProtectionLog, LogLevel::Debug)) { + nsAutoCString oaSuffix; + originAttributes.CreateSuffix(oaSuffix); + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: Running purge algorithm for OA: '%s'", __FUNCTION__, + oaSuffix.get())); + } + + PurgeBounceTrackersForStateGlobal(stateGlobal, originAttributes); + } + + // Wait for all data clearing operations to complete. mClearPromises contains + // one promise per host / clear task. + return ClearDataMozPromise::AllSettled(GetCurrentSerialEventTarget(), + mClearPromises) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [&](ClearDataMozPromise::AllSettledPromiseType::ResolveOrRejectValue&& + aResults) { + MOZ_ASSERT(aResults.IsResolve(), "AllSettled never rejects"); + + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Info, + ("%s: Done. Cleared %zu hosts.", __FUNCTION__, + aResults.ResolveValue().Length())); + + nsTArray purgedSiteHosts; + // If any clear call failed reject. + for (auto& result : aResults.ResolveValue()) { + if (result.IsReject()) { + mClearPromises.Clear(); + return PurgeBounceTrackersMozPromise::CreateAndReject( + NS_ERROR_FAILURE, __func__); + } + purgedSiteHosts.AppendElement(result.ResolveValue()); + } + + // No clearing errors, resolve. + mClearPromises.Clear(); + return PurgeBounceTrackersMozPromise::CreateAndResolve( + std::move(purgedSiteHosts), __func__); + }); +} + +nsresult BounceTrackingProtection::PurgeBounceTrackersForStateGlobal( + BounceTrackingStateGlobal* aStateGlobal, + const OriginAttributes& aOriginAttributes) { + MOZ_ASSERT(aStateGlobal); + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: #mUserActivation: %d, #mBounceTrackers: %d", __FUNCTION__, + aStateGlobal->UserActivationMapRef().Count(), + aStateGlobal->BounceTrackersMapRef().Count())); + + // Purge already in progress. + if (!mClearPromises.IsEmpty()) { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: Skip: Purge already in progress.", __FUNCTION__)); + return NS_ERROR_NOT_AVAILABLE; + } + + const PRTime now = PR_Now(); + // Convert the user activation lifetime into microseconds for calculation with + // PRTime values. The pref is a 32-bit value. Cast into 64-bit before + // multiplying so we get the correct result. + int64_t activationLifetimeUsec = + static_cast( + StaticPrefs:: + privacy_bounceTrackingProtection_bounceTrackingActivationLifetimeSec()) * + PR_USEC_PER_SEC; + + // 1. Remove hosts from the user activation map whose user activation flag has + // expired. + nsresult rv = + aStateGlobal->ClearUserActivationBefore(now - activationLifetimeUsec); + NS_ENSURE_SUCCESS(rv, rv); + + // 2. Go over bounce tracker candidate map and purge state. + rv = NS_OK; + nsCOMPtr clearDataService = + do_GetService("@mozilla.org/clear-data-service;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + mClearPromises.Clear(); + nsTArray purgedSiteHosts; + + // Collect hosts to remove from the bounce trackers map. We can not remove + // them while iterating over the map. + nsTArray bounceTrackerCandidatesToRemove; + + for (auto hostIter = aStateGlobal->BounceTrackersMapRef().ConstIter(); + !hostIter.Done(); hostIter.Next()) { + const nsACString& host = hostIter.Key(); + const PRTime& bounceTime = hostIter.Data(); + + // If bounceTime + bounce tracking grace period is after now, then continue. + // The host is still within the grace period and must not be purged. + if (bounceTime + + StaticPrefs:: + privacy_bounceTrackingProtection_bounceTrackingGracePeriodSec() * + PR_USEC_PER_SEC > + now) { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: Skip host within bounce tracking grace period %s", + __FUNCTION__, PromiseFlatCString(host).get())); + + continue; + } + + // If there is a top-level traversable whose active document's origin's + // site's host equals host, then continue. + // TODO: Bug 1842047: Implement a more accurate check that calls into the + // browser implementations to determine whether the site is currently open + // on the top level. + bool hostIsActive; + rv = BounceTrackingState::HasBounceTrackingStateForSite(host, hostIsActive); + if (NS_WARN_IF(NS_FAILED(rv))) { + hostIsActive = false; + } + if (hostIsActive) { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: Skip host which is active %s", __FUNCTION__, + PromiseFlatCString(host).get())); + continue; + } + + // No exception above applies, clear state for the given host. + + RefPtr clearPromise = + new ClearDataMozPromise::Private(__func__); + RefPtr cb = new ClearDataCallback(clearPromise, host); + + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: Purge state for host: %s", __FUNCTION__, + PromiseFlatCString(host).get())); + + // TODO: Bug 1842067: Clear by site + OA. + rv = clearDataService->DeleteDataFromBaseDomain(host, false, + TRACKER_PURGE_FLAGS, cb); + if (NS_WARN_IF(NS_FAILED(rv))) { + clearPromise->Reject(0, __func__); + } + + mClearPromises.AppendElement(clearPromise); + + // Remove it from the bounce trackers map, it's about to be purged. If the + // clear call fails still remove it. We want to avoid an ever growing list + // of hosts in case of repeated failures. + bounceTrackerCandidatesToRemove.AppendElement(host); + } + + // Remove hosts from the bounce trackers map which we executed purge calls + // for. + return aStateGlobal->RemoveBounceTrackers(bounceTrackerCandidatesToRemove); +} + +// ClearDataCallback + +NS_IMPL_ISUPPORTS(BounceTrackingProtection::ClearDataCallback, + nsIClearDataCallback); + +// nsIClearDataCallback implementation +NS_IMETHODIMP BounceTrackingProtection::ClearDataCallback::OnDataDeleted( + uint32_t aFailedFlags) { + if (aFailedFlags) { + mPromise->Reject(aFailedFlags, __func__); + } else { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Info, + ("%s: Cleared %s", __FUNCTION__, mHost.get())); + mPromise->Resolve(std::move(mHost), __func__); + } + return NS_OK; +} + +} // namespace mozilla diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtection.h b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtection.h new file mode 100644 index 0000000000..98c61504c0 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtection.h @@ -0,0 +1,84 @@ +/* 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_BounceTrackingProtection_h__ +#define mozilla_BounceTrackingProtection_h__ + +#include "mozilla/Logging.h" +#include "mozilla/MozPromise.h" +#include "nsIBounceTrackingProtection.h" +#include "nsIClearDataService.h" + +class nsIPrincipal; +class nsITimer; + +namespace mozilla { + +class BounceTrackingState; +class BounceTrackingStateGlobal; +class BounceTrackingProtectionStorage; +class OriginAttributes; + +extern LazyLogModule gBounceTrackingProtectionLog; + +class BounceTrackingProtection final : public nsIBounceTrackingProtection { + NS_DECL_ISUPPORTS + NS_DECL_NSIBOUNCETRACKINGPROTECTION + + public: + static already_AddRefed GetSingleton(); + + // This algorithm is called when detecting the end of an extended navigation. + // This could happen if a user-initiated navigation is detected in process + // navigation start for bounce tracking, or if the client bounce detection + // timer expires after process response received for bounce tracking without + // observing a client redirect. + nsresult RecordStatefulBounces(BounceTrackingState* aBounceTrackingState); + + // Stores a user activation flag with a timestamp for the given principal. + nsresult RecordUserActivation(nsIPrincipal* aPrincipal); + + private: + BounceTrackingProtection(); + ~BounceTrackingProtection() = default; + + // Timer which periodically runs PurgeBounceTrackers. + nsCOMPtr mBounceTrackingPurgeTimer; + + // Storage for user agent globals. + RefPtr mStorage; + + // Clear state for classified bounce trackers. To be called on an interval. + using PurgeBounceTrackersMozPromise = + MozPromise, nsresult, true>; + RefPtr PurgeBounceTrackers(); + + nsresult PurgeBounceTrackersForStateGlobal( + BounceTrackingStateGlobal* aStateGlobal, + const OriginAttributes& aOriginAttributes); + + // Pending clear operations are stored as ClearDataMozPromise, one per host. + using ClearDataMozPromise = MozPromise; + nsTArray> mClearPromises; + + // Wraps nsIClearDataCallback in MozPromise. + class ClearDataCallback final : public nsIClearDataCallback { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSICLEARDATACALLBACK + + explicit ClearDataCallback(ClearDataMozPromise::Private* aPromise, + const nsACString& aHost) + : mHost(aHost), mPromise(aPromise){}; + + private: + virtual ~ClearDataCallback() { mPromise->Reject(0, __func__); } + + nsCString mHost; + RefPtr mPromise; + }; +}; + +} // namespace mozilla + +#endif diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtectionStorage.cpp b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtectionStorage.cpp new file mode 100644 index 0000000000..bdd2c7dc18 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtectionStorage.cpp @@ -0,0 +1,841 @@ +/* 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 "BounceTrackingProtectionStorage.h" +#include + +#include "BounceTrackingState.h" +#include "BounceTrackingStateGlobal.h" +#include "ErrorList.h" +#include "MainThreadUtils.h" +#include "mozIStorageConnection.h" +#include "mozIStorageService.h" +#include "mozIStorageStatement.h" +#include "mozStorageCID.h" +#include "mozilla/Components.h" +#include "mozilla/Monitor.h" +#include "mozilla/AppShutdown.h" +#include "mozilla/Services.h" +#include "mozilla/ShutdownPhase.h" +#include "nsCOMPtr.h" +#include "nsDirectoryServiceUtils.h" +#include "nsIObserverService.h" +#include "nsIPrincipal.h" +#include "nsIScriptSecurityManager.h" +#include "nsStringFwd.h" +#include "nsVariant.h" +#include "nscore.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsCRT.h" + +#define BOUNCE_TRACKING_PROTECTION_DB_FILENAME \ + "bounce-tracking-protection.sqlite"_ns +#define SCHEMA_VERSION 1 + +namespace mozilla { + +NS_IMPL_ISUPPORTS(BounceTrackingProtectionStorage, nsIAsyncShutdownBlocker, + nsIObserver); + +BounceTrackingStateGlobal* +BounceTrackingProtectionStorage::GetOrCreateStateGlobal( + nsIPrincipal* aPrincipal) { + MOZ_ASSERT(aPrincipal); + return GetOrCreateStateGlobal(aPrincipal->OriginAttributesRef()); +} + +BounceTrackingStateGlobal* +BounceTrackingProtectionStorage::GetOrCreateStateGlobal( + BounceTrackingState* aBounceTrackingState) { + MOZ_ASSERT(aBounceTrackingState); + return GetOrCreateStateGlobal(aBounceTrackingState->OriginAttributesRef()); +} + +BounceTrackingStateGlobal* +BounceTrackingProtectionStorage::GetOrCreateStateGlobal( + const OriginAttributes& aOriginAttributes) { + return mStateGlobal.GetOrInsertNew(aOriginAttributes, this, + aOriginAttributes); +} + +nsresult BounceTrackingProtectionStorage::ClearBySiteHost( + const nsACString& aSiteHost, OriginAttributes* aOriginAttributes) { + NS_ENSURE_TRUE(!aSiteHost.IsEmpty(), NS_ERROR_INVALID_ARG); + + // If OriginAttributes are passed only clear the matching state global. + if (aOriginAttributes) { + RefPtr stateGlobal = + mStateGlobal.Get(*aOriginAttributes); + if (stateGlobal) { + nsresult rv = stateGlobal->ClearSiteHost(aSiteHost, true); + NS_ENSURE_SUCCESS(rv, rv); + } + } else { + // Otherwise we need to clear the host across all state globals. + for (auto iter = mStateGlobal.Iter(); !iter.Done(); iter.Next()) { + BounceTrackingStateGlobal* stateGlobal = iter.Data(); + MOZ_ASSERT(stateGlobal); + // Update in memory state. Skip storage so we can batch the writes later. + nsresult rv = stateGlobal->ClearSiteHost(aSiteHost, true); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + // Update the database. + // Private browsing data is not written to disk. + if (aOriginAttributes && + aOriginAttributes->mPrivateBrowsingId != + nsIScriptSecurityManager::DEFAULT_PRIVATE_BROWSING_ID) { + return NS_OK; + } + return DeleteDBEntries(aOriginAttributes, aSiteHost); +} + +nsresult BounceTrackingProtectionStorage::ClearByTimeRange(PRTime aFrom, + PRTime aTo) { + for (auto iter = mStateGlobal.Iter(); !iter.Done(); iter.Next()) { + BounceTrackingStateGlobal* stateGlobal = iter.Data(); + MOZ_ASSERT(stateGlobal); + // Update in memory state. Skip storage so we can batch the writes later. + nsresult rv = + stateGlobal->ClearByTimeRange(aFrom, Some(aTo), Nothing(), true); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Update the database. + return DeleteDBEntriesInTimeRange(nullptr, aFrom, Some(aTo)); +} + +nsresult BounceTrackingProtectionStorage::ClearByOriginAttributesPattern( + const OriginAttributesPattern& aOriginAttributesPattern) { + // Clear in memory state. + for (auto iter = mStateGlobal.Iter(); !iter.Done(); iter.Next()) { + if (aOriginAttributesPattern.Matches(iter.Key())) { + iter.Remove(); + } + } + + // Update the database. + // Private browsing data is not written to disk. + if (aOriginAttributesPattern.mPrivateBrowsingId.WasPassed() && + aOriginAttributesPattern.mPrivateBrowsingId.Value() != + nsIScriptSecurityManager::DEFAULT_PRIVATE_BROWSING_ID) { + return NS_OK; + } + return DeleteDBEntriesByOriginAttributesPattern(aOriginAttributesPattern); +} + +nsresult BounceTrackingProtectionStorage::UpdateDBEntry( + const OriginAttributes& aOriginAttributes, const nsACString& aSiteHost, + EntryType aEntryType, PRTime aTimeStamp) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!aSiteHost.IsEmpty()); + MOZ_ASSERT(aTimeStamp); + + nsresult rv = WaitForInitialization(); + NS_ENSURE_SUCCESS(rv, rv); + + if (MOZ_LOG_TEST(gBounceTrackingProtectionLog, LogLevel::Debug)) { + nsAutoCString originAttributeSuffix; + aOriginAttributes.CreateSuffix(originAttributeSuffix); + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: originAttributes: %s, siteHost=%s, entryType=%d, " + "timeStamp=%" PRId64, + __FUNCTION__, originAttributeSuffix.get(), + PromiseFlatCString(aSiteHost).get(), + static_cast(aEntryType), aTimeStamp)); + } + + IncrementPendingWrites(); + + RefPtr self = this; + nsCString siteHost(aSiteHost); + + mBackgroundThread->Dispatch( + NS_NewRunnableFunction( + "BounceTrackingProtectionStorage::UpdateEntry", + [self, aOriginAttributes, siteHost, aEntryType, aTimeStamp]() { + nsresult rv = + UpsertData(self->mDatabaseConnection, aOriginAttributes, + siteHost, aEntryType, aTimeStamp); + self->DecrementPendingWrites(); + NS_ENSURE_SUCCESS_VOID(rv); + }), + NS_DISPATCH_EVENT_MAY_BLOCK); + + return NS_OK; +} + +nsresult BounceTrackingProtectionStorage::DeleteDBEntries( + OriginAttributes* aOriginAttributes, const nsACString& aSiteHost) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!aSiteHost.IsEmpty()); + MOZ_ASSERT(!aOriginAttributes || + aOriginAttributes->mPrivateBrowsingId == + nsIScriptSecurityManager::DEFAULT_PRIVATE_BROWSING_ID, + "Must not write private browsing data to the table."); + + nsresult rv = WaitForInitialization(); + NS_ENSURE_SUCCESS(rv, rv); + + if (MOZ_LOG_TEST(gBounceTrackingProtectionLog, LogLevel::Debug)) { + nsAutoCString originAttributeSuffix("*"); + if (aOriginAttributes) { + aOriginAttributes->CreateSuffix(originAttributeSuffix); + } + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: originAttributes: %s, siteHost=%s", __FUNCTION__, + originAttributeSuffix.get(), PromiseFlatCString(aSiteHost).get())); + } + + RefPtr self = this; + nsCString siteHost(aSiteHost); + Maybe originAttributes; + if (aOriginAttributes) { + originAttributes.emplace(*aOriginAttributes); + } + + IncrementPendingWrites(); + mBackgroundThread->Dispatch( + NS_NewRunnableFunction("BounceTrackingProtectionStorage::DeleteEntry", + [self, originAttributes, siteHost]() { + nsresult rv = + DeleteData(self->mDatabaseConnection, + originAttributes, siteHost); + self->DecrementPendingWrites(); + NS_ENSURE_SUCCESS_VOID(rv); + }), + NS_DISPATCH_EVENT_MAY_BLOCK); + + return NS_OK; +} + +nsresult BounceTrackingProtectionStorage::Clear() { + MOZ_ASSERT(NS_IsMainThread()); + // Clear in memory data. + mStateGlobal.Clear(); + + // Clear on disk data. + nsresult rv = WaitForInitialization(); + NS_ENSURE_SUCCESS(rv, rv); + + IncrementPendingWrites(); + RefPtr self = this; + mBackgroundThread->Dispatch( + NS_NewRunnableFunction("BounceTrackingProtectionStorage::Clear", + [self]() { + nsresult rv = + ClearData(self->mDatabaseConnection); + self->DecrementPendingWrites(); + NS_ENSURE_SUCCESS_VOID(rv); + }), + NS_DISPATCH_EVENT_MAY_BLOCK); + return NS_OK; +} + +nsresult BounceTrackingProtectionStorage::DeleteDBEntriesInTimeRange( + OriginAttributes* aOriginAttributes, PRTime aFrom, Maybe aTo, + Maybe aEntryType) { + MOZ_ASSERT(NS_IsMainThread()); + NS_ENSURE_ARG_MIN(aFrom, 0); + NS_ENSURE_TRUE(aTo.isNothing() || aTo.value() > aFrom, NS_ERROR_INVALID_ARG); + + nsresult rv = WaitForInitialization(); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr self = this; + Maybe originAttributes; + if (aOriginAttributes) { + originAttributes.emplace(*aOriginAttributes); + } + + IncrementPendingWrites(); + mBackgroundThread->Dispatch( + NS_NewRunnableFunction( + "BounceTrackingProtectionStorage::DeleteDBEntriesInTimeRange", + [self, originAttributes, aFrom, aTo, aEntryType]() { + nsresult rv = + DeleteDataInTimeRange(self->mDatabaseConnection, + originAttributes, aFrom, aTo, aEntryType); + self->DecrementPendingWrites(); + NS_ENSURE_SUCCESS_VOID(rv); + }), + NS_DISPATCH_EVENT_MAY_BLOCK); + return NS_OK; +} + +nsresult +BounceTrackingProtectionStorage::DeleteDBEntriesByOriginAttributesPattern( + const OriginAttributesPattern& aOriginAttributesPattern) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!aOriginAttributesPattern.mPrivateBrowsingId.WasPassed() || + aOriginAttributesPattern.mPrivateBrowsingId.Value() == + nsIScriptSecurityManager::DEFAULT_PRIVATE_BROWSING_ID, + "Must not clear private browsing data from the table."); + + nsresult rv = WaitForInitialization(); + NS_ENSURE_SUCCESS(rv, rv); + + IncrementPendingWrites(); + RefPtr self = this; + mBackgroundThread->Dispatch( + NS_NewRunnableFunction( + "BounceTrackingProtectionStorage::" + "DeleteEntriesByOriginAttributesPattern", + [self, aOriginAttributesPattern]() { + nsresult rv = DeleteDataByOriginAttributesPattern( + self->mDatabaseConnection, aOriginAttributesPattern); + self->DecrementPendingWrites(); + NS_ENSURE_SUCCESS_VOID(rv); + }), + NS_DISPATCH_EVENT_MAY_BLOCK); + return NS_OK; +} + +// nsIAsyncShutdownBlocker + +NS_IMETHODIMP BounceTrackingProtectionStorage::BlockShutdown( + nsIAsyncShutdownClient* aClient) { + MOZ_ASSERT(NS_IsMainThread()); + nsresult rv = WaitForInitialization(); + NS_ENSURE_SUCCESS(rv, rv); + + MonitorAutoLock lock(mMonitor); + mShuttingDown.Flip(); + + RefPtr self = this; + mBackgroundThread->Dispatch( + NS_NewRunnableFunction( + "BounceTrackingProtectionStorage::BlockShutdown", + [self]() { + MonitorAutoLock lock(self->mMonitor); + + MOZ_ASSERT(self->mPendingWrites == 0); + + if (self->mDatabaseConnection) { + Unused << self->mDatabaseConnection->Close(); + self->mDatabaseConnection = nullptr; + } + + self->mFinalized.Flip(); + self->mMonitor.NotifyAll(); + NS_DispatchToMainThread(NS_NewRunnableFunction( + "BounceTrackingProtectionStorage::BlockShutdown " + "- mainthread callback", + [self]() { self->Finalize(); })); + }), + NS_DISPATCH_EVENT_MAY_BLOCK); + + return NS_OK; +} + +nsresult BounceTrackingProtectionStorage::WaitForInitialization() { + MOZ_ASSERT(NS_IsMainThread(), + "Must only wait for initialization in the main thread."); + MonitorAutoLock lock(mMonitor); + while (!mInitialized && !mErrored && !mShuttingDown) { + mMonitor.Wait(); + } + if (mErrored) { + return NS_ERROR_FAILURE; + } + if (mShuttingDown) { + return NS_ERROR_NOT_AVAILABLE; + } + return NS_OK; +} + +void BounceTrackingProtectionStorage::Finalize() { + nsCOMPtr asc = GetAsyncShutdownBarrier(); + MOZ_ASSERT(asc); + DebugOnly rv = asc->RemoveBlocker(this); + MOZ_ASSERT(NS_SUCCEEDED(rv)); +} + +// nsIObserver + +NS_IMETHODIMP +BounceTrackingProtectionStorage::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) { + AssertIsOnMainThread(); + if (nsCRT::strcmp(aTopic, "last-pb-context-exited") != 0) { + return nsresult::NS_ERROR_FAILURE; + } + + uint32_t removedCount = 0; + // Clear in-memory private browsing entries. + for (auto iter = mStateGlobal.Iter(); !iter.Done(); iter.Next()) { + BounceTrackingStateGlobal* stateGlobal = iter.Data(); + MOZ_ASSERT(stateGlobal); + if (stateGlobal->IsPrivateBrowsing()) { + iter.Remove(); + removedCount++; + } + } + MOZ_LOG( + gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: last-pb-context-exited: Removed %d private browsing state globals", + __FUNCTION__, removedCount)); + + return NS_OK; +} + +// nsIAsyncShutdownBlocker + +already_AddRefed +BounceTrackingProtectionStorage::GetAsyncShutdownBarrier() const { + nsCOMPtr svc = components::AsyncShutdown::Service(); + MOZ_RELEASE_ASSERT(svc); + + nsCOMPtr client; + nsresult rv = svc->GetProfileBeforeChange(getter_AddRefs(client)); + MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv)); + MOZ_RELEASE_ASSERT(client); + + return client.forget(); +} + +NS_IMETHODIMP BounceTrackingProtectionStorage::GetState(nsIPropertyBag**) { + return NS_OK; +} + +NS_IMETHODIMP BounceTrackingProtectionStorage::GetName(nsAString& aName) { + aName.AssignLiteral("BounceTrackingProtectionStorage: Flushing to disk"); + return NS_OK; +} + +nsresult BounceTrackingProtectionStorage::Init() { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, ("%s", __FUNCTION__)); + + // Init shouldn't be called if the feature is disabled. + NS_ENSURE_TRUE( + StaticPrefs::privacy_bounceTrackingProtection_enabled_AtStartup(), + NS_ERROR_FAILURE); + + // Register a shutdown blocker so we can flush pending changes to disk before + // shutdown. + // Init may also be called during shutdown, e.g. because of clearing data + // during shutdown. + nsCOMPtr shutdownBarrier = GetAsyncShutdownBarrier(); + NS_ENSURE_TRUE(shutdownBarrier, NS_ERROR_FAILURE); + + bool closed; + nsresult rv = shutdownBarrier->GetIsClosed(&closed); + if (closed || NS_WARN_IF(NS_FAILED(rv))) { + MonitorAutoLock lock(mMonitor); + mShuttingDown.Flip(); + return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; + } + + rv = shutdownBarrier->AddBlocker( + this, NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__, u""_ns); + NS_ENSURE_SUCCESS(rv, rv); + + // Listen for last private browsing context exited message so we can clean up + // in memory state when the PBM session ends. + nsCOMPtr observerService = + mozilla::services::GetObserverService(); + NS_ENSURE_TRUE(observerService, NS_ERROR_FAILURE); + rv = observerService->AddObserver(this, "last-pb-context-exited", false); + NS_ENSURE_SUCCESS(rv, rv); + + // Create the database file. + rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(mDatabaseFile)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mDatabaseFile->AppendNative(BOUNCE_TRACKING_PROTECTION_DB_FILENAME); + NS_ENSURE_SUCCESS(rv, rv); + + // Init the database and import data. + NS_ENSURE_SUCCESS( + NS_CreateBackgroundTaskQueue("BounceTrackingProtectionStorage", + getter_AddRefs(mBackgroundThread)), + NS_ERROR_FAILURE); + + RefPtr self = this; + + mBackgroundThread->Dispatch( + NS_NewRunnableFunction("BounceTrackingProtectionStorage::Init", + [self]() { + MonitorAutoLock lock(self->mMonitor); + nsresult rv = self->CreateDatabaseConnection(); + if (NS_WARN_IF(NS_FAILED(rv))) { + self->mErrored.Flip(); + self->mMonitor.Notify(); + return; + } + + rv = self->LoadMemoryStateFromDisk(); + if (NS_WARN_IF(NS_FAILED(rv))) { + self->mErrored.Flip(); + self->mMonitor.Notify(); + return; + } + + self->mInitialized.Flip(); + self->mMonitor.Notify(); + }), + NS_DISPATCH_EVENT_MAY_BLOCK); + + return NS_OK; +} + +nsresult BounceTrackingProtectionStorage::CreateDatabaseConnection() { + MOZ_ASSERT(!NS_IsMainThread()); + NS_ENSURE_TRUE(mDatabaseFile, NS_ERROR_NULL_POINTER); + + nsCOMPtr storage = + do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID); + NS_ENSURE_TRUE(storage, NS_ERROR_UNEXPECTED); + + nsresult rv = storage->OpenDatabase(mDatabaseFile, + mozIStorageService::CONNECTION_DEFAULT, + getter_AddRefs(mDatabaseConnection)); + if (rv == NS_ERROR_FILE_CORRUPTED) { + rv = mDatabaseFile->Remove(false); + NS_ENSURE_SUCCESS(rv, rv); + rv = storage->OpenDatabase(mDatabaseFile, + mozIStorageService::CONNECTION_DEFAULT, + getter_AddRefs(mDatabaseConnection)); + } + NS_ENSURE_SUCCESS(rv, rv); + + NS_ENSURE_TRUE(mDatabaseConnection, NS_ERROR_UNEXPECTED); + bool ready = false; + mDatabaseConnection->GetConnectionReady(&ready); + NS_ENSURE_TRUE(ready, NS_ERROR_UNEXPECTED); + + return EnsureTable(); +} + +nsresult BounceTrackingProtectionStorage::EnsureTable() { + MOZ_ASSERT(!NS_IsMainThread()); + NS_ENSURE_TRUE(mDatabaseConnection, NS_ERROR_UNEXPECTED); + + nsresult rv = mDatabaseConnection->SetSchemaVersion(SCHEMA_VERSION); + NS_ENSURE_SUCCESS(rv, rv); + + const constexpr auto createTableQuery = + "CREATE TABLE IF NOT EXISTS sites (" + "originAttributeSuffix TEXT NOT NULL," + "siteHost TEXT NOT NULL, " + "entryType INTEGER NOT NULL, " + "timeStamp INTEGER NOT NULL, " + "PRIMARY KEY (originAttributeSuffix, siteHost)" + ");"_ns; + + return mDatabaseConnection->ExecuteSimpleSQL(createTableQuery); +} + +nsresult BounceTrackingProtectionStorage::LoadMemoryStateFromDisk() { + MOZ_ASSERT(!NS_IsMainThread(), + "Must not load the table from disk in the main thread."); + + const constexpr auto selectAllQuery( + "SELECT originAttributeSuffix, siteHost, entryType, timeStamp FROM sites;"_ns); + + nsCOMPtr readStmt; + nsresult rv = mDatabaseConnection->CreateStatement(selectAllQuery, + getter_AddRefs(readStmt)); + NS_ENSURE_SUCCESS(rv, rv); + + bool hasResult; + // Collect DB entries into an array to hand to the main thread later. + nsTArray importEntries; + while (NS_SUCCEEDED(readStmt->ExecuteStep(&hasResult)) && hasResult) { + nsAutoCString originAttributeSuffix, siteHost; + int64_t timeStamp; + int32_t typeInt; + + rv = readStmt->GetUTF8String(0, originAttributeSuffix); + NS_ENSURE_SUCCESS(rv, rv); + rv = readStmt->GetUTF8String(1, siteHost); + NS_ENSURE_SUCCESS(rv, rv); + rv = readStmt->GetInt32(2, &typeInt); + NS_ENSURE_SUCCESS(rv, rv); + rv = readStmt->GetInt64(3, &timeStamp); + NS_ENSURE_SUCCESS(rv, rv); + + // Convert entryType field to enum. + BounceTrackingProtectionStorage::EntryType entryType = + static_cast(typeInt); + // Check that the enum value is valid. + if (NS_WARN_IF( + entryType != + BounceTrackingProtectionStorage::EntryType::BounceTracker && + entryType != + BounceTrackingProtectionStorage::EntryType::UserActivation)) { + continue; + } + + OriginAttributes oa; + bool success = oa.PopulateFromSuffix(originAttributeSuffix); + if (NS_WARN_IF(!success)) { + continue; + } + + // Collect entries to dispatch to main thread later. + importEntries.AppendElement( + ImportEntry{oa, siteHost, entryType, timeStamp}); + } + + // We can only access the state map on the main thread. + RefPtr self = this; + return NS_DispatchToMainThread(NS_NewRunnableFunction( + "BounceTrackingProtectionStorage::LoadMemoryStateFromDisk", + [self, importEntries = std::move(importEntries)]() { + // For each entry get or create BounceTrackingStateGlobal and insert it + // into global state map. + for (const ImportEntry& entry : importEntries) { + RefPtr stateGlobal = + self->GetOrCreateStateGlobal(entry.mOriginAttributes); + MOZ_ASSERT(stateGlobal); + + nsresult rv; + if (entry.mEntryType == + BounceTrackingProtectionStorage::EntryType::BounceTracker) { + rv = stateGlobal->RecordBounceTracker(entry.mSiteHost, + entry.mTimeStamp, true); + } else { + rv = stateGlobal->RecordUserActivation(entry.mSiteHost, + entry.mTimeStamp, true); + } + if (NS_WARN_IF(NS_FAILED(rv)) && + MOZ_LOG_TEST(gBounceTrackingProtectionLog, LogLevel::Debug)) { + nsAutoCString originAttributeSuffix; + entry.mOriginAttributes.CreateSuffix(originAttributeSuffix); + + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: Failed to load entry from disk: " + "originAttributeSuffix=%s, siteHost=%s, entryType=%d, " + "timeStamp=%" PRId64, + __FUNCTION__, originAttributeSuffix.get(), + PromiseFlatCString(entry.mSiteHost).get(), + static_cast(entry.mEntryType), entry.mTimeStamp)); + } + } + })); +} + +void BounceTrackingProtectionStorage::IncrementPendingWrites() { + MonitorAutoLock lock(mMonitor); + MOZ_ASSERT(mPendingWrites < std::numeric_limits::max()); + mPendingWrites++; +} + +void BounceTrackingProtectionStorage::DecrementPendingWrites() { + MonitorAutoLock lock(mMonitor); + MOZ_ASSERT(mPendingWrites > 0); + mPendingWrites--; +} + +// static +nsresult BounceTrackingProtectionStorage::UpsertData( + mozIStorageConnection* aDatabaseConnection, + const OriginAttributes& aOriginAttributes, const nsACString& aSiteHost, + BounceTrackingProtectionStorage::EntryType aEntryType, PRTime aTimeStamp) { + MOZ_ASSERT(!NS_IsMainThread(), + "Must not write to the table from the main thread."); + MOZ_ASSERT(aDatabaseConnection); + MOZ_ASSERT(!aSiteHost.IsEmpty()); + MOZ_ASSERT(aTimeStamp > 0); + MOZ_ASSERT(aOriginAttributes.mPrivateBrowsingId == + nsIScriptSecurityManager::DEFAULT_PRIVATE_BROWSING_ID, + "Must not write private browsing data to the table."); + + auto constexpr upsertQuery = + "INSERT INTO sites (originAttributeSuffix, siteHost, entryType, " + "timeStamp)" + "VALUES (:originAttributeSuffix, :siteHost, :entryType, :timeStamp)" + "ON CONFLICT (originAttributeSuffix, siteHost)" + "DO UPDATE SET entryType = :entryType, timeStamp = :timeStamp;"_ns; + + nsCOMPtr upsertStmt; + nsresult rv = aDatabaseConnection->CreateStatement( + upsertQuery, getter_AddRefs(upsertStmt)); + NS_ENSURE_SUCCESS(rv, rv); + + // Serialize OriginAttributes. + nsAutoCString originAttributeSuffix; + aOriginAttributes.CreateSuffix(originAttributeSuffix); + + rv = upsertStmt->BindUTF8StringByName("originAttributeSuffix"_ns, + originAttributeSuffix); + NS_ENSURE_SUCCESS(rv, rv); + + rv = upsertStmt->BindUTF8StringByName("siteHost"_ns, aSiteHost); + NS_ENSURE_SUCCESS(rv, rv); + + rv = upsertStmt->BindInt32ByName("entryType"_ns, + static_cast(aEntryType)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = upsertStmt->BindInt64ByName("timeStamp"_ns, aTimeStamp); + NS_ENSURE_SUCCESS(rv, rv); + + return upsertStmt->Execute(); +} + +// static +nsresult BounceTrackingProtectionStorage::DeleteData( + mozIStorageConnection* aDatabaseConnection, + Maybe aOriginAttributes, const nsACString& aSiteHost) { + MOZ_ASSERT(!NS_IsMainThread(), + "Must not write to the table from the main thread."); + MOZ_ASSERT(aDatabaseConnection); + MOZ_ASSERT(!aSiteHost.IsEmpty()); + MOZ_ASSERT(aOriginAttributes.isNothing() || + aOriginAttributes->mPrivateBrowsingId == + nsIScriptSecurityManager::DEFAULT_PRIVATE_BROWSING_ID); + + nsAutoCString deleteQuery("DELETE FROM sites WHERE siteHost = :siteHost"); + + if (aOriginAttributes) { + deleteQuery.AppendLiteral( + " AND originAttributeSuffix = :originAttributeSuffix"); + } + + nsCOMPtr upsertStmt; + nsresult rv = aDatabaseConnection->CreateStatement( + deleteQuery, getter_AddRefs(upsertStmt)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = upsertStmt->BindUTF8StringByName("siteHost"_ns, aSiteHost); + NS_ENSURE_SUCCESS(rv, rv); + + if (aOriginAttributes) { + nsAutoCString originAttributeSuffix; + aOriginAttributes->CreateSuffix(originAttributeSuffix); + rv = upsertStmt->BindUTF8StringByName("originAttributeSuffix"_ns, + originAttributeSuffix); + NS_ENSURE_SUCCESS(rv, rv); + } + + return upsertStmt->Execute(); +} + +// static +nsresult BounceTrackingProtectionStorage::DeleteDataInTimeRange( + mozIStorageConnection* aDatabaseConnection, + Maybe aOriginAttributes, PRTime aFrom, Maybe aTo, + Maybe aEntryType) { + MOZ_ASSERT(!NS_IsMainThread(), + "Must not write to the table from the main thread."); + MOZ_ASSERT(aDatabaseConnection); + MOZ_ASSERT(aOriginAttributes.isNothing() || + aOriginAttributes->mPrivateBrowsingId == + nsIScriptSecurityManager::DEFAULT_PRIVATE_BROWSING_ID); + MOZ_ASSERT(aFrom >= 0); + MOZ_ASSERT(aTo.isNothing() || aTo.value() > aFrom); + + nsAutoCString deleteQuery( + "DELETE FROM sites " + "WHERE timeStamp >= :aFrom"_ns); + + if (aTo.isSome()) { + deleteQuery.AppendLiteral(" AND timeStamp <= :aTo"); + } + + if (aOriginAttributes) { + deleteQuery.AppendLiteral( + " AND originAttributeSuffix = :originAttributeSuffix"); + } + + if (aEntryType.isSome()) { + deleteQuery.AppendLiteral(" AND entryType = :entryType"); + } + deleteQuery.AppendLiteral(";"); + + nsCOMPtr deleteStmt; + nsresult rv = aDatabaseConnection->CreateStatement( + deleteQuery, getter_AddRefs(deleteStmt)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = deleteStmt->BindInt64ByName("aFrom"_ns, aFrom); + NS_ENSURE_SUCCESS(rv, rv); + + if (aTo.isSome()) { + rv = deleteStmt->BindInt64ByName("aTo"_ns, aTo.value()); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (aOriginAttributes) { + nsAutoCString originAttributeSuffix; + aOriginAttributes->CreateSuffix(originAttributeSuffix); + rv = deleteStmt->BindUTF8StringByName("originAttributeSuffix"_ns, + originAttributeSuffix); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (aEntryType.isSome()) { + rv = deleteStmt->BindInt32ByName("entryType"_ns, + static_cast(*aEntryType)); + NS_ENSURE_SUCCESS(rv, rv); + } + + return deleteStmt->Execute(); +} + +nsresult BounceTrackingProtectionStorage::DeleteDataByOriginAttributesPattern( + mozIStorageConnection* aDatabaseConnection, + const OriginAttributesPattern& aOriginAttributesPattern) { + MOZ_ASSERT(!NS_IsMainThread(), + "Must not write to the table from the main thread."); + MOZ_ASSERT(aDatabaseConnection); + + nsCOMPtr patternMatchFunction( + new OriginAttrsPatternMatchOASuffixSQLFunction(aOriginAttributesPattern)); + + nsresult rv = aDatabaseConnection->CreateFunction( + "ORIGIN_ATTRS_PATTERN_MATCH_OA_SUFFIX"_ns, 1, patternMatchFunction); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aDatabaseConnection->ExecuteSimpleSQL( + "DELETE FROM sites WHERE " + "ORIGIN_ATTRS_PATTERN_MATCH_OA_SUFFIX(originAttributeSuffix);"_ns); + NS_ENSURE_SUCCESS(rv, rv); + + return aDatabaseConnection->RemoveFunction( + "ORIGIN_ATTRS_PATTERN_MATCH_OA_SUFFIX"_ns); +} + +// static +nsresult BounceTrackingProtectionStorage::ClearData( + mozIStorageConnection* aDatabaseConnection) { + MOZ_ASSERT(!NS_IsMainThread(), + "Must not write to the table from the main thread."); + NS_ENSURE_ARG_POINTER(aDatabaseConnection); + return aDatabaseConnection->ExecuteSimpleSQL("DELETE FROM sites;"_ns); +} + +NS_IMPL_ISUPPORTS(OriginAttrsPatternMatchOASuffixSQLFunction, + mozIStorageFunction) + +NS_IMETHODIMP +OriginAttrsPatternMatchOASuffixSQLFunction::OnFunctionCall( + mozIStorageValueArray* aFunctionArguments, nsIVariant** aResult) { + nsresult rv; + + nsAutoCString originAttributeSuffix; + rv = aFunctionArguments->GetUTF8String(0, originAttributeSuffix); + NS_ENSURE_SUCCESS(rv, rv); + + OriginAttributes originAttributes; + bool parsedSuccessfully = + originAttributes.PopulateFromSuffix(originAttributeSuffix); + NS_ENSURE_TRUE(parsedSuccessfully, NS_ERROR_FAILURE); + + bool result = mPattern.Matches(originAttributes); + + RefPtr outVar(new nsVariant()); + rv = outVar->SetAsBool(result); + NS_ENSURE_SUCCESS(rv, rv); + + outVar.forget(aResult); + return NS_OK; +} + +} // namespace mozilla diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtectionStorage.h b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtectionStorage.h new file mode 100644 index 0000000000..8d1d4e0417 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtectionStorage.h @@ -0,0 +1,222 @@ +/* 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_BounceTrackingProtectionStorage_h__ +#define mozilla_BounceTrackingProtectionStorage_h__ + +#include "mozIStorageFunction.h" +#include "mozilla/Logging.h" +#include "mozilla/Monitor.h" +#include "mozilla/ThreadSafety.h" +#include "mozilla/WeakPtr.h" +#include "mozilla/dom/FlippedOnce.h" +#include "nsIAsyncShutdown.h" +#include "nsIFile.h" +#include "nsIObserver.h" +#include "nsISupports.h" +#include "nsTHashMap.h" +#include "mozIStorageConnection.h" +#include "mozilla/OriginAttributesHashKey.h" + +class nsIPrincipal; +class mozIStorageConnection; + +namespace mozilla { + +class BounceTrackingStateGlobal; +class BounceTrackingState; +class OriginAttributes; + +extern LazyLogModule gBounceTrackingProtectionLog; + +class BounceTrackingProtectionStorage final : public nsIObserver, + public nsIAsyncShutdownBlocker, + public SupportsWeakPtr { + friend class BounceTrackingStateGlobal; + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIOBSERVER + NS_DECL_NSIASYNCSHUTDOWNBLOCKER + + public: + BounceTrackingProtectionStorage() + : mMonitor("mozilla::BounceTrackingProtectionStorage::mMonitor"), + mPendingWrites(0){}; + + // Initialises the storage including the on-disk database. + [[nodiscard]] nsresult Init(); + + // Getters for mStateGlobal. + BounceTrackingStateGlobal* GetOrCreateStateGlobal( + const OriginAttributes& aOriginAttributes); + + BounceTrackingStateGlobal* GetOrCreateStateGlobal(nsIPrincipal* aPrincipal); + + BounceTrackingStateGlobal* GetOrCreateStateGlobal( + BounceTrackingState* aBounceTrackingState); + + using StateGlobalMap = + nsTHashMap>; + // Provides a read-only reference to the state global map. + const StateGlobalMap& StateGlobalMapRef() { return mStateGlobal; } + + // The enum values match the database type field. Updating them requires a DB + // migration. + enum class EntryType : uint8_t { BounceTracker = 0, UserActivation = 1 }; + + // Clear all state for a given site host. If aOriginAttributes is passed, only + // entries for that OA will be deleted. + [[nodiscard]] nsresult ClearBySiteHost(const nsACString& aSiteHost, + OriginAttributes* aOriginAttributes); + + // Clear all state within a given time range. + [[nodiscard]] nsresult ClearByTimeRange(PRTime aFrom, PRTime aTo); + + // Clear all state for a given OriginAttributesPattern. + [[nodiscard]] nsresult ClearByOriginAttributesPattern( + const OriginAttributesPattern& aOriginAttributesPattern); + + // Clear all state. + [[nodiscard]] nsresult Clear(); + + private: + ~BounceTrackingProtectionStorage() = default; + + // Worker thread. This should be a valid thread after Init() returns and be + // destroyed when we finalize + nsCOMPtr mBackgroundThread; // main thread only + + // Database connections. Guaranteed to be non-null and working once + // initialized and not-yet finalized + RefPtr mDatabaseConnection; // Worker thread only + + // Wait (non-blocking) until the service is fully initialized. We may be + // waiting for that async work started by Init(). + [[nodiscard]] nsresult WaitForInitialization(); + + // Called to indicate to the async shutdown service that we are all wrapped + // up. This also spins down the worker thread, since it is called after all + // disk database connections are closed. + void Finalize(); + + // Utility function to grab the correct barrier this service needs to shut + // down by + already_AddRefed GetAsyncShutdownBarrier() const; + + // Initialises the DB connection on the worker thread. + [[nodiscard]] nsresult CreateDatabaseConnection(); + + // Creates amd initialises the database table if needed. Worker thread only. + [[nodiscard]] nsresult EnsureTable(); + + // Temporary data structure used to import db data into memory. + struct ImportEntry { + OriginAttributes mOriginAttributes; + nsCString mSiteHost; + EntryType mEntryType; + PRTime mTimeStamp; + }; + + // Imports state from the database on disk into memory. + [[nodiscard]] nsresult LoadMemoryStateFromDisk(); + + // Used to (thread-safely) track how many operations have been launched to the + // worker thread so that we can wait for it to hit zero before close the disk + // database connection + void IncrementPendingWrites(); + void DecrementPendingWrites(); + + // Update or create database entry. Worker thread only. + [[nodiscard]] static nsresult UpsertData( + mozIStorageConnection* aDatabaseConnection, + const OriginAttributes& aOriginAttributes, const nsACString& aSiteHost, + EntryType aEntryType, PRTime aTimeStamp); + + // Delete database entries. Worker thread only. + [[nodiscard]] static nsresult DeleteData( + mozIStorageConnection* aDatabaseConnection, + Maybe aOriginAttributes, const nsACString& aSiteHost); + + // Delete all entries before a given time. Worker thread only. + // If aEntryType is passed only entries of that type will be deleted. + [[nodiscard]] static nsresult DeleteDataInTimeRange( + mozIStorageConnection* aDatabaseConnection, + Maybe aOriginAttributes, PRTime aFrom, + Maybe aTo, + Maybe aEntryType = Nothing{}); + + // Delete all entries matching the given OriginAttributesPattern. Worker + // thread only. + [[nodiscard]] static nsresult DeleteDataByOriginAttributesPattern( + mozIStorageConnection* aDatabaseConnection, + const OriginAttributesPattern& aOriginAttributesPattern); + + // Clear all entries from the database. + [[nodiscard]] static nsresult ClearData( + mozIStorageConnection* aDatabaseConnection); + + // Service state management. We protect these variables with a monitor. This + // monitor is also used to signal the completion of initialization and + // finalization performed in the worker thread. + Monitor mMonitor; + + FlippedOnce mInitialized MOZ_GUARDED_BY(mMonitor); + FlippedOnce mErrored MOZ_GUARDED_BY(mMonitor); + FlippedOnce mShuttingDown MOZ_GUARDED_BY(mMonitor); + FlippedOnce mFinalized MOZ_GUARDED_BY(mMonitor); + uint32_t mPendingWrites MOZ_GUARDED_BY(mMonitor); + + // The database file handle. We can only create this in the main thread and + // need it in the worker to perform blocking disk IO. So we put it on this, + // since we pass this to the worker anyway + nsCOMPtr mDatabaseFile; + + // Map of origin attributes to global state object. This enables us to track + // bounce tracking state per OA, e.g. to separate private browsing from normal + // browsing. + StateGlobalMap mStateGlobal{}; + + // Helpers used to sync updates to BounceTrackingStateGlobal with the + // database. + + // Updates or inserts a DB entry keyed by OA + site host. + [[nodiscard]] nsresult UpdateDBEntry( + const OriginAttributes& aOriginAttributes, const nsACString& aSiteHost, + EntryType aEntryType, PRTime aTimeStamp); + + // Deletes a DB entry keyed by OA + site host. If only aSiteHost is passed, + // all entries for that host will be deleted across OriginAttributes. + [[nodiscard]] nsresult DeleteDBEntries(OriginAttributes* aOriginAttributes, + const nsACString& aSiteHost); + + // Delete all DB entries before a given time. + // If aEntryType is passed only entries of that type will be deleted. + [[nodiscard]] nsresult DeleteDBEntriesInTimeRange( + OriginAttributes* aOriginAttributes, PRTime aFrom, + Maybe aTo = Nothing{}, Maybe aEntryType = Nothing{}); + + // Deletes all DB entries matching the given OriginAttributesPattern. + [[nodiscard]] nsresult DeleteDBEntriesByOriginAttributesPattern( + const OriginAttributesPattern& aOriginAttributesPattern); +}; + +// A SQL function to match DB entries by OriginAttributesPattern. +class OriginAttrsPatternMatchOASuffixSQLFunction final + : public mozIStorageFunction { + NS_DECL_ISUPPORTS + NS_DECL_MOZISTORAGEFUNCTION + + explicit OriginAttrsPatternMatchOASuffixSQLFunction( + OriginAttributesPattern const& aPattern) + : mPattern(aPattern) {} + OriginAttrsPatternMatchOASuffixSQLFunction() = delete; + + private: + ~OriginAttrsPatternMatchOASuffixSQLFunction() = default; + + OriginAttributesPattern mPattern; +}; + +} // namespace mozilla + +#endif // mozilla_BounceTrackingProtectionStorage_h__ diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingRecord.cpp b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingRecord.cpp new file mode 100644 index 0000000000..14ee178ae2 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingRecord.cpp @@ -0,0 +1,76 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "BounceTrackingRecord.h" +#include "mozilla/Logging.h" +#include "nsPrintfCString.h" + +namespace mozilla { + +extern LazyLogModule gBounceTrackingProtectionLog; + +NS_IMPL_CYCLE_COLLECTION(BounceTrackingRecord); + +void BounceTrackingRecord::SetInitialHost(const nsACString& aHost) { + mInitialHost = aHost; +} + +const nsACString& BounceTrackingRecord::GetInitialHost() { + return mInitialHost; +} + +void BounceTrackingRecord::SetFinalHost(const nsACString& aHost) { + mFinalHost = aHost; +} + +const nsACString& BounceTrackingRecord::GetFinalHost() { return mFinalHost; } + +void BounceTrackingRecord::AddBounceHost(const nsACString& aHost) { + mBounceHosts.Insert(aHost); + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: %s", __FUNCTION__, Describe().get())); +} + +// static +nsCString BounceTrackingRecord::DescribeSet(const nsTHashSet& set) { + nsAutoCString setStr; + + setStr.AppendLiteral("["); + + if (!set.IsEmpty()) { + for (const nsACString& host : set) { + setStr.Append(host); + setStr.AppendLiteral(","); + } + setStr.Truncate(setStr.Length() - 1); + } + + setStr.AppendLiteral("]"); + + return std::move(setStr); +} + +void BounceTrackingRecord::AddStorageAccessHost(const nsACString& aHost) { + mStorageAccessHosts.Insert(aHost); +} + +const nsTHashSet& BounceTrackingRecord::GetBounceHosts() { + return mBounceHosts; +} + +const nsTHashSet& BounceTrackingRecord::GetStorageAccessHosts() { + return mStorageAccessHosts; +} + +nsCString BounceTrackingRecord::Describe() { + return nsPrintfCString( + "{mInitialHost:%s, mFinalHost:%s, mBounceHosts:%s, " + "mStorageAccessHosts:%s}", + mInitialHost.get(), mFinalHost.get(), DescribeSet(mBounceHosts).get(), + DescribeSet(mStorageAccessHosts).get()); +} + +} // namespace mozilla diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingRecord.h b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingRecord.h new file mode 100644 index 0000000000..d3e980d00b --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingRecord.h @@ -0,0 +1,72 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_BounceTrackingRecord_h +#define mozilla_BounceTrackingRecord_h + +#include "nsISupports.h" +#include "nsStringFwd.h" +#include "nsCycleCollectionParticipant.h" +#include "nsTHashSet.h" + +namespace mozilla { + +namespace dom { +class CanonicalBrowsingContext; +} + +// Stores per-tab data relevant to bounce tracking protection for every extended +// navigation. +class BounceTrackingRecord final { + public: + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(BounceTrackingRecord); + NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(BounceTrackingRecord); + + void SetInitialHost(const nsACString& aHost); + + const nsACString& GetInitialHost(); + + void SetFinalHost(const nsACString& aHost); + + const nsACString& GetFinalHost(); + + void AddBounceHost(const nsACString& aHost); + + void AddStorageAccessHost(const nsACString& aHost); + + const nsTHashSet& GetBounceHosts(); + + const nsTHashSet& GetStorageAccessHosts(); + + // Create a string that describes this record. Used for logging. + nsCString Describe(); + + private: + ~BounceTrackingRecord() = default; + + // A site's host. The initiator site of the current extended navigation. + nsAutoCString mInitialHost; + + // A site's host or null. The destination of the current extended navigation. + // Updated after every document load. + nsAutoCString mFinalHost; + + // A set of sites' hosts. All server-side and client-side redirects hit during + // this extended navigation. + nsTHashSet mBounceHosts; + + // A set of sites' hosts. All sites which accessed storage during this + // extended navigation. + nsTHashSet mStorageAccessHosts; + + // Create a comma-delimited string that describes a string set. Used for + // logging. + static nsCString DescribeSet(const nsTHashSet& set); +}; + +} // namespace mozilla + +#endif diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingState.cpp b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingState.cpp new file mode 100644 index 0000000000..c5abb8b8d7 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingState.cpp @@ -0,0 +1,612 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "BounceTrackingProtection.h" +#include "BounceTrackingState.h" +#include "BounceTrackingRecord.h" + +#include "BounceTrackingStorageObserver.h" +#include "ErrorList.h" +#include "mozilla/OriginAttributes.h" +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/BrowsingContextWebProgress.h" +#include "mozilla/dom/CanonicalBrowsingContext.h" +#include "nsCOMPtr.h" +#include "nsDebug.h" +#include "nsError.h" +#include "nsIBrowser.h" +#include "nsIChannel.h" +#include "nsIEffectiveTLDService.h" +#include "nsIRedirectHistoryEntry.h" +#include "nsIURI.h" +#include "nsIWebProgressListener.h" +#include "nsIPrincipal.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/dom/WindowGlobalParent.h" +#include "mozilla/ClearOnShutdown.h" +#include "nsTHashMap.h" +#include "mozilla/dom/Element.h" + +namespace mozilla { + +// Global map: browserId -> BounceTrackingState +static StaticAutoPtr>> + sBounceTrackingStates; + +static StaticRefPtr sStorageObserver; + +NS_IMPL_ISUPPORTS(BounceTrackingState, nsIWebProgressListener, + nsISupportsWeakReference); + +BounceTrackingState::BounceTrackingState() { + MOZ_ASSERT(StaticPrefs::privacy_bounceTrackingProtection_enabled_AtStartup()); + mBounceTrackingProtection = BounceTrackingProtection::GetSingleton(); +}; + +BounceTrackingState::~BounceTrackingState() { + if (sBounceTrackingStates) { + sBounceTrackingStates->Remove(mBrowserId); + } +} + +// static +already_AddRefed BounceTrackingState::GetOrCreate( + dom::BrowsingContextWebProgress* aWebProgress) { + MOZ_ASSERT(aWebProgress); + + if (!ShouldCreateBounceTrackingStateForWebProgress(aWebProgress)) { + return nullptr; + } + + // Create BounceTrackingState instance and populate the global + // BounceTrackingState map. + if (!sBounceTrackingStates) { + sBounceTrackingStates = + new nsTHashMap>(); + ClearOnShutdown(&sBounceTrackingStates); + } + + if (!sStorageObserver) { + sStorageObserver = new BounceTrackingStorageObserver(); + ClearOnShutdown(&sStorageObserver); + + DebugOnly rv = sStorageObserver->Init(); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to init storage observer"); + } + + dom::BrowsingContext* browsingContext = aWebProgress->GetBrowsingContext(); + if (!browsingContext) { + return nullptr; + } + uint64_t browserId = browsingContext->BrowserId(); + bool createdNew; + RefPtr bounceTrackingState = + do_AddRef(sBounceTrackingStates->LookupOrInsertWith(browserId, [&] { + createdNew = true; + return do_AddRef(new BounceTrackingState()); + })); + + if (createdNew) { + nsresult rv = bounceTrackingState->Init(aWebProgress); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + } + + return bounceTrackingState.forget(); +}; + +// static +void BounceTrackingState::ResetAll() { Reset(nullptr, nullptr); } + +// static +void BounceTrackingState::ResetAllForOriginAttributes( + const OriginAttributes& aOriginAttributes) { + Reset(&aOriginAttributes, nullptr); +} + +// static +void BounceTrackingState::ResetAllForOriginAttributesPattern( + const OriginAttributesPattern& aPattern) { + Reset(nullptr, &aPattern); +} + +nsresult BounceTrackingState::Init( + dom::BrowsingContextWebProgress* aWebProgress) { + NS_ENSURE_ARG_POINTER(aWebProgress); + NS_ENSURE_TRUE( + StaticPrefs::privacy_bounceTrackingProtection_enabled_AtStartup(), + NS_ERROR_NOT_AVAILABLE); + NS_ENSURE_TRUE(mBounceTrackingProtection, NS_ERROR_FAILURE); + + // Store the browser ID so we can get the associated BC later without having + // to hold a reference to aWebProgress. + dom::BrowsingContext* browsingContext = aWebProgress->GetBrowsingContext(); + NS_ENSURE_TRUE(browsingContext, NS_ERROR_FAILURE); + mBrowserId = browsingContext->BrowserId(); + // Create a copy of the BC's OriginAttributes so we can use it later without + // having to hold a reference to the BC. + mOriginAttributes = browsingContext->OriginAttributesRef(); + MOZ_ASSERT(mOriginAttributes.mPartitionKey.IsEmpty(), + "Top level BCs mus not have a partition key."); + + // Add a listener for window load. See BounceTrackingState::OnStateChange for + // the listener code. + nsresult rv = aWebProgress->AddProgressListener( + this, nsIWebProgress::NOTIFY_STATE_WINDOW); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +void BounceTrackingState::ResetBounceTrackingRecord() { + mBounceTrackingRecord = nullptr; +} + +BounceTrackingRecord* BounceTrackingState::GetBounceTrackingRecord() { + return mBounceTrackingRecord; +} + +nsCString BounceTrackingState::Describe() { + nsAutoCString oaSuffix; + OriginAttributesRef().CreateSuffix(oaSuffix); + + return nsPrintfCString( + "{ mBounceTrackingRecord: %s, mOriginAttributes: %s }", + mBounceTrackingRecord ? mBounceTrackingRecord->Describe().get() : "null", + oaSuffix.get()); +} + +// static +void BounceTrackingState::Reset(const OriginAttributes* aOriginAttributes, + const OriginAttributesPattern* aPattern) { + if (aOriginAttributes || aPattern) { + MOZ_ASSERT((aOriginAttributes != nullptr) != (aPattern != nullptr), + "Must not pass both aOriginAttributes and aPattern."); + } + + if (!sBounceTrackingStates) { + return; + } + for (const RefPtr& bounceTrackingState : + sBounceTrackingStates->Values()) { + if ((aOriginAttributes && + *aOriginAttributes != bounceTrackingState->OriginAttributesRef()) || + (aPattern && + !aPattern->Matches(bounceTrackingState->OriginAttributesRef()))) { + continue; + } + if (bounceTrackingState->mClientBounceDetectionTimeout) { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: mClientBounceDetectionTimeout->Cancel()", __FUNCTION__)); + bounceTrackingState->mClientBounceDetectionTimeout->Cancel(); + bounceTrackingState->mClientBounceDetectionTimeout = nullptr; + } + bounceTrackingState->ResetBounceTrackingRecord(); + } +} + +// static +bool BounceTrackingState::ShouldCreateBounceTrackingStateForWebProgress( + dom::BrowsingContextWebProgress* aWebProgress) { + NS_ENSURE_TRUE(aWebProgress, false); + + // Feature is globally disabled. + if (!StaticPrefs::privacy_bounceTrackingProtection_enabled_AtStartup()) { + return false; + } + + // Only keep track of top level content browsing contexts. + dom::BrowsingContext* browsingContext = aWebProgress->GetBrowsingContext(); + if (!browsingContext || !browsingContext->IsTopContent()) { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Verbose, + ("%s: Skip non top-content.", __FUNCTION__)); + return false; + } + + return true; +} + +// static +nsresult BounceTrackingState::HasBounceTrackingStateForSite( + const nsACString& aSiteHost, bool& aResult) { + aResult = false; + NS_ENSURE_TRUE(aSiteHost.Length(), NS_ERROR_FAILURE); + + if (!sBounceTrackingStates) { + return NS_OK; + } + + // Iterate over all browsing contexts which have a bounce tracking state. Use + // the content principal base domain field to determine whether a BC has an + // active site that matches aSiteHost. + for (const RefPtr& state : + sBounceTrackingStates->Values()) { + RefPtr browsingContext = + state->CurrentBrowsingContext(); + + if (!browsingContext || browsingContext->IsDiscarded() || + browsingContext->IsInBFCache()) { + continue; + } + + RefPtr embedderElement = + browsingContext->GetEmbedderElement(); + if (!embedderElement) { + continue; + } + + nsCOMPtr browser = embedderElement->AsBrowser(); + if (!browser) { + continue; + } + + nsCOMPtr contentPrincipal; + nsresult rv = + browser->GetContentPrincipal(getter_AddRefs(contentPrincipal)); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + + nsAutoCString baseDomain; + rv = contentPrincipal->GetBaseDomain(baseDomain); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + + if (aSiteHost.Equals(baseDomain)) { + aResult = true; + return NS_OK; + } + } + + return NS_OK; +} + +already_AddRefed +BounceTrackingState::CurrentBrowsingContext() { + MOZ_ASSERT(mBrowserId != 0); + return dom::BrowsingContext::GetCurrentTopByBrowserId(mBrowserId); +} + +const OriginAttributes& BounceTrackingState::OriginAttributesRef() { + return mOriginAttributes; +} + +nsresult BounceTrackingState::OnDocumentStartRequest(nsIChannel* aChannel) { + NS_ENSURE_ARG_POINTER(aChannel); + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, ("%s", __FUNCTION__)); + + nsCOMPtr loadInfo; + nsresult rv = aChannel->GetLoadInfo(getter_AddRefs(loadInfo)); + NS_ENSURE_SUCCESS(rv, rv); + + // Collect uri list including any redirects. + nsTArray siteList; + + for (const nsCOMPtr& redirectHistoryEntry : + loadInfo->RedirectChain()) { + nsCOMPtr principal; + rv = redirectHistoryEntry->GetPrincipal(getter_AddRefs(principal)); + NS_ENSURE_SUCCESS(rv, rv); + + // Filter out non-content principals. + if (!principal->GetIsContentPrincipal()) { + continue; + } + + nsAutoCString baseDomain; + rv = principal->GetBaseDomain(baseDomain); + NS_ENSURE_SUCCESS(rv, rv); + + siteList.AppendElement(baseDomain); + } + + // Add site via the current URI which is the end of the chain. + nsCOMPtr channelURI; + rv = aChannel->GetURI(getter_AddRefs(channelURI)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr tldService = + do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString siteHost; + rv = tldService->GetSchemelessSite(channelURI, siteHost); + + if (NS_FAILED(rv)) { + NS_WARNING("Failed to retrieve site for final channel URI."); + } + + siteList.AppendElement(siteHost); + + return OnResponseReceived(siteList); +} + +// nsIWebProgressListener + +NS_IMETHODIMP +BounceTrackingState::OnStateChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, uint32_t aStateFlags, + nsresult aStatus) { + NS_ENSURE_ARG_POINTER(aWebProgress); + NS_ENSURE_ARG_POINTER(aRequest); + + bool isTopLevel = false; + nsresult rv = aWebProgress->GetIsTopLevel(&isTopLevel); + NS_ENSURE_SUCCESS(rv, rv); + + // Filter for top level loads. + if (!isTopLevel) { + return NS_OK; + } + + // Filter for window loads. + if (!(aStateFlags & nsIWebProgressListener::STATE_STOP) || + !(aStateFlags & nsIWebProgressListener::STATE_IS_WINDOW)) { + return NS_OK; + } + + // Get the document principal via the current window global. + dom::BrowsingContext* browsingContext = aWebProgress->GetBrowsingContext(); + NS_ENSURE_TRUE(browsingContext, NS_ERROR_FAILURE); + + dom::WindowGlobalParent* windowGlobalParent = + browsingContext->Canonical()->GetCurrentWindowGlobal(); + NS_ENSURE_TRUE(windowGlobalParent, NS_ERROR_FAILURE); + + return OnDocumentLoaded(windowGlobalParent->DocumentPrincipal()); +} + +NS_IMETHODIMP +BounceTrackingState::OnProgressChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + int32_t aCurSelfProgress, + int32_t aMaxSelfProgress, + int32_t aCurTotalProgress, + int32_t aMaxTotalProgress) { + MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +NS_IMETHODIMP +BounceTrackingState::OnLocationChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, nsIURI* aLocation, + uint32_t aFlags) { + MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +NS_IMETHODIMP +BounceTrackingState::OnStatusChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, nsresult aStatus, + const char16_t* aMessage) { + MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +NS_IMETHODIMP +BounceTrackingState::OnSecurityChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, uint32_t aState) { + MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +NS_IMETHODIMP +BounceTrackingState::OnContentBlockingEvent(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + uint32_t aEvent) { + MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +nsresult BounceTrackingState::OnStartNavigation( + nsIPrincipal* aTriggeringPrincipal, + const bool aHasValidUserGestureActivation) { + NS_ENSURE_ARG_POINTER(aTriggeringPrincipal); + + // Logging + if (MOZ_LOG_TEST(gBounceTrackingProtectionLog, LogLevel::Debug)) { + nsAutoCString origin; + nsresult rv = aTriggeringPrincipal->GetOrigin(origin); + if (NS_FAILED(rv)) { + origin = "err"; + } + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: origin: %s, mBounceTrackingRecord: %s", __FUNCTION__, + origin.get(), + mBounceTrackingRecord ? mBounceTrackingRecord->Describe().get() + : "null")); + } + + // Remove any queued global tasks to record stateful bounces for bounce + // tracking from the networking task source. + if (mClientBounceDetectionTimeout) { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: mClientBounceDetectionTimeout->Cancel()", __FUNCTION__)); + mClientBounceDetectionTimeout->Cancel(); + mClientBounceDetectionTimeout = nullptr; + } + + // Obtain the (schemeless) site to keep track of bounces. + nsAutoCString siteHost; + + // If origin is an opaque origin, set initialHost to empty host. Strictly + // speaking we only need to check IsNullPrincipal, but we're generally only + // interested in content principals. Other principal types are not considered + // to be trackers. + if (!aTriggeringPrincipal->GetIsContentPrincipal()) { + siteHost = ""; + } + + // obtain site + nsresult rv = aTriggeringPrincipal->GetBaseDomain(siteHost); + if (NS_WARN_IF(NS_FAILED(rv))) { + siteHost = ""; + } + + // If navigable’s bounce tracking record is null: Set navigable’s bounce + // tracking record to a new bounce tracking record with initial host set to + // initialHost. + if (!mBounceTrackingRecord) { + mBounceTrackingRecord = new BounceTrackingRecord(); + mBounceTrackingRecord->SetInitialHost(siteHost); + + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: new BounceTrackingRecord(): %s", __FUNCTION__, + mBounceTrackingRecord ? mBounceTrackingRecord->Describe().get() + : "null")); + + return NS_OK; + } + + // If sourceSnapshotParams’s has transient activation is true: The user + // activation ends the extended navigation. Process the bounce candidates. + // Also treat system principal navigation as having user interaction + bool hasUserActivation = aHasValidUserGestureActivation || + aTriggeringPrincipal->IsSystemPrincipal(); + + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: site: %s, hasUserActivation? %d", __FUNCTION__, siteHost.get(), + hasUserActivation)); + if (hasUserActivation) { + rv = mBounceTrackingProtection->RecordStatefulBounces(this); + NS_ENSURE_SUCCESS(rv, rv); + + MOZ_ASSERT(!mBounceTrackingRecord); + mBounceTrackingRecord = new BounceTrackingRecord(); + mBounceTrackingRecord->SetInitialHost(siteHost); + + return NS_OK; + } + + // There is no transient user activation. Add host as a bounce candidate. + mBounceTrackingRecord->AddBounceHost(siteHost); + + return NS_OK; +} + +// Private + +nsresult BounceTrackingState::OnResponseReceived( + const nsTArray& aSiteList) { + NS_ENSURE_TRUE(mBounceTrackingRecord, NS_ERROR_FAILURE); + + // Logging + if (MOZ_LOG_TEST(gBounceTrackingProtectionLog, LogLevel::Debug)) { + nsAutoCString siteListStr; + + for (const nsACString& site : aSiteList) { + siteListStr.Append(site); + siteListStr.AppendLiteral(", "); + } + + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: #%zu siteList: %s", __FUNCTION__, siteListStr.Length(), + siteListStr.get())); + } + + // Check if there is still an active timeout. This shouldn't happen since + // OnStartNavigation already cancels it. + if (NS_WARN_IF(mClientBounceDetectionTimeout)) { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: mClientBounceDetectionTimeout->Cancel()", __FUNCTION__)); + mClientBounceDetectionTimeout->Cancel(); + mClientBounceDetectionTimeout = nullptr; + } + + // Run steps after a timeout: queue a global task on the networking task + // source with global to record stateful bounces for bounce. + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: Scheduling mClientBounceDetectionTimeout", __FUNCTION__)); + + // Use a weak reference to this to avoid keeping the object alive if the tab + // is closed during the timeout. + WeakPtr thisWeak = this; + nsresult rv = NS_NewTimerWithCallback( + getter_AddRefs(mClientBounceDetectionTimeout), + [thisWeak](auto) { + if (!thisWeak) { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: !thisWeak", __FUNCTION__)); + return; + } + MOZ_LOG( + gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: Calling RecordStatefulBounces after timeout.", __FUNCTION__)); + + BounceTrackingState* bounceTrackingState = thisWeak; + bounceTrackingState->mBounceTrackingProtection->RecordStatefulBounces( + bounceTrackingState); + + bounceTrackingState->mClientBounceDetectionTimeout = nullptr; + }, + StaticPrefs:: + privacy_bounceTrackingProtection_clientBounceDetectionTimerPeriodMS(), + nsITimer::TYPE_ONE_SHOT, "mClientBounceDetectionTimeout"); + NS_ENSURE_SUCCESS(rv, rv); + + // For each URL in URLs: Insert host to the navigable’s bounce tracking + // record's bounce set. + for (const nsACString& site : aSiteList) { + mBounceTrackingRecord->AddBounceHost(site); + } + + return NS_OK; +} + +nsresult BounceTrackingState::OnDocumentLoaded( + nsIPrincipal* aDocumentPrincipal) { + NS_ENSURE_ARG_POINTER(aDocumentPrincipal); + + // Assert: navigable’s bounce tracking record is not null. + NS_ENSURE_TRUE(mBounceTrackingRecord, NS_ERROR_FAILURE); + + // Logging + if (MOZ_LOG_TEST(gBounceTrackingProtectionLog, LogLevel::Debug)) { + nsAutoCString origin; + nsresult rv = aDocumentPrincipal->GetOrigin(origin); + if (NS_FAILED(rv)) { + origin = "err"; + } + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: origin: %s, mBounceTrackingRecord: %s", __FUNCTION__, + origin.get(), + mBounceTrackingRecord ? mBounceTrackingRecord->Describe().get() + : "null")); + } + + nsAutoCString siteHost; + if (!aDocumentPrincipal->GetIsContentPrincipal()) { + siteHost = ""; + } else { + nsresult rv = aDocumentPrincipal->GetBaseDomain(siteHost); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Set the navigable’s bounce tracking record's final host to the host of + // finalSite. + mBounceTrackingRecord->SetFinalHost(siteHost); + + return NS_OK; +} + +nsresult BounceTrackingState::OnCookieWrite(const nsACString& aSiteHost) { + NS_ENSURE_TRUE(!aSiteHost.IsEmpty(), NS_ERROR_FAILURE); + + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Verbose, + ("%s: OnCookieWrite: %s.", __FUNCTION__, + PromiseFlatCString(aSiteHost).get())); + + if (!mBounceTrackingRecord) { + return NS_OK; + } + + mBounceTrackingRecord->AddStorageAccessHost(aSiteHost); + return NS_OK; +} + +} // namespace mozilla diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingState.h b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingState.h new file mode 100644 index 0000000000..70deee5abe --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingState.h @@ -0,0 +1,156 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_BounceTrackingState_h +#define mozilla_BounceTrackingState_h + +#include "mozilla/WeakPtr.h" +#include "mozilla/OriginAttributes.h" +#include "nsIPrincipal.h" +#include "nsIWeakReferenceUtils.h" +#include "nsStringFwd.h" +#include "nsIWebProgressListener.h" +#include "nsWeakReference.h" + +class nsIChannel; +class nsITimer; +class nsIPrincipal; + +namespace mozilla { + +class BounceTrackingProtection; +class BounceTrackingRecord; + +namespace dom { +class CanonicalBrowsingContext; +class BrowsingContext; +class BrowsingContextWebProgress; +} // namespace dom + +/** + * This class manages the bounce tracking state for a given tab. It is attached + * to top-level CanonicalBrowsingContexts. + */ +class BounceTrackingState : public nsIWebProgressListener, + public nsSupportsWeakReference, + public SupportsWeakPtr { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIWEBPROGRESSLISTENER + + // Gets or creates an existing BrowsingContextState keyed by browserId. May + // return nullptr if the given web progress / browsing context is not suitable + // (see ShouldCreateBounceTrackingStateForWebProgress). + static already_AddRefed GetOrCreate( + dom::BrowsingContextWebProgress* aWebProgress); + + // Reset state for all BounceTrackingState instances this includes resetting + // BounceTrackingRecords and cancelling any running timers. + static void ResetAll(); + static void ResetAllForOriginAttributes( + const OriginAttributes& aOriginAttributes); + static void ResetAllForOriginAttributesPattern( + const OriginAttributesPattern& aPattern); + + BounceTrackingRecord* GetBounceTrackingRecord(); + + void ResetBounceTrackingRecord(); + + // Callback for when we received a response from the server and are about to + // create a document for the response. Calls into + // BounceTrackingState::OnResponseReceived. + nsresult OnDocumentStartRequest(nsIChannel* aChannel); + + // At the start of a navigation, either initialize a new bounce tracking + // record, or append a client-side redirect to the current bounce tracking + // record. + // Should only be called for top level content navigations. + nsresult OnStartNavigation(nsIPrincipal* aTriggeringPrincipal, + const bool aHasValidUserGestureActivation); + + // Record sites which have written cookies in the current extended + // navigation. + nsresult OnCookieWrite(const nsACString& aSiteHost); + + // Whether the given BrowsingContext should hold a BounceTrackingState + // instance to monitor bounce tracking navigations. + static bool ShouldCreateBounceTrackingStateForBC( + dom::CanonicalBrowsingContext* aBrowsingContext); + + // Check if there is a BounceTrackingState which current browsing context is + // associated with aSiteHost. + // This is an approximation for checking if a given site is currently loaded + // in the top level context, e.g. in a tab. See Bug 1842047 for adding a more + // accurate check that calls into the browser implementations. + static nsresult HasBounceTrackingStateForSite(const nsACString& aSiteHost, + bool& aResult); + + // Get the currently associated BrowsingContext. Returns nullptr if it has not + // been attached yet. + already_AddRefed CurrentBrowsingContext(); + + uint64_t GetBrowserId() { return mBrowserId; } + + const OriginAttributes& OriginAttributesRef(); + + // Create a string that describes this object. Used for logging. + nsCString Describe(); + + private: + explicit BounceTrackingState(); + virtual ~BounceTrackingState(); + + uint64_t mBrowserId{}; + + // OriginAttributes associated with the browser this state is attached to. + OriginAttributes mOriginAttributes; + + // Reference to the BounceTrackingProtection singleton. + RefPtr mBounceTrackingProtection; + + // Record to keep track of extended navigation data. Reset on extended + // navigation end. + RefPtr mBounceTrackingRecord; + + // Timer to wait to wait for a client redirect after a navigation ends. + RefPtr mClientBounceDetectionTimeout; + + // Reset state for all BounceTrackingState instances this includes resetting + // BounceTrackingRecords and cancelling any running timers. + // Optionally filter by OriginAttributes or OriginAttributesPattern. + static void Reset(const OriginAttributes* aOriginAttributes, + const OriginAttributesPattern* aPattern); + + // Whether the given web progress should hold a BounceTrackingState + // instance to monitor bounce tracking navigations. + static bool ShouldCreateBounceTrackingStateForWebProgress( + dom::BrowsingContextWebProgress* aWebProgress); + + // Init to be called after creation, attaches nsIWebProgressListener. + nsresult Init(dom::BrowsingContextWebProgress* aWebProgress); + + // When the response is received at the end of a navigation, fill the + // bounce set. + nsresult OnResponseReceived(const nsTArray& aSiteList); + + // When the document is loaded at the end of a navigation, update the + // final host. + nsresult OnDocumentLoaded(nsIPrincipal* aDocumentPrincipal); + + // TODO: Bug 1839918: Detection of stateful bounces. + + // Record sites which have accessed storage in the current extended + // navigation. + nsresult OnStorageAccess(); + + // Record sites which have activated service workers in the current + // extended navigation. + nsresult OnServiceWorkerActivation(); +}; + +} // namespace mozilla + +#endif diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStateGlobal.cpp b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStateGlobal.cpp new file mode 100644 index 0000000000..3481753431 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStateGlobal.cpp @@ -0,0 +1,190 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "BounceTrackingStateGlobal.h" +#include "BounceTrackingProtectionStorage.h" +#include "ErrorList.h" +#include "mozilla/Assertions.h" +#include "mozilla/Logging.h" +#include "nsIPrincipal.h" + +namespace mozilla { + +NS_IMPL_CYCLE_COLLECTION(BounceTrackingStateGlobal); + +extern LazyLogModule gBounceTrackingProtectionLog; + +BounceTrackingStateGlobal::BounceTrackingStateGlobal( + BounceTrackingProtectionStorage* aStorage, const OriginAttributes& aAttrs) + : mStorage(aStorage), mOriginAttributes(aAttrs) { + MOZ_ASSERT(aStorage); +} + +bool BounceTrackingStateGlobal::HasUserActivation( + const nsACString& aSiteHost) const { + return mUserActivation.Contains(aSiteHost); +} + +nsresult BounceTrackingStateGlobal::RecordUserActivation( + const nsACString& aSiteHost, PRTime aTime, bool aSkipStorage) { + NS_ENSURE_TRUE(aSiteHost.Length(), NS_ERROR_INVALID_ARG); + NS_ENSURE_TRUE(aTime > 0, NS_ERROR_INVALID_ARG); + + // A site must only be in one of the maps at a time. + bool hasRemoved = mBounceTrackers.Remove(aSiteHost); + + if (hasRemoved) { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: Removed bounce tracking candidate due to user activation: %s", + __FUNCTION__, PromiseFlatCString(aSiteHost).get())); + } + + mUserActivation.InsertOrUpdate(aSiteHost, aTime); + + if (aSkipStorage || !ShouldPersistToDisk()) { + return NS_OK; + } + + // Write the change to storage. + NS_ENSURE_TRUE(mStorage, NS_ERROR_FAILURE); + return mStorage->UpdateDBEntry( + mOriginAttributes, aSiteHost, + BounceTrackingProtectionStorage::EntryType::UserActivation, aTime); +} + +nsresult BounceTrackingStateGlobal::TestRemoveUserActivation( + const nsACString& aSiteHost) { + bool hasRemoved = mUserActivation.Remove(aSiteHost); + + // Avoid potentially removing a bounce tracking entry if there is no user + // activation entry. + if (!hasRemoved) { + return NS_OK; + } + + if (!ShouldPersistToDisk()) { + return NS_OK; + } + + // Write the change to storage. + NS_ENSURE_TRUE(mStorage, NS_ERROR_FAILURE); + return mStorage->DeleteDBEntries(&mOriginAttributes, aSiteHost); +} + +nsresult BounceTrackingStateGlobal::ClearUserActivationBefore(PRTime aTime) { + return ClearByTimeRange( + 0, Some(aTime), + Some(BounceTrackingProtectionStorage::EntryType::UserActivation)); +} + +nsresult BounceTrackingStateGlobal::ClearSiteHost(const nsACString& aSiteHost, + bool aSkipStorage) { + NS_ENSURE_TRUE(aSiteHost.Length(), NS_ERROR_INVALID_ARG); + + bool removedUserActivation = mUserActivation.Remove(aSiteHost); + bool removedBounceTracker = mBounceTrackers.Remove(aSiteHost); + if (removedUserActivation || removedBounceTracker) { + MOZ_ASSERT(removedUserActivation != removedBounceTracker, + "A site must only be in one of the maps at a time."); + } + + if (aSkipStorage || !ShouldPersistToDisk()) { + return NS_OK; + } + + NS_ENSURE_TRUE(mStorage, NS_ERROR_FAILURE); + return mStorage->DeleteDBEntries(&mOriginAttributes, aSiteHost); +} + +nsresult BounceTrackingStateGlobal::ClearByTimeRange( + PRTime aFrom, Maybe aTo, + Maybe aEntryType, + bool aSkipStorage) { + NS_ENSURE_ARG_MIN(aFrom, 0); + NS_ENSURE_TRUE(!aTo || aTo.value() > aFrom, NS_ERROR_INVALID_ARG); + + // Clear in memory user activation data. + if (aEntryType.isNothing() || + aEntryType.value() == + BounceTrackingProtectionStorage::EntryType::UserActivation) { + for (auto iter = mUserActivation.Iter(); !iter.Done(); iter.Next()) { + if (iter.Data() >= aFrom && + (aTo.isNothing() || iter.Data() <= aTo.value())) { + iter.Remove(); + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: Remove user activation for %s", __FUNCTION__, + PromiseFlatCString(iter.Key()).get())); + } + } + } + + // Clear in memory bounce tracker data. + if (aEntryType.isNothing() || + aEntryType.value() == + BounceTrackingProtectionStorage::EntryType::BounceTracker) { + for (auto iter = mBounceTrackers.Iter(); !iter.Done(); iter.Next()) { + if (iter.Data() >= aFrom && + (aTo.isNothing() || iter.Data() <= aTo.value())) { + iter.Remove(); + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, + ("%s: Remove bouncer tracker for %s", __FUNCTION__, + PromiseFlatCString(iter.Key()).get())); + } + } + } + + if (aSkipStorage || !ShouldPersistToDisk()) { + return NS_OK; + } + + // Write the change to storage. + NS_ENSURE_TRUE(mStorage, NS_ERROR_FAILURE); + return mStorage->DeleteDBEntriesInTimeRange(&mOriginAttributes, aFrom, aTo, + aEntryType); +} + +bool BounceTrackingStateGlobal::HasBounceTracker( + const nsACString& aSiteHost) const { + return mBounceTrackers.Contains(aSiteHost); +} + +nsresult BounceTrackingStateGlobal::RecordBounceTracker( + const nsACString& aSiteHost, PRTime aTime, bool aSkipStorage) { + NS_ENSURE_TRUE(aSiteHost.Length(), NS_ERROR_INVALID_ARG); + NS_ENSURE_TRUE(aTime > 0, NS_ERROR_INVALID_ARG); + + // Can not record a bounce tracker if the site has a user activation. + NS_ENSURE_TRUE(!mUserActivation.Contains(aSiteHost), NS_ERROR_FAILURE); + mBounceTrackers.InsertOrUpdate(aSiteHost, aTime); + + if (aSkipStorage || !ShouldPersistToDisk()) { + return NS_OK; + } + + // Write the change to storage. + NS_ENSURE_TRUE(mStorage, NS_ERROR_FAILURE); + return mStorage->UpdateDBEntry( + mOriginAttributes, aSiteHost, + BounceTrackingProtectionStorage::EntryType::BounceTracker, aTime); +} + +nsresult BounceTrackingStateGlobal::RemoveBounceTrackers( + const nsTArray& aSiteHosts) { + for (const nsCString& siteHost : aSiteHosts) { + mBounceTrackers.Remove(siteHost); + + // TODO: Create a bulk delete query. + if (ShouldPersistToDisk()) { + NS_ENSURE_TRUE(mStorage, NS_ERROR_FAILURE); + nsresult rv = mStorage->DeleteDBEntries(&mOriginAttributes, siteHost); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + return NS_OK; +} + +} // namespace mozilla diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStateGlobal.h b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStateGlobal.h new file mode 100644 index 0000000000..6680ceae6f --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStateGlobal.h @@ -0,0 +1,110 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_BounceTrackingStateGlobal_h +#define mozilla_BounceTrackingStateGlobal_h + +#include "BounceTrackingProtectionStorage.h" +#include "mozilla/WeakPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsTHashMap.h" +#include "nsISupports.h" + +namespace mozilla { + +/** + * This class holds the global state maps which are used to keep track of + * potential bounce trackers and user activations. + * @see BounceTrackingState for the per browser / tab state. + * + * Updates to the state maps are persisted to storage. + */ +class BounceTrackingStateGlobal final { + public: + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(BounceTrackingStateGlobal); + NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(BounceTrackingStateGlobal); + + BounceTrackingStateGlobal(BounceTrackingProtectionStorage* aStorage, + const OriginAttributes& aAttrs); + + bool IsPrivateBrowsing() const { + return mOriginAttributes.mPrivateBrowsingId != + nsIScriptSecurityManager::DEFAULT_PRIVATE_BROWSING_ID; + } + + bool ShouldPersistToDisk() const { return !IsPrivateBrowsing(); } + + bool HasUserActivation(const nsACString& aSiteHost) const; + + // Store a user interaction flag for the given host. This will remove the + // host from the bounce tracker map if it exists. + [[nodiscard]] nsresult RecordUserActivation(const nsACString& aSiteHost, + PRTime aTime, + bool aSkipStorage = false); + + // Test-only method to clear a user activation flag. + [[nodiscard]] nsresult TestRemoveUserActivation(const nsACString& aSiteHost); + + // Clear any user interactions that happened before aTime. + [[nodiscard]] nsresult ClearUserActivationBefore(PRTime aTime); + + bool HasBounceTracker(const nsACString& aSiteHost) const; + + // Store a bounce tracker flag for the given host. A host which received user + // interaction recently can not be recorded as a bounce tracker. + [[nodiscard]] nsresult RecordBounceTracker(const nsACString& aSiteHost, + PRTime aTime, + bool aSkipStorage = false); + + // Remove one or many bounce trackers identified by site host. + [[nodiscard]] nsresult RemoveBounceTrackers( + const nsTArray& aSiteHosts); + + [[nodiscard]] nsresult ClearSiteHost(const nsACString& aSiteHost, + bool aSkipStorage = false); + + [[nodiscard]] nsresult ClearByTimeRange( + PRTime aFrom, Maybe aTo = Nothing(), + Maybe aEntryType = Nothing(), + bool aSkipStorage = false); + + const nsTHashMap& UserActivationMapRef() { + return mUserActivation; + } + + const nsTHashMap& BounceTrackersMapRef() { + return mBounceTrackers; + } + + private: + ~BounceTrackingStateGlobal() = default; + + // The storage which manages this state global. Used to persist changes to + // this state global in storage. + // This needs to be a weak pointer to avoid BounceTrackingProtectionStorage + // and BounceTrackingStateGlobal holding strong references to each other + // leading to memory leaks. + WeakPtr mStorage; + + // Origin attributes this state global is associated with. e.g. if the state + // was associated with a PBM window this would set privateBrowsingId: 1. + OriginAttributes mOriginAttributes; + + // Map of site hosts to moments. The moments represent the most recent wall + // clock time at which the user activated a top-level document on the + // associated site host. + nsTHashMap mUserActivation{}; + + // Map of site hosts to moments. The moments represent the first wall clock + // time since the last execution of the bounce tracking timer at which a page + // on the given site host performed an action that could indicate stateful + // bounce tracking took place. + nsTHashMap mBounceTrackers{}; +}; + +} // namespace mozilla + +#endif diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStorageObserver.cpp b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStorageObserver.cpp new file mode 100644 index 0000000000..cc9c3ce971 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStorageObserver.cpp @@ -0,0 +1,107 @@ +/* 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 "BounceTrackingStorageObserver.h" + +#include "BounceTrackingState.h" +#include "mozilla/Services.h" +#include "mozilla/StaticPrefs_privacy.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/dom/CanonicalBrowsingContext.h" +#include "nsCOMPtr.h" +#include "nsICookieNotification.h" +#include "nsIObserverService.h" +#include "mozilla/dom/BrowsingContext.h" +#include "nsICookie.h" + +namespace mozilla { + +NS_IMPL_ISUPPORTS(BounceTrackingStorageObserver, nsIObserver); + +nsresult BounceTrackingStorageObserver::Init() { + MOZ_ASSERT(XRE_IsParentProcess()); + + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug, ("%s", __FUNCTION__)); + + // Add observers to listen for cookie changes. + nsCOMPtr observerService = + mozilla::services::GetObserverService(); + NS_ENSURE_TRUE(observerService, NS_ERROR_FAILURE); + + nsresult rv = observerService->AddObserver(this, "cookie-changed", false); + NS_ENSURE_SUCCESS(rv, rv); + return observerService->AddObserver(this, "private-cookie-changed", false); +} + +// nsIObserver +NS_IMETHODIMP +BounceTrackingStorageObserver::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Verbose, + ("Observe topic %s", aTopic)); + + NS_ENSURE_TRUE(aSubject, NS_ERROR_FAILURE); + + nsresult rv = NS_OK; + nsCOMPtr notification = + do_QueryInterface(aSubject, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsICookieNotification::Action action = notification->GetAction(); + // Filter for cookies added, changed or deleted. We don't care about other + // actions such as clearing the entire cookie store. + if (action != nsICookieNotification::COOKIE_ADDED && + action != nsICookieNotification::COOKIE_CHANGED && + action != nsICookieNotification::COOKIE_DELETED) { + return NS_OK; + } + + // Ensure the notification is associated with a BrowsingContext. It's only set + // for cases where a website updated a cookie. + RefPtr browsingContext; + rv = notification->GetBrowsingContext(getter_AddRefs(browsingContext)); + NS_ENSURE_SUCCESS(rv, rv); + if (!browsingContext) { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Verbose, + ("Could not get BC for CookieNotification.")); + return NS_OK; + } + + // Check if the cookie is partitioned. Partitioned cookies can not be used for + // bounce tracking. + nsCOMPtr cookie; + rv = notification->GetCookie(getter_AddRefs(cookie)); + NS_ENSURE_SUCCESS(rv, rv); + MOZ_ASSERT(cookie); + + if (!cookie->OriginAttributesNative().mPartitionKey.IsEmpty()) { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Verbose, + ("Skipping partitioned cookie.")); + return NS_OK; + } + + dom::BrowsingContext* topBC = browsingContext->Top(); + dom::BrowsingContextWebProgress* webProgress = + topBC->Canonical()->GetWebProgress(); + if (!webProgress) { + return NS_OK; + } + + RefPtr bounceTrackingState = + webProgress->GetBounceTrackingState(); + if (!bounceTrackingState) { + MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Verbose, + ("BC does not have BounceTrackingState.")); + return NS_OK; + } + + nsAutoCString baseDomain; + rv = notification->GetBaseDomain(baseDomain); + NS_ENSURE_SUCCESS(rv, rv); + + return bounceTrackingState->OnCookieWrite(baseDomain); +} + +} // namespace mozilla diff --git a/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStorageObserver.h b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStorageObserver.h new file mode 100644 index 0000000000..1e76c85a3c --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingStorageObserver.h @@ -0,0 +1,28 @@ +/* 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_BounceTrackingStorageObserver_h__ +#define mozilla_BounceTrackingStorageObserver_h__ + +#include "mozilla/Logging.h" +#include "nsIObserver.h" + +namespace mozilla { + +extern LazyLogModule gBounceTrackingProtectionLog; + +class BounceTrackingStorageObserver final : public nsIObserver { + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + + public: + BounceTrackingStorageObserver() = default; + nsresult Init(); + + private: + ~BounceTrackingStorageObserver() = default; +}; + +} // namespace mozilla + +#endif diff --git a/toolkit/components/antitracking/bouncetrackingprotection/components.conf b/toolkit/components/antitracking/bouncetrackingprotection/components.conf new file mode 100644 index 0000000000..c0b202bb18 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/components.conf @@ -0,0 +1,19 @@ +# -*- 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/. + +Classes = [ + { + 'name': 'BounceTrackingProtection', + 'cid': '{4866F748-29DA-4C10-8EAA-ED2F7851E6B1}', + 'interfaces': ['nsIBounceTrackingProtection'], + 'contract_ids': ['@mozilla.org/bounce-tracking-protection;1'], + 'type': 'mozilla::BounceTrackingProtection', + 'headers': ['/toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingProtection.h'], + 'singleton': True, + 'constructor': 'mozilla::BounceTrackingProtection::GetSingleton', + 'processes': ProcessSelector.MAIN_PROCESS_ONLY, + }, +] diff --git a/toolkit/components/antitracking/bouncetrackingprotection/moz.build b/toolkit/components/antitracking/bouncetrackingprotection/moz.build new file mode 100644 index 0000000000..09107bb782 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/moz.build @@ -0,0 +1,51 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +with Files("**"): + BUG_COMPONENT = ("Core", "Privacy: Anti-Tracking") + +XPIDL_SOURCES += [ + "nsIBounceTrackingProtection.idl", +] + +XPIDL_MODULE = "toolkit_antitracking" + + +XPCOM_MANIFESTS += [ + "components.conf", +] + +EXPORTS.mozilla += [ + "BounceTrackingProtection.h", + "BounceTrackingProtectionStorage.h", + "BounceTrackingRecord.h", + "BounceTrackingState.h", + "BounceTrackingStateGlobal.h", + "BounceTrackingStorageObserver.h", +] + +UNIFIED_SOURCES += [ + "BounceTrackingProtection.cpp", + "BounceTrackingProtectionStorage.cpp", + "BounceTrackingRecord.cpp", + "BounceTrackingState.cpp", + "BounceTrackingStateGlobal.cpp", + "BounceTrackingStorageObserver.cpp", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +LOCAL_INCLUDES += [ + "/dom/base", +] + +FINAL_LIBRARY = "xul" + +BROWSER_CHROME_MANIFESTS += [ + "test/browser/browser.toml", +] + +XPCSHELL_TESTS_MANIFESTS += ["test/xpcshell/xpcshell.toml"] diff --git a/toolkit/components/antitracking/bouncetrackingprotection/nsIBounceTrackingProtection.idl b/toolkit/components/antitracking/bouncetrackingprotection/nsIBounceTrackingProtection.idl new file mode 100644 index 0000000000..9ade9cb0ea --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/nsIBounceTrackingProtection.idl @@ -0,0 +1,46 @@ +/* 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" + +[scriptable, uuid(4866F748-29DA-4C10-8EAA-ED2F7851E6B1)] +interface nsIBounceTrackingProtection : nsISupports { + // Reset the global bounce tracking state, including the maps for tracking + // bounce tracker candidates and user activation. + void clearAll(); + + // Clear bounce tracking state for a specific site host and OriginAttributes pair. + [implicit_jscontext] + void clearBySiteHostAndOA(in ACString aSiteHost, in jsval originAttributes); + + // Clear bounce tracking state for a specific site host for all OriginAttributes. + void clearBySiteHost(in ACString aSiteHost); + + // Clear bounce tracking state for a specific time range. + void clearByTimeRange(in PRTime aFrom, in PRTime aTo); + + // Clear bounce tracking state for the given origin attributes. + void clearByOriginAttributesPattern(in AString aPattern); + + // Trigger the bounce tracking timer algorithm that clears state for + // classified bounce trackers. + [implicit_jscontext] + Promise testRunPurgeBounceTrackers(); + + // Getters and setters for user activation and bounce tracker state. + // These are used for testing purposes only. + // State is keyed by OriginAttributes. + + [implicit_jscontext] + Array testGetBounceTrackerCandidateHosts(in jsval originAttributes); + + [implicit_jscontext] + Array testGetUserActivationHosts(in jsval originAttributes); + + [implicit_jscontext] + void testAddBounceTrackerCandidate(in jsval originAttributes, in ACString aSiteHost, in PRTime aBounceTime); + + [implicit_jscontext] + void testAddUserActivation(in jsval originAttributes, in ACString aSiteHost, in PRTime aActivationTime); +}; diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser.toml b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser.toml new file mode 100644 index 0000000000..1c44d7804e --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser.toml @@ -0,0 +1,20 @@ +[DEFAULT] +head = "head.js" +prefs = [ + "privacy.bounceTrackingProtection.enabled=true", + "privacy.bounceTrackingProtection.enableTestMode=true", + "privacy.bounceTrackingProtection.bounceTrackingPurgeTimerPeriodSec=0", +] +support-files = [ + "file_start.html", + "file_bounce.sjs", + "file_bounce.html", +] + +["browser_bouncetracking_oa_isolation.js"] + +["browser_bouncetracking_purge.js"] + +["browser_bouncetracking_simple.js"] + +["browser_bouncetracking_stateful.js"] diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_oa_isolation.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_oa_isolation.js new file mode 100644 index 0000000000..12c2c943dd --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_oa_isolation.js @@ -0,0 +1,73 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.bounceTrackingProtection.requireStatefulBounces", true], + ["privacy.bounceTrackingProtection.bounceTrackingGracePeriodSec", 0], + ], + }); +}); + +// Tests that bounces in PBM don't affect state in normal browsing. +add_task(async function test_pbm_data_isolated() { + await runTestBounce({ + bounceType: "client", + setState: "cookie-client", + originAttributes: { privateBrowsingId: 1 }, + postBounceCallback: () => { + // After the PBM bounce assert that we haven't recorded any data for normal browsing. + Assert.equal( + bounceTrackingProtection.testGetBounceTrackerCandidateHosts({}).length, + 0, + "No bounce tracker candidates for normal browsing." + ); + Assert.equal( + bounceTrackingProtection.testGetUserActivationHosts({}).length, + 0, + "No user activations for normal browsing." + ); + }, + }); +}); + +// Tests that bounces in PBM don't affect state in normal browsing. +add_task(async function test_containers_isolated() { + await runTestBounce({ + bounceType: "server", + setState: "cookie-server", + originAttributes: { userContextId: 2 }, + postBounceCallback: () => { + // After the bounce in the container tab assert that we haven't recorded any data for normal browsing. + Assert.equal( + bounceTrackingProtection.testGetBounceTrackerCandidateHosts({}).length, + 0, + "No bounce tracker candidates for normal browsing." + ); + Assert.equal( + bounceTrackingProtection.testGetUserActivationHosts({}).length, + 0, + "No user activations for normal browsing." + ); + + // Or in another container tab. + Assert.equal( + bounceTrackingProtection.testGetBounceTrackerCandidateHosts({ + userContextId: 1, + }).length, + 0, + "No bounce tracker candidates for container tab 1." + ); + Assert.equal( + bounceTrackingProtection.testGetUserActivationHosts({ + userContextId: 1, + }).length, + 0, + "No user activations for container tab 1." + ); + }, + }); +}); diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_purge.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_purge.js new file mode 100644 index 0000000000..a8e98b80f0 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_purge.js @@ -0,0 +1,121 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const BOUNCE_TRACKING_GRACE_PERIOD_SEC = 30; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "privacy.bounceTrackingProtection.bounceTrackingGracePeriodSec", + BOUNCE_TRACKING_GRACE_PERIOD_SEC, + ], + ["privacy.bounceTrackingProtection.requireStatefulBounces", false], + ], + }); +}); + +/** + * The following tests ensure that sites that have open tabs are exempt from purging. + */ + +function initBounceTrackerState() { + bounceTrackingProtection.clearAll(); + + // Bounce time of 1 is out of the grace period which means we should purge. + bounceTrackingProtection.testAddBounceTrackerCandidate({}, "example.com", 1); + bounceTrackingProtection.testAddBounceTrackerCandidate({}, "example.net", 1); + + // Should not purge because within grace period. + let timestampWithinGracePeriod = + Date.now() - (BOUNCE_TRACKING_GRACE_PERIOD_SEC * 1000) / 2; + bounceTrackingProtection.testAddBounceTrackerCandidate( + {}, + "example.org", + timestampWithinGracePeriod * 1000 + ); +} + +add_task(async function test_purging_skip_open_foreground_tab() { + initBounceTrackerState(); + + // Foreground tab + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "https://example.com" + ); + Assert.deepEqual( + await bounceTrackingProtection.testRunPurgeBounceTrackers(), + ["example.net"], + "Should only purge example.net. example.org is within the grace period, example.com has an open tab." + ); + + info("Close the tab for example.com and test that it gets purged now."); + initBounceTrackerState(); + + BrowserTestUtils.removeTab(tab); + Assert.deepEqual( + (await bounceTrackingProtection.testRunPurgeBounceTrackers()).sort(), + ["example.net", "example.com"].sort(), + "example.com should have been purged now that it no longer has an open tab." + ); + + bounceTrackingProtection.clearAll(); +}); + +add_task(async function test_purging_skip_open_background_tab() { + initBounceTrackerState(); + + // Background tab + let tab = BrowserTestUtils.addTab(gBrowser, "https://example.com"); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + Assert.deepEqual( + await bounceTrackingProtection.testRunPurgeBounceTrackers(), + ["example.net"], + "Should only purge example.net. example.org is within the grace period, example.com has an open tab." + ); + + info("Close the tab for example.com and test that it gets purged now."); + initBounceTrackerState(); + + BrowserTestUtils.removeTab(tab); + Assert.deepEqual( + (await bounceTrackingProtection.testRunPurgeBounceTrackers()).sort(), + ["example.net", "example.com"].sort(), + "example.com should have been purged now that it no longer has an open tab." + ); + + bounceTrackingProtection.clearAll(); +}); + +add_task(async function test_purging_skip_open_tab_extra_window() { + initBounceTrackerState(); + + // Foreground tab in new window. + let win = await BrowserTestUtils.openNewBrowserWindow({}); + await BrowserTestUtils.openNewForegroundTab( + win.gBrowser, + "https://example.com" + ); + Assert.deepEqual( + await bounceTrackingProtection.testRunPurgeBounceTrackers(), + ["example.net"], + "Should only purge example.net. example.org is within the grace period, example.com has an open tab." + ); + + info( + "Close the window with the tab for example.com and test that it gets purged now." + ); + initBounceTrackerState(); + + await BrowserTestUtils.closeWindow(win); + Assert.deepEqual( + (await bounceTrackingProtection.testRunPurgeBounceTrackers()).sort(), + ["example.net", "example.com"].sort(), + "example.com should have been purged now that it no longer has an open tab." + ); + + bounceTrackingProtection.clearAll(); +}); diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_simple.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_simple.js new file mode 100644 index 0000000000..dfbd4d0fc0 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_simple.js @@ -0,0 +1,89 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.bounceTrackingProtection.requireStatefulBounces", false], + ["privacy.bounceTrackingProtection.bounceTrackingGracePeriodSec", 0], + ], + }); +}); + +// Tests a stateless bounce via client redirect. +add_task(async function test_client_bounce_simple() { + await runTestBounce({ bounceType: "client" }); +}); + +// Tests a stateless bounce via server redirect. +add_task(async function test_server_bounce_simple() { + await runTestBounce({ bounceType: "server" }); +}); + +// Tests a chained redirect consisting of a server and a client redirect. +add_task(async function test_bounce_chain() { + Assert.equal( + bounceTrackingProtection.testGetBounceTrackerCandidateHosts({}).length, + 0, + "No bounce tracker hosts initially." + ); + Assert.equal( + bounceTrackingProtection.testGetUserActivationHosts({}).length, + 0, + "No user activation hosts initially." + ); + + await BrowserTestUtils.withNewTab( + getBaseUrl(ORIGIN_A) + "file_start.html", + async browser => { + let promiseRecordBounces = waitForRecordBounces(browser); + + // The final destination after the bounces. + let targetURL = new URL(getBaseUrl(ORIGIN_B) + "file_start.html"); + + // Construct last hop. + let bounceChainUrlEnd = getBounceURL({ bounceType: "server", targetURL }); + // Construct first hop, nesting last hop. + let bounceChainUrlFull = getBounceURL({ + bounceType: "client", + redirectDelayMS: 100, + bounceOrigin: ORIGIN_TRACKER_B, + targetURL: bounceChainUrlEnd, + }); + + info("bounceChainUrl: " + bounceChainUrlFull.href); + + // Navigate through the bounce chain. + await navigateLinkClick(browser, bounceChainUrlFull); + + // Wait for the final site to be loaded which complete the BounceTrackingRecord. + await BrowserTestUtils.browserLoaded(browser, false, targetURL); + + // Navigate again with user gesture which triggers + // BounceTrackingProtection::RecordStatefulBounces. We could rely on the + // timeout (mClientBounceDetectionTimeout) here but that can cause races + // in debug where the load is quite slow. + await navigateLinkClick( + browser, + new URL(getBaseUrl(ORIGIN_C) + "file_start.html") + ); + + await promiseRecordBounces; + + Assert.deepEqual( + bounceTrackingProtection.testGetBounceTrackerCandidateHosts({}).sort(), + [SITE_TRACKER_B, SITE_TRACKER].sort(), + `Identified all bounce trackers in the redirect chain.` + ); + Assert.deepEqual( + bounceTrackingProtection.testGetUserActivationHosts({}).sort(), + [SITE_A, SITE_B].sort(), + "Should only have user activation for sites where we clicked links." + ); + + bounceTrackingProtection.clearAll(); + } + ); +}); diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful.js new file mode 100644 index 0000000000..e7fb4521a7 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_stateful.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +let bounceTrackingProtection; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.bounceTrackingProtection.requireStatefulBounces", true], + ["privacy.bounceTrackingProtection.bounceTrackingGracePeriodSec", 0], + ], + }); + bounceTrackingProtection = Cc[ + "@mozilla.org/bounce-tracking-protection;1" + ].getService(Ci.nsIBounceTrackingProtection); +}); + +// Cookie tests. + +add_task(async function test_bounce_stateful_cookies_client() { + info("Test client bounce with cookie."); + await runTestBounce({ + bounceType: "client", + setState: "cookie-client", + }); + info("Test client bounce without cookie."); + await runTestBounce({ + bounceType: "client", + setState: null, + expectCandidate: false, + expectPurge: false, + }); +}); + +add_task(async function test_bounce_stateful_cookies_server() { + info("Test server bounce with cookie."); + await runTestBounce({ + bounceType: "server", + setState: "cookie-server", + }); + info("Test server bounce without cookie."); + await runTestBounce({ + bounceType: "server", + setState: null, + expectCandidate: false, + expectPurge: false, + }); +}); + +// Storage tests. + +// TODO: Bug 1848406: Implement stateful bounce detection for localStorage. +add_task(async function test_bounce_stateful_localStorage() { + info("TODO: client bounce with localStorage."); + await runTestBounce({ + bounceType: "client", + setState: "localStorage", + expectCandidate: false, + expectPurge: false, + }); +}); diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.html b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.html new file mode 100644 index 0000000000..2756555fa5 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.html @@ -0,0 +1,59 @@ + + + + + + Bounce! + + + +

Nothing to see here...

+ + + diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.sjs b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.sjs new file mode 100644 index 0000000000..5e948a899b --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_bounce.sjs @@ -0,0 +1,19 @@ +function handleRequest(request, response) { + response.setHeader("Cache-Control", "no-cache", false); + + let query = new URLSearchParams(request.queryString); + + let setState = query.get("setState"); + if (setState == "cookie-server") { + response.setHeader("Set-Cookie", "foo=bar"); + } + + let statusCode = 302; + let statusCodeQuery = query.get("statusCode"); + if (statusCodeQuery) { + statusCode = Number.parseInt(statusCodeQuery); + } + + response.setStatusLine("1.1", statusCode, "Found"); + response.setHeader("Location", query.get("target"), false); +} diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_start.html b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_start.html new file mode 100644 index 0000000000..ded691023b --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/file_start.html @@ -0,0 +1,11 @@ + + + + + + Blank + + + + + diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/head.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/head.js new file mode 100644 index 0000000000..f5857b6919 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/head.js @@ -0,0 +1,275 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const SITE_A = "example.com"; +const ORIGIN_A = `https://${SITE_A}`; + +const SITE_B = "example.org"; +const ORIGIN_B = `https://${SITE_B}`; + +const SITE_C = "example.net"; +const ORIGIN_C = `https://${SITE_C}`; + +const SITE_TRACKER = "itisatracker.org"; +const ORIGIN_TRACKER = `https://${SITE_TRACKER}`; + +const SITE_TRACKER_B = "trackertest.org"; +// eslint-disable-next-line @microsoft/sdl/no-insecure-url +const ORIGIN_TRACKER_B = `http://${SITE_TRACKER_B}`; + +// Test message used for observing when the record-bounces method in +// BounceTrackingProtection.cpp has finished. +const OBSERVER_MSG_RECORD_BOUNCES_FINISHED = "test-record-bounces-finished"; + +const ROOT_DIR = getRootDirectory(gTestPath); + +XPCOMUtils.defineLazyServiceGetter( + this, + "bounceTrackingProtection", + "@mozilla.org/bounce-tracking-protection;1", + "nsIBounceTrackingProtection" +); + +/** + * Get the base url for the current test directory using the given origin. + * @param {string} origin - Origin to use in URL. + * @returns {string} - Generated URL as a string. + */ +function getBaseUrl(origin) { + return ROOT_DIR.replace("chrome://mochitests/content", origin); +} + +/** + * Constructs a url for an intermediate "bounce" hop which represents a tracker. + * @param {*} options - URL generation options. + * @param {('server'|'client')} options.bounceType - Redirect type to use for + * the bounce. + * @param {string} [options.bounceOrigin] - The origin of the bounce URL. + * @param {string} [options.targetURL] - URL to redirect to after the bounce. + * @param {("cookie"|null)} [options.setState] - What type of state should be set during + * the bounce. No state by default. + * @param {number} [options.statusCode] - HTTP status code to use for server + * side redirect. Only applies to bounceType == "server". + * @param {number} [options.redirectDelayMS] - How long to wait before + * redirecting. Only applies to bounceType == "client". + * @returns {URL} Generated URL which points to an endpoint performing the + * redirect. + */ +function getBounceURL({ + bounceType, + bounceOrigin = ORIGIN_TRACKER, + targetURL = new URL(getBaseUrl(ORIGIN_B) + "file_start.html"), + setState = null, + statusCode = 302, + redirectDelayMS = 50, +}) { + if (!["server", "client"].includes(bounceType)) { + throw new Error("Invalid bounceType"); + } + + let bounceFile = + bounceType == "client" ? "file_bounce.html" : "file_bounce.sjs"; + + let bounceUrl = new URL(getBaseUrl(bounceOrigin) + bounceFile); + + let { searchParams } = bounceUrl; + searchParams.set("target", targetURL.href); + if (setState) { + searchParams.set("setState", setState); + } + + if (bounceType == "server") { + searchParams.set("statusCode", statusCode); + } else if (bounceType == "client") { + searchParams.set("redirectDelay", redirectDelayMS); + } + + return bounceUrl; +} + +/** + * Insert an element with the given target and perform a synthesized + * click on it. + * @param {MozBrowser} browser - Browser to insert the link in. + * @param {URL} targetURL - Destination for navigation. + * @returns {Promise} Resolves once the click is done. Does not wait for + * navigation or load. + */ +async function navigateLinkClick(browser, targetURL) { + await SpecialPowers.spawn(browser, [targetURL.href], targetURL => { + let link = content.document.createElement("a"); + + link.href = targetURL; + link.textContent = targetURL; + // The link needs display: block, otherwise synthesizeMouseAtCenter doesn't + // hit it. + link.style.display = "block"; + + content.document.body.appendChild(link); + }); + + await BrowserTestUtils.synthesizeMouseAtCenter("a[href]", {}, browser); +} + +/** + * Wait for the record-bounces method to run for the given tab / browser. + * @param {browser} browser - Browser element which represents the tab we want + * to observe. + * @returns {Promise} Promise which resolves once the record-bounces method has + * run for the given browser. + */ +async function waitForRecordBounces(browser) { + return TestUtils.topicObserved( + OBSERVER_MSG_RECORD_BOUNCES_FINISHED, + subject => { + // Ensure the message was dispatched for the browser we're interested in. + let propBag = subject.QueryInterface(Ci.nsIPropertyBag2); + let browserId = propBag.getProperty("browserId"); + return browser.browsingContext.browserId == browserId; + } + ); +} + +/** + * Test helper which loads an initial blank page, then navigates to a url which + * performs a bounce. Checks that the bounce hosts are properly identified as + * trackers. + * @param {object} options - Test Options. + * @param {('server'|'client')} options.bounceType - Whether to perform a client + * or server side redirect. + * @param {('cookie-server'|'cookie-client'|'localStorage')} [options.setState] + * Type of state to set during the redirect. Defaults to non stateful redirect. + * @param {boolean} [options.expectCandidate=true] - Expect the redirecting site to be + * identified as a bounce tracker (candidate). + * @param {boolean} [options.expectPurge=true] - Expect the redirecting site to have + * its storage purged. + * @param {OriginAttributes} [options.originAttributes={}] - Origin attributes + * to use for the test. This determines whether the test is run in normal + * browsing, a private window or a container tab. By default the test is run + * in normal browsing. + * @param {function} [options.postBounceCallback] - Optional function to run after the + * bounce has completed. + */ +async function runTestBounce(options = {}) { + let { + bounceType, + setState = null, + expectCandidate = true, + expectPurge = true, + originAttributes = {}, + postBounceCallback = () => {}, + } = options; + info(`runTestBounce ${JSON.stringify(options)}`); + + Assert.equal( + bounceTrackingProtection.testGetBounceTrackerCandidateHosts( + originAttributes + ).length, + 0, + "No bounce tracker hosts initially." + ); + Assert.equal( + bounceTrackingProtection.testGetUserActivationHosts(originAttributes) + .length, + 0, + "No user activation hosts initially." + ); + + let win = window; + let { privateBrowsingId, userContextId } = originAttributes; + let usePrivateWindow = + privateBrowsingId != null && + privateBrowsingId != + Services.scriptSecurityManager.DEFAULT_PRIVATE_BROWSING_ID; + if (userContextId != null && userContextId > 0 && usePrivateWindow) { + throw new Error("userContextId is not supported in private windows"); + } + + if (usePrivateWindow) { + win = await BrowserTestUtils.openNewBrowserWindow({ private: true }); + } + + let tab = win.gBrowser.addTab(getBaseUrl(ORIGIN_A) + "file_start.html", { + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + userContextId, + }); + win.gBrowser.selectedTab = tab; + + let browser = tab.linkedBrowser; + await BrowserTestUtils.browserLoaded(browser); + + let promiseRecordBounces = waitForRecordBounces(browser); + + // The final destination after the bounce. + let targetURL = new URL(getBaseUrl(ORIGIN_B) + "file_start.html"); + + // Navigate through the bounce chain. + await navigateLinkClick( + browser, + getBounceURL({ bounceType, targetURL, setState }) + ); + + // Wait for the final site to be loaded which complete the BounceTrackingRecord. + await BrowserTestUtils.browserLoaded(browser, false, targetURL); + + // Navigate again with user gesture which triggers + // BounceTrackingProtection::RecordStatefulBounces. We could rely on the + // timeout (mClientBounceDetectionTimeout) here but that can cause races + // in debug where the load is quite slow. + await navigateLinkClick( + browser, + new URL(getBaseUrl(ORIGIN_C) + "file_start.html") + ); + + await promiseRecordBounces; + + Assert.deepEqual( + bounceTrackingProtection.testGetBounceTrackerCandidateHosts( + originAttributes + ), + expectCandidate ? [SITE_TRACKER] : [], + `Should ${ + expectCandidate ? "" : "not " + }have identified ${SITE_TRACKER} as a bounce tracker.` + ); + Assert.deepEqual( + bounceTrackingProtection + .testGetUserActivationHosts(originAttributes) + .sort(), + [SITE_A, SITE_B].sort(), + "Should only have user activation for sites where we clicked links." + ); + + // If the caller specified a function to run after the bounce, run it now. + await postBounceCallback(); + + Assert.deepEqual( + await bounceTrackingProtection.testRunPurgeBounceTrackers(), + expectPurge ? [SITE_TRACKER] : [], + `Should ${expectPurge ? "" : "not "}purge state for ${SITE_TRACKER}.` + ); + + // Clean up + BrowserTestUtils.removeTab(tab); + if (usePrivateWindow) { + await BrowserTestUtils.closeWindow(win); + + info( + "Closing the last PBM window should trigger a purge of all PBM state." + ); + Assert.ok( + !bounceTrackingProtection.testGetBounceTrackerCandidateHosts( + originAttributes + ).length, + "No bounce tracker hosts after closing private window." + ); + Assert.ok( + !bounceTrackingProtection.testGetUserActivationHosts(originAttributes) + .length, + "No user activation hosts after closing private window." + ); + } + bounceTrackingProtection.clearAll(); +} diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/marionette/manifest.toml b/toolkit/components/antitracking/bouncetrackingprotection/test/marionette/manifest.toml new file mode 100644 index 0000000000..7caad6eb15 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/marionette/manifest.toml @@ -0,0 +1,7 @@ +[DEFAULT] +prefs = [ + "privacy.bounceTrackingProtection.enabled=true", + "privacy.bounceTrackingProtection.enableTestMode=true", +] + +["test_bouncetracking_storage_persistence.py"] diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/marionette/test_bouncetracking_storage_persistence.py b/toolkit/components/antitracking/bouncetrackingprotection/test/marionette/test_bouncetracking_storage_persistence.py new file mode 100644 index 0000000000..afc3239839 --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/marionette/test_bouncetracking_storage_persistence.py @@ -0,0 +1,133 @@ +# 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/. + +from marionette_harness import MarionetteTestCase + +# Tests the persistence of the bounce tracking protection storage across +# restarts. + + +class BounceTrackingStoragePersistenceTestCase(MarionetteTestCase): + def setUp(self): + super(BounceTrackingStoragePersistenceTestCase, self).setUp() + self.marionette.enforce_gecko_prefs( + { + "privacy.bounceTrackingProtection.enabled": True, + "privacy.bounceTrackingProtection.enableTestMode": True, + } + ) + + self.marionette.set_context("chrome") + self.populate_state() + + def populate_state(self): + # Add some data to test persistence. + self.marionette.execute_script( + """ + let bounceTrackingProtection = Cc["@mozilla.org/bounce-tracking-protection;1"].getService( + Ci.nsIBounceTrackingProtection + ); + + bounceTrackingProtection.testAddBounceTrackerCandidate({}, "bouncetracker.net", Date.now() * 10000); + bounceTrackingProtection.testAddBounceTrackerCandidate({}, "bouncetracker.org", Date.now() * 10000); + bounceTrackingProtection.testAddBounceTrackerCandidate({ userContextId: 3 }, "tracker.com", Date.now() * 10000); + // A private browsing entry which must not be persisted across restarts. + bounceTrackingProtection.testAddBounceTrackerCandidate({ privateBrowsingId: 1 }, "tracker.net", Date.now() * 10000); + + bounceTrackingProtection.testAddUserActivation({}, "example.com", (Date.now() + 5000) * 10000); + // A private browsing entry which must not be persisted across restarts. + bounceTrackingProtection.testAddUserActivation({ privateBrowsingId: 1 }, "example.org", (Date.now() + 2000) * 10000); + """ + ) + + def test_state_after_restart(self): + self.marionette.restart(clean=False, in_app=True) + bounceTrackerCandidates = self.marionette.execute_script( + """ + let bounceTrackingProtection = Cc["@mozilla.org/bounce-tracking-protection;1"].getService( + Ci.nsIBounceTrackingProtection + ); + return bounceTrackingProtection.testGetBounceTrackerCandidateHosts({}).sort(); + """, + ) + self.assertEqual( + len(bounceTrackerCandidates), + 2, + msg="There should be two entries for default OA", + ) + self.assertEqual(bounceTrackerCandidates[0], "bouncetracker.net") + self.assertEqual(bounceTrackerCandidates[1], "bouncetracker.org") + + bounceTrackerCandidates = self.marionette.execute_script( + """ + let bounceTrackingProtection = Cc["@mozilla.org/bounce-tracking-protection;1"].getService( + Ci.nsIBounceTrackingProtection + ); + return bounceTrackingProtection.testGetBounceTrackerCandidateHosts({ userContextId: 3 }).sort(); + """, + ) + self.assertEqual( + len(bounceTrackerCandidates), + 1, + msg="There should be only one entry for user context 3", + ) + self.assertEqual(bounceTrackerCandidates[0], "tracker.com") + + # Unrelated user context should not have any entries. + bounceTrackerCandidates = self.marionette.execute_script( + """ + let bounceTrackingProtection = Cc["@mozilla.org/bounce-tracking-protection;1"].getService( + Ci.nsIBounceTrackingProtection + ); + return bounceTrackingProtection.testGetBounceTrackerCandidateHosts({ userContextId: 4 }).length; + """, + ) + self.assertEqual( + bounceTrackerCandidates, + 0, + msg="There should be no entries for user context 4", + ) + + # Private browsing entries should not be persisted across restarts. + bounceTrackerCandidates = self.marionette.execute_script( + """ + let bounceTrackingProtection = Cc["@mozilla.org/bounce-tracking-protection;1"].getService( + Ci.nsIBounceTrackingProtection + ); + return bounceTrackingProtection.testGetBounceTrackerCandidateHosts({ privateBrowsingId: 1 }).length; + """, + ) + self.assertEqual( + bounceTrackerCandidates, + 0, + msg="There should be no entries for private browsing", + ) + + userActivations = self.marionette.execute_script( + """ + let bounceTrackingProtection = Cc["@mozilla.org/bounce-tracking-protection;1"].getService( + Ci.nsIBounceTrackingProtection + ); + return bounceTrackingProtection.testGetUserActivationHosts({}).sort(); + """, + ) + self.assertEqual( + len(userActivations), + 1, + msg="There should be only one entry for user activation", + ) + self.assertEqual(userActivations[0], "example.com") + + # Private browsing entries should not be persisted across restarts. + userActivations = self.marionette.execute_script( + """ + let bounceTrackingProtection = Cc["@mozilla.org/bounce-tracking-protection;1"].getService( + Ci.nsIBounceTrackingProtection + ); + return bounceTrackingProtection.testGetUserActivationHosts({ privateBrowsingId: 1 }).length; + """, + ) + self.assertEqual( + userActivations, 0, msg="There should be no entries for private browsing" + ) diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_purge.js b/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_purge.js new file mode 100644 index 0000000000..5ede57a08b --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/test_bouncetracking_purge.js @@ -0,0 +1,307 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { SiteDataTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/SiteDataTestUtils.sys.mjs" +); + +let btp; +let bounceTrackingGracePeriodSec; +let bounceTrackingActivationLifetimeSec; + +/** + * Adds brackets to a host if it's an IPv6 address. + * @param {string} host - Host which may be an IPv6. + * @returns {string} bracketed IPv6 or host if host is not an IPv6. + */ +function maybeFixupIpv6(host) { + if (!host.includes(":")) { + return host; + } + return `[${host}]`; +} + +/** + * Adds cookies and indexedDB test data for the given host. + * @param {string} host + */ +async function addStateForHost(host) { + info(`adding state for host ${host}`); + SiteDataTestUtils.addToCookies({ host }); + await SiteDataTestUtils.addToIndexedDB(`https://${maybeFixupIpv6(host)}`); +} + +/** + * Checks if the given host as cookies or indexedDB data. + * @param {string} host + * @returns {boolean} + */ +async function hasStateForHost(host) { + let origin = `https://${maybeFixupIpv6(host)}`; + if (SiteDataTestUtils.hasCookies(origin)) { + return true; + } + return SiteDataTestUtils.hasIndexedDB(origin); +} + +/** + * Assert that there are no bounce tracker candidates or user activations + * recorded. + */ +function assertEmpty() { + Assert.equal( + btp.testGetBounceTrackerCandidateHosts({}).length, + 0, + "No tracker candidates." + ); + Assert.equal( + btp.testGetUserActivationHosts({}).length, + 0, + "No user activation hosts." + ); +} + +add_setup(function () { + // Need a profile to data clearing calls. + do_get_profile(); + + btp = Cc["@mozilla.org/bounce-tracking-protection;1"].getService( + Ci.nsIBounceTrackingProtection + ); + + // Reset global bounce tracking state. + btp.clearAll(); + + bounceTrackingGracePeriodSec = Services.prefs.getIntPref( + "privacy.bounceTrackingProtection.bounceTrackingGracePeriodSec" + ); + bounceTrackingActivationLifetimeSec = Services.prefs.getIntPref( + "privacy.bounceTrackingProtection.bounceTrackingActivationLifetimeSec" + ); +}); + +/** + * When both maps are empty running PurgeBounceTrackers should be a no-op. + */ +add_task(async function test_empty() { + assertEmpty(); + + info("Run PurgeBounceTrackers"); + await btp.testRunPurgeBounceTrackers(); + + assertEmpty(); +}); + +/** + * Tests that the PurgeBounceTrackers behaves as expected by adding site state + * and adding simulated bounce state and user activations. + */ +add_task(async function test_purge() { + let now = Date.now(); + + // Epoch in MS. + let timestampWithinGracePeriod = + now - (bounceTrackingGracePeriodSec * 1000) / 2; + let timestampWithinGracePeriod2 = + now - (bounceTrackingGracePeriodSec * 1000) / 4; + let timestampOutsideGracePeriodFiveSeconds = + now - (bounceTrackingGracePeriodSec + 5) * 1000; + let timestampOutsideGracePeriodThreeDays = + now - (bounceTrackingGracePeriodSec + 60 * 60 * 24 * 3) * 1000; + let timestampFuture = now + bounceTrackingGracePeriodSec * 1000 * 2; + + let timestampValidUserActivation = + now - (bounceTrackingActivationLifetimeSec * 1000) / 2; + let timestampExpiredUserActivationFourSeconds = + now - (bounceTrackingActivationLifetimeSec + 4) * 1000; + let timestampExpiredUserActivationTenDays = + now - (bounceTrackingActivationLifetimeSec + 60 * 60 * 24 * 10) * 1000; + + const TEST_TRACKERS = { + "example.com": { + bounceTime: timestampWithinGracePeriod, + userActivationTime: null, + message: "Should not purge within grace period.", + shouldPurge: bounceTrackingGracePeriodSec == 0, + }, + "example2.com": { + bounceTime: timestampWithinGracePeriod2, + userActivationTime: null, + message: "Should not purge within grace period (2).", + shouldPurge: bounceTrackingGracePeriodSec == 0, + }, + "example.net": { + bounceTime: timestampOutsideGracePeriodFiveSeconds, + userActivationTime: null, + message: "Should purge after grace period.", + shouldPurge: true, + }, + // Also ensure that clear data calls with IP sites succeed. + "1.2.3.4": { + bounceTime: timestampOutsideGracePeriodThreeDays, + userActivationTime: null, + message: "Should purge after grace period (2).", + shouldPurge: true, + }, + "2606:4700:4700::1111": { + bounceTime: timestampOutsideGracePeriodThreeDays, + userActivationTime: null, + message: "Should purge after grace period (3).", + shouldPurge: true, + }, + "example.org": { + bounceTime: timestampWithinGracePeriod, + userActivationTime: null, + message: "Should not purge within grace period.", + shouldPurge: false, + }, + "example2.org": { + bounceTime: timestampFuture, + userActivationTime: null, + message: "Should not purge for future bounce time (within grace period).", + shouldPurge: false, + }, + "1.1.1.1": { + bounceTime: null, + userActivationTime: timestampValidUserActivation, + message: "Should not purge without bounce (valid user activation).", + shouldPurge: false, + }, + // Also testing domains with trailing ".". + "mozilla.org.": { + bounceTime: null, + userActivationTime: timestampExpiredUserActivationFourSeconds, + message: "Should not purge without bounce (expired user activation).", + shouldPurge: false, + }, + "firefox.com": { + bounceTime: null, + userActivationTime: timestampExpiredUserActivationTenDays, + message: "Should not purge without bounce (expired user activation) (2).", + shouldPurge: false, + }, + }; + + info("Assert empty initially."); + assertEmpty(); + + info("Populate bounce and user activation sets."); + + let expectedBounceTrackerHosts = []; + let expectedUserActivationHosts = []; + + let expiredUserActivationHosts = []; + let expectedPurgedHosts = []; + + // This would normally happen over time while browsing. + let initPromises = Object.entries(TEST_TRACKERS).map( + async ([siteHost, { bounceTime, userActivationTime, shouldPurge }]) => { + // Add site state so we can later assert it has been purged. + await addStateForHost(siteHost); + + if (bounceTime != null) { + if (userActivationTime != null) { + throw new Error( + "Attempting to construct invalid map state. testGetBounceTrackerCandidateHosts({}) and testGetUserActivationHosts({}) must be disjoint." + ); + } + + expectedBounceTrackerHosts.push(siteHost); + + // Convert bounceTime timestamp to nanoseconds (PRTime). + info( + `Adding bounce. siteHost: ${siteHost}, bounceTime: ${bounceTime} ms` + ); + btp.testAddBounceTrackerCandidate({}, siteHost, bounceTime * 1000); + } + + if (userActivationTime != null) { + if (bounceTime != null) { + throw new Error( + "Attempting to construct invalid map state. testGetBounceTrackerCandidateHosts({}) and testGetUserActivationHosts({}) must be disjoint." + ); + } + + expectedUserActivationHosts.push(siteHost); + if ( + userActivationTime + bounceTrackingActivationLifetimeSec * 1000 > + now + ) { + expiredUserActivationHosts.push(siteHost); + } + + // Convert userActivationTime timestamp to nanoseconds (PRTime). + info( + `Adding user interaction. siteHost: ${siteHost}, userActivationTime: ${userActivationTime} ms` + ); + btp.testAddUserActivation({}, siteHost, userActivationTime * 1000); + } + + if (shouldPurge) { + expectedPurgedHosts.push(siteHost); + } + } + ); + await Promise.all(initPromises); + + info( + "Check that bounce and user activation data has been correctly recorded." + ); + Assert.deepEqual( + btp.testGetBounceTrackerCandidateHosts({}).sort(), + expectedBounceTrackerHosts.sort(), + "Has added bounce tracker hosts." + ); + Assert.deepEqual( + btp.testGetUserActivationHosts({}).sort(), + expectedUserActivationHosts.sort(), + "Has added user activation hosts." + ); + + info("Run PurgeBounceTrackers"); + let actualPurgedHosts = await btp.testRunPurgeBounceTrackers(); + + Assert.deepEqual( + actualPurgedHosts.sort(), + expectedPurgedHosts.sort(), + "Should have purged all expected hosts." + ); + + let expectedBounceTrackerHostsAfterPurge = expectedBounceTrackerHosts + .filter(host => !expectedPurgedHosts.includes(host)) + .sort(); + Assert.deepEqual( + btp.testGetBounceTrackerCandidateHosts({}).sort(), + expectedBounceTrackerHostsAfterPurge.sort(), + "After purge the bounce tracker candidate host set should be updated correctly." + ); + + Assert.deepEqual( + btp.testGetUserActivationHosts({}).sort(), + expiredUserActivationHosts.sort(), + "After purge any expired user activation records should have been removed" + ); + + info("Test that we actually purged the correct sites."); + for (let siteHost of expectedPurgedHosts) { + Assert.ok( + !(await hasStateForHost(siteHost)), + `Site ${siteHost} should no longer have state.` + ); + } + for (let siteHost of expectedBounceTrackerHostsAfterPurge) { + Assert.ok( + await hasStateForHost(siteHost), + `Site ${siteHost} should still have state.` + ); + } + + info("Reset bounce tracking state."); + btp.clearAll(); + assertEmpty(); + + info("Clean up site data."); + await SiteDataTestUtils.clear(); +}); diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/xpcshell.toml b/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/xpcshell.toml new file mode 100644 index 0000000000..16e270b85c --- /dev/null +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/xpcshell/xpcshell.toml @@ -0,0 +1,8 @@ +[DEFAULT] +prefs = [ + "privacy.bounceTrackingProtection.enabled=true", + "privacy.bounceTrackingProtection.enableTestMode=true", + "privacy.bounceTrackingProtection.bounceTrackingPurgeTimerPeriodSec=0", +] + +["test_bouncetracking_purge.js"] -- cgit v1.2.3