ContextId.sys.mjs (8127B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 6 import { 7 ContextIdCallback, 8 ContextIdComponent, 9 } from "moz-src:///toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustContextId.sys.mjs"; 10 11 const CONTEXT_ID_PREF = "browser.contextual-services.contextId"; 12 const CONTEXT_ID_TIMESTAMP_PREF = 13 "browser.contextual-services.contextId.timestamp-in-seconds"; 14 const CONTEXT_ID_ROTATION_DAYS_PREF = 15 "browser.contextual-services.contextId.rotation-in-days"; 16 const CONTEXT_ID_RUST_COMPONENT_ENABLED_PREF = 17 "browser.contextual-services.contextId.rust-component.enabled"; 18 const SHUTDOWN_TOPIC = "profile-before-change"; 19 20 const lazy = {}; 21 22 ChromeUtils.defineESModuleGetters(lazy, { 23 ObliviousHTTP: "resource://gre/modules/ObliviousHTTP.sys.mjs", 24 }); 25 26 XPCOMUtils.defineLazyPreferenceGetter( 27 lazy, 28 "CURRENT_CONTEXT_ID", 29 CONTEXT_ID_PREF, 30 "" 31 ); 32 33 XPCOMUtils.defineLazyPreferenceGetter( 34 lazy, 35 "UNIFIED_ADS_ENDPOINT", 36 "browser.newtabpage.activity-stream.unifiedAds.endpoint", 37 "" 38 ); 39 40 XPCOMUtils.defineLazyPreferenceGetter( 41 lazy, 42 "OHTTP_RELAY_URL", 43 "browser.newtabpage.activity-stream.discoverystream.ohttp.relayURL", 44 "" 45 ); 46 47 XPCOMUtils.defineLazyPreferenceGetter( 48 lazy, 49 "OHTTP_CONFIG_URL", 50 "browser.newtabpage.activity-stream.discoverystream.ohttp.configURL", 51 "" 52 ); 53 54 class JsContextIdCallback extends ContextIdCallback { 55 constructor(dispatchEvent) { 56 super(); 57 this.dispatchEvent = dispatchEvent; 58 } 59 60 persist(newContextId, creationTimestamp) { 61 Services.prefs.setCharPref(CONTEXT_ID_PREF, newContextId); 62 Services.prefs.setIntPref(CONTEXT_ID_TIMESTAMP_PREF, creationTimestamp); 63 this.dispatchEvent(new CustomEvent("ContextId:Persisted")); 64 } 65 66 rotated(oldContextId) { 67 GleanPings.contextIdDeletionRequest.setEnabled(true); 68 69 Glean.contextualServices.contextId.set(oldContextId); 70 GleanPings.contextIdDeletionRequest.submit(); 71 ContextId.sendMARSDeletionRequest(oldContextId); 72 } 73 } 74 75 /** 76 * A class that manages and (optionally) rotates the context ID, which is a 77 * a unique identifier used by Contextual Services. 78 */ 79 export class _ContextId extends EventTarget { 80 #comp = null; 81 #rotationDays = 0; 82 #rustComponentEnabled = false; 83 #observer = null; 84 85 constructor() { 86 super(); 87 88 this.#rustComponentEnabled = Services.prefs.getBoolPref( 89 CONTEXT_ID_RUST_COMPONENT_ENABLED_PREF, 90 false 91 ); 92 93 if (this.#rustComponentEnabled) { 94 // We intentionally read this once at construction, and cache the result. 95 // This is because enabling or disabling rotation may affect external 96 // uses of _ContextId which (for example) send the context_id UUID to 97 // Shredder in the context-id-deletion-request ping (which we only want to 98 // do when rotation is disabled), and that sort of thing tends to get set 99 // once during startup. 100 this.#rotationDays = Services.prefs.getIntPref( 101 CONTEXT_ID_ROTATION_DAYS_PREF, 102 0 103 ); 104 // Note that we're setting `running_in_test_automation` to true 105 // all of the time. This is because we don't want the ContextID 106 // component to be responsible for sending the DELETE request to MARS, 107 // since it doesn't know to do it over OHTTP. We'll send the DELETE 108 // request ourselves over OHTTP at rotation time. 109 this.#comp = ContextIdComponent.init( 110 lazy.CURRENT_CONTEXT_ID, 111 Services.prefs.getIntPref(CONTEXT_ID_TIMESTAMP_PREF, 0), 112 true /* running_in_test_automation */, 113 new JsContextIdCallback(this.dispatchEvent.bind(this)) 114 ); 115 this.#observer = (subject, topic, data) => { 116 this.observe(subject, topic, data); 117 }; 118 119 Services.obs.addObserver(this.#observer, SHUTDOWN_TOPIC); 120 } 121 } 122 123 /** 124 * nsIObserver implementation. 125 * 126 * @param {nsISupports} _subject 127 * @param {string} topic 128 * @param {string} _data 129 */ 130 observe(_subject, topic, _data) { 131 if (topic == SHUTDOWN_TOPIC) { 132 // Unregister ourselves as the callback to avoid leak assertions. 133 this.#comp.unsetCallback(); 134 Services.obs.removeObserver(this.#observer, SHUTDOWN_TOPIC); 135 } 136 } 137 138 /** 139 * Returns the stored context ID for this profile, if one exists. If one 140 * doesn't exist, one is generated and then returned. In the event that 141 * context ID rotation is in effect, then this may return a different 142 * context ID if we've determined it's time to rotate. This means that 143 * consumers _should not_ cache the context ID, but always request it. 144 * 145 * @returns {Promise<string>} 146 * The context ID for this profile. 147 */ 148 async request() { 149 if (this.#rustComponentEnabled) { 150 return this.#comp.request(this.#rotationDays); 151 } 152 153 // Fallback to the legacy behaviour of just returning the pref, or 154 // generating / returning a UUID if the pref is false-y. 155 if (!lazy.CURRENT_CONTEXT_ID) { 156 let _contextId = Services.uuid.generateUUID().toString(); 157 Services.prefs.setStringPref(CONTEXT_ID_PREF, _contextId); 158 } 159 160 return Promise.resolve(lazy.CURRENT_CONTEXT_ID); 161 } 162 163 /** 164 * Forces the rotation of the context ID. This should be used by callers when 165 * some surface that uses the context ID is disabled. This is only supported 166 * with the Rust backend, and is a no-op when the Rust backend is not enabled. 167 * 168 * @returns {Promise<undefined>} 169 */ 170 async forceRotation() { 171 if (this.#rustComponentEnabled) { 172 return this.#comp.forceRotation(); 173 } 174 return Promise.resolve(); 175 } 176 177 /** 178 * Returns true if context ID rotation is enabled. 179 * 180 * @returns {boolean} 181 */ 182 get rotationEnabled() { 183 return this.#rustComponentEnabled && this.#rotationDays > 0; 184 } 185 186 /** 187 * A compatibility shim that only works if rotationEnabled is false which 188 * returns the context ID synchronously. This will throw if rotationEnabled 189 * is true - so callers should ensure that rotationEnabled is false before 190 * using this. This will eventually be removed. 191 */ 192 requestSynchronously() { 193 if (this.rotationEnabled) { 194 throw new Error( 195 "Cannot request context ID synchronously when rotation is enabled." 196 ); 197 } 198 199 return lazy.CURRENT_CONTEXT_ID; 200 } 201 202 /** 203 * For now, the context_id application-services component does not know how 204 * to send the MARS deletion request over OHTTP, so we do it ourselves 205 * manually, using the New Tab unified ads preferences. This will eventually 206 * go away once the context_id component knows how to use OHTTP itself. 207 * 208 * @param {string} oldContextId 209 * The old context_id being rotated away from. 210 * @returns {Promise<undefined>} 211 */ 212 async sendMARSDeletionRequest(oldContextId) { 213 if ( 214 !lazy.UNIFIED_ADS_ENDPOINT || 215 !lazy.OHTTP_RELAY_URL || 216 !lazy.OHTTP_CONFIG_URL 217 ) { 218 return; 219 } 220 221 const endpoint = `${lazy.UNIFIED_ADS_ENDPOINT}v1/delete_user`; 222 const body = { 223 context_id: oldContextId, 224 }; 225 const headers = new Headers(); 226 headers.append("content-type", "application/json"); 227 228 const config = await lazy.ObliviousHTTP.getOHTTPConfig( 229 lazy.OHTTP_CONFIG_URL 230 ); 231 232 if (!config) { 233 console.error( 234 new Error( 235 `OHTTP was configured for ${endpoint} but we couldn't fetch a valid config` 236 ) 237 ); 238 } 239 240 // We don't actually use this AbortController, but ObliviousHTTP wants it. 241 const controller = new AbortController(); 242 const { signal } = controller; 243 244 const response = await lazy.ObliviousHTTP.ohttpRequest( 245 lazy.OHTTP_RELAY_URL, 246 config, 247 endpoint, 248 { 249 method: "DELETE", 250 headers, 251 body: JSON.stringify(body), 252 credentials: "omit", 253 signal, 254 } 255 ); 256 257 if (!response.ok) { 258 console.error(new Error(`Unexpected status (${response.status})`)); 259 } 260 } 261 } 262 263 export const ContextId = new _ContextId();