Commit 48a9ad58 authored by Richard Pospesel's avatar Richard Pospesel Committed by Pier Angelo Vendrame
Browse files

TB 31286: Implementation of bridge, proxy, and firewall settings in about:preferences#connection

This patch adds a new about:preferences#connection page which allows
modifying bridge, proxy, and firewall settings from within Tor Browser.
All of the functionality present in tor-launcher's Network
Configuration panel is present:

 - Setting built-in bridges
 - Requesting bridges from BridgeDB via moat
 - Using user-provided bridges
 - Configuring SOCKS4, SOCKS5, and HTTP/HTTPS proxies
 - Setting firewall ports
 - Viewing and Copying Tor's logs
 - The Networking Settings in General preferences has been removed

Bug 40774: Update about:preferences page to match new UI designs
parent 84147100
Loading
Loading
Loading
Loading
+2 −0
Changes for .prettierignore: 2 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -1814,5 +1814,7 @@ xpcom/idl-parser/xpidl/fixtures/xpctest.d.json

browser/app/profile/001-base-profile.js
browser/app/profile/000-tor-browser.js
# We do not use the preprocessor here, so we can lint it.
!browser/components/torpreferences/content/connectionPane.inc.xhtml
toolkit/content/pt_config.json
toolkit/content/moat_countries_dev_build.json
+1 −0
Changes for browser/components/moz.build: 1 added line, 0 removed lines.
Original line number Diff line number Diff line
@@ -81,6 +81,7 @@ DIRS += [
    "urlbar",
    "webrtc",
    "torcircuit",
    "torpreferences",
]

