Source code

Revision control

Copy as Markdown

Other Tools

// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
JsonSchema: "resource://gre/modules/JsonSchema.sys.mjs",
});
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
ChromeUtils.defineLazyGetter(lazy, "logConsole", () => {
return console.createInstance({
prefix: "FingerprintingWebCompatService",
maxLogLevelPref:
"privacy.fingerprintingProtection.WebCompatService.logLevel",
});
});
const SCHEMA = `{
"type": "object",
"title": "Fingerprinting Overrides",
"required": [
"firstPartyDomain",
"overrides"
],
"properties": {
"overrides": {
"type": "string",
"pattern": "^[+-][A-Za-z]+(?:,[+-][A-Za-z]+)*$",
"description": "The fingerprinting overrides. See https://searchfox.org/mozilla-central/source/toolkit/components/resistfingerprinting/RFPTargets.inc for details."
},
"firstPartyDomain": {
"type": "string",
"pattern": "^(\\*|(?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.)+[A-Za-z]{2,6}$",
"description": "The first-party domain associated with the override. Use '*' to match all domains. Only legit domains allowed."
},
"thirdPartyDomain": {
"type": "string",
"pattern": "^(\\*|(?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.)+[A-Za-z]{2,6}$",
"description": "The third-party domain associated with the override. Use '*' to match all domains. Only legit domains allowed. Leave this field empty if the override is only for the first-party context."
}
}
}`;
const COLLECTION_NAME = "fingerprinting-protection-overrides";
const PREF_GRANULAR_OVERRIDES =
"privacy.fingerprintingProtection.granularOverrides";
const PREF_REMOTE_OVERRIDES_ENABLED =
"privacy.fingerprintingProtection.remoteOverrides.enabled";
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"granularOverridesPref",
PREF_GRANULAR_OVERRIDES
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"remoteOverridesEnabled",
PREF_REMOTE_OVERRIDES_ENABLED
);
/**
* The object represents a fingerprinting override.
*/
export class FingerprintingOverride {
classID = Components.ID("{07f45442-1806-44be-9230-12eb79de9bac}");
QueryInterface = ChromeUtils.generateQI(["nsIFingerprintingOverride"]);
constructor(firstPartyDomain, thirdPartyDomain, overrides) {
this.firstPartyDomain = firstPartyDomain;
this.thirdPartyDomain = thirdPartyDomain;
this.overrides = overrides;
}
}
/**
* The singleton service that is responsible for the WebCompat of the
* fingerprinting protection. It gets fingerprinting overrides from remote
* settings and the local test pref.
*/
export class FingerprintingWebCompatService {
classId = Components.ID("{e7b1da06-2594-4670-aea4-131070baca4c}");
QueryInterface = ChromeUtils.generateQI([
"nsIFingerprintingWebCompatService",
]);
#initialized = false;
#remoteOverrides;
#granularOverrides;
#rs;
#validator;
#isParentProcess;
constructor() {
this.#isParentProcess =
Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT;
this.#remoteOverrides = new Set();
this.#granularOverrides = new Set();
this.#rs = lazy.RemoteSettings(COLLECTION_NAME);
this.#validator = new lazy.JsonSchema.Validator(SCHEMA);
}
async init() {
lazy.logConsole.debug("init");
// We can only access remote settings from the parent process. So, never
// init it in the content process.
if (!this.#isParentProcess) {
throw new Error(
"Shouldn't init FingerprintingWebCompatService in content processes."
);
}
// Return if we have initiated.
if (this.#initialized) {
return;
}
this.#initialized = true;
// Register listener to import overrides when the overrides pref changes.
Services.prefs.addObserver(PREF_GRANULAR_OVERRIDES, this);
// Register the sync event for the remote settings updates.
this.#rs.on("sync", event => {
let {
data: { current },
} = event;
this.#onRemoteUpdate(current);
this.#populateOverrides();
});
// Get the remote overrides from the remote settings.
await this.#importRemoteSettingsOverrides();
// Get the granular overrides from the pref.
this.#importPrefOverrides();
// Populate the overrides to the nsRFPService.
this.#populateOverrides();
lazy.logConsole.debug("Init completes");
}
// Import fingerprinting overrides from the local granular pref.
#importPrefOverrides() {
lazy.logConsole.debug("importLocalGranularOverrides");
// Clear overrides before we update.
this.#granularOverrides.clear();
let overrides;
try {
overrides = JSON.parse(lazy.granularOverridesPref || "[]");
} catch (error) {
lazy.logConsole.error(
`Failed to parse granular override JSON string: Not a valid JSON.`,
error
);
return;
}
// Ensure we have an array we can iterate over and not an object.
if (!Array.isArray(overrides)) {
lazy.logConsole.error(
"Failed to parse granular overrides JSON String: Not an array."
);
return;
}
for (let override of overrides) {
// Validate the override.
let { valid, errors } = this.#validator.validate(override);
if (!valid) {
lazy.logConsole.debug("Override validation error", override, errors);
continue;
}
this.#granularOverrides.add(
this.#createFingerprintingOverrideFrom(override)
);
}
}
// Import fingerprinting overrides from the remote settings.
async #importRemoteSettingsOverrides() {
lazy.logConsole.debug("importRemoteSettingsOverrides");
let entries;
try {
entries = await this.#rs.get();
} catch (e) {}
this.#onRemoteUpdate(entries || []);
}
#onRemoteUpdate(entries) {
lazy.logConsole.debug("onUpdateEntries", { entries });
if (!lazy.remoteOverridesEnabled) {
lazy.logConsole.debug("Abort remote overrides");
return;
}
// Clear all overrides before we update the overrides.
this.#remoteOverrides.clear();
for (let entry of entries) {
this.#remoteOverrides.add(this.#createFingerprintingOverrideFrom(entry));
}
}
#createFingerprintingOverrideFrom(entry) {
return new FingerprintingOverride(
entry.firstPartyDomain,
entry.thirdPartyDomain,
entry.overrides
);
}
#populateOverrides() {
lazy.logConsole.debug("populateOverrides");
// Create the array that contains all overrides. We explicitly concat the
// overrides from testing pref after the ones from remote settings to ensure
// that the testing pref will take precedence.
let overrides = Array.from(this.#remoteOverrides).concat(
Array.from(this.#granularOverrides)
);
// Set the remote override to the RFP service.
Services.rfp.setFingerprintingOverrides(Array.from(overrides));
}
observe(subject, topic, prefName) {
if (prefName != PREF_GRANULAR_OVERRIDES) {
return;
}
this.#importPrefOverrides();
this.#populateOverrides();
}
shutdown() {
lazy.logConsole.debug("shutdown");
Services.prefs.removeObserver(PREF_GRANULAR_OVERRIDES, this);
}
}