Bug 1882422: Use a local testing page to return complex data and make use of it in C++-land r=peterv

- Create a local page we will access via an about: URL
- In that page, demonstrate how to do some stuff, then fire a custom event
  indicating its stuff is done _and_ return complex data in that event.
  console.log() didn't seem to be visible in the HiddenWindow, so I also
  piped out debug strings for development purposes, so they can be
  console.logged in the Service, where we can see them.
- Instead of listening for DOMContentLoaded/pageshow, instead listen for
  the new CustomEvent.
- In UserCharacteristicsPageService, receive the data from the page and
  populate the appropriate Glean metrics
- Refactor the calling of nsUserCharacteristics::PopulateData() and
  SubmitPing().  Now we call PopulateDataAndEventuallySubmit() which will
  eventually call SubmitPing after our promise is resolved.
- To make it a little cleaner (so ContentPageStuff() isn't calling
  SubmitPing()) we return the promise out of ContentPageStuff() that
  PopulateDataAndEventuallySubmit() will await and then call SubmitPing()
  when that promise resolves

Differential Revision: https://phabricator.services.mozilla.com/D203055
This commit is contained in:
Tom Ritter 2024-04-03 23:43:17 +00:00
parent c8e3989334
commit c816dcfce3
15 changed files with 204 additions and 56 deletions

View File

