diff options
Diffstat (limited to 'toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingState.cpp')
-rw-r--r-- | toolkit/components/antitracking/bouncetrackingprotection/BounceTrackingState.cpp | 612 |
1 files changed, 612 insertions, 0 deletions
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<nsTHashMap<uint64_t, RefPtr<BounceTrackingState>>> + sBounceTrackingStates; + +static StaticRefPtr<BounceTrackingStorageObserver> 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> 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<nsUint64HashKey, RefPtr<BounceTrackingState>>(); + ClearOnShutdown(&sBounceTrackingStates); + } + + if (!sStorageObserver) { + sStorageObserver = new BounceTrackingStorageObserver(); + ClearOnShutdown(&sStorageObserver); + + DebugOnly<nsresult> 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> 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>& 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<BounceTrackingState>& state : + sBounceTrackingStates->Values()) { + RefPtr<dom::BrowsingContext> browsingContext = + state->CurrentBrowsingContext(); + + if (!browsingContext || browsingContext->IsDiscarded() || + browsingContext->IsInBFCache()) { + continue; + } + + RefPtr<dom::Element> embedderElement = + browsingContext->GetEmbedderElement(); + if (!embedderElement) { + continue; + } + + nsCOMPtr<nsIBrowser> browser = embedderElement->AsBrowser(); + if (!browser) { + continue; + } + + nsCOMPtr<nsIPrincipal> 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<dom::BrowsingContext> +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<nsILoadInfo> loadInfo; + nsresult rv = aChannel->GetLoadInfo(getter_AddRefs(loadInfo)); + NS_ENSURE_SUCCESS(rv, rv); + + // Collect uri list including any redirects. + nsTArray<nsCString> siteList; + + for (const nsCOMPtr<nsIRedirectHistoryEntry>& redirectHistoryEntry : + loadInfo->RedirectChain()) { + nsCOMPtr<nsIPrincipal> 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<nsIURI> channelURI; + rv = aChannel->GetURI(getter_AddRefs(channelURI)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIEffectiveTLDService> 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<nsCString>& 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<BounceTrackingState> 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 |