[tor-commits] [Git][tpo/applications/tor-browser][tor-browser-115.1.0esr-13.0-1] 2 commits: fixup! Bug 40933: Add tor-launcher functionality

Pier Angelo Vendrame (@pierov) git at gitlab.torproject.org
Sat Aug 5 10:07:56 UTC 2023



Pier Angelo Vendrame pushed to branch tor-browser-115.1.0esr-13.0-1 at The Tor Project / Applications / Tor Browser


Commits:
d5283d94 by Pier Angelo Vendrame at 2023-08-04T20:03:25+02:00
fixup! Bug 40933: Add tor-launcher functionality

Make TorProtocolService an ES class, and change _ with actual private
stuff.

- - - - -
ab8a15b7 by Pier Angelo Vendrame at 2023-08-04T20:03:26+02:00
fixup! Bug 40933: Add tor-launcher functionality

Merged TorMonitorService into TorProtocolService.

- - - - -


5 changed files:

- toolkit/components/tor-launcher/TorMonitorService.sys.mjs
- toolkit/components/tor-launcher/TorProtocolService.sys.mjs
- + toolkit/components/tor-launcher/TorProviderBuilder.sys.mjs
- toolkit/components/tor-launcher/TorStartupService.sys.mjs
- toolkit/components/tor-launcher/moz.build


Changes:

=====================================
toolkit/components/tor-launcher/TorMonitorService.sys.mjs
=====================================
@@ -1,73 +1,17 @@
 // Copyright (c) 2022, The Tor Project, Inc.
 
-import { clearTimeout, setTimeout } from "resource://gre/modules/Timer.sys.mjs";
-import { ConsoleAPI } from "resource://gre/modules/Console.sys.mjs";
-
-import {
-  TorParsers,
-  TorStatuses,
-} from "resource://gre/modules/TorParsers.sys.mjs";
-import { TorProcess } from "resource://gre/modules/TorProcess.sys.mjs";
-
-import { TorLauncherUtil } from "resource://gre/modules/TorLauncherUtil.sys.mjs";
+import { TorProviderTopics } from "resource://gre/modules/TorProviderBuilder.sys.mjs";
 
 const lazy = {};
-
-ChromeUtils.defineESModuleGetters(lazy, {
-  TorProtocolService: "resource://gre/modules/TorProtocolService.sys.mjs",
-  controller: "resource://gre/modules/TorControlPort.sys.mjs",
-});
-
 ChromeUtils.defineESModuleGetters(lazy, {
   TorProtocolService: "resource://gre/modules/TorProtocolService.sys.mjs",
 });
 
-const logger = new ConsoleAPI({
-  maxLogLevel: "warn",
-  maxLogLevelPref: "browser.tor_monitor_service.log_level",
-  prefix: "TorMonitorService",
-});
-
-const Preferences = Object.freeze({
-  PromptAtStartup: "extensions.torlauncher.prompt_at_startup",
-});
-
-const TorTopics = Object.freeze({
-  BootstrapError: "TorBootstrapError",
-  HasWarnOrErr: "TorLogHasWarnOrErr",
-  ProcessExited: "TorProcessExited",
-  ProcessIsReady: "TorProcessIsReady",
-  ProcessRestarted: "TorProcessRestarted",
-});
-
 export const TorMonitorTopics = Object.freeze({
-  BridgeChanged: "TorBridgeChanged",
-  StreamSucceeded: "TorStreamSucceeded",
-});
-
-const ControlConnTimings = Object.freeze({
-  initialDelayMS: 25, // Wait 25ms after the process has started, before trying to connect
-  maxRetryMS: 10000, // Retry at most every 10 seconds
-  timeoutMS: 5 * 60 * 1000, // Wait at most 5 minutes for tor to start
+  BridgeChanged: TorProviderTopics.BridgeChanged,
+  StreamSucceeded: TorProviderTopics.StreamSucceeded,
 });
 
