tor-browser

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

SponsorProtection.sys.mjs (7264B)


      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 
      7 const lazy = {};
      8 
      9 XPCOMUtils.defineLazyServiceGetter(
     10  lazy,
     11  "ProxyService",
     12  "@mozilla.org/network/protocol-proxy-service;1",
     13  Ci.nsIProtocolProxyService
     14 );
     15 
     16 ChromeUtils.defineLazyGetter(lazy, "logConsole", function () {
     17  return console.createInstance({
     18    prefix: "SponsorProtection",
     19    maxLogLevel: Services.prefs.getBoolPref(
     20      "browser.newtabpage.sponsor-protection.debug",
     21      false
     22    )
     23      ? "Debug"
     24      : "Warn",
     25  });
     26 });
     27 
     28 XPCOMUtils.defineLazyPreferenceGetter(
     29  lazy,
     30  "SPONSOR_PROTECTION_ENABLED",
     31  "browser.newtabpage.sponsor-protection.enabled",
     32  false
     33 );
     34 
     35 const HTTP_STOP_REQUEST_TOPIC = "http-on-stop-request";
     36 const BYTES_PER_KB = 1024;
     37 
     38 /**
     39 * This class tracks a list of <browser> elements that have done a navigation
     40 * by way of a sponsored link from New Tab. The goal is to eventually apply
     41 * client protections on these <browser> elements such that network traffic is
     42 * forwarded through an HTTP CONNECT or MASQUE relay, thus hiding the client's
     43 * real IP address from the advertiser site. A less unique UA string will also
     44 * be assigned to <browser> elements under protection.
     45 */
     46 export class _SponsorProtection {
     47  #protectedBrowsers = new WeakSet();
     48  #observerAndFilterAdded = false;
     49  #debugEnabled = false;
     50 
     51  constructor() {
     52    this.#debugEnabled = Services.prefs.getBoolPref(
     53      "browser.newtabpage.sponsor-protection.debug",
     54      false
     55    );
     56  }
     57 
     58  /**
     59   * True if the SponsorProtection mechanism is enabled.
     60   */
     61  get enabled() {
     62    return lazy.SPONSOR_PROTECTION_ENABLED;
     63  }
     64 
     65  /**
     66   * True if the debug mode for SponsorProtection was enabled upon construction.
     67   * Debug mode adds a decoration to the tab hover preview to help identify
     68   * protected browsers, and also emits logging to the console.
     69   */
     70  get debugEnabled() {
     71    return this.#debugEnabled;
     72  }
     73 
     74  /**
     75   * Registers a <browser> so that sponsor protection is applied for
     76   * subsequent network connections from that <browser>.
     77   *
     78   * @param {Browser} browser
     79   *   The <browser> to have sponsor protected applied.
     80   */
     81  addProtectedBrowser(browser) {
     82    if (!this.enabled) {
     83      return;
     84    }
     85 
     86    if (!this.#observerAndFilterAdded) {
     87      this.#addObserverAndChannelFilter();
     88    }
     89 
     90    this.#protectedBrowsers.add(browser.permanentKey);
     91    lazy.logConsole.debug("Registering browser for sponsor protection");
     92 
     93    // TODO: This is where the clamped UA string will be applied to this
     94    // browser.
     95  }
     96 
     97  /**
     98   * Unregisters a <browser> so that sponsor protection is no longer
     99   * applied for subsequent network connections from that <browser>.
    100   * This is a no-op if the <browser> was not actually being protected.
    101   *
    102   * @param {Browser} browser
    103   *   The <browser> to have sponsor protected removed.
    104   */
    105  removeProtectedBrowser(browser) {
    106    this.#protectedBrowsers.delete(browser.permanentKey);
    107    lazy.logConsole.debug("Unregistering browser for sponsor protection");
    108 
    109    // TODO: This is where we remove the clamped UA string from this browser.
    110  }
    111 
    112  /**
    113   * Returns true if the <browser> is having sponsor protection applied.
    114   *
    115   * @param {Browser} browser
    116   * @returns {boolean}
    117   */
    118  isProtectedBrowser(browser) {
    119    return this.#protectedBrowsers.has(browser.permanentKey);
    120  }
    121 
    122  /**
    123   * Sets up the HTTP_STOP_REQUEST_TOPIC observer and proxy channel filter when
    124   * the number of protected browsers in the WeakSet goes from 0 to 1.
    125   */
    126  #addObserverAndChannelFilter() {
    127    Services.obs.addObserver(this, HTTP_STOP_REQUEST_TOPIC);
    128 
    129    lazy.ProxyService.registerChannelFilter(this, 0);
    130    this.#observerAndFilterAdded = true;
    131    lazy.logConsole.debug("Added observer and channel filter.");
    132  }
    133 
    134  /**
    135   * Removes the HTTP_STOP_REQUEST_TOPIC observer and proxy channel filter when
    136   * the number of protected browsers in the WeakSet goes to 0.
    137   */
    138  #removeObserverAndChannelFilter() {
    139    Services.obs.removeObserver(this, HTTP_STOP_REQUEST_TOPIC);
    140 
    141    lazy.ProxyService.unregisterChannelFilter(this);
    142    this.#observerAndFilterAdded = false;
    143    lazy.logConsole.debug("Removed observer and channel filter.");
    144  }
    145 
    146  /* nsIObserver */
    147 
    148  /**
    149   * Observes the HTTP_STOP_REQUEST_TOPIC observer notification and, if the
    150   * associated channel comes from a protected browser, records the request
    151   * and response sizes to telemetry.
    152   *
    153   * This observer is also used to determine if there are remaining protected
    154   * browsers - and if not, to unregister the observer and channel filter.
    155   *
    156   * @param {nsIChannel} subject
    157   *   For HTTP_STOP_REQUEST_TOPIC, this should be an nsIChannel.
    158   * @param {string} topic
    159   * @param {string} _data
    160   */
    161  observe(subject, topic, _data) {
    162    if (topic != HTTP_STOP_REQUEST_TOPIC) {
    163      return;
    164    }
    165 
    166    // If all the <browser> elements have gone away from our WeakSet, at this
    167    // point we can go ahead and get rid of our observer and filter.
    168    if (
    169      !ChromeUtils.nondeterministicGetWeakSetKeys(this.#protectedBrowsers)
    170        .length
    171    ) {
    172      this.#removeObserverAndChannelFilter();
    173      return;
    174    }
    175 
    176    if (!(subject instanceof Ci.nsIHttpChannel)) {
    177      return;
    178    }
    179 
    180    let channel = subject;
    181    const { browsingContext } = channel.loadInfo;
    182    let browser = browsingContext?.top.embedderElement;
    183    if (!browser || !this.#protectedBrowsers.has(browser.permanentKey)) {
    184      return;
    185    }
    186 
    187    // requestSize includes the request headers and payload
    188    const requestSize = channel.requestSize;
    189 
    190    // transferSize includes the response headers and payload
    191    const responseSize = channel.transferSize;
    192 
    193    Glean.newtab.sponsNavTrafficSent.accumulate(
    194      Math.round(requestSize / BYTES_PER_KB)
    195    );
    196    Glean.newtab.sponsNavTrafficRecvd.accumulate(
    197      Math.round(responseSize / BYTES_PER_KB)
    198    );
    199 
    200    lazy.logConsole.debug(
    201      `Channel for ${browser.currentURI.spec} (${channel.URI.spec}) - sent: ${requestSize} recv'd: ${responseSize}`
    202    );
    203  }
    204 
    205  /* nsIProtocolProxyChannelFilter */
    206 
    207  /**
    208   * Checks a created nsIChannel to see if it qualifies for proxying. If it
    209   * does, proxy configuration appropriate for this client is applied.
    210   *
    211   * @param {nsIChannel} channel
    212   * @param {nsIProxyInfo} proxyInfo
    213   * @param {nsIProxyProtocolFilterResult} callback
    214   */
    215  applyFilter(channel, proxyInfo, callback) {
    216    const { browsingContext } = channel.loadInfo;
    217    let browser = browsingContext?.top.embedderElement;
    218    if (!browser || !this.#protectedBrowsers.has(browser)) {
    219      callback.onProxyFilterResult(proxyInfo);
    220      return;
    221    }
    222 
    223    // This is where proxy information, if we have any, will be applied for
    224    // the connection. For now however, we're not proxying anything, so just
    225    // fallthrough to the default behaviour.
    226    callback.onProxyFilterResult(proxyInfo);
    227  }
    228 
    229  QueryInterface = ChromeUtils.generateQI([
    230    Ci.nsIObserver,
    231    Ci.nsIProtocolProxyChannelFilter,
    232  ]);
    233 }
    234 
    235 export const SponsorProtection = new _SponsorProtection();