264 lines
7.9 KiB
JavaScript
264 lines
7.9 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/. */
|
|
|
|
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
|
import {
|
|
ContextIdCallback,
|
|
ContextIdComponent,
|
|
} from "moz-src:///toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustContextId.sys.mjs";
|
|
|
|
const CONTEXT_ID_PREF = "browser.contextual-services.contextId";
|
|
const CONTEXT_ID_TIMESTAMP_PREF =
|
|
"browser.contextual-services.contextId.timestamp-in-seconds";
|
|
const CONTEXT_ID_ROTATION_DAYS_PREF =
|
|
"browser.contextual-services.contextId.rotation-in-days";
|
|
const CONTEXT_ID_RUST_COMPONENT_ENABLED_PREF =
|
|
"browser.contextual-services.contextId.rust-component.enabled";
|
|
const SHUTDOWN_TOPIC = "profile-before-change";
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
ObliviousHTTP: "resource://gre/modules/ObliviousHTTP.sys.mjs",
|
|
});
|
|
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
lazy,
|
|
"CURRENT_CONTEXT_ID",
|
|
CONTEXT_ID_PREF,
|
|
""
|
|
);
|
|
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
lazy,
|
|
"UNIFIED_ADS_ENDPOINT",
|
|
"browser.newtabpage.activity-stream.unifiedAds.endpoint",
|
|
""
|
|
);
|
|
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
lazy,
|
|
"OHTTP_RELAY_URL",
|
|
"browser.newtabpage.activity-stream.discoverystream.ohttp.relayURL",
|
|
""
|
|
);
|
|
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
lazy,
|
|
"OHTTP_CONFIG_URL",
|
|
"browser.newtabpage.activity-stream.discoverystream.ohttp.configURL",
|
|
""
|
|
);
|
|
|
|
class JsContextIdCallback extends ContextIdCallback {
|
|
constructor(dispatchEvent) {
|
|
super();
|
|
this.dispatchEvent = dispatchEvent;
|
|
}
|
|
|
|
persist(newContextId, creationTimestamp) {
|
|
Services.prefs.setCharPref(CONTEXT_ID_PREF, newContextId);
|
|
Services.prefs.setIntPref(CONTEXT_ID_TIMESTAMP_PREF, creationTimestamp);
|
|
this.dispatchEvent(new CustomEvent("ContextId:Persisted"));
|
|
}
|
|
|
|
rotated(oldContextId) {
|
|
GleanPings.contextIdDeletionRequest.setEnabled(true);
|
|
|
|
Glean.contextualServices.contextId.set(oldContextId);
|
|
GleanPings.contextIdDeletionRequest.submit();
|
|
ContextId.sendMARSDeletionRequest(oldContextId);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A class that manages and (optionally) rotates the context ID, which is a
|
|
* a unique identifier used by Contextual Services.
|
|
*/
|
|
export class _ContextId extends EventTarget {
|
|
#comp = null;
|
|
#rotationDays = 0;
|
|
#rustComponentEnabled = false;
|
|
#observer = null;
|
|
|
|
constructor() {
|
|
super();
|
|
|
|
this.#rustComponentEnabled = Services.prefs.getBoolPref(
|
|
CONTEXT_ID_RUST_COMPONENT_ENABLED_PREF,
|
|
false
|
|
);
|
|
|
|
if (this.#rustComponentEnabled) {
|
|
// We intentionally read this once at construction, and cache the result.
|
|
// This is because enabling or disabling rotation may affect external
|
|
// uses of _ContextId which (for example) send the context_id UUID to
|
|
// Shredder in the context-id-deletion-request ping (which we only want to
|
|
// do when rotation is disabled), and that sort of thing tends to get set
|
|
// once during startup.
|
|
this.#rotationDays = Services.prefs.getIntPref(
|
|
CONTEXT_ID_ROTATION_DAYS_PREF,
|
|
0
|
|
);
|
|
// Note that we're setting `running_in_test_automation` to true
|
|
// all of the time. This is because we don't want the ContextID
|
|
// component to be responsible for sending the DELETE request to MARS,
|
|
// since it doesn't know to do it over OHTTP. We'll send the DELETE
|
|
// request ourselves over OHTTP at rotation time.
|
|
this.#comp = ContextIdComponent.init(
|
|
lazy.CURRENT_CONTEXT_ID,
|
|
Services.prefs.getIntPref(CONTEXT_ID_TIMESTAMP_PREF, 0),
|
|
true /* running_in_test_automation */,
|
|
new JsContextIdCallback(this.dispatchEvent.bind(this))
|
|
);
|
|
this.#observer = (subject, topic, data) => {
|
|
this.observe(subject, topic, data);
|
|
};
|
|
|
|
Services.obs.addObserver(this.#observer, SHUTDOWN_TOPIC);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* nsIObserver implementation.
|
|
*
|
|
* @param {nsISupports} _subject
|
|
* @param {string} topic
|
|
* @param {string} _data
|
|
*/
|
|
observe(_subject, topic, _data) {
|
|
if (topic == SHUTDOWN_TOPIC) {
|
|
// Unregister ourselves as the callback to avoid leak assertions.
|
|
this.#comp.unsetCallback();
|
|
Services.obs.removeObserver(this.#observer, SHUTDOWN_TOPIC);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the stored context ID for this profile, if one exists. If one
|
|
* doesn't exist, one is generated and then returned. In the event that
|
|
* context ID rotation is in effect, then this may return a different
|
|
* context ID if we've determined it's time to rotate. This means that
|
|
* consumers _should not_ cache the context ID, but always request it.
|
|
*
|
|
* @returns {Promise<string>}
|
|
* The context ID for this profile.
|
|
*/
|
|
async request() {
|
|
if (this.#rustComponentEnabled) {
|
|
return this.#comp.request(this.#rotationDays);
|
|
}
|
|
|
|
// Fallback to the legacy behaviour of just returning the pref, or
|
|
// generating / returning a UUID if the pref is false-y.
|
|
if (!lazy.CURRENT_CONTEXT_ID) {
|
|
let _contextId = Services.uuid.generateUUID().toString();
|
|
Services.prefs.setStringPref(CONTEXT_ID_PREF, _contextId);
|
|
}
|
|
|
|
return Promise.resolve(lazy.CURRENT_CONTEXT_ID);
|
|
}
|
|
|
|
/**
|
|
* Forces the rotation of the context ID. This should be used by callers when
|
|
* some surface that uses the context ID is disabled. This is only supported
|
|
* with the Rust backend, and is a no-op when the Rust backend is not enabled.
|
|
*
|
|
* @returns {Promise<undefined>}
|
|
*/
|
|
async forceRotation() {
|
|
if (this.#rustComponentEnabled) {
|
|
return this.#comp.forceRotation();
|
|
}
|
|
return Promise.resolve();
|
|
}
|
|
|
|
/**
|
|
* Returns true if context ID rotation is enabled.
|
|
*
|
|
* @returns {boolean}
|
|
*/
|
|
get rotationEnabled() {
|
|
return this.#rustComponentEnabled && this.#rotationDays > 0;
|
|
}
|
|
|
|
/**
|
|
* A compatibility shim that only works if rotationEnabled is false which
|
|
* returns the context ID synchronously. This will throw if rotationEnabled
|
|
* is true - so callers should ensure that rotationEnabled is false before
|
|
* using this. This will eventually be removed.
|
|
*/
|
|
requestSynchronously() {
|
|
if (this.rotationEnabled) {
|
|
throw new Error(
|
|
"Cannot request context ID synchronously when rotation is enabled."
|
|
);
|
|
}
|
|
|
|
return lazy.CURRENT_CONTEXT_ID;
|
|
}
|
|
|
|
/**
|
|
* For now, the context_id application-services component does not know how
|
|
* to send the MARS deletion request over OHTTP, so we do it ourselves
|
|
* manually, using the New Tab unified ads preferences. This will eventually
|
|
* go away once the context_id component knows how to use OHTTP itself.
|
|
*
|
|
* @param {string} oldContextId
|
|
* The old context_id being rotated away from.
|
|
* @returns {Promise<undefined>}
|
|
*/
|
|
async sendMARSDeletionRequest(oldContextId) {
|
|
if (
|
|
!lazy.UNIFIED_ADS_ENDPOINT ||
|
|
!lazy.OHTTP_RELAY_URL ||
|
|
!lazy.OHTTP_CONFIG_URL
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const endpoint = `${lazy.UNIFIED_ADS_ENDPOINT}v1/delete_user`;
|
|
const body = {
|
|
context_id: oldContextId,
|
|
};
|
|
const headers = new Headers();
|
|
headers.append("content-type", "application/json");
|
|
|
|
const config = await lazy.ObliviousHTTP.getOHTTPConfig(
|
|
lazy.OHTTP_CONFIG_URL
|
|
);
|
|
|
|
if (!config) {
|
|
console.error(
|
|
new Error(
|
|
`OHTTP was configured for ${endpoint} but we couldn't fetch a valid config`
|
|
)
|
|
);
|
|
}
|
|
|
|
// We don't actually use this AbortController, but ObliviousHTTP wants it.
|
|
const controller = new AbortController();
|
|
const { signal } = controller;
|
|
|
|
const response = await lazy.ObliviousHTTP.ohttpRequest(
|
|
lazy.OHTTP_RELAY_URL,
|
|
config,
|
|
endpoint,
|
|
{
|
|
method: "DELETE",
|
|
headers,
|
|
body: JSON.stringify(body),
|
|
credentials: "omit",
|
|
signal,
|
|
}
|
|
);
|
|
|
|
if (!response.ok) {
|
|
console.error(new Error(`Unexpected status (${response.status})`));
|
|
}
|
|
}
|
|
}
|
|
|
|
export const ContextId = new _ContextId();
|