-/**
- * From control-spec.txt:
- *   CircuitID = 1*16 IDChar
- *   IDChar = ALPHA / DIGIT
- *   Currently, Tor only uses digits, but this may change.
- *
- * @typedef {string} CircuitID
- */
-/**
- * The fingerprint of a node.
- * From control-spec.txt:
- *   Fingerprint = "$" 40*HEXDIG
- * However, we do not keep the $ in our structures.
- *
- * @typedef {string} NodeFingerprint
- */
-
 /**
  * This service monitors an existing Tor instance, or starts one, if needed, and
  * then starts monitoring it.
@@ -76,575 +20,23 @@ const ControlConnTimings = Object.freeze({
  * status of the bootstrap, the logs, etc...
  */
 export const TorMonitorService = {
-  _connection: null,
-  _eventHandlers: {},
-  _torLog: [], // Array of objects with date, type, and msg properties.
-  _startTimeout: null,
-
-  _isBootstrapDone: false,
-  _lastWarningPhase: null,
-  _lastWarningReason: null,
-
-  _torProcess: null,
-
-  _inited: false,
-
-  /**
-   * Stores the nodes of a circuit. Keys are cicuit IDs, and values are the node
-   * fingerprints.
-   *
-   * Theoretically, we could hook this map up to the new identity notification,
-   * but in practice it does not work. Tor pre-builds circuits, and the NEWNYM
-   * signal does not affect them. So, we might end up using a circuit that was
-   * built before the new identity but not yet used. If we cleaned the map, we
-   * risked of not having the data about it.
-   *
-   * @type {Map<CircuitID, NodeFingerprint[]>}
-   */
-  _circuits: new Map(),
-  /**
-   * The last used bridge, or null if bridges are not in use or if it was not
-   * possible to detect the bridge. This needs the user to have specified bridge
-   * lines with fingerprints to work.
-   *
-   * @type {NodeFingerprint?}
-   */
-  _currentBridge: null,
-
-  // Public methods
-
-  // Starts Tor, if needed, and starts monitoring for events
-  init() {
-    if (this._inited) {
-      return;
-    }
-    this._inited = true;
-
-    // We always liten to these events, because they are needed for the circuit
-    // display.
-    this._eventHandlers = new Map([
-      ["CIRC", this._processCircEvent.bind(this)],
-      ["STREAM", this._processStreamEvent.bind(this)],
-    ]);
-
-    if (this.ownsTorDaemon) {
-      // When we own the tor daemon, we listen to more events, that are used
-      // for about:torconnect or for showing the logs in the settings page.
-      this._eventHandlers.set("STATUS_CLIENT", (_eventType, lines) =>
-        this._processBootstrapStatus(lines[0], false)
-      );
-      this._eventHandlers.set("NOTICE", this._processLog.bind(this));
-      this._eventHandlers.set("WARN", this._processLog.bind(this));
-      this._eventHandlers.set("ERR", this._processLog.bind(this));
-      this._controlTor();
-    } else {
-      this._startEventMonitor();
-    }
-    logger.info("TorMonitorService initialized");
-  },
-
-  // Closes the connection that monitors for events.
-  // When Tor is started by Tor Browser, it is configured to exit when the
-  // control connection is closed. Therefore, as a matter of facts, calling this
-  // function also makes the child Tor instance stop.
-  uninit() {
-    if (this._torProcess) {
-      this._torProcess.forget();
-      this._torProcess.onExit = null;
-      this._torProcess.onRestart = null;
-      this._torProcess = null;
-    }
-    this._shutDownEventMonitor();
-  },
-
-  async retrieveBootstrapStatus() {
-    if (!this._connection) {
-      throw new Error("Event monitor connection not available");
-    }
-
-    // TODO: Unify with TorProtocolService.sendCommand and put everything in the
-    // reviewed torbutton replacement.
-    const cmd = "GETINFO";
-    const key = "status/bootstrap-phase";
-    let reply = await this._connection.sendCommand(`${cmd} ${key}`);
-
-    // A typical reply looks like:
-    //  250-status/bootstrap-phase=NOTICE BOOTSTRAP PROGRESS=100 TAG=done SUMMARY="Done"
-    //  250 OK
-    reply = TorParsers.parseCommandResponse(reply);
-    if (!TorParsers.commandSucceeded(reply)) {
-      throw new Error(`${cmd} failed`);
-    }
-    reply = TorParsers.parseReply(cmd, key, reply);
-    if (reply.length) {
-      this._processBootstrapStatus(reply[0], true);
-    }
-  },
-
-  // Returns captured log message as a text string (one message per line).
-  getLog() {
-    return this._torLog
-      .map(logObj => {
-        const timeStr = logObj.date
-          .toISOString()
-          .replace("T", " ")
-          .replace("Z", "");
-        return `${timeStr} [${logObj.type}] ${logObj.msg}`;
-      })
-      .join(TorLauncherUtil.isWindows ? "\r\n" : "\n");
+  get currentBridge() {
+    return lazy.TorProtocolService.currentBridge;
   },
 
-  // true if we launched and control tor, false if using system tor
   get ownsTorDaemon() {
-    return TorLauncherUtil.shouldStartAndOwnTor;
-  },
-
-  get isBootstrapDone() {
-    return this._isBootstrapDone;
-  },
-
-  clearBootstrapError() {
-    this._lastWarningPhase = null;
-    this._lastWarningReason = null;
+    return lazy.TorProtocolService.ownsTorDaemon;
   },
 
   get isRunning() {
-    return !!this._connection;
+    return lazy.TorProtocolService.isRunning;
   },
 
-  /**
-   * Return the data about the current bridge, if any, or null.
-   * We can detect bridge only when the configured bridge lines include the
-   * fingerprints.
-   *
-   * @returns {NodeData?} The node information, or null if the first node
-   * is not a bridge, or no circuit has been opened, yet.
-   */
-  get currentBridge() {
-    return this._currentBridge;
-  },
-
-  // Private methods
-
-  async _startProcess() {
-    // 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(
-        lazy.TorProtocolService.torControlPortInfo,
-        lazy.TorProtocolService.torSOCKSPortInfo
-      );
-      this._torProcess.onExit = () => {
-        this._shutDownEventMonitor();
-        Services.obs.notifyObservers(null, TorTopics.ProcessExited);
-      };
-      this._torProcess.onRestart = async () => {
-        this._shutDownEventMonitor();
-        await this._controlTor();
-        Services.obs.notifyObservers(null, TorTopics.ProcessRestarted);
-      };
-    }
-
-    // Already running, but we did not start it
-    if (this._torProcess.isRunning) {
-      return false;
-    }
-
-    try {
-      await this._torProcess.start();
-      if (this._torProcess.isRunning) {
-        logger.info("tor started");
-        this._torProcessStartTime = Date.now();
-      }
-    } catch (e) {
-      // TorProcess already logs the error.
-      this._lastWarningPhase = "startup";
-      this._lastWarningReason = e.toString();
-    }
-    return this._torProcess.isRunning;
-  },
-
-  async _controlTor() {
-    if (!this._torProcess?.isRunning && !(await this._startProcess())) {
-      logger.error("Tor not running, not starting to monitor it.");
-      return;
-    }
-
-    let delayMS = ControlConnTimings.initialDelayMS;
-    const callback = async () => {
-      if (await this._startEventMonitor()) {
-        this.retrieveBootstrapStatus().catch(e => {
-          logger.warn("Could not get the initial bootstrap status", e);
-        });
-
-        // FIXME: TorProcess is misleading here. We should use a topic related
-        // to having a control port connection, instead.
-        logger.info(`Notifying ${TorTopics.ProcessIsReady}`);
-        Services.obs.notifyObservers(null, TorTopics.ProcessIsReady);
-
-        // We reset this here hoping that _shutDownEventMonitor can interrupt
-        // the current monitor, either by calling clearTimeout and preventing it
-        // from starting, or by closing the control port connection.
-        if (this._startTimeout === null) {
-          logger.warn("Someone else reset _startTimeout!");
-        }
-        this._startTimeout = null;
-      } else if (
-        Date.now() - this._torProcessStartTime >
-        ControlConnTimings.timeoutMS
-      ) {
-        let s = TorLauncherUtil.getLocalizedString("tor_controlconn_failed");
-        this._lastWarningPhase = "startup";
-        this._lastWarningReason = s;
-        logger.info(s);
-        if (this._startTimeout === null) {
-          logger.warn("Someone else reset _startTimeout!");
-        }
-        this._startTimeout = null;
-      } else {
-        delayMS *= 2;
-        if (delayMS > ControlConnTimings.maxRetryMS) {
-          delayMS = ControlConnTimings.maxRetryMS;
-        }
-        this._startTimeout = setTimeout(() => {
-          logger.debug(`Control port not ready, waiting ${delayMS / 1000}s.`);
-          callback();
-        }, delayMS);
-      }
-    };
-    // Check again, in the unfortunate case in which the execution was alrady
-    // queued, but was waiting network code.
-    if (this._startTimeout === null) {
-      this._startTimeout = setTimeout(callback, delayMS);
-    } else {
-      logger.error("Possible race? Refusing to start the timeout again");
-    }
-  },
-
-  async _startEventMonitor() {
-    if (this._connection) {
-      return true;
-    }
-
-    let conn;
-    try {
-      conn = await lazy.controller();
-    } catch (e) {
-      logger.error("Cannot open a control port connection", e);
-      if (conn) {
-        try {
-          conn.close();
-        } catch (e) {
-          logger.error(
-            "Also, the connection is not null but cannot be closed",
-            e
-          );
-        }
-      }
-      return false;
-    }
-
-    // TODO: optionally monitor INFO and DEBUG log messages.
-    try {
-      await conn.setEvents(Array.from(this._eventHandlers.keys()));
-    } catch (e) {
-      logger.error("SETEVENTS failed", e);
-      conn.close();
-      return false;
-    }
-
-    if (this._torProcess) {
-      this._torProcess.connectionWorked();
-    }
-    if (this.ownsTorDaemon && !TorLauncherUtil.shouldOnlyConfigureTor) {
-      try {
-        await this._takeTorOwnership(conn);
-      } catch (e) {
-        logger.warn("Could not take ownership of the Tor daemon", e);
-      }
-    }
-
-    this._connection = conn;
-
-    for (const [type, callback] of this._eventHandlers.entries()) {
-      this._monitorEvent(type, callback);
-    }
-
-    // Populate the circuit map already, in case we are connecting to an
-    // external tor daemon.
-    try {
-      const reply = await this._connection.sendCommand(
-        "GETINFO circuit-status"
-      );
-      const lines = reply.split(/\r?\n/);
-      if (lines.shift() === "250+circuit-status=") {
-        for (const line of lines) {
-          if (line === ".") {
-            break;
-          }
-          // _processCircEvent processes only one line at a time
-          this._processCircEvent("CIRC", [line]);
-        }
-      }
-    } catch (e) {
-      logger.warn("Could not populate the initial circuit map", e);
-    }
-
-    return true;
-  },
-
-  // Try to become the primary controller (TAKEOWNERSHIP).
-  async _takeTorOwnership(conn) {
-    try {
-      conn.takeOwnership();
-    } catch (e) {
-      logger.warn("Take ownership failed", e);
-      return;
-    }
-    try {
-      conn.resetOwningControllerProcess();
-    } catch (e) {
-      logger.warn("Clear owning controller process failed", e);
-    }
-  },
-
-  _monitorEvent(type, callback) {
-    logger.info(`Watching events of type ${type}.`);
-    let replyObj = {};
-    this._connection.watchEvent(
-      type,
-      null,
-      line => {
-        if (!line) {
-          return;
-        }
-        logger.debug("Event response: ", line);
-        const isComplete = TorParsers.parseReplyLine(line, replyObj);
-        if (!isComplete || replyObj._parseError || !replyObj.lineArray.length) {
-          return;
-        }
-        const reply = replyObj;
-        replyObj = {};
-        if (reply.statusCode !== TorStatuses.EventNotification) {
-          logger.error("Unexpected event status code:", reply.statusCode);
-          return;
-        }
-        if (!reply.lineArray[0].startsWith(`${type} `)) {
-          logger.error("Wrong format for the first line:", reply.lineArray[0]);
-          return;
-        }
-        reply.lineArray[0] = reply.lineArray[0].substring(type.length + 1);
-        try {
-          callback(type, reply.lineArray);
-        } catch (e) {
-          logger.error("Exception while handling an event", reply, e);
-        }
-      },
-      true
-    );
-  },
-
-  _processLog(type, lines) {
-    if (type === "WARN" || type === "ERR") {
-      // Notify so that Copy Log can be enabled.
-      Services.obs.notifyObservers(null, TorTopics.HasWarnOrErr);
-    }
-
-    const date = new Date();
-    const maxEntries = Services.prefs.getIntPref(
-      "extensions.torlauncher.max_tor_log_entries",
-      1000
-    );
-    if (maxEntries > 0 && this._torLog.length >= maxEntries) {
-      this._torLog.splice(0, 1);
-    }
-
-    const msg = lines.join("\n");
-    this._torLog.push({ date, type, msg });
-    const logString = `Tor ${type}: ${msg}`;
-    logger.info(logString);
-  },
-
-  // Process a bootstrap status to update the current state, and broadcast it
-  // to TorBootstrapStatus observers.
-  // If aSuppressErrors is true, errors are ignored. This is used when we
-  // are handling the response to a "GETINFO status/bootstrap-phase" command.
-  _processBootstrapStatus(aStatusMsg, aSuppressErrors) {
-    const statusObj = TorParsers.parseBootstrapStatus(aStatusMsg);
-    if (!statusObj) {
-      return;
-    }
-
-    // Notify observers
-    statusObj.wrappedJSObject = statusObj;
-    Services.obs.notifyObservers(statusObj, "TorBootstrapStatus");
-
-    if (statusObj.PROGRESS === 100) {
-      this._isBootstrapDone = true;
-      try {
-        Services.prefs.setBoolPref(Preferences.PromptAtStartup, false);
-      } catch (e) {
-        logger.warn(`Cannot set ${Preferences.PromptAtStartup}`, e);
-      }
-      return;
-    }
-
-    this._isBootstrapDone = false;
-
-    if (
-      statusObj.TYPE === "WARN" &&
-      statusObj.RECOMMENDATION !== "ignore" &&
-      !aSuppressErrors
-    ) {
-      this._notifyBootstrapError(statusObj);
-    }
-  },
-
-  _notifyBootstrapError(statusObj) {
-    try {
-      Services.prefs.setBoolPref(Preferences.PromptAtStartup, true);
-    } catch (e) {
-      logger.warn(`Cannot set ${Preferences.PromptAtStartup}`, e);
-    }
-    const phase = TorLauncherUtil.getLocalizedBootstrapStatus(statusObj, "TAG");
-    const reason = TorLauncherUtil.getLocalizedBootstrapStatus(
-      statusObj,
-      "REASON"
-    );
-    const details = TorLauncherUtil.getFormattedLocalizedString(
-      "tor_bootstrap_failed_details",
-      [phase, reason],
-      2
-    );
-    logger.error(
-      `Tor bootstrap error: [${statusObj.TAG}/${statusObj.REASON}] ${details}`
-    );
-
-    if (
-      statusObj.TAG !== this._lastWarningPhase ||
-      statusObj.REASON !== this._lastWarningReason
-    ) {
-      this._lastWarningPhase = statusObj.TAG;
-      this._lastWarningReason = statusObj.REASON;
-
-      const message = TorLauncherUtil.getLocalizedString(
-        "tor_bootstrap_failed"
-      );
-      Services.obs.notifyObservers(
-        { message, details },
-        TorTopics.BootstrapError
-      );
-    }
-  },
-
-  async _processCircEvent(_type, lines) {
-    const builtEvent =
-      /^(?<CircuitID>[a-zA-Z0-9]{1,16})\sBUILT\s(?<Path>(?:,?\$[0-9a-fA-F]{40}(?:~[a-zA-Z0-9]{1,19})?)+)/.exec(
-        lines[0]
-      );
-    const closedEvent = /^(?<ID>[a-zA-Z0-9]{1,16})\sCLOSED/.exec(lines[0]);
-    if (builtEvent) {
-      const fp = /\$([0-9a-fA-F]{40})/g;
-      const nodes = Array.from(builtEvent.groups.Path.matchAll(fp), g =>
-        g[1].toUpperCase()
-      );
-      this._circuits.set(builtEvent.groups.CircuitID, nodes);
-      // Ignore circuits of length 1, that are used, for example, to probe
-      // bridges. So, only store them, since we might see streams that use them,
-      // but then early-return.
-      if (nodes.length === 1) {
-        return;
-      }
-      // In some cases, we might already receive SOCKS credentials in the line.
-      // However, this might be a problem with onion services: we get also a
-      // 4-hop circuit that we likely do not want to show to the user,
-      // especially because it is used only temporarily, and it would need a
-      // technical explaination.
-      // this._checkCredentials(lines[0], nodes);
-      if (this._currentBridge?.fingerprint !== nodes[0]) {
-        const nodeInfo = await lazy.TorProtocolService.getNodeInfo(nodes[0]);
-        let notify = false;
-        if (nodeInfo?.bridgeType) {
-          logger.info(`Bridge changed to ${nodes[0]}`);
-          this._currentBridge = nodeInfo;
-          notify = true;
-        } else if (this._currentBridge) {
-          logger.info("Bridges disabled");
-          this._currentBridge = null;
-          notify = true;
-        }
-        if (notify) {
-          Services.obs.notifyObservers(
-            null,
-            TorMonitorTopics.BridgeChanged,
-            this._currentBridge
-          );
-        }
-      }
-    } else if (closedEvent) {
-      this._circuits.delete(closedEvent.groups.ID);
-    }
-  },
-
-  _processStreamEvent(_type, lines) {
-    // The first block is the stream ID, which we do not need at the moment.
-    const succeeedEvent =
-      /^[a-zA-Z0-9]{1,16}\sSUCCEEDED\s(?<CircuitID>[a-zA-Z0-9]{1,16})/.exec(
-        lines[0]
-      );
-    if (!succeeedEvent) {
-      return;
-    }
-    const circuit = this._circuits.get(succeeedEvent.groups.CircuitID);
-    if (!circuit) {
-      logger.error(
-        "Seen a STREAM SUCCEEDED with an unknown circuit. Not notifying observers.",
-        lines[0]
-      );
-      return;
-    }
-    this._checkCredentials(lines[0], circuit);
-  },
-
-  /**
-   * Check if a STREAM or CIRC response line contains SOCKS_USERNAME and
-   * SOCKS_PASSWORD. In case, notify observers that we could associate a certain
-   * circuit to these credentials.
-   *
-   * @param {string} line The circ or stream line to check
-   * @param {NodeFingerprint[]} circuit The fingerprints of the nodes in the
-   * circuit.
-   */
-  _checkCredentials(line, circuit) {
-    const username = /SOCKS_USERNAME=("(?:[^"\\]|\\.)*")/.exec(line);
-    const password = /SOCKS_PASSWORD=("(?:[^"\\]|\\.)*")/.exec(line);
-    if (!username || !password) {
-      return;
-    }
-    Services.obs.notifyObservers(
-      {
-        wrappedJSObject: {
-          username: TorParsers.unescapeString(username[1]),
-          password: TorParsers.unescapeString(password[1]),
-          circuit,
-        },
-      },
-      TorMonitorTopics.StreamSucceeded
-    );
+  get isBootstrapDone() {
+    return lazy.TorProtocolService.isBootstrapDone;
   },
 
