mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-11-09 04:39:03 +02:00
We're using nsIStorageActivityService to get only principals that were using storage in the last 3 days. A few notes on that: - 3 days is based on the assumption that it's very unlikely that a client would miss idle daily for 3 days in a row. - We're currently only persisting activity for up to 24 hours. Bug 1630598 tracks extension to 3 days. - We're currently not persisting storage activity service across restarts. This is bug 1459974 which we're aiming to resolve. - This will not immediately clear all old tracking storage, only when it is used another time. In the same vein there's a chance the we miss clearing if the user manages to persistently have Firefox sessions that are so short that idle-daily is rarely triggered. This would be problematic for cookie purging in general, though. - This produces a significantly lower number of principals to check. We could consider switching cookies to the same approach (only get the last x days of activity). I talked to Steve Englehardt and we're generally okay with these caveats in favor of the simplified implementation. Differential Revision: https://phabricator.services.mozilla.com/D71173
281 lines
9.1 KiB
JavaScript
281 lines
9.1 KiB
JavaScript
/* 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/. */
|
|
|
|
"use strict";
|
|
|
|
var EXPORTED_SYMBOLS = ["PurgeTrackerService"];
|
|
|
|
const { XPCOMUtils } = ChromeUtils.import(
|
|
"resource://gre/modules/XPCOMUtils.jsm"
|
|
);
|
|
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
|
|
|
const THREE_DAYS_MS = 3 * 24 * 60 * 1000;
|
|
|
|
XPCOMUtils.defineLazyServiceGetter(
|
|
this,
|
|
"gClassifier",
|
|
"@mozilla.org/url-classifier/dbservice;1",
|
|
"nsIURIClassifier"
|
|
);
|
|
XPCOMUtils.defineLazyServiceGetter(
|
|
this,
|
|
"gStorageActivityService",
|
|
"@mozilla.org/storage/activity-service;1",
|
|
"nsIStorageActivityService"
|
|
);
|
|
|
|
this.PurgeTrackerService = function() {};
|
|
|
|
PurgeTrackerService.prototype = {
|
|
classID: Components.ID("{90d1fd17-2018-4e16-b73c-a04a26fa6dd4}"),
|
|
QueryInterface: ChromeUtils.generateQI([Ci.nsIPurgeTrackerService]),
|
|
|
|
// We can only know asynchronously if a host is matched by the tracking
|
|
// protection list, so we cache the result for faster future lookups.
|
|
_trackingState: new Map(),
|
|
/**
|
|
* We use this collator to compare strings as if they were numbers.
|
|
* Timestamps are saved as strings since the number is too large for an int.
|
|
**/
|
|
collator: new Intl.Collator(undefined, {
|
|
numeric: true,
|
|
sensitivity: "base",
|
|
}),
|
|
|
|
observe(aSubject, aTopic, aData) {
|
|
switch (aTopic) {
|
|
case "idle-daily":
|
|
// only allow one idle-daily listener to trigger until the list has been fully parsed.
|
|
Services.obs.removeObserver(this, "idle-daily");
|
|
this.purgeTrackingCookieJars();
|
|
break;
|
|
case "profile-after-change":
|
|
Services.obs.addObserver(this, "idle-daily");
|
|
break;
|
|
}
|
|
},
|
|
|
|
async isTracker(principal, feature) {
|
|
if (principal.isNullPrincipal || principal.isSystemPrincipal) {
|
|
return false;
|
|
}
|
|
let host;
|
|
try {
|
|
host = principal.URI.asciiHost;
|
|
} catch (error) {
|
|
return false;
|
|
}
|
|
|
|
if (!this._trackingState.has(host)) {
|
|
// Temporarily set to false to avoid doing several lookups if a site has
|
|
// several subframes on the same domain.
|
|
this._trackingState.set(host, false);
|
|
|
|
await new Promise(resolve => {
|
|
try {
|
|
gClassifier.asyncClassifyLocalWithFeatures(
|
|
principal.URI,
|
|
[feature],
|
|
Ci.nsIUrlClassifierFeature.blacklist,
|
|
list => {
|
|
if (list.length) {
|
|
this._trackingState.set(host, true);
|
|
}
|
|
resolve();
|
|
}
|
|
);
|
|
} catch {
|
|
// Error in asyncClassifyLocalWithFeatures, it is not a tracker.
|
|
this._trackingState.set(host, false);
|
|
resolve();
|
|
}
|
|
});
|
|
}
|
|
|
|
return this._trackingState.get(host);
|
|
},
|
|
|
|
resetPurgeList() {
|
|
// We've reached the end of the cookies.
|
|
// Restore the idle-daily listener so it will purge again tomorrow.
|
|
Services.obs.addObserver(this, "idle-daily");
|
|
// Set the date to 0 so we will start at the beginning of the list next time.
|
|
Services.prefs.setStringPref(
|
|
"privacy.purge_trackers.date_in_cookie_database",
|
|
"0"
|
|
);
|
|
},
|
|
|
|
/**
|
|
* This loops through all cookies saved in the database and checks if they are a tracking cookie, if it is it checks
|
|
* that they have an interaction permission which is still valid. If the Permission is not valid we delete all data
|
|
* associated with the site that owns that cookie.
|
|
*/
|
|
async purgeTrackingCookieJars() {
|
|
let purgeEnabled = Services.prefs.getBoolPref(
|
|
"privacy.purge_trackers.enabled",
|
|
false
|
|
);
|
|
// Only purge if ETP is enabled.
|
|
let cookieBehavior = Services.prefs.getIntPref(
|
|
"network.cookie.cookieBehavior",
|
|
Ci.nsICookieService.BEHAVIOR_ACCEPT
|
|
);
|
|
|
|
let etpActive =
|
|
cookieBehavior == Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER ||
|
|
cookieBehavior ==
|
|
Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN;
|
|
|
|
if (!etpActive || !purgeEnabled) {
|
|
LOG(
|
|
`returning early, etpActive: ${etpActive}, purgeEnabled: ${purgeEnabled}`
|
|
);
|
|
this.resetPurgeList();
|
|
return;
|
|
}
|
|
LOG("Purging trackers enabled, beginning batch.");
|
|
// How many cookies to loop through in each batch before we quit
|
|
const MAX_PURGE_COUNT = Services.prefs.getIntPref(
|
|
"privacy.purge_trackers.max_purge_count",
|
|
100
|
|
);
|
|
|
|
/**
|
|
* We record the creationTime of the last cookie we looked at and
|
|
* start from there next time. This way even if new cookies are added or old ones are deleted we
|
|
* have a reliable way of finding our spot.
|
|
**/
|
|
let saved_date = Services.prefs.getStringPref(
|
|
"privacy.purge_trackers.date_in_cookie_database",
|
|
"0"
|
|
);
|
|
|
|
let maybeClearPrincipals = new Map();
|
|
|
|
// TODO We only need the host name and creationTime, this gives too much info. See bug 1610373.
|
|
let cookies = Services.cookies.cookies;
|
|
|
|
// ensure we have only cookies that have a greater or equal creationTime than the saved creationTime.
|
|
// TODO only get cookies that fulfill this condition. See bug 1610373.
|
|
cookies = cookies.filter(cookie => {
|
|
return (
|
|
cookie.creationTime &&
|
|
this.collator.compare(cookie.creationTime, saved_date) > 0
|
|
);
|
|
});
|
|
|
|
// ensure the cookies are sorted by creationTime oldest to newest.
|
|
// TODO get cookies in this order. See bug 1610373.
|
|
cookies.sort((a, b) =>
|
|
this.collator.compare(a.creationTime, b.creationTime)
|
|
);
|
|
cookies = cookies.slice(0, MAX_PURGE_COUNT);
|
|
|
|
for (let cookie of cookies) {
|
|
let httpPrincipal = Services.scriptSecurityManager.createContentPrincipalFromOrigin(
|
|
"http://" +
|
|
cookie.rawHost +
|
|
ChromeUtils.originAttributesToSuffix(cookie.originAttributes)
|
|
);
|
|
let httpsPrincipal = Services.scriptSecurityManager.createContentPrincipalFromOrigin(
|
|
"https://" +
|
|
cookie.rawHost +
|
|
ChromeUtils.originAttributesToSuffix(cookie.originAttributes)
|
|
);
|
|
maybeClearPrincipals.set(httpPrincipal.origin, httpPrincipal);
|
|
maybeClearPrincipals.set(httpsPrincipal.origin, httpsPrincipal);
|
|
saved_date = cookie.creationTime;
|
|
}
|
|
|
|
let startDate = Date.now() - THREE_DAYS_MS;
|
|
let storagePrincipals = gStorageActivityService.getActiveOrigins(
|
|
startDate * 1000,
|
|
Date.now() * 1000
|
|
);
|
|
|
|
for (let principal of storagePrincipals.enumerate()) {
|
|
maybeClearPrincipals.set(principal.origin, principal);
|
|
}
|
|
|
|
let feature = gClassifier.getFeatureByName("tracking-annotation");
|
|
if (!feature) {
|
|
LOG("returning early, feature undefined.");
|
|
this.resetPurgeList();
|
|
return;
|
|
}
|
|
|
|
let baseDomainsWithInteraction = new Set();
|
|
for (let perm of Services.perms.getAllWithTypePrefix("storageAccessAPI")) {
|
|
baseDomainsWithInteraction.add(perm.principal.baseDomain);
|
|
}
|
|
|
|
for (let principal of maybeClearPrincipals.values()) {
|
|
// Either the interaction permission was never granted or it expired.
|
|
if (!baseDomainsWithInteraction.has(principal.baseDomain)) {
|
|
// We purge if we also find it is a tracker.
|
|
let isTracker = await this.isTracker(principal, feature);
|
|
if (isTracker) {
|
|
LOG(
|
|
"tracking cookie found with no interaction permission, deleting related data.",
|
|
principal.origin
|
|
);
|
|
|
|
await new Promise(resolve => {
|
|
Services.clearData.deleteDataFromPrincipal(
|
|
principal,
|
|
false,
|
|
Ci.nsIClearDataService.CLEAR_ALL_CACHES |
|
|
Ci.nsIClearDataService.CLEAR_COOKIES |
|
|
Ci.nsIClearDataService.CLEAR_DOM_STORAGES |
|
|
Ci.nsIClearDataService.CLEAR_SECURITY_SETTINGS |
|
|
Ci.nsIClearDataService.CLEAR_EME |
|
|
Ci.nsIClearDataService.CLEAR_PLUGIN_DATA |
|
|
Ci.nsIClearDataService.CLEAR_MEDIA_DEVICES |
|
|
Ci.nsIClearDataService.CLEAR_STORAGE_ACCESS |
|
|
Ci.nsIClearDataService.CLEAR_AUTH_TOKENS |
|
|
Ci.nsIClearDataService.CLEAR_AUTH_CACHE,
|
|
resolve
|
|
);
|
|
});
|
|
LOG(`Data deleted from: `, principal.origin);
|
|
}
|
|
}
|
|
}
|
|
|
|
Services.prefs.setStringPref(
|
|
"privacy.purge_trackers.date_in_cookie_database",
|
|
saved_date
|
|
);
|
|
|
|
// We've reached the end, no need to repeat again until next idle-daily.
|
|
if (!cookies.length || cookies.length < 100) {
|
|
LOG("All cookie purging finished, resetting list until tomorrow.");
|
|
this.resetPurgeList();
|
|
return;
|
|
}
|
|
|
|
LOG("Batch finished, queueing next batch.");
|
|
Services.tm.idleDispatchToMainThread(() => {
|
|
this.purgeTrackingCookieJars();
|
|
});
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Outputs the message to the JavaScript console as well as to stdout.
|
|
*
|
|
* @param {...string} args The message to output.
|
|
*/
|
|
var logConsole;
|
|
function LOG(...args) {
|
|
if (!logConsole) {
|
|
logConsole = console.createInstance({
|
|
prefix: "*** PurgeTrackerService:",
|
|
maxLogLevelPref: "privacy.purge_trackers.logging.level",
|
|
});
|
|
}
|
|
logConsole.log(...args);
|
|
}
|