Verified Commit 71b08c4e authored by Pier Angelo Vendrame's avatar Pier Angelo Vendrame 🎃
Browse files

fixup! Bug 40933: Add tor-launcher functionality

TorProcess: use real private properties instead of _, and removed the
dependency on TorProtocolService (temporarily moved it to
TorMonitorService, but eventually we should unify TorProtocolService
and TorMonitorService, to then split them again in a smarter way).
parent f5a3f4af
Loading
Loading
Loading
Loading
+154 −0
Original line number Diff line number Diff line
@@ -5,6 +5,12 @@
 * Tor Launcher Util JS Module
 *************************************************************************/

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  FileUtils: "resource://gre/modules/FileUtils.sys.jsm",
});

const kPropBundleURI = "chrome://torbutton/locale/torlauncher.properties";
const kPropNamePrefix = "torlauncher.";
const kIPCDirPrefName = "extensions.torlauncher.tmp_ipc_dir";
@@ -453,6 +459,154 @@ export const TorLauncherUtil = Object.freeze({
    return result ? result : "";
  },

  /**
   * Determine what kind of SOCKS port has been requested for this session or
   * the browser has been configured for.
   * On Windows (where Unix domain sockets are not supported), TCP is always
   * used.
   *
   * The following environment variables are supported and take precedence over
   * preferences:
   *    TOR_TRANSPROXY (do not use a proxy)
   *    TOR_SOCKS_IPC_PATH (file system path; ignored on Windows)
   *    TOR_SOCKS_HOST
   *    TOR_SOCKS_PORT
   *
   * The following preferences are consulted:
   *    network.proxy.socks
   *    network.proxy.socks_port
   *    extensions.torlauncher.socks_port_use_ipc (Boolean)
   *    extensions.torlauncher.socks_ipc_path (file system path)
   * If extensions.torlauncher.socks_ipc_path is empty, a default path is used.
   *
   * When using TCP, if a value is not defined via an env variable it is
   * taken from the corresponding browser preference if possible. The
   * exceptions are:
   *   If network.proxy.socks contains a file: URL, a default value of
   *     "127.0.0.1" is used instead.
   *   If the network.proxy.socks_port value is not valid (outside the
   *     (0; 65535] range), a default value of 9150 is used instead.
   *
   * The SOCKS configuration will not influence the launch of a tor daemon and
   * the configuration of the control port in any way.
   * When a SOCKS configuration is required without TOR_SKIP_LAUNCH, the browser
   * will try to configure the tor instance to use the required configuration.
   * This also applies to TOR_TRANSPROXY (at least for now): tor will be
   * launched with its defaults.
   *
   * TODO: add a preference to ignore the current configuration, and let tor
   * listen on any free port. Then, the browser will prompt the daemon the port
   * to use through the control port (even though this is quite dangerous at the
   * moment, because with network disabled tor will disable also the SOCKS
   * listeners, so it means that we will have to check it every time we change
   * the network status).
   */
  getPreferredSocksConfiguration() {
    if (Services.env.exists("TOR_TRANSPROXY")) {
      Services.prefs.setBoolPref("network.proxy.socks_remote_dns", false);
      Services.prefs.setIntPref("network.proxy.type", 0);
      Services.prefs.setIntPref("network.proxy.socks_port", 0);
      Services.prefs.setCharPref("network.proxy.socks", "");
      return { transproxy: true };
    }

    let useIPC;
    const socksPortInfo = {
      transproxy: false,
    };

    if (!this.isWindows && Services.env.exists("TOR_SOCKS_IPC_PATH")) {
      useIPC = true;
      const ipcPath = Services.env.get("TOR_SOCKS_IPC_PATH");
      if (ipcPath) {
        socksPortInfo.ipcFile = new lazy.FileUtils.File(ipcPath);
      }
    } else {
      // Check for TCP host and port environment variables.
      if (Services.env.exists("TOR_SOCKS_HOST")) {
        socksPortInfo.host = Services.env.get("TOR_SOCKS_HOST");
        useIPC = false;
      }
      if (Services.env.exists("TOR_SOCKS_PORT")) {
        const port = parseInt(Services.env.get("TOR_SOCKS_PORT"), 10);
        if (Number.isInteger(port) && port > 0 && port <= 65535) {
          socksPortInfo.port = port;
          useIPC = false;
        }
      }
    }

    if (useIPC === undefined) {
      socksPortInfo.useIPC =
        !this.isWindows &&
        Services.prefs.getBoolPref(
          "extensions.torlauncher.socks_port_use_ipc",
          false
        );
    }

    // Fill in missing SOCKS info from prefs.
    if (socksPortInfo.useIPC) {
      if (!socksPortInfo.ipcFile) {
        socksPortInfo.ipcFile = TorLauncherUtil.getTorFile("socks_ipc", false);
      }
    } else {
      if (!socksPortInfo.host) {
        let socksAddr = Services.prefs.getCharPref(
          "network.proxy.socks",
          "127.0.0.1"
        );
        let socksAddrHasHost = socksAddr && !socksAddr.startsWith("file:");
        socksPortInfo.host = socksAddrHasHost ? socksAddr : "127.0.0.1";
      }

      if (!socksPortInfo.port) {
        let socksPort = Services.prefs.getIntPref(
          "network.proxy.socks_port",
          0
        );
        // This pref is set as 0 by default in Firefox, use 9150 if we get 0.
        socksPortInfo.port =
          socksPort > 0 && socksPort <= 65535 ? socksPort : 9150;
      }
    }

    return socksPortInfo;
  },

  setProxyConfiguration(socksPortInfo) {
    if (socksPortInfo.transproxy) {
      return;
    }

    if (socksPortInfo.useIPC) {
      const fph = Services.io
        .getProtocolHandler("file")
        .QueryInterface(Ci.nsIFileProtocolHandler);
      const fileURI = fph.newFileURI(socksPortInfo.ipcFile);
      Services.prefs.setCharPref("network.proxy.socks", fileURI.spec);
      Services.prefs.setIntPref("network.proxy.socks_port", 0);
    } else {
      if (socksPortInfo.host) {
        Services.prefs.setCharPref("network.proxy.socks", socksPortInfo.host);
      }
      if (socksPortInfo.port) {
        Services.prefs.setIntPref(
          "network.proxy.socks_port",
          socksPortInfo.port
        );
      }
    }

    if (socksPortInfo.ipcFile || socksPortInfo.host || socksPortInfo.port) {
      Services.prefs.setBoolPref("network.proxy.socks_remote_dns", true);
      Services.prefs.setIntPref("network.proxy.type", 1);
    }

    // Force prefs to be synced to disk
    Services.prefs.savePrefFile(null);
  },

  get shouldStartAndOwnTor() {
    const kPrefStartTor = "extensions.torlauncher.start_tor";
    try {
+9 −1
Original line number Diff line number Diff line
@@ -13,6 +13,10 @@ import { TorLauncherUtil } from "resource://gre/modules/TorLauncherUtil.sys.mjs"

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  TorProtocolService: "resource://gre/modules/TorProtocolService.sys.mjs",
});

ChromeUtils.defineModuleGetter(
  lazy,
  "controller",
@@ -233,7 +237,10 @@ export const TorMonitorService = {
    // TorProcess should be instanced once, then always reused and restarted
    // only through the prompt it exposes when the controlled process dies.
    if (!this._torProcess) {
      this._torProcess = new TorProcess();
      this._torProcess = new TorProcess(
        lazy.TorProtocolService.torControlPortInfo,
        lazy.TorProtocolService.torSOCKSPortInfo
      );
      this._torProcess.onExit = () => {
        this._shutDownEventMonitor();
        Services.obs.notifyObservers(null, TorTopics.ProcessExited);
@@ -254,6 +261,7 @@ export const TorMonitorService = {
      await this._torProcess.start();
      if (this._torProcess.isRunning) {
        logger.info("tor started");
        this._torProcessStartTime = Date.now();
      }
    } catch (e) {
      // TorProcess already logs the error.
+113 −82
Original line number Diff line number Diff line
@@ -4,18 +4,10 @@ import { Subprocess } from "resource://gre/modules/Subprocess.sys.mjs";

const lazy = {};

ChromeUtils.defineModuleGetter(
  lazy,
  "TorProtocolService",
  "resource://gre/modules/TorProtocolService.jsm"
);
const { TorLauncherUtil } = ChromeUtils.import(
  "resource://gre/modules/TorLauncherUtil.jsm"
);

const { TorParsers } = ChromeUtils.import(
  "resource://gre/modules/TorParsers.jsm"
);
ChromeUtils.defineESModuleGetters(lazy, {
  TorLauncherUtil: "resource://gre/modules/TorLauncherUtil.sys.mjs",
  TorParsers: "resource://gre/modules/TorParsers.sys.mjs",
});

const TorProcessStatus = Object.freeze({
  Unknown: 0,
@@ -30,17 +22,52 @@ const logger = new ConsoleAPI({
});

export class TorProcess {
  #controlSettings;
  #socksSettings;
  #exeFile = null;
  #dataDir = null;
  #args = [];
  #subprocess = null;
  #status = TorProcessStatus.Unknown;
  #torProcessStartTime = null; // JS Date.now()
  #didConnectToTorControlPort = false; // Have we ever made a connection?
  // Have we ever made a connection on the control port?
  #didConnectToTorControlPort = false;

  onExit = () => {};
  onRestart = () => {};

  constructor(controlSettings, socksSettings) {
    if (
      controlSettings &&
      !controlSettings.password &&
      !controlSettings.cookieFile
    ) {
      throw new Error("Unauthenticated control port is not supported");
    }

    const checkPort = port =>
      port === undefined ||
      (Number.isInteger(controlSettings.port) &&
        controlSettings.port > 0 &&
        controlSettings.port < 65535);
    if (!checkPort(controlSettings?.port)) {
      throw new Error("Invalid control port");
    }
    if (!checkPort(socksSettings.port)) {
      throw new Error("Invalid port specified for the SOCKS port");
    }

    this.#controlSettings = { ...controlSettings };
    const ipcFileToString = file =>
      "unix:" + lazy.TorParsers.escapeString(file.path);
    if (controlSettings.ipcFile) {
      this.#controlSettings.ipcFile = ipcFileToString(controlSettings.ipcFile);
    }
    this.#socksSettings = { ...socksSettings };
    if (socksSettings.ipcFile) {
      this.#socksSettings.ipcFile = ipcFileToString(socksSettings.ipcFile);
    }
  }

  get status() {
    return this.#status;
  }
@@ -61,7 +88,7 @@ export class TorProcess {

    try {
      this.#makeArgs();
      this.#addControlPortArg();
      this.#addControlPortArgs();
      this.#addSocksPortArg();

      const pid = Services.appinfo.processID;
@@ -69,7 +96,7 @@ export class TorProcess {
        this.#args.push("__OwningControllerProcess", pid.toString());
      }

      if (TorLauncherUtil.shouldShowNetworkSettings) {
      if (lazy.TorLauncherUtil.shouldShowNetworkSettings) {
        this.#args.push("DisableNetwork", "1");
      }

@@ -91,19 +118,21 @@ export class TorProcess {
        command: this.#exeFile.path,
        arguments: this.#args,
        stderr: "stdout",
        workdir: TorLauncherUtil.getTorFile("pt-startup-dir", false).path,
        workdir: lazy.TorLauncherUtil.getTorFile("pt-startup-dir", false).path,
      };
      this.#subprocess = await Subprocess.call(options);
      this.#dumpStdout();
      this.#watchProcess();
      this.#status = TorProcessStatus.Running;
      this.#torProcessStartTime = Date.now();
    } catch (e) {
      this.#status = TorProcessStatus.Exited;
      this.#subprocess = null;
      logger.error("startTor error:", e);
      throw e;
    }

    // Do not await the following functions, as they will return only when the
    // process exits.
    this.#dumpStdout();
    this.#watchProcess();
  }

  // Forget about a process.
@@ -175,16 +204,17 @@ export class TorProcess {
    if (!this.#didConnectToTorControlPort) {
      // tor might be misconfigured, becauser we could never connect to it
      const key = "tor_exited_during_startup";
      s = TorLauncherUtil.getLocalizedString(key);
      s = lazy.TorLauncherUtil.getLocalizedString(key);
    } else {
      // tor exited suddenly, so configuration should be okay
      s =
        TorLauncherUtil.getLocalizedString("tor_exited") +
        lazy.TorLauncherUtil.getLocalizedString("tor_exited") +
        "\n\n" +
        TorLauncherUtil.getLocalizedString("tor_exited2");
        lazy.TorLauncherUtil.getLocalizedString("tor_exited2");
    }
    logger.info(s);
    const defaultBtnLabel = TorLauncherUtil.getLocalizedString("restart_tor");
    const defaultBtnLabel =
      lazy.TorLauncherUtil.getLocalizedString("restart_tor");
    let cancelBtnLabel = "OK";
    try {
      const kSysBundleURI = "chrome://global/locale/commonDialogs.properties";
@@ -194,7 +224,7 @@ export class TorProcess {
      logger.warn("Could not localize the cancel button", e);
    }

    const restart = TorLauncherUtil.showConfirm(
    const restart = lazy.TorLauncherUtil.showConfirm(
      null,
      s,
      defaultBtnLabel,
@@ -212,17 +242,15 @@ export class TorProcess {
  }

  #makeArgs() {
    // Ideally, we would cd to the Firefox application directory before
    // starting tor (but we don't know how to do that). Instead, we
    // rely on the TBB launcher to start Firefox from the right place.

    this.#exeFile = lazy.TorLauncherUtil.getTorFile("tor", false);
    const torrcFile = lazy.TorLauncherUtil.getTorFile("torrc", true);
    // Get the Tor data directory first so it is created before we try to
    // construct paths to files that will be inside it.
    this.#exeFile = TorLauncherUtil.getTorFile("tor", false);
    const torrcFile = TorLauncherUtil.getTorFile("torrc", true);
    this.#dataDir = TorLauncherUtil.getTorFile("tordatadir", true);
    const onionAuthDir = TorLauncherUtil.getTorFile("toronionauthdir", true);
    const hashedPassword = lazy.TorProtocolService.torGetPassword(true);
    this.#dataDir = lazy.TorLauncherUtil.getTorFile("tordatadir", true);
    const onionAuthDir = lazy.TorLauncherUtil.getTorFile(
      "toronionauthdir",
      true
    );
    let detailsKey;
    if (!this.#exeFile) {
      detailsKey = "tor_missing";
@@ -232,13 +260,11 @@ export class TorProcess {
      detailsKey = "datadir_missing";
    } else if (!onionAuthDir) {
      detailsKey = "onionauthdir_missing";
    } else if (!hashedPassword) {
      detailsKey = "password_hash_missing";
    }
    if (detailsKey) {
      const details = TorLauncherUtil.getLocalizedString(detailsKey);
      const details = lazy.TorLauncherUtil.getLocalizedString(detailsKey);
      const key = "unable_to_start_tor";
      const err = TorLauncherUtil.getFormattedLocalizedString(
      const err = lazy.TorLauncherUtil.getFormattedLocalizedString(
        key,
        [details],
        1
@@ -246,7 +272,7 @@ export class TorProcess {
      throw new Error(err);
    }

    const torrcDefaultsFile = TorLauncherUtil.getTorFile(
    const torrcDefaultsFile = lazy.TorLauncherUtil.getTorFile(
      "torrc-defaults",
      false
    );
@@ -265,41 +291,54 @@ export class TorProcess {
    this.#args.push("ClientOnionAuthDir", onionAuthDir.path);
    this.#args.push("GeoIPFile", geoipFile.path);
    this.#args.push("GeoIPv6File", geoip6File.path);
    this.#args.push("HashedControlPassword", hashedPassword);
  }

  #addControlPortArg() {
    // Include a ControlPort argument to support switching between
    // a TCP port and an IPC port (e.g., a Unix domain socket). We
    // include a "+__" prefix so that (1) this control port is added
    // to any control ports that the user has defined in their torrc
    // file and (2) it is never written to torrc.
  /**
   * Add all the arguments related to the control port.
   * We use the + prefix so that the the port is added to any other port already
   * defined in the torrc, and the __ prefix so that it is never written to
   * torrc.
   */
  #addControlPortArgs() {
    if (!this.#controlSettings) {
      return;
    }

    let controlPortArg;
    const controlIPCFile = lazy.TorProtocolService.torGetControlIPCFile();
    const controlPort = lazy.TorProtocolService.torGetControlPort();
    if (controlIPCFile) {
      controlPortArg = this.#ipcPortArg(controlIPCFile);
    } else if (controlPort) {
      controlPortArg = "" + controlPort;
    if (this.#controlSettings.ipcFile) {
      controlPortArg = this.#controlSettings.ipcFile;
    } else if (this.#controlSettings.port) {
      controlPortArg = this.#controlSettings.host
        ? `${this.#controlSettings.host}:${this.#controlSettings.port}`
        : this.#controlSettings.port.toString();
    }
    if (controlPortArg) {
      this.#args.push("+__ControlPort", controlPortArg);
    }

    if (this.#controlSettings.password) {
      this.#args.push("HashedControlPassword", this.#controlSettings.password);
    }
    if (this.#controlSettings.cookieFile) {
      this.#args.push("CookieAuthentication", "1");
      this.#args.push("CookieAuthFile", this.#controlSettings.cookieFile);
    }
  }

  /**
   * Add the argument related to the control port.
   * We use the + prefix so that the the port is added to any other port already
   * defined in the torrc, and the __ prefix so that it is never written to
   * torrc.
   */
  #addSocksPortArg() {
    // Include a SocksPort argument to support switching between
    // a TCP port and an IPC port (e.g., a Unix domain socket). We
    // include a "+__" prefix so that (1) this SOCKS port is added
    // to any SOCKS ports that the user has defined in their torrc
    // file and (2) it is never written to torrc.
    const socksPortInfo = lazy.TorProtocolService.torGetSOCKSPortInfo();
    if (socksPortInfo) {
    let socksPortArg;
      if (socksPortInfo.ipcFile) {
        socksPortArg = this.#ipcPortArg(socksPortInfo.ipcFile);
      } else if (socksPortInfo.host && socksPortInfo.port != 0) {
        socksPortArg = socksPortInfo.host + ":" + socksPortInfo.port;
    if (this.#socksSettings.ipcFile) {
      socksPortArg = this.#socksSettings.ipcFile;
    } else if (this.#socksSettings.port != 0) {
      socksPortArg = this.#socksSettings.host
        ? `${this.#socksSettings.host}:${this.#socksSettings.port}`
        : this.#socksSettings.port.toString();
    }
    if (socksPortArg) {
      const socksPortFlags = Services.prefs.getCharPref(
@@ -313,11 +352,3 @@ export class TorProcess {
    }
  }
}

  // Return a ControlPort or SocksPort argument for aIPCFile (an nsIFile).
  // The result is unix:/path or unix:"/path with spaces" with appropriate
  // C-style escaping within the path portion.
  #ipcPortArg(aIPCFile) {
    return "unix:" + TorParsers.escapeString(aIPCFile.path);
  }
}
+20 −109
Original line number Diff line number Diff line
@@ -306,6 +306,24 @@ export const TorProtocolService = {
    return this._SOCKSPortInfo;
  },

  get torControlPortInfo() {
    const info = {
      password: this._hashPassword(this._controlPassword),
    };
    if (this._controlIPCFile) {
      info.ipcFile = this._controlIPCFile?.clone();
    }
    if (this._controlPort) {
      info.host = this._controlHost;
      info.port = this._controlPort;
    }
    return info;
  },

  get torSOCKSPortInfo() {
    return this._SOCKSPortInfo;
  },

  // Public, but called only internally

  // Executes a command on the control port.
@@ -469,115 +487,8 @@ export const TorProtocolService = {
        this._controlPassword = this._generateRandomPassword();
      }

      // Determine what kind of SOCKS port Tor and the browser will use.
      // On Windows (where Unix domain sockets are not supported), TCP is
      // always used.
      //
      // The following environment variables are supported and take
      // precedence over preferences:
      //    TOR_SOCKS_IPC_PATH  (file system path; ignored on Windows)
      //    TOR_SOCKS_HOST
      //    TOR_SOCKS_PORT
      //
      // The following preferences are consulted:
      //    network.proxy.socks
      //    network.proxy.socks_port
      //    extensions.torlauncher.socks_port_use_ipc (Boolean)
      //    extensions.torlauncher.socks_ipc_path (file system path)
      // If extensions.torlauncher.socks_ipc_path is empty, a default
      // path is used (<tor-data-directory>/socks.socket).
      //
      // When using TCP, if a value is not defined via an env variable it is
      // taken from the corresponding browser preference if possible. The
      // exceptions are:
      //   If network.proxy.socks contains a file: URL, a default value of
      //     "127.0.0.1" is used instead.
      //   If the network.proxy.socks_port value is 0, a default value of
      //     9150 is used instead.
      //
      // Supported scenarios:
      // 1. By default, an IPC object at a default path is used.
      // 2. If extensions.torlauncher.socks_port_use_ipc is set to false,
      //    a TCP socket at 127.0.0.1:9150 is used, unless different values
      //    are set in network.proxy.socks and network.proxy.socks_port.
      // 3. If the TOR_SOCKS_IPC_PATH env var is set, an IPC object at that
      //    path is used (e.g., a Unix domain socket).
      // 4. If the TOR_SOCKS_HOST and/or TOR_SOCKS_PORT env vars are set, TCP
      //    is used. Values not set via env vars will be taken from the
      //    network.proxy.socks and network.proxy.socks_port prefs as described
      //    above.
      // 5. If extensions.torlauncher.socks_port_use_ipc is true and
      //    extensions.torlauncher.socks_ipc_path is set, an IPC object at
      //    the specified path is used.
      // 6. Tor Launcher is disabled. Torbutton will respect the env vars if
      //    present; if not, the values in network.proxy.socks and
      //    network.proxy.socks_port are used without modification.

      let useIPC;
      this._SOCKSPortInfo = { ipcFile: undefined, host: undefined, port: 0 };
      if (!isWindows && Services.env.exists("TOR_SOCKS_IPC_PATH")) {
        let ipcPath = Services.env.get("TOR_SOCKS_IPC_PATH");
        this._SOCKSPortInfo.ipcFile = new lazy.FileUtils.File(ipcPath);
        useIPC = true;
      } else {
        // Check for TCP host and port environment variables.
        if (Services.env.exists("TOR_SOCKS_HOST")) {
          this._SOCKSPortInfo.host = Services.env.get("TOR_SOCKS_HOST");
          useIPC = false;
        }
        if (Services.env.exists("TOR_SOCKS_PORT")) {
          this._SOCKSPortInfo.port = parseInt(
            Services.env.get("TOR_SOCKS_PORT"),
            10
          );
          useIPC = false;
        }
      }

      if (useIPC === undefined) {
        useIPC =
          !isWindows &&
          Services.prefs.getBoolPref(
            "extensions.torlauncher.socks_port_use_ipc",
            false
          );
      }

      // Fill in missing SOCKS info from prefs.
      if (useIPC) {
        if (!this._SOCKSPortInfo.ipcFile) {
          this._SOCKSPortInfo.ipcFile = TorLauncherUtil.getTorFile(
            "socks_ipc",
            false
          );
        }
      } else {
        if (!this._SOCKSPortInfo.host) {
          let socksAddr = Services.prefs.getCharPref(
            "network.proxy.socks",
            "127.0.0.1"
          );
          let socksAddrHasHost = socksAddr && !socksAddr.startsWith("file:");
          this._SOCKSPortInfo.host = socksAddrHasHost ? socksAddr : "127.0.0.1";
        }

        if (!this._SOCKSPortInfo.port) {
          let socksPort = Services.prefs.getIntPref(
            "network.proxy.socks_port",
            0
          );
          // This pref is set as 0 by default in Firefox, use 9150 if we get 0.
          this._SOCKSPortInfo.port = socksPort != 0 ? socksPort : 9150;
        }
      }

      logger.info("SOCKS port type: " + (useIPC ? "IPC" : "TCP"));
      if (useIPC) {
        logger.info(`ipcFile: ${this._SOCKSPortInfo.ipcFile.path}`);
      } else {
        logger.info(`SOCKS host: ${this._SOCKSPortInfo.host}`);
        logger.info(`SOCKS port: ${this._SOCKSPortInfo.port}`);
      }
      this._SOCKSPortInfo = TorLauncherUtil.getPreferredSocksConfiguration();
      TorLauncherUtil.setProxyConfiguration(this._SOCKSPortInfo);

      // Set the global control port info parameters.
      // These values may be overwritten by torbutton when it initializes, but