tor-browser

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

ThirdPartyCookieBlockingExceptionListService.sys.mjs (7095B)


      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 const lazy = {};
      6 
      7 ChromeUtils.defineESModuleGetters(lazy, {
      8  RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
      9 });
     10 
     11 // Name of the RemoteSettings collection containing the records.
     12 const COLLECTION_NAME = "third-party-cookie-blocking-exempt-urls";
     13 const PREF_NAME = "network.cookie.cookieBehavior.optInPartitioning.skip_list";
     14 
     15 export class ThirdPartyCookieBlockingExceptionListService {
     16  classId = Components.ID("{1ee0cc18-c968-4105-a895-bdea08e187eb}");
     17  QueryInterface = ChromeUtils.generateQI([
     18    "nsIThirdPartyCookieBlockingExceptionListService",
     19  ]);
     20 
     21  #rs = null;
     22  #onSyncCallback = null;
     23 
     24  // Sets to keep track of the exceptions in the pref. It uses the string in the
     25  // format "firstPartySite,thirdPartySite" as the key.
     26  #prefValueSet = null;
     27  // Set to keep track of exceptions from RemoteSettings. It uses the same
     28  // keying as above.
     29  #rsValueSet = null;
     30 
     31  constructor() {
     32    this.#rs = lazy.RemoteSettings(COLLECTION_NAME);
     33  }
     34 
     35  async init() {
     36    await this.importAllExceptions();
     37 
     38    Services.prefs.addObserver(PREF_NAME, this);
     39 
     40    if (!this.#onSyncCallback) {
     41      this.#onSyncCallback = this.onSync.bind(this);
     42      this.#rs.on("sync", this.#onSyncCallback);
     43    }
     44 
     45    // Import for initial pref state.
     46    this.onPrefChange();
     47  }
     48 
     49  shutdown() {
     50    Services.prefs.removeObserver(PREF_NAME, this);
     51 
     52    if (this.#onSyncCallback) {
     53      this.#rs.off("sync", this.#onSyncCallback);
     54      this.#onSyncCallback = null;
     55    }
     56  }
     57 
     58  #handleExceptionChange(created = [], deleted = []) {
     59    if (created.length) {
     60      Services.cookies.addThirdPartyCookieBlockingExceptions(created);
     61    }
     62    if (deleted.length) {
     63      Services.cookies.removeThirdPartyCookieBlockingExceptions(deleted);
     64    }
     65  }
     66 
     67  onSync({ data: { created = [], updated = [], deleted = [] } }) {
     68    // Convert the RemoteSettings records to exception entries.
     69    created = created.map(ex =>
     70      ThirdPartyCookieExceptionEntry.fromRemoteSettingsRecord(ex)
     71    );
     72    deleted = deleted.map(ex =>
     73      ThirdPartyCookieExceptionEntry.fromRemoteSettingsRecord(ex)
     74    );
     75 
     76    updated.forEach(ex => {
     77      let newEntry = ThirdPartyCookieExceptionEntry.fromRemoteSettingsRecord(
     78        ex.new
     79      );
     80      let oldEntry = ThirdPartyCookieExceptionEntry.fromRemoteSettingsRecord(
     81        ex.old
     82      );
     83 
     84      // We only care about changes in the sites.
     85      if (newEntry.equals(oldEntry)) {
     86        return;
     87      }
     88      created.push(newEntry);
     89      deleted.push(oldEntry);
     90    });
     91 
     92    this.#rsValueSet ??= new Set();
     93 
     94    // Remove items in sitesToRemove
     95    for (const site of deleted) {
     96      this.#rsValueSet.delete(site.serialize());
     97    }
     98 
     99    // Add items from sitesToAdd
    100    for (const site of created) {
    101      this.#rsValueSet.add(site.serialize());
    102    }
    103 
    104    this.#handleExceptionChange(created, deleted);
    105  }
    106 
    107  onPrefChange() {
    108    let newExceptions = Services.prefs.getStringPref(PREF_NAME, "").split(";");
    109 
    110    // Convert the exception strings to exception entries.
    111    newExceptions = newExceptions
    112      .map(ex => ThirdPartyCookieExceptionEntry.fromString(ex))
    113      .filter(Boolean);
    114 
    115    // If this is the first time we're initializing from pref, we can directly
    116    // call handleExceptionChange to create the exceptions.
    117    if (!this.#prefValueSet) {
    118      this.#handleExceptionChange({
    119        data: { created: newExceptions },
    120        prefUpdate: true,
    121      });
    122      // Serialize the exception entries to the string format and store in the
    123      // pref set.
    124      this.#prefValueSet = new Set(newExceptions.map(ex => ex.serialize()));
    125      return;
    126    }
    127 
    128    // Otherwise, we need to check for changes in the pref.
    129 
    130    // Find added items
    131    let created = [...newExceptions].filter(
    132      ex => !this.#prefValueSet.has(ex.serialize())
    133    );
    134 
    135    // Convert the new exceptions to the string format to check against the pref
    136    // set.
    137    let newExceptionStringSet = new Set(
    138      newExceptions.map(ex => ex.serialize())
    139    );
    140 
    141    // Find removed items
    142    let deleted = Array.from(this.#prefValueSet)
    143      .filter(item => !newExceptionStringSet.has(item))
    144      .map(ex => ThirdPartyCookieExceptionEntry.fromString(ex));
    145 
    146    // We shouldn't remove the exceptions in the remote settings list.
    147    if (this.#rsValueSet) {
    148      deleted = deleted.filter(ex => !this.#rsValueSet.has(ex.serialize()));
    149    }
    150 
    151    this.#prefValueSet = newExceptionStringSet;
    152 
    153    // Calling handleExceptionChange to handle the changes.
    154    this.#handleExceptionChange(created, deleted);
    155  }
    156 
    157  observe(subject, topic, data) {
    158    if (topic != "nsPref:changed" || data != PREF_NAME) {
    159      throw new Error(`Unexpected event ${topic} with ${data}`);
    160    }
    161 
    162    this.onPrefChange();
    163  }
    164 
    165  async importAllExceptions() {
    166    try {
    167      let exceptions = await this.#rs.get();
    168      if (!exceptions.length) {
    169        return;
    170      }
    171      this.onSync({ data: { created: exceptions } });
    172    } catch (error) {
    173      console.error(
    174        "Error while importing 3pcb exceptions from RemoteSettings",
    175        error
    176      );
    177    }
    178  }
    179 }
    180 
    181 export class ThirdPartyCookieExceptionEntry {
    182  classId = Components.ID("{8200e12c-416c-42eb-8af5-db9745d2e527}");
    183  QueryInterface = ChromeUtils.generateQI([
    184    "nsIThirdPartyCookieExceptionEntry",
    185  ]);
    186 
    187  constructor(fpSite, tpSite) {
    188    this.firstPartySite = fpSite;
    189    this.thirdPartySite = tpSite;
    190  }
    191 
    192  // Serialize the exception entry into a string. This is used for keying the
    193  // exception in the pref and RemoteSettings set.
    194  serialize() {
    195    return `${this.firstPartySite},${this.thirdPartySite}`;
    196  }
    197 
    198  equals(other) {
    199    return (
    200      this.firstPartySite === other.firstPartySite &&
    201      this.thirdPartySite === other.thirdPartySite
    202    );
    203  }
    204 
    205  static fromString(exStr) {
    206    if (!exStr) {
    207      return null;
    208    }
    209 
    210    let [fpSite, tpSite] = exStr.split(",");
    211    try {
    212      fpSite = this.#sanitizeSite(fpSite, true);
    213      tpSite = this.#sanitizeSite(tpSite);
    214 
    215      return new ThirdPartyCookieExceptionEntry(fpSite, tpSite);
    216    } catch (e) {
    217      console.error(
    218        `Error while constructing 3pcd exception entry from string`,
    219        exStr
    220      );
    221      return null;
    222    }
    223  }
    224 
    225  static fromRemoteSettingsRecord(record) {
    226    try {
    227      let fpSite = this.#sanitizeSite(record.fpSite, true);
    228      let tpSite = this.#sanitizeSite(record.tpSite);
    229 
    230      return new ThirdPartyCookieExceptionEntry(fpSite, tpSite);
    231    } catch (e) {
    232      console.error(
    233        `Error while constructing 3pcd exception entry from RemoteSettings record`,
    234        record
    235      );
    236      return null;
    237    }
    238  }
    239 
    240  // A helper function to sanitize the site using the eTLD service.
    241  static #sanitizeSite(site, acceptWildcard = false) {
    242    if (acceptWildcard && site === "*") {
    243      return "*";
    244    }
    245 
    246    let uri = Services.io.newURI(site);
    247    return Services.eTLD.getSite(uri);
    248  }
    249 }