Bug 1882422: JSActor and Component Structure to get some data back from JS-land in C++-land r=Gijs

- Create a new XPCOM component UserCharacteristicsPageService backed by
  a JS implementation.  It will be accessed as a Service, so there is
  only ever one of them
- Inside UserCharacteristicsPageService use a HiddenFrame to open a
  page invisible to the user. (In this patch it's a random https://
  site, in the next patch it will become an about: page)
- Create a Parent/Child JSWindowActor so that the hidden webpage (which
  will eventually be in the privilegedabout content process) can
  communicate back to the parent.
- Use Topics/Observers so the Actor can be registered when we need it
  and then unregistered when we're done. The registering occurs in a
  RFPHelper which a separate thing already running and listening for
  stuff.
- Inside nsUserCharacteristics.cpp::CanvasStuff() we ask our service
  to load the webpage, then will await a promise indicating the page has
  been loaded. That promise is the return value of
  UserCharacteristicsPageService::createContentPage()  and is resolved after
  UserCharacteristicsPageService::pageLoaded calls backgroundResolve().
  That, in turn, happens after UserCharacteristicsParent gets the
  message from UserCharacteristicsChild that the DOMContentLoaded
  event has occurred.

The HiddenFrame stuff is influenced by PageDataService.sys.mjs -
that's where the HiddenBrowserManager stuff came from.

In the next patch we'll actually do something when that promise is
resolved.

Differential Revision: https://phabricator.services.mozilla.com/D202893
This commit is contained in:
Tom Ritter 2024-04-03 23:43:16 +00:00
parent 2d6b956c61
commit c8e3989334
10 changed files with 350 additions and 7 deletions

View file

@ -641,6 +641,7 @@ pref("toolkit.telemetry.user_characteristics_ping.last_version_sent", 0);
// the telemetry client id (which is not sent in this ping), it is cleared when a // the telemetry client id (which is not sent in this ping), it is cleared when a
// user opts-out of telemetry, it is set upon first telemetry submission // user opts-out of telemetry, it is set upon first telemetry submission
pref("toolkit.telemetry.user_characteristics_ping.uuid", ""); pref("toolkit.telemetry.user_characteristics_ping.uuid", "");
pref("toolkit.telemetry.user_characteristics_ping.logLevel", "Warn");
// AsyncShutdown delay before crashing in case of shutdown freeze // AsyncShutdown delay before crashing in case of shutdown freeze
// ASan, TSan and code coverage builds can be considerably slower. Extend the // ASan, TSan and code coverage builds can be considerably slower. Extend the

View file

@ -0,0 +1,29 @@
/* -*- 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.defineLazyGetter(lazy, "console", () => {
return console.createInstance({
prefix: "UserCharacteristicsPage",
maxLogLevelPref: "toolkit.telemetry.user_characteristics_ping.logLevel",
});
});
export class UserCharacteristicsChild extends JSWindowActorChild {
handleEvent(event) {
lazy.console.debug("Got ", event.type);
switch (event.type) {
case "DOMContentLoaded":
case "pageshow":
lazy.console.debug("creating IdleDispatch");
ChromeUtils.idleDispatch(() => {
lazy.console.debug("sending PageReady");
this.sendAsyncMessage("UserCharacteristics::PageReady");
});
break;
}
}
}

View file

@ -0,0 +1,32 @@
/* -*- 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/. */
//import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
ChromeUtils.defineLazyGetter(lazy, "console", () => {
return console.createInstance({
prefix: "UserCharacteristicsPage",
maxLogLevelPref: "toolkit.telemetry.user_characteristics_ping.logLevel",
});
});
/*
XPCOMUtils.defineLazyServiceGetter(
lazy,
"UserCharacteristicsPageService",
"@mozilla.org/user-characteristics-page;1",
"nsIUserCharacteristicsPageService"
);
*/
export class UserCharacteristicsParent extends JSWindowActorParent {
receiveMessage(aMessage) {
lazy.console.debug("Got ", aMessage.name);
if (aMessage.name == "UserCharacteristics::PageReady") {
lazy.console.debug("Got pageReady");
}
}
}

View file