-  _shutDownEventMonitor() {
-    try {
-      this._connection?.close();
-    } catch (e) {
-      logger.error("Could not close the connection to the control port", e);
-    }
-    this._connection = null;
-    if (this._startTimeout !== null) {
-      clearTimeout(this._startTimeout);
-      this._startTimeout = null;
-    }
-    this._isBootstrapDone = false;
-    this.clearBootstrapError();
+  getLog() {
+    return lazy.TorProtocolService.getLog();
   },
 };


=====================================
toolkit/components/tor-launcher/TorProtocolService.sys.mjs
=====================================
@@ -1,32 +1,22 @@
 // Copyright (c) 2021, The Tor Project, Inc.
 
-import { setTimeout } from "resource://gre/modules/Timer.sys.mjs";
+import { setTimeout, clearTimeout } from "resource://gre/modules/Timer.sys.mjs";
 import { ConsoleAPI } from "resource://gre/modules/Console.sys.mjs";
 
-import { TorParsers } from "resource://gre/modules/TorParsers.sys.mjs";
 import { TorLauncherUtil } from "resource://gre/modules/TorLauncherUtil.sys.mjs";
+import {
+  TorParsers,
+  TorStatuses,
+} from "resource://gre/modules/TorParsers.sys.mjs";
+import { TorProviderTopics } from "resource://gre/modules/TorProviderBuilder.sys.mjs";
 
 const lazy = {};
 
