fune/toolkit/components/resistfingerprinting/UserCharacteristicsPageService.sys.mjs
Tom Ritter 71b7d5ccb5 Bug 1894958: Let's name this page more accurately r=timhuang
The purpose of this page is to improve the fingerprinting protections
in Firefox.

Differential Revision: https://phabricator.services.mozilla.com/D209599
2024-06-04 21:24:00 +00:00

267 lines
7.5 KiB
JavaScript

// -*- 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, {
HiddenFrame: "resource://gre/modules/HiddenFrame.sys.mjs",
});
ChromeUtils.defineLazyGetter(lazy, "console", () => {
return console.createInstance({
prefix: "UserCharacteristicsPage",
maxLogLevelPref: "toolkit.telemetry.user_characteristics_ping.logLevel",
});
});
ChromeUtils.defineLazyGetter(lazy, "contentPrefs", () => {
return Cc["@mozilla.org/content-pref/service;1"].getService(
Ci.nsIContentPrefService2
);
});
const BACKGROUND_WIDTH = 1024;
const BACKGROUND_HEIGHT = 768;
/**
* A manager for hidden browsers. Responsible for creating and destroying a
* hidden frame to hold them.
* All of this is copied from PageDataService.sys.mjs
*/
class HiddenBrowserManager {
/**
* The hidden frame if one has been created.
*
* @type {HiddenFrame | null}
*/
#frame = null;
/**
* The number of hidden browser elements currently in use.
*
* @type {number}
*/
#browsers = 0;
/**
* Creates and returns a new hidden browser.
*
* @returns {Browser}
*/
async #acquireBrowser() {
this.#browsers++;
if (!this.#frame) {
this.#frame = new lazy.HiddenFrame();
}
let frame = await this.#frame.get();
let doc = frame.document;
let browser = doc.createXULElement("browser");
browser.setAttribute("remote", "true");
browser.setAttribute("type", "content");
browser.setAttribute(
"style",
`
width: ${BACKGROUND_WIDTH}px;
min-width: ${BACKGROUND_WIDTH}px;
height: ${BACKGROUND_HEIGHT}px;
min-height: ${BACKGROUND_HEIGHT}px;
`
);
browser.setAttribute("maychangeremoteness", "true");
doc.documentElement.appendChild(browser);
return browser;
}
/**
* Releases the given hidden browser.
*
* @param {Browser} browser
* The hidden browser element.
*/
#releaseBrowser(browser) {
browser.remove();
this.#browsers--;
if (this.#browsers == 0) {
this.#frame.destroy();
this.#frame = null;
}
}
/**
* Calls a callback function with a new hidden browser.
* This function will return whatever the callback function returns.
*
* @param {Callback} callback
* The callback function will be called with the browser element and may
* be asynchronous.
* @returns {T}
*/
async withHiddenBrowser(callback) {
let browser = await this.#acquireBrowser();
try {
return await callback(browser);
} finally {
this.#releaseBrowser(browser);
}
}
}
export class UserCharacteristicsPageService {
classId = Components.ID("{ce3e9659-e311-49fb-b18b-7f27c6659b23}");
QueryInterface = ChromeUtils.generateQI([
"nsIUserCharacteristicsPageService",
]);
_initialized = false;
_isParentProcess = false;
/**
* A manager for hidden browsers.
*
* @type {HiddenBrowserManager}
*/
_browserManager = new HiddenBrowserManager();
/**
* A map of hidden browsers to a resolve function that should be passed the
* actor that was created for the browser.
*
* @type {WeakMap<Browser, function(PageDataParent): void>}
*/
_backgroundBrowsers = new WeakMap();
constructor() {
lazy.console.debug("Init");
if (
Services.appinfo.processType !== Services.appinfo.PROCESS_TYPE_DEFAULT
) {
throw new Error(
"Shouldn't init UserCharacteristicsPage in content processes."
);
}
// Return if we have initiated.
if (this._initialized) {
lazy.console.warn("preventing re-initilization...");
return;
}
this._initialized = true;
}
shutdown() {}
createContentPage() {
lazy.console.debug("called createContentPage");
return this._browserManager.withHiddenBrowser(async browser => {
lazy.console.debug(`In withHiddenBrowser`);
try {
let { promise, resolve } = Promise.withResolvers();
this._backgroundBrowsers.set(browser, resolve);
let principal = Services.scriptSecurityManager.getSystemPrincipal();
let loadURIOptions = {
triggeringPrincipal: principal,
};
let userCharacteristicsPageURI = Services.io.newURI(
"about:fingerprintingprotection"
);
browser.loadURI(userCharacteristicsPageURI, loadURIOptions);
let data = await promise;
if (data.debug) {
lazy.console.debug(`Debugging Output:`);
for (let line of data.debug) {
lazy.console.debug(line);
}
lazy.console.debug(`(debugging output done)`);
}
lazy.console.debug(`Data:`, data.output);
lazy.console.debug("Populating Glean metrics...");
Glean.characteristics.timezone.set(data.output.foo);
for (let gamepad of data.output.gamepads) {
Glean.characteristics.gamepads.add(gamepad);
}
Glean.characteristics.zoomCount.set(await this.populateZoomPrefs());
try {
Glean.characteristics.canvasdata1.set(data.output.canvas1data);
Glean.characteristics.canvasdata2.set(data.output.canvas2data);
Glean.characteristics.canvasdata3.set(data.output.canvas3data);
Glean.characteristics.canvasdata4.set(data.output.canvas4data);
Glean.characteristics.canvasdata5.set(data.output.canvas5data);
Glean.characteristics.canvasdata6.set(data.output.canvas6data);
Glean.characteristics.canvasdata7.set(data.output.canvas7data);
Glean.characteristics.canvasdata8.set(data.output.canvas8data);
Glean.characteristics.canvasdata9.set(data.output.canvas9data);
Glean.characteristics.canvasdata10.set(data.output.canvas10data);
Glean.characteristics.canvasdata11Webgl.set(data.output.glcanvasdata);
Glean.characteristics.canvasdata12Fingerprintjs1.set(
data.output.fingerprintjscanvas1data
);
Glean.characteristics.canvasdata13Fingerprintjs2.set(
data.output.fingerprintjscanvas2data
);
for (let gamepad of data.output.gamepads) {
Glean.characteristics.gamepads.add(gamepad);
}
} catch (e) {
// Grab the exception and send it to the console
// (we don't see it otherwise)
lazy.console.debug(e);
// But still fail
throw e;
}
lazy.console.debug("Unregistering actor");
Services.obs.notifyObservers(
null,
"user-characteristics-populating-data-done"
);
} finally {
this._backgroundBrowsers.delete(browser);
}
});
}
async populateZoomPrefs() {
const zoomPrefsCount = await new Promise(resolve => {
lazy.contentPrefs.getByName("browser.content.full-zoom", null, {
_result: 0,
handleResult(_) {
this._result++;
},
handleCompletion() {
resolve(this._result);
},
});
});
return zoomPrefsCount;
}
async pageLoaded(browsingContext, data) {
lazy.console.debug(
`pageLoaded browsingContext=${browsingContext} data=${data}`
);
let browser = browsingContext.embedderElement;
let backgroundResolve = this._backgroundBrowsers.get(browser);
if (backgroundResolve) {
backgroundResolve(data);
return;
}
throw new Error(`No backround resolve for ${browser} found`);
}
}