tor-browser

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

IPProtectionServerlist.sys.mjs (8547B)


      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 /**
      6 * This file contains functions that work on top of the RemoteSettings
      7 * Bucket for the IP Protection server list.
      8 */
      9 
     10 const lazy = {};
     11 
     12 ChromeUtils.defineESModuleGetters(lazy, {
     13  IPPStartupCache:
     14    "moz-src:///browser/components/ipprotection/IPPStartupCache.sys.mjs",
     15  IPProtectionService:
     16    "moz-src:///browser/components/ipprotection/IPProtectionService.sys.mjs",
     17  IPProtectionStates:
     18    "moz-src:///browser/components/ipprotection/IPProtectionService.sys.mjs",
     19  RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
     20 });
     21 
     22 /**
     23 *
     24 */
     25 export class IProtocol {
     26  name = "";
     27  static construct(data) {
     28    switch (data.name) {
     29      case "masque":
     30        return new MasqueProtocol(data);
     31      case "connect":
     32        return new ConnectProtocol(data);
     33      default:
     34        throw new Error("Unknown protocol: " + data.name);
     35    }
     36  }
     37 }
     38 
     39 /**
     40 *
     41 */
     42 export class MasqueProtocol extends IProtocol {
     43  name = "masque";
     44  host = "";
     45  port = 0;
     46  templateString = "";
     47  constructor(data) {
     48    super();
     49    this.host = data.host || "";
     50    this.port = data.port || 0;
     51    this.templateString = data.templateString || "";
     52  }
     53 }
     54 
     55 /**
     56 *
     57 */
     58 export class ConnectProtocol extends IProtocol {
     59  name = "connect";
     60  host = "";
     61  port = 0;
     62  scheme = "https";
     63  constructor(data) {
     64    super();
     65    this.host = data.host || "";
     66    this.port = data.port || 0;
     67    this.scheme = data.scheme || "https";
     68  }
     69 }
     70 
     71 /**
     72 * Class representing a server.
     73 */
     74 export class Server {
     75  /**
     76   * Port of the server
     77   *
     78   * @type {number}
     79   */
     80  port = 443;
     81  /**
     82   * Hostname of the server
     83   *
     84   * @type {string}
     85   */
     86  hostname = "";
     87  /**
     88   * If true the server is quarantined
     89   * and should not be used
     90   *
     91   * @type {boolean}
     92   */
     93  quarantined = false;
     94 
     95  /**
     96   * List of supported protocols
     97   *
     98   * @type {Array<MasqueProtocol|ConnectProtocol>}
     99   */
    100  protocols = [];
    101 
    102  constructor(data) {
    103    this.port = data.port || 443;
    104    this.hostname = data.hostname || "";
    105    this.quarantined = !!data.quarantined;
    106    this.protocols = (data.protocols || []).map(p => IProtocol.construct(p));
    107 
    108    // Default to connect if no protocols are specified
    109    if (this.protocols.length === 0) {
    110      this.protocols = [
    111        new ConnectProtocol({
    112          name: "connect",
    113          host: this.hostname,
    114          port: this.port,
    115        }),
    116      ];
    117    }
    118  }
    119 }
    120 
    121 /**
    122 * Class representing a city.
    123 */
    124 class City {
    125  /**
    126   * Fallback name for the city if not available
    127   *
    128   * @type {string}
    129   */
    130  name = "";
    131  /**
    132   * A stable identifier for the city
    133   * (Usually a Wikidata ID)
    134   *
    135   * @type {string}
    136   */
    137  code = "";
    138  /**
    139   * List of servers in this city
    140   *
    141   * @type {Server[]}
    142   */
    143  servers = [];
    144 
    145  constructor(data) {
    146    this.name = data.name || "";
    147    this.code = data.code || "";
    148    this.servers = (data.servers || []).map(s => new Server(s));
    149  }
    150 }
    151 
    152 /**
    153 * Class representing a country.
    154 */
    155 class Country {
    156  /**
    157   * Fallback name for the country if not available
    158   *
    159   * @type {string}
    160   */
    161  name;
    162  /**
    163   * A stable identifier for the country
    164   * Usually a ISO 3166-1 alpha-2 code
    165   *
    166   * @type {string}
    167   */
    168  code;
    169 
    170  /**
    171   * List of cities in this country
    172   *
    173   * @type {City[]}
    174   */
    175  cities;
    176 
    177  constructor(data) {
    178    this.name = data.name || "";
    179    this.code = data.code || "";
    180    this.cities = (data.cities || []).map(c => new City(c));
    181  }
    182 }
    183 
    184 /**
    185 * Base Class for the Serverlist
    186 */
    187 export class IPProtectionServerlistBase {
    188  __list = null;
    189 
    190  init() {}
    191 
    192  async initOnStartupCompleted() {}
    193 
    194  uninit() {}
    195 
    196  /**
    197   * Tries to refresh the list from the underlining source.
    198   *
    199   * @param {*} _forceUpdate - if true, forces a refresh even if the list is already populated.
    200   */
    201  maybeFetchList(_forceUpdate = false) {
    202    throw new Error("Not implemented");
    203  }
    204 
    205  /**
    206   * Selects a default location - for alpha this is only the US.
    207   *
    208   * @returns {{Country, City}} - The best country/city to use.
    209   */
    210  getDefaultLocation() {
    211    /** @type {Country} */
    212    const usa = this.__list.find(country => country.code === "US");
    213    if (!usa) {
    214      return null;
    215    }
    216 
    217    const city = usa.cities.find(c => c.servers.length);
    218    return {
    219      city,
    220      country: usa,
    221    };
    222  }
    223 
    224  /**
    225   * Given a city, it selects an available server.
    226   *
    227   * @param {City?} city
    228   * @returns {Server|null}
    229   */
    230  selectServer(city) {
    231    if (!city) {
    232      return null;
    233    }
    234 
    235    const servers = city.servers.filter(server => !server.quarantined);
    236    if (servers.length === 1) {
    237      return servers[0];
    238    }
    239 
    240    if (servers.length > 1) {
    241      return servers[Math.floor(Math.random() * servers.length)];
    242    }
    243 
    244    return null;
    245  }
    246 
    247  get hasList() {
    248    return this.__list.length !== 0;
    249  }
    250 
    251  static dataToList(list) {
    252    if (!Array.isArray(list)) {
    253      return [];
    254    }
    255    return list.map(c => new Country(c));
    256  }
    257 }
    258 
    259 /**
    260 * Class representing the IP Protection Serverlist
    261 * fetched from Remote Settings.
    262 */
    263 export class RemoteSettingsServerlist extends IPProtectionServerlistBase {
    264  #bucket = null;
    265  #runningPromise = null;
    266 
    267  constructor() {
    268    super();
    269    this.handleEvent = this.#handleEvent.bind(this);
    270    this.__list = IPProtectionServerlistBase.dataToList(
    271      lazy.IPPStartupCache.locationList
    272    );
    273  }
    274  init() {
    275    lazy.IPProtectionService.addEventListener(
    276      "IPProtectionService:StateChanged",
    277      this.handleEvent
    278    );
    279  }
    280 
    281  async initOnStartupCompleted() {
    282    this.bucket.on("sync", async () => {
    283      await this.maybeFetchList(true);
    284    });
    285  }
    286 
    287  uninit() {
    288    lazy.IPProtectionService.removeEventListener(
    289      "IPProtectionService:StateChanged",
    290      this.handleEvent
    291    );
    292  }
    293 
    294  #handleEvent(_event) {
    295    if (lazy.IPProtectionService.state === lazy.IPProtectionStates.READY) {
    296      this.maybeFetchList();
    297    }
    298  }
    299 
    300  maybeFetchList(forceUpdate = false) {
    301    if (this.__list.length !== 0 && !forceUpdate) {
    302      return Promise.resolve();
    303    }
    304 
    305    if (this.#runningPromise) {
    306      return this.#runningPromise;
    307    }
    308 
    309    const fetchList = async () => {
    310      this.__list = IPProtectionServerlistBase.dataToList(
    311        await this.bucket.get()
    312      );
    313 
    314      lazy.IPPStartupCache.storeLocationList(this.__list);
    315    };
    316 
    317    this.#runningPromise = fetchList().finally(
    318      () => (this.#runningPromise = null)
    319    );
    320 
    321    return this.#runningPromise;
    322  }
    323 
    324  get bucket() {
    325    if (!this.#bucket) {
    326      this.#bucket = lazy.RemoteSettings("vpn-serverlist");
    327    }
    328    return this.#bucket;
    329  }
    330 }
    331 /**
    332 * Class representing the IP Protection Serverlist
    333 * from about:config preferences.
    334 */
    335 export class PrefServerList extends IPProtectionServerlistBase {
    336  #observer = null;
    337 
    338  constructor() {
    339    super();
    340    this.#observer = this.onPrefChange.bind(this);
    341    this.maybeFetchList();
    342  }
    343 
    344  onPrefChange() {
    345    this.maybeFetchList();
    346  }
    347 
    348  async initOnStartupCompleted() {
    349    Services.prefs.addObserver(
    350      IPProtectionServerlist.PREF_NAME,
    351      this.#observer
    352    );
    353  }
    354 
    355  uninit() {
    356    Services.prefs.removeObserver(
    357      IPProtectionServerlist.PREF_NAME,
    358      this.#observer
    359    );
    360  }
    361  maybeFetchList(_forceUpdate = false) {
    362    this.__list = IPProtectionServerlistBase.dataToList(
    363      PrefServerList.prefValue
    364    );
    365    return Promise.resolve();
    366  }
    367 
    368  static get PREF_NAME() {
    369    return "browser.ipProtection.override.serverlist";
    370  }
    371  /**
    372   * Returns true if the preference has a valid value.
    373   */
    374  static get hasPrefValue() {
    375    return (
    376      Services.prefs.getPrefType(this.PREF_NAME) ===
    377        Services.prefs.PREF_STRING &&
    378      !!Services.prefs.getStringPref(this.PREF_NAME).length
    379    );
    380  }
    381  static get prefValue() {
    382    try {
    383      const value = Services.prefs.getStringPref(this.PREF_NAME);
    384      return JSON.parse(value);
    385    } catch (e) {
    386      console.error(`IPProtection: Error parsing serverlist pref value: ${e}`);
    387      return null;
    388    }
    389  }
    390 }
    391 /**
    392 *
    393 * @returns {IPProtectionServerlistBase} - The appropriate serverlist implementation.
    394 */
    395 export function IPProtectionServerlistFactory() {
    396  return PrefServerList.hasPrefValue
    397    ? new PrefServerList()
    398    : new RemoteSettingsServerlist();
    399 }
    400 
    401 // Only check once which implementation to use.
    402 const IPProtectionServerlist = IPProtectionServerlistFactory();
    403 
    404 export { IPProtectionServerlist };