Commit 9cd74389 authored by Richard Pospesel's avatar Richard Pospesel
Browse files

Bug 40597: Implement TorSettings module

- migrated in-page settings read/write implementation from about:preferences#tor
  to the TorSettings module
- TorSettings initially loads settings from the tor daemon, and saves them to
  firefox prefs
- TorSettings notifies observers when a setting has changed; currently only
  QuickStart notification is implemented for parity with previous preference
  notify logic in about:torconnect and about:preferences#tor
- about:preferences#tor, and about:torconnect now read and write settings
  thorugh the TorSettings module
- all tor settings live in the torbrowser.settings.* preference branch
- removed unused pref modify permission for about:torconnect content page from
  AsyncPrefs.jsm
parent 547a3d82
Loading
Loading
Loading
Loading
+14 −11
Original line number Diff line number Diff line
@@ -7,10 +7,9 @@ const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm");
const { TorConnect, TorConnectTopics, TorConnectState } = ChromeUtils.import(
  "resource:///modules/TorConnect.jsm"
);

const TorLauncherPrefs = Object.freeze({
  quickstart: "extensions.torlauncher.quickstart",
});
const { TorSettings, TorSettingsTopics, TorSettingsData } = ChromeUtils.import(
  "resource:///modules/TorSettings.jsm"
);