-ChromeUtils.defineModuleGetter(
-  lazy,
-  "FileUtils",
-  "resource://gre/modules/FileUtils.jsm"
-);
-
-ChromeUtils.defineModuleGetter(
-  lazy,
-  "TorMonitorService",
-  "resource://gre/modules/TorMonitorService.jsm"
-);
 ChromeUtils.defineESModuleGetters(lazy, {
   controller: "resource://gre/modules/TorControlPort.sys.mjs",
   configureControlPortModule: "resource://gre/modules/TorControlPort.sys.mjs",
-});
-
-const TorTopics = Object.freeze({
-  ProcessExited: "TorProcessExited",
-  ProcessRestarted: "TorProcessRestarted",
+  FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
+  TorProcess: "resource://gre/modules/TorProcess.sys.mjs",
 });
 
 const logger = new ConsoleAPI({
@@ -34,11 +24,27 @@ const logger = new ConsoleAPI({
   prefix: "TorProtocolService",
 });
 
+/**
+ * From control-spec.txt:
+ *   CircuitID = 1*16 IDChar
+ *   IDChar = ALPHA / DIGIT
+ *   Currently, Tor only uses digits, but this may change.
+ *
+ * @typedef {string} CircuitID
+ */
+/**
+ * The fingerprint of a node.
+ * From control-spec.txt:
+ *   Fingerprint = "$" 40*HEXDIG
+ * However, we do not keep the $ in our structures.
+ *
+ * @typedef {string} NodeFingerprint
+ */
 /**
  * Stores the data associated with a circuit node.
  *
  * @typedef NodeData
- * @property {string} fingerprint The node fingerprint.
+ * @property {NodeFingerprint} fingerprint The node fingerprint.
  * @property {string[]} ipAddrs - The ip addresses associated with this node.
  * @property {string?} bridgeType - The bridge type for this node, or "" if the
  *   node is a bridge but the type is unknown, or null if this is not a bridge
@@ -48,60 +54,71 @@ const logger = new ConsoleAPI({
  *   valid BCP47 Region subtag.
  */
 
-// Manage the connection to tor's control port, to update its settings and query
-// other useful information.
-//
-// NOTE: Many Tor protocol functions return a reply object, which is a
-// a JavaScript object that has the following fields:
-//   reply.statusCode  -- integer, e.g., 250
-//   reply.lineArray   -- an array of strings returned by tor
-// For GetConf calls, the aKey prefix is removed from the lineArray strings.
-export const TorProtocolService = {
-  _inited: false,
+const Preferences = Object.freeze({
+  PromptAtStartup: "extensions.torlauncher.prompt_at_startup",
+});
+
+const ControlConnTimings = Object.freeze({
+  initialDelayMS: 25, // Wait 25ms after the process has started, before trying to connect
+  maxRetryMS: 10000, // Retry at most every 10 seconds
+  timeoutMS: 5 * 60 * 1000, // Wait at most 5 minutes for tor to start
+});
+
+/**
+ * This is a Tor provider for the C Tor daemon.
+ *
+ * It can start a new tor instance, or connect to an existing one.
+ * In the former case, it also takes its ownership by default.
+ */
+class TorProvider {
+  #inited = false;
 
   // Maintain a map of tor settings set by Tor Browser so that we don't
   // repeatedly set the same key/values over and over.
   // This map contains string keys to primitives or array values.
-  _settingsCache: new Map(),
+  #settingsCache = new Map();
 
-  _controlPort: null,
-  _controlHost: null,
-  _controlIPCFile: null, // An nsIFile if using IPC for control port.
-  _controlPassword: null, // JS string that contains hex-encoded password.
-  _SOCKSPortInfo: null, // An object that contains ipcFile, host, port.
+  #controlPort = null;
+  #controlHost = null;
+  #controlIPCFile = null; // An nsIFile if using IPC for control port.
+  #controlPassword = null; // JS string that contains hex-encoded password.
+  #SOCKSPortInfo = null; // An object that contains ipcFile, host, port.
 
-  _controlConnection: null, // This is cached and reused.
-  _connectionQueue: [],
+  #controlConnection = null; // This is cached and reused.
+  #connectionQueue = [];
 
   // Public methods
 
   async init() {
-    if (this._inited) {
+    if (this.#inited) {
       return;
     }
-    this._inited = true;
+    this.#inited = true;
+
+    Services.obs.addObserver(this, TorProviderTopics.ProcessExited);
+    Services.obs.addObserver(this, TorProviderTopics.ProcessRestarted);
 
-    Services.obs.addObserver(this, TorTopics.ProcessExited);
-    Services.obs.addObserver(this, TorTopics.ProcessRestarted);
+    await this.#setSockets();
 
-    await this._setSockets();
+    this._monitorInit();
 
-    logger.debug("TorProtocolService initialized");
-  },
+    logger.debug("TorProvider initialized");
+  }
 
   uninit() {
-    Services.obs.removeObserver(this, TorTopics.ProcessExited);
-    Services.obs.removeObserver(this, TorTopics.ProcessRestarted);
-    this._closeConnection();
-  },
+    Services.obs.removeObserver(this, TorProviderTopics.ProcessExited);
+    Services.obs.removeObserver(this, TorProviderTopics.ProcessRestarted);
+    this.#closeConnection();
+    this._monitorUninit();
+  }
 
   observe(subject, topic, data) {
-    if (topic === TorTopics.ProcessExited) {
-      this._closeConnection();
-    } else if (topic === TorTopics.ProcessRestarted) {
-      this._reconnect();
+    if (topic === TorProviderTopics.ProcessExited) {
+      this.#closeConnection();
+    } else if (topic === TorProviderTopics.ProcessRestarted) {
+      this.#reconnect();
     }
-  },
+  }
 
   // takes a Map containing tor settings
   // throws on error
@@ -109,14 +126,14 @@ export const TorProtocolService = {
     // only write settings that have changed
     const newSettings = Array.from(aSettingsObj).filter(([setting, value]) => {
       // make sure we have valid data here
-      this._assertValidSetting(setting, value);
+      this.#assertValidSetting(setting, value);
 
-      if (!this._settingsCache.has(setting)) {
+      if (!this.#settingsCache.has(setting)) {
         // no cached setting, so write
         return true;
       }
 
-      const cachedValue = this._settingsCache.get(setting);
+      const cachedValue = this.#settingsCache.get(setting);
       if (value === cachedValue) {
         return false;
       } else if (Array.isArray(value) && Array.isArray(cachedValue)) {
@@ -142,21 +159,21 @@ export const TorProtocolService = {
 
       // save settings to cache after successfully writing to Tor
       for (const [setting, value] of newSettings) {
-        this._settingsCache.set(setting, value);
+        this.#settingsCache.set(setting, value);
       }
     }
-  },
+  }
 
   async readStringArraySetting(aSetting) {
-    const value = await this._readSetting(aSetting);
-    this._settingsCache.set(aSetting, value);
+    const value = await this.#readSetting(aSetting);
+    this.#settingsCache.set(aSetting, value);
     return value;
-  },
+  }
 
   // writes current tor settings to disk
   async flushSettings() {
     await this.sendCommand("SAVECONF");
-  },
+  }
 
   async connect() {
     const kTorConfKeyDisableNetwork = "DisableNetwork";
@@ -164,9 +181,9 @@ export const TorProtocolService = {
     settings[kTorConfKeyDisableNetwork] = false;
     await this.setConfWithReply(settings);
     await this.sendCommand("SAVECONF");
-    lazy.TorMonitorService.clearBootstrapError();
-    lazy.TorMonitorService.retrieveBootstrapStatus();
-  },
+    this.clearBootstrapError();
+    this.retrieveBootstrapStatus();
+  }
 
   async stopBootstrap() {
     // Tell tor to disable use of the network; this should stop the bootstrap
@@ -180,12 +197,12 @@ export const TorProtocolService = {
     // We are not interested in waiting for this, nor in **catching its error**,
     // so we do not await this. We just want to be notified when the bootstrap
     // status is actually updated through observers.
-    lazy.TorMonitorService.retrieveBootstrapStatus();
-  },
+    this.retrieveBootstrapStatus();
+  }
 
   async newnym() {
     return this.sendCommand("SIGNAL NEWNYM");
-  },
+  }
 
   // Ask tor which ports it is listening to for SOCKS connections.
   // At the moment this is used only in TorCheckService.
@@ -194,7 +211,7 @@ export const TorProtocolService = {
     const keyword = "net/listeners/socks";
     const response = await this.sendCommand(cmd, keyword);
     return TorParsers.parseReply(cmd, keyword, response);
-  },
+  }
 
   async getBridges() {
     // Ideally, we would not need this function, because we should be the one
@@ -203,19 +220,19 @@ export const TorProtocolService = {
     // is the most reliable way of getting the configured bridges, at the
     // moment. Also, we are using this for the circuit display, which should
     // work also when we are not configuring the tor daemon, but just using it.
-    return this._withConnection(conn => {
+    return this.#withConnection(conn => {
       return conn.getConf("bridge");
     });
-  },
+  }
 
   /**
    * Returns tha data about a relay or a bridge.
    *
    * @param {string} id The fingerprint of the node to get data about
-   * @returns {NodeData}
+   * @returns {Promise<NodeData>}
    */
   async getNodeInfo(id) {
-    return this._withConnection(async conn => {
+    return this.#withConnection(async conn => {
       const node = {
         fingerprint: id,
         ipAddrs: [],
@@ -259,62 +276,62 @@ export const TorProtocolService = {
       }
       return node;
     });
-  },
+  }
 
   async onionAuthAdd(hsAddress, b64PrivateKey, isPermanent) {
-    return this._withConnection(conn => {
+    return this.#withConnection(conn => {
       return conn.onionAuthAdd(hsAddress, b64PrivateKey, isPermanent);
     });
-  },
+  }
 
   async onionAuthRemove(hsAddress) {
-    return this._withConnection(conn => {
+    return this.#withConnection(conn => {
       return conn.onionAuthRemove(hsAddress);
     });
-  },
+  }
 
   async onionAuthViewKeys() {
-    return this._withConnection(conn => {
+    return this.#withConnection(conn => {
       return conn.onionAuthViewKeys();
     });
-  },
+  }
 
   // TODO: transform the following 4 functions in getters.
 
   // Returns Tor password string or null if an error occurs.
   torGetPassword() {
-    return this._controlPassword;
-  },
+    return this.#controlPassword;
+  }
 
   torGetControlIPCFile() {
-    return this._controlIPCFile?.clone();
-  },
+    return this.#controlIPCFile?.clone();
+  }
 
   torGetControlPort() {
-    return this._controlPort;
-  },
+    return this.#controlPort;
+  }
 
   torGetSOCKSPortInfo() {
-    return this._SOCKSPortInfo;
-  },
+    return this.#SOCKSPortInfo;
+  }
 
   get torControlPortInfo() {
     const info = {
-      password: this._controlPassword,
+      password: this.#controlPassword,
     };
-    if (this._controlIPCFile) {
-      info.ipcFile = this._controlIPCFile?.clone();
+    if (this.#controlIPCFile) {
+      info.ipcFile = this.#controlIPCFile?.clone();
     }
-    if (this._controlPort) {
-      info.host = this._controlHost;
-      info.port = this._controlPort;
+    if (this.#controlPort) {
+      info.host = this.#controlHost;
+      info.port = this.#controlPort;
     }
     return info;
-  },
+  }
 
   get torSOCKSPortInfo() {
-    return this._SOCKSPortInfo;
-  },
+    return this.#SOCKSPortInfo;
+  }
 
   // Public, but called only internally
 