DIRS += ["build"]
+28 −0
Changes for browser/components/preferences/preferences.js: 28 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -115,6 +115,7 @@ ChromeUtils.defineESModuleGetters(this, {
    "resource:///modules/SelectionChangedMenulist.sys.mjs",
  ShortcutUtils: "resource://gre/modules/ShortcutUtils.sys.mjs",
  SiteDataManager: "resource:///modules/SiteDataManager.sys.mjs",
  TorConnect: "moz-src:///toolkit/modules/TorConnect.sys.mjs",
  TransientPrefs: "resource:///modules/TransientPrefs.sys.mjs",
  UIState: "resource://services-sync/UIState.sys.mjs",
  UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs",
@@ -266,6 +267,16 @@ const CONFIG_PANES = Object.freeze({
    visible: () =>
      Services.prefs.getBoolPref("browser.settings-redesign.enabled", false),
  },
  connection: {
    l10nId: "tor-connection-settings-pane",
    iconSrc: "chrome://browser/content/torconnect/tor-connect.svg",
    groupIds: ["connectionStatus", "torBridges", "torAdvanced"],
    module: "chrome://browser/content/torpreferences/config/connection.mjs",
    visible: () => {
      return TorConnect.enabled;
    },
    replaces: "connection",
  },
  connectionSecurity: {
    parent: "privacy",
    l10nId: "preferences-connection-header",
@@ -569,6 +580,23 @@ function init_all() {
    register_module("paneSync", gSyncPane);
  }
  register_module("paneSearchResults", gSearchResultsPane);
  if (!redesignEnabled) {
    if (TorConnect.enabled) {
      register_module("paneConnection", {
        init() {
          ChromeUtils.importESModule(
            "chrome://browser/content/torpreferences/config/connection.mjs",
            { global: "current" }
          );
          initSettingGroup("connectionStatus");
          initSettingGroup("torBridges");
          initSettingGroup("torAdvanced");
        },
      });
    } else {
      document.getElementById("category-connection").remove();
    }
  }
  for (let [id, config] of Object.entries(CONFIG_PANES)) {
    // Skip over configs we do not want, including all its children.
    // See tor-browser#44711.
+12 −0
Changes for browser/components/preferences/preferences.xhtml: 12 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -43,6 +43,8 @@
    href="chrome://browser/content/preferences/letterboxing.css"
  />

  <link rel="stylesheet" href="chrome://browser/content/torpreferences/torPreferences.css" />

  <link rel="localization" href="branding/brand.ftl"/>
  <link rel="localization" href="browser/browser.ftl"/>
    <!-- Used by fontbuilder.js -->
@@ -106,6 +108,10 @@
  <script type="module" src="chrome://browser/content/ipprotection/bandwidth-usage.mjs"></script>
  <script type="module" src="chrome://browser/content/securitylevel/widgets/security-level-description.mjs"></script>
  <script type="module" src="chrome://browser/content/securitylevel/widgets/security-level-display.mjs"></script>
  <script type="module" src="chrome://browser/content/torpreferences/widgets/tor-bridges-display.mjs"></script>
  <script type="module" src="chrome://browser/content/torpreferences/widgets/tor-connection-assist-banner.mjs"></script>
  <script type="module" src="chrome://browser/content/torpreferences/widgets/tor-connection-status.mjs"></script>
  <script src="chrome://browser/content/torpreferences/bridgemoji/BridgeEmoji.js"/>
</head>

<html:body xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
@@ -147,6 +153,11 @@
        iconsrc="chrome://browser/skin/preferences/category-privacy-security.svg"
        data-l10n-id="pane-privacy-title3">
      </html:moz-page-nav-button>
      <html:moz-page-nav-button id="category-connection"
        view="paneConnection"
        iconsrc="chrome://browser/content/torconnect/tor-connect.svg"
        data-l10n-id="tor-connection-settings-nav-button">
      </html:moz-page-nav-button>
      <html:moz-page-nav-button id="category-passwords-autofill"
        view="panePasswordsAutofill"
        iconsrc="chrome://browser/skin/login.svg"
@@ -261,6 +272,7 @@
#include privacy.inc.xhtml
#include sync.inc.xhtml
#include moreFromMozilla.inc.xhtml
#include ../torpreferences/content/connectionPane.inc.xhtml
        </vbox>
      </vbox>
    </vbox>
+614 −0
Changes for browser/components/torpreferences/config/connection.mjs: 614 added lines, 0 removed lines.
Original line number Diff line number Diff line
import { SettingGroupManager } from "chrome://browser/content/preferences/config/SettingGroupManager.mjs";
import { Preferences } from "chrome://global/content/preferences/Preferences.mjs";

const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
  InternetStatus: "moz-src:///toolkit/modules/TorConnect.sys.mjs",
  moveFocusToBridgeHeading:
    "chrome://browser/content/torpreferences/config/helpers.mjs",
  openBridgeDialog:
    "chrome://browser/content/torpreferences/config/helpers.mjs",
  openUserProvideBridgeDialog:
    "chrome://browser/content/torpreferences/config/helpers.mjs",
  TorBridgeSource: "moz-src:///toolkit/modules/TorSettings.sys.mjs",
  TorConnect: "moz-src:///toolkit/modules/TorConnect.sys.mjs",
  TorConnectStage: "moz-src:///toolkit/modules/TorConnect.sys.mjs",
  TorConnectTopics: "moz-src:///toolkit/modules/TorConnect.sys.mjs",
  TorProviderBuilder:
    "moz-src:///toolkit/components/tor-launcher/TorProviderBuilder.sys.mjs",
  TorProviderState:
    "moz-src:///toolkit/components/tor-launcher/TorProviderBuilder.sys.mjs",
  TorProviderTopics:
    "moz-src:///toolkit/components/tor-launcher/TorProviderBuilder.sys.mjs",
  TorSettings: "moz-src:///toolkit/modules/TorSettings.sys.mjs",
  TorSettingsTopics: "moz-src:///toolkit/modules/TorSettings.sys.mjs",
});

// TODO: Change to GetLoxBridges if Lox enabled, and the account is set up.
const TELEGRAM_USER_NAME = "GetBridgesBot";
const TELEGRAM_HREF = `https://t.me/${TELEGRAM_USER_NAME}`;

const TOR_BRIDGES_URL_NAME = "bridges.torproject.org";
const TOR_BRIDGES_HREF = "https://bridges.torproject.org";

const TOR_BRIDGES_EMAIL = "bridges@torproject.org";

SettingGroupManager.registerGroups({
  connectionStatus: {
    l10nId: "tor-connection-internet-status-group",
    supportPage: "tor-manual:getting-started__about-tor-browser",
    headingLevel: 2,
    items: [
      {
        id: "connectionStatusGroup",
        control: "moz-box-group",
        items: [
          {
            id: "internetStatus",
            control: "tor-connection-status",
            controlAttrs: { "status-type": "internet" },
          },
          {
            id: "torStatus",
            control: "tor-connection-status",
            controlAttrs: { "status-type": "tor" },
          },
        ],
      },
      {
        id: "connectAutomatically",
        l10nId: "tor-connection-quickstart-checkbox",
        control: "moz-toggle",
      },
    ],
  },
  torBridges: {
    l10nId: "tor-bridges-group",
    supportPage: "tor-manual:bridges",
    headingLevel: 2,
    controlAttrs: { "focusable-heading": true },
    items: [
      {
        id: "connectionAssistBanner",
        // NOTE: Instead of using a custom widget for this one banner, we could
        // use moz-message-bar and populate it's children. However, we want to
        // intercept the "click" event for the "Connection Assist" link within
        // the banner text. As of ESR 153, onUserClick would not allow us to
        // intercept the event because the `<a>` would need to be wrapped in a
        // `<setting-control>`. But Fluent would not allow wrapping the
        // `<setting-control>` element as part of a wider string (unlike
        // `<a data-l10n-name="link">`, which is allowed). tor-browser#43939.
        control: "tor-connection-assist-banner",
      },
      {
        id: "bridgesEnabled",
        l10nId: "tor-bridges-use-bridges",
        control: "moz-toggle",
      },
      {
        id: "torBridgesDisplay",
        control: "tor-bridges-display",
      },
      {
        id: "newBridgesGroup",
        control: "moz-fieldset",
        controlAttrs: {
          headinglevel: 3,
        },
        options: [
          {
            control: "moz-box-group",
            items: [
              {
                id: "builtinBridges",
                l10nId: "tor-bridges-choose-built-in-button",
                control: "moz-box-button",
              },
              {
                id: "userProvidedBridges",
                l10nId: "tor-bridges-enter-bridges-button",
                control: "moz-box-button",
              },
            ],
          },
        ],
      },
      {
        id: "findMoreBridgesGroup",
        l10nId: "tor-bridges-find-more-group",
        control: "moz-fieldset",
        controlAttrs: {
          headinglevel: 3,
        },
        options: [
          {
            control: "moz-box-item",
            options: [
              {
                id: "torBridgesRequestBanner",
                control: "article",
                options: [
                  {
                    control: "img",
                    controlAttrs: {
                      alt: "",
                      src: "chrome://browser/content/torpreferences/bridge-bot.svg",
                    },
                  },
                  {
                    control: "p",
                    l10nId: "tor-bridges-request-from-browser2",
                  },
                  {
                    // NOTE: We use the wrapping `div` to simply switch from the
                    // `options` context to the `items` context, with the latter
                    // wrapping the elements in a setting-control.
                    control: "div",
                    items: [
                      {
                        id: "requestBridges",
                        l10nId: "tor-bridges-request-button2",
                        control: "moz-button",
                      },
                    ],
                  },
                ],
              },
            ],
          },
          {
            control: "moz-box-group",
            options: [
              {
                l10nId: "tor-bridges-source-telegram-link",
                l10nArgs: { telegramUserName: TELEGRAM_USER_NAME },
                control: "moz-box-link",
                iconSrc:
                  "chrome://browser/content/torpreferences/telegram-logo.svg",
                controlAttrs: {
                  href: TELEGRAM_HREF,
                },
              },
              {
                l10nId: "tor-bridges-source-web-link",
                l10nArgs: { url: TOR_BRIDGES_URL_NAME },
                control: "moz-box-link",
                iconSrc: "chrome://browser/content/torconnect/network.svg",
                controlAttrs: {
                  href: TOR_BRIDGES_HREF,
                },
              },
              {
                l10nId: "tor-bridges-source-email-link",
                l10nArgs: { address: TOR_BRIDGES_EMAIL },
                control: "moz-box-item",
                iconSrc: "chrome://browser/content/torpreferences/mail.svg",
              },
            ],
          },
        ],
      },
    ],
  },
  torAdvanced: {
    l10nId: "tor-advanced-group",
    headingLevel: 2,
    items: [
      {
        id: "torAdvancedGroup",
        control: "moz-box-group",
        items: [
          {
            id: "torAdvancedConfigure",
            l10nId: "tor-advanced-configure-button",
            control: "moz-box-button",
          },
          {
            id: "torViewLog",
            subcategory: "viewlogs",
            l10nId: "tor-view-log-button2",
            control: "moz-box-button",
          },
        ],
      },
    ],
  },
});

Preferences.addSetting({
  id: "connectionStatusGroup",
});

Preferences.addSetting({
  id: "internetStatus",
  setup(emitChange) {
    Services.obs.addObserver(
      emitChange,
      lazy.TorConnectTopics.InternetStatusChange
    );
    return () => {
      Services.obs.removeObserver(
        emitChange,
        lazy.TorConnectTopics.InternetStatusChange
      );
    };
  },
  get() {
    switch (lazy.TorConnect.internetStatus) {
      case lazy.InternetStatus.Online:
        return "online";
      case lazy.InternetStatus.Offline:
        return "offline";
    }
    return "unknown";
  },
});

Preferences.addSetting({
  id: "torStatus",
  setup(emitChange) {
    Services.obs.addObserver(emitChange, lazy.TorConnectTopics.StageChange);
    return () => {
      Services.obs.removeObserver(
        emitChange,
        lazy.TorConnectTopics.StageChange
      );
    };
  },
  get() {
    if (lazy.TorConnect.stageName === lazy.TorConnectStage.Bootstrapped) {
      return "connected";
    }
    if (lazy.TorConnect.potentiallyBlocked) {
      return "potentially-blocked";
    }
    return "not-connected";
  },
});

Preferences.addSetting({
  id: "connectAutomatically",
  setup(emitChange) {
    Services.obs.addObserver(
      emitChange,
      lazy.TorConnectTopics.QuickstartChange
    );
    return () => {
      Services.obs.removeObserver(
        emitChange,
        lazy.TorConnectTopics.QuickstartChange
      );
    };
  },
  get() {
    return lazy.TorConnect.quickstart;
  },
  set(val) {
    lazy.TorConnect.quickstart = val;
  },
});

Preferences.addSetting({
  id: "connectionAssistBanner",
  deps: ["torStatus"],
  _wasVisible: false,
  visible({ torStatus }) {
    const visible = torStatus.value === "potentially-blocked";
    if (
      !visible &&
      this._wasVisible &&
      document
        .getElementById("connectionAssistBanner")
        ?.contains(document.activeElement)
    ) {
      // About to loose focus, move focus to the bridge heading.
      lazy.moveFocusToBridgeHeading(window, true);
    }
    this._wasVisible = visible;
    return visible;
  },
});

Preferences.addSetting({
  id: "torSettingsReady",
  _ready: false,
  setup(emitChange) {
    if (!lazy.TorSettings.enabled) {
      // Remain in `false`.
      return;
    }
    // Most likely, TorSettings will already be initialised.
    if (lazy.TorSettings.initialized) {
      this._ready = true;
      return;
    }
    // Else, wait for it to be initialised.
    lazy.TorSettings.initializedPromise.then(
      () => {
        this._ready = true;
        emitChange();
      },
      error => {
        // No change in state.
        console.error("TorSettings failed to initialize.", error);
      }
    );
  },
  get() {
    return this._ready;
  },
});

Preferences.addSetting({
  id: "torBridgesRaw",
  deps: ["torSettingsReady"],
  _value: null,
  setup(emitChange) {
    const observer = subject => {
      const { changes } = subject.wrappedJSObject;
      // NOTE: We do not include "bridges.lox_id" in the changes. Instead, any
      // widgets should wait for LoxTopics.UpdateActiveLoxId to ensure that the
      // Lox module has responded to the change in ID strictly *before* we do.
      // In particular, we want to make sure the invites and event data has been
      // cleared.
      if (
        changes.includes("bridges.source") ||
        changes.includes("bridges.bridge_strings") ||
        changes.includes("bridges.builtin_type")
      ) {
        // Reset.
        this._value = null;
        emitChange();
      }
    };
    Services.obs.addObserver(observer, lazy.TorSettingsTopics.SettingsChanged);
    return () => {
      Services.obs.removeObserver(
        observer,
        lazy.TorSettingsTopics.SettingsChanged
      );
    };
  },
  get(_pref, { torSettingsReady }) {
    if (this._value === null) {
      if (!torSettingsReady.value) {
        // TorSettings getter will throw.
        return null;
      }
      const source = lazy.TorSettings.bridges.source;
      // Cache a value.
      this._value = {
        haveBridges: source !== lazy.TorBridgeSource.Invalid,
        source,
        builtinType: lazy.TorSettings.bridges.builtin_type,
        bridgeStrings: lazy.TorSettings.bridges.bridge_strings,
      };
    }
    return this._value;
  },
});

Preferences.addSetting({
  id: "connectedBridgeId",
  _value: null,
  setup(emitChange) {
    const observer = async () => {
      // NOTE: It should be safe for this method to be called concurrently.
      let bridge = null;
      try {
        if (
          lazy.TorProviderBuilder.currentState() ===
          lazy.TorProviderState.Running
        ) {
          bridge = (await lazy.TorProviderBuilder.build()).currentBridge;
        }
        // Else, bridge is `null` whilst the provider is not running.
      } catch (e) {
        console.warn("Could not get current bridge", e);
      }
      const prevVal = this._value;
      this._value = bridge?.fingerprint ?? null;
      if (prevVal !== this._value) {
        emitChange();
      }
    };
    Services.obs.addObserver(observer, lazy.TorProviderTopics.BridgeChanged);
    // NOTE: BridgeChanged is only fired directly by the provider instances,
    // rather than by TorProviderBuilder. In particular, it will not fire when
    // the previous provider had a bridge and the new one does not, because
    // neither provider saw a change in their own bridge. But from the user's
    // point of view, the overall change would mean that the current bridge has
    // changed.
    // Moreover, we want to show no connected bridge whilst we are missing a
    // provider. Therefore, we also need to listen for a change in provider and
    // its state.
    // TODO: Maybe this logic should be moved to TorProviderBuilder itself if it
    // is ever needed by other parts of the UI.
    Services.obs.addObserver(
      observer,
      lazy.TorProviderTopics.ProviderStateChanged
    );
    // Get the initial value.
    observer();

    return () => {
      Services.obs.removeObserver(
        observer,
        lazy.TorProviderTopics.ProviderStateChanged
      );
      Services.obs.removeObserver(
        observer,
        lazy.TorProviderTopics.BridgeChanged
      );
    };
  },
  get() {
    return this._value;
  },
});

Preferences.addSetting({
  id: "bridgesEnabled",
  deps: ["torSettingsReady", "torBridgesRaw"],
  setup(emitChange) {
    const observer = subject => {
      const { changes } = subject.wrappedJSObject;
      if (changes.includes("bridges.enabled")) {
        emitChange();
      }
    };
    Services.obs.addObserver(observer, lazy.TorSettingsTopics.SettingsChanged);
    return () => {
      Services.obs.removeObserver(
        observer,
        lazy.TorSettingsTopics.SettingsChanged
      );
    };
  },
  get(_prefVal, { torSettingsReady }) {
    if (!torSettingsReady.value) {
      // TorSettings.bridges will throw before TorSettings has finished
      // initialisation.
      return false;
    }
    return lazy.TorSettings.bridges.enabled;
  },
  set(val) {
    lazy.TorSettings.changeSettings({
      bridges: { enabled: val },
    });
  },
  visible({ torSettingsReady }) {
    return torSettingsReady.value;
  },
  disabled({ torBridgesRaw }) {
    return !torBridgesRaw.value?.haveBridges;
  },
});

Preferences.addSetting({
  id: "torBridgesDisplay",
  deps: ["torSettingsReady", "torBridgesRaw", "connectedBridgeId"],
  getControlConfig(config, { torBridgesRaw, connectedBridgeId }) {
    config.controlAttrs = {
      ...config.controlAttrs,
      // Set the `bridges` and `connnectedBridgeId` object *properties* (rather
      // than attributes) by using the `.` prefix.
      ".bridges": torBridgesRaw.value,
      ".connectedBridgeId": connectedBridgeId.value,
    };
    return config;
  },
  visible({ torSettingsReady }) {
    return torSettingsReady.value;
  },
});

Preferences.addSetting({
  id: "newBridgesGroup",
  deps: ["torSettingsReady", "torBridgesRaw"],
  getControlConfig(config, { torBridgesRaw }) {
    config.l10nId = torBridgesRaw.value?.haveBridges
      ? "tor-bridges-replace-bridges-group"
      : "tor-bridges-add-bridges-group";
    return config;
  },
  visible({ torSettingsReady }) {
    return torSettingsReady.value;
  },
});

Preferences.addSetting({
  id: "builtinBridges",
  onUserClick() {
    lazy.openBridgeDialog(
      window,
      "chrome://browser/content/torpreferences/builtinBridgeDialog.xhtml",
      null,
      result => {
        if (!result.type) {
          return null;
        }
        return lazy.TorSettings.changeSettings({
          bridges: {
            enabled: true,
            source: lazy.TorBridgeSource.BuiltIn,
            builtin_type: result.type,
          },
        });
      }
    );
  },
});

Preferences.addSetting({
  id: "userProvidedBridges",
  deps: ["torBridgesRaw"],
  onUserClick(_event, { torBridgesRaw }) {
    lazy.openUserProvideBridgeDialog(
      window,
      torBridgesRaw.value?.haveBridges ? "replace" : "add"
    );
  },
});

Preferences.addSetting({
  id: "findMoreBridgesGroup",
  deps: ["torSettingsReady"],
  visible({ torSettingsReady }) {
    return torSettingsReady.value;
  },
});

Preferences.addSetting({
  id: "requestBridges",
  onUserClick() {
    lazy.openBridgeDialog(
      window,
      "chrome://browser/content/torpreferences/requestBridgeDialog.xhtml",
      null,
      result => {
        if (!result.bridges?.length) {
          return null;
        }
        return lazy.TorSettings.changeSettings({
          bridges: {
            enabled: true,
            source: lazy.TorBridgeSource.BridgeDB,
            bridge_strings: result.bridges,
          },
        });
      }
    );
  },
});

Preferences.addSetting({
  id: "torAdvancedGroup",
});

Preferences.addSetting({
  id: "torAdvancedConfigure",
  deps: ["torSettingsReady"],
  visible({ torSettingsReady }) {
    // The dialog needs to be able to read TorSettings, so we keep this hidden
    // until it is ready.
    return torSettingsReady.value;
  },
  onUserClick() {
    window.gSubDialog.open(
      "chrome://browser/content/torpreferences/connectionSettingsDialog.xhtml",
      { features: "resizable=yes" }
    );
  },
});

Preferences.addSetting({
  id: "torViewLog",
  onUserClick() {
    window.gSubDialog.open(
      "chrome://browser/content/torpreferences/torLogDialog.xhtml",
      { features: "resizable=yes" }
    );
  },
});
Loading