tor-browser

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

IPProtection.sys.mjs (10377B)


      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 { ERRORS } from "chrome://browser/content/ipprotection/ipprotection-constants.mjs";
      7 
      8 const lazy = {};
      9 
     10 ChromeUtils.defineESModuleGetters(lazy, {
     11  ASRouter: "resource:///modules/asrouter/ASRouter.sys.mjs",
     12  CustomizableUI:
     13    "moz-src:///browser/components/customizableui/CustomizableUI.sys.mjs",
     14  IPProtectionPanel:
     15    "moz-src:///browser/components/ipprotection/IPProtectionPanel.sys.mjs",
     16  IPProtectionService:
     17    "moz-src:///browser/components/ipprotection/IPProtectionService.sys.mjs",
     18  IPProtectionStates:
     19    "moz-src:///browser/components/ipprotection/IPProtectionService.sys.mjs",
     20  IPPProxyManager:
     21    "moz-src:///browser/components/ipprotection/IPPProxyManager.sys.mjs",
     22  IPPProxyStates:
     23    "moz-src:///browser/components/ipprotection/IPPProxyManager.sys.mjs",
     24  requestIdleCallback: "resource://gre/modules/Timer.sys.mjs",
     25  cancelIdleCallback: "resource://gre/modules/Timer.sys.mjs",
     26 });
     27 
     28 const FXA_WIDGET_ID = "fxa-toolbar-menu-button";
     29 const EXT_WIDGET_ID = "unified-extensions-button";
     30 
     31 /**
     32 * IPProtectionWidget is the class for the singleton IPProtection.
     33 *
     34 * It is a minimal manager for creating and removing a CustomizableUI widget
     35 * for IP protection features.
     36 *
     37 * It maintains the state of the panels and updates them when the
     38 * panel is shown or hidden.
     39 */
     40 class IPProtectionWidget {
     41  static WIDGET_ID = "ipprotection-button";
     42  static PANEL_ID = "PanelUI-ipprotection";
     43 
     44  static ENABLED_PREF = "browser.ipProtection.enabled";
     45  static VARIANT_PREF = "browser.ipProtection.variant";
     46  static ADDED_PREF = "browser.ipProtection.added";
     47 
     48  #inited = false;
     49  created = false;
     50  #panels = new WeakMap();
     51 
     52  constructor() {
     53    this.sendReadyTrigger = this.#sendReadyTrigger.bind(this);
     54    this.handleEvent = this.#handleEvent.bind(this);
     55  }
     56 
     57  /**
     58   * Creates the widget.
     59   */
     60  init() {
     61    if (this.#inited) {
     62      return;
     63    }
     64    this.#inited = true;
     65 
     66    if (!this.created) {
     67      this.#createWidget();
     68    }
     69 
     70    lazy.CustomizableUI.addListener(this);
     71  }
     72 
     73  /**
     74   * Destroys the widget and prevents any updates.
     75   */
     76  uninit() {
     77    if (!this.#inited) {
     78      return;
     79    }
     80    this.#destroyWidget();
     81    this.#uninitPanels();
     82 
     83    lazy.CustomizableUI.removeListener(this);
     84 
     85    this.#inited = false;
     86  }
     87 
     88  /**
     89   * Returns the initialization status
     90   */
     91  get isInitialized() {
     92    return this.#inited;
     93  }
     94 
     95  /**
     96   * Updates the toolbar icon to reflect the VPN connection status
     97   *
     98   * @param {XULElement} toolbaritem - toolbaritem to update
     99   * @param {object} status - VPN connection status
    100   */
    101  updateIconStatus(toolbaritem, status = { isActive: false, isError: false }) {
    102    let isActive = status.isActive;
    103    let isError = status.isError;
    104    let l10nId = isError ? "ipprotection-button-error" : "ipprotection-button";
    105 
    106    if (isError) {
    107      toolbaritem.classList.remove("ipprotection-on");
    108      toolbaritem.classList.add("ipprotection-error");
    109    } else if (isActive) {
    110      toolbaritem.classList.remove("ipprotection-error");
    111      toolbaritem.classList.add("ipprotection-on");
    112    } else {
    113      toolbaritem.classList.remove("ipprotection-error");
    114      toolbaritem.classList.remove("ipprotection-on");
    115    }
    116 
    117    toolbaritem.setAttribute("data-l10n-id", l10nId);
    118  }
    119 
    120  /**
    121   * Creates the CustomizableUI widget.
    122   */
    123  #createWidget() {
    124    const onViewShowing = this.#onViewShowing.bind(this);
    125    const onViewHiding = this.#onViewHiding.bind(this);
    126    const onBeforeCreated = this.#onBeforeCreated.bind(this);
    127    const onCreated = this.#onCreated.bind(this);
    128    const onDestroyed = this.#onDestroyed.bind(this);
    129    const item = {
    130      id: IPProtectionWidget.WIDGET_ID,
    131      l10nId: "ipprotection-button",
    132      type: "view",
    133      viewId: IPProtectionWidget.PANEL_ID,
    134      onViewShowing,
    135      onViewHiding,
    136      onBeforeCreated,
    137      onCreated,
    138      onDestroyed,
    139    };
    140    lazy.CustomizableUI.createWidget(item);
    141 
    142    this.#placeWidget();
    143 
    144    this.created = true;
    145  }
    146 
    147  /**
    148   * Places the widget in the nav bar, next to the FxA widget.
    149   */
    150  #placeWidget() {
    151    let wasAddedToToolbar = Services.prefs.getBoolPref(
    152      IPProtectionWidget.ADDED_PREF,
    153      false
    154    );
    155    let alreadyPlaced = lazy.CustomizableUI.getPlacementOfWidget(
    156      IPProtectionWidget.WIDGET_ID,
    157      false,
    158      true
    159    );
    160    if (wasAddedToToolbar || alreadyPlaced) {
    161      return;
    162    }
    163 
    164    let prevWidget =
    165      lazy.CustomizableUI.getPlacementOfWidget(FXA_WIDGET_ID) ||
    166      lazy.CustomizableUI.getPlacementOfWidget(EXT_WIDGET_ID);
    167    let pos = prevWidget ? prevWidget.position - 1 : null;
    168 
    169    lazy.CustomizableUI.addWidgetToArea(
    170      IPProtectionWidget.WIDGET_ID,
    171      lazy.CustomizableUI.AREA_NAVBAR,
    172      pos
    173    );
    174    Services.prefs.setBoolPref(IPProtectionWidget.ADDED_PREF, true);
    175  }
    176 
    177  /**
    178   * Destroys the widget if it has been created.
    179   *
    180   * This will not remove the pref listeners, so the widget
    181   * can be recreated later.
    182   */
    183  #destroyWidget() {
    184    if (!this.created) {
    185      return;
    186    }
    187    this.#destroyPanels();
    188    lazy.CustomizableUI.destroyWidget(IPProtectionWidget.WIDGET_ID);
    189    this.created = false;
    190    if (this.readyTriggerIdleCallback) {
    191      lazy.cancelIdleCallback(this.readyTriggerIdleCallback);
    192    }
    193  }
    194 
    195  /**
    196   * Get the IPProtectionPanel for q given window.
    197   *
    198   * @param {Window} window - which window to get the panel for.
    199   * @returns {IPProtectionPanel}
    200   */
    201  getPanel(window) {
    202    if (!this.created || !window?.PanelUI) {
    203      return null;
    204    }
    205 
    206    return this.#panels.get(window);
    207  }
    208 
    209  /**
    210   * Remove all panels content, but maintains state for if the widget is
    211   * re-enabled in the same window.
    212   *
    213   * Panels will only be removed from the WeakMap if their window is closed.
    214   */
    215  #destroyPanels() {
    216    let panels = ChromeUtils.nondeterministicGetWeakMapKeys(this.#panels);
    217    for (let panel of panels) {
    218      this.#panels.get(panel).destroy();
    219    }
    220  }
    221 
    222  /**
    223   * Uninit all panels and clear the WeakMap.
    224   */
    225  #uninitPanels() {
    226    let panels = ChromeUtils.nondeterministicGetWeakMapKeys(this.#panels);
    227    for (let panel of panels) {
    228      this.#panels.get(panel).uninit();
    229    }
    230    this.#panels = new WeakMap();
    231  }
    232 
    233  /**
    234   * Updates the state of the panel before it is shown.
    235   *
    236   * @param {Event} event - the panel shown.
    237   */
    238  #onViewShowing(event) {
    239    let { ownerGlobal } = event.target;
    240    if (this.#panels.has(ownerGlobal)) {
    241      let panel = this.#panels.get(ownerGlobal);
    242      panel.showing(event.target);
    243    }
    244  }
    245 
    246  /**
    247   * Updates the panels visibility.
    248   *
    249   * @param {Event} event - the panel hidden.
    250   */
    251  #onViewHiding(event) {
    252    let { ownerGlobal } = event.target;
    253    if (this.#panels.has(ownerGlobal)) {
    254      let panel = this.#panels.get(ownerGlobal);
    255      panel.hiding();
    256    }
    257  }
    258 
    259  /**
    260   * Creates a new IPProtectionPanel for a browser window.
    261   *
    262   * @param {Document} doc - the document containing the panel.
    263   */
    264  #onBeforeCreated(doc) {
    265    let { ownerGlobal } = doc;
    266    if (ownerGlobal && !this.#panels.has(ownerGlobal)) {
    267      let panel = new lazy.IPProtectionPanel(ownerGlobal, this.variant);
    268      this.#panels.set(ownerGlobal, panel);
    269    }
    270  }
    271 
    272  /**
    273   * Gets the toolbaritem after the widget has been created and
    274   * adds content to the panel.
    275   *
    276   * @param {XULElement} toolbaritem - the widget toolbaritem.
    277   */
    278  #onCreated(toolbaritem) {
    279    let isActive = lazy.IPPProxyManager.state === lazy.IPPProxyStates.ACTIVE;
    280    let isError =
    281      lazy.IPPProxyManager.state === lazy.IPPProxyStates.ERROR &&
    282      lazy.IPPProxyManager.errors.includes(ERRORS.GENERIC);
    283    this.updateIconStatus(toolbaritem, {
    284      isActive,
    285      isError,
    286    });
    287 
    288    this.readyTriggerIdleCallback = lazy.requestIdleCallback(
    289      this.sendReadyTrigger
    290    );
    291 
    292    lazy.IPProtectionService.addEventListener(
    293      "IPProtectionService:StateChanged",
    294      this.handleEvent
    295    );
    296    lazy.IPPProxyManager.addEventListener(
    297      "IPPProxyManager:StateChanged",
    298      this.handleEvent
    299    );
    300  }
    301 
    302  #onDestroyed() {
    303    lazy.IPPProxyManager.removeEventListener(
    304      "IPPProxyManager:StateChanged",
    305      this.handleEvent
    306    );
    307    lazy.IPProtectionService.removeEventListener(
    308      "IPProtectionService:StateChanged",
    309      this.handleEvent
    310    );
    311  }
    312 
    313  async onWidgetRemoved(widgetId) {
    314    if (widgetId != IPProtectionWidget.WIDGET_ID) {
    315      return;
    316    }
    317 
    318    // Shut down VPN connection when widget is removed,
    319    // but wait to check if it has been moved.
    320    await Promise.resolve();
    321    let moved = !!lazy.CustomizableUI.getPlacementOfWidget(widgetId);
    322    if (!moved) {
    323      lazy.IPPProxyManager.stop();
    324    }
    325  }
    326 
    327  async #sendReadyTrigger() {
    328    await lazy.ASRouter.waitForInitialized;
    329    const win = Services.wm.getMostRecentBrowserWindow();
    330    const browser = win?.gBrowser?.selectedBrowser;
    331    await lazy.ASRouter.sendTriggerMessage({
    332      browser,
    333      id: "ipProtectionReady",
    334    });
    335  }
    336 
    337  #handleEvent(event) {
    338    if (
    339      event.type == "IPProtectionService:StateChanged" ||
    340      event.type == "IPPProxyManager:StateChanged"
    341    ) {
    342      if (
    343        lazy.IPProtectionService.state === lazy.IPProtectionStates.OPTED_OUT
    344      ) {
    345        lazy.CustomizableUI.removeWidgetFromArea(IPProtectionWidget.WIDGET_ID);
    346        return;
    347      }
    348 
    349      let status = {
    350        isActive: lazy.IPPProxyManager.state === lazy.IPPProxyStates.ACTIVE,
    351        isError:
    352          lazy.IPPProxyManager.state === lazy.IPPProxyStates.ERROR &&
    353          lazy.IPPProxyManager.errors.includes(ERRORS.GENERIC),
    354      };
    355 
    356      let widget = lazy.CustomizableUI.getWidget(IPProtectionWidget.WIDGET_ID);
    357      let windows = ChromeUtils.nondeterministicGetWeakMapKeys(this.#panels);
    358      for (let win of windows) {
    359        let toolbaritem = widget.forWindow(win).node;
    360        this.updateIconStatus(toolbaritem, status);
    361      }
    362    }
    363  }
    364 }
    365 
    366 const IPProtection = new IPProtectionWidget();
    367 
    368 XPCOMUtils.defineLazyPreferenceGetter(
    369  IPProtection,
    370  "variant",
    371  IPProtectionWidget.VARIANT_PREF,
    372  ""
    373 );
    374 
    375 export { IPProtection, IPProtectionWidget };