Commit c34b67ca authored by Pier Angelo Vendrame's avatar Pier Angelo Vendrame 🎃
Browse files

fixup! Bug 40597: Implement TorSettings module

Bug 42336: Rework the relationship between TorSettings and TorProvider.
parent 93fdc92f
Loading
Loading
Loading
Loading
+3 −2
Changes for toolkit/components/tor-launcher/TorStartupService.sys.mjs: 3 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -30,15 +30,16 @@ export class TorStartupService {
    }
  }

  async #init() {
  #init() {
    Services.obs.addObserver(this, BrowserTopics.QuitApplicationGranted);

    lazy.TorSettings.init();

    // Theoretically, build() is expected to await the initialization of the
    // provider, and anything needing the Tor Provider should be able to just
    // await on TorProviderBuilder.build().
    lazy.TorProviderBuilder.init();

    await lazy.TorSettings.init();
    lazy.TorConnect.init();

    gInited = true;
+15 −5
Changes for toolkit/modules/Moat.sys.mjs: 15 added lines, 5 removed lines.
Original line number Diff line number Diff line
@@ -2,13 +2,13 @@
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import { TorBridgeSource } from "resource://gre/modules/TorSettings.sys.mjs";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  DomainFrontRequestBuilder:
    "resource://gre/modules/DomainFrontedRequests.sys.mjs",
  TorBridgeSource: "resource://gre/modules/TorSettings.sys.mjs",
  TorSettings: "resource://gre/modules/TorSettings.sys.mjs",
});

