tor-browser

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

IPPChannelFilter.sys.mjs (12477B)


      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 = XPCOMUtils.declareLazy({
      8  IPPExceptionsManager:
      9    "moz-src:///browser/components/ipprotection/IPPExceptionsManager.sys.mjs",
     10  ProxyService: {
     11    service: "@mozilla.org/network/protocol-proxy-service;1",
     12    iid: Ci.nsIProtocolProxyService,
     13  },
     14 });
     15 const { TRANSPARENT_PROXY_RESOLVES_HOST } = Ci.nsIProxyInfo;
     16 const failOverTimeout = 10; // seconds
     17 
     18 const MODE_PREF = "browser.ipProtection.mode";
     19 
     20 export const IPPMode = Object.freeze({
     21  MODE_FULL: 0,
     22  MODE_PB: 1,
     23  MODE_TRACKER: 2,
     24 });
     25 
     26 const TRACKING_FLAGS =
     27  Ci.nsIClassifiedChannel.CLASSIFIED_TRACKING |
     28  Ci.nsIClassifiedChannel.CLASSIFIED_TRACKING_AD |
     29  Ci.nsIClassifiedChannel.CLASSIFIED_TRACKING_ANALYTICS |
     30  Ci.nsIClassifiedChannel.CLASSIFIED_TRACKING_SOCIAL |
     31  Ci.nsIClassifiedChannel.CLASSIFIED_TRACKING_CONTENT;
     32 
     33 const DEFAULT_EXCLUDED_URL_PREFS = [
     34  "browser.ipProtection.guardian.endpoint",
     35  "identity.fxaccounts.remote.profile.uri",
     36  "identity.fxaccounts.auth.uri",
     37  "identity.fxaccounts.remote.profile.uri",
     38 ];
     39 
     40 const ESSENTIAL_URL_PREFS = [
     41  "toolkit.telemetry.server",
     42  "network.trr.uri",
     43  "network.trr.default_provider_uri",
     44 ];
     45 
     46 /**
     47 * IPPChannelFilter is a class that implements the nsIProtocolProxyChannelFilter
     48 * when active it will funnel all requests to its provided proxy.
     49 *
     50 * the connection can be stopped
     51 *
     52 */
     53 export class IPPChannelFilter {
     54  /**
     55   * Creates a new IPPChannelFilter that can connect to a proxy server. After
     56   * created, the proxy can be immediately activated. It will suspend all the
     57   * received nsIChannel until the object is fully initialized.
     58   *
     59   * @param {Array<string>} [excludedPages] - list of page URLs whose *origin* should bypass the proxy
     60   */
     61  static create(excludedPages = []) {
     62    return new IPPChannelFilter(excludedPages);
     63  }
     64 
     65  /**
     66   * Sets the IPP Mode.
     67   *
     68   * @param {IPPMode} [mode] - the new mode
     69   */
     70  static setMode(mode) {
     71    Services.prefs.setIntPref(MODE_PREF, mode);
     72  }
     73 
     74  /**
     75   * Takes a protocol definition and constructs the appropriate nsIProxyInfo
     76   *
     77   * @typedef {import("./IPProtectionServerlist.sys.mjs").MasqueProtocol} MasqueProtocol
     78   * @typedef {import("./IPProtectionServerlist.sys.mjs").ConnectProtocol } ConnectProtocol
     79   *
     80   * @param {string} authToken - a bearer token for the proxy server.
     81   * @param {string} isolationKey - the isolation key for the proxy connection.
     82   * @param {MasqueProtocol|ConnectProtocol} protocol - the protocol definition.
     83   * @param {nsIProxyInfo} fallBackInfo - optional fallback proxy info.
     84   * @returns {nsIProxyInfo}
     85   */
     86  static constructProxyInfo(
     87    authToken,
     88    isolationKey,
     89    protocol,
     90    fallBackInfo = null
     91  ) {
     92    switch (protocol.name) {
     93      case "masque":
     94        return lazy.ProxyService.newMASQUEProxyInfo(
     95          protocol.host,
     96          protocol.port,
     97          protocol.templateString,
     98          authToken,
     99          isolationKey,
    100          TRANSPARENT_PROXY_RESOLVES_HOST,
    101          failOverTimeout,
    102          fallBackInfo
    103        );
    104      case "connect":
    105        return lazy.ProxyService.newProxyInfo(
    106          protocol.scheme,
    107          protocol.host,
    108          protocol.port,
    109          authToken,
    110          isolationKey,
    111          TRANSPARENT_PROXY_RESOLVES_HOST,
    112          failOverTimeout,
    113          fallBackInfo
    114        );
    115      default:
    116        throw new Error(
    117          "Cannot construct ProxyInfo for Unknown server-protocol: " +
    118            protocol.name
    119        );
    120    }
    121  }
    122  /**
    123   * Takes a server definition and constructs the appropriate nsIProxyInfo
    124   * If the server supports multiple Protocols, a fallback chain will be created.
    125   * The first protocol in the list will be the primary one, with the others as fallbacks.
    126   *
    127   * @typedef {import("./IPProtectionServerlist.sys.mjs").Server} Server
    128   * @param {string} authToken - a bearer token for the proxy server.
    129   * @param {Server} server - the server to connect to.
    130   * @returns {nsIProxyInfo}
    131   */
    132  static serverToProxyInfo(authToken, server) {
    133    const isolationKey = IPPChannelFilter.makeIsolationKey();
    134    return server.protocols.reduceRight((fallBackInfo, protocol) => {
    135      return IPPChannelFilter.constructProxyInfo(
    136        authToken,
    137        isolationKey,
    138        protocol,
    139        fallBackInfo
    140      );
    141    }, null);
    142  }
    143 
    144  /**
    145   * Initialize a IPPChannelFilter object. After this step, the filter, if
    146   * active, will process the new and the pending channels.
    147   *
    148   * @typedef {import("./IPProtectionServerlist.sys.mjs").Server} Server
    149   * @param {string} authToken - a bearer token for the proxy server.
    150   * @param {Server} server - the server to connect to.
    151   */
    152  initialize(authToken = "", server) {
    153    if (this.proxyInfo) {
    154      throw new Error("Double initialization?!?");
    155    }
    156    const proxyInfo = IPPChannelFilter.serverToProxyInfo(authToken, server);
    157    Object.freeze(proxyInfo);
    158    this.proxyInfo = proxyInfo;
    159 
    160    this.#processPendingChannels();
    161  }
    162 
    163  /**
    164   * @param {Array<string>} [excludedPages]
    165   */
    166  constructor(excludedPages = []) {
    167    // Normalize and store excluded origins (scheme://host[:port])
    168    this.#excludedOrigins = new Set();
    169    excludedPages.forEach(url => {
    170      this.addPageExclusion(url);
    171    });
    172 
    173    DEFAULT_EXCLUDED_URL_PREFS.forEach(pref => {
    174      const prefValue = Services.prefs.getStringPref(pref, "");
    175      if (prefValue) {
    176        this.addPageExclusion(prefValue);
    177      }
    178    });
    179 
    180    // Get origins essential to starting the proxy and exclude
    181    // them prior to connecting
    182    this.#essentialOrigins = new Set();
    183    ESSENTIAL_URL_PREFS.forEach(pref => {
    184      const prefValue = Services.prefs.getStringPref(pref, "");
    185      if (prefValue) {
    186        this.addEssentialExclusion(prefValue);
    187      }
    188    });
    189 
    190    XPCOMUtils.defineLazyPreferenceGetter(
    191      this,
    192      "mode",
    193      MODE_PREF,
    194      IPPMode.MODE_FULL
    195    );
    196  }
    197 
    198  /**
    199   * This method (which is required by the nsIProtocolProxyService interface)
    200   * is called to apply proxy filter rules for the given URI and proxy object
    201   * (or list of proxy objects).
    202   *
    203   * @param {nsIChannel} channel The channel for which these proxy settings apply.
    204   * @param {nsIProxyInfo} _defaultProxyInfo The proxy (or list of proxies) that
    205   *     would be used by default for the given URI. This may be null.
    206   * @param {nsIProxyProtocolFilterResult} proxyFilter
    207   */
    208  applyFilter(channel, _defaultProxyInfo, proxyFilter) {
    209    // If this channel should be excluded (origin match), do nothing
    210    if (!this.#matchMode(channel) || this.shouldExclude(channel)) {
    211      // Calling this with "null" will enforce a non-proxy connection
    212      proxyFilter.onProxyFilterResult(null);
    213      return;
    214    }
    215 
    216    if (!this.proxyInfo) {
    217      // We are not initialized yet!
    218      this.#pendingChannels.push({ channel, proxyFilter });
    219      return;
    220    }
    221 
    222    proxyFilter.onProxyFilterResult(this.proxyInfo);
    223 
    224    // Notify observers that the channel is being proxied
    225    this.#observers.forEach(observer => {
    226      observer(channel);
    227    });
    228  }
    229 
    230  #matchMode(channel) {
    231    switch (this.mode) {
    232      case IPPMode.MODE_PB:
    233        return !!channel.loadInfo.originAttributes.privateBrowsingId;
    234 
    235      case IPPMode.MODE_TRACKER:
    236        return (
    237          TRACKING_FLAGS &
    238          channel.loadInfo.triggeringThirdPartyClassificationFlags
    239        );
    240 
    241      case IPPMode.MODE_FULL:
    242      default:
    243        return true;
    244    }
    245  }
    246 
    247  /**
    248   * Decide whether a channel should bypass the proxy based on origin.
    249   *
    250   * @param {nsIChannel} channel
    251   * @returns {boolean}
    252   */
    253  shouldExclude(channel) {
    254    try {
    255      const uri = channel.URI; // nsIURI
    256      if (!uri) {
    257        return true;
    258      }
    259 
    260      if (!["http", "https"].includes(uri.scheme)) {
    261        return true;
    262      }
    263 
    264      const origin = uri.prePath; // scheme://host[:port]
    265 
    266      if (!this.proxyInfo && this.#essentialOrigins.has(origin)) {
    267        return true;
    268      }
    269 
    270      let loadingPrincipal = channel.loadInfo?.loadingPrincipal;
    271      let hasExclusion =
    272        loadingPrincipal &&
    273        lazy.IPPExceptionsManager.hasExclusion(loadingPrincipal);
    274 
    275      if (hasExclusion) {
    276        return true;
    277      }
    278 
    279      return this.#excludedOrigins.has(origin);
    280    } catch (_) {
    281      return true;
    282    }
    283  }
    284 
    285  /**
    286   * Adds a page URL to the exclusion list.
    287   *
    288   * @param {string} url - The URL to exclude.
    289   * @param {Set<string>} [list] - The exclusion list to add the URL to.
    290   */
    291  addPageExclusion(url, list = this.#excludedOrigins) {
    292    try {
    293      const uri = Services.io.newURI(url);
    294      // prePath is scheme://host[:port]
    295      list.add(uri.prePath);
    296    } catch (_) {
    297      // ignore bad entries
    298    }
    299  }
    300 
    301  /**
    302   * Adds a URL to the essential exclusion list.
    303   *
    304   * @param {string} url - The URL to exclude.
    305   */
    306  addEssentialExclusion(url) {
    307    this.addPageExclusion(url, this.#essentialOrigins);
    308  }
    309 
    310  /**
    311   * Starts the Channel Filter, feeding all following Requests through the proxy.
    312   */
    313  start() {
    314    lazy.ProxyService.registerChannelFilter(
    315      this /* nsIProtocolProxyChannelFilter aFilter */,
    316      0 /* unsigned long aPosition */
    317    );
    318    this.#active = true;
    319  }
    320 
    321  /**
    322   * Stops the Channel Filter, stopping all following Requests from being proxied.
    323   */
    324  stop() {
    325    if (!this.#active) {
    326      return;
    327    }
    328 
    329    lazy.ProxyService.unregisterChannelFilter(this);
    330 
    331    this.#abortPendingChannels();
    332 
    333    this.#active = false;
    334    this.#abort.abort();
    335  }
    336 
    337  /**
    338   * Returns the isolation key of the proxy connection.
    339   * All ProxyInfo objects related to this Connection will have the same isolation key.
    340   */
    341  get isolationKey() {
    342    return this.proxyInfo.connectionIsolationKey;
    343  }
    344 
    345  get hasPendingChannels() {
    346    return !!this.#pendingChannels.length;
    347  }
    348 
    349  /**
    350   * Replaces the authentication token used by the proxy connection.
    351   * --> Important <--: This Changes the isolationKey of the Connection!
    352   *
    353   * @param {string} newToken - The new authentication token.
    354   */
    355  replaceAuthToken(newToken) {
    356    const newInfo = lazy.ProxyService.newProxyInfo(
    357      this.proxyInfo.type,
    358      this.proxyInfo.host,
    359      this.proxyInfo.port,
    360      newToken,
    361      IPPChannelFilter.makeIsolationKey(),
    362      TRANSPARENT_PROXY_RESOLVES_HOST,
    363      failOverTimeout,
    364      null // Failover proxy info
    365    );
    366    Object.freeze(newInfo);
    367    this.proxyInfo = newInfo;
    368  }
    369 
    370  /**
    371   * Returns an async generator that yields channels this Connection is proxying.
    372   *
    373   * This allows to introspect channels that are proxied, i.e
    374   * to measure usage, or catch proxy errors.
    375   *
    376   * @returns {AsyncGenerator<nsIChannel>} An async generator that yields proxied channels.
    377   * @yields {object}
    378   *   Proxied channels.
    379   */
    380  async *proxiedChannels() {
    381    const stop = Promise.withResolvers();
    382    this.#abort.signal.addEventListener(
    383      "abort",
    384      () => {
    385        stop.reject();
    386      },
    387      { once: true }
    388    );
    389    while (this.#active) {
    390      const { promise, resolve } = Promise.withResolvers();
    391      this.#observers.push(resolve);
    392      try {
    393        const result = await Promise.race([stop.promise, promise]);
    394        this.#observers = this.#observers.filter(
    395          observer => observer !== resolve
    396        );
    397        yield result;
    398      } catch (error) {
    399        // Stop iteration if the filter is stopped or aborted
    400        return;
    401      }
    402    }
    403  }
    404 
    405  /**
    406   * Returns true if this filter is active.
    407   */
    408  get active() {
    409    return this.#active;
    410  }
    411 
    412  #processPendingChannels() {
    413    if (this.#pendingChannels.length) {
    414      this.#pendingChannels.forEach(data =>
    415        this.applyFilter(data.channel, null, data.proxyFilter)
    416      );
    417      this.#pendingChannels = [];
    418    }
    419  }
    420 
    421  #abortPendingChannels() {
    422    if (this.#pendingChannels.length) {
    423      this.#pendingChannels.forEach(data =>
    424        data.channel.cancel(Cr.NS_BINDING_ABORTED)
    425      );
    426      this.#pendingChannels = [];
    427    }
    428  }
    429 
    430  #abort = new AbortController();
    431  #observers = [];
    432  #active = false;
    433  #excludedOrigins = new Set();
    434  #essentialOrigins = new Set();
    435  #pendingChannels = [];
    436 
    437  static makeIsolationKey() {
    438    return Math.random().toString(36).slice(2, 18).padEnd(16, "0");
    439  }
    440 }