@@ -326,7 +343,7 @@ export const TorProtocolService = {
     let timeout = 250;
     let reply;
     while (leftConnAttempts-- > 0) {
-      const response = await this._trySend(cmd, args, leftConnAttempts == 0);
+      const response = await this.#trySend(cmd, args, leftConnAttempts === 0);
       if (response.connected) {
         reply = response.reply;
         break;
@@ -360,7 +377,7 @@ export const TorProtocolService = {
     }
 
     return reply;
-  },
+  }
 
   // Perform a SETCONF command.
   // aSettingsObj should be a JavaScript object with keys (property values)
@@ -398,39 +415,39 @@ export const TorProtocolService = {
     }
 
     await this.sendCommand("SETCONF", args.join(" "));
-  },
+  }
 
   // Public, never called?
 
   async readBoolSetting(aSetting) {
-    let value = await this._readBoolSetting(aSetting);
-    this._settingsCache.set(aSetting, value);
+    let value = await this.#readBoolSetting(aSetting);
+    this.#settingsCache.set(aSetting, value);
     return value;
-  },
+  }
 
   async readStringSetting(aSetting) {
-    let value = await this._readStringSetting(aSetting);
-    this._settingsCache.set(aSetting, value);
+    let value = await this.#readStringSetting(aSetting);
+    this.#settingsCache.set(aSetting, value);
     return value;
-  },
+  }
 
   // Private
 
