tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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();