@ -108,6 +108,12 @@ static const RedirEntry kRedirMap[] = {
{"credits", "https://www.mozilla.org/credits/",
nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
nsIAboutModule::URI_MUST_LOAD_IN_CHILD},
{"fingerprinting",
"chrome://global/content/usercharacteristics/usercharacteristics.html",
nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
nsIAboutModule::HIDE_FROM_ABOUTABOUT | nsIAboutModule::ALLOW_SCRIPT |
nsIAboutModule::URI_MUST_LOAD_IN_CHILD |
nsIAboutModule::URI_CAN_LOAD_IN_PRIVILEGEDABOUT_PROCESS},
{"httpsonlyerror", "chrome://global/content/httpsonlyerror/errorpage.html",
nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
nsIAboutModule::URI_CAN_LOAD_IN_CHILD | nsIAboutModule::ALLOW_SCRIPT |

View File

@ -16,6 +16,7 @@ about_pages = [
'crashgpu',
'crashextensions',
'credits',
'fingerprinting',
'httpsonlyerror',
'license',
'logging',

View File

@ -1367,6 +1367,7 @@ void nsContentSecurityUtils::AssertAboutPageHasCSP(Document* aDocument) {
StringBeginsWith(aboutSpec, "about:preferences"_ns) ||
StringBeginsWith(aboutSpec, "about:settings"_ns) ||
StringBeginsWith(aboutSpec, "about:downloads"_ns) ||
StringBeginsWith(aboutSpec, "about:fingerprinting"_ns) ||
StringBeginsWith(aboutSpec, "about:asrouter"_ns) ||
StringBeginsWith(aboutSpec, "about:newtab"_ns) ||
StringBeginsWith(aboutSpec, "about:logins"_ns) ||

View File

@ -16,12 +16,11 @@ export class UserCharacteristicsChild extends JSWindowActorChild {
handleEvent(event) {
lazy.console.debug("Got ", event.type);
switch (event.type) {
case "DOMContentLoaded":
case "pageshow":
case "UserCharacteristicsDataDone":
lazy.console.debug("creating IdleDispatch");
ChromeUtils.idleDispatch(() => {
lazy.console.debug("sending PageReady");
this.sendAsyncMessage("UserCharacteristics::PageReady");
this.sendAsyncMessage("UserCharacteristics::PageReady", event.detail);
});
break;
}

View File

@ -3,7 +3,7 @@
* 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";
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
ChromeUtils.defineLazyGetter(lazy, "console", () => {
@ -13,20 +13,22 @@ ChromeUtils.defineLazyGetter(lazy, "console", () => {
});
});
/*
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");
lazy.UserCharacteristicsPageService.pageLoaded(
this.browsingContext,
aMessage.data
);
}
}
}

View File

@ -114,10 +114,11 @@ class _RFPHelper {
child: {
esModuleURI: "resource://gre/actors/UserCharacteristicsChild.sys.mjs",
events: {
DOMContentLoaded: {},
pageshow: {},
UserCharacteristicsDataDone: { wantUntrusted: true },
},
},
matches: ["about:fingerprinting"],
remoteTypes: ["privilegedabout"],
});
}

View File

@ -145,13 +145,9 @@ export class UserCharacteristicsPageService {
return;
}
this._initialized = true;
lazy.console.debug("Init completes");
}
shutdown() {
lazy.console.debug("shutdown");
}
shutdown() {}
createContentPage() {
lazy.console.debug("called createContentPage");
@ -161,38 +157,51 @@ export class UserCharacteristicsPageService {
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,
};
let userCharacteristicsPageURI = Services.io.newURI(
"about:fingerprinting"
);
browser.loadURI(userCharacteristicsPageURI, loadURIOptions);
await promise;
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...");
// e.g. Glean.characteristics.timezone.set(data.output.foo)
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}`);
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();
backgroundResolve(data);
return;
}
throw new Error(`No backround resolve for ${browser} found`);

View File

@ -0,0 +1,18 @@
<!-- 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/. -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'none'; object-src 'none'; script-src chrome:"
/>
<title>about:fingerprinting</title>
</head>
<body>
<script src="chrome://global/content/usercharacteristics/usercharacteristics.js"></script>
</body>
</html>

View File

@ -0,0 +1,40 @@
/* 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/. */
var debugMsgs = [];
function debug(...args) {
let msg = "";
if (!args.length) {
debugMsgs.push("");
return;
}
let stringify = o => {
if (typeof o == "string") {
return o;
}
return JSON.stringify(o);
};
let stringifiedArgs = args.map(stringify);
msg += stringifiedArgs.join(" ");
debugMsgs.push(msg);
}
debug("Debug Line");
debug("Another debug line, with", { an: "object" });
let output = {
foo: "Hello World",
};
document.dispatchEvent(
new CustomEvent("UserCharacteristicsDataDone", {
bubbles: true,
detail: {
debug: debugMsgs,
output,
},
})
);

View File

@ -0,0 +1,7 @@
# 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/.
toolkit.jar:
content/global/usercharacteristics/usercharacteristics.html (content/usercharacteristics.html)
content/global/usercharacteristics/usercharacteristics.js (content/usercharacteristics.js)

View File

@ -9,6 +9,8 @@ with Files("**"):
TEST_DIRS += ["tests"]
JAR_MANIFESTS += ["jar.mn"]
UNIFIED_SOURCES += [
"nsRFPService.cpp",
"RelativeTimeline.cpp",

View File

@ -9,7 +9,7 @@ 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.
*/
@ -17,6 +17,7 @@ interface nsIUserCharacteristicsPageService : nsISupports {
/*
* Called when the UserCharacteristics about: page has been loaded
* and supplied data back to the actor, which is passed as `data`
*/
void pageLoaded(in BrowsingContext browsingContext);
void pageLoaded(in BrowsingContext browsingContext, in jsval data);
};

View File

@ -10,12 +10,14 @@
#include "nsIUserCharacteristicsPageService.h"
#include "nsServiceManagerUtils.h"
#include "mozilla/Components.h"
#include "mozilla/Logging.h"
#include "mozilla/dom/Promise-inl.h"
#include "mozilla/glean/GleanPings.h"
#include "mozilla/glean/GleanMetrics.h"
#include "jsapi.h"
#include "mozilla/Components.h"
#include "mozilla/dom/Promise-inl.h"
#include "mozilla/StaticPrefs_dom.h"
#include "mozilla/StaticPrefs_general.h"
#include "mozilla/StaticPrefs_media.h"
@ -62,13 +64,23 @@ int MaxTouchPoints() {
}; // namespace testing
// ==================================================================
void CanvasStuff() {
// ==================================================================
already_AddRefed<mozilla::dom::Promise> ContentPageStuff() {
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));
nsresult rv = ucp->CreateContentPage(getter_AddRefs(promise));
if (NS_FAILED(rv)) {
MOZ_LOG(gUserCharacteristicsLog, mozilla::LogLevel::Error,
("Could not create Content Page"));
return nullptr;
}
MOZ_LOG(gUserCharacteristicsLog, mozilla::LogLevel::Debug,
("Created Content Page"));
return promise.forget();
}
void PopulateCSSProperties() {
@ -168,6 +180,11 @@ void PopulatePrefs() {
// the source of the data we are looking at.
const int kSubmissionSchema = 1;
const auto* const kLastVersionPref =
"toolkit.telemetry.user_characteristics_ping.last_version_sent";
const auto* const kCurrentVersionPref =
"toolkit.telemetry.user_characteristics_ping.current_version";
/* static */
void nsUserCharacteristics::MaybeSubmitPing() {
MOZ_LOG(gUserCharacteristicsLog, LogLevel::Debug, ("In MaybeSubmitPing()"));
@ -188,11 +205,6 @@ void nsUserCharacteristics::MaybeSubmitPing() {
* Sent = Current Version.
*
*/
const auto* const kLastVersionPref =
"toolkit.telemetry.user_characteristics_ping.last_version_sent";
const auto* const kCurrentVersionPref =
"toolkit.telemetry.user_characteristics_ping.current_version";
auto lastSubmissionVersion = Preferences::GetInt(kLastVersionPref, 0);
auto currentVersion = Preferences::GetInt(kCurrentVersionPref, 0);
@ -216,9 +228,7 @@ void nsUserCharacteristics::MaybeSubmitPing() {
// currentVersion = -1 is a development value to force a ping submission
MOZ_LOG(gUserCharacteristicsLog, LogLevel::Debug,
("Force-Submitting Ping"));
if (NS_SUCCEEDED(PopulateData())) {
SubmitPing();
}
PopulateDataAndEventuallySubmit(false);
return;
}
if (lastSubmissionVersion > currentVersion) {
@ -237,11 +247,7 @@ void nsUserCharacteristics::MaybeSubmitPing() {
return;
}
if (lastSubmissionVersion < currentVersion) {
if (NS_SUCCEEDED(PopulateData())) {
if (NS_SUCCEEDED(SubmitPing())) {
Preferences::SetInt(kLastVersionPref, currentVersion);
}
}
PopulateDataAndEventuallySubmit(false);
} else {
MOZ_ASSERT_UNREACHABLE("Should never reach here");
}
@ -251,13 +257,18 @@ const auto* const kUUIDPref =
"toolkit.telemetry.user_characteristics_ping.uuid";
/* static */
nsresult nsUserCharacteristics::PopulateData(bool aTesting /* = false */) {
void nsUserCharacteristics::PopulateDataAndEventuallySubmit(
bool aUpdatePref /* = true */, bool aTesting /* = false */
) {
MOZ_LOG(gUserCharacteristicsLog, LogLevel::Warning, ("Populating Data"));
MOZ_ASSERT(XRE_IsParentProcess());
nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
NS_ENSURE_TRUE(obs, NS_ERROR_NOT_AVAILABLE);
if (!obs) {
return;
}
// This notification tells us to register the actor
obs->NotifyObservers(nullptr, "user-characteristics-populating-data",
nullptr);
@ -268,12 +279,17 @@ nsresult nsUserCharacteristics::PopulateData(bool aTesting /* = false */) {
if (NS_FAILED(rv) || uuidString.Length() == 0) {
nsCOMPtr<nsIUUIDGenerator> uuidgen =
do_GetService("@mozilla.org/uuid-generator;1", &rv);
NS_ENSURE_SUCCESS(rv, rv);
if (NS_FAILED(rv)) {
return;
}
nsIDToCString id(nsID::GenerateUUID());
uuidString = id.get();
Preferences::SetCString(kUUIDPref, uuidString);
}
// ------------------------------------------------------------------------
glean::characteristics::client_identifier.Set(uuidString);
glean::characteristics::max_touch_points.Set(testing::MaxTouchPoints());
@ -281,9 +297,11 @@ nsresult nsUserCharacteristics::PopulateData(bool aTesting /* = false */) {
if (aTesting) {
// Many of the later peices of data do not work in a gtest
// so just populate something, and return
return NS_OK;
return;
}
// ------------------------------------------------------------------------
PopulateMissingFonts();
PopulateCSSProperties();
PopulateScreenProperties();
@ -318,13 +336,47 @@ nsresult nsUserCharacteristics::PopulateData(bool aTesting /* = false */) {
intl::OSPreferences::GetInstance()->GetSystemLocale(locale);
glean::characteristics::system_locale.Set(locale);
return NS_OK;
// When this promise resolves, everything succeeded and we can submit.
RefPtr<mozilla::dom::Promise> promise = ContentPageStuff();
// ------------------------------------------------------------------------
auto fulfillSteps = [aUpdatePref, aTesting](
JSContext* aCx, JS::Handle<JS::Value> aPromiseResult,
mozilla::ErrorResult& aRv) {
MOZ_LOG(gUserCharacteristicsLog, mozilla::LogLevel::Debug,
("ContentPageStuff Promise Resolved"));
nsUserCharacteristics::SubmitPing();
if (aUpdatePref) {
MOZ_LOG(gUserCharacteristicsLog, mozilla::LogLevel::Debug,
("Updating preference"));
auto current_version =
mozilla::Preferences::GetInt(kCurrentVersionPref, 0);
mozilla::Preferences::SetInt(kLastVersionPref, current_version);
}
};
// Something failed in the Content Page...
auto rejectSteps = [](JSContext* aCx, JS::Handle<JS::Value> aReason,
mozilla::ErrorResult& aRv) {
MOZ_LOG(gUserCharacteristicsLog, mozilla::LogLevel::Error,
("ContentPageStuff Promise Rejected"));
};
if (promise) {
promise->AddCallbacksWithCycleCollectedArgs(std::move(fulfillSteps),
std::move(rejectSteps));
} else {
MOZ_LOG(gUserCharacteristicsLog, mozilla::LogLevel::Error,
("Did not get a Promise back from ContentPageStuff"));
}
}
/* static */
nsresult nsUserCharacteristics::SubmitPing() {
MOZ_LOG(gUserCharacteristicsLog, LogLevel::Warning, ("Submitting Ping"));
void nsUserCharacteristics::SubmitPing() {
MOZ_LOG(gUserCharacteristicsLog, mozilla::LogLevel::Warning,
("Submitting Ping"));
glean_pings::UserCharacteristics.Submit();
return NS_OK;
}

View File

@ -12,9 +12,15 @@ class nsUserCharacteristics {
public:
static void MaybeSubmitPing();
// Public For testing
static nsresult PopulateData(bool aTesting = false);
static nsresult SubmitPing();
/*
* These APIs are public only for testing using the gtest
* When PopulateDataAndEventuallySubmit is called with aTesting = true
* it will not submit the data, and SubmitPing must be called explicitly.
* This is perfect because that's what we want for the gtest.
*/
static void PopulateDataAndEventuallySubmit(bool aUpdatePref = true,
bool aTesting = false);
static void SubmitPing();
};
namespace testing {

View File

@ -35,7 +35,8 @@ TEST(ResistFingerprinting, UserCharacteristics_Simple)
TEST(ResistFingerprinting, UserCharacteristics_Complex)
{
nsUserCharacteristics::PopulateData(true);
nsUserCharacteristics::PopulateDataAndEventuallySubmit(
/* aUpdatePref = */ false, /* aTesting = */ true);
bool submitted = false;
mozilla::glean_pings::UserCharacteristics.TestBeforeNextSubmit(
@ -102,7 +103,8 @@ TEST(ResistFingerprinting, UserCharacteristics_ClearPref)
.value()
.get());
});
nsUserCharacteristics::PopulateData(true);
nsUserCharacteristics::PopulateDataAndEventuallySubmit(
/* aUpdatePref = */ false, /* aTesting = */ true);
nsUserCharacteristics::SubmitPing();
auto original_value =
@ -135,7 +137,8 @@ TEST(ResistFingerprinting, UserCharacteristics_ClearPref)
Preferences::GetCString(kUUIDPref, uuidValue);
ASSERT_STRNE("", uuidValue.get());
});
nsUserCharacteristics::PopulateData(true);
nsUserCharacteristics::PopulateDataAndEventuallySubmit(
/* aUpdatePref = */ false, /* aTesting = */ true);
nsUserCharacteristics::SubmitPing();
Preferences::SetBool("datareporting.healthreport.uploadEnabled",