@ -75,6 +75,8 @@ FINAL_TARGET_FILES.actors += [
"UAWidgetsChild.sys.mjs", "UAWidgetsChild.sys.mjs",
"UnselectedTabHoverChild.sys.mjs", "UnselectedTabHoverChild.sys.mjs",
"UnselectedTabHoverParent.sys.mjs", "UnselectedTabHoverParent.sys.mjs",
"UserCharacteristicsChild.sys.mjs",
"UserCharacteristicsParent.sys.mjs",
"ViewSourceChild.sys.mjs", "ViewSourceChild.sys.mjs",
"ViewSourcePageChild.sys.mjs", "ViewSourcePageChild.sys.mjs",
"ViewSourcePageParent.sys.mjs", "ViewSourcePageParent.sys.mjs",

View file

@ -43,6 +43,8 @@ class _RFPHelper {
this._initialized = true; this._initialized = true;
// Add unconditional observers // Add unconditional observers
Services.obs.addObserver(this, "user-characteristics-populating-data");
Services.obs.addObserver(this, "user-characteristics-populating-data-done");
Services.prefs.addObserver(kPrefResistFingerprinting, this); Services.prefs.addObserver(kPrefResistFingerprinting, this);
Services.prefs.addObserver(kPrefLetterboxing, this); Services.prefs.addObserver(kPrefLetterboxing, this);
XPCOMUtils.defineLazyPreferenceGetter( XPCOMUtils.defineLazyPreferenceGetter(
@ -80,6 +82,12 @@ class _RFPHelper {
observe(subject, topic, data) { observe(subject, topic, data) {
switch (topic) { switch (topic) {
case "user-characteristics-populating-data":
this._registerUserCharacteristicsActor();
break;
case "user-characteristics-populating-data-done":
this._unregisterUserCharacteristicsActor();
break;
case "nsPref:changed": case "nsPref:changed":
this._handlePrefChanged(data); this._handlePrefChanged(data);
break; break;
@ -97,6 +105,27 @@ class _RFPHelper {
} }
} }
_registerUserCharacteristicsActor() {
log("_registerUserCharacteristicsActor()");
ChromeUtils.registerWindowActor("UserCharacteristics", {
parent: {
esModuleURI: "resource://gre/actors/UserCharacteristicsParent.sys.mjs",
},
child: {
esModuleURI: "resource://gre/actors/UserCharacteristicsChild.sys.mjs",
events: {
DOMContentLoaded: {},
pageshow: {},
},
},
});
}
_unregisterUserCharacteristicsActor() {
log("_unregisterUserCharacteristicsActor()");
ChromeUtils.unregisterWindowActor("UserCharacteristics");
}
handleEvent(aMessage) { handleEvent(aMessage) {
switch (aMessage.type) { switch (aMessage.type) {
case "TabOpen": { case "TabOpen": {
@ -283,16 +312,16 @@ class _RFPHelper {
_handleLetterboxingPrefChanged() { _handleLetterboxingPrefChanged() {
if (Services.prefs.getBoolPref(kPrefLetterboxing, false)) { if (Services.prefs.getBoolPref(kPrefLetterboxing, false)) {
Services.ww.registerNotification(this); Services.ww.registerNotification(this);
this._registerActor(); this._registerLetterboxingActor();
this._attachAllWindows(); this._attachAllWindows();
} else { } else {
this._unregisterActor(); this._unregisterLetterboxingActor();
this._detachAllWindows(); this._detachAllWindows();
Services.ww.unregisterNotification(this); Services.ww.unregisterNotification(this);
} }
} }
_registerActor() { _registerLetterboxingActor() {
ChromeUtils.registerWindowActor("RFPHelper", { ChromeUtils.registerWindowActor("RFPHelper", {
parent: { parent: {
esModuleURI: "resource:///actors/RFPHelperParent.sys.mjs", esModuleURI: "resource:///actors/RFPHelperParent.sys.mjs",
@ -307,7 +336,7 @@ class _RFPHelper {
}); });
} }
_unregisterActor() { _unregisterLetterboxingActor() {
ChromeUtils.unregisterWindowActor("RFPHelper"); ChromeUtils.unregisterWindowActor("RFPHelper");
} }

View file

@ -0,0 +1,200 @@
// -*- 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",
});
});
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;
lazy.console.debug("Init completes");
}
shutdown() {
lazy.console.debug("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 userCharacteristicsPageURI =
Services.io.newURI("https://ritter.vg");
let principal = Services.scriptSecurityManager.getSystemPrincipal();
let loadURIOptions = {
triggeringPrincipal: principal,
};
browser.loadURI(userCharacteristicsPageURI, loadURIOptions);
await promise;
lazy.console.debug("Unregistering actor");
Services.obs.notifyObservers(
null,
"user-characteristics-populating-data-done"
);
lazy.console.debug(`Returning`);
} finally {
lazy.console.debug(`In finally`);
this._backgroundBrowsers.delete(browser);
}
});
}
async pageLoaded(browsingContext) {
lazy.console.debug(`pageLoaded browsingContext=${browsingContext}`);
let browser = browsingContext.embedderElement;
let backgroundResolve = this._backgroundBrowsers.get(browser);
if (backgroundResolve) {
backgroundResolve();
return;
}
throw new Error(`No backround resolve for ${browser} found`);
}
}

View file

@ -2,7 +2,7 @@
# vim: set filetype=python: # vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public # 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 # 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/. # file, You can obtain one at https://mozilla.org/MPL/2.0/.
Classes = [ Classes = [
{ {
@ -34,4 +34,11 @@ Classes = [
'constructor': 'FingerprintingWebCompatService', 'constructor': 'FingerprintingWebCompatService',
'processes': ProcessSelector.MAIN_PROCESS_ONLY, 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
}, },
{
'cid': '{ce3e9659-e311-49fb-b18b-7f27c6659b23}',
'contract_ids': ['@mozilla.org/user-characteristics-page;1'],
'esModule': 'resource://gre/modules/UserCharacteristicsPageService.sys.mjs',
'constructor': 'UserCharacteristicsPageService',
'processes': ProcessSelector.MAIN_PROCESS_ONLY,
},
] ]

View file

@ -2,7 +2,7 @@
# vim: set filetype=python: # vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public # 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 # 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/. # file, You can obtain one at https://mozilla.org/MPL/2.0/.
with Files("**"): with Files("**"):
BUG_COMPONENT = ("Core", "Privacy: Anti-Tracking") BUG_COMPONENT = ("Core", "Privacy: Anti-Tracking")
@ -39,6 +39,7 @@ EXPORTS.mozilla.gtest += ["nsUserCharacteristics.h"]
EXTRA_JS_MODULES += [ EXTRA_JS_MODULES += [
"FingerprintingWebCompatService.sys.mjs", "FingerprintingWebCompatService.sys.mjs",
"RFPHelper.sys.mjs", "RFPHelper.sys.mjs",
"UserCharacteristicsPageService.sys.mjs",
] ]
XPIDL_MODULE = "toolkit_resistfingerprinting" XPIDL_MODULE = "toolkit_resistfingerprinting"
@ -50,6 +51,7 @@ XPCOM_MANIFESTS += [
XPIDL_SOURCES += [ XPIDL_SOURCES += [
"nsIFingerprintingWebCompatService.idl", "nsIFingerprintingWebCompatService.idl",
"nsIRFPService.idl", "nsIRFPService.idl",
"nsIUserCharacteristicsPageService.idl",
] ]
include("/ipc/chromium/chromium-config.mozbuild") include("/ipc/chromium/chromium-config.mozbuild")

View file

@ -0,0 +1,22 @@
/* 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/. */
#include "nsISupports.idl"
webidl BrowsingContext;
[scriptable, uuid(ce3e9659-e311-49fb-b18b-7f27c6659b23)]
interface nsIUserCharacteristicsPageService : nsISupports {
/* (In the next patch, this will:)
* Create the UserCharacteristics about: page as a HiddenFrame
* and begin the data collection.
*/
Promise createContentPage();
/*
* Called when the UserCharacteristics about: page has been loaded
*/
void pageLoaded(in BrowsingContext browsingContext);
};

View file

@ -1,15 +1,18 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public /* 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 * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
#include "nsUserCharacteristics.h" #include "nsUserCharacteristics.h"
#include "nsID.h" #include "nsID.h"
#include "nsIUUIDGenerator.h" #include "nsIUUIDGenerator.h"
#include "nsIUserCharacteristicsPageService.h"
#include "nsServiceManagerUtils.h" #include "nsServiceManagerUtils.h"
#include "mozilla/Components.h"
#include "mozilla/Logging.h" #include "mozilla/Logging.h"
#include "mozilla/dom/Promise-inl.h"
#include "mozilla/glean/GleanPings.h" #include "mozilla/glean/GleanPings.h"
#include "mozilla/glean/GleanMetrics.h" #include "mozilla/glean/GleanMetrics.h"
@ -59,6 +62,15 @@ int MaxTouchPoints() {
}; // namespace testing }; // namespace testing
// ================================================================== // ==================================================================
void CanvasStuff() {
nsCOMPtr<nsIUserCharacteristicsPageService> ucp =
do_GetService("@mozilla.org/user-characteristics-page;1");
MOZ_ASSERT(ucp);
RefPtr<mozilla::dom::Promise> promise;
mozilla::Unused << ucp->CreateContentPage(getter_AddRefs(promise));
}
void PopulateCSSProperties() { void PopulateCSSProperties() {
glean::characteristics::prefers_reduced_transparency.Set( glean::characteristics::prefers_reduced_transparency.Set(
LookAndFeel::GetInt(LookAndFeel::IntID::PrefersReducedTransparency)); LookAndFeel::GetInt(LookAndFeel::IntID::PrefersReducedTransparency));
@ -242,6 +254,13 @@ const auto* const kUUIDPref =
nsresult nsUserCharacteristics::PopulateData(bool aTesting /* = false */) { nsresult nsUserCharacteristics::PopulateData(bool aTesting /* = false */) {
MOZ_LOG(gUserCharacteristicsLog, LogLevel::Warning, ("Populating Data")); MOZ_LOG(gUserCharacteristicsLog, LogLevel::Warning, ("Populating Data"));
MOZ_ASSERT(XRE_IsParentProcess()); MOZ_ASSERT(XRE_IsParentProcess());
nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
NS_ENSURE_TRUE(obs, NS_ERROR_NOT_AVAILABLE);
obs->NotifyObservers(nullptr, "user-characteristics-populating-data",
nullptr);
glean::characteristics::submission_schema.Set(kSubmissionSchema); glean::characteristics::submission_schema.Set(kSubmissionSchema);
nsAutoCString uuidString; nsAutoCString uuidString;