-  async _setSockets() {
+  async #setSockets() {
     try {
       const isWindows = TorLauncherUtil.isWindows;
       // Determine how Tor Launcher will connect to the Tor control port.
       // Environment variables get top priority followed by preferences.
       if (!isWindows && Services.env.exists("TOR_CONTROL_IPC_PATH")) {
         const ipcPath = Services.env.get("TOR_CONTROL_IPC_PATH");
-        this._controlIPCFile = new lazy.FileUtils.File(ipcPath);
+        this.#controlIPCFile = new lazy.FileUtils.File(ipcPath);
       } else {
         // Check for TCP host and port environment variables.
         if (Services.env.exists("TOR_CONTROL_HOST")) {
-          this._controlHost = Services.env.get("TOR_CONTROL_HOST");
+          this.#controlHost = Services.env.get("TOR_CONTROL_HOST");
         }
         if (Services.env.exists("TOR_CONTROL_PORT")) {
-          this._controlPort = parseInt(
+          this.#controlPort = parseInt(
             Services.env.get("TOR_CONTROL_PORT"),
             10
           );
@@ -442,20 +459,20 @@ export const TorProtocolService = {
             "extensions.torlauncher.control_port_use_ipc",
             false
           );
-        if (!this._controlHost && !this._controlPort && useIPC) {
-          this._controlIPCFile = TorLauncherUtil.getTorFile(
+        if (!this.#controlHost && !this.#controlPort && useIPC) {
+          this.#controlIPCFile = TorLauncherUtil.getTorFile(
             "control_ipc",
             false
           );
         } else {
-          if (!this._controlHost) {
-            this._controlHost = Services.prefs.getCharPref(
+          if (!this.#controlHost) {
+            this.#controlHost = Services.prefs.getCharPref(
               "extensions.torlauncher.control_host",
               "127.0.0.1"
             );
           }
-          if (!this._controlPort) {
-            this._controlPort = Services.prefs.getIntPref(
+          if (!this.#controlPort) {
+            this.#controlPort = Services.prefs.getIntPref(
               "extensions.torlauncher.control_port",
               9151
             );
@@ -465,46 +482,46 @@ export const TorProtocolService = {
 
       // Populate _controlPassword so it is available when starting tor.
       if (Services.env.exists("TOR_CONTROL_PASSWD")) {
-        this._controlPassword = Services.env.get("TOR_CONTROL_PASSWD");
+        this.#controlPassword = Services.env.get("TOR_CONTROL_PASSWD");
       } else if (Services.env.exists("TOR_CONTROL_COOKIE_AUTH_FILE")) {
         // TODO: test this code path (TOR_CONTROL_COOKIE_AUTH_FILE).
         const cookiePath = Services.env.get("TOR_CONTROL_COOKIE_AUTH_FILE");
         if (cookiePath) {
-          this._controlPassword = await this._readAuthenticationCookie(
+          this.#controlPassword = await this.#readAuthenticationCookie(
             cookiePath
           );
         }
       }
-      if (!this._controlPassword) {
-        this._controlPassword = this._generateRandomPassword();
+      if (!this.#controlPassword) {
+        this.#controlPassword = this.#generateRandomPassword();
       }
 
-      this._SOCKSPortInfo = TorLauncherUtil.getPreferredSocksConfiguration();
-      TorLauncherUtil.setProxyConfiguration(this._SOCKSPortInfo);
+      this.#SOCKSPortInfo = TorLauncherUtil.getPreferredSocksConfiguration();
+      TorLauncherUtil.setProxyConfiguration(this.#SOCKSPortInfo);
 
       // Set the global control port info parameters.
       lazy.configureControlPortModule(
-        this._controlIPCFile,
-        this._controlHost,
-        this._controlPort,
-        this._controlPassword
+        this.#controlIPCFile,
+        this.#controlHost,
+        this.#controlPort,
+        this.#controlPassword
       );
     } catch (e) {
       logger.error("Failed to get environment variables", e);
     }
-  },
+  }
 
-  _assertValidSettingKey(aSetting) {
+  #assertValidSettingKey(aSetting) {
     // ensure the 'key' is a string
     if (typeof aSetting !== "string") {
       throw new Error(
         `Expected setting of type string but received ${typeof aSetting}`
       );
     }
-  },
+  }
 
-  _assertValidSetting(aSetting, aValue) {
-    this._assertValidSettingKey(aSetting);
+  #assertValidSetting(aSetting, aValue) {
+    this.#assertValidSettingKey(aSetting);
     switch (typeof aValue) {
       case "boolean":
       case "string":
@@ -528,29 +545,29 @@ export const TorProtocolService = {
           `Invalid object type received for setting '${aSetting}'`
         );
     }
-  },
+  }
 
   // Perform a GETCONF command.
-  async _readSetting(aSetting) {
-    this._assertValidSettingKey(aSetting);
+  async #readSetting(aSetting) {
+    this.#assertValidSettingKey(aSetting);
 
     const cmd = "GETCONF";
     let reply = await this.sendCommand(cmd, aSetting);
     return TorParsers.parseReply(cmd, aSetting, reply);
-  },
+  }
 
-  async _readStringSetting(aSetting) {
-    let lineArray = await this._readSetting(aSetting);
+  async #readStringSetting(aSetting) {
+    let lineArray = await this.#readSetting(aSetting);
     if (lineArray.length !== 1) {
       throw new Error(
         `Expected an array with length 1 but received array of length ${lineArray.length}`
       );
     }
     return lineArray[0];
-  },
+  }
 
-  async _readBoolSetting(aSetting) {
-    const value = this._readStringSetting(aSetting);
+  async #readBoolSetting(aSetting) {
+    const value = this.#readStringSetting(aSetting);
     switch (value) {
       case "0":
         return false;
@@ -559,16 +576,16 @@ export const TorProtocolService = {
       default:
         throw new Error(`Expected boolean (1 or 0) but received '${value}'`);
     }
-  },
+  }
 
-  async _trySend(cmd, args, rethrow) {
+  async #trySend(cmd, args, rethrow) {
     let connected = false;
     let reply;
     let leftAttempts = 2;
     while (leftAttempts-- > 0) {
       let conn;
       try {
-        conn = await this._getConnection();
+        conn = await this.#getConnection();
       } catch (e) {
         logger.error("Cannot get a connection to the control port", e);
         if (leftAttempts == 0 && rethrow) {
@@ -584,105 +601,105 @@ export const TorProtocolService = {
         reply = await conn.sendCommand(cmd + (args ? " " + args : ""));
         if (reply) {
           // Return for reuse.
-          this._returnConnection();
+          this.#returnConnection();
         } else {
           // Connection is bad.
           logger.warn(
             "sendCommand returned an empty response, taking the connection as broken and closing it."
           );
-          this._closeConnection();
+          this.#closeConnection();
         }
       } catch (e) {
         logger.error(`Cannot send the command ${cmd}`, e);
-        this._closeConnection();
+        this.#closeConnection();
         if (leftAttempts == 0 && rethrow) {
           throw e;
         }
       }
     }
     return { connected, reply };
-  },
+  }
 
-  // Opens an authenticated connection, sets it to this._controlConnection, and
+  // Opens an authenticated connection, sets it to this.#controlConnection, and
   // return it.
-  async _getConnection() {
-    if (!this._controlConnection) {
-      this._controlConnection = await lazy.controller();
+  async #getConnection() {
+    if (!this.#controlConnection) {
+      this.#controlConnection = await lazy.controller();
     }
-    if (this._controlConnection.inUse) {
+    if (this.#controlConnection.inUse) {
       await new Promise((resolve, reject) =>
-        this._connectionQueue.push({ resolve, reject })
+        this.#connectionQueue.push({ resolve, reject })
       );
     } else {
-      this._controlConnection.inUse = true;
+      this.#controlConnection.inUse = true;
     }
-    return this._controlConnection;
-  },
+    return this.#controlConnection;
+  }
 
-  _returnConnection() {
-    if (this._connectionQueue.length) {
-      this._connectionQueue.shift().resolve();
+  #returnConnection() {
+    if (this.#connectionQueue.length) {
+      this.#connectionQueue.shift().resolve();
     } else {
-      this._controlConnection.inUse = false;
+      this.#controlConnection.inUse = false;
     }
-  },
+  }
 
-  async _withConnection(func) {
+  async #withConnection(func) {
     // TODO: Make more robust?
-    const conn = await this._getConnection();
+    const conn = await this.#getConnection();
     try {
       return await func(conn);
     } finally {
-      this._returnConnection();
+      this.#returnConnection();
     }
-  },
+  }
 
   // If aConn is omitted, the cached connection is closed.
-  _closeConnection() {
-    if (this._controlConnection) {
+  #closeConnection() {
+    if (this.#controlConnection) {
       logger.info("Closing the control connection");
-      this._controlConnection.close();
-      this._controlConnection = null;
+      this.#controlConnection.close();
+      this.#controlConnection = null;
     }
-    for (const promise of this._connectionQueue) {
+    for (const promise of this.#connectionQueue) {
       promise.reject("Connection closed");
     }
-    this._connectionQueue = [];
-  },
+    this.#connectionQueue = [];
+  }
 
-  async _reconnect() {
-    this._closeConnection();
-    const conn = await this._getConnection();
+  async #reconnect() {
+    this.#closeConnection();
+    const conn = await this.#getConnection();
     logger.debug("Reconnected to the control port.");
-    this._returnConnection(conn);
-  },
+    this.#returnConnection(conn);
+  }
 
-  async _readAuthenticationCookie(aPath) {
+  async #readAuthenticationCookie(aPath) {
     const bytes = await IOUtils.read(aPath);
-    return Array.from(bytes, b => this._toHex(b, 2)).join("");
-  },
+    return Array.from(bytes, b => this.#toHex(b, 2)).join("");
+  }
 
   // Returns a random 16 character password, hex-encoded.
-  _generateRandomPassword() {
+  #generateRandomPassword() {
     // Similar to Vidalia's crypto_rand_string().
     const kPasswordLen = 16;
     const kMinCharCode = "!".charCodeAt(0);
     const kMaxCharCode = "~".charCodeAt(0);
     let pwd = "";
     for (let i = 0; i < kPasswordLen; ++i) {
-      const val = this._cryptoRandInt(kMaxCharCode - kMinCharCode + 1);
+      const val = this.#cryptoRandInt(kMaxCharCode - kMinCharCode + 1);
       if (val < 0) {
         logger.error("_cryptoRandInt() failed");
         return null;
       }
-      pwd += this._toHex(kMinCharCode + val, 2);
+      pwd += this.#toHex(kMinCharCode + val, 2);
     }
 
     return pwd;
-  },
+  }
 
   // Returns -1 upon failure.
-  _cryptoRandInt(aMax) {
+  #cryptoRandInt(aMax) {
     // Based on tor's crypto_rand_int().
     const maxUInt = 0xffffffff;
     if (aMax <= 0 || aMax > maxUInt) {
@@ -697,9 +714,588 @@ export const TorProtocolService = {
       val = uint32[0];
     }
     return val % aMax;
-  },
+  }
 
-  _toHex(aValue, aMinLen) {
+  #toHex(aValue, aMinLen) {
     return aValue.toString(16).padStart(aMinLen, "0");
-  },
-};
+  }
+
+  // Former TorMonitorService implementation.
+  // FIXME: Refactor and integrate more with the rest of the class.
+
+  _connection = null;
+  _eventHandlers = {};
+  _torLog = []; // Array of objects with date, type, and msg properties
+  _startTimeout = null;
+
+  _isBootstrapDone = false;
+  _lastWarningPhase = null;
+  _lastWarningReason = null;
+
+  _torProcess = null;
+
+  _inited = false;
+
+  /**
+   * Stores the nodes of a circuit. Keys are cicuit IDs, and values are the node
+   * fingerprints.
+   *
+   * Theoretically, we could hook this map up to the new identity notification,
+   * but in practice it does not work. Tor pre-builds circuits, and the NEWNYM
+   * signal does not affect them. So, we might end up using a circuit that was
+   * built before the new identity but not yet used. If we cleaned the map, we
+   * risked of not having the data about it.
+   *
+   * @type {Map<CircuitID, NodeFingerprint[]>}
+   */
+  _circuits = new Map();
+  /**
+   * The last used bridge, or null if bridges are not in use or if it was not
+   * possible to detect the bridge. This needs the user to have specified bridge
+   * lines with fingerprints to work.
+   *
+   * @type {NodeFingerprint?}
+   */
+  _currentBridge = null;
+
+  // Public methods
+
+  // Starts Tor, if needed, and starts monitoring for events
+  _monitorInit() {
+    if (this._inited) {
+      return;
+    }
+    this._inited = true;
+
+    // We always liten to these events, because they are needed for the circuit
+    // display.
+    this._eventHandlers = new Map([
+      ["CIRC", this._processCircEvent.bind(this)],
+      ["STREAM", this._processStreamEvent.bind(this)],
+    ]);
+
+    if (this.ownsTorDaemon) {
+      // When we own the tor daemon, we listen to more events, that are used
+      // for about:torconnect or for showing the logs in the settings page.
+      this._eventHandlers.set("STATUS_CLIENT", (_eventType, lines) =>
+        this._processBootstrapStatus(lines[0], false)
+      );
+      this._eventHandlers.set("NOTICE", this._processLog.bind(this));
+      this._eventHandlers.set("WARN", this._processLog.bind(this));
+      this._eventHandlers.set("ERR", this._processLog.bind(this));
+      this._controlTor();
+    } else {
+      this._startEventMonitor();
+    }
+    logger.info("TorMonitorService initialized");
+  }
+
+  // Closes the connection that monitors for events.
+  // When Tor is started by Tor Browser, it is configured to exit when the
+  // control connection is closed. Therefore, as a matter of facts, calling this
+  // function also makes the child Tor instance stop.
+  _monitorUninit() {
+    if (this._torProcess) {
+      this._torProcess.forget();
+      this._torProcess.onExit = null;
+      this._torProcess.onRestart = null;
+      this._torProcess = null;
+    }
+    this._shutDownEventMonitor();
+  }
+
+  async retrieveBootstrapStatus() {
+    if (!this._connection) {
+      throw new Error("Event monitor connection not available");
+    }
+
+    // TODO: Unify with TorProtocolService.sendCommand and put everything in the
+    // reviewed torbutton replacement.
+    const cmd = "GETINFO";
+    const key = "status/bootstrap-phase";
+    let reply = await this._connection.sendCommand(`${cmd} ${key}`);
+
+    // A typical reply looks like:
+    //  250-status/bootstrap-phase=NOTICE BOOTSTRAP PROGRESS=100 TAG=done SUMMARY="Done"
+    //  250 OK
+    reply = TorParsers.parseCommandResponse(reply);
+    if (!TorParsers.commandSucceeded(reply)) {
+      throw new Error(`${cmd} failed`);
+    }
+    reply = TorParsers.parseReply(cmd, key, reply);
+    if (reply.length) {
+      this._processBootstrapStatus(reply[0], true);
+    }
+  }
+
+  // Returns captured log message as a text string (one message per line).
+  getLog() {
+    return this._torLog
+      .map(logObj => {
+        const timeStr = logObj.date
+          .toISOString()
+          .replace("T", " ")
+          .replace("Z", "");
+        return `${timeStr} [${logObj.type}] ${logObj.msg}`;
+      })
+      .join(TorLauncherUtil.isWindows ? "\r\n" : "\n");
+  }
+
+  // true if we launched and control tor, false if using system tor
+  get ownsTorDaemon() {
+    return TorLauncherUtil.shouldStartAndOwnTor;
+  }
+
+  get isBootstrapDone() {
+    return this._isBootstrapDone;
+  }
+
+  clearBootstrapError() {
+    this._lastWarningPhase = null;
+    this._lastWarningReason = null;
+  }
+
+  get isRunning() {
+    return !!this._connection;
+  }
+
+  /**
+   * Return the data about the current bridge, if any, or null.
+   * We can detect bridge only when the configured bridge lines include the
+   * fingerprints.
+   *
+   * @returns {NodeData?} The node information, or null if the first node
+   * is not a bridge, or no circuit has been opened, yet.
+   */
+  get currentBridge() {
+    return this._currentBridge;
+  }
+
+  // Private methods
+
+  async _startProcess() {
+    // 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 lazy.TorProcess(
+        this.torControlPortInfo,
+        this.torSOCKSPortInfo
+      );
+      this._torProcess.onExit = () => {
+        this._shutDownEventMonitor();
+        Services.obs.notifyObservers(null, TorProviderTopics.ProcessExited);
+      };
+      this._torProcess.onRestart = async () => {
+        this._shutDownEventMonitor();
+        await this._controlTor();
+        Services.obs.notifyObservers(null, TorProviderTopics.ProcessRestarted);
+      };
+    }
+
+    // Already running, but we did not start it
+    if (this._torProcess.isRunning) {
+      return false;
+    }
+
+    try {
+      await this._torProcess.start();
+      if (this._torProcess.isRunning) {
+        logger.info("tor started");
+        this._torProcessStartTime = Date.now();
+      }
+    } catch (e) {
+      // TorProcess already logs the error.
+      this._lastWarningPhase = "startup";
+      this._lastWarningReason = e.toString();
+    }
+    return this._torProcess.isRunning;
+  }
+
+  async _controlTor() {
+    if (!this._torProcess?.isRunning && !(await this._startProcess())) {
+      logger.error("Tor not running, not starting to monitor it.");
+      return;
+    }
+
+    let delayMS = ControlConnTimings.initialDelayMS;
+    const callback = async () => {
+      if (await this._startEventMonitor()) {
+        this.retrieveBootstrapStatus().catch(e => {
+          logger.warn("Could not get the initial bootstrap status", e);
+        });
+
+        // FIXME: TorProcess is misleading here. We should use a topic related
+        // to having a control port connection, instead.
+        logger.info(`Notifying ${TorProviderTopics.ProcessIsReady}`);
+        Services.obs.notifyObservers(null, TorProviderTopics.ProcessIsReady);
+
+        // We reset this here hoping that _shutDownEventMonitor can interrupt
+        // the current monitor, either by calling clearTimeout and preventing it
+        // from starting, or by closing the control port connection.
+        if (this._startTimeout === null) {
+          logger.warn("Someone else reset _startTimeout!");
+        }
+        this._startTimeout = null;
+      } else if (
+        Date.now() - this._torProcessStartTime >
+        ControlConnTimings.timeoutMS
+      ) {
+        let s = TorLauncherUtil.getLocalizedString("tor_controlconn_failed");
+        this._lastWarningPhase = "startup";
+        this._lastWarningReason = s;
+        logger.info(s);
+        if (this._startTimeout === null) {
+          logger.warn("Someone else reset _startTimeout!");
+        }
+        this._startTimeout = null;
+      } else {
+        delayMS *= 2;
+        if (delayMS > ControlConnTimings.maxRetryMS) {
+          delayMS = ControlConnTimings.maxRetryMS;
+        }
+        this._startTimeout = setTimeout(() => {
+          logger.debug(`Control port not ready, waiting ${delayMS / 1000}s.`);
+          callback();
+        }, delayMS);
+      }
+    };
+    // Check again, in the unfortunate case in which the execution was alrady
+    // queued, but was waiting network code.
+    if (this._startTimeout === null) {
+      this._startTimeout = setTimeout(callback, delayMS);
+    } else {
+      logger.error("Possible race? Refusing to start the timeout again");
+    }
+  }
+
+  async _startEventMonitor() {
+    if (this._connection) {
+      return true;
+    }
+
+    let conn;
+    try {
+      conn = await lazy.controller();
+    } catch (e) {
+      logger.error("Cannot open a control port connection", e);
+      if (conn) {
+        try {
+          conn.close();
+        } catch (e) {
+          logger.error(
+            "Also, the connection is not null but cannot be closed",
+            e
+          );
+        }
+      }
+      return false;
+    }
+
+    // TODO: optionally monitor INFO and DEBUG log messages.
+    try {
+      await conn.setEvents(Array.from(this._eventHandlers.keys()));
+    } catch (e) {
+      logger.error("SETEVENTS failed", e);
+      conn.close();
+      return false;
+    }
+
+    if (this._torProcess) {
+      this._torProcess.connectionWorked();
+    }
+    if (this.ownsTorDaemon && !TorLauncherUtil.shouldOnlyConfigureTor) {
+      try {
+        await this._takeTorOwnership(conn);
+      } catch (e) {
+        logger.warn("Could not take ownership of the Tor daemon", e);
+      }
+    }
+
+    this._connection = conn;
+
+    for (const [type, callback] of this._eventHandlers.entries()) {
+      this._monitorEvent(type, callback);
+    }
+
+    // Populate the circuit map already, in case we are connecting to an
+    // external tor daemon.
+    try {
+      const reply = await this._connection.sendCommand(
+        "GETINFO circuit-status"
+      );
+      const lines = reply.split(/\r?\n/);
+      if (lines.shift() === "250+circuit-status=") {
+        for (const line of lines) {
+          if (line === ".") {
+            break;
+          }
+          // _processCircEvent processes only one line at a time
+          this._processCircEvent("CIRC", [line]);
+        }
+      }
+    } catch (e) {
+      logger.warn("Could not populate the initial circuit map", e);
+    }
+
+    return true;
+  }
+
+  // Try to become the primary controller (TAKEOWNERSHIP).
+  async _takeTorOwnership(conn) {
+    try {
+      conn.takeOwnership();
+    } catch (e) {
+      logger.warn("Take ownership failed", e);
+      return;
+    }
+    try {
+      conn.resetOwningControllerProcess();
+    } catch (e) {
+      logger.warn("Clear owning controller process failed", e);
+    }
+  }
+
+  _monitorEvent(type, callback) {
+    logger.info(`Watching events of type ${type}.`);
+    let replyObj = {};
+    this._connection.watchEvent(
+      type,
+      null,
+      line => {
+        if (!line) {
+          return;
+        }
+        logger.debug("Event response: ", line);
+        const isComplete = TorParsers.parseReplyLine(line, replyObj);
+        if (!isComplete || replyObj._parseError || !replyObj.lineArray.length) {
+          return;
+        }
+        const reply = replyObj;
+        replyObj = {};
+        if (reply.statusCode !== TorStatuses.EventNotification) {
+          logger.error("Unexpected event status code:", reply.statusCode);
+          return;
+        }
+        if (!reply.lineArray[0].startsWith(`${type} `)) {
+          logger.error("Wrong format for the first line:", reply.lineArray[0]);
+          return;
+        }
+        reply.lineArray[0] = reply.lineArray[0].substring(type.length + 1);
+        try {
+          callback(type, reply.lineArray);
+        } catch (e) {
+          logger.error("Exception while handling an event", reply, e);
+        }
+      },
+      true
+    );
+  }
+
+  _processLog(type, lines) {
+    if (type === "WARN" || type === "ERR") {
+      // Notify so that Copy Log can be enabled.
+      Services.obs.notifyObservers(null, TorProviderTopics.HasWarnOrErr);
+    }
+
+    const date = new Date();
+    const maxEntries = Services.prefs.getIntPref(
+      "extensions.torlauncher.max_tor_log_entries",
+      1000
+    );
+    if (maxEntries > 0 && this._torLog.length >= maxEntries) {
+      this._torLog.splice(0, 1);
+    }
+
+    const msg = lines.join("\n");
+    this._torLog.push({ date, type, msg });
+    const logString = `Tor ${type}: ${msg}`;
+    logger.info(logString);
+  }
+
+  // Process a bootstrap status to update the current state, and broadcast it
+  // to TorBootstrapStatus observers.
+  // If aSuppressErrors is true, errors are ignored. This is used when we
+  // are handling the response to a "GETINFO status/bootstrap-phase" command.
+  _processBootstrapStatus(aStatusMsg, aSuppressErrors) {
+    const statusObj = TorParsers.parseBootstrapStatus(aStatusMsg);
+    if (!statusObj) {
+      return;
+    }
+
+    // Notify observers
+    statusObj.wrappedJSObject = statusObj;
+    Services.obs.notifyObservers(statusObj, "TorBootstrapStatus");
+
+    if (statusObj.PROGRESS === 100) {
+      this._isBootstrapDone = true;
+      try {
+        Services.prefs.setBoolPref(Preferences.PromptAtStartup, false);
+      } catch (e) {
+        logger.warn(`Cannot set ${Preferences.PromptAtStartup}`, e);
+      }
+      return;
+    }
+
+    this._isBootstrapDone = false;
+
+    if (
+      statusObj.TYPE === "WARN" &&
+      statusObj.RECOMMENDATION !== "ignore" &&
+      !aSuppressErrors
+    ) {
+      this._notifyBootstrapError(statusObj);
+    }
+  }
+
+  _notifyBootstrapError(statusObj) {
+    try {
+      Services.prefs.setBoolPref(Preferences.PromptAtStartup, true);
+    } catch (e) {
+      logger.warn(`Cannot set ${Preferences.PromptAtStartup}`, e);
+    }
+    const phase = TorLauncherUtil.getLocalizedBootstrapStatus(statusObj, "TAG");
+    const reason = TorLauncherUtil.getLocalizedBootstrapStatus(
+      statusObj,
+      "REASON"
+    );
+    const details = TorLauncherUtil.getFormattedLocalizedString(
+      "tor_bootstrap_failed_details",
+      [phase, reason],
+      2
+    );
+    logger.error(
+      `Tor bootstrap error: [${statusObj.TAG}/${statusObj.REASON}] ${details}`
+    );
+
+    if (
+      statusObj.TAG !== this._lastWarningPhase ||
+      statusObj.REASON !== this._lastWarningReason
+    ) {
+      this._lastWarningPhase = statusObj.TAG;
+      this._lastWarningReason = statusObj.REASON;
+
+      const message = TorLauncherUtil.getLocalizedString(
+        "tor_bootstrap_failed"
+      );
+      Services.obs.notifyObservers(
+        { message, details },
+        TorProviderTopics.BootstrapError
+      );
+    }
+  }
+
+  async _processCircEvent(_type, lines) {
+    const builtEvent =
+      /^(?<CircuitID>[a-zA-Z0-9]{1,16})\sBUILT\s(?<Path>(?:,?\$[0-9a-fA-F]{40}(?:~[a-zA-Z0-9]{1,19})?)+)/.exec(
+        lines[0]
+      );
+    const closedEvent = /^(?<ID>[a-zA-Z0-9]{1,16})\sCLOSED/.exec(lines[0]);
+    if (builtEvent) {
+      const fp = /\$([0-9a-fA-F]{40})/g;
+      const nodes = Array.from(builtEvent.groups.Path.matchAll(fp), g =>
+        g[1].toUpperCase()
+      );
+      this._circuits.set(builtEvent.groups.CircuitID, nodes);
+      // Ignore circuits of length 1, that are used, for example, to probe
+      // bridges. So, only store them, since we might see streams that use them,
+      // but then early-return.
+      if (nodes.length === 1) {
+        return;
+      }
+      // In some cases, we might already receive SOCKS credentials in the line.
+      // However, this might be a problem with onion services: we get also a
+      // 4-hop circuit that we likely do not want to show to the user,
+      // especially because it is used only temporarily, and it would need a
+      // technical explaination.
+      // this._checkCredentials(lines[0], nodes);
+      if (this._currentBridge?.fingerprint !== nodes[0]) {
+        const nodeInfo = await this.getNodeInfo(nodes[0]);
+        let notify = false;
+        if (nodeInfo?.bridgeType) {
+          logger.info(`Bridge changed to ${nodes[0]}`);
+          this._currentBridge = nodeInfo;
+          notify = true;
+        } else if (this._currentBridge) {
+          logger.info("Bridges disabled");
+          this._currentBridge = null;
+          notify = true;
+        }
+        if (notify) {
+          Services.obs.notifyObservers(
+            null,
+            TorProviderTopics.BridgeChanged,
+            this._currentBridge
+          );
+        }
+      }
+    } else if (closedEvent) {
+      this._circuits.delete(closedEvent.groups.ID);
+    }
+  }
+
+  _processStreamEvent(_type, lines) {
+    // The first block is the stream ID, which we do not need at the moment.
+    const succeeedEvent =
+      /^[a-zA-Z0-9]{1,16}\sSUCCEEDED\s(?<CircuitID>[a-zA-Z0-9]{1,16})/.exec(
+        lines[0]
+      );
+    if (!succeeedEvent) {
+      return;
+    }
+    const circuit = this._circuits.get(succeeedEvent.groups.CircuitID);
+    if (!circuit) {
+      logger.error(
+        "Seen a STREAM SUCCEEDED with an unknown circuit. Not notifying observers.",
+        lines[0]
+      );
+      return;
+    }
+    this._checkCredentials(lines[0], circuit);
+  }
+
+  /**
+   * Check if a STREAM or CIRC response line contains SOCKS_USERNAME and
+   * SOCKS_PASSWORD. In case, notify observers that we could associate a certain
+   * circuit to these credentials.
+   *
+   * @param {string} line The circ or stream line to check
+   * @param {NodeFingerprint[]} circuit The fingerprints of the nodes in the
+   * circuit.
+   */
+  _checkCredentials(line, circuit) {
+    const username = /SOCKS_USERNAME=("(?:[^"\\]|\\.)*")/.exec(line);
+    const password = /SOCKS_PASSWORD=("(?:[^"\\]|\\.)*")/.exec(line);
+    if (!username || !password) {
+      return;
+    }
+    Services.obs.notifyObservers(
+      {
+        wrappedJSObject: {
+          username: TorParsers.unescapeString(username[1]),
+          password: TorParsers.unescapeString(password[1]),
+          circuit,
+        },
+      },
+      TorProviderTopics.StreamSucceeded
+    );
+  }
+
+  _shutDownEventMonitor() {
+    try {
+      this._connection?.close();
+    } catch (e) {
+      logger.error("Could not close the connection to the control port", e);
+    }
+    this._connection = null;
+    if (this._startTimeout !== null) {
+      clearTimeout(this._startTimeout);
+      this._startTimeout = null;
+    }
+    this._isBootstrapDone = false;
+    this.clearBootstrapError();
+  }
+}
+
+// TODO: Stop defining TorProtocolService, make the builder instance the
+// TorProvider.
+export const TorProtocolService = new TorProvider();