const TorLauncherPrefs = Object.freeze({
@@ -211,13 +211,23 @@ export class MoatRPC {
    };
    switch (settings.bridges.source) {
      case "builtin":
        retval.bridges.source = TorBridgeSource.BuiltIn;
        retval.bridges.source = lazy.TorBridgeSource.BuiltIn;
        retval.bridges.builtin_type = settings.bridges.type;
        // TorSettings will ignore strings for built-in bridges, and use the
        // ones it already knows, instead.
        // ones it already knows, instead. However, when we try these settings
        // in the connect assist, we skip TorSettings. Therefore, we set the
        // lines also here (the ones we already known, not the ones we receive
        // from Moat). This needs TorSettings to be initialized, which by now
        // should have already happened (this method is used only by TorConnect,
        // that needs TorSettings to be initialized).
        // In any case, getBuiltinBridges will throw if the data is not ready,
        // yet.
        retval.bridges.bridge_strings = lazy.TorSettings.getBuiltinBridges(
          settings.bridges.type
        );
        break;
      case "bridgedb":
        retval.bridges.source = TorBridgeSource.BridgeDB;
        retval.bridges.source = lazy.TorBridgeSource.BridgeDB;
        if (settings.bridges.bridge_strings) {
          retval.bridges.bridge_strings = settings.bridges.bridge_strings;
        } else {
+62 −32
Changes for toolkit/modules/TorConnect.sys.mjs: 62 added lines, 32 removed lines.
Original line number Diff line number Diff line
@@ -9,6 +9,7 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
  MoatRPC: "resource://gre/modules/Moat.sys.mjs",
  TorBootstrapRequest: "resource://gre/modules/TorBootstrapRequest.sys.mjs",
  TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs",
});

// TODO: Should we move this to the about:torconnect actor?
@@ -19,10 +20,7 @@ ChromeUtils.defineModuleGetter(
);

import { TorLauncherUtil } from "resource://gre/modules/TorLauncherUtil.sys.mjs";
import {
  TorSettings,
  TorSettingsTopics,
} from "resource://gre/modules/TorSettings.sys.mjs";
import { TorSettings } from "resource://gre/modules/TorSettings.sys.mjs";

import { TorStrings } from "resource://gre/modules/TorStrings.sys.mjs";

@@ -650,10 +648,22 @@ export const TorConnect = (() => {
                  }
                }

                // apply each of our settings and try to bootstrap with each
                const restoreOriginalSettings = async () => {
                  try {
                  this.originalSettings = TorSettings.getSettings();
                    await TorSettings.applySettings();
                  } catch (e) {
                    // We cannot do much if the original settings were bad or
                    // if the connection closed, so just report it in the
                    // console.
                    console.warn(
                      "TorConnect: Failed to restore original settings.",
                      e
                    );
                  }
                };

                // apply each of our settings and try to bootstrap with each
                try {
                  for (const [
                    index,
                    currentSetting,
@@ -669,8 +679,26 @@ export const TorConnect = (() => {
                      }/${this.settings.length}`
                    );

                    TorSettings.setSettings(currentSetting);
                    await TorSettings.applySettings();
                    // Send the new settings directly to the provider. We will
                    // save them only if the bootstrap succeeds.
                    // FIXME: We should somehow signal TorSettings users that we
                    // have set custom settings, and they should not apply
                    // theirs until we are done with trying ours.
                    // Otherwise, the new settings provided by the user while we
                    // were bootstrapping could be the ones that cause the
                    // bootstrap to succeed, but we overwrite them (unless we
                    // backup the original settings, and then save our new
                    // settings only if they have not changed).
                    // Another idea (maybe easier to implement) is to disable
                    // the settings UI while *any* bootstrap is going on.
                    // This is also documented in tor-browser#41921.
                    const provider = await lazy.TorProviderBuilder.build();
                    // We need to merge with old settings, in case the user is
                    // using a proxy or is behind a firewall.
                    await provider.writeSettings({
                      ...TorSettings.getSettings(),
                      ...currentSetting,
                    });

                    // build out our bootstrap request
                    const tbr = new lazy.TorBootstrapRequest();
@@ -687,6 +715,7 @@ export const TorConnect = (() => {
                    this.on_transition = async nextState => {
                      if (nextState === TorConnectState.Configuring) {
                        await tbr.cancel();
                        await restoreOriginalSettings();
                      }
                      resolve();
                    };
@@ -694,23 +723,20 @@ export const TorConnect = (() => {
                    // begin bootstrap
                    if (await tbr.bootstrap()) {
                      // persist the current settings to preferences
                      TorSettings.setSettings(currentSetting);
                      TorSettings.saveToPrefs();
                      await TorSettings.applySettings();
                      TorConnect._changeState(TorConnectState.Bootstrapped);
                      return;
                    }
                  }

                  // bootstrapped failed for all potential settings, so reset daemon to use original
                  TorSettings.setSettings(this.originalSettings);
                  // The original settings should be good, so we save them to
                  // preferences before trying to apply them, as it might fail
                  // if the actual problem is with the connection to the control
                  // port.
                  // FIXME: We should handle this case in a better way.
                  TorSettings.saveToPrefs();
                  await TorSettings.applySettings();
                  // Bootstrap failed for all potential settings, so restore the
                  // original settings the provider.
                  await restoreOriginalSettings();

                  // only explicitly change state here if something else has not transitioned us
                  // Only explicitly change state here if something else has not
                  // transitioned us.
                  if (!this.transitioning) {
                    throw_error(
                      TorStrings.torConnect.autoBootstrappingFailed,
@@ -719,18 +745,8 @@ export const TorConnect = (() => {
                  }
                  return;
                } catch (err) {
                  // restore original settings in case of error
                  try {
                    TorSettings.setSettings(this.originalSettings);
                    // As above
                    TorSettings.saveToPrefs();
                    await TorSettings.applySettings();
                  } catch (errRestore) {
                    console.log(
                      `TorConnect: Failed to restore original settings => ${errRestore}`
                    );
                  }
                  // throw to outer catch to transition us
                  await restoreOriginalSettings();
                  // throw to outer catch to transition us.
                  throw err;
                }
              } catch (err) {
@@ -877,6 +893,14 @@ export const TorConnect = (() => {
          console.log(`TorConnect: Observing topic '${addTopic}'`);
        };

        // Wait for TorSettings, as we will need it.
        // We will wait for a TorProvider only after TorSettings is ready,
        // because the TorProviderBuilder initialization might not have finished
        // at this point, and TorSettings initialization is a prerequisite for
        // having a provider.
        // So, we prefer initializing TorConnect as soon as possible, so that
        // the UI will be able to detect it is in the Initializing state and act
        // consequently.
        TorSettings.initializedPromise.then(() => this._settingsInitialized());

        // register the Tor topics we always care about
@@ -918,19 +942,25 @@ export const TorConnect = (() => {
      }
    },

    _settingsInitialized() {
    async _settingsInitialized() {
      // TODO: Handle failures here, instead of the prompt to restart the
      // daemon when it exits (tor-browser#21053, tor-browser#41921).
      await lazy.TorProviderBuilder.build();

      // tor-browser#41907: This is only a workaround to avoid users being
      // bounced back to the initial panel without any explanation.
      // Longer term we should disable the clickable elements, or find a UX
      // to prevent this from happening (e.g., allow buttons to be clicked,
      // but show an intermediate starting state, or a message that tor is
      // starting while the butons are disabled, etc...).
      // See also tor-browser#41921.
      if (this.state !== TorConnectState.Initial) {
        console.warn(
          "TorConnect: Seen the torsettings:ready after the state has already changed, ignoring the notification."
          "TorConnect: The TorProvider was built after the state had already changed."
        );
        return;
      }
      console.log("TorConnect: The TorProvider is ready, changing state.");
      if (this.shouldQuickStart) {
        // Quickstart
        this._changeState(TorConnectState.Bootstrapping);
+20 −140
Changes for toolkit/modules/TorSettings.sys.mjs: 20 added lines, 140 removed lines.
Original line number Diff line number Diff line
@@ -6,10 +6,9 @@ const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  TorLauncherUtil: "resource://gre/modules/TorLauncherUtil.sys.mjs",
  TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs",
  TorProviderTopics: "resource://gre/modules/TorProviderBuilder.sys.mjs",
  Lox: "resource://gre/modules/Lox.sys.mjs",
  TorParsers: "resource://gre/modules/TorParsers.sys.mjs",
  TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs",
});

ChromeUtils.defineLazyGetter(lazy, "logger", () => {
@@ -71,20 +70,6 @@ const TorSettingsPrefs = Object.freeze({
  },
});

/* Config Keys used to configure tor daemon */
const TorConfigKeys = Object.freeze({
  useBridges: "UseBridges",
  bridgeList: "Bridge",
  socks4Proxy: "Socks4Proxy",
  socks5Proxy: "Socks5Proxy",
  socks5ProxyUsername: "Socks5ProxyUsername",
  socks5ProxyPassword: "Socks5ProxyPassword",
  httpsProxy: "HTTPSProxy",
  httpsProxyAuthenticator: "HTTPSProxyAuthenticator",
  reachableAddresses: "ReachableAddresses",
  clientTransportPlugin: "ClientTransportPlugin",
});

export const TorBridgeSource = Object.freeze({
  Invalid: -1,
  BuiltIn: 0,
@@ -322,7 +307,7 @@ class TorSettingsImpl {
          if (!val) {
            return;
          }
          const bridgeStrings = this.#getBuiltinBridges(val);
          const bridgeStrings = this.getBuiltinBridges(val);
          if (bridgeStrings.length) {
            this.bridges.bridge_strings = bridgeStrings;
            return;
@@ -659,14 +644,17 @@ class TorSettingsImpl {
   * @param {string} pt The pluggable transport to return the lines for
   * @returns {string[]} The bridge lines in random order
   */
  #getBuiltinBridges(pt) {
  getBuiltinBridges(pt) {
    if (!this.#allowUninitialized) {
      this.#checkIfInitialized();
    }
    // Shuffle so that Tor Browser users do not all try the built-in bridges in
    // the same order.
    return arrayShuffle(this.#builtinBridges[pt] ?? []);
  }

  /**
   * Load or init our settings, and register observers.
   * Load or init our settings.
   */
  async init() {
    if (this.#initialized) {
@@ -677,6 +665,7 @@ class TorSettingsImpl {
      await this.#initInternal();
      this.#initialized = true;
      this.#initComplete();
      Services.obs.notifyObservers(null, TorSettingsTopics.Ready);
    } catch (e) {
      this.#initFailed(e);
      throw e;
@@ -698,19 +687,18 @@ class TorSettingsImpl {
      lazy.logger.error("Could not load the built-in PT config.", e);
    }

    // Initialize this before loading from prefs because we need Lox initialized before
    // any calls to Lox.getBridges()
    // Initialize this before loading from prefs because we need Lox initialized
    // before any calls to Lox.getBridges().
    try {
      await lazy.Lox.init();
    } catch (e) {
      lazy.logger.error("Could not initialize Lox.", e.type);
    }

    // TODO: We could use a shared promise, and wait for it to be fullfilled
    // instead of Service.obs.
    if (lazy.TorLauncherUtil.shouldStartAndOwnTor) {
      // if the settings branch exists, load settings from prefs
      if (Services.prefs.getBoolPref(TorSettingsPrefs.enabled, false)) {
    if (
      lazy.TorLauncherUtil.shouldStartAndOwnTor &&
      Services.prefs.getBoolPref(TorSettingsPrefs.enabled, false)
    ) {
      // Do not want notifications for initially loaded prefs.
      this.freezeNotifications();
      try {
@@ -722,21 +710,12 @@ class TorSettingsImpl {
        this.thawNotifications();
      }
    }
      try {
        const provider = await lazy.TorProviderBuilder.build();
        if (provider.isRunning) {
          this.#handleProcessReady();
          // No need to add an observer to call this again.
          return;
        }
      } catch {}

      Services.obs.addObserver(this, lazy.TorProviderTopics.ProcessIsReady);
    }
    lazy.logger.info("Ready");
  }

  /**
   * Unload or uninit our settings, and unregister observers.
   * Unload or uninit our settings.
   */
  async uninit() {
    await lazy.Lox.uninit();
@@ -764,34 +743,6 @@ class TorSettingsImpl {
    return this.#initialized;
  }

  /**
   * Wait for relevant life-cycle events to apply saved settings.
   */
  async observe(subject, topic, data) {
    lazy.logger.debug(`Observed ${topic}`);

    switch (topic) {
      case lazy.TorProviderTopics.ProcessIsReady:
        Services.obs.removeObserver(
          this,
          lazy.TorProviderTopics.ProcessIsReady
        );
        await this.#handleProcessReady();
        break;
    }
  }

  /**
   * Apply the settings once the tor provider is ready and notify any observer
   * that the settings can be used.
   */
  async #handleProcessReady() {
    // push down settings to tor
    await this.#applySettings(true);
    lazy.logger.info("Ready");
    Services.obs.notifyObservers(null, TorSettingsTopics.Ready);
  }

  /**
   * Load our settings from prefs.
   */
@@ -972,85 +923,14 @@ class TorSettingsImpl {

  /**
   * Push our settings down to the tor provider.
   *
   * Even though this introduces a circular depdency, it makes the API nicer for
   * frontend consumers.
   */
  async applySettings() {
    this.#checkIfInitialized();
    return this.#applySettings(false);
  }

  /**
   * Internal implementation of applySettings that does not check if we are
   * initialized.
   */
  async #applySettings(allowUninitialized) {
    lazy.logger.debug("#applySettings()");

    this.#cleanupSettings();

    const settingsMap = new Map();

    // #applySettings can be called only when #allowUninitialized is false
    this.#allowUninitialized = allowUninitialized;

    try {
      /* Bridges */
      const haveBridges =
        this.bridges.enabled && !!this.bridges.bridge_strings.length;
      settingsMap.set(TorConfigKeys.useBridges, haveBridges);
      if (haveBridges) {
        settingsMap.set(TorConfigKeys.bridgeList, this.bridges.bridge_strings);
      } else {
        settingsMap.set(TorConfigKeys.bridgeList, null);
      }

      /* Proxy */
      settingsMap.set(TorConfigKeys.socks4Proxy, null);
      settingsMap.set(TorConfigKeys.socks5Proxy, null);
      settingsMap.set(TorConfigKeys.socks5ProxyUsername, null);
      settingsMap.set(TorConfigKeys.socks5ProxyPassword, null);
      settingsMap.set(TorConfigKeys.httpsProxy, null);
      settingsMap.set(TorConfigKeys.httpsProxyAuthenticator, null);
      if (this.proxy.enabled) {
        const address = this.proxy.address;
        const port = this.proxy.port;
        const username = this.proxy.username;
        const password = this.proxy.password;

        switch (this.proxy.type) {
          case TorProxyType.Socks4:
            settingsMap.set(TorConfigKeys.socks4Proxy, `${address}:${port}`);
            break;
          case TorProxyType.Socks5:
            settingsMap.set(TorConfigKeys.socks5Proxy, `${address}:${port}`);
            settingsMap.set(TorConfigKeys.socks5ProxyUsername, username);
            settingsMap.set(TorConfigKeys.socks5ProxyPassword, password);
            break;
          case TorProxyType.HTTPS:
            settingsMap.set(TorConfigKeys.httpsProxy, `${address}:${port}`);
            settingsMap.set(
              TorConfigKeys.httpsProxyAuthenticator,
              `${username}:${password}`
            );
            break;
        }
      }

      /* Firewall */
      if (this.firewall.enabled) {
        const reachableAddresses = this.firewall.allowed_ports
          .map(port => `*:${port}`)
          .join(",");
        settingsMap.set(TorConfigKeys.reachableAddresses, reachableAddresses);
      } else {
        settingsMap.set(TorConfigKeys.reachableAddresses, null);
      }
    } finally {
      this.#allowUninitialized = false;
    }

    /* Push to Tor */
    const provider = await lazy.TorProviderBuilder.build();
    await provider.writeSettings(settingsMap);
    await provider.writeSettings(this.getSettings());
  }

  /**