/*
This object is basically a marshalling interface between the TorConnect module
@@ -31,7 +30,7 @@ class TorConnectParent extends JSWindowActorParent {
      BootstrapProgress: TorConnect.bootstrapProgress,
      BootstrapStatus: TorConnect.bootstrapStatus,
      ShowCopyLog: TorConnect.logHasWarningOrError,
      QuickStartEnabled: Services.prefs.getBoolPref(TorLauncherPrefs.quickstart, false),
      QuickStartEnabled: TorSettings.quickstart.enabled,
    };

    // JSWindowActiveParent derived objects cannot observe directly, so create a member
@@ -78,9 +77,12 @@ class TorConnectParent extends JSWindowActorParent {
            // TODO: handle
            break;
          }
          case "nsPref:changed": {
            if (aData === TorLauncherPrefs.quickstart) {
              self.state.QuickStartEnabled = Services.prefs.getBoolPref(TorLauncherPrefs.quickstart);
          case TorSettingsTopics.SettingChanged:{
            if (aData === TorSettingsData.QuickStartEnabled) {
              self.state.QuickStartEnabled = obj.value;
            } else {
              // this isn't a setting torconnect cares about
              return;
            }
            break;
          }
@@ -98,7 +100,7 @@ class TorConnectParent extends JSWindowActorParent {
      const topic = TorConnectTopics[key];
      Services.obs.addObserver(this.torConnectObserver, topic);
    }
    Services.prefs.addObserver(TorLauncherPrefs.quickstart, this.torConnectObserver);
    Services.obs.addObserver(this.torConnectObserver, TorSettingsTopics.SettingChanged);
  }

  willDestroy() {
@@ -107,13 +109,14 @@ class TorConnectParent extends JSWindowActorParent {
      const topic = TorConnectTopics[key];
      Services.obs.removeObserver(this.torConnectObserver, topic);
    }
    Services.prefs.removeObserver(TorLauncherPrefs.quickstart, this.torConnectObserver);
    Services.obs.removeObserver(this.torConnectObserver, TorSettingsTopics.SettingChanged);
  }

  receiveMessage(message) {
    switch (message.name) {
      case "torconnect:set-quickstart":
        Services.prefs.setBoolPref(TorLauncherPrefs.quickstart, message.data);
        TorSettings.quickstart.enabled = message.data;
        TorSettings.saveToPrefs().applySettings();
        break;
      case "torconnect:open-tor-preferences":
        TorConnect.openTorPreferences();
+0 −89
Original line number Diff line number Diff line
"use strict";

var EXPORTED_SYMBOLS = [
  "parsePort",
  "parseAddrPort",
  "parseUsernamePassword",
  "parseAddrPortList",
  "parseBridgeStrings",
  "parsePortList",
];

// expects a string representation of an integer from 1 to 65535
let parsePort = function(aPort) {
  // ensure port string is a valid positive integer
  const validIntRegex = /^[0-9]+$/;
  if (!validIntRegex.test(aPort)) {
    throw new Error(`Invalid PORT string : '${aPort}'`);
  }

  // ensure port value is on valid range
  let port = Number.parseInt(aPort);
  if (port < 1 || port > 65535) {
    throw new Error(
      `Invalid PORT value, needs to be on range [1,65535] : '${port}'`
    );
  }

  return port;
};
// expects a string in the format: "ADDRESS:PORT"
let parseAddrPort = function(aAddrColonPort) {
  let tokens = aAddrColonPort.split(":");
  if (tokens.length != 2) {
    throw new Error(`Invalid ADDRESS:PORT string : '${aAddrColonPort}'`);
  }
  let address = tokens[0];
  let port = parsePort(tokens[1]);
  return [address, port];
};

// expects a string in the format: "USERNAME:PASSWORD"
// split on the first colon and any subsequent go into password
let parseUsernamePassword = function(aUsernameColonPassword) {
  let colonIndex = aUsernameColonPassword.indexOf(":");
  if (colonIndex < 0) {
    // we don't log the contents of the potentially password containing string
    throw new Error("Invalid USERNAME:PASSWORD string");
  }

  let username = aUsernameColonPassword.substring(0, colonIndex);
  let password = aUsernameColonPassword.substring(colonIndex + 1);

  return [username, password];
};

// expects a string in the format: ADDRESS:PORT,ADDRESS:PORT,...
// returns array of ports (as ints)
let parseAddrPortList = function(aAddrPortList) {
  let addrPorts = aAddrPortList.split(",");
  // parse ADDRESS:PORT string and only keep the port (second element in returned array)
  let retval = addrPorts.map(addrPort => parseAddrPort(addrPort)[1]);
  return retval;
};

// expects a '/n' or '/r/n' delimited bridge string, which we split and trim
// each bridge string can also optionally have 'bridge' at the beginning ie:
// bridge $(type) $(address):$(port) $(certificate)
// we strip out the 'bridge' prefix here
let parseBridgeStrings = function(aBridgeStrings) {

  // replace carriage returns ('\r') with new lines ('\n')
  aBridgeStrings = aBridgeStrings.replace(/\r/g, "\n");
  // then replace contiguous new lines ('\n') with a single one
  aBridgeStrings = aBridgeStrings.replace(/[\n]+/g, "\n");

  // split on the newline and for each bridge string: trim, remove starting 'bridge' string
  // finally discard entries that are empty strings; empty strings could occur if we receive
  // a new line containing only whitespace
  let splitStrings = aBridgeStrings.split("\n");
  return splitStrings.map(val => val.trim().replace(/^bridge\s+/i, ""))
                     .filter(bridgeString => bridgeString != "");
};

// expecting a ',' delimited list of ints with possible white space between
// returns an array of ints
let parsePortList = function(aPortListString) {
  let splitStrings = aPortListString.split(",");
  return splitStrings.map(val => parsePort(val.trim()));
};
+0 −325
Original line number Diff line number Diff line
"use strict";

var EXPORTED_SYMBOLS = [
  "TorBridgeSource",
  "TorBridgeSettings",
  "makeTorBridgeSettingsNone",
  "makeTorBridgeSettingsBuiltin",
  "makeTorBridgeSettingsBridgeDB",
  "makeTorBridgeSettingsUserProvided",
];

const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { TorProtocolService } = ChromeUtils.import(
  "resource:///modules/TorProtocolService.jsm"
);
const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm");

const TorBridgeSource = {
  NONE: "NONE",
  BUILTIN: "BUILTIN",
  BRIDGEDB: "BRIDGEDB",
  USERPROVIDED: "USERPROVIDED",
};

class TorBridgeSettings {
  constructor() {
    this._bridgeSource = TorBridgeSource.NONE;
    this._selectedDefaultBridgeType = null;
    this._bridgeStrings = [];
  }

  get selectedDefaultBridgeType() {
    if (this._bridgeSource == TorBridgeSource.BUILTIN) {
      return this._selectedDefaultBridgeType;
    }
    return undefined;
  }

  get bridgeSource() {
    return this._bridgeSource;
  }

  // for display
  get bridgeStrings() {
    return this._bridgeStrings.join("\n");
  }

  // raw
  get bridgeStringsArray() {
    return this._bridgeStrings;
  }

  static get defaultBridgeTypes() {
    if (TorBridgeSettings._defaultBridgeTypes) {
      return TorBridgeSettings._defaultBridgeTypes;
    }

    let bridgeListBranch = Services.prefs.getBranch(
      TorStrings.preferenceBranches.defaultBridge
    );
    let bridgePrefs = bridgeListBranch.getChildList("", {});

    // an unordered set for shoving bridge types into
    let bridgeTypes = new Set();
    // look for keys ending in ".N" and treat string before that as the bridge type
    const pattern = /\.[0-9]+$/;
    for (const key of bridgePrefs) {
      const offset = key.search(pattern);
      if (offset != -1) {
        const bt = key.substring(0, offset);
        bridgeTypes.add(bt);
      }
    }

    // recommended bridge type goes first in the list
    let recommendedBridgeType = Services.prefs.getCharPref(
      TorStrings.preferenceKeys.recommendedBridgeType,
      null
    );

    let retval = [];
    if (recommendedBridgeType && bridgeTypes.has(recommendedBridgeType)) {
      retval.push(recommendedBridgeType);
    }

    for (const bridgeType of bridgeTypes.values()) {
      if (bridgeType != recommendedBridgeType) {
        retval.push(bridgeType);
      }
    }

    // cache off
    TorBridgeSettings._defaultBridgeTypes = retval;
    return retval;
  }

  _readDefaultBridges(aBridgeType) {
    let bridgeBranch = Services.prefs.getBranch(
      TorStrings.preferenceBranches.defaultBridge
    );
    let bridgeBranchPrefs = bridgeBranch.getChildList("", {});

    let retval = [];

    // regex matches against strings ending in ".N" where N is a positive integer
    let pattern = /\.[0-9]+$/;
    for (const key of bridgeBranchPrefs) {
      // verify the location of the match is the correct offset required for aBridgeType
      // to fit, and that the string begins with aBridgeType
      if (
        key.search(pattern) == aBridgeType.length &&
        key.startsWith(aBridgeType)
      ) {
        let bridgeStr = bridgeBranch.getCharPref(key);
        retval.push(bridgeStr);
      }
    }

    // fisher-yates shuffle
    // shuffle so that Tor Browser users don't all try the built-in bridges in the same order
    for (let i = retval.length - 1; i > 0; --i) {
      // number n such that 0.0 <= n < 1.0
      const n = Math.random();
      // integer j such that 0 <= j <= i
      const j = Math.floor(n * (i + 1));

      // swap values at indices i and j
      const tmp = retval[i];
      retval[i] = retval[j];
      retval[j] = tmp;
    }

    return retval;
  }

  _readBridgeDBBridges() {
    let bridgeBranch = Services.prefs.getBranch(
      `${TorStrings.preferenceBranches.bridgeDBBridges}`
    );
    let bridgeBranchPrefs = bridgeBranch.getChildList("", {});
    // the child prefs do not come in any particular order so sort the keys
    // so the values can be compared to what we get out off torrc
    bridgeBranchPrefs.sort();

    // just assume all of the prefs under the parent point to valid bridge string
    let retval = bridgeBranchPrefs.map(key =>
      bridgeBranch.getCharPref(key).trim()
    );

    return retval;
  }

  _readTorrcBridges() {
    let bridgeList = TorProtocolService.readStringArraySetting(
      TorStrings.configKeys.bridgeList
    );

    let retval = [];
    for (const line of bridgeList) {
      let trimmedLine = line.trim();
      if (trimmedLine) {
        retval.push(trimmedLine);
      }
    }

    return retval;
  }

  // analagous to initBridgeSettings()
  readSettings() {
    // restore to defaults
    this._bridgeSource = TorBridgeSource.NONE;
    this._selectedDefaultBridgeType = null;
    this._bridgeStrings = [];

    // So the way tor-launcher determines the origin of the configured bridges is a bit
    // weird and depends on inferring our scenario based on some firefox prefs and the
    // relationship between the saved list of bridges in about:config vs the list saved in torrc

    // first off, if "extensions.torlauncher.default_bridge_type" is set to one of our
    // builtin default types (obfs4, meek-azure, snowflake, etc) then we provide the
    // bridges in "extensions.torlauncher.default_bridge.*" (filtered by our default_bridge_type)

    // next, we compare the list of bridges saved in torrc to the bridges stored in the
    // "extensions.torlauncher.bridgedb_bridge."" branch. If they match *exactly* then we assume
    // the bridges were retrieved from BridgeDB and use those. If the torrc list is empty then we know
    // we have no bridge settings

    // finally, if none of the previous conditions are not met, it is assumed the bridges stored in
    // torrc are user-provided

    // what we should(?) do once we excise tor-launcher entirely is explicitly store an int/enum in
    // about:config that tells us which scenario we are in so we don't have to guess

    let defaultBridgeType = Services.prefs.getCharPref(
      TorStrings.preferenceKeys.defaultBridgeType,
      null
    );

    // check if source is BUILTIN
    if (defaultBridgeType) {
      this._bridgeStrings = this._readDefaultBridges(defaultBridgeType);
      this._bridgeSource = TorBridgeSource.BUILTIN;
      this._selectedDefaultBridgeType = defaultBridgeType;
      return;
    }

    let torrcBridges = this._readTorrcBridges();

    // no stored bridges means no bridge is in use
    if (torrcBridges.length == 0) {
      this._bridgeStrings = [];
      this._bridgeSource = TorBridgeSource.NONE;
      return;
    }

    let bridgedbBridges = this._readBridgeDBBridges();

    // if these two lists are equal then we got our bridges from bridgedb
    // ie: same element in identical order
    let arraysEqual = (left, right) => {
      if (left.length != right.length) {
        return false;
      }
      const length = left.length;
      for (let i = 0; i < length; ++i) {
        if (left[i] != right[i]) {
          return false;
        }
      }
      return true;
    };

    // agreement between prefs and torrc means bridgedb bridges
    if (arraysEqual(torrcBridges, bridgedbBridges)) {
      this._bridgeStrings = torrcBridges;
      this._bridgeSource = TorBridgeSource.BRIDGEDB;
      return;
    }

    // otherwise they must be user provided
    this._bridgeStrings = torrcBridges;
    this._bridgeSource = TorBridgeSource.USERPROVIDED;
  }

  writeSettings() {
    let settingsObject = new Map();

    // init tor bridge settings to null
    settingsObject.set(TorStrings.configKeys.useBridges, null);
    settingsObject.set(TorStrings.configKeys.bridgeList, null);

    // clear bridge related firefox prefs
    Services.prefs.setCharPref(TorStrings.preferenceKeys.defaultBridgeType, "");
    let bridgeBranch = Services.prefs.getBranch(
      `${TorStrings.preferenceBranches.bridgeDBBridges}`
    );
    let bridgeBranchPrefs = bridgeBranch.getChildList("", {});
    for (const pref of bridgeBranchPrefs) {
      Services.prefs.clearUserPref(
        `${TorStrings.preferenceBranches.bridgeDBBridges}${pref}`
      );
    }

    switch (this._bridgeSource) {
      case TorBridgeSource.BUILTIN:
        // set builtin bridge type to use in prefs
        Services.prefs.setCharPref(
          TorStrings.preferenceKeys.defaultBridgeType,
          this._selectedDefaultBridgeType
        );
        break;
      case TorBridgeSource.BRIDGEDB:
        // save bridges off to prefs
        for (let i = 0; i < this.bridgeStringsArray.length; ++i) {
          Services.prefs.setCharPref(
            `${TorStrings.preferenceBranches.bridgeDBBridges}${i}`,
            this.bridgeStringsArray[i]
          );
        }
        break;
    }

    // write over our bridge list if bridges are enabled
    if (this._bridgeSource != TorBridgeSource.NONE) {
      settingsObject.set(TorStrings.configKeys.useBridges, true);
      settingsObject.set(
        TorStrings.configKeys.bridgeList,
        this.bridgeStringsArray
      );
    }
    TorProtocolService.writeSettings(settingsObject);
  }
}

function makeTorBridgeSettingsNone() {
  return new TorBridgeSettings();
}

function makeTorBridgeSettingsBuiltin(aBridgeType) {
  let retval = new TorBridgeSettings();
  retval._bridgeSource = TorBridgeSource.BUILTIN;
  retval._selectedDefaultBridgeType = aBridgeType;
  retval._bridgeStrings = retval._readDefaultBridges(aBridgeType);

  return retval;
}

function makeTorBridgeSettingsBridgeDB(aBridges) {
  let retval = new TorBridgeSettings();
  retval._bridgeSource = TorBridgeSource.BRIDGEDB;
  retval._selectedDefaultBridgeType = null;
  retval._bridgeStrings = aBridges;

  return retval;
}

function makeTorBridgeSettingsUserProvided(aBridges) {
  let retval = new TorBridgeSettings();
  retval._bridgeSource = TorBridgeSource.USERPROVIDED;
  retval._selectedDefaultBridgeType = null;
  retval._bridgeStrings = aBridges;

  return retval;
}
+0 −72
Original line number Diff line number Diff line
"use strict";

var EXPORTED_SYMBOLS = [
  "TorFirewallSettings",
  "makeTorFirewallSettingsNone",
  "makeTorFirewallSettingsCustom",
];

const { TorProtocolService } = ChromeUtils.import(
  "resource:///modules/TorProtocolService.jsm"
);
const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm");
const { parseAddrPortList } = ChromeUtils.import(
  "chrome://browser/content/torpreferences/parseFunctions.jsm"
);

class TorFirewallSettings {
  constructor() {
    this._allowedPorts = [];
  }

  get portsConfigurationString() {
    let portStrings = this._allowedPorts.map(port => `*:${port}`);
    return portStrings.join(",");
  }

  get commaSeparatedListString() {
    return this._allowedPorts.join(",");
  }

  get hasPorts() {
    return this._allowedPorts.length > 0;
  }

  readSettings() {
    let addressPortList = TorProtocolService.readStringSetting(
      TorStrings.configKeys.reachableAddresses
    );

    let allowedPorts = [];
    if (addressPortList) {
      allowedPorts = parseAddrPortList(addressPortList);
    }
    this._allowedPorts = allowedPorts;
  }

  writeSettings() {
    let settingsObject = new Map();

    // init to null so Tor daemon resets if no ports
    settingsObject.set(TorStrings.configKeys.reachableAddresses, null);

    if (this._allowedPorts.length > 0) {
      settingsObject.set(
        TorStrings.configKeys.reachableAddresses,
        this.portsConfigurationString
      );
    }

    TorProtocolService.writeSettings(settingsObject);
  }
}

function makeTorFirewallSettingsNone() {
  return new TorFirewallSettings();
}

function makeTorFirewallSettingsCustom(aPortsList) {
  let retval = new TorFirewallSettings();
  retval._allowedPorts = aPortsList;
  return retval;
}
+131 −170

File changed.

Preview size limit exceeded, changes collapsed.

Loading