1440 lines
51 KiB
C++
1440 lines
51 KiB
C++
/* 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 "BounceTrackingAllowList.h"
|
||
#include "BounceTrackingProtectionStorage.h"
|
||
#include "BounceTrackingState.h"
|
||
#include "BounceTrackingRecord.h"
|
||
#include "BounceTrackingMapEntry.h"
|
||
#include "ClearDataCallback.h"
|
||
#include "PromiseNativeWrapper.h"
|
||
|
||
#include "BounceTrackingStateGlobal.h"
|
||
#include "ErrorList.h"
|
||
#include "mozilla/AlreadyAddRefed.h"
|
||
#include "mozilla/ClearOnShutdown.h"
|
||
#include "mozilla/Logging.h"
|
||
#include "mozilla/Maybe.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 "nsIPermissionManager.h"
|
||
#include "nsIPrincipal.h"
|
||
#include "nsISupports.h"
|
||
#include "nsISupportsPrimitives.h"
|
||
#include "nsServiceManagerUtils.h"
|
||
#include "nscore.h"
|
||
#include "prtime.h"
|
||
#include "mozilla/dom/BrowsingContext.h"
|
||
#include "xpcpublic.h"
|
||
#include "mozilla/glean/AntitrackingBouncetrackingprotectionMetrics.h"
|
||
#include "mozilla/ContentBlockingLog.h"
|
||
#include "mozilla/glean/GleanPings.h"
|
||
#include "mozilla/dom/WindowContext.h"
|
||
#include "mozilla/dom/WindowGlobalChild.h"
|
||
#include "mozilla/dom/WindowGlobalParent.h"
|
||
#include "nsIConsoleService.h"
|
||
#include "mozilla/intl/Localization.h"
|
||
#include "mozilla/browser/NimbusFeatures.h"
|
||
|
||
#define TEST_OBSERVER_MSG_RECORD_BOUNCES_FINISHED "test-record-bounces-finished"
|
||
|
||
namespace mozilla {
|
||
|
||
NS_IMPL_ISUPPORTS(BounceTrackingProtection, nsIObserver,
|
||
nsISupportsWeakReference, nsIBounceTrackingProtection);
|
||
|
||
LazyLogModule gBounceTrackingProtectionLog("BounceTrackingProtection");
|
||
|
||
static StaticRefPtr<BounceTrackingProtection> sBounceTrackingProtection;
|
||
static bool sInitFailed = false;
|
||
|
||
Maybe<uint32_t> BounceTrackingProtection::sLastRecordedModeTelemetry;
|
||
|
||
static const char kBTPModePref[] = "privacy.bounceTrackingProtection.mode";
|
||
|
||
// static
|
||
already_AddRefed<BounceTrackingProtection>
|
||
BounceTrackingProtection::GetSingleton() {
|
||
MOZ_ASSERT(XRE_IsParentProcess());
|
||
|
||
// Init previously failed, don't try again.
|
||
if (sInitFailed) {
|
||
return nullptr;
|
||
}
|
||
|
||
RecordModePrefTelemetry();
|
||
|
||
// Feature is disabled.
|
||
if (StaticPrefs::privacy_bounceTrackingProtection_mode() ==
|
||
nsIBounceTrackingProtection::MODE_DISABLED) {
|
||
return nullptr;
|
||
}
|
||
|
||
// Feature is enabled, lazily create singleton instance.
|
||
if (!sBounceTrackingProtection) {
|
||
sBounceTrackingProtection = new BounceTrackingProtection();
|
||
RunOnShutdown([] {
|
||
if (sBounceTrackingProtection &&
|
||
sBounceTrackingProtection->mRemoteExceptionList) {
|
||
Unused << sBounceTrackingProtection->mRemoteExceptionList->Shutdown();
|
||
}
|
||
sBounceTrackingProtection = nullptr;
|
||
});
|
||
|
||
nsresult rv = sBounceTrackingProtection->Init();
|
||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||
sInitFailed = true;
|
||
return nullptr;
|
||
}
|
||
}
|
||
|
||
return do_AddRef(sBounceTrackingProtection);
|
||
}
|
||
|
||
// static
|
||
void BounceTrackingProtection::RecordModePrefTelemetry() {
|
||
uint32_t featureMode = StaticPrefs::privacy_bounceTrackingProtection_mode();
|
||
|
||
// Record mode telemetry, but only if the mode changed.
|
||
if (sLastRecordedModeTelemetry.isNothing() ||
|
||
sLastRecordedModeTelemetry.value() != featureMode) {
|
||
glean::bounce_tracking_protection::mode.Set(featureMode);
|
||
sLastRecordedModeTelemetry = Some(featureMode);
|
||
}
|
||
}
|
||
|
||
nsresult BounceTrackingProtection::Init() {
|
||
MOZ_ASSERT(StaticPrefs::privacy_bounceTrackingProtection_mode() !=
|
||
nsIBounceTrackingProtection::MODE_DISABLED,
|
||
"Mode pref must have an enabled state for init to be called.");
|
||
MOZ_LOG(
|
||
gBounceTrackingProtectionLog, LogLevel::Info,
|
||
("Init BounceTrackingProtection. Config: mode: %d, "
|
||
"bounceTrackingActivationLifetimeSec: %d, bounceTrackingGracePeriodSec: "
|
||
"%d, bounceTrackingPurgeTimerPeriodSec: %d, "
|
||
"clientBounceDetectionTimerPeriodMS: %d, requireStatefulBounces: %d, "
|
||
"HasMigratedUserActivationData: %d",
|
||
StaticPrefs::privacy_bounceTrackingProtection_mode(),
|
||
StaticPrefs::
|
||
privacy_bounceTrackingProtection_bounceTrackingActivationLifetimeSec(),
|
||
StaticPrefs::
|
||
privacy_bounceTrackingProtection_bounceTrackingGracePeriodSec(),
|
||
StaticPrefs::
|
||
privacy_bounceTrackingProtection_bounceTrackingPurgeTimerPeriodSec(),
|
||
StaticPrefs::
|
||
privacy_bounceTrackingProtection_clientBounceDetectionTimerPeriodMS(),
|
||
StaticPrefs::privacy_bounceTrackingProtection_requireStatefulBounces(),
|
||
StaticPrefs::
|
||
privacy_bounceTrackingProtection_hasMigratedUserActivationData()));
|
||
|
||
mStorage = new BounceTrackingProtectionStorage();
|
||
|
||
nsresult rv = mStorage->Init();
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
rv = MaybeMigrateUserInteractionPermissions();
|
||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||
MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Error,
|
||
("user activation permission migration failed"));
|
||
}
|
||
|
||
// Register feature pref listener which dynamically enables or disables the
|
||
// feature depending on feature pref state.
|
||
rv = Preferences::RegisterCallback(&BounceTrackingProtection::OnPrefChange,
|
||
kBTPModePref);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
// Run the remaining init logic.
|
||
return OnModeChange(true);
|
||
}
|
||
|
||
nsresult BounceTrackingProtection::UpdateBounceTrackingPurgeTimer(
|
||
bool aShouldEnable) {
|
||
// Cancel the existing timer.
|
||
// If disabling: we're done now.
|
||
// If enabling: schedule a new timer so interval changes (as controlled by the
|
||
// pref) are taken into account.
|
||
if (mBounceTrackingPurgeTimer) {
|
||
mBounceTrackingPurgeTimer->Cancel();
|
||
mBounceTrackingPurgeTimer = nullptr;
|
||
}
|
||
|
||
if (!aShouldEnable) {
|
||
return NS_OK;
|
||
}
|
||
|
||
// 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 NS_OK;
|
||
}
|
||
|
||
MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
|
||
("Scheduling mBounceTrackingPurgeTimer. Interval: %d seconds.",
|
||
purgeTimerPeriod));
|
||
|
||
return 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");
|
||
}
|
||
|
||
// static
|
||
void BounceTrackingProtection::OnPrefChange(const char* aPref, void* aData) {
|
||
MOZ_ASSERT(sBounceTrackingProtection);
|
||
MOZ_ASSERT(strcmp(kBTPModePref, aPref) == 0);
|
||
|
||
RecordModePrefTelemetry();
|
||
|
||
sBounceTrackingProtection->OnModeChange(false);
|
||
}
|
||
|
||
nsresult BounceTrackingProtection::OnModeChange(bool aIsStartup) {
|
||
// Get feature mode from pref and ensure its within bounds.
|
||
uint8_t modeInt = StaticPrefs::privacy_bounceTrackingProtection_mode();
|
||
NS_ENSURE_TRUE(modeInt <= nsIBounceTrackingProtection::MAX_MODE_VALUE,
|
||
NS_ERROR_FAILURE);
|
||
nsIBounceTrackingProtection::Modes mode =
|
||
static_cast<nsIBounceTrackingProtection::Modes>(modeInt);
|
||
|
||
MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
|
||
("%s: mode: %d.", __FUNCTION__, mode));
|
||
if (sInitFailed) {
|
||
return NS_ERROR_FAILURE;
|
||
}
|
||
|
||
nsresult result = NS_OK;
|
||
|
||
if (!aIsStartup) {
|
||
// Clear bounce tracker candidate map for any mode change so it's not leaked
|
||
// into other modes. For example if we switch from dry-run mode into fully
|
||
// enabled we want a clean slate to not purge trackers that we've classified
|
||
// in dry-run mode. User activation data must be kept to avoid false
|
||
// positives.
|
||
MOZ_ASSERT(mStorage);
|
||
result = mStorage->ClearByType(
|
||
BounceTrackingProtectionStorage::EntryType::BounceTracker);
|
||
}
|
||
|
||
// On disable
|
||
if (mode == nsIBounceTrackingProtection::MODE_DISABLED ||
|
||
mode == nsIBounceTrackingProtection::MODE_ENABLED_STANDBY) {
|
||
// No further cleanup needed if we're just starting up.
|
||
if (aIsStartup) {
|
||
MOZ_ASSERT(!mStorageObserver);
|
||
MOZ_ASSERT(!mBounceTrackingPurgeTimer);
|
||
return result;
|
||
}
|
||
|
||
// Destroy storage observer to stop receiving storage notifications.
|
||
mStorageObserver = nullptr;
|
||
|
||
// Stop regular purging.
|
||
nsresult rv = UpdateBounceTrackingPurgeTimer(false);
|
||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||
result = rv;
|
||
// Even if this step fails try to do more cleanup.
|
||
}
|
||
|
||
// Clear all per-tab state.
|
||
BounceTrackingState::DestroyAll();
|
||
return result;
|
||
}
|
||
|
||
// On enable
|
||
MOZ_ASSERT(mode == nsIBounceTrackingProtection::MODE_ENABLED ||
|
||
mode == nsIBounceTrackingProtection::MODE_ENABLED_DRY_RUN);
|
||
|
||
// Create and init storage observer.
|
||
mStorageObserver = new BounceTrackingStorageObserver();
|
||
nsresult rv = mStorageObserver->Init();
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
// Schedule regular purging.
|
||
rv = UpdateBounceTrackingPurgeTimer(true);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
return result;
|
||
}
|
||
|
||
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.
|
||
const Maybe<BounceTrackingRecord>& record =
|
||
aBounceTrackingState->GetBounceTrackingRecord();
|
||
NS_ENSURE_TRUE(record, NS_ERROR_FAILURE);
|
||
|
||
// Get the bounce tracker map and the user activation map.
|
||
RefPtr<BounceTrackingStateGlobal> globalState =
|
||
mStorage->GetOrCreateStateGlobal(aBounceTrackingState);
|
||
MOZ_ASSERT(globalState);
|
||
|
||
nsTArray<nsCString> classifiedHosts;
|
||
|
||
// For each host in navigable’s bounce tracking record's bounce set:
|
||
for (const nsACString& host : record->GetBounceHosts()) {
|
||
// Skip "null" entries, they are only used for logging purposes.
|
||
if (host.EqualsLiteral("null")) {
|
||
continue;
|
||
}
|
||
|
||
// 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;
|
||
}
|
||
|
||
classifiedHosts.AppendElement(host);
|
||
|
||
MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Info,
|
||
("%s: Added bounce tracker candidate. siteHost: %s, "
|
||
"aBounceTrackingState: %s",
|
||
__FUNCTION__, PromiseFlatCString(host).get(),
|
||
aBounceTrackingState->Describe().get()));
|
||
}
|
||
|
||
// Set navigable’s bounce tracking record to null.
|
||
aBounceTrackingState->ResetBounceTrackingRecord();
|
||
MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
|
||
("%s: Done, reset aBounceTrackingState: %s", __FUNCTION__,
|
||
aBounceTrackingState->Describe().get()));
|
||
|
||
// Log a message to the web console for each classified host.
|
||
nsresult rv = LogBounceTrackersClassifiedToWebConsole(aBounceTrackingState,
|
||
classifiedHosts);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
// If running in test automation, dispatch an observer message indicating
|
||
// we're finished recording bounces.
|
||
if (StaticPrefs::privacy_bounceTrackingProtection_enableTestMode()) {
|
||
nsCOMPtr<nsIObserverService> obsSvc =
|
||
mozilla::services::GetObserverService();
|
||
NS_ENSURE_TRUE(obsSvc, NS_ERROR_FAILURE);
|
||
|
||
RefPtr<nsHashPropertyBag> 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, Maybe<PRTime> aActivationTime,
|
||
dom::CanonicalBrowsingContext* aTopBrowsingContext) {
|
||
MOZ_ASSERT(XRE_IsParentProcess());
|
||
NS_ENSURE_ARG_POINTER(aPrincipal);
|
||
NS_ENSURE_TRUE(!aTopBrowsingContext || aTopBrowsingContext->IsTop(),
|
||
NS_ERROR_INVALID_ARG);
|
||
|
||
RefPtr<BounceTrackingProtection> btp = GetSingleton();
|
||
// May be nullptr if feature is disabled.
|
||
if (!btp) {
|
||
return NS_OK;
|
||
}
|
||
|
||
if (!BounceTrackingState::ShouldTrackPrincipal(aPrincipal)) {
|
||
return NS_OK;
|
||
}
|
||
|
||
nsAutoCString siteHost;
|
||
nsresult rv = aPrincipal->GetBaseDomain(siteHost);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
if (MOZ_LOG_TEST(gBounceTrackingProtectionLog, LogLevel::Debug)) {
|
||
nsAutoCString oaStr;
|
||
aPrincipal->OriginAttributesRef().CreateSuffix(oaStr);
|
||
|
||
MOZ_LOG_FMT(gBounceTrackingProtectionLog, LogLevel::Debug,
|
||
"{}: originAttributes: {}, siteHost: {}", __FUNCTION__, oaStr,
|
||
siteHost.get());
|
||
}
|
||
|
||
RefPtr<BounceTrackingStateGlobal> globalState =
|
||
btp->mStorage->GetOrCreateStateGlobal(aPrincipal);
|
||
MOZ_ASSERT(globalState);
|
||
|
||
// aActivationTime defaults to current time if no value is provided.
|
||
rv = globalState->RecordUserActivation(siteHost,
|
||
aActivationTime.valueOr(PR_Now()));
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
if (aTopBrowsingContext) {
|
||
MOZ_ASSERT(aTopBrowsingContext->IsTop());
|
||
dom::BrowsingContextWebProgress* webProgress =
|
||
aTopBrowsingContext->GetWebProgress();
|
||
NS_ENSURE_TRUE(webProgress, NS_ERROR_FAILURE);
|
||
|
||
RefPtr<BounceTrackingState> bounceTrackingState =
|
||
webProgress->GetBounceTrackingState();
|
||
// We may not always get a BounceTrackingState, e.g the feature is disabled,
|
||
// if the user has opted into all cookies, no cookies, or some other case.
|
||
// If we don't have it, just return OK here.
|
||
NS_ENSURE_TRUE(bounceTrackingState, NS_OK);
|
||
|
||
return bounceTrackingState->OnUserActivation(siteHost);
|
||
}
|
||
return NS_OK;
|
||
}
|
||
|
||
nsresult BounceTrackingProtection::RecordUserActivation(
|
||
dom::WindowContext* aWindowContext) {
|
||
NS_ENSURE_ARG_POINTER(aWindowContext);
|
||
|
||
if (XRE_IsContentProcess()) {
|
||
dom::WindowGlobalChild* wgc = aWindowContext->GetWindowGlobalChild();
|
||
NS_ENSURE_TRUE(wgc, NS_ERROR_FAILURE);
|
||
NS_ENSURE_TRUE(wgc->SendRecordUserActivationForBTP(), NS_ERROR_FAILURE);
|
||
return NS_OK;
|
||
}
|
||
MOZ_ASSERT(XRE_IsParentProcess());
|
||
|
||
dom::WindowGlobalParent* wgp = aWindowContext->Canonical();
|
||
MOZ_ASSERT(wgp);
|
||
|
||
NS_ENSURE_TRUE(wgp->RecvRecordUserActivationForBTP(), NS_ERROR_FAILURE);
|
||
|
||
return NS_OK;
|
||
}
|
||
|
||
NS_IMETHODIMP
|
||
BounceTrackingProtection::Observe(nsISupports* aSubject, const char* aTopic,
|
||
const char16_t* aData) {
|
||
MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
|
||
("%s: aTopic: %s", __FUNCTION__, aTopic));
|
||
|
||
if (!strcmp(aTopic, "idle-daily")) {
|
||
#ifndef MOZ_WIDGET_ANDROID // OHTTP is not supported on Android.
|
||
// Submit custom telemetry ping.
|
||
glean_pings::BounceTrackingProtection.Submit();
|
||
#endif // #ifndef MOZ_WIDGET_ANDROID
|
||
}
|
||
return NS_OK;
|
||
}
|
||
|
||
NS_IMETHODIMP
|
||
BounceTrackingProtection::TestGetBounceTrackerCandidateHosts(
|
||
JS::Handle<JS::Value> aOriginAttributes, JSContext* aCx,
|
||
nsTArray<RefPtr<nsIBounceTrackingMapEntry>>& aCandidates) {
|
||
MOZ_ASSERT(aCx);
|
||
|
||
OriginAttributes oa;
|
||
if (!aOriginAttributes.isObject() || !oa.Init(aCx, aOriginAttributes)) {
|
||
return NS_ERROR_INVALID_ARG;
|
||
}
|
||
|
||
RefPtr<BounceTrackingStateGlobal> globalState = mStorage->GetStateGlobal(oa);
|
||
if (!globalState) {
|
||
return NS_OK;
|
||
}
|
||
|
||
for (auto iter = globalState->BounceTrackersMapRef().ConstIter();
|
||
!iter.Done(); iter.Next()) {
|
||
RefPtr<nsIBounceTrackingMapEntry> candidate = new BounceTrackingMapEntry(
|
||
globalState->OriginAttributesRef(), iter.Key(), iter.Data());
|
||
aCandidates.AppendElement(candidate);
|
||
}
|
||
|
||
return NS_OK;
|
||
}
|
||
|
||
NS_IMETHODIMP
|
||
BounceTrackingProtection::TestGetUserActivationHosts(
|
||
JS::Handle<JS::Value> aOriginAttributes, JSContext* aCx,
|
||
nsTArray<RefPtr<nsIBounceTrackingMapEntry>>& aHosts) {
|
||
MOZ_ASSERT(aCx);
|
||
|
||
OriginAttributes oa;
|
||
if (!aOriginAttributes.isObject() || !oa.Init(aCx, aOriginAttributes)) {
|
||
return NS_ERROR_INVALID_ARG;
|
||
}
|
||
|
||
RefPtr<BounceTrackingStateGlobal> globalState = mStorage->GetStateGlobal(oa);
|
||
if (!globalState) {
|
||
return NS_OK;
|
||
}
|
||
|
||
for (auto iter = globalState->UserActivationMapRef().ConstIter();
|
||
!iter.Done(); iter.Next()) {
|
||
RefPtr<nsIBounceTrackingMapEntry> candidate = new BounceTrackingMapEntry(
|
||
globalState->OriginAttributesRef(), iter.Key(), iter.Data());
|
||
aHosts.AppendElement(candidate);
|
||
}
|
||
|
||
return NS_OK;
|
||
}
|
||
|
||
NS_IMETHODIMP
|
||
BounceTrackingProtection::TestGetRecentlyPurgedTrackers(
|
||
JS::Handle<JS::Value> aOriginAttributes, JSContext* aCx,
|
||
nsTArray<RefPtr<nsIBounceTrackingPurgeEntry>>& aPurgedTrackers) {
|
||
MOZ_ASSERT(aCx);
|
||
|
||
OriginAttributes oa;
|
||
if (!aOriginAttributes.isObject() || !oa.Init(aCx, aOriginAttributes)) {
|
||
return NS_ERROR_INVALID_ARG;
|
||
}
|
||
|
||
RefPtr<BounceTrackingStateGlobal> globalState = mStorage->GetStateGlobal(oa);
|
||
if (!globalState) {
|
||
return NS_OK;
|
||
}
|
||
|
||
nsTArray<RefPtr<BounceTrackingPurgeEntry>> purgeEntriesSorted;
|
||
for (auto iter = globalState->RecentPurgesMapRef().ConstIter(); !iter.Done();
|
||
iter.Next()) {
|
||
for (const auto& entry : iter.Data()) {
|
||
purgeEntriesSorted.InsertElementSorted(entry, PurgeEntryTimeComparator{});
|
||
}
|
||
}
|
||
|
||
aPurgedTrackers.AppendElements(purgeEntriesSorted);
|
||
|
||
return NS_OK;
|
||
}
|
||
|
||
NS_IMETHODIMP
|
||
BounceTrackingProtection::ClearAll() {
|
||
BounceTrackingState::ResetAll();
|
||
return mStorage->Clear();
|
||
}
|
||
|
||
NS_IMETHODIMP
|
||
BounceTrackingProtection::ClearBySiteHostAndOriginAttributes(
|
||
const nsACString& aSiteHost, JS::Handle<JS::Value> 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::ClearBySiteHostAndOriginAttributesPattern(
|
||
const nsACString& aSiteHost, JS::Handle<JS::Value> aOriginAttributesPattern,
|
||
JSContext* aCx) {
|
||
NS_ENSURE_ARG_POINTER(aCx);
|
||
|
||
OriginAttributesPattern pattern;
|
||
if (!aOriginAttributesPattern.isObject() ||
|
||
!pattern.Init(aCx, aOriginAttributesPattern)) {
|
||
return NS_ERROR_INVALID_ARG;
|
||
}
|
||
|
||
// Clear per-tab state.
|
||
BounceTrackingState::ResetAllForOriginAttributesPattern(pattern);
|
||
|
||
// Clear global state including on-disk state.
|
||
return mStorage->ClearByOriginAttributesPattern(pattern,
|
||
Some(nsCString(aSiteHost)));
|
||
}
|
||
|
||
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::AddSiteHostExceptions(
|
||
const nsTArray<nsCString>& aSiteHosts) {
|
||
for (const auto& host : aSiteHosts) {
|
||
mRemoteSiteHostExceptions.Insert(host);
|
||
}
|
||
|
||
return NS_OK;
|
||
}
|
||
|
||
NS_IMETHODIMP
|
||
BounceTrackingProtection::RemoveSiteHostExceptions(
|
||
const nsTArray<nsCString>& aSiteHosts) {
|
||
for (const auto& host : aSiteHosts) {
|
||
mRemoteSiteHostExceptions.Remove(host);
|
||
}
|
||
|
||
return NS_OK;
|
||
}
|
||
|
||
NS_IMETHODIMP
|
||
BounceTrackingProtection::HasRecentlyPurgedSite(const nsACString& aSiteHost,
|
||
bool* aResult) {
|
||
NS_ENSURE_ARG_POINTER(aResult);
|
||
*aResult = false;
|
||
NS_ENSURE_TRUE(!aSiteHost.IsEmpty(), NS_ERROR_INVALID_ARG);
|
||
|
||
// Look for a purge log entry matching the site and return greedily if we find
|
||
// one. We look through all state globals because we want to search across all
|
||
// OriginAttributes.
|
||
for (const auto& entry : mStorage->StateGlobalMapRef()) {
|
||
RefPtr<BounceTrackingStateGlobal> stateGlobal = entry.GetData();
|
||
MOZ_ASSERT(stateGlobal);
|
||
|
||
if (stateGlobal->RecentPurgesMapRef().Contains(aSiteHost)) {
|
||
*aResult = true;
|
||
return NS_OK;
|
||
}
|
||
}
|
||
return NS_OK;
|
||
}
|
||
|
||
NS_IMETHODIMP
|
||
BounceTrackingProtection::TestGetSiteHostExceptions(
|
||
nsTArray<nsCString>& aSiteHostExceptions) {
|
||
aSiteHostExceptions.Clear();
|
||
|
||
for (const auto& host : mRemoteSiteHostExceptions) {
|
||
aSiteHostExceptions.AppendElement(host);
|
||
}
|
||
|
||
return NS_OK;
|
||
}
|
||
|
||
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<dom::Promise> 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&
|
||
purgedEntries) {
|
||
// Convert array of BounceTrackingMapEntry to array of site host
|
||
// strings for the JS caller. We can't pass an XPCOM type as the
|
||
// resolver value of a JS Promise.
|
||
nsTArray<nsCString> purgedSitesHosts;
|
||
for (const auto& entry : purgedEntries) {
|
||
purgedSitesHosts.AppendElement(entry->SiteHostRef());
|
||
}
|
||
promise->MaybeResolve(purgedSitesHosts);
|
||
},
|
||
[promise](const PurgeBounceTrackersMozPromise::RejectValueType& error) {
|
||
promise->MaybeReject(error);
|
||
});
|
||
|
||
promise.forget(aPromise);
|
||
return NS_OK;
|
||
}
|
||
|
||
NS_IMETHODIMP
|
||
BounceTrackingProtection::TestClearExpiredUserActivations() {
|
||
return ClearExpiredUserInteractions();
|
||
}
|
||
|
||
NS_IMETHODIMP
|
||
BounceTrackingProtection::TestAddBounceTrackerCandidate(
|
||
JS::Handle<JS::Value> 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;
|
||
}
|
||
|
||
RefPtr<BounceTrackingStateGlobal> stateGlobal =
|
||
mStorage->GetOrCreateStateGlobal(oa);
|
||
|
||
// 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<JS::Value> 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;
|
||
}
|
||
|
||
RefPtr<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);
|
||
}
|
||
|
||
NS_IMETHODIMP
|
||
BounceTrackingProtection::TestMaybeMigrateUserInteractionPermissions() {
|
||
return MaybeMigrateUserInteractionPermissions();
|
||
}
|
||
|
||
// static
|
||
nsresult BounceTrackingProtection::LogBounceTrackersClassifiedToWebConsole(
|
||
BounceTrackingState* aBounceTrackingState,
|
||
const nsTArray<nsCString>& aSiteHosts) {
|
||
NS_ENSURE_ARG(aBounceTrackingState);
|
||
|
||
// Nothing to log.
|
||
if (aSiteHosts.IsEmpty()) {
|
||
return NS_OK;
|
||
}
|
||
|
||
RefPtr<dom::BrowsingContext> browsingContext =
|
||
aBounceTrackingState->CurrentBrowsingContext();
|
||
if (!browsingContext) {
|
||
return NS_OK;
|
||
}
|
||
|
||
// Get the localized copy from antiTracking.ftl and insert the variables.
|
||
nsTArray<nsCString> resourceIDs = {"toolkit/global/antiTracking.ftl"_ns};
|
||
RefPtr<intl::Localization> l10n =
|
||
intl::Localization::Create(resourceIDs, true);
|
||
|
||
for (const nsACString& siteHost : aSiteHosts) {
|
||
auto l10nArgs = dom::Optional<intl::L10nArgs>();
|
||
l10nArgs.Construct();
|
||
|
||
auto siteHostArg = l10nArgs.Value().Entries().AppendElement();
|
||
siteHostArg->mKey = "siteHost";
|
||
siteHostArg->mValue.SetValue().SetAsUTF8String().Assign(siteHost);
|
||
|
||
auto gracePeriodArg = l10nArgs.Value().Entries().AppendElement();
|
||
gracePeriodArg->mKey = "gracePeriodSeconds";
|
||
gracePeriodArg->mValue.SetValue().SetAsDouble() = StaticPrefs::
|
||
privacy_bounceTrackingProtection_bounceTrackingGracePeriodSec();
|
||
|
||
// Construct the localized string.
|
||
nsAutoCString message;
|
||
ErrorResult errorResult;
|
||
l10n->FormatValueSync("btp-warning-tracker-classified"_ns, l10nArgs,
|
||
message, errorResult);
|
||
if (NS_WARN_IF(errorResult.Failed())) {
|
||
return errorResult.StealNSResult();
|
||
}
|
||
|
||
// Log to the console via nsIScriptError object.
|
||
nsresult rv = NS_OK;
|
||
nsCOMPtr<nsIScriptError> error =
|
||
do_CreateInstance(NS_SCRIPTERROR_CONTRACTID, &rv);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
rv = error->InitWithWindowID(
|
||
NS_ConvertUTF8toUTF16(message), ""_ns, 0, 0,
|
||
nsIScriptError::warningFlag, "bounceTrackingProtection",
|
||
browsingContext->GetCurrentInnerWindowId(), true);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
nsCOMPtr<nsIConsoleService> consoleService =
|
||
do_GetService(NS_CONSOLESERVICE_CONTRACTID, &rv);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
// The actual log call.
|
||
rv = consoleService->LogMessage(error);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
}
|
||
|
||
return NS_OK;
|
||
}
|
||
|
||
RefPtr<GenericNonExclusivePromise>
|
||
BounceTrackingProtection::EnsureRemoteExceptionListService() {
|
||
// mRemoteExceptionList already initialized or currently initializing.
|
||
if (mRemoteExceptionListInitPromise) {
|
||
return mRemoteExceptionListInitPromise;
|
||
}
|
||
|
||
// Create the service instance.
|
||
nsresult rv;
|
||
mRemoteExceptionList =
|
||
do_GetService(NS_NSIBTPEXCEPTIONLISTSERVICE_CONTRACTID, &rv);
|
||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||
mRemoteExceptionListInitPromise =
|
||
GenericNonExclusivePromise::CreateAndReject(rv, __func__);
|
||
return mRemoteExceptionListInitPromise;
|
||
}
|
||
|
||
// Call the init method and get the Promise. It resolves once the allow-list
|
||
// entries have been imported.
|
||
RefPtr<dom::Promise> jsPromise;
|
||
rv = mRemoteExceptionList->Init(this, getter_AddRefs(jsPromise));
|
||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||
mRemoteExceptionListInitPromise =
|
||
GenericNonExclusivePromise::CreateAndReject(rv, __func__);
|
||
return mRemoteExceptionListInitPromise;
|
||
}
|
||
MOZ_ASSERT(jsPromise);
|
||
|
||
// Convert to MozPromise so it can be handled from C++ side. Also store the
|
||
// promise so that subsequent calls to this method can wait for init too.
|
||
mRemoteExceptionListInitPromise =
|
||
PromiseNativeWrapper::ConvertJSPromiseToMozPromise(jsPromise);
|
||
|
||
return mRemoteExceptionListInitPromise;
|
||
}
|
||
|
||
RefPtr<BounceTrackingProtection::PurgeBounceTrackersMozPromise>
|
||
BounceTrackingProtection::PurgeBounceTrackers() {
|
||
// Only purge when the feature is actually enabled.
|
||
if (StaticPrefs::privacy_bounceTrackingProtection_mode() !=
|
||
nsIBounceTrackingProtection::MODE_ENABLED &&
|
||
StaticPrefs::privacy_bounceTrackingProtection_mode() !=
|
||
nsIBounceTrackingProtection::MODE_ENABLED_DRY_RUN) {
|
||
MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
|
||
("%s: Skip: Purging disabled via mode pref.", __FUNCTION__));
|
||
return PurgeBounceTrackersMozPromise::CreateAndReject(
|
||
nsresult::NS_ERROR_NOT_AVAILABLE, __func__);
|
||
}
|
||
|
||
// Prevent multiple purge operations from running at the same time.
|
||
if (mPurgeInProgress) {
|
||
MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
|
||
("%s: Skip: Purge already in progress.", __FUNCTION__));
|
||
return PurgeBounceTrackersMozPromise::CreateAndReject(
|
||
nsresult::NS_ERROR_NOT_AVAILABLE, __func__);
|
||
}
|
||
mPurgeInProgress = true;
|
||
|
||
RefPtr<PurgeBounceTrackersMozPromise::Private> resultPromise =
|
||
new PurgeBounceTrackersMozPromise::Private(__func__);
|
||
|
||
RefPtr<BounceTrackingProtection> self = this;
|
||
|
||
// Wait for the remote exception list service to be ready before purging.
|
||
EnsureRemoteExceptionListService()->Then(
|
||
GetCurrentSerialEventTarget(), __func__,
|
||
[self, resultPromise](
|
||
const GenericNonExclusivePromise::ResolveOrRejectValue& aResult) {
|
||
if (aResult.IsReject()) {
|
||
nsresult rv = aResult.RejectValue();
|
||
resultPromise->Reject(rv, __func__);
|
||
return;
|
||
}
|
||
// Remote exception list is ready.
|
||
|
||
// Obtain a cache of allow-list permissions so we only need to
|
||
// fetch permissions once even when we do multiple base domain lookups.
|
||
BounceTrackingAllowList bounceTrackingAllowList;
|
||
|
||
// Collect promises for all clearing operations to later await on.
|
||
nsTArray<RefPtr<ClearDataMozPromise>> clearPromises;
|
||
|
||
// Run the purging algorithm for all global state objects.
|
||
for (const auto& entry : self->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()));
|
||
}
|
||
|
||
nsresult rv = self->PurgeBounceTrackersForStateGlobal(
|
||
stateGlobal, bounceTrackingAllowList, clearPromises);
|
||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||
resultPromise->Reject(rv, __func__);
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Wait for all data clearing operations to complete. mClearPromises
|
||
// contains one promise per host / clear task.
|
||
ClearDataMozPromise::AllSettled(GetCurrentSerialEventTarget(),
|
||
clearPromises)
|
||
->Then(
|
||
GetCurrentSerialEventTarget(), __func__,
|
||
[resultPromise,
|
||
self](ClearDataMozPromise::AllSettledPromiseType::
|
||
ResolveOrRejectValue&& aResults) {
|
||
MOZ_ASSERT(aResults.IsResolve(), "AllSettled never rejects");
|
||
|
||
MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
|
||
("%s: Done. Cleared %zu hosts.", __FUNCTION__,
|
||
aResults.ResolveValue().Length()));
|
||
|
||
if (!aResults.ResolveValue().IsEmpty()) {
|
||
glean::bounce_tracking_protection::num_hosts_per_purge_run
|
||
.AccumulateSingleSample(
|
||
aResults.ResolveValue().Length());
|
||
}
|
||
|
||
// Check if any clear call failed.
|
||
bool anyFailed = false;
|
||
|
||
nsTArray<RefPtr<BounceTrackingPurgeEntry>> purgedSites;
|
||
|
||
// If any clear call failed reject.
|
||
for (auto& result : aResults.ResolveValue()) {
|
||
if (result.IsReject()) {
|
||
anyFailed = true;
|
||
} else {
|
||
purgedSites.AppendElement(result.ResolveValue());
|
||
}
|
||
}
|
||
|
||
// Record exposure of the feature for Nimbus
|
||
// experimentation.
|
||
// The error result returned by this method isn't very
|
||
// useful, so we ignore it. Thee method will also return
|
||
// errors if the client is not enrolled in an experiment
|
||
// involving BTP which we don't consider a failure state.
|
||
//
|
||
// We record exposure for MODE_ENABLED_DRY_RUN in addition to
|
||
// MODE_ENABLED so we know in Nimbus when a client would have
|
||
// been exposed to BTP had it been enabled. This enables us to
|
||
// compare the control and treatment branches with exposure.
|
||
Unused << NimbusFeatures::RecordExposureEvent(
|
||
"bounceTrackingProtection"_ns, false);
|
||
|
||
if (StaticPrefs::privacy_bounceTrackingProtection_mode() ==
|
||
nsIBounceTrackingProtection::MODE_ENABLED) {
|
||
// Log successful purges.
|
||
for (const auto& entry : purgedSites) {
|
||
RefPtr<BounceTrackingStateGlobal> stateGlobal =
|
||
self->mStorage->GetOrCreateStateGlobal(
|
||
entry->OriginAttributesRef());
|
||
MOZ_ASSERT(stateGlobal);
|
||
DebugOnly<nsresult> rv =
|
||
stateGlobal->RecordPurgedTracker(entry);
|
||
NS_WARNING_ASSERTION(
|
||
NS_SUCCEEDED(rv),
|
||
"Failed to record purged tracker in log.");
|
||
}
|
||
|
||
if (purgedSites.Length() > 0) {
|
||
// Record successful purges via nsITrackingDBService for
|
||
// tracker stats.
|
||
ReportPurgedTrackersToAntiTrackingDB(purgedSites);
|
||
}
|
||
}
|
||
|
||
self->mPurgeInProgress = false;
|
||
|
||
// If any clear call failed reject the promise.
|
||
if (anyFailed) {
|
||
resultPromise->Reject(NS_ERROR_FAILURE, __func__);
|
||
return;
|
||
}
|
||
resultPromise->Resolve(std::move(purgedSites), __func__);
|
||
});
|
||
});
|
||
return resultPromise.forget();
|
||
}
|
||
|
||
// static
|
||
void BounceTrackingProtection::ReportPurgedTrackersToAntiTrackingDB(
|
||
const nsTArray<RefPtr<BounceTrackingPurgeEntry>>& aPurgedSiteHosts) {
|
||
MOZ_ASSERT(!aPurgedSiteHosts.IsEmpty());
|
||
MOZ_ASSERT(StaticPrefs::privacy_bounceTrackingProtection_mode() ==
|
||
nsIBounceTrackingProtection::MODE_ENABLED);
|
||
|
||
ContentBlockingLog log;
|
||
for (const RefPtr<BounceTrackingPurgeEntry>& entry : aPurgedSiteHosts) {
|
||
nsAutoCString origin("https://");
|
||
origin.Append(entry->SiteHostRef());
|
||
|
||
log.RecordLogParent(
|
||
origin, nsIWebProgressListener::STATE_PURGED_BOUNCETRACKER, true);
|
||
}
|
||
log.ReportLog();
|
||
}
|
||
|
||
nsresult BounceTrackingProtection::PurgeBounceTrackersForStateGlobal(
|
||
BounceTrackingStateGlobal* aStateGlobal,
|
||
BounceTrackingAllowList& aBounceTrackingAllowList,
|
||
nsTArray<RefPtr<ClearDataMozPromise>>& aClearPromises) {
|
||
MOZ_ASSERT(aStateGlobal);
|
||
MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
|
||
("%s: %s", __FUNCTION__, aStateGlobal->Describe().get()));
|
||
|
||
// Ensure we only purge when pref configuration allows it.
|
||
if (StaticPrefs::privacy_bounceTrackingProtection_mode() !=
|
||
nsIBounceTrackingProtection::MODE_ENABLED &&
|
||
StaticPrefs::privacy_bounceTrackingProtection_mode() !=
|
||
nsIBounceTrackingProtection::MODE_ENABLED_DRY_RUN) {
|
||
return NS_ERROR_NOT_AVAILABLE;
|
||
}
|
||
|
||
const PRTime now = PR_Now();
|
||
|
||
// 1. Remove hosts from the user activation map whose user activation flag has
|
||
// expired.
|
||
nsresult rv = ClearExpiredUserInteractions(aStateGlobal);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
// 2. Go over bounce tracker candidate map and purge state.
|
||
|
||
// Collect hosts to remove from the bounce trackers map. We can not remove
|
||
// them while iterating over the map.
|
||
nsTArray<nsCString> 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, aStateGlobal->OriginAttributesRef(), 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;
|
||
}
|
||
|
||
// Gecko specific: If the host is on the content blocking allow-list or
|
||
// allow-listed via RemoteSettings continue.
|
||
bool isAllowListed = mRemoteSiteHostExceptions.Contains(host);
|
||
// If remote settings doesn't allowlist also check the content blocking
|
||
// allow-list.
|
||
if (!isAllowListed) {
|
||
rv = aBounceTrackingAllowList.CheckForBaseDomain(
|
||
host, aStateGlobal->OriginAttributesRef(), isAllowListed);
|
||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||
continue;
|
||
}
|
||
}
|
||
if (isAllowListed) {
|
||
if (MOZ_LOG_TEST(gBounceTrackingProtectionLog, LogLevel::Debug)) {
|
||
nsAutoCString originAttributeSuffix;
|
||
aStateGlobal->OriginAttributesRef().CreateSuffix(originAttributeSuffix);
|
||
MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
|
||
("%s: Skip allow-listed: host: %s, "
|
||
"originAttributes: %s",
|
||
__FUNCTION__, PromiseFlatCString(host).get(),
|
||
originAttributeSuffix.get()));
|
||
}
|
||
// Remove allow-listed host so we don't need to check in again next purge
|
||
// run. If it gets classified again and the allow-list entry gets removed
|
||
// it will be purged in the next run.
|
||
bounceTrackerCandidatesToRemove.AppendElement(host);
|
||
continue;
|
||
}
|
||
|
||
// No exception above applies, clear state for the given host.
|
||
MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Info,
|
||
("%s: Purging bounce tracker. siteHost: %s, bounceTime: %" PRIu64
|
||
" aStateGlobal: %s",
|
||
__FUNCTION__, PromiseFlatCString(host).get(), bounceTime,
|
||
aStateGlobal->Describe().get()));
|
||
|
||
// 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);
|
||
|
||
RefPtr<ClearDataMozPromise> clearDataPromise;
|
||
rv = PurgeStateForHostAndOriginAttributes(
|
||
host, bounceTime, aStateGlobal->OriginAttributesRef(),
|
||
getter_AddRefs(clearDataPromise));
|
||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||
continue;
|
||
}
|
||
MOZ_ASSERT(clearDataPromise);
|
||
|
||
aClearPromises.AppendElement(clearDataPromise);
|
||
}
|
||
|
||
// Remove hosts from the bounce trackers map which we executed purge calls
|
||
// for.
|
||
return aStateGlobal->RemoveBounceTrackers(bounceTrackerCandidatesToRemove);
|
||
}
|
||
|
||
nsresult BounceTrackingProtection::PurgeStateForHostAndOriginAttributes(
|
||
const nsACString& aHost, PRTime bounceTime,
|
||
const OriginAttributes& aOriginAttributes,
|
||
ClearDataMozPromise** aClearPromise) {
|
||
MOZ_ASSERT(!aHost.IsEmpty());
|
||
MOZ_ASSERT(aClearPromise);
|
||
|
||
RefPtr<ClearDataMozPromise::Private> clearPromise =
|
||
new ClearDataMozPromise::Private(__func__);
|
||
RefPtr<ClearDataCallback> cb =
|
||
new ClearDataCallback(clearPromise, aOriginAttributes, aHost, bounceTime);
|
||
|
||
if (StaticPrefs::privacy_bounceTrackingProtection_mode() ==
|
||
nsIBounceTrackingProtection::MODE_ENABLED_DRY_RUN) {
|
||
// In dry-run mode, we don't actually clear the data, but we still want to
|
||
// resolve the promise to indicate that the data would have been cleared.
|
||
cb->OnDataDeleted(0);
|
||
|
||
clearPromise.forget(aClearPromise);
|
||
return NS_OK;
|
||
}
|
||
|
||
nsresult rv = NS_OK;
|
||
nsCOMPtr<nsIClearDataService> clearDataService =
|
||
do_GetService("@mozilla.org/clear-data-service;1", &rv);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
// nsIClearDataService expects a schemeless site which for IPV6 addresses
|
||
// includes brackets. Add them if needed.
|
||
nsAutoCString hostToPurge(aHost);
|
||
nsContentUtils::MaybeFixIPv6Host(hostToPurge);
|
||
|
||
// When clearing data for a specific site host we need to ensure that we
|
||
// only clear for matching OriginAttributes. For example if the current
|
||
// state global is private browsing only we must not clear normal browsing
|
||
// data. To restrict clearing we pass in a (stringified)
|
||
// OriginAttributesPattern which matches the state global's
|
||
// OriginAttributes.
|
||
nsAutoString oaPatternString;
|
||
OriginAttributesPattern pattern;
|
||
|
||
// partitionKey and firstPartyDomain are omitted making them wildcards. We
|
||
// want to clear across partitions since BTP should clear all data for a
|
||
// given top level.
|
||
pattern.mUserContextId.Construct(aOriginAttributes.mUserContextId);
|
||
pattern.mPrivateBrowsingId.Construct(aOriginAttributes.mPrivateBrowsingId);
|
||
pattern.mGeckoViewSessionContextId.Construct(
|
||
aOriginAttributes.mGeckoViewSessionContextId);
|
||
|
||
NS_ENSURE_TRUE(pattern.ToJSON(oaPatternString), NS_ERROR_FAILURE);
|
||
|
||
rv = clearDataService->DeleteDataFromSiteAndOriginAttributesPatternString(
|
||
hostToPurge, oaPatternString, false,
|
||
// Exempt purging our own state for the given tracker since we already
|
||
// update it ourselves. Additionally a nested call to the
|
||
// BounceTrackingProtectionCleaner while iterating over the candidate set
|
||
// may lead to crashes.
|
||
nsIClearDataService::CLEAR_STATE_FOR_TRACKER_PURGING &
|
||
~nsIClearDataService::CLEAR_BOUNCE_TRACKING_PROTECTION_STATE,
|
||
cb);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
clearPromise.forget(aClearPromise);
|
||
|
||
return NS_OK;
|
||
}
|
||
|
||
nsresult BounceTrackingProtection::ClearExpiredUserInteractions(
|
||
BounceTrackingStateGlobal* aStateGlobal) {
|
||
if (!aStateGlobal && mStorage->StateGlobalMapRef().IsEmpty()) {
|
||
// Nothing to clear.
|
||
return NS_OK;
|
||
}
|
||
|
||
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<int64_t>(
|
||
StaticPrefs::
|
||
privacy_bounceTrackingProtection_bounceTrackingActivationLifetimeSec()) *
|
||
PR_USEC_PER_SEC;
|
||
|
||
// Clear user activation for the given state global.
|
||
if (aStateGlobal) {
|
||
return aStateGlobal->ClearUserActivationBefore(now -
|
||
activationLifetimeUsec);
|
||
}
|
||
|
||
// aStateGlobal not passed, clear user activation for all state globals.
|
||
for (const auto& entry : mStorage->StateGlobalMapRef()) {
|
||
const RefPtr<BounceTrackingStateGlobal>& stateGlobal = entry.GetData();
|
||
MOZ_ASSERT(stateGlobal);
|
||
|
||
nsresult rv =
|
||
stateGlobal->ClearUserActivationBefore(now - activationLifetimeUsec);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
}
|
||
|
||
return NS_OK;
|
||
}
|
||
|
||
void BounceTrackingProtection::MaybeLogPurgedWarningForSite(
|
||
nsIPrincipal* aPrincipal, BounceTrackingState* aBounceTrackingState) {
|
||
NS_ENSURE_TRUE_VOID(aPrincipal);
|
||
NS_ENSURE_TRUE_VOID(aBounceTrackingState);
|
||
|
||
// Ensure that the BTS is connected to a BrowsingContext. We will later use
|
||
// this BC to get the DevTools console to log to.
|
||
RefPtr<dom::BrowsingContext> browsingContext =
|
||
aBounceTrackingState->CurrentBrowsingContext();
|
||
if (!browsingContext) {
|
||
return;
|
||
}
|
||
|
||
// Check if the site is in the recently purged log. If it is we should log a
|
||
// console warning.
|
||
RefPtr<BounceTrackingStateGlobal> stateGlobal =
|
||
mStorage->GetStateGlobal(aPrincipal);
|
||
if (!stateGlobal) {
|
||
// No state global which means we don't have any purge data for the given
|
||
// OriginAttributes on record.
|
||
return;
|
||
}
|
||
|
||
nsAutoCString siteHost;
|
||
nsresult rv = aPrincipal->GetBaseDomain(siteHost);
|
||
NS_ENSURE_SUCCESS_VOID(rv);
|
||
|
||
if (!stateGlobal->RecentPurgesMapRef().Contains(siteHost)) {
|
||
// No recent purge found for the given site.
|
||
return;
|
||
}
|
||
|
||
// Recently purged the site. Log a warning.
|
||
|
||
// Get the localized copy from antiTracking.ftl and insert the variables.
|
||
nsTArray<nsCString> resourceIDs = {"toolkit/global/antiTracking.ftl"_ns};
|
||
RefPtr<intl::Localization> l10n =
|
||
intl::Localization::Create(resourceIDs, true);
|
||
|
||
auto l10nArgs = dom::Optional<intl::L10nArgs>();
|
||
l10nArgs.Construct();
|
||
|
||
auto siteHostArg = l10nArgs.Value().Entries().AppendElement();
|
||
siteHostArg->mKey = "siteHost";
|
||
siteHostArg->mValue.SetValue().SetAsUTF8String().Assign(siteHost);
|
||
|
||
// Construct the localized string.
|
||
nsAutoCString message;
|
||
ErrorResult errorResult;
|
||
l10n->FormatValueSync("btp-warning-tracker-purged"_ns, l10nArgs, message,
|
||
errorResult);
|
||
if (NS_WARN_IF(errorResult.Failed())) {
|
||
return;
|
||
}
|
||
|
||
rv = nsContentUtils::ReportToConsoleByWindowID(
|
||
NS_ConvertUTF8toUTF16(message), nsIScriptError::warningFlag,
|
||
"bounceTrackingProtection"_ns,
|
||
browsingContext->GetCurrentInnerWindowId());
|
||
|
||
NS_ENSURE_SUCCESS_VOID(rv);
|
||
}
|
||
|
||
nsresult BounceTrackingProtection::MaybeMigrateUserInteractionPermissions() {
|
||
// Only run the migration once.
|
||
if (StaticPrefs::
|
||
privacy_bounceTrackingProtection_hasMigratedUserActivationData()) {
|
||
return NS_OK;
|
||
}
|
||
|
||
MOZ_LOG(
|
||
gBounceTrackingProtectionLog, LogLevel::Debug,
|
||
("%s: Importing user activation data from permissions", __FUNCTION__));
|
||
|
||
// Get all user activation permissions that are within our user activation
|
||
// lifetime. We don't care about the rest since they are considered expired
|
||
// for BTP.
|
||
|
||
nsresult rv = NS_OK;
|
||
nsCOMPtr<nsIPermissionManager> permManager =
|
||
do_GetService(NS_PERMISSIONMANAGER_CONTRACTID, &rv);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
NS_ENSURE_TRUE(permManager, NS_ERROR_FAILURE);
|
||
|
||
// Construct the since time param. The permission manager expects epoch in
|
||
// miliseconds.
|
||
int64_t nowMS = PR_Now() / PR_USEC_PER_MSEC;
|
||
int64_t activationLifetimeMS =
|
||
static_cast<int64_t>(
|
||
StaticPrefs::
|
||
privacy_bounceTrackingProtection_bounceTrackingActivationLifetimeSec()) *
|
||
PR_MSEC_PER_SEC;
|
||
int64_t since = nowMS - activationLifetimeMS;
|
||
MOZ_ASSERT(since > 0);
|
||
|
||
// Get all user activation permissions last modified between "since" and now.
|
||
nsTArray<RefPtr<nsIPermission>> userActivationPermissions;
|
||
rv = permManager->GetAllByTypeSince("storageAccessAPI"_ns, since,
|
||
userActivationPermissions);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
|
||
MOZ_LOG(gBounceTrackingProtectionLog, LogLevel::Debug,
|
||
("%s: Found %zu (non-expired) user activation permissions",
|
||
__FUNCTION__, userActivationPermissions.Length()));
|
||
|
||
for (const auto& perm : userActivationPermissions) {
|
||
nsCOMPtr<nsIPrincipal> permPrincipal;
|
||
|
||
rv = perm->GetPrincipal(getter_AddRefs(permPrincipal));
|
||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||
continue;
|
||
}
|
||
MOZ_ASSERT(permPrincipal);
|
||
|
||
// The time the permission was last modified is the time of last user
|
||
// activation.
|
||
int64_t modificationTimeMS;
|
||
rv = perm->GetModificationTime(&modificationTimeMS);
|
||
NS_ENSURE_SUCCESS(rv, rv);
|
||
MOZ_ASSERT(modificationTimeMS >= since,
|
||
"Unexpected permission modification time");
|
||
|
||
// We may end up with duplicates here since user activation permissions are
|
||
// tracked by origin, while BTP tracks user activation by site host.
|
||
// RecordUserActivation is responsible for only keeping the most recent user
|
||
// activation flag for a given site host and needs to make sure existing
|
||
// activation flags are not overwritten by older timestamps.
|
||
// RecordUserActivation expects epoch in microseconds.
|
||
rv = RecordUserActivation(permPrincipal,
|
||
Some(modificationTimeMS * PR_USEC_PER_MSEC));
|
||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||
continue;
|
||
}
|
||
}
|
||
|
||
// Migration successful, set the pref to indicate that we have migrated.
|
||
return mozilla::Preferences::SetBool(
|
||
"privacy.bounceTrackingProtection.hasMigratedUserActivationData", true);
|
||
}
|
||
|
||
} // namespace mozilla
|