Commit 3058bb98 authored by henry's avatar henry Committed by Pier Angelo Vendrame
Browse files

fixup! TB 40597: Implement TorSettings module

TB 41921: Return only the bridge settings from Moat.

Rather than return { bridges: enabled: true, ... } from MoatRPC API, we
just return the relevant parts. This should make it clearer that Moat
can *only* change TorSettings.bridges, and nothing else.
parent 4adb5447
Loading
Loading
Loading
Loading
+62 −43
Changes for toolkit/modules/Moat.sys.mjs: 62 added lines, 43 removed lines.
Original line number Diff line number Diff line
@@ -73,6 +73,25 @@ class InternetTestResponseListener {
  }
}

/**
 * @typedef {Object} MoatBridges
 *
 * Bridge settings that can be passed to TorSettings.bridges.
 *
 * @property {number} source - The `TorBridgeSource` type.
 * @property {string} [builtin_type] - The built-in bridge type.
 * @property {string[]} bridge_strings - The bridge lines.
 */

/**
 * @typedef {Object} MoatSettings
 *
 * The settings returned by Moat.
 *
 * @property {MoatBridges[]} bridgesList - The list of bridges found.
 * @property {string} [country] - The detected country (region).
 */

/**
 * Constructs JSON objects and sends requests over Moat.
 * The documentation about the JSON schemas to use are available at
@@ -213,20 +232,21 @@ export class MoatRPC {
    return { bridges, qrcode: qrcodeImg };
  }

  // Convert received settings object to format used by TorSettings module.
  #fixupSettings(settings) {
  /**
   * Extract bridges from the received Moat settings object.
   *
   * @param {Object} settings - The received settings.
   * @return {MoatBridge} The extracted bridges.
   */
  #extractBridges(settings) {
    if (!("bridges" in settings)) {
      throw new Error("Expected to find `bridges` in the settings object.");
    }
    const retval = {
      bridges: {
        enabled: true,
      },
    };
    const bridges = {};
    switch (settings.bridges.source) {
      case "builtin":
        retval.bridges.source = lazy.TorBridgeSource.BuiltIn;
        retval.bridges.builtin_type = settings.bridges.type;
        bridges.source = lazy.TorBridgeSource.BuiltIn;
        bridges.builtin_type = settings.bridges.type;
        // TorSettings will ignore strings for built-in bridges, and use the
        // ones it already knows, instead. However, when we try these settings
        // in the connect assist, we skip TorSettings. Therefore, we set the
@@ -236,14 +256,14 @@ export class MoatRPC {
        // 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(
        bridges.bridge_strings = lazy.TorSettings.getBuiltinBridges(
          settings.bridges.type
        );
        break;
      case "bridgedb":
        retval.bridges.source = lazy.TorBridgeSource.BridgeDB;
        bridges.source = lazy.TorBridgeSource.BridgeDB;
        if (settings.bridges.bridge_strings) {
          retval.bridges.bridge_strings = settings.bridges.bridge_strings;
          bridges.bridge_strings = settings.bridges.bridge_strings;
        } else {
          throw new Error(
            "Received no bridge-strings for BridgeDB bridge source"
@@ -255,37 +275,38 @@ export class MoatRPC {
          `Unexpected bridge source '${settings.bridges.source}'`
        );
    }
    return retval;
    return bridges;
  }

  // Converts a list of settings objects received from BridgeDB to a list of
  // settings objects understood by the TorSettings module.
  // In the event of error, returns an empty list.
  #fixupSettingsList(settingsList) {
    const retval = [];
  /**
   * Extract a list of bridges from the received Moat settings object.
   *
   * @param {Object} settings - The received settings.
   * @return {MoatBridge[]} The list of extracted bridges.
   */
  #extractBridgesList(settingsList) {
    const bridgesList = [];
    for (const settings of settingsList) {
      try {
        retval.push(this.#fixupSettings(settings));
        bridgesList.push(this.#extractBridges(settings));
      } catch (ex) {
        log.error(ex);
      }
    }
    return retval;
    return bridgesList;
  }

  // Request tor settings for the user optionally based on their location
  // (derived from their IP). Takes the following parameters:
  // - transports: optional, an array of transports available to the client; if
  //   empty (or not given) returns settings using all working transports known
  //   to the server
  // - country: optional, an ISO 3166-1 alpha-2 country code to request settings
  //   for; if not provided the country is determined by the user's IP address
  //
  // Returns an object with the detected country code and an array of settings
  // in a format that can be passed to the TorSettings module. This array might
  // be empty if the country has no associated settings.
  // If the server cannot determine the user's country (and no country code is
  // provided), then null is returned instead of the object.
  /**
   * Request tor settings for the user optionally based on their location
   * (derived from their IP). Takes the following parameters:
   *
   * @param {string[]} transports - A list of transports we support.
   * @param {?string} country - The region to request bridges for, as an
   *   ISO 3166-1 alpha-2 region code, or `null` to have the server
   *   automatically determine the region.
   * @returns {?MoatSettings} - The returned settings from the server, or `null`
   *   if the region could not be determined by the server.
   */
  async circumvention_settings(transports, country) {
    const args = {
      transports: transports ? transports : [],
@@ -306,7 +327,7 @@ export class MoatRPC {

      throw new Error(`MoatRPC: ${detail} (${code})`);
    } else if ("settings" in response) {
      settings.settings = this.#fixupSettingsList(response.settings);
      settings.bridgesList = this.#extractBridgesList(response.settings);
    }
    if ("country" in response) {
      settings.country = response.country;
@@ -349,14 +370,12 @@ export class MoatRPC {
    return map;
  }

  // Request a copy of the defaul/fallback bridge settings, takes the following
  // parameters:
  // - transports: optional, an array of transports available to the client; if
  //   empty (or not given) returns settings using all working transports known
  //   to the server
  //
  // returns an array of settings objects in roughly the same format as the
  // _settings object on the TorSettings module
  /**
   * Request a copy of the default/fallback bridge settings.
   *
   * @param {string[]} transports - A list of transports we support.
   * @returns {MoatBridges[]} - The list of bridges found.
   */
  async circumvention_defaults(transports) {
    const args = {
      transports: transports ? transports : [],
@@ -367,7 +386,7 @@ export class MoatRPC {
      const detail = response.errors[0].detail;
      throw new Error(`MoatRPC: ${detail} (${code})`);
    } else if ("settings" in response) {
      return this.#fixupSettingsList(response.settings);
      return this.#extractBridgesList(response.settings);
    }
    return [];
  }
+28 −27
Changes for toolkit/modules/TorConnect.sys.mjs: 28 added lines, 27 removed lines.
Original line number Diff line number Diff line
@@ -103,11 +103,12 @@ export const TorConnectTopics = Object.freeze({
 *   failing bootstrap.
 * @property {integer} [options.simulateDelay] - The delay in microseconds to
 *   apply to simulated bootstraps.
 * @property {object} [options.simulateMoatResponse] - Simulate a Moat response
 *   for circumvention settings. Should include a "settings" property, and
 *   optionally a "country" property. You may add a "simulateCensorship"
 *   property to some of the settings to make only their bootstrap attempts
 *   fail.
 * @property {MoatSettings} [options.simulateMoatResponse] - Simulate a Moat
 *   response for circumvention settings. Should include a "bridgesList"
 *   property, and optionally a "country" property. The "bridgesList" property
 *   should be an Array of MoatBridges objects that match the bridge settings
 *   accepted by TorSettings.bridges, plus you may add a "simulateCensorship"
 *   property to make only their bootstrap attempts fail.
 * @property {boolean} [options.testInternet] - Whether to also test the
 *   internet connection.
 * @property {boolean} [options.simulateOffline] - Whether to simulate an
@@ -395,11 +396,11 @@ class AutoBootstrapAttempt {
   */
  #cancelledPromise = null;
  /**
   * The found settings from Moat.
   * The list of bridge configurations from Moat.
   *
   * @type {?object[]}
   * @type {?MoatBridges[]}
   */
  #settings = null;
  #bridgesList = null;
  /**
   * The last settings that have been applied to the TorProvider, if any.
   *
@@ -477,12 +478,12 @@ class AutoBootstrapAttempt {
   * @param {BootstrapOptions} options - Options to apply to the bootstrap.
   */
  async #runInternal(progressCallback, options) {
    await this.#fetchSettings(options);
    await this.#fetchBridges(options);
    if (this.#cancelled || this.#resolved) {
      return;
    }

    if (!this.#settings?.length) {
    if (!this.#bridgesList?.length) {
      this.#resolveRun({
        error: new TorConnectError(
          options.regionCode === "automatic" && !this.detectedRegion
@@ -493,14 +494,14 @@ class AutoBootstrapAttempt {
    }

    // Apply each of our settings and try to bootstrap with each.
    for (const [index, currentSetting] of this.#settings.entries()) {
    for (const [index, bridges] of this.#bridgesList.entries()) {
      lazy.logger.info(
        `Attempting Bootstrap with configuration ${index + 1}/${
          this.#settings.length
          this.#bridgesList.length
        }`
      );

      await this.#trySetting(currentSetting, progressCallback, options);
      await this.#tryBridges(bridges, progressCallback, options);

      if (this.#cancelled || this.#resolved) {
        return;
@@ -518,7 +519,7 @@ class AutoBootstrapAttempt {
   *
   * @param {BootstrapOptions} options - Options to apply to the bootstrap.
   */
  async #fetchSettings(options) {
  async #fetchBridges(options) {
    if (options.simulateMoatResponse) {
      await Promise.race([
        new Promise(res => setTimeout(res, options.simulateDelay || 0)),
@@ -530,7 +531,7 @@ class AutoBootstrapAttempt {
      }

      this.detectedRegion = options.simulateMoatResponse.country || null;
      this.#settings = options.simulateMoatResponse.settings ?? null;
      this.#bridgesList = options.simulateMoatResponse.bridgesList ?? null;

      return;
    }
@@ -564,16 +565,16 @@ class AutoBootstrapAttempt {

      this.detectedRegion = maybeSettings?.country || null;

      if (maybeSettings?.settings?.length) {
        this.#settings = maybeSettings.settings;
      if (maybeSettings?.bridgesList?.length) {
        this.#bridgesList = maybeSettings.bridgesList;
      } else {
        // Keep consistency with the other call.
        this.#settings = await Promise.race([
        this.#bridgesList = await Promise.race([
          moat.circumvention_defaults([
            ...lazy.TorSettings.builtinBridgeTypes,
            "vanilla",
          ]),
          // This might set this.#settings to undefined.
          // This might set this.#bridgesList to undefined.
          this.#cancelledPromise,
        ]);
      }
@@ -586,21 +587,21 @@ class AutoBootstrapAttempt {
  /**
   * Try to apply the settings we fetched.
   *
   * @param {object} setting - The setting to try.
   * @param {MoatBridges} bridges - The bridges to try.
   * @param {ProgressCallback} progressCallback - The callback to invoke with
   *   the bootstrap progress.
   * @param {BootstrapOptions} options - Options to apply to the bootstrap.
   */
  async #trySetting(setting, progressCallback, options) {
  async #tryBridges(bridges, progressCallback, options) {
    if (this.#cancelled || this.#resolved) {
      return;
    }

    if (options.simulateMoatResponse && setting.simulateCensorship) {
    if (options.simulateMoatResponse && bridges.simulateCensorship) {
      // Move the simulateCensorship option to the options for the next
      // BootstrapAttempt.
      setting = structuredClone(setting);
      delete setting.simulateCensorship;
      bridges = structuredClone(bridges);
      delete bridges.simulateCensorship;
      options = { ...options, simulateCensorship: true };
    }

@@ -617,12 +618,12 @@ class AutoBootstrapAttempt {
    // UI while *any* bootstrap is going on.
    // This is also documented in tor-browser#41921.
    const provider = await lazy.TorProviderBuilder.build();
    this.#changedSetting = setting;
    this.#changedSetting = { bridges: { enabled: true, ...bridges } };
    // We need to merge with old settings, in case the user is using a proxy
    // or is behind a firewall.
    await provider.writeSettings({
      ...lazy.TorSettings.getSettings(),
      ...setting,
      ...this.#changedSetting,
    });

    if (this.#cancelled || this.#resolved) {
@@ -642,7 +643,7 @@ class AutoBootstrapAttempt {
        error instanceof TorConnectError &&
        error.code === TorConnectError.BootstrapError
      ) {
        lazy.logger.info("TorConnect setting failed", setting, error);
        lazy.logger.info("TorConnect setting failed", bridges, error);
        // Try with the next settings.
        // NOTE: We do not restore the user settings in between these runs.
        // Instead we wait for #resolveRun callback to do so.