=====================================
toolkit/components/tor-launcher/TorProviderBuilder.sys.mjs
=====================================
@@ -0,0 +1,34 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * 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/. */
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+  TorProtocolService: "resource://gre/modules/TorProtocolService.sys.mjs",
+});
+
+export const TorProviderTopics = Object.freeze({
+  ProcessIsReady: "TorProcessIsReady",
+  ProcessExited: "TorProcessExited",
+  ProcessRestarted: "TorProcessRestarted",
+  BootstrapStatus: "TorBootstrapStatus",
+  BootstrapError: "TorBootstrapError",
+  HasWarnOrErr: "TorLogHasWarnOrErr",
+  BridgeChanged: "TorBridgeChanged",
+  StreamSucceeded: "TorStreamSucceeded",
+});
+
+export class TorProviderBuilder {
+  static async init() {
+    await lazy.TorProtocolService.init();
+  }
+
+  static uninit() {
+    lazy.TorProtocolService.uninit();
+  }
+
+  // TODO: Switch to an async build?
+  static build() {
+    return lazy.TorProtocolService;
+  }
+}


=====================================
toolkit/components/tor-launcher/TorStartupService.sys.mjs
=====================================
@@ -5,8 +5,7 @@ const lazy = {};
 ChromeUtils.defineESModuleGetters(lazy, {
   TorDomainIsolator: "resource://gre/modules/TorDomainIsolator.sys.mjs",
   TorLauncherUtil: "resource://gre/modules/TorLauncherUtil.sys.mjs",
-  TorMonitorService: "resource://gre/modules/TorMonitorService.sys.mjs",
-  TorProtocolService: "resource://gre/modules/TorProtocolService.sys.mjs",
+  TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs",
 });
 
 ChromeUtils.defineModuleGetter(
@@ -33,24 +32,18 @@ let gInited = false;
 // When it observes profile-after-change, it initializes whatever is needed to
 // launch Tor.
 export class TorStartupService {
-  _defaultPreferencesAreLoaded = false;
-
   observe(aSubject, aTopic, aData) {
     if (aTopic === BrowserTopics.ProfileAfterChange && !gInited) {
-      this._init();
+      this.#init();
     } else if (aTopic === BrowserTopics.QuitApplicationGranted) {
-      this._uninit();
+      this.#uninit();
     }
   }
 
-  async _init() {
+  async #init() {
     Services.obs.addObserver(this, BrowserTopics.QuitApplicationGranted);
 
-    // Starts TorProtocolService first, because it configures the controller
-    // factory, too.
-    await lazy.TorProtocolService.init();
-    lazy.TorMonitorService.init();
-
+    await lazy.TorProviderBuilder.init();
     lazy.TorSettings.init();
     lazy.TorConnect.init();
 
@@ -59,17 +52,11 @@ export class TorStartupService {
     gInited = true;
   }
 
-  _uninit() {
+  #uninit() {
     Services.obs.removeObserver(this, BrowserTopics.QuitApplicationGranted);
 
     lazy.TorDomainIsolator.uninit();
-
-    // Close any helper connection first...
-    lazy.TorProtocolService.uninit();
-    // ... and only then closes the event monitor connection, which will cause
-    // Tor to stop.
-    lazy.TorMonitorService.uninit();
-
+    lazy.TorProviderBuilder.uninit();
     lazy.TorLauncherUtil.cleanupTempDirectories();
   }
 }


=====================================
toolkit/components/tor-launcher/moz.build
=====================================
@@ -7,6 +7,7 @@ EXTRA_JS_MODULES += [
     "TorParsers.sys.mjs",
     "TorProcess.sys.mjs",
     "TorProtocolService.sys.mjs",
+    "TorProviderBuilder.sys.mjs",
     "TorStartupService.sys.mjs",
 ]
 



View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/9722ca2637a1a68efa7207600c897f786f3b7c19...ab8a15b721bc218d4b1588f06337c96054cac91b

-- 
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/9722ca2637a1a68efa7207600c897f786f3b7c19...ab8a15b721bc218d4b1588f06337c96054cac91b
You're receiving this email because of your account on gitlab.torproject.org.


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.torproject.org/pipermail/tor-commits/attachments/20230805/22cd30ac/attachment-0001.htm>


More information about the tor